'use client'; import { useTranslations } from 'next-intl'; import { useTheme } from 'next-themes'; import { type CSSProperties, useCallback, useEffect, useRef, useState } from 'react'; import { updateDashboardBackgroundAction } from '@/lib/auth-actions'; import { BACKGROUND_PRESETS, type DashboardBackground, NO_BACKGROUND, presetBackground, takeLegacyDashboardBackground, } from '@/lib/dashboard-background'; import { type DashboardImageMeta, dashboardImageSrc, fetchDashboardImages, uploadDashboardImage, } from '@/lib/dashboard-images-api'; import { useAuthStore } from '@/lib/stores/auth-store'; /** Schreibt die Wahl in den Auth-Store, sofern noch derselbe Benutzer angemeldet ist. */ function setStoredBackground(userId: string, value: DashboardBackground | null) { const current = useAuthStore.getState().user; if (!current || current.id !== userId) return; useAuthStore.getState().setUser({ ...current, dashboardBackground: value }); } /** * Nimmt eine fehlgeschlagene Wahl zurueck — aber nur, wenn im Store noch * GENAU dieser optimistisch gesetzte Wert steht (quick-260930). Hat der * Benutzer inzwischen etwas anderes gewaehlt, wuerde der Fehler des ersten * Klicks sonst die zweite Wahl ueberschreiben. */ function revertStoredBackground( userId: string, optimistic: DashboardBackground, previous: DashboardBackground | null, ) { const current = useAuthStore.getState().user; if (!current || current.id !== userId) return; if (current.dashboardBackground !== optimistic) return; setStoredBackground(userId, previous); } /** * Wahl des Hintergrunds fuer den angemeldeten Benutzer (quick-260928-ujj). * * Gelesen aus dem Auth-Store (Sitzungsantwort, `null` = nie gewaehlt = * kein Hintergrund). `choose()` setzt den Store sofort und speichert ueber * die Server-Aktion; schlaegt das fehl, kommt der vorige Wert zurueck. * Ist der Server-Wert `null` und liegt noch eine alte localStorage-Wahl vor, * wird sie genau einmal je Benutzerkennung uebernommen. * * quick-260930: Zeigt die Wahl auf ein eigenes Bild, das nicht (mehr) zu * laden ist — etwa weil es im Bilderrahmen geloescht wurde (die API setzt die * gespeicherte Wahl dabei auf „kein Hintergrund“, eine schon geladene Sitzung * kennt aber noch den alten Wert) —, gilt sie als „kein Hintergrund“. Das * ist nur Anzeige; gespeichert wird dabei nichts. */ export function useDashboardBackground() { const user = useAuthStore((s) => s.user); const userId = user?.id ?? null; const stored = user?.dashboardBackground ?? null; const imageId = stored?.kind === 'image' ? stored.imageId : null; const [brokenImageId, setBrokenImageId] = useState(null); const background = stored === null || (imageId !== null && imageId === brokenImageId) ? NO_BACKGROUND : stored; const migratedFor = useRef(null); // Laedt das gewaehlte Bild einmal zur Probe; schlaegt das fehl, wird die // Wahl als „kein Hintergrund“ angezeigt. useEffect(() => { if (imageId === null || typeof Image === 'undefined') return; let active = true; const probe = new Image(); probe.onerror = () => { if (active) setBrokenImageId(imageId); }; probe.src = dashboardImageSrc(imageId); return () => { active = false; probe.onerror = null; }; }, [imageId]); const save = useCallback( (id: string, value: DashboardBackground, previous: DashboardBackground | null) => { setStoredBackground(id, value); const revert = () => revertStoredBackground(id, value, previous); updateDashboardBackgroundAction(value) .then((result) => { if (!result.success) revert(); }) .catch(revert); }, [], ); const choose = useCallback( (value: DashboardBackground) => { if (!userId) return; save(userId, value, stored); }, [userId, stored, save], ); useEffect(() => { if (!userId || migratedFor.current === userId) return; migratedFor.current = userId; // Auch bei gesetztem Server-Wert auslesen: der alte Schluessel wird so // aufgeraeumt, uebernommen wird aber nur, wenn noch nie gewaehlt wurde. const legacy = takeLegacyDashboardBackground(userId); if (legacy && stored === null) save(userId, legacy, null); }, [userId, stored, save]); return { background, choose }; } /** CSS fuer eine Wahl; Flaechenfarbe oder Bild, jeweils bildschirmfuellend. */ function backgroundStyle(value: DashboardBackground, dark: boolean): CSSProperties | null { if (value.kind === 'none') return null; if (value.kind === 'image') { return { backgroundImage: `url("${dashboardImageSrc(value.imageId)}")`, backgroundSize: 'cover', backgroundPosition: 'center', }; } const css = presetBackground(value.id, dark); if (!css) return null; return css.startsWith('url(') ? { backgroundImage: css, backgroundSize: 'cover', backgroundPosition: 'center' } : { backgroundColor: css }; } /** * Bildschirmfuellende Hintergrundebene hinter dem Dashboard. Liegt fest * unter Kopfzeile und Seitenleiste (beide haben eigene Flaechen). Im dunklen * Erscheinungsbild werden eigene Bilder abgedunkelt — so bleibt der Text auf * den durchscheinenden Kacheln auch bei einem sehr hellen Foto lesbar. */ export function DashboardBackdrop({ background }: { background: DashboardBackground }) { const { resolvedTheme } = useTheme(); const dark = resolvedTheme === 'dark'; const style = backgroundStyle(background, dark); if (!style) return null; return (