Skip to main content
Once authenticated, the admin dashboard at /admin/registrations renders live event data sourced from Supabase. This page documents each section of the dashboard and the underlying API.

Summary statistics

The four animated stat cards at the top of the dashboard are computed from the get_registrations_stats RPC response:

Total registrations

The raw count of all rows in the registrations table within the event date range, returned as total_registrations from the stats RPC.

Peak daily attendance

The highest single-day value in daily_expected_attendance — computed client-side with Math.max(...Object.values(stats.daily_expected_attendance)).

Avg daily arrivals

The mean of all values in counts_by_arrival_date, rounded to the nearest integer. Calculated as total / values.length across all arrival dates.

Unique ghaams

The number of distinct Ghaam keys in counts_by_ghaam, derived from Object.keys(stats.counts_by_ghaam).length.
Cards animate their count from 0 to the final value when they enter the viewport, using Framer Motion’s useMotionValue and animate.

Charts

Charts are loaded lazily via React.lazy and Suspense. A skeleton placeholder is shown while they load. All charts use Recharts.

Daily expected attendance

A full-width line chart plotting daily_expected_attendance over time. Core event dates (July 27 – August 2, 2026) are highlighted with a shaded ReferenceArea. Dates are formatted as M/d on the X axis.
// Data shape fed to the line chart
type DailyAttendancePoint = {
  date: string       // ISO date key, e.g. "2026-07-29"
  attendance: number // expected attendee count
  label: string      // formatted X-axis label, e.g. "7/29"
}

Arrivals by date

A bar chart of registrant arrival dates from counts_by_arrival_date. Bars are sorted chronologically, and core event dates are shaded. X-axis labels are rotated –35° to accommodate dense date ranges.

Departures by date

A bar chart mirroring the arrivals chart, using counts_by_departure_date. Shares the same visual style and core-date shading logic.

Highest attendees by Ghaam

A ranked table showing the top 8 Ghaams by registrant count from counts_by_ghaam. “Unknown” values are excluded. If there are more than 8 Ghaams, a “View all” button opens a scrollable dialog listing all Ghaams sorted by count.

Highest attendees by Mandal

The same ranked-table pattern for counts_by_mandal. Mandal names are converted from their stored format to display labels via mandalStoredToDisplay() before rendering.
The charts section renders client-side only. If JavaScript is disabled or the bundle fails to load, the Suspense fallback skeleton remains visible.

Registrations table

The table does not load automatically — it requires clicking the Load Registrations button. This avoids running an expensive query before the admin needs it. Table columns:
ColumnSource field
#Row number (1-based, tracks across pages)
Namefirst_name + middle_name + last_name
Emailemail
Ghaamghaam
Mandalmandal (display label via mandalStoredToDisplay)
Arrivalarrival_date (formatted as MMM d, yyyy)
Departuredeparture_date (formatted as MMM d, yyyy)
Ageage
The search box queries name, email, and mobile_number fields simultaneously. Search terms shorter than 2 characters are ignored. Input is debounced by 350 ms before triggering a new fetch.

Filters

Ghaam

Dropdown populated from the GET /api/admin/registrations/distinct endpoint. Shows all distinct Ghaam values present in the database.

Mandal

Dropdown populated from the static getAllMandalOptionsStored() helper. Labels are converted for display via mandalStoredToDisplay().

Country

Dropdown populated from GET /api/admin/registrations/distinct. Shows all distinct country values.

Age filters

Three numeric inputs: exact age, age minimum, and age maximum. Exposed behind the “Show age & date filters” toggle. age_min must be ≤ age_max.

Arrival date range

Arrival from and Arrival to date pickers, constrained to the event date range from REGISTRATION_DATE_RANGE.

Departure date range

Departure from and Departure to date pickers, using the same event date range constraints.
When any filter is active, a Clear filters button and an Export current view button appear inline with the filter row.

Pagination

The table uses keyset (cursor-based) pagination ordered by id DESC. Page size options are 25, 50, or 100 rows. The footer shows the current row range and the total row count.
  • Navigating forward uses nextCursor from the previous response.
  • Navigating backward uses prevCursor.
  • Changing page size resets to the first page.
  • Changing any filter resets to the first page and fires a new count request.
The total row count is fetched from GET /api/admin/registrations/count when filters change. When no filters are active, the count from the stats RPC (total_registrations) is used instead to avoid a redundant query.

CSV export

