diff --git a/Questionable.Model/EExpansionVersion.cs b/Questionable.Model/EExpansionVersion.cs
index 038d43ec..b5b1714c 100644
--- a/Questionable.Model/EExpansionVersion.cs
+++ b/Questionable.Model/EExpansionVersion.cs
@@ -24,4 +24,13 @@ public static class ExpansionData
             { EExpansionVersion.Endwalker, "6.x - Endwalker" },
             { EExpansionVersion.Dawntrail, "7.x - Dawntrail" }
         };
+
+    public static string ToFriendlyString(this EExpansionVersion expansionVersion)
+    {
+        return expansionVersion switch
+        {
+            EExpansionVersion.ARealmReborn => "A Realm Reborn",
+            _ => expansionVersion.ToString(),
+        };
+    }
 }
diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs
index 77440ad6..8517d135 100644
--- a/Questionable/QuestionablePlugin.cs
+++ b/Questionable/QuestionablePlugin.cs
@@ -24,6 +24,7 @@ using Questionable.Functions;
 using Questionable.Validation;
 using Questionable.Validation.Validators;
 using Questionable.Windows;
+using Questionable.Windows.JournalComponents;
 using Questionable.Windows.QuestComponents;
 using Action = Questionable.Controller.Steps.Interactions.Action;
 
@@ -211,6 +212,9 @@ public sealed class QuestionablePlugin : IDalamudPlugin
         serviceCollection.AddSingleton<QuickAccessButtonsComponent>();
         serviceCollection.AddSingleton<RemainingTasksComponent>();
 
+        serviceCollection.AddSingleton<QuestJournalComponent>();
+        serviceCollection.AddSingleton<GatheringJournalComponent>();
+
         serviceCollection.AddSingleton<QuestWindow>();
         serviceCollection.AddSingleton<ConfigWindow>();
         serviceCollection.AddSingleton<DebugOverlay>();
