+ 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
+
+
+