From 6ca89c37f84c6b1d63c869e6471d3570d51f63be Mon Sep 17 00:00:00 2001 From: Petri Hienonen Date: Fri, 14 Nov 2025 09:24:17 +0200 Subject: Make the sidebar more manageable --- app/components.js | 746 +++++++++++++++++++++++++++--------------------------- 1 file changed, 377 insertions(+), 369 deletions(-) (limited to 'app') diff --git a/app/components.js b/app/components.js index 0995692..cc08fb4 100644 --- a/app/components.js +++ b/app/components.js @@ -169,6 +169,312 @@ export class Sidebar { /** @type {(param: string) => void} */ #onAreaColorChange; + /** + * @param {Weights} weights + * @param {(key: string, value: number) => void} onChange + */ + 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), + + // 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, + ), + ], + }), + ); + } + + /** + * @param {(param: string) => void} onHouseChange + * @param {(param: string) => void} onAreaChange + */ + static dataSection(onHouseChange, onAreaChange) { + return Dom.div( + new DomOptions({ + children: [ + Dom.heading( + 3, + "Map Colors", + new DomOptions({ + styles: { + color: "#333", + fontSize: "1.1rem", + margin: "0 0 1rem 0", + }, + }), + ), + Dom.div( + new DomOptions({ + children: [ + Dom.label( + "color-parameter", + "Color houses by", + new DomOptions({ + styles: { + fontSize: "0.85rem", + fontWeight: "bold", + marginBottom: "0.25rem", + }, + }), + ), + Dom.select( + (e) => { + const target = /** @type {HTMLSelectElement} */ (e.target); + onHouseChange(target.value); + }, + new DomOptions({ + children: [ + Dom.option(HouseParameter.price, "Price"), + Dom.option(HouseParameter.score, "Score"), + Dom.option(HouseParameter.year, "Construction Year"), + Dom.option(HouseParameter.area, "Living Area"), + ], + id: "color-parameter", + styles: { + border: "1px solid #ddd", + borderRadius: "4px", + fontSize: "0.9rem", + padding: "0.5rem", + width: "100%", + }, + }), + ), + ], + styles: { + display: "flex", + flexDirection: "column", + marginBottom: "1rem", + }, + }), + ), + Dom.div( + new DomOptions({ + children: [ + Dom.label( + "area-color-parameter", + "Color areas by", + new DomOptions({ + styles: { + fontSize: "0.85rem", + fontWeight: "bold", + marginBottom: "0.25rem", + }, + }), + ), + Dom.select( + (e) => { + const target = /** @type {HTMLSelectElement} */ (e.target); + onAreaChange(target.value); + }, + new DomOptions({ + children: [ + Dom.option(AreaParam.none, "None"), + Dom.option(AreaParam.foreignSpeakers, "Foreign speakers"), + Dom.option(AreaParam.unemploymentRate, "Unemployment rate"), + Dom.option(AreaParam.averageIncome, "Average income"), + Dom.option(AreaParam.higherEducation, "Higher education"), + ], + id: "area-color-parameter", + styles: { + border: "1px solid #ddd", + borderRadius: "4px", + fontSize: "0.9rem", + padding: "0.5rem", + width: "100%", + }, + }), + ), + ], + styles: { display: "flex", flexDirection: "column" }, + }), + ), + ], + styles: { + borderBottom: "1px solid #eee", + paddingBottom: "1rem", + }, + }), + ); + } + + /** + * @param {Filters} filters + * @param {() => void} onChange + */ + static filtersSection(filters, onChange) { + return Dom.div( + new DomOptions({ + children: [ + Dom.heading( + 3, + "Filters", + new DomOptions({ + styles: { + color: "#333", + fontSize: "1.1rem", + margin: "1rem 0 1rem 0", + }, + }), + ), + Dom.div( + new DomOptions({ + children: [ + Widgets.numberFilter("min-price", "Min price (€)", (v) => { + filters.minPrice = v ?? 0; + onChange(); + }), + Widgets.numberFilter("max-price", "Max price (€)", (v) => { + filters.maxPrice = v ?? Number.POSITIVE_INFINITY; + onChange(); + }), + ], + id: "price-row", + styles: { + display: "flex", + gap: "0.5rem", + }, + }), + ), + Widgets.numberFilter("min-year", "Min year", (v) => { + filters.minYear = v ?? 0; + onChange(); + }), + Widgets.numberFilter("min-area", "Min area (m²)", (v) => { + filters.minArea = v ?? 0; + onChange(); + }), + Dom.div( + new DomOptions({ + children: [ + Dom.label( + "district-select", + "Districts", + new DomOptions({ + styles: { + fontSize: "0.85rem", + fontWeight: "bold", + marginBottom: "0.25rem", + }, + }), + ), + Dom.select( + (e) => { + const target = /** @type {HTMLSelectElement} */ (e.target); + const selectedOptions = Array.from(target.selectedOptions).map( + (opt) => opt.value, + ); + filters.districts = selectedOptions; + onChange(); + }, + new DomOptions({ + attributes: { multiple: "true" }, + children: [], + id: "district-select", + styles: { + border: "1px solid #ddd", + borderRadius: "4px", + minHeight: "120px", + padding: "0.5rem", + width: "100%", + }, + }), + ), + ], + id: "district-multi-select", + styles: { display: "flex", flexDirection: "column", marginTop: "1rem" }, + }), + ), + ], + styles: { + borderBottom: "1px solid #eee", + paddingBottom: "1rem", + }, + }), + ); + } + /** * @param {Filters} filters * @param {Weights} weights @@ -219,312 +525,9 @@ export class Sidebar { Dom.div( new DomOptions({ children: [ - // Map Colors Section - Dom.div( - new DomOptions({ - children: [ - Dom.heading( - 3, - "Map Colors", - new DomOptions({ - styles: { - color: "#333", - fontSize: "1.1rem", - margin: "0 0 1rem 0", - }, - }), - ), - Dom.div( - new DomOptions({ - children: [ - Dom.label( - "color-parameter", - "Color houses by", - new DomOptions({ - styles: { - fontSize: "0.85rem", - fontWeight: "bold", - marginBottom: "0.25rem", - }, - }), - ), - Dom.select( - (e) => { - const target = /** @type {HTMLSelectElement} */ (e.target); - this.#onColorChange(target.value); - }, - new DomOptions({ - children: [ - Dom.option(HouseParameter.price, "Price"), - Dom.option(HouseParameter.score, "Score"), - Dom.option(HouseParameter.year, "Construction Year"), - Dom.option(HouseParameter.area, "Living Area"), - ], - id: "color-parameter", - styles: { - border: "1px solid #ddd", - borderRadius: "4px", - fontSize: "0.9rem", - padding: "0.5rem", - width: "100%", - }, - }), - ), - ], - styles: { - display: "flex", - flexDirection: "column", - marginBottom: "1rem", - }, - }), - ), - Dom.div( - new DomOptions({ - children: [ - Dom.label( - "area-color-parameter", - "Color areas by", - new DomOptions({ - styles: { - fontSize: "0.85rem", - fontWeight: "bold", - marginBottom: "0.25rem", - }, - }), - ), - Dom.select( - (e) => { - const target = /** @type {HTMLSelectElement} */ (e.target); - this.#onAreaColorChange(target.value); - }, - new DomOptions({ - children: [ - Dom.option(AreaParam.none, "None"), - Dom.option(AreaParam.foreignSpeakers, "Foreign speakers"), - Dom.option(AreaParam.unemploymentRate, "Unemployment rate"), - Dom.option(AreaParam.averageIncome, "Average income"), - Dom.option(AreaParam.higherEducation, "Higher education"), - ], - id: "area-color-parameter", - styles: { - border: "1px solid #ddd", - borderRadius: "4px", - fontSize: "0.9rem", - padding: "0.5rem", - width: "100%", - }, - }), - ), - ], - styles: { display: "flex", flexDirection: "column" }, - }), - ), - ], - styles: { - borderBottom: "1px solid #eee", - paddingBottom: "1rem", - }, - }), - ), - // Filters Section - Dom.div( - new DomOptions({ - children: [ - Dom.heading( - 3, - "Filters", - new DomOptions({ - styles: { - color: "#333", - fontSize: "1.1rem", - margin: "1rem 0 1rem 0", - }, - }), - ), - Dom.div( - new DomOptions({ - children: [ - Widgets.numberFilter("min-price", "Min price (€)", (v) => { - this.#filters.minPrice = v ?? 0; - this.#onFilterChange(); - }), - Widgets.numberFilter("max-price", "Max price (€)", (v) => { - this.#filters.maxPrice = v ?? Number.POSITIVE_INFINITY; - this.#onFilterChange(); - }), - ], - id: "price-row", - styles: { - display: "flex", - gap: "0.5rem", - }, - }), - ), - Widgets.numberFilter("min-year", "Min year", (v) => { - this.#filters.minYear = v ?? 0; - this.#onFilterChange(); - }), - Widgets.numberFilter("min-area", "Min area (m²)", (v) => { - this.#filters.minArea = v ?? 0; - this.#onFilterChange(); - }), - Dom.div( - new DomOptions({ - children: [ - Dom.label( - "district-select", - "Districts", - new DomOptions({ - styles: { - fontSize: "0.85rem", - fontWeight: "bold", - marginBottom: "0.25rem", - }, - }), - ), - Dom.select( - (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: [], - id: "district-select", - styles: { - border: "1px solid #ddd", - borderRadius: "4px", - minHeight: "120px", - padding: "0.5rem", - width: "100%", - }, - }), - ), - ], - id: "district-multi-select", - styles: { display: "flex", flexDirection: "column", marginTop: "1rem" }, - }), - ), - ], - styles: { - borderBottom: "1px solid #eee", - paddingBottom: "1rem", - }, - }), - ), - // Weights Section - 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", - 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, - ), - ], - }), - ), + Sidebar.dataSection(this.#onColorChange, this.#onAreaColorChange), + Sidebar.filtersSection(this.#filters, this.#onFilterChange), + Sidebar.weightSection(this.#weights, this.#onWeightChange), ], id: "sidebar-content", }), @@ -641,6 +644,73 @@ export class Modal { /** @type {() => void} */ #onClearMapTimer; + /** + * @param {House} house + */ + static imageSection(house) { + return Dom.div( + new DomOptions({ + children: [ + Dom.span( + "Images", + new DomOptions({ + styles: { + fontSize: "14px", + fontWeight: "bold", + marginBottom: "10px", + }, + }), + ), + Dom.div( + new DomOptions({ + children: house.images.slice(0, 3).map((src) => { + // Wrap image in anchor tag that opens in new tab + return Dom.a( + new DomOptions({ + attributes: { + href: src, + rel: "noopener noreferrer", + target: "_blank", + }, + children: [ + Dom.img( + src, + new DomOptions({ + attributes: { + alt: "House image", + loading: "lazy", + }, + styles: { + borderRadius: "4px", + cursor: "pointer", + flexShrink: "0", + height: "100px", + transition: "opacity 0.2s ease", + }, + }), + ), + ], + styles: { + display: "block", + textDecoration: "none", + }, + }), + ); + }), + styles: { + display: "flex", + gap: "10px", + overflowX: "auto", + paddingBottom: "5px", + }, + }), + ), + ], + styles: { marginBottom: "20px" }, + }), + ); + } + /** * Build modal content for a house * @param {House} house @@ -747,69 +817,7 @@ export class Modal { ); if (house.images?.length) { - const imgSect = Dom.div( - new DomOptions({ - children: [ - Dom.span( - "Images", - new DomOptions({ - styles: { - fontSize: "14px", - fontWeight: "bold", - marginBottom: "10px", - }, - }), - ), - Dom.div( - new DomOptions({ - children: house.images.slice(0, 3).map((src) => { - // Wrap image in anchor tag that opens in new tab - return Dom.a( - new DomOptions({ - attributes: { - href: src, - rel: "noopener noreferrer", - target: "_blank", - }, - children: [ - Dom.img( - src, - new DomOptions({ - attributes: { - alt: "House image", - loading: "lazy", - }, - styles: { - borderRadius: "4px", - cursor: "pointer", - flexShrink: "0", - height: "100px", - transition: "opacity 0.2s ease", - }, - }), - ), - ], - styles: { - display: "block", - textDecoration: "none", - }, - }), - ); - }), - styles: { - display: "flex", - gap: "10px", - overflowX: "auto", - paddingBottom: "5px", - }, - }), - ), - ], - styles: { marginBottom: "20px" }, - }), - ); - - frag.appendChild(imgSect); + frag.appendChild(Modal.imageSection(house)); } return frag; } -- cgit v1.2.3-70-g09d2