From d1834c56940c8b0c0e3127f28dc326ba470dd5c8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Mar 2026 15:32:20 +0100 Subject: [PATCH] feat: enhance dashboard and canvas page functionality - Added theme support to the dashboard with light, dark, and system options. - Improved canvas ID handling with validation and fetching logic. - Updated layout component to suppress hydration warnings for better rendering. - Refactored dashboard to include user session management and workspace creation functionality. --- app/(app)/canvas/[canvasId]/page.tsx | 19 +++- app/dashboard/page.tsx | 157 +++++++++++++++++++++------ app/layout.tsx | 1 + components/providers.tsx | 29 ++--- 4 files changed, 156 insertions(+), 50 deletions(-) diff --git a/app/(app)/canvas/[canvasId]/page.tsx b/app/(app)/canvas/[canvasId]/page.tsx index c1b586d..b987d52 100644 --- a/app/(app)/canvas/[canvasId]/page.tsx +++ b/app/(app)/canvas/[canvasId]/page.tsx @@ -17,7 +17,24 @@ export default async function CanvasPage({ } const { canvasId } = await params; - const typedCanvasId = canvasId as Id<"canvases">; + let typedCanvasId: Id<"canvases">; + + if (/^\d+$/.test(canvasId)) { + const oneBasedIndex = Number(canvasId); + if (!Number.isSafeInteger(oneBasedIndex) || oneBasedIndex < 1) { + notFound(); + } + + const canvases = await fetchAuthQuery(api.canvases.list, {}); + const selectedCanvas = canvases[oneBasedIndex - 1]; + if (!selectedCanvas) { + notFound(); + } + + typedCanvasId = selectedCanvas._id; + } else { + typedCanvasId = canvasId as Id<"canvases">; + } try { const canvas = await fetchAuthQuery(api.canvases.get, { diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 9f59305..03d907c 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,14 +1,21 @@ "use client"; import Image from "next/image"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { useTheme } from "next-themes"; +import { useMutation, useQuery } from "convex/react"; import { Activity, ArrowUpRight, ChevronDown, Coins, LayoutTemplate, + Monitor, + Moon, Search, Sparkles, + Sun, } from "lucide-react"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; @@ -19,11 +26,15 @@ import { DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { Progress } from "@/components/ui/progress"; +import { api } from "@/convex/_generated/api"; +import { authClient } from "@/lib/auth-client"; import { cn } from "@/lib/utils"; const formatEurFromCents = (cents: number) => @@ -71,12 +82,6 @@ const mockRuns = [ }, ]; -const mockWorkspaces = [ - { name: "Sommer-Kampagne", nodes: 24, frames: 3, initial: "S" }, - { name: "Produktfotos", nodes: 11, frames: 2, initial: "P" }, - { name: "Social Variants", nodes: 8, frames: 1, initial: "V" }, -]; - function StatusDot({ status }: { status: (typeof mockRuns)[0]["status"] }) { const base = "inline-block size-2 rounded-full"; switch (status) { @@ -109,7 +114,54 @@ function statusLabel(status: (typeof mockRuns)[0]["status"]) { } } +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(); +} + export default function DashboardPage() { + const router = useRouter(); + const { theme = "system", setTheme } = useTheme(); + const { data: session, isPending: isSessionPending } = authClient.useSession(); + const canvases = useQuery( + api.canvases.list, + session?.user && !isSessionPending ? {} : "skip", + ); + const createCanvas = useMutation(api.canvases.create); + const [isCreatingWorkspace, setIsCreatingWorkspace] = useState(false); + + const displayName = session?.user.name?.trim() || session?.user.email || "Nutzer"; + const initials = getInitials(displayName); + + const handleSignOut = async () => { + 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); + } + }; + const balanceCents = 4320; const reservedCents = 180; const monthlyPoolCents = 5000; @@ -151,22 +203,43 @@ export default function DashboardPage() { - Account (Demo) + Account + + + Theme + + setTheme(value)} + > + + + Light + + + + Dark + + + + System + + Einstellungen Abrechnung - Abmelden + Abmelden @@ -177,7 +250,7 @@ export default function DashboardPage() { {/* Greeting & Context */}

- Guten Tag, Mock Nutzer + Guten Tag, {displayName}

Überblick über deine Credits und laufende Generierungen. @@ -268,35 +341,47 @@ export default function DashboardPage() { variant="ghost" size="sm" className="text-muted-foreground" - disabled + type="button" + onClick={handleCreateWorkspace} + disabled={isCreatingWorkspace || isSessionPending || !session?.user} > - Neuer Workspace + {isCreatingWorkspace ? "Erstelle..." : "Neuer Workspace"}

-
- {mockWorkspaces.map((ws) => ( - - ))} -
+ {isSessionPending || canvases === undefined ? ( +
+ Workspaces werden geladen... +
+ ) : canvases.length === 0 ? ( +
+ Noch kein Workspace vorhanden. Mit "Neuer Workspace" legst du den + ersten an. +
+ ) : ( +
+ {canvases.map((canvas) => ( + + ))} +
+ )} {/* Recent Activity */} diff --git a/app/layout.tsx b/app/layout.tsx index 0a936f8..f2474bd 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -21,6 +21,7 @@ export default function RootLayout({ return ( diff --git a/components/providers.tsx b/components/providers.tsx index 112cdc5..170d397 100644 --- a/components/providers.tsx +++ b/components/providers.tsx @@ -4,6 +4,7 @@ import { ReactNode } from "react"; import { ConvexReactClient } from "convex/react"; import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react"; import { AuthUIProvider } from "@daveyplate/better-auth-ui"; +import { ThemeProvider } from "next-themes"; import Link from "next/link"; import { useRouter } from "next/navigation"; @@ -21,20 +22,22 @@ export function Providers({ const router = useRouter(); return ( - - + router.refresh()} - Link={Link} + initialToken={initialToken} > - {children} - - + router.refresh()} + Link={Link} + > + {children} + + + ); }