From d4db94d15613f1ce21fb6c77e89529ac69117e41 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Sun, 7 Dec 2025 17:23:53 +0100 Subject: [PATCH] feat: enhance UI with new background gradients and responsive design adjustments across various components --- app/accounts/page.tsx | 195 ++++++++++++------- app/globals.css | 76 ++++++++ app/login/page.tsx | 4 +- app/page.tsx | 30 ++- components/accounts/account-bulk-actions.tsx | 35 +++- components/accounts/account-card.tsx | 123 +++++++----- components/dashboard/overview-cards.tsx | 79 +++++--- components/layout/page-layout.tsx | 4 +- components/ui/account-filter-combobox.tsx | 32 +-- components/ui/card.tsx | 12 +- 10 files changed, 399 insertions(+), 191 deletions(-) diff --git a/app/accounts/page.tsx b/app/accounts/page.tsx index 34d9d5b..e06ac77 100644 --- a/app/accounts/page.tsx +++ b/app/accounts/page.tsx @@ -30,10 +30,26 @@ import { } from "@/lib/store-db"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; -import { Building2, Folder, Plus, List, LayoutGrid } from "lucide-react"; +import { + Building2, + Folder, + Plus, + List, + LayoutGrid, + MoreVertical, + Pencil, + Trash2, +} from "lucide-react"; import type { Account, Folder as FolderType } from "@/lib/types"; import { cn } from "@/lib/utils"; import { getAccountBalance } from "@/lib/account-utils"; +import { useIsMobile } from "@/hooks/use-mobile"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; // Composant wrapper pour les zones de drop des dossiers function FolderDropZone({ @@ -61,6 +77,7 @@ function FolderDropZone({ export default function AccountsPage() { const queryClient = useQueryClient(); + const isMobile = useIsMobile(); const { data: metadata, isLoading: isLoadingMetadata } = useBankingMetadata(); const { data: accountsWithStats, isLoading: isLoadingAccounts } = useAccountsWithStats(); @@ -376,12 +393,12 @@ export default function AccountsPage() { - } rightContent={ -
-

Solde total

+
+ {!isMobile && ( +

Solde total

+ )}

= 0 ? "text-emerald-600" : "text-red-600", )} > + {isMobile && Total:} {formatCurrency(totalBalance)}

@@ -440,32 +466,36 @@ export default function AccountsPage() { {accountsByFolder["no-folder"] && accountsByFolder["no-folder"].length > 0 && ( -
- -

Sans dossier

- - ({accountsByFolder["no-folder"].length}) - - sum + getAccountBalance(a), - 0, - ) >= 0 - ? "text-emerald-600" - : "text-red-600", - )} - > - {formatCurrency( - accountsByFolder["no-folder"].reduce( - (sum, a) => sum + getAccountBalance(a), - 0, - ), - )} - +
+ +
+
+

Sans dossier

