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:
@@ -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'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>
|
||||
|
||||
Reference in New Issue
Block a user