import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useState } from "react";
import { z } from "zod";
import { CategoryHeader } from "../../components/CategoryHeader";
import { CreateThreadModal } from "../../components/CreateThreadModal";
import { ItemCard } from "../../components/ItemCard";
import { ThreadCard } from "../../components/ThreadCard";
import { ThreadTabs } from "../../components/ThreadTabs";
import { useCategories } from "../../hooks/useCategories";
import { useItems } from "../../hooks/useItems";
import { useThreads } from "../../hooks/useThreads";
import { useTotals } from "../../hooks/useTotals";
import { LucideIcon } from "../../lib/iconData";
import { useUIStore } from "../../stores/uiStore";
const searchSchema = z.object({
tab: z.enum(["gear", "planning"]).catch("gear"),
});
export const Route = createFileRoute("/collection/")({
validateSearch: searchSchema,
component: CollectionPage,
});
function CollectionPage() {
const { tab } = Route.useSearch();
const navigate = useNavigate();
function handleTabChange(newTab: "gear" | "planning") {
navigate({ to: "/collection", search: { tab: newTab } });
}
return (
);
}
function CollectionView() {
const { data: items, isLoading: itemsLoading } = useItems();
const { data: totals } = useTotals();
const openAddPanel = useUIStore((s) => s.openAddPanel);
if (itemsLoading) {
return (
{[1, 2, 3].map((i) => (
))}
);
}
if (!items || items.length === 0) {
return (
Your collection is empty
Start cataloging your gear by adding your first item. Track weight,
price, and organize by category.
);
}
// Group items by categoryId
const groupedItems = new Map<
number,
{ items: typeof items; categoryName: string; categoryIcon: string }
>();
for (const item of items) {
const group = groupedItems.get(item.categoryId);
if (group) {
group.items.push(item);
} else {
groupedItems.set(item.categoryId, {
items: [item],
categoryName: item.categoryName,
categoryIcon: item.categoryIcon,
});
}
}
// Build category totals lookup
const categoryTotalsMap = new Map<
number,
{ totalWeight: number; totalCost: number; itemCount: number }
>();
if (totals?.categories) {
for (const ct of totals.categories) {
categoryTotalsMap.set(ct.categoryId, {
totalWeight: ct.totalWeight,
totalCost: ct.totalCost,
itemCount: ct.itemCount,
});
}
}
return (
<>
{Array.from(groupedItems.entries()).map(
([
categoryId,
{ items: categoryItems, categoryName, categoryIcon },
]) => {
const catTotals = categoryTotalsMap.get(categoryId);
return (
{categoryItems.map((item) => (
))}
);
},
)}
>
);
}
function PlanningView() {
const [activeTab, setActiveTab] = useState<"active" | "resolved">("active");
const [categoryFilter, setCategoryFilter] = useState(null);
const openCreateThreadModal = useUIStore((s) => s.openCreateThreadModal);
const { data: categories } = useCategories();
const { data: threads, isLoading } = useThreads(activeTab === "resolved");
if (isLoading) {
return (
);
}
// Filter threads by active tab and category
const filteredThreads = (threads ?? [])
.filter((t) => t.status === activeTab)
.filter((t) => (categoryFilter ? t.categoryId === categoryFilter : true));
// Determine if we should show the educational empty state
const isEmptyNoFilters =
filteredThreads.length === 0 &&
activeTab === "active" &&
categoryFilter === null &&
(!threads || threads.length === 0);
return (
{/* Header row */}
Planning Threads
{/* Filter row */}
{/* Pill tabs */}
{/* Category filter */}
{/* Content: empty state or thread grid */}
{isEmptyNoFilters ? (
Plan your next purchase
1
Create a thread
Start a research thread for gear you're considering
2
Add candidates
Add products you're comparing with prices and weights
3
Pick a winner
Resolve the thread and the winner joins your collection
) : filteredThreads.length === 0 ? (
) : (
{filteredThreads.map((thread) => (
))}
)}
);
}