import { useEffect, useState, useCallback } from 'react' import { Bell, BellOff, Copy, Plus, Smartphone, Trash2, TriangleAlert } from 'lucide-react' import type { CaldavCredential, CaldavCredentialCreated } from '../types' import { fetchCaldavCredentials, createCaldavCredential, deleteCaldavCredential, fetchVapidKey, subscribePush, unsubscribePush } from '../api' function urlBase64ToUint8Array(base64: string): ArrayBuffer { const padding = '='.repeat((4 - (base64.length % 4)) % 4) const b64 = (base64 + padding).replace(/-/g, '+').replace(/_/g, '/') const raw = atob(b64) const buf = new Uint8Array(raw.length) for (let i = 0; i < raw.length; i++) buf[i] = raw.charCodeAt(i) return buf.buffer } type PushState = 'unsupported' | 'denied' | 'off' | 'on' | 'checking' export default function CalDavSetup() { const [credentials, setCredentials] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [label, setLabel] = useState('') const [creating, setCreating] = useState(false) const [revealed, setRevealed] = useState(null) const [pushState, setPushState] = useState('checking') const [pushBusy, setPushBusy] = useState(false) const [pushError, setPushError] = useState(null) const refreshPushState = useCallback(async () => { if (!('serviceWorker' in navigator) || !('PushManager' in window)) { setPushState('unsupported') return } if (Notification.permission === 'denied') { setPushState('denied') return } try { const reg = await navigator.serviceWorker.ready const sub = await reg.pushManager.getSubscription() setPushState(sub ? 'on' : 'off') } catch { setPushState('off') } }, []) useEffect(() => { refreshPushState() }, [refreshPushState]) async function enablePush() { setPushBusy(true) setPushError(null) try { const reg = await navigator.serviceWorker.ready if (Notification.permission === 'default') { const permission = await Notification.requestPermission() if (permission !== 'granted') { setPushState('denied'); return } } else if (Notification.permission === 'denied') { setPushState('denied') return } const { publicKey } = await fetchVapidKey() const sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(publicKey), }) await subscribePush(sub.toJSON()) setPushState('on') } catch (err) { setPushError(err instanceof Error ? err.message : 'Failed to enable push notifications') } finally { setPushBusy(false) } } async function disablePush() { setPushBusy(true) setPushError(null) try { const reg = await navigator.serviceWorker.ready const sub = await reg.pushManager.getSubscription() if (sub) { await unsubscribePush(sub.endpoint) await sub.unsubscribe() } setPushState('off') } catch (err) { setPushError(err instanceof Error ? err.message : 'Failed to disable push notifications') } finally { setPushBusy(false) } } const caldavUrl = `${window.location.origin}/calendar/caldav/` const reload = useCallback(() => { setLoading(true) setError(null) fetchCaldavCredentials().then(setCredentials).catch(err => setError(err.message)).finally(() => setLoading(false)) }, []) useEffect(() => { reload() }, [reload]) async function handleCreate() { setCreating(true) setError(null) try { const created = await createCaldavCredential(label.trim() || undefined) setRevealed(created) setLabel('') reload() } catch (err) { setError(err instanceof Error ? err.message : 'Failed to create credential') } finally { setCreating(false) } } async function handleRevoke(id: number) { if (!confirm('Revoke this device? It will stop syncing immediately.')) return try { await deleteCaldavCredential(id) if (revealed?.id === id) setRevealed(null) reload() } catch (err) { setError(err instanceof Error ? err.message : 'Failed to revoke credential') } } function copy(text: string) { navigator.clipboard?.writeText(text).catch(() => {}) } return (

Phone Sync

Subscribe to this calendar from your phone or computer's own calendar app (Apple Calendar, Google Calendar, Outlook, …) using CalDAV. Once set up, events created here show up on your device automatically, and new device-created events sync back — no separate app needed.

Each device needs its own generated username and password below — never share your normal hotel login for this.

{error &&
{error}
}
Push notifications
{pushState === 'on' ? ( ) : ( )}
{pushState === 'unsupported' && 'Not supported on this device/browser'} {pushState === 'denied' && 'Blocked — enable notifications for this site in your browser settings'} {pushState === 'checking' && 'Checking status…'} {pushState === 'off' && 'Notifications are off on this device'} {pushState === 'on' && 'Notifications are on for this device'}
{pushError &&
{pushError}
}
{(pushState === 'off' || pushState === 'on') && ( )}
Your CalDAV devices
{loading ? (
Loading…
) : credentials.length === 0 ? (
No devices set up yet.
) : (
{credentials.map(c => ( ))}
Label Username Created Last used
{c.label || '—'} {c.username} {new Date(c.created_at).toLocaleDateString()} {c.last_used_at ? new Date(c.last_used_at).toLocaleString() : 'Never'}
)}
setLabel(e.target.value)} placeholder="e.g. Sarah's iPhone" />
{revealed && (
Save this now — the password won't be shown again.
Username: {revealed.username} copy(revealed.username)} />
Password: {revealed.password} copy(revealed.password)} />
Server URL: {caldavUrl} copy(caldavUrl)} />
)}
Set-up instructions
Apple Calendar (iPhone / Mac)
  1. Settings → Calendar → Accounts → Add Account → Other → Add CalDAV Account.
  2. Server: {caldavUrl}
  3. User Name / Password: the credentials generated above.
  4. Tap Next, then Save.
Google Calendar
  1. Google Calendar doesn't support direct CalDAV subscriptions on the free tier — easiest is to use a CalDAV-sync app such as "CalDAV-Sync" (Android) with the server URL and credentials above.
  2. Alternatively, on desktop, add it as a "secondary" calendar in a CalDAV-aware client and it will appear alongside Google Calendar.
Outlook
  1. Outlook (desktop): File → Account Settings → Internet Calendars → New, then paste {caldavUrl}.
  2. When prompted, enter the username and password generated above.
) }