From 7b11f80a54c8189ba1adf7d84c1b97df1084d2ae Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Mon, 20 Apr 2026 21:01:33 +0200 Subject: [PATCH] docs(07): pattern mapping for setup wizard Co-Authored-By: Claude Sonnet 4.6 --- .../phases/07-setup-wizard/07-PATTERNS.md | 284 ++++++++++++++++++ 1 file changed, 284 insertions(+) create mode 100644 .planning/phases/07-setup-wizard/07-PATTERNS.md diff --git a/.planning/phases/07-setup-wizard/07-PATTERNS.md b/.planning/phases/07-setup-wizard/07-PATTERNS.md new file mode 100644 index 0000000..62d6a78 --- /dev/null +++ b/.planning/phases/07-setup-wizard/07-PATTERNS.md @@ -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 ( +
+ +``` + +**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