Admin event page: redesign UI, export endpoints, mobile fixes

- Backend: Add /events/:eventId/attendees/export and /events/:eventId/tickets/export with q/status; legacy redirect for old export path
- API: exportAttendees q param, new exportTicketsCSV for tickets CSV
- Admin event page: unified tabs+content container, portal dropdowns to fix clipping, separate mobile export/add-ticket sheets (fix double menu), responsive tab bar and card layout

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Michilis
2026-02-14 18:27:27 +00:00
parent c3897efd02
commit 6bc7e13e78
3 changed files with 1745 additions and 1295 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -373,16 +373,17 @@ export const adminApi = {
return fetchApi<{ payments: ExportedPayment[]; summary: FinancialSummary }>(`/api/admin/export/financial?${query}`);
},
/** Download attendee export as a file (CSV). Returns a Blob. */
exportAttendees: async (eventId: string, params?: { status?: string; format?: string }) => {
exportAttendees: async (eventId: string, params?: { status?: string; format?: string; q?: string }) => {
const query = new URLSearchParams();
if (params?.status) query.set('status', params.status);
if (params?.format) query.set('format', params.format);
if (params?.q) query.set('q', params.q);
const token = typeof window !== 'undefined'
? localStorage.getItem('spanglish-token')
: null;
const headers: Record<string, string> = {};
if (token) headers['Authorization'] = `Bearer ${token}`;
const res = await fetch(`${API_BASE}/api/admin/events/${eventId}/export?${query}`, { headers });
const res = await fetch(`${API_BASE}/api/admin/events/${eventId}/attendees/export?${query}`, { headers });
if (!res.ok) {
const errorData = await res.json().catch(() => ({ error: 'Export failed' }));
throw new Error(errorData.error || 'Export failed');
@@ -393,6 +394,27 @@ export const adminApi = {
const blob = await res.blob();
return { blob, filename };
},
/** Download tickets export as CSV. Returns a Blob. */
exportTicketsCSV: async (eventId: string, params?: { status?: string; q?: string }) => {
const query = new URLSearchParams();
if (params?.status) query.set('status', params.status);
if (params?.q) query.set('q', params.q);
const token = typeof window !== 'undefined'
? localStorage.getItem('spanglish-token')
: null;
const headers: Record<string, string> = {};
if (token) headers['Authorization'] = `Bearer ${token}`;
const res = await fetch(`${API_BASE}/api/admin/events/${eventId}/tickets/export?${query}`, { headers });
if (!res.ok) {
const errorData = await res.json().catch(() => ({ error: 'Export failed' }));
throw new Error(errorData.error || 'Export failed');
}
const disposition = res.headers.get('Content-Disposition') || '';
const filenameMatch = disposition.match(/filename="?([^"]+)"?/);
const filename = filenameMatch ? filenameMatch[1] : `tickets-${new Date().toISOString().split('T')[0]}.csv`;
const blob = await res.blob();
return { blob, filename };
},
};
// Emails API