From e914cf2a7ee20003cf62e8e7aafa900191b50406 Mon Sep 17 00:00:00 2001 From: DJCrossman Date: Fri, 21 Aug 2026 11:15:13 -0600 Subject: [PATCH] feat: add admin upcoming events list Replaces the /admin placeholder with the events list: prefetched server- side via the shared adminEventsQueryKey (exported from the hook so the hydration key can't drift from the client query), rendered by AdminEventsScene as a card grid with Regina-zone start time, venue, draft badge, and sold/capacity progress. Cards link to the event detail page added in the next PR. Co-Authored-By: Claude Fable 5 --- app/admin/(protected)/page.tsx | 18 +++++-- hooks/useAdminEvents.ts | 24 +++++++++ scenes/AdminEventsScene/AdminEventsScene.tsx | 37 ++++++++++++++ .../components/AdminEventCard.tsx | 50 +++++++++++++++++++ scenes/AdminEventsScene/components/index.ts | 1 + utils/generateQueryKey.ts | 2 +- 6 files changed, 126 insertions(+), 6 deletions(-) create mode 100644 hooks/useAdminEvents.ts create mode 100644 scenes/AdminEventsScene/AdminEventsScene.tsx create mode 100644 scenes/AdminEventsScene/components/AdminEventCard.tsx create mode 100644 scenes/AdminEventsScene/components/index.ts diff --git a/app/admin/(protected)/page.tsx b/app/admin/(protected)/page.tsx index fcba953..f647b1a 100644 --- a/app/admin/(protected)/page.tsx +++ b/app/admin/(protected)/page.tsx @@ -1,14 +1,22 @@ +import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'; import type { Metadata } from 'next'; +import { fetchAdminEvents } from '@/fetch/adminEvents'; +import { adminEventsQueryKey } from '@/hooks/useAdminEvents'; import { requireAdminOrRedirect } from '@/lib/adminAuth'; +import { AdminEventsScene } from '@/scenes/AdminEventsScene/AdminEventsScene'; export const metadata: Metadata = { title: 'Admin - HackRegina' }; export default async function AdminPage() { - const session = await requireAdminOrRedirect(); + await requireAdminOrRedirect(); + const queryClient = new QueryClient(); + await queryClient.prefetchQuery({ + queryKey: adminEventsQueryKey, + queryFn: () => fetchAdminEvents(), + }); return ( -

- Signed in as {session.user.name ?? 'unknown user'}. Event management is coming in the next - update. -

+ + + ); } diff --git a/hooks/useAdminEvents.ts b/hooks/useAdminEvents.ts new file mode 100644 index 0000000..f9b9170 --- /dev/null +++ b/hooks/useAdminEvents.ts @@ -0,0 +1,24 @@ +import { useQuery } from '@tanstack/react-query'; +import type { IAdminEvent } from '@/fetch/adminEvents'; +import { generateQueryKey } from '@/utils/generateQueryKey'; + +// Shared with the server prefetch in app/admin/(protected)/page.tsx so the +// hydrated cache and the client query can never drift apart. +export const adminEventsQueryKey = generateQueryKey({ key: 'admin-events' }); + +export const useAdminEvents = () => { + const { + data: events = [], + isLoading, + isError, + } = useQuery({ + queryKey: adminEventsQueryKey, + queryFn: async () => { + const response = await fetch('/api/admin/events'); + if (!response.ok) throw new Error(`Failed to load admin events (${response.status})`); + const { data } = await response.json(); + return data; + }, + }); + return { events, isLoading, isError }; +}; diff --git a/scenes/AdminEventsScene/AdminEventsScene.tsx b/scenes/AdminEventsScene/AdminEventsScene.tsx new file mode 100644 index 0000000..305949c --- /dev/null +++ b/scenes/AdminEventsScene/AdminEventsScene.tsx @@ -0,0 +1,37 @@ +'use client'; + +import { useAdminEvents } from '@/hooks/useAdminEvents'; +import { AdminEventCard } from './components'; + +export const AdminEventsScene = () => { + const { events, isLoading, isError } = useAdminEvents(); + + return ( +
+

+ Upcoming events +

+ {isError && ( +

+ Failed to load events. Try refreshing, or sign in again. +

+ )} + {isLoading && ( +
+ {[...Array(6)].map((_, i) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: static list of skeleton placeholders +
+ ))} +
+ )} + {!isLoading && !isError && events.length === 0 && ( +

No upcoming events.

+ )} +
+ {events.map((event) => ( + + ))} +
+
+ ); +}; diff --git a/scenes/AdminEventsScene/components/AdminEventCard.tsx b/scenes/AdminEventsScene/components/AdminEventCard.tsx new file mode 100644 index 0000000..e2d7087 --- /dev/null +++ b/scenes/AdminEventsScene/components/AdminEventCard.tsx @@ -0,0 +1,50 @@ +'use client'; + +import { DateTime } from 'luxon'; +import Link from 'next/link'; +import { Badge } from '@/components/ui/badge'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Progress } from '@/components/ui/progress'; +import type { IAdminEvent } from '@/fetch/adminEvents'; +import { routes } from '@/lib/route'; + +interface AdminEventCardProps { + event: IAdminEvent; +} + +export const AdminEventCard = ({ event }: AdminEventCardProps) => { + const start = DateTime.fromMillis(event.start).setZone('America/Regina'); + const soldPercent = + event.capacity && event.capacity > 0 + ? Math.round(((event.sold ?? 0) / event.capacity) * 100) + : null; + + return ( + + + +
+ {event.name} + {event.status !== 'live' && {event.status}} +
+

+ {start.toLocaleString(DateTime.DATETIME_MED_WITH_WEEKDAY)} + {event.venueName ? ` ยท ${event.venueName}` : ''} +

+
+ + {soldPercent !== null ? ( +
+

+ {event.sold} / {event.capacity} registered +

+ +
+ ) : ( +

No ticket classes yet

+ )} +
+
+ + ); +}; diff --git a/scenes/AdminEventsScene/components/index.ts b/scenes/AdminEventsScene/components/index.ts new file mode 100644 index 0000000..c9eb1be --- /dev/null +++ b/scenes/AdminEventsScene/components/index.ts @@ -0,0 +1 @@ +export { AdminEventCard } from './AdminEventCard'; diff --git a/utils/generateQueryKey.ts b/utils/generateQueryKey.ts index dae9913..e204cbd 100644 --- a/utils/generateQueryKey.ts +++ b/utils/generateQueryKey.ts @@ -1,4 +1,4 @@ -type KeyType = 'members' | 'organizations' | 'website' | 'events'; +type KeyType = 'members' | 'organizations' | 'website' | 'events' | 'admin-events'; export const generateQueryKey = ({ key,