--- phase: 24-public-access-infrastructure plan: 02 type: execute wave: 1 depends_on: [] files_modified: - src/client/routes/__root.tsx - src/client/stores/uiStore.ts - src/client/components/AuthPromptModal.tsx - src/client/hooks/useSetups.ts - src/client/hooks/useSettings.ts - src/client/routes/global-items/$globalItemId.tsx - src/client/routes/setups/$setupId.tsx autonomous: false requirements: [PUBL-01, PUBL-02, PUBL-03, PUBL-04, PUBL-05] must_haves: truths: - "Anonymous visitor sees app content immediately on any public route — no spinner, no redirect" - "Anonymous visitor can browse the global item catalog and open catalog detail pages" - "Anonymous visitor can view a public setup with its items and totals" - "Anonymous visitor can view a user profile page" - "Anonymous visitor clicking 'Add to Collection' or 'Add to Thread' sees a sign-in/sign-up prompt instead of the action" - "Authenticated user experience is unchanged — all write actions work as before" artifacts: - path: "src/client/routes/__root.tsx" provides: "Render-first root layout with expanded isPublicRoute" contains: "pathname.startsWith(\"/global-items\")" - path: "src/client/stores/uiStore.ts" provides: "showAuthPrompt state for auth modal" contains: "showAuthPrompt" - path: "src/client/components/AuthPromptModal.tsx" provides: "Modal prompting anonymous users to sign in or sign up" contains: "sign in or sign up" - path: "src/client/hooks/useSetups.ts" provides: "usePublicSetup hook for anonymous setup viewing" exports: ["usePublicSetup"] - path: "src/client/routes/global-items/$globalItemId.tsx" provides: "Auth-guarded write action buttons on catalog detail" contains: "openAuthPrompt" - path: "src/client/routes/setups/$setupId.tsx" provides: "Conditional public vs private setup rendering" contains: "usePublicSetup" key_links: - from: "src/client/routes/__root.tsx" to: "src/client/components/AuthPromptModal.tsx" via: "rendered in root layout" pattern: " Make the app render immediately for anonymous visitors, expand public route access to catalog and setups, and intercept write actions with a friendly auth prompt. Purpose: Transform GearBox from a login-first tool into a public-first browsing experience (PUBL-01 through PUBL-05). Anonymous visitors see content instantly; write actions prompt sign-in/sign-up instead of hard-redirecting. Output: Reworked root layout, auth prompt modal, public setup hook, guarded write actions. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/24-public-access-infrastructure/24-CONTEXT.md @.planning/phases/24-public-access-infrastructure/24-RESEARCH.md @src/client/routes/__root.tsx @src/client/stores/uiStore.ts @src/client/hooks/useSetups.ts @src/client/hooks/useAuth.ts @src/client/hooks/useSettings.ts @src/client/routes/global-items/$globalItemId.tsx @src/client/routes/setups/$setupId.tsx @src/client/components/TotalsBar.tsx From src/client/hooks/useAuth.ts: ```typescript interface AuthState { user: { id: string; email?: string } | null; authenticated: boolean; } export function useAuth(): UseQueryResult; ``` From src/client/stores/uiStore.ts: ```typescript // Existing pattern — all boolean state with open/close actions showAuthPrompt: boolean; openAuthPrompt: () => void; closeAuthPrompt: () => void; ``` From src/client/hooks/useSetups.ts: ```typescript export function useSetup(setupId: number | null): UseQueryResult; // New hook to add: export function usePublicSetup(id: number): UseQueryResult; ``` From src/client/components/TotalsBar.tsx: ```typescript // Already handles Sign in vs UserMenu — NO changes needed (D-05, D-10 satisfied) ``` Task 1: Add auth prompt state to uiStore, create AuthPromptModal, add usePublicSetup hook src/client/stores/uiStore.ts, src/client/components/AuthPromptModal.tsx, src/client/hooks/useSetups.ts, src/client/hooks/useSettings.ts - src/client/stores/uiStore.ts (current state shape and patterns) - src/client/hooks/useSetups.ts (existing hooks, types) - src/client/hooks/useSettings.ts (useOnboardingComplete — needs `enabled` guard) - src/client/routes/__root.tsx (CandidateDeleteDialog pattern for modal structure) - src/client/components/TotalsBar.tsx (confirm D-10 already handled) **1. Extend uiStore.ts** — Add auth prompt state following the existing pattern (e.g., `externalLinkUrl`): Add to the `UIState` interface: ```typescript // Auth prompt modal showAuthPrompt: boolean; openAuthPrompt: () => void; closeAuthPrompt: () => void; ``` Add to the `create` implementation: ```typescript // Auth prompt modal showAuthPrompt: false, openAuthPrompt: () => set({ showAuthPrompt: true }), closeAuthPrompt: () => set({ showAuthPrompt: false }), ``` **2. Create AuthPromptModal.tsx** per D-06 — inline popup/modal with "sign in or sign up" language. Follow the exact pattern of CandidateDeleteDialog in `__root.tsx` (fixed overlay, centered card, bg-black/30 backdrop): ```typescript import { Link } from "@tanstack/react-router"; import { useUIStore } from "../stores/uiStore"; export function AuthPromptModal() { const showAuthPrompt = useUIStore((s) => s.showAuthPrompt); const closeAuthPrompt = useUIStore((s) => s.closeAuthPrompt); if (!showAuthPrompt) return null; return (
{ if (e.key === "Escape") closeAuthPrompt(); }} />

