Skip to content

Commit 45ba02d

Browse files
authored
Judging dashboard (#384)
* fix: modify api.ts * fix: remove google auth buttom * fix: fixes responsive fixes on organization * fix: minor fixes * fix: modify create organization * fix: modify create organization * fix: fix organization permission * fix: merge into main * feat: hackathon overview page * feat: hackathon overview page * feat: implement participant overview * feat: implement participant overview * feat: implement resources tab * feat: implement the submission tab * feat: implement comment tab * fix: implement provider for hackathon * fix: implement provider for hackathon * fix: minor fixes * fix: hackathon banner * fix: hackathon banner * fix: fix organization page * fix: fix organization page * fix: use transform * fix: add tagline * fix: add tagline * fix: minor fixes * fix: minor fixes * fix: fix timeline and prizes * fix: correct timeline events * fix: implement registration deadline policy * fix: implement registration deadline policy * feat: implement leave hackathon * feat: implement leave hackathon * fix: delete hackathon * fix: implement invite participants * fix: implement participant profile viewing * feat: fetch participants team * fix: redesign hackathon banner * fix: fix hackthon card * fix: fix search bar in blog page * fix: fix search bar in blog page * fix: fix search bar in blog page * fix: fix error in fetching team posts * feat: implement create team, get my team * feat: implement create team, get my team * feat: implement hackathon project submission flow * feat: implement voting for submission * fix: team formation updates * fix: implement team invitation * feat: hackathon submissions bulk actions, ranking and ui refinements * fix: implement empty state for hackathons page * feat: Implement submission visibility and explore submissions * feat: Implement winners tab for hackathon winners display * feat: implement hackathon analytics * fix: fix coderabbit corrections * fix: fix coderabbit corrections * fix: fix coderabbit corrections * fix: fix organization settings data persistence, hackathondrafts and population * feat: fix user profile * feat: Implemented the participant-facing and hackathon organizers announcement features * feat: Implement judging dashboard and detailed breakdowns * feat: Implement judging dashboard and detailed breakdowns * fix: fix coderabbit corrections * fix: submission management ux refinements * fix: submission management ux refinements * fix: submission management ux refinements * fix: submission management ux refinements * fix: submission management ux refinements
1 parent d5f7977 commit 45ba02d

13 files changed

Lines changed: 907 additions & 453 deletions

File tree

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

Lines changed: 65 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { useEffect, useState, useCallback } from 'react';
44
import MetricsCard from '@/components/organization/cards/MetricsCard';
55
import JudgingParticipant from '@/components/organization/cards/JudgingParticipant';
6+
import EmptyState from '@/components/EmptyState';
67
import { useParams } from 'next/navigation';
78
import {
89
getJudgingSubmissions,
@@ -16,6 +17,7 @@ import {
1617
type JudgingCriterion,
1718
type JudgingSubmission,
1819
type JudgingResult,
20+
type AggregatedJudgingResults,
1921
} from '@/lib/api/hackathons/judging';
2022
import { getSubmissionDetails } from '@/lib/api/hackathons/participants';
2123
import { getOrganizationMembers } from '@/lib/api/organization';
@@ -49,8 +51,12 @@ export default function JudgingPage() {
4951
'owner' | 'admin' | 'member' | null
5052
>(null);
5153
const [judgingResults, setJudgingResults] = useState<JudgingResult[]>([]);
54+
const [judgingSummary, setJudgingSummary] =
55+
useState<AggregatedJudgingResults | null>(null);
5256
const [isFetchingResults, setIsFetchingResults] = useState(false);
5357
const [winners, setWinners] = useState<JudgingResult[]>([]);
58+
const [winnersSummary, setWinnersSummary] =
59+
useState<AggregatedJudgingResults | null>(null);
5460
const [isFetchingWinners, setIsFetchingWinners] = useState(false);
5561
const [isPublishing, setIsPublishing] = useState(false);
5662
const [isCurrentUserJudge, setIsCurrentUserJudge] = useState(false);
@@ -151,16 +157,21 @@ export default function JudgingPage() {
151157
setIsFetchingResults(true);
152158
try {
153159
const res = await getJudgingResults(organizationId, hackathonId);
154-
console.log('Judging Results Response:', res);
155-
if (res.success) {
156-
setJudgingResults(res.data || []);
160+
161+
if (res.success && res.data) {
162+
setJudgingResults(res.data.results || []);
163+
setJudgingSummary(res.data);
157164
} else {
158165
setJudgingResults([]);
159-
toast.error(res.message || 'Failed to load judging results');
166+
setJudgingSummary(null);
167+
if (!res.success) {
168+
toast.error((res as any).message || 'Failed to load judging results');
169+
}
160170
}
161171
} catch (error: any) {
162172
console.error('Error fetching results:', error);
163173
setJudgingResults([]);
174+
setJudgingSummary(null);
164175
toast.error(
165176
error.response?.data?.message ||
166177
error.message ||
@@ -340,8 +351,9 @@ export default function JudgingPage() {
340351
setIsFetchingWinners(true);
341352
try {
342353
const res = await getJudgingWinners(organizationId, hackathonId);
343-
if (res.success) {
344-
setWinners(res.data || []);
354+
if (res.success && res.data) {
355+
setWinners(res.data.results || []);
356+
setWinnersSummary(res.data);
345357
}
346358
} catch (error) {
347359
console.error('Error fetching winners:', error);
@@ -368,16 +380,28 @@ export default function JudgingPage() {
368380
}
369381
};
370382

371-
// Calculate statistics safely
372-
const gradedCount = judgingResults.length;
373-
const averageHackathonScore =
374-
judgingResults.length > 0
383+
// Use pre-calculated statistics from the API if available, otherwise fallback to local calculation
384+
const gradedCount = judgingSummary
385+
? judgingSummary.submissionsScoredCount
386+
: judgingResults.length;
387+
388+
const totalPossibleSubmissions = judgingSummary
389+
? judgingSummary.totalSubmissions
390+
: submissions.length;
391+
392+
const averageHackathonScore = judgingSummary
393+
? judgingSummary.averageScoreAcrossAll
394+
: judgingResults.length > 0
375395
? judgingResults.reduce(
376396
(acc, curr) => acc + (curr.averageScore || 0),
377397
0
378398
) / judgingResults.length
379399
: 0;
380400

401+
const assignedJudgesCount = judgingSummary
402+
? judgingSummary.judgesAssigned
403+
: currentJudges.length;
404+
381405
return (
382406
<AuthGuard redirectTo='/auth?mode=signin' fallback={<Loading />}>
383407
<div className='bg-background min-h-screen space-y-6 p-8 text-white'>
@@ -392,8 +416,8 @@ export default function JudgingPage() {
392416
<div className='flex gap-4'>
393417
<MetricsCard
394418
title='Graded Projects'
395-
value={`${gradedCount} / ${submissions.length}`}
396-
subtitle={`${submissions.length > 0 ? Math.round((gradedCount / submissions.length) * 100) : 0}% Completion`}
419+
value={`${gradedCount} / ${totalPossibleSubmissions}`}
420+
subtitle={`${totalPossibleSubmissions > 0 ? Math.round((gradedCount / totalPossibleSubmissions) * 100) : 0}% Completion`}
397421
/>
398422
<MetricsCard
399423
title='Avg. Hackathon Score'
@@ -402,7 +426,7 @@ export default function JudgingPage() {
402426
/>
403427
<MetricsCard
404428
title='Assigned Judges'
405-
value={currentJudges.length}
429+
value={assignedJudgesCount}
406430
subtitle='on this hackathon'
407431
/>
408432
</div>
@@ -451,7 +475,7 @@ export default function JudgingPage() {
451475
<div className='flex items-center justify-center py-12'>
452476
<Loader2 className='h-8 w-8 animate-spin text-gray-400' />
453477
</div>
454-
) : (
478+
) : submissions.length > 0 ? (
455479
<div className='flex flex-col gap-4'>
456480
{submissions.map(submission => (
457481
<JudgingParticipant
@@ -470,6 +494,11 @@ export default function JudgingPage() {
470494
/>
471495
))}
472496
</div>
497+
) : (
498+
<EmptyState
499+
title='No Submissions Yet'
500+
description='There are currently no submissions to judge.'
501+
/>
473502
)}
474503
</TabsContent>
475504

@@ -489,11 +518,14 @@ export default function JudgingPage() {
489518
</h3>
490519
<div className='space-y-4'>
491520
{currentJudges.length === 0 ? (
492-
<p className='py-4 text-sm text-gray-400 italic'>
493-
No judges assigned yet.
494-
</p>
521+
<EmptyState
522+
title='No Judges Assigned'
523+
description='No judges assigned yet.'
524+
type='compact'
525+
className='py-8'
526+
/>
495527
) : (
496-
currentJudges.map((judge: any) => (
528+
currentJudges.map((judge: any, index: number) => (
497529
<div
498530
key={judge.id}
499531
className='flex items-center justify-between rounded-md border border-white/10 bg-white/5 p-3'
@@ -517,7 +549,7 @@ export default function JudgingPage() {
517549
{judge.name}
518550
</p>
519551
<p className='text-xs text-gray-500'>
520-
{judge.userId}
552+
Judge {index + 1}
521553
</p>
522554
</div>
523555
</div>
@@ -605,9 +637,12 @@ export default function JudgingPage() {
605637
);
606638
})}
607639
{orgMembers.length === 0 && !isRefreshingJudges && (
608-
<p className='py-8 text-center text-sm text-gray-400'>
609-
No organization members found.
610-
</p>
640+
<EmptyState
641+
title='No Members Found'
642+
description='No organization members found.'
643+
type='compact'
644+
className='py-8'
645+
/>
611646
)}
612647
</div>
613648
</div>
@@ -648,6 +683,7 @@ export default function JudgingPage() {
648683
organizationId={organizationId}
649684
hackathonId={hackathonId}
650685
totalJudges={currentJudges.length}
686+
criteria={criteria}
651687
/>
652688
</div>
653689
)}
@@ -660,12 +696,18 @@ export default function JudgingPage() {
660696
<div className='flex items-center justify-center py-12'>
661697
<Loader2 className='h-8 w-8 animate-spin text-gray-400' />
662698
</div>
663-
) : (
699+
) : judgingResults.length > 0 ? (
664700
<JudgingResultsTable
665701
results={judgingResults}
666702
organizationId={organizationId}
667703
hackathonId={hackathonId}
668704
totalJudges={currentJudges.length}
705+
criteria={criteria}
706+
/>
707+
) : (
708+
<EmptyState
709+
title='No Results Yet'
710+
description='No judging results available yet. Results appear once judges submit scores.'
669711
/>
670712
)}
671713
</div>

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

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
'use client';
22

33
import { useParams } from 'next/navigation';
4-
import { useEffect } from 'react';
4+
import { useEffect, useState } from 'react';
55
import { Loader2, AlertCircle } from 'lucide-react';
66
import { useHackathonSubmissions } from '@/hooks/hackathon/use-hackathon-submissions';
77
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
88
import { AuthGuard } from '@/components/auth';
99
import Loading from '@/components/Loading';
1010
import { SubmissionsManagement } from '@/components/organization/hackathons/submissions/SubmissionsManagement';
11+
import { authClient } from '@/lib/auth-client';
12+
import { getHackathon, type Hackathon } from '@/lib/api/hackathons';
1113

1214
export default function SubmissionsPage() {
1315
const params = useParams();
@@ -26,12 +28,40 @@ export default function SubmissionsPage() {
2628
refresh,
2729
} = useHackathonSubmissions(hackathonId);
2830

31+
const [currentUserId, setCurrentUserId] = useState<string | null>(null);
32+
const [hackathon, setHackathon] = useState<Hackathon | null>(null);
33+
2934
useEffect(() => {
3035
if (hackathonId) {
3136
fetchSubmissions();
37+
const fetchHackathonDetails = async () => {
38+
try {
39+
const res = await getHackathon(hackathonId);
40+
if (res.success && res.data) {
41+
setHackathon(res.data);
42+
}
43+
} catch (err) {
44+
console.error('Failed to fetch hackathon details:', err);
45+
}
46+
};
47+
fetchHackathonDetails();
3248
}
3349
}, [hackathonId, fetchSubmissions]);
3450

51+
useEffect(() => {
52+
const fetchSession = async () => {
53+
try {
54+
const { data: session } = await authClient.getSession();
55+
if (session?.user?.id) {
56+
setCurrentUserId(session.user.id);
57+
}
58+
} catch (err) {
59+
console.error('Failed to fetch session:', err);
60+
}
61+
};
62+
fetchSession();
63+
}, []);
64+
3565
if (error) {
3666
return (
3767
<div className='flex min-h-screen items-center justify-center bg-black p-6'>
@@ -84,6 +114,8 @@ export default function SubmissionsPage() {
84114
onRefresh={refresh}
85115
organizationId={organizationId}
86116
hackathonId={hackathonId}
117+
currentUserId={currentUserId || undefined}
118+
hackathon={hackathon || undefined}
87119
/>
88120
)}
89121
</div>

components/hackathons/hackathonNavTabs.tsx

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
'use client';
1+
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area';
22

33
interface HackathonNavTab {
44
id: string;
@@ -24,30 +24,33 @@ export function HackathonNavTabs({
2424
return (
2525
<div className='w-full border-b border-[#a7f950]/20'>
2626
<div className='mx-auto max-w-7xl px-6'>
27-
<div className='flex items-center gap-1 overflow-x-auto'>
28-
{tabs.map(tab => {
29-
const isActive = activeTab === tab.id;
30-
return (
31-
<button
32-
key={tab.id}
33-
onClick={() => handleTabChange(tab.id)}
34-
className={`relative px-4 py-4 text-sm font-medium whitespace-nowrap transition-colors duration-200 ${isActive ? 'text-[#a7f950]' : 'text-gray-400 hover:text-white'} `}
35-
>
36-
<div className='flex items-center gap-2'>
37-
<span>{tab.label}</span>
38-
{tab.badge !== undefined && (
39-
<span className='rounded-full bg-[#a7f950]/20 px-2 py-1 text-xs text-[#a7f950]'>
40-
{tab.badge}
41-
</span>
27+
<ScrollArea className='w-full whitespace-nowrap'>
28+
<div className='flex items-center gap-1'>
29+
{tabs.map(tab => {
30+
const isActive = activeTab === tab.id;
31+
return (
32+
<button
33+
key={tab.id}
34+
onClick={() => handleTabChange(tab.id)}
35+
className={`relative px-4 py-4 text-sm font-medium whitespace-nowrap transition-colors duration-200 ${isActive ? 'text-[#a7f950]' : 'text-gray-400 hover:text-white'} `}
36+
>
37+
<div className='flex items-center gap-2'>
38+
<span>{tab.label}</span>
39+
{tab.badge !== undefined && (
40+
<span className='rounded-full bg-[#a7f950]/20 px-2 py-1 text-xs text-[#a7f950]'>
41+
{tab.badge}
42+
</span>
43+
)}
44+
</div>
45+
{isActive && (
46+
<div className='absolute right-0 bottom-0 left-0 h-0.5 bg-[#a7f950]' />
4247
)}
43-
</div>
44-
{isActive && (
45-
<div className='absolute right-0 bottom-0 left-0 h-0.5 bg-[#a7f950]' />
46-
)}
47-
</button>
48-
);
49-
})}
50-
</div>
48+
</button>
49+
);
50+
})}
51+
</div>
52+
<ScrollBar orientation='horizontal' />
53+
</ScrollArea>
5154
</div>
5255
</div>
5356
);

components/organization/cards/GradeSubmissionModal/useScoreForm.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,18 @@ export const useScoreForm = ({
170170
setShowSuccess(false);
171171
onClose();
172172
}, 2000);
173+
} else {
174+
// Handle API error response
175+
const errorMessage =
176+
response.message || 'Failed to submit grade. Please try again.';
177+
toast.error(errorMessage);
173178
}
174-
} catch (error) {
179+
} catch (error: any) {
180+
// Handle network or unexpected errors
175181
const errorMessage =
176-
error instanceof Error
177-
? error.message
178-
: 'Failed to submit grade. Please try again.';
182+
error?.response?.data?.message ||
183+
error?.message ||
184+
'Failed to submit grade. Please try again.';
179185
toast.error(errorMessage);
180186
} finally {
181187
setIsLoading(false);

0 commit comments

Comments
 (0)