feat: refactor dashboard and account pages to utilize new layout components, enhancing structure and loading states

This commit is contained in:
Julien Froidefond
2025-11-27 12:44:44 +01:00
parent e469656e0d
commit 88937579e2
40 changed files with 2781 additions and 2226 deletions

View File

@@ -0,0 +1,72 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from "recharts";
interface BalanceChartData {
date: string;
solde: number;
}
interface BalanceLineChartProps {
data: BalanceChartData[];
formatCurrency: (amount: number) => string;
}
export function BalanceLineChart({
data,
formatCurrency,
}: BalanceLineChartProps) {
return (
<Card>
<CardHeader>
<CardTitle>Évolution du solde</CardTitle>
</CardHeader>
<CardContent>
{data.length > 0 ? (
<div className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={data}>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis
dataKey="date"
className="text-xs"
interval="preserveStartEnd"
/>
<YAxis className="text-xs" tickFormatter={(v) => `${v}`} />
<Tooltip
formatter={(value: number) => formatCurrency(value)}
contentStyle={{
backgroundColor: "hsl(var(--card))",
border: "1px solid hsl(var(--border))",
borderRadius: "8px",
}}
/>
<Line
type="monotone"
dataKey="solde"
stroke="#6366f1"
strokeWidth={2}
dot={false}
/>
</LineChart>
</ResponsiveContainer>
</div>
) : (
<div className="h-[300px] flex items-center justify-center text-muted-foreground">
Pas de données pour cette période
</div>
)}
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,76 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
PieChart,
Pie,
Cell,
Tooltip,
ResponsiveContainer,
Legend,
} from "recharts";
interface CategoryChartData {
name: string;
value: number;
color: string;
}
interface CategoryPieChartProps {
data: CategoryChartData[];
formatCurrency: (amount: number) => string;
}
export function CategoryPieChart({
data,
formatCurrency,
}: CategoryPieChartProps) {
return (
<Card>
<CardHeader>
<CardTitle>Répartition par catégorie</CardTitle>
</CardHeader>
<CardContent>
{data.length > 0 ? (
<div className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={data}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={100}
paddingAngle={2}
dataKey="value"
>
{data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip
formatter={(value: number) => formatCurrency(value)}
contentStyle={{
backgroundColor: "hsl(var(--card))",
border: "1px solid hsl(var(--border))",
borderRadius: "8px",
}}
/>
<Legend
formatter={(value) => (
<span className="text-sm text-foreground">{value}</span>
)}
/>
</PieChart>
</ResponsiveContainer>
</div>
) : (
<div className="h-[300px] flex items-center justify-center text-muted-foreground">
Pas de données pour cette période
</div>
)}
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,6 @@
export { StatsSummaryCards } from "./stats-summary-cards";
export { MonthlyChart } from "./monthly-chart";
export { CategoryPieChart } from "./category-pie-chart";
export { BalanceLineChart } from "./balance-line-chart";
export { TopExpensesList } from "./top-expenses-list";

View File

@@ -0,0 +1,64 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Legend,
} from "recharts";
interface MonthlyChartData {
month: string;
revenus: number;
depenses: number;
solde: number;
}
interface MonthlyChartProps {
data: MonthlyChartData[];
formatCurrency: (amount: number) => string;
}
export function MonthlyChart({ data, formatCurrency }: MonthlyChartProps) {
return (
<Card>
<CardHeader>
<CardTitle>Revenus vs Dépenses par mois</CardTitle>
</CardHeader>
<CardContent>
{data.length > 0 ? (
<div className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data}>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis dataKey="month" className="text-xs" />
<YAxis className="text-xs" tickFormatter={(v) => `${v}`} />
<Tooltip
formatter={(value: number) => formatCurrency(value)}
contentStyle={{
backgroundColor: "hsl(var(--card))",
border: "1px solid hsl(var(--border))",
borderRadius: "8px",
}}
/>
<Legend />
<Bar dataKey="revenus" fill="#22c55e" radius={[4, 4, 0, 0]} />
<Bar dataKey="depenses" fill="#ef4444" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
) : (
<div className="h-[300px] flex items-center justify-center text-muted-foreground">
Pas de données pour cette période
</div>
)}
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,86 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { TrendingUp, TrendingDown, ArrowRight } from "lucide-react";
import { cn } from "@/lib/utils";
interface StatsSummaryCardsProps {
totalIncome: number;
totalExpenses: number;
avgMonthlyExpenses: number;
formatCurrency: (amount: number) => string;
}
export function StatsSummaryCards({
totalIncome,
totalExpenses,
avgMonthlyExpenses,
formatCurrency,
}: StatsSummaryCardsProps) {
const savings = totalIncome - totalExpenses;
return (
<div className="grid gap-4 md:grid-cols-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<TrendingUp className="w-4 h-4 text-emerald-600" />
Total Revenus
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-emerald-600">
{formatCurrency(totalIncome)}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<TrendingDown className="w-4 h-4 text-red-600" />
Total Dépenses
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-red-600">
{formatCurrency(totalExpenses)}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<ArrowRight className="w-4 h-4" />
Moyenne mensuelle
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{formatCurrency(avgMonthlyExpenses)}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Économies
</CardTitle>
</CardHeader>
<CardContent>
<div
className={cn(
"text-2xl font-bold",
savings >= 0 ? "text-emerald-600" : "text-red-600"
)}
>
{formatCurrency(savings)}
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,77 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { CategoryIcon } from "@/components/ui/category-icon";
import type { Transaction, Category } from "@/lib/types";
interface TopExpensesListProps {
expenses: Transaction[];
categories: Category[];
formatCurrency: (amount: number) => string;
}
export function TopExpensesList({
expenses,
categories,
formatCurrency,
}: TopExpensesListProps) {
return (
<Card>
<CardHeader>
<CardTitle>Top 5 dépenses</CardTitle>
</CardHeader>
<CardContent>
{expenses.length > 0 ? (
<div className="space-y-4">
{expenses.map((expense, index) => {
const category = categories.find(
(c) => c.id === expense.categoryId
);
return (
<div key={expense.id} className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center text-sm font-semibold">
{index + 1}
</div>
<div className="flex-1 min-w-0">
<p className="font-medium text-sm truncate">
{expense.description}
</p>
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">
{new Date(expense.date).toLocaleDateString("fr-FR")}
</span>
{category && (
<span
className="text-xs px-1.5 py-0.5 rounded inline-flex items-center gap-1"
style={{
backgroundColor: `${category.color}20`,
color: category.color,
}}
>
<CategoryIcon
icon={category.icon}
color={category.color}
size={10}
/>
{category.name}
</span>
)}
</div>
</div>
<div className="text-red-600 font-semibold tabular-nums">
{formatCurrency(expense.amount)}
</div>
</div>
);
})}
</div>
) : (
<div className="h-[200px] flex items-center justify-center text-muted-foreground">
Pas de dépenses pour cette période
</div>
)}
</CardContent>
</Card>
);
}