aboutsummaryrefslogtreecommitdiffstats
path: root/app/main.js
diff options
context:
space:
mode:
authorPetri Hienonen <petri.hienonen@gmail.com>2025-11-14 16:25:45 +0200
committerPetri Hienonen <petri.hienonen@gmail.com>2025-11-14 16:25:45 +0200
commit55085dae685305d24c29b60b1c16fc7dc76831af (patch)
tree4833cb2974bf8922c804bbb463113f0fe568f7a3 /app/main.js
parenta92ad2b817b2c28b26e869897e03c14d30d0f991 (diff)
downloadhousing-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.js497
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();
}