Refactor layout and home page for improved localization and session handling

- Updated RootLayout to include locale support and integrated ThemeProvider and LocaleProvider for better internationalization.
- Replaced static home page content with a redirect based on user session status, enhancing user experience by directing to the appropriate dashboard or login page.
- Switched font from Geist to Inter for improved typography consistency.
This commit is contained in:
smueller
2026-07-22 14:01:09 +02:00
parent 04e04eb11d
commit 84476e6277
30 changed files with 1073 additions and 128 deletions

View File

@@ -0,0 +1,60 @@
import type { SessionUser } from '@nexumi/shared';
import { type NextRequest, NextResponse } from 'next/server';
import { exchangeCodeForToken, fetchDiscordUser } from '@/lib/discord-oauth';
import { env } from '@/lib/env';
import { redis } from '@/lib/redis';
import { createSession } from '@/lib/session';
function oauthStateKey(state: string): string {
return `webui:oauth:state:${state}`;
}
function loginRedirect(error: string): NextResponse {
const url = new URL('/login', env.WEBUI_URL);
url.searchParams.set('error', error);
return NextResponse.redirect(url);
}
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const code = searchParams.get('code');
const state = searchParams.get('state');
const oauthError = searchParams.get('error');
if (oauthError) {
return loginRedirect('access_denied');
}
if (!code || !state) {
return loginRedirect('invalid_request');
}
const stateKey = oauthStateKey(state);
const stateExists = await redis.get(stateKey);
if (!stateExists) {
return loginRedirect('invalid_state');
}
await redis.del(stateKey);
try {
const token = await exchangeCodeForToken(code);
const discordUser = await fetchDiscordUser(token.access_token);
const user: SessionUser = {
id: discordUser.id,
username: discordUser.username,
globalName: discordUser.global_name ?? null,
avatar: discordUser.avatar ?? null
};
await createSession({
user,
accessToken: token.access_token,
tokenExpiresAt: Date.now() + token.expires_in * 1000
});
} catch {
return loginRedirect('oauth_failed');
}
return NextResponse.redirect(new URL('/dashboard', env.WEBUI_URL));
}

View File

@@ -0,0 +1,16 @@
import { randomBytes } from 'crypto';
import { NextResponse } from 'next/server';
import { buildAuthorizeUrl } from '@/lib/discord-oauth';
import { redis } from '@/lib/redis';
const OAUTH_STATE_TTL_SECONDS = 600;
function oauthStateKey(state: string): string {
return `webui:oauth:state:${state}`;
}
export async function GET() {
const state = randomBytes(24).toString('hex');
await redis.set(oauthStateKey(state), '1', 'EX', OAUTH_STATE_TTL_SECONDS);
return NextResponse.redirect(buildAuthorizeUrl(state));
}

View File

@@ -0,0 +1,7 @@
import { NextResponse } from 'next/server';
import { destroySession } from '@/lib/session';
export async function POST() {
await destroySession();
return NextResponse.json({ ok: true });
}

View File

@@ -0,0 +1,7 @@
import { NextResponse } from 'next/server';
import { getSession } from '@/lib/session';
export async function GET() {
const session = await getSession();
return NextResponse.json({ user: session?.user ?? null });
}

View File

@@ -0,0 +1,46 @@
import { DashboardAccessRulesUpdateSchema } from '@nexumi/shared';
import { type NextRequest, NextResponse } from 'next/server';
import { listAccessRules, replaceAccessRules } from '@/lib/access-rules';
import { requireGuildAccess, toApiErrorResponse } from '@/lib/auth';
import { writeDashboardAudit } from '@/lib/audit';
import { prisma } from '@/lib/prisma';
interface RouteParams {
params: Promise<{ guildId: string }>;
}
export async function GET(_request: NextRequest, { params }: RouteParams) {
const { guildId } = await params;
try {
await requireGuildAccess(guildId);
const rules = await listAccessRules(guildId);
return NextResponse.json({ rules });
} catch (error) {
return toApiErrorResponse(error);
}
}
export async function PUT(request: NextRequest, { params }: RouteParams) {
const { guildId } = await params;
try {
const session = await requireGuildAccess(guildId);
const body = await request.json();
const { rules: patch } = DashboardAccessRulesUpdateSchema.parse(body);
const before = await listAccessRules(guildId);
const after = await replaceAccessRules(guildId, patch);
await writeDashboardAudit(prisma, {
guildId,
actorUserId: session.user.id,
action: 'access-rules.update',
path: `/dashboard/${guildId}/access`,
before,
after
});
return NextResponse.json({ rules: after });
} catch (error) {
return toApiErrorResponse(error);
}
}

