diff options
| author | Petri Hienonen <petri.hienonen@gmail.com> | 2025-11-14 16:25:45 +0200 |
|---|---|---|
| committer | Petri Hienonen <petri.hienonen@gmail.com> | 2025-11-14 16:25:45 +0200 |
| commit | 55085dae685305d24c29b60b1c16fc7dc76831af (patch) | |
| tree | 4833cb2974bf8922c804bbb463113f0fe568f7a3 /app/main.js | |
| parent | a92ad2b817b2c28b26e869897e03c14d30d0f991 (diff) | |
| download | housing-55085dae685305d24c29b60b1c16fc7dc76831af.tar.zst | |
Add slider filters and initialisation screen that handles data loading before application starts
Diffstat (limited to 'app/main.js')
| -rw-r--r-- | app/main.js | 497 |
1 files changed, 381 insertions, 116 deletions
diff --git a/app/main.js b/app/main.js index 7adb227..d7fb1b2 100644 --- a/app/main.js +++ b/app/main.js @@ -13,13 +13,271 @@ import { Weights, } from "models"; -export class App { +export class Init { + /** @type {HTMLElement} */ + #loadingElement; /** @type {Collection|null} */ - 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 = []; + #filtered; /** @type {Filters} */ - #filters = new Filters(); + #filters; /** @type {Weights} */ #weights = new Weights(); /** @type {MapEl} */ @@ -35,75 +293,65 @@ export class App { /** @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", - }); + /** + * @param {Collection} collection + * @param {Filters} filters + * @param {House[]} filteredHouses + */ + constructor(collection, filters, filteredHouses) { + this.#collection = collection; + this.#filters = filters; + this.#filtered = filteredHouses; + + this.#setupLayout(); - // Create sidebar instance this.#sidebar = new Sidebar( - null, + this.#collection.houses, this.#filters, this.#weights, - () => { - this.#applyFiltersAndScoring(); - }, - (key, value) => { - if (key in this.#weights) { - this.#weights[/** @type {keyof Weights} */ (key)] = value; - } - this.#applyFiltersAndScoring(); - }, - (param) => { - this.#houseParameter = param; - this.#map.updateHousesColor(this.#houseParameter); - }, - (param) => { - this.#areaParameter = param; - this.#map.updateArea(this.#areaParameter); - }, + () => 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) => { - if (hide) { - this.#modal?.hide(); - } else { - this.#showHouseModal(houseId, false); - } - }, + onHouseHover: (houseId, hide) => this.#handleHouseHover(houseId, hide), }); - 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", - }, - }), - ); + // 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(); - this.#stats = App.#getStats(this.#filtered); - document.body.append( - loading, + // 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: [ @@ -130,7 +378,50 @@ export class App { }), ), ); - this.#initialize(loading); + } + + /** + * 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); + } } /** @@ -139,7 +430,7 @@ export class App { * @param {boolean} persistent */ #showHouseModal(houseId, persistent) { - const house = this.collection?.houses.find((h) => h.id === houseId); + const house = this.#collection.houses.find((h) => h.id === houseId); if (!house) return; this.#map.setModalPersistence(persistent); @@ -174,27 +465,27 @@ export class App { } /** - * Load data and initialize application - * @param {HTMLElement} loading + * Apply filters and recalculate scores */ - async #initialize(loading) { - try { - this.collection = await Collection.get(); + #applyFiltersAndScoring() { + // Recalculate all scores with current weights + App.#recalculateScores(this.#collection.houses, this.#weights); - App.#recalculateScores(this.collection.houses, this.#weights); - this.#filtered = this.collection.houses.filter((h) => h.matchesFilters(this.#filters)); - this.#map.initialize(this.collection, this.#houseParameter, this.#areaParameter); + // Apply filters + this.#filtered = this.#collection.houses.filter((h) => h.matchesFilters(this.#filters)); - this.#sidebar.updateDistricts(this.collection.houses); - this.#sidebar.setAreaColorParameter(this.#areaParameter); - this.#sidebar.updateHistogram(this.#filtered, this.#houseParameter); + // 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); - const stats = App.#getStats(this.#filtered); - this.#stats.replaceWith(stats); - this.#stats = stats; - } finally { - loading.remove(); - } + // Update statistics + const stats = this.#createStats(); + this.#stats.replaceWith(stats); + this.#stats = stats; + + // Update sidebar + this.#sidebar.update(this.#filtered, this.#houseParameter); } /** @@ -210,46 +501,20 @@ export class App { } /** - * Apply filters and recalculate scores + * Create statistics display + * @returns {HTMLElement} */ - #applyFiltersAndScoring() { - if (!this.collection) return; - - // First recalculate all scores with current weights - App.#recalculateScores(this.collection.houses, this.#weights); - - // Then 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 = App.#getStats(this.#filtered); - this.#stats.replaceWith(stats); - this.#stats = stats; + #createStats() { + const averageScore = this.#filtered.length + ? Math.round(this.#filtered.reduce((s, h) => s + h.scores.current, 0) / this.#filtered.length) + : 0; - this.#sidebar.updateHistogram(this.#filtered, this.#houseParameter); - } - - /** - * Update statistics display using DOM methods - * @param {House[]} filtered - */ - static #getStats(filtered) { return Dom.div( new DomOptions({ children: [ - Dom.strong(filtered.length.toString()), + Dom.strong(this.#filtered.length.toString()), Dom.span(" houses shown • Average score: "), - Dom.strong( - (filtered.length - ? Math.round(filtered.reduce((s, h) => s + h.scores.current, 0) / filtered.length) - : 0 - ).toString(), - ), + Dom.strong(averageScore.toString()), Dom.span(" • Use weights sliders to adjust scoring"), ], id: "stats", @@ -266,5 +531,5 @@ export class App { } if (import.meta.url === new URL("./main.js", document.baseURI).href) { - new App(); + new Init(); } |
