diff --git a/src/APIFunctions/SCEvents.js b/src/APIFunctions/SCEvents.js index ec2d48d17..2ed9fabc4 100644 --- a/src/APIFunctions/SCEvents.js +++ b/src/APIFunctions/SCEvents.js @@ -151,7 +151,7 @@ export async function deleteSCEvent(id, token) { return status; } -export async function getEventRegistrations(eventId, token, { limit = 50, offset = 0 } = {}) { +export async function getEventRegistrations(eventId, token, { limit = 50, offset = 0, signal } = {}) { const status = new ApiResponse(); try { const params = new URLSearchParams({ limit: String(limit), offset: String(offset) }); @@ -160,6 +160,7 @@ export async function getEventRegistrations(eventId, token, { limit = 50, offset headers: { Authorization: `Bearer ${token}`, }, + ...(signal != null ? { signal } : {}), }); const result = await res.json(); status.responseData = result; @@ -167,13 +168,17 @@ export async function getEventRegistrations(eventId, token, { limit = 50, offset status.error = true; } } catch (err) { + if (err?.name === 'AbortError') { + status.aborted = true; + return status; + } status.error = true; status.responseData = { error: err?.message || 'Failed to connect to SCEvents API' }; } return status; } -export async function getEventRegistrationByRequestId(eventId, requestId, token) { +export async function getEventRegistrationByRequestId(eventId, requestId, token, signal) { const status = new ApiResponse(); try { const url = new URL(`${SCEVENTS_API_URL}/events/${eventId}/registrations/${requestId}`, window.location.origin); @@ -181,6 +186,7 @@ export async function getEventRegistrationByRequestId(eventId, requestId, token) headers: { Authorization: `Bearer ${token}`, }, + ...(signal != null ? { signal } : {}), }); const result = await res.json(); status.responseData = result; @@ -188,6 +194,10 @@ export async function getEventRegistrationByRequestId(eventId, requestId, token) status.error = true; } } catch (err) { + if (err?.name === 'AbortError') { + status.aborted = true; + return status; + } status.error = true; status.responseData = { error: err?.message || 'Failed to connect to SCEvents API' }; } diff --git a/src/Pages/Events/EventAttendeesDashboard.js b/src/Pages/Events/EventAttendeesDashboard.js index 7fada0f4c..9b058058e 100644 --- a/src/Pages/Events/EventAttendeesDashboard.js +++ b/src/Pages/Events/EventAttendeesDashboard.js @@ -3,6 +3,8 @@ import { Link, Redirect, useParams } from 'react-router-dom'; import { getEventByID, getEventRegistrationByRequestId, getEventRegistrations } from '../../APIFunctions/SCEvents'; import { useSCE } from '../../Components/context/SceContext'; +const EVENT_REGISTRATIONS_PAGE_SIZE = 10; + function formatDateTime(dateValue) { if (!dateValue) return 'N/A'; const date = new Date(dateValue); @@ -41,6 +43,8 @@ export default function EventAttendeesDashboard() { const [detailError, setDetailError] = useState(''); const [isDetailOpen, setIsDetailOpen] = useState(false); const [event, setEvent] = useState(null); + const [registrationsOffset, setRegistrationsOffset] = useState(0); + const [jumpPageDraft, setJumpPageDraft] = useState('1'); useEffect(() => { if (!authenticated || !user?.token || !id) return; @@ -56,12 +60,23 @@ export default function EventAttendeesDashboard() { }, [authenticated, id, user?.token]); useEffect(() => { - if (!authenticated || !user?.token || !id) return; + setRegistrationsOffset(0); + }, [id, user?.token]); + + useEffect(() => { + if (!authenticated || !user?.token || !id) { + setIsLoadingList(false); + return; + } + + let active = true; + const controller = new AbortController(); async function fetchRegistrations() { setIsLoadingList(true); setListError(''); - const response = await getEventRegistrations(id, user.token, { limit: 100, offset: 0 }); + const response = await getEventRegistrations(id, user.token, { limit: EVENT_REGISTRATIONS_PAGE_SIZE, offset: registrationsOffset, signal: controller.signal }); + if (!active || response.aborted) return; if (response.error) { setListError(response.responseData?.error || 'Failed to load attendees.'); setAttendees([]); @@ -74,15 +89,30 @@ export default function EventAttendeesDashboard() { } fetchRegistrations(); - }, [authenticated, id, user?.token]); + return () => { + active = false; + controller.abort(); + }; + }, [authenticated, id, user?.token, registrationsOffset]); useEffect(() => { - if (!selectedRequestId || !user?.token) return; + setJumpPageDraft(String(Math.floor(registrationsOffset / EVENT_REGISTRATIONS_PAGE_SIZE) + 1)); + }, [registrationsOffset]); + + useEffect(() => { + if (!selectedRequestId || !user?.token || !id) { + setIsLoadingDetail(false); + return; + } + + let active = true; + const controller = new AbortController(); async function fetchAttendeeDetail() { setIsLoadingDetail(true); setDetailError(''); - const response = await getEventRegistrationByRequestId(id, selectedRequestId, user.token); + const response = await getEventRegistrationByRequestId(id, selectedRequestId, user.token, controller.signal); + if (!active || response.aborted) return; if (response.error) { setDetailError(response.responseData?.error || 'Failed to load attendee details.'); setSelectedAttendee(null); @@ -93,6 +123,10 @@ export default function EventAttendeesDashboard() { } fetchAttendeeDetail(); + return () => { + active = false; + controller.abort(); + }; }, [id, selectedRequestId, user?.token]); useEffect(() => { @@ -108,6 +142,25 @@ export default function EventAttendeesDashboard() { setIsDetailOpen(false); } + function handleRegistrationsPrevPage() { + setRegistrationsOffset((prev) => Math.max(0, prev - EVENT_REGISTRATIONS_PAGE_SIZE)); + } + + function handleRegistrationsNextPage() { + setRegistrationsOffset((prev) => prev + EVENT_REGISTRATIONS_PAGE_SIZE); + } + + function handleJumpToRegistrationsPage(event) { + event.preventDefault(); + const total = summary.total || 0; + const totalPages = Math.max(1, Math.ceil(total / EVENT_REGISTRATIONS_PAGE_SIZE)); + let page = parseInt(String(jumpPageDraft).trim(), 10); + if (!Number.isFinite(page) || page <= 0) page = 1; + else if (page > totalPages) page = totalPages; + setRegistrationsOffset((page - 1) * EVENT_REGISTRATIONS_PAGE_SIZE); + setJumpPageDraft(String(page)); + } + const selectedAnswers = useMemo(() => { if (!selectedAttendee?.answers || typeof selectedAttendee.answers !== 'object') return []; @@ -129,6 +182,17 @@ export default function EventAttendeesDashboard() { if (!authenticated) return ; + const registrationsTotal = summary.total || 0; + const registrationsTotalPages = Math.max(1, Math.ceil(registrationsTotal / EVENT_REGISTRATIONS_PAGE_SIZE)); + const registrationsCurrentPage = Math.min( + registrationsTotalPages, + Math.floor(registrationsOffset / EVENT_REGISTRATIONS_PAGE_SIZE) + 1, + ); + const canPageRegistrationsPrev = registrationsOffset > 0; + const canPageRegistrationsNext = registrationsOffset + attendees.length < registrationsTotal; + const showRegistrationsPagination = + registrationsTotal > EVENT_REGISTRATIONS_PAGE_SIZE || registrationsOffset > 0; + return (
@@ -158,6 +222,56 @@ export default function EventAttendeesDashboard() {

Attendees

Click an attendee to open details

+ {showRegistrationsPagination && ( +
+
+

+ Page {registrationsCurrentPage} of {registrationsTotalPages} +

+

+ {attendees.length > 0 + ? `${registrationsOffset + 1}–${registrationsOffset + attendees.length} of ${registrationsTotal}` + : `${registrationsTotal} total`} +

+
+
+ + +
+ setJumpPageDraft(e.target.value)} + className="w-14 rounded-lg border border-white/20 bg-black/20 px-2 py-1.5 text-center text-sm text-white" + disabled={isLoadingList} + /> + +
+
+
+ )} {attendees.length === 0 ? (

No attendees found for this event yet.

) : (