Skip to content

Commit f7dc1d0

Browse files
committed
feat: enhance hackathon status display and judging page functionality
- Introduced a new HackathonStatus type and a getStatusDisplay function to improve the display of hackathon statuses across the application. - Updated the HackathonsPage to utilize the new status display logic for better visual representation of hackathon states. - Added functionality to fetch and display winners in the JudgingPage, enhancing the user experience by indicating when results are published. - Refactored the API response handling for fetching judging winners to ensure consistent data structure. - Improved the ParticipantsPage to accurately reflect statistics for active participants and total submissions.
1 parent d0eb566 commit f7dc1d0

7 files changed

Lines changed: 211 additions & 89 deletions

File tree

app/(landing)/organizations/[id]/hackathons/[hackathonId]/judging/page.tsx

Lines changed: 49 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { getOrganizationMembers } from '@/lib/api/organization';
2424
import { getCrowdfundingProject } from '@/features/projects/api';
2525
import { authClient } from '@/lib/auth-client';
2626
import { useOrganization } from '@/lib/providers/OrganizationProvider';
27-
import { Loader2, Trophy } from 'lucide-react';
27+
import { Loader2, Trophy, CheckCircle2 } from 'lucide-react';
2828
import { toast } from 'sonner';
2929
import { Button } from '@/components/ui/button';
3030
import { AuthGuard } from '@/components/auth/AuthGuard';
@@ -63,6 +63,7 @@ export default function JudgingPage() {
6363
const canManageJudges =
6464
currentUserRole === 'owner' || currentUserRole === 'admin';
6565
const canPublishResults = canManageJudges && isCurrentUserJudge;
66+
const resultsPublished = winners.length > 0;
6667

6768
const fetchJudges = useCallback(async () => {
6869
// Priority: activeOrgId from context, then params.id
@@ -180,6 +181,21 @@ export default function JudgingPage() {
180181
}
181182
}, [organizationId, hackathonId]);
182183

184+
const fetchWinners = useCallback(async () => {
185+
if (!organizationId || !hackathonId) return;
186+
setIsFetchingWinners(true);
187+
try {
188+
const res = await getJudgingWinners(organizationId, hackathonId);
189+
if (res.success && res.data) {
190+
setWinners(Array.isArray(res.data) ? res.data : []);
191+
}
192+
} catch (error) {
193+
console.error('Error fetching winners:', error);
194+
} finally {
195+
setIsFetchingWinners(false);
196+
}
197+
}, [organizationId, hackathonId]);
198+
183199
const fetchData = useCallback(async () => {
184200
if (!organizationId || !hackathonId) return;
185201

@@ -191,9 +207,10 @@ export default function JudgingPage() {
191207
getJudgingCriteria(hackathonId),
192208
]);
193209

194-
// Trigger judges and results fetch in parallel but handle separately
210+
// Trigger judges, results, and winners fetch in parallel (winners => results published state)
195211
fetchJudges();
196212
fetchResults();
213+
fetchWinners();
197214

198215
let enrichedSubmissions: JudgingSubmission[] = [];
199216

@@ -289,7 +306,7 @@ export default function JudgingPage() {
289306
} finally {
290307
setIsLoading(false);
291308
}
292-
}, [organizationId, hackathonId, fetchJudges, fetchResults]);
309+
}, [organizationId, hackathonId, fetchJudges, fetchResults, fetchWinners]);
293310

