From ed65b36b7365437a8b0aa5b658bda32c158bbf19 Mon Sep 17 00:00:00 2001 From: Nnaji Benjamin <60315147+Benjtalkshow@users.noreply.github.com> Date: Mon, 16 Mar 2026 14:31:09 +0100 Subject: [PATCH 1/2] Pr 486 (#488) * feat: Implement comprehensive hackathon detail page with new header, sidebar, and tabbed content sections including teams, announcements, winners, participants, and resources. * fix: update coderabit * feat: Enhance hackathon team invitation flow, recruitment post management, and dynamic tab visibility. * feat: Integrate AuthModalProvider for authentication handling and enhance messaging features across hackathon components * fix: fix pagination issues in particiapants and submissions page for organizers * fix: remove any type --------- Co-authored-by: Collins Ikechukwu --- .../[hackathonId]/participants/page.tsx | 46 ++++++++- .../[hackathonId]/submissions/page.tsx | 96 +++++++++++++++---- .../submissions/SubmissionDetailModal.tsx | 10 +- components/organization/cards/Participant.tsx | 2 +- .../hackathons/ParticipantsGrid.tsx | 4 +- .../hackathons/ParticipantsTable.tsx | 23 +++-- .../submissions/SubmissionsManagement.tsx | 63 +++--------- hooks/hackathon/use-organizer-submissions.ts | 69 +++++++++---- hooks/hackathon/use-register-hackathon.ts | 2 +- hooks/use-hackathons.ts | 24 +++-- hooks/use-submission-actions.ts | 4 +- lib/api/hackathons.ts | 9 +- 12 files changed, 230 insertions(+), 122 deletions(-) diff --git a/app/(landing)/organizations/[id]/hackathons/[hackathonId]/participants/page.tsx b/app/(landing)/organizations/[id]/hackathons/[hackathonId]/participants/page.tsx index b5a17ef1..e475a6a9 100644 --- a/app/(landing)/organizations/[id]/hackathons/[hackathonId]/participants/page.tsx +++ b/app/(landing)/organizations/[id]/hackathons/[hackathonId]/participants/page.tsx @@ -43,8 +43,8 @@ interface FilterState { const mapFiltersToParams = (filters: FilterState, searchOverride?: string) => ({ search: searchOverride !== undefined ? searchOverride : filters.search, - status: filters.status === 'all' ? undefined : filters.status, - type: filters.type === 'all' ? undefined : filters.type, + status: filters.status === 'all' ? undefined : filters.status.toUpperCase(), + type: filters.type === 'all' ? undefined : filters.type.toUpperCase(), }); const ParticipantsPage: React.FC = () => { @@ -228,9 +228,45 @@ const ParticipantsPage: React.FC = () => { } }; + // Frontend-side filtering as per requirement + const filteredParticipants = useMemo(() => { + return participants.filter(participant => { + // Search: filter by name or username + const search = filters.search.toLowerCase(); + const matchesSearch = search + ? (participant.user?.profile?.name || '') + .toLowerCase() + .includes(search) || + (participant.user?.profile?.username || '') + .toLowerCase() + .includes(search) + : true; + + // Status: filter by participant.submission.status + // Filter values are 'submitted', 'not_submitted', etc. + // ParticipantSubmission.status values are 'submitted', 'shortlisted', etc. + const matchesStatus = + filters.status === 'all' + ? true + : filters.status === 'not_submitted' + ? !participant.submission + : participant.submission?.status?.toLowerCase() === + filters.status.toLowerCase(); + + // Type: filter by participant.participationType + const matchesType = + filters.type === 'all' + ? true + : participant.participationType?.toLowerCase() === + filters.type.toLowerCase(); + + return matchesSearch && matchesStatus && matchesType; + }); + }, [participants, filters.search, filters.status, filters.type]); + // Mock table instance for DataTablePagination const table = useReactTable({ - data: participants, + data: filteredParticipants, columns: [], // Not used for rendering here getCoreRowModel: getCoreRowModel(), manualPagination: true, @@ -316,7 +352,7 @@ const ParticipantsPage: React.FC = () => {
{view === 'table' ? ( { /> ) : ( (null); @@ -34,7 +41,6 @@ export default function SubmissionsPage() { useEffect(() => { if (hackathonId) { - fetchSubmissions(); const fetchHackathonDetails = async () => { try { const res = await getHackathon(hackathonId); @@ -50,7 +56,7 @@ export default function SubmissionsPage() { }; fetchHackathonDetails(); } - }, [hackathonId, fetchSubmissions]); + }, [hackathonId]); useEffect(() => { const fetchSession = async () => { @@ -66,6 +72,52 @@ export default function SubmissionsPage() { fetchSession(); }, []); + // Frontend-side filtering + const filteredSubmissions = useMemo(() => { + return allSubmissions.filter(sub => { + const search = filters.search?.toLowerCase() || ''; + const matchesSearch = search + ? (sub.projectName || '').toLowerCase().includes(search) || + (sub.participant?.name || '').toLowerCase().includes(search) || + (sub.participant?.username || '').toLowerCase().includes(search) + : true; + + const matchesStatus = !filters.status || sub.status === filters.status; + + const matchesType = + !filters.type || sub.participationType === filters.type; + + return matchesSearch && matchesStatus && matchesType; + }); + }, [allSubmissions, filters.search, filters.status, filters.type]); + + const table = useReactTable({ + data: filteredSubmissions, + columns: [], + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + pageCount: pagination.totalPages, + state: { + pagination: { + pageIndex: pagination.page - 1, + pageSize: pagination.limit, + }, + }, + onPaginationChange: updater => { + if (typeof updater === 'function') { + const newState = updater({ + pageIndex: pagination.page - 1, + pageSize: pagination.limit, + }); + if (newState.pageSize !== pagination.limit) { + updateLimit(newState.pageSize); + } else { + goToPage(newState.pageIndex + 1); + } + } + }, + }); + if (error) { return (
@@ -100,7 +152,7 @@ export default function SubmissionsPage() { {/* Main Content */}
- {loading && submissions.length === 0 ? ( + {loading && allSubmissions.length === 0 ? (
@@ -108,19 +160,25 @@ export default function SubmissionsPage() {
) : ( - +
+ + +
+ +
+
)}
diff --git a/components/hackathons/submissions/SubmissionDetailModal.tsx b/components/hackathons/submissions/SubmissionDetailModal.tsx index 3cb2bf64..99413574 100644 --- a/components/hackathons/submissions/SubmissionDetailModal.tsx +++ b/components/hackathons/submissions/SubmissionDetailModal.tsx @@ -166,16 +166,16 @@ export function SubmissionDetailModal({
- {submission.status === 'shortlisted' + {submission.status === 'SHORTLISTED' ? 'Shortlisted' - : submission.status === 'disqualified' + : submission.status === 'DISQUALIFIED' ? 'Disqualified' : 'Submitted'} @@ -295,7 +295,7 @@ export function SubmissionDetailModal({
{/* Disqualification Reason */} - {submission.status === 'disqualified' && + {submission.status === 'DISQUALIFIED' && submission.disqualificationReason && (

diff --git a/components/organization/cards/Participant.tsx b/components/organization/cards/Participant.tsx index 8d527083..c62bf112 100644 --- a/components/organization/cards/Participant.tsx +++ b/components/organization/cards/Participant.tsx @@ -45,7 +45,7 @@ const Participant = ({ // Check if submission is shortlisted const isShortlisted = useMemo(() => { - return participant.submission?.status === 'shortlisted'; + return participant.submission?.status === 'SHORTLISTED'; }, [participant.submission?.status]); // Fetch criteria when opening judge modal diff --git a/components/organization/hackathons/ParticipantsGrid.tsx b/components/organization/hackathons/ParticipantsGrid.tsx index 42d3a4ed..72281d88 100644 --- a/components/organization/hackathons/ParticipantsGrid.tsx +++ b/components/organization/hackathons/ParticipantsGrid.tsx @@ -67,8 +67,8 @@ export const ParticipantsGrid: React.FC = ({ const user = participant.user; const submission = participant.submission; const hasSubmission = !!submission; - const isShortlisted = submission?.status === 'shortlisted'; - const isDisqualified = submission?.status === 'disqualified'; + const isShortlisted = submission?.status === 'SHORTLISTED'; + const isDisqualified = submission?.status === 'DISQUALIFIED'; const isTeam = participant.participationType === 'team'; const votesCount = Array.isArray(submission?.votes) diff --git a/components/organization/hackathons/ParticipantsTable.tsx b/components/organization/hackathons/ParticipantsTable.tsx index 1ab5a3ee..c1e141b8 100644 --- a/components/organization/hackathons/ParticipantsTable.tsx +++ b/components/organization/hackathons/ParticipantsTable.tsx @@ -110,22 +110,25 @@ export function ParticipantsTable({ ); } - const status = submission.status; + const status = submission.status as + | 'SHORTLISTED' + | 'DISQUALIFIED' + | 'SUBMITTED'; return ( - {status === 'shortlisted' + {status === 'SHORTLISTED' ? 'Shortlisted' - : status === 'disqualified' + : status === 'DISQUALIFIED' ? 'Disqualified' : 'Submitted'} @@ -150,7 +153,7 @@ export function ParticipantsTable({ const participant = row.original; const hasSubmission = !!participant.submission; const isShortlisted = - participant.submission?.status === 'shortlisted'; + participant.submission?.status === 'SHORTLISTED'; return ( diff --git a/components/organization/hackathons/submissions/SubmissionsManagement.tsx b/components/organization/hackathons/submissions/SubmissionsManagement.tsx index f75edeab..3a55e8a2 100644 --- a/components/organization/hackathons/submissions/SubmissionsManagement.tsx +++ b/components/organization/hackathons/submissions/SubmissionsManagement.tsx @@ -32,11 +32,7 @@ import { useUpdateRank } from '@/hooks/hackathon/use-update-rank'; /* Types */ /* -------------------------------------------------------------------------- */ -type SubmissionStatus = - | 'SUBMITTED' - | 'SHORTLISTED' - | 'DISQUALIFIED' - | 'WITHDRAWN'; +type SubmissionStatus = 'SUBMITTED' | 'SHORTLISTED' | 'DISQUALIFIED'; type SubmissionType = 'INDIVIDUAL' | 'TEAM'; @@ -58,6 +54,7 @@ interface SubmissionsManagementProps { loading: boolean; onFilterChange: (filters: SubmissionFilters) => void; onPageChange: (page: number) => void; + onPageSizeChange?: (size: number) => void; onRefresh: () => void; organizationId?: string; hackathonId?: string; @@ -83,7 +80,6 @@ export function SubmissionsManagement({ hackathon, }: SubmissionsManagementProps) { const [viewMode, setViewMode] = useState<'grid' | 'table'>('grid'); - const [searchTerm, setSearchTerm] = useState(filters.search ?? ''); const [selectedIds, setSelectedIds] = useState([]); const [showBulkDisqualifyDialog, setShowBulkDisqualifyDialog] = useState(false); @@ -143,9 +139,8 @@ export function SubmissionsManagement({ onRefresh(); }; - const handleSearchSubmit = (e: FormEvent) => { - e.preventDefault(); - onFilterChange({ ...filters, search: searchTerm }); + const handleSearchTermChange = (value: string) => { + onFilterChange({ ...filters, search: value }); }; const handleStatusChange = (value: string) => { @@ -214,18 +209,16 @@ export function SubmissionsManagement({

) : ( -
-
- - setSearchTerm(e.target.value)} - className='focus:border-primary focus:ring-primary border-gray-700/50 bg-gray-900/50 pl-10 text-white placeholder:text-gray-500' - /> -
-
+
+ + handleSearchTermChange(e.target.value)} + className='focus:border-primary focus:ring-primary border-gray-700/50 bg-gray-900/50 pl-10 text-white placeholder:text-gray-500' + /> +
)} @@ -244,7 +237,6 @@ export function SubmissionsManagement({ Submitted Shortlisted Disqualified - Withdrawn @@ -338,32 +330,7 @@ export function SubmissionsManagement({ isSubmitting={isBulkLoading} /> - {/* Pagination */} - {pagination.totalPages > 1 && ( -
- - - Page {pagination.page} of {pagination.totalPages} - - -
- )} + {/* Pagination removed - controlled externally by page.tsx */} ); } diff --git a/hooks/hackathon/use-organizer-submissions.ts b/hooks/hackathon/use-organizer-submissions.ts index 504b6eb0..f0b402fd 100644 --- a/hooks/hackathon/use-organizer-submissions.ts +++ b/hooks/hackathon/use-organizer-submissions.ts @@ -1,9 +1,10 @@ -import { useState, useCallback } from 'react'; +import { useState, useCallback, useEffect } from 'react'; import { getHackathonSubmissions } from '@/lib/api/hackathons'; import type { ParticipantSubmission } from '@/lib/api/hackathons'; +import { useDebounce } from '@/hooks/use-debounce'; export type OrganizerSubmissionFilters = { - status?: 'SUBMITTED' | 'SHORTLISTED' | 'DISQUALIFIED' | 'WITHDRAWN'; + status?: 'SUBMITTED' | 'SHORTLISTED' | 'DISQUALIFIED'; type?: 'INDIVIDUAL' | 'TEAM'; search?: string; }; @@ -33,6 +34,7 @@ export interface UseOrganizerSubmissionsReturn { ) => Promise; goToPage: (page: number) => void; refresh: () => void; + updateLimit: (limit: number) => void; } export function useOrganizerSubmissions( @@ -47,6 +49,8 @@ export function useOrganizerSubmissions( const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + const debouncedSearch = useDebounce(filters.search, 500); + const fetchSubmissions = useCallback( async (page = 1, filterOverrides?: OrganizerSubmissionFilters) => { if (!hackathonId) return; @@ -55,24 +59,32 @@ export function useOrganizerSubmissions( setError(null); try { - const appliedFilters = filterOverrides ?? filters; + const appliedFilters = filterOverrides ?? { + ...filters, + search: debouncedSearch, + }; const res = await getHackathonSubmissions( hackathonId, page, - initialLimit, + pagination.limit || initialLimit, appliedFilters ); const list = res.data?.submissions ?? []; - const pag = res.data?.pagination ?? { - page: 1, - limit: initialLimit, - total: 0, - totalPages: 0, - }; + const rawPag = res.data?.pagination; + + const currentPage = rawPag?.page || page; + const limit = rawPag?.limit || pagination.limit || initialLimit; + const total = rawPag?.total || 0; + const totalPages = rawPag?.totalPages || Math.ceil(total / limit) || 1; setSubmissions(list); - setPagination(pag); + setPagination({ + page: currentPage, + limit, + total, + totalPages, + }); } catch (err) { setError( err instanceof Error ? err.message : 'Failed to fetch submissions' @@ -83,16 +95,30 @@ export function useOrganizerSubmissions( setLoading(false); } }, - [hackathonId, initialLimit, filters] + [ + hackathonId, + initialLimit, + debouncedSearch, + pagination.limit, + filters.status, + filters.type, + ] ); - const updateFilters = useCallback( - (next: OrganizerSubmissionFilters) => { - setFilters(next); - fetchSubmissions(1, next); - }, - [fetchSubmissions] - ); + // Sync with backend on filter/pagination changes + useEffect(() => { + fetchSubmissions(1); + }, [ + fetchSubmissions, + debouncedSearch, + filters.status, + filters.type, + pagination.limit, + ]); + + const updateFilters = useCallback((next: OrganizerSubmissionFilters) => { + setFilters(next); + }, []); const goToPage = useCallback( (page: number) => { @@ -105,6 +131,10 @@ export function useOrganizerSubmissions( fetchSubmissions(pagination.page); }, [fetchSubmissions, pagination.page]); + const updateLimit = useCallback((nextLimit: number) => { + setPagination(prev => ({ ...prev, limit: nextLimit })); + }, []); + return { submissions, pagination, @@ -115,5 +145,6 @@ export function useOrganizerSubmissions( fetchSubmissions, goToPage, refresh, + updateLimit, }; } diff --git a/hooks/hackathon/use-register-hackathon.ts b/hooks/hackathon/use-register-hackathon.ts index f15a93b9..4e7cb921 100644 --- a/hooks/hackathon/use-register-hackathon.ts +++ b/hooks/hackathon/use-register-hackathon.ts @@ -122,6 +122,6 @@ export function useRegisterHackathon({ hasCheckedInitially, // Expose setters for immediate updates setParticipant, - hasSubmitted: participant?.submission?.status === 'submitted', + hasSubmitted: participant?.submission?.status === 'SUBMITTED', }; } diff --git a/hooks/use-hackathons.ts b/hooks/use-hackathons.ts index 4b56eff7..f6a79a58 100644 --- a/hooks/use-hackathons.ts +++ b/hooks/use-hackathons.ts @@ -31,8 +31,8 @@ export interface UseHackathonsOptions { search?: string; }; participantFilters?: { - status?: 'submitted' | 'not_submitted' | 'shortlisted' | 'disqualified'; - type?: 'individual' | 'team'; + status?: string; + type?: string; search?: string; }; } @@ -579,16 +579,22 @@ export function useHackathons( const pagination = (response.data?.pagination || response.meta?.pagination) as any; + const responsePage = pagination?.page || 1; + const totalItems = pagination?.total || 0; + const itemsPerPage = pagination?.limit || pageSize; + const totalPages = + pagination?.totalPages || Math.ceil(totalItems / itemsPerPage) || 1; + setParticipants(response.data?.participants || []); setParticipantsPagination({ - currentPage: pagination?.page || 1, - totalPages: pagination?.totalPages || 1, - totalItems: pagination?.total || 0, - itemsPerPage: pagination?.limit || pageSize, - hasNext: !!pagination?.hasNext, - hasPrev: !!pagination?.hasPrev, + currentPage: responsePage, + totalPages, + totalItems, + itemsPerPage, + hasNext: !!pagination?.hasNext || responsePage < totalPages, + hasPrev: !!pagination?.hasPrev || responsePage > 1, }); - participantsPageRef.current = pagination?.page || 1; + participantsPageRef.current = responsePage; } catch (error) { const errorMessage = error instanceof Error diff --git a/hooks/use-submission-actions.ts b/hooks/use-submission-actions.ts index 64216884..fd43b9d2 100644 --- a/hooks/use-submission-actions.ts +++ b/hooks/use-submission-actions.ts @@ -35,7 +35,7 @@ export const useSubmissionActions = ({ if (response.success && response.data) { const newStatus = response.data.submission?.status; - const isShortlisted = newStatus === 'shortlisted'; + const isShortlisted = newStatus === 'SHORTLISTED'; toast.success( isShortlisted @@ -84,7 +84,7 @@ export const useSubmissionActions = ({ if (response.success && response.data) { const newStatus = response.data.submission?.status; - const isDisqualified = newStatus === 'disqualified'; + const isDisqualified = newStatus === 'DISQUALIFIED'; toast.success( isDisqualified diff --git a/lib/api/hackathons.ts b/lib/api/hackathons.ts index dc223a57..a105755c 100644 --- a/lib/api/hackathons.ts +++ b/lib/api/hackathons.ts @@ -692,7 +692,14 @@ export interface ParticipantSubmission { comments?: number | ParticipantComment[]; submissionDate?: string; submittedAt?: string; - status: 'submitted' | 'shortlisted' | 'disqualified' | string; + status: SubmissionStatus; + participationType?: 'INDIVIDUAL' | 'TEAM' | 'TEAM_OR_INDIVIDUAL'; + participant?: { + id: string; + name: string; + username: string; + image?: string; + }; disqualificationReason?: string | null; reviewedBy?: { id: string; From d536c43373d6dc81e955b4d0b15176c6e63d9dac Mon Sep 17 00:00:00 2001 From: Nnaji Benjamin <60315147+Benjtalkshow@users.noreply.github.com> Date: Mon, 16 Mar 2026 22:07:58 +0100 Subject: [PATCH 2/2] Pr 486 (#490) * feat: Implement comprehensive hackathon detail page with new header, sidebar, and tabbed content sections including teams, announcements, winners, participants, and resources. * fix: update coderabit * feat: Enhance hackathon team invitation flow, recruitment post management, and dynamic tab visibility. * feat: Integrate AuthModalProvider for authentication handling and enhance messaging features across hackathon components * fix: fix pagination issues in particiapants and submissions page for organizers * fix: remove any type * fix: enhanced filtering for organizations * fix: fix coderabbit corrections --------- Co-authored-by: Collins Ikechukwu --- .../components/header/ActionButtons.tsx | 3 +- .../components/tabs/contents/Participants.tsx | 75 ++++++++++++++++--- .../[hackathonId]/participants/page.tsx | 42 +---------- .../[hackathonId]/submissions/page.tsx | 23 +----- .../hackathons/submissions/submissionCard.tsx | 2 +- .../submissions/submissionCard2.tsx | 2 +- .../organization/cards/ParticipantInfo.tsx | 2 +- .../SubmissionVotesTab.tsx | 7 +- .../ReviewSubmissionModal/TeamSection.tsx | 7 +- .../cards/ReviewSubmissionModal/types.ts | 6 +- components/organization/cards/TeamModal.tsx | 7 +- .../hackathons/ParticipantsTable.tsx | 5 +- hooks/hackathon/use-hackathon-queries.ts | 3 + hooks/hackathon/use-organizer-submissions.ts | 2 +- hooks/hackathon/use-register-hackathon.ts | 5 +- hooks/use-hackathons.ts | 3 +- hooks/use-participant-submission.ts | 6 +- lib/api/hackathon.ts | 20 ++++- lib/api/hackathons.ts | 8 +- types/hackathon/participant.ts | 14 ++-- 20 files changed, 139 insertions(+), 103 deletions(-) diff --git a/app/(landing)/hackathons/[slug]/components/header/ActionButtons.tsx b/app/(landing)/hackathons/[slug]/components/header/ActionButtons.tsx index e422e1f6..236d204a 100644 --- a/app/(landing)/hackathons/[slug]/components/header/ActionButtons.tsx +++ b/app/(landing)/hackathons/[slug]/components/header/ActionButtons.tsx @@ -38,7 +38,8 @@ const ActionButtons = () => { const isParticipant = user ? !!hackathon?.isParticipant || (hackathon?.participants || []).some( - (p: Participant) => p.userId === user.id + (p: Participant) => + (p as any).userId === user.id || p.user?.id === user.id ) : false; diff --git a/app/(landing)/hackathons/[slug]/components/tabs/contents/Participants.tsx b/app/(landing)/hackathons/[slug]/components/tabs/contents/Participants.tsx index e935ee23..f82da490 100644 --- a/app/(landing)/hackathons/[slug]/components/tabs/contents/Participants.tsx +++ b/app/(landing)/hackathons/[slug]/components/tabs/contents/Participants.tsx @@ -6,6 +6,7 @@ import { useHackathon, useHackathonParticipants, } from '@/hooks/hackathon/use-hackathon-queries'; +import { useDebounce } from '@/hooks/use-debounce'; import { TabsContent } from '@/components/ui/tabs'; import { ChevronDown, Search, Filter, Loader2 } from 'lucide-react'; import { cn } from '@/lib/utils'; @@ -53,6 +54,12 @@ const Participants = () => { 'all' | 'submitted' | 'in_progress' >('all'); const [skillFilter, setSkillFilter] = useState('all'); + const [participationType, setParticipationType] = useState< + 'all' | 'individual' | 'team' + >('all'); + const [searchQuery, setSearchQuery] = useState(''); + const debouncedSearch = useDebounce(searchQuery, 300); + const [page, setPage] = useState(1); const [accumulatedParticipants, setAccumulatedParticipants] = useState< Participant[] @@ -68,13 +75,15 @@ const Participants = () => { limit, status: statusFilter === 'all' ? undefined : statusFilter, skill: skillFilter === 'all' ? undefined : skillFilter, + type: participationType === 'all' ? undefined : participationType, + search: debouncedSearch || undefined, }); // Reset page and list when filters change React.useEffect(() => { setPage(1); setAccumulatedParticipants([]); - }, [statusFilter, skillFilter]); + }, [statusFilter, skillFilter, participationType, debouncedSearch]); // Accumulate participants as they are fetched React.useEffect(() => { @@ -133,14 +142,28 @@ const Participants = () => { > {/* Header with Count and Filters */}
-
-

Participants

-

- - {totalBuilders.toLocaleString()} - {' '} - builders competing in {hackathon.name} -

+
+
+

Participants

+

+ + {totalBuilders.toLocaleString()} + {' '} + builders competing in {hackathon.name} +

+
+ + {/* Search Bar */} +
+ setSearchQuery(e.target.value)} + className='hover:border-primary/20 focus:border-primary/30 h-10 w-full rounded-xl border border-white/5 bg-[#141517] pr-4 pl-10 text-sm text-white transition-all outline-none placeholder:text-gray-500' + /> + +
@@ -175,6 +198,38 @@ const Participants = () => { + {/* Type Filter */} + + + + + + setParticipationType('all')}> + All Types + + setParticipationType('individual')} + > + Individual + + setParticipationType('team')}> + Team + + + + {/* Status Filter */} @@ -222,7 +277,7 @@ const Participants = () => { key={p.id} name={p.user.profile.name} username={p.user.profile.username} - image={p.user.profile.image} + image={p.user.profile.image || undefined} submitted={!!p.submittedAt} skills={p.user.profile.skills} userId={p.userId ?? p.user?.id} diff --git a/app/(landing)/organizations/[id]/hackathons/[hackathonId]/participants/page.tsx b/app/(landing)/organizations/[id]/hackathons/[hackathonId]/participants/page.tsx index e475a6a9..ce74910f 100644 --- a/app/(landing)/organizations/[id]/hackathons/[hackathonId]/participants/page.tsx +++ b/app/(landing)/organizations/[id]/hackathons/[hackathonId]/participants/page.tsx @@ -228,45 +228,11 @@ const ParticipantsPage: React.FC = () => { } }; - // Frontend-side filtering as per requirement - const filteredParticipants = useMemo(() => { - return participants.filter(participant => { - // Search: filter by name or username - const search = filters.search.toLowerCase(); - const matchesSearch = search - ? (participant.user?.profile?.name || '') - .toLowerCase() - .includes(search) || - (participant.user?.profile?.username || '') - .toLowerCase() - .includes(search) - : true; - - // Status: filter by participant.submission.status - // Filter values are 'submitted', 'not_submitted', etc. - // ParticipantSubmission.status values are 'submitted', 'shortlisted', etc. - const matchesStatus = - filters.status === 'all' - ? true - : filters.status === 'not_submitted' - ? !participant.submission - : participant.submission?.status?.toLowerCase() === - filters.status.toLowerCase(); - - // Type: filter by participant.participationType - const matchesType = - filters.type === 'all' - ? true - : participant.participationType?.toLowerCase() === - filters.type.toLowerCase(); - - return matchesSearch && matchesStatus && matchesType; - }); - }, [participants, filters.search, filters.status, filters.type]); + // Removed redundant frontend-side filtering as backend now handles it. // Mock table instance for DataTablePagination const table = useReactTable({ - data: filteredParticipants, + data: participants, columns: [], // Not used for rendering here getCoreRowModel: getCoreRowModel(), manualPagination: true, @@ -352,7 +318,7 @@ const ParticipantsPage: React.FC = () => {
{view === 'table' ? ( { /> ) : ( { - return allSubmissions.filter(sub => { - const search = filters.search?.toLowerCase() || ''; - const matchesSearch = search - ? (sub.projectName || '').toLowerCase().includes(search) || - (sub.participant?.name || '').toLowerCase().includes(search) || - (sub.participant?.username || '').toLowerCase().includes(search) - : true; - - const matchesStatus = !filters.status || sub.status === filters.status; - - const matchesType = - !filters.type || sub.participationType === filters.type; - - return matchesSearch && matchesStatus && matchesType; - }); - }, [allSubmissions, filters.search, filters.status, filters.type]); - const table = useReactTable({ - data: filteredSubmissions, + data: allSubmissions, columns: [], getCoreRowModel: getCoreRowModel(), manualPagination: true, @@ -162,7 +143,7 @@ export default function SubmissionsPage() { ) : (
= ({
- + {userName.charAt(0).toUpperCase()}

{userName}

diff --git a/components/organization/cards/ReviewSubmissionModal/SubmissionVotesTab.tsx b/components/organization/cards/ReviewSubmissionModal/SubmissionVotesTab.tsx index fd67bbe8..b1105c1f 100644 --- a/components/organization/cards/ReviewSubmissionModal/SubmissionVotesTab.tsx +++ b/components/organization/cards/ReviewSubmissionModal/SubmissionVotesTab.tsx @@ -11,7 +11,7 @@ interface Voter { id: string; name: string; username: string; - avatar?: string; + avatar?: string | null; votedAt?: string; voteType?: 'positive' | 'negative'; } @@ -42,7 +42,10 @@ export const SubmissionVotesTab: React.FC = ({
- + {voter.name.charAt(0).toUpperCase()} diff --git a/components/organization/cards/ReviewSubmissionModal/TeamSection.tsx b/components/organization/cards/ReviewSubmissionModal/TeamSection.tsx index 627dffb8..99f3d6aa 100644 --- a/components/organization/cards/ReviewSubmissionModal/TeamSection.tsx +++ b/components/organization/cards/ReviewSubmissionModal/TeamSection.tsx @@ -11,7 +11,7 @@ interface TeamMember { id: string; name: string; role: string; - avatar?: string; + avatar?: string | null; username?: string; } @@ -37,7 +37,10 @@ export const TeamSection: React.FC = ({ teamMembers }) => { className='group bg-background-card/20 hover:bg-background-card/40 flex items-center gap-4 rounded-xl border border-gray-900/60 p-4 transition-all hover:border-gray-800' > - + {member.name.charAt(0).toUpperCase()} diff --git a/components/organization/cards/ReviewSubmissionModal/types.ts b/components/organization/cards/ReviewSubmissionModal/types.ts index 76f80aa9..f7e3752e 100644 --- a/components/organization/cards/ReviewSubmissionModal/types.ts +++ b/components/organization/cards/ReviewSubmissionModal/types.ts @@ -2,7 +2,7 @@ export interface TeamMember { id: string; name: string; role: string; - avatar?: string; + avatar?: string | null; username?: string; } @@ -10,7 +10,7 @@ export interface Voter { id: string; name: string; username: string; - avatar?: string; + avatar?: string | null; votedAt?: string; voteType?: 'positive' | 'negative'; } @@ -21,7 +21,7 @@ export interface Comment { author: { name: string; username: string; - avatar?: string; + avatar?: string | null; }; createdAt: string; reactions?: { diff --git a/components/organization/cards/TeamModal.tsx b/components/organization/cards/TeamModal.tsx index 21adea3e..f3524345 100644 --- a/components/organization/cards/TeamModal.tsx +++ b/components/organization/cards/TeamModal.tsx @@ -19,7 +19,7 @@ interface TeamMember { id: string; name: string; role: string; - avatar?: string; + avatar?: string | null; } type ParticipationType = 'team' | 'individual' | 'no-submission'; @@ -168,7 +168,10 @@ export default function TeamModal({ className='group flex cursor-pointer items-center gap-3 rounded-lg p-3 transition-colors hover:bg-gray-900/50' > - + {member.name.charAt(0).toUpperCase()} diff --git a/components/organization/hackathons/ParticipantsTable.tsx b/components/organization/hackathons/ParticipantsTable.tsx index c1e141b8..69591e93 100644 --- a/components/organization/hackathons/ParticipantsTable.tsx +++ b/components/organization/hackathons/ParticipantsTable.tsx @@ -57,7 +57,10 @@ export function ParticipantsTable({ return (
- + {user.profile.name?.substring(0, 2).toUpperCase()} diff --git a/hooks/hackathon/use-hackathon-queries.ts b/hooks/hackathon/use-hackathon-queries.ts index e8972915..bc7589e2 100644 --- a/hooks/hackathon/use-hackathon-queries.ts +++ b/hooks/hackathon/use-hackathon-queries.ts @@ -43,6 +43,8 @@ export interface ParticipantsQueryParams { page?: number; limit?: number; status?: string; + search?: string; + type?: string; skill?: string; } @@ -50,6 +52,7 @@ export interface SubmissionsQueryParams { page?: number; limit?: number; status?: string; + search?: string; sort?: string; } diff --git a/hooks/hackathon/use-organizer-submissions.ts b/hooks/hackathon/use-organizer-submissions.ts index f0b402fd..7030cef8 100644 --- a/hooks/hackathon/use-organizer-submissions.ts +++ b/hooks/hackathon/use-organizer-submissions.ts @@ -76,7 +76,7 @@ export function useOrganizerSubmissions( const currentPage = rawPag?.page || page; const limit = rawPag?.limit || pagination.limit || initialLimit; const total = rawPag?.total || 0; - const totalPages = rawPag?.totalPages || Math.ceil(total / limit) || 1; + const totalPages = Math.ceil(total / limit) || 1; setSubmissions(list); setPagination({ diff --git a/hooks/hackathon/use-register-hackathon.ts b/hooks/hackathon/use-register-hackathon.ts index 4e7cb921..75ddc226 100644 --- a/hooks/hackathon/use-register-hackathon.ts +++ b/hooks/hackathon/use-register-hackathon.ts @@ -122,6 +122,9 @@ export function useRegisterHackathon({ hasCheckedInitially, // Expose setters for immediate updates setParticipant, - hasSubmitted: participant?.submission?.status === 'SUBMITTED', + hasSubmitted: + participant?.submission?.status === 'SUBMITTED' || + participant?.submission?.status === 'SHORTLISTED' || + participant?.submission?.status === 'DISQUALIFIED', }; } diff --git a/hooks/use-hackathons.ts b/hooks/use-hackathons.ts index f6a79a58..f3c6709e 100644 --- a/hooks/use-hackathons.ts +++ b/hooks/use-hackathons.ts @@ -582,8 +582,7 @@ export function useHackathons( const responsePage = pagination?.page || 1; const totalItems = pagination?.total || 0; const itemsPerPage = pagination?.limit || pageSize; - const totalPages = - pagination?.totalPages || Math.ceil(totalItems / itemsPerPage) || 1; + const totalPages = Math.ceil(totalItems / itemsPerPage) || 1; setParticipants(response.data?.participants || []); setParticipantsPagination({ diff --git a/hooks/use-participant-submission.ts b/hooks/use-participant-submission.ts index 37fc420b..930f08d3 100644 --- a/hooks/use-participant-submission.ts +++ b/hooks/use-participant-submission.ts @@ -16,7 +16,7 @@ export interface SubmissionData { id: string; name: string; role: string; - avatar?: string; + avatar?: string | null; username?: string; }>; links?: Array<{ type: string; url: string }>; @@ -24,7 +24,7 @@ export interface SubmissionData { id: string; name: string; username: string; - avatar?: string; + avatar?: string | null; votedAt?: string; voteType: 'positive' | 'negative'; }>; @@ -34,7 +34,7 @@ export interface SubmissionData { author: { name: string; username: string; - avatar?: string; + avatar?: string | null; }; createdAt: string; reactions?: { diff --git a/lib/api/hackathon.ts b/lib/api/hackathon.ts index 2501f4df..e7b77408 100644 --- a/lib/api/hackathon.ts +++ b/lib/api/hackathon.ts @@ -71,13 +71,22 @@ export const getFeaturedHackathons = // Get participants for a hackathon export const getHackathonParticipants = async ( slug: string, - params?: { page?: number; limit?: number; status?: string; skill?: string } + params?: { + page?: number; + limit?: number; + status?: string; + skill?: string; + search?: string; + type?: string; + } ): Promise => { const queryParams = new URLSearchParams(); if (params?.page) queryParams.append('page', params.page.toString()); if (params?.limit) queryParams.append('limit', params.limit.toString()); if (params?.status) queryParams.append('status', params.status); if (params?.skill) queryParams.append('skill', params.skill); + if (params?.search) queryParams.append('search', params.search); + if (params?.type) queryParams.append('type', params.type); const response = await api.get( `/hackathons/${slug}/participants?${queryParams.toString()}` @@ -97,12 +106,19 @@ export const getHackathonAnalytics = async ( // Get submissions for a hackathon export const getHackathonSubmissions = async ( slug: string, - params?: { page?: number; limit?: number; status?: string; sort?: string } + params?: { + page?: number; + limit?: number; + status?: string; + search?: string; + sort?: string; + } ): Promise => { const queryParams = new URLSearchParams(); if (params?.page) queryParams.append('page', params.page.toString()); if (params?.limit) queryParams.append('limit', params.limit.toString()); if (params?.status) queryParams.append('status', params.status); + if (params?.search) queryParams.append('search', params.search); if (params?.sort) queryParams.append('sort', params.sort); const response = await api.get( diff --git a/lib/api/hackathons.ts b/lib/api/hackathons.ts index a105755c..ec935a13 100644 --- a/lib/api/hackathons.ts +++ b/lib/api/hackathons.ts @@ -636,7 +636,7 @@ export interface ParticipantTeamMember { name: string; username: string; role: string; - avatar?: string; + avatar?: string | null; } export interface ParticipantVote { @@ -698,7 +698,7 @@ export interface ParticipantSubmission { id: string; name: string; username: string; - image?: string; + image?: string | null; }; disqualificationReason?: string | null; reviewedBy?: { @@ -772,7 +772,7 @@ export interface Participant { profile: { name: string; username: string; - image?: string; + image?: string | null; }; email: string; }; @@ -2403,7 +2403,7 @@ export interface TeamMember { username: string; name: string; role: string; - image?: string; + image?: string | null; joinedAt: string; } diff --git a/types/hackathon/participant.ts b/types/hackathon/participant.ts index 0346f572..cda60e9f 100644 --- a/types/hackathon/participant.ts +++ b/types/hackathon/participant.ts @@ -26,7 +26,7 @@ export interface ParticipantTeamMember { name: string; username: string; role: string; - avatar?: string; + avatar?: string | null; } export interface ParticipantVote { @@ -38,7 +38,7 @@ export interface ParticipantVote { firstName: string; lastName: string; username: string; - avatar?: string; + avatar?: string | null; }; email: string; }; @@ -55,7 +55,7 @@ export interface ParticipantComment { firstName: string; lastName: string; username: string; - avatar?: string; + avatar?: string | null; }; email: string; }; @@ -89,7 +89,7 @@ export interface ParticipantSubmission { firstName: string; lastName: string; username: string; - avatar?: string; + avatar?: string | null; }; email: string; } | null; @@ -106,7 +106,7 @@ export interface Participant { profile: { name: string; username: string; - image?: string; + image?: string | null; skills?: string[]; }; email: string; @@ -150,7 +150,7 @@ export interface CreateSubmissionRequest { name: string; username?: string; role: string; - avatar?: string; + avatar?: string | null; }>; projectName: string; category: string; @@ -181,7 +181,7 @@ export interface SubmissionCardProps { projectName: string; description: string; submitterName: string; - submitterAvatar?: string; + submitterAvatar?: string | null; category?: string; categories?: string[]; status?: 'Pending' | 'Approved' | 'Rejected';