This document walks through how the DEVSA admin dashboard handles event creation, community editing (with images and links), RSVP data capture, and CSV export.
- Creating Events
- Editing Community Descriptions, Images & Links
- Capturing RSVP Data
- Exporting RSVPs to CSV
- Deleting Newsletter & RSVP Records
The event creation form is a dedicated page accessible from the admin dashboard. It is protected by a client-side auth check that redirects unauthenticated users back to /admin.
Form fields:
| Field | Type | Required | Notes |
|---|---|---|---|
| Community | Select dropdown | ✅ | Organizers are locked to their assigned community; admins/superadmins can pick any |
| Title | Text input | ✅ | Used to auto-generate the URL slug |
| Date | Date picker | ✅ | Combined with start time into an ISO datetime |
| Start Time | Time picker | ✅ | Default 18:00 |
| End Time | Time picker | ✅ | Default 20:00 — used by the "Happening Now" feature |
| Location | Text input | ✅ | Free text address |
| Description | Rich text editor | ✅ | Supports bold, bullet lists, and hyperlinks via the RichTextEditor component |
| Enable RSVP | Toggle switch | ❌ | When on, an RSVP form appears on the public event page |
| Status | Select | ❌ | published (default) or draft — drafts are only visible in the admin dashboard |
Rich text editing is powered by components/rich-text-editor.tsx — a custom textarea component that shows a floating toolbar when text is selected. It wraps selected text in HTML tags (<strong>, <ul><li>, <a href="...">) and stores the result as an HTML string.
POST /api/events
Content-Type: application/json
{
"title": "Monthly Meetup",
"date": "2026-02-15T00:00:00.000Z",
"endTime": "2026-02-15T02:00:00.000Z",
"location": "Geekdom, 110 E Houston St",
"description": "<p>Join us for...</p>",
"communityId": "san-antonio-devs",
"status": "published",
"rsvpEnabled": true,
"organizerEmail": "organizer@example.com"
}
Server-side flow:
- Validates all required fields are present.
- Verifies the
organizerEmailexists in theapproved_adminsFirestore collection. - If the user is an organizer, confirms their
communityIdmatches the event's community. - Generates a URL-safe slug from the title (e.g.
monthly-meetup-k5f3a2). - Creates a document in the
eventsFirestore collection. - Returns the new
eventIdandslug.
The GET /api/events endpoint merges events from two sources:
- Firestore — dynamically created events (editable/deletable)
- Static data (
data/events.ts) — hardcoded seed events (read-only)
Firestore events take precedence when slugs collide. Results are sorted by date.
From the Communities tab, admins can click Edit on any community to open an edit modal. The modal contains:
- Name — text input
- Logo — either upload an image file or paste a URL
- File uploads go through the
/api/uploadendpoint → Vercel Blob Storage - Supported types: JPEG, PNG, WebP, SVG, GIF (max 5 MB)
- File uploads go through the
- Description — textarea for the community's bio (plain text)
- Social / link fields — inputs for website, Discord, Meetup, Luma, Instagram, Twitter/X, LinkedIn, YouTube, Twitch, Facebook, and GitHub
POST /api/upload
Content-Type: multipart/form-data
file: <binary>
adminEmail: admin@example.com
communityId: san-antonio-devs
Server-side flow:
- Validates the file type is in the allow-list and size is ≤ 5 MB.
- Verifies the uploader is an approved admin (or an organizer for that specific community).
- Uploads the file to Vercel Blob Storage under
communities/{communityId}-{timestamp}.{ext}. - Returns the public
urlfor the uploaded blob.
PUT /api/communities
Content-Type: application/json
{
"id": "san-antonio-devs",
"name": "San Antonio Devs",
"logo": "https://blob.vercel-storage.com/communities/...",
"description": "A community for developers...",
"website": "https://sadevs.com",
"discord": "https://discord.gg/...",
"twitter": "https://twitter.com/...",
"adminEmail": "admin@example.com"
}
Permission model:
| Role | Can edit |
|---|---|
superadmin |
Any community |
admin |
Any community |
organizer |
Only their assigned community (communityId must match) |
The API builds a partial update object from the provided fields and applies it with communityRef.update(), preserving any fields not included in the request.
When creating or editing an event, toggle the Enable RSVP switch. This sets rsvpEnabled: true on the event document in Firestore. The public event page (app/events/[slug]) checks this flag and renders an RSVP form when enabled.
| Field | Required | Notes |
|---|---|---|
| First Name | ✅ | |
| Last Name | ✅ | |
| ✅ | Validated with regex, normalized to lowercase | |
| Join Newsletter | ❌ | Opt-in checkbox — automatically adds the email to the newsletter if checked |
POST /api/rsvp
Content-Type: application/json
{
"eventId": "abc123",
"eventSlug": "monthly-meetup-k5f3a2",
"communityId": "san-antonio-devs",
"firstName": "Jane",
"lastName": "Doe",
"email": "jane@example.com",
"joinNewsletter": true
}
Server-side flow:
- Validates required fields and email format.
- Runs MAGEN bot-detection verification (currently in log-only mode).
- Confirms the event exists and has
rsvpEnabled: true. - Checks for duplicate RSVPs (same
eventId+email) — returns409if already registered. - Creates a document in the
event_rsvpsFirestore collection. - If
joinNewsletteris true, adds the email to thenewsletter_subscriptionscollection (with sourceevent-rsvp:{slug}), skipping if already subscribed. - Sends a thank-you confirmation email via Resend with event details (title, date, location, community name, event URL).
- Returns success. Email send failures are logged but don't block the RSVP.
interface EventRSVP {
eventId: string // Firestore document ID of the event
eventSlug: string // URL slug for linking
communityId: string // Community the event belongs to
firstName: string
lastName: string
email: string // Normalized to lowercase
joinNewsletter: boolean
submittedAt: Date
}In the admin dashboard's RSVPs tab, there are two filter dropdowns and an Export CSV button:
- Community filter — narrow RSVPs to a specific community
- Event filter — narrow to a specific RSVP-enabled event (dynamically filtered by the selected community)
- Export CSV — downloads a
.csvfile for the currently filtered view
GET /api/rsvp?adminEmail=admin@example.com&eventId=abc123&format=csv
Parameters:
| Param | Required | Description |
|---|---|---|
adminEmail |
✅ | Used to verify permissions |
eventId |
❌ | Filter RSVPs to a specific event |
communityId |
❌ | Filter RSVPs to a specific community |
format |
❌ | Set to csv for file download; omit for JSON |
Permission model:
| Role | Sees |
|---|---|
superadmin / admin |
All RSVPs (or filtered by query params) |
organizer |
Only RSVPs for events in their assigned community |
CSV output columns:
First Name, Last Name, Email, Event, Joined Newsletter, Submitted At
The response is returned with Content-Type: text/csv and a Content-Disposition: attachment header so the browser triggers a download. The filename follows the pattern rsvps-{eventId|communityId|all}-{date}.csv.
Admins and superadmins can delete individual records directly from the admin dashboard tables.
DELETE /api/newsletter
Content-Type: application/json
{
"subscriptionId": "firestore-doc-id",
"adminEmail": "admin@example.com"
}
Verifies the caller has admin or superadmin role, then deletes the document from the newsletter_subscriptions collection.
DELETE /api/rsvp
Content-Type: application/json
{
"rsvpId": "firestore-doc-id",
"adminEmail": "admin@example.com"
}
Same permission check — only admin or superadmin roles can delete RSVP records from the event_rsvps collection.
Both endpoints update the admin UI immediately by removing the deleted record from local state without requiring a full data refetch.
| Concern | Technology |
|---|---|
| Framework | Next.js (App Router, Server + Client Components) |
| Database | Firebase / Firestore |
| File storage | Vercel Blob Storage |
| Resend | |
| Bot detection | MAGEN (log-only mode) |
| Rich text | Custom RichTextEditor component (HTML output) |
| Auth | Email-based admin verification against approved_admins collection |