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,