241 lines
9.7 KiB
Markdown
241 lines
9.7 KiB
Markdown
# Architecture
|
|
|
|
**Analysis Date:** 2026-03-16
|
|
|
|
## Pattern Overview
|
|
|
|
**Overall:** Layered client-side SPA with React, using hooks for data access and state management through TanStack Query, centralized authentication with Supabase, and component-based UI rendering.
|
|
|
|
**Key Characteristics:**
|
|
- Three-tier vertical slice: Pages → Hooks → Library (Supabase + utilities)
|
|
- TanStack Query for caching and synchronization of remote state
|
|
- Route-based code organization with role-based access control (protected/public routes)
|
|
- Supabase PostgreSQL backend with real-time subscriptions capability
|
|
- Internationalization (i18n) at the root level with JSON resource files
|
|
|
|
## Layers
|
|
|
|
**Presentation Layer (Pages & Components):**
|
|
- Purpose: Render UI, handle user interactions, coordinate component composition
|
|
- Location: `src/pages/` and `src/components/`
|
|
- Contains: Page components (DashboardPage, TemplatePage, BudgetListPage, etc.), UI primitives from `src/components/ui/`, and custom components (AppLayout, QuickAddPicker)
|
|
- Depends on: Hooks, UI utilities, i18n, icons (lucide-react), formatting utilities
|
|
- Used by: React Router for page routing
|
|
|
|
**Hooks Layer (Data Access & State):**
|
|
- Purpose: Encapsulate all server communication and query/mutation logic, manage request/response caching via TanStack Query
|
|
- Location: `src/hooks/`
|
|
- Contains: Custom hooks for each domain (useAuth, useCategories, useBudgets, useTemplate, useQuickAdd)
|
|
- Depends on: Supabase client, TanStack Query, type definitions
|
|
- Used by: Page and component layers for CRUD operations and state retrieval
|
|
|
|
**Library Layer (Core Services & Utilities):**
|
|
- Purpose: Provide primitive implementations, type definitions, and configuration
|
|
- Location: `src/lib/`
|
|
- Contains:
|
|
- `supabase.ts` - Supabase client initialization and environment validation
|
|
- `types.ts` - TypeScript interfaces for all domain entities (Profile, Category, Budget, BudgetItem, Template, TemplateItem, QuickAddItem)
|
|
- `format.ts` - Currency formatting using Intl.NumberFormat
|
|
- `palette.ts` - Category color mapping and labels (internationalized)
|
|
- `utils.ts` - General utilities
|
|
- Depends on: Supabase SDK, TypeScript types
|
|
- Used by: Hooks and components for types and helpers
|
|
|
|
**Authentication Layer:**
|
|
- Purpose: Manage session state and auth operations
|
|
- Location: `src/hooks/useAuth.ts`
|
|
- Implementation: Reactive Supabase auth listener with automatic session refresh
|
|
- State: Session, user, loading flag
|
|
- Operations: signUp, signIn, signInWithOAuth (Google, GitHub), signOut
|
|
|
|
**Internationalization (i18n):**
|
|
- Purpose: Provide multi-language support at runtime
|
|
- Location: `src/i18n/index.ts` with JSON resource files (`en.json`, `de.json`)
|
|
- Framework: react-i18next with i18next
|
|
- Initialization: Automatic at app startup before any render
|
|
|
|
## Data Flow
|
|
|
|
**Read Pattern (Fetch & Display):**
|
|
|
|
1. Component renders and mounts
|
|
2. Component calls custom hook (e.g., `useBudgets()`, `useCategories()`)
|
|
3. Hook initializes TanStack Query with async queryFn
|
|
4. Query function calls Supabase table select with filters/joins
|
|
5. Supabase returns typed rows from database
|
|
6. Hook caches result in Query store with staleTime of 5 minutes
|
|
7. Component receives `data`, `loading`, and error states
|
|
8. Component renders based on data state
|
|
9. Subsequent mounts of same component use cached data (until stale)
|
|
|
|
**Write Pattern (Mutate & Sync):**
|
|
|
|
1. Component invokes mutation handler (e.g., click "Save")
|
|
2. Handler calls `mutation.mutateAsync(payload)`
|
|
3. Mutation function marshals payload and calls Supabase insert/update/delete
|
|
4. Supabase executes DB operation and returns modified row(s)
|
|
5. Mutation onSuccess callback triggers Query invalidation
|
|
6. Query re-fetches from server with fresh data
|
|
7. Component re-renders with new cached data
|
|
8. Toast notification indicates success or error
|
|
|
|
**Real-time Budget Updates:**
|
|
|
|
Example flow for editing a budget item (from `useBudgets.ts`):
|
|
1. User edits amount in budget detail table → calls `updateItem.mutateAsync({ id, budgetId, budgeted_amount: X })`
|
|
2. Hook serializes to Supabase `.update()` with .eq("id", id)
|
|
3. Response contains updated BudgetItem with joined Category data
|
|
4. onSuccess invalidates `["budgets", budgetId, "items"]` cache key
|
|
5. DashboardContent's useBudgetDetail query re-fetches entire items array
|
|
6. Component recalculates totals and re-renders pie chart and progress bars
|
|
|
|
**State Management:**
|
|
- No Redux or Zustand — TanStack Query handles async state
|
|
- Local component state for UI interactions (dialogs, forms, selections)
|
|
- Session state maintained by Supabase auth listener in useAuth hook
|
|
- Cache invalidation is the primary sync mechanism
|
|
|
|
## Key Abstractions
|
|
|
|
**Query Key Pattern:**
|
|
|
|
Each hook defines typed query keys as const arrays:
|
|
- `["categories"]` - all user categories
|
|
- `["budgets"]` - all user budgets
|
|
- `["budgets", id]` - single budget
|
|
- `["budgets", id, "items"]` - items for a specific budget
|
|
- `["template"]` - user's monthly template
|
|
- `["template-items"]` - items in template
|
|
- `["quick-add"]` - user's quick-add library
|
|
|
|
Purpose: Enables granular invalidation (e.g., update one budget doesn't refetch all budgets)
|
|
|
|
**Mutation Factories:**
|
|
|
|
Hooks expose mutations as properties of returned object:
|
|
- `useCategories()` returns `{ categories, loading, create, update, remove }`
|
|
- `useBudgets()` returns `{ budgets, loading, createBudget, generateFromTemplate, updateItem, createItem, deleteItem, deleteBudget }`
|
|
|
|
Each mutation includes:
|
|
- `mutationFn` - async operation against Supabase
|
|
- `onSuccess` - cache invalidation strategy
|
|
|
|
**Hook Composition:**
|
|
|
|
`useBudgetDetail(id)` is both a standalone export and accessible via `useBudgets().getBudget(id)`:
|
|
- Enables flexible usage patterns
|
|
- Query with `.enabled: Boolean(id)` prevents queries when id is falsy
|
|
- Used by DashboardPage to fetch current month's budget data
|
|
|
|
**Type Hierarchy:**
|
|
|
|
Core types in `src/lib/types.ts`:
|
|
- `Profile` - user profile with locale and currency
|
|
- `Category` - user-defined expense category
|
|
- `Template` & `TemplateItem` - monthly budget template (fixed/variable items)
|
|
- `Budget` & `BudgetItem` - actual budget with tracked actual/budgeted amounts
|
|
- `QuickAddItem` - quick entry library for rapid data entry
|
|
|
|
Relationships:
|
|
- User → many Profiles (one per row, per user)
|
|
- User → many Categories
|
|
- User → one Template (auto-created)
|
|
- Template → many TemplateItems → Category (join)
|
|
- User → many Budgets (monthly)
|
|
- Budget → many BudgetItems → Category (join)
|
|
- User → many QuickAddItems
|
|
|
|
## Entry Points
|
|
|
|
**Application Root:**
|
|
- Location: `src/main.tsx`
|
|
- Initializes React 19 with context providers:
|
|
- QueryClientProvider (TanStack Query)
|
|
- BrowserRouter (React Router)
|
|
- TooltipProvider (Radix UI)
|
|
- Toaster (Sonner notifications)
|
|
- Creates single QueryClient with 5-minute staleTime default
|
|
|
|
**Route Configuration:**
|
|
- Location: `src/App.tsx`
|
|
- Defines protected and public routes
|
|
- Protected routes wrapped in `ProtectedRoute` which checks auth state via `useAuth()`
|
|
- Public routes wrapped in `PublicRoute` which redirects authenticated users away
|
|
- Routes:
|
|
- `/login`, `/register` - public (redirect home if logged in)
|
|
- `/`, `/categories`, `/template`, `/budgets`, `/budgets/:id`, `/quick-add`, `/settings` - protected
|
|
|
|
**Layout Root:**
|
|
- Location: `src/components/AppLayout.tsx`
|
|
- Renders Sidebar with navigation items (using Radix UI primitives)
|
|
- Renders Outlet for nested route content
|
|
- Provides sign-out button in footer
|
|
|
|
**Page Entry Points:**
|
|
|
|
Each page component is stateless renderer with logic split between:
|
|
1. Page component (routing layer, layout)
|
|
2. Sub-component(s) (content logic)
|
|
3. Hook(s) (data fetching)
|
|
|
|
Example: `DashboardPage.tsx` →
|
|
- Main component finds current month's budget
|
|
- Delegates to `DashboardContent` with `budgetId` prop
|
|
- DashboardContent calls `useBudgetDetail(budgetId)` for data
|
|
|
|
## Error Handling
|
|
|
|
**Strategy:** Try-catch in mutation handlers with toast notifications for user feedback.
|
|
|
|
**Patterns:**
|
|
|
|
1. **Mutation Errors:**
|
|
- Try block executes `mutation.mutateAsync()`
|
|
- Catch block logs to console and shows `toast.error(t("common.error"))`
|
|
- Button remains clickable if error is transient
|
|
- Example in `TemplatePage.tsx` lines 182-204
|
|
|
|
2. **Query Errors:**
|
|
- Query errors are captured by TanStack Query internally
|
|
- Hooks return loading state but not explicit error state
|
|
- Pages render null/loading state during failed queries
|
|
- Retry is configured to attempt once by default
|
|
|
|
3. **Auth Errors:**
|
|
- `useAuth.ts` catches Supabase auth errors and re-throws
|
|
- LoginPage catches and displays error message in red text
|
|
- Session state remains null if auth fails
|
|
|
|
4. **Missing Environment Variables:**
|
|
- `src/lib/supabase.ts` throws during module load if VITE_SUPABASE_URL or VITE_SUPABASE_ANON_KEY missing
|
|
- Prevents runtime errors from undefined client
|
|
|
|
## Cross-Cutting Concerns
|
|
|
|
**Logging:**
|
|
Minimal logging — relies on:
|
|
- Browser DevTools React Query Devtools (installable)
|
|
- Console errors from try-catch blocks
|
|
- Sonner toast notifications for user-facing issues
|
|
|
|
**Validation:**
|
|
- Input validation in mutation handlers (e.g., category ID check, amount > 0)
|
|
- Type validation via TypeScript compile-time
|
|
- Supabase schema constraints (NOT NULL, FOREIGN KEY, CHECK constraints)
|
|
- Database uniqueness constraints (e.g., one template per user)
|
|
|
|
**Authentication:**
|
|
- Supabase session token stored in localStorage (browser SDK default)
|
|
- Auth state checked at page render time via `useAuth()` hook
|
|
- Protected routes redirect unauthenticated users to /login
|
|
- Session persists across page refreshes (Supabase handles recovery)
|
|
|
|
**Authorization:**
|
|
- Row-level security (RLS) policies on Supabase tables enforce user_id filters
|
|
- No explicit authorization logic in client (relies on DB policies)
|
|
- All queries filter by current user via `await supabase.auth.getUser()`
|
|
|
|
---
|
|
|
|
*Architecture analysis: 2026-03-16*
|