Created Turnpike, event attendee and volunteer management

Built after prototype, Traverse, an attendee and volunteer list
maintainer.
This commit is contained in:
Pen Anderson 2026-03-03 11:27:07 -06:00
commit 1033cdb29b
59 changed files with 8663 additions and 0 deletions

View file

@ -0,0 +1,13 @@
<script>
let { onclick } = $props()
let loading = $state(false)
async function handle() {
loading = true
try { await onclick() } finally { loading = false }
}
</script>
<button class="btn btn-success btn-sm" onclick={handle} disabled={loading}>
{loading ? '…' : '✓ Check in'}
</button>

View file

@ -0,0 +1,57 @@
<script>
let { session, active, onLogout } = $props()
const role = $derived(session?.user?.role ?? '')
// Role-specific nav sets
const links = $derived.by(() => {
if (role === 'ticketing') return [
{ href: '#/attendees', label: 'Attendees', icon: '✓' },
{ href: '#/import', label: 'Import', icon: '↑' },
]
if (role === 'volunteer_lead') return [
{ href: '#/', label: 'Schedule', icon: '◷' },
{ href: '#/volunteers', label: 'Volunteers', icon: '◎' },
{ href: '#/departments', label: 'Departments', icon: '⬡' },
]
if (role === 'coordinator') return [
{ href: '#/', label: 'Dashboard', icon: '⊞' },
{ href: '#/schedule', label: 'Schedule', icon: '◷' },
{ href: '#/volunteers', label: 'Volunteers', icon: '◎' },
{ href: '#/departments', label: 'Departments', icon: '⬡' },
{ href: '#/shifts', label: 'Shifts', icon: '◑' },
]
// admin — all links
return [
{ href: '#/', label: 'Dashboard', icon: '⊞' },
{ href: '#/attendees', label: 'Attendees', icon: '✓' },
{ href: '#/volunteers', label: 'Volunteers', icon: '◎' },
{ href: '#/departments', label: 'Departments', icon: '⬡' },
{ href: '#/shifts', label: 'Shifts', icon: '◑' },
{ href: '#/schedule', label: 'Schedule', icon: '◷' },
{ href: '#/import', label: 'Import', icon: '↑' },
{ href: '#/users', label: 'Users', icon: '⊕' },
{ href: '#/settings', label: 'Settings', icon: '⚙' },
]
})
function isActive(href) {
const p = href.replace(/^#/, '')
if (p === '/') return active === '/' || active === ''
return active.startsWith(p)
}
</script>
<nav class="sidebar">
<div class="sidebar-brand">Turn<span>pike</span></div>
{#each links as link}
<a href={link.href} class="nav-link" class:active={isActive(link.href)}>
<span class="icon">{link.icon}</span>
{link.label}
</a>
{/each}
<div style="flex:1"></div>
<button class="nav-link btn-ghost" style="border:none;cursor:pointer;width:100%;text-align:left" onclick={onLogout}>
<span class="icon"></span> Sign out
</button>
</nav>

View file

@ -0,0 +1,46 @@
<script>
import { onMount, onDestroy } from 'svelte'
import { getLastSync } from '../db.js'
import { syncPull } from '../sync.js'
let online = $state(navigator.onLine)
let syncing = $state(false)
let lastSync = $state('')
async function refresh() {
lastSync = await getLastSync()
}
async function manualSync() {
syncing = true
await syncPull()
await refresh()
syncing = false
}
function onOnline() { online = true; manualSync() }
function onOffline() { online = false }
onMount(() => {
refresh()
window.addEventListener('online', onOnline)
window.addEventListener('offline', onOffline)
})
onDestroy(() => {
window.removeEventListener('online', onOnline)
window.removeEventListener('offline', onOffline)
})
const dotClass = $derived(syncing ? 'syncing' : online ? 'online' : 'offline')
const label = $derived(syncing ? 'Syncing…' : online ? 'Online' : 'Offline')
const lastSyncLabel = $derived(lastSync ? new Date(lastSync).toLocaleTimeString() : 'Never')
</script>
<div class="sync-bar">
<div class="sync-dot {dotClass}"></div>
<span>{label}</span>
<span class="text-muted" style="margin-left:auto;font-size:0.72rem">Last sync: {lastSyncLabel}</span>
{#if online && !syncing}
<button class="btn btn-ghost btn-sm" onclick={manualSync}>↻</button>
{/if}
</div>