/* NATEGODD frontend — form, city search, SVG wheel, tables, report. */ "use strict"; const $ = (s) => document.querySelector(s); let selectedCity = null; let lastChart = null; /* ---------------- city autocomplete ---------------- */ const placeInput = $("#f-place"); const cityBox = $("#city-results"); let searchTimer = null; placeInput.addEventListener("input", () => { selectedCity = null; clearTimeout(searchTimer); const q = placeInput.value.trim(); if (q.length < 2) { cityBox.hidden = true; return; } searchTimer = setTimeout(async () => { const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`); const rows = await res.json(); cityBox.innerHTML = ""; rows.forEach((r) => { const div = document.createElement("div"); div.innerHTML = `${r.name}${r.country} · ${r.lat.toFixed(2)}, ${r.lon.toFixed(2)}`; div.addEventListener("mousedown", () => pickCity(r)); cityBox.appendChild(div); }); cityBox.hidden = rows.length === 0; }, 180); }); placeInput.addEventListener("blur", () => setTimeout(() => (cityBox.hidden = true), 200)); function pickCity(r) { selectedCity = r; placeInput.value = r.label; $("#f-lat").value = r.lat; $("#f-lon").value = r.lon; cityBox.hidden = true; } /* ---------------- form submit ---------------- */ $("#f-unknown").addEventListener("change", (e) => { $("#f-time").disabled = e.target.checked; }); $("#chart-form").addEventListener("submit", async (e) => { e.preventDefault(); const err = $("#form-error"); err.hidden = true; const date = $("#f-date").value; if (!date) return showErr("Enter a birth date."); const [year, month, day] = date.split("-").map(Number); const unknown = $("#f-unknown").checked; const time = $("#f-time").value || "12:00"; const [hour, minute] = time.split(":").map(Number); const lat = parseFloat($("#f-lat").value); const lon = parseFloat($("#f-lon").value); if (isNaN(lat) || isNaN(lon)) return showErr("Pick a city from the list, or enter coordinates manually."); const body = { name: $("#f-name").value, place: selectedCity ? selectedCity.label : placeInput.value, year, month, day, hour, minute, time_unknown: unknown, lat, lon, tz: selectedCity ? selectedCity.tz : null, utc_offset: $("#f-offset").value, house_system: $("#f-houses").value, }; $("#go").textContent = "Casting…"; try { const res = await fetch("/api/chart", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); const data = await res.json(); if (data.error) return showErr(data.error); lastChart = data; saveChart(body); render(data); } catch (ex) { showErr("Server error: " + ex.message); } finally { $("#go").textContent = "Cast Chart ✶"; } }); function showErr(msg) { const err = $("#form-error"); err.textContent = msg; err.hidden = false; $("#go").textContent = "Cast Chart ✶"; } /* ---------------- saved charts (localStorage) ---------------- */ function loadSaved() { try { return JSON.parse(localStorage.getItem("nategodd") || "[]"); } catch { return []; } } function saveChart(body) { try { let saved = loadSaved().filter((s) => !(s.name === body.name && s.year === body.year && s.month === body.month && s.day === body.day)); saved.unshift(body); saved = saved.slice(0, 20); localStorage.setItem("nategodd", JSON.stringify(saved)); fillSaved(); } catch {} } function fillSaved() { const sel = $("#saved-charts"); sel.innerHTML = ''; loadSaved().forEach((s, i) => { const o = document.createElement("option"); o.value = i; o.textContent = `${s.name || "unnamed"} · ${s.year}-${String(s.month).padStart(2, "0")}-${String(s.day).padStart(2, "0")}`; sel.appendChild(o); }); } $("#saved-charts").addEventListener("change", (e) => { if (e.target.value === "") return; const s = loadSaved()[Number(e.target.value)]; if (!s) return; $("#f-name").value = s.name || ""; $("#f-date").value = `${s.year}-${String(s.month).padStart(2, "0")}-${String(s.day).padStart(2, "0")}`; $("#f-time").value = `${String(s.hour).padStart(2, "0")}:${String(s.minute).padStart(2, "0")}`; $("#f-unknown").checked = !!s.time_unknown; $("#f-time").disabled = !!s.time_unknown; placeInput.value = s.place || ""; selectedCity = s.tz ? { label: s.place, lat: s.lat, lon: s.lon, tz: s.tz } : null; $("#f-lat").value = s.lat; $("#f-lon").value = s.lon; $("#f-houses").value = s.house_system || "P"; }); fillSaved(); /* ---------------- tabs ---------------- */ document.querySelectorAll("#tabs button").forEach((b) => b.addEventListener("click", () => { document.querySelectorAll("#tabs button").forEach((x) => x.classList.remove("active")); document.querySelectorAll(".tab").forEach((x) => x.classList.remove("active")); b.classList.add("active"); $("#tab-" + b.dataset.tab).classList.add("active"); }) ); $("#print-btn").addEventListener("click", () => window.print()); /* ---------------- render ---------------- */ function render(d) { $("#results").hidden = false; const inp = d.input; $("#chart-header").innerHTML = `
${inp.date}${inp.time ? " · " + inp.time + " local" : " · time unknown (solar chart)"} · ${esc(inp.place)} · ${inp.lat.toFixed(3)}°, ${inp.lon.toFixed(3)}° · TZ ${esc(String(inp.tz))} (UTC${inp.utc_offset >= 0 ? "+" : ""}${inp.utc_offset}) · ${d.house_system} houses · ${d.is_day_chart ? "Day" : "Night"} chart
`; drawWheel(d); renderBalances(d); renderPositions(d); renderAspects(d); renderReport(d); $("#results").scrollIntoView({ behavior: "smooth" }); } function esc(s) { return String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c])); } /* ---------------- SVG wheel ---------------- */ const ELEM_COLOR = ["#e06a4f", "#7aa85c", "#d9c86a", "#5f8fd0"]; // fire earth air water const SIGN_GLYPHS = ["♈", "♉", "♊", "♋", "♌", "♍", "♎", "♏", "♐", "♑", "♒", "♓"]; function drawWheel(d) { const C = 360, size = 720; const asc = d.angles[0].lon; // longitude -> screen point. ASC at 9 o'clock, zodiac counterclockwise. const pt = (lonDeg, r) => { const a = ((lonDeg - asc + 180) * Math.PI) / 180; return [C + r * Math.cos(a), C - r * Math.sin(a)]; }; let s = ``; $("#wheel-box").innerHTML = s; } /* ---------------- side panels ---------------- */ function renderBalances(d) { const b = d.balances; const colors = { Fire: "#e06a4f", Earth: "#7aa85c", Air: "#d9c86a", Water: "#5f8fd0", Cardinal: "#8f7bd8", Fixed: "#e8c66a", Mutable: "#5fa8e0", Positive: "#e8e4f0", Negative: "#9a93ad" }; let html = "${esc(it.body)}