Two export actions are available:
ButtonEndpointBehavior
Export entire registration listGET /api/registrations/exportExports all rows regardless of current filters
Export current viewGET /api/admin/registrations/export?{filters}Exports only rows matching the current filter state
Both endpoints stream rows in chunks of 500 using the get_registrations_filtered RPC in a while (hasMore) loop. The file is delivered as text/csv; charset=utf-8 with a UTF-8 BOM (\uFEFF) for Excel compatibility. Exported columns (in order):
id, first_name, middle_name, last_name, email, mobile_number,
phone_country_code, country, ghaam, mandal, arrival_date, departure_date, age
Filenames use the pattern registrations-{ISO-timestamp}.csv or registrations-filtered-{ISO-timestamp}.csv.

API reference

GET /api/admin/stats

Returns aggregated statistics for the dashboard stat cards and charts. Authentication: Requires an active Supabase session with an @nj.sgadi.us email. Query parameters:
ParameterTypeDescription
start_dateISO dateStart of date range (default: REGISTRATION_DATE_RANGE.start = 2026-07-23)
end_dateISO dateEnd of date range (default: REGISTRATION_DATE_RANGE.end = 2026-08-08)
Response:
{
  "success": true,
  "stats": {
    "total_registrations": 3421,
    "daily_expected_attendance": { "2026-07-29": 1200, "2026-07-30": 1450 },
    "counts_by_arrival_date": { "2026-07-28": 230, "2026-07-29": 890 },
    "counts_by_departure_date": { "2026-08-01": 440, "2026-08-02": 1100 },
    "counts_by_ghaam": { "Surat": 520, "Ahmedabad": 310 },
    "counts_by_mandal": { "new-jersey": 820, "california": 210 }
  }
}
Errors: 401 (unauthenticated), 403 (wrong domain), 500 (Supabase not configured or RPC error).

GET /api/registrations/export

Streams all registrations as a CSV file regardless of any filters. This is the “Export entire registration list” button in the dashboard. Authentication: Requires @nj.sgadi.us domain session. Response: text/csv; charset=utf-8 stream with UTF-8 BOM. Uses the registrations table directly with keyset pagination (id ASC, chunk size 500). Filename: registrations-{ISO-timestamp}.csv.

GET /api/admin/registrations

Returns a single page of registrations with keyset pagination. Authentication: Requires an active Supabase session with an @nj.sgadi.us email. Returns 401 if unauthenticated, 403 if the email domain is not allowed. Query parameters:
ParameterTypeDescription
page_size25 | 50 | 100Number of rows per page (default: 25)
cursorintegerID cursor for keyset pagination
directionnext | prevPagination direction (default: next)
searchstringFull-text search across name, email, phone (min 2 chars)
ghaamstringExact Ghaam filter
mandalstringExact Mandal filter (stored value)
countrystringExact country filter
ageintegerExact age filter
age_minintegerMinimum age (inclusive)
age_maxintegerMaximum age (inclusive)
arrival_fromISO dateEarliest arrival date (inclusive)
arrival_toISO dateLatest arrival date (inclusive)
departure_fromISO dateEarliest departure date (inclusive)
departure_toISO dateLatest departure date (inclusive)
Response:
{
  "success": true,
  "rows": [ /* RegistrationRow[] */ ],
  "pageSize": 25,
  "nextCursor": 1234,
  "prevCursor": 5678,
  "hasMore": true,
  "hasPrev": false
}
Errors:
StatusMeaning
400age_min > age_max
401No active session
403Email not in @nj.sgadi.us domain
500Supabase not configured or RPC error

GET /api/admin/registrations/count

Returns the total number of rows matching the given filters. Accepts the same filter parameters as the main endpoint (no pagination params). Times out after 12 seconds and caps at 100,000 to prevent runaway queries.
{ "count": 3421 }

GET /api/admin/registrations/export

Streams all matching rows as a CSV file. Accepts the same filter parameters as the main endpoint. No pagination parameters — all rows are exported in chunks of 500.

GET /api/admin/registrations/distinct

Returns distinct values for ghaam and country columns, used to populate the filter dropdowns.
{
  "ghaam": ["Ahmedabad", "Surat", "Vadodara"],
  "country": ["USA", "Canada", "UK"]
}
All admin API routes set Cache-Control: no-store, max-age=0 on their responses. This is enforced both in the route handlers and in the Next.js middleware for all /admin paths.

Build docs developers (and LLMs) love