From 37f589d50070d26e6caa7da4a139821f36d6e89f Mon Sep 17 00:00:00 2001 From: Kamron Khakimov Date: Mon, 2 Mar 2026 11:07:33 +0100 Subject: [PATCH] refactor: apply clean code --- index.html | 13 -- src/dashboard/dashboard.js | 272 ++++++++++++----------- src/formular/formular.js | 251 ++++++++++++--------- src/settings/settings.html | 4 +- src/settings/{setting.js => settings.js} | 0 src/tables/tables.html | 5 - src/tables/tables.js | 249 +++++++++++++-------- 7 files changed, 454 insertions(+), 340 deletions(-) delete mode 100644 index.html rename src/settings/{setting.js => settings.js} (100%) diff --git a/index.html b/index.html deleted file mode 100644 index 6e4354e..0000000 --- a/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - BMI-Web-App - - - - - \ No newline at end of file diff --git a/src/dashboard/dashboard.js b/src/dashboard/dashboard.js index 3f49590..d84f6a1 100644 --- a/src/dashboard/dashboard.js +++ b/src/dashboard/dashboard.js @@ -1,195 +1,219 @@ // src/dashboard/dashboard.js -// Hash route -> view file -const routes = { +const ROUTES = { dashboard: "./dashboard/dashboard.html", formular: "./formular/formular.html", tables: "./tables/tables.html", }; +const DEFAULT_ROUTE = "dashboard"; +const VIEW_ID = "view"; +const SIDEBAR_ID = "sidebarNav"; + const loadedScripts = new Set(); -function getRouteFromHash() { - const raw = (location.hash || "#dashboard").replace("#", "").trim(); - return routes[raw] ? raw : "dashboard"; +/* ========================================================= + Routing +========================================================= */ + +function getCurrentRoute() { + const hash = location.hash.replace("#", "").trim(); + return ROUTES[hash] ? hash : DEFAULT_ROUTE; } -function getBaseDir(filePath) { - const i = filePath.lastIndexOf("/"); - return i >= 0 ? filePath.slice(0, i + 1) : "./"; +function getRouteFile(route) { + return ROUTES[route]; } -function isAbsoluteUrl(url) { - return ( - /^(https?:)?\/\//i.test(url) || - url.startsWith("data:") || - url.startsWith("blob:") - ); +/* ========================================================= + Path Utilities +========================================================= */ + +function getBaseDirectory(path) { + const index = path.lastIndexOf("/"); + return index >= 0 ? path.slice(0, index + 1) : "./"; +} + +function isExternalUrl(url) { + return /^(https?:)?\/\//i.test(url); } -function isRootAbsolute(url) { +function isRootPath(url) { return url.startsWith("/"); } -function isSpecialUrl(url) { - return ( - url.startsWith("#") || - url.startsWith("mailto:") || - url.startsWith("tel:") || - url.startsWith("javascript:") +function isSpecialProtocol(url) { + return ["#", "mailto:", "tel:", "javascript:"].some((prefix) => + url.startsWith(prefix), ); } -function toResolvedPath(url, baseDir) { - // baseDir ist z. B. "./dashboard/" - // Dokument liegt bei /src/app.html -> daraus wird /src/dashboard/... +function shouldRewrite(url) { + if (!url) return false; + if (isExternalUrl(url)) return false; + if (isRootPath(url)) return false; + if (isSpecialProtocol(url)) return false; + if (url.startsWith("data:") || url.startsWith("blob:")) return false; + return true; +} + +function resolvePath(url, baseDir) { const base = new URL(baseDir, window.location.href); const resolved = new URL(url, base); - - // Nur path + query + hash zurückgeben (kein kompletter Origin nötig) return `${resolved.pathname}${resolved.search}${resolved.hash}`; } -function rewriteRelativeAssets(containerEl, baseDir) { - const nodes = containerEl.querySelectorAll( - "link[href], script[src], img[src], source[src], iframe[src]", +/* ========================================================= + Asset Handling +========================================================= */ + +function rewriteRelativeAssets(container, baseDir) { + const elements = container.querySelectorAll( + "link[href], script[src], img[src], source[src], iframe[src]" ); - nodes.forEach((el) => { - const attr = el.hasAttribute("href") ? "href" : "src"; - const val = (el.getAttribute(attr) || "").trim(); + elements.forEach((element) => { + const attribute = element.hasAttribute("href") ? "href" : "src"; + const value = element.getAttribute(attribute)?.trim(); - if (!val) { - return; - } + if (!shouldRewrite(value)) return; - // Absolute / Root / Sonderfälle nicht anfassen - if (isAbsoluteUrl(val) || isRootAbsolute(val) || isSpecialUrl(val)) { - return; - } - - // ALLE relativen Pfade auflösen (nicht nur "./...") - const newVal = toResolvedPath(val, baseDir); - el.setAttribute(attr, newVal); + element.setAttribute(attribute, resolvePath(value, baseDir)); }); } -function runInlineScripts(containerEl) { - // Inline + + diff --git a/src/settings/setting.js b/src/settings/settings.js similarity index 100% rename from src/settings/setting.js rename to src/settings/settings.js diff --git a/src/tables/tables.html b/src/tables/tables.html index 08cae36..0485059 100644 --- a/src/tables/tables.html +++ b/src/tables/tables.html @@ -7,9 +7,6 @@ - - - BMI Tabellen @@ -56,8 +53,6 @@

