/** * — collaborative trip planning dashboard. * * Views: trip list → trip detail (tabs: overview, destinations, * itinerary, bookings, expenses, packing). */ class FolkTripsPlanner extends HTMLElement { private shadow: ShadowRoot; private space = ""; private view: "list" | "detail" = "list"; private trips: any[] = []; private trip: any = null; private tab: "overview" | "destinations" | "itinerary" | "bookings" | "expenses" | "packing" = "overview"; private error = ""; constructor() { super(); this.shadow = this.attachShadow({ mode: "open" }); } connectedCallback() { this.space = this.getAttribute("space") || "demo"; this.loadTrips(); this.render(); } private getApiBase(): string { const path = window.location.pathname; const match = path.match(/^\/([^/]+)\/trips/); return match ? `/${match[1]}/trips` : ""; } private async loadTrips() { try { const base = this.getApiBase(); const res = await fetch(`${base}/api/trips`); if (res.ok) this.trips = await res.json(); } catch { this.trips = []; } this.render(); } private async loadTrip(id: string) { try { const base = this.getApiBase(); const res = await fetch(`${base}/api/trips/${id}`); if (res.ok) this.trip = await res.json(); } catch { this.error = "Failed to load trip"; } this.render(); } private async createTrip() { const title = prompt("Trip name:"); if (!title?.trim()) return; try { const base = this.getApiBase(); const res = await fetch(`${base}/api/trips`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title: title.trim() }), }); if (res.ok) this.loadTrips(); } catch { this.error = "Failed to create trip"; this.render(); } } private render() { this.shadow.innerHTML = ` ${this.error ? `
${this.esc(this.error)}
` : ""} ${this.view === "list" ? this.renderList() : this.renderDetail()} `; this.attachListeners(); } private renderList(): string { return `
\u2708\uFE0F My Trips
${this.trips.length > 0 ? `
${this.trips.map(t => `
${this.esc(t.title)}
${t.destination_count || 0} destinations \u00B7 ${t.start_date ? new Date(t.start_date).toLocaleDateString() : "No dates"}
${t.budget_total ? `
Budget: $${parseFloat(t.budget_total).toFixed(0)} \u00B7 Spent: $${parseFloat(t.total_spent || 0).toFixed(0)}
` : ""} ${t.status || "PLANNING"}
`).join("")}
` : `

No trips yet

Start planning your next adventure

`} `; } private renderDetail(): string { if (!this.trip) return '
Loading...
'; const t = this.trip; return `
\u2708\uFE0F ${this.esc(t.title)} ${t.status || "PLANNING"}
${(["overview", "destinations", "itinerary", "bookings", "expenses", "packing"] as const).map(tab => `` ).join("")}
${this.renderTab()} `; } private renderTab(): string { const t = this.trip; switch (this.tab) { case "overview": { const spent = (t.expenses || []).reduce((s: number, e: any) => s + parseFloat(e.amount || 0), 0); const pct = t.budget_total ? Math.min(100, (spent / parseFloat(t.budget_total)) * 100) : 0; return `
Trip Details
${t.description || "No description"}
${t.start_date ? `
Dates: ${new Date(t.start_date).toLocaleDateString()} \u2014 ${t.end_date ? new Date(t.end_date).toLocaleDateString() : "open"}
` : ""} ${t.budget_total ? `
Budget
$${spent.toFixed(0)} spent $${parseFloat(t.budget_total).toFixed(0)} budget
` : ""}
Summary
${(t.destinations || []).length} destinations \u00B7 ${(t.itinerary || []).length} activities \u00B7 ${(t.bookings || []).length} bookings \u00B7 ${(t.packing || []).length} packing items
`; } case "destinations": return (t.destinations || []).length > 0 ? (t.destinations || []).map((d: any) => `
\u{1F4CD}
${this.esc(d.name)}
${d.country || ""} ${d.arrival_date ? `\u00B7 ${new Date(d.arrival_date).toLocaleDateString()}` : ""}
`).join("") : '
No destinations added yet
'; case "itinerary": return (t.itinerary || []).length > 0 ? (t.itinerary || []).map((i: any) => `
${i.category || "ACTIVITY"}
${this.esc(i.title)}
${i.date ? new Date(i.date).toLocaleDateString() : ""} ${i.start_time || ""}
`).join("") : '
No itinerary items yet
'; case "bookings": return (t.bookings || []).length > 0 ? (t.bookings || []).map((b: any) => `
${b.type || "OTHER"}
${this.esc(b.provider || "Booking")}
${b.confirmation_number ? `#${b.confirmation_number}` : ""} ${b.cost ? `\u00B7 $${parseFloat(b.cost).toFixed(0)}` : ""}
`).join("") : '
No bookings yet
'; case "expenses": return (t.expenses || []).length > 0 ? (t.expenses || []).map((e: any) => `
${e.category || "OTHER"}
${this.esc(e.description)}
${e.date ? new Date(e.date).toLocaleDateString() : ""}
$${parseFloat(e.amount).toFixed(2)}
`).join("") : '
No expenses recorded yet
'; case "packing": return (t.packing || []).length > 0 ? `
${(t.packing || []).map((p: any) => `
${this.esc(p.name)} ${p.category} ${p.quantity > 1 ? `x${p.quantity}` : ""}
`).join("")}
` : '
No packing items yet
'; default: return ""; } } private attachListeners() { this.shadow.getElementById("create-trip")?.addEventListener("click", () => this.createTrip()); this.shadow.querySelectorAll("[data-trip]").forEach(el => { el.addEventListener("click", () => { this.view = "detail"; this.tab = "overview"; this.loadTrip((el as HTMLElement).dataset.trip!); }); }); this.shadow.querySelectorAll("[data-back]").forEach(el => { el.addEventListener("click", () => { this.view = "list"; this.loadTrips(); }); }); this.shadow.querySelectorAll("[data-tab]").forEach(el => { el.addEventListener("click", () => { this.tab = (el as HTMLElement).dataset.tab as any; this.render(); }); }); this.shadow.querySelectorAll("[data-pack]").forEach(el => { el.addEventListener("change", async () => { const checkbox = el as HTMLInputElement; try { const base = this.getApiBase(); await fetch(`${base}/api/packing/${checkbox.dataset.pack}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ packed: checkbox.checked }), }); } catch {} }); }); } private esc(s: string): string { const d = document.createElement("div"); d.textContent = s || ""; return d.innerHTML; } } customElements.define("folk-trips-planner", FolkTripsPlanner);