An internal analytics and management dashboard for the DSMLC (UCalgary) club. Built with Next.js App Router and Supabase, it gives club executives a single place to track events, memberships, registrations, and overall club performance with authenticated access, CSV exports, and live charts.
- Overview
- Tech Stack
- Features
- Project Structure
- Data Model
- Architecture Notes
- Authentication Flow
- Getting Started
- Environment Variables
- Scripts
- Styling & Design System
The admin portal is a Next.js 16 (App Router) application backed by Supabase (Postgres + Auth). It is scoped to a dedicated Postgres schema (adminportal) rather than public, so the whole app talks to Supabase through that schema on both the browser and server clients.
The app is split into two route groups:
/and/auth/*— public landing/login page and the auth flows (forgot password, reset password, set password/onboarding), all rendered on a shared animated shader background./dashboard/*— the authenticated app shell (sidebar + topbar + content) with four main sections: Overview, Events, Memberships, and People.
| Concern | Library / Tool |
|---|---|
| Framework | Next.js 16 (App Router, Server Components, Server Actions) |
| Language | TypeScript |
| Auth & DB | Supabase (@supabase/supabase-js, @supabase/ssr) |
| Styling | Tailwind CSS v4 (@theme tokens in globals.css) |
| Animation | Framer Motion |
| Charts | Recharts |
| Shader background | @paper-design/shaders-react |
| Icons | lucide-react |
| Utilities | clsx + tailwind-merge (via a cn() helper) |
- Email/password sign-in against Supabase Auth, gated by an
ADMINStable lookup (is_activemust be true, otherwise the session is signed out immediately). - Forgot / reset password flow using Supabase's
resetPasswordForEmail+updateUser, including a dedicated/auth/confirmroute that verifies the OTP token from the email link and redirects. - Invite-based onboarding (
/auth/set-password) for new admins to set their name and password in one step, calling a Postgres RPC (complete_admin_profile) to finish their profile. - Password strength meter (client-side heuristic: length + case mix + digits + symbols).
- Route protection via
proxy.ts(Next.js middleware) — any request to/dashboard/*without an authenticated Supabase session is redirected to/.
- KPI stat cards: total events, total attendees, average turnout rate, average rating — each with an animated count-up and a mini trend sparkline.
- Attendance Over Time area chart.
- RSVP vs. Actual Turnout bar chart (RSVP / attended / at-door, per event).
- Avg Rating by Category breakdown bars.
- Member Growth area chart (cumulative membership signups over time).
- Sign-up Source breakdown (Mailman, Instagram, LinkedIn, Website, Word-of-Mouth, etc.).
- Sortable/searchable/filterable table of all events with RSVP, at-door, attended counts, turnout rate, and average rating.
- "Events by Category" breakdown chart.
- Create event dialog (Server Action →
EVENTSinsert, redirects into the new event's detail page). - Event detail page (
/dashboard/events/[eventId]):- Turnout rate & average rating stat cards.
- RSVP vs. actual turnout chart for that single event.
- Feedback list (star rating + comment) with a scrollable panel.
- Course Collab Retention chart — compares attendance rate between registrants tied to a course-credit collaboration vs. general attendees (only rendered when course-linked registrations exist).
- Paginated, searchable, filterable registrations table with inline add/edit registration dialogs.
- Course credit CSV export button, filterable by course name.
- Copyable event ID chip for quick reference/debugging.
- Full members table (joins
MEMBERSHIP↔PEOPLE) with search, and filters by membership type and mailing-list status. - Add/edit member dialogs — creation auto-matches an existing
PEOPLErow by email or student ID before creating a new one, and sets membership expiry to the next September 4th. - "Memberships by Major" breakdown chart.
- Mailing list actions: copy all subscribed emails to clipboard, or export them as CSV.
- Paginated, debounced-search directory of every person on record (members, event registrants, etc.), independent of membership status.
- Animated page transitions between dashboard routes (Framer Motion,
AnimatePresence). - Responsive sidebar (desktop) + slide-out mobile nav drawer with account footer (initials avatar, name/email, sign out).
- Reusable
DataTablecomponent: generic column config, built-in search box, pill-style filter chips, and cursor-based pagination controls — used by every table in the app (events, memberships, people, registrations). - Reusable modal
FormDialogShell(Framer Motion enter/exit, Escape-to-close, backdrop click-to-close) used by all create/edit forms. - CSV export helper (
lib/csv.ts) shared by mailing list and course-credit exports.
admin_portal/
├── app/
│ ├── auth/ # Public auth routes (forgot/reset/set password, OTP confirm)
│ ├── dashboard/ # Authenticated app (layout + overview/events/memberships/people)
│ ├── layout.tsx # Root layout, fonts, metadata
│ └── page.tsx # Landing page (login card)
├── components/
│ ├── features/
│ │ ├── auth/ # Login form, auth field, password strength, auth card
│ │ └── dashboard/
│ │ ├── forms/ # Create/edit dialogs (event, member, registration)
│ │ ├── graphs/ # Recharts wrappers (attendance, rsvp, growth, etc.)
│ │ ├── tables/ # Column configs for each entity's DataTable
│ │ └── *.tsx # Shell, sidebar, mobile nav, header, mailing list, etc.
│ ├── motion/ # AnimatedNumber, PageTransition
│ └── ui/ # Design-system primitives: Button, DataTable, StatCard,
│ # SearchInput, Pagination, FormField, hero shader background
├── config/site.ts # App name + sidebar nav config
├── lib/
│ ├── actions/ # Server Actions (events, members, registrations)
│ ├── queries/ # Server-side data-fetching functions (per entity)
│ ├── supabase/ # Browser + server Supabase client factories
│ ├── csv.ts, date.ts, palette.ts, nav-icons.tsx, utils.ts
├── proxy.ts # Next.js middleware — protects /dashboard routes
├── types/ # Shared TS types (auth, database rows)
└── public/ # Logos
Types are declared in types/database.ts; all tables live under the Postgres schema adminportal (not public).
| Table | Purpose | Key columns |
|---|---|---|
ADMINS |
Portal users (1:1 with Supabase auth.users) |
admin_id, first_name, last_name, email, role, is_active |
EVENTS |
Club events | event_id, event_name, event_date, event_type, admin_id |
PEOPLE |
Any person on record (member or not) | people_id, first_name, last_name, student_id, email, major |
FEEDBACK |
Post-event ratings/comments | feedback_id, event_id, rating, comment |
REGISTRATIONS |
Event sign-ups / attendance | registration_id, event_id, people_id, status, course_name, coming_from, registered_at |
MEMBERSHIP |
Club membership records | membership_id, people_id, membership_type, mailing, expires_at, created_at |
Derived concepts worth knowing:
statusonREGISTRATIONSis one ofregistered,attended,at-door. Turnout rate =attended / rsvpwherersvpexcludes walk-ins (at-door); total "attended" for display purposes includes bothattendedandat-door.- A registration with a non-null
course_nameis treated as a "course collab" sign-up, used both for the retention chart and the course-credit CSV export. - Membership
expires_atis automatically set to the next upcoming September 4th on creation (getNextSeptemberFourthinlib/actions/members.ts). - Member/registration creation dedupes people by email first, then student ID, only inserting a new
PEOPLErow if neither match is found.
- Server Components by default. Every dashboard page (
app/dashboard/**/page.tsx) is an async Server Component that calls query functions directly (lib/queries/*) — no client-side data fetching/loading spinners for initial page data. - Server Actions for mutations. All creates/updates (
createEvent,createMember,updateMember,createRegistration,updateRegistration) live inlib/actions/*, are marked"use server", useuseActionStateon the client, and callrevalidatePathon success so lists refresh without a full reload. - Two Supabase clients, both scoped to the
adminportalschema:lib/supabase/server.ts—createServerClientwired to Next.js cookies, used in Server Components/Actions.lib/supabase/client.ts—createBrowserClient, used in client components (login form, password reset, sign out, etc.).
- Generic, composable UI:
DataTable<T>takes a column config, optional search/filter/pagination config, and renders consistent tables everywhere — event, membership, people, and registration tables are just column definitions on top of it.FormDialogShellcentralizes modal behavior (animation, escape key, backdrop click) so every create/edit form only has to supply its fields.
- CSV exports are generated entirely client-side (
lib/csv.tsbuilds a CSV string and triggers a Blob download) — no server round trip needed. - Chart theming is centralized in
lib/palette.ts(brand color, category/status color mapping, gradient helpers) so every Recharts component stays visually consistent.
- Landing page (
/) rendersAuthCard→LoginForm. On submit, it signs in via Supabase, then checks theADMINStable for anis_activerecord matching the user ID; if missing/inactive it signs the user back out and shows an error. - Middleware (
proxy.ts) runs on every request (excluding static assets), refreshes the Supabase session cookies, and redirects unauthenticated users away from/dashboard/*. - Forgot password (
/auth/forgot-password) → Supabase sends a reset email pointing at/auth/reset-password. /auth/confirm(route handler) verifies the emailed OTP (token_hash+type) and redirects into the app, or back to/with anerror=invalid_or_expired_linkquery param.- Reset password (
/auth/reset-password) checks for a valid session (from the confirm redirect) before allowing a new password to be set; shows an "invalid or expired" state otherwise. - Set password (
/auth/set-password) is the onboarding flow for newly invited admins — collects first/last name and a password in one form, then calls thecomplete_admin_profileRPC to finish setting up theADMINSrow.
# install dependencies
npm install
# run the dev server
npm run devThe app will be available at http://localhost:3000.
Note:
next.config.tssetsallowedDevOrigins: ['10.0.0.103']— update or remove this if you're not developing on that LAN address.
Create a .env.local file in admin_portal/ with:
NEXT_PUBLIC_SUPABASE_URL=your-supabase-project-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-keyBoth Supabase clients (browser and server) are configured to use the adminportal Postgres schema — make sure that schema exists in your Supabase project and is exposed via the API settings (Database → API → Exposed schemas).
| Command | Description |
|---|---|
npm run dev |
Start the Next.js dev server |
npm run build |
Production build |
npm run start |
Run the production build |
npm run lint |
Run ESLint (flat config, Next.js core-web-vitals + TypeScript rules) |
- Tailwind CSS v4, configured via
@themeinapp/globals.css— the brand orange (--color-brand: #ff5a2e) and hover variant are the only custom theme tokens; everything else uses Tailwind's default scale plus arbitrary values for the dark palette (#0a0a0a,#111111,#1e1e1e, etc.). - Fonts: Geist Sans/Mono + Instrument Serif (Google Fonts via
next/font). - Motion: Framer Motion drives page transitions, dialog enter/exit, sidebar active-pill morphing (
layoutId), stat card sparklines, and animated numbers — all respectinguseReducedMotion(). - Shader background:
ShaderBackground(wrapping@paper-design/shaders-react'sMeshGradient) powers the animated backdrop on the landing and auth pages.