From df2a6c1759fcbc6309461243e6f315873304a8e6 Mon Sep 17 00:00:00 2001 From: Matthias Meister Date: Fri, 3 Apr 2026 18:55:13 +0200 Subject: [PATCH] refactor(app): move auth gating and metadata to server-first patterns --- app/dashboard/page-client.tsx | 247 +++++++++++++++++++++++++++++++ app/dashboard/page.tsx | 264 ++-------------------------------- app/layout.tsx | 52 ++++--- app/page.tsx | 74 +++------- 4 files changed, 309 insertions(+), 328 deletions(-) create mode 100644 app/dashboard/page-client.tsx diff --git a/app/dashboard/page-client.tsx b/app/dashboard/page-client.tsx new file mode 100644 index 0000000..9cb5222 --- /dev/null +++ b/app/dashboard/page-client.tsx @@ -0,0 +1,247 @@ +"use client"; + +import Image from "next/image"; +import { useRouter } from "next/navigation"; +import { useEffect, useRef, useState } from "react"; +import { useTheme } from "next-themes"; +import { useMutation } from "convex/react"; +import { useTranslations } from "next-intl"; +import { + ChevronDown, + Coins, + LayoutTemplate, + 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 { RecentTransactions } from "@/components/dashboard/recent-transactions"; +import CanvasCard from "@/components/dashboard/canvas-card"; +import { toast } from "@/lib/toast"; +import { useAuthQuery } from "@/hooks/use-auth-query"; + +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 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 canvases = useAuthQuery( + api.canvases.list, + session?.user && !isSessionPending ? {} : "skip", + ); + const createCanvas = useMutation(api.canvases.create); + const [isCreatingWorkspace, setIsCreatingWorkspace] = useState(false); + const [hasClientMounted, setHasClientMounted] = useState(false); + + useEffect(() => { + setHasClientMounted(true); + }, []); + + const displayName = session?.user.name?.trim() || session?.user.email || "Nutzer"; + const initials = getInitials(displayName); + + 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]); + + 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}`)} + /> + ))} +
+ )} +
+ +
+ +
+
+
+ ); +} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 4289c24..269319c 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,263 +1,15 @@ -"use client"; +import { redirect } from "next/navigation"; -import Image from "next/image"; -import { useRouter } from "next/navigation"; -import { useEffect, useRef, useState } from "react"; -import { useTheme } from "next-themes"; -import { useMutation } from "convex/react"; -import { useTranslations } from "next-intl"; -import { - ChevronDown, - Coins, - LayoutTemplate, - Monitor, - Moon, - Search, - Sun, -} from "lucide-react"; +import { isAuthenticated } from "@/lib/auth-server"; -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 { RecentTransactions } from "@/components/dashboard/recent-transactions"; -import CanvasCard from "@/components/dashboard/canvas-card"; -import { toast } from "@/lib/toast"; -import { useAuthQuery } from "@/hooks/use-auth-query"; +import { DashboardPageClient } from "./page-client"; +export default async function DashboardPage() { + const authenticated = await isAuthenticated(); -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(); + if (!authenticated) { + redirect("/auth/sign-in"); } - return normalized.slice(0, 2).toUpperCase(); -} - -export default function DashboardPage() { - const t = useTranslations('toasts'); - const router = useRouter(); - const welcomeToastSentRef = useRef(false); - const { theme = "system", setTheme } = useTheme(); - const { data: session, isPending: isSessionPending } = authClient.useSession(); - const canvases = useAuthQuery( - api.canvases.list, - session?.user && !isSessionPending ? {} : "skip", - ); - const createCanvas = useMutation(api.canvases.create); - const [isCreatingWorkspace, setIsCreatingWorkspace] = useState(false); - const [hasClientMounted, setHasClientMounted] = useState(false); - - useEffect(() => { - setHasClientMounted(true); - }, []); - - useEffect(() => { - if (!isSessionPending && !session?.user) { - router.replace("/auth/sign-in"); - } - }, [isSessionPending, router, session?.user]); - - const displayName = session?.user.name?.trim() || session?.user.email || "Nutzer"; - const initials = getInitials(displayName); - - 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]); - - 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 ( -
- {/* Header */} -
-
-
- -
- -
- - -
- -
- - - - - - Account - - - Theme - - setTheme(value)} - > - - - Light - - - - Dark - - - - System - - - - Einstellungen - Abrechnung - - Abmelden - - -
-
-
- -
- {/* Greeting & Context */} -
-

- Guten Tag, {displayName} -

-

- Überblick über deine Credits und laufende Generierungen. -

-
- - {/* Credits Overview */} -
-
- - Credit-Übersicht -
- -
- - {/* Workspaces */} -
-
-
- - 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}`)} - /> - ))} -
- )} -
- - {/* Recent Transactions */} -
- -
-
-
- ); + return ; } diff --git a/app/layout.tsx b/app/layout.tsx index af2e435..9282acb 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,4 +1,5 @@ import type { Metadata } from "next"; +import Script from "next/script"; import { Manrope } from "next/font/google"; import * as Sentry from "@sentry/nextjs"; import "./globals.css"; @@ -11,8 +12,34 @@ import { getLocale, getMessages, getTimeZone } from "next-intl/server"; const manrope = Manrope({ subsets: ["latin"], variable: "--font-sans" }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + metadataBase: new URL("https://app.lemonspace.io"), + title: { + default: "LemonSpace", + template: "%s | LemonSpace", + }, + description: "LemonSpace is a platform for creating and sharing digital content with nodes.", + keywords: ["LemonSpace", "digital content", "platform", "nodes"], + authors: [{ name: "LemonSpace" }], + robots: { + index: true, + follow: true, + googleBot: { + index: true, + follow: true, + }, + }, + openGraph: { + type: "website", + url: "https://app.lemonspace.io", + siteName: "LemonSpace", + title: "LemonSpace", + description: "LemonSpace is a platform for creating and sharing digital content with nodes.", + }, + twitter: { + card: "summary_large_image", + title: "LemonSpace", + description: "LemonSpace is a platform for creating and sharing digital content with nodes.", + }, }; export default async function RootLayout({ @@ -41,25 +68,12 @@ export default async function RootLayout({ suppressHydrationWarning className={cn("h-full", "antialiased", "font-sans", manrope.variable)} > - - - - - - - - - - - - + strategy="afterInteractive" + /> -

Laden...

- - ); +export default async function Home() { + const authenticated = await isAuthenticated(); + + if (authenticated) { + redirect("/dashboard"); } return (

🍋 LemonSpace

- {session?.user ? ( -
-

- Willkommen, {session.user.name} -

- - Zum Dashboard - - -
- ) : ( -
- - Anmelden - - - Registrieren - -
- )} +
+ + Anmelden + + + Registrieren + +
); }