import { FolkShape } from "./folk-shape"; import { css, html } from "./tags"; const styles = css` :host { background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); min-width: 300px; min-height: 200px; } .header { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; background: #0d9488; color: white; border-radius: 8px 8px 0 0; font-size: 12px; font-weight: 600; cursor: move; } .header-title { display: flex; align-items: center; gap: 6px; } .header-actions { display: flex; gap: 4px; } .header-actions button { background: transparent; border: none; color: white; cursor: pointer; padding: 2px 6px; border-radius: 4px; font-size: 14px; } .header-actions button:hover { background: rgba(255, 255, 255, 0.2); } .itinerary-container { padding: 12px; max-height: 400px; overflow-y: auto; } .day-group { margin-bottom: 12px; } .day-label { font-size: 11px; font-weight: 600; color: #0d9488; padding: 4px 0; border-bottom: 1px solid #e2e8f0; margin-bottom: 6px; } .item { display: flex; align-items: flex-start; gap: 8px; padding: 6px 8px; font-size: 12px; border-radius: 4px; margin-bottom: 4px; background: #f8fafc; } .item:hover { background: #f1f5f9; } .item-icon { font-size: 14px; flex-shrink: 0; margin-top: 1px; } .item-content { flex: 1; min-width: 0; } .item-title { font-weight: 500; color: #1e293b; } .item-time { font-size: 10px; color: #64748b; margin-top: 1px; } .item-desc { font-size: 11px; color: #94a3b8; margin-top: 2px; } .category-badge { font-size: 9px; padding: 1px 5px; border-radius: 3px; background: #e2e8f0; color: #475569; white-space: nowrap; } .add-form { padding: 8px 12px; border-top: 1px solid #e2e8f0; } .add-form input { width: 100%; padding: 6px 8px; border: 1px solid #e2e8f0; border-radius: 4px; font-size: 12px; outline: none; } .add-form input:focus { border-color: #0d9488; } .empty-state { text-align: center; padding: 20px; color: #94a3b8; font-size: 12px; } `; export interface ItineraryEntry { id: string; title: string; date?: string; startTime?: string; endTime?: string; category: string; description?: string; } const CATEGORY_ICONS: Record = { FLIGHT: "\u2708\uFE0F", TRANSPORT: "\uD83D\uDE8C", ACCOMMODATION: "\uD83C\uDFE8", ACTIVITY: "\uD83C\uDFAF", MEAL: "\uD83C\uDF7D\uFE0F", FREE_TIME: "\u2600\uFE0F", OTHER: "\uD83D\uDCCC", }; declare global { interface HTMLElementTagNameMap { "folk-itinerary": FolkItinerary; } } export class FolkItinerary extends FolkShape { static override tagName = "folk-itinerary"; static { const sheet = new CSSStyleSheet(); const parentRules = Array.from(FolkShape.styles.cssRules) .map((r) => r.cssText) .join("\n"); const childRules = Array.from(styles.cssRules) .map((r) => r.cssText) .join("\n"); sheet.replaceSync(`${parentRules}\n${childRules}`); this.styles = sheet; } #items: ItineraryEntry[] = []; #tripTitle = "Trip Itinerary"; #listEl: HTMLElement | null = null; get items() { return this.#items; } set items(items: ItineraryEntry[]) { this.#items = items; this.#render(); this.dispatchEvent(new CustomEvent("content-change")); } get tripTitle() { return this.#tripTitle; } set tripTitle(title: string) { this.#tripTitle = title; this.#render(); } addItem(item: ItineraryEntry) { this.#items.push(item); this.#render(); this.dispatchEvent(new CustomEvent("content-change")); } override createRenderRoot() { const root = super.createRenderRoot(); const wrapper = document.createElement("div"); wrapper.innerHTML = html`
\uD83D\uDCC5 Itinerary
`; const slot = root.querySelector("slot"); const containerDiv = slot?.parentElement as HTMLElement; if (containerDiv) { containerDiv.replaceWith(wrapper); } this.#listEl = wrapper.querySelector(".item-list"); const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement; const addInput = wrapper.querySelector(".add-input") as HTMLInputElement; closeBtn.addEventListener("click", (e) => { e.stopPropagation(); this.dispatchEvent(new CustomEvent("close")); }); addInput.addEventListener("keydown", (e) => { if (e.key === "Enter" && addInput.value.trim()) { e.stopPropagation(); this.addItem({ id: `item-${Date.now()}`, title: addInput.value.trim(), category: "ACTIVITY", }); addInput.value = ""; } }); addInput.addEventListener("click", (e) => e.stopPropagation()); this.#render(); return root; } #escapeHtml(text: string): string { const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } #render() { if (!this.#listEl) return; if (this.#items.length === 0) { this.#listEl.innerHTML = '
No items yet. Type below to add one.
'; return; } // Group by date const groups = new Map(); const noDate: ItineraryEntry[] = []; for (const item of this.#items) { if (item.date) { const key = item.date.split("T")[0]; if (!groups.has(key)) groups.set(key, []); groups.get(key)!.push(item); } else { noDate.push(item); } } let result = ""; // Sorted date groups const sortedDates = Array.from(groups.keys()).sort(); for (const dateStr of sortedDates) { const items = groups.get(dateStr)!; const date = new Date(dateStr); const label = date.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" }); result += `
`; result += `
${this.#escapeHtml(label)}
`; for (const item of items) { result += this.#renderItem(item); } result += `
`; } // Items without dates if (noDate.length > 0) { result += `
`; result += `
Unscheduled
`; for (const item of noDate) { result += this.#renderItem(item); } result += `
`; } this.#listEl.innerHTML = result; } #renderItem(item: ItineraryEntry): string { const icon = CATEGORY_ICONS[item.category] || CATEGORY_ICONS.OTHER; let timeStr = ""; if (item.startTime) { timeStr = item.startTime; if (item.endTime) timeStr += ` - ${item.endTime}`; } return `
${icon}
${this.#escapeHtml(item.title)}
${timeStr ? `
${this.#escapeHtml(timeStr)}
` : ""} ${item.description ? `
${this.#escapeHtml(item.description)}
` : ""}
${this.#escapeHtml(item.category)}
`; } override toJSON() { return { ...super.toJSON(), type: "folk-itinerary", tripTitle: this.#tripTitle, items: this.#items, }; } }