/**
* hyper-booking — client-side SPA logic.
* Minimal vanilla JS, no framework.
*/
/* global HyperBookingIdentity */
// ---- Path Prefix ----
// Detect prefix from URL so /api/* calls work behind reverse proxies like 7rnx.net/hyper-booking/
const PATH_PREFIX = (function () {
const path = window.location.pathname
const idx = path.indexOf('/web/')
if (idx > 0) return path.substring(0, idx)
const dashIdx = path.indexOf('/dashboard')
if (dashIdx > 0) return path.substring(0, dashIdx)
// Check for known prefix patterns
const match = path.match(/^(\/[^/]+)\//)
if (match && match[1] !== '/api' && match[1] !== '/web') return match[1]
return ''
})()
// ---- Utilities ----
function showToast (message, type = 'info') {
const toast = document.createElement('div')
toast.className = 'toast' + (type !== 'info' ? ' ' + type : '')
toast.textContent = message
document.body.appendChild(toast)
setTimeout(() => toast.remove(), 3500)
}
async function api (method, path, body) {
const opts = {
method,
headers: { 'Content-Type': 'application/json' }
}
if (body) opts.body = JSON.stringify(body)
const res = await fetch(PATH_PREFIX + path, opts)
const data = await res.json()
if (!res.ok) throw new Error(data.error || 'Request failed')
return data
}
function formatTime (time24) {
const [h, m] = time24.split(':').map(Number)
const ampm = h >= 12 ? 'PM' : 'AM'
const h12 = h % 12 || 12
return `${h12}:${String(m).padStart(2, '0')} ${ampm}`
}
function formatDate (dateStr) {
const d = new Date(dateStr + 'T12:00:00Z')
return d.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' })
}
function formatPrice (cents) {
if (cents === 0) return 'Free'
return '$' + (cents / 100).toFixed(cents % 100 === 0 ? 0 : 2)
}
// ---- Sign In ----
const signInBtn = document.getElementById('sign-in-btn')
if (signInBtn) {
signInBtn.addEventListener('click', async () => {
try {
if (typeof HyperBookingIdentity === 'undefined') {
showToast('Identity module not loaded', 'error')
return
}
if (HyperBookingIdentity.hasKeypair()) {
showToast('Already signed in')
return
}
signInBtn.textContent = 'Signing in...'
await HyperBookingIdentity.generate()
showToast('Identity created', 'success')
signInBtn.textContent = 'Signed In'
showDashboardLink()
} catch (err) {
showToast('Sign-in failed: ' + err.message, 'error')
signInBtn.textContent = 'Sign In'
console.error('[Sign-in error]', err)
}
})
// Update button if already signed in
if (typeof HyperBookingIdentity !== 'undefined' && HyperBookingIdentity.hasKeypair()) {
signInBtn.textContent = 'Signed In'
showDashboardLink()
}
}
function showDashboardLink () {
const link = document.getElementById('dashboard-link')
if (link) link.style.display = ''
}
// ---- Booking Page ----
// When a booking page loads, this handles the calendar + slot selection
async function initBookingPage (providerId, providerSlug, packageId) {
const pkg = await api('GET', `/api/packages/${packageId}`)
const today = new Date()
let weekStart = new Date(today)
weekStart.setDate(weekStart.getDate() - weekStart.getDay() + 1) // Monday
const container = document.getElementById('booking-container')
if (!container) return
let selectedSlot = null
async function loadWeek () {
const weekStr = weekStart.toISOString().split('T')[0]
const data = await api('GET', `/api/providers/${providerSlug}/slots?weekOf=${weekStr}&duration=${pkg.sessionDuration}`)
const weekEnd = new Date(weekStart)
weekEnd.setDate(weekEnd.getDate() + 6)
let html = `
<div class="calendar-nav">
<button id="prev-week">←</button>
<span>${formatDate(weekStr)} – ${formatDate(weekEnd.toISOString().split('T')[0])}</span>
<button id="next-week">→</button>
</div>
<div class="calendar-grid">
`
for (const [date, slots] of Object.entries(data.days)) {
html += `<div class="day-column"><h4>${formatDate(date)}</h4>`
if (slots.length === 0) {
html += '<p class="no-slots">No availability</p>'
} else {
for (const slot of slots) {
html += `<button class="slot-btn" data-date="${date}" data-time="${slot.startTime}">${formatTime(slot.startTime)}</button>`
}
}
html += '</div>'
}
html += '</div>'
html += '<div id="selected-summary"></div>'
html += '<button class="btn btn-primary" id="confirm-booking" disabled>Select a time to continue</button>'
container.innerHTML = html
// Event listeners
document.getElementById('prev-week').addEventListener('click', () => {
weekStart.setDate(weekStart.getDate() - 7)
loadWeek()
})
document.getElementById('next-week').addEventListener('click', () => {
weekStart.setDate(weekStart.getDate() + 7)
loadWeek()
})
container.querySelectorAll('.slot-btn').forEach(btn => {
btn.addEventListener('click', () => {
container.querySelectorAll('.slot-btn').forEach(b => b.classList.remove('selected'))
btn.classList.add('selected')
selectedSlot = { date: btn.dataset.date, time: btn.dataset.time }
document.getElementById('selected-summary').innerHTML = `
<div class="selected-slot">
<strong>${formatDate(selectedSlot.date)}</strong> at <strong>${formatTime(selectedSlot.time)}</strong>
</div>
`
const confirmBtn = document.getElementById('confirm-booking')
confirmBtn.disabled = false
confirmBtn.textContent = pkg.price === 0 ? 'Book Free Session' : `Book — ${formatPrice(pkg.price)}`
})
})
document.getElementById('confirm-booking').addEventListener('click', async () => {
if (!selectedSlot) return
// Ensure identity exists
if (typeof HyperBookingIdentity !== 'undefined' && !HyperBookingIdentity.hasKeypair()) {
await HyperBookingIdentity.generate()
}
try {
const booking = await api('POST', '/api/bookings', {
providerId,
packageId,
date: selectedSlot.date,
startTime: selectedSlot.time
})
container.innerHTML = `
<div style="text-align: center; padding: 3rem 0;">
<h2 style="color: var(--success);">Booking Confirmed!</h2>
<p style="margin: 1rem 0;">
<strong>${formatDate(booking.date)}</strong> at <strong>${formatTime(booking.startTimeLocal)}</strong>
</p>
<p style="color: var(--text-secondary);">${pkg.name}</p>
<p style="margin-top: 2rem;">
<a href="/${providerSlug}" class="btn btn-secondary">Back to Provider</a>
</p>
</div>
`
showToast('Booking confirmed!', 'success')
} catch (err) {
showToast(err.message, 'error')
}
})
}
await loadWeek()
}
// Expose for inline use
window.initBookingPage = initBookingPage
window.showToast = showToast