From 27a008e49d371daee8c817643dcd3baeca4b0d25 Mon Sep 17 00:00:00 2001 From: Collins Ikechukwu Date: Fri, 20 Mar 2026 22:36:16 +0100 Subject: [PATCH 1/3] feat: enhance submission grading modal with improved UI, detailed score breakdown, and submission-centric scoring. --- .../hackathons/[hackathonId]/judging/page.tsx | 919 ++++++++++++------ .../[hackathonId]/participants/page.tsx | 1 + .../GradeSubmissionModal/ModalFooter.tsx | 6 +- .../cards/GradeSubmissionModal/index.tsx | 61 +- .../useScoreCalculation.ts | 8 +- .../GradeSubmissionModal/useScoreForm.ts | 23 +- .../useSubmissionScores.ts | 110 ++- .../organization/cards/JudgingParticipant.tsx | 592 +++++++---- .../IndividualScoresBreakdown.tsx | 50 +- components/organization/cards/MetricsCard.tsx | 22 +- components/organization/cards/Participant.tsx | 1 + .../judging/JudgingResultsTable.tsx | 109 ++- lib/api/hackathons.ts | 6 +- lib/api/hackathons/judging.ts | 115 ++- 14 files changed, 1434 insertions(+), 589 deletions(-) diff --git a/app/(landing)/organizations/[id]/hackathons/[hackathonId]/judging/page.tsx b/app/(landing)/organizations/[id]/hackathons/[hackathonId]/judging/page.tsx index 21cfba35..e10b2629 100644 --- a/app/(landing)/organizations/[id]/hackathons/[hackathonId]/judging/page.tsx +++ b/app/(landing)/organizations/[id]/hackathons/[hackathonId]/judging/page.tsx @@ -1,12 +1,12 @@ 'use client'; -import { useEffect, useState, useCallback } from 'react'; +import { useEffect, useState, useCallback, useMemo, useRef } from 'react'; import MetricsCard from '@/components/organization/cards/MetricsCard'; import JudgingParticipant from '@/components/organization/cards/JudgingParticipant'; import EmptyState from '@/components/EmptyState'; import { useParams } from 'next/navigation'; import { - getJudgingSubmissions, + getJudgingSubmissionsForJudge, getJudgingCriteria, addJudge, removeJudge, @@ -24,7 +24,25 @@ import { getOrganizationMembers } from '@/lib/api/organization'; import { getCrowdfundingProject } from '@/features/projects/api'; import { authClient } from '@/lib/auth-client'; import { useOrganization } from '@/lib/providers/OrganizationProvider'; -import { Loader2, Trophy, CheckCircle2 } from 'lucide-react'; +import { + Loader2, + Trophy, + CheckCircle2, + Search, + ArrowUpRight, + ExternalLink, + ChevronDown, + ChevronUp, + Mail, + Users, + Star, + Zap, + LayoutGrid, + List, + ArrowUpDown, + ChevronLeft, + ChevronRight, +} from 'lucide-react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { AuthGuard } from '@/components/auth/AuthGuard'; @@ -33,6 +51,24 @@ import { reportError, reportMessage } from '@/lib/error-reporting'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { JudgingCriteriaList } from '@/components/organization/hackathons/judging/JudgingCriteriaList'; import JudgingResultsTable from '@/components/organization/hackathons/judging/JudgingResultsTable'; +import { Input } from '@/components/ui/input'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; export default function JudgingPage() { const params = useParams(); @@ -60,11 +96,36 @@ export default function JudgingPage() { const [isPublishing, setIsPublishing] = useState(false); const [isCurrentUserJudge, setIsCurrentUserJudge] = useState(false); const [currentUserId, setCurrentUserId] = useState(null); + const [memberSearchTerm, setMemberSearchTerm] = useState(''); + const [submissionSearchTerm, setSubmissionSearchTerm] = useState(''); + const [searchQuery, setSearchQuery] = useState(''); + const [sortBy, setSortBy] = useState<'date' | 'name' | 'score' | 'rank'>( + 'date' + ); + const [viewMode, setViewMode] = useState<'detailed' | 'compact'>('detailed'); + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [isFetchingSubmissions, setIsFetchingSubmissions] = useState(false); + + // Debounce search term + useEffect(() => { + const timer = setTimeout(() => { + setSubmissionSearchTerm(searchQuery); + }, 500); + return () => clearTimeout(timer); + }, [searchQuery]); + + // Cache using useRef to prevent redundant fetches on tab switch without triggering re-renders + const lastFetchedRef = useRef<{ [key: string]: number }>({}); + const CACHE_TIMEOUT = 30000; // 30 seconds + + const [isPublishDialogOpen, setIsPublishDialogOpen] = useState(false); + const [judgeToRemove, setJudgeToRemove] = useState(null); const canManageJudges = currentUserRole === 'owner' || currentUserRole === 'admin'; - const canPublishResults = canManageJudges && isCurrentUserJudge; - const resultsPublished = winners.length > 0; + const canPublishResults = canManageJudges; // Admins can always publish + const resultsPublished = !!judgingSummary?.resultsPublished; const fetchJudges = useCallback(async () => { // Priority: activeOrgId from context, then params.id @@ -155,164 +216,155 @@ export default function JudgingPage() { } }, [organizationId, hackathonId, activeOrgId]); - const fetchResults = useCallback(async () => { - if (!organizationId || !hackathonId) return; + const fetchResults = useCallback( + async (force = false) => { + if (!organizationId || !hackathonId) return; - setIsFetchingResults(true); - try { - const res = await getJudgingResults(organizationId, hackathonId); + // Check cache + const now = Date.now(); + if ( + !force && + lastFetchedRef.current['results'] && + now - lastFetchedRef.current['results'] < CACHE_TIMEOUT + ) { + return; + } - if (res.success && res.data) { - setJudgingResults(res.data.results || []); - setJudgingSummary(res.data); - } else { + setIsFetchingResults(true); + try { + const res = await getJudgingResults( + organizationId, + hackathonId, + currentPage, + 10, + submissionSearchTerm, + sortBy, + 'desc' + ); + + if (res.success && res.data) { + setJudgingResults(res.data.results || []); + setJudgingSummary(res.data); + + if (res.data.pagination?.totalPages) { + setTotalPages(res.data.pagination.totalPages); + } + + lastFetchedRef.current['results'] = now; + } else { + setJudgingResults([]); + setJudgingSummary(null); + if (!res.success) { + toast.error( + (res as any).message || 'Failed to load judging results' + ); + } + } + } catch (error: any) { + reportError(error, { + context: 'judging-fetchResults', + organizationId, + hackathonId, + }); setJudgingResults([]); setJudgingSummary(null); - if (!res.success) { - toast.error((res as any).message || 'Failed to load judging results'); - } + toast.error( + error.response?.data?.message || + error.message || + 'Failed to load judging results' + ); + } finally { + setIsFetchingResults(false); } - } catch (error: any) { - reportError(error, { - context: 'judging-fetchResults', - organizationId, - hackathonId, - }); - setJudgingResults([]); - setJudgingSummary(null); - toast.error( - error.response?.data?.message || - error.message || - 'Failed to load judging results' - ); - } finally { - setIsFetchingResults(false); - } - }, [organizationId, hackathonId]); + }, + [organizationId, hackathonId] + ); - const fetchWinners = useCallback(async () => { - if (!organizationId || !hackathonId) return; - setIsFetchingWinners(true); - try { - const res = await getJudgingWinners(organizationId, hackathonId); - if (res.success && res.data) { - setWinners(Array.isArray(res.data) ? res.data : []); + const fetchWinners = useCallback( + async (force = false) => { + if (!organizationId || !hackathonId) return; + + // Check cache + const now = Date.now(); + if ( + !force && + lastFetchedRef.current['winners'] && + now - lastFetchedRef.current['winners'] < CACHE_TIMEOUT + ) { + return; } - } catch (error) { - reportError(error, { - context: 'judging-fetchWinners', - organizationId, - hackathonId, - }); - } finally { - setIsFetchingWinners(false); - } - }, [organizationId, hackathonId]); + + setIsFetchingWinners(true); + try { + const res = await getJudgingWinners( + organizationId, + hackathonId, + 1, + 50, + submissionSearchTerm, + 'score', + 'desc' + ); + if (res.success && res.data) { + setWinners(Array.isArray(res.data) ? res.data : []); + lastFetchedRef.current['winners'] = now; + } + } catch (error) { + reportError(error, { + context: 'judging-fetchWinners', + organizationId, + hackathonId, + }); + } finally { + setIsFetchingWinners(false); + } + }, + [organizationId, hackathonId] + ); const fetchData = useCallback(async () => { if (!organizationId || !hackathonId) return; - setIsLoading(true); + // Only show global loading on initial fetch + if (submissions.length === 0) { + setIsLoading(true); + } + setIsFetchingSubmissions(true); try { - // Fetch submissions, criteria, and judges/members - const [submissionsRes, criteriaRes] = await Promise.all([ - getJudgingSubmissions(organizationId, hackathonId, 1, 50), - getJudgingCriteria(hackathonId), - ]); + // Use the new optimized endpoint that returns submissions, criteria and myScore in one go + const submissionsRes = await getJudgingSubmissionsForJudge( + hackathonId, + currentPage, + 12, + submissionSearchTerm, + sortBy, + 'desc' + ); - // Trigger judges, results, and winners fetch in parallel (winners => results published state) + // Trigger other data fetches in parallel fetchJudges(); fetchResults(); fetchWinners(); - let enrichedSubmissions: JudgingSubmission[] = []; - - if (submissionsRes.success) { - // Standard submissions endpoint returns { data: { submissions: [], pagination: {} } } - const submissionData = - (submissionsRes.data as any)?.submissions || - submissionsRes.data || - []; - const basicSubmissions = Array.isArray(submissionData) - ? submissionData - : []; - - // 2. Fetch full details for each submission to get user info - // We do this by fetching the project details, as submission endpoints lack user data - const detailsPromises = basicSubmissions.map(async (sub: any) => { - try { - // Check if we already have sufficient user data - if ( - sub.participant?.user?.profile?.firstName || - sub.participant?.name - ) - return sub; - - // Try fetch project details if we have projectId - if (sub.projectId) { - const project = await getCrowdfundingProject(sub.projectId); - if (project && project.project && project.project.creator) { - const creator = project.project.creator; - return { - ...sub, - participant: { - ...sub.participant, - // Use creator info for participant - name: creator.name, - username: creator.username, - image: creator.image, - email: creator.email, - user: { - ...sub.participant?.user, - name: creator.name, - username: creator.username, - image: creator.image, - email: creator.email, - profile: { - ...sub.participant?.user?.profile, - firstName: creator.name?.split(' ')[0] || '', - lastName: - creator.name?.split(' ').slice(1).join(' ') || '', - username: creator.username, - avatar: creator.image, - }, - }, - }, - }; - } - } - - // Fallback to submission details check if project fail or no projectId - const detailsRes = await getSubmissionDetails(sub.id); - if (detailsRes.success && detailsRes.data) { - const details = detailsRes.data as any; - return { - ...sub, - participant: { - ...sub.participant, - ...details.participant, - user: details.participant?.user || sub.participant?.user, - }, - }; - } - return sub; - } catch (err) { - reportError(err, { - context: 'judging-fetchSubmissionDetails', - submissionId: sub.id, - }); - return sub; - } - }); + if (submissionsRes.success && submissionsRes.data) { + const { + submissions: subData, + criteria: critData, + pagination, + } = submissionsRes.data; + + setSubmissions(subData || []); + setCriteria(critData || []); - enrichedSubmissions = await Promise.all(detailsPromises); - setSubmissions(enrichedSubmissions); + if (pagination?.totalPages) { + setTotalPages(pagination.totalPages); + } else if (pagination?.total) { + setTotalPages(Math.ceil(pagination.total / 12)); + } } else { setSubmissions([]); + setCriteria([]); } - - // Handle criteria response safely - setCriteria(Array.isArray(criteriaRes) ? criteriaRes : []); } catch (error) { reportError(error, { context: 'judging-fetchData', @@ -322,8 +374,18 @@ export default function JudgingPage() { toast.error('Failed to load judging data'); } finally { setIsLoading(false); + setIsFetchingSubmissions(false); } - }, [organizationId, hackathonId, fetchJudges, fetchResults, fetchWinners]); + }, [ + organizationId, + hackathonId, + currentPage, + submissionSearchTerm, + fetchJudges, + fetchResults, + fetchWinners, + sortBy, + ]); useEffect(() => { fetchData(); @@ -377,12 +439,18 @@ export default function JudgingPage() { context: 'judging-removeJudge', organizationId, hackathonId, + userId, }); - toast.error( - error.response?.data?.message || - error.message || - 'Failed to remove judge' - ); + toast.error(error.message || 'Failed to remove judge'); + } + }; + + // Use submissions directly as they are now filtered/sorted by the backend + const filteredAndSortedSubmissions = submissions; + + const handlePageChange = (newPage: number) => { + if (newPage >= 1 && newPage <= totalPages) { + setCurrentPage(newPage); } }; @@ -392,8 +460,8 @@ export default function JudgingPage() { const res = await publishJudgingResults(organizationId, hackathonId); if (res.success) { toast.success('Results published successfully!'); - fetchResults(); - fetchWinners(); + fetchResults(true); + fetchWinners(true); } else { toast.error(res.message || 'Failed to publish results'); } @@ -430,28 +498,54 @@ export default function JudgingPage() { }>
-
-

Judging Dashboard

-

- Manage and grade shortlisted submissions -

+
+
+

+ Judging Dashboard +

+

+ Manage and grade shortlisted submissions +

+
+ +
+ {/* Optional: Add a refresh button here for premium feel */} + +
-
+
+ } + className='min-w-[200px] border-gray-900 bg-white/5' + /> 0 ? Math.round((gradedCount / totalPossibleSubmissions) * 100) : 0}% Completion`} + title='Graded / Shortlisted' + value={`${gradedCount} / ${submissions.length}`} + subtitle={`${submissions.length > 0 ? Math.round((gradedCount / submissions.length) * 100) : 0}% Completion`} + // icon={} + className='min-w-[200px] border-gray-900 bg-white/5' /> } + className='min-w-[200px] border-gray-900 bg-white/5' /> } + className='min-w-[200px] border-gray-900 bg-white/5' />
@@ -461,68 +555,185 @@ export default function JudgingPage() { onValueChange={value => { setActiveTab(value); if (value === 'results') { - fetchResults(); - fetchWinners(); + fetchResults(false); + fetchWinners(false); } }} className='w-full' > - - - Overview - - - Criteria - - - Judges - - - Results - - +
+
+ + + Overview + + + Criteria + + + Judges + + + Results + + + + {/* Global Filters & Search */} +
+
+ {isFetchingSubmissions || isFetchingResults ? ( + + ) : ( + + )} + ) => { + setSearchQuery(e.target.value); + setCurrentPage(1); + }} + /> +
- - {isLoading ? ( +
+
+ Sort: + +
+ +
+ +
+ + +
+
+
+
+ +
+
+ + + {isLoading && submissions.length === 0 ? (
- ) : submissions.length > 0 ? ( -
- {submissions.map(submission => ( - 0} - judges={currentJudges} - isJudgesLoading={isRefreshingJudges} - currentUserId={currentUserId || undefined} - canOverrideScores={canManageJudges} - onSuccess={handleSuccess} - /> - ))} + ) : filteredAndSortedSubmissions.length > 0 ? ( +
+
+ {filteredAndSortedSubmissions.map(submission => ( + 0} + criteria={criteria} + judges={currentJudges} + isJudgesLoading={isRefreshingJudges} + currentUserId={currentUserId || undefined} + canOverrideScores={canManageJudges} + onSuccess={handleSuccess} + variant={ + viewMode === 'compact' ? 'compact' : 'detailed' + } + /> + ))} +
+ + {/* Pagination Controls */} + {totalPages > 1 && ( +
+ + + Page {currentPage} of {totalPages} + + +
+ )}
) : ( )} @@ -584,7 +795,7 @@ export default function JudgingPage() { size='sm' className='text-red-400 hover:bg-red-400/10 hover:text-red-300' onClick={() => - handleRemoveJudge(judge.userId || judge.id) + setJudgeToRemove(judge.userId || judge.id) } > Remove @@ -599,68 +810,90 @@ export default function JudgingPage() { {/* Org Members List - Only visible to admin/owner */} {canManageJudges && (
-

- Add from Organization Members -

-

- Select members from your organization to assign them as - judges. -

-
- {orgMembers.map((member: any) => { - const isAlreadyJudge = currentJudges.some( - j => j.id === member.id || j.userId === member.id - ); - return ( -
-
-
- {member.image ? ( - - ) : ( -
- {member.name?.[0] || - member.username?.[0] || - '?'} -
- )} -
-
-

- {member.name || member.username} -

-

- {member.email} -

+
+

+ Add from Organization Members +

+
+ + ) => + setMemberSearchTerm(e.target.value) + } + /> +
+

+ Select members from your organization to assign them as + judges. +

+
+
+ {orgMembers + .filter(m => { + const search = memberSearchTerm.toLowerCase(); + return ( + m.name?.toLowerCase().includes(search) || + m.email?.toLowerCase().includes(search) || + m.username?.toLowerCase().includes(search) + ); + }) + .map((member: any) => { + const isAlreadyJudge = currentJudges.some( + j => j.id === member.id || j.userId === member.id + ); + return ( +
+
+
+ {member.image ? ( + + ) : ( +
+ {member.name?.[0] || + member.username?.[0] || + '?'} +
+ )} +
+
+

+ {member.name || member.username} +

+

+ {member.email} +

+
+
- -
- ); - })} + ); + })} {orgMembers.length === 0 && !isRefreshingJudges && ( 0 && ( -
-
-

- Finalize Competition -

-

- Publish the current rankings to name the winners. -

+
+
+
+ +
+
+

+ Finalize Competition +

+

+ Publish the current rankings to name the winners. +

+
@@ -726,6 +964,8 @@ export default function JudgingPage() { hackathonId={hackathonId} totalJudges={currentJudges.length} criteria={criteria} + canManage={canManageJudges} + winnerOverrides={judgingSummary?.winnerOverrides} />
)} @@ -734,18 +974,53 @@ export default function JudgingPage() {

Current Standings

- {isFetchingResults ? ( + {isFetchingResults && judgingResults.length === 0 ? (
) : judgingResults.length > 0 ? ( - + <> + + + {/* Pagination Controls for Results */} + {totalPages > 1 && ( +
+ + + Page {currentPage} of {totalPages} + + +
+ )} + ) : (
+ {/* Global Dialogs */} + + + + Publish Judging Results? + + This will finalize the rankings and announce the winners. This + action is irreversible. + + + + + Cancel + + + Yes, Publish Results + + + + + + !open && setJudgeToRemove(null)} + > + + + Remove Judge? + + Are you sure you want to remove this judge? Their existing + scores will be preserved but they will no longer have access to + the judging panel. + + + + + Cancel + + { + if (judgeToRemove) handleRemoveJudge(judgeToRemove); + setJudgeToRemove(null); + }} + className='bg-red-600 text-white hover:bg-red-700' + > + Remove Judge + + + +
); diff --git a/app/(landing)/organizations/[id]/hackathons/[hackathonId]/participants/page.tsx b/app/(landing)/organizations/[id]/hackathons/[hackathonId]/participants/page.tsx index ce74910f..3423f4af 100644 --- a/app/(landing)/organizations/[id]/hackathons/[hackathonId]/participants/page.tsx +++ b/app/(landing)/organizations/[id]/hackathons/[hackathonId]/participants/page.tsx @@ -408,6 +408,7 @@ const ParticipantsPage: React.FC = () => { onOpenChange={setIsJudgeModalOpen} organizationId={organizationId} hackathonId={hackathonId} + submissionId={selectedParticipant.submission.id} participantId={selectedParticipant.id} judgingCriteria={criteria} submission={{ diff --git a/components/organization/cards/GradeSubmissionModal/ModalFooter.tsx b/components/organization/cards/GradeSubmissionModal/ModalFooter.tsx index c9df713c..2fcc1a6b 100644 --- a/components/organization/cards/GradeSubmissionModal/ModalFooter.tsx +++ b/components/organization/cards/GradeSubmissionModal/ModalFooter.tsx @@ -67,10 +67,10 @@ export const ModalFooter = ({ isLoading || isFetching || isFetchingCriteria || !hasCriteria } className={cn( - 'rounded-lg px-6 py-2.5 font-medium text-white transition-all', - 'from-success-500 to-success-600 hover:from-success-600 hover:to-success-700 bg-gradient-to-r', + 'text-background rounded-lg px-6 py-2.5 font-medium transition-all', + 'hover:bg-primary/95 bg-primary', 'disabled:cursor-not-allowed disabled:from-gray-700 disabled:to-gray-700 disabled:text-gray-500', - 'shadow-success-500/20 hover:shadow-success-500/30 shadow-lg' + 'shadow-primary/20 hover:shadow-primary/70 shadow-lg' )} > {isLoading ? ( diff --git a/components/organization/cards/GradeSubmissionModal/index.tsx b/components/organization/cards/GradeSubmissionModal/index.tsx index c6daaeb2..db80b55e 100644 --- a/components/organization/cards/GradeSubmissionModal/index.tsx +++ b/components/organization/cards/GradeSubmissionModal/index.tsx @@ -31,6 +31,7 @@ interface SubmissionData { votes: number; comments: number; logo?: string; + videoUrl?: string; } interface GradeSubmissionModalProps { @@ -38,6 +39,7 @@ interface GradeSubmissionModalProps { onOpenChange: (open: boolean) => void; organizationId: string; hackathonId: string; + submissionId: string; participantId: string; judgingCriteria?: JudgingCriterion[]; submission: SubmissionData; @@ -51,6 +53,7 @@ interface GradeSubmissionModalProps { image?: string; role?: string; }>; + initialScore?: any; onSuccess?: () => void; } @@ -59,12 +62,14 @@ export default function GradeSubmissionModal({ onOpenChange, organizationId, hackathonId, + submissionId, participantId, judgingCriteria, submission, mode = 'judge', overrideJudgeId, judges = [], + initialScore, onSuccess, }: GradeSubmissionModalProps) { const isOverride = mode === 'organizer-override'; @@ -118,8 +123,11 @@ export default function GradeSubmissionModal({ open, organizationId, hackathonId, - participantId: submission.id, + submissionId, + participantId, criteria, + targetJudgeId: creditJudge ? selectedJudgeId : undefined, + initialScore, }); const { @@ -143,7 +151,8 @@ export default function GradeSubmissionModal({ criteria, organizationId, hackathonId, - participantId: submission.id, + submissionId, + participantId, existingScore, mode, overrideJudgeId: creditJudge ? selectedJudgeId : undefined, @@ -175,10 +184,21 @@ export default function GradeSubmissionModal({
{isOverride && ( -
-
- Organizer override: this action directly assigns scores and - bypasses judge assignment checks. +
+
+
+ ! +
+
+ + Organizer scoring override + +

+ Directly assign results to this submission. These values + will bypass standard judge assignment and + conflict-of-interest checks. +

+
@@ -260,7 +280,7 @@ export default function GradeSubmissionModal({ getScoreColor={getScoreColor} overallComment={overallComment} onOverallCommentChange={setOverallComment} - showComments={!isOverride} + showComments={true} />
@@ -288,23 +308,28 @@ export default function GradeSubmissionModal({ / {criteria.length}
-
- Comments Added - +
+ + Insights + + { Object.values(comments).filter( c => c.trim().length > 0 ).length - } + }{' '} + Comments
-
-

- Your scores and comments are saved automatically - when you submit. You can return later to update - them. -

+
+
+
+

+ Changes are staged in real-time. Finalize by + clicking the primary action below. +

+
@@ -315,7 +340,7 @@ export default function GradeSubmissionModal({ )}
-
+
Math.round(totalScore), [totalScore]); const getScoreColor = (percentage: number): string => { - if (percentage >= 80) return 'bg-success-500'; - if (percentage >= 60) return 'bg-primary'; - if (percentage >= 40) return 'bg-warning-500'; - return 'bg-error-500'; + if (percentage >= 80) return 'bg-primary text-background'; + if (percentage >= 60) return 'bg-chart-2 text-white'; + if (percentage >= 40) return 'bg-warning-500 text-background'; + return 'bg-error-500 text-white'; }; return { diff --git a/components/organization/cards/GradeSubmissionModal/useScoreForm.ts b/components/organization/cards/GradeSubmissionModal/useScoreForm.ts index ddca07f7..46339b41 100644 --- a/components/organization/cards/GradeSubmissionModal/useScoreForm.ts +++ b/components/organization/cards/GradeSubmissionModal/useScoreForm.ts @@ -19,6 +19,7 @@ interface UseScoreFormProps { criteria: JudgingCriterion[]; organizationId: string; hackathonId: string; + submissionId: string; participantId: string; existingScore: { scores: CriterionScore[]; notes?: string } | null; mode?: 'judge' | 'organizer-override'; @@ -37,6 +38,7 @@ export const useScoreForm = ({ criteria, organizationId, hackathonId, + submissionId, participantId, existingScore, mode = 'judge', @@ -141,7 +143,7 @@ export const useScoreForm = ({ setIsLoading(true); try { - const includeComments = mode !== 'organizer-override'; + const includeComments = true; const scoreData = criteria.map(criterion => { const key = getCriterionKey(criterion); const payload: { @@ -163,14 +165,15 @@ export const useScoreForm = ({ ? await overrideSubmissionScore( organizationId, hackathonId, - participantId, + submissionId, { criteriaScores: scoreData, judgeId: overrideJudgeId, + notes: overallComment, } ) : await submitJudgingScore({ - submissionId: participantId, + submissionId, criteriaScores: scoreData, comment: overallComment, }); @@ -207,11 +210,21 @@ export const useScoreForm = ({ } } catch (error: any) { // Handle network or unexpected errors - const errorMessage = + const status = error?.response?.status; + let errorMessage = error?.response?.data?.message || error?.message || 'Failed to submit grade. Please try again.'; - toast.error(errorMessage); + + // Explicitly handle Conflict of Interest + if (status === 403) { + errorMessage = + 'Conflict of Interest: You are not permitted to score this submission (e.g., self-submission or team member).'; + } + + toast.error(errorMessage, { + description: status === 403 ? 'Rules of Competition' : undefined, + }); } finally { setIsLoading(false); } diff --git a/components/organization/cards/GradeSubmissionModal/useSubmissionScores.ts b/components/organization/cards/GradeSubmissionModal/useSubmissionScores.ts index 35185f67..a1f46977 100644 --- a/components/organization/cards/GradeSubmissionModal/useSubmissionScores.ts +++ b/components/organization/cards/GradeSubmissionModal/useSubmissionScores.ts @@ -12,8 +12,11 @@ interface UseSubmissionScoresProps { open: boolean; organizationId: string; hackathonId: string; + submissionId: string; participantId: string; criteria: JudgingCriterion[]; + targetJudgeId?: string; + initialScore?: IndividualJudgeScore; } interface ExistingScore { @@ -25,8 +28,11 @@ export const useSubmissionScores = ({ open, organizationId, hackathonId, + submissionId, participantId, criteria, + targetJudgeId, + initialScore, }: UseSubmissionScoresProps) => { const [scores, setScores] = useState>({}); const [comments, setComments] = useState>({}); @@ -37,7 +43,8 @@ export const useSubmissionScores = ({ ); // Track which submission we've successfully loaded to avoid resets - const loadedParticipantIdRef = useRef(null); + const loadedSubmissionIdRef = useRef(null); + const loadedTargetJudgeIdRef = useRef(null); const getCriterionKey = (criterion: JudgingCriterion) => { return criterion.id || criterion.name || (criterion as any).title; @@ -71,14 +78,62 @@ export const useSubmissionScores = ({ setComments({}); setOverallComment(''); setExistingScore(null); - loadedParticipantIdRef.current = null; + loadedSubmissionIdRef.current = null; + loadedTargetJudgeIdRef.current = null; } return; } - // If we already loaded data for this participant, don't refetch/reset - // This prevents the "clearing" bug when criteria or other deps change slightly - if (loadedParticipantIdRef.current === participantId) { + if ( + loadedSubmissionIdRef.current === submissionId && + loadedTargetJudgeIdRef.current === (targetJudgeId || 'session') + ) { + return; + } + + // Optimization: If initialScore is provided and matches the target, use it + // but only if we haven't already loaded something for this modal session + if (initialScore && !loadedSubmissionIdRef.current) { + const initialScores: Record = {}; + const initialComments: Record = {}; + + // Initialize with 0 + criteria.forEach(c => { + const key = getCriterionKey(c); + initialScores[key] = 0; + initialComments[key] = ''; + }); + + // Map existing scores + initialScore.criteriaScores.forEach(cs => { + const matchingCriterion = criteria.find( + c => + c.id === cs.criterionId || + c.name === cs.criterionId || + c.id === cs.criterionName || + c.name === cs.criterionName || + (c as any).title === cs.criterionName + ); + + const key = matchingCriterion + ? getCriterionKey(matchingCriterion) + : cs.criterionId; + + if (key) { + initialScores[key] = cs.score; + initialComments[key] = cs.comment || ''; + } + }); + + setExistingScore({ + scores: initialScore.criteriaScores, + notes: initialScore.comment || '', + }); + setScores(initialScores); + setComments(initialComments); + setOverallComment(initialScore.comment || ''); + loadedSubmissionIdRef.current = submissionId; + loadedTargetJudgeIdRef.current = targetJudgeId || 'session'; return; } @@ -87,7 +142,7 @@ export const useSubmissionScores = ({ try { const [{ data: sessionData }, response] = await Promise.all([ authClient.getSession(), - getSubmissionScores(organizationId, hackathonId, participantId), + getSubmissionScores(organizationId, hackathonId, submissionId), ]); if (cancelled) return; @@ -99,17 +154,18 @@ export const useSubmissionScores = ({ } if (response.success && Array.isArray(response.data)) { - // Safer judge identifier matching - prioritize ID and email - const currentUserScore = ( - response.data as IndividualJudgeScore[] - ).find( - s => - s.judgeId === user.id || - s.judgeEmail === user.email || - (s.judgeName === user.name && - user.name !== undefined && - user.name.trim() !== '') - ); + // Identify the correct score based on targetJudgeId or current user + const scoreData = response.data as IndividualJudgeScore[]; + const currentUserScore = targetJudgeId + ? scoreData.find(s => s.judgeId === targetJudgeId) + : scoreData.find( + s => + s.judgeId === user.id || + s.judgeEmail === user.email || + (s.judgeName === user.name && + user.name !== undefined && + user.name.trim() !== '') + ); if (currentUserScore) { setExistingScore({ @@ -157,10 +213,15 @@ export const useSubmissionScores = ({ setScores(initialScores); setComments(initialComments); setOverallComment(currentUserScore.comment || ''); - loadedParticipantIdRef.current = participantId; + loadedSubmissionIdRef.current = submissionId; + loadedTargetJudgeIdRef.current = targetJudgeId || 'session'; } } else { - if (!cancelled) initializeForm(criteria); + if (!cancelled) { + initializeForm(criteria); + loadedSubmissionIdRef.current = submissionId; + loadedTargetJudgeIdRef.current = targetJudgeId || 'session'; + } } } else { if (!cancelled) initializeForm(criteria); @@ -168,7 +229,7 @@ export const useSubmissionScores = ({ } catch (err) { reportError(err, { context: 'useSubmissionScores-fetch', - participantId, + submissionId, }); if (!cancelled) initializeForm(criteria); } finally { @@ -181,7 +242,14 @@ export const useSubmissionScores = ({ return () => { cancelled = true; }; - }, [open, organizationId, hackathonId, participantId, criteria]); + }, [ + open, + organizationId, + hackathonId, + submissionId, + criteria, + targetJudgeId, + ]); return { scores, diff --git a/components/organization/cards/JudgingParticipant.tsx b/components/organization/cards/JudgingParticipant.tsx index 82b85d75..e459bb63 100644 --- a/components/organization/cards/JudgingParticipant.tsx +++ b/components/organization/cards/JudgingParticipant.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useState, useMemo } from 'react'; +import { useState, useCallback, useMemo, useEffect, memo } from 'react'; import { ArrowUpRight, Mail, @@ -8,6 +8,7 @@ import { ChevronDown, ChevronUp, Loader2, + Trophy, } from 'lucide-react'; import Image from 'next/image'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; @@ -17,6 +18,7 @@ import TeamModal from './TeamModal'; import GradeSubmissionModal from './GradeSubmissionModal'; import { getJudgingCriteria, + getSubmissionScores, type JudgingSubmission, type JudgingCriterion, } from '@/lib/api/hackathons/judging'; @@ -25,6 +27,7 @@ import { reportError } from '@/lib/error-reporting'; import IndividualScoresBreakdown from './JudgingParticipant/IndividualScoresBreakdown'; import { authClient } from '@/lib/auth-client'; +import BasicAvatar from '@/components/avatars/BasicAvatar'; interface JudgingParticipantProps { submission: JudgingSubmission; @@ -36,6 +39,8 @@ interface JudgingParticipantProps { currentUserId?: string; canOverrideScores?: boolean; onSuccess?: () => void; + variant?: 'detailed' | 'compact'; + criteria?: JudgingCriterion[]; } const JudgingParticipant = ({ @@ -48,6 +53,8 @@ const JudgingParticipant = ({ currentUserId, canOverrideScores = false, onSuccess, + variant = 'detailed', + criteria: passedCriteria = [], }: JudgingParticipantProps) => { const [isTeamModalOpen, setIsTeamModalOpen] = useState(false); const [isScoreModalOpen, setIsScoreModalOpen] = useState(false); @@ -65,61 +72,87 @@ const JudgingParticipant = ({ Array<{ title: string; weight: number; description?: string }> >([]); const [isLoadingCriteria, setIsLoadingCriteria] = useState(false); - const [showBreakdown, setShowBreakdown] = useState(false); - - const sub = submission as any; - const participant = sub.participant || sub; + const sub = submission; + const participant = sub.participant; const submissionData = sub.submission || sub; - const userProfile = - participant.user?.profile || participant.submitterProfile || {}; - let userName = 'Unknown User'; - if (participant.name) { - userName = participant.name; - } else if (participant.user?.name) { - userName = participant.user.name; - } else if (userProfile.firstName || userProfile.lastName) { - userName = - `${userProfile.firstName || ''} ${userProfile.lastName || ''}`.trim(); - } else if (participant.submitterName) { - userName = participant.submitterName; - } - const userAvatar = - participant.image || - participant.user?.image || - userProfile.avatar || - userProfile.image || - participant.submitterAvatar || - ''; + const [showBreakdown, setShowBreakdown] = useState(false); + const [localAverageScore, setLocalAverageScore] = useState( + sub.averageScore !== undefined && sub.averageScore !== null + ? Number(sub.averageScore) + : null + ); + + const handleScoresLoaded = (avg: number | null) => { + if (avg !== null) { + setLocalAverageScore(avg); + } + }; - const username = - participant.username || - participant.user?.username || - userProfile.username || - participant.submitterUsername || - 'anonymous'; + useEffect(() => { + if (sub.averageScore !== undefined && sub.averageScore !== null) { + setLocalAverageScore(Number(sub.averageScore)); + } else if (hasCriteria) { + // Proactively fetch scores if backend aggregation is missing + const fetchScores = async () => { + try { + const res = await getSubmissionScores( + organizationId, + hackathonId, + sub.submission.id + ); + if (res.success && Array.isArray(res.data) && res.data.length > 0) { + const scores = res.data; + const avg = + scores.reduce( + (sum: number, s: any) => + sum + + (s.totalScore ?? + s.criteriaScores?.reduce( + (cSum: number, c: any) => cSum + (c.score || 0), + 0 + ) ?? + 0), + 0 + ) / scores.length; + setLocalAverageScore(avg); + } + } catch (err) { + // Silent fail for proactive fetch + } + }; + fetchScores(); + } + }, [ + sub.averageScore, + hasCriteria, + organizationId, + hackathonId, + participant.id, + ]); - // Try to find the email - const userEmail = - participant.email || - participant.user?.email || - participant.submitterEmail || - ''; + // New API response provides name/image/username directly on participant + const userName = participant.user?.name || 'Unknown User'; + const userAvatar = participant.user?.image || ''; + const username = participant.user?.username || 'anonymous'; + const userEmail = participant.user?.email || ''; // Determine participation type for TeamModal const participationType = useMemo(() => { - const type = - participant.participationType?.toLowerCase() || sub.type?.toLowerCase(); + const type = participant.participationType?.toLowerCase(); return type === 'team' ? 'team' : 'individual'; - }, [participant.participationType, sub.type]); + }, [participant.participationType]); // Fetch criteria when opening judge modal const handleOpenScoreModal = async (mode: 'judge' | 'organizer-override') => { if (!hasCriteria) return; // Guard clause setIsLoadingCriteria(true); try { - const response = await getJudgingCriteria(hackathonId); - const criteriaList = Array.isArray(response) ? response : []; + let criteriaList = passedCriteria; + if (criteriaList.length === 0) { + const response = await getJudgingCriteria(hackathonId); + criteriaList = Array.isArray(response) ? response : []; + } if (criteriaList.length > 0) { const validCriteria = criteriaList @@ -155,188 +188,353 @@ const JudgingParticipant = ({ } }; - return ( -
- {/* Project Image - Fixed square */} -
- {submissionData.projectName + {/* Project Image - Fixed square */} +
+ {submissionData.projectName +
+ + {/* Content Area */} +
+ {/* Project Info */} +
+
+
+ {submissionData.projectName || 'Untitled Project'} +
+ + {submissionData.category || 'General'} + + {submissionData.rank && ( +
+ + #{submissionData.rank} +
+ )} + {sub.myScore && ( + + You graded + + )} +
+

+ {submissionData.description || 'No description provided.'} +

+
+ + Submitted{' '} + {new Date( + submissionData.submissionDate || Date.now() + ).toLocaleDateString()} + +
+ + +
+ {submissionData.id && ( + <> + + + View Details + + + )} +
+
+ + {/* User Info & Actions */} +
+
+ +
+ + {/* Grade Button */} +
+ {isJudgesLoading ? ( + + ) : ( + <> + {hasCriteria && isAssignedJudge && ( + + )} + {hasCriteria && canOverrideScores && ( + + )} + + )} +
+
+
+ + {/* Breakdown & Modals */} + {showBreakdown && ( +
+ +
+ )} + +
+ ); + } - {/* Content Area */} -
- {/* Project Info */} -
-
-
- {submissionData.projectName || 'Untitled Project'} + // Compact View (New) + return ( +
+
+
+ +
+
+
+
+ {submissionData.projectName || 'Untitled'}
- - {submissionData.category || 'General'} - + • + + {userName} +
-

- {submissionData.description || 'No description provided.'} -

-
- - Submitted{' '} - {new Date( - submissionData.submissionDate || participant.registeredAt - ).toLocaleDateString()} +
+ + {submissionData.category || 'General'} -
- - -
- {/* Link */} - {submissionData.id && ( - <> - - - View Details - - + {sub.myScore && ( + + (You graded) + )} + | +
+
- {/* User Info & Actions */} -
-
- - - - {userName.charAt(0).toUpperCase()} - - -
-

- {userName} -

-

- @{username} -

-
+
+ {submissionData.rank && ( +
+ + #{submissionData.rank}
+ )} - {/* Grade Button - Only for assigned judges */} -
- {isJudgesLoading ? ( - - ) : ( - <> - {hasCriteria && isAssignedJudge && ( - - )} - {hasCriteria && canOverrideScores && ( - - )} - + {isJudgesLoading ? ( + + ) : ( +
+ {hasCriteria && isAssignedJudge && ( + + )} + {hasCriteria && canOverrideScores && ( + )} + + +
-
+ )}
- {/* Expanded Breakdown */} + {/* Breakdown Overlay */} {showBreakdown && ( -
+
)} - {/* Team Modal */} - { - // TODO: Navigate to team details page or show team information - }} - /> - - {/* Grade Submission Modal */} - { - if (onSuccess) { - onSuccess(); - } +
); }; -export default JudgingParticipant; +// Helper component to avoid duplication +const SharedModals = ({ + isTeamModalOpen, + setIsTeamModalOpen, + isScoreModalOpen, + setIsScoreModalOpen, + criteria, + scoreMode, + judges, + organizationId, + hackathonId, + sub, + participant, + submissionData, + participationType, + onSuccess, +}: any) => ( + <> + {}} + /> + + + +); + +export default memo(JudgingParticipant); diff --git a/components/organization/cards/JudgingParticipant/IndividualScoresBreakdown.tsx b/components/organization/cards/JudgingParticipant/IndividualScoresBreakdown.tsx index 8c153a02..6adf8ac0 100644 --- a/components/organization/cards/JudgingParticipant/IndividualScoresBreakdown.tsx +++ b/components/organization/cards/JudgingParticipant/IndividualScoresBreakdown.tsx @@ -18,12 +18,13 @@ import { interface IndividualScoresBreakdownProps { organizationId: string; hackathonId: string; - participantId: string; + submissionId: string; initialScores?: Array<{ judgeId: string; judgeName: string; score: number; }>; + onScoresLoaded?: (average: number | null) => void; } interface JudgeScore { @@ -43,8 +44,9 @@ interface JudgeScore { const IndividualScoresBreakdown = ({ organizationId, hackathonId, - participantId, + submissionId, initialScores, + onScoresLoaded, }: IndividualScoresBreakdownProps) => { const [scores, setScores] = useState([]); const [isLoading, setIsLoading] = useState(!initialScores); @@ -74,14 +76,13 @@ const IndividualScoresBreakdown = ({ const res = await getSubmissionScores( organizationId, hackathonId, - participantId + submissionId ); if (res.success && Array.isArray(res.data)) { // Map API response to internal state shape const mappedScores: JudgeScore[] = res.data.map((item: any) => ({ judgeId: item.judgeId, judgeName: item.judgeName, - // Ensure totalScore is available, fallback to sum of criteria scores if missing totalScore: item.totalScore ?? item.criteriaScores?.reduce( @@ -89,23 +90,50 @@ const IndividualScoresBreakdown = ({ 0 ) ?? 0, - score: item.totalScore, // Keep score for backward compatibility if needed + score: item.totalScore, comment: item.comment, criteriaScores: item.criteriaScores, })); setScores(mappedScores); + + if (onScoresLoaded) { + const avg = + mappedScores.length > 0 + ? mappedScores.reduce( + (sum, s) => sum + (s.totalScore ?? 0), + 0 + ) / mappedScores.length + : null; + onScoresLoaded(avg); + } } else if (initialScores) { - // Fallback to initialScores if API fails - setScores(normalizeInitialScores(initialScores)); + const normalized = normalizeInitialScores(initialScores); + setScores(normalized); + if (onScoresLoaded) { + const avg = + normalized.length > 0 + ? normalized.reduce((sum, s) => sum + (s.totalScore ?? 0), 0) / + normalized.length + : null; + onScoresLoaded(avg); + } } } catch (err) { reportError(err, { context: 'judging-individualScores', - participantId, + submissionId, }); - // Fallback to initialScores if fetch fails if (initialScores) { - setScores(normalizeInitialScores(initialScores)); + const normalized = normalizeInitialScores(initialScores); + setScores(normalized); + if (onScoresLoaded) { + const avg = + normalized.length > 0 + ? normalized.reduce((sum, s) => sum + (s.totalScore ?? 0), 0) / + normalized.length + : null; + onScoresLoaded(avg); + } } } finally { setIsLoading(false); @@ -114,7 +142,7 @@ const IndividualScoresBreakdown = ({ fetchScores(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [organizationId, hackathonId, participantId]); + }, [organizationId, hackathonId, submissionId]); const toggleExpand = (judgeId: string) => { setExpandedJudges(prev => ({ diff --git a/components/organization/cards/MetricsCard.tsx b/components/organization/cards/MetricsCard.tsx index b8f0e275..1ba7d6d8 100644 --- a/components/organization/cards/MetricsCard.tsx +++ b/components/organization/cards/MetricsCard.tsx @@ -7,8 +7,9 @@ interface MetricsCardProps { title: string; value: string | number; subtitle?: string; - icon?: string; + icon?: string | React.ReactNode; showTrend?: boolean; + className?: string; } const MetricsCard = ({ @@ -17,6 +18,7 @@ const MetricsCard = ({ subtitle, icon = '/user-border.svg', showTrend = false, + className, }: MetricsCardProps) => { const formattedValue = typeof value === 'number' ? value.toLocaleString() : value; @@ -27,13 +29,17 @@ const MetricsCard = ({ style={{ backgroundImage: 'url("/metric-image.svg")' }} > - Metrics + {typeof icon === 'string' ? ( + Metrics + ) : ( + icon + )}
{title}
diff --git a/components/organization/cards/Participant.tsx b/components/organization/cards/Participant.tsx index c62bf112..750670bb 100644 --- a/components/organization/cards/Participant.tsx +++ b/components/organization/cards/Participant.tsx @@ -150,6 +150,7 @@ const Participant = ({ onOpenChange={setIsJudgeModalOpen} organizationId={organizationId} hackathonId={hackathonId} + submissionId={participant.submission.id} participantId={participant.id} judgingCriteria={criteria} submission={{ diff --git a/components/organization/hackathons/judging/JudgingResultsTable.tsx b/components/organization/hackathons/judging/JudgingResultsTable.tsx index 39dc095a..fe8eebf0 100644 --- a/components/organization/hackathons/judging/JudgingResultsTable.tsx +++ b/components/organization/hackathons/judging/JudgingResultsTable.tsx @@ -21,6 +21,10 @@ import { cn } from '@/lib/utils'; import IndividualScoresBreakdown from '@/components/organization/cards/JudgingParticipant/IndividualScoresBreakdown'; import AggregatedCriteriaBreakdown from './AggregatedCriteriaBreakdown'; import { JudgingCriterion } from '@/lib/api/hackathons/judging'; +import { updateHackathon, getHackathon } from '@/lib/api/hackathons'; +import { toast } from 'sonner'; +import { Input } from '@/components/ui/input'; +import { Loader2 } from 'lucide-react'; interface JudgingResultsTableProps { results: JudgingResult[]; @@ -28,6 +32,8 @@ interface JudgingResultsTableProps { hackathonId: string; totalJudges?: number; criteria?: JudgingCriterion[]; + canManage?: boolean; + winnerOverrides?: Record; } // Helper function to safely extract score from JudgingResult @@ -41,10 +47,31 @@ const JudgingResultsTable = ({ hackathonId, totalJudges, criteria = [], + canManage = false, + winnerOverrides: initialWinnerOverrides = {}, }: JudgingResultsTableProps) => { const [expandedRows, setExpandedRows] = React.useState< Record >({}); + const [winnerOverrides, setWinnerOverrides] = React.useState< + Record + >({}); + const [isUpdating, setIsUpdating] = React.useState(null); + + // Initialize overrides from props or results + React.useEffect(() => { + const overrides: Record = { + ...initialWinnerOverrides, + }; + + // Fallback to results[].rank if not in overrides (for published results) + results.forEach(res => { + if (res.rank && !overrides[res.submissionId]) { + overrides[res.submissionId] = res.rank; + } + }); + setWinnerOverrides(overrides); + }, [results, initialWinnerOverrides]); const toggleRow = (id: string) => { setExpandedRows(prev => ({ @@ -53,11 +80,46 @@ const JudgingResultsTable = ({ })); }; - const sortedResults = React.useMemo(() => { - return [...results].sort((a, b) => { - return getScore(b) - getScore(a); - }); - }, [results]); + const sortedResults = results; + + const handleOverrideRank = async ( + submissionId: string, + rank: number | null + ) => { + try { + setIsUpdating(submissionId); + setWinnerOverrides(prev => ({ ...prev, [submissionId]: rank })); + + // Fetch current hackathon to get latest state + const hackathonRes = await getHackathon(hackathonId); + const hackathonData = hackathonRes.data; + + // Update overrides record + const updatedOverrides = { + ...(hackathonData.winnerOverrides || {}), + }; + + if (rank === null || isNaN(rank)) { + delete updatedOverrides[submissionId]; + } else { + updatedOverrides[submissionId] = rank; + } + + await updateHackathon(hackathonId, { + rewards: { + prizeTiers: (hackathonData.prizeTiers as any) || [], + winnerOverrides: updatedOverrides, + }, + }); + + toast.success('Manual rank updated'); + } catch (error: any) { + console.error('Failed to update rank override:', error); + toast.error('Failed to update manual rank'); + } finally { + setIsUpdating(null); + } + }; const getRankIcon = (index: number) => { switch (index) { @@ -85,6 +147,11 @@ const JudgingResultsTable = ({ Participation + {canManage && ( + + Manual Rank + + )} @@ -200,10 +267,36 @@ const JudgingResultsTable = ({
+ {canManage && ( + e.stopPropagation()} + > +
+ {isUpdating === result.submissionId ? ( + + ) : ( + { + const val = e.target.value + ? parseInt(e.target.value) + : null; + handleOverrideRank(result.submissionId, val); + }} + /> + )} +
+
+ )} {expandedRows[result.submissionId] && ( - +
{/* Aggregated Criteria Breakdown */} {result.criteriaBreakdown && @@ -218,7 +311,7 @@ const JudgingResultsTable = ({
@@ -231,7 +324,7 @@ const JudgingResultsTable = ({ {results.length === 0 && ( No judging results available yet. Results appear once judges diff --git a/lib/api/hackathons.ts b/lib/api/hackathons.ts index 57a21c0b..0edbf693 100644 --- a/lib/api/hackathons.ts +++ b/lib/api/hackathons.ts @@ -121,6 +121,7 @@ export interface PrizeTier { export interface HackathonRewards { prizeTiers: PrizeTier[]; + winnerOverrides?: Record; // submissionId -> rank } // Judging Tab Types @@ -432,6 +433,7 @@ export type Hackathon = { publishedAt: string; createdAt: string; updatedAt: string; + winnerOverrides?: Record; // Added for manual rank overrides participants: Participant[]; _count: { @@ -475,7 +477,9 @@ export interface PublishHackathonRequest extends Hackathon { escrowDetails?: object; } -export type UpdateHackathonRequest = Partial; +export type UpdateHackathonRequest = Partial & { + rewards?: HackathonRewards; +}; // Response Types export interface CreateDraftResponse extends ApiResponse { diff --git a/lib/api/hackathons/judging.ts b/lib/api/hackathons/judging.ts index 41a27bed..eff483a5 100644 --- a/lib/api/hackathons/judging.ts +++ b/lib/api/hackathons/judging.ts @@ -81,6 +81,7 @@ export interface JudgingResult { isPending: boolean; hasDisagreement: boolean; prize?: string; + overriddenRank?: number; // Added to track manual overrides } export interface AggregatedJudgingResults { @@ -90,7 +91,9 @@ export interface AggregatedJudgingResults { submissionsPendingCount: number; averageScoreAcrossAll: number; judgesAssigned: number; + resultsPublished: boolean; results: JudgingResult[]; + winnerOverrides?: Record; // submissionId -> rank generatedAt: string; metadata: { sortedBy: string; @@ -98,27 +101,29 @@ export interface AggregatedJudgingResults { includesIndividualScores: boolean; includesProgressTracking: boolean; }; + pagination?: { + page: number; + limit: number; + total: number; + totalPages: number; + }; } export interface JudgingSubmission { participant: { id: string; userId: string; - hackathonId: string; - organizationId: string; user: { id: string; - profile: { - firstName: string; - lastName: string; - username: string; - avatar?: string; - }; email: string; + name: string; + username: string; + image?: string; }; - participationType: 'individual' | 'team'; + participationType: string; teamId?: string; teamName?: string; + teamMembers?: any[]; }; submission: { id: string; @@ -130,11 +135,11 @@ export interface JudgingSubmission { introduction?: string; links?: Array<{ type: string; url: string }>; submissionDate: string; - status: 'shortlisted'; + status: string; rank?: number; + socialLinks?: any; }; - criteria: JudgingCriterion[]; - scores: JudgeScore[]; + myScore?: IndividualJudgeScore; averageScore: number | null; judgeCount: number; } @@ -229,6 +234,7 @@ export interface SubmitJudgingScoreRequest { export interface OverrideSubmissionScoreRequest { criteriaScores: CriterionScoreRequest[]; judgeId?: string; + notes?: string; } export interface OverrideSubmissionScoreResponse extends ApiResponse<{ @@ -363,13 +369,20 @@ export const getJudgingSubmissions = async ( hackathonId: string, page = 1, limit = 10, - status?: string + status?: string, + search?: string, + sortBy?: string, + order?: 'asc' | 'desc' ): Promise => { const params = new URLSearchParams({ page: page.toString(), limit: limit.toString(), }); + if (search) params.append('search', search); + if (sortBy) params.append('sortBy', sortBy); + if (order) params.append('order', order); + // Default to SHORTLISTED for backwards-compatibility, passing 'all' bypasses it const filterStatus = status === 'all' ? undefined : status || 'SHORTLISTED'; if (filterStatus) { @@ -404,12 +417,12 @@ export const submitGrade = async ( export const getSubmissionScores = async ( organizationId: string, hackathonId: string, - participantId: string + submissionId: string ): Promise => { const res = await api.get< IndividualJudgeScore[] | ApiResponse >( - `/organizations/${organizationId}/hackathons/${hackathonId}/judging/submissions/${participantId}/scores` + `/organizations/${organizationId}/hackathons/${hackathonId}/judging/submissions/${submissionId}/scores` ); // Handle raw array response format @@ -469,12 +482,26 @@ export const overrideSubmissionScore = async ( */ export const getJudgingResults = async ( organizationId: string, - hackathonId: string + hackathonId: string, + page = 1, + limit = 10, + search?: string, + sortBy?: string, + order?: 'asc' | 'desc' ): Promise => { + const params = new URLSearchParams({ + page: page.toString(), + limit: limit.toString(), + }); + + if (search) params.append('search', search); + if (sortBy) params.append('sortBy', sortBy); + if (order) params.append('order', order); + const res = await api.get< AggregatedJudgingResults | ApiResponse >( - `/organizations/${organizationId}/hackathons/${hackathonId}/judging/results` + `/organizations/${organizationId}/hackathons/${hackathonId}/judging/results?${params.toString()}` ); // Handle case where backend returns AggregatedJudgingResults directly vs ApiResponse wrapped @@ -547,12 +574,26 @@ export const getHackathonJudges = async ( */ export const getJudgingWinners = async ( organizationId: string, - hackathonId: string + hackathonId: string, + page = 1, + limit = 10, + search?: string, + sortBy?: string, + order?: 'asc' | 'desc' ): Promise => { + const params = new URLSearchParams({ + page: page.toString(), + limit: limit.toString(), + }); + + if (search) params.append('search', search); + if (sortBy) params.append('sortBy', sortBy); + if (order) params.append('order', order); + const res = await api.get<{ data: JudgingResult[] | AggregatedJudgingResults; }>( - `/organizations/${organizationId}/hackathons/${hackathonId}/judging/winners` + `/organizations/${organizationId}/hackathons/${hackathonId}/judging/winners?${params.toString()}` ); const payload = res.data?.data; @@ -592,3 +633,39 @@ export const publishJudgingResults = async ( ); return res.data; }; + +/** + * Optimized endpoint for judges to get their assigned submissions with criteria and scores + */ +export const getJudgingSubmissionsForJudge = async ( + hackathonId: string, + page = 1, + limit = 10, + search?: string, + sortBy?: string, + order?: 'asc' | 'desc' +): Promise< + ApiResponse<{ + submissions: JudgingSubmission[]; + criteria: JudgingCriterion[]; + pagination: { + page: number; + limit: number; + total: number; + totalPages: number; + }; + }> +> => { + const params = new URLSearchParams({ + page: page.toString(), + limit: limit.toString(), + }); + if (search) params.append('search', search); + if (sortBy) params.append('sortBy', sortBy); + if (order) params.append('order', order); + + const res = await api.get( + `/hackathons/${hackathonId}/judging/submissions?${params.toString()}` + ); + return res.data; +}; From 1f273dce78bb902a545d59051b30bf6a8ce8a694 Mon Sep 17 00:00:00 2001 From: Collins Ikechukwu Date: Fri, 20 Mar 2026 22:38:00 +0100 Subject: [PATCH 2/3] chore: Update `package-lock.json` to reflect dependency changes. --- package-lock.json | 153 +++++++++++++++++++++++----------------------- 1 file changed, 75 insertions(+), 78 deletions(-) diff --git a/package-lock.json b/package-lock.json index d1f366f9..f67fd6df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -243,7 +243,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -471,7 +470,6 @@ "version": "1.4.18", "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.4.18.tgz", "integrity": "sha512-q+awYgC7nkLEBdx2sW0iJjkzgSHlIxGnOpsN1r/O1+a4m7osJNHtfK2mKJSL1I+GfNyIlxJF8WvD/NLuYMpmcg==", - "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.3.5" @@ -501,14 +499,12 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.3.0.tgz", "integrity": "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@better-fetch/fetch": { "version": "1.1.21", "resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.21.tgz", - "integrity": "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==", - "peer": true + "integrity": "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==" }, "node_modules/@braintree/sanitize-url": { "version": "7.1.2", @@ -599,7 +595,6 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", - "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -895,7 +890,6 @@ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", "license": "MIT", - "peer": true, "dependencies": { "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" @@ -1511,6 +1505,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -1788,7 +1783,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -1810,7 +1804,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.6.0.tgz", "integrity": "sha512-L8UyDwqpTcbkIK5cgwDRDYDoEhQoj8wp8BwsO19w3LB1Z41yEQm2VJyNfAi9DrLP/YTqXqWpKHyZfR9/tFYo1Q==", "license": "Apache-2.0", - "peer": true, "engines": { "node": "^18.19.0 || >=20.6.0" }, @@ -1823,7 +1816,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.0.tgz", "integrity": "sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -2247,7 +2239,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.0.tgz", "integrity": "sha512-D4y/+OGe3JSuYUCBxtH5T9DSAWNcvCb/nQWIga8HNtXTVPQn59j0nTBAgaAXxUVBDl40mG3Tc76b46wPlZaiJQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "2.6.0", "@opentelemetry/semantic-conventions": "^1.29.0" @@ -2264,7 +2255,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.6.0.tgz", "integrity": "sha512-g/OZVkqlxllgFM7qMKqbPV9c1DUPhQ7d4n3pgZFcrnrNft9eJXZM2TNHTPYREJBrtNdRytYyvwjgL5geDKl3EQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "2.6.0", "@opentelemetry/resources": "2.6.0", @@ -2282,7 +2272,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=14" } @@ -4243,7 +4232,6 @@ "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.5.0.tgz", "integrity": "sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.17.8", "@types/webxr": "*", @@ -4347,7 +4335,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -5585,7 +5572,6 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", "license": "MIT", - "peer": true, "dependencies": { "@tanstack/query-core": "5.90.20" }, @@ -5652,7 +5638,6 @@ "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.19.0.tgz", "integrity": "sha512-bpqELwPW+DG8gWiD8iiFtSl4vIBooG5uVJod92Qxn3rA9nFatyXRr4kNbMJmOZ66ezUvmCjXVe/5/G4i5cyzKA==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -5888,7 +5873,6 @@ "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.19.0.tgz", "integrity": "sha512-N6nKbFB2VwMsPlCw67RlAtYSK48TAsAUgjnD+vd3ieSlIufdQnLXDFUP6hFKx9mwoUVUgZGz02RA6bkxOdYyTw==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -5994,7 +5978,6 @@ "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.19.0.tgz", "integrity": "sha512-ZmGUhLbMWaGqnJh2Bry+6V4M6gMpUDYo4D1xNux5Gng/E/eYtc+PMxMZ/6F7tNTAuujLBOQKj6D+4SsSm457jw==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -6009,7 +5992,6 @@ "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.19.0.tgz", "integrity": "sha512-789zcnM4a8OWzvbD2DL31d0wbSm9BVeO/R7PLQwLIGysDI3qzrcclyZ8yhqOEVuvPitRRwYLq+mY14jz7kY4cw==", "license": "MIT", - "peer": true, "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", @@ -6399,6 +6381,7 @@ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -6409,6 +6392,7 @@ "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "license": "MIT", + "peer": true, "dependencies": { "@types/eslint": "*", "@types/estree": "*" @@ -6588,7 +6572,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -6598,7 +6581,6 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -6641,7 +6623,6 @@ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.182.0.tgz", "integrity": "sha512-WByN9V3Sbwbe2OkWuSGyoqQO8Du6yhYaXtXLoA5FkKTUJorZ+yOHBZ35zUUPQXlAKABZmbYp5oAqpA4RBjtJ/Q==", "license": "MIT", - "peer": true, "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", @@ -6743,7 +6724,6 @@ "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -7176,6 +7156,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" @@ -7185,25 +7166,29 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", @@ -7214,13 +7199,15 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -7233,6 +7220,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "license": "MIT", + "peer": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } @@ -7242,6 +7230,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@xtuc/long": "4.2.2" } @@ -7250,13 +7239,15 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -7273,6 +7264,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", @@ -7286,6 +7278,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -7298,6 +7291,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", @@ -7312,6 +7306,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" @@ -7327,20 +7322,21 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7362,6 +7358,7 @@ "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.13.0" }, @@ -7400,6 +7397,7 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "license": "MIT", + "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -7417,6 +7415,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -7432,7 +7431,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/ansi-escapes": { "version": "7.3.0", @@ -7918,7 +7918,6 @@ "resolved": "https://registry.npmjs.org/better-call/-/better-call-1.1.8.tgz", "integrity": "sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw==", "license": "MIT", - "peer": true, "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", @@ -8001,7 +8000,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -8044,7 +8042,8 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/call-bind": { "version": "1.0.8", @@ -8200,7 +8199,6 @@ "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.2.tgz", "integrity": "sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "11.1.2", "@chevrotain/gast": "11.1.2", @@ -8227,6 +8225,7 @@ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.0" } @@ -8493,7 +8492,6 @@ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10" } @@ -8903,7 +8901,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -9057,7 +9054,6 @@ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/kossnocorp" @@ -9322,8 +9318,7 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/embla-carousel-autoplay": { "version": "8.6.0", @@ -9556,7 +9551,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/es-object-atoms": { "version": "1.1.1", @@ -9675,7 +9671,6 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -9736,7 +9731,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -10108,6 +10102,7 @@ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.8.x" } @@ -10189,7 +10184,8 @@ "url": "https://opencollective.com/fastify" } ], - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/feaxios": { "version": "0.0.23", @@ -10555,7 +10551,8 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "peer": true }, "node_modules/glob/node_modules/balanced-match": { "version": "4.0.4", @@ -10688,8 +10685,7 @@ "version": "3.14.2", "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.14.2.tgz", "integrity": "sha512-P8/mMxVLU7o4+55+1TCnQrPmgjPKnwkzkXOK1asnR9Jg2lna4tEY5qBJjMmAaOBDDZWtlRjBXjLa0w53G/uBLA==", - "license": "Standard 'no charge' license: https://gsap.com/standard-license.", - "peer": true + "license": "Standard 'no charge' license: https://gsap.com/standard-license." }, "node_modules/hachure-fill": { "version": "0.5.2", @@ -11830,6 +11826,7 @@ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -11844,6 +11841,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "license": "MIT", + "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -11868,7 +11866,6 @@ "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/panva" } @@ -11924,7 +11921,8 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/json-schema-traverse": { "version": "0.4.1", @@ -12018,11 +12016,10 @@ } }, "node_modules/kysely": { - "version": "0.28.13", - "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.13.tgz", - "integrity": "sha512-jCkYDvlfzOyHaVsrvR4vnNZxG30oNv2jbbFBjTQAUG8n0h07HW0sZJHk4KAQIRyu9ay+Rg+L8qGa3lwt8Gve9w==", + "version": "0.28.14", + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.14.tgz", + "integrity": "sha512-SU3lgh0rPvq7upc6vvdVrCsSMUG1h3ChvHVOY7wJ2fw4C9QEB7X3d5eyYEyULUX7UQtxZJtZXGuT6U2US72UYA==", "license": "MIT", - "peer": true, "engines": { "node": ">=20.0.0" } @@ -12385,6 +12382,7 @@ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.11.5" }, @@ -12890,7 +12888,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/mermaid": { "version": "11.12.3", @@ -13842,7 +13841,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": "^20.0.0 || >=22.0.0" } @@ -13858,14 +13856,14 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/next": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/next/-/next-16.2.0.tgz", "integrity": "sha512-NLBVrJy1pbV1Yn00L5sU4vFyAHt5XuSjzrNyFnxo6Com0M0KrL6hHM5B99dbqXb2bE9pm4Ow3Zl1xp6HVY9edQ==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "16.2.0", "@swc/helpers": "0.5.15", @@ -14661,7 +14659,6 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -14692,7 +14689,6 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -14955,7 +14951,6 @@ "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==", "license": "MIT", - "peer": true, "dependencies": { "orderedmap": "^2.0.0" } @@ -14985,7 +14980,6 @@ "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", "license": "MIT", - "peer": true, "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0", @@ -15034,7 +15028,6 @@ "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.6.tgz", "integrity": "sha512-mxpcDG4hNQa/CPtzxjdlir5bJFDlm0/x5nGBbStB2BWX+XOQ9M8ekEG+ojqB5BcVu2Rc80/jssCMZzSstJuSYg==", "license": "MIT", - "peer": true, "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", @@ -15304,7 +15297,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -15335,7 +15327,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -15365,7 +15356,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz", "integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -16104,7 +16094,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -16266,6 +16255,7 @@ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "license": "MIT", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -16302,6 +16292,7 @@ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -16313,7 +16304,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/section-matter": { "version": "1.0.0", @@ -16657,6 +16649,7 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "license": "MIT", + "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -16667,6 +16660,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "license": "BSD-3-Clause", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -17028,8 +17022,7 @@ "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tapable": { "version": "2.3.0", @@ -17049,6 +17042,7 @@ "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -17067,6 +17061,7 @@ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.17.tgz", "integrity": "sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==", "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", @@ -17099,14 +17094,14 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/three": { "version": "0.180.0", "resolved": "https://registry.npmjs.org/three/-/three-0.180.0.tgz", "integrity": "sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/three-mesh-bvh": { "version": "0.8.3", @@ -17196,7 +17191,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -17487,7 +17481,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -17956,6 +17949,7 @@ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "license": "MIT", + "peer": true, "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -17996,6 +17990,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz", "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -18044,6 +18039,7 @@ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.13.0" } @@ -18053,6 +18049,7 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "license": "BSD-2-Clause", + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -18066,6 +18063,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=4.0" } @@ -18291,7 +18289,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } From 40790b7774f7c36e89933df72ace8344f8a8ec64 Mon Sep 17 00:00:00 2001 From: Collins Ikechukwu Date: Sat, 21 Mar 2026 00:29:03 +0100 Subject: [PATCH 3/3] feat: Display winner's logo in `TopWinnerCard` by updating its source and adding a `logo` field to the `HackathonWinner` interface. --- .../components/tabs/contents/winners/TopWinnerCard.tsx | 9 ++++----- lib/api/hackathons.ts | 1 + 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/(landing)/hackathons/[slug]/components/tabs/contents/winners/TopWinnerCard.tsx b/app/(landing)/hackathons/[slug]/components/tabs/contents/winners/TopWinnerCard.tsx index 1f81e040..d1b5cea7 100644 --- a/app/(landing)/hackathons/[slug]/components/tabs/contents/winners/TopWinnerCard.tsx +++ b/app/(landing)/hackathons/[slug]/components/tabs/contents/winners/TopWinnerCard.tsx @@ -11,18 +11,18 @@ interface TopWinnerCardProps { } export const TopWinnerCard = ({ winner, submission }: TopWinnerCardProps) => { - const bannerUrl = submission?.logo || '/images/default-project-banner.png'; // Fallback to logo or default + const bannerUrl = winner?.logo || '/images/default-project-banner.png'; // Fallback to logo or default return (
- {/* Project Visual */}
- {submission?.logo ? ( + {bannerUrl ? ( {winner.projectName} ) : ( @@ -32,7 +32,6 @@ export const TopWinnerCard = ({ winner, submission }: TopWinnerCardProps) => { )}
- {/* Project Info */}
diff --git a/lib/api/hackathons.ts b/lib/api/hackathons.ts index 0edbf693..52b95d3b 100644 --- a/lib/api/hackathons.ts +++ b/lib/api/hackathons.ts @@ -2924,6 +2924,7 @@ export const toggleRoleHired = async ( export interface HackathonWinner { rank: number; projectName: string; + logo: string; projectId?: string; teamName: string | null; participants: Array<{