294311
useEffect(() => {
295312
fetchData();
@@ -344,21 +361,6 @@ export default function JudgingPage() {
344361
}
345362
};
346363

347-
const fetchWinners = useCallback(async () => {
348-
if (!organizationId || !hackathonId) return;
349-
setIsFetchingWinners(true);
350-
try {
351-
const res = await getJudgingWinners(organizationId, hackathonId);
352-
if (res.success && res.data) {
353-
setWinners(Array.isArray(res.data) ? res.data : []);
354-
}
355-
} catch (error) {
356-
console.error('Error fetching winners:', error);
357-
} finally {
358-
setIsFetchingWinners(false);
359-
}
360-
}, [organizationId, hackathonId]);
361-
362364
const handlePublishResults = async () => {
363365
setIsPublishing(true);
364366
try {
@@ -650,26 +652,43 @@ export default function JudgingPage() {
650652

651653
<TabsContent value='results' className='mt-6'>
652654
<div className='flex flex-col gap-6'>
653-
{canPublishResults && judgingResults.length > 0 && (
654-
<div className='bg-primary/5 border-primary/10 flex items-center justify-between rounded-lg border p-4'>
655+
{resultsPublished && (
656+
<div className='flex items-center gap-4 rounded-lg border border-green-500/20 bg-green-500/10 p-4'>
657+
<CheckCircle2 className='h-10 w-10 shrink-0 text-green-500' />
655658
<div>
656-
<h3 className='text-primary text-sm font-semibold'>
657-
Finalize Competition
659+
<h3 className='text-sm font-semibold text-green-400'>
660+
Results published
658661
</h3>
659662
<p className='text-xs text-gray-400'>
660-
Publish the current rankings to name the winners.
663+
Winner rankings are live. This hackathon&apos;s results
664+
have been finalized.
661665
</p>
662666
</div>
663-
<Button
664-
onClick={handlePublishResults}
665-
disabled={isPublishing}
666-
className='bg-primary text-primary-foreground hover:bg-primary/90 px-8 font-bold'
667-
>
668-
{isPublishing ? 'Publishing...' : 'Publish Results'}
669-
</Button>
670667
</div>
671668
)}
672669

670+
{!resultsPublished &&
671+
canPublishResults &&
672+
judgingResults.length > 0 && (
673+
<div className='bg-primary/5 border-primary/10 flex items-center justify-between rounded-lg border p-4'>
674+
<div>
675+
<h3 className='text-primary text-sm font-semibold'>
676+
Finalize Competition
677+
</h3>
678+
<p className='text-xs text-gray-400'>
679+
Publish the current rankings to name the winners.
680+
</p>
681+
</div>
682+
<Button
683+
onClick={handlePublishResults}
684+
disabled={isPublishing}
685+
className='bg-primary text-primary-foreground hover:bg-primary/90 px-8 font-bold'
686+
>
687+
{isPublishing ? 'Publishing...' : 'Publish Results'}
688+
</Button>
689+
</div>
690+
)}
691+
673692
{winners.length > 0 && (
674693
<div className='space-y-4'>
675694
<h2 className='flex items-center gap-2 text-lg font-bold text-yellow-500'>

app/(landing)/organizations/[id]/hackathons/[hackathonId]/participants/page.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,13 @@ const ParticipantsPage: React.FC = () => {
137137
organizationId,
138138
actualHackathonId
139139
);
140+
const stats = response.data as {
141+
totalSubmissions?: number;
142+
activeParticipants?: number;
143+
};
140144
setStatistics({
141-
participantsCount: response.data.participantsCount,
142-
submissionsCount: response.data.submissionsCount,
145+
participantsCount: stats.activeParticipants ?? 0,
146+
submissionsCount: stats.totalSubmissions ?? 0,
143147
});
144148
} catch (err) {
145149
console.error('Failed to load statistics', err);

app/(landing)/organizations/[id]/hackathons/page.tsx

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,58 @@ const getTimeRemaining = (endDate: string): string => {
7272
return `${Math.floor(days / 365)}y`;
7373
};
7474

75+
type HackathonStatus =
76+
| 'DRAFT'
77+
| 'UPCOMING'
78+
| 'ACTIVE'
79+
| 'JUDGING'
80+
| 'COMPLETED'
81+
| 'ARCHIVED'
82+
| 'CANCELLED';
83+
84+
const getStatusDisplay = (
85+
status: HackathonStatus | undefined
86+
): { label: string; className: string } => {
87+
switch (status) {
88+
case 'UPCOMING':
89+
return {
90+
label: 'Upcoming',
91+
className: 'bg-blue-500/10 text-blue-400 border-blue-500/20',
92+
};
93+
case 'ACTIVE':
94+
return {
95+
label: 'Live',
96+
className: 'bg-green-500/10 text-green-400 border-green-500/20',
97+
};
98+
case 'JUDGING':
99+
return {
100+
label: 'Judging',
101+
className: 'bg-amber-500/10 text-amber-400 border-amber-500/20',
102+
};
103+
case 'COMPLETED':
104+
return {
105+
label: 'Ended',
106+
className: 'bg-zinc-500/10 text-zinc-400 border-zinc-500/20',
107+
};
108+
case 'ARCHIVED':
109+
return {
110+
label: 'Archived',
111+
className: 'bg-zinc-600/10 text-zinc-500 border-zinc-600/20',
112+
};
113+
case 'CANCELLED':
114+
return {
115+
label: 'Cancelled',
116+
className: 'bg-red-500/10 text-red-400 border-red-500/20',
117+
};
118+
case 'DRAFT':
119+
default:
120+
return {
121+
label: status === 'DRAFT' ? 'Draft' : (status ?? 'Draft'),
122+
className: 'bg-zinc-500/10 text-zinc-400 border-zinc-500/20',
123+
};
124+
}
125+
};
126+
75127
export default function HackathonsPage() {
76128
const params = useParams();
77129
const organizationId = params.id as string;
@@ -406,16 +458,19 @@ export default function HackathonsPage() {
406458
)}
407459
<div className='flex flex-1 flex-col gap-2 p-5'>
408460
<div className='mb-1 flex items-center gap-2'>
409-
<Badge
410-
variant='outline'
411-
className={`rounded-full border-none px-3 py-1 text-xs font-semibold ${['UPCOMING', 'ACTIVE', 'JUDGING', 'COMPLETED'].includes(hackathon.status) ? 'bg-green-500/10 text-green-500' : 'bg-secondary-500/10 text-secondary-500'}`}
412-
>
413-
{['UPCOMING', 'ACTIVE', 'JUDGING'].includes(
414-
hackathon.status
415-
)
416-
? 'Live'
417-
: hackathon.status}
418-
</Badge>
461+
{(() => {
462+
const { label, className } = getStatusDisplay(
463+
hackathon.status as HackathonStatus
464+
);
465+
return (
466+
<Badge
467+
variant='outline'
468+
className={`rounded-full border px-3 py-1 text-xs font-semibold ${className}`}
469+
>
470+
{label}
471+
</Badge>
472+
);
473+
})()}
419474
{endDate && (
420475
<span className='flex items-center gap-1.5 text-xs text-zinc-500'>
421476
<Calendar className='h-3 w-3' />

0 commit comments

Comments
 (0)