// 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 Init { /** @type {HTMLElement} */ #loadingElement; /** @type {Collection|null} */ #collection = null; constructor() { this.#loadingElement = Dom.div( new DomOptions({ children: [ Dom.div( new DomOptions({ children: [ Dom.span( "🏠", new DomOptions({ styles: { fontSize: "3rem", marginBottom: "1rem", }, }), ), Dom.span( "Loading Housing Application...", new DomOptions({ styles: { color: "#333", fontSize: "1.2rem", fontWeight: "500", }, }), ), Dom.span( "Please wait while we load and process the data", new DomOptions({ styles: { color: "#666", fontSize: "0.9rem", marginTop: "0.5rem", }, }), ), ], 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", }, }), ); this.#showLoadingScreen(); this.#initialize(); } /** * Show loading screen */ #showLoadingScreen() { document.body.appendChild(this.#loadingElement); // Set basic body styles Object.assign(document.body.style, { fontFamily: "Roboto Mono, monospace", margin: "0", padding: "0", }); } /** * Initialize application data */ async #initialize() { try { // Load collection data this.#collection = await Collection.get(); // Pre-calculate scores for all houses this.#precalculateScores(); // Initialize filters with actual data ranges const filters = this.#initializeFilters(); // Apply initial filtering const filteredHouses = this.#applyInitialFilters(filters); // Create app with fully initialized data this.#createApp(filters, filteredHouses); } catch (error) { console.error("Initialization failed:", error); this.#showError("Failed to load application data. Please refresh the page."); } } /** * Pre-calculate scores for all houses */ #precalculateScores() { if (!this.#collection) return; 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; }); } /** * Initialize filters with data ranges * @returns {Filters} */ #initializeFilters() { const filters = new Filters(); if (this.#collection) { filters.updateRanges(this.#collection.houses); } return filters; } /** * Apply initial filters * @param {Filters} filters * @returns {House[]} */ #applyInitialFilters(filters) { if (!this.#collection) return []; return this.#collection.houses.filter((house) => house.matchesFilters(filters)); } /** * Create the main application * @param {Filters} filters * @param {House[]} filteredHouses */ #createApp(filters, filteredHouses) { if (!this.#collection) return; // Remove loading screen this.#loadingElement.remove(); // Create app with fully initialized data new App(this.#collection, filters, filteredHouses); } /** * Show error message * @param {string} message */ #showError(message) { this.#loadingElement.remove(); const errorElement = Dom.div( new DomOptions({ children: [ Dom.div( new DomOptions({ children: [ Dom.span( "❌", new DomOptions({ styles: { fontSize: "3rem", marginBottom: "1rem", }, }), ), Dom.span( "Application Error", new DomOptions({ styles: { color: "#c53030", fontSize: "1.5rem", fontWeight: "bold", marginBottom: "1rem", }, }), ), Dom.span( message, new DomOptions({ styles: { color: "#744210", fontSize: "1rem", lineHeight: "1.5", marginBottom: "2rem", textAlign: "center", }, }), ), Dom.button( "Refresh Page", () => location.reload(), new DomOptions({ styles: { background: "#c53030", border: "none", borderRadius: "6px", color: "white", cursor: "pointer", fontSize: "1rem", padding: "0.75rem 1.5rem", transition: "background-color 0.2s", }, }), ), ], 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", }, }), ); document.body.appendChild(errorElement); } } export class App { /** @type {Collection} */ #collection; /** @type {House[]} */ #filtered; /** @type {Filters} */ #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; /** * @param {Collection} collection * @param {Filters} filters * @param {House[]} filteredHouses */ constructor(collection, filters, filteredHouses) { this.#collection = collection; this.#filters = filters; this.#filtered = filteredHouses; this.#setupLayout(); this.#sidebar = new Sidebar( this.#collection.houses, this.#filters, this.#weights, () => this.#applyFiltersAndScoring(), (key, value) => this.#handleWeightChange(key, value), (param) => this.#handleHouseColorChange(param), (param) => this.#handleAreaColorChange(param), ); // Create map this.#map = new MapEl({ onHouseClick: (houseId, persistent) => this.#showHouseModal(houseId, persistent), onHouseHover: (houseId, hide) => this.#handleHouseHover(houseId, hide), }); // Initialize map with data this.#map.initialize(this.#collection, this.#houseParameter, this.#areaParameter); // Create stats display this.#stats = this.#createStats(); // Assemble the main UI this.#renderUI(); // Update sidebar with current state this.#sidebar.update(this.#filtered, this.#houseParameter); } /** * Set up the main application layout */ #setupLayout() { Object.assign(document.body.style, { display: "flex", flexDirection: "column", fontFamily: "Roboto Mono", height: "100vh", margin: "0", }); } /** * Render the main UI */ #renderUI() { document.body.appendChild( 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", }, }), ), ); } /** * Handle weight changes * @param {string} key * @param {number} value */ #handleWeightChange(key, value) { if (key in this.#weights) { this.#weights[/** @type {keyof Weights} */ (key)] = value; } this.#applyFiltersAndScoring(); } /** * Handle house color parameter change * @param {string} param */ #handleHouseColorChange(param) { this.#houseParameter = param; this.#map.updateHousesColor(this.#houseParameter); this.#sidebar.update(this.#filtered, this.#houseParameter); } /** * Handle area color parameter change * @param {string} param */ #handleAreaColorChange(param) { this.#areaParameter = param; this.#map.updateArea(this.#areaParameter); } /** * Handle house hover events * @param {string} houseId * @param {boolean} hide */ #handleHouseHover(houseId, hide) { if (hide) { this.#modal?.hide(); } else { this.#showHouseModal(houseId, false); } } /** * 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(); } /** * Apply filters and recalculate scores */ #applyFiltersAndScoring() { // Recalculate all scores with current weights App.#recalculateScores(this.#collection.houses, this.#weights); // Apply filters this.#filtered = this.#collection.houses.filter((h) => h.matchesFilters(this.#filters)); // Update map with filtered houses and new scores const filteredIds = this.#filtered.map((h) => h.id); this.#map.updateHouseVisibility(filteredIds); this.#map.updateHousesColor(this.#houseParameter); // Update statistics const stats = this.#createStats(); this.#stats.replaceWith(stats); this.#stats = stats; // Update sidebar this.#sidebar.update(this.#filtered, this.#houseParameter); } /** * 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)); h.value = h.scores.current; } } /** * Create statistics display * @returns {HTMLElement} */ #createStats() { const averageScore = this.#filtered.length ? Math.round(this.#filtered.reduce((s, h) => s + h.scores.current, 0) / this.#filtered.length) : 0; return Dom.div( new DomOptions({ children: [ Dom.strong(this.#filtered.length.toString()), Dom.span(" houses shown • Average score: "), Dom.strong(averageScore.toString()), Dom.span(" • 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(); }