// App bootstrap: auth check, top nav, and a tiny hash router. // Routes: #/login, #/trips, #/trip/:id import { api } from './api.js'; import { el, clear, mount, toast, loading } from './dom.js'; import { renderAuth } from './views/auth.js'; import { renderTrips } from './views/trips.js'; import { renderTripDetail } from './views/tripDetail.js'; const state = { user: null }; function root() { return document.getElementById('app'); } function navigate(hash) { if (location.hash === hash) route(); else location.hash = hash; } // Parse "#/trip/5" -> { name: 'trip', params: { id: '5' } } function parseHash() { const raw = (location.hash || '').replace(/^#/, ''); const parts = raw.split('/').filter(Boolean); if (parts.length === 0) return { name: 'trips', params: {} }; if (parts[0] === 'login') return { name: 'login', params: {} }; if (parts[0] === 'trips') return { name: 'trips', params: {} }; if (parts[0] === 'trip' && parts[1]) return { name: 'trip', params: { id: parts[1] } }; return { name: 'trips', params: {} }; } function renderNav() { const nav = el( 'header', { class: 'topnav' }, el( 'a', { class: 'brand', href: '#/trips' }, el('span', { class: 'brand-mark' }, '🧭'), el('span', { class: 'brand-name' }, 'Trip Plan'), ), el( 'div', { class: 'nav-right' }, state.user ? renderUserArea() : null, state.user ? el('button', { class: 'btn btn-ghost', onClick: onLogout }, 'Log out') : null, ), ); return nav; } // Current user's display name with an inline pencil-edit (PATCH /api/auth/me). function renderUserArea() { const area = el('span', { class: 'nav-user' }); function initial() { return (state.user.display_name || '?').charAt(0).toUpperCase(); } function showDisplay() { mount( area, el('span', { class: 'nav-avatar' }, initial()), el('span', { class: 'nav-name' }, state.user.display_name || 'me'), el('button', { class: 'nav-edit', title: 'Edit name', type: 'button', onClick: showEdit }, '✎'), ); } function showEdit() { const input = el('input', { class: 'input input-sm nav-name-input', type: 'text', maxlength: '40', value: state.user.display_name || '', 'aria-label': 'Display name', }); async function save() { const dn = input.value.trim(); if (!dn) return showDisplay(); try { const data = await api.auth.updateMe({ display_name: dn }); state.user = data.user; showDisplay(); } catch (err) { toast(err.message); } } input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); save(); } else if (e.key === 'Escape') showDisplay(); }); mount( area, input, el('button', { class: 'btn btn-sm btn-primary', type: 'button', onClick: save }, 'Save'), el('button', { class: 'btn btn-sm btn-ghost', type: 'button', onClick: showDisplay }, 'Cancel'), ); input.focus(); input.select(); } showDisplay(); return area; } async function onLogout() { try { await api.auth.logout(); } catch (e) { // Even if the request fails, drop local state. } state.user = null; navigate('#/login'); } function render() { const view = parseHash(); // Defensively remove any body-level overlay (e.g. an open day editor) so it // can never orphan on top of a freshly rendered view. document.querySelectorAll('.overlay').forEach((n) => n.remove()); document.body.classList.remove('no-scroll'); // Auth guards. if (!state.user && view.name !== 'login') { navigate('#/login'); return; } if (state.user && view.name === 'login') { navigate('#/trips'); return; } const container = root(); clear(container); if (state.user) container.appendChild(renderNav()); const viewEl = el('main', { class: 'view', id: 'view' }); container.appendChild(viewEl); const ctx = { state, navigate, refresh: render }; if (view.name === 'login') renderAuth(viewEl, ctx); else if (view.name === 'trips') renderTrips(viewEl, ctx); else if (view.name === 'trip') renderTripDetail(viewEl, ctx, view.params.id); } // Exposed so views can update the current user after login/register. function route() { render(); } async function bootstrap() { const container = root(); clear(container); container.appendChild(loading('Starting Trip Plan…')); try { const data = await api.auth.me(); state.user = data && data.user ? data.user : null; } catch (e) { state.user = null; // 401 is expected when logged out. } window.addEventListener('hashchange', route); // render()'s guards redirect a signed-out visitor to #/login and a // signed-in one away from it, so a single render() is enough here. render(); } bootstrap();