View File

@@ -0,0 +1,20 @@
import { type NextRequest, NextResponse } from 'next/server';
import { requireGuildAccess, toApiErrorResponse } from '@/lib/auth';
import { listRecentDashboardAudit } from '@/lib/audit';
interface RouteParams {
params: Promise<{ guildId: string }>;
}
export async function GET(request: NextRequest, { params }: RouteParams) {
const { guildId } = await params;
try {
await requireGuildAccess(guildId);
const limitParam = request.nextUrl.searchParams.get('limit');
const limit = limitParam ? Math.min(Math.max(Number.parseInt(limitParam, 10) || 10, 1), 50) : 10;
const entries = await listRecentDashboardAudit(guildId, limit);
return NextResponse.json({ entries });
} catch (error) {
return toApiErrorResponse(error);
}
}

View File

@@ -0,0 +1,46 @@
import { ModulesUpdateSchema } from '@nexumi/shared';
import { type NextRequest, NextResponse } from 'next/server';
import { requireGuildAccess, toApiErrorResponse } from '@/lib/auth';
import { writeDashboardAudit } from '@/lib/audit';
import { getModuleStatuses, updateModuleStatuses } from '@/lib/modules';
import { prisma } from '@/lib/prisma';
interface RouteParams {
params: Promise<{ guildId: string }>;
}
export async function GET(_request: NextRequest, { params }: RouteParams) {
const { guildId } = await params;
try {
await requireGuildAccess(guildId);
const modules = await getModuleStatuses(guildId);
return NextResponse.json({ modules });
} catch (error) {
return toApiErrorResponse(error);
}
}
export async function PATCH(request: NextRequest, { params }: RouteParams) {
const { guildId } = await params;
try {
const session = await requireGuildAccess(guildId);
const body = await request.json();
const { modules: patch } = ModulesUpdateSchema.parse(body);
const before = await getModuleStatuses(guildId);
const after = await updateModuleStatuses(guildId, patch);
await writeDashboardAudit(prisma, {
guildId,
actorUserId: session.user.id,
action: 'modules.update',
path: `/dashboard/${guildId}/modules`,
before,
after
});
return NextResponse.json({ modules: after });
} catch (error) {
return toApiErrorResponse(error);
}
}

View File

@@ -0,0 +1,46 @@
import { GuildGeneralSettingsPatchSchema } from '@nexumi/shared';
import { type NextRequest, NextResponse } from 'next/server';
import { requireGuildAccess, toApiErrorResponse } from '@/lib/auth';
import { writeDashboardAudit } from '@/lib/audit';
import { getGuildGeneralSettings, updateGuildGeneralSettings } from '@/lib/guild-settings';
import { prisma } from '@/lib/prisma';
interface RouteParams {
params: Promise<{ guildId: string }>;
}
export async function GET(_request: NextRequest, { params }: RouteParams) {
const { guildId } = await params;
try {
await requireGuildAccess(guildId);
const settings = await getGuildGeneralSettings(guildId);
return NextResponse.json(settings);
} catch (error) {
return toApiErrorResponse(error);
}
}
export async function PATCH(request: NextRequest, { params }: RouteParams) {
const { guildId } = await params;
try {
const session = await requireGuildAccess(guildId);
const body = await request.json();
const patch = GuildGeneralSettingsPatchSchema.parse(body);
const before = await getGuildGeneralSettings(guildId);
const after = await updateGuildGeneralSettings(guildId, patch);
await writeDashboardAudit(prisma, {
guildId,
actorUserId: session.user.id,
action: 'settings.update',
path: `/dashboard/${guildId}/settings`,
before,
after
});
return NextResponse.json(after);
} catch (error) {
return toApiErrorResponse(error);
}
}

View File

@@ -0,0 +1,13 @@
import { NextResponse } from 'next/server';
import { requireAuth, toApiErrorResponse } from '@/lib/auth';
import { getManageableGuilds } from '@/lib/guilds';
export async function GET() {
try {
const session = await requireAuth();
const guilds = await getManageableGuilds(session);
return NextResponse.json({ guilds });
} catch (error) {
return toApiErrorResponse(error);
}
}

View File

@@ -0,0 +1,5 @@
import { NextResponse } from 'next/server';
export async function GET() {
return NextResponse.json({ ok: true });
}

View File

@@ -0,0 +1,39 @@
import { DASHBOARD_MODULES } from '@nexumi/shared';
import { Construction } from 'lucide-react';
import Link from 'next/link';
import { notFound } from 'next/navigation';
import { Card, CardContent } from '@/components/ui/card';
import { getLocale, t } from '@/lib/i18n';
interface ModulePlaceholderPageProps {
params: Promise<{ guildId: string; module: string }>;
}
export default async function ModulePlaceholderPage({ params }: ModulePlaceholderPageProps) {
const { guildId, module: moduleHref } = await params;
const moduleEntry = DASHBOARD_MODULES.find((entry) => entry.href === moduleHref);
if (!moduleEntry) {
notFound();
}
const locale = await getLocale();
const moduleLabel = t(locale, `modules.${moduleEntry.id}.label`);
return (
<Card>
<CardContent className="flex flex-col items-center gap-4 py-16 text-center">
<div className="flex size-12 items-center justify-center rounded-full bg-muted">
<Construction className="size-6 text-muted-foreground" />
</div>
<div className="space-y-1">
<h1 className="text-xl font-semibold">{t(locale, 'modulePlaceholder.title', { module: moduleLabel })}</h1>
<p className="text-sm text-muted-foreground">{t(locale, 'modulePlaceholder.message')}</p>
</div>
<Link href={`/dashboard/${guildId}/modules`} className="text-sm text-primary hover:underline">
{t(locale, 'modulePlaceholder.backToModules')}
</Link>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,10 @@
import { Skeleton } from '@/components/ui/skeleton';
export default function AccessLoading() {
return (
<div className="space-y-4">
<Skeleton className="h-8 w-56" />
<Skeleton className="h-48 rounded-lg" />
</div>
);
}

View File

@@ -0,0 +1,13 @@
import { AccessRulesForm } from '@/components/settings/access-rules-form';
import { listAccessRules } from '@/lib/access-rules';
interface AccessPageProps {
params: Promise<{ guildId: string }>;
}
export default async function GuildAccessPage({ params }: AccessPageProps) {
const { guildId } = await params;
const rules = await listAccessRules(guildId);
return <AccessRulesForm guildId={guildId} initialRules={rules} />;
}

View File

@@ -0,0 +1,29 @@
import { notFound } from 'next/navigation';
import type { ReactNode } from 'react';
import { DashboardShell } from '@/components/layout/dashboard-shell';
import { requireGuildAccessOrRedirect } from '@/lib/auth';
import { getManageableGuilds } from '@/lib/guilds';
interface GuildLayoutProps {
children: ReactNode;
params: Promise<{ guildId: string }>;
}
export default async function GuildLayout({ children, params }: GuildLayoutProps) {
const { guildId } = await params;
const session = await requireGuildAccessOrRedirect(guildId);
const guilds = await getManageableGuilds(session);
const currentGuild = guilds.find((guild) => guild.id === guildId);
if (!currentGuild) {
notFound();
}
const otherGuilds = guilds.filter((guild) => guild.id !== guildId);
return (
<DashboardShell guildId={guildId} currentGuild={currentGuild} otherGuilds={otherGuilds} user={session.user}>
{children}
</DashboardShell>
);
}

View File

@@ -0,0 +1,15 @@
import { Skeleton } from '@/components/ui/skeleton';
export default function GuildOverviewLoading() {
return (
<div className="space-y-8">
<Skeleton className="h-8 w-40" />
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{Array.from({ length: 6 }).map((_, index) => (
<Skeleton key={index} className="h-16 rounded-lg" />
))}
</div>
<Skeleton className="h-64 rounded-lg" />
</div>
);
}

View File

@@ -0,0 +1,15 @@
import { Skeleton } from '@/components/ui/skeleton';
export default function ModulesLoading() {
return (
<div className="space-y-6">
<div className="space-y-2">
<Skeleton className="h-8 w-40" />
<Skeleton className="h-4 w-80" />
</div>
{Array.from({ length: 3 }).map((_, index) => (
<Skeleton key={index} className="h-40 rounded-lg" />
))}
</div>
);
}

View File

@@ -0,0 +1,22 @@
import { ModuleTogglesForm } from '@/components/settings/module-toggles-form';
import { getLocale, t } from '@/lib/i18n';
import { getModuleStatuses } from '@/lib/modules';
interface ModulesPageProps {
params: Promise<{ guildId: string }>;
}
export default async function GuildModulesPage({ params }: ModulesPageProps) {
const { guildId } = await params;
const [locale, statuses] = await Promise.all([getLocale(), getModuleStatuses(guildId)]);
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold">{t(locale, 'modulesPage.title')}</h1>
<p className="text-sm text-muted-foreground">{t(locale, 'modulesPage.description')}</p>
</div>
<ModuleTogglesForm guildId={guildId} initialStatuses={statuses} />
</div>
);
}

View File

@@ -0,0 +1,87 @@
import Link from 'next/link';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { listRecentDashboardAudit } from '@/lib/audit';
import { getLocale, t } from '@/lib/i18n';
import { getModuleStatuses } from '@/lib/modules';
interface OverviewPageProps {
params: Promise<{ guildId: string }>;
}
function formatDate(iso: string, locale: string): string {
return new Date(iso).toLocaleString(locale === 'de' ? 'de-DE' : 'en-US', {
dateStyle: 'medium',
timeStyle: 'short'
});
}
export default async function GuildOverviewPage({ params }: OverviewPageProps) {
const { guildId } = await params;
const locale = await getLocale();
const [modules, auditEntries] = await Promise.all([
getModuleStatuses(guildId),
listRecentDashboardAudit(guildId, 10)
]);
return (
<div className="space-y-8">
<h1 className="text-2xl font-semibold">{t(locale, 'dashboard.overview.title')}</h1>
<section className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-medium">{t(locale, 'dashboard.overview.moduleStatusTitle')}</h2>
<Link href={`/dashboard/${guildId}/modules`} className="text-sm text-primary hover:underline">
{t(locale, 'dashboard.overview.viewAllModules')}
</Link>
</div>
{modules.length === 0 ? (
<p className="text-sm text-muted-foreground">{t(locale, 'dashboard.overview.moduleStatusEmpty')}</p>
) : (
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{modules.map((module) => (
<Card key={module.id}>
<CardContent className="flex items-center justify-between p-4">
<span className="font-medium">{t(locale, `modules.${module.id}.label`)}</span>
<Badge variant={module.enabled ? 'success' : 'secondary'}>
{t(locale, module.enabled ? 'common.enabled' : 'common.disabled')}
</Badge>
</CardContent>
</Card>
))}
</div>
)}
</section>
<section className="space-y-4">
<h2 className="text-lg font-medium">{t(locale, 'dashboard.overview.recentAuditTitle')}</h2>
<Card>
<CardHeader>
<CardTitle className="text-sm text-muted-foreground">
{t(locale, 'dashboard.overview.recentAuditTitle')}
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{auditEntries.length === 0 ? (
<p className="text-sm text-muted-foreground">{t(locale, 'dashboard.overview.auditEmpty')}</p>
) : (
<ul className="divide-y divide-border">
{auditEntries.map((entry) => (
<li key={entry.id} className="flex items-center justify-between gap-4 py-2 text-sm">
<div className="min-w-0">
<p className="truncate font-medium">{entry.action}</p>
{entry.path && <p className="truncate text-xs text-muted-foreground">{entry.path}</p>}
</div>
<span className="whitespace-nowrap text-xs text-muted-foreground">
{formatDate(entry.createdAt, locale)}
</span>
</li>
))}
</ul>
)}
</CardContent>
</Card>
</section>
</div>
);
}

View File

@@ -0,0 +1,10 @@
import { Skeleton } from '@/components/ui/skeleton';
export default function SettingsLoading() {
return (
<div className="space-y-6">
<Skeleton className="h-40 rounded-lg" />
<Skeleton className="h-24 rounded-lg" />
</div>
);
}

View File

@@ -0,0 +1,13 @@
import { GeneralSettingsForm } from '@/components/settings/general-settings-form';
import { getGuildGeneralSettings } from '@/lib/guild-settings';
interface SettingsPageProps {
params: Promise<{ guildId: string }>;
}
export default async function GuildSettingsPage({ params }: SettingsPageProps) {
const { guildId } = await params;
const settings = await getGuildGeneralSettings(guildId);
return <GeneralSettingsForm guildId={guildId} initialSettings={settings} />;
}

View File

@@ -0,0 +1,19 @@
import { Skeleton } from '@/components/ui/skeleton';
export default function DashboardListLoading() {
return (
<main className="flex min-h-screen justify-center px-6 py-10">
<div className="w-full max-w-[1200px] space-y-6">
<div className="space-y-2">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-4 w-96" />
</div>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{Array.from({ length: 6 }).map((_, index) => (
<Skeleton key={index} className="h-32 rounded-lg" />
))}
</div>
</div>
</main>
);
}

View File

@@ -0,0 +1,93 @@
import type { DashboardGuild } from '@nexumi/shared';
import Link from 'next/link';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { requireAuthOrRedirect } from '@/lib/auth';
import { env } from '@/lib/env';
import { getManageableGuilds } from '@/lib/guilds';
import { getLocale, t } from '@/lib/i18n';
function guildIconUrl(guild: Pick<DashboardGuild, 'id' | 'icon'>): string | undefined {
return guild.icon ? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.png?size=64` : undefined;
}
function initials(name: string): string {
return (
name
.split(' ')
.filter(Boolean)
.slice(0, 2)
.map((part) => part[0]?.toUpperCase())
.join('') || '?'
);
}
function buildInviteUrl(guildId: string): string {
const url = new URL('https://discord.com/api/oauth2/authorize');
url.searchParams.set('client_id', env.BOT_CLIENT_ID);
url.searchParams.set('permissions', '8');
url.searchParams.set('scope', 'bot applications.commands');
url.searchParams.set('guild_id', guildId);
return url.toString();
}
export default async function DashboardGuildListPage() {
const session = await requireAuthOrRedirect();
const guilds = await getManageableGuilds(session);
const locale = await getLocale();
return (
<main className="flex min-h-screen justify-center px-6 py-10">
<div className="w-full max-w-[1200px] space-y-6">
<div>
<h1 className="text-2xl font-semibold">{t(locale, 'dashboard.guildList.title')}</h1>
<p className="text-sm text-muted-foreground">{t(locale, 'dashboard.guildList.subtitle')}</p>
</div>
{guilds.length === 0 ? (
<Card>
<CardContent className="py-10 text-center text-sm text-muted-foreground">
{t(locale, 'dashboard.guildList.empty')}
</CardContent>
</Card>
) : (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{guilds.map((guild) => (
<Card key={guild.id}>
<CardContent className="flex flex-col gap-4 p-5">
<div className="flex items-center gap-3">
<Avatar className="size-10">
<AvatarImage src={guildIconUrl(guild)} alt={guild.name} />
<AvatarFallback>{initials(guild.name)}</AvatarFallback>
</Avatar>
<div className="min-w-0">
<p className="truncate font-medium">{guild.name}</p>
{!guild.botPresent && (
<Badge variant="outline" className="mt-1">
{t(locale, 'dashboard.guildList.botMissing')}
</Badge>
)}
</div>
</div>
{guild.botPresent ? (
<Button asChild>
<Link href={`/dashboard/${guild.id}`}>{t(locale, 'dashboard.guildList.manage')}</Link>
</Button>
) : (
<Button asChild variant="outline">
<a href={buildInviteUrl(guild.id)} target="_blank" rel="noreferrer">
{t(locale, 'dashboard.guildList.invite')}
</a>
</Button>
)}
</CardContent>
</Card>
))}
</div>
)}
</div>
</main>
);
}

View File

@@ -1,33 +1,31 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import type { ReactNode } from 'react';
import { Toaster } from 'sonner';
import { LocaleProvider } from '@/components/locale-provider';
import { ThemeProvider } from '@/components/layout/theme-provider';
import { getLocale, MESSAGES } from '@/lib/i18n';
import './globals.css';
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
const inter = Inter({ subsets: ['latin'], variable: '--font-inter', display: 'swap' });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: 'Nexumi Dashboard',
description: 'Manage your Nexumi Discord bot'
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
export default async function RootLayout({ children }: { children: ReactNode }) {
const locale = await getLocale();
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
<html lang={locale} className={inter.variable} suppressHydrationWarning>
<body className="antialiased">
<ThemeProvider>
<LocaleProvider locale={locale} messages={MESSAGES[locale]}>
{children}
<Toaster richColors position="top-right" />
</LocaleProvider>
</ThemeProvider>
</body>
</html>
);

View File

@@ -0,0 +1,52 @@
import { redirect } from 'next/navigation';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { getLocale, t } from '@/lib/i18n';
import { getSession } from '@/lib/session';
interface LoginPageProps {
searchParams: Promise<{ error?: string }>;
}
const KNOWN_ERRORS = ['access_denied', 'invalid_request', 'invalid_state', 'oauth_failed'] as const;
export default async function LoginPage({ searchParams }: LoginPageProps) {
const session = await getSession();
if (session) {
redirect('/dashboard');
}
const { error } = await searchParams;
const locale = await getLocale();
const errorMessage =
error && (KNOWN_ERRORS as readonly string[]).includes(error)
? t(locale, `login.errors.${error}`)
: undefined;
return (
<main className="flex min-h-screen items-center justify-center px-4">
<Card className="w-full max-w-sm">
<CardHeader className="items-center text-center">
<div className="mb-2 flex size-12 items-center justify-center rounded-xl bg-primary text-lg font-semibold text-primary-foreground">
N
</div>
<CardTitle className="text-xl">{t(locale, 'login.title')}</CardTitle>
<CardDescription>{t(locale, 'login.subtitle')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{errorMessage && (
<p className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
{errorMessage}
</p>
)}
<a
href="/api/auth/login"
className="inline-flex h-10 w-full items-center justify-center gap-2 rounded-md bg-primary text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
{t(locale, 'login.cta')}
</a>
<p className="text-center text-xs text-muted-foreground">{t(locale, 'login.footer')}</p>
</CardContent>
</Card>
</main>
);
}

View File

@@ -1,103 +1,7 @@
import Image from "next/image";
import { redirect } from 'next/navigation';
import { getSession } from '@/lib/session';
export default function Home() {
return (
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
<li className="mb-2 tracking-[-.01em]">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
src/app/page.tsx
</code>
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
</div>
);
export default async function RootPage() {
const session = await getSession();
redirect(session ? '/dashboard' : '/login');
}