app.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. "use strict";
  2. const $ = (sel) => document.querySelector(sel);
  3. function pad(n) {
  4. return String(n).padStart(2, "0");
  5. }
  6. function setStatus(ok) {
  7. const el = $("#status");
  8. if (!el) return;
  9. el.classList.toggle("ok", ok === true);
  10. el.classList.toggle("err", ok === false);
  11. }
  12. // --- Välistemperatuur (Suuremõisa) ---
  13. async function loadLatest() {
  14. setStatus(null);
  15. try {
  16. const res = await fetch("/api/latest");
  17. if (!res.ok) throw new Error(await res.text());
  18. const data = await res.json();
  19. renderCards(data);
  20. setStatus(true);
  21. } catch (e) {
  22. console.error(e);
  23. setStatus(false);
  24. }
  25. }
  26. function renderCards(data) {
  27. const host = $("#cards");
  28. if (!host) return;
  29. const out = data.outside || {};
  30. const temp = out.temperature == null ? "–" : out.temperature;
  31. const updated = out.hour ? `<div class="card-sub">Uuendatud ${out.hour}</div>` : "";
  32. host.innerHTML = `<div class="card out">
  33. <div class="k">🌤️ Välistemperatuur</div>
  34. <div class="v">${temp}<small> °C</small></div>
  35. <div class="card-sub">Suuremõisa</div>
  36. ${updated}
  37. </div>`;
  38. }
  39. // --- Teema ---
  40. function setTheme(theme) {
  41. document.documentElement.setAttribute("data-theme", theme);
  42. localStorage.setItem("minukodu-theme", theme);
  43. const toggle = $("#theme-toggle");
  44. if (toggle) toggle.textContent = theme === "dark" ? "☀️" : "🌙";
  45. const meta = $("#meta-theme");
  46. if (meta) meta.setAttribute("content", theme === "dark" ? "#0e1116" : "#1f6feb");
  47. }
  48. // --- Vahelehed (tabid): Andurid | Transport ---
  49. const transport = { active: false, days: 1 };
  50. function switchView(view) {
  51. const isTransport = view === "transport";
  52. document.querySelectorAll("#tabs .tab").forEach((b) => {
  53. b.classList.toggle("active", b.dataset.view === view);
  54. });
  55. const vAnd = document.getElementById("view-andurid");
  56. const vTr = document.getElementById("view-transport");
  57. if (vAnd) vAnd.hidden = isTransport;
  58. if (vTr) vTr.hidden = !isTransport;
  59. transport.active = isTransport;
  60. if (location.hash !== "#" + view) history.replaceState(null, "", "#" + view);
  61. if (isTransport) loadTransport();
  62. }
  63. function initTabs() {
  64. document.querySelectorAll("#tabs .tab").forEach((btn) => {
  65. btn.addEventListener("click", () => switchView(btn.dataset.view));
  66. });
  67. const days = document.getElementById("praam-days");
  68. if (days) {
  69. days.querySelectorAll("button").forEach((b) => {
  70. b.addEventListener("click", () => {
  71. days.querySelectorAll("button").forEach((x) => x.classList.remove("active"));
  72. b.classList.add("active");
  73. transport.days = Number(b.dataset.days) || 1;
  74. loadPraam();
  75. });
  76. });
  77. }
  78. setInterval(() => {
  79. if (transport.active) loadTransport();
  80. }, 60000);
  81. switchView(location.hash === "#transport" ? "transport" : "andurid");
  82. }
  83. function loadTransport() {
  84. loadBus();
  85. loadPraam();
  86. }
  87. function fmtDayLabel(iso, today) {
  88. if (!iso || iso === today) return "";
  89. const d = new Date(iso + "T00:00:00");
  90. const t = new Date((today || iso) + "T00:00:00");
  91. const diff = Math.round((d - t) / 864e5);
  92. if (diff === 1) return "homme";
  93. return d.toLocaleDateString("et-EE", { weekday: "short", day: "numeric", month: "short" });
  94. }
  95. function setTransportUpdated(updated) {
  96. const el = document.getElementById("transport-updated");
  97. if (el && updated) el.textContent = `Uuendatud ${updated}`;
  98. }
  99. async function loadBus() {
  100. const hint = document.getElementById("bus-hint");
  101. try {
  102. const res = await fetch("/api/buss?limit=12");
  103. if (!res.ok) throw new Error("HTTP " + res.status);
  104. renderBus(await res.json());
  105. } catch (e) {
  106. console.error(e);
  107. if (hint) { hint.hidden = false; hint.textContent = "Bussiaegade laadimine ebaõnnestus."; }
  108. }
  109. }
  110. function renderBus(data) {
  111. const list = document.getElementById("bus-list");
  112. const hint = document.getElementById("bus-hint");
  113. if (!list) return;
  114. const rows = data.departures || [];
  115. if (!rows.length) {
  116. list.innerHTML = "";
  117. if (hint) { hint.hidden = false; hint.textContent = "Täna enam busse ei välju."; }
  118. return;
  119. }
  120. if (hint) hint.hidden = true;
  121. list.innerHTML = rows.map((d) => {
  122. const day = fmtDayLabel(d.date, data.today);
  123. const dayBadge = day ? `<span class="badge muted">${day}</span>` : "";
  124. let rt = "";
  125. if (d.realtime && d.delay_min != null && d.delay_min !== 0) {
  126. rt = d.delay_min > 0
  127. ? `<span class="badge warn">+${d.delay_min} min</span>`
  128. : `<span class="badge ok">${d.delay_min} min</span>`;
  129. }
  130. return `<div class="dep">
  131. <span class="dep-time">${d.time}</span>
  132. <span class="dep-route">${d.route || ""}</span>
  133. <span class="dep-dest">→ ${d.headsign || "?"}</span>
  134. ${rt}${dayBadge}
  135. </div>`;
  136. }).join("");
  137. }
  138. async function loadPraam() {
  139. const hint = document.getElementById("praam-hint");
  140. try {
  141. const res = await fetch(`/api/praam?days=${transport.days}`);
  142. if (!res.ok) throw new Error("HTTP " + res.status);
  143. renderPraam(await res.json());
  144. } catch (e) {
  145. console.error(e);
  146. if (hint) { hint.hidden = false; hint.textContent = "Praamiaegade laadimine ebaõnnestus."; }
  147. }
  148. }
  149. function renderPraam(data) {
  150. const list = document.getElementById("praam-list");
  151. const hint = document.getElementById("praam-hint");
  152. const warn = document.getElementById("praam-warning");
  153. const rows = data.departures || [];
  154. if (warn) {
  155. if (Array.isArray(data.warnings) && data.warnings.length) {
  156. warn.hidden = false;
  157. warn.innerHTML = "⚠️ <b>Ilmahoiatuse kontekst (Väinameri)</b>" +
  158. data.warnings.map((w) => `<div>• ${w.area}: ${w.valid || w.text}</div>`).join("") +
  159. `<div class="warn-sub">Praamiliiklus võib häiruda — kontrolli ` +
  160. `<a href="https://praamid.ee" target="_blank" rel="noopener">praamid.ee</a>.</div>`;
  161. } else {
  162. warn.hidden = true;
  163. }
  164. }
  165. if (!rows.length) {
  166. list.innerHTML = "";
  167. if (hint) { hint.hidden = false; hint.textContent = "Väljumisi ei leitud."; }
  168. return;
  169. }
  170. if (hint) hint.hidden = true;
  171. setTransportUpdated(data.updated);
  172. const groups = {};
  173. for (const d of rows) (groups[d.direction] = groups[d.direction] || []).push(d);
  174. list.innerHTML = ["HR", "RH"].filter((k) => groups[k]).map((k) => {
  175. const items = groups[k].map((d) => {
  176. const day = fmtDayLabel(d.date, data.today);
  177. const dayBadge = day ? `<span class="badge muted">${day}</span>` : "";
  178. const st = d.disrupted
  179. ? `<span class="badge err">${d.status_et}</span>`
  180. : `<span class="badge ok">${d.status_et}</span>`;
  181. return `<div class="dep">
  182. <span class="dep-time">${d.time}</span>
  183. <span class="dep-ship">${d.ship || ""}</span>
  184. <span class="dep-dest"></span>
  185. ${st}${dayBadge}
  186. </div>`;
  187. }).join("");
  188. return `<div class="dep-group"><div class="dep-group-title">${groups[k][0].direction_name}</div>${items}</div>`;
  189. }).join("");
  190. }
  191. function init() {
  192. const saved = localStorage.getItem("minukodu-theme");
  193. const prefersDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
  194. setTheme(saved || (prefersDark ? "dark" : "light"));
  195. const toggle = $("#theme-toggle");
  196. if (toggle) {
  197. toggle.addEventListener("click", () => {
  198. setTheme(document.documentElement.getAttribute("data-theme") === "dark" ? "light" : "dark");
  199. });
  200. }
  201. initTabs();
  202. loadLatest();
  203. setInterval(loadLatest, 60000);
  204. }
  205. document.addEventListener("DOMContentLoaded", init);