Files
NextEdu/src/app/(dashboard)/teacher/textbooks/page.tsx
SpecialX 1a9377222c feat(app): add error/loading boundaries and update dashboard routes
- Add error.tsx and loading.tsx boundaries for admin, parent, student, teacher routes

- Add dashboard-error-fallback and dashboard-loading-skeleton components

- Add student/learning page, parent/leave routes, teacher textbook components

- Update existing app routes across auth, dashboard, and API endpoints

- Update proxy middleware and next-auth type declarations
2026-06-23 17:38:28 +08:00

92 lines
3.0 KiB
TypeScript

import type { JSX } from "react"
import { Suspense } from "react"
import { BookOpen } from "lucide-react"
import { getTranslations } from "next-intl/server"
import { TextbookCard } from "@/modules/textbooks/components/textbook-card"
import { TextbookFormDialog } from "@/modules/textbooks/components/textbook-form-dialog"
import { getTextbooks } from "@/modules/textbooks/data-access"
import { TextbookFilters } from "@/modules/textbooks/components/textbook-filters"
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"
async function TextbooksResults({
searchParams,
t,
}: {
searchParams: Promise<SearchParams>
t: Awaited<ReturnType<typeof getTranslations<"textbooks">>>
}): Promise<JSX.Element> {
await requirePermission(Permissions.TEXTBOOK_READ)
const params = await searchParams
const q = getParam(params, "q") || undefined
const subject = getParam(params, "subject")
const grade = getParam(params, "grade")
const textbooks = await getTextbooks(q, subject || undefined, grade || undefined)
const hasFilters = Boolean(q || (subject && subject !== "all") || (grade && grade !== "all"))
if (textbooks.length === 0) {
return (
<EmptyState
icon={BookOpen}
title={hasFilters ? t("list.empty.withFilters") : t("list.empty.withoutFilters")}
description={
hasFilters
? t("list.empty.withFiltersDesc")
: t("list.empty.withoutFiltersDesc")
}
action={
hasFilters
? { label: t("list.clearFilters"), href: "/teacher/textbooks" }
: undefined
}
className="min-h-[400px] border-muted-foreground/10"
/>
)
}
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{textbooks.map((textbook) => (
<TextbookCard key={textbook.id} textbook={textbook} />
))}
</div>
)
}
export default async function TextbooksPage({
searchParams,
}: {
searchParams: Promise<SearchParams>
}): Promise<JSX.Element> {
const t = await getTranslations("textbooks")
return (
<div className="space-y-6 p-8">
{/* Page Header */}
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<h1 className="text-2xl font-bold tracking-tight">{t("list.title")}</h1>
<p className="text-muted-foreground">{t("list.subtitle")}</p>
</div>
<TextbookFormDialog />
</div>
<Suspense fallback={<div className="h-14 w-full animate-pulse rounded-lg bg-muted" />}>
<TextbookFilters />
</Suspense>
<Suspense fallback={<div className="h-[360px] w-full animate-pulse rounded-md bg-muted" />}>
<TextbooksResults searchParams={searchParams} t={t} />
</Suspense>
</div>
)
}