/** * — 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"; if (this.space === "demo") { this.loadDemoData(); return; } this.loadTrips(); this.render(); } private loadDemoData() { this.trips = [ { id: "t1", title: "Berlin Maker Week", status: "PLANNING", start_date: "2026-04-14", end_date: "2026-04-20", budget_total: "2500", total_spent: "850", destination_count: 3, description: "Visiting makerspaces and cosmolocal print providers across Berlin" }, { id: "t2", title: "Mediterranean Commons Tour", status: "BOOKED", start_date: "2026-06-01", end_date: "2026-06-15", budget_total: "4000", total_spent: "3200", destination_count: 4, description: "Connecting with commons communities in Barcelona, Marseille, and Athens" } ]; this.render(); } private getDemoTripDetail(id: string): any { if (id === "t1") { return { id: "t1", title: "Berlin Maker Week", status: "PLANNING", start_date: "2026-04-14", end_date: "2026-04-20", budget_total: "2500", description: "Visiting makerspaces and cosmolocal print providers across Berlin", destinations: [ { id: "d1", name: "Druckwerkstatt Berlin", country: "Germany", arrival_date: "2026-04-14" }, { id: "d2", name: "c-base Hackerspace", country: "Germany", arrival_date: "2026-04-16" }, { id: "d3", name: "Fab Lab Berlin", country: "Germany", arrival_date: "2026-04-18" } ], itinerary: [ { id: "i1", title: "Visit Druckwerkstatt", category: "ACTIVITY", date: "2026-04-14", start_time: "10:00" }, { id: "i2", title: "Print workshop", category: "WORKSHOP", date: "2026-04-15", start_time: "09:00" }, { id: "i3", title: "c-base tour", category: "ACTIVITY", date: "2026-04-16", start_time: "14:00" }, { id: "i4", title: "Fab Lab session", category: "WORKSHOP", date: "2026-04-18", start_time: "10:00" }, { id: "i5", title: "Team dinner", category: "SOCIAL", date: "2026-04-19", start_time: "19:00" } ], bookings: [ { id: "bk1", type: "HOTEL", provider: "Hotel Amano", confirmation_number: "AMN-29381", cost: "560" }, { id: "bk2", type: "TRANSPORT", provider: "Deutsche Bahn", confirmation_number: "DB-773920", cost: "85" } ], expenses: [ { id: "e1", category: "TRANSPORT", description: "Flights", amount: "450", date: "2026-04-14" }, { id: "e2", category: "ACCOMMODATION", description: "Accommodation", amount: "280", date: "2026-04-14" }, { id: "e3", category: "ACTIVITY", description: "Workshop fee", amount: "120", date: "2026-04-15" } ], packing: [ { id: "pk1", name: "Laptop", category: "TECH", quantity: 1, packed: true }, { id: "pk2", name: "Notebook", category: "SUPPLIES", quantity: 1, packed: true }, { id: "pk3", name: "USB drives", category: "TECH", quantity: 3, packed: false }, { id: "pk4", name: "Camera", category: "TECH", quantity: 1, packed: false }, { id: "pk5", name: "Adapters", category: "TECH", quantity: 2, packed: false } ] }; } return { id: "t2", title: "Mediterranean Commons Tour", status: "BOOKED", start_date: "2026-06-01", end_date: "2026-06-15", budget_total: "4000", description: "Connecting with commons communities in Barcelona, Marseille, and Athens", destinations: [ { id: "d4", name: "Fab Lab Barcelona", country: "Spain", arrival_date: "2026-06-01" }, { id: "d5", name: "La Coop des Communs", country: "France", arrival_date: "2026-06-05" }, { id: "d6", name: "P2P Foundation Athens", country: "Greece", arrival_date: "2026-06-09" }, { id: "d7", name: "Synergatika Cooperative", country: "Greece", arrival_date: "2026-06-12" } ], itinerary: [ { id: "i6", title: "Fab Lab Barcelona tour", category: "ACTIVITY", date: "2026-06-01", start_time: "10:00" }, { id: "i7", title: "Commons workshop", category: "WORKSHOP", date: "2026-06-02", start_time: "09:00" }, { id: "i8", title: "Marseille meetup", category: "SOCIAL", date: "2026-06-05", start_time: "18:00" }, { id: "i9", title: "P2P Foundation visit", category: "ACTIVITY", date: "2026-06-09", start_time: "11:00" }, { id: "i10", title: "Cooperative workshop", category: "WORKSHOP", date: "2026-06-12", start_time: "10:00" } ], bookings: [ { id: "bk3", type: "TRANSPORT", provider: "Vueling Airlines", confirmation_number: "VY-482910", cost: "320" }, { id: "bk4", type: "HOTEL", provider: "Hotel Casa Bonay", confirmation_number: "CB-11204", cost: "780" }, { id: "bk5", type: "TRANSPORT", provider: "Eurostar", confirmation_number: "ES-556271", cost: "190" } ], expenses: [ { id: "e4", category: "TRANSPORT", description: "Flights and trains", amount: "1200", date: "2026-06-01" }, { id: "e5", category: "ACCOMMODATION", description: "Hotels and hostels", amount: "1400", date: "2026-06-01" }, { id: "e6", category: "FOOD", description: "Meals and groceries", amount: "450", date: "2026-06-01" }, { id: "e7", category: "ACTIVITY", description: "Workshop fees", amount: "150", date: "2026-06-02" } ], packing: [ { id: "pk6", name: "Laptop", category: "TECH", quantity: 1, packed: true }, { id: "pk7", name: "Sunscreen", category: "PERSONAL", quantity: 1, packed: true }, { id: "pk8", name: "Phrasebooks", category: "SUPPLIES", quantity: 2, packed: false }, { id: "pk9", name: "Camera", category: "TECH", quantity: 1, packed: true }, { id: "pk10", name: "Power bank", category: "TECH", quantity: 1, packed: false } ] }; } 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) { if (this.space === "demo") { this.trip = this.getDemoTripDetail(id); this.render(); return; } 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 `
My Trips
${this.trips.length > 0 ? `
${this.trips.map(t => `
${this.esc(t.title)}
${t.destination_count || 0} destinations · ${t.start_date ? new Date(t.start_date).toLocaleDateString() : "No dates"}
${t.budget_total ? `
Budget: $${parseFloat(t.budget_total).toFixed(0)} · 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 `
${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()} — ${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 · ${(t.itinerary || []).length} activities · ${(t.bookings || []).length} bookings · ${(t.packing || []).length} packing items
`; } case "destinations": return (t.destinations || []).length > 0 ? (t.destinations || []).map((d: any) => `
📍
${this.esc(d.name)}
${d.country || ""} ${d.arrival_date ? `· ${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 ? `· $${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);