feat: enhance responsive design and layout consistency across various components, including dashboard, statistics, and rules pages

This commit is contained in:
Julien Froidefond
2025-12-01 08:34:28 +01:00
parent 86236aeb04
commit b3b25412ad
19 changed files with 731 additions and 349 deletions

View File

@@ -5,6 +5,7 @@ import { ChevronDown, ChevronRight, Plus, Tag } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { CategoryCombobox } from "@/components/ui/category-combobox";
import { useIsMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils";
import type { Transaction, Category } from "@/lib/types";
@@ -38,6 +39,7 @@ export function RuleGroupCard({
formatDate,
}: RuleGroupCardProps) {
const [selectedCategoryId, setSelectedCategoryId] = useState<string | null>(null);
const isMobile = useIsMobile();
const avgAmount =
group.transactions.reduce((sum, t) => sum + t.amount, 0) /
@@ -53,59 +55,87 @@ export function RuleGroupCard({
<div className="border border-border rounded-lg bg-card overflow-hidden">
{/* Header */}
<div
className="flex items-center gap-3 p-4 cursor-pointer hover:bg-accent/5 transition-colors"
className="flex flex-col md:flex-row md:items-center gap-2 md:gap-3 p-3 md:p-4 cursor-pointer hover:bg-accent/5 transition-colors"
onClick={onToggleExpand}
>
<Button variant="ghost" size="icon" className="h-6 w-6 shrink-0">
{isExpanded ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</Button>
<div className="flex items-center gap-2 md:gap-3 flex-1 min-w-0">
<Button variant="ghost" size="icon" className="h-5 w-5 md:h-6 md:w-6 shrink-0">
{isExpanded ? (
<ChevronDown className="h-3 w-3 md:h-4 md:w-4" />
) : (
<ChevronRight className="h-3 w-3 md:h-4 md:w-4" />
)}
</Button>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-foreground truncate">
{group.displayName}
</span>
<Badge variant="secondary" className="shrink-0">
{group.transactions.length} transaction
{group.transactions.length > 1 ? "s" : ""}
</Badge>
</div>
<div className="flex items-center gap-2 mt-1 text-sm text-muted-foreground">
<Tag className="h-3 w-3" />
<span className="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">
{group.suggestedKeyword}
</span>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5 md:gap-2 flex-wrap">
<span className="font-medium text-xs md:text-base text-foreground truncate">
{group.displayName}
</span>
<Badge variant="secondary" className="text-[10px] md:text-xs shrink-0">
{group.transactions.length} 💳
</Badge>
</div>
<div className="flex items-center gap-1.5 md:gap-2 mt-1 text-[10px] md:text-sm text-muted-foreground">
<Tag className="h-2.5 w-2.5 md:h-3 md:w-3" />
<span className="font-mono text-[10px] md:text-xs bg-muted px-1 md:px-1.5 py-0.5 rounded">
{group.suggestedKeyword}
</span>
</div>
</div>
</div>
<div className="flex items-center gap-4">
<div className="text-right">
<div
className={cn(
"font-semibold tabular-nums",
isDebit ? "text-destructive" : "text-success"
)}
>
{formatCurrency(group.totalAmount)}
{!isMobile && (
<div className="flex items-center gap-4">
<div className="text-right">
<div
className={cn(
"font-semibold tabular-nums text-sm",
isDebit ? "text-destructive" : "text-success"
)}
>
{formatCurrency(group.totalAmount)}
</div>
<div className="text-xs text-muted-foreground">
Moy: {formatCurrency(avgAmount)}
</div>
</div>
<div className="text-xs text-muted-foreground">
Moy: {formatCurrency(avgAmount)}
<div className="flex items-center gap-2 shrink-0">
<div onClick={(e) => e.stopPropagation()}>
<CategoryCombobox
categories={categories}
value={selectedCategoryId}
onChange={handleCategorySelect}
placeholder="Catégoriser..."
width="w-[300px]"
buttonWidth="w-auto"
/>
</div>
<Button
size="sm"
onClick={(e) => {
e.stopPropagation();
onCreateRule();
}}
>
<Plus className="h-4 w-4 mr-1" />
Créer règle
</Button>
</div>
</div>
)}
<div className="flex items-center gap-2 shrink-0">
<div onClick={(e) => e.stopPropagation()}>
{isMobile && (
<div className="flex items-center gap-2 shrink-0 ml-7">
<div onClick={(e) => e.stopPropagation()} className="flex-1">
<CategoryCombobox
categories={categories}
value={selectedCategoryId}
onChange={handleCategorySelect}
placeholder="Catégoriser..."
width="w-[300px]"
buttonWidth="w-auto"
width="w-full"
buttonWidth="w-full"
/>
</div>
<Button
@@ -114,64 +144,101 @@ export function RuleGroupCard({
e.stopPropagation();
onCreateRule();
}}
className="shrink-0"
>
<Plus className="h-4 w-4 mr-1" />
Créer règle
<Plus className="h-3 w-3 md:h-4 md:w-4" />
</Button>
</div>
</div>
)}
</div>
{/* Expanded transactions list */}
{isExpanded && (
<div className="border-t border-border bg-muted/30">
<div className="max-h-64 overflow-y-auto">
<table className="w-full text-sm">
<thead className="bg-muted/50 sticky top-0">
<tr>
<th className="px-4 py-2 text-left font-medium text-muted-foreground">
Date
</th>
<th className="px-4 py-2 text-left font-medium text-muted-foreground">
Description
</th>
<th className="px-4 py-2 text-right font-medium text-muted-foreground">
Montant
</th>
</tr>
</thead>
<tbody>
{group.transactions.map((transaction) => (
<tr
key={transaction.id}
className="border-t border-border/50 hover:bg-muted/50"
>
<td className="px-4 py-2 text-muted-foreground whitespace-nowrap">
{formatDate(transaction.date)}
</td>
<td className="px-4 py-2 truncate max-w-md">
{transaction.description}
{isMobile ? (
<div className="max-h-64 overflow-y-auto divide-y divide-border">
{group.transactions.map((transaction) => (
<div
key={transaction.id}
className="p-3 hover:bg-muted/50"
>
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<p className="text-xs md:text-sm font-medium truncate">
{transaction.description}
</p>
{transaction.memo && (
<span className="text-muted-foreground ml-2">
({transaction.memo})
</span>
<p className="text-[10px] md:text-xs text-muted-foreground truncate mt-0.5">
{transaction.memo}
</p>
)}
</td>
<td
<p className="text-[10px] md:text-xs text-muted-foreground mt-1">
{formatDate(transaction.date)}
</p>
</div>
<div
className={cn(
"px-4 py-2 text-right tabular-nums whitespace-nowrap",
"text-xs md:text-sm font-semibold tabular-nums shrink-0",
transaction.amount < 0
? "text-destructive"
: "text-success"
)}
>
{formatCurrency(transaction.amount)}
</td>
</div>
</div>
</div>
))}
</div>
) : (
<div className="max-h-64 overflow-y-auto">
<table className="w-full text-sm">
<thead className="bg-muted/50 sticky top-0">
<tr>
<th className="px-4 py-2 text-left font-medium text-muted-foreground">
Date
</th>
<th className="px-4 py-2 text-left font-medium text-muted-foreground">
Description
</th>
<th className="px-4 py-2 text-right font-medium text-muted-foreground">
Montant
</th>
</tr>
))}
</tbody>
</table>
</div>
</thead>
<tbody>
{group.transactions.map((transaction) => (
<tr
key={transaction.id}
className="border-t border-border/50 hover:bg-muted/50"
>
<td className="px-4 py-2 text-muted-foreground whitespace-nowrap">
{formatDate(transaction.date)}
</td>
<td className="px-4 py-2 truncate max-w-md">
{transaction.description}
{transaction.memo && (
<span className="text-muted-foreground ml-2">
({transaction.memo})
</span>
)}
</td>
<td
className={cn(
"px-4 py-2 text-right tabular-nums whitespace-nowrap",
transaction.amount < 0
? "text-destructive"
: "text-success"
)}
>
{formatCurrency(transaction.amount)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)}
</div>