Switched to path routing. Added data management.
This commit is contained in:
parent
8dc5d3ed01
commit
4bba0ed3a0
14 changed files with 256 additions and 46 deletions
|
|
@ -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 ?? '')
|
||||
</script>
|
||||
|
||||
|
|
@ -107,7 +113,7 @@
|
|||
{#if mobileNavOpen}
|
||||
<div class="nav-overlay" onclick={() => mobileNavOpen = false} onkeydown={() => {}}></div>
|
||||
{/if}
|
||||
<Nav {session} {onLogout} active={path} open={mobileNavOpen} />
|
||||
<Nav {session} {onLogout} {navigate} active={path} open={mobileNavOpen} />
|
||||
<div class="main">
|
||||
<header class="mobile-header">
|
||||
<button class="hamburger" onclick={() => mobileNavOpen = !mobileNavOpen} aria-label="Menu">
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export async function apiFetch(path, options = {}) {
|
|||
const res = await fetch(path, { ...options, headers })
|
||||
if (res.status === 401) {
|
||||
await db.session.clear()
|
||||
window.location.hash = '#/login'
|
||||
window.location.pathname = '/login'
|
||||
throw new Error('unauthorized')
|
||||
}
|
||||
return res
|
||||
|
|
@ -110,6 +110,11 @@ export const api = {
|
|||
update: (data) => apiJSON('/api/settings', { method: 'PUT', body: JSON.stringify(data) }),
|
||||
testEmail: (to) => apiJSON('/api/settings/test-email', { method: 'POST', body: JSON.stringify({ to }) }),
|
||||
toggleShiftSignups: (open) => apiJSON('/api/settings/shift-signups', { method: 'POST', body: JSON.stringify({ open }) }),
|
||||
resetAttendees: () => apiJSON('/api/settings/reset-attendees', { method: 'POST' }),
|
||||
resetVolunteers: () => apiJSON('/api/settings/reset-volunteers', { method: 'POST' }),
|
||||
resetShifts: () => apiJSON('/api/settings/reset-shifts', { method: 'POST' }),
|
||||
resetDepartments: () => apiJSON('/api/settings/reset-departments', { method: 'POST' }),
|
||||
resetVolunteerShifts: () => apiJSON('/api/settings/reset-volunteer-shifts', { method: 'POST' }),
|
||||
},
|
||||
signup: {
|
||||
config: () => kioskFetch('/api/public/signup-config'),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { LayoutDashboard, ClipboardCheck, Heart, Hexagon, Clock, CalendarDays, Upload, Users, Settings, LogOut } from 'lucide-svelte'
|
||||
|
||||
let { session, active, onLogout, open = false } = $props()
|
||||
let { session, active, onLogout, navigate, open = false } = $props()
|
||||
|
||||
const role = $derived(session?.user?.role ?? '')
|
||||
|
||||
|
|
@ -9,45 +9,45 @@
|
|||
|
||||
const links = $derived.by(() => {
|
||||
if (role === 'ticketing') return [
|
||||
{ href: '#/attendees', label: 'Attendees', icon: ClipboardCheck },
|
||||
{ href: '#/import', label: 'Import', icon: Upload },
|
||||
{ href: '/attendees', label: 'Attendees', icon: ClipboardCheck },
|
||||
{ href: '/import', label: 'Import', icon: Upload },
|
||||
]
|
||||
if (role === 'volunteer_lead') return [
|
||||
{ href: '#/', label: 'Schedule', icon: CalendarDays },
|
||||
{ href: '#/volunteers', label: 'Volunteers', icon: Heart },
|
||||
{ href: '#/departments', label: 'Departments', icon: Hexagon },
|
||||
{ href: '/', label: 'Schedule', icon: CalendarDays },
|
||||
{ href: '/volunteers', label: 'Volunteers', icon: Heart },
|
||||
{ href: '/departments', label: 'Departments', icon: Hexagon },
|
||||
]
|
||||
if (role === 'coordinator') return [
|
||||
{ href: '#/', label: 'Dashboard', icon: LayoutDashboard },
|
||||
{ href: '#/schedule', label: 'Schedule', icon: CalendarDays },
|
||||
{ href: '#/volunteers', label: 'Volunteers', icon: Heart },
|
||||
{ href: '#/departments', label: 'Departments', icon: Hexagon },
|
||||
{ href: '#/shifts', label: 'Shifts', icon: Clock },
|
||||
{ href: '/', label: 'Dashboard', icon: LayoutDashboard },
|
||||
{ href: '/schedule', label: 'Schedule', icon: CalendarDays },
|
||||
{ href: '/volunteers', label: 'Volunteers', icon: Heart },
|
||||
{ href: '/departments', label: 'Departments', icon: Hexagon },
|
||||
{ href: '/shifts', label: 'Shifts', icon: Clock },
|
||||
]
|
||||
return [
|
||||
{ href: '#/', label: 'Dashboard', icon: LayoutDashboard },
|
||||
{ href: '#/attendees', label: 'Attendees', icon: ClipboardCheck },
|
||||
{ href: '#/volunteers', label: 'Volunteers', icon: Heart },
|
||||
{ href: '#/departments', label: 'Departments', icon: Hexagon },
|
||||
{ href: '#/shifts', label: 'Shifts', icon: Clock },
|
||||
{ href: '#/schedule', label: 'Schedule', icon: CalendarDays },
|
||||
{ href: '#/import', label: 'Import', icon: Upload },
|
||||
{ href: '#/users', label: 'Users', icon: Users },
|
||||
{ href: '#/settings', label: 'Settings', icon: Settings },
|
||||
{ href: '/', label: 'Dashboard', icon: LayoutDashboard },
|
||||
{ href: '/attendees', label: 'Attendees', icon: ClipboardCheck },
|
||||
{ href: '/volunteers', label: 'Volunteers', icon: Heart },
|
||||
{ href: '/departments', label: 'Departments', icon: Hexagon },
|
||||
{ href: '/shifts', label: 'Shifts', icon: Clock },
|
||||
{ href: '/schedule', label: 'Schedule', icon: CalendarDays },
|
||||
{ href: '/import', label: 'Import', icon: Upload },
|
||||
{ href: '/users', label: 'Users', icon: Users },
|
||||
{ href: '/settings', label: 'Settings', icon: Settings },
|
||||
]
|
||||
})
|
||||
|
||||
function isActive(href) {
|
||||
const p = href.replace(/^#/, '')
|
||||
if (p === '/') return active === '/' || active === ''
|
||||
return active.startsWith(p)
|
||||
if (href === '/') return active === '/' || active === ''
|
||||
return active.startsWith(href)
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav class="sidebar" class:open>
|
||||
<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)}>
|
||||
<a href={link.href} class="nav-link" class:active={isActive(link.href)}
|
||||
onclick={(e) => { e.preventDefault(); navigate(link.href) }}>
|
||||
<link.icon {...iconProps} />
|
||||
{link.label}
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
let error = $state('')
|
||||
|
||||
onMount(async () => {
|
||||
const match = window.location.hash.match(/^#\/confirm\/(.+)/)
|
||||
const match = window.location.pathname.match(/^\/confirm\/(.+)/)
|
||||
const token = match?.[1] ?? ''
|
||||
if (!token) {
|
||||
status = 'invalid'
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@
|
|||
import { onMount } from 'svelte'
|
||||
import { api } from '../api.js'
|
||||
|
||||
// Token comes from the URL hash: /#/v/TOKEN
|
||||
const token = $derived(window.location.hash.match(/^#\/v\/([A-Z0-9]+)/i)?.[1] ?? '')
|
||||
const token = $derived(window.location.pathname.match(/^\/v\/([A-Z0-9]+)/i)?.[1] ?? '')
|
||||
|
||||
let state = $state(null) // { volunteer, shifts, available }
|
||||
let loading = $state(true)
|
||||
|
|
|
|||
|
|
@ -84,6 +84,18 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function resetModel(label, fn) {
|
||||
if (!confirm(`PERMANENTLY DELETE all ${label}? This cannot be undone.`)) return
|
||||
error = ''
|
||||
success = ''
|
||||
try {
|
||||
const result = await fn()
|
||||
success = `Deleted ${result.deleted} ${label}.`
|
||||
} catch (err) {
|
||||
error = err.message
|
||||
}
|
||||
}
|
||||
|
||||
async function sendTest() {
|
||||
if (!testEmail) return
|
||||
testing = true
|
||||
|
|
@ -186,12 +198,12 @@
|
|||
Note field is required
|
||||
</label>
|
||||
<p class="text-muted" style="font-size:0.78rem;margin-top:0.75rem">
|
||||
Signup form: <a href="/#/volunteer-signup" target="_blank" style="color:var(--c-accent)">/#/volunteer-signup</a>
|
||||
Signup form: <a href="/volunteer-signup" target="_blank" style="color:var(--c-accent)">/volunteer-signup</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Shift Signups -->
|
||||
<div class="card">
|
||||
<div class="card" style="margin-bottom:1.5rem">
|
||||
<h2 style="font-size:0.95rem;font-weight:700;margin-bottom:1rem">Shift Signups</h2>
|
||||
<div style="display:flex;align-items:center;gap:1rem">
|
||||
<span style="font-size:0.875rem">
|
||||
|
|
@ -212,5 +224,30 @@
|
|||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Data Management -->
|
||||
<div class="card">
|
||||
<h2 style="font-size:0.95rem;font-weight:700;margin-bottom:0.5rem">Data Management</h2>
|
||||
<p class="text-muted" style="font-size:0.78rem;margin-bottom:1rem">
|
||||
Permanently delete all records of a given type. This cannot be undone.
|
||||
</p>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:0.5rem">
|
||||
<button class="btn btn-danger" onclick={() => resetModel('attendees', api.settings.resetAttendees)}>
|
||||
Delete All Attendees
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick={() => resetModel('volunteers', api.settings.resetVolunteers)}>
|
||||
Delete All Volunteers
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick={() => resetModel('shifts', api.settings.resetShifts)}>
|
||||
Delete All Shifts
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick={() => resetModel('departments', api.settings.resetDepartments)}>
|
||||
Delete All Departments
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick={() => resetModel('volunteer shift assignments', api.settings.resetVolunteerShifts)}>
|
||||
Delete All Shift Assignments
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue