` with 3 StatCards
+- Income card: `title={t("dashboard.totalIncome")}`, `valueClassName="text-income"`, variance with direction "neutral" and label `t("budgets.budgeted")`
+- Expenses card: `title={t("dashboard.totalExpenses")}`, `valueClassName="text-destructive"`, variance with direction "neutral" and label `t("budgets.budgeted")`
+- Balance card: `title={t("dashboard.availableBalance")}`, `valueClassName={balance.isPositive ? "text-on-budget" : "text-over-budget"}`, no variance prop
+
+Note: The `t` function is passed as a prop to keep SummaryStrip as a presentational component that does not call `useTranslation()` internally. The parent (DashboardContent) already has `t` from `useTranslation()`.
+
+**File 4: src/components/dashboard/DashboardSkeleton.tsx**
+
+Follow the pattern from research (Pattern 4). Named export `DashboardSkeleton`.
+
+Implementation:
+- Import `Skeleton` from `@/components/ui/skeleton`
+- Import `Card`, `CardContent`, `CardHeader` from `@/components/ui/card`
+- Renders a `
` with:
+ 1. Summary cards skeleton: `
` with 3 skeleton cards matching StatCard layout (Skeleton h-4 w-24 for title, Skeleton h-8 w-32 for value, Skeleton h-3 w-20 for variance)
+ 2. Chart area skeleton: `
` with 2 skeleton cards (Skeleton h-5 w-40 for chart title, Skeleton h-[240px] w-full rounded-md for chart area)
+
+This mirrors the real dashboard grid exactly so there is no layout shift when data loads.
+
+All 4 files use named exports. Follow import order convention: React first, third-party, internal types, internal utilities, components.
+
+
+ npm run build
+
+
All 4 component files exist, export the correct named exports, follow project conventions, and build passes. PageShell accepts title/description/action/children. StatCard accepts title/value/valueClassName/variance. SummaryStrip renders 3 StatCards in responsive grid with semantic color classes. DashboardSkeleton mirrors the real layout structure.
+
+
+
+ Task 2: Integrate new components into DashboardPage
+ src/pages/DashboardPage.tsx
+
+Refactor `src/pages/DashboardPage.tsx` to use the new shared components. This is a MODIFY operation -- preserve all existing logic (derived totals, pie chart, progress groups) while replacing the presentation layer.
+
+**Changes to make:**
+
+1. **Remove the inline SummaryCard component** (lines 45-66). Delete the entire `SummaryCardProps` interface and `SummaryCard` function. These are replaced by `StatCard`/`SummaryStrip`.
+
+2. **Add new imports** at the appropriate positions in the import order:
+ ```typescript
+ import { PageShell } from "@/components/shared/PageShell"
+ import { SummaryStrip } from "@/components/dashboard/SummaryStrip"
+ import { DashboardSkeleton } from "@/components/dashboard/DashboardSkeleton"
+ ```
+
+3. **Replace loading states with DashboardSkeleton:**
+ - In `DashboardContent`: Replace `if (loading) return null` (line 76) with `if (loading) return `
+ - In `DashboardPage`: Replace `if (loading) return null` (line 291) with:
+ ```tsx
+ if (loading) return (
+
+
+
+ )
+ ```
+
+4. **Replace hardcoded balance color** (lines 95-98):
+ - BEFORE: `const balanceColor = availableBalance >= 0 ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400"`
+ - AFTER: `const balanceColor = availableBalance >= 0 ? "text-on-budget" : "text-over-budget"`
+
+5. **Replace hardcoded progress bar colors** (lines 219-221):
+ - BEFORE: `const barColor = group.overBudget ? "bg-red-500 dark:bg-red-400" : "bg-green-500 dark:bg-green-400"`
+ - AFTER: `const barColor = group.overBudget ? "bg-over-budget" : "bg-on-budget"`
+
+6. **Replace hardcoded progress text color** (lines 235-239):
+ - BEFORE: `group.overBudget ? "text-red-600 dark:text-red-400" : "text-muted-foreground"`
+ - AFTER: `group.overBudget ? "text-over-budget" : "text-muted-foreground"`
+
+7. **Replace inline summary cards with SummaryStrip** in DashboardContent's return JSX. Replace the `` block (lines 135-149) with:
+ ```tsx
+
i.category?.type === "income").reduce((sum, i) => sum + i.budgeted_amount, 0),
+ currency
+ ),
+ }}
+ expenses={{
+ value: formatCurrency(totalExpenses, currency),
+ budgeted: formatCurrency(
+ items.filter((i) => i.category?.type !== "income").reduce((sum, i) => sum + i.budgeted_amount, 0),
+ currency
+ ),
+ }}
+ balance={{
+ value: formatCurrency(availableBalance, currency),
+ isPositive: availableBalance >= 0,
+ }}
+ t={t}
+ />
+ ```
+
+ To avoid recomputing budgeted totals inline, derive them alongside the existing totalIncome/totalExpenses calculations:
+ ```typescript
+ const budgetedIncome = items
+ .filter((i) => i.category?.type === "income")
+ .reduce((sum, i) => sum + i.budgeted_amount, 0)
+
+ const budgetedExpenses = items
+ .filter((i) => i.category?.type !== "income")
+ .reduce((sum, i) => sum + i.budgeted_amount, 0)
+ ```
+
+8. **Replace the page header with PageShell** in the `DashboardPage` component's return. Replace:
+ ```tsx
+
+
+
{t("dashboard.title")}
+
+ {/* content */}
+
+ ```
+ With:
+ ```tsx
+
+ {/* content */}
+
+ ```
+
+**What to preserve:**
+- All imports for Recharts (PieChart, Pie, Cell, ResponsiveContainer, Tooltip)
+- The `EXPENSE_TYPES` constant
+- The `currentMonthStart` helper
+- The `DashboardContent` component structure (budgetId prop, hooks, derived totals, pie chart, progress groups)
+- The `QuickAddPicker` usage
+- The entire pie chart + legend section
+- The entire category progress section (but with updated color classes)
+- The no-budget empty state with Link to /budgets
+
+**What to remove:**
+- The `SummaryCardProps` interface and `SummaryCard` function component
+- The hardcoded `text-green-600`, `text-red-600`, `bg-red-500`, `bg-green-500` color classes
+- The `if (loading) return null` patterns (both in DashboardContent and DashboardPage)
+- The inline `` header
+
+
+ npm run build && npm run lint
+
+
DashboardPage imports and uses PageShell, SummaryStrip, and DashboardSkeleton. No more inline SummaryCard component. Loading states show skeleton instead of null. All hardcoded green/red color classes replaced with semantic token classes (text-on-budget, text-over-budget, bg-on-budget, bg-over-budget). Build and lint pass.
+
+
+
+
+
+1. `npm run build && npm run lint` passes
+2. `src/components/shared/PageShell.tsx` exports `PageShell`
+3. `src/components/dashboard/StatCard.tsx` exports `StatCard`
+4. `src/components/dashboard/SummaryStrip.tsx` exports `SummaryStrip` and imports `StatCard`
+5. `src/components/dashboard/DashboardSkeleton.tsx` exports `DashboardSkeleton`
+6. `src/pages/DashboardPage.tsx` imports PageShell, SummaryStrip, DashboardSkeleton
+7. No occurrences of `text-green-600`, `text-red-600`, `bg-red-500`, `bg-green-500` remain in DashboardPage.tsx
+8. No occurrences of `SummaryCard` remain in DashboardPage.tsx
+9. No `return null` for loading states in DashboardPage.tsx
+
+
+
+- All 4 new component files exist and are well-typed
+- DashboardPage uses PageShell for header, SummaryStrip for KPI cards, DashboardSkeleton for loading
+- Zero hardcoded green/red color values in DashboardPage
+- Build and lint pass cleanly
+- Summary cards display in responsive grid (1/2/3 columns by breakpoint)
+
+
+