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) => ( +