Skip to content

Commit b33b4e7

Browse files
committed
feat: add ContestCard component for individual contest display and registration handling
1 parent 3950bee commit b33b4e7

File tree

1 file changed

+78
-33
lines changed

1 file changed

+78
-33
lines changed

app/(main)/contests/page.tsx

Lines changed: 78 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { Button } from '@/components/ui/button';
99
import Link from 'next/link';
1010
import { Skeleton } from '@/components/ui/skeleton';
1111
import { format } from 'date-fns';
12-
import { Calendar, Clock, BookOpen, Trophy, CheckCircle, Edit3 } from 'lucide-react';
12+
import { Calendar, Clock, BookOpen, Trophy, CheckCircle, Edit3, Loader2 } from 'lucide-react'; // 导入 Loader2
1313
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
1414
import { useToast } from "@/hooks/use-toast";
1515
import MarkdownViewer from '@/components/shared/markdown-viewer';
@@ -22,12 +22,84 @@ import EchartsTrendChart from '@/components/charts/echarts-trend-chart';
2222

2323
const fetcher = (url: string) => api.get(url).then(res => res.data.data);
2424

25-
// --- ContestList ---
25+
function ContestCard({ contest }: { contest: Contest }) {
26+
const { data: history, isLoading: isHistoryLoading } = useSWR<ScoreHistoryPoint[]>(`/contests/${contest.id}/history`, fetcher);
27+
const { mutate } = useSWRConfig();
28+
const { toast } = useToast();
29+
const [isRegistered, setIsRegistered] = useState(false);
30+
const [isRegistering, setIsRegistering] = useState(false);
31+
32+
useEffect(() => {
33+
setIsRegistered(!!history && history.length > 0);
34+
}, [history]);
35+
36+
const handleRegister = async (e: React.MouseEvent) => {
37+
e.preventDefault();
38+
e.stopPropagation();
39+
setIsRegistering(true);
40+
try {
41+
await api.post(`/contests/${contest.id}/register`);
42+
toast({ title: "Success", description: "You have successfully registered for the contest." });
43+
mutate(`/contests/${contest.id}/history`);
44+
} catch (error: any) {
45+
toast({ variant: "destructive", title: "Registration Failed", description: error.response?.data?.message || "An unexpected error occurred." });
46+
} finally {
47+
setIsRegistering(false);
48+
}
49+
};
50+
51+
const now = new Date();
52+
const startTime = new Date(contest.starttime);
53+
const endTime = new Date(contest.endtime);
54+
const hasStarted = now >= startTime;
55+
const hasEnded = now > endTime;
56+
57+
let statusText = "Upcoming";
58+
if (hasStarted && !hasEnded) statusText = "Ongoing";
59+
if (hasEnded) statusText = "Finished";
60+
61+
const canRegister = statusText === "Ongoing";
62+
const isLoadingRegistration = isHistoryLoading || isRegistering;
63+
64+
return (
65+
<Card>
66+
<CardHeader>
67+
<CardTitle className="text-xl">{contest.name}</CardTitle>
68+
<CardDescription>
69+
<span className={`font-semibold ${statusText === 'Ongoing' ? 'text-green-500' : statusText === 'Finished' ? 'text-red-500' : 'text-blue-500'}`}>{statusText}</span>
70+
</CardDescription>
71+
</CardHeader>
72+
<CardContent className="space-y-2 text-sm text-muted-foreground">
73+
<div className="flex items-center gap-2"><Calendar className="h-4 w-4" /><span>{format(startTime, 'MMM d, yyyy')} - {format(endTime, 'MMM d, yyyy')}</span></div>
74+
<div className="flex items-center gap-2"><Clock className="h-4 w-4" /><span>{format(startTime, 'p')} to {format(endTime, 'p')}</span></div>
75+
</CardContent>
76+
<CardFooter className="flex justify-between items-center">
77+
<Link href={`/contests?id=${contest.id}`} passHref>
78+
<Button>View Details</Button>
79+
</Link>
80+
{canRegister && (
81+
isRegistered ? (
82+
<Button disabled variant="secondary">
83+
<CheckCircle className="mr-2 h-4 w-4" /> Registered
84+
</Button>
85+
) : (
86+
<Button onClick={handleRegister} disabled={isLoadingRegistration} variant="outline">
87+
{isLoadingRegistration ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Edit3 className="mr-2 h-4 w-4" />}
88+
{isLoadingRegistration ? "Checking..." : "Register"}
89+
</Button>
90+
)
91+
)}
92+
</CardFooter>
93+
</Card>
94+
);
95+
}
96+
97+
2698
function ContestList() {
2799
const { data: contests, error, isLoading } = useSWR<Record<string, Contest>>('/contests', fetcher);
28100

29101
if (isLoading) return (
30-
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
102+
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
31103
{[...Array(3)].map((_, i) => (
32104
<Card key={i}>
33105
<CardHeader><Skeleton className="h-6 w-3/4" /><Skeleton className="h-4 w-1/2" /></CardHeader>
@@ -42,36 +114,9 @@ function ContestList() {
42114

43115
return (
44116
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
45-
{Object.values(contests).map(contest => {
46-
const now = new Date();
47-
const startTime = new Date(contest.starttime);
48-
const endTime = new Date(contest.endtime);
49-
const hasStarted = now >= startTime;
50-
const hasEnded = now > endTime;
51-
let statusText = "Upcoming";
52-
if (hasStarted && !hasEnded) statusText = "Ongoing";
53-
if (hasEnded) statusText = "Finished";
54-
55-
return (
56-
<Card key={contest.id}>
57-
<CardHeader>
58-
<CardTitle className="text-xl">{contest.name}</CardTitle>
59-
<CardDescription>
60-
<span className={`font-semibold ${statusText === 'Ongoing' ? 'text-green-500' : statusText === 'Finished' ? 'text-red-500' : 'text-blue-500'}`}>{statusText}</span>
61-
</CardDescription>
62-
</CardHeader>
63-
<CardContent className="space-y-2 text-sm text-muted-foreground">
64-
<div className="flex items-center gap-2"><Calendar className="h-4 w-4" /><span>{format(startTime, 'MMM d, yyyy')} - {format(endTime, 'MMM d, yyyy')}</span></div>
65-
<div className="flex items-center gap-2"><Clock className="h-4 w-4" /><span>{format(startTime, 'p')} to {format(endTime, 'p')}</span></div>
66-
</CardContent>
67-
<CardFooter>
68-
<Link href={`/contests?id=${contest.id}`} passHref>
69-
<Button>View Details</Button>
70-
</Link>
71-
</CardFooter>
72-
</Card>
73-
)
74-
})}
117+
{Object.values(contests).map(contest => (
118+
<ContestCard key={contest.id} contest={contest} />
119+
))}
75120
</div>
76121
);
77122
}

0 commit comments

Comments
 (0)