Created Turnpike, event attendee and volunteer management
Built after prototype, Traverse, an attendee and volunteer list maintainer.
This commit is contained in:
commit
1033cdb29b
59 changed files with 8663 additions and 0 deletions
140
frontend/src/api.js
Normal file
140
frontend/src/api.js
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
import { db } from './db.js'
|
||||
|
||||
async function getToken() {
|
||||
const session = await db.session.get(1)
|
||||
return session?.token ?? null
|
||||
}
|
||||
|
||||
export async function apiFetch(path, options = {}) {
|
||||
const token = await getToken()
|
||||
const headers = {}
|
||||
// Don't set Content-Type for FormData — browser sets it with correct boundary
|
||||
if (!(options.body instanceof FormData)) {
|
||||
headers['Content-Type'] = 'application/json'
|
||||
}
|
||||
Object.assign(headers, options.headers)
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`
|
||||
|
||||
const res = await fetch(path, { ...options, headers })
|
||||
if (res.status === 401) {
|
||||
await db.session.clear()
|
||||
window.location.hash = '#/login'
|
||||
throw new Error('unauthorized')
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
export async function apiJSON(path, options = {}) {
|
||||
const res = await apiFetch(path, options)
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ error: res.statusText }))
|
||||
throw new Error(err.error || res.statusText)
|
||||
}
|
||||
return res.json()
|
||||
}
|
||||
|
||||
// Unauthenticated fetch for the kiosk (no JWT, no redirect on 401)
|
||||
async function kioskFetch(path, options = {}) {
|
||||
const headers = { 'Content-Type': 'application/json', ...options.headers }
|
||||
const res = await fetch(path, { ...options, headers })
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ error: res.statusText }))
|
||||
const e = new Error(err.error || res.statusText)
|
||||
e.status = res.status
|
||||
e.body = err
|
||||
throw e
|
||||
}
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export const api = {
|
||||
login: (username, password) =>
|
||||
apiJSON('/api/login', { method: 'POST', body: JSON.stringify({ username, password }) }),
|
||||
logout: () => apiFetch('/api/logout', { method: 'POST' }),
|
||||
me: () => apiJSON('/api/me'),
|
||||
event: {
|
||||
get: () => apiJSON('/api/event'),
|
||||
update: (data) => apiJSON('/api/event', { method: 'PUT', body: JSON.stringify(data) }),
|
||||
},
|
||||
attendees: {
|
||||
list: (params = {}) => apiJSON('/api/attendees?' + new URLSearchParams(params)),
|
||||
get: (id) => apiJSON(`/api/attendees/${id}`),
|
||||
create: (data) => apiJSON('/api/attendees', { method: 'POST', body: JSON.stringify(data) }),
|
||||
update: (id, data) => apiJSON(`/api/attendees/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
|
||||
delete: (id) => apiFetch(`/api/attendees/${id}`, { method: 'DELETE' }),
|
||||
checkIn: (id, opts = {}) =>
|
||||
apiJSON(`/api/attendees/${id}/checkin`, { method: 'POST', body: JSON.stringify(opts) }),
|
||||
generateTokens: () =>
|
||||
apiJSON('/api/attendees/generate-tokens', { method: 'POST' }),
|
||||
emailToken: (id) =>
|
||||
apiJSON(`/api/attendees/${id}/email-token`, { method: 'POST' }),
|
||||
emailAllTokens: () =>
|
||||
apiJSON('/api/attendees/email-tokens', { method: 'POST' }),
|
||||
},
|
||||
volunteers: {
|
||||
list: (params = {}) => apiJSON('/api/volunteers?' + new URLSearchParams(params)),
|
||||
get: (id) => apiJSON(`/api/volunteers/${id}`),
|
||||
create: (data) => apiJSON('/api/volunteers', { method: 'POST', body: JSON.stringify(data) }),
|
||||
update: (id, data) => apiJSON(`/api/volunteers/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
|
||||
delete: (id) => apiFetch(`/api/volunteers/${id}`, { method: 'DELETE' }),
|
||||
checkIn: (id) => apiJSON(`/api/volunteers/${id}/checkin`, { method: 'POST' }),
|
||||
assignShift: (id, shiftId) => apiFetch(`/api/volunteers/${id}/shifts`, { method: 'POST', body: JSON.stringify({ shift_id: shiftId }) }),
|
||||
unassignShift: (id, shiftId) => apiFetch(`/api/volunteers/${id}/shifts/${shiftId}`, { method: 'DELETE' }),
|
||||
},
|
||||
departments: {
|
||||
list: () => apiJSON('/api/departments'),
|
||||
create: (data) => apiJSON('/api/departments', { method: 'POST', body: JSON.stringify(data) }),
|
||||
update: (id, data) => apiJSON(`/api/departments/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
|
||||
delete: (id) => apiFetch(`/api/departments/${id}`, { method: 'DELETE' }),
|
||||
},
|
||||
shifts: {
|
||||
list: (params = {}) => apiJSON('/api/shifts?' + new URLSearchParams(params)),
|
||||
create: (data) => apiJSON('/api/shifts', { method: 'POST', body: JSON.stringify(data) }),
|
||||
update: (id, data) => apiJSON(`/api/shifts/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
|
||||
delete: (id) => apiFetch(`/api/shifts/${id}`, { method: 'DELETE' }),
|
||||
assignVolunteer: (shiftId, volunteerId, force = false) =>
|
||||
apiFetch(`/api/shifts/${shiftId}/volunteers`, { method: 'POST', body: JSON.stringify({ volunteer_id: volunteerId, force }) }),
|
||||
unassignVolunteer: (shiftId, volunteerId) =>
|
||||
apiFetch(`/api/shifts/${shiftId}/volunteers/${volunteerId}`, { method: 'DELETE' }),
|
||||
reorder: (positions) =>
|
||||
apiFetch('/api/shifts/reorder', { method: 'POST', body: JSON.stringify(positions) }),
|
||||
},
|
||||
users: {
|
||||
list: () => apiJSON('/api/users'),
|
||||
create: (data) => apiJSON('/api/users', { method: 'POST', body: JSON.stringify(data) }),
|
||||
update: (id, data) => apiJSON(`/api/users/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
|
||||
delete: (id) => apiFetch(`/api/users/${id}`, { method: 'DELETE' }),
|
||||
},
|
||||
settings: {
|
||||
get: () => apiJSON('/api/settings'),
|
||||
update: (data) => apiJSON('/api/settings', { method: 'PUT', body: JSON.stringify(data) }),
|
||||
testEmail: (to) => apiJSON('/api/settings/test-email', { method: 'POST', body: JSON.stringify({ to }) }),
|
||||
},
|
||||
import: async (formData) => {
|
||||
const res = await apiFetch('/api/import', { method: 'POST', body: formData })
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ error: res.statusText }))
|
||||
throw new Error(err.error || res.statusText)
|
||||
}
|
||||
return res.json()
|
||||
},
|
||||
sync: {
|
||||
pull: (since) => apiJSON('/api/sync/pull' + (since ? `?since=${encodeURIComponent(since)}` : '')),
|
||||
},
|
||||
kiosk: {
|
||||
get: (token) => kioskFetch(`/api/v/${token}`),
|
||||
// claim returns {conflict: true, conflicting_shifts: [...]} on 409, or the updated kiosk state on success.
|
||||
claim: async (token, shiftId, force = false) => {
|
||||
const res = await fetch(`/api/v/${token}/shifts/${shiftId}${force ? '?force=true' : ''}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
const body = await res.json().catch(() => ({}))
|
||||
if (res.status === 409) return { conflict: true, ...body }
|
||||
if (!res.ok) throw new Error(body.error || res.statusText)
|
||||
return body
|
||||
},
|
||||
unclaim: (token, shiftId) =>
|
||||
kioskFetch(`/api/v/${token}/shifts/${shiftId}`, { method: 'DELETE' }),
|
||||
},
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue