Files
CICD/src/modules/student/components/student-courses-view.tsx
SpecialX 57807def37
Some checks failed
CI / build-and-test (push) Failing after 3m50s
CI / deploy (push) Has been skipped
完整性更新
现在已经实现了大部分基础功能
2026-01-08 11:14:03 +08:00

157 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import Link from "next/link"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { toast } from "sonner"
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
import { Badge } from "@/shared/components/ui/badge"
import { Button } from "@/shared/components/ui/button"
import { EmptyState } from "@/shared/components/ui/empty-state"
import { Input } from "@/shared/components/ui/input"
import { Label } from "@/shared/components/ui/label"
import { BookOpen, Building2, Inbox } from "lucide-react"
import type { StudentEnrolledClass } from "@/modules/classes/types"
import { joinClassByInvitationCodeAction } from "@/modules/classes/actions"
export function StudentCoursesView({
classes,
}: {
classes: StudentEnrolledClass[]
}) {
const router = useRouter()
const [code, setCode] = useState("")
const [isWorking, setIsWorking] = useState(false)
const handleJoin = async (formData: FormData) => {
setIsWorking(true)
try {
const res = await joinClassByInvitationCodeAction(null, formData)
if (res.success) {
toast.success(res.message || "Joined class")
setCode("")
router.refresh()
} else {
toast.error(res.message || "Failed to join class")
}
} catch {
toast.error("Failed to join class")
} finally {
setIsWorking(false)
}
}
if (classes.length === 0) {
return (
<div className="space-y-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-base">Join a class</CardTitle>
</CardHeader>
<CardContent>
<form action={handleJoin} className="grid gap-3 sm:grid-cols-[1fr_auto] sm:items-end">
<div className="space-y-2">
<Label htmlFor="join-invitation-code">Invitation code</Label>
<Input
id="join-invitation-code"
name="code"
inputMode="numeric"
autoComplete="one-time-code"
placeholder="6-digit code"
value={code}
onChange={(e) => setCode(e.target.value)}
maxLength={6}
required
/>
</div>
<Button type="submit" disabled={isWorking}>
{isWorking ? "Joining..." : "Join"}
</Button>
</form>
</CardContent>
</Card>
<EmptyState
icon={Inbox}
title="No courses"
description="You are not enrolled in any class yet."
className="h-80"
/>
</div>
)
}
return (
<div className="space-y-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-base">Join a class</CardTitle>
</CardHeader>
<CardContent>
<form action={handleJoin} className="grid gap-3 sm:grid-cols-[1fr_auto] sm:items-end">
<div className="space-y-2">
<Label htmlFor="join-invitation-code">Invitation code</Label>
<Input
id="join-invitation-code"
name="code"
inputMode="numeric"
autoComplete="one-time-code"
placeholder="6-digit code"
value={code}
onChange={(e) => setCode(e.target.value)}
maxLength={6}
required
/>
</div>
<Button type="submit" disabled={isWorking}>
{isWorking ? "Joining..." : "Join"}
</Button>
</form>
</CardContent>
</Card>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{classes.map((c) => (
<Card key={c.id} className="overflow-hidden">
<CardHeader className="pb-2">
<CardTitle className="flex items-center justify-between gap-3">
<div className="min-w-0">
<div className="truncate text-base font-semibold leading-none">{c.name}</div>
<div className="mt-1 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
<span className="inline-flex items-center gap-1">
<BookOpen className="h-3 w-3" />
Grade {c.grade}
</span>
{c.homeroom ? (
<Badge variant="outline" className="font-normal">
{c.homeroom}
</Badge>
) : null}
{c.room ? (
<span className="inline-flex items-center gap-1">
<Building2 className="h-3 w-3" />
{c.room}
</span>
) : null}
</div>
</div>
<Badge variant="secondary" className="shrink-0">
Enrolled
</Badge>
</CardTitle>
</CardHeader>
<CardContent className="flex items-center justify-between gap-3 pt-2">
<div className="text-sm text-muted-foreground">Open schedule for this class.</div>
<Button asChild variant="outline" size="sm">
<Link href={`/student/schedule?classId=${encodeURIComponent(c.id)}`}>Schedule</Link>
</Button>
</CardContent>
</Card>
))}
</div>
</div>
)
}