docs(07): pattern mapping for setup wizard
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
284
.planning/phases/07-setup-wizard/07-PATTERNS.md
Normal file
284
.planning/phases/07-setup-wizard/07-PATTERNS.md
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
# 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 (
|
||||||
|
<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):
|
||||||
|
```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
|
||||||
|
<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):
|
||||||
|
```typescript
|
||||||
|
<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):
|
||||||
|
```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 (
|
||||||
|
<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):
|
||||||
|
```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<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):
|
||||||
|
```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 (
|
||||||
|
<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:
|
||||||
|
```typescript
|
||||||
|
<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):
|
||||||
|
```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 <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)
|
||||||
|
```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
|
||||||
Reference in New Issue
Block a user