Turnpike/frontend/src/App.svelte

168 lines
4.4 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 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'
const clientBuild = __BUILD_ID__
let session = $state(null)
let loading = $state(true)
let route = $state(window.location.hash || '#/')
let updateAvailable = $state(false)
// 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] ?? '')
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()
// 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 || '#/'
})
// Periodically check for updates
setInterval(checkVersion, 60000)
})
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>
{#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}
<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">
<Nav {session} {onLogout} active={path} />
<div class="main">
{#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}
<footer class="build-footer">{clientBuild}</footer>
<style>
.build-footer {
position: fixed;
bottom: 4px;
right: 8px;
font-size: 11px;
color: var(--c-muted);
opacity: 0.5;
pointer-events: none;
}
.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>