// main.js - Updated with Sidebar class and new Dom interface import { BottomBar, LeftSidebar, Modal, RightSidebar } from "components"; import { Dom } from "dom"; import { MapEl } from "map"; import { AreaParam, Collection, Filters, House, HouseParameter, ScoringEngine, Weights, } from "models"; export class Init { /** @type {HTMLElement} */ #loadingElement; /** @type {Collection|null} */ #collection = null; constructor() { this.#loadingElement = Dom.div({ children: [ Dom.div({ children: [ Dom.span({ styles: { fontSize: "3rem", marginBottom: "1rem", }, text: "🏠", }), Dom.span({ styles: { color: "#333", fontSize: "1.2rem", fontWeight: "500", }, text: "Loading Housing Application...", }), Dom.span({ styles: { color: "#666", fontSize: "0.9rem", marginTop: "0.5rem", }, text: "Please wait while we load and process the data", }), ], styles: { alignItems: "center", display: "flex", flexDirection: "column", textAlign: "center", }, }), ], styles: { alignItems: "center", background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)", color: "white", display: "flex", fontFamily: "Roboto Mono, monospace", height: "100%", justifyContent: "center", left: "0", position: "fixed", top: "0", width: "100%", zIndex: "9999", }, }); document.body.appendChild(this.#loadingElement); Object.assign(document.body.style, { fontFamily: "Roboto Mono, monospace", margin: "0", padding: "0", }); this.#initialize(); } /** * Initialize application data */ async #initialize() { try { // Load collection data this.#collection = await Collection.get(); const weights = new Weights(); // Default weights for initial calculation this.#collection.houses.forEach((house) => { house.scores.current = Math.round(ScoringEngine.calculate(house, weights)); house.value = house.scores.current; }); const filters = new Filters(this.#collection.houses); this.#loadingElement.remove(); new App(this.#collection, filters); } catch (error) { console.error("Initialization failed:", error); this.#showError("Failed to load application data. Please refresh the page."); } } /** * @param {string} message */ static getError(message) { return Dom.div({ children: [ Dom.div({ children: [ Dom.span({ styles: { fontSize: "3rem", marginBottom: "1rem", }, text: "❌", }), Dom.span({ styles: { color: "#c53030", fontSize: "1.5rem", fontWeight: "bold", marginBottom: "1rem", }, text: "Application Error", }), Dom.span({ styles: { color: "#744210", fontSize: "1rem", lineHeight: "1.5", marginBottom: "2rem", textAlign: "center", }, text: message, }), Dom.button({ onClick: () => location.reload(), styles: { background: "#c53030", border: "none", borderRadius: "6px", color: "white", cursor: "pointer", fontSize: "1rem", padding: "0.75rem 1.5rem", transition: "background-color 0.2s", }, text: "Refresh Page", }), ], styles: { alignItems: "center", display: "flex", flexDirection: "column", maxWidth: "400px", textAlign: "center", }, }), ], styles: { alignItems: "center", background: "#fed7d7", display: "flex", fontFamily: "Roboto Mono, monospace", height: "100%", justifyContent: "center", left: "0", position: "fixed", top: "0", width: "100%", zIndex: "9999", }, }); } /** * @param {string} message */ #showError(message) { this.#loadingElement.remove(); const errorElement = Init.getError(message); document.body.appendChild(errorElement); } } export class App { /** @type {Collection} */ #collection; /** @type {Filters} */ #filters; /** @type {Weights} */ #weights = new Weights(); /** @type {MapEl} */ #map; /** @type {HTMLElement} */ #stats; /** @type {LeftSidebar} */ #leftSidebar; /** @type {RightSidebar} */ #rightSidebar; /** @type {Modal|null} */ #modal = null; /** @type {HouseParameter} */ #houseParameter = HouseParameter.price; /** @type {AreaParam} */ #areaParameter = AreaParam.unemploymentRate; /** @type {BottomBar} */ #bottomBar; /** * @param {Collection} collection * @param {Filters} filters */ constructor(collection, filters) { this.#collection = collection; this.#filters = filters; this.#bottomBar = new BottomBar({ houses: this.#collection.houses.sort((a, b) => b.scores.current - a.scores.current), onHouseClick: (houseId) => this.#showHouseModal(houseId, true), }); Object.assign(document.body.style, { display: "flex", flexDirection: "column", fontFamily: "Roboto Mono", height: "100vh", margin: "0", }); // Create header with global toggle buttons const header = Dom.div({ children: [ // Left sidebar toggle button Dom.button({ onClick: () => this.#leftSidebar.toggle(), styles: { background: "#fff", border: "1px solid #ddd", borderRadius: "4px", cursor: "pointer", fontSize: "1rem", margin: "0.5rem", padding: "0.5rem 1rem", }, text: "☰ Filters", }), // Right sidebar toggle button Dom.button({ onClick: () => { this.#bottomBar.show(); this.#rightSidebar.toggle(); }, styles: { background: "#fff", border: "1px solid #ddd", borderRadius: "4px", cursor: "pointer", fontSize: "1rem", margin: "0.5rem", padding: "0.5rem 1rem", }, text: "⚙️ Weights", }), ], styles: { background: "#f5f5f5", borderBottom: "1px solid #ddd", display: "flex", justifyContent: "space-between", padding: "0", }, }); this.#leftSidebar = new LeftSidebar({ allHouses: this.#collection.houses, areaParam: this.#areaParameter, filters: this.#filters, houseParam: this.#houseParameter, onAreaColorChange: (param) => { this.#areaParameter = param; this.#map.updateArea(this.#areaParameter); }, onColorChange: (param) => { this.#houseParameter = param; this.#map.updateHousesParameter(this.#houseParameter); }, onFilterChange: () => { this.#map.updateHouseVisibility(this.#filters); const stats = App.#createStats(this.#collection.houses, this.#filters); this.#stats.replaceWith(stats); this.#stats = stats; }, }); this.#rightSidebar = new RightSidebar({ onWeightChange: (key, value) => { if (key in this.#weights) { this.#weights[/** @type {keyof Weights} */ (key)] = value; } for (const h of this.#collection.houses) { h.scores.current = Math.round(ScoringEngine.calculate(h, this.#weights)); h.value = h.scores.current; } const stats = App.#createStats(this.#collection.houses, this.#filters); this.#stats.replaceWith(stats); this.#stats = stats; }, weights: this.#weights, }); this.#map = new MapEl({ areaParameter: this.#areaParameter, collection: this.#collection, houseParameter: this.#houseParameter, /** @param {string} houseId @param {boolean} persistent */ onHouseClick: (houseId, persistent) => this.#showHouseModal(houseId, persistent), /** @param {string} houseId @param {boolean} hide */ onHouseHover: (houseId, hide) => { hide ? this.#modal?.remove() : this.#showHouseModal(houseId, false); }, }); this.#stats = App.#createStats(this.#collection.houses, this.#filters); document.body.appendChild( Dom.div({ children: [ header, Dom.div({ children: [ this.#leftSidebar.render(), Dom.div({ children: [this.#map.svg, this.#stats], id: "map-container", styles: { display: "flex", flex: "1", flexDirection: "column", minWidth: "0", }, }), this.#rightSidebar.render(), this.#bottomBar.render(), ], id: "main-content", styles: { display: "flex", flex: "1", height: "calc(100vh - 60px)", overflow: "hidden", }, }), ], id: "main", styles: { display: "flex", flexDirection: "column", height: "100vh", }, }), ); } /** * 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) throw new Error("Parameter is not a number!"); this.#map.setModalPersistence(persistent); this.#modal?.remove(); this.#modal = new Modal({ house: house, onClearMapTimer: () => { this.#map.clearModalTimer(); }, onHide: () => { this.#modal = null; this.#map.setModalPersistence(false); this.#map.clearModalTimer(); }, persistent: persistent, positionStyles: { left: "auto", maxHeight: "80vh", maxWidth: "400px", right: "20px", top: "50%", transform: "translateY(-50%)", width: "90%", }, }); document.body.appendChild(this.#modal.render()); this.#modal.show(); } /** * Create statistics display * @param {House[]} houses * @param {Filters} filters * @returns {HTMLElement} */ static #createStats(houses, filters) { const filtered = houses.filter((h) => h.matchesFilters(filters)); const averageScore = filtered.length ? Math.round(filtered.reduce((s, h) => s + h.scores.current, 0) / filtered.length) : 0; return Dom.div({ children: [ Dom.strong({ text: `${filtered.length.toString()}/${houses.length}` }), Dom.span({ text: " houses shown • Average score: " }), Dom.strong({ text: averageScore.toString() }), Dom.span({ text: " • 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 Init(); }