Skip to content

Commit 5259f82

Browse files
committed
fix(app): isolate route providers from root layout
- Move intl, theme, auth, and toast providers out of the root layout into route-scoped wrappers. - Split the admin layout into a server wrapper and client shell, and add explicit not-found/global-error pages for stable prerendering.
1 parent e0b62f8 commit 5259f82

File tree

9 files changed

+154
-74
lines changed

9 files changed

+154
-74
lines changed

web/app/(main)/layout.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { ReactNode } from "react";
2+
import RouteProviders from "@/app/route-providers";
3+
4+
export default function MainLayout({ children }: { children: ReactNode }) {
5+
return <RouteProviders>{children}</RouteProviders>;
6+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"use client";
2+
3+
import React, { useEffect } from "react";
4+
import { useRouter } from "next/navigation";
5+
import { useAuth } from "@/contexts/auth-context";
6+
import { AdminSidebar } from "@/components/admin/admin-sidebar";
7+
import { useTranslations } from "@/hooks/use-translations";
8+
import {
9+
SidebarInset,
10+
SidebarProvider,
11+
SidebarTrigger,
12+
} from "@/components/animate-ui/components/radix/sidebar";
13+
import { Separator } from "@/components/ui/separator";
14+
import { Loader2 } from "lucide-react";
15+
16+
export default function AdminLayoutClient({
17+
children,
18+
}: {
19+
children: React.ReactNode;
20+
}) {
21+
const router = useRouter();
22+
const { user, isLoading } = useAuth();
23+
const t = useTranslations();
24+
25+
const isAdmin = user?.roles?.includes("Admin") ?? false;
26+
27+
useEffect(() => {
28+
if (!isLoading && !isAdmin) {
29+
router.push("/");
30+
}
31+
}, [isAdmin, isLoading, router]);
32+
33+
if (isLoading) {
34+
return (
35+
<div className="flex h-screen items-center justify-center">
36+
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
37+
</div>
38+
);
39+
}
40+
41+
if (!isAdmin) {
42+
return null;
43+
}
44+
45+
return (
46+
<SidebarProvider defaultOpen={true}>
47+
<AdminSidebar />
48+
<SidebarInset>
49+
<header className="sticky top-0 z-10 flex h-14 shrink-0 items-center gap-2 border-b bg-background/95 px-4 backdrop-blur supports-[backdrop-filter]:bg-background/60">
50+
<SidebarTrigger className="-ml-1" />
51+
<Separator orientation="vertical" className="mr-2 h-4" />
52+
<h1 className="text-sm font-semibold">{t("common.adminPanel")}</h1>
53+
</header>
54+
<main className="flex-1 overflow-auto">
55+
<div className="container mx-auto p-6">
56+
{children}
57+
</div>
58+
</main>
59+
</SidebarInset>
60+
</SidebarProvider>
61+
);
62+
}

web/app/admin/layout.tsx

Lines changed: 8 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,17 @@
1-
"use client";
1+
import type { ReactNode } from "react";
2+
import RouteProviders from "@/app/route-providers";
3+
import AdminLayoutClient from "./admin-layout-client";
24

3-
import React, { useEffect } from "react";
4-
import { useRouter } from "next/navigation";
5-
import { useAuth } from "@/contexts/auth-context";
6-
import { AdminSidebar } from "@/components/admin/admin-sidebar";
7-
import { useTranslations } from "@/hooks/use-translations";
8-
import {
9-
SidebarInset,
10-
SidebarProvider,
11-
SidebarTrigger,
12-
} from "@/components/animate-ui/components/radix/sidebar";
13-
import { Separator } from "@/components/ui/separator";
14-
import { Loader2 } from "lucide-react";
5+
export const dynamic = "force-dynamic";
156

167
export default function AdminLayout({
178
children,
189
}: {
19-
children: React.ReactNode;
10+
children: ReactNode;
2011
}) {
21-
const router = useRouter();
22-
const { user, isLoading } = useAuth();
23-
const t = useTranslations();
24-
25-
const isAdmin = user?.roles?.includes("Admin") ?? false;
26-
27-
useEffect(() => {
28-
if (!isLoading && !isAdmin) {
29-
router.push("/");
30-
}
31-
}, [isAdmin, isLoading, router]);
32-
33-
if (isLoading) {
34-
return (
35-
<div className="flex h-screen items-center justify-center">
36-
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
37-
</div>
38-
);
39-
}
40-
41-
if (!isAdmin) {
42-
return null;
43-
}
44-
4512
return (
46-
<SidebarProvider defaultOpen={true}>
47-
<AdminSidebar />
48-
<SidebarInset>
49-
<header className="sticky top-0 z-10 flex h-14 shrink-0 items-center gap-2 border-b bg-background/95 px-4 backdrop-blur supports-[backdrop-filter]:bg-background/60">
50-
<SidebarTrigger className="-ml-1" />
51-
<Separator orientation="vertical" className="mr-2 h-4" />
52-
<h1 className="text-sm font-semibold">{t("common.adminPanel")}</h1>
53-
</header>
54-
<main className="flex-1 overflow-auto">
55-
<div className="container mx-auto p-6">
56-
{children}
57-
</div>
58-
</main>
59-
</SidebarInset>
60-
</SidebarProvider>
13+
<RouteProviders>
14+
<AdminLayoutClient>{children}</AdminLayoutClient>
15+
</RouteProviders>
6116
);
6217
}

web/app/auth/layout.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { ReactNode } from "react";
2+
import RouteProviders from "@/app/route-providers";
3+
4+
export default function AuthLayout({ children }: { children: ReactNode }) {
5+
return <RouteProviders>{children}</RouteProviders>;
6+
}

web/app/client-providers.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"use client";
2+
3+
import type { ReactNode } from "react";
4+
import { ThemeProvider } from "next-themes";
5+
import { AuthProvider } from "@/contexts/auth-context";
6+
import { Toaster } from "@/components/ui/sonner";
7+
8+
export default function ClientProviders({ children }: { children: ReactNode }) {
9+
return (
10+
<ThemeProvider
11+
attribute="class"
12+
defaultTheme="system"
13+
enableSystem
14+
disableTransitionOnChange
15+
>
16+
<AuthProvider>
17+
{children}
18+
<Toaster />
19+
</AuthProvider>
20+
</ThemeProvider>
21+
);
22+
}

web/app/global-error.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"use client";
2+
3+
export default function GlobalError() {
4+
return (
5+
<main className="flex min-h-screen flex-col items-center justify-center px-6 text-center">
6+
<h1 className="text-3xl font-semibold">页面加载失败</h1>
7+
<p className="mt-3 text-sm text-muted-foreground">
8+
发生了未预期的错误,请稍后重试。
9+
</p>
10+
<a
11+
href="/"
12+
className="mt-6 inline-flex h-10 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
13+
>
14+
返回首页
15+
</a>
16+
</main>
17+
);
18+
}

web/app/layout.tsx

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
import type { Metadata } from "next";
22
import { Geist, Geist_Mono } from "next/font/google";
33
import "./globals.css";
4-
import { ThemeProvider } from "next-themes";
5-
import { NextIntlClientProvider } from 'next-intl';
6-
import { getMessages } from 'next-intl/server';
7-
import { AuthProvider } from "@/contexts/auth-context";
8-
import { Toaster } from "@/components/ui/sonner";
94

105
const geistSans = Geist({
116
variable: "--font-geist-sans",
@@ -25,31 +20,17 @@ export const metadata: Metadata = {
2520
},
2621
};
2722

28-
export default async function RootLayout({
23+
export default function RootLayout({
2924
children,
3025
}: Readonly<{
3126
children: React.ReactNode;
3227
}>) {
33-
const messages = await getMessages();
34-
3528
return (
3629
<html lang="en" suppressHydrationWarning>
3730
<body
3831
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
3932
>
40-
<NextIntlClientProvider messages={messages}>
41-
<ThemeProvider
42-
attribute="class"
43-
defaultTheme="system"
44-
enableSystem
45-
disableTransitionOnChange
46-
>
47-
<AuthProvider>
48-
{children}
49-
<Toaster />
50-
</AuthProvider>
51-
</ThemeProvider>
52-
</NextIntlClientProvider>
33+
{children}
5334
</body>
5435
</html>
5536
);

web/app/not-found.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export default function NotFound() {
2+
return (
3+
<main className="flex min-h-[60vh] flex-col items-center justify-center px-6 text-center">
4+
<h1 className="text-3xl font-semibold">页面不存在</h1>
5+
<p className="mt-3 text-sm text-muted-foreground">
6+
你访问的页面不存在或已被移动。
7+
</p>
8+
<a
9+
href="/"
10+
className="mt-6 inline-flex h-10 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
11+
>
12+
返回首页
13+
</a>
14+
</main>
15+
);
16+
}

web/app/route-providers.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { ReactNode } from "react";
2+
import { NextIntlClientProvider } from "next-intl";
3+
import { getLocale, getMessages } from "next-intl/server";
4+
import ClientProviders from "@/app/client-providers";
5+
6+
export default async function RouteProviders({ children }: { children: ReactNode }) {
7+
const [messages, locale] = await Promise.all([getMessages(), getLocale()]);
8+
9+
return (
10+
<NextIntlClientProvider locale={locale} messages={messages}>
11+
<ClientProviders>{children}</ClientProviders>
12+
</NextIntlClientProvider>
13+
);
14+
}

0 commit comments

Comments
 (0)