BMI-Messungen

- - \ No newline at end of file diff --git a/src/tables/tables.js b/src/tables/tables.js index 254d0a8..e8e7f1c 100644 --- a/src/tables/tables.js +++ b/src/tables/tables.js @@ -1,150 +1,213 @@ // src/tables/tables.js (() => { - // Prevent double-execution (router + iframe + route switches) - if (window.__tablesLoaded) { - // Still expose init in case router wants to call it again - if (typeof window.tablesInit === "function") { - return; - } - } + if (window.__tablesLoaded) return; window.__tablesLoaded = true; + /* ========================================================= + Constants + ========================================================= */ + const STORAGE_KEY = "bmiData"; - let bmiList = []; + const DAY_IN_MS = 1000 * 60 * 60 * 24; + + /* ========================================================= + State + ========================================================= */ - function calculateBMI(weight, height) { + let bmiEntries = []; + + /* ========================================================= + Pure Utilities + ========================================================= */ + + function calculateBMI(weight, heightCm) { const w = Number(weight); - const hCm = Number(height); - if (!Number.isFinite(w) || !Number.isFinite(hCm) || hCm <= 0) { + const h = Number(heightCm); + + if (!Number.isFinite(w) || !Number.isFinite(h) || h <= 0) { return null; } - const h = hCm / 100; - return Number((w / (h * h)).toFixed(1)); + + const heightM = h / 100; + return Number((w / (heightM * heightM)).toFixed(1)); } - function bmiRating(bmi) { - const v = Number(bmi); - if (!Number.isFinite(v)) return "—"; - if (v < 18.5) return "Untergewicht"; - if (v < 25) return "Normalgewicht"; - if (v < 30) return "Übergewicht"; + function getBMICategory(bmi) { + if (!Number.isFinite(bmi)) return "—"; + if (bmi < 18.5) return "Untergewicht"; + if (bmi < 25) return "Normalgewicht"; + if (bmi < 30) return "Übergewicht"; return "Adipositas"; } + function isWithinDays(dateString, days) { + const entryDate = new Date(dateString); + if (Number.isNaN(entryDate.getTime())) return false; + + const diff = Date.now() - entryDate.getTime(); + return diff <= days * DAY_IN_MS; + } + + /* ========================================================= + Storage + ========================================================= */ + function loadFromStorage() { - const data = localStorage.getItem(STORAGE_KEY); try { - return data ? JSON.parse(data) : []; + return JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]"); } catch { return []; } } function saveToStorage() { - localStorage.setItem(STORAGE_KEY, JSON.stringify(bmiList)); + localStorage.setItem(STORAGE_KEY, JSON.stringify(bmiEntries)); } - function buildTable(filterSelect, sortSelect, tableBody) { - let list = [...bmiList]; - const now = new Date(); - - if (filterSelect.value !== "all") { - list = list.filter((entry) => { - const entryDate = new Date(entry.date); - const diffDays = (now - entryDate) / (1000 * 60 * 60 * 24); - if (filterSelect.value === "week") return diffDays <= 7; - if (filterSelect.value === "month") return diffDays <= 30; - return true; - }); + function removeEntryByTimestamp(timestamp) { + bmiEntries = bmiEntries.filter((e) => e.timestamp !== timestamp); + saveToStorage(); + } + + /* ========================================================= + Filtering & Sorting + ========================================================= */ + + function filterEntries(entries, filterValue) { + if (filterValue === "week") { + return entries.filter((e) => isWithinDays(e.date, 7)); } - switch (sortSelect.value) { + if (filterValue === "month") { + return entries.filter((e) => isWithinDays(e.date, 30)); + } + + return entries; + } + + function sortEntries(entries, sortValue) { + const list = [...entries]; + + switch (sortValue) { case "date-asc": - list.sort((a, b) => new Date(a.date) - new Date(b.date)); - break; + return list.sort((a, b) => new Date(a.date) - new Date(b.date)); + case "date-desc": - list.sort((a, b) => new Date(b.date) - new Date(a.date)); - break; + return list.sort((a, b) => new Date(b.date) - new Date(a.date)); + case "bmi-asc": - list.sort((a, b) => { - const aBmi = calculateBMI(a.weight, a.height) ?? 1e15; - const bBmi = calculateBMI(b.weight, b.height) ?? 1e15; - return aBmi - bBmi; - }); - break; + return list.sort( + (a, b) => + (calculateBMI(a.weight, a.height) ?? Infinity) - + (calculateBMI(b.weight, b.height) ?? Infinity), + ); + case "bmi-desc": - list.sort((a, b) => { - const aBmi = calculateBMI(a.weight, a.height) ?? -1e15; - const bBmi = calculateBMI(b.weight, b.height) ?? -1e15; - return bBmi - aBmi; - }); - break; + return list.sort( + (a, b) => + (calculateBMI(b.weight, b.height) ?? -Infinity) - + (calculateBMI(a.weight, a.height) ?? -Infinity), + ); + + default: + return list; } + } - tableBody.innerHTML = ""; - - list.forEach((entry, index) => { - const bmi = calculateBMI(entry.weight, entry.height); - - const tr = document.createElement("tr"); - tr.innerHTML = ` - ${entry.date ?? "—"} - ${entry.weight ?? "—"} - ${entry.height ?? "—"} - ${bmi ?? "—"} - ${bmiRating(bmi)} - - `; - - tr.querySelector("button")?.addEventListener("click", () => { - bmiList.splice(index, 1); - saveToStorage(); // neu speichern - buildTable(filterSelect, sortSelect, tableBody); - }); + /* ========================================================= + Rendering + ========================================================= */ + + function createRow(entry) { + const bmi = calculateBMI(entry.weight, entry.height); + + const tr = document.createElement("tr"); + + tr.innerHTML = ` + ${entry.date ?? "—"} + ${entry.weight ?? "—"} + ${entry.height ?? "—"} + ${bmi ?? "—"} + ${getBMICategory(bmi)} + + + + `; + + return tr; + } - tableBody.appendChild(tr); - }); + function renderTable(entries, tableBody) { + tableBody.innerHTML = ""; + entries.forEach((entry) => + tableBody.appendChild(createRow(entry)), + ); } - function tablesInit() { - const filterSelect = document.getElementById("filterSelect"); - const sortSelect = document.getElementById("sortSelect"); - const tableBody = document.getElementById("bmiTableBody"); + /* ========================================================= + Controller + ========================================================= */ - if (!filterSelect || !sortSelect || !tableBody) { - // view not present (yet) - return; - } + function rebuildTable(filterSelect, sortSelect, tableBody) { + const filtered = filterEntries(bmiEntries, filterSelect.value); + const sorted = sortEntries(filtered, sortSelect.value); + renderTable(sorted, tableBody); + } - // Bind listeners only once per DOM element + function bindControls(filterSelect, sortSelect, tableBody) { if (!filterSelect.dataset.bound) { filterSelect.addEventListener("change", () => - buildTable(filterSelect, sortSelect, tableBody), + rebuildTable(filterSelect, sortSelect, tableBody), ); filterSelect.dataset.bound = "1"; } if (!sortSelect.dataset.bound) { sortSelect.addEventListener("change", () => - buildTable(filterSelect, sortSelect, tableBody), + rebuildTable(filterSelect, sortSelect, tableBody), ); sortSelect.dataset.bound = "1"; } - // Prefer stable root path if you serve /src as web root: - // fetch("/data/mock.json") - // Otherwise keep script-relative: - // const jsonUrl = resolveUrl("../data/mock.json"); + // Event delegation for delete buttons + if (!tableBody.dataset.bound) { + tableBody.addEventListener("click", (event) => { + const btn = event.target.closest("button[data-timestamp]"); + if (!btn) return; + + removeEntryByTimestamp(btn.dataset.timestamp); + rebuildTable(filterSelect, sortSelect, tableBody); + }); + + tableBody.dataset.bound = "1"; + } + } + + /* ========================================================= + Initialization + ========================================================= */ + + function tablesInit() { + const filterSelect = document.getElementById("filterSelect"); + const sortSelect = document.getElementById("sortSelect"); + const tableBody = document.getElementById("bmiTableBody"); + + if (!filterSelect || !sortSelect || !tableBody) { + return; // View not loaded yet + } + + bmiEntries = loadFromStorage(); - // Daten jetzt aus LocalStorage laden statt aus mock.json - bmiList = loadFromStorage(); - buildTable(filterSelect, sortSelect, tableBody); + bindControls(filterSelect, sortSelect, tableBody); + rebuildTable(filterSelect, sortSelect, tableBody); } - // Export global, so the router can call it after injecting the view window.tablesInit = tablesInit; - // If tables.html is opened directly or inside an iframe, init normally if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", tablesInit); } else {