/** * — guest self-cancel page. * * Reads booking via `/api/bookings/:id?token=...`, lets guest confirm cancel * with optional reason. Phase F adds reschedule suggestions email. */ class FolkScheduleCancel extends HTMLElement { private shadow: ShadowRoot; private space = ""; private bookingId = ""; private token = ""; private booking: any = null; private err = ""; private done = false; constructor() { super(); this.shadow = this.attachShadow({ mode: "open" }); } connectedCallback() { this.space = this.getAttribute("space") || "demo"; this.bookingId = this.getAttribute("booking-id") || ""; const qs = new URLSearchParams(window.location.search); this.token = qs.get("token") || ""; void this.load(); } private apiBase(): string { const path = window.location.pathname; const match = path.match(/^(\/[^/]+)?\/rschedule/); return `${match?.[0] || "/rschedule"}/api`; } private async load() { if (!this.token) { this.err = "Missing cancel token in link."; this.render(); return; } try { const res = await fetch(`${this.apiBase()}/bookings/${this.bookingId}?token=${encodeURIComponent(this.token)}`); if (!res.ok) throw new Error((await res.json().catch(() => ({})))?.error || "Failed to load"); this.booking = await res.json(); } catch (e: any) { this.err = e?.message || String(e); } this.render(); } private async cancel(reason: string) { try { const res = await fetch(`${this.apiBase()}/bookings/${this.bookingId}/cancel`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ token: this.token, reason }), }); if (!res.ok) throw new Error((await res.json().catch(() => ({})))?.error || "Cancel failed"); this.done = true; } catch (e: any) { this.err = e?.message || String(e); } this.render(); } private render() { const b = this.booking; let body = ""; if (this.err) { body = `
${escapeHtml(this.err)}
`; } else if (this.done) { body = `
Booking cancelled. A confirmation email is on the way.
`; } else if (!b) { body = `
Loading booking…
`; } else { const start = new Date(b.startTime).toLocaleString(); body = `

Cancel this booking?

With
${escapeHtml(b.host?.label || b.host?.id || "")}
When
${escapeHtml(start)}
Guest
${escapeHtml(b.guestName || "")}
← Back `; } this.shadow.innerHTML = `
${body}
`; this.shadow.getElementById("cancel-btn")?.addEventListener("click", () => { const reason = (this.shadow.getElementById("reason") as HTMLTextAreaElement)?.value || ""; void this.cancel(reason); }); } } function escapeHtml(s: string): string { return String(s) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } customElements.define("folk-schedule-cancel", FolkScheduleCancel);