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
14 changes: 12 additions & 2 deletions src/APIFunctions/SCEvents.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) });
Expand All @@ -160,34 +160,44 @@ export async function getEventRegistrations(eventId, token, { limit = 50, offset
headers: {
Authorization: `Bearer ${token}`,
},
...(signal != null ? { signal } : {}),
});
const result = await res.json();
status.responseData = result;
if (!res.ok) {
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);
const res = await fetch(url.href, {
headers: {
Authorization: `Bearer ${token}`,
},
...(signal != null ? { signal } : {}),
});
const result = await res.json();
status.responseData = result;
if (!res.ok) {
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' };
}
Expand Down
124 changes: 119 additions & 5 deletions src/Pages/Events/EventAttendeesDashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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([]);
Expand All @@ -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);
Expand All @@ -93,6 +123,10 @@ export default function EventAttendeesDashboard() {
}

fetchAttendeeDetail();
return () => {
active = false;
controller.abort();
};
}, [id, selectedRequestId, user?.token]);

useEffect(() => {
Expand All @@ -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 [];

Expand All @@ -129,6 +182,17 @@ export default function EventAttendeesDashboard() {

if (!authenticated) return <Redirect to="/login" />;

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 (
<div className="min-h-screen bg-gradient-to-r from-gray-800 to-gray-600 px-6 py-10 text-white">
<div className="mx-auto max-w-7xl">
Expand Down Expand Up @@ -158,6 +222,56 @@ export default function EventAttendeesDashboard() {
<h2 className="text-lg font-semibold">Attendees</h2>
<p className="text-xs text-gray-300">Click an attendee to open details</p>
</div>
{showRegistrationsPagination && (
<div className="mb-4 flex flex-wrap items-end justify-between gap-3">
<div className="space-y-1 text-xs text-gray-400">
<p>
Page {registrationsCurrentPage} of {registrationsTotalPages}
</p>
<p>
{attendees.length > 0
? `${registrationsOffset + 1}–${registrationsOffset + attendees.length} of ${registrationsTotal}`
: `${registrationsTotal} total`}
</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<button
type="button"
className="rounded-lg border border-white/20 px-3 py-1.5 text-sm hover:bg-white/10 disabled:cursor-not-allowed disabled:opacity-40"
disabled={isLoadingList || !canPageRegistrationsPrev}
onClick={handleRegistrationsPrevPage}
>
Previous
</button>
<button
type="button"
className="rounded-lg border border-white/20 px-3 py-1.5 text-sm hover:bg-white/10 disabled:cursor-not-allowed disabled:opacity-40"
disabled={isLoadingList || !canPageRegistrationsNext}
onClick={handleRegistrationsNextPage}
>
Next
</button>
<form className="flex items-center gap-2" onSubmit={handleJumpToRegistrationsPage}>
<input
type="text"
inputMode="numeric"
aria-label="Go to page"
value={jumpPageDraft}
onChange={(e) => 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}
/>
<button
type="submit"
className="rounded-lg border border-white/20 px-3 py-1.5 text-sm hover:bg-white/10 disabled:cursor-not-allowed disabled:opacity-40"
disabled={isLoadingList}
>
Go
</button>
</form>
</div>
</div>
)}
{attendees.length === 0 ? (
<p className="text-sm text-gray-300">No attendees found for this event yet.</p>
) : (
Expand Down
Loading