# Phase 7: Setup Wizard - Pattern Map **Mapped:** 2026-04-20 **Files analyzed:** 9 **Analogs found:** 9 / 9 ## File Classification | New/Modified File | Role | Data Flow | Closest Analog | Match Quality | |-------------------|------|-----------|----------------|---------------| | `src/pages/SetupPage.tsx` | page | request-response | `src/pages/LoginPage.tsx` | exact | | `src/components/setup/WizardStepper.tsx` | component | transform | `src/components/dashboard/SummaryStrip.tsx` | role-match | | `src/components/setup/IncomeStep.tsx` | component | request-response | `src/pages/SettingsPage.tsx` (form section) | role-match | | `src/components/setup/RecurringItemsStep.tsx` | component | transform | `src/pages/DashboardPage.tsx` (grouped data) | partial | | `src/components/setup/ReviewStep.tsx` | component | transform | `src/components/dashboard/SummaryStrip.tsx` | role-match | | `src/components/setup/AllocationBar.tsx` | component | transform | `src/components/dashboard/StatCard.tsx` | role-match | | `src/components/setup/PresetItemRow.tsx` | component | request-response | `src/components/dashboard/CategorySection.tsx` | partial | | `src/App.tsx` (modify) | route | request-response | `src/App.tsx` (existing) | exact | | `src/pages/DashboardPage.tsx` (modify) | page | request-response | `src/pages/DashboardPage.tsx` (existing) | exact | ## Pattern Assignments ### `src/pages/SetupPage.tsx` (page, request-response) **Analog:** `src/pages/LoginPage.tsx` **Imports pattern** (lines 1-9): ```typescript import { useState } from "react" import { Link, useNavigate } from "react-router-dom" import { useTranslation } from "react-i18next" import { useAuth } from "@/hooks/useAuth" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" ``` **Page layout pattern** (lines 34-36): ```typescript // Standalone centered card outside AppLayout -- same pattern as LoginPage return (
``` **Async action with loading state** (lines 20-31): ```typescript const [loading, setLoading] = useState(false) async function handleSubmit(e: React.FormEvent) { e.preventDefault() setError("") setLoading(true) try { await signIn(email, password) navigate("/") } catch (err) { setError(err instanceof Error ? err.message : t("common.error")) } finally { setLoading(false) } } ``` **Key adaptation:** SetupPage uses `max-w-2xl` (not `max-w-sm`) per CONTEXT decision. It manages multi-step state internally with useState + localStorage sync. --- ### `src/components/setup/IncomeStep.tsx` (component, request-response) **Analog:** `src/pages/SettingsPage.tsx` (form input section) **Form field pattern** (lines 88-96): ```typescript
setProfile((p) => p && { ...p, display_name: e.target.value }) } />
``` **Button with disabled state** (line 133): ```typescript ``` --- ### `src/components/setup/WizardStepper.tsx` (component, transform) **Analog:** `src/components/dashboard/SummaryStrip.tsx` **Presentational component pattern** (lines 1-16): ```typescript // Props interface with clear typing, named export, no hooks beyond props interface SummaryStripProps { income: { value: string; budgeted: string } expenses: { value: string; budgeted: string } balance: { value: string; isPositive: boolean } t: (key: string) => string } export function SummaryStrip({ income, expenses, balance, t }: SummaryStripProps) { return (
{/* child components */}
) } ``` **Key adaptation:** WizardStepper accepts `currentStep: 1|2|3` and `onStepClick: (step) => void` props. Renders 3 numbered circles with connecting lines. --- ### `src/components/setup/RecurringItemsStep.tsx` (component, transform) **Analog:** `src/pages/DashboardPage.tsx` (grouped items pattern) **Grouping by category type** (lines 138-155): ```typescript const groupedSections = useMemo(() => CATEGORY_TYPES_ALL .map((type) => { const groupItems = items.filter((i) => i.category?.type === type) if (groupItems.length === 0) return null return { type, label: t(`categories.types.${type}`), items: groupItems, } }) .filter((g): g is NonNullable => g !== null), [items, t] ) ``` **Key adaptation:** Groups PRESETS by `type` field. Each group has a header and list of PresetItemRow components. --- ### `src/components/setup/AllocationBar.tsx` (component, transform) **Analog:** `src/components/dashboard/SummaryStrip.tsx` **Conditional color class pattern** (line 47): ```typescript valueClassName={balance.isPositive ? "text-on-budget" : "text-over-budget"} ``` **Key adaptation:** Displays "Remaining: {amount}" with red text when negative. Uses sticky positioning. --- ### `src/App.tsx` (modify - add /setup route) **Existing routing pattern** (lines 28-65): ```typescript export default function App() { return ( } /> } /> {/* Add /setup here -- protected but outside AppLayout */} }> } /> ... } /> ) } ``` **New route insertion point** -- after public routes, before AppLayout route: ```typescript } /> ``` --- ### `src/pages/DashboardPage.tsx` (modify - add first-run redirect) **Pattern to add** (at top of DashboardPage component, before existing logic): ```typescript // Source: useFirstRunState hook (src/hooks/useFirstRunState.ts) import { useFirstRunState } from "@/hooks/useFirstRunState" import { Navigate } from "react-router-dom" // Inside component body, early return: const { isFirstRun, loading: firstRunLoading } = useFirstRunState() if (firstRunLoading) return if (isFirstRun) return ``` --- ## Shared Patterns ### Toast Notifications **Source:** `src/pages/SettingsPage.tsx` lines 4, 56-59 **Apply to:** SetupPage (on completion and on error) ```typescript import { toast } from "sonner" // Success: toast.success(t("settings.saved")) // Error: toast.error(t("common.error")) ``` ### Supabase Profile Update **Source:** `src/pages/SettingsPage.tsx` lines 44-62 **Apply to:** SetupPage (marking setup_completed = true) ```typescript import { supabase } from "@/lib/supabase" const { error } = await supabase .from("profiles") .update({ setup_completed: true }) .eq("id", user.id) ``` ### useCategories Mutation **Source:** `src/hooks/useCategories.ts` lines 21-38 **Apply to:** SetupPage completion logic ```typescript const { create } = useCategories() // Usage: create.mutateAsync({ name, type }) const cat = await create.mutateAsync({ name: t(`categories.types.${type}`), type }) // Returns Category with .id for linking template items ``` ### React Query Invalidation After Completion **Source:** `src/hooks/useCategories.ts` line 37 **Apply to:** SetupPage (prevent redirect loop after completion) ```typescript import { useQueryClient } from "@tanstack/react-query" const queryClient = useQueryClient() // After creating categories + template items: await queryClient.invalidateQueries({ queryKey: ["categories"] }) await queryClient.invalidateQueries({ queryKey: ["template-items"] }) ``` ### i18n Translation Pattern **Source:** `src/pages/LoginPage.tsx` lines 3, 12, 39 **Apply to:** All setup components ```typescript import { useTranslation } from "react-i18next" const { t } = useTranslation() // Usage: t("setup.stepTitle") or t(`presets.${type}.${slug}`) for preset names ``` ### PRESETS Data Access **Source:** `src/data/presets.ts` **Apply to:** RecurringItemsStep, ReviewStep, SetupPage state initialization ```typescript import { PRESETS } from "@/data/presets" // Group by type: const grouped = Object.groupBy(PRESETS, (p) => p.type) // Or filter: PRESETS.filter(p => p.type === "bill") ``` ## No Analog Found | File | Role | Data Flow | Reason | |------|------|-----------|--------| | `src/components/setup/PresetItemRow.tsx` | component | request-response | No existing checkbox-list-row pattern; closest is CategorySection but quite different. Use shadcn Checkbox + Input + Badge inline. | | `src/components/setup/CategoryGroupHeader.tsx` | component | transform | Simple heading; trivial enough to not need an analog. | ## Metadata **Analog search scope:** `src/pages/`, `src/components/`, `src/hooks/`, `src/data/` **Files scanned:** 40+ **Pattern extraction date:** 2026-04-20