2026-03-03 11:27:07 -06:00
|
|
|
<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 Attendees from './pages/Attendees.svelte'
|
|
|
|
|
import Volunteers from './pages/Volunteers.svelte'
|
|
|
|
|
import Departments from './pages/Departments.svelte'
|
|
|
|
|
import Shifts from './pages/Shifts.svelte'
|
|
|
|
|
import Users from './pages/Users.svelte'
|
|
|
|
|
import Import from './pages/Import.svelte'
|
|
|
|
|
import Kiosk from './pages/Kiosk.svelte'
|
|
|
|
|
import GateUI from './pages/GateUI.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'
|
|
|
|
|
|
2026-03-03 13:38:31 -06:00
|
|
|
const clientBuild = __BUILD_ID__
|
|
|
|
|
|
2026-03-03 11:27:07 -06:00
|
|
|
let session = $state(null)
|
|
|
|
|
let loading = $state(true)
|
|
|
|
|
let route = $state(window.location.hash || '#/')
|
2026-03-03 13:38:31 -06:00
|
|
|
let updateAvailable = $state(false)
|
2026-03-03 16:09:43 -06:00
|
|
|
let mobileNavOpen = $state(false)
|
2026-03-03 11:27:07 -06:00
|
|
|
|
|
|
|
|
// Check if this is a kiosk token URL before doing anything else
|
|
|
|
|
const kioskToken = $derived(window.location.hash.match(/^#\/v\/([A-Z0-9]+)/i)?.[1] ?? '')
|
|
|
|
|
|
2026-03-03 13:38:31 -06:00
|
|
|
async function checkVersion() {
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch('/api/version')
|
|
|
|
|
const { build } = await res.json()
|
|
|
|
|
if (build && build !== clientBuild) updateAvailable = true
|
|
|
|
|
} catch {}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-03 11:27:07 -06:00
|
|
|
onMount(async () => {
|
2026-03-03 13:38:31 -06:00
|
|
|
checkVersion()
|
|
|
|
|
|
2026-03-03 11:27:07 -06:00
|
|
|
// Kiosk pages don't need auth
|
|
|
|
|
if (kioskToken) {
|
|
|
|
|
loading = false
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
session = await getSession()
|
|
|
|
|
loading = false
|
|
|
|
|
if (session) {
|
|
|
|
|
await syncPull()
|
|
|
|
|
startSSE()
|
|
|
|
|
startSyncLoop()
|
|
|
|
|
}
|
|
|
|
|
window.addEventListener('hashchange', () => {
|
|
|
|
|
route = window.location.hash || '#/'
|
2026-03-03 16:09:43 -06:00
|
|
|
mobileNavOpen = false
|
2026-03-03 11:27:07 -06:00
|
|
|
})
|
2026-03-03 13:38:31 -06:00
|
|
|
|
|
|
|
|
// Periodically check for updates
|
|
|
|
|
setInterval(checkVersion, 60000)
|
2026-03-03 11:27:07 -06:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
function onLogin(s) {
|
|
|
|
|
session = s
|
|
|
|
|
window.location.hash = '#/'
|
|
|
|
|
syncPull().then(() => { startSSE(); startSyncLoop() })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function onLogout() {
|
|
|
|
|
await clearSession()
|
|
|
|
|
session = null
|
|
|
|
|
window.location.hash = '#/login'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const path = $derived(route.replace(/^#/, '') || '/')
|
|
|
|
|
const role = $derived(session?.user?.role ?? '')
|
|
|
|
|
</script>
|
|
|
|
|
|
2026-03-03 13:38:31 -06:00
|
|
|
{#if updateAvailable}
|
|
|
|
|
<div class="update-banner">
|
|
|
|
|
A new version is available.
|
|
|
|
|
<button onclick={() => location.reload()}>Refresh</button>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
2026-03-03 11:27:07 -06:00
|
|
|
{#if loading}
|
|
|
|
|
<!-- checking session -->
|
|
|
|
|
{:else if kioskToken}
|
|
|
|
|
<Kiosk />
|
|
|
|
|
{:else if !session}
|
|
|
|
|
<Login onlogin={onLogin} />
|
|
|
|
|
{:else if role === 'gate'}
|
|
|
|
|
<!-- Gate users get the full-screen GateUI instead of the standard layout -->
|
|
|
|
|
<GateUI {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} active={path} open={mobileNavOpen} />
|
2026-03-03 11:27:07 -06:00
|
|
|
<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>
|
2026-03-03 11:27:07 -06:00
|
|
|
{#if path === '/' || path === ''}
|
|
|
|
|
{#if role === 'volunteer_lead'}
|
|
|
|
|
<ScheduleBoard {session} />
|
|
|
|
|
{:else}
|
|
|
|
|
<Dashboard {session} />
|
|
|
|
|
{/if}
|
|
|
|
|
{:else if path.startsWith('/attendees')}
|
|
|
|
|
<Attendees {session} />
|
|
|
|
|
{:else if path.startsWith('/volunteers')}
|
|
|
|
|
<Volunteers {session} />
|
|
|
|
|
{:else if path.startsWith('/departments')}
|
|
|
|
|
<Departments {session} />
|
|
|
|
|
{:else if path.startsWith('/shifts')}
|
|
|
|
|
<Shifts {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}
|
2026-03-03 13:38:31 -06:00
|
|
|
|
|
|
|
|
<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>
|