feat(dashboard): optimize teacher dashboard ui and layout

- Refactor layout: move Needs Grading to main column, Homework to sidebar
- Enhance TeacherStats: replace static counts with actionable metrics (Needs Grading, Active Assignments, Avg Score, Submission Rate)
- Update RecentSubmissions: table view with quick grade actions and late status
- Update TeacherSchedule: vertical timeline view with scroll hints
- Update TeacherHomeworkCard: compact list view
- Integrate Recharts: add TeacherGradeTrends chart and shared chart component
- Update documentation
This commit is contained in:
SpecialX
2026-01-12 11:38:27 +08:00
parent 8577280ab2
commit ade8d4346c
17 changed files with 1383 additions and 234 deletions

View File

@@ -1,67 +1,114 @@
import Link from "next/link";
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 { Badge } from "@/shared/components/ui/badge";
import { Button } from "@/shared/components/ui/button";
import { EmptyState } from "@/shared/components/ui/empty-state";
import { Inbox } from "lucide-react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/shared/components/ui/table";
import { formatDate } from "@/shared/lib/utils";
import type { HomeworkSubmissionListItem } from "@/modules/homework/types";
export function RecentSubmissions({ submissions }: { submissions: HomeworkSubmissionListItem[] }) {
export function RecentSubmissions({
submissions,
title = "Recent Submissions",
emptyTitle = "No New Submissions",
emptyDescription = "All caught up! There are no new submissions to review."
}: {
submissions: HomeworkSubmissionListItem[],
title?: string,
emptyTitle?: string,
emptyDescription?: string
}) {
const hasSubmissions = submissions.length > 0;
return (
<Card className="col-span-4 lg:col-span-4">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Inbox className="h-4 w-4 text-muted-foreground" />
Recent Submissions
<Card className="h-full flex flex-col">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="flex items-center gap-2 text-lg">
<Inbox className="h-5 w-5 text-primary" />
{title}
</CardTitle>
<Button variant="ghost" size="sm" className="text-muted-foreground hover:text-primary" asChild>
<Link href="/teacher/homework/submissions" className="flex items-center gap-1">
View All <ArrowRight className="h-3 w-3" />
</Link>
</Button>
</CardHeader>
<CardContent>
<CardContent className="flex-1">
{!hasSubmissions ? (
<EmptyState
icon={Inbox}
title="No New Submissions"
description="All caught up! There are no new submissions to review."
title={emptyTitle}
description={emptyDescription}
action={{ label: "View submissions", href: "/teacher/homework/submissions" }}
className="border-none h-[300px]"
className="border-none h-full min-h-[200px]"
/>
) : (
<div className="space-y-6">
{submissions.map((item) => (
<div key={item.id} className="flex items-center justify-between group">
<div className="flex items-center space-x-4">
<Avatar className="h-9 w-9">
<AvatarImage src={undefined} alt={item.studentName} />
<AvatarFallback>{item.studentName.charAt(0)}</AvatarFallback>
</Avatar>
<div className="space-y-1">
<p className="text-sm font-medium leading-none">
{item.studentName}
</p>
<p className="text-sm text-muted-foreground">
<Link
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow className="bg-muted/50">
<TableHead className="w-[200px]">Student</TableHead>
<TableHead>Assignment</TableHead>
<TableHead className="w-[140px]">Submitted</TableHead>
<TableHead className="w-[100px] text-right">Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{submissions.map((item) => (
<TableRow key={item.id} className="hover:bg-muted/50">
<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>
</Avatar>
<span className="font-medium text-sm">{item.studentName}</span>
</div>
</TableCell>
<TableCell>
<Link
href={`/teacher/homework/submissions/${item.id}`}
className="font-medium text-foreground hover:underline"
className="font-medium hover:text-primary hover:underline transition-colors block truncate max-w-[240px]"
title={item.assignmentTitle}
>
{item.assignmentTitle}
</Link>
</p>
</div>
</div>
<div className="flex items-center space-x-2">
<div className="text-sm text-muted-foreground">
{item.submittedAt ? formatDate(item.submittedAt) : "-"}
</div>
{item.isLate && (
<span className="inline-flex items-center rounded-full border border-destructive px-2 py-0.5 text-xs font-semibold text-destructive transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2">
Late
</span>
)}
</div>
</div>
))}
</TableCell>
<TableCell>
<div className="flex flex-col gap-1">
<span className="text-xs text-muted-foreground">
{item.submittedAt ? formatDate(item.submittedAt) : "-"}
</span>
{item.isLate && (
<Badge variant="destructive" className="w-fit text-[10px] h-4 px-1.5 font-normal">
Late
</Badge>
)}
</div>
</TableCell>
<TableCell className="text-right">
<Button size="sm" variant="secondary" className="h-8 px-3" asChild>
<Link href={`/teacher/homework/submissions/${item.id}`}>
Grade
</Link>
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</CardContent>

View File

@@ -9,8 +9,6 @@ import type { TeacherClass } from "@/modules/classes/types"
export function TeacherClassesCard({ classes }: { classes: TeacherClass[] }) {
const totalStudents = classes.reduce((sum, c) => sum + (c.studentCount ?? 0), 0)
const topClassesByStudents = [...classes].sort((a, b) => (b.studentCount ?? 0) - (a.studentCount ?? 0)).slice(0, 8)
const maxStudentCount = Math.max(1, ...topClassesByStudents.map((c) => c.studentCount ?? 0))
return (
<Card>
@@ -33,52 +31,40 @@ export function TeacherClassesCard({ classes }: { classes: TeacherClass[] }) {
className="border-none h-72"
/>
) : (
<>
{topClassesByStudents.length > 0 ? (
<div className="rounded-md border bg-card p-4">
<div className="flex items-center justify-between">
<div className="text-sm font-medium">Students by class</div>
<div className="text-xs text-muted-foreground tabular-nums">Total {totalStudents}</div>
</div>
<div className="mt-3 grid gap-2">
{topClassesByStudents.map((c) => {
const count = c.studentCount ?? 0
const pct = Math.max(0, Math.min(100, (count / maxStudentCount) * 100))
return (
<div key={c.id} className="grid grid-cols-[minmax(0,1fr)_120px_52px] items-center gap-3">
<div className="truncate text-sm">{c.name}</div>
<div className="h-2 rounded-full bg-muted">
<div className="h-2 rounded-full bg-primary" style={{ width: `${pct}%` }} />
</div>
<div className="text-right text-xs tabular-nums text-muted-foreground">{count}</div>
</div>
)
})}
</div>
</div>
) : null}
<div className="space-y-1">
{classes.slice(0, 6).map((c) => (
<Link
key={c.id}
href={`/teacher/classes/my/${encodeURIComponent(c.id)}`}
className="flex items-center justify-between rounded-md border bg-card px-4 py-3 hover:bg-muted/50"
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"
>
<div className="min-w-0">
<div className="font-medium truncate">{c.name}</div>
<div className="text-sm text-muted-foreground">
{c.grade}
{c.homeroom ? ` · ${c.homeroom}` : ""}
{c.room ? ` · ${c.room}` : ""}
<div className="min-w-0 flex-1 mr-3">
<div className="font-medium truncate group-hover:text-primary transition-colors">{c.name}</div>
<div className="text-xs text-muted-foreground truncate flex items-center gap-1.5">
<span className="inline-flex items-center rounded-sm bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
{c.grade}
</span>
{c.homeroom && (
<>
<span>·</span>
<span>Homeroom: {c.homeroom}</span>
</>
)}
{c.room && (
<>
<span>·</span>
<span>Room {c.room}</span>
</>
)}
</div>
</div>
<Badge variant="outline" className="flex items-center gap-1">
<Users className="h-3 w-3" />
{c.studentCount} students
</Badge>
<div className="flex items-center text-xs text-muted-foreground tabular-nums">
<Users className="mr-1.5 h-3 w-3 opacity-70" />
{c.studentCount}
</div>
</Link>
))}
</>
</div>
)}
</CardContent>
</Card>

View File

@@ -1,11 +1,22 @@
import { TeacherQuickActions } from "./teacher-quick-actions"
export function TeacherDashboardHeader() {
interface TeacherDashboardHeaderProps {
teacherName: string
}
export function TeacherDashboardHeader({ teacherName }: TeacherDashboardHeaderProps) {
const today = new Date().toLocaleDateString("en-US", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
})
return (
<div className="flex flex-col justify-between space-y-4 md:flex-row md:items-center md:space-y-0">
<div>
<h2 className="text-2xl font-bold tracking-tight">Teacher</h2>
<p className="text-muted-foreground">Overview of today&apos;s work and your classes.</p>
<h2 className="text-2xl font-bold tracking-tight">Good morning, {teacherName}</h2>
<p className="text-muted-foreground">It&apos;s {today}. Here&apos;s your daily overview.</p>
</div>
<TeacherQuickActions />
</div>

View File

@@ -6,6 +6,7 @@ import { TeacherHomeworkCard } from "./teacher-homework-card"
import { RecentSubmissions } from "./recent-submissions"
import { TeacherSchedule } from "./teacher-schedule"
import { TeacherStats } from "./teacher-stats"
import { TeacherGradeTrends } from "./teacher-grade-trends"
const toWeekday = (d: Date): 1 | 2 | 3 | 4 | 5 | 6 | 7 => {
const day = d.getDay()
@@ -32,27 +33,52 @@ export function TeacherDashboardView({ data }: { data: TeacherDashboardData }) {
const submittedSubmissions = data.submissions.filter((s) => Boolean(s.submittedAt))
const toGradeCount = submittedSubmissions.filter((s) => s.status === "submitted").length
const recentSubmissions = submittedSubmissions.slice(0, 6)
// Filter for submissions that actually need grading (status === "submitted")
// If we have less than 5 to grade, maybe also show some recently graded ones?
// For now, let's stick to "Needs Grading" as it's more useful.
const submissionsToGrade = submittedSubmissions
.filter(s => s.status === "submitted")
.sort((a, b) => new Date(a.submittedAt!).getTime() - new Date(b.submittedAt!).getTime()) // Oldest first? Or Newest? Usually oldest first for queue.
.slice(0, 6);
// Calculate stats for the dashboard
const activeAssignmentsCount = data.assignments.filter(a => a.status === "published").length
const totalTrendScore = data.gradeTrends.reduce((acc, curr) => acc + curr.averageScore, 0)
const averageScore = data.gradeTrends.length > 0 ? totalTrendScore / data.gradeTrends.length : 0
const totalSubmissions = data.gradeTrends.reduce((acc, curr) => acc + curr.submissionCount, 0)
const totalPotentialSubmissions = data.gradeTrends.reduce((acc, curr) => acc + curr.totalStudents, 0)
const submissionRate = totalPotentialSubmissions > 0 ? (totalSubmissions / totalPotentialSubmissions) * 100 : 0
return (
<div className="flex h-full flex-col space-y-8 p-8">
<TeacherDashboardHeader />
<div className="flex h-full flex-col space-y-6 p-8">
<TeacherDashboardHeader teacherName={data.teacherName} />
<TeacherStats
totalStudents={totalStudents}
classCount={data.classes.length}
toGradeCount={toGradeCount}
todayScheduleCount={todayScheduleItems.length}
activeAssignmentsCount={activeAssignmentsCount}
averageScore={averageScore}
submissionRate={submissionRate}
/>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
<TeacherSchedule items={todayScheduleItems} />
<RecentSubmissions submissions={recentSubmissions} />
</div>
<div className="grid gap-6 lg:grid-cols-2">
<TeacherClassesCard classes={data.classes} />
<TeacherHomeworkCard assignments={data.assignments} />
<div className="grid gap-6 lg:grid-cols-12">
<div className="flex flex-col gap-6 lg:col-span-8">
<TeacherGradeTrends trends={data.gradeTrends} />
<RecentSubmissions
submissions={submissionsToGrade}
title="Needs Grading"
emptyTitle="All caught up!"
emptyDescription="You have no pending submissions to grade."
/>
</div>
<div className="flex flex-col gap-6 lg:col-span-4">
<TeacherSchedule items={todayScheduleItems} />
<TeacherHomeworkCard assignments={data.assignments} />
<TeacherClassesCard classes={data.classes} />
</div>
</div>
</div>
)

View File

@@ -0,0 +1,135 @@
"use client"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/shared/components/ui/card"
import { TrendingUp } from "lucide-react"
import { EmptyState } from "@/shared/components/ui/empty-state"
import type { TeacherGradeTrendItem } from "@/modules/homework/types"
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/shared/components/ui/chart"
export function TeacherGradeTrends({ trends }: { trends: TeacherGradeTrendItem[] }) {
const hasTrends = trends.length > 0
// Calculate percentages for the chart
const chartData = trends.map((item) => {
const percentage = item.maxScore > 0 ? (item.averageScore / item.maxScore) * 100 : 0
return {
title: item.title,
score: Math.round(percentage),
fullTitle: item.title, // For tooltip
submissionCount: item.submissionCount,
totalStudents: item.totalStudents,
}
})
const chartConfig = {
score: {
label: "Average Score (%)",
color: "hsl(var(--primary))",
},
}
return (
<Card className="col-span-1">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base font-medium">
<TrendingUp className="h-4 w-4 text-primary" />
Class Performance
</CardTitle>
<CardDescription>
Average scores for the last {trends.length} assignments
</CardDescription>
</CardHeader>
<CardContent>
{!hasTrends ? (
<EmptyState
icon={TrendingUp}
title="No data available"
description="Publish assignments to see class performance trends."
className="border-none h-[200px] p-0"
/>
) : (
<div className="space-y-4">
<ChartContainer config={chartConfig} className="h-[200px] w-full">
<LineChart
data={chartData}
margin={{
left: 12,
right: 12,
top: 12,
bottom: 12,
}}
>
<CartesianGrid vertical={false} strokeDasharray="4 4" strokeOpacity={0.4} />
<XAxis
dataKey="title"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) => value.slice(0, 10) + (value.length > 10 ? "..." : "")}
/>
<YAxis
domain={[0, 100]}
tickLine={false}
axisLine={false}
tickFormatter={(value) => `${value}%`}
width={30}
/>
<ChartTooltip
cursor={{
stroke: "hsl(var(--muted-foreground))",
strokeWidth: 1,
strokeDasharray: "4 4",
}}
content={
<ChartTooltipContent
indicator="line"
labelKey="fullTitle"
className="w-[200px]"
/>
}
/>
<Line
dataKey="score"
type="monotone"
stroke="var(--color-score)"
strokeWidth={2}
dot={{
fill: "var(--color-score)",
r: 4,
strokeWidth: 2,
stroke: "hsl(var(--background))"
}}
activeDot={{
r: 6,
strokeWidth: 2,
stroke: "hsl(var(--background))"
}}
/>
</LineChart>
</ChartContainer>
{/* Metric Summary */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{chartData.slice().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 className="text-xs text-muted-foreground truncate" title={item.fullTitle}>
{item.fullTitle}
</div>
<div className="flex items-baseline gap-2">
<span className="text-xl font-bold tabular-nums">
{item.score}%
</span>
</div>
<div className="text-[10px] text-muted-foreground">
{item.submissionCount}/{item.totalStudents} submitted
</div>
</div>
))}
</div>
</div>
)}
</CardContent>
</Card>
)
}

View File

@@ -1,54 +1,93 @@
import Link from "next/link"
import { PenTool } from "lucide-react"
import { PenTool, Calendar, Plus } from "lucide-react"
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"
import { cn, formatDate } from "@/shared/lib/utils"
import type { HomeworkAssignmentListItem } from "@/modules/homework/types"
export function TeacherHomeworkCard({ assignments }: { assignments: HomeworkAssignmentListItem[] }) {
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardHeader className="flex flex-row items-center justify-between pb-3">
<CardTitle className="text-base flex items-center gap-2">
<PenTool className="h-4 w-4 text-muted-foreground" />
Homework
</CardTitle>
<div className="flex items-center gap-2">
<Button asChild variant="outline" size="sm">
<Link href="/teacher/homework/assignments">Open list</Link>
</Button>
<Button asChild size="sm">
<Link href="/teacher/homework/assignments/create">New</Link>
</Button>
</div>
<Button asChild size="icon" variant="ghost" className="h-8 w-8">
<Link href="/teacher/homework/assignments/create" title="Create new assignment">
<Plus className="h-4 w-4" />
</Link>
</Button>
</CardHeader>
<CardContent className="grid gap-3">
<CardContent>
{assignments.length === 0 ? (
<EmptyState
icon={PenTool}
title="No homework assignments yet"
description="Create an assignment from an exam and publish it to students."
action={{ label: "Create assignment", href: "/teacher/homework/assignments/create" }}
className="border-none h-72"
title="No assignments"
description="Create an assignment to get started."
action={{ label: "Create", href: "/teacher/homework/assignments/create" }}
className="border-none h-48"
/>
) : (
assignments.slice(0, 6).map((a) => (
<Link
key={a.id}
href={`/teacher/homework/assignments/${encodeURIComponent(a.id)}`}
className="flex items-center justify-between rounded-md border bg-card px-4 py-3 hover:bg-muted/50"
>
<div className="min-w-0">
<div className="font-medium truncate">{a.title}</div>
<div className="text-sm text-muted-foreground truncate">{a.sourceExamTitle}</div>
</div>
<Badge variant="outline" className="capitalize">
{a.status}
</Badge>
</Link>
))
<div className="space-y-1">
{assignments.slice(0, 6).map((a) => {
const isPublished = a.status === "published"
const isDraft = a.status === "draft"
return (
<Link
key={a.id}
href={`/teacher/homework/assignments/${encodeURIComponent(a.id)}`}
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"
>
<div className="min-w-0 flex-1 mr-3">
<div className="flex items-center gap-2 mb-0.5">
<div className={cn(
"h-2 w-2 rounded-full",
isPublished ? "bg-emerald-500" :
isDraft ? "bg-amber-400" : "bg-muted-foreground"
)} />
<div className="font-medium truncate text-sm group-hover:text-primary transition-colors">
{a.title}
</div>
</div>
<div className="text-xs text-muted-foreground truncate pl-4">
{a.sourceExamTitle}
</div>
</div>
<div className="flex flex-col items-end gap-1">
{a.dueAt ? (
<div className="flex items-center text-xs text-muted-foreground tabular-nums">
<Calendar className="mr-1 h-3 w-3 opacity-70" />
{formatDate(a.dueAt)}
</div>
) : (
<span className="text-[10px] text-muted-foreground italic">No due date</span>
)}
<Badge
variant="outline"
className={cn(
"text-[10px] h-4 px-1.5 capitalize font-normal border-transparent bg-muted/50",
isPublished && "text-emerald-600 bg-emerald-500/10",
isDraft && "text-amber-600 bg-amber-500/10"
)}
>
{a.status}
</Badge>
</div>
</Link>
)
})}
<div className="pt-2">
<Button asChild variant="link" size="sm" className="w-full text-muted-foreground h-auto py-1 text-xs">
<Link href="/teacher/homework/assignments">View all assignments</Link>
</Button>
</div>
</div>
)}
</CardContent>
</Card>

View File

@@ -1,9 +1,10 @@
import Link from "next/link";
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card";
import { Badge } from "@/shared/components/ui/badge";
import { Clock, MapPin, CalendarDays, CalendarX } from "lucide-react";
import { CalendarDays, CalendarX, MapPin } from "lucide-react";
import { EmptyState } from "@/shared/components/ui/empty-state";
import { cn } from "@/shared/lib/utils";
import { ScrollArea } from "@/shared/components/ui/scroll-area";
type TeacherTodayScheduleItem = {
id: string;
@@ -17,55 +18,131 @@ type TeacherTodayScheduleItem = {
export function TeacherSchedule({ items }: { items: TeacherTodayScheduleItem[] }) {
const hasSchedule = items.length > 0;
const getStatus = (start: string, end: string) => {
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 * 60 + startM;
const endTime = endH * 60 + endM;
if (currentTime >= startTime && currentTime <= endTime) return "live";
if (currentTime < startTime) return "upcoming";
return "past";
};
return (
<Card className="col-span-3">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Card>
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-base">
<CalendarDays className="h-4 w-4 text-muted-foreground" />
Today&apos;s Schedule
</CardTitle>
</CardHeader>
<CardContent>
<CardContent className="p-0">
{!hasSchedule ? (
<EmptyState
icon={CalendarX}
title="No Classes Today"
description="No timetable entries for today."
description="No timetable entries."
action={{ label: "View schedule", href: "/teacher/classes/schedule" }}
className="border-none h-[300px]"
className="border-none h-[200px]"
/>
) : (
<div className="space-y-4">
{items.map((item) => (
<div
key={item.id}
className="flex items-center justify-between border-b pb-4 last:border-0 last:pb-0"
>
<div className="space-y-1">
<Link
href={`/teacher/classes/schedule?classId=${encodeURIComponent(item.classId)}`}
className="font-medium leading-none hover:underline"
>
{item.course}
</Link>
<div className="flex items-center text-sm text-muted-foreground">
<Clock className="mr-1 h-3 w-3" />
<span className="mr-3">{item.startTime}{item.endTime}</span>
{item.location ? (
<>
<MapPin className="mr-1 h-3 w-3" />
<span>{item.location}</span>
</>
) : null}
<ScrollArea className="h-[240px] px-6 py-2">
<div className="relative space-y-0 ml-1">
{/* Vertical Timeline Line */}
<div className="absolute left-[11px] -top-2 -bottom-2 w-px bg-border/50" />
{/* Top Fade Hint */}
<div className="absolute left-[11px] -top-3 h-3 w-px bg-gradient-to-t from-border/50 to-transparent" />
{items.map((item, index) => {
const status = getStatus(item.startTime, item.endTime);
const isLive = status === "live";
const isPast = status === "past";
const isLast = index === items.length - 1;
return (
<div key={item.id} className="relative pl-8 py-2 first:pt-0 last:pb-0 group">
{/* Timeline Dot */}
<div className={cn(
"absolute left-[7px] top-[14px] h-2.5 w-2.5 rounded-full border-2 ring-4 ring-background transition-colors z-10",
isLive ? "bg-primary border-primary" :
isPast ? "bg-muted border-muted-foreground/30" :
"bg-background border-primary"
)} />
<Link
href={`/teacher/classes/my/${encodeURIComponent(item.classId)}`}
className={cn(
"block rounded-md border p-2.5 transition-all hover:bg-muted/50",
isLive ? "bg-primary/5 border-primary/50 shadow-sm" :
isPast ? "opacity-60 grayscale bg-muted/20 border-transparent" :
"bg-card"
)}
>
<div className="flex items-start justify-between gap-3">
<div className="space-y-0.5 min-w-0">
<div className="flex items-center gap-2">
<span className={cn(
"font-medium text-sm truncate",
isLive ? "text-primary" : "text-foreground"
)}>
{item.course}
</span>
{isLive && (
<Badge variant="default" className="h-4 px-1 text-[9px] animate-pulse">
LIVE
</Badge>
)}
</div>
<div className="flex items-center gap-1.5 text-xs text-muted-foreground truncate">
<span>{item.className}</span>
{item.location && (
<>
<span>·</span>
<span className="flex items-center">
<MapPin className="mr-0.5 h-2.5 w-2.5" />
{item.location}
</span>
</>
)}
</div>
</div>
<div className={cn(
"text-right text-xs font-medium tabular-nums whitespace-nowrap",
isLive ? "text-primary" : "text-muted-foreground"
)}>
{item.startTime}
<span className="text-[10px] opacity-70 ml-0.5"> {item.endTime}</span>
</div>
</div>
</Link>
{/* Connection Line to Next (if not last) */}
{!isLast && (
<div className="absolute left-[11px] top-[24px] bottom-[-8px] w-px bg-border" />
)}
</div>
);
})}
{/* Bottom Hint */}
{items.length > 3 ? (
<div className="text-[10px] text-center text-muted-foreground pt-2 pb-1 opacity-50">
Scroll for more
</div>
</div>
<Badge variant="secondary">
{item.className}
</Badge>
</div>
))}
</div>
) : (
<div className="text-[10px] text-center text-muted-foreground pt-4 pb-1 opacity-50 italic">
No more classes today
</div>
)}
</div>
</ScrollArea>
)}
</CardContent>
</Card>

View File

@@ -1,20 +1,22 @@
import Link from "next/link";
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card";
import { Users, BookOpen, FileCheck, Calendar } from "lucide-react";
import { FileCheck, PenTool, TrendingUp, BarChart } from "lucide-react";
import { Skeleton } from "@/shared/components/ui/skeleton";
import { cn } from "@/shared/lib/utils";
interface TeacherStatsProps {
totalStudents: number;
classCount: number;
toGradeCount: number;
todayScheduleCount: number;
activeAssignmentsCount: number;
averageScore: number;
submissionRate: number;
isLoading?: boolean;
}
export function TeacherStats({
totalStudents,
classCount,
toGradeCount,
todayScheduleCount,
activeAssignmentsCount,
averageScore,
submissionRate,
isLoading = false,
}: TeacherStatsProps) {
if (isLoading) {
@@ -38,48 +40,59 @@ export function TeacherStats({
const stats = [
{
title: "Total Students",
value: String(totalStudents),
description: "Across all your classes",
icon: Users,
},
{
title: "My Classes",
value: String(classCount),
description: "Active classes you manage",
icon: BookOpen,
},
{
title: "To Grade",
title: "Needs Grading",
value: String(toGradeCount),
description: "Submitted homework waiting for grading",
description: "Submissions pending review",
icon: FileCheck,
href: "/teacher/homework/submissions?status=submitted",
highlight: toGradeCount > 0,
color: "text-amber-500",
},
{
title: "Today",
value: String(todayScheduleCount),
description: "Scheduled items today",
icon: Calendar,
title: "Active Assignments",
value: String(activeAssignmentsCount),
description: "Published and ongoing",
icon: PenTool,
href: "/teacher/homework/assignments?status=published",
color: "text-blue-500",
},
{
title: "Average Score",
value: `${Math.round(averageScore)}%`,
description: "Across recent assignments",
icon: TrendingUp,
href: "#grade-trends",
color: "text-emerald-500",
},
{
title: "Submission Rate",
value: `${Math.round(submissionRate)}%`,
description: "Overall completion rate",
icon: BarChart,
href: "#grade-trends",
color: "text-purple-500",
},
] as const;
return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{stats.map((stat, i) => (
<Card key={i}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
{stat.title}
</CardTitle>
<stat.icon className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stat.value}</div>
<p className="text-xs text-muted-foreground">
{stat.description}
</p>
</CardContent>
</Card>
<Link key={i} href={stat.href} className="block transition-transform hover:-translate-y-1">
<Card className={cn(stat.highlight && "border-amber-200 bg-amber-50/50 dark:border-amber-900 dark:bg-amber-950/20")}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
{stat.title}
</CardTitle>
<stat.icon className={cn("h-4 w-4", stat.color)} />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stat.value}</div>
<p className="text-xs text-muted-foreground">
{stat.description}
</p>
</CardContent>
</Card>
</Link>
))}
</div>
);