Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -316,15 +352,15 @@ const ParticipantsPage: React.FC = () => {
<div className='space-y-6'>
{view === 'table' ? (
<ParticipantsTable
data={participants}
data={filteredParticipants}
loading={participantsLoading}
onReview={handleReview}
onViewTeam={handleViewTeam}
onGrade={handleGrade}
/>
) : (
<ParticipantsGrid
data={participants}
data={filteredParticipants}
loading={participantsLoading}
onReview={handleReview}
onViewTeam={handleViewTeam}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
'use client';

import { useParams } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useEffect, useState, useMemo } from 'react';
import { Loader2, AlertCircle } from 'lucide-react';
import { useOrganizerSubmissions } from '@/hooks/hackathon/use-organizer-submissions';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { AuthGuard } from '@/components/auth';
import Loading from '@/components/Loading';
import { SubmissionsManagement } from '@/components/organization/hackathons/submissions/SubmissionsManagement';
import { authClient } from '@/lib/auth-client';
import { getHackathon, type Hackathon } from '@/lib/api/hackathons';
import {
getHackathon,
type Hackathon,
type ParticipantSubmission,
} from '@/lib/api/hackathons';
import { reportError } from '@/lib/error-reporting';
import { useReactTable, getCoreRowModel } from '@tanstack/react-table';
import { DataTablePagination } from '@/components/ui/data-table-pagination';

export default function SubmissionsPage() {
const params = useParams();
const hackathonId = params.hackathonId as string;
const organizationId = params.id as string;

const {
submissions,
submissions: allSubmissions,
pagination,
filters,
loading,
Expand All @@ -27,14 +33,14 @@ export default function SubmissionsPage() {
updateFilters,
goToPage,
refresh,
updateLimit,
} = useOrganizerSubmissions(hackathonId);

const [currentUserId, setCurrentUserId] = useState<string | null>(null);
const [hackathon, setHackathon] = useState<Hackathon | null>(null);

useEffect(() => {
if (hackathonId) {
fetchSubmissions();
const fetchHackathonDetails = async () => {
try {
const res = await getHackathon(hackathonId);
Expand All @@ -50,7 +56,7 @@ export default function SubmissionsPage() {
};
fetchHackathonDetails();
}
}, [hackathonId, fetchSubmissions]);
}, [hackathonId]);

useEffect(() => {
const fetchSession = async () => {
Expand All @@ -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);
}
}
},
});
Comment on lines +75 to +119
Copy link

@coderabbitai coderabbitai bot Mar 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Frontend filtering only operates on the current page, not the full dataset.

The current implementation has a fundamental mismatch between filtering and pagination:

  1. allSubmissions contains only the current page's data from the backend (e.g., 10 items)
  2. filteredSubmissions filters within this single page
  3. pagination.totalPages reflects the backend's total (e.g., 50 pages)
  4. DataTablePagination displays "Page 1 of 50" even when filtered results are empty

This creates confusing UX where users search for a term, see "0 results" on the current page, but pagination still shows 50 pages available. Navigating to another page fetches fresh backend data, making the filter appear to reset.

The backend already supports server-side filtering via getHackathonSubmissions(hackathonId, page, limit, filters). The useOrganizerSubmissions hook already passes filters.search, filters.status, and filters.type to the API. The frontend filtering layer here is redundant and creates the pagination mismatch.

Consider removing the frontend filteredSubmissions layer and relying entirely on the existing server-side filtering:

🔧 Suggested approach
- // 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]);

  // ... in render:
  <SubmissionsManagement
-   submissions={filteredSubmissions}
+   submissions={allSubmissions}
    // ... rest of props
  />

This ensures filtering and pagination are both handled server-side, providing accurate page counts and complete dataset filtering.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/`(landing)/organizations/[id]/hackathons/[hackathonId]/submissions/page.tsx
around lines 75 - 119, The frontend is filtering only the current page
(filteredSubmissions via useMemo over allSubmissions) causing pagination
mismatch (pagination.totalPages stays backend total); remove the client-side
filtering and feed the table the backend-provided data directly so server-side
filters are authoritative: delete the filteredSubmissions useMemo and change
useReactTable data from filteredSubmissions to allSubmissions (keep pagination
state and onPaginationChange), and ensure filters from the UI are passed to the
existing useOrganizerSubmissions/getHackathonSubmissions so the backend returns
properly filtered pages.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea but the backend doesnt have filters implemented. We only implemented pagination and page size

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Benjtalkshow, thanks for the clarification — that context makes the frontend filtering approach make total sense.

There is still a smaller residual issue worth noting: when filters are active, useReactTable still receives pageCount: pagination.totalPages (the server's unfiltered total). This means if a filter reduces the visible results to, say, 3 items on the current page, the pagination control will still display "Page 1 of 50" — because the backend total hasn't changed.

A minimal fix would be to compute a local page count based on the filtered results for display purposes:

const filteredPageCount = Math.max(1, Math.ceil(filteredSubmissions.length / pagination.limit));

const table = useReactTable({
  data: filteredSubmissions,
  columns: [],
  getCoreRowModel: getCoreRowModel(),
  manualPagination: true,
  pageCount: filters.search || filters.status || filters.type
    ? filteredPageCount   // local estimate when filters are active
    : pagination.totalPages, // server total when no filters
  // ...rest unchanged
});

This keeps the server-driven pagination when there are no active filters, and falls back to a locally-derived page count when filters narrow the results. It's not perfect (since filtering only covers the loaded page), but it avoids the confusing "0 results — Page 1 of 50" display.

That said, this is a known trade-off with client-side filtering over paginated data. If the datasets stay small enough to load all at once in the future, a full client-side approach (no server pagination + useMemo filter) would eliminate the ambiguity entirely.


✏️ Learnings added
Learnt from: Benjtalkshow
Repo: boundlessfi/boundless PR: 488
File: app/(landing)/organizations/[id]/hackathons/[hackathonId]/submissions/page.tsx:75-119
Timestamp: 2026-03-16T13:16:41.680Z
Learning: In the `app/(landing)/organizations/[id]/hackathons/[hackathonId]/submissions/page.tsx` file and related submissions hooks/components, the backend API (`getHackathonSubmissions`) only supports pagination and page size — it does NOT support server-side filtering (search, status, type). Frontend filtering via `useMemo` over `allSubmissions` is intentional and necessary as a workaround for this backend limitation.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


if (error) {
return (
<div className='flex min-h-screen items-center justify-center bg-black p-6'>
Expand Down Expand Up @@ -100,27 +152,33 @@ export default function SubmissionsPage() {

{/* Main Content */}
<div className='mx-auto max-w-7xl px-6 py-12 sm:px-8 lg:px-12'>
{loading && submissions.length === 0 ? (
{loading && allSubmissions.length === 0 ? (
<div className='flex items-center justify-center py-20'>
<div className='flex flex-col items-center gap-3'>
<Loader2 className='h-6 w-6 animate-spin text-gray-400' />
<p className='text-sm text-gray-500'>Loading submissions...</p>
</div>
</div>
) : (
<SubmissionsManagement
submissions={submissions}
pagination={pagination}
filters={filters}
loading={loading}
onFilterChange={updateFilters}
onPageChange={goToPage}
onRefresh={refresh}
organizationId={organizationId}
hackathonId={hackathonId}
currentUserId={currentUserId || undefined}
hackathon={hackathon || undefined}
/>
<div className='space-y-6'>
<SubmissionsManagement
submissions={filteredSubmissions}
pagination={pagination}
filters={filters}
loading={loading}
onFilterChange={updateFilters}
onPageChange={goToPage}
onRefresh={refresh}
organizationId={organizationId}
hackathonId={hackathonId}
currentUserId={currentUserId || undefined}
hackathon={hackathon || undefined}
/>

<div className='flex justify-end'>
<DataTablePagination table={table} loading={loading} />
</div>
</div>
)}
</div>
</div>
Expand Down
10 changes: 5 additions & 5 deletions components/hackathons/submissions/SubmissionDetailModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,16 +166,16 @@ export function SubmissionDetailModal({
<div className='flex items-center gap-4'>
<Badge
className={`${
submission.status === 'shortlisted'
submission.status === 'SHORTLISTED'
? 'border-primary bg-[#E5FFE5] text-[#4E9E00]'
: submission.status === 'disqualified'
: submission.status === 'DISQUALIFIED'
? 'border-[#FF5757] bg-[#FFEAEA] text-[#D33]'
: 'border-[#645D5D] bg-[#E4DBDB] text-[#645D5D]'
}`}
>
{submission.status === 'shortlisted'
{submission.status === 'SHORTLISTED'
? 'Shortlisted'
: submission.status === 'disqualified'
: submission.status === 'DISQUALIFIED'
? 'Disqualified'
: 'Submitted'}
</Badge>
Expand Down Expand Up @@ -295,7 +295,7 @@ export function SubmissionDetailModal({
</div>

{/* Disqualification Reason */}
{submission.status === 'disqualified' &&
{submission.status === 'DISQUALIFIED' &&
submission.disqualificationReason && (
<div className='rounded-lg border border-red-500/50 bg-red-500/10 p-4'>
<h4 className='mb-2 font-semibold text-red-400'>
Expand Down
2 changes: 1 addition & 1 deletion components/organization/cards/Participant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions components/organization/hackathons/ParticipantsGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ export const ParticipantsGrid: React.FC<ParticipantsGridProps> = ({
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)
Expand Down
23 changes: 13 additions & 10 deletions components/organization/hackathons/ParticipantsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,22 +110,25 @@ export function ParticipantsTable({
);
}

const status = submission.status;
const status = submission.status as
| 'SHORTLISTED'
| 'DISQUALIFIED'
| 'SUBMITTED';
return (
<Badge
variant='outline'
className={cn(
'capitalize',
status === 'shortlisted'
? 'border-green-500/30 bg-green-500/10 text-green-400'
: status === 'disqualified'
? 'border-red-500/30 bg-red-500/10 text-red-400'
: 'border-yellow-500/30 bg-yellow-500/10 text-yellow-400'
'rounded-full border px-2 py-0.5 text-[10px] font-medium transition-colors',
status === 'SHORTLISTED'
? 'border-primary/30 bg-primary/5 text-primary'
: status === 'DISQUALIFIED'
? 'border-red-500/30 bg-red-500/5 text-red-400'
: 'border-yellow-500/30 bg-yellow-500/5 text-yellow-400'
)}
>
{status === 'shortlisted'
{status === 'SHORTLISTED'
? 'Shortlisted'
: status === 'disqualified'
: status === 'DISQUALIFIED'
? 'Disqualified'
: 'Submitted'}
</Badge>
Expand All @@ -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 (
<DropdownMenu>
Expand Down
Loading
Loading