diff options
Diffstat (limited to 'app/dom.js')
| -rw-r--r-- | app/dom.js | 410 |
1 files changed, 0 insertions, 410 deletions
@@ -1,6 +1,3 @@ -// dom.js -import { House } from "models"; - export class DomOptions { attributes; children; @@ -214,410 +211,3 @@ export class Dom { return p; } } - -export class Widgets { - /** - * Show toast notification - * @param {string} message - * @param {ToastType} [type=ToastType.error] - */ - static toast(message, type = ToastType.error) { - return Dom.div( - new DomOptions({ - children: [Dom.p(message)], - classes: ["toast", `toast-${type}`], - id: "app-toast", - styles: { - background: - type === ToastType.error - ? "#f44336" - : type === ToastType.warning - ? "#ff9800" - : "#4caf50", - borderRadius: "4px", - boxShadow: "0 4px 12px rgba(0,0,0,0.15)", - color: "white", - fontSize: "14px", - fontWeight: "500", - maxWidth: "300px", - padding: "12px 20px", - position: "fixed", - right: "20px", - top: "20px", - transition: "all .3s ease", - zIndex: "10000", - }, - }), - ); - } - - /** - * Remove all children - * @param {HTMLElement} el - */ - static clear(el) { - while (el.firstChild) el.removeChild(el.firstChild); - } - - /** - * Create a weight slider - * @param {string} id - * @param {string} labelText - * @param {string} weightKey - * @param {number} initialValue - * @param {(key: string, value: number) => void} onChange - * @returns {HTMLElement} - */ - static slider(id, labelText, weightKey, initialValue, onChange) { - const output = Dom.span( - initialValue.toFixed(1), - new DomOptions({ - id: `${id}-value`, - styles: { color: "#0066cc", fontSize: "0.85rem", fontWeight: "bold", textAlign: "center" }, - }), - ); - - return Dom.div( - new DomOptions({ - 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" }, - }), - ); - } - - /** - * Create a number filter input - * @param {string} id - * @param {string} labelText - * @param {(value: number | null) => void} onChange - * @returns {HTMLElement} - */ - static numberFilter(id, labelText, onChange) { - return Dom.div( - new DomOptions({ - children: [ - Dom.label( - id, - labelText, - new DomOptions({ - styles: { fontSize: "0.85rem", fontWeight: "bold", marginBottom: "0.25rem" }, - }), - ), - Dom.input( - "number", - (e) => { - const target = /** @type {HTMLInputElement} */ (e.target); - const raw = target.value.trim(); - onChange(raw === "" ? null : Number(raw)); - }, - "any", - "", - new DomOptions({ - id, - styles: { - border: "1px solid #ddd", - borderRadius: "4px", - fontSize: "0.9rem", - padding: "0.5rem", - }, - }), - ), - ], - styles: { display: "flex", flexDirection: "column", marginBottom: "1.75rem" }, - }), - ); - } -} - -export class Modal { - /** @type {HTMLDialogElement} */ - #dialog; - /** @type {AbortController} */ - #abortController; - /** @type {number | undefined} */ - #timer; - /** @type {boolean} */ - #persistent; - /** @type {House} */ - #house; - /** @type {() => void} */ - #onHide; - /** @type {() => void} */ - #onClearMapTimer; - - /** - * Build modal content for a house - * @param {House} house - * @returns {DocumentFragment} - */ - static buildModalContent(house) { - const frag = document.createDocumentFragment(); - 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", - }, - }), - ), - ); - - const grid = Dom.div( - new DomOptions({ - styles: { - display: "grid", - gap: "15px", - gridTemplateColumns: "repeat(2,1fr)", - marginBottom: "20px", - }, - }), - ); - const details = [ - { label: "Price", value: `${house.price} €` }, - { label: "Building Type", value: house.buildingType }, - { label: "Construction Year", value: house.constructionYear?.toString() ?? "N/A" }, - { label: "Living Area", value: `${house.livingArea} m²` }, - { label: "District", value: house.district }, - { label: "Rooms", value: house.rooms?.toString() ?? "N/A" }, - { label: "Price", value: `${house.price} €` }, - { label: "Building Type", value: house.buildingType }, - { label: "Construction Year", value: house.constructionYear?.toString() ?? "N/A" }, - { label: "Living Area", value: `${house.livingArea} m²` }, - { label: "District", value: house.district }, - { label: "Rooms", value: house.rooms?.toString() ?? "N/A" }, - ]; - for (const { label, value } of details) { - const item = Dom.div( - new DomOptions({ - children: [ - Dom.span( - label, - new DomOptions({ - styles: { fontSize: "14px", fontWeight: "bold", marginBottom: "4px" }, - }), - ), - Dom.span(value, new DomOptions({ styles: { color: "#333", fontSize: "14px" } })), - ], - }), - ); - grid.appendChild(item); - } - frag.appendChild(grid); - 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" }, - }), - ), - ); - - if (house.images?.length) { - const imgSect = Dom.div( - new DomOptions({ - 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" }, - }), - ); - frag.appendChild(imgSect); - } - - return frag; - } - - /** - * @param {House} house - * @param {boolean} persistent - * @param {object} positionStyles - * @param {() => void} onHide - * @param {() => void} onClearMapTimer - */ - constructor(house, persistent, positionStyles, onHide, onClearMapTimer) { - this.#house = house; - this.#persistent = persistent; - this.#onHide = onHide; - this.#onClearMapTimer = onClearMapTimer; - this.#abortController = new AbortController(); - this.#dialog = document.createElement("dialog"); - - Object.assign( - this.#dialog.style, - { - background: "white", - border: "none", - borderRadius: "8px", - boxShadow: "0 4px 20px rgba(0,0,0,0.2)", - maxHeight: "80vh", - maxWidth: "600px", - overflowY: "auto", - padding: "20px", - position: "fixed", - top: "50%", - transform: "translateY(-50%)", - width: "90%", - zIndex: "1000", - }, - positionStyles, - ); - - 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), - ); - - // Add event listeners with AbortController - this.#dialog.addEventListener("close", () => this.hide(), { - signal: this.#abortController.signal, - }); - this.#dialog.addEventListener( - "mouseenter", - () => { - clearTimeout(this.#timer); - this.#onClearMapTimer(); - }, - { signal: this.#abortController.signal }, - ); - this.#dialog.addEventListener( - "mouseleave", - () => { - if (!this.#persistent) { - this.#timer = setTimeout(() => this.hide(), 200); - } - }, - { signal: this.#abortController.signal }, - ); - } - - render() { - return this.#dialog; - } - - show() { - if (this.#persistent) { - this.#dialog.showModal(); - } else { - this.#dialog.show(); - } - } - - hide() { - clearTimeout(this.#timer); - this.#dialog.close(); - this.#dialog.remove(); - this.#abortController.abort(); - this.#onHide(); - } -} |
