aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPetri Hienonen <petri.hienonen@gmail.com>2025-11-09 07:09:23 +0200
committerPetri Hienonen <petri.hienonen@gmail.com>2025-11-09 07:09:23 +0200
commit843faa37b2bf376f6fbb58637c75692b29c14858 (patch)
tree804ea735b6e9c3d3f3fa88e0c0bcf8d75bc88c18
parent644652342e4d57f2c3c9f70969596937823aa207 (diff)
downloadhousing-843faa37b2bf376f6fbb58637c75692b29c14858.tar.zst
Update
-rw-r--r--app/dom.js288
-rw-r--r--app/main.js32
2 files changed, 161 insertions, 159 deletions
diff --git a/app/dom.js b/app/dom.js
index 2782b14..ab43570 100644
--- a/app/dom.js
+++ b/app/dom.js
@@ -220,21 +220,20 @@ export class Widgets {
* Show toast notification
* @param {string} message
* @param {ToastType} [type=ToastType.error]
- * @param {number} [duration=5000]
*/
- static showToast(message, type = ToastType.error, duration = 5000) {
- document.getElementById("app-toast")?.remove();
-
- const bg =
- type === ToastType.error ? "#f44336" : type === ToastType.warning ? "#ff9800" : "#4caf50";
-
- const toast = Dom.div(
+ static toast(message, type = ToastType.error) {
+ return Dom.div(
new DomOptions({
children: [Dom.p(message)],
classes: ["toast", `toast-${type}`],
id: "app-toast",
styles: {
- background: bg,
+ background:
+ type === ToastType.error
+ ? "#f44336"
+ : type === ToastType.warning
+ ? "#ff9800"
+ : "#4caf50",
borderRadius: "4px",
boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
color: "white",
@@ -250,15 +249,6 @@ export class Widgets {
},
}),
);
-
- document.body.appendChild(toast);
-
- setTimeout(() => {
- if (toast.parentNode) {
- toast.style.opacity = "0";
- setTimeout(() => toast.remove(), 300);
- }
- }, duration);
}
/**
@@ -279,13 +269,6 @@ export class Widgets {
* @returns {HTMLElement}
*/
static slider(id, labelText, weightKey, initialValue, onChange) {
- const group = Dom.div(
- new DomOptions({
- styles: { display: "flex", flexDirection: "column", marginBottom: "1rem" },
- }),
- );
-
- const label = Dom.label(id, labelText);
const output = Dom.span(
initialValue.toFixed(1),
new DomOptions({
@@ -294,37 +277,48 @@ export class Widgets {
}),
);
- const labelTextSpan = Dom.span(
- labelText,
- new DomOptions({
- styles: { fontSize: "0.85rem" },
- }),
- );
-
- label.append(labelTextSpan, " ", output);
-
- const slider = Dom.input(
- "range",
- (e) => {
- const target = /** @type {HTMLInputElement} */ (e.target);
- const val = Number(target.value);
- output.textContent = val.toFixed(1);
- onChange(weightKey, val);
- },
- "",
- "",
+ return Dom.div(
new DomOptions({
- attributes: { max: "1", min: "0", step: "0.1", value: initialValue.toString() },
- id,
- styles: {
- margin: "0.5rem 0",
- width: "100%",
- },
+ children: [
+ Dom.label(
+ id,
+ labelText,
+ new DomOptions({
+ children: [
+ Dom.span(
+ labelText,
+ new DomOptions({
+ styles: { fontSize: "0.85rem" },
+ }),
+ ),
+ Dom.span(" "),
+ output,
+ ],
+ }),
+ ),
+ Dom.input(
+ "range",
+ (e) => {
+ const target = /** @type {HTMLInputElement} */ (e.target);
+ const val = Number(target.value);
+ output.textContent = val.toFixed(1);
+ onChange(weightKey, val);
+ },
+ "",
+ "",
+ new DomOptions({
+ attributes: { max: "1", min: "0", step: "0.1", value: initialValue.toString() },
+ id,
+ styles: {
+ margin: "0.5rem 0",
+ width: "100%",
+ },
+ }),
+ ),
+ ],
+ styles: { display: "flex", flexDirection: "column", marginBottom: "1rem" },
}),
);
-
- group.append(label, slider);
- return group;
}
/**
@@ -334,7 +328,7 @@ export class Widgets {
* @param {(value: number | null) => void} onChange
* @returns {HTMLElement}
*/
- static addNumberFilter(id, labelText, onChange) {
+ static numberFilter(id, labelText, onChange) {
return Dom.div(
new DomOptions({
children: [
@@ -394,41 +388,41 @@ export class Modal {
*/
static buildModalContent(house) {
const frag = document.createDocumentFragment();
-
- /* Header */
- const header = Dom.div(
- new DomOptions({
- styles: {
- alignItems: "center",
- display: "flex",
- justifyContent: "space-between",
- marginBottom: "20px",
- },
- }),
- );
-
- const title = Dom.heading(
- 2,
- house.address,
- new DomOptions({
- styles: { color: "#333", fontSize: "20px", margin: "0" },
- }),
- );
- const score = Dom.span(
- `Score: ${house.scores.current.toFixed(1)}`,
- new DomOptions({
- styles: {
- background: "#e8f5e9",
- borderRadius: "4px",
- color: "#2e7d32",
- fontSize: "16px",
- fontWeight: "bold",
- padding: "4px 8px",
- },
- }),
+ frag.appendChild(
+ Dom.div(
+ new DomOptions({
+ children: [
+ Dom.heading(
+ 2,
+ house.address,
+ new DomOptions({
+ styles: { color: "#333", fontSize: "20px", margin: "0" },
+ }),
+ ),
+ Dom.span(
+ `Score: ${house.scores.current.toFixed(1)}`,
+ new DomOptions({
+ styles: {
+ background: "#e8f5e9",
+ borderRadius: "4px",
+ color: "#2e7d32",
+ fontSize: "16px",
+ fontWeight: "bold",
+ padding: "4px 8px",
+ },
+ }),
+ ),
+ ],
+ id: "modal-header",
+ styles: {
+ alignItems: "center",
+ display: "flex",
+ justifyContent: "space-between",
+ marginBottom: "20px",
+ },
+ }),
+ ),
);
- header.append(title, score);
- frag.appendChild(header);
const grid = Dom.div(
new DomOptions({
@@ -471,49 +465,57 @@ export class Modal {
grid.appendChild(item);
}
frag.appendChild(grid);
-
- /* Description */
- const descSect = Dom.div(new DomOptions({ styles: { marginBottom: "20px" } }));
- const descTitle = Dom.span(
- "Description",
- new DomOptions({
- styles: { fontSize: "14px", fontWeight: "bold", marginBottom: "5px" },
- }),
- );
- const descText = Dom.p(
- house.description ?? "No description available.",
- new DomOptions({
- styles: { color: "#333", fontSize: "14px", lineHeight: "1.4", marginTop: "5px" },
- }),
+ frag.appendChild(
+ Dom.div(
+ new DomOptions({
+ children: [
+ Dom.span(
+ "Description",
+ new DomOptions({
+ styles: { fontSize: "14px", fontWeight: "bold", marginBottom: "5px" },
+ }),
+ ),
+ Dom.p(
+ house.description ?? "No description available.",
+ new DomOptions({
+ styles: { color: "#333", fontSize: "14px", lineHeight: "1.4", marginTop: "5px" },
+ }),
+ ),
+ ],
+ styles: { marginBottom: "20px" },
+ }),
+ ),
);
- descSect.append(descTitle, descText);
- frag.appendChild(descSect);
- /* Images */
if (house.images?.length) {
- const imgSect = Dom.div(new DomOptions({ styles: { marginBottom: "20px" } }));
- const imgTitle = Dom.div(
+ const imgSect = Dom.div(
new DomOptions({
- styles: { fontSize: "14px", fontWeight: "bold", marginBottom: "10px" },
- }),
- );
- const imgCont = Dom.div(
- new DomOptions({
- styles: { display: "flex", gap: "10px", overflowX: "auto", paddingBottom: "5px" },
+ children: [
+ Dom.div(
+ new DomOptions({
+ id: "img_title",
+ styles: { fontSize: "14px", fontWeight: "bold", marginBottom: "10px" },
+ }),
+ ),
+ Dom.div(
+ new DomOptions({
+ children: house.images.slice(0, 3).map((src) => {
+ return Dom.img(
+ src,
+ new DomOptions({
+ attributes: { loading: "lazy" },
+ styles: { borderRadius: "4px", flexShrink: "0", height: "100px" },
+ }),
+ );
+ }),
+
+ styles: { display: "flex", gap: "10px", overflowX: "auto", paddingBottom: "5px" },
+ }),
+ ),
+ ],
+ styles: { marginBottom: "20px" },
}),
);
- for (const src of house.images.slice(0, 3)) {
- imgCont.appendChild(
- Dom.img(
- src,
- new DomOptions({
- attributes: { loading: "lazy" },
- styles: { borderRadius: "4px", flexShrink: "0", height: "100px" },
- }),
- ),
- );
- }
- imgSect.append(imgTitle, imgCont);
frag.appendChild(imgSect);
}
@@ -555,25 +557,27 @@ export class Modal {
positionStyles,
);
- const closeBtn = Dom.button(
- "x",
- () => this.hide(),
- new DomOptions({
- styles: {
- background: "none",
- border: "none",
- color: "#666",
- cursor: "pointer",
- fontSize: "24px",
- position: "absolute",
- right: "10px",
- top: "10px",
- },
- }),
+ this.#dialog.append(
+ Dom.button(
+ "x",
+ () => this.hide(),
+ new DomOptions({
+ id: "close-modal-btn",
+ styles: {
+ background: "none",
+ border: "none",
+ color: "#666",
+ cursor: "pointer",
+ fontSize: "24px",
+ position: "absolute",
+ right: "10px",
+ top: "10px",
+ },
+ }),
+ ),
+ Modal.buildModalContent(house),
);
- this.#dialog.append(closeBtn, Modal.buildModalContent(house));
-
// Add event listeners with AbortController
this.#dialog.addEventListener("close", () => this.hide(), {
signal: this.#abortController.signal,
diff --git a/app/main.js b/app/main.js
index c8a10a3..30770ed 100644
--- a/app/main.js
+++ b/app/main.js
@@ -27,8 +27,8 @@ export class App {
#weights = new Weights();
/** @type {District[]} */
#districts = [];
- /** @type {MapEl|null} */
- #map = null;
+ /** @type {MapEl} */
+ #map;
/** @type {HTMLElement} */
#stats;
/** @type {HTMLElement} */
@@ -259,12 +259,12 @@ export class App {
Dom.div(
new DomOptions({
children: [
- Widgets.addNumberFilter("min-price", "Min price (€)", (v) => {
+ Widgets.numberFilter("min-price", "Min price (€)", (v) => {
filters.minPrice = v ?? 0;
onFilterChange();
}),
- Widgets.addNumberFilter("max-price", "Max price (€)", (v) => {
+ Widgets.numberFilter("max-price", "Max price (€)", (v) => {
filters.maxPrice = v ?? Number.POSITIVE_INFINITY;
onFilterChange();
}),
@@ -276,12 +276,12 @@ export class App {
},
}),
),
- Widgets.addNumberFilter("min-year", "Min year", (v) => {
+ Widgets.numberFilter("min-year", "Min year", (v) => {
filters.minYear = v ?? 0;
onFilterChange();
}),
- Widgets.addNumberFilter("min-area", "Min area (m²)", (v) => {
+ Widgets.numberFilter("min-area", "Min area (m²)", (v) => {
filters.minArea = v ?? 0;
onFilterChange();
}),
@@ -464,17 +464,15 @@ export class App {
this.#filtered = houses.slice();
- if (this.#map) {
- this.#map.initialize(
- districts,
- coastLine,
- mainRoads,
- trainTracks,
- trainStations,
- houses,
- this.#colorParameter,
- );
- }
+ this.#map.initialize(
+ districts,
+ coastLine,
+ mainRoads,
+ trainTracks,
+ trainStations,
+ houses,
+ this.#colorParameter,
+ );
// Populate district multi-select
const districtOptions = App.#renderDistrictOptions(this.#districts, this.#houses);