import { useState, useRef, useCallback } from "preact/hooks"; import type { ActualEntry, ActualKind, ActualDay, EntryWeather, WeatherCondition, CellularLevel } from "../../lib/types"; const KIND_META: Record = { food: { icon: "\u{1F37D}\uFE0F", label: "Food", color: "#CCA050", bg: "rgba(204,160,80,0.12)" }, stop: { icon: "\u{1F4CD}", label: "Stop", color: "#7E98B0", bg: "rgba(126,152,176,0.12)" }, note: { icon: "\u{1F4DD}", label: "Note", color: "#6B7280", bg: "rgba(107,114,128,0.12)" }, departure: { icon: "\u{1F697}", label: "Departure", color: "#85A894", bg: "rgba(133,168,148,0.12)" }, arrival: { icon: "\u{1F3E8}", label: "Arrival", color: "#C9A84C", bg: "rgba(201,168,76,0.12)" }, }; const CONDITION_ICON: Record = { "sunny": "\u2600\uFE0F", "mostly-clear": "\uD83C\uDF24\uFE0F", "partly-cloudy": "\u26C5", "cloudy": "\u2601\uFE0F", "foggy": "\uD83C\uDF2B\uFE0F", "rain": "\uD83C\uDF27\uFE0F", }; function tempColor(t: number): string { if (t < 10) return "#60A5FA"; if (t < 18) return "#67E8F9"; if (t < 25) return "#6EE7A0"; if (t < 30) return "#FBBF24"; return "#F87171"; } function uvColor(uv: number): string { if (uv <= 2) return "#6EE7A0"; if (uv <= 5) return "#FBBF24"; if (uv <= 7) return "#FB923C"; if (uv <= 10) return "#F87171"; return "#C084FC"; } function uvLabel(uv: number): string { if (uv <= 2) return "Low"; if (uv <= 5) return "Mod"; if (uv <= 7) return "High"; if (uv <= 10) return "V.High"; return "Extreme"; } const CELL_META: Record = { good: { icon: "\u{1F4F6}", label: "Good", color: "#6EE7A0", bg: "rgba(110,231,160,0.15)" }, spotty: { icon: "\u{1F4F6}", label: "Spotty", color: "#FBBF24", bg: "rgba(251,191,36,0.15)" }, none: { icon: "\u{1F4F5}", label: "None", color: "#F87171", bg: "rgba(248,113,113,0.15)" }, }; function shortTime(iso: string): string { const d = new Date(iso); if (isNaN(d.getTime())) return ""; const h = d.getHours(); const ampm = h >= 12 ? "p" : "a"; const h12 = h % 12 || 12; return `${h12}${ampm}`; } function DayStrips({ entries }: { entries: ActualEntry[] }) { const withWeather = entries.filter(e => e.weather); const withCell = entries.filter(e => e.cellular && e.cellular !== "good"); if (withWeather.length === 0 && withCell.length === 0) return null; const temps = withWeather.map(e => ({ time: e.time, temp: e.weather!.temp })); const uvs = withWeather.filter(e => e.weather!.uv != null).map(e => ({ time: e.time, uv: e.weather!.uv!, title: e.title })); type CellSegment = { start: string; end: string; level: CellularLevel; titles: string[] }; const cellSegments: CellSegment[] = []; for (const e of entries) { const level = e.cellular || "good"; if (level === "good") continue; const last = cellSegments[cellSegments.length - 1]; if (last && last.level === level) { last.end = e.time; last.titles.push(e.title.split("—")[0].trim()); } else { cellSegments.push({ start: e.time, end: e.time, level, titles: [e.title.split("—")[0].trim()] }); } } return (
{temps.length > 0 && (
Temp
{temps.map((t, i) => (
{t.temp}° {shortTime(t.time)}
))}
)} {uvs.length > 0 && (
UV
{uvs.map((u, i) => (
{u.uv} {shortTime(u.time)}
))}
)} {cellSegments.length > 0 && (
Signal
{cellSegments.map((seg, i) => { const meta = CELL_META[seg.level]; return (
{meta.icon} {meta.label} {shortTime(seg.start)}{seg.end !== seg.start ? `–${shortTime(seg.end)}` : ""} {seg.titles.join(", ")}
); })}
)}
); } function WeatherDetails({ w }: { w: EntryWeather }) { const hasDetails = w.wind || w.humidity != null || w.uv != null || w.sunrise || w.sunset || w.note; if (!hasDetails) return null; return (
{w.wind && ( {w.wind.dir} {w.wind.speed} km/h )} {w.humidity != null && ( {w.humidity}% )} {w.uv != null && ( = 6 ? "high" : "mod"}>UV {w.uv} )} {(w.sunrise || w.sunset) && ( {w.sunrise && <>{"\u2191"}{w.sunrise}} {w.sunrise && w.sunset && " "} {w.sunset && <>{"\u2193"}{w.sunset}} )} {w.note && {w.note}}
); } function formatTime(iso: string): string { const d = new Date(iso); if (isNaN(d.getTime())) return ""; const h = d.getHours(); const m = d.getMinutes(); const ampm = h >= 12 ? "PM" : "AM"; const h12 = h % 12 || 12; return `${h12}:${m.toString().padStart(2, "0")} ${ampm}`; } function totalEntries(days: ActualDay[]): number { return days.reduce((sum, d) => sum + d.entries.length, 0); } export function ActualTab({ actuals, tripId }: { actuals: ActualDay[]; tripId: string }) { const [days, setDays] = useState(actuals); const [noteText, setNoteText] = useState(""); const saveTimeout = useRef | null>(null); const persist = useCallback((dayIndex: number, entry: ActualEntry) => { fetch(`/travel/api/actuals?tripId=${tripId}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ dayIndex, entry }), }).catch(() => {}); }, [tripId]); const addNote = () => { const text = noteText.trim(); if (!text) return; const entry: ActualEntry = { time: new Date().toISOString().slice(0, 19), kind: "note", title: text.length > 60 ? text.slice(0, 57) + "..." : text, body: text.length > 60 ? text : undefined, tags: ["quick-note"], }; const updated = [...days]; let dayIndex: number; if (updated.length === 0) { updated.push({ label: "Journal", entries: [entry] }); dayIndex = 0; } else { dayIndex = updated.length - 1; const last = { ...updated[dayIndex] }; last.entries = [...last.entries, entry]; updated[dayIndex] = last; } setDays(updated); setNoteText(""); persist(dayIndex, entry); }; return (
{totalEntries(days)} entries
{days.map((day, dayIdx) => (
{day.label}
{day.entries.length} entries
{day.entries.map((entry, i) => { const uid = dayIdx + "-" + i; const meta = KIND_META[entry.kind] || KIND_META.note; const w = entry.weather; const hasWeatherDetails = w && (w.wind || w.humidity != null || w.uv != null || w.sunrise || w.sunset || w.note); const hasDetail = entry.body || entry.items?.length || entry.rating || entry.mood || entry.placeName || hasWeatherDetails; const cell = entry.cellular ? CELL_META[entry.cellular] : null; const showCell = cell && entry.cellular !== "good"; return (
{formatTime(entry.time)} {entry.title} {showCell && ( {cell!.icon} )} {meta.icon} {meta.label}
{hasDetail && (
{entry.placeName && (
{"\uD83D\uDCCD"} {entry.placeName}
)} {entry.body &&
{entry.body}
} {entry.items && entry.items.length > 0 && (
{(entry.suggestedOrder ? entry.suggestedOrder.map((id: string) => entry.items!.find((i: any) => i.id === id)).filter(Boolean) : entry.items ).map((item: any, idx: number) => typeof item === "string" ? ( {item} ) : (
{item.name} {item.duration && {item.duration}} {item.free && free}
{item.where &&
{item.where}
} {item.note &&
{item.note}
}
) )}
)} {entry.rating != null && (
{entry.rating}/10
)} {entry.mood && (
{entry.mood}
)} {w && } {entry.tags && entry.tags.length > 0 && (
{entry.tags.map(t => ( {t} ))}
)}
)}
); })}
))} {totalEntries(days) === 0 && (
No journal entries yet.
)}
Quick Note