refactor: move setups list into collection page as third tab

Setups now lives alongside My Gear and Planning under /collection?tab=setups
instead of its own /setups route. Dashboard card updated to link to the new
tab. Setup detail pages (/setups/:id) remain unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-16 00:07:48 +01:00
parent 5938a686c7
commit 6c62111ff1
6 changed files with 110 additions and 135 deletions

View File

@@ -1,14 +1,17 @@
interface ThreadTabsProps {
active: "gear" | "planning";
onChange: (tab: "gear" | "planning") => void;
type TabKey = "gear" | "planning" | "setups";
interface CollectionTabsProps {
active: TabKey;
onChange: (tab: TabKey) => void;
}
const tabs = [
{ key: "gear" as const, label: "My Gear" },
{ key: "planning" as const, label: "Planning" },
{ key: "setups" as const, label: "Setups" },
];
export function ThreadTabs({ active, onChange }: ThreadTabsProps) {
export function CollectionTabs({ active, onChange }: CollectionTabsProps) {
return (
<div className="flex border-b border-gray-200">
{tabs.map((tab) => (

View File

@@ -10,7 +10,6 @@
import { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index'
import { Route as SetupsIndexRouteImport } from './routes/setups/index'
import { Route as CollectionIndexRouteImport } from './routes/collection/index'
import { Route as ThreadsThreadIdRouteImport } from './routes/threads/$threadId'
import { Route as SetupsSetupIdRouteImport } from './routes/setups/$setupId'
@@ -20,11 +19,6 @@ const IndexRoute = IndexRouteImport.update({
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const SetupsIndexRoute = SetupsIndexRouteImport.update({
id: '/setups/',
path: '/setups/',
getParentRoute: () => rootRouteImport,
} as any)
const CollectionIndexRoute = CollectionIndexRouteImport.update({
id: '/collection/',
path: '/collection/',
@@ -46,14 +40,12 @@ export interface FileRoutesByFullPath {
'/setups/$setupId': typeof SetupsSetupIdRoute
'/threads/$threadId': typeof ThreadsThreadIdRoute
'/collection/': typeof CollectionIndexRoute
'/setups/': typeof SetupsIndexRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/setups/$setupId': typeof SetupsSetupIdRoute
'/threads/$threadId': typeof ThreadsThreadIdRoute
'/collection': typeof CollectionIndexRoute
'/setups': typeof SetupsIndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
@@ -61,30 +53,18 @@ export interface FileRoutesById {
'/setups/$setupId': typeof SetupsSetupIdRoute
'/threads/$threadId': typeof ThreadsThreadIdRoute
'/collection/': typeof CollectionIndexRoute
'/setups/': typeof SetupsIndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/'
| '/setups/$setupId'
| '/threads/$threadId'
| '/collection/'
| '/setups/'
fullPaths: '/' | '/setups/$setupId' | '/threads/$threadId' | '/collection/'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
| '/setups/$setupId'
| '/threads/$threadId'
| '/collection'
| '/setups'
to: '/' | '/setups/$setupId' | '/threads/$threadId' | '/collection'
id:
| '__root__'
| '/'
| '/setups/$setupId'
| '/threads/$threadId'
| '/collection/'
| '/setups/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
@@ -92,7 +72,6 @@ export interface RootRouteChildren {
SetupsSetupIdRoute: typeof SetupsSetupIdRoute
ThreadsThreadIdRoute: typeof ThreadsThreadIdRoute
CollectionIndexRoute: typeof CollectionIndexRoute
SetupsIndexRoute: typeof SetupsIndexRoute
}
declare module '@tanstack/react-router' {
@@ -104,13 +83,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/setups/': {
id: '/setups/'
path: '/setups'
fullPath: '/setups/'
preLoaderRoute: typeof SetupsIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/collection/': {
id: '/collection/'
path: '/collection'
@@ -140,7 +112,6 @@ const rootRouteChildren: RootRouteChildren = {
SetupsSetupIdRoute: SetupsSetupIdRoute,
ThreadsThreadIdRoute: ThreadsThreadIdRoute,
CollectionIndexRoute: CollectionIndexRoute,
SetupsIndexRoute: SetupsIndexRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)

View File

@@ -95,7 +95,8 @@ function RootLayout() {
const showFab =
isCollection &&
(!collectionSearch ||
(collectionSearch as Record<string, string>).tab !== "planning");
!(collectionSearch as Record<string, string>).tab ||
(collectionSearch as Record<string, string>).tab === "gear");
// Show a minimal loading state while checking onboarding status
if (onboardingLoading) {

View File

@@ -4,17 +4,19 @@ import { z } from "zod";
import { CategoryHeader } from "../../components/CategoryHeader";
import { CreateThreadModal } from "../../components/CreateThreadModal";
import { ItemCard } from "../../components/ItemCard";
import { SetupCard } from "../../components/SetupCard";
import { ThreadCard } from "../../components/ThreadCard";
import { ThreadTabs } from "../../components/ThreadTabs";
import { CollectionTabs } from "../../components/ThreadTabs";
import { useCategories } from "../../hooks/useCategories";
import { useItems } from "../../hooks/useItems";
import { useCreateSetup, useSetups } from "../../hooks/useSetups";
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"),
tab: z.enum(["gear", "planning", "setups"]).catch("gear"),
});
export const Route = createFileRoute("/collection/")({
@@ -26,15 +28,21 @@ function CollectionPage() {
const { tab } = Route.useSearch();
const navigate = useNavigate();
function handleTabChange(newTab: "gear" | "planning") {
function handleTabChange(newTab: "gear" | "planning" | "setups") {
navigate({ to: "/collection", search: { tab: newTab } });
}
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<ThreadTabs active={tab} onChange={handleTabChange} />
<CollectionTabs active={tab} onChange={handleTabChange} />
<div className="mt-6">
{tab === "gear" ? <CollectionView /> : <PlanningView />}
{tab === "gear" ? (
<CollectionView />
) : tab === "planning" ? (
<PlanningView />
) : (
<SetupsView />
)}
</div>
</div>
);
@@ -374,3 +382,87 @@ function PlanningView() {
</div>
);
}
function SetupsView() {
const [newSetupName, setNewSetupName] = useState("");
const { data: setups, isLoading } = useSetups();
const createSetup = useCreateSetup();
function handleCreateSetup(e: React.FormEvent) {
e.preventDefault();
const name = newSetupName.trim();
if (!name) return;
createSetup.mutate({ name }, { onSuccess: () => setNewSetupName("") });
}
return (
<div>
{/* Create setup form */}
<form onSubmit={handleCreateSetup} className="flex gap-2 mb-6">
<input
type="text"
value={newSetupName}
onChange={(e) => setNewSetupName(e.target.value)}
placeholder="New setup name..."
className="flex-1 px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent"
/>
<button
type="submit"
disabled={!newSetupName.trim() || createSetup.isPending}
className="px-4 py-2 bg-gray-700 hover:bg-gray-800 disabled:opacity-50 text-white text-sm font-medium rounded-lg transition-colors"
>
{createSetup.isPending ? "Creating..." : "Create"}
</button>
</form>
{/* Loading skeleton */}
{isLoading && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{[1, 2].map((i) => (
<div
key={i}
className="h-24 bg-gray-200 rounded-xl animate-pulse"
/>
))}
</div>
)}
{/* Empty state */}
{!isLoading && (!setups || setups.length === 0) && (
<div className="py-16 text-center">
<div className="max-w-md mx-auto">
<div className="mb-4">
<LucideIcon
name="tent"
size={48}
className="text-gray-400 mx-auto"
/>
</div>
<h2 className="text-xl font-semibold text-gray-900 mb-2">
No setups yet
</h2>
<p className="text-sm text-gray-500">
Create one to plan your loadout.
</p>
</div>
</div>
)}
{/* Setup grid */}
{!isLoading && setups && setups.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{setups.map((setup) => (
<SetupCard
key={setup.id}
id={setup.id}
name={setup.name}
itemCount={setup.itemCount}
totalWeight={setup.totalWeight}
totalCost={setup.totalCost}
/>
))}
</div>
)}
</div>
);
}

View File

@@ -45,7 +45,8 @@ function DashboardPage() {
]}
/>
<DashboardCard
to="/setups"
to="/collection"
search={{ tab: "setups" }}
title="Setups"
icon="tent"
stats={[{ label: "Setups", value: String(setupCount) }]}

View File

@@ -1,93 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import { useState } from "react";
import { SetupCard } from "../../components/SetupCard";
import { useCreateSetup, useSetups } from "../../hooks/useSetups";
import { LucideIcon } from "../../lib/iconData";
export const Route = createFileRoute("/setups/")({
component: SetupsListPage,
});
function SetupsListPage() {
const [newSetupName, setNewSetupName] = useState("");
const { data: setups, isLoading } = useSetups();
const createSetup = useCreateSetup();
function handleCreateSetup(e: React.FormEvent) {
e.preventDefault();
const name = newSetupName.trim();
if (!name) return;
createSetup.mutate({ name }, { onSuccess: () => setNewSetupName("") });
}
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
{/* Create setup form */}
<form onSubmit={handleCreateSetup} className="flex gap-2 mb-6">
<input
type="text"
value={newSetupName}
onChange={(e) => setNewSetupName(e.target.value)}
placeholder="New setup name..."
className="flex-1 px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent"
/>
<button
type="submit"
disabled={!newSetupName.trim() || createSetup.isPending}
className="px-4 py-2 bg-gray-700 hover:bg-gray-800 disabled:opacity-50 text-white text-sm font-medium rounded-lg transition-colors"
>
{createSetup.isPending ? "Creating..." : "Create"}
</button>
</form>
{/* Loading skeleton */}
{isLoading && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{[1, 2].map((i) => (
<div
key={i}
className="h-24 bg-gray-200 rounded-xl animate-pulse"
/>
))}
</div>
)}
{/* Empty state */}
{!isLoading && (!setups || setups.length === 0) && (
<div className="py-16 text-center">
<div className="max-w-md mx-auto">
<div className="mb-4">
<LucideIcon
name="tent"
size={48}
className="text-gray-400 mx-auto"
/>
</div>
<h2 className="text-xl font-semibold text-gray-900 mb-2">
No setups yet
</h2>
<p className="text-sm text-gray-500">
Create one to plan your loadout.
</p>
</div>
</div>
)}
{/* Setup grid */}
{!isLoading && setups && setups.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{setups.map((setup) => (
<SetupCard
key={setup.id}
id={setup.id}
name={setup.name}
itemCount={setup.itemCount}
totalWeight={setup.totalWeight}
totalCost={setup.totalCost}
/>
))}
</div>
)}
</div>
);
}