// main.js - Updated with Sidebar class import { Modal, Sidebar } from "components"; import { Dom, DomOptions } from "dom"; import { AreaColorParameter, ColorParameter, MapEl } from "map"; import { DataProvider, District, Filters, House, ScoringEngine, StatisticalArea, TrainStation, TrainTracks, Weights, } from "models"; export class App { /** @type {House[]} */ #houses = []; /** @type {TrainTracks[]} */ #trainTracks = []; /** @type {TrainStation[]} */ #trainStations = []; /** @type {StatisticalArea[]} */ #statAreas = []; /** @type {House[]} */ #filtered = []; /** @type {Filters} */ #filters = new Filters(); /** @type {Weights} */ #weights = new Weights(); /** @type {District[]} */ #districts = []; /** @type {MapEl} */ #map; /** @type {HTMLElement} */ #stats; /** @type {Sidebar} */ #sidebar; /** @type {Modal|null} */ #modal = null; /** @type {boolean} */ #persistent = false; /** @type {string} */ #colorParameter = ColorParameter.price; /** @type {string} */ #areaColorParameter = AreaColorParameter.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.#onFilterChange(), (key, value) => this.#onWeightChange(key, value), (param) => this.#onColorChange(param), (param) => this.#onAreaColorChange(param), ); this.#map = new MapEl({ onHouseClick: (houseId, persistent) => this.#showHouseModal(houseId, persistent), onHouseHover: (houseId, hide) => { if (hide) { this.#modal?.hide(); } else { this.#showHouseModal(houseId, false); } }, }); this.#stats = Dom.div( new DomOptions({ id: "stats", styles: { background: "#fff", borderTop: "1px solid #ddd", flexShrink: "0", fontSize: "0.95rem", padding: "0.75rem 1rem", }, }), ); 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", }, }), ); 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); } /** * Handle filter changes */ #onFilterChange() { this.#filtered = this.#houses.filter((h) => h.matchesFilters(this.#filters)); if (this.#map) { const filteredIds = this.#filtered.map((h) => h.id); this.#map.updateHouseVisibility(filteredIds); } this.#updateStats(); } /** * Handle weight changes * @param {string} key * @param {number} value */ #onWeightChange(key, value) { if (key in this.#weights) { this.#weights[/** @type {keyof Weights} */ (key)] = value; } App.#recalculateScores(this.#houses, this.#weights); this.#map?.setColorParameter(this.#colorParameter); this.#updateStats(); } /** * Handle color parameter changes * @param {string} param */ #onColorChange(param) { this.#colorParameter = param; this.#map?.setColorParameter(this.#colorParameter); } /** * Handle area color parameter changes * @param {string} param */ #onAreaColorChange(param) { this.#areaColorParameter = param; this.#map?.setAreaColorParameter(this.#areaColorParameter); } /** * Show modal with house details * @param {string} houseId * @param {boolean} persistent */ #showHouseModal(houseId, persistent) { const house = this.#houses.find((h) => h.id === houseId); if (!house) return; this.#persistent = persistent; if (this.#map) { 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.#persistent = false; if (this.#map) { this.#map.setModalPersistence(false); this.#map.clearModalTimer(); } }, () => { if (this.#map) { this.#map.clearModalTimer(); } }, ); document.body.appendChild(this.#modal.render()); this.#modal.show(); } /** * Load data and initialize application * @param {HTMLElement} loading */ async #initialize(loading) { try { const [districts, houses, trainStations, trainTracks, coastLine, mainRoads, statAreas] = await Promise.all([ DataProvider.getDistricts(), DataProvider.getHouses(), DataProvider.getTrainStations(), DataProvider.getTrainTracks(), DataProvider.getCoastline(), DataProvider.getMainRoads(), DataProvider.getStatisticalAreas(), ]); this.#districts = districts; this.#houses = houses; this.#trainStations = trainStations; this.#trainTracks = trainTracks; this.#statAreas = statAreas; this.#filtered = houses.slice(); this.#map.initialize( districts, coastLine, mainRoads, trainTracks, trainStations, houses, statAreas, this.#colorParameter, ); // Set default area coloring to unemployment rate this.#map.setAreaColorParameter(this.#areaColorParameter); // Update sidebar with districts and area color parameter this.#sidebar.updateDistricts(this.#districts, this.#houses); this.#sidebar.setAreaColorParameter(this.#areaColorParameter); this.#updateStats(); } 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 */ #updateStats() { const count = this.#filtered.length; const avg = count ? Math.round(this.#filtered.reduce((s, h) => s + h.scores.current, 0) / count) : 0; // Clear existing content this.#stats.innerHTML = ""; // Create elements using DOM methods const countStrong = document.createElement("strong"); countStrong.textContent = count.toString(); const avgStrong = document.createElement("strong"); avgStrong.textContent = avg.toString(); // Append all elements this.#stats.append( countStrong, document.createTextNode(" houses shown • Average score: "), avgStrong, document.createTextNode(" • Use weights sliders to adjust scoring"), ); } } if (import.meta.url === new URL("./main.js", document.baseURI).href) { new App(); }