// main.js - Updated with Sidebar class import { Modal, Sidebar } from "components"; import { Dom, DomOptions } from "dom"; import { MapEl } from "map"; import { AreaParam, Collection, Filters, House, HouseParameter, ScoringEngine, Weights, } from "models"; export class App { /** @type {Collection|null} */ collection = null; /** @type {House[]} */ #filtered = []; /** @type {Filters} */ #filters = new Filters(); /** @type {Weights} */ #weights = new Weights(); /** @type {MapEl} */ #map; /** @type {HTMLElement} */ #stats; /** @type {Sidebar} */ #sidebar; /** @type {Modal|null} */ #modal = null; /** @type {HouseParameter} */ #houseParameter = HouseParameter.price; /** @type {AreaParam} */ #areaParameter = AreaParam.unemploymentRate; constructor() { // Set up main layout container Object.assign(document.body.style, { display: "flex", flexDirection: "column", fontFamily: "Roboto Mono", height: "100vh", margin: "0", }); // Create sidebar instance this.#sidebar = new Sidebar( this.#filters, this.#weights, () => { this.#filtered = this.collection?.houses.filter((h) => h.matchesFilters(this.#filters)); const filteredIds = this.#filtered.map((h) => h.id); this.#map.updateHouseVisibility(filteredIds); const stats = App.#getStats(this.#filtered); this.#stats.replaceWith(stats); this.#stats = stats; }, (key, value) => { if (key in this.#weights) { this.#weights[/** @type {keyof Weights} */ (key)] = value; } App.#recalculateScores(this.collection?.houses, this.#weights); this.#map.updateHousesColor(this.#houseParameter); const stats = App.#getStats(this.#filtered); this.#stats.replaceWith(stats); this.#stats = stats; }, (param) => { this.#houseParameter = param; this.#map.updateHousesColor(this.#houseParameter); }, (param) => { this.#areaParameter = param; this.#map.updateArea(this.#areaParameter); }, ); this.#map = new MapEl({ onHouseClick: (houseId, persistent) => this.#showHouseModal(houseId, persistent), onHouseHover: (houseId, hide) => { if (hide) { this.#modal?.hide(); } else { this.#showHouseModal(houseId, false); } }, }); const loading = Dom.span( "Loading data…", new DomOptions({ id: "loading", styles: { background: "white", borderRadius: "8px", boxShadow: "0 2px 10px rgba(0,0,0,0.1)", color: "#555", fontSize: "1.2rem", left: "50%", padding: "2rem", position: "absolute", textAlign: "center", top: "50%", transform: "translate(-50%, -50%)", zIndex: "1000", }, }), ); this.#stats = App.#getStats(this.#filtered); document.body.append( loading, Dom.div( new DomOptions({ children: [ this.#sidebar.render(), Dom.div( new DomOptions({ children: [this.#map.svg, this.#stats], id: "map-container", styles: { display: "flex", flex: "1", flexDirection: "column", minWidth: "0", }, }), ), ], id: "main", styles: { display: "flex", flex: "1", overflow: "hidden", }, }), ), ); this.#initialize(loading); } /** * Show modal with house details * @param {string} houseId * @param {boolean} persistent */ #showHouseModal(houseId, persistent) { const house = this.collection?.houses.find((h) => h.id === houseId); if (!house) return; this.#map.setModalPersistence(persistent); // Hide existing modal this.#modal?.hide(); this.#modal = new Modal( house, persistent, { left: "auto", maxHeight: "80vh", maxWidth: "400px", right: "20px", top: "50%", transform: "translateY(-50%)", width: "90%", }, () => { this.#modal = null; this.#map.setModalPersistence(false); this.#map.clearModalTimer(); }, () => { this.#map.clearModalTimer(); }, ); document.body.appendChild(this.#modal.render()); this.#modal.show(); } /** * Load data and initialize application * @param {HTMLElement} loading */ async #initialize(loading) { try { this.collection = await Collection.get(); this.#filtered = this.collection.houses.slice(); this.#map.initialize(this.collection, this.#houseParameter, this.#areaParameter); this.#sidebar.updateDistricts(this.collection.houses); this.#sidebar.setAreaColorParameter(this.#areaParameter); const stats = App.#getStats(this.#filtered); this.#stats.replaceWith(stats); this.#stats = stats; } finally { loading.remove(); } } /** * Recalculate scores statically * @param {House[]} houses * @param {Weights} weights */ static #recalculateScores(houses, weights) { for (const h of houses) { h.scores.current = Math.round(ScoringEngine.calculate(h, weights)); } } /** * Update statistics display using DOM methods * @param {House[]} filtered */ static #getStats(filtered) { return Dom.div( new DomOptions({ children: [ Dom.strong(filtered.length.toString()), document.createTextNode(" houses shown • Average score: "), Dom.strong( (filtered.length ? Math.round(filtered.reduce((s, h) => s + h.scores.current, 0) / filtered.length) : 0 ).toString(), ), document.createTextNode(" • Use weights sliders to adjust scoring"), ], id: "stats", styles: { background: "#fff", borderTop: "1px solid #ddd", flexShrink: "0", fontSize: "0.95rem", padding: "0.75rem 1rem", }, }), ); } } if (import.meta.url === new URL("./main.js", document.baseURI).href) { new App(); }