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:
SpecialX
2026-06-22 14:04:55 +08:00
parent a4d096a6fc
commit c90748124d
25 changed files with 2911 additions and 30 deletions

View 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>
)
}