@@ -9,7 +9,7 @@ import { Button } from '@/components/ui/button';
99import Link from 'next/link' ;
1010import { Skeleton } from '@/components/ui/skeleton' ;
1111import { 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
1313import { Tabs , TabsList , TabsTrigger } from "@/components/ui/tabs" ;
1414import { useToast } from "@/hooks/use-toast" ;
1515import MarkdownViewer from '@/components/shared/markdown-viewer' ;
@@ -22,12 +22,84 @@ import EchartsTrendChart from '@/components/charts/echarts-trend-chart';
2222
2323const 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+
2698function 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