diff options
| author | Petri Hienonen <petri.hienonen@gmail.com> | 2025-11-15 21:49:03 +0200 |
|---|---|---|
| committer | Petri Hienonen <petri.hienonen@gmail.com> | 2025-11-15 21:49:03 +0200 |
| commit | 02fedba64c93258044945b3d1ac5d1ecb6ab64c3 (patch) | |
| tree | 660de4cc5aece12baa89250e764708294d80dce4 | |
| parent | a44f2de806d0557b148dcdf36a3107cb4ecf31ce (diff) | |
| download | housing-02fedba64c93258044945b3d1ac5d1ecb6ab64c3.tar.zst | |
Change layout
| -rw-r--r-- | app/components.js | 910 | ||||
| -rw-r--r-- | app/main.js | 100 |
2 files changed, 565 insertions, 445 deletions
diff --git a/app/components.js b/app/components.js index 72c0595..27ad900 100644 --- a/app/components.js +++ b/app/components.js @@ -617,56 +617,46 @@ export class Widgets { } } -export class Sidebar { +export class LeftSidebar { + /** @type {HTMLElement} */ + #rootElement; /** @type {Histogram} */ #histogram; /** @type {House[]} */ #allHouses; - /** @type {HTMLElement} */ - #rootElement; - /** @type {boolean} */ - #collapsed = false; /** @type {Filters} */ #filters; /** @type {AreaParam} */ #areaParam; /** @type {HouseParameter} */ #houseParam; - /** @type {Weights} */ - #weights; + /** @type {boolean} */ + #collapsed = true; /** @type {() => void} */ #onFilterChange; - /** @type {(key: string, value: number) => void} */ - #onWeightChange; /** @type {(param: string) => void} */ #onColorChange; /** @type {(param: string) => void} */ #onAreaColorChange; - /** @type {HTMLElement|null} */ - #filtersSectionElement; /** * @param {Object} options * @param {House[]} options.allHouses - * @param {AreaParam} options.areaParam * - * @param {AreaParam} options.houseParam + * @param {AreaParam} options.areaParam + * @param {HouseParameter} options.houseParam * @param {Filters} options.filters - * @param {Weights} options.weights * @param {() => void} options.onFilterChange - * @param {(key: string, value: number) => void} options.onWeightChange - * @param {(param: string) => void} options.onHouseParamChange - * @param {(param: string) => void} options.onAreaParamChange + * @param {(param: string) => void} options.onColorChange + * @param {(param: string) => void} options.onAreaColorChange */ constructor(options) { this.#areaParam = options.areaParam; this.#houseParam = options.houseParam; this.#allHouses = options.allHouses; this.#filters = options.filters; - this.#weights = options.weights; this.#onFilterChange = options.onFilterChange; - this.#onWeightChange = options.onWeightChange; - this.#onColorChange = options.onHouseParamChange; - this.#onAreaColorChange = options.onAreaParamChange; + this.#onColorChange = options.onColorChange; + this.#onAreaColorChange = options.onAreaColorChange; const initialValues = this.#allHouses?.map((house) => house.get(this.#houseParam)); this.#histogram = new Histogram({ @@ -676,452 +666,549 @@ export class Sidebar { values: initialValues || [], }); - this.#filtersSectionElement = null; this.#rootElement = this.#render(); } /** - * @param {Weights} weights - * @param {(key: string, value: number) => void} onChange + * Handle histogram bar click + * @param {number} min + * @param {number} max */ - static weightSection(weights, onChange) { - return Dom.div( - new DomOptions({ - children: [ - Dom.heading( - 3, - "Scoring Weights", - new DomOptions({ - styles: { - color: "#333", - fontSize: "1.1rem", - margin: "1rem 0 1rem 0", - }, - }), - ), - // Basic house properties - Widgets.slider("w-price", "Price", "price", weights.price, onChange), - Widgets.slider( - "w-year", - "Construction Year", - "constructionYear", - weights.constructionYear, - onChange, - ), - Widgets.slider("w-area", "Living Area", "livingArea", weights.livingArea, onChange), - - // Location factors - Widgets.slider( - "w-market", - "Market Distance", - "distanceMarket", - weights.distanceMarket, - onChange, - ), - Widgets.slider( - "w-school", - "School Distance", - "distanceSchool", - weights.distanceSchool, - onChange, - ), - - // Transit distances - Widgets.slider( - "w-train", - "Train Distance", - "distanceTrain", - weights.distanceTrain, - onChange, - ), - Widgets.slider( - "w-lightrail", - "Light Rail Distance", - "distanceLightRail", - weights.distanceLightRail, - onChange, - ), - Widgets.slider("w-tram", "Tram Distance", "distanceTram", weights.distanceTram, onChange), + #handleHistogramClick(min, max) { + const param = this.#houseParam; - // Statistical area factors - Widgets.slider( - "w-foreign", - "Foreign Speakers", - "foreignSpeakers", - weights.foreignSpeakers, - onChange, - ), - Widgets.slider( - "w-unemployment", - "Unemployment Rate", - "unemploymentRate", - weights.unemploymentRate, - onChange, - ), - Widgets.slider( - "w-income", - "Average Income", - "averageIncome", - weights.averageIncome, - onChange, - ), - Widgets.slider( - "w-education", - "Higher Education", - "higherEducation", - weights.higherEducation, - onChange, - ), - ], - }), - ); + switch (param) { + case HouseParameter.price: { + this.#filters.minPrice = min; + this.#filters.maxPrice = max; + break; + } + case HouseParameter.area: { + this.#filters.minArea = min; + this.#filters.maxArea = max; + break; + } + case HouseParameter.year: { + this.#filters.minYear = min; + this.#filters.maxYear = max; + break; + } + case HouseParameter.score: { + // Handle score filtering if needed + console.log(`Score range: ${min} - ${max}`); + break; + } + } + this.#onFilterChange(); } /** - * @param {AreaParam} areaParam + * Update histogram when house parameter changes * @param {HouseParameter} houseParam - * @param {(param: string) => void} onHouseChange - * @param {(param: string) => void} onAreaChange */ - static dataSection(areaParam, houseParam, onHouseChange, onAreaChange) { - return Dom.div( - new DomOptions({ - children: [ - Dom.heading( - 3, - "Visualisation parameters", - new DomOptions({ - styles: { - color: "#333", - fontSize: "1.1rem", - margin: "0 0 1rem 0", - }, - }), - ), - Widgets.dropdown( - "Color houses by", - [ - { text: "Price", value: HouseParameter.price }, - { text: "Score", value: HouseParameter.score }, - { text: "Construction Year", value: HouseParameter.year }, - { text: "Living Area", value: HouseParameter.area }, - ], - houseParam, - (value) => onHouseChange(value), - new DomOptions({ - id: "color-parameter", - styles: { - marginBottom: "1rem", - }, - }), - ), - Widgets.dropdown( - "Color areas by", - [ - { text: "None", value: AreaParam.none }, - { text: "Foreign speakers", value: AreaParam.foreignSpeakers }, - { text: "Unemployment rate", value: AreaParam.unemploymentRate }, - { text: "Average income", value: AreaParam.averageIncome }, - { text: "Higher education", value: AreaParam.higherEducation }, - ], - areaParam, - (value) => onAreaChange(value), - new DomOptions({ - id: "area-color-parameter", - }), - ), - ], - styles: { - borderBottom: "1px solid #eee", - paddingBottom: "1rem", - }, - }), - ); + #updateHistogram(houseParam) { + this.#houseParam = houseParam; + const values = this.#allHouses.map((house) => house.get(houseParam)); + this.#histogram.update(values, houseParam); } + /** - * @param {Filters} filters - * @param {House[]} houses - * @param {() => void} onChange + * Render sidebar content + * @returns {HTMLElement} */ - static filtersSection(filters, houses, onChange) { - const priceRange = { - max: filters.maxPrice, - min: filters.minPrice, - step: 10000, - }; - - const yearRange = { - max: filters.maxYear, - min: filters.minYear, - step: 1, - }; - - const areaRange = { - max: filters.maxArea, - min: filters.minArea, - step: 10, - }; - - const lotRange = { - max: filters.maxArea, - min: filters.minArea, - step: 100, - }; - + #renderContent() { return Dom.div( new DomOptions({ children: [ - Dom.heading( - 3, - "Filters", + // Histogram section + Dom.div( new DomOptions({ + children: [ + Dom.heading( + 3, + "Distribution", + new DomOptions({ + styles: { + color: "#333", + fontSize: "1.1rem", + margin: "0 0 0.5rem 0", + }, + }), + ), + this.#histogram.render(), + ], styles: { - color: "#333", - fontSize: "1.1rem", - margin: "1rem 0 1rem 0", + borderBottom: "1px solid #eee", + paddingBottom: "1rem", }, }), ), - Widgets.range( - "Price Range (€)", - priceRange.min, - priceRange.max, - filters.minPrice, - filters.maxPrice, - (min, max) => { - filters.minPrice = min; - filters.maxPrice = max === priceRange.max ? Number.POSITIVE_INFINITY : max; - onChange(); - }, - priceRange.step, - new DomOptions({ - id: "price-range", - }), - ), - Widgets.range( - "Construction Year", - yearRange.min, - yearRange.max, - filters.minYear, - filters.maxYear, - (min, max) => { - filters.minYear = min; - filters.maxYear = max === yearRange.max ? Number.POSITIVE_INFINITY : max; - onChange(); - }, - yearRange.step, - new DomOptions({ - id: "year-range", - }), - ), - - Widgets.range( - "Living Area (m²)", - areaRange.min, - areaRange.max, - filters.minArea, - filters.maxArea, - (min, max) => { - filters.minArea = min; - filters.maxArea = max === areaRange.max ? Number.POSITIVE_INFINITY : max; - onChange(); - }, - areaRange.step, - new DomOptions({ - id: "area-range", - }), - ), - Widgets.range( - "Lot Size (m²)", - lotRange.min, - lotRange.max, - filters.minLot, - filters.maxLot, - (min, max) => { - filters.minLot = min; - filters.maxLot = max === lotRange.max ? Number.POSITIVE_INFINITY : max; - onChange(); - }, - lotRange.step, + // Data visualization parameters + Dom.div( new DomOptions({ - id: "lot-range", + children: [ + Dom.heading( + 3, + "Visualisation parameters", + new DomOptions({ + styles: { + color: "#333", + fontSize: "1.1rem", + margin: "0 0 1rem 0", + }, + }), + ), + Widgets.dropdown( + "Color houses by", + [ + { text: "Price", value: HouseParameter.price }, + { text: "Score", value: HouseParameter.score }, + { text: "Construction Year", value: HouseParameter.year }, + { text: "Living Area", value: HouseParameter.area }, + ], + this.#houseParam, + (value) => { + this.#onColorChange(value); + this.#updateHistogram(value); + }, + new DomOptions({ + id: "color-parameter", + styles: { + marginBottom: "1rem", + }, + }), + ), + Widgets.dropdown( + "Color areas by", + [ + { text: "None", value: AreaParam.none }, + { text: "Foreign speakers", value: AreaParam.foreignSpeakers }, + { text: "Unemployment rate", value: AreaParam.unemploymentRate }, + { text: "Average income", value: AreaParam.averageIncome }, + { text: "Higher education", value: AreaParam.higherEducation }, + ], + this.#areaParam, + (value) => this.#onAreaColorChange(value), + new DomOptions({ + id: "area-color-parameter", + }), + ), + ], + styles: { + borderBottom: "1px solid #eee", + paddingBottom: "1rem", + }, }), ), - // Districts Multi-select + // Filters section Dom.div( new DomOptions({ children: [ - Dom.label( - "district-select", - "Districts", + Dom.heading( + 3, + "Filters", new DomOptions({ styles: { - fontSize: "0.85rem", - fontWeight: "bold", - marginBottom: "0.25rem", + color: "#333", + fontSize: "1.1rem", + margin: "1rem 0 1rem 0", }, }), ), - Dom.select( - undefined, - (e) => { - const target = /** @type {HTMLSelectElement} */ (e.target); - const selectedOptions = Array.from(target.selectedOptions).map( - (opt) => opt.value, - ); - filters.districts = selectedOptions; - onChange(); + + // Price filter + Widgets.range( + "Price Range (€)", + this.#filters.minPrice, + this.#filters.maxPrice, + this.#filters.minPrice, + this.#filters.maxPrice, + (min, max) => { + this.#filters.minPrice = min; + this.#filters.maxPrice = + max === this.#filters.maxPrice ? Number.POSITIVE_INFINITY : max; + this.#onFilterChange(); + }, + 10000, + new DomOptions({ + id: "price-range", + }), + ), + + // Construction year filter + Widgets.range( + "Construction Year", + this.#filters.minYear, + this.#filters.maxYear, + this.#filters.minYear, + this.#filters.maxYear, + (min, max) => { + this.#filters.minYear = min; + this.#filters.maxYear = + max === this.#filters.maxYear ? Number.POSITIVE_INFINITY : max; + this.#onFilterChange(); + }, + 1, + new DomOptions({ + id: "year-range", + }), + ), + + // Living area filter + Widgets.range( + "Living Area (m²)", + this.#filters.minArea, + this.#filters.maxArea, + this.#filters.minArea, + this.#filters.maxArea, + (min, max) => { + this.#filters.minArea = min; + this.#filters.maxArea = + max === this.#filters.maxArea ? Number.POSITIVE_INFINITY : max; + this.#onFilterChange(); + }, + 10, + new DomOptions({ + id: "area-range", + }), + ), + + // Lot size filter + Widgets.range( + "Lot Size (m²)", + this.#filters.minLot, + this.#filters.maxLot, + this.#filters.minLot, + this.#filters.maxLot, + (min, max) => { + this.#filters.minLot = min; + this.#filters.maxLot = + max === this.#filters.maxLot ? Number.POSITIVE_INFINITY : max; + this.#onFilterChange(); + }, + 100, + new DomOptions({ + id: "lot-range", + }), + ), + + // Districts Multi-select + Dom.div( + new DomOptions({ + children: [ + Dom.label( + "district-select", + "Districts", + new DomOptions({ + styles: { + fontSize: "0.85rem", + fontWeight: "bold", + marginBottom: "0.25rem", + }, + }), + ), + Dom.select( + undefined, + (e) => { + const target = /** @type {HTMLSelectElement} */ (e.target); + const selectedOptions = Array.from(target.selectedOptions).map( + (opt) => opt.value, + ); + this.#filters.districts = selectedOptions; + this.#onFilterChange(); + }, + new DomOptions({ + attributes: { multiple: "true" }, + children: [...this.#renderDistrictOptions()], + id: "district-select", + styles: { + border: "1px solid #ddd", + borderRadius: "4px", + minHeight: "120px", + padding: "0.5rem", + width: "100%", + }, + }), + ), + ], + styles: { + display: "flex", + flexDirection: "column", + }, + }), + ), + + // Clear Filters Button + Dom.button( + "Clear All Filters", + () => { + this.#filters.reset(); + this.#onFilterChange(); }, new DomOptions({ - attributes: { multiple: "true" }, - children: [...Sidebar.#renderDistrictOptions(houses)], - id: "district-select", styles: { - border: "1px solid #ddd", + background: "#f44336", + border: "none", borderRadius: "4px", - minHeight: "120px", - padding: "0.5rem", + color: "white", + cursor: "pointer", + fontSize: "0.85rem", + marginTop: "1rem", + padding: "0.5rem 1rem", width: "100%", }, }), ), ], styles: { - display: "flex", - flexDirection: "column", + borderBottom: "1px solid #eee", + paddingBottom: "1rem", }, }), ), + ], + id: "left-sidebar-content", + styles: { + display: this.#collapsed ? "none" : "block", + height: "100%", + overflowY: "auto", + }, + }), + ); + } - // Clear Filters Button - Dom.button( - "Clear All Filters", - () => { - // Reset all ranges to their maximum possible values - filters.reset(); + /** + * Render district options for multi-select + * @returns {HTMLOptionElement[]} + */ + #renderDistrictOptions() { + const houseDistricts = [ + ...new Set(this.#allHouses.map((h) => h.district).filter((d) => d)), + ].sort(); + return houseDistricts.map((districtName) => Dom.option(districtName, districtName)); + } - // Update the UI by triggering onChange - onChange(); - }, + /** + * Render the complete sidebar + * @returns {HTMLElement} + */ + #render() { + return Dom.div( + new DomOptions({ + children: [ + // Toggle button + Dom.button( + "☰", + () => this.toggle(), new DomOptions({ + id: "left-sidebar-toggle", styles: { - background: "#f44336", + background: "none", border: "none", - borderRadius: "4px", - color: "white", + color: "#333", cursor: "pointer", - fontSize: "0.85rem", - marginTop: "1rem", - padding: "0.5rem 1rem", - width: "100%", + fontSize: "1.5rem", + left: "0.5rem", + padding: "0.5rem", + position: "absolute", + top: "0.5rem", + zIndex: "10", }, }), ), + this.#renderContent(), ], + id: "left-sidebar", styles: { - borderBottom: "1px solid #eee", - paddingBottom: "1rem", + background: "#fff", + borderRight: "1px solid #ddd", + display: "flex", + flexDirection: "column", + flexShrink: "0", + height: "100%", + overflowY: "auto", + padding: this.#collapsed ? "0" : "1rem", + position: "relative", + transition: "width 0.3s ease, padding 0.3s ease", + width: this.#collapsed ? "0" : "300px", }, }), ); } /** - * Update sidebar with new data - * @param {HouseParameter} houseParam + * Get the root DOM element + * @returns {HTMLElement} */ - update(houseParam) { - const values = this.#allHouses.map((house) => house.get(houseParam)); - this.#histogram.update(values, houseParam); - this.#updateFiltersSection(this.#allHouses); + render() { + return this.#rootElement; } - /** - * Update filters section with current data - * @param {House[]} houses - */ - #updateFiltersSection(houses) { - if (!houses || houses.length === 0) return; + /** Toggle sidebar visibility */ + toggle() { + this.#collapsed = !this.#collapsed; + const sidebarContent = this.#rootElement.querySelector("#left-sidebar-content"); - const newFiltersSection = Sidebar.filtersSection( - this.#filters, - this.#allHouses, - this.#onFilterChange, - ); + if (this.#collapsed) { + this.#rootElement.style.width = "0"; + this.#rootElement.style.padding = "0"; + if (sidebarContent) sidebarContent.style.display = "none"; + } else { + this.#rootElement.style.width = "300px"; + this.#rootElement.style.padding = "1rem"; + if (sidebarContent) sidebarContent.style.display = "block"; + } + } - if (this.#filtersSectionElement) { - this.#filtersSectionElement.replaceWith(newFiltersSection); + /** Show the sidebar */ + show() { + if (this.#collapsed) { + this.toggle(); } + } - this.#filtersSectionElement = newFiltersSection; + /** Hide the sidebar */ + hide() { + if (!this.#collapsed) { + this.toggle(); + } } +} + +export class RightSidebar { + /** @type {HTMLElement} */ + #rootElement; + /** @type {Weights} */ + #weights; + /** @type {boolean} */ + #collapsed = true; + /** @type {(key: string, value: number) => void} */ + #onWeightChange; /** - * Handle histogram bar click - * @param {number} min - * @param {number} max + * @param {Object} options + * @param {Weights} options.weights + * @param {(key: string, value: number) => void} options.onWeightChange */ - #handleHistogramClick(min, max) { - const param = this.#houseParam; - - switch (param) { - case HouseParameter.price: { - this.#filters.minPrice = min; - this.#filters.maxPrice = max; - break; - } - case HouseParameter.area: { - this.#filters.minArea = min; - this.#filters.maxArea = max; - break; - } - case HouseParameter.year: { - this.#filters.minYear = min; - this.#filters.maxYear = max; - break; - } - case HouseParameter.score: { - // Handle score filtering if needed - console.log(`Score range: ${min} - ${max}`); - break; - } - } - // Trigger the filter change to update the application - this.#onFilterChange(); + constructor(options) { + this.#weights = options.weights; + this.#onWeightChange = options.onWeightChange; + this.#rootElement = this.#render(); } /** - * @param {Histogram} histogram + * Render weights section + * @returns {HTMLElement} */ - static histogramSection(histogram) { + #renderWeights() { return Dom.div( new DomOptions({ children: [ Dom.heading( 3, - "Distribution", + "Scoring Weights", new DomOptions({ styles: { color: "#333", fontSize: "1.1rem", - margin: "0 0 0.5rem 0", + margin: "1rem 0 1rem 0", }, }), ), - histogram.render(), + // Basic house properties + Widgets.slider("w-price", "Price", "price", this.#weights.price, this.#onWeightChange), + Widgets.slider( + "w-year", + "Construction Year", + "constructionYear", + this.#weights.constructionYear, + this.#onWeightChange, + ), + Widgets.slider( + "w-area", + "Living Area", + "livingArea", + this.#weights.livingArea, + this.#onWeightChange, + ), + + // Location factors + Widgets.slider( + "w-market", + "Market Distance", + "distanceMarket", + this.#weights.distanceMarket, + this.#onWeightChange, + ), + Widgets.slider( + "w-school", + "School Distance", + "distanceSchool", + this.#weights.distanceSchool, + this.#onWeightChange, + ), + + // Transit distances + Widgets.slider( + "w-train", + "Train Distance", + "distanceTrain", + this.#weights.distanceTrain, + this.#onWeightChange, + ), + Widgets.slider( + "w-lightrail", + "Light Rail Distance", + "distanceLightRail", + this.#weights.distanceLightRail, + this.#onWeightChange, + ), + Widgets.slider( + "w-tram", + "Tram Distance", + "distanceTram", + this.#weights.distanceTram, + this.#onWeightChange, + ), + + // Statistical area factors + Widgets.slider( + "w-foreign", + "Foreign Speakers", + "foreignSpeakers", + this.#weights.foreignSpeakers, + this.#onWeightChange, + ), + Widgets.slider( + "w-unemployment", + "Unemployment Rate", + "unemploymentRate", + this.#weights.unemploymentRate, + this.#onWeightChange, + ), + Widgets.slider( + "w-income", + "Average Income", + "averageIncome", + this.#weights.averageIncome, + this.#onWeightChange, + ), + Widgets.slider( + "w-education", + "Higher Education", + "higherEducation", + this.#weights.higherEducation, + this.#onWeightChange, + ), ], - styles: { - borderBottom: "1px solid #eee", - paddingBottom: "1rem", - }, }), ); } /** - * Render sidebar container + * Render the complete sidebar * @returns {HTMLElement} */ #render() { @@ -1130,10 +1217,10 @@ export class Sidebar { children: [ // Toggle button Dom.button( - "☰", + "⚙️", () => this.toggle(), new DomOptions({ - id: "sidebar-toggle", + id: "right-sidebar-toggle", styles: { background: "none", border: "none", @@ -1150,33 +1237,29 @@ export class Sidebar { ), Dom.div( new DomOptions({ - children: [ - Sidebar.histogramSection(this.#histogram), - Sidebar.dataSection( - this.#areaParam, - this.#houseParam, - this.#onColorChange, - this.#onAreaColorChange, - ), - Sidebar.filtersSection(this.#filters, this.#allHouses, this.#onFilterChange), - Sidebar.weightSection(this.#weights, this.#onWeightChange), - ], - id: "sidebar-content", + children: [this.#renderWeights()], + id: "right-sidebar-content", + styles: { + display: this.#collapsed ? "none" : "block", + height: "100%", + overflowY: "auto", + }, }), ), ], - id: "sidebar", + id: "right-sidebar", styles: { background: "#fff", - borderRight: "1px solid #ddd", + borderLeft: "1px solid #ddd", display: "flex", flexDirection: "column", flexShrink: "0", + height: "100%", overflowY: "auto", - padding: "1rem", + padding: this.#collapsed ? "0" : "1rem", position: "relative", - transition: "width 0.3s ease", - width: "300px", + transition: "width 0.3s ease, padding 0.3s ease", + width: this.#collapsed ? "0" : "300px", }, }), ); @@ -1190,63 +1273,34 @@ export class Sidebar { return this.#rootElement; } - /** Show the sidebar */ - show() { - if (this.#collapsed) { - this.toggle(); - } - } - - /** Hide the sidebar */ - hide() { - if (!this.#collapsed) { - this.toggle(); - } - } - + /** Toggle sidebar visibility */ toggle() { this.#collapsed = !this.#collapsed; - const sidebarContent = this.#rootElement.querySelector("#sidebar-content"); - const toggleButton = this.#rootElement.querySelector("#sidebar-toggle"); + const sidebarContent = this.#rootElement.querySelector("#right-sidebar-content"); if (this.#collapsed) { this.#rootElement.style.width = "0"; this.#rootElement.style.padding = "0"; if (sidebarContent) sidebarContent.style.display = "none"; - if (toggleButton) { - toggleButton.textContent = "☰"; - toggleButton.style.right = "0.5rem"; - } } else { this.#rootElement.style.width = "300px"; this.#rootElement.style.padding = "1rem"; if (sidebarContent) sidebarContent.style.display = "block"; - if (toggleButton) { - toggleButton.textContent = "☰"; - toggleButton.style.right = "0.5rem"; - } } } - /** - * Set the area color parameter in the dropdown - * @param {string} param - */ - setAreaParameter(param) { - const areaColorSelect = this.#rootElement.querySelector("#area-color-parameter"); - if (areaColorSelect) { - areaColorSelect.value = param; + /** Show the sidebar */ + show() { + if (this.#collapsed) { + this.toggle(); } } - /** - * Render district options for multi-select - * @param {House[]} houses - * @returns {HTMLOptionElement[]} - */ - static #renderDistrictOptions(houses) { - const houseDistricts = [...new Set(houses.map((h) => h.district).filter((d) => d))].sort(); - return houseDistricts.map((districtName) => Dom.option(districtName, districtName)); + /** Hide the sidebar */ + hide() { + if (!this.#collapsed) { + this.toggle(); + } } } diff --git a/app/main.js b/app/main.js index 9a4da0c..ef51345 100644 --- a/app/main.js +++ b/app/main.js @@ -1,6 +1,6 @@ // main.js - Updated with Sidebar class -import { Modal, Sidebar } from "components"; +import { LeftSidebar, Modal, RightSidebar } from "components"; import { Dom, DomOptions } from "dom"; import { MapEl } from "map"; import { @@ -223,8 +223,10 @@ export class App { #map; /** @type {HTMLElement} */ #stats; - /** @type {Sidebar} */ - #sidebar; + /** @type {LeftSidebar} */ + #leftSidebar; + /** @type {RightSidebar} */ + #rightSidebar; /** @type {Modal|null} */ #modal = null; /** @type {HouseParameter} */ @@ -248,26 +250,75 @@ export class App { margin: "0", }); - this.#sidebar = new Sidebar({ + // Create header with global toggle buttons + const header = Dom.div( + new DomOptions({ + children: [ + // Left sidebar toggle button + Dom.button( + "☰ Filters", + () => this.#leftSidebar.toggle(), + new DomOptions({ + styles: { + background: "#fff", + border: "1px solid #ddd", + borderRadius: "4px", + cursor: "pointer", + fontSize: "1rem", + margin: "0.5rem", + padding: "0.5rem 1rem", + }, + }), + ), + // Right sidebar toggle button + Dom.button( + "⚙️ Weights", + () => this.#rightSidebar.toggle(), + new DomOptions({ + styles: { + background: "#fff", + border: "1px solid #ddd", + borderRadius: "4px", + cursor: "pointer", + fontSize: "1rem", + margin: "0.5rem", + padding: "0.5rem 1rem", + }, + }), + ), + ], + 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, - onAreaParamChange: (param) => { + 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; }, - onHouseParamChange: (param) => { - this.#houseParameter = param; - this.#map.updateHousesParameter(this.#houseParameter); - }, + }); + + this.#rightSidebar = new RightSidebar({ onWeightChange: (key, value) => { if (key in this.#weights) { this.#weights[/** @type {keyof Weights} */ (key)] = value; @@ -300,16 +351,31 @@ export class App { Dom.div( new DomOptions({ children: [ - this.#sidebar.render(), + header, Dom.div( new DomOptions({ - children: [this.#map.svg, this.#stats], - id: "map-container", + children: [ + this.#leftSidebar.render(), + Dom.div( + new DomOptions({ + children: [this.#map.svg, this.#stats], + id: "map-container", + styles: { + display: "flex", + flex: "1", + flexDirection: "column", + minWidth: "0", + }, + }), + ), + this.#rightSidebar.render(), + ], + id: "main-content", styles: { display: "flex", flex: "1", - flexDirection: "column", - minWidth: "0", + height: "calc(100vh - 60px)", + overflow: "hidden", }, }), ), @@ -317,8 +383,8 @@ export class App { id: "main", styles: { display: "flex", - flex: "1", - overflow: "hidden", + flexDirection: "column", + height: "100vh", }, }), ), |
