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