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
This commit is contained in:
SpecialX
2026-06-23 17:38:28 +08:00
parent c4d3433cc9
commit 1a9377222c
90 changed files with 1690 additions and 741 deletions

View File

@@ -1,9 +1,9 @@
import Link from "next/link"
import { getTranslations } from "next-intl/server"
import { getTranslations, getLocale } from "next-intl/server"
import { Inbox, ArrowRight } from "lucide-react"
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
import { Avatar, AvatarFallback, AvatarImage } from "@/shared/components/ui/avatar"
import { Avatar, AvatarFallback } from "@/shared/components/ui/avatar"
import { Badge } from "@/shared/components/ui/badge"
import { Button } from "@/shared/components/ui/button"
import { EmptyState } from "@/shared/components/ui/empty-state"
@@ -32,6 +32,7 @@ export async function RecentSubmissions({
emptyDescription,
}: RecentSubmissionsProps) {
const t = await getTranslations("dashboard")
const locale = await getLocale()
const hasSubmissions = submissions.length > 0
return (
@@ -73,7 +74,6 @@ export async function RecentSubmissions({
<TableCell>
<div className="flex items-center gap-3">
<Avatar className="h-8 w-8 border">
<AvatarImage src={undefined} alt={item.studentName} />
<AvatarFallback className="bg-primary/10 text-primary text-xs">
{item.studentName.charAt(0)}
</AvatarFallback>
@@ -93,7 +93,7 @@ export async function RecentSubmissions({
<TableCell>
<div className="flex flex-col gap-1">
<span className="text-xs text-muted-foreground">
{item.submittedAt ? formatDate(item.submittedAt) : "-"}
{item.submittedAt ? formatDate(item.submittedAt, locale) : "-"}
</span>
{item.isLate && (
<Badge variant="destructive" className="w-fit text-[10px] h-4 px-1.5 font-normal">

View File

@@ -1,5 +1,3 @@
"use client"
import { DashboardGreetingHeader } from "../dashboard-greeting-header"
import { TeacherQuickActions } from "./teacher-quick-actions"

View File

@@ -59,7 +59,7 @@ export function TeacherGradeTrends({ trends }: { trends: TeacherGradeTrendItem[]
.reverse()
.slice(0, 3)
.map((item, i) => (
<div key={i} className="flex flex-col gap-1 rounded-lg border p-3 bg-card/50">
<div key={item.fullTitle || `item-${i}`} className="flex flex-col gap-1 rounded-lg border p-3 bg-card/50">
<div className="text-xs text-muted-foreground truncate" title={item.fullTitle}>
{item.fullTitle}
</div>

View File

@@ -1,13 +1,11 @@
"use client"
import Link from "next/link"
import { PlusCircle, CheckSquare, Users } from "lucide-react"
import { useTranslations } from "next-intl"
import { getTranslations } from "next-intl/server"
import { Button } from "@/shared/components/ui/button"
export function TeacherQuickActions() {
const t = useTranslations("dashboard")
export async function TeacherQuickActions() {
const t = await getTranslations("dashboard")
return (
<div className="flex items-center space-x-2">

View File

@@ -21,14 +21,14 @@ export async function TeacherSchedule({ items }: { items: TeacherTodayScheduleIt
const t = await getTranslations("dashboard")
const hasSchedule = items.length > 0
const getStatus = (start: string, end: string) => {
const getStatus = (start: string, end: string): "live" | "upcoming" | "past" => {
const now = new Date()
const currentTime = now.getHours() * 60 + now.getMinutes()
const [startH, startM] = start.split(":").map(Number)
const [endH, endM] = end.split(":").map(Number)
const startTime = (startH ?? 0) * 60 + (startM ?? 0)
const endTime = (endH ?? 0) * 60 + (endM ?? 0)
const startTime = (Number.isFinite(startH) ? startH : 0) * 60 + (Number.isFinite(startM) ? startM : 0)
const endTime = (Number.isFinite(endH) ? endH : 0) * 60 + (Number.isFinite(endM) ? endM : 0)
if (currentTime >= startTime && currentTime <= endTime) return "live"
if (currentTime < startTime) return "upcoming"

View File

@@ -7,7 +7,6 @@ interface TeacherStatsProps {
activeAssignmentsCount: number
averageScore: number
submissionRate: number
isLoading?: boolean
}
export async function TeacherStats({
@@ -15,7 +14,6 @@ export async function TeacherStats({
activeAssignmentsCount,
averageScore,
submissionRate,
isLoading = false,
}: TeacherStatsProps) {
const t = await getTranslations("dashboard")
@@ -29,7 +27,6 @@ export async function TeacherStats({
href="/teacher/homework/submissions?status=submitted"
highlight={toGradeCount > 0}
color="text-amber-500"
isLoading={isLoading}
/>
<StatCard
title={t("stats.activeAssignments")}
@@ -38,7 +35,6 @@ export async function TeacherStats({
icon={PenTool}
href="/teacher/homework/assignments?status=published"
color="text-blue-500"
isLoading={isLoading}
/>
<StatCard
title={t("stats.averageScore")}
@@ -47,7 +43,6 @@ export async function TeacherStats({
icon={TrendingUp}
href="#grade-trends"
color="text-emerald-500"
isLoading={isLoading}
/>
<StatCard
title={t("stats.submissionRate")}
@@ -56,7 +51,6 @@ export async function TeacherStats({
icon={BarChart}
href="#grade-trends"
color="text-purple-500"
isLoading={isLoading}
/>
</div>
)

View File

@@ -49,13 +49,17 @@ export async function TeacherTodoCard({ items }: TeacherTodoCardProps) {
<div className="space-y-1">
{items
.filter((item) => item.count > 0)
.sort((a, b) => (a.variant === "urgent" ? -1 : 1) - (b.variant === "urgent" ? -1 : 1))
.sort((a, b) => {
if (a.variant === "urgent" && b.variant !== "urgent") return -1
if (a.variant !== "urgent" && b.variant === "urgent") return 1
return 0
})
.map((item, idx) => {
const style = VARIANT_STYLES[item.variant]
const Icon = style.icon
return (
<Link
key={idx}
key={item.href || item.label || `item-${idx}`}
href={item.href}
className="group flex items-center justify-between rounded-md border border-transparent px-3 py-2 hover:bg-muted/50 hover:border-border transition-colors"
>