feat: introduce i18n system and class invitation codes
Add complete i18n infrastructure using next-intl (cookie-driven, without i18n routing) with zh-CN/en dictionary files, locale switcher, and NextIntlClientProvider in root layout. Add class invitation code system with new class_invitation_codes table, data-access layer (generate/validate/consume/revoke), server actions with permission checks, rate limiting, and audit logging. Add class-invitation-manager UI component. Refactor onboarding stepper to use i18n translations and accept new invitation code format (6-char alphanumeric) with backward compatibility for legacy 6-digit codes.
This commit is contained in:
70
src/app/(onboarding)/onboarding/page.tsx
Normal file
70
src/app/(onboarding)/onboarding/page.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Suspense } from "react"
|
||||
import { redirect } from "next/navigation"
|
||||
import type { Metadata } from "next"
|
||||
import { getTranslations } from "next-intl/server"
|
||||
|
||||
import { auth } from "@/auth"
|
||||
import { getOnboardingStatus } from "@/modules/onboarding/data-access"
|
||||
import { OnboardingStepper } from "@/modules/onboarding/components/onboarding-stepper"
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const t = await getTranslations("onboarding")
|
||||
return {
|
||||
title: t("title"),
|
||||
description: t("description"),
|
||||
}
|
||||
}
|
||||
|
||||
export default async function OnboardingPage() {
|
||||
const session = await auth()
|
||||
const userId = session?.user?.id
|
||||
|
||||
if (!userId) {
|
||||
redirect("/login")
|
||||
}
|
||||
|
||||
// 已完成 onboarding 的用户不应停留在此页
|
||||
if (session.user.onboarded) {
|
||||
redirect("/dashboard")
|
||||
}
|
||||
|
||||
const status = await getOnboardingStatus(userId)
|
||||
|
||||
// 二次校验:DB 层面已 onboarded 但 session 未刷新
|
||||
if (!status.required) {
|
||||
redirect("/dashboard")
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="flex min-h-screen items-center justify-center bg-muted/30 p-4">
|
||||
<div className="w-full max-w-2xl rounded-lg border bg-background p-8 shadow-sm">
|
||||
{/* useSearchParams 需要 Suspense 边界(P1-1 URL query 持久化步骤) */}
|
||||
<Suspense fallback={<OnboardingLoading />}>
|
||||
<OnboardingStepper initialStatus={status} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
function OnboardingLoading() {
|
||||
return (
|
||||
<div className="grid gap-6" aria-busy="true" aria-live="polite">
|
||||
<div className="grid gap-1.5">
|
||||
<div className="h-7 w-40 animate-pulse rounded bg-muted" />
|
||||
<div className="h-4 w-64 animate-pulse rounded bg-muted" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-1 flex-1 rounded bg-muted" />
|
||||
<div className="h-1 flex-1 rounded bg-muted" />
|
||||
<div className="h-1 flex-1 rounded bg-muted" />
|
||||
<div className="h-1 flex-1 rounded bg-muted" />
|
||||
</div>
|
||||
<div className="grid gap-4">
|
||||
<div className="h-10 animate-pulse rounded bg-muted" />
|
||||
<div className="h-10 animate-pulse rounded bg-muted" />
|
||||
<div className="h-10 animate-pulse rounded bg-muted" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { getLocale, getMessages } from "next-intl/server";
|
||||
|
||||
import { ThemeProvider } from "@/shared/components/theme-provider";
|
||||
import { Toaster } from "@/shared/components/ui/sonner";
|
||||
import { NuqsAdapter } from 'nuqs/adapters/next/app'
|
||||
import { AuthSessionProvider } from "@/shared/components/auth-session-provider"
|
||||
import { OnboardingGate } from "@/shared/components/onboarding-gate"
|
||||
import { NuqsAdapter } from "nuqs/adapters/next/app";
|
||||
import { AuthSessionProvider } from "@/shared/components/auth-session-provider";
|
||||
import "./globals.css";
|
||||
|
||||
const inter = Inter({
|
||||
@@ -18,31 +20,34 @@ export const metadata: Metadata = {
|
||||
description: "Enterprise Grade K12 Education Management System",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
// v3 i18n:从 cookie 读取 locale(getRequestConfig 已配置),SSR 注入字典
|
||||
const locale = await getLocale();
|
||||
const messages = await getMessages();
|
||||
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<html lang={locale} suppressHydrationWarning>
|
||||
<body
|
||||
className={`${inter.variable} antialiased font-sans`}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<ThemeProvider
|
||||
<NextIntlClientProvider locale={locale} messages={messages}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<AuthSessionProvider>
|
||||
<NuqsAdapter>
|
||||
{children}
|
||||
<OnboardingGate />
|
||||
</NuqsAdapter>
|
||||
</AuthSessionProvider>
|
||||
<Toaster />
|
||||
</ThemeProvider>
|
||||
<AuthSessionProvider>
|
||||
<NuqsAdapter>{children}</NuqsAdapter>
|
||||
</AuthSessionProvider>
|
||||
<Toaster />
|
||||
</ThemeProvider>
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user