From 8b3a19b5521917c88411dc99ce9bcb988b234965 Mon Sep 17 00:00:00 2001 From: Carlos Escalante Date: Wed, 29 Apr 2026 22:02:22 -0600 Subject: [PATCH] Add Skeleton primitive and budget detail loading state Replaces the blank flash on the budget detail tab with skeleton placeholders that mirror the final card layout, so the page no longer shifts when the API returns. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/components/budget/MonthlyDetail.tsx | 117 ++++++++++++++++-- frontend/src/components/ui/skeleton.tsx | 14 +++ frontend/src/pages/Budget.tsx | 14 +-- 3 files changed, 129 insertions(+), 16 deletions(-) create mode 100644 frontend/src/components/ui/skeleton.tsx diff --git a/frontend/src/components/budget/MonthlyDetail.tsx b/frontend/src/components/budget/MonthlyDetail.tsx index a5e2c4d..6fb9c52 100644 --- a/frontend/src/components/budget/MonthlyDetail.tsx +++ b/frontend/src/components/budget/MonthlyDetail.tsx @@ -1,12 +1,13 @@ import { useState } from 'react'; import { PieChart, Pie, Cell } from 'recharts'; -import { type MonthlyDetail as MonthlyDetailType } from '@/api'; +import { type MonthlyDetail as MonthlyDetailType } from '@/lib/api'; import { formatAmount } from '@/lib/format'; import { cn } from '@/lib/utils'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Separator } from '@/components/ui/separator'; import { Button } from '@/components/ui/button'; +import { Skeleton } from '@/components/ui/skeleton'; import { ChartContainer, ChartTooltip, @@ -51,22 +52,122 @@ const SOURCE_LABELS: Record = }; interface MonthlyDetailProps { - detail: MonthlyDetailType; + detail: MonthlyDetailType | null; loading?: boolean; onNavigateToTransactions?: () => void; } +function PieCardSkeleton({ titleIcon: TitleIcon, title }: { titleIcon: typeof TrendingUp; title: string }) { + return ( + + + + + {title} + + + +
+
+ +
+
+ {Array.from({ length: 4 }).map((_, i) => ( +
+ + +
+ ))} +
+ +
+ + +
+
+
+
+ ); +} + export default function MonthlyDetail({ detail, loading, onNavigateToTransactions }: MonthlyDetailProps) { const [paletteMode, setPaletteMode] = useState('chatgpt'); - if (loading) { + if (loading || !detail) { return ( -
- {[1, 2, 3].map((i) => ( - - +
+
+ Paleta: + + +
+
+ + +
+ + + + + Tarjeta de Crédito + + + +
+
+ +
+
+ {Array.from({ length: 6 }).map((_, i) => ( +
+ + +
+ ))} +
+
+ +
+ + +
+
+
+
+ + + + + Efectivo o Transferencias + + + + {Array.from({ length: 2 }).map((_, i) => ( +
+ + +
+ ))} +
- ))} + + +
+ + +
+
+ + +
+ +
+ + +
+
+
+
); } diff --git a/frontend/src/components/ui/skeleton.tsx b/frontend/src/components/ui/skeleton.tsx new file mode 100644 index 0000000..c974fa1 --- /dev/null +++ b/frontend/src/components/ui/skeleton.tsx @@ -0,0 +1,14 @@ +import { cn } from "@/lib/utils"; + +export function Skeleton({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ); +} diff --git a/frontend/src/pages/Budget.tsx b/frontend/src/pages/Budget.tsx index 4cd6ff7..d969979 100644 --- a/frontend/src/pages/Budget.tsx +++ b/frontend/src/pages/Budget.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useCallback } from 'react'; import { ChevronLeft, ChevronRight, Calculator } from 'lucide-react'; -import api, { type Transaction } from '@/api'; +import api, { type Transaction } from '@/lib/api'; import { useBudget } from '@/hooks/useBudget'; import { Button } from '@/components/ui/button'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; @@ -154,13 +154,11 @@ export default function Budget() {
- {monthDetail && ( - - )} +