Events
The STD events kit (src/components/ds/events/) — the presentational layer that formalizes the bespoke portal/public event components. Everything here takes plain props + callbacks (no server actions, no Prisma rows): cards & lists for browsing, the meta chips + viewer-local date formatters, the RSVP/attendance controls, the two-column detail layout with its calendar/share actions, the event-form building blocks, and the staffed-door check-in kiosk. Flip the theme in the top bar to confirm every surface re-skins from the semantic tokens.
- EventSummary
- The render-ready event projection most pieces accept (./types). Deliberately loose: ids string|number, dates ISO string|Date, enums carried as plain strings — so callers pass DB rows without importing the generated enums.
- meta helpers
- eventTypeMeta / eventStatusMeta (./event-meta) narrow a loose type/status string to { label, icon, tone } on DS Badge tones (good|warn|bad|neutral|accent). EVENT_TYPE_OPTIONS / EVENT_STATUS_OPTIONS drive filter/option UIs.
- presentational
- No fetch, no server actions, no router. Interactivity is delegated via callbacks (onSelect / onChange / onSuggest / onMatch / onSubmit). Real wiring lands in a later migration phase.
- viewer-tz dates
- Date/time components (EventCard, EventHero, EventDateTime, TimeRange, CheckinCounter) format in the VIEWER's timezone — they're client components and suppressHydrationWarning, since the SSR (server tz) and first client pass may legitimately differ.
Cards & lists
The event “chip” in every layout — featured headline, standard card, compact card, and dense row — plus the responsive grid that lays standard cards out for the index, and the compact UpcomingEventsList for a dashboard rail. EventCard delegates its RSVP control + navigation: pass an rsvp slot and an href (or onSelect).
Volunteer Happy Hour
Sep 17, 2026 · Zeitgeist
Tuesday Phonebank
Sep 15, 2026
onSelect → last activated: — none yet —
March for the Dogs — City Hall Rally
Saturday Canvass Training
Tuesday Phonebank
Volunteer Happy Hour
Welcome Orientation (Zoom)
Lab Protest — Downtown
Upcoming Events
All events →- Training
Saturday Canvass Training
Sep 26, 2026 · Mission Community Room
- Phonebank
Tuesday Phonebank
Sep 15, 2026
- Social
Volunteer Happy Hour
Sep 17, 2026 · Zeitgeist
Sep 20, 2026
- EventCard
- event: EventSummary; variant: 'featured'|'standard'|'compact'|'row' (default 'standard'); href (null = no link, else falls back to event.href / portal path); onSelect(event); rsvp (footer/trailing/overlay slot, alias children); showStatus.
- EventCardGrid
- Layout-only wrapper. columns: 'auto' (default, fluid auto-fill) | 2 | 3 | 4 (responsive ladder). Map an EventSummary[] into <EventCard> children.
- UpcomingEventsList
- events: EventSummary[]; title / allHref / allLabel / hideAllLink; renderRsvp(event) per-row control; hrefFor / onSelect / showStatus; emptyState. Rows reuse EventCard variant='row' so the chip look stays identical.
Event meta
The small descriptive pieces: the type / status / recurrence / waitlist chips (all on DS Badge tones via the shared meta helpers) and the viewer-local date/time formatters. EventTypeBadge degrades unknown/null types to a neutral “Event” chip; RecurrenceBadge humanizes an iCal FREQ token and renders nothing when blank.
EventDateTime — long date + time
- Saturday, September 12, 2026 · 1:30 PM CDT
- Saturday, September 12, 2026 · 1:30 PM CDT – 4:00 PM CDT
- Saturday, September 12, 2026, 1:30 PM CDT – Saturday, September 26, 2026, 11:00 AM CDT
EventDateRange — compact date range
- Sep 12, 2026
- Sep 12, 2026
- Sep 12 – Sep 26, 2026
TimeRange — time-only (shared meridiem collapses)
- 1:30 – 4:00 PM
- 1:30 PM
- EventTypeBadge / EventStatusBadge
- type / status (loose string). Forward DS Badge props (outline, pill); hideIcon drops the leading lucide glyph. Tone + label + icon resolve from event-meta.
- RecurrenceBadge
- recurrence (string|null). Humanizes an iCal FREQ=… token (→ 'Weekly'); passes already-human strings through; renders null when blank. formatRecurrence() is exported for reuse.
- WaitlistBadge
- Warn-tone 'Waitlisted' chip; optional position ('Waitlisted · #3'); hideIcon. Surfaced when a 'going' RSVP lands past capacity.
- EventDateTime / EventDateRange / TimeRange
- Viewer-tz formatters (client + suppressHydrationWarning). EventDateTime = long date + time; EventDateRange = compact 'Sep 12 – Sep 26, 2026'; TimeRange = time-only, collapsing a shared meridiem ('2:30 – 4:00 PM'). All take startsAt + optional endsAt (ISO string|Date).
- LocationBlock
- location / locationNote / latitude / longitude; renders the STD MapView locator (single accent pin) only when both coords are present, and a Google Maps Directions link whenever there's any location. directionsHref() is exported.
RSVP & attendance
The RSVP affordance and the “who's coming” read-outs. RsvpControl is presentational — it owns no server state: the parent holds the authoritative status and reacts to onChange, with pending greying the controls mid-mutation. CapacityMeter escalates tone (accent → warn → bad) as the event fills; the avatar cluster + list both cap with a “+N” overflow.
- Jane Doe7:42 PM
- Marcus Lee7:39 PM
- Priya PatelWaitlisted
+39 more
- RsvpControl
- status: 'going'|'maybe'|'declined'|null; onChange(nextStatus); waitlisted (shows the chip on a 'going' RSVP); pending (greys/disables mid-mutation). Holds no server state — the parent is authoritative.
- CapacityMeter
- confirmed + capacity (null/0 = uncapped → plain count); waitlist count surfaced once full; hideLabel. Tone escalates accent (<75%) → warn (<100%) → bad (full).
- AttendeeAvatars
- attendees: { id, name, avatarUrl? }[]; max caps visible avatars (default 5); total overrides the '+N' when attendees is a sampled head; size; label. Renders nothing when empty.
- AttendeeList
- attendees: { id, name, waitlisted?, meta? }[]; max (default 8) caps rows into a '+N more'; total drives that count for a truncated head; emptyText.
Detail layout & actions
The event-detail composition: EventHero (the full-bleed banner header, cover photo or accent gradient, title/date/location overlaid) inside EventDetailLayout (a two-column responsive shell — wide main beside a narrower rail), plus the agenda timeline and the calendar/share actions. The combined demo wires the hero, layout, agenda, attendees, map, RSVP, capacity, add-to-calendar, and share together exactly as a real detail page would.
March for the Dogs — City Hall Rally
Join hundreds of advocates on the City Hall steps for a peaceful rally. Speakers, sign-making, and a march to the Federal Plaza. All ages welcome — leashed dogs encouraged.
Schedule
9:00 AM
Doors open · coffee + sign-in
9:30 AM
Welcome + safety briefing
10:00 AM
Canvass team breakouts
12:30 PM
Lunch + debrief
Who’s going
- Jane Doe
- Marcus Lee
- Priya PatelWaitlisted
+185 more
Schedule
9:00 AM
Doors open · coffee + sign-in
9:30 AM
Welcome + safety briefing
10:00 AM
Canvass team breakouts
12:30 PM
Lunch + debrief
- EventHero
- event: EventSummary; ended (a 'Past' chip); showStatus; actions (top-right slot, kept outside any link); as ('h1' default | 'h2'). Cover photo behind a scrim, or an accent gradient band. Client (viewer-tz date line).
- EventDetailLayout
- header (full-width, spans both columns), children (main column), aside (rail), asidePosition ('right' default | 'left'). Stacks (main first) on small screens; splits into main + 20rem rail at lg.
- EventAgendaTimeline
- agenda: the loose Json array of { time, label } (normalized defensively); heading (default 'Schedule', null to omit). Returns null on empty/malformed data, so it's drop-in safe.
- AddToCalendar
- title / startsAt / endsAt? / description? / location? build the Google TEMPLATE link; icsHref (the app's existing .ics route) powers Apple / Outlook / Download. Route-agnostic. googleCalendarUrl() is exported.
- EventShare
- shareUrl (shown inline + opened) + optional shareText (copied; falls back to the URL); copyLabel. Clipboard copy with a transient 'Copied' flash; the link is always visible as a manual fallback.
Form building blocks
The pieces an event-edit form is assembled from. EventFormSection groups related fields under a titled band (the EventForm 'Mini-site' divider look). DateTimeField splits a datetime into a DatePicker + a native HH:MM TimeInput (controlled in either an ISO string or a { date, time } object). AgendaEditor builds a repeatable timeline of rows; EntityMultiSelect is a typeahead multi-select keyed by string id for co-hosts / teams / docs.
When
Split date + time fields, controlled in either ISO or parts mode.
ISO mode — emits a 'YYYY-MM-DDTHH:MM' string.
Parts mode — emits { date, time }.
Start date is required.
starts: 2026-09-12T11:30 · ends: 2026-09-12 14:00 · doors: 09:00
2 rows
selected ids: [march]
Mini-site
Optional public landing page for this event.
…your fields render here…
- EventFormSection
- title + optional description + optional action (trailing control). Renders an uppercase tracked section label over a top rule, with children in a consistent vertical rhythm. Layout-only.
- DateTimeField
- Controlled DatePicker + TimeInput pair under one label. mode 'iso' (default; value 'YYYY-MM-DDTHH:MM') or 'parts' (value { date, time }); onChange echoes the same shape. label / hint / error / disabled / required.
- TimeInput
- Token-styled native <input type='time'>. Controlled: value ('HH:MM', 24h) + onChange(next); invalid for the error ring. Composes with a date input into a datetime (see DateTimeField).
- AgendaEditor
- value: AgendaRow[] ({ time, label }) + onChange(nextRows). Add / remove / inline-edit; useTimePicker swaps the free-text time cell for an HH:MM TimeInput; emptyHint / addLabel / placeholders.
- EntityMultiSelect
- options: ComboboxOption[] ({ value: id, label }); value: string[] (ids) + onChange(ids). Built on the DS MultiSelect; resolves ids to chips and drops stale ids not in options. placeholder / emptyText.
Check-in
The staffed-door kiosk. CheckinKioskLayout is the centered shell (event header over a main slot + a side rail); GuestMatcher is the walk-up sign-in form with name typeahead, guest-list matching, an “Is this you?” candidate confirm, and a success reset; CheckinQr renders a real QR for the self-serve sign-in URL; CheckinCounter is the live arrivals read-out. All presentational — the network (suggest / match / submit, the QR target, the poll) is delegated to callbacks/props.
Demo: type “Ada”, then enter ada@example.com and tab out to see the “Is this you?” match. Any other identity falls through to the full form.
Prefer your own phone?
Scan to check in yourself.
app.savethedogs.io/events/demo/checkin
Checked in tonight
42
people checked in
- Jane Doe1:30 PM
- Marcus Lee1:42 PM
- Priya Patel1:39 PM
Checked in tonight
0
people checked in
No one checked in yet.
Checked in tonight
42
people checked in
- Jane Doe1:30 PM
- Marcus Lee1:42 PM
- Priya Patel1:39 PM
Checked in tonight
0
people checked in
No one checked in yet.
Event check-in
March for the Dogs — City Hall Rally
Prefer your own phone?
Scan to check in yourself.
app.savethedogs.io/events/demo/checkin
Checked in tonight
42
people checked in
- Jane Doe1:30 PM
- Marcus Lee1:42 PM
- CheckinKioskLayout
- title + dateLabel? + location? + eyebrow build the centered event header; children = main slot (the sign-in form), side = rail (QR + counter). Big-touch sizing; the rail sticks on lg and stacks on mobile. Layout-only.
- GuestMatcher
- Presentational walk-up form. Callbacks own the network: onSuggest(query) → name[]; onMatch({name,email,phone}) → GuestCandidate|null; onConfirm({confirmToken,…}) → result; onSubmit(values) → result. interestOptions / consentCopy. Holds only its own form/UI state.
- CheckinQr
- url (encoded into the QR, rendered client-side via the qrcode lib + copied by the copy button); title / subtitle / alt / showUrl / copyable. Degrades to the printed URL if the QR fails or clipboard is blocked.
- CheckinCounter
- count (the big aria-live number) + arrivals: { name, at }[] (newest first; 'at' rendered as a short local time); label; loaded (false → 'Loading…', true → empty-state copy). Owns no polling — the parent supplies + refreshes the data.