- 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
238 lines
8.3 KiB
TypeScript
238 lines
8.3 KiB
TypeScript
import Link from "next/link"
|
|
import { notFound } from "next/navigation"
|
|
import {
|
|
BookOpen,
|
|
Building2,
|
|
CalendarDays,
|
|
ChevronLeft,
|
|
Mail,
|
|
PenTool,
|
|
School,
|
|
User,
|
|
} from "lucide-react"
|
|
import { getTranslations } from "next-intl/server"
|
|
|
|
import { getStudentClassById, getStudentSchedule } from "@/modules/classes/data-access"
|
|
import { getCurrentStudentUser } from "@/modules/users/data-access"
|
|
import { Badge } from "@/shared/components/ui/badge"
|
|
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"
|
|
|
|
export const dynamic = "force-dynamic"
|
|
|
|
const WEEKDAY_KEYS: Record<number, string> = {
|
|
1: "mon",
|
|
2: "tue",
|
|
3: "wed",
|
|
4: "thu",
|
|
5: "fri",
|
|
6: "sat",
|
|
7: "sun",
|
|
}
|
|
|
|
export default async function StudentClassDetailPage({
|
|
params,
|
|
}: {
|
|
params: Promise<{ classId: string }>
|
|
}) {
|
|
const { classId } = await params
|
|
const student = await getCurrentStudentUser()
|
|
if (!student) return notFound()
|
|
|
|
const t = await getTranslations("student")
|
|
|
|
const [classInfo, schedule] = await Promise.all([
|
|
getStudentClassById(student.id, classId),
|
|
getStudentSchedule(student.id),
|
|
])
|
|
|
|
if (!classInfo) return notFound()
|
|
|
|
// Filter schedule items for this class
|
|
const classSchedule = schedule
|
|
.filter((s) => s.classId === classId)
|
|
.sort((a, b) => a.weekday - b.weekday || a.startTime.localeCompare(b.startTime))
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
|
|
<div className="space-y-2">
|
|
<Button asChild variant="ghost" size="sm" className="-ml-2 mb-1">
|
|
<Link href="/student/learning/courses">
|
|
<ChevronLeft className="mr-1 h-4 w-4" />
|
|
{t("classDetail.backToCourses")}
|
|
</Link>
|
|
</Button>
|
|
<h2 className="text-2xl font-bold tracking-tight">{classInfo.name}</h2>
|
|
<div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
|
|
<span className="flex items-center gap-1">
|
|
<BookOpen className="h-4 w-4" />
|
|
{t("classDetail.grade", { grade: classInfo.grade })}
|
|
</span>
|
|
{classInfo.homeroom && (
|
|
<>
|
|
<span aria-hidden="true">•</span>
|
|
<span>{classInfo.homeroom}</span>
|
|
</>
|
|
)}
|
|
{classInfo.room && (
|
|
<>
|
|
<span aria-hidden="true">•</span>
|
|
<span className="flex items-center gap-1">
|
|
<Building2 className="h-4 w-4" />
|
|
{t("classDetail.room", { room: classInfo.room })}
|
|
</span>
|
|
</>
|
|
)}
|
|
<Badge variant="secondary">{t("classDetail.active")}</Badge>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button asChild variant="outline" size="sm">
|
|
<Link href={`/student/schedule?classId=${encodeURIComponent(classInfo.id)}`}>
|
|
<CalendarDays className="mr-2 h-4 w-4" />
|
|
{t("classDetail.fullSchedule")}
|
|
</Link>
|
|
</Button>
|
|
<Button asChild size="sm">
|
|
<Link href="/student/learning/assignments">
|
|
<PenTool className="mr-2 h-4 w-4" />
|
|
{t("classDetail.assignments")}
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid gap-6 md:grid-cols-3">
|
|
{/* Teacher Info */}
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="flex items-center gap-2 text-sm font-medium">
|
|
<User className="h-4 w-4" />
|
|
{t("classDetail.teacher")}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3 text-sm">
|
|
{classInfo.teacherName ? (
|
|
<div className="flex items-center gap-2">
|
|
<User className="h-4 w-4 text-muted-foreground" />
|
|
<span className="font-medium">{classInfo.teacherName}</span>
|
|
</div>
|
|
) : (
|
|
<p className="text-muted-foreground">{t("classDetail.noTeacher")}</p>
|
|
)}
|
|
{classInfo.teacherEmail && (
|
|
<div className="flex items-center gap-2">
|
|
<Mail className="h-4 w-4 text-muted-foreground" />
|
|
<a
|
|
href={`mailto:${classInfo.teacherEmail}`}
|
|
className="text-primary hover:underline"
|
|
>
|
|
{classInfo.teacherEmail}
|
|
</a>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* School Info */}
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="flex items-center gap-2 text-sm font-medium">
|
|
<School className="h-4 w-4" />
|
|
{t("classDetail.school")}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3 text-sm">
|
|
{classInfo.schoolName ? (
|
|
<div className="flex items-center gap-2">
|
|
<School className="h-4 w-4 text-muted-foreground" />
|
|
<span className="font-medium">{classInfo.schoolName}</span>
|
|
</div>
|
|
) : (
|
|
<p className="text-muted-foreground">{t("classDetail.schoolNotAvailable")}</p>
|
|
)}
|
|
{classInfo.grade && (
|
|
<div className="flex items-center gap-2">
|
|
<BookOpen className="h-4 w-4 text-muted-foreground" />
|
|
<span>{t("classDetail.grade", { grade: classInfo.grade })}</span>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Class Info */}
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="flex items-center gap-2 text-sm font-medium">
|
|
<Building2 className="h-4 w-4" />
|
|
{t("classDetail.classroom")}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3 text-sm">
|
|
{classInfo.room ? (
|
|
<div className="flex items-center gap-2">
|
|
<Building2 className="h-4 w-4 text-muted-foreground" />
|
|
<span className="font-medium">{t("classDetail.room", { room: classInfo.room })}</span>
|
|
</div>
|
|
) : (
|
|
<p className="text-muted-foreground">{t("classDetail.roomNotAssigned")}</p>
|
|
)}
|
|
{classInfo.homeroom && (
|
|
<div className="flex items-center gap-2">
|
|
<User className="h-4 w-4 text-muted-foreground" />
|
|
<span>{t("classDetail.homeroom", { homeroom: classInfo.homeroom })}</span>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Schedule */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<CalendarDays className="h-5 w-5" />
|
|
{t("classDetail.classSchedule")}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{classSchedule.length === 0 ? (
|
|
<EmptyState
|
|
icon={CalendarDays}
|
|
title={t("classDetail.noSchedule")}
|
|
description={t("classDetail.noScheduleDesc")}
|
|
className="border-none shadow-none"
|
|
/>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{classSchedule.map((s) => (
|
|
<div
|
|
key={s.id}
|
|
className="flex items-center justify-between rounded-md border p-3"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<Badge variant="outline" className="w-12 justify-center">
|
|
{t(`weekdays.${WEEKDAY_KEYS[s.weekday]}`)}
|
|
</Badge>
|
|
<div>
|
|
<p className="font-medium">{s.course}</p>
|
|
{s.location && (
|
|
<p className="text-xs text-muted-foreground">{s.location}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="text-sm text-muted-foreground tabular-nums">
|
|
{s.startTime} - {s.endTime}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|