"use client"; import Image from "next/image"; import { useRouter } from "next/navigation"; import { useEffect, useMemo, useRef, useState } from "react"; import { useTheme } from "next-themes"; import { useMutation } from "convex/react"; import { useTranslations } from "next-intl"; import { ChevronDown, Coins, ImageIcon, LayoutTemplate, Loader2, Monitor, Moon, Search, Sun, } from "lucide-react"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { api } from "@/convex/_generated/api"; import type { Doc } from "@/convex/_generated/dataModel"; import { authClient } from "@/lib/auth-client"; import { CreditOverview } from "@/components/dashboard/credit-overview"; import { CreditsActivityChart } from "@/components/dashboard/credits-activity-chart"; import { RecentTransactions } from "@/components/dashboard/recent-transactions"; import CanvasCard from "@/components/dashboard/canvas-card"; import { MediaLibraryDialog } from "@/components/media/media-library-dialog"; import { collectMediaStorageIdsForResolution, resolveMediaPreviewUrl, } from "@/components/media/media-preview-utils"; import { useDashboardSnapshot } from "@/hooks/use-dashboard-snapshot"; import { toast } from "@/lib/toast"; function getInitials(nameOrEmail: string) { const normalized = nameOrEmail.trim(); if (!normalized) return "U"; const parts = normalized.split(/\s+/).filter(Boolean); if (parts.length >= 2) { return `${parts[0][0]}${parts[1][0]}`.toUpperCase(); } return normalized.slice(0, 2).toUpperCase(); } function formatDimensions(width: number | undefined, height: number | undefined): string { if (typeof width === "number" && typeof height === "number") { return `${width} x ${height}px`; } return "Größe unbekannt"; } export function DashboardPageClient() { const t = useTranslations("toasts"); const router = useRouter(); const welcomeToastSentRef = useRef(false); const { theme = "system", setTheme } = useTheme(); const { data: session, isPending: isSessionPending } = authClient.useSession(); const { snapshot: dashboardSnapshot } = useDashboardSnapshot(session?.user?.id); const createCanvas = useMutation(api.canvases.create); const resolveMediaPreviewUrls = useMutation(api.storage.batchGetUrlsForUserMedia); const [isCreatingWorkspace, setIsCreatingWorkspace] = useState(false); const [isMediaLibraryDialogOpen, setIsMediaLibraryDialogOpen] = useState(false); const [mediaPreviewUrlMap, setMediaPreviewUrlMap] = useState>({}); const [isResolvingMediaPreview, setIsResolvingMediaPreview] = useState(false); const [mediaPreviewError, setMediaPreviewError] = useState(null); const [hasClientMounted, setHasClientMounted] = useState(false); useEffect(() => { setHasClientMounted(true); }, []); const displayName = session?.user.name?.trim() || session?.user.email || "Nutzer"; const initials = getInitials(displayName); const canvases = dashboardSnapshot?.canvases; const mediaPreview = dashboardSnapshot?.mediaPreview; const mediaPreviewStorageIds = useMemo(() => { const previewItems = mediaPreview ?? []; return collectMediaStorageIdsForResolution(previewItems); }, [mediaPreview]); useEffect(() => { if (!session?.user || welcomeToastSentRef.current) return; const key = `ls-dashboard-welcome-${session.user.id}`; if (typeof window !== "undefined" && sessionStorage.getItem(key)) return; welcomeToastSentRef.current = true; sessionStorage.setItem(key, "1"); toast.success(t("auth.welcomeOnDashboard")); }, [t, session?.user]); useEffect(() => { let isCancelled = false; async function run() { if (dashboardSnapshot === undefined) { setMediaPreviewUrlMap({}); setMediaPreviewError(null); setIsResolvingMediaPreview(false); return; } if (mediaPreviewStorageIds.length === 0) { setMediaPreviewUrlMap({}); setMediaPreviewError(null); setIsResolvingMediaPreview(false); return; } setIsResolvingMediaPreview(true); setMediaPreviewError(null); try { const resolved = await resolveMediaPreviewUrls({ storageIds: mediaPreviewStorageIds }); if (isCancelled) { return; } setMediaPreviewUrlMap(resolved); } catch (error) { if (isCancelled) { return; } setMediaPreviewUrlMap({}); setMediaPreviewError( error instanceof Error ? error.message : "Vorschau konnte nicht geladen werden.", ); } finally { if (!isCancelled) { setIsResolvingMediaPreview(false); } } } void run(); return () => { isCancelled = true; }; }, [dashboardSnapshot, mediaPreviewStorageIds, resolveMediaPreviewUrls]); const handleSignOut = async () => { toast.info(t("auth.signedOut")); await authClient.signOut(); router.replace("/auth/sign-in"); router.refresh(); }; const handleCreateWorkspace = async () => { if (isCreatingWorkspace) return; if (!session?.user) return; setIsCreatingWorkspace(true); try { const canvasId = await createCanvas({ name: "Neuer Workspace", description: "", }); router.push(`/canvas/${canvasId}`); } finally { setIsCreatingWorkspace(false); } }; return (
Account Theme setTheme(value)} > Light Dark System Einstellungen Abrechnung Abmelden

Guten Tag, {displayName}

Überblick über deine Credits und laufende Generierungen.

Credit-Übersicht
Arbeitsbereiche
{isSessionPending || canvases === undefined ? (
Arbeitsbereiche werden geladen...
) : canvases.length === 0 ? (
Noch kein Arbeitsbereich vorhanden. Mit „Neuer Arbeitsbereich“ legst du den ersten an.
) : (
{canvases.map((canvas: Doc<"canvases">) => ( router.push(`/canvas/${id}`)} /> ))}
)}
Mediathek
{dashboardSnapshot === undefined ? (
Mediathek wird geladen...
) : mediaPreviewError ? (
Mediathek-Vorschau konnte nicht geladen werden. {mediaPreviewError}
) : !mediaPreview || mediaPreview.length === 0 ? (
Noch keine Medien vorhanden. Sobald du Bilder hochlädst oder generierst, werden sie hier angezeigt.
) : (
{(mediaPreview ?? []).map((item) => { const previewUrl = resolveMediaPreviewUrl(item, mediaPreviewUrlMap); return (
{previewUrl ? ( // eslint-disable-next-line @next/next/no-img-element {item.filename ) : isResolvingMediaPreview ? (
) : (
)}

{item.filename ?? "Unbenanntes Bild"}

{formatDimensions(item.width, item.height)}

); })}
)}
); }