Paginate the bookings page and the event attendee/ticket tabs.

Extract the pagination controls shared by the events and users lists into
components/admin/Pagination, along with a usePaginatedList helper that slices a
list and keeps the page in range when the list shrinks under it.

Bookings, the Attendees tab and the Tickets tab paginate client-side rather than
on the server: each already loads its full set for figures a slice would break —
the bookings stat cards, the group-booking totals and the sibling payment-method
lookup, and the per-status counts the two tabs share with the event's other tabs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Michilis
2026-08-23 06:01:27 +00:00
co-authored by Claude Opus 5
parent 87bf9a6151
commit ed1d3a8c12
6 changed files with 223 additions and 153 deletions
@@ -1,8 +1,10 @@
import { useEffect, useRef, useState } from 'react';
import { Ticket } from '@/lib/api';
import { parseDate, EVENT_TIMEZONE } from '@/lib/utils';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import { Dropdown, DropdownItem, MoreMenu } from '@/components/admin/MobileComponents';
import Pagination, { usePaginatedList } from '@/components/admin/Pagination';
import clsx from 'clsx';
import {
MagnifyingGlassIcon,
@@ -79,6 +81,20 @@ export function AttendeesTab({
handleMarkPaid,
handleCheckin,
}: AttendeesTabProps) {
// Paginated client-side: the parent already holds every ticket for the event
// so the status counts and the other tabs keep seeing the full set.
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(25);
const filterKey = `${searchQuery}|${statusFilter}`;
const prevFilterKey = useRef(filterKey);
useEffect(() => {
if (prevFilterKey.current !== filterKey) {
prevFilterKey.current = filterKey;
setPage(1);
}
}, [filterKey]);
const pagedTickets = usePaginatedList(filteredTickets, page, pageSize, setPage);
return (
<div className="space-y-3">
{/* Desktop toolbar */}
@@ -237,7 +253,7 @@ export function AttendeesTab({
</td>
</tr>
) : (
filteredTickets.map((ticket) => {
pagedTickets.map((ticket) => {
const primary = getPrimaryAction(ticket);
return (
<tr key={ticket.id} className="hover:bg-gray-50/50">
@@ -323,7 +339,7 @@ export function AttendeesTab({
{tickets.length === 0 ? 'No attendees yet' : 'No attendees match the current filters'}
</div>
) : (
filteredTickets.map((ticket) => {
pagedTickets.map((ticket) => {
const primary = getPrimaryAction(ticket);
return (
<Card key={ticket.id} className="p-3">
@@ -380,6 +396,16 @@ export function AttendeesTab({
)}
</div>
<Pagination
id="attendees"
page={page}
pageSize={pageSize}
total={filteredTickets.length}
onPageChange={setPage}
onPageSizeChange={setPageSize}
className="mb-20 md:mb-0"
/>
{/* Mobile FAB */}
<div className="md:hidden fixed bottom-6 right-6 z-40">
<button
@@ -1,8 +1,10 @@
import { useEffect, useRef, useState } from 'react';
import { Ticket } from '@/lib/api';
import { parseDate, EVENT_TIMEZONE } from '@/lib/utils';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import { Dropdown, DropdownItem, MoreMenu } from '@/components/admin/MobileComponents';
import Pagination, { usePaginatedList } from '@/components/admin/Pagination';
import {
MagnifyingGlassIcon,
ChevronDownIcon,
@@ -48,6 +50,20 @@ export function TicketsTab({
handleRemoveCheckin,
setShowTicketExportSheet,
}: TicketsTabProps) {
// Paginated client-side, same as the Attendees tab: the parent keeps the full
// ticket list for the header counts and the export actions.
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(25);
const filterKey = `${ticketSearchQuery}|${ticketStatusFilter}`;
const prevFilterKey = useRef(filterKey);
useEffect(() => {
if (prevFilterKey.current !== filterKey) {
prevFilterKey.current = filterKey;
setPage(1);
}
}, [filterKey]);
const pagedTickets = usePaginatedList(filteredConfirmedTickets, page, pageSize, setPage);
return (
<div className="space-y-3">
{/* Desktop toolbar */}
@@ -152,7 +168,7 @@ export function TicketsTab({
</td>
</tr>
) : (
filteredConfirmedTickets.map((ticket) => (
pagedTickets.map((ticket) => (
<tr key={ticket.id} className="hover:bg-gray-50/50">
<td className="px-4 py-2.5">
<p className="font-medium text-sm">{ticket.attendeeFirstName} {ticket.attendeeLastName || ''}</p>
@@ -216,7 +232,7 @@ export function TicketsTab({
{confirmedTickets.length === 0 ? 'No confirmed tickets yet' : 'No tickets match the current filters'}
</div>
) : (
filteredConfirmedTickets.map((ticket) => (
pagedTickets.map((ticket) => (
<Card key={ticket.id} className="p-3">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex-1">
@@ -253,6 +269,15 @@ export function TicketsTab({
))
)}
</div>
<Pagination
id="tickets"
page={page}
pageSize={pageSize}
total={filteredConfirmedTickets.length}
onPageChange={setPage}
onPageSizeChange={setPageSize}
/>
</div>
);
}