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/components.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/components.js')
| -rw-r--r-- | app/components.js | 379 |
1 files changed, 303 insertions, 76 deletions
diff --git a/app/components.js b/app/components.js index 3308b62..42d7242 100644 --- a/app/components.js +++ b/app/components.js @@ -441,6 +441,57 @@ export class Widgets { } /** + * Create a dual range filter input + * @param {string} id + * @param {string} labelText + * @param {number} minValue + * @param {number} maxValue + * @param {number} currentMin + * @param {number} currentMax + * @param {number} step + * @param {(min: number, max: number) => void} onChange + * @returns {HTMLElement} + */ + static rangeFilter(id, labelText, minValue, maxValue, currentMin, currentMax, step, onChange) { + return Dom.div( + new DomOptions({ + children: [ + Dom.label( + id, + labelText, + new DomOptions({ + styles: { + fontSize: "0.85rem", + fontWeight: "bold", + marginBottom: "0.25rem", + }, + }), + ), + Dom.range( + minValue, + maxValue, + currentMin, + currentMax, + step, + (min, max) => onChange(min, max), + new DomOptions({ + id, + styles: { + marginBottom: "1rem", + }, + }), + ), + ], + styles: { + display: "flex", + flexDirection: "column", + marginBottom: "1.75rem", + }, + }), + ); + } + + /** * Create a number filter input * @param {string} id * @param {string} labelText @@ -506,6 +557,8 @@ export class Sidebar { #onColorChange; /** @type {(param: string) => void} */ #onAreaColorChange; + /** @type {HTMLElement|null} */ + #filtersSectionElement; /** * @param {House[]|null} allHouses @@ -542,6 +595,7 @@ export class Sidebar { initialParam, ); + this.#filtersSectionElement = null; this.#rootElement = this.#render(); } @@ -754,12 +808,36 @@ export class Sidebar { }), ); } - /** * @param {Filters} filters * @param {() => void} onChange */ static filtersSection(filters, onChange) { + // Calculate reasonable ranges based on typical values + const priceRange = { + max: 1000000, + min: 0, + step: 10000, + }; + + const yearRange = { + max: new Date().getFullYear(), + min: 1800, + step: 1, + }; + + const areaRange = { + max: 500, + min: 0, + step: 10, + }; + + const lotRange = { + max: 5000, + min: 0, + step: 100, + }; + return Dom.div( new DomOptions({ children: [ @@ -774,33 +852,172 @@ export class Sidebar { }, }), ), + + // Price Range + Dom.div( + new DomOptions({ + children: [ + Dom.label( + "price-range", + "Price Range (€)", + new DomOptions({ + styles: { + fontSize: "0.85rem", + fontWeight: "bold", + marginBottom: "0.25rem", + }, + }), + ), + Dom.range( + priceRange.min, + priceRange.max, + filters.minPrice || priceRange.min, + filters.maxPrice === Number.POSITIVE_INFINITY ? priceRange.max : filters.maxPrice, + priceRange.step, + (min, max) => { + filters.minPrice = min; + filters.maxPrice = max === priceRange.max ? Number.POSITIVE_INFINITY : max; + onChange(); + }, + new DomOptions({ + id: "price-range", + styles: { + marginBottom: "1.5rem", + }, + }), + ), + ], + styles: { + display: "flex", + flexDirection: "column", + }, + }), + ), + + // Construction Year Range Dom.div( new DomOptions({ children: [ - Widgets.numberFilter("min-price", "Min price (€)", filters.minPrice, (v) => { - filters.minPrice = v ?? 0; - onChange(); - }), - Widgets.numberFilter("max-price", "Max price (€)", filters.maxPrice, (v) => { - filters.maxPrice = v ?? Number.POSITIVE_INFINITY; - onChange(); - }), + Dom.label( + "year-range", + "Construction Year", + new DomOptions({ + styles: { + fontSize: "0.85rem", + fontWeight: "bold", + marginBottom: "0.25rem", + }, + }), + ), + Dom.range( + yearRange.min, + yearRange.max, + filters.minYear || yearRange.min, + filters.maxYear === Number.POSITIVE_INFINITY ? yearRange.max : filters.maxYear, + yearRange.step, + (min, max) => { + filters.minYear = min; + filters.maxYear = max === yearRange.max ? Number.POSITIVE_INFINITY : max; + onChange(); + }, + new DomOptions({ + id: "year-range", + styles: { + marginBottom: "1.5rem", + }, + }), + ), ], - id: "price-row", styles: { display: "flex", - gap: "0.5rem", + flexDirection: "column", }, }), ), - Widgets.numberFilter("min-year", "Min year", filters.minYear, (v) => { - filters.minYear = v ?? 0; - onChange(); - }), - Widgets.numberFilter("min-area", "Min area (m²)", filters.minArea, (v) => { - filters.minArea = v ?? 0; - onChange(); - }), + + // Living Area Range + Dom.div( + new DomOptions({ + children: [ + Dom.label( + "area-range", + "Living Area (m²)", + new DomOptions({ + styles: { + fontSize: "0.85rem", + fontWeight: "bold", + marginBottom: "0.25rem", + }, + }), + ), + Dom.range( + areaRange.min, + areaRange.max, + filters.minArea || areaRange.min, + filters.maxArea === Number.POSITIVE_INFINITY ? areaRange.max : filters.maxArea, + areaRange.step, + (min, max) => { + filters.minArea = min; + filters.maxArea = max === areaRange.max ? Number.POSITIVE_INFINITY : max; + onChange(); + }, + new DomOptions({ + id: "area-range", + styles: { + marginBottom: "1.5rem", + }, + }), + ), + ], + styles: { + display: "flex", + flexDirection: "column", + }, + }), + ), + + // Lot Size Range + Dom.div( + new DomOptions({ + children: [ + Dom.label( + "lot-range", + "Lot Size (m²)", + new DomOptions({ + styles: { + fontSize: "0.85rem", + fontWeight: "bold", + marginBottom: "0.25rem", + }, + }), + ), + Dom.range( + lotRange.min, + lotRange.max, + filters.minLot || lotRange.min, + filters.maxLot === Number.POSITIVE_INFINITY ? lotRange.max : filters.maxLot, + lotRange.step, + (min, max) => { + filters.minLot = min; + filters.maxLot = max === lotRange.max ? Number.POSITIVE_INFINITY : max; + onChange(); + }, + new DomOptions({ + id: "lot-range", + styles: { + marginBottom: "1.5rem", + }, + }), + ), + ], + styles: { + display: "flex", + flexDirection: "column", + }, + }), + ), + + // Districts Multi-select Dom.div( new DomOptions({ children: [ @@ -838,8 +1055,43 @@ export class Sidebar { }), ), ], - id: "district-multi-select", - styles: { display: "flex", flexDirection: "column", marginTop: "1rem" }, + styles: { + display: "flex", + flexDirection: "column", + }, + }), + ), + + // Clear Filters Button + Dom.button( + "Clear All Filters", + () => { + // Reset all ranges to their maximum possible values + filters.minPrice = 0; + filters.maxPrice = Number.POSITIVE_INFINITY; + filters.minYear = 1800; + filters.maxYear = Number.POSITIVE_INFINITY; + filters.minArea = 0; + filters.maxArea = Number.POSITIVE_INFINITY; + filters.minLot = 0; + filters.maxLot = Number.POSITIVE_INFINITY; + filters.districts = []; + + // Update the UI by triggering onChange + onChange(); + }, + new DomOptions({ + styles: { + background: "#f44336", + border: "none", + borderRadius: "4px", + color: "white", + cursor: "pointer", + fontSize: "0.85rem", + marginTop: "1rem", + padding: "0.5rem 1rem", + width: "100%", + }, }), ), ], @@ -852,22 +1104,39 @@ export class Sidebar { } /** - * Update histogram data + * Update sidebar with new data * @param {House[]} houses - * @param {string} parameter + * @param {string} histogramParameter */ - updateHistogram(houses, parameter) { - const values = houses.map((house) => house.get(parameter)); - if (this.#histogram) { - this.#histogram.update(values, parameter); - } + update(houses, histogramParameter) { + this.#allHouses = houses; + + // Update histogram + const values = houses.map((house) => house.get(histogramParameter)); + this.#histogram.update(values, histogramParameter); + + // Update filters section + this.#updateFiltersSection(); + + // Update districts + this.updateDistricts(houses); } /** - * Handle histogram bar click - * @param {number} min - * @param {number} max + * Update filters section with current data */ + #updateFiltersSection() { + if (!this.#allHouses || this.#allHouses.length === 0) return; + + const newFiltersSection = Sidebar.filtersSection(this.#filters, this.#onFilterChange); + + if (this.#filtersSectionElement) { + this.#filtersSectionElement.replaceWith(newFiltersSection); + } + + this.#filtersSectionElement = newFiltersSection; + } + /** * Handle histogram bar click * @param {number} min @@ -880,69 +1149,27 @@ export class Sidebar { case HouseParameter.price: { this.#filters.minPrice = min; this.#filters.maxPrice = max; - // Update the input fields to reflect the new filter values - const minPriceInput = document.getElementById("min-price"); - const maxPriceInput = document.getElementById("max-price"); - if (minPriceInput) minPriceInput.value = Math.round(min).toString(); - if (maxPriceInput) maxPriceInput.value = Math.round(max).toString(); break; } case HouseParameter.area: { this.#filters.minArea = min; - // Update the input field to reflect the new filter value - const minAreaInput = document.getElementById("min-area"); - if (minAreaInput) minAreaInput.value = Math.round(min).toString(); + this.#filters.maxArea = max; break; } case HouseParameter.year: { this.#filters.minYear = min; - // Update the input field to reflect the new filter value - const minYearInput = document.getElementById("min-year"); - if (minYearInput) minYearInput.value = Math.round(min).toString(); + this.#filters.maxYear = max; break; } case HouseParameter.score: { - // Note: You might want to add score filters to your Filters class + // Handle score filtering if needed console.log(`Score range: ${min} - ${max}`); - // If you add score filters to Filters class, you would do: - // this.#filters.minScore = min; - // this.#filters.maxScore = max; break; } } - // Update the input fields to show the new filter values - this.#updateFilterInputs(); - // Trigger the filter change to update the application this.#onFilterChange(); } - /** - * Update filter input values to match current filters - */ - #updateFilterInputs() { - const minPriceInput = document.getElementById("min-price"); - const maxPriceInput = document.getElementById("max-price"); - const minAreaInput = document.getElementById("min-area"); - const minYearInput = document.getElementById("min-year"); - - if (minPriceInput) - minPriceInput.value = this.#filters.minPrice - ? Math.round(this.#filters.minPrice).toString() - : ""; - if (maxPriceInput) - maxPriceInput.value = - this.#filters.maxPrice !== Number.POSITIVE_INFINITY - ? Math.round(this.#filters.maxPrice).toString() - : ""; - if (minAreaInput) - minAreaInput.value = this.#filters.minArea - ? Math.round(this.#filters.minArea).toString() - : ""; - if (minYearInput) - minYearInput.value = this.#filters.minYear - ? Math.round(this.#filters.minYear).toString() - : ""; - } /** * @param {Histogram} histogram |
