diff --git a/app/globals.css b/app/globals.css
index 5557e7d..21c90a2 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -375,6 +375,54 @@
overflow: hidden;
}
+ /* Fonds personnalisables */
+ .page-background.bg-default {
+ background: linear-gradient(
+ 135deg,
+ oklch(0.98 0.01 280) 0%,
+ oklch(0.97 0.012 270) 50%,
+ oklch(0.98 0.01 290) 100%
+ );
+ }
+
+ .page-background.bg-gradient-blue {
+ background: linear-gradient(135deg, #e0f2fe 0%, #bae6fd 50%, #7dd3fc 100%);
+ }
+
+ .page-background.bg-gradient-purple {
+ background: linear-gradient(135deg, #f3e8ff 0%, #e9d5ff 50%, #d8b4fe 100%);
+ }
+
+ .page-background.bg-gradient-green {
+ background: linear-gradient(135deg, #dcfce7 0%, #bbf7d0 50%, #86efac 100%);
+ }
+
+ .page-background.bg-gradient-orange {
+ background: linear-gradient(135deg, #fff7ed 0%, #ffedd5 50%, #fed7aa 100%);
+ }
+
+ .page-background.bg-solid-light {
+ background: #ffffff;
+ }
+
+ .page-background.bg-solid-dark {
+ background: #0f172a !important;
+ }
+
+ .page-background.bg-custom-image {
+ background-image: var(--custom-background-image);
+ background-size: cover;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-attachment: fixed;
+ }
+
+ .page-background.bg-custom-image::before,
+ .page-background.bg-solid-light::before,
+ .page-background.bg-solid-dark::before {
+ display: none;
+ }
+
.page-background::before {
content: "";
position: fixed;
@@ -415,7 +463,7 @@
opacity: 1;
}
- .dark .page-background {
+ .dark .page-background.bg-default {
background: linear-gradient(
135deg,
oklch(0.1 0.015 280) 0%,
@@ -424,6 +472,30 @@
);
}
+ .dark .page-background.bg-gradient-blue {
+ background: linear-gradient(135deg, #0c4a6e 0%, #075985 50%, #0369a1 100%);
+ }
+
+ .dark .page-background.bg-gradient-purple {
+ background: linear-gradient(135deg, #581c87 0%, #6b21a8 50%, #7c3aed 100%);
+ }
+
+ .dark .page-background.bg-gradient-green {
+ background: linear-gradient(135deg, #14532d 0%, #166534 50%, #16a34a 100%);
+ }
+
+ .dark .page-background.bg-gradient-orange {
+ background: linear-gradient(135deg, #7c2d12 0%, #9a3412 50%, #c2410c 100%);
+ }
+
+ .dark .page-background.bg-solid-light {
+ background: #f8fafc;
+ }
+
+ .dark .page-background.bg-solid-dark {
+ background: #0f172a !important;
+ }
+
.dark .page-background::before {
background:
radial-gradient(
@@ -481,8 +553,11 @@
content: "";
position: absolute;
inset: 0;
- background-image:
- radial-gradient(circle at 1px 1px, color-mix(in srgb, var(--foreground) 2%, transparent) 1px, transparent 0);
+ background-image: radial-gradient(
+ circle at 1px 1px,
+ color-mix(in srgb, var(--foreground) 2%, transparent) 1px,
+ transparent 0
+ );
background-size: 20px 20px;
opacity: 0.15;
pointer-events: none;
@@ -490,25 +565,46 @@
}
.dark .fintech-card::before {
- background-image:
- radial-gradient(circle at 1px 1px, color-mix(in srgb, white 3%, transparent) 1px, transparent 0);
+ background-image: radial-gradient(
+ circle at 1px 1px,
+ color-mix(in srgb, white 3%, transparent) 1px,
+ transparent 0
+ );
opacity: 0.1;
}
/* Texture plus prononcée pour les cards de statistiques */
.stat-card-textured::before {
- background-image:
- radial-gradient(circle at 1px 1px, color-mix(in srgb, var(--foreground) 3%, transparent) 1px, transparent 0),
- radial-gradient(circle at 11px 11px, color-mix(in srgb, var(--foreground) 1.5%, transparent) 1px, transparent 0);
- background-size: 16px 16px, 24px 24px;
+ background-image:
+ radial-gradient(
+ circle at 1px 1px,
+ color-mix(in srgb, var(--foreground) 3%, transparent) 1px,
+ transparent 0
+ ),
+ radial-gradient(
+ circle at 11px 11px,
+ color-mix(in srgb, var(--foreground) 1.5%, transparent) 1px,
+ transparent 0
+ );
+ background-size:
+ 16px 16px,
+ 24px 24px;
opacity: 0.2;
z-index: 1;
}
.dark .stat-card-textured::before {
- background-image:
- radial-gradient(circle at 1px 1px, color-mix(in srgb, white 4%, transparent) 1px, transparent 0),
- radial-gradient(circle at 11px 11px, color-mix(in srgb, white 2%, transparent) 1px, transparent 0);
+ background-image:
+ radial-gradient(
+ circle at 1px 1px,
+ color-mix(in srgb, white 4%, transparent) 1px,
+ transparent 0
+ ),
+ radial-gradient(
+ circle at 11px 11px,
+ color-mix(in srgb, white 2%, transparent) 1px,
+ transparent 0
+ );
opacity: 0.15;
}
diff --git a/app/layout.tsx b/app/layout.tsx
index dcf9878..398513b 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -4,6 +4,7 @@ import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { AuthSessionProvider } from "@/components/providers/session-provider";
import { QueryProvider } from "@/components/providers/query-provider";
+import { BackgroundProvider } from "@/components/providers/background-provider";
const _geist = Geist({ subsets: ["latin"] });
const _geistMono = Geist_Mono({ subsets: ["latin"] });
@@ -23,6 +24,7 @@ export default function RootLayout({
return (
+
{children}
diff --git a/app/settings/page.tsx b/app/settings/page.tsx
index 8df5ca7..215d3c0 100644
--- a/app/settings/page.tsx
+++ b/app/settings/page.tsx
@@ -9,6 +9,7 @@ import {
BackupCard,
PasswordCard,
ReconcileDateRangeCard,
+ BackgroundCard,
} from "@/components/settings";
import { useBankingData } from "@/lib/hooks";
import type { BankingData } from "@/lib/types";
@@ -126,6 +127,8 @@ export default function SettingsPage() {
+
+
-
+
diff --git a/components/providers/background-provider.tsx b/components/providers/background-provider.tsx
new file mode 100644
index 0000000..3642864
--- /dev/null
+++ b/components/providers/background-provider.tsx
@@ -0,0 +1,89 @@
+"use client";
+
+import { useEffect } from "react";
+
+interface BackgroundSettings {
+ type: string;
+ customImageUrl?: string;
+}
+
+export function BackgroundProvider() {
+ useEffect(() => {
+ const applyBackground = () => {
+ try {
+ const pageBackground = document.querySelector(
+ ".page-background"
+ ) as HTMLElement;
+ if (!pageBackground) return;
+
+ const stored = localStorage.getItem("background-settings");
+ const settings: BackgroundSettings = stored
+ ? JSON.parse(stored)
+ : { type: "default" };
+
+ // Retirer toutes les classes de fond
+ pageBackground.classList.remove(
+ "bg-default",
+ "bg-gradient-blue",
+ "bg-gradient-purple",
+ "bg-gradient-green",
+ "bg-gradient-orange",
+ "bg-solid-light",
+ "bg-solid-dark",
+ "bg-custom-image"
+ );
+
+ const root = document.documentElement;
+
+ if (settings.type === "custom-image" && settings.customImageUrl) {
+ pageBackground.classList.add("bg-custom-image");
+ root.style.setProperty(
+ "--custom-background-image",
+ `url(${settings.customImageUrl})`
+ );
+ } else {
+ pageBackground.classList.add(`bg-${settings.type || "default"}`);
+ root.style.removeProperty("--custom-background-image");
+ }
+ } catch (error) {
+ console.error("Error applying background:", error);
+ }
+ };
+
+ // Appliquer immédiatement
+ applyBackground();
+
+ // Observer pour les changements de DOM (si le page-background est ajouté plus tard)
+ const observer = new MutationObserver(() => {
+ applyBackground();
+ });
+
+ observer.observe(document.body, {
+ childList: true,
+ subtree: true,
+ });
+
+ // Écouter les changements dans localStorage (autres onglets)
+ const handleStorageChange = (e: StorageEvent) => {
+ if (e.key === "background-settings") {
+ applyBackground();
+ }
+ };
+
+ // Écouter les événements personnalisés (même onglet)
+ const handleBackgroundChanged = () => {
+ applyBackground();
+ };
+
+ window.addEventListener("storage", handleStorageChange);
+ window.addEventListener("background-changed", handleBackgroundChanged);
+
+ return () => {
+ observer.disconnect();
+ window.removeEventListener("storage", handleStorageChange);
+ window.removeEventListener("background-changed", handleBackgroundChanged);
+ };
+ }, []);
+
+ return null;
+}
diff --git a/components/settings/background-card.tsx b/components/settings/background-card.tsx
new file mode 100644
index 0000000..711542f
--- /dev/null
+++ b/components/settings/background-card.tsx
@@ -0,0 +1,312 @@
+"use client";
+
+import { useState, useEffect, useMemo } from "react";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
+import { Image, X } from "lucide-react";
+import { useLocalStorage } from "@/hooks/use-local-storage";
+import { cn } from "@/lib/utils";
+
+type BackgroundType =
+ | "default"
+ | "gradient-blue"
+ | "gradient-purple"
+ | "gradient-green"
+ | "gradient-orange"
+ | "solid-light"
+ | "solid-dark"
+ | "custom-image";
+
+interface BackgroundSettings {
+ type: BackgroundType;
+ customImageUrl?: string;
+}
+
+const DEFAULT_BACKGROUNDS: Array<{
+ value: BackgroundType;
+ label: string;
+ preview: string;
+}> = [
+ {
+ value: "default",
+ label: "Par défaut",
+ preview:
+ "linear-gradient(135deg, oklch(0.98 0.01 280) 0%, oklch(0.97 0.012 270) 50%, oklch(0.98 0.01 290) 100%)",
+ },
+ {
+ value: "gradient-blue",
+ label: "Dégradé bleu",
+ preview: "linear-gradient(135deg, #e0f2fe 0%, #bae6fd 50%, #7dd3fc 100%)",
+ },
+ {
+ value: "gradient-purple",
+ label: "Dégradé violet",
+ preview: "linear-gradient(135deg, #f3e8ff 0%, #e9d5ff 50%, #d8b4fe 100%)",
+ },
+ {
+ value: "gradient-green",
+ label: "Dégradé vert",
+ preview: "linear-gradient(135deg, #dcfce7 0%, #bbf7d0 50%, #86efac 100%)",
+ },
+ {
+ value: "gradient-orange",
+ label: "Dégradé orange",
+ preview: "linear-gradient(135deg, #fff7ed 0%, #ffedd5 50%, #fed7aa 100%)",
+ },
+ {
+ value: "solid-light",
+ label: "Solide clair",
+ preview: "#ffffff",
+ },
+ {
+ value: "solid-dark",
+ label: "Solide sombre",
+ preview: "#1e293b",
+ },
+];
+
+export function BackgroundCard() {
+ const [backgroundSettings, setBackgroundSettings] =
+ useLocalStorage
("background-settings", {
+ type: "default",
+ });
+
+ const currentSettings = useMemo(
+ () => backgroundSettings || { type: "default" },
+ [backgroundSettings]
+ );
+
+ const [customImageUrl, setCustomImageUrl] = useState(
+ currentSettings.customImageUrl || ""
+ );
+ const [showCustomInput, setShowCustomInput] = useState(
+ currentSettings.type === "custom-image"
+ );
+
+ // Synchroniser customImageUrl avec les settings
+ useEffect(() => {
+ if (
+ currentSettings.type === "custom-image" &&
+ currentSettings.customImageUrl
+ ) {
+ setCustomImageUrl(currentSettings.customImageUrl);
+ }
+ }, [currentSettings]);
+
+ const applyBackground = (settings: BackgroundSettings) => {
+ const root = document.documentElement;
+ const pageBackground = document.querySelector(
+ ".page-background"
+ ) as HTMLElement;
+
+ if (!pageBackground) return;
+
+ // Retirer toutes les classes de fond
+ pageBackground.classList.remove(
+ "bg-default",
+ "bg-gradient-blue",
+ "bg-gradient-purple",
+ "bg-gradient-green",
+ "bg-gradient-orange",
+ "bg-solid-light",
+ "bg-solid-dark",
+ "bg-custom-image"
+ );
+
+ if (settings.type === "custom-image" && settings.customImageUrl) {
+ pageBackground.classList.add("bg-custom-image");
+ root.style.setProperty(
+ "--custom-background-image",
+ `url(${settings.customImageUrl})`
+ );
+ } else {
+ pageBackground.classList.add(`bg-${settings.type || "default"}`);
+ root.style.removeProperty("--custom-background-image");
+ }
+
+ // Déclencher un événement personnalisé pour notifier les autres composants
+ window.dispatchEvent(
+ new CustomEvent("background-changed", { detail: settings })
+ );
+ };
+
+ const handleBackgroundChange = (type: BackgroundType) => {
+ if (type === "custom-image") {
+ setShowCustomInput(true);
+ if (customImageUrl.trim()) {
+ const newSettings: BackgroundSettings = {
+ type,
+ customImageUrl: customImageUrl.trim(),
+ };
+ setBackgroundSettings(newSettings);
+ applyBackground(newSettings);
+ } else {
+ const newSettings: BackgroundSettings = { type };
+ setBackgroundSettings(newSettings);
+ }
+ } else {
+ setShowCustomInput(false);
+ const newSettings: BackgroundSettings = { type };
+ setBackgroundSettings(newSettings);
+ applyBackground(newSettings);
+ }
+ };
+
+ const handleCustomImageSubmit = () => {
+ if (customImageUrl.trim()) {
+ const newSettings: BackgroundSettings = {
+ type: "custom-image",
+ customImageUrl: customImageUrl.trim(),
+ };
+ setBackgroundSettings(newSettings);
+ applyBackground(newSettings);
+ }
+ };
+
+ const handleRemoveCustomImage = () => {
+ setCustomImageUrl("");
+ setShowCustomInput(false);
+ const newSettings: BackgroundSettings = { type: "default" };
+ setBackgroundSettings(newSettings);
+ applyBackground(newSettings);
+ };
+
+ // Appliquer le fond au chargement et quand il change
+ useEffect(() => {
+ if (typeof window !== "undefined") {
+ applyBackground(currentSettings);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [currentSettings.type, currentSettings.customImageUrl]);
+
+ return (
+
+
+
+
+ Fond du site
+
+
+ Personnalisez l'apparence du fond de l'application
+
+
+
+
+ handleBackgroundChange(value as BackgroundType)
+ }
+ >
+
+ {DEFAULT_BACKGROUNDS.map((bg) => (
+
+ ))}
+
+
+
+
+ {showCustomInput && (
+
+
+
+
+ setCustomImageUrl(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ handleCustomImageSubmit();
+ }
+ }}
+ />
+ {customImageUrl && (
+
+ )}
+
+
+
+ {currentSettings.type === "custom-image" &&
+ currentSettings.customImageUrl && (
+
+ )}
+
+ )}
+
+
+ );
+}
diff --git a/components/settings/index.ts b/components/settings/index.ts
index 2c58ec6..e28bc05 100644
--- a/components/settings/index.ts
+++ b/components/settings/index.ts
@@ -4,3 +4,4 @@ export { OFXInfoCard } from "./ofx-info-card";
export { BackupCard } from "./backup-card";
export { PasswordCard } from "./password-card";
export { ReconcileDateRangeCard } from "./reconcile-date-range-card";
+export { BackgroundCard } from "./background-card";