Files
NextEdu/src/app/(dashboard)/student/learning/textbooks/page.tsx
SpecialX 21142f9b99 feat(app): add error/loading boundaries across all dashboard routes and new routes
- Add error.tsx and loading.tsx boundaries for admin, parent, student, teacher routes

- Add admin announcements edit, audit-logs overview, curriculum-map, invitation-codes, permissions, questions, roles routes

- Add admin elective detail and components, files, course-plans, users, scheduling boundaries

- Add messages group-compose route

- Add parent course-plans, elective, grades report-card, practice routes

- Add student course-plans, elective detail, error-book dialogs, grades report-card, learning study-path, leave, schedule boundaries

- Add teacher attendance report, classes boundaries, course-plans boundaries, elective, exams analytics/edit-rich/all/create/new, grades report-card, homework boundaries, leave, lesson-plans calendar

- Add auth loading, onboarding loading, api cron
2026-07-03 10:26:25 +08:00

92 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { JSX } from "react"
import { BookOpen, UserX } from "lucide-react"
import { getTranslations } from "next-intl/server"
import { getTextbooksWithScope } from "@/modules/textbooks/data-access"
import { TextbookCard } from "@/modules/textbooks/components/textbook-card"
import { TextbookFilters } from "@/modules/textbooks/components/textbook-filters"
import { getCurrentStudentUser } from "@/modules/users/data-access"
import { getGradeNameById } from "@/modules/school/data-access"
import { EmptyState } from "@/shared/components/ui/empty-state"
import { getParam, type SearchParams } from "@/shared/lib/search-params"
import { requirePermission } from "@/shared/lib/auth-guard"
import { Permissions } from "@/shared/types/permissions"
export const dynamic = "force-dynamic"
export default async function StudentTextbooksPage({
searchParams,
}: {
searchParams: Promise<SearchParams>
}): Promise<JSX.Element> {
const t = await getTranslations("textbooks")
// P0 显式权限校验:与教师端一致,校验 TEXTBOOK_READ 权限点
await requirePermission(Permissions.TEXTBOOK_READ)
const [student, sp] = await Promise.all([getCurrentStudentUser(), searchParams])
if (!student) {
return (
<div className="space-y-8">
<EmptyState
title={t("student.noUser")}
description={t("student.noUserDesc")}
icon={UserX}
/>
</div>
)
}
const q = getParam(sp, "q")
const subject = getParam(sp, "subject")
const grade = getParam(sp, "grade")
// P1-1 数据范围过滤:学生端强制按学生所在年级过滤
// student.gradeId 是 grades 表 id需通过 getGradeNameById 解析为年级名称(如 "Grade 7"
// 才能与 textbooks.grade 字符串字段匹配
const studentGradeName = student.gradeId ? await getGradeNameById(student.gradeId) : null
const textbooks = await getTextbooksWithScope(q, subject, grade, {
grade: studentGradeName ?? undefined,
})
const hasFilters = Boolean(q || (subject && subject !== "all") || (grade && grade !== "all"))
return (
<div className="space-y-8">
<div>
<h2 className="text-2xl font-bold tracking-tight">{t("student.list.title")}</h2>
<p className="text-muted-foreground">{t("student.list.subtitle")}</p>
</div>
<TextbookFilters />
{textbooks.length === 0 ? (
<EmptyState
icon={BookOpen}
title={hasFilters ? t("student.list.empty.withFilters") : t("student.list.empty.withoutFilters")}
description={
hasFilters
? t("student.list.empty.withFiltersDesc")
: t("student.list.empty.withoutFiltersDesc")
}
action={
hasFilters
? { label: t("list.clearFilters"), href: "/student/learning/textbooks" }
: undefined
}
className="bg-card"
/>
) : (
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{textbooks.map((textbook) => (
<TextbookCard
key={textbook.id}
textbook={textbook}
hrefBase="/student/learning/textbooks"
hideActions
/>
))}
</div>
)}
</div>
)
}