Files
SimpleFinanceDash/.planning/phases/07-setup-wizard/07-PATTERNS.md
2026-04-20 21:01:33 +02:00

9.1 KiB

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):

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):

// Standalone centered card outside AppLayout -- same pattern as LoginPage
return (
  <div className="flex min-h-screen items-center justify-center bg-muted/60 p-4">
    <Card className="w-full max-w-sm border-t-4 border-t-primary shadow-lg">

Async action with loading state (lines 20-31):

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):

<div className="space-y-2">
  <Label>{t("settings.displayName")}</Label>
  <Input
    value={profile?.display_name ?? ""}
    onChange={(e) =>
      setProfile((p) => p && { ...p, display_name: e.target.value })
    }
  />
</div>

Button with disabled state (line 133):

<Button onClick={handleSave} disabled={saving}>
  {t("settings.save")}
</Button>

src/components/setup/WizardStepper.tsx (component, transform)

Analog: src/components/dashboard/SummaryStrip.tsx

Presentational component pattern (lines 1-16):

// 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 (
    <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
      {/* child components */}
    </div>
  )
}

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):

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<typeof g> => 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):

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):

export default function App() {
  return (
    <Routes>
      <Route path="/login" element={<PublicRoute><LoginPage /></PublicRoute>} />
      <Route path="/register" element={<PublicRoute><RegisterPage /></PublicRoute>} />
      {/* Add /setup here -- protected but outside AppLayout */}
      <Route element={<ProtectedRoute><AppLayout /></ProtectedRoute>}>
        <Route index element={<DashboardPage />} />
        ...
      </Route>
      <Route path="*" element={<Navigate to="/" replace />} />
    </Routes>
  )
}

New route insertion point -- after public routes, before AppLayout route:

<Route path="/setup" element={<ProtectedRoute><SetupPage /></ProtectedRoute>} />

src/pages/DashboardPage.tsx (modify - add first-run redirect)

Pattern to add (at top of DashboardPage component, before existing logic):

// 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 <DashboardSkeleton />
if (isFirstRun) return <Navigate to="/setup" replace />

Shared Patterns

Toast Notifications

Source: src/pages/SettingsPage.tsx lines 4, 56-59 Apply to: SetupPage (on completion and on error)

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)

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

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)

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

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

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