+ + ({accountsByFolder["no-folder"].length}) + + sum + getAccountBalance(a), + 0, + ) >= 0 + ? "text-emerald-600" + : "text-red-600", + )} + > + {formatCurrency( + accountsByFolder["no-folder"].reduce( + (sum, a) => sum + getAccountBalance(a), + 0, + ), + )} + +
+
-
+
{accountsByFolder["no-folder"].map((account) => { const folder = metadata.folders.find( (f: FolderType) => f.id === account.folderId, @@ -501,9 +531,9 @@ export default function AccountsPage() { return ( -
+
-

{folder.name}

- - ({folderAccounts.length}) - - {folderAccounts.length > 0 && ( - = 0 - ? "text-emerald-600" - : "text-red-600", +
+
+

+ {folder.name} +

+ + ({folderAccounts.length}) + + {folderAccounts.length > 0 && ( + = 0 + ? "text-emerald-600" + : "text-red-600", + )} + > + {formatCurrency(folderBalance)} + )} - > - {formatCurrency(folderBalance)} - +
+
+ {isMobile ? ( + + + + + + handleEditFolder(folder)}> + + Modifier + + handleDeleteFolder(folder.id)} + variant="destructive" + > + + Supprimer + + + + ) : ( + <> + + + )} - -
{folderAccounts.length > 0 ? ( -
+
{folderAccounts.map((account) => { const accountFolder = metadata.folders.find( (f: FolderType) => f.id === account.folderId, diff --git a/app/globals.css b/app/globals.css index 840b33f..fed63f2 100644 --- a/app/globals.css +++ b/app/globals.css @@ -122,4 +122,80 @@ body { @apply bg-background text-foreground; } + + /* Background avec beau dégradé */ + .page-background { + position: relative; + background: var(--background); + } + + .page-background::before { + content: ""; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 0; + background: + radial-gradient(ellipse 80% 50% at 50% -20%, + color-mix(in srgb, var(--chart-1) 25%, transparent) 0%, + transparent 50% + ), + radial-gradient(ellipse 60% 80% at -10% 50%, + color-mix(in srgb, var(--chart-2) 20%, transparent) 0%, + transparent 50% + ), + radial-gradient(ellipse 60% 80% at 110% 50%, + color-mix(in srgb, var(--chart-3) 20%, transparent) 0%, + transparent 50% + ), + radial-gradient(ellipse 80% 50% at 50% 120%, + color-mix(in srgb, var(--chart-4) 25%, transparent) 0%, + transparent 50% + ), + linear-gradient( + 135deg, + color-mix(in srgb, var(--chart-1) 8%, transparent) 0%, + transparent 25%, + transparent 75%, + color-mix(in srgb, var(--chart-2) 8%, transparent) 100% + ); + background-size: 100% 100%; + pointer-events: none; + opacity: 1; + } + + .dark .page-background::before { + background: + radial-gradient(ellipse 80% 50% at 50% -20%, + color-mix(in srgb, var(--chart-1) 35%, transparent) 0%, + transparent 50% + ), + radial-gradient(ellipse 60% 80% at -10% 50%, + color-mix(in srgb, var(--chart-2) 30%, transparent) 0%, + transparent 50% + ), + radial-gradient(ellipse 60% 80% at 110% 50%, + color-mix(in srgb, var(--chart-3) 30%, transparent) 0%, + transparent 50% + ), + radial-gradient(ellipse 80% 50% at 50% 120%, + color-mix(in srgb, var(--chart-4) 35%, transparent) 0%, + transparent 50% + ), + linear-gradient( + 135deg, + color-mix(in srgb, var(--chart-1) 12%, transparent) 0%, + transparent 25%, + transparent 75%, + color-mix(in srgb, var(--chart-2) 12%, transparent) 100% + ); + opacity: 1; + } + + .page-content { + position: relative; + z-index: 1; + } } diff --git a/app/login/page.tsx b/app/login/page.tsx index 4be878d..28201fb 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -47,8 +47,8 @@ export default function LoginPage() { }; return ( -
- +
+
diff --git a/app/page.tsx b/app/page.tsx index 0acc37b..b92b2d9 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -27,11 +27,11 @@ export default function DashboardPage() { } const filteredAccounts = data.accounts.filter((a) => - selectedAccounts.includes(a.id), + selectedAccounts.includes(a.id) ); const filteredAccountIds = new Set(filteredAccounts.map((a) => a.id)); const filteredTransactions = data.transactions.filter((t) => - filteredAccountIds.has(t.accountId), + filteredAccountIds.has(t.accountId) ); return { @@ -60,26 +60,24 @@ export default function DashboardPage() { } /> - - -
- -
+ + + -
+
-
+
diff --git a/components/accounts/account-bulk-actions.tsx b/components/accounts/account-bulk-actions.tsx index 708ee67..f2471a8 100644 --- a/components/accounts/account-bulk-actions.tsx +++ b/components/accounts/account-bulk-actions.tsx @@ -3,6 +3,8 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Trash2 } from "lucide-react"; +import { useIsMobile } from "@/hooks/use-mobile"; +import { cn } from "@/lib/utils"; interface AccountBulkActionsProps { selectedCount: number; @@ -13,18 +15,39 @@ export function AccountBulkActions({ selectedCount, onDelete, }: AccountBulkActionsProps) { + const isMobile = useIsMobile(); + if (selectedCount === 0) return null; return ( - - -
- + + +
+ {selectedCount} compte{selectedCount > 1 ? "s" : ""} sélectionné {selectedCount > 1 ? "s" : ""} -
diff --git a/components/accounts/account-card.tsx b/components/accounts/account-card.tsx index e89e652..ce356c8 100644 --- a/components/accounts/account-card.tsx +++ b/components/accounts/account-card.tsx @@ -23,6 +23,7 @@ import type { Account, Folder } from "@/lib/types"; import { accountTypeIcons, accountTypeLabels } from "./constants"; import { Checkbox } from "@/components/ui/checkbox"; import { getAccountBalance } from "@/lib/account-utils"; +import { useIsMobile } from "@/hooks/use-mobile"; interface AccountCardProps { account: Account; @@ -49,6 +50,7 @@ export function AccountCard({ draggableId, compact = false, }: AccountCardProps) { + const isMobile = useIsMobile(); const Icon = accountTypeIcons[account.type]; const realBalance = getAccountBalance(account); @@ -82,14 +84,14 @@ export function AccountCard({ isDragging && "bg-muted/80", )} > - -
-
- {draggableId && ( + +
+
+ {draggableId && !isMobile && ( @@ -138,7 +155,7 @@ export function AccountCard({ onDelete(account.id)} - className="text-red-600" + variant="destructive" > Supprimer @@ -147,13 +164,18 @@ export function AccountCard({
- -
+ +
= 0 ? "text-emerald-600" : "text-red-600", )} > @@ -162,45 +184,58 @@ export function AccountCard({ {compact && ( - {transactionCount} transactions + {transactionCount} txns )}
{!compact && ( <> -
+
{transactionCount} transactions - {folder && {folder.name}} -
- {account.initialBalance !== undefined && - account.initialBalance !== null && ( -

- Solde initial: {formatCurrency(account.initialBalance)} -

+ {folder && !isMobile && ( + {folder.name} )} - {account.lastImport && ( -

- Dernier import:{" "} - {new Date(account.lastImport).toLocaleDateString("fr-FR")} -

- )} - {account.externalUrl && ( - - - Accéder au portail banque - +
+ {!isMobile && ( + <> + {account.initialBalance !== undefined && + account.initialBalance !== null && ( +

+ Solde initial: {formatCurrency(account.initialBalance)} +

+ )} + {account.lastImport && ( +

+ Dernier import:{" "} + {new Date(account.lastImport).toLocaleDateString("fr-FR")} +

+ )} + {account.externalUrl && ( + + + Accéder au portail banque + + )} + )} )} diff --git a/components/dashboard/overview-cards.tsx b/components/dashboard/overview-cards.tsx index 61040d8..a4789a5 100644 --- a/components/dashboard/overview-cards.tsx +++ b/components/dashboard/overview-cards.tsx @@ -13,7 +13,7 @@ interface OverviewCardsProps { export function OverviewCards({ data }: OverviewCardsProps) { const totalBalance = data.accounts.reduce( (sum, acc) => sum + getAccountBalance(acc), - 0, + 0 ); const thisMonth = new Date(); @@ -21,7 +21,7 @@ export function OverviewCards({ data }: OverviewCardsProps) { const thisMonthStr = thisMonth.toISOString().slice(0, 7); const monthTransactions = data.transactions.filter((t) => - t.date.startsWith(thisMonthStr), + t.date.startsWith(thisMonthStr) ); const income = monthTransactions @@ -45,41 +45,52 @@ export function OverviewCards({ data }: OverviewCardsProps) { }; return ( -
- - - +
+ = 0 + ? "from-emerald-50/50 to-card dark:from-emerald-950/20 dark:to-card" + : "from-red-50/50 to-card dark:from-red-950/20 dark:to-card" + )} + > + + Solde Total - +
+ +
- +
= 0 ? "text-emerald-600" : "text-red-600", + "text-2xl sm:text-2xl md:text-3xl font-bold tracking-tight", + totalBalance >= 0 ? "text-emerald-600" : "text-red-600" )} > {formatCurrency(totalBalance)}
-

+

{data.accounts.length} compte{data.accounts.length > 1 ? "s" : ""}

- - - + + + Revenus du mois - +
+ +
- -
+ +
{formatCurrency(income)}
-

+

{monthTransactions.filter((t) => t.amount > 0).length} opération {monthTransactions.filter((t) => t.amount > 0).length > 1 ? "s" @@ -88,18 +99,20 @@ export function OverviewCards({ data }: OverviewCardsProps) { - - - + + + Dépenses du mois - +

+ +
- -
+ +
{formatCurrency(expenses)}
-

+

{monthTransactions.filter((t) => t.amount < 0).length} opération {monthTransactions.filter((t) => t.amount < 0).length > 1 ? "s" @@ -108,18 +121,20 @@ export function OverviewCards({ data }: OverviewCardsProps) { - - - + + + Pointage - +

+ +
- -
+ +
{reconciledPercent}%
-

+

{reconciled} / {total} opérations pointées

diff --git a/components/layout/page-layout.tsx b/components/layout/page-layout.tsx index 7c3ac7f..496a630 100644 --- a/components/layout/page-layout.tsx +++ b/components/layout/page-layout.tsx @@ -15,9 +15,9 @@ export function PageLayout({ children }: PageLayoutProps) { -
+
-
+
{children}
diff --git a/components/ui/account-filter-combobox.tsx b/components/ui/account-filter-combobox.tsx index 71b11f2..f1a6d14 100644 --- a/components/ui/account-filter-combobox.tsx +++ b/components/ui/account-filter-combobox.tsx @@ -64,7 +64,7 @@ export function AccountFilterCombobox({ // Get root folders (folders without parent) - same as folders/page.tsx const rootFolders = useMemo( () => folders.filter((f) => f.parentId === null), - [folders], + [folders] ); // Get child folders for a given parent - same as FolderTreeItem @@ -78,7 +78,7 @@ export function AccountFilterCombobox({ // Get accounts without folder const orphanAccounts = useMemo( () => accounts.filter((a) => !a.folderId), - [accounts], + [accounts] ); const selectedAccounts = accounts.filter((a) => value.includes(a.id)); @@ -89,7 +89,7 @@ export function AccountFilterCombobox({ const directAccounts = getFolderAccounts(folderId); const childFoldersList = getChildFolders(folderId); const childAccounts = childFoldersList.flatMap((cf) => - getAllAccountsInFolder(cf.id), + getAllAccountsInFolder(cf.id) ); return [...directAccounts, ...childAccounts]; }; @@ -126,7 +126,7 @@ export function AccountFilterCombobox({ if (allSelected) { const newSelection = value.filter( - (v) => !allFolderAccountIds.includes(v), + (v) => !allFolderAccountIds.includes(v) ); onChange(newSelection.length > 0 ? newSelection : ["all"]); } else { @@ -153,7 +153,7 @@ export function AccountFilterCombobox({ const folderAccounts = getAllAccountsInFolder(folderId); if (folderAccounts.length === 0) return false; const selectedCount = folderAccounts.filter((a) => - value.includes(a.id), + value.includes(a.id) ).length; return selectedCount > 0 && selectedCount < folderAccounts.length; }; @@ -185,7 +185,7 @@ export function AccountFilterCombobox({
@@ -213,7 +213,7 @@ export function AccountFilterCombobox({ @@ -222,7 +222,7 @@ export function AccountFilterCombobox({ {/* Child folders - recursive */} {childFoldersList.map((childFolder) => - renderFolder(childFolder, depth + 1, currentPath), + renderFolder(childFolder, depth + 1, currentPath) )}
); @@ -235,7 +235,10 @@ export function AccountFilterCombobox({ variant="outline" role="combobox" aria-expanded={open} - className={cn("justify-between min-w-0", className)} + className={cn( + "justify-between min-w-0 h-10 w-full text-sm font-normal", + className + )} >
{selectedAccounts.length === 1 ? ( @@ -285,7 +288,7 @@ export function AccountFilterCombobox({ e.preventDefault()} > @@ -301,10 +304,7 @@ export function AccountFilterCombobox({ ( {formatCurrency( - filteredTransactions.reduce( - (sum, t) => sum + t.amount, - 0, - ), + filteredTransactions.reduce((sum, t) => sum + t.amount, 0) )} ) @@ -312,7 +312,7 @@ export function AccountFilterCombobox({ @@ -348,7 +348,7 @@ export function AccountFilterCombobox({ "ml-auto h-4 w-4 shrink-0", value.includes(account.id) ? "opacity-100" - : "opacity-0", + : "opacity-0" )} /> diff --git a/components/ui/card.tsx b/components/ui/card.tsx index 4f88024..9d8e279 100644 --- a/components/ui/card.tsx +++ b/components/ui/card.tsx @@ -7,8 +7,8 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
@@ -20,8 +20,8 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
@@ -54,7 +54,7 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) { data-slot="card-action" className={cn( "col-start-2 row-span-2 row-start-1 self-start justify-self-end", - className, + className )} {...props} /> @@ -65,7 +65,7 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) { return (
);