const EURJPY = 182.85; function mapsLink(q) { return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(q)}`; } function jpyToEur(jpy) { return jpy / EURJPY; } function eurToJpy(eur) { return eur * EURJPY; } function formatJpy(n) { return "¥" + Number(n || 0).toLocaleString("it-IT", { maximumFractionDigits: 0 }); } function formatEur(n) { return new Intl.NumberFormat("it-IT", { style: "currency", currency: "EUR" }).format(n || 0); } // Minimal “calendar” dataset (lorem ipsum placeholders) const DAYS = [ { id: "d01", dateLabel: "14 giugno", title: "Tokyo · Arrivo", city: "Tokyo", base: "Asakusa", summary: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", movements: [ { time: "10:30", from: "Narita", to: "Asakusa", mode: "Train", mapsQuery: "Narita Airport to Asakusa" }, { time: "15:00", from: "Asakusa", to: "Shibuya", mode: "Metro", mapsQuery: "Asakusa to Shibuya" }, ], attractions: [ { name: "Lorem Shrine", note: "Lorem ipsum dolor sit amet.", mapsQuery: "Meiji Jingu" }, { name: "Ipsum Crossing", note: "Sed do eiusmod tempor.", mapsQuery: "Shibuya Crossing" }, ], lunch: [ { name: "Lorem Ramen", mapsQuery: "Ramen Asakusa" }, { name: "Ipsum Sushi", mapsQuery: "Sushi Asakusa" }, ], dinner: [ { name: "Dolor Izakaya", mapsQuery: "Izakaya Shinjuku" }, { name: "Sit Tempura", mapsQuery: "Tempura Shinjuku" }, ], }, { id: "d02", dateLabel: "15 giugno", title: "Tokyo · Tradizione", city: "Tokyo", base: "Asakusa", summary: "Ut enim ad minim veniam, quis nostrud exercitation ullamco.", movements: [ { time: "09:00", from: "Asakusa", to: "Senso-ji", mode: "Walk", mapsQuery: "Senso-ji" }, { time: "12:00", from: "Senso-ji", to: "Skytree", mode: "Walk", mapsQuery: "Tokyo Skytree" }, ], attractions: [ { name: "Senso-ji", note: "Lorem ipsum dolor sit amet.", mapsQuery: "Senso-ji" }, { name: "Tokyo Skytree", note: "Consectetur adipiscing elit.", mapsQuery: "Tokyo Skytree" }, ], lunch: [ { name: "Lorem Bento", mapsQuery: "Tokyo Solamachi food" }, { name: "Ipsum Udon", mapsQuery: "Udon Asakusa" }, ], dinner: [ { name: "Dolor Gyoza", mapsQuery: "Gyoza Akihabara" }, { name: "Sit Soba", mapsQuery: "Soba Asakusa" }, ], }, ]; const elCalendar = document.getElementById("calendar"); const elDayView = document.getElementById("dayView"); function renderCalendar(selectedId) { elCalendar.innerHTML = DAYS.map((d) => { const active = d.id === selectedId ? " is-active" : ""; return ` `; }).join(""); elCalendar.querySelectorAll(".cal-day").forEach((b) => { b.addEventListener("click", () => { const id = b.getAttribute("data-id"); renderDay(id); renderCalendar(id); location.hash = "#giorno"; }); }); } function renderDay(dayId) { const day = DAYS.find((d) => d.id === dayId) || DAYS[0]; const movements = day.movements .map( (m) => `
${m.time}
${m.from} → ${m.to}
${m.mode} · Lorem ipsum dolor sit amet.
Maps
` ) .join(""); const attractions = day.attractions .map( (a) => `
  • ${a.name}
    ${a.note}
  • ` ) .join(""); const lunch = day.lunch .map((p) => `
  • ${p.name}
  • `) .join(""); const dinner = day.dinner .map((p) => `
  • ${p.name}
  • `) .join(""); elDayView.innerHTML = `
    Dove siete
    ${day.city} · ${day.base}
    Nota
    ${day.summary}

    Spostamenti

    ${movements}

    Attrazioni

    Pranzo (suggeriti)

    Cena (suggeriti)

    `; } async function apiFetch(path, options) { const res = await fetch(path, { headers: { "Content-Type": "application/json" }, ...options, }); if (!res.ok) { const text = await res.text(); throw new Error(text || `HTTP ${res.status}`); } if (res.status === 204) return null; return res.json(); } // ----------------- // TODO UI // ----------------- const elTodoList = document.getElementById("todoList"); const elTodoNewText = document.getElementById("todoNewText"); const elTodoAddBtn = document.getElementById("todoAddBtn"); async function renderTodos() { const todos = await apiFetch("/api/todos"); elTodoList.innerHTML = todos.length ? todos .map( (t) => `
    ` ) .join("") : `
    Nessuna voce. Lorem ipsum.
    `; elTodoList.querySelectorAll("input[type=checkbox]").forEach((cb) => { cb.addEventListener("change", async () => { const id = cb.getAttribute("data-id"); await apiFetch(`/api/todos/${id}`, { method: "PATCH", body: JSON.stringify({ done: cb.checked }) }); }); }); elTodoList.querySelectorAll(".todo-del").forEach((b) => { b.addEventListener("click", async () => { const id = b.getAttribute("data-id"); await apiFetch(`/api/todos/${id}`, { method: "DELETE" }); await renderTodos(); }); }); elTodoList.querySelectorAll(".todo-edit").forEach((b) => { b.addEventListener("click", async () => { const id = b.getAttribute("data-id"); const current = b.closest(".todo-item").querySelector(".todo-check span").textContent; const next = prompt("Modifica testo", current); if (next === null) return; await apiFetch(`/api/todos/${id}`, { method: "PATCH", body: JSON.stringify({ text: next }) }); await renderTodos(); }); }); } elTodoAddBtn.addEventListener("click", async () => { const text = (elTodoNewText.value || "").trim(); if (!text) return; await apiFetch("/api/todos", { method: "POST", body: JSON.stringify({ text }) }); elTodoNewText.value = ""; await renderTodos(); }); elTodoNewText.addEventListener("keydown", async (e) => { if (e.key === "Enter") { e.preventDefault(); elTodoAddBtn.click(); } }); // ----------------- // EXPENSES UI // ----------------- const elCostList = document.getElementById("costList"); const elCostDesc = document.getElementById("costDesc"); const elCostJpy = document.getElementById("costJpy"); const elCostEur = document.getElementById("costEur"); const elAddCostBtn = document.getElementById("addCostBtn"); async function renderExpenses() { const expenses = await apiFetch("/api/expenses"); elCostList.innerHTML = expenses.length ? expenses .map( (c) => `
    ${c.description}
    ${formatJpy(c.jpy)} · ${formatEur(c.eur)}
    ` ) .join("") : `
    Nessuna spesa inserita. Lorem ipsum.
    `; const totalJpy = expenses.reduce((a, c) => a + Number(c.jpy || 0), 0); const totalEur = expenses.reduce((a, c) => a + Number(c.eur || 0), 0); document.getElementById("totalJpy").textContent = formatJpy(totalJpy); document.getElementById("totalEur").textContent = formatEur(totalEur); elCostList.querySelectorAll(".cost-del").forEach((b) => { b.addEventListener("click", async () => { const id = b.getAttribute("data-id"); await apiFetch(`/api/expenses/${id}`, { method: "DELETE" }); await renderExpenses(); }); }); elCostList.querySelectorAll(".cost-edit").forEach((b) => { b.addEventListener("click", async () => { const id = b.getAttribute("data-id"); const desc = decodeURIComponent(b.getAttribute("data-desc")); const jpy = Number(b.getAttribute("data-jpy")); const eur = Number(b.getAttribute("data-eur")); const nextDesc = prompt("Descrizione", desc); if (nextDesc === null) return; const nextJpyRaw = prompt("Importo JPY", String(jpy)); if (nextJpyRaw === null) return; const nextJpy = Number(nextJpyRaw); if (!Number.isFinite(nextJpy) || nextJpy < 0) return; const nextEur = jpyToEur(nextJpy); await apiFetch(`/api/expenses/${id}`, { method: "PATCH", body: JSON.stringify({ description: nextDesc, jpy: nextJpy, eur: nextEur }), }); await renderExpenses(); }); }); } elAddCostBtn.addEventListener("click", async () => { const description = (elCostDesc.value || "").trim(); const jpyRaw = elCostJpy.value; const eurRaw = elCostEur.value; if (!description) return; const jpyVal = Number(jpyRaw); const eurVal = Number(eurRaw); let jpy = 0; let eur = 0; if (jpyVal > 0) { jpy = jpyVal; eur = jpyToEur(jpyVal); } else if (eurVal > 0) { eur = eurVal; jpy = eurToJpy(eurVal); } else { return; } await apiFetch("/api/expenses", { method: "POST", body: JSON.stringify({ description, jpy, eur }) }); elCostDesc.value = ""; elCostJpy.value = ""; elCostEur.value = ""; await renderExpenses(); }); (function init() { document.getElementById("rate").textContent = String(EURJPY); renderCalendar(DAYS[0].id); renderDay(DAYS[0].id); renderTodos(); renderExpenses(); })();