From 49cabe0ecc1181e3c600eecf34e2f6477ecbe14c Mon Sep 17 00:00:00 2001 From: averel10 Date: Fri, 3 Apr 2026 08:42:13 +0200 Subject: [PATCH] feat: optimized participants list with sorting and search --- src/app/actions/participants.ts | 15 ++- src/components/ParticipantsList.tsx | 196 ++++++++++++++++++++-------- 2 files changed, 152 insertions(+), 59 deletions(-) diff --git a/src/app/actions/participants.ts b/src/app/actions/participants.ts index 2203141..e3f0b7b 100644 --- a/src/app/actions/participants.ts +++ b/src/app/actions/participants.ts @@ -5,6 +5,7 @@ import { participant } from '@/lib/model/participant'; import { annotation } from '@/lib/model/annotation'; import { experiment } from '@/lib/model/experiment'; import { dataset_entry } from '@/lib/model/dataset_entry'; +import { user } from '@/lib/model/auth-schema'; import { and, eq, count } from 'drizzle-orm'; import { requireAdmin } from '@/lib/auth'; import { isParticipantCalibrationDone, getDialectScoresFromCalibration } from './calibration-scoring'; @@ -13,6 +14,7 @@ export interface ParticipantListItem { id: number; experimentId: number; userId: string; + email: string; completedOnboarding: boolean; completedCalibration: boolean; annotationCount: number; @@ -53,8 +55,18 @@ export async function getParticipantsList(experimentId: number): Promise('createdAt'); + const [sortOrder, setSortOrder] = useState('desc'); + + const handleSort = (field: SortField) => { + if (sortField === field) { + setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); + } else { + setSortField(field); + setSortOrder('asc'); + } + }; + + const getSortIndicator = (field: SortField) => { + if (sortField !== field) return ' ↑↓'; + return sortOrder === 'asc' ? ' ↑' : ' ↓'; + }; + + const filteredAndSortedParticipants = useMemo(() => { + let filtered = participants.filter((p) => { + const query = searchQuery.toLowerCase(); + return ( + p.userId.toLowerCase().includes(query) || + p.email.toLowerCase().includes(query) + ); + }); + + filtered.sort((a, b) => { + let aValue: any = a[sortField]; + let bValue: any = b[sortField]; + + if (sortField === 'createdAt') { + aValue = new Date(aValue).getTime(); + bValue = new Date(bValue).getTime(); + } + + const comparison = aValue < bValue ? -1 : aValue > bValue ? 1 : 0; + return sortOrder === 'asc' ? comparison : -comparison; + }); + + return filtered; + }, [participants, searchQuery, sortField, sortOrder]); + if (participants.length === 0) { return (
@@ -18,65 +65,98 @@ export default function ParticipantsList({ experimentId, participants }: Partici } return ( -
-
- - - - - - - - - - - - - {participants.map((participant) => ( - - - - - - - +
+ {/* Search Bar */} +
+ setSearchQuery(e.target.value)} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+ + {/* Table */} +
+
+
User IDOnboardingCalibrationAnnotationsJoinedAction
{participant.userId} - - {participant.completedOnboarding ? '✓ Done' : '○ Pending'} - - - - {participant.completedCalibration ? '✓ Done' : '○ Pending'} - - - - {participant.annotationCount} - - - {new Date(participant.createdAt).toLocaleDateString()} - - - View - -
+ + + + + + + + + - ))} - -
handleSort('userId')}> + User ID{getSortIndicator('userId')} + handleSort('email')}> + Email{getSortIndicator('email')} + OnboardingCalibration handleSort('annotationCount')}> + Annotations{getSortIndicator('annotationCount')} + handleSort('createdAt')}> + Joined{getSortIndicator('createdAt')} + Action
+ + + {filteredAndSortedParticipants.map((participant) => ( + + {participant.userId} + {participant.email} + + + {participant.completedOnboarding ? '✓ Done' : '○ Pending'} + + + + + {participant.completedCalibration ? '✓ Done' : '○ Pending'} + + + + + {participant.annotationCount} + + + + {new Date(participant.createdAt).toLocaleDateString()} + + + + View + + + + ))} + + +
+ {filteredAndSortedParticipants.length === 0 && ( +
+ No participants match your search +
+ )}
);