Turnpike/frontend/src/App.svelte

181 lines
5.3 KiB
Svelte
Raw Normal View History

<script>
import { onMount } from 'svelte'
import { getSession, clearSession } from './db.js'
import { syncPull, startSSE, startSyncLoop } from './sync.js'
import Login from './pages/Login.svelte'
import Dashboard from './pages/Dashboard.svelte'
import Participants from './pages/Participants.svelte'
import Volunteers from './pages/Volunteers.svelte'
import Departments from './pages/Departments.svelte'
import Users from './pages/Users.svelte'
import Import from './pages/Import.svelte'
import VolunteerKiosk from './pages/VolunteerKiosk.svelte'
2026-03-03 17:59:35 -06:00
import VolunteerSignup from './pages/VolunteerSignup.svelte'
import ConfirmEmail from './pages/ConfirmEmail.svelte'
import GateKiosk from './pages/GateKiosk.svelte'
import ScheduleBoard from './pages/ScheduleBoard.svelte'
import Settings from './pages/Settings.svelte'
import Nav from './components/Nav.svelte'
import SyncStatus from './components/SyncStatus.svelte'
const clientBuild = __BUILD_ID__
let session = $state(null)
let loading = $state(true)
let route = $state(window.location.pathname)
let updateAvailable = $state(false)
2026-03-03 16:09:43 -06:00
let mobileNavOpen = $state(false)
2026-03-03 17:59:35 -06:00
// Check if this is a public page (no auth needed)
const kioskToken = $derived(route.match(/^\/v\/([A-Z0-9]+)/i)?.[1] ?? '')
const isVolunteerSignup = $derived(route.startsWith('/volunteer-signup'))
const isConfirmEmail = $derived(route.startsWith('/confirm/'))
2026-03-03 17:59:35 -06:00
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')
const { build } = await res.json()
if (build && build !== clientBuild) updateAvailable = true
} catch {}
}
onMount(async () => {
checkVersion()
2026-03-03 17:59:35 -06:00
// Public pages don't need auth
if (isPublicPage) {
loading = false
return
}
session = await getSession()
loading = false
if (session) {
await syncPull()
startSSE()
startSyncLoop()
}
window.addEventListener('popstate', () => {
route = window.location.pathname
2026-03-03 16:09:43 -06:00
mobileNavOpen = false
})
// Periodically check for updates
setInterval(checkVersion, 60000)
})
function onLogin(s) {
session = s
navigate('/')
syncPull().then(() => { startSSE(); startSyncLoop() })
}
async function onLogout() {
await clearSession()
session = null
navigate('/login')
}
const path = $derived(route || '/')
const roles = $derived(session?.user?.roles ?? [])
function hasRole(...allowed) { return roles.some(r => allowed.includes(r)) }
</script>
{#if updateAvailable}
<div class="update-banner">
A new version is available.
<button onclick={() => location.reload()}>Refresh</button>
</div>
{/if}
{#if loading}
<!-- checking session -->
{:else if kioskToken}
<VolunteerKiosk />
2026-03-03 17:59:35 -06:00
{:else if isVolunteerSignup}
<VolunteerSignup />
{:else if isConfirmEmail}
<ConfirmEmail />
{:else if !session}
<Login onlogin={onLogin} />
{:else if roles.length === 1 && roles[0] === 'gatekeeper'}
<!-- Gate-only users get the full-screen GateKiosk instead of the standard layout -->
<GateKiosk {session} {onLogout} />
{:else}
<div class="layout">
2026-03-03 16:09:43 -06:00
<!-- svelte-ignore a11y_no_static_element_interactions -->
{#if mobileNavOpen}
<div class="nav-overlay" onclick={() => mobileNavOpen = false} onkeydown={() => {}}></div>
{/if}
<Nav {session} {onLogout} {navigate} active={path} open={mobileNavOpen} />
<div class="main">
2026-03-03 16:09:43 -06:00
<header class="mobile-header">
<button class="hamburger" onclick={() => mobileNavOpen = !mobileNavOpen} aria-label="Menu">
<span></span><span></span><span></span>
</button>
<span class="mobile-brand">Turn<span class="accent">pike</span></span>
</header>
{#if path === '/' || path === ''}
{#if roles.length === 1 && roles[0] === 'colead'}
<ScheduleBoard {session} />
{:else}
<Dashboard {session} />
{/if}
{:else if path.startsWith('/participants')}
<Participants {session} />
{:else if path.startsWith('/volunteers')}
<Volunteers {session} />
{:else if path.startsWith('/departments')}
<Departments {session} />
{:else if path.startsWith('/schedule')}
<ScheduleBoard {session} />
{:else if path.startsWith('/users')}
<Users {session} />
{:else if path.startsWith('/import')}
<Import {session} />
{:else if path.startsWith('/settings')}
<Settings {session} />
{:else}
<div class="page"><p class="text-muted">Page not found.</p></div>
{/if}
<SyncStatus />
</div>
</div>
{/if}
<style>
.update-banner {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 9999;
padding: 8px 16px;
background: var(--c-accent);
color: white;
text-align: center;
font-size: 14px;
}
.update-banner button {
margin-left: 12px;
padding: 2px 12px;
border: 1px solid rgba(255,255,255,0.5);
border-radius: var(--radius);
background: transparent;
color: white;
cursor: pointer;
font-size: 14px;
}
.update-banner button:hover {
background: rgba(255,255,255,0.15);
}
</style>