aboutsummaryrefslogtreecommitdiffstats
path: root/app/components.js
diff options
context:
space:
mode:
authorPetri Hienonen <petri.hienonen@gmail.com>2025-11-14 09:24:17 +0200
committerPetri Hienonen <petri.hienonen@gmail.com>2025-11-14 09:24:17 +0200
commit6ca89c37f84c6b1d63c869e6471d3570d51f63be (patch)
tree1e0a5f8ba780499262720061ca0099186bab96f6 /app/components.js
parentc06dccd86e8cd40fb98cbe0b734428d60ab39e32 (diff)
downloadhousing-6ca89c37f84c6b1d63c869e6471d3570d51f63be.tar.zst
Make the sidebar more manageable
Diffstat (limited to '')
-rw-r--r--app/components.js746
1 files changed, 377 insertions, 369 deletions
diff --git a/app/components.js b/app/components.js
index 0995692..cc08fb4 100644
--- a/app/components.js
+++ b/app/components.js
@@ -170,6 +170,312 @@ export class Sidebar {
#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
* @param {() => void} onFilterChange
@@ -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",
}),
@@ -642,6 +645,73 @@ export class Modal {
#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
* @returns {DocumentFragment}
@@ -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;
}