diff --git a/Questionable/Windows/JournalComponents/GatheringJournalComponent.cs b/Questionable/Windows/JournalComponents/GatheringJournalComponent.cs
new file mode 100644
index 00000000..699a651d
--- /dev/null
+++ b/Questionable/Windows/JournalComponents/GatheringJournalComponent.cs
@@ -0,0 +1,380 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using Dalamud.Interface;
+using Dalamud.Interface.Colors;
+using Dalamud.Interface.Utility.Raii;
+using Dalamud.Plugin;
+using Dalamud.Plugin.Services;
+using Dalamud.Utility;
+using Dalamud.Utility.Signatures;
+using ImGuiNET;
+using LLib.GameData;
+using Lumina.Excel.GeneratedSheets;
+using Questionable.Controller;
+using Questionable.Model;
+using Questionable.Model.Gathering;
+
+namespace Questionable.Windows.JournalComponents;
+
+internal sealed class GatheringJournalComponent
+{
+    private readonly IDalamudPluginInterface _pluginInterface;
+    private readonly UiUtils _uiUtils;
+    private readonly GatheringPointRegistry _gatheringPointRegistry;
+    private readonly Dictionary<int, string> _gatheringItems;
+    private readonly List<ExpansionPoints> _gatheringPoints;
+    private readonly List<ushort> _gatheredItems = [];
+
+    private delegate byte GetIsGatheringItemGatheredDelegate(ushort item);
+
+    [Signature("48 89 5C 24 ?? 57 48 83 EC 20 8B D9 8B F9")]
+    private GetIsGatheringItemGatheredDelegate _getIsGatheringItemGathered = null!;
+
+    internal bool IsGatheringItemGathered(uint item) => _getIsGatheringItemGathered((ushort)item) != 0;
+
+    public GatheringJournalComponent(IDataManager dataManager, IDalamudPluginInterface pluginInterface, UiUtils uiUtils,
+        IGameInteropProvider gameInteropProvider, GatheringPointRegistry gatheringPointRegistry)
+    {
+        _pluginInterface = pluginInterface;
+        _uiUtils = uiUtils;
+        _gatheringPointRegistry = gatheringPointRegistry;
+        var routeToGatheringPoint = dataManager.GetExcelSheet<GatheringLeveRoute>()!
+            .Where(x => x.UnkData0[0].GatheringPoint != 0)
+            .SelectMany(x => x.UnkData0
+                .Where(y => y.GatheringPoint != 0)
+                .Select(y => new
+                {
+                    RouteId = x.RowId,
+                    GatheringPointId = y.GatheringPoint
+                }))
+            .GroupBy(x => x.RouteId)
+            .ToDictionary(x => x.Key, x => x.Select(y => y.GatheringPointId).ToList());
+        var gatheringLeveSheet = dataManager.GetExcelSheet<GatheringLeve>()!;
+        var territoryTypeSheet = dataManager.GetExcelSheet<TerritoryType>()!;
+        var gatheringPointToLeve = dataManager.GetExcelSheet<Leve>()!
+            .Where(x => x.RowId > 0)
+            .Select(x =>
+            {
+                uint startZonePlaceName = x.PlaceNameStartZone.Row;
+                startZonePlaceName = startZonePlaceName switch
+                {
+                    27 => 28, // limsa
+                    39 => 52, // gridania
+                    51 => 40, // uldah
+                    62 => 2300, // ishgard
+                    _ => startZonePlaceName
+                };
+
+                var territoryType = territoryTypeSheet.FirstOrDefault(y => startZonePlaceName == y.PlaceName.Row)
+                                    ?? throw new InvalidOperationException($"Unable to use {startZonePlaceName}");
+                return new
+                {
+                    LeveId = x.RowId,
+                    LeveName = x.Name.ToString(),
+                    TerritoryType = (ushort)territoryType.RowId,
+                    TerritoryName = territoryType.Name.ToString(),
+                    GatheringLeve = gatheringLeveSheet.GetRow((uint)x.DataId),
+                };
+            })
+            .Where(x => x.GatheringLeve != null)
+            .Select(x => new
+            {
+                x.LeveId,
+                x.LeveName,
+                x.TerritoryType,
+                x.TerritoryName,
+                GatheringPoints = x.GatheringLeve!.Route
+                    .Where(y => y.Row != 0)
+                    .SelectMany(y => routeToGatheringPoint[y.Row]),
+            })
+            .SelectMany(x => x.GatheringPoints.Select(y => new
+            {
+                x.LeveId,
+                x.LeveName,
+                x.TerritoryType,
+                x.TerritoryName,
+                GatheringPointId = y
+            }))
+            .GroupBy(x => x.GatheringPointId)
+            .ToDictionary(x => x.Key, x => x.First());
+
+        var itemSheet = dataManager.GetExcelSheet<Item>()!;
+
+        _gatheringItems = dataManager.GetExcelSheet<GatheringItem>()!
+            .Where(x => x.RowId != 0 && x.GatheringItemLevel.Row != 0)
+            .Select(x => new
+            {
+                GatheringItemId = (int)x.RowId,
+                Name = itemSheet.GetRow((uint)x.Item)?.Name.ToString()
+            })
+            .Where(x => !string.IsNullOrEmpty(x.Name))
+            .ToDictionary(x => x.GatheringItemId, x => x.Name!);
+
+        _gatheringPoints = dataManager.GetExcelSheet<GatheringPoint>()!
+            .Where(x => x.GatheringPointBase.Row != 0)
+            .DistinctBy(x => x.GatheringPointBase.Row)
+            .Select(x => new
+            {
+                GatheringPointId = x.RowId,
+                Point = new DefaultGatheringPoint(new GatheringPointId((ushort)x.GatheringPointBase.Row),
+                    x.GatheringPointBase.Value!.GatheringType.Row switch
+                    {
+                        0 or 1 => EClassJob.Miner,
+                        2 or 3 => EClassJob.Botanist,
+                        _ => EClassJob.Fisher
+                    },
+                    x.GatheringPointBase.Value.GatheringLevel,
+                    x.GatheringPointBase.Value.Item.Where(y => y != 0).Select(y => (ushort)y).ToList(),
+                    (EExpansionVersion?)x.TerritoryType.Value?.ExVersion.Row ?? (EExpansionVersion)byte.MaxValue,
+                    (ushort)x.TerritoryType.Row,
+                    x.TerritoryType.Value?.PlaceName.Value?.Name.ToString(),
+                    $"{x.GatheringPointBase.Row} - {x.PlaceName.Value?.Name}")
+            })
+            .Where(x => x.Point.ClassJob != EClassJob.Fisher)
+            .Select(x =>
+            {
+                if (gatheringPointToLeve.TryGetValue((int)x.GatheringPointId, out var leve))
+                {
+                    // it's a leve
+                    return x.Point with
+                    {
+                        Expansion = EExpansionVersion.Shadowbringers,
+                        TerritoryType = leve.TerritoryType,
+                        TerritoryName = leve.TerritoryName,
+                        PlaceName = leve.LeveName,
+                    };
+                }
+                else if (x.Point.TerritoryType == 1 && _gatheringPointRegistry.TryGetGatheringPoint(x.Point.Id, out GatheringRoot? gatheringRoot))
+                {
+                    // for some reason the game doesn't know where this gathering location is
+                    var territoryType = territoryTypeSheet.GetRow(gatheringRoot.Steps.Last().TerritoryId)!;
+                    return x.Point with
+                    {
+                        Expansion = (EExpansionVersion)territoryType.ExVersion.Row,
+                        TerritoryType = (ushort)territoryType.RowId,
+                        TerritoryName = territoryType.PlaceName.Value?.Name.ToString(),
+                    };
+                }
+                else
+                    return x.Point;
+            })
+            .Where(x => x.Expansion != (EExpansionVersion)byte.MaxValue)
+            .Where(x => x.GatheringItemIds.Count > 0)
+            .GroupBy(x => x.Expansion)
+            .Select(x => new ExpansionPoints(x.Key, x
+                .GroupBy(y => new
+                {
+                    y.TerritoryType,
+                    TerritoryName = $"{(!string.IsNullOrEmpty(y.TerritoryName) ? y.TerritoryName : "???")} ({y.TerritoryType})"
+                })
+                .Select(y => new TerritoryPoints(y.Key.TerritoryType, y.Key.TerritoryName, y.ToList()))
+                .Where(y => y.Points.Count > 0)
+                .ToList()))
+            .OrderBy(x => x.Expansion)
+            .ToList();
+
+        gameInteropProvider.InitializeFromAttributes(this);
+    }
+
+    public void DrawGatheringItems()
+    {
+        using var tab = ImRaii.TabItem("Gathering Points");
+        if (!tab)
+            return;
+
+        using var table = ImRaii.Table("GatheringPoints", 3, ImGuiTableFlags.NoSavedSettings);
+        if (!table)
+            return;
+
+        ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.NoHide);
+        ImGui.TableSetupColumn("Supported", ImGuiTableColumnFlags.WidthFixed, 100 * ImGui.GetIO().FontGlobalScale);
+        ImGui.TableSetupColumn("Collected", ImGuiTableColumnFlags.WidthFixed, 100 * ImGui.GetIO().FontGlobalScale);
+        ImGui.TableHeadersRow();
+
+        foreach (var expansion in _gatheringPoints)
+            DrawExpansion(expansion);
+    }
+
+    private void DrawExpansion(ExpansionPoints expansion)
+    {
+        ImGui.TableNextRow();
+        ImGui.TableNextColumn();
+
+        bool open = ImGui.TreeNodeEx(expansion.Expansion.ToFriendlyString(), ImGuiTreeNodeFlags.SpanFullWidth);
+
+        ImGui.TableNextColumn();
+        DrawCount(expansion.CompletedPoints, expansion.TotalPoints);
+        ImGui.TableNextColumn();
+        DrawCount(expansion.CompletedItems, expansion.TotalItems);
+
+        if (open)
+        {
+            foreach (var territory in expansion.PointsByTerritories)
+                DrawTerritory(territory);
+
+            ImGui.TreePop();
+        }
+    }
+
+    private void DrawTerritory(TerritoryPoints territory)
+    {
+        ImGui.TableNextRow();
+        ImGui.TableNextColumn();
+
+        bool open = ImGui.TreeNodeEx(territory.ToFriendlyString(), ImGuiTreeNodeFlags.SpanFullWidth);
+
+        ImGui.TableNextColumn();
+        DrawCount(territory.CompletedPoints, territory.TotalPoints);
+        ImGui.TableNextColumn();
+        DrawCount(territory.CompletedItems, territory.TotalItems);
+
+        if (open)
+        {
+            foreach (var point in territory.Points)
+                DrawPoint(point);
+
+            ImGui.TreePop();
+        }
+    }
+
+    private void DrawPoint(DefaultGatheringPoint point)
+    {
+        ImGui.TableNextRow();
+        ImGui.TableNextColumn();
+
+        bool open = ImGui.TreeNodeEx($"{point.PlaceName} ({point.ClassJob} Lv. {point.Level})",
+            ImGuiTreeNodeFlags.SpanFullWidth);
+
+        ImGui.TableNextColumn();
+        float spacing;
+        // ReSharper disable once UnusedVariable
+        using (var font = _pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
+        {
+            spacing = ImGui.GetColumnWidth() / 2 - ImGui.CalcTextSize(FontAwesomeIcon.Check.ToIconString()).X;
+        }
+
+        ImGui.SetCursorPosX(ImGui.GetCursorPosX() + spacing);
+        _uiUtils.ChecklistItem(string.Empty, point.IsComplete);
+
+        ImGui.TableNextColumn();
+        DrawCount(point.CompletedItems, point.TotalItems);
+
+        if (open)
+        {
+            foreach (var item in point.GatheringItemIds)
+                DrawItem(item);
+
+            ImGui.TreePop();
+        }
+    }
+
+    private void DrawItem(ushort item)
+    {
+        ImGui.TableNextRow();
+        ImGui.TableNextColumn();
+        ImGui.TreeNodeEx(_gatheringItems.GetValueOrDefault(item, "???"),
+            ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen | ImGuiTreeNodeFlags.SpanFullWidth);
+
+        ImGui.TableNextColumn();
+
+        ImGui.TableNextColumn();
+        float spacing;
+        // ReSharper disable once UnusedVariable
+        using (var font = _pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
+        {
+            spacing = ImGui.GetColumnWidth() / 2 - ImGui.CalcTextSize(FontAwesomeIcon.Check.ToIconString()).X;
+        }
+
+        ImGui.SetCursorPosX(ImGui.GetCursorPosX() + spacing);
+        if (item < 10_000)
+            _uiUtils.ChecklistItem(string.Empty, _gatheredItems.Contains(item));
+        else
+            _uiUtils.ChecklistItem(string.Empty, ImGuiColors.DalamudGrey, FontAwesomeIcon.Minus);
+    }
+
+    private static void DrawCount(int count, int total)
+    {
+        string len = 999.ToString(CultureInfo.CurrentCulture);
+        ImGui.PushFont(UiBuilder.MonoFont);
+
+        string text =
+            $"{count.ToString(CultureInfo.CurrentCulture).PadLeft(len.Length)} / {total.ToString(CultureInfo.CurrentCulture).PadLeft(len.Length)}";
+        if (count == total)
+            ImGui.TextColored(ImGuiColors.ParsedGreen, text);
+        else
+            ImGui.TextUnformatted(text);
+
+        ImGui.PopFont();
+    }
+
+    internal void RefreshCounts()
+    {
+        _gatheredItems.Clear();
+        foreach (ushort key in _gatheringItems.Keys)
+        {
+            if (IsGatheringItemGathered(key))
+                _gatheredItems.Add(key);
+        }
+
+        foreach (var expansion in _gatheringPoints)
+        {
+            foreach (var territory in expansion.PointsByTerritories)
+            {
+                foreach (var point in territory.Points)
+                {
+                    point.TotalItems = point.GatheringItemIds.Count(x => x < 10_000);
+                    point.CompletedItems = point.GatheringItemIds.Count(_gatheredItems.Contains);
+                    point.IsComplete = _gatheringPointRegistry.TryGetGatheringPoint(point.Id, out _);
+                }
+
+                territory.TotalItems = territory.Points.Sum(x => x.TotalItems);
+                territory.CompletedItems = territory.Points.Sum(x => x.CompletedItems);
+                territory.CompletedPoints = territory.Points.Count(x => x.IsComplete);
+            }
+
+            expansion.TotalItems = expansion.PointsByTerritories.Sum(x => x.TotalItems);
+            expansion.CompletedItems = expansion.PointsByTerritories.Sum(x => x.CompletedItems);
+            expansion.TotalPoints = expansion.PointsByTerritories.Sum(x => x.TotalPoints);
+            expansion.CompletedPoints = expansion.PointsByTerritories.Sum(x => x.CompletedPoints);
+        }
+    }
+
+    private sealed record ExpansionPoints(EExpansionVersion Expansion, List<TerritoryPoints> PointsByTerritories)
+    {
+        public int TotalItems { get; set; }
+        public int TotalPoints { get; set; }
+        public int CompletedItems { get; set; }
+        public int CompletedPoints { get; set; }
+    }
+
+    private sealed record TerritoryPoints(
+        ushort TerritoryType,
+        string TerritoryName,
+        List<DefaultGatheringPoint> Points)
+    {
+        public int TotalItems { get; set; }
+        public int TotalPoints => Points.Count;
+        public int CompletedItems { get; set; }
+        public int CompletedPoints { get; set; }
+        public string ToFriendlyString() =>
+            !string.IsNullOrEmpty(TerritoryName) ? TerritoryName : $"??? ({TerritoryType})";
+    }
+
+    private sealed record DefaultGatheringPoint(
+        GatheringPointId Id,
+        EClassJob ClassJob,
+        byte Level,
+        List<ushort> GatheringItemIds,
+        EExpansionVersion Expansion,
+        ushort TerritoryType,
+        string? TerritoryName,
+        string? PlaceName)
+    {
+        public int TotalItems { get; set; }
+        public int CompletedItems { get; set; }
+        public bool IsComplete { get; set; }
+    }
+}
diff --git a/Questionable/Windows/JournalComponents/QuestJournalComponent.cs b/Questionable/Windows/JournalComponents/QuestJournalComponent.cs
new file mode 100644
index 00000000..94ed2ea1
--- /dev/null
+++ b/Questionable/Windows/JournalComponents/QuestJournalComponent.cs
@@ -0,0 +1,351 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using Dalamud.Interface;
+using Dalamud.Interface.Colors;
+using Dalamud.Interface.Utility.Raii;
+using Dalamud.Plugin;
+using Dalamud.Plugin.Services;
+using ImGuiNET;
+using Questionable.Controller;
+using Questionable.Data;
+using Questionable.Functions;
+using Questionable.Model;
+using Questionable.Windows.QuestComponents;
+
+namespace Questionable.Windows.JournalComponents;
+
+internal sealed class QuestJournalComponent
+{
+    private readonly Dictionary<JournalData.Genre, (int Available, int Completed)> _genreCounts = new();
+    private readonly Dictionary<JournalData.Category, (int Available, int Completed)> _categoryCounts = new();
+    private readonly Dictionary<JournalData.Section, (int Available, int Completed)> _sectionCounts = new();
+
+    private readonly JournalData _journalData;
+    private readonly QuestRegistry _questRegistry;
+    private readonly QuestFunctions _questFunctions;
+    private readonly UiUtils _uiUtils;
+    private readonly QuestTooltipComponent _questTooltipComponent;
+    private readonly IDalamudPluginInterface _pluginInterface;
+    private readonly ICommandManager _commandManager;
+
+    private List<FilteredSection> _filteredSections = [];
+    private string _searchText = string.Empty;
+
+    public QuestJournalComponent(JournalData journalData, QuestRegistry questRegistry, QuestFunctions questFunctions,
+        UiUtils uiUtils, QuestTooltipComponent questTooltipComponent, IDalamudPluginInterface pluginInterface,
+        ICommandManager commandManager)
+    {
+        _journalData = journalData;
+        _questRegistry = questRegistry;
+        _questFunctions = questFunctions;
+        _uiUtils = uiUtils;
+        _questTooltipComponent = questTooltipComponent;
+        _pluginInterface = pluginInterface;
+        _commandManager = commandManager;
+    }
+
+    public void DrawQuests()
+    {
+        using var tab = ImRaii.TabItem("Quests");
+        if (!tab)
+            return;
+
+        if (ImGui.CollapsingHeader("Explanation", ImGuiTreeNodeFlags.DefaultOpen))
+        {
+            ImGui.Text("The list below contains all quests that appear in your journal.");
+            ImGui.BulletText("'Supported' lists quests that Questionable can do for you");
+            ImGui.BulletText("'Completed' lists quests your current character has completed.");
+            ImGui.BulletText(
+                "Not all quests can be completed even if they're listed as available, e.g. starting city quest chains.");
+
+            ImGui.Spacing();
+            ImGui.Separator();
+            ImGui.Spacing();
+        }
+
+        ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
+        if (ImGui.InputTextWithHint(string.Empty, "Search quests and categories", ref _searchText, 256))
+            UpdateFilter();
+
+        if (_filteredSections.Count > 0)
+        {
+            using var table = ImRaii.Table("Quests", 3, ImGuiTableFlags.NoSavedSettings);
+            if (!table)
+                return;
+
+            ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.NoHide);
+            ImGui.TableSetupColumn("Supported", ImGuiTableColumnFlags.WidthFixed, 120 * ImGui.GetIO().FontGlobalScale);
+            ImGui.TableSetupColumn("Completed", ImGuiTableColumnFlags.WidthFixed, 120 * ImGui.GetIO().FontGlobalScale);
+            ImGui.TableHeadersRow();
+
+            foreach (var section in _filteredSections)
+                DrawSection(section);
+        }
+        else
+            ImGui.Text("No quest or category matches your search text.");
+    }
+
+    private void DrawSection(FilteredSection filter)
+    {
+        if (filter.Section.QuestCount == 0)
+            return;
+
+        (int supported, int completed) = _sectionCounts.GetValueOrDefault(filter.Section);
+
+        ImGui.TableNextRow();
+        ImGui.TableNextColumn();
+
+        bool open = ImGui.TreeNodeEx(filter.Section.Name, ImGuiTreeNodeFlags.SpanFullWidth);
+
+        ImGui.TableNextColumn();
+        DrawCount(supported, filter.Section.QuestCount);
+        ImGui.TableNextColumn();
+        DrawCount(completed, filter.Section.QuestCount);
+
+        if (open)
+        {
+            foreach (var category in filter.Categories)
+                DrawCategory(category);
+
+            ImGui.TreePop();
+        }
+    }
+
+    private void DrawCategory(FilteredCategory filter)
+    {
+        if (filter.Category.QuestCount == 0)
+            return;
+
+        (int supported, int completed) = _categoryCounts.GetValueOrDefault(filter.Category);
+
+        ImGui.TableNextRow();
+        ImGui.TableNextColumn();
+
+        bool open = ImGui.TreeNodeEx(filter.Category.Name, ImGuiTreeNodeFlags.SpanFullWidth);
+
+        ImGui.TableNextColumn();
+        DrawCount(supported, filter.Category.QuestCount);
+        ImGui.TableNextColumn();
+        DrawCount(completed, filter.Category.QuestCount);
+
+        if (open)
+        {
+            foreach (var genre in filter.Genres)
+                DrawGenre(genre);
+
+            ImGui.TreePop();
+        }
+    }
+
+    private void DrawGenre(FilteredGenre filter)
+    {
+        if (filter.Genre.QuestCount == 0)
+            return;
+
+        (int supported, int completed) = _genreCounts.GetValueOrDefault(filter.Genre);
+
+        ImGui.TableNextRow();
+        ImGui.TableNextColumn();
+
+        bool open = ImGui.TreeNodeEx(filter.Genre.Name, ImGuiTreeNodeFlags.SpanFullWidth);
+
+        ImGui.TableNextColumn();
+        DrawCount(supported, filter.Genre.QuestCount);
+        ImGui.TableNextColumn();
+        DrawCount(completed, filter.Genre.QuestCount);
+
+        if (open)
+        {
+            foreach (var quest in filter.Quests)
+                DrawQuest(quest);
+
+            ImGui.TreePop();
+        }
+    }
+
+    private void DrawQuest(IQuestInfo questInfo)
+    {
+        _questRegistry.TryGetQuest(questInfo.QuestId, out var quest);
+
+        ImGui.TableNextRow();
+        ImGui.TableNextColumn();
+        ImGui.TreeNodeEx(questInfo.Name,
+            ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen | ImGuiTreeNodeFlags.SpanFullWidth);
+
+
+        if (questInfo is QuestInfo && ImGui.IsItemClicked() &&
+            _commandManager.Commands.TryGetValue("/questinfo", out var commandInfo))
+        {
+            _commandManager.DispatchCommand("/questinfo", questInfo.QuestId.ToString() ?? string.Empty, commandInfo);
+        }
+
+        if (ImGui.IsItemHovered())
+            _questTooltipComponent.Draw(questInfo);
+
+        ImGui.TableNextColumn();
+        float spacing;
+        // ReSharper disable once UnusedVariable
+        using (var font = _pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
+        {
+            spacing = ImGui.GetColumnWidth() / 2 - ImGui.CalcTextSize(FontAwesomeIcon.Check.ToIconString()).X;
+        }
+
+        ImGui.SetCursorPosX(ImGui.GetCursorPosX() + spacing);
+        _uiUtils.ChecklistItem(string.Empty, quest is { Root.Disabled: false });
+
+        ImGui.TableNextColumn();
+        var (color, icon, text) = _uiUtils.GetQuestStyle(questInfo.QuestId);
+        _uiUtils.ChecklistItem(text, color, icon);
+    }
+
+    private static void DrawCount(int count, int total)
+    {
+        string len = 9999.ToString(CultureInfo.CurrentCulture);
+        ImGui.PushFont(UiBuilder.MonoFont);
+
+        string text =
+            $"{count.ToString(CultureInfo.CurrentCulture).PadLeft(len.Length)} / {total.ToString(CultureInfo.CurrentCulture).PadLeft(len.Length)}";
+        if (count == total)
+            ImGui.TextColored(ImGuiColors.ParsedGreen, text);
+        else
+            ImGui.TextUnformatted(text);
+
+        ImGui.PopFont();
+    }
+
+    public void UpdateFilter()
+    {
+        Predicate<string> match;
+        if (string.IsNullOrWhiteSpace(_searchText))
+            match = _ => true;
+        else
+            match = x => x.Contains(_searchText, StringComparison.CurrentCultureIgnoreCase);
+
+        _filteredSections = _journalData.Sections
+            .Select(section => FilterSection(section, match))
+            .Where(x => x != null)
+            .Cast<FilteredSection>()
+            .ToList();
+    }
+
+    private static FilteredSection? FilterSection(JournalData.Section section, Predicate<string> match)
+    {
+        if (match(section.Name))
+        {
+            return new FilteredSection(section,
+                section.Categories
+                    .Select(x => FilterCategory(x, _ => true))
+                    .Cast<FilteredCategory>()
+                    .ToList());
+        }
+        else
+        {
+            List<FilteredCategory> filteredCategories = section.Categories
+                .Select(category => FilterCategory(category, match))
+                .Where(x => x != null)
+                .Cast<FilteredCategory>()
+                .ToList();
+            if (filteredCategories.Count > 0)
+                return new FilteredSection(section, filteredCategories);
+
+            return null;
+        }
+    }
+
+    private static FilteredCategory? FilterCategory(JournalData.Category category, Predicate<string> match)
+    {
+        if (match(category.Name))
+        {
+            return new FilteredCategory(category,
+                category.Genres
+                    .Select(x => FilterGenre(x, _ => true))
+                    .Cast<FilteredGenre>()
+                    .ToList());
+        }
+        else
+        {
+            List<FilteredGenre> filteredGenres = category.Genres
+                .Select(genre => FilterGenre(genre, match))
+                .Where(x => x != null)
+                .Cast<FilteredGenre>()
+                .ToList();
+            if (filteredGenres.Count > 0)
+                return new FilteredCategory(category, filteredGenres);
+
+            return null;
+        }
+    }
+
+    private static FilteredGenre? FilterGenre(JournalData.Genre genre, Predicate<string> match)
+    {
+        if (match(genre.Name))
+            return new FilteredGenre(genre, genre.Quests);
+        else
+        {
+            List<IQuestInfo> filteredQuests = genre.Quests
+                .Where(x => match(x.Name))
+                .ToList();
+            if (filteredQuests.Count > 0)
+                return new FilteredGenre(genre, filteredQuests);
+        }
+
+        return null;
+    }
+
+    internal void RefreshCounts()
+    {
+        _genreCounts.Clear();
+        _categoryCounts.Clear();
+        _sectionCounts.Clear();
+
+        foreach (var genre in _journalData.Genres)
+        {
+            int available = genre.Quests.Count(x =>
+                _questRegistry.TryGetQuest(x.QuestId, out var quest) && !quest.Root.Disabled);
+            int completed = genre.Quests.Count(x => _questFunctions.IsQuestComplete(x.QuestId));
+            _genreCounts[genre] = (available, completed);
+        }
+
+        foreach (var category in _journalData.Categories)
+        {
+            var counts = _genreCounts
+                .Where(x => category.Genres.Contains(x.Key))
+                .Select(x => x.Value)
+                .ToList();
+            int available = counts.Sum(x => x.Available);
+            int completed = counts.Sum(x => x.Completed);
+            _categoryCounts[category] = (available, completed);
+        }
+
+        foreach (var section in _journalData.Sections)
+        {
+            var counts = _categoryCounts
+                .Where(x => section.Categories.Contains(x.Key))
+                .Select(x => x.Value)
+                .ToList();
+            int available = counts.Sum(x => x.Available);
+            int completed = counts.Sum(x => x.Completed);
+            _sectionCounts[section] = (available, completed);
+        }
+    }
+
+    internal void ClearCounts()
+    {
+        foreach (var genreCount in _genreCounts.ToList())
+            _genreCounts[genreCount.Key] = (genreCount.Value.Available, 0);
+
+        foreach (var categoryCount in _categoryCounts.ToList())
+            _categoryCounts[categoryCount.Key] = (categoryCount.Value.Available, 0);
+
+        foreach (var sectionCount in _sectionCounts.ToList())
+            _sectionCounts[sectionCount.Key] = (sectionCount.Value.Available, 0);
+    }
+
+    private sealed record FilteredSection(JournalData.Section Section, List<FilteredCategory> Categories);
+
+    private sealed record FilteredCategory(JournalData.Category Category, List<FilteredGenre> Genres);
+
+    private sealed record FilteredGenre(JournalData.Genre Genre, List<IQuestInfo> Quests);
+}
diff --git a/Questionable/Windows/JournalProgressWindow.cs b/Questionable/Windows/JournalProgressWindow.cs
index bf00be39..db5758f2 100644
--- a/Questionable/Windows/JournalProgressWindow.cs
+++ b/Questionable/Windows/JournalProgressWindow.cs
@@ -14,49 +14,33 @@ using Questionable.Controller;
 using Questionable.Data;
 using Questionable.Functions;
 using Questionable.Model;
+using Questionable.Windows.JournalComponents;
 using Questionable.Windows.QuestComponents;
 
 namespace Questionable.Windows;
 
 internal sealed class JournalProgressWindow : LWindow, IDisposable
 {
-    private readonly JournalData _journalData;
+    private readonly QuestJournalComponent _questJournalComponent;
+    private readonly GatheringJournalComponent _gatheringJournalComponent;
     private readonly QuestRegistry _questRegistry;
-    private readonly QuestFunctions _questFunctions;
-    private readonly UiUtils _uiUtils;
-    private readonly QuestTooltipComponent _questTooltipComponent;
-    private readonly IDalamudPluginInterface _pluginInterface;
     private readonly IClientState _clientState;
-    private readonly ICommandManager _commandManager;
 
-    private readonly Dictionary<JournalData.Genre, (int Available, int Completed)> _genreCounts = new();
-    private readonly Dictionary<JournalData.Category, (int Available, int Completed)> _categoryCounts = new();
-    private readonly Dictionary<JournalData.Section, (int Available, int Completed)> _sectionCounts = new();
-
-    private List<FilteredSection> _filteredSections = [];
-    private string _searchText = string.Empty;
-
-    public JournalProgressWindow(JournalData journalData,
+    public JournalProgressWindow(
+        QuestJournalComponent questJournalComponent,
+        GatheringJournalComponent gatheringJournalComponent,
         QuestRegistry questRegistry,
-        QuestFunctions questFunctions,
-        UiUtils uiUtils,
-        QuestTooltipComponent questTooltipComponent,
-        IDalamudPluginInterface pluginInterface,
-        IClientState clientState,
-        ICommandManager commandManager)
+        IClientState clientState)
         : base("Journal Progress###QuestionableJournalProgress")
     {
-        _journalData = journalData;
+        _questJournalComponent = questJournalComponent;
+        _gatheringJournalComponent = gatheringJournalComponent;
         _questRegistry = questRegistry;
-        _questFunctions = questFunctions;
-        _uiUtils = uiUtils;
-        _questTooltipComponent = questTooltipComponent;
-        _pluginInterface = pluginInterface;
         _clientState = clientState;
-        _commandManager = commandManager;
 
-        _clientState.Login += RefreshCounts;
-        _clientState.Logout -= ClearCounts;
+        _clientState.Login += _questJournalComponent.RefreshCounts;
+        _clientState.Login += _gatheringJournalComponent.RefreshCounts;
+        _clientState.Logout -= _questJournalComponent.ClearCounts;
         _questRegistry.Reloaded += OnQuestsReloaded;
 
         SizeConstraints = new WindowSizeConstraints
@@ -65,318 +49,34 @@ internal sealed class JournalProgressWindow : LWindow, IDisposable
         };
     }
 
-    private void OnQuestsReloaded(object? sender, EventArgs e) => RefreshCounts();
+    private void OnQuestsReloaded(object? sender, EventArgs e)
+    {
+        _questJournalComponent.RefreshCounts();
+        _gatheringJournalComponent.RefreshCounts();
+    }
 
     public override void OnOpen()
     {
-        UpdateFilter();
-        RefreshCounts();
+        _questJournalComponent.UpdateFilter();
+        _questJournalComponent.RefreshCounts();
+        _gatheringJournalComponent.RefreshCounts();
     }
 
     public override void Draw()
     {
-        if (ImGui.CollapsingHeader("Explanation", ImGuiTreeNodeFlags.DefaultOpen))
-        {
-            ImGui.Text("The list below contains all quests that appear in your journal.");
-            ImGui.BulletText("'Supported' lists quests that Questionable can do for you");
-            ImGui.BulletText("'Completed' lists quests your current character has completed.");
-            ImGui.BulletText(
-                "Not all quests can be completed even if they're listed as available, e.g. starting city quest chains.");
-
-            ImGui.Spacing();
-            ImGui.Separator();
-            ImGui.Spacing();
-        }
-
-        ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
-        if (ImGui.InputTextWithHint(string.Empty, "Search quests and categories", ref _searchText, 256))
-            UpdateFilter();
-
-        if (_filteredSections.Count > 0)
-        {
-            using var table = ImRaii.Table("Quests", 3, ImGuiTableFlags.NoSavedSettings);
-            if (!table)
-                return;
-
-            ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.NoHide);
-            ImGui.TableSetupColumn("Supported", ImGuiTableColumnFlags.WidthFixed, 120 * ImGui.GetIO().FontGlobalScale);
-            ImGui.TableSetupColumn("Completed", ImGuiTableColumnFlags.WidthFixed, 120 * ImGui.GetIO().FontGlobalScale);
-            ImGui.TableHeadersRow();
-
-            foreach (var section in _filteredSections)
-            {
-                DrawSection(section);
-            }
-        }
-        else
-            ImGui.Text("No quest or category matches your search text.");
-    }
-
-    private void DrawSection(FilteredSection filter)
-    {
-        if (filter.Section.QuestCount == 0)
+        using var tabBar = ImRaii.TabBar("Journal");
+        if (!tabBar)
             return;
 
-        (int supported, int completed) = _sectionCounts.GetValueOrDefault(filter.Section);
-
-        ImGui.TableNextRow();
-        ImGui.TableNextColumn();
-
-        bool open = ImGui.TreeNodeEx(filter.Section.Name, ImGuiTreeNodeFlags.SpanFullWidth);
-
-        ImGui.TableNextColumn();
-        DrawCount(supported, filter.Section.QuestCount);
-        ImGui.TableNextColumn();
-        DrawCount(completed, filter.Section.QuestCount);
-
-        if (open)
-        {
-            foreach (var category in filter.Categories)
-                DrawCategory(category);
-
-            ImGui.TreePop();
-        }
-    }
-
-    private void DrawCategory(FilteredCategory filter)
-    {
-        if (filter.Category.QuestCount == 0)
-            return;
-
-        (int supported, int completed) = _categoryCounts.GetValueOrDefault(filter.Category);
-
-        ImGui.TableNextRow();
-        ImGui.TableNextColumn();
-
-        bool open = ImGui.TreeNodeEx(filter.Category.Name, ImGuiTreeNodeFlags.SpanFullWidth);
-
-        ImGui.TableNextColumn();
-        DrawCount(supported, filter.Category.QuestCount);
-        ImGui.TableNextColumn();
-        DrawCount(completed, filter.Category.QuestCount);
-
-        if (open)
-        {
-            foreach (var genre in filter.Genres)
-                DrawGenre(genre);
-
-            ImGui.TreePop();
-        }
-    }
-
-    private void DrawGenre(FilteredGenre filter)
-    {
-        if (filter.Genre.QuestCount == 0)
-            return;
-
-        (int supported, int completed) = _genreCounts.GetValueOrDefault(filter.Genre);
-
-        ImGui.TableNextRow();
-        ImGui.TableNextColumn();
-
-        bool open = ImGui.TreeNodeEx(filter.Genre.Name, ImGuiTreeNodeFlags.SpanFullWidth);
-
-        ImGui.TableNextColumn();
-        DrawCount(supported, filter.Genre.QuestCount);
-        ImGui.TableNextColumn();
-        DrawCount(completed, filter.Genre.QuestCount);
-
-        if (open)
-        {
-            foreach (var quest in filter.Quests)
-                DrawQuest(quest);
-
-            ImGui.TreePop();
-        }
-    }
-
-    private void DrawQuest(IQuestInfo questInfo)
-    {
-        _questRegistry.TryGetQuest(questInfo.QuestId, out var quest);
-
-        ImGui.TableNextRow();
-        ImGui.TableNextColumn();
-        ImGui.TreeNodeEx(questInfo.Name,
-            ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen | ImGuiTreeNodeFlags.SpanFullWidth);
-
-
-        if (questInfo is QuestInfo && ImGui.IsItemClicked() && _commandManager.Commands.TryGetValue("/questinfo", out var commandInfo))
-        {
-            _commandManager.DispatchCommand("/questinfo", questInfo.QuestId.ToString() ?? string.Empty, commandInfo);
-        }
-
-        if (ImGui.IsItemHovered())
-            _questTooltipComponent.Draw(questInfo);
-
-        ImGui.TableNextColumn();
-        float spacing;
-        // ReSharper disable once UnusedVariable
-        using (var font = _pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
-        {
-            spacing = ImGui.GetColumnWidth() / 2 - ImGui.CalcTextSize(FontAwesomeIcon.Check.ToIconString()).X;
-        }
-
-        ImGui.SetCursorPosX(ImGui.GetCursorPosX() + spacing);
-        _uiUtils.ChecklistItem(string.Empty, quest is { Root.Disabled: false });
-
-        ImGui.TableNextColumn();
-        var (color, icon, text) = _uiUtils.GetQuestStyle(questInfo.QuestId);
-        _uiUtils.ChecklistItem(text, color, icon);
-    }
-
-    private static void DrawCount(int count, int total)
-    {
-        string len = 9999.ToString(CultureInfo.CurrentCulture);
-        ImGui.PushFont(UiBuilder.MonoFont);
-
-        string text =
-            $"{count.ToString(CultureInfo.CurrentCulture).PadLeft(len.Length)} / {total.ToString(CultureInfo.CurrentCulture).PadLeft(len.Length)}";
-        if (count == total)
-            ImGui.TextColored(ImGuiColors.ParsedGreen, text);
-        else
-            ImGui.TextUnformatted(text);
-
-        ImGui.PopFont();
-    }
-
-    private void UpdateFilter()
-    {
-        Predicate<string> match;
-        if (string.IsNullOrWhiteSpace(_searchText))
-            match = _ => true;
-        else
-            match = x => x.Contains(_searchText, StringComparison.CurrentCultureIgnoreCase);
-
-        _filteredSections = _journalData.Sections
-            .Select(section => FilterSection(section, match))
-            .Where(x => x != null)
-            .Cast<FilteredSection>()
-            .ToList();
-    }
-
-    private static FilteredSection? FilterSection(JournalData.Section section, Predicate<string> match)
-    {
-        if (match(section.Name))
-        {
-            return new FilteredSection(section,
-                section.Categories
-                    .Select(x => FilterCategory(x, _ => true))
-                    .Cast<FilteredCategory>()
-                    .ToList());
-        }
-        else
-        {
-            List<FilteredCategory> filteredCategories = section.Categories
-                .Select(category => FilterCategory(category, match))
-                .Where(x => x != null)
-                .Cast<FilteredCategory>()
-                .ToList();
-            if (filteredCategories.Count > 0)
-                return new FilteredSection(section, filteredCategories);
-
-            return null;
-        }
-    }
-
-    private static FilteredCategory? FilterCategory(JournalData.Category category, Predicate<string> match)
-    {
-        if (match(category.Name))
-        {
-            return new FilteredCategory(category,
-                category.Genres
-                    .Select(x => FilterGenre(x, _ => true))
-                    .Cast<FilteredGenre>()
-                    .ToList());
-        }
-        else
-        {
-            List<FilteredGenre> filteredGenres = category.Genres
-                .Select(genre => FilterGenre(genre, match))
-                .Where(x => x != null)
-                .Cast<FilteredGenre>()
-                .ToList();
-            if (filteredGenres.Count > 0)
-                return new FilteredCategory(category, filteredGenres);
-
-            return null;
-        }
-    }
-
-    private static FilteredGenre? FilterGenre(JournalData.Genre genre, Predicate<string> match)
-    {
-        if (match(genre.Name))
-            return new FilteredGenre(genre, genre.Quests);
-        else
-        {
-            List<IQuestInfo> filteredQuests = genre.Quests
-                .Where(x => match(x.Name))
-                .ToList();
-            if (filteredQuests.Count > 0)
-                return new FilteredGenre(genre, filteredQuests);
-        }
-
-        return null;
-    }
-
-    private void RefreshCounts()
-    {
-        _genreCounts.Clear();
-        _categoryCounts.Clear();
-        _sectionCounts.Clear();
-
-        foreach (var genre in _journalData.Genres)
-        {
-            int available = genre.Quests.Count(x =>
-                _questRegistry.TryGetQuest(x.QuestId, out var quest) && !quest.Root.Disabled);
-            int completed = genre.Quests.Count(x => _questFunctions.IsQuestComplete(x.QuestId));
-            _genreCounts[genre] = (available, completed);
-        }
-
-        foreach (var category in _journalData.Categories)
-        {
-            var counts = _genreCounts
-                .Where(x => category.Genres.Contains(x.Key))
-                .Select(x => x.Value)
-                .ToList();
-            int available = counts.Sum(x => x.Available);
-            int completed = counts.Sum(x => x.Completed);
-            _categoryCounts[category] = (available, completed);
-        }
-
-        foreach (var section in _journalData.Sections)
-        {
-            var counts = _categoryCounts
-                .Where(x => section.Categories.Contains(x.Key))
-                .Select(x => x.Value)
-                .ToList();
-            int available = counts.Sum(x => x.Available);
-            int completed = counts.Sum(x => x.Completed);
-            _sectionCounts[section] = (available, completed);
-        }
-    }
-
-    private void ClearCounts()
-    {
-        foreach (var genreCount in _genreCounts.ToList())
-            _genreCounts[genreCount.Key] = (genreCount.Value.Available, 0);
-
-        foreach (var categoryCount in _categoryCounts.ToList())
-            _categoryCounts[categoryCount.Key] = (categoryCount.Value.Available, 0);
-
-        foreach (var sectionCount in _sectionCounts.ToList())
-            _sectionCounts[sectionCount.Key] = (sectionCount.Value.Available, 0);
+        _questJournalComponent.DrawQuests();
+        _gatheringJournalComponent.DrawGatheringItems();
     }
 
     public void Dispose()
     {
         _questRegistry.Reloaded -= OnQuestsReloaded;
-        _clientState.Logout -= ClearCounts;
-        _clientState.Login -= RefreshCounts;
+        _clientState.Logout -= _questJournalComponent.ClearCounts;
+        _clientState.Login -= _gatheringJournalComponent.RefreshCounts;
+        _clientState.Login -= _questJournalComponent.RefreshCounts;
     }
-
-    private sealed record FilteredSection(JournalData.Section Section, List<FilteredCategory> Categories);
-
-    private sealed record FilteredCategory(JournalData.Category Category, List<FilteredGenre> Genres);
-
-    private sealed record FilteredGenre(JournalData.Genre Genre, List<IQuestInfo> Quests);
 }