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:
@@ -14,6 +14,7 @@ import {
|
||||
} from "@/modules/parent/components/child-detail-panel"
|
||||
import { EmptyState } from "@/shared/components/ui/empty-state"
|
||||
import { ShieldAlert } from "lucide-react"
|
||||
import { getTranslations } from "next-intl/server"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
@@ -27,6 +28,7 @@ export default async function ChildDetailPage({
|
||||
const { studentId } = await params
|
||||
const sp = await searchParams
|
||||
const ctx = await requireAuth()
|
||||
const t = await getTranslations("common")
|
||||
|
||||
// 校验当前家长与该子女存在关系(同时按 parentId + studentId 过滤,防止跨家庭信息泄露)
|
||||
const relation = await verifyParentChildRelation(studentId, ctx.userId)
|
||||
@@ -41,8 +43,8 @@ export default async function ChildDetailPage({
|
||||
<div className="p-6 md:p-8">
|
||||
<EmptyState
|
||||
icon={ShieldAlert}
|
||||
title="Access denied"
|
||||
description="This student is not linked to your account. Please contact the school administrator if you believe this is an error."
|
||||
title={t("accessDenied")}
|
||||
description={t("accessDeniedDesc")}
|
||||
className="border-none shadow-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "@/modules/parent/components/parent-children-data-page"
|
||||
import { getUserNamesByIds } from "@/modules/users/data-access"
|
||||
import { Card, CardContent } from "@/shared/components/ui/card"
|
||||
import { getTranslations } from "next-intl/server"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
@@ -34,15 +35,16 @@ type ChildDiagnosticItem = ChildDiagnosticSuccessItem | ChildDiagnosticErrorItem
|
||||
|
||||
export default async function ParentDiagnosticPage() {
|
||||
const ctx = await requirePermission(Permissions.DIAGNOSTIC_READ)
|
||||
const t = await getTranslations("diagnostic")
|
||||
|
||||
if (ctx.dataScope.type !== "children" || ctx.dataScope.childrenIds.length === 0) {
|
||||
return (
|
||||
<ParentNoChildrenPage
|
||||
title="Children Diagnostic"
|
||||
description="View your children's knowledge point mastery and diagnostic reports."
|
||||
title={t("parent.title")}
|
||||
description={t("parent.description")}
|
||||
icon={Stethoscope}
|
||||
emptyTitle="No children linked"
|
||||
emptyDescription="Your account is not linked to any student accounts yet. Please contact the school administrator."
|
||||
emptyTitle={t("parent.noChildren")}
|
||||
emptyDescription={t("parent.noChildrenDesc")}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -68,7 +70,7 @@ export default async function ParentDiagnosticPage() {
|
||||
|
||||
const items: ChildDiagnosticItem[] = results.map((r, idx) => {
|
||||
const studentId = childrenIds[idx]
|
||||
const studentName = nameMap.get(studentId)?.name ?? "Unknown student"
|
||||
const studentName = nameMap.get(studentId)?.name ?? t("parent.selectChild")
|
||||
if (r.status === "fulfilled") {
|
||||
return {
|
||||
studentId,
|
||||
@@ -88,11 +90,11 @@ export default async function ParentDiagnosticPage() {
|
||||
|
||||
return (
|
||||
<ParentChildrenDataPage
|
||||
title="Children Diagnostic"
|
||||
description="View knowledge point mastery and diagnostic reports for all your children."
|
||||
title={t("parent.title")}
|
||||
description={t("parent.description")}
|
||||
icon={Stethoscope}
|
||||
noRecordsTitle="No diagnostic data"
|
||||
noRecordsDescription="Your children don't have any diagnostic data yet."
|
||||
noRecordsTitle={t("parent.noReports")}
|
||||
noRecordsDescription={t("parent.noReports")}
|
||||
items={items}
|
||||
renderItem={(item) => (
|
||||
<>
|
||||
@@ -111,10 +113,10 @@ export default async function ParentDiagnosticPage() {
|
||||
<CardContent className="flex flex-col items-center gap-3 py-8 text-center">
|
||||
<AlertCircle className="h-8 w-8 text-destructive" aria-hidden="true" />
|
||||
<p className="text-sm font-medium text-destructive">
|
||||
Failed to load diagnostic data for {item.studentName}.
|
||||
{t("error.loadFailed")} for {item.studentName}.
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Please refresh the page or contact the school administrator if the problem persists.
|
||||
{t("error.loadFailed")}. Please refresh the page or contact the school administrator if the problem persists.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -17,23 +17,25 @@ import {
|
||||
} from "@/modules/error-book/data-access"
|
||||
import { ErrorBookStatsCards } from "@/modules/error-book/components/error-book-stats-cards"
|
||||
import { TopWrongQuestions } from "@/modules/error-book/components/top-wrong-questions"
|
||||
import { getTranslations } from "next-intl/server"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
export default async function ParentErrorBookPage(): Promise<JSX.Element> {
|
||||
const ctx = await requirePermission(Permissions.ERROR_BOOK_READ)
|
||||
const t = await getTranslations("errorBook")
|
||||
|
||||
if (ctx.dataScope.type !== "children" || ctx.dataScope.childrenIds.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full flex-col space-y-8 p-8">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight">子女错题本</h1>
|
||||
<p className="text-muted-foreground">查看子女的错题情况与学习进度。</p>
|
||||
<h1 className="text-2xl font-bold tracking-tight">{t("parent.title")}</h1>
|
||||
<p className="text-muted-foreground">{t("parent.description")}</p>
|
||||
</div>
|
||||
<EmptyState
|
||||
icon={Users}
|
||||
title="暂无子女关联"
|
||||
description="您的账号尚未关联子女,请联系学校管理员进行关联。"
|
||||
title={t("parent.noChild")}
|
||||
description={t("parent.noChildDesc")}
|
||||
className="h-[360px] bg-card"
|
||||
/>
|
||||
</div>
|
||||
@@ -56,8 +58,8 @@ export default async function ParentErrorBookPage(): Promise<JSX.Element> {
|
||||
return (
|
||||
<div className="flex h-full flex-col space-y-8 p-8">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight">子女错题本</h1>
|
||||
<p className="text-muted-foreground">查看子女的错题情况与学习进度。</p>
|
||||
<h1 className="text-2xl font-bold tracking-tight">{t("parent.title")}</h1>
|
||||
<p className="text-muted-foreground">{t("parent.description")}</p>
|
||||
</div>
|
||||
|
||||
{childrenIds.length === 1 ? (
|
||||
@@ -68,35 +70,35 @@ export default async function ParentErrorBookPage(): Promise<JSX.Element> {
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{childrenIds.map((childId, idx) => {
|
||||
const stats = childStatsList[idx]
|
||||
const name = nameMap.get(childId) ?? "未知"
|
||||
const name = nameMap.get(childId) ?? t("parent.unknown")
|
||||
return (
|
||||
<Card key={childId}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between text-base">
|
||||
<span>{name}</span>
|
||||
<Badge variant="outline">
|
||||
{formatNumber(stats.masteredRate * 100, 0)}% 掌握
|
||||
{t("parent.mastery", { rate: formatNumber(stats.masteredRate * 100, 0) })}
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div>
|
||||
<div className="text-muted-foreground">错题总数</div>
|
||||
<div className="text-muted-foreground">{t("parent.totalErrors")}</div>
|
||||
<div className="text-lg font-bold">{stats.totalCount}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground">待复习</div>
|
||||
<div className="text-muted-foreground">{t("parent.dueReview")}</div>
|
||||
<div className="text-lg font-bold text-rose-600 dark:text-rose-400">
|
||||
{stats.dueReviewCount}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground">待学习</div>
|
||||
<div className="text-muted-foreground">{t("parent.newItems")}</div>
|
||||
<div className="font-medium text-blue-600 dark:text-blue-400">{stats.newCount}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground">已掌握</div>
|
||||
<div className="text-muted-foreground">{t("parent.mastered")}</div>
|
||||
<div className="font-medium text-emerald-600 dark:text-emerald-400">{stats.masteredCount}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -112,7 +114,7 @@ export default async function ParentErrorBookPage(): Promise<JSX.Element> {
|
||||
{weakKps.length > 0 ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">薄弱知识点</CardTitle>
|
||||
<CardTitle className="text-base">{t("parent.weakPoints")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
@@ -121,7 +123,7 @@ export default async function ParentErrorBookPage(): Promise<JSX.Element> {
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span>{kp.knowledgePointName}</span>
|
||||
<span className="text-muted-foreground">
|
||||
{kp.errorCount} 错 · {formatNumber(kp.masteryRate * 100, 0)}% 掌握
|
||||
{t("parent.errorsAndMastery", { count: kp.errorCount, rate: formatNumber(kp.masteryRate * 100, 0) })}
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={kp.masteryRate * 100} className="h-1.5" />
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { EmptyState } from "@/shared/components/ui/empty-state"
|
||||
import { AlertTriangle } from "lucide-react"
|
||||
import { useTranslations } from "next-intl"
|
||||
|
||||
export default function ParentError({
|
||||
error,
|
||||
@@ -10,13 +11,15 @@ export default function ParentError({
|
||||
error: Error & { digest?: string }
|
||||
reset: () => void
|
||||
}) {
|
||||
const t = useTranslations("common")
|
||||
|
||||
return (
|
||||
<div className="p-6 md:p-8">
|
||||
<EmptyState
|
||||
icon={AlertTriangle}
|
||||
title="Something went wrong"
|
||||
title={t("somethingWentWrong")}
|
||||
description={error.message || "An unexpected error occurred. Please try again."}
|
||||
action={{ label: "Try again", onClick: reset }}
|
||||
action={{ label: t("tryAgain"), onClick: reset }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { ParentExportButton } from "@/modules/parent/components/parent-export-button"
|
||||
import { GraduationCap } from "lucide-react"
|
||||
import type { ClassAverageTrendResult } from "@/modules/grades/types"
|
||||
import { getTranslations } from "next-intl/server"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
@@ -22,15 +23,16 @@ interface ChildGradeItem {
|
||||
|
||||
export default async function ParentGradesPage() {
|
||||
const ctx = await requirePermission(Permissions.GRADE_RECORD_READ)
|
||||
const t = await getTranslations("grades")
|
||||
|
||||
if (ctx.dataScope.type !== "children" || ctx.dataScope.childrenIds.length === 0) {
|
||||
return (
|
||||
<ParentNoChildrenPage
|
||||
title="Children Grades"
|
||||
description="View your children's grade records."
|
||||
title={t("parent.title")}
|
||||
description={t("parent.description")}
|
||||
icon={GraduationCap}
|
||||
emptyTitle="No children linked"
|
||||
emptyDescription="Your account is not linked to any student accounts yet. Please contact the school administrator."
|
||||
emptyTitle={t("parent.noChildren")}
|
||||
emptyDescription={t("parent.noChildrenDesc")}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -64,11 +66,11 @@ export default async function ParentGradesPage() {
|
||||
|
||||
return (
|
||||
<ParentChildrenDataPage
|
||||
title="Children Grades"
|
||||
description="Compare grades across all your children. For single-child analysis, open the child's detail page."
|
||||
title={t("parent.title")}
|
||||
description={t("parent.description")}
|
||||
icon={GraduationCap}
|
||||
noRecordsTitle="No grade records"
|
||||
noRecordsDescription="Your children don't have any grade records yet."
|
||||
noRecordsTitle={t("parent.noGrades")}
|
||||
noRecordsDescription={t("parent.noGradesDesc")}
|
||||
items={validItems}
|
||||
renderItem={({ studentId, summary, classAverageTrend }) => (
|
||||
<>
|
||||
|
||||
@@ -4,23 +4,26 @@ import { CalendarDays, ArrowLeft, Phone, Mail } from "lucide-react"
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
|
||||
import { EmptyState } from "@/shared/components/ui/empty-state"
|
||||
import { getTranslations } from "next-intl/server"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
export default async function ParentLeavePage() {
|
||||
const t = await getTranslations("leave")
|
||||
|
||||
return (
|
||||
<div className="p-6 md:p-8 space-y-6">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Leave Request</h1>
|
||||
<h1 className="text-2xl font-bold tracking-tight">{t("title")}</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Submit a leave request for your child.
|
||||
{t("description")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button asChild variant="ghost" size="sm" className="gap-2 -ml-2">
|
||||
<Link href="/parent/dashboard" aria-label="Back to Dashboard">
|
||||
<Link href="/parent/dashboard" aria-label={t("backToDashboard")}>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Back to Dashboard
|
||||
{t("backToDashboard")}
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
@@ -28,33 +31,33 @@ export default async function ParentLeavePage() {
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<CalendarDays className="h-4 w-4 text-muted-foreground" aria-hidden />
|
||||
Online Leave Request
|
||||
{t("onlineLeave")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<EmptyState
|
||||
icon={CalendarDays}
|
||||
title="Coming soon"
|
||||
description="The online leave request feature is being developed and will be available soon. For now, please contact the homeroom teacher or school office directly."
|
||||
title={t("comingSoon")}
|
||||
description={t("comingSoonDesc")}
|
||||
className="border-none shadow-none"
|
||||
/>
|
||||
<div className="rounded-md border bg-muted/30 p-4 space-y-2">
|
||||
<div className="text-sm font-medium">Contact options</div>
|
||||
<div className="text-sm font-medium">{t("contactOptions")}</div>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
<li className="flex items-center gap-2">
|
||||
<Phone className="h-4 w-4" aria-hidden />
|
||||
<span>Call the school office during working hours</span>
|
||||
<span>{t("callOffice")}</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Mail className="h-4 w-4" aria-hidden />
|
||||
<span>Send a message to the homeroom teacher via the Messages page</span>
|
||||
<span>{t("sendMessage")}</span>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/messages"
|
||||
className="inline-flex h-9 items-center rounded-md bg-primary px-3 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors mt-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
>
|
||||
Go to Messages
|
||||
{t("goToMessages")}
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
"use client"
|
||||
|
||||
import { useTranslations } from "next-intl"
|
||||
import { BookOpen } from "lucide-react"
|
||||
import { EmptyState } from "@/shared/components/ui/empty-state"
|
||||
|
||||
export default function ParentLessonPlanViewError() {
|
||||
const t = useTranslations("lessonPreparation")
|
||||
return (
|
||||
<div className="p-8">
|
||||
<EmptyState
|
||||
icon={BookOpen}
|
||||
title={t("error.loadFailed")}
|
||||
description={t("error.loadFailedDesc")}
|
||||
action={{ label: t("error.retry"), onClick: () => window.location.reload() }}
|
||||
className="border-none shadow-none"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Skeleton } from "@/shared/components/ui/skeleton"
|
||||
|
||||
export default function ParentLessonPlanViewLoading() {
|
||||
return (
|
||||
<div className="h-[calc(100vh-4rem)]">
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<Skeleton className="h-[80%] w-[80%]" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { JSX } from "react"
|
||||
import { Suspense } from "react"
|
||||
import { getTranslations } from "next-intl/server"
|
||||
import { getAuthContext } from "@/shared/lib/auth-guard"
|
||||
import { getLessonPlanById } from "@/modules/lesson-preparation/data-access"
|
||||
import { getTextbookById, getChaptersByTextbookId } from "@/modules/textbooks/data-access"
|
||||
import { LessonPlanReadonlyView } from "@/modules/lesson-preparation/components/lesson-plan-readonly-view"
|
||||
import { Skeleton } from "@/shared/components/ui/skeleton"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
export default async function ParentLessonPlanViewPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ planId: string }>
|
||||
}): Promise<JSX.Element> {
|
||||
const { planId } = await params
|
||||
const t = await getTranslations("lessonPreparation")
|
||||
const ctx = await getAuthContext()
|
||||
|
||||
const plan = await getLessonPlanById(planId, ctx.userId)
|
||||
if (!plan) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="rounded-md border border-outline-variant bg-surface-container-low p-4 text-on-surface-variant">
|
||||
{t("readonly.notFound")}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (plan.status !== "published") {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="rounded-md border border-outline-variant bg-surface-container-low p-4 text-on-surface-variant">
|
||||
{t("readonly.notPublished")}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
let textbookTitle: string | undefined
|
||||
let chapterTitle: string | undefined
|
||||
if (plan.textbookId) {
|
||||
const textbook = await getTextbookById(plan.textbookId)
|
||||
textbookTitle = textbook?.title
|
||||
if (plan.chapterId) {
|
||||
const chapters = await getChaptersByTextbookId(plan.textbookId)
|
||||
const findChapter = (list: typeof chapters): typeof chapters[number] | undefined => {
|
||||
for (const ch of list) {
|
||||
if (ch.id === plan.chapterId) return ch
|
||||
if (ch.children && ch.children.length > 0) {
|
||||
const found = findChapter(ch.children as typeof chapters)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
const chapter = findChapter(chapters)
|
||||
chapterTitle = chapter?.title
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-[calc(100vh-4rem)]">
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<Skeleton className="h-[80%] w-[80%]" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<LessonPlanReadonlyView
|
||||
doc={plan.content}
|
||||
textbookTitle={textbookTitle}
|
||||
chapterTitle={chapterTitle}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
20
src/app/(dashboard)/parent/lesson-plans/error.tsx
Normal file
20
src/app/(dashboard)/parent/lesson-plans/error.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
"use client"
|
||||
|
||||
import { useTranslations } from "next-intl"
|
||||
import { BookOpen } from "lucide-react"
|
||||
import { EmptyState } from "@/shared/components/ui/empty-state"
|
||||
|
||||
export default function ParentLessonPlansError() {
|
||||
const t = useTranslations("lessonPreparation")
|
||||
return (
|
||||
<div className="p-8">
|
||||
<EmptyState
|
||||
icon={BookOpen}
|
||||
title={t("error.loadFailed")}
|
||||
description={t("error.loadFailedDesc")}
|
||||
action={{ label: t("error.retry"), onClick: () => window.location.reload() }}
|
||||
className="border-none shadow-none"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
17
src/app/(dashboard)/parent/lesson-plans/loading.tsx
Normal file
17
src/app/(dashboard)/parent/lesson-plans/loading.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Skeleton } from "@/shared/components/ui/skeleton"
|
||||
|
||||
export default function ParentLessonPlansLoading() {
|
||||
return (
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-8 w-[180px]" />
|
||||
<Skeleton className="h-4 w-[300px]" />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-[180px] w-full" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
44
src/app/(dashboard)/parent/lesson-plans/page.tsx
Normal file
44
src/app/(dashboard)/parent/lesson-plans/page.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { JSX } from "react"
|
||||
import { Suspense } from "react"
|
||||
import { getTranslations } from "next-intl/server"
|
||||
import { getAuthContext } from "@/shared/lib/auth-guard"
|
||||
import { getLessonPlans } from "@/modules/lesson-preparation/data-access"
|
||||
import { getSubjectOptions } from "@/modules/school/data-access"
|
||||
import { LessonPlanList } from "@/modules/lesson-preparation/components/lesson-plan-list"
|
||||
import { Skeleton } from "@/shared/components/ui/skeleton"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
export default async function ParentLessonPlansPage(): Promise<JSX.Element> {
|
||||
const t = await getTranslations("lessonPreparation")
|
||||
const ctx = await getAuthContext()
|
||||
|
||||
const [items, subjects] = await Promise.all([
|
||||
getLessonPlans({ status: "published" }, ctx.dataScope, ctx.userId),
|
||||
getSubjectOptions(),
|
||||
])
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight">{t("parent.title")}</h1>
|
||||
<p className="text-muted-foreground">{t("parent.description")}</p>
|
||||
</div>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-[180px] w-full" />
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<LessonPlanList
|
||||
initialItems={items}
|
||||
subjects={subjects}
|
||||
viewMode="parent"
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user