feat(app): add lesson-plans, practice, and grade dashboard routes

- Add admin/lesson-plans, parent/lesson-plans, student/lesson-plans routes

- Add student/practice and teacher/practice routes for adaptive practice

- Add management/grade/dashboard and management/grade/practice routes

- Add teacher/lesson-plans error and loading boundaries

- Update existing admin, parent, student, teacher pages with new features

- Update globals.css and proxy middleware
This commit is contained in:
SpecialX
2026-06-24 12:03:47 +08:00
parent 8c2fe14c20
commit 37d2688a28
84 changed files with 2665 additions and 661 deletions

View File

@@ -0,0 +1,32 @@
"use client"
import { useEffect } from "react"
import Link from "next/link"
import { useTranslations } from "next-intl"
import { Button } from "@/shared/components/ui/button"
export default function PracticeSessionError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}): React.ReactNode {
const t = useTranslations("student")
useEffect(() => {
console.error("Practice session error:", error)
}, [error])
return (
<div className="flex h-full flex-col items-center justify-center space-y-4 p-8">
<h2 className="text-xl font-semibold">{t("error.title")}</h2>
<p className="text-sm text-muted-foreground">{error.message}</p>
<div className="flex gap-2">
<Button onClick={reset}>{t("error.retry")}</Button>
<Button variant="outline" asChild>
<Link href="/student/practice">{t("classDetail.backToCourses")}</Link>
</Button>
</div>
</div>
)
}

View File

@@ -0,0 +1,14 @@
import { Skeleton } from "@/shared/components/ui/skeleton"
export default function PracticeSessionLoading(): React.ReactNode {
return (
<div className="flex h-full flex-col space-y-6 p-8">
<Skeleton className="h-20 w-full rounded-md" />
<Skeleton className="h-96 w-full rounded-md" />
<div className="flex justify-between">
<Skeleton className="h-10 w-24 rounded-md" />
<Skeleton className="h-10 w-24 rounded-md" />
</div>
</div>
)
}

View File

@@ -0,0 +1,31 @@
import type { JSX } from "react"
import { notFound } from "next/navigation"
import { requirePermission } from "@/shared/lib/auth-guard"
import { Permissions } from "@/shared/types/permissions"
import { getPracticeSessionById } from "@/modules/adaptive-practice/data-access"
import { PracticeSessionView } from "@/modules/adaptive-practice/components/practice-session-view"
export const dynamic = "force-dynamic"
export default async function PracticeSessionPage({
params,
}: {
params: Promise<{ sessionId: string }>
}): Promise<JSX.Element> {
const ctx = await requirePermission(Permissions.ADAPTIVE_PRACTICE_READ)
const { sessionId } = await params
const session = await getPracticeSessionById(sessionId, ctx.userId)
if (!session) {
notFound()
}
return (
<div className="flex h-full flex-col space-y-6 p-8">
<PracticeSessionView session={session} />
</div>
)
}

View File

@@ -0,0 +1,26 @@
"use client"
import { useEffect } from "react"
import { useTranslations } from "next-intl"
import { Button } from "@/shared/components/ui/button"
export default function PracticeError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}): React.ReactNode {
const t = useTranslations("student")
useEffect(() => {
console.error("Practice page error:", error)
}, [error])
return (
<div className="flex h-full flex-col items-center justify-center space-y-4 p-8">
<h2 className="text-xl font-semibold">{t("error.title")}</h2>
<p className="text-sm text-muted-foreground">{error.message}</p>
<Button onClick={reset}>{t("error.retry")}</Button>
</div>
)
}

View File

@@ -0,0 +1,25 @@
import { Skeleton } from "@/shared/components/ui/skeleton"
export default function PracticeLoading(): React.ReactNode {
return (
<div className="flex h-full flex-col space-y-8 p-8">
<div className="space-y-2">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-4 w-72" />
</div>
<div className="grid gap-4 md:grid-cols-4">
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} className="h-24 w-full rounded-md" />
))}
</div>
<div className="grid gap-6 lg:grid-cols-3">
<Skeleton className="h-96 w-full rounded-md" />
<div className="space-y-3 lg:col-span-2">
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton key={i} className="h-20 w-full rounded-md" />
))}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,45 @@
import type { JSX } from "react"
import { getTranslations } from "next-intl/server"
import { requirePermission } from "@/shared/lib/auth-guard"
import { Permissions } from "@/shared/types/permissions"
import { getPracticeSessions, getPracticeStats } from "@/modules/adaptive-practice/data-access"
import { PracticeStarter } from "@/modules/adaptive-practice/components/practice-starter"
import { PracticeHistory } from "@/modules/adaptive-practice/components/practice-history"
import { PracticeStatsCards } from "@/modules/adaptive-practice/components/practice-stats-cards"
import { getKnowledgePointOptions } from "@/modules/questions/data-access"
export const dynamic = "force-dynamic"
export default async function StudentPracticePage(): Promise<JSX.Element> {
const ctx = await requirePermission(Permissions.ADAPTIVE_PRACTICE_READ)
const t = await getTranslations("practice")
const [stats, sessionsResult, knowledgePoints] = await Promise.all([
getPracticeStats(ctx.userId),
getPracticeSessions(ctx.userId, { pageSize: 20 }),
getKnowledgePointOptions(),
])
return (
<div className="flex h-full flex-col space-y-8 p-8">
<div>
<h1 className="text-2xl font-bold tracking-tight">{t("page.title")}</h1>
<p className="text-muted-foreground">{t("page.description")}</p>
</div>
<PracticeStatsCards stats={stats} />
<div className="grid gap-6 lg:grid-cols-3">
<div className="lg:col-span-1">
<PracticeStarter knowledgePoints={knowledgePoints} />
</div>
<div className="lg:col-span-2 space-y-4">
<h2 className="text-lg font-semibold">{t("history.title")}</h2>
<PracticeHistory sessions={sessionsResult.data} />
</div>
</div>
</div>
)
}