From 94ebd84cc740e11f896365c00a48fe432bb835c9 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Mon, 16 Mar 2026 00:07:48 +0100 Subject: [PATCH] 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 --- src/client/components/ThreadTabs.tsx | 11 ++- src/client/routeTree.gen.ts | 33 +------- src/client/routes/__root.tsx | 3 +- src/client/routes/collection/index.tsx | 102 +++++++++++++++++++++++-- src/client/routes/index.tsx | 3 +- src/client/routes/setups/index.tsx | 93 ---------------------- 6 files changed, 110 insertions(+), 135 deletions(-) delete mode 100644 src/client/routes/setups/index.tsx diff --git a/src/client/components/ThreadTabs.tsx b/src/client/components/ThreadTabs.tsx index 6c8edec..a365f3a 100644 --- a/src/client/components/ThreadTabs.tsx +++ b/src/client/components/ThreadTabs.tsx @@ -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 (
{tabs.map((tab) => ( diff --git a/src/client/routeTree.gen.ts b/src/client/routeTree.gen.ts index 2cebe4c..4073734 100644 --- a/src/client/routeTree.gen.ts +++ b/src/client/routeTree.gen.ts @@ -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) diff --git a/src/client/routes/__root.tsx b/src/client/routes/__root.tsx index e66e8e2..97c0c3d 100644 --- a/src/client/routes/__root.tsx +++ b/src/client/routes/__root.tsx @@ -95,7 +95,8 @@ function RootLayout() { const showFab = isCollection && (!collectionSearch || - (collectionSearch as Record).tab !== "planning"); + !(collectionSearch as Record).tab || + (collectionSearch as Record).tab === "gear"); // Show a minimal loading state while checking onboarding status if (onboardingLoading) { diff --git a/src/client/routes/collection/index.tsx b/src/client/routes/collection/index.tsx index ac45278..dcd8cae 100644 --- a/src/client/routes/collection/index.tsx +++ b/src/client/routes/collection/index.tsx @@ -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 (
- +
- {tab === "gear" ? : } + {tab === "gear" ? ( + + ) : tab === "planning" ? ( + + ) : ( + + )}
); @@ -374,3 +382,87 @@ function PlanningView() {
); } + +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 ( +
+ {/* Create setup form */} +
+ 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" + /> + +
+ + {/* Loading skeleton */} + {isLoading && ( +
+ {[1, 2].map((i) => ( +
+ ))} +
+ )} + + {/* Empty state */} + {!isLoading && (!setups || setups.length === 0) && ( +
+
+
+ +
+

+ No setups yet +

+

+ Create one to plan your loadout. +

+
+
+ )} + + {/* Setup grid */} + {!isLoading && setups && setups.length > 0 && ( +
+ {setups.map((setup) => ( + + ))} +
+ )} +
+ ); +} diff --git a/src/client/routes/index.tsx b/src/client/routes/index.tsx index d94ebd0..f3ee600 100644 --- a/src/client/routes/index.tsx +++ b/src/client/routes/index.tsx @@ -45,7 +45,8 @@ function DashboardPage() { ]} /> setNewSetupName("") }); - } - - return ( -
- {/* Create setup form */} -
- 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" - /> - -
- - {/* Loading skeleton */} - {isLoading && ( -
- {[1, 2].map((i) => ( -
- ))} -
- )} - - {/* Empty state */} - {!isLoading && (!setups || setups.length === 0) && ( -
-
-
- -
-

- No setups yet -

-

- Create one to plan your loadout. -

-
-
- )} - - {/* Setup grid */} - {!isLoading && setups && setups.length > 0 && ( -
- {setups.map((setup) => ( - - ))} -
- )} -
- ); -}