From 4bba0ed3a079bfdd73f5dddfc07b503697230136 Mon Sep 17 00:00:00 2001 From: Pen Anderson Date: Tue, 3 Mar 2026 19:55:35 -0600 Subject: [PATCH] Switched to path routing. Added data management. --- README.md | 3 +- docs/USAGE.md | 30 +++++++++++ email.go | 4 +- frontend/src/App.svelte | 26 ++++++---- frontend/src/api.js | 7 ++- frontend/src/components/Nav.svelte | 48 ++++++++--------- frontend/src/pages/ConfirmEmail.svelte | 2 +- frontend/src/pages/Kiosk.svelte | 3 +- frontend/src/pages/Settings.svelte | 41 ++++++++++++++- handle_settings.go | 55 ++++++++++++++++++++ handle_settings_test.go | 72 ++++++++++++++++++++++++++ handle_signup.go | 4 +- handle_tokens.go | 2 +- main.go | 5 ++ 14 files changed, 256 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index b526fbe..cd596c4 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Turnpike handles gate check-in, volunteer scheduling, and department coordinatio - **Attendee management** — CSV import (CrowdWork/Zeffy auto-detected), party-size tracking, search, check-in - **Volunteer scheduling** — departments, shifts with capacity, conflict detection, drag-and-drop reordering +- **Public volunteer signup** — self-registration form with email confirmation, auto-attendee linking - **Volunteer kiosk** — token-authenticated self-service shift signup, no login required - **Gate check-in** — full-screen UI with QR scanner, party check-in ("2/3 checked in"), volunteer dual check-in - **Schedule board** — department leads and coordinators manage shift assignments with conflict awareness @@ -90,7 +91,7 @@ The Vite dev server runs on `:5173` and proxies `/api` requests to the Go server ## Documentation -- [Usage Guide](docs/USAGE.md) — event setup, attendee import, volunteer kiosk, gate check-in, schedule board +- [Usage Guide](docs/USAGE.md) — event setup, attendee import, volunteer signup, volunteer kiosk, gate check-in, schedule board - [Installation Guide](docs/INSTALLATION.md) — building, deploying, systemd, Docker, NixOS, reverse proxy, backup ## License diff --git a/docs/USAGE.md b/docs/USAGE.md index 80b25f0..8e0d174 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -65,6 +65,36 @@ The import result shows `inserted` (new records), `grouped` (merged into existin Re-importing the same CSV is safe — existing records are skipped, not duplicated. +## Volunteer Signup + +Turnpike provides a public signup form for volunteers at `/#/volunteer-signup`. No login is required. + +### Signup flow + +1. Volunteer visits the signup form and fills in: preferred name (required), ticket name, email (required), pronouns, phone, department preference, and an optional note. +2. Turnpike creates a volunteer record and auto-links it to an existing attendee by email match, or creates a new attendee record. +3. A confirmation email is sent with a unique link (`/#/confirm/{token}`). +4. The volunteer clicks the link to confirm their email. +5. If shift signups are already open, the confirmation page includes a link to the kiosk for shift selection. + +Duplicate signups with the same email silently succeed — no error is shown and no duplicate is created. This prevents email enumeration. + +### Configuring the signup form + +In **Settings**, the "Volunteer Signup" card controls: + +- **Note field label** — customize the label shown on the form (default: "Additional note") +- **Note field required** — when checked, volunteers must fill in the note to submit + +### Opening shift signups + +In **Settings**, the "Shift Signups" card has an open/close toggle: + +- **Opening** signups generates kiosk tokens for all confirmed volunteers and emails them their shift signup links. A confirmation dialog warns before sending. +- **Closing** signups prevents new kiosk links from being issued on confirmation, but existing links continue to work. + +If a volunteer confirms their email while signups are already open, they receive their kiosk link immediately in the confirmation response and via email. + ## Managing Volunteers Under **Volunteers**, you can: diff --git a/email.go b/email.go index 8d1d463..0a1c65d 100644 --- a/email.go +++ b/email.go @@ -133,7 +133,7 @@ func (app *App) sendTokenEmail(a Attendee) error { cfg := app.loadSMTPConfig() eventName := app.eventName() - link := fmt.Sprintf("%s/#/v/%s", app.resolveBaseURL(), *a.VolunteerToken) + link := fmt.Sprintf("%s/v/%s", app.resolveBaseURL(), *a.VolunteerToken) subject := fmt.Sprintf("Your volunteer link for %s", eventName) body := fmt.Sprintf( "Hi %s,\n\nThank you for volunteering at %s!\n\nYour volunteer token: %s\nYour signup link: %s\n\nUse this link to sign up for available shifts in your department.\n\nSee you there!\n", @@ -146,7 +146,7 @@ func (app *App) sendTokenEmail(a Attendee) error { func (app *App) sendConfirmationEmail(to, name, confirmToken string) error { cfg := app.loadSMTPConfig() eventName := app.eventName() - link := fmt.Sprintf("%s/#/confirm/%s", app.resolveBaseURL(), confirmToken) + link := fmt.Sprintf("%s/confirm/%s", app.resolveBaseURL(), confirmToken) subject := fmt.Sprintf("Please confirm your email for %s", eventName) body := fmt.Sprintf( "Hi %s,\n\nThank you for signing up to volunteer at %s!\n\nPlease confirm your email address by visiting:\n%s\n\nIf you did not sign up, you can safely ignore this email.\n", diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 179cc2c..e9c0638 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -23,16 +23,22 @@ let session = $state(null) let loading = $state(true) - let route = $state(window.location.hash || '#/') + let route = $state(window.location.pathname) let updateAvailable = $state(false) let mobileNavOpen = $state(false) // Check if this is a public page (no auth needed) - const kioskToken = $derived(window.location.hash.match(/^#\/v\/([A-Z0-9]+)/i)?.[1] ?? '') - const isVolunteerSignup = $derived(window.location.hash.startsWith('#/volunteer-signup')) - const isConfirmEmail = $derived(window.location.hash.startsWith('#/confirm/')) + const kioskToken = $derived(route.match(/^\/v\/([A-Z0-9]+)/i)?.[1] ?? '') + const isVolunteerSignup = $derived(route.startsWith('/volunteer-signup')) + const isConfirmEmail = $derived(route.startsWith('/confirm/')) const isPublicPage = $derived(!!kioskToken || isVolunteerSignup || isConfirmEmail) + function navigate(path) { + history.pushState(null, '', path) + route = path + mobileNavOpen = false + } + async function checkVersion() { try { const res = await fetch('/api/version') @@ -56,8 +62,8 @@ startSSE() startSyncLoop() } - window.addEventListener('hashchange', () => { - route = window.location.hash || '#/' + window.addEventListener('popstate', () => { + route = window.location.pathname mobileNavOpen = false }) @@ -67,17 +73,17 @@ function onLogin(s) { session = s - window.location.hash = '#/' + navigate('/') syncPull().then(() => { startSSE(); startSyncLoop() }) } async function onLogout() { await clearSession() session = null - window.location.hash = '#/login' + navigate('/login') } - const path = $derived(route.replace(/^#/, '') || '/') + const path = $derived(route || '/') const role = $derived(session?.user?.role ?? '') @@ -107,7 +113,7 @@ {#if mobileNavOpen} {/if} -