Join GearBox

To manage your own collection, sign in or sign up.

Sign in Create account
); } ``` Both links go to `/login` because Logto handles both sign-in and sign-up at the same OIDC redirect. The UX distinction is in the button labels per the user's emphasis on welcoming new users (from specifics in CONTEXT.md). **3. Add usePublicSetup hook** in `src/client/hooks/useSetups.ts`: Add after the existing `useSetup` function: ```typescript export function usePublicSetup(setupId: number | null) { return useQuery({ queryKey: ["setups", setupId, "public"], queryFn: () => apiGet(`/api/setups/${setupId}/public`), enabled: setupId != null, retry: (count, error) => error instanceof ApiError && error.status === 404 ? false : count < 3, }); } ``` The public endpoint returns the same shape as the private one (SetupWithItems) but with `isPublic` always `true` and the owner's category names included as read-only context per D-03. **4. Guard useOnboardingComplete** in `src/client/hooks/useSettings.ts` — Pitfall 2 from research. The `useSetting` hook calls an auth-gated endpoint. For unauthenticated users, it returns an error and `isLoading` may be `true` briefly, blocking render. Change `useOnboardingComplete` to accept an `enabled` parameter: ```typescript export function useOnboardingComplete(enabled = true) { return useQuery({ queryKey: ["settings", "onboardingComplete"], queryFn: async () => { try { const result = await apiGet(`/api/settings/onboardingComplete`); return result.value; } catch (err: any) { if (err?.status === 404) return null; throw err; } }, enabled, }); } ``` This replaces the current delegation to `useSetting("onboardingComplete")` with a direct `useQuery` call that accepts an `enabled` parameter. The query logic is identical to `useSetting` — just inlined so `enabled` can be passed through. cd /home/jean-luc-makiola/Development/projects/GearBox && bun run lint - uiStore.ts contains `showAuthPrompt: boolean` in the interface - uiStore.ts contains `openAuthPrompt: () => set({ showAuthPrompt: true })` - uiStore.ts contains `closeAuthPrompt: () => set({ showAuthPrompt: false })` - AuthPromptModal.tsx exists and contains `sign in or sign up` - AuthPromptModal.tsx contains `to="/login"` (both links point to /login) - AuthPromptModal.tsx contains `Create account` button text - AuthPromptModal.tsx contains `className="fixed inset-0 z-50` - useSetups.ts contains `export function usePublicSetup(` - useSetups.ts contains `/api/setups/${setupId}/public` - useSettings.ts `useOnboardingComplete` accepts `enabled` parameter - `bun run lint` exits 0 Auth prompt modal component created, uiStore extended, usePublicSetup hook added, useOnboardingComplete accepts enabled flag Task 2: Rework __root.tsx for render-first, guard write actions on catalog and setup pages src/client/routes/__root.tsx, src/client/routes/global-items/$globalItemId.tsx, src/client/routes/setups/$setupId.tsx - src/client/routes/__root.tsx (current auth loading spinner, redirect logic, isPublicRoute) - src/client/routes/global-items/$globalItemId.tsx (action buttons to guard) - src/client/routes/setups/$setupId.tsx (full file — need to understand write actions and data flow) - src/client/stores/uiStore.ts (after Task 1 — confirm showAuthPrompt exists) - src/client/components/AuthPromptModal.tsx (after Task 1 — confirm component exists) - src/client/hooks/useSetups.ts (after Task 1 — confirm usePublicSetup exists) **1. Rework __root.tsx** per D-04 and D-09: **a. Remove the authLoading spinner gate (lines 121-127).** Delete this entire block: ```typescript // REMOVE THIS: if (authLoading) { return (
); } ``` **b. Expand isPublicRoute** (replace current line 131-132): ```typescript const isPublicRoute = location.pathname === "/" || location.pathname.startsWith("/users/") || location.pathname.startsWith("/global-items") || location.pathname.startsWith("/setups/") || location.pathname === "/login"; ``` **c. Replace hard redirect** (replace lines 138-145). Remove `window.location.href = "/login"` and replace with soft redirect that only fires after auth resolves: ```typescript if (!isAuthenticated && !isPublicRoute && !authLoading) { navigate({ to: "/login" }); return null; } ``` **d. Remove onboarding loading spinner gate** (lines 147-154). Delete the entire `if (onboardingLoading)` block. The `showWizard` check already guards on `isAuthenticated`, so this gate is unnecessary. Update the `useOnboardingComplete` call to pass `enabled: isAuthenticated`: ```typescript const { data: onboardingComplete, isLoading: onboardingLoading } = useOnboardingComplete(isAuthenticated); ``` **e. Add AuthPromptModal** to the return JSX. Import at top: ```typescript import { AuthPromptModal } from "../components/AuthPromptModal"; ``` Add inside the root `
`, after the `` and before the onboarding wizard: ```tsx {/* Auth Prompt Modal */} ``` **2. Guard write actions in global-items/$globalItemId.tsx** per D-06 and PUBL-05: Add imports: ```typescript import { useAuth } from "../../hooks/useAuth"; ``` Inside the `GlobalItemDetail` component, add: ```typescript const { data: auth } = useAuth(); const isAuthenticated = !!auth?.user; const openAuthPrompt = useUIStore((s) => s.openAuthPrompt); ``` Replace the two button onClick handlers. For "Add to Collection": ```typescript onClick={() => { if (!isAuthenticated) { openAuthPrompt(); return; } openAddToCollection(item.id, `${item.brand} ${item.model}`); }} ``` For "Add to Thread": ```typescript onClick={() => { if (!isAuthenticated) { openAuthPrompt(); return; } openAddToThread(item.id, `${item.brand} ${item.model}`); }} ``` **3. Rework setups/$setupId.tsx** for anonymous viewing per PUBL-02: This is the most complex change. The current page calls `useSetup(id)` which hits the auth-gated `GET /api/setups/:id`. Anonymous visitors get a 401. Add imports: ```typescript import { useAuth } from "../../hooks/useAuth"; import { usePublicSetup } from "../../hooks/useSetups"; ``` At the top of `SetupDetailPage`, add auth detection: ```typescript const { data: auth } = useAuth(); const isAuthenticated = !!auth?.user; ``` Change the data fetching to be conditional: ```typescript const privateSetup = useSetup(isAuthenticated ? numericId : null); const publicSetup = usePublicSetup(!isAuthenticated ? numericId : null); const { data: setup, isLoading } = isAuthenticated ? privateSetup : publicSetup; ``` Wrap all write action UI elements (Delete button, Add Items button, Public toggle, remove item buttons, classification dropdowns) in `isAuthenticated` guards: ```typescript {isAuthenticated && ( )} ``` Apply this guard to: - The "Add Items" button - The "Delete Setup" button and its confirmation dialog - The "Public" toggle switch - The remove button on individual items (the X icon) - The classification dropdown on individual items - The `ItemPicker` component render The read-only display (setup name, items list, weight summary, totals) should render for everyone. Also: the mutation hooks (`useDeleteSetup`, `useUpdateSetup`, `useRemoveSetupItem`, `useUpdateItemClassification`) can remain — they just won't be invoked since their triggers are hidden. But `useDeleteSetup()` and `useUpdateSetup(numericId)` calls at the top of the component are fine to keep (they return mutation objects, no network call until `.mutate()` is called). cd /home/jean-luc-makiola/Development/projects/GearBox && bun run lint - __root.tsx does NOT contain `if (authLoading)` followed by a spinner return - __root.tsx does NOT contain `window.location.href = "/login"` - __root.tsx contains `pathname === "/" ||` in isPublicRoute - __root.tsx contains `pathname.startsWith("/global-items")` in isPublicRoute - __root.tsx contains `pathname.startsWith("/setups/")` in isPublicRoute - __root.tsx contains `navigate({ to: "/login" })` (soft redirect for private routes) - __root.tsx contains `!authLoading` in the redirect condition - __root.tsx contains ` Root layout renders immediately for anonymous visitors. Public routes include /, /global-items/*, /setups/*, /users/*, /login. Write actions on catalog detail show auth prompt. Setup detail page shows read-only view for anonymous visitors using the public API endpoint. Task 3: Verify public access flows Complete public access infrastructure: anonymous visitors can browse catalog, view public setups, and view profiles without logging in. Write actions show a friendly sign-in/sign-up prompt instead of redirecting. 1. Start dev server: `bun run dev` 2. Open an incognito/private browser window (no session) 3. Visit `http://localhost:5173/` — should see the app immediately (no spinner, no redirect to /login) 4. Visit `http://localhost:5173/global-items` — catalog page loads with items 5. Click on any catalog item — detail page loads with image, specs, action buttons 6. Click "Add to Collection" — auth prompt modal appears with "sign in or sign up" message, two buttons (Sign in, Create account) 7. Close the modal (click backdrop or press Escape) 8. Click "Add to Thread" — same auth prompt modal appears 9. Visit a public setup URL (e.g., `http://localhost:5173/setups/1` if a public setup exists) — setup renders with items and totals, no write action buttons visible 10. Visit a user profile (e.g., `http://localhost:5173/users/1`) — profile page loads 11. Verify top-right corner shows "Sign in" link (already existing in TotalsBar) 12. Now log in normally — verify all write actions work as before (FAB appears, Add to Collection works, setup edit buttons appear) Type "approved" or describe issues - Anonymous visitor can browse catalog without login (PUBL-01) - Anonymous visitor can view public setups (PUBL-02) - Anonymous visitor can view user profiles (PUBL-03) - No auth spinner or redirect on first visit (PUBL-04) - Write actions prompt sign-in instead of executing (PUBL-05) - `bun run lint` passes - `bun test` passes (no regressions) - Root layout renders immediately for anonymous visitors - isPublicRoute includes /, /global-items/*, /setups/*, /users/*, /login - AuthPromptModal shows friendly sign-in/sign-up prompt on write action attempts - Setup detail page uses public API endpoint for anonymous visitors - Catalog detail page guards both "Add to Collection" and "Add to Thread" buttons - No hard redirects (window.location.href) remain in root layout - Authenticated user experience is completely unchanged After completion, create `.planning/phases/24-public-access-infrastructure/24-02-SUMMARY.md`