From f35b6aee1ef205f48156118d45a7c257af7b1bb6 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Mon, 27 Apr 2026 23:47:24 +0800 Subject: [PATCH] feat: transfer admin check to middleware and add vertical padding --- app/(admin)/layout.tsx | 58 +- app/devices/[slug]/device-detail-client.tsx | 925 +++++++++++--------- app/devices/page-client.tsx | 438 ++++----- lib/routes.ts | 9 +- proxy.ts | 14 +- 5 files changed, 793 insertions(+), 651 deletions(-) diff --git a/app/(admin)/layout.tsx b/app/(admin)/layout.tsx index 14e9756..08ec25b 100644 --- a/app/(admin)/layout.tsx +++ b/app/(admin)/layout.tsx @@ -1,44 +1,36 @@ -import { redirect } from "next/navigation" -import { headers } from "next/headers" -import { auth } from "@/lib/auth" import { AdminSidebar } from "@/components/admin/admin-sidebar" import type { Metadata } from "next" export const metadata: Metadata = { - title: { template: "%s | Admin — DeckyVault", default: "Admin — DeckyVault" }, - robots: { index: false, follow: false }, + title: { + template: "%s | Admin — DeckyVault", + default: "Admin — DeckyVault", + }, + robots: { index: false, follow: false }, } export default async function AdminLayout({ - children, + children, }: { - children: React.ReactNode + children: React.ReactNode }) { - const session = await auth.api.getSession({ - headers: await headers(), - }) + return ( +
+
+
+

Admin

+

+ Manage users, hardware, and games +

+
+
- if (!session || session.user.role !== "admin") { - redirect("/") - } - - return ( -
-
-
-

Admin

-

- Manage users, hardware, and games -

-
-
- -
-
- -
{children}
-
-
-
- ) +
+
+ +
{children}
+
+
+
+ ) } diff --git a/app/devices/[slug]/device-detail-client.tsx b/app/devices/[slug]/device-detail-client.tsx index 9b1df0b..c298874 100644 --- a/app/devices/[slug]/device-detail-client.tsx +++ b/app/devices/[slug]/device-detail-client.tsx @@ -5,442 +5,565 @@ import Link from "next/link" import Image from "next/image" import { motion } from "motion/react" import { - Gamepad2Icon, - TrendingUpIcon, - DatabaseIcon, - CheckCircleIcon, - ArrowRightIcon, - Loader2, - RefreshCwIcon, - MonitorIcon, + Gamepad2Icon, + TrendingUpIcon, + DatabaseIcon, + CheckCircleIcon, + ArrowRightIcon, + Loader2, + RefreshCwIcon, + MonitorIcon, } from "lucide-react" import { - EChartWrapper, - CHART_THEME, - getDeviceColor, + EChartWrapper, + CHART_THEME, + getDeviceColor, } from "@/components/charts/EChartWrapper" import type { EChartsOption } from "echarts" interface DeviceInfo { - slug: string - name: string - deviceType: string - image: string | null - colorIndex: number + slug: string + name: string + deviceType: string + image: string | null + colorIndex: number } interface UpscalerEntry { - upscalerType: string - count: number - avgFps: number + upscalerType: string + count: number + avgFps: number } interface DeviceStats { - slug: string - name: string - deviceType: string - totalBenchmarks: number - avgFps: number | null - verifiedCount: number - gameCount: number - boxplot: Array<{ - gameId: string - gameTitle: string - min: number - q1: number - median: number - q3: number - max: number - count: number - }> - historical: Array<{ - period: string - avgFps: number - count: number - }> - topGames: Array<{ - gameId: string - gameTitle: string - headerImage: string | null - avgFps: number - benchmarkCount: number - }> - genreBreakdown: Array<{ genre: string; count: number }> - protonBreakdown: Array<{ version: string; count: number }> - upscalerBreakdown: UpscalerEntry[] + slug: string + name: string + deviceType: string + totalBenchmarks: number + avgFps: number | null + verifiedCount: number + gameCount: number + boxplot: Array<{ + gameId: string + gameTitle: string + min: number + q1: number + median: number + q3: number + max: number + count: number + }> + historical: Array<{ + period: string + avgFps: number + count: number + }> + topGames: Array<{ + gameId: string + gameTitle: string + headerImage: string | null + avgFps: number + benchmarkCount: number + }> + genreBreakdown: Array<{ genre: string; count: number }> + protonBreakdown: Array<{ version: string; count: number }> + upscalerBreakdown: UpscalerEntry[] } const deviceTypeLabel: Record = { - handheld: "Handheld", - console: "Console", + handheld: "Handheld", + console: "Console", } const deviceTypeColor: Record = { - handheld: "text-primary bg-primary/10 border-primary/20", - console: "text-secondary bg-secondary/10 border-secondary/20", + handheld: "text-primary bg-primary/10 border-primary/20", + console: "text-secondary bg-secondary/10 border-secondary/20", } export function DeviceDetailClient({ device }: { device: DeviceInfo }) { - const [stats, setStats] = useState(null) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) + const [stats, setStats] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) - const deviceColor = getDeviceColor(device.colorIndex) + const deviceColor = getDeviceColor(device.colorIndex) - async function fetchStats() { - setLoading(true) - setError(null) - try { - const res = await fetch(`/api/hardware/${device.slug}/stats`) - if (!res.ok) throw new Error(`HTTP ${res.status}`) - const data = await res.json() - setStats(data) - } catch (err) { - console.error("Failed to fetch device stats:", err) - setError("Failed to load device statistics. Please try again.") - } finally { - setLoading(false) + async function fetchStats() { + setLoading(true) + setError(null) + try { + const res = await fetch(`/api/hardware/${device.slug}/stats`) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + const data = await res.json() + setStats(data) + } catch (err) { + console.error("Failed to fetch device stats:", err) + setError("Failed to load device statistics. Please try again.") + } finally { + setLoading(false) + } } - } - useEffect(() => { - // eslint-disable-next-line react-hooks/set-state-in-effect - fetchStats() - }, [device.slug]) // eslint-disable-line react-hooks/exhaustive-deps + useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect + fetchStats() + }, [device.slug]) // eslint-disable-line react-hooks/exhaustive-deps - const historicalOption = useMemo(() => { - if (!stats || stats.historical.length === 0) return {} - return { - tooltip: { - trigger: "axis", - backgroundColor: "#1a1020", - borderColor: CHART_THEME.border, - textStyle: { color: CHART_THEME.text }, - }, - grid: { left: 50, right: 20, top: 10, bottom: 30 }, - xAxis: { - type: "category", - data: stats.historical.map((h) => h.period), - axisLine: { lineStyle: { color: CHART_THEME.border } }, - axisLabel: { color: CHART_THEME.textMuted, fontSize: 11 }, - }, - yAxis: { - type: "value", - axisLine: { lineStyle: { color: CHART_THEME.border } }, - splitLine: { lineStyle: { color: CHART_THEME.border, opacity: 0.3 } }, - axisLabel: { color: CHART_THEME.textMuted, fontSize: 11 }, - }, - series: [ - { - type: "line", - data: stats.historical.map((h) => h.avgFps), - smooth: true, - lineStyle: { color: deviceColor, width: 2 }, - areaStyle: { - color: { - type: "linear", - x: 0, y: 0, x2: 0, y2: 1, - colorStops: [ - { offset: 0, color: deviceColor + "40" }, - { offset: 1, color: deviceColor + "05" }, - ], + const historicalOption = useMemo(() => { + if (!stats || stats.historical.length === 0) return {} + return { + tooltip: { + trigger: "axis", + backgroundColor: "#1a1020", + borderColor: CHART_THEME.border, + textStyle: { color: CHART_THEME.text }, }, - }, - symbol: "circle", - symbolSize: 4, - itemStyle: { color: deviceColor }, - }, - ], - } - }, [stats, deviceColor]) - - const boxplotOption = useMemo(() => { - if (!stats || stats.boxplot.length === 0) return {} - return { - tooltip: { - trigger: "item", - backgroundColor: "#1a1020", - borderColor: CHART_THEME.border, - textStyle: { color: CHART_THEME.text }, - }, - grid: { left: 80, right: 20, top: 10, bottom: 40 }, - xAxis: { - type: "category", - data: stats.boxplot.map((b) => b.gameTitle), - axisLine: { lineStyle: { color: CHART_THEME.border } }, - axisLabel: { color: CHART_THEME.textMuted, fontSize: 10, rotate: 30 }, - }, - yAxis: { - type: "value", - name: "FPS", - axisLine: { lineStyle: { color: CHART_THEME.border } }, - splitLine: { lineStyle: { color: CHART_THEME.border, opacity: 0.3 } }, - axisLabel: { color: CHART_THEME.textMuted, fontSize: 11 }, - nameTextStyle: { color: CHART_THEME.textMuted, fontSize: 11 }, - }, - series: [ - { - type: "boxplot", - data: stats.boxplot.map((b) => [b.min, b.q1, b.median, b.q3, b.max]), - itemStyle: { color: deviceColor + "30", borderColor: deviceColor }, - }, - ], - } - }, [stats, deviceColor]) - - const genreOption = useMemo(() => { - if (!stats || stats.genreBreakdown.length === 0) return {} - return { - tooltip: { - trigger: "item", - backgroundColor: "#1a1025", - borderColor: CHART_THEME.border, - textStyle: { color: CHART_THEME.text }, - }, - series: [ - { - type: "pie", - radius: ["40%", "70%"], - center: ["50%", "50%"], - data: stats.genreBreakdown.map((g, i) => ({ - name: g.genre, - value: g.count, - itemStyle: { - color: CHART_THEME.deviceColors[i % CHART_THEME.deviceColors.length], + grid: { left: 50, right: 20, top: 10, bottom: 30 }, + xAxis: { + type: "category", + data: stats.historical.map((h) => h.period), + axisLine: { lineStyle: { color: CHART_THEME.border } }, + axisLabel: { color: CHART_THEME.textMuted, fontSize: 11 }, }, - })), - label: { color: CHART_THEME.textMuted, fontSize: 10 }, - emphasis: { - itemStyle: { shadowBlur: 10, shadowColor: "rgba(0,0,0,0.5)" }, - }, - }, - ], - } - }, [stats]) + yAxis: { + type: "value", + axisLine: { lineStyle: { color: CHART_THEME.border } }, + splitLine: { + lineStyle: { color: CHART_THEME.border, opacity: 0.3 }, + }, + axisLabel: { color: CHART_THEME.textMuted, fontSize: 11 }, + }, + series: [ + { + type: "line", + data: stats.historical.map((h) => h.avgFps), + smooth: true, + lineStyle: { color: deviceColor, width: 2 }, + areaStyle: { + color: { + type: "linear", + x: 0, + y: 0, + x2: 0, + y2: 1, + colorStops: [ + { offset: 0, color: deviceColor + "40" }, + { offset: 1, color: deviceColor + "05" }, + ], + }, + }, + symbol: "circle", + symbolSize: 4, + itemStyle: { color: deviceColor }, + }, + ], + } + }, [stats, deviceColor]) - return ( -
- {/* Hero Header */} - -
-
-
(() => { + if (!stats || stats.boxplot.length === 0) return {} + return { + tooltip: { + trigger: "item", + backgroundColor: "#1a1020", + borderColor: CHART_THEME.border, + textStyle: { color: CHART_THEME.text }, + }, + grid: { left: 80, right: 20, top: 10, bottom: 40 }, + xAxis: { + type: "category", + data: stats.boxplot.map((b) => b.gameTitle), + axisLine: { lineStyle: { color: CHART_THEME.border } }, + axisLabel: { + color: CHART_THEME.textMuted, + fontSize: 10, + rotate: 30, + }, + }, + yAxis: { + type: "value", + name: "FPS", + axisLine: { lineStyle: { color: CHART_THEME.border } }, + splitLine: { + lineStyle: { color: CHART_THEME.border, opacity: 0.3 }, + }, + axisLabel: { color: CHART_THEME.textMuted, fontSize: 11 }, + nameTextStyle: { color: CHART_THEME.textMuted, fontSize: 11 }, + }, + series: [ + { + type: "boxplot", + data: stats.boxplot.map((b) => [ + b.min, + b.q1, + b.median, + b.q3, + b.max, + ]), + itemStyle: { + color: deviceColor + "30", + borderColor: deviceColor, + }, + }, + ], + } + }, [stats, deviceColor]) + + const genreOption = useMemo(() => { + if (!stats || stats.genreBreakdown.length === 0) return {} + return { + tooltip: { + trigger: "item", + backgroundColor: "#1a1025", + borderColor: CHART_THEME.border, + textStyle: { color: CHART_THEME.text }, + }, + series: [ + { + type: "pie", + radius: ["40%", "70%"], + center: ["50%", "50%"], + data: stats.genreBreakdown.map((g, i) => ({ + name: g.genre, + value: g.count, + itemStyle: { + color: CHART_THEME.deviceColors[ + i % CHART_THEME.deviceColors.length + ], + }, + })), + label: { color: CHART_THEME.textMuted, fontSize: 10 }, + emphasis: { + itemStyle: { + shadowBlur: 10, + shadowColor: "rgba(0,0,0,0.5)", + }, + }, + }, + ], + } + }, [stats]) + + return ( +
+ {/* Hero Header */} + - {device.image ? ( - {device.name} - ) : ( - - )} -
-
-

{device.name}

-
- - - {deviceTypeLabel[device.deviceType] || device.deviceType} - - {stats && stats.verifiedCount > 0 && ( - - - {stats.verifiedCount} verified - - )} -
-
-
-

Performance benchmarks and statistics

-
-
- - {/* Overview Stats */} - {stats && !loading && ( - -
- - - - -
-
- )} - - {/* Loading */} - {loading && ( -
- -
- )} - - {/* Error State */} - {error && !loading && ( -
-
- -

{error}

- -
-
- )} - - {/* Charts */} - {stats && !loading && !error && ( - -
- {stats.historical.length > 0 && ( -
-

Historical Performance

- -
- )} - -
- {stats.boxplot.length > 0 && ( -
-

FPS Distribution by Game

- -
- )} - {stats.genreBreakdown.length > 0 && ( -
-

Genre Breakdown

- -
- )} -
- - {(stats.protonBreakdown.length > 0 || stats.upscalerBreakdown.length > 0) && ( -
- {stats.protonBreakdown.length > 0 && ( -
-

Proton Version Distribution

-
- {stats.protonBreakdown.map((p) => ( -
- {p.version} -
-
-
+
+
+
+ {device.image ? ( + {device.name} + ) : ( + + )} +
+
+

+ {device.name} +

+
+ + + {deviceTypeLabel[device.deviceType] || + device.deviceType} + + {stats && stats.verifiedCount > 0 && ( + + + {stats.verifiedCount} verified + + )}
- {p.count} -
- ))}
-
- )} - {stats.upscalerBreakdown.length > 0 && ( -
-

Upscaler Performance

-
- {stats.upscalerBreakdown.map((f) => ( -
- - {f.upscalerType === "none" ? "Native" : f.upscalerType.toUpperCase()} - -
- {f.avgFps} FPS - ({f.count}) -
-
- ))} -
-
- )} -
- )} - - {stats.topGames.length > 0 && ( -
-

Top Games by Average FPS

-
- {stats.topGames.slice(0, 8).map((game, i) => ( - -
- {i + 1} -
-
-

- {game.gameTitle} -

-
- {game.avgFps} FPS - {game.benchmarkCount} runs -
-
- - - ))} +

+ Performance benchmarks and statistics +

-
+ + + {/* Overview Stats */} + {stats && !loading && ( + +
+ + + + +
+
)} -
- - )} - {/* Empty state */} - {stats && !loading && !error && stats.totalBenchmarks === 0 && ( -
-
- -

No benchmark data yet for this device

-

Data will appear as benchmarks are submitted

-
+ {/* Loading */} + {loading && ( +
+ +
+ )} + + {/* Error State */} + {error && !loading && ( +
+
+ +

{error}

+ +
+
+ )} + + {/* Charts */} + {stats && !loading && !error && ( + +
+ {stats.historical.length > 0 && ( +
+

+ Historical Performance +

+ +
+ )} + +
+ {stats.boxplot.length > 0 && ( +
+

+ FPS Distribution by Game +

+ +
+ )} + {stats.genreBreakdown.length > 0 && ( +
+

+ Genre Breakdown +

+ +
+ )} +
+ + {(stats.protonBreakdown.length > 0 || + stats.upscalerBreakdown.length > 0) && ( +
+ {stats.protonBreakdown.length > 0 && ( +
+

+ Proton Version Distribution +

+
+ {stats.protonBreakdown.map((p) => ( +
+ + {p.version} + +
+
+
+
+ + {p.count} + +
+
+ ))} +
+
+ )} + {stats.upscalerBreakdown.length > 0 && ( +
+

+ Upscaler Performance +

+
+ {stats.upscalerBreakdown.map( + (f) => ( +
+ + {f.upscalerType === + "none" + ? "Native" + : f.upscalerType.toUpperCase()} + +
+ + {f.avgFps} FPS + + + ({f.count}) + +
+
+ ), + )} +
+
+ )} +
+ )} + + {stats.topGames.length > 0 && ( +
+

+ Top Games by Average FPS +

+
+ {stats.topGames + .slice(0, 8) + .map((game, i) => ( + +
+ {i + 1} +
+
+

+ {game.gameTitle} +

+
+ + {game.avgFps} FPS + + + { + game.benchmarkCount + }{" "} + runs + +
+
+ + + ))} +
+
+ )} +
+ + )} + + {/* Empty state */} + {stats && !loading && !error && stats.totalBenchmarks === 0 && ( +
+
+ +

No benchmark data yet for this device

+

+ Data will appear as benchmarks are submitted +

+
+
+ )} +
+ ) +} + +function StatCard({ + icon: Icon, + label, + value, +}: { + icon: React.ElementType + label: string + value: string +}) { + return ( +
+
+ +
+
+ {label} + + {value} + +
- )} - - ) -} - -function StatCard({ icon: Icon, label, value }: { icon: React.ElementType; label: string; value: string }) { - return ( -
-
- -
-
- {label} - {value} -
-
- ) + ) } diff --git a/app/devices/page-client.tsx b/app/devices/page-client.tsx index 9d02252..2708308 100644 --- a/app/devices/page-client.tsx +++ b/app/devices/page-client.tsx @@ -5,241 +5,257 @@ import Link from "next/link" import Image from "next/image" import { motion } from "motion/react" import { - Gamepad2Icon, - TrendingUpIcon, - DatabaseIcon, - ArrowRightIcon, - MonitorIcon, - CheckCircleIcon, + Gamepad2Icon, + TrendingUpIcon, + DatabaseIcon, + ArrowRightIcon, + MonitorIcon, + CheckCircleIcon, } from "lucide-react" import { getDeviceColor } from "@/components/charts/EChartWrapper" export interface DeviceStats { - slug: string - name: string - deviceType: string - image: string | null - sortOrder: number - colorIndex: number - totalBenchmarks: number - avgFps: number | null - gameCount: number - verifiedCount: number - bestGame: { - id: string - title: string - headerImage: string | null - fpsAvg: number - } | null + slug: string + name: string + deviceType: string + image: string | null + sortOrder: number + colorIndex: number + totalBenchmarks: number + avgFps: number | null + gameCount: number + verifiedCount: number + bestGame: { + id: string + title: string + headerImage: string | null + fpsAvg: number + } | null } const deviceTypeLabel: Record = { - handheld: "Handheld", - console: "Console", + handheld: "Handheld", + console: "Console", } const deviceTypeColor: Record = { - handheld: "text-primary bg-primary/10 border-primary/20", - console: "text-secondary bg-secondary/10 border-secondary/20", + handheld: "text-primary bg-primary/10 border-primary/20", + console: "text-secondary bg-secondary/10 border-secondary/20", } type FilterType = "all" | "handheld" | "console" const filterOptions: { id: FilterType; label: string }[] = [ - { id: "all", label: "All" }, - { id: "handheld", label: "Handheld" }, - { id: "console", label: "Console" }, + { id: "all", label: "All" }, + { id: "handheld", label: "Handheld" }, + { id: "console", label: "Console" }, ] export function DevicesPageClient({ devices }: { devices: DeviceStats[] }) { - const [activeFilter, setActiveFilter] = useState("all") + const [activeFilter, setActiveFilter] = useState("all") - const filteredDevices = - activeFilter === "all" - ? devices - : devices.filter((d) => d.deviceType === activeFilter) + const filteredDevices = + activeFilter === "all" + ? devices + : devices.filter((d) => d.deviceType === activeFilter) + + if (devices.length === 0) { + return ( +
+
+ +

No devices found

+

+ Benchmark data will appear as devices are added +

+
+
+ ) + } - if (devices.length === 0) { return ( -
-
- -

No devices found

-

- Benchmark data will appear as devices are added -

-
-
- ) - } - - return ( -
- {/* Hero Header */} - -
-

Devices

-

- Browse benchmark data for handheld and console devices -

-
-
- - {/* Filter Tabs */} - -
-
- {filterOptions.map((opt) => ( - - ))} -
-
-
- - {/* Device Grid */} - -
- {filteredDevices.map((device, i) => ( +
+ {/* Hero Header */} - - {/* Image / Icon Header */} -
- {device.image ? ( - {device.name} - ) : ( - - )} +
+

Devices

+

+ Browse benchmark data for handheld and console devices +

- - {/* Card Body */} -
- {/* Name & Type */} -
-
-

- {device.name} -

- - - {deviceTypeLabel[device.deviceType] || device.deviceType} - -
- -
- - {/* Stats Row */} -
-
- - - {device.totalBenchmarks} - - Benchmarks -
-
- - - {device.avgFps !== null ? device.avgFps : "—"} - - Avg FPS -
-
- - - {device.gameCount} - - Games -
-
- - - {device.verifiedCount} - - Verified -
-
- - {/* Best game */} - {device.bestGame && ( -
- Top:{" "} - - {device.bestGame.title} - {" "} - · {device.bestGame.fpsAvg} FPS -
- )} -
- - ))} -
- {filteredDevices.length === 0 && devices.length > 0 && ( -
- -

No {activeFilter} devices found

-
- )} -
-
- ) + {/* Filter Tabs */} + +
+
+ {filterOptions.map((opt) => ( + + ))} +
+
+
+ + {/* Device Grid */} + +
+ {filteredDevices.map((device, i) => ( + + + {/* Image / Icon Header */} +
+ {device.image ? ( + {device.name} + ) : ( + + )} +
+ + {/* Card Body */} +
+ {/* Name & Type */} +
+
+

+ {device.name} +

+ + + {deviceTypeLabel[ + device.deviceType + ] || device.deviceType} + +
+ +
+ + {/* Stats Row */} +
+
+ + + {device.totalBenchmarks} + + + Benchmarks + +
+
+ + + {device.avgFps !== null + ? device.avgFps + : "—"} + + + Avg FPS + +
+
+ + + {device.gameCount} + + + Games + +
+
+ + + {device.verifiedCount} + + + Verified + +
+
+ + {/* Best game */} + {device.bestGame && ( +
+ Top:{" "} + + {device.bestGame.title} + {" "} + · {device.bestGame.fpsAvg} FPS +
+ )} +
+ +
+ ))} +
+ + {filteredDevices.length === 0 && devices.length > 0 && ( +
+ +

No {activeFilter} devices found

+
+ )} +
+
+ ) } diff --git a/lib/routes.ts b/lib/routes.ts index 24a26e7..546e40f 100644 --- a/lib/routes.ts +++ b/lib/routes.ts @@ -7,11 +7,10 @@ export const routes = [ title: "Search", href: "/search", }, - // TODO: Add back when games page is implemented - // { - // title: "Games", - // href: "/games", - // }, + { + title: "Games", + href: "/games", + }, // TODO: Add back when compare page is implemented // { // title: "Compare", diff --git a/proxy.ts b/proxy.ts index 2279c5b..407cf6a 100644 --- a/proxy.ts +++ b/proxy.ts @@ -9,11 +9,14 @@ const authRoutes = [ "/reset-password", ] +const adminRoutes = ["/admin"] + export async function proxy(req: NextRequest) { const path = req.nextUrl.pathname const isAuthRoute = authRoutes.some((route) => path.startsWith(route)) + const isAdminRoute = adminRoutes.some((route) => path.startsWith(route)) - if (!isAuthRoute) { + if (!isAuthRoute && !isAdminRoute) { return NextResponse.next() } @@ -33,6 +36,15 @@ export async function proxy(req: NextRequest) { headers: req.headers, }) + // Admin routes: require an admin session + if (isAdminRoute) { + if (!session || session.user.role !== "admin") { + return NextResponse.redirect(new URL("/", req.url)) + } + return NextResponse.next() + } + + // Auth routes: redirect authenticated users away if (session) { return NextResponse.redirect(new URL("/", req.url)) }