From 6d81af8df57a5cfa4811f96918f9c184962c778b Mon Sep 17 00:00:00 2001 From: Vincent van der Wal Date: Sun, 19 Jul 2026 14:57:26 +0200 Subject: [PATCH] refactor --- package-lock.json | 84 ---------- package.json | 11 +- src/demo.spec.ts | 7 - src/lib/components/charts/EChart.svelte | 10 +- src/lib/components/charts/echarts.ts | 34 ++++ .../location/location-search.svelte | 10 +- src/lib/services/index.ts | 27 ---- src/lib/utils/location.ts | 152 ++++++++++-------- src/lib/utils/meteo.ts | 0 src/routes/+layout.svelte | 9 +- src/routes/weather/+layout.ts | 13 -- src/routes/weather/14-day/+layout.ts | 13 -- src/routes/weather/14-day/+page.svelte | 23 +++ src/routes/weather/14-day/+page.ts | 15 -- .../weather/14-day/[location]/+page.svelte | 90 +++++++---- src/routes/weather/compare/+page.svelte | 23 +++ src/routes/weather/compare/+page.ts | 15 -- .../weather/compare/[location]/+page.svelte | 93 +++++++---- .../weather/locations/city-names10.json | 12 -- src/routes/weather/maps/+page.svelte | 20 +-- src/routes/weather/utils/colors.ts | 4 +- src/routes/weather/week/+page.svelte | 23 +++ src/routes/weather/week/+page.ts | 15 -- .../weather/week/[location]/+page.svelte | 96 ++++++----- .../week/[location]/MeteogramCharts.svelte | 18 +-- .../weather/week/[location]/SunInfo.svelte | 57 ------- 26 files changed, 408 insertions(+), 466 deletions(-) delete mode 100644 src/demo.spec.ts create mode 100644 src/lib/components/charts/echarts.ts delete mode 100644 src/lib/services/index.ts delete mode 100644 src/lib/utils/meteo.ts delete mode 100644 src/routes/weather/+layout.ts delete mode 100644 src/routes/weather/14-day/+layout.ts create mode 100644 src/routes/weather/14-day/+page.svelte delete mode 100644 src/routes/weather/14-day/+page.ts create mode 100644 src/routes/weather/compare/+page.svelte delete mode 100644 src/routes/weather/compare/+page.ts delete mode 100644 src/routes/weather/locations/city-names10.json create mode 100644 src/routes/weather/week/+page.svelte delete mode 100644 src/routes/weather/week/+page.ts delete mode 100644 src/routes/weather/week/[location]/SunInfo.svelte diff --git a/package-lock.json b/package-lock.json index 55f5f64..bd54024 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,6 @@ "eslint-config-prettier": "^10.1.8", "eslint-plugin-svelte": "^3.21.0", "globals": "^17.7.0", - "mode-watcher": "^1.1.0", "openmeteo": "^1.2.3", "playwright": "^1.61.1", "prettier": "^3.9.5", @@ -3138,73 +3137,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/mode-watcher": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mode-watcher/-/mode-watcher-1.1.0.tgz", - "integrity": "sha512-mUT9RRGPDYenk59qJauN1rhsIMKBmWA3xMF+uRwE8MW/tjhaDSCCARqkSuDTq8vr4/2KcAxIGVjACxTjdk5C3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "runed": "^0.25.0", - "svelte-toolbelt": "^0.7.1" - }, - "peerDependencies": { - "svelte": "^5.27.0" - } - }, - "node_modules/mode-watcher/node_modules/runed": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/runed/-/runed-0.25.0.tgz", - "integrity": "sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg==", - "dev": true, - "funding": [ - "https://github.com/sponsors/huntabyte", - "https://github.com/sponsors/tglide" - ], - "dependencies": { - "esm-env": "^1.0.0" - }, - "peerDependencies": { - "svelte": "^5.7.0" - } - }, - "node_modules/mode-watcher/node_modules/svelte-toolbelt": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.7.1.tgz", - "integrity": "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==", - "dev": true, - "funding": [ - "https://github.com/sponsors/huntabyte" - ], - "dependencies": { - "clsx": "^2.1.1", - "runed": "^0.23.2", - "style-to-object": "^1.0.8" - }, - "engines": { - "node": ">=18", - "pnpm": ">=8.7.0" - }, - "peerDependencies": { - "svelte": "^5.0.0" - } - }, - "node_modules/mode-watcher/node_modules/svelte-toolbelt/node_modules/runed": { - "version": "0.23.4", - "resolved": "https://registry.npmjs.org/runed/-/runed-0.23.4.tgz", - "integrity": "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==", - "dev": true, - "funding": [ - "https://github.com/sponsors/huntabyte", - "https://github.com/sponsors/tglide" - ], - "dependencies": { - "esm-env": "^1.0.0" - }, - "peerDependencies": { - "svelte": "^5.7.0" - } - }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -4559,22 +4491,6 @@ } } }, - "node_modules/yaml": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", - "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", - "extraneous": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index b2b930d..c3549b4 100644 --- a/package.json +++ b/package.json @@ -30,10 +30,14 @@ "@vitest/browser-playwright": "^4.1.10", "bits-ui": "^2.18.1", "clsx": "^2.1.1", + "date-fns": "^4.4.0", + "date-fns-tz": "^3.2.0", + "echarts": "^6.1.0", "eslint": "^10.7.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-svelte": "^3.21.0", "globals": "^17.7.0", + "openmeteo": "^1.2.3", "playwright": "^1.61.1", "prettier": "^3.9.5", "prettier-plugin-svelte": "^3.5.2", @@ -49,11 +53,6 @@ "typescript-eslint": "^8.64.0", "vite": "^8.1.5", "vitest": "^4.1.10", - "vitest-browser-svelte": "^3.0.0", - "date-fns": "^4.4.0", - "date-fns-tz": "^3.2.0", - "echarts": "^6.1.0", - "mode-watcher": "^1.1.0", - "openmeteo": "^1.2.3" + "vitest-browser-svelte": "^3.0.0" } } diff --git a/src/demo.spec.ts b/src/demo.spec.ts deleted file mode 100644 index 5d40967..0000000 --- a/src/demo.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -describe('sum test', () => { - it('adds 1 + 2 to equal 3', () => { - expect(1 + 2).toBe(3); - }); -}); diff --git a/src/lib/components/charts/EChart.svelte b/src/lib/components/charts/EChart.svelte index e56575c..22652b0 100644 --- a/src/lib/components/charts/EChart.svelte +++ b/src/lib/components/charts/EChart.svelte @@ -16,7 +16,9 @@ diff --git a/src/routes/weather/14-day/+page.ts b/src/routes/weather/14-day/+page.ts deleted file mode 100644 index a860658..0000000 --- a/src/routes/weather/14-day/+page.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { get } from 'svelte/store'; - -import { redirect } from '@sveltejs/kit'; - -import { storedLocation } from '$lib/stores/settings'; - -import { buildLocationRoute } from '$lib/utils/location'; - -import type { PageLoad } from './$types'; - -export const load = (async () => { - const location = get(storedLocation); - const locationRoute = buildLocationRoute(location); - throw redirect(303, '/weather/14-day/' + locationRoute); -}) satisfies PageLoad; diff --git a/src/routes/weather/14-day/[location]/+page.svelte b/src/routes/weather/14-day/[location]/+page.svelte index 0cb98ae..9f26483 100644 --- a/src/routes/weather/14-day/[location]/+page.svelte +++ b/src/routes/weather/14-day/[location]/+page.svelte @@ -1,7 +1,7 @@ diff --git a/src/routes/weather/compare/+page.ts b/src/routes/weather/compare/+page.ts deleted file mode 100644 index e41391f..0000000 --- a/src/routes/weather/compare/+page.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { get } from 'svelte/store'; - -import { redirect } from '@sveltejs/kit'; - -import { storedLocation } from '$lib/stores/settings'; - -import { buildLocationRoute } from '$lib/utils/location'; - -import type { PageLoad } from './$types'; - -export const load = (async () => { - const location = get(storedLocation); - const locationRoute = buildLocationRoute(location); - throw redirect(303, '/weather/compare/' + locationRoute); -}) satisfies PageLoad; diff --git a/src/routes/weather/compare/[location]/+page.svelte b/src/routes/weather/compare/[location]/+page.svelte index b2675fe..7a29abe 100644 --- a/src/routes/weather/compare/[location]/+page.svelte +++ b/src/routes/weather/compare/[location]/+page.svelte @@ -2,7 +2,7 @@ import { onDestroy, onMount } from 'svelte'; import { fade } from 'svelte/transition'; - import { type GeoLocation, storedLocation } from '$lib/stores/settings'; + import { storedLocation } from '$lib/stores/settings'; import { buildAverageSeries, @@ -31,6 +31,7 @@ import { defaultParameters } from '../../options'; import ModelPictogramTimeline from './ModelPictogramTimeline.svelte'; + import type { PageData } from './$types'; import type * as echarts from 'echarts'; const models = [modelsFlat]; @@ -46,10 +47,17 @@ let chartOptions: Array> = $state([]); let mounted = $state(false); let loading = $state(true); + let loadError = $state(null); + let requestVersion = 0; - let location = $state($storedLocation); - storedLocation.subscribe((value) => { - location = value; + let { data }: { data: PageData } = $props(); + + // the URL is the source of truth: location comes from the load function, + // which is also correct on hydrated prerendered pages. The persisted store + // only mirrors it so the header and bare /weather/* redirects follow along. + let location = $derived(data.location); + $effect(() => { + storedLocation.set(data.location); }); let params = $state({ @@ -104,6 +112,11 @@ chartInstances = [...chartInstances, chart]; } + // chart components persist across refetches (options update in place), so + // the registry is never reset; only instances disposed because the chart + // count shrank are filtered out + let liveCharts = $derived(chartInstances.filter((chart) => !chart.isDisposed())); + // ─── Data Fetching (only when params.hourly or params.models change) ─────── $effect(() => { @@ -114,36 +127,41 @@ const loc = location; - const loadData = async () => { - loading = true; - chartInstances = []; - chartComponents = []; + // versioned so a slow stale response can never overwrite a newer one + const version = ++requestVersion; + loading = true; + loadError = null; - const result: ModelCompareResult = await fetchModelComparison({ - latitude: loc.latitude!, - longitude: loc.longitude!, - hourlyVariables: [...new Set([...hourlyVars, 'weather_code'])], - models: modelList, - temperature_unit: params.temperature_unit as 'celsius' | 'fahrenheit', - wind_speed_unit: params.wind_speed_unit as 'kmh' | 'ms' | 'mph' | 'kn', - precipitation_unit: params.precipitation_unit as 'mm' | 'inch', - timezone: loc.timezone + fetchModelComparison({ + latitude: loc.latitude!, + longitude: loc.longitude!, + hourlyVariables: [...new Set([...hourlyVars, 'weather_code'])], + models: modelList, + temperature_unit: params.temperature_unit as 'celsius' | 'fahrenheit', + wind_speed_unit: params.wind_speed_unit as 'kmh' | 'ms' | 'mph' | 'kn', + precipitation_unit: params.precipitation_unit as 'mm' | 'inch', + timezone: loc.timezone + }) + .then((result: ModelCompareResult) => { + if (version !== requestVersion) return; + + fetchedData = { + hourly: result.hourlyFlat, + hourly_units: result.hourlyUnitsFlat, + timezone: result.timezone, + markAreas: result.markAreas, + timestamps: result.timestamps, + sunrise: result.sunrise, + sunset: result.sunset + }; + + loading = false; + }) + .catch((err: unknown) => { + if (version !== requestVersion) return; + loadError = err instanceof Error ? err.message : String(err); + loading = false; }); - - fetchedData = { - hourly: result.hourlyFlat, - hourly_units: result.hourlyUnitsFlat, - timezone: result.timezone, - markAreas: result.markAreas, - timestamps: result.timestamps, - sunrise: result.sunrise, - sunset: result.sunset - }; - - loading = false; - }; - - loadData(); }); // ─── Chart Option Building (runs when fetchedData OR display toggles change) ─ @@ -228,10 +246,19 @@ +{#if loadError} +
+ Failed to load weather data: {loadError} +
+{/if} + {#each chartOptions as option, i (i)}
- + {#snippet controls()}
diff --git a/src/routes/weather/locations/city-names10.json b/src/routes/weather/locations/city-names10.json deleted file mode 100644 index 3b3cff6..0000000 --- a/src/routes/weather/locations/city-names10.json +++ /dev/null @@ -1,12 +0,0 @@ -[ - "kinshasa", - "shenzhen", - "shanghai", - "guangzhou", - "chengdu", - "beijing", - "mumbai", - "lagos", - "lahore", - "istanbul" -] diff --git a/src/routes/weather/maps/+page.svelte b/src/routes/weather/maps/+page.svelte index b1db662..c0d967e 100644 --- a/src/routes/weather/maps/+page.svelte +++ b/src/routes/weather/maps/+page.svelte @@ -1,7 +1,12 @@ @@ -10,18 +15,15 @@ - -
+ +
diff --git a/src/routes/weather/utils/colors.ts b/src/routes/weather/utils/colors.ts index fa33bb1..08c264c 100644 --- a/src/routes/weather/utils/colors.ts +++ b/src/routes/weather/utils/colors.ts @@ -45,7 +45,9 @@ export const getColor = (temperature: number, unit = 'celsius'): string => { } else if (temperature >= 60) { index = colorScaleHex.length - 1; } else { - index = Math.round(temperature) + 45; + // clamp: the scale has exactly 100 entries (-45..54), temperatures in + // [55, 60) would otherwise index past the end + index = Math.min(colorScaleHex.length - 1, Math.max(0, Math.round(temperature) + 45)); } return colorScaleHex[index]; diff --git a/src/routes/weather/week/+page.svelte b/src/routes/weather/week/+page.svelte new file mode 100644 index 0000000..165b826 --- /dev/null +++ b/src/routes/weather/week/+page.svelte @@ -0,0 +1,23 @@ + diff --git a/src/routes/weather/week/+page.ts b/src/routes/weather/week/+page.ts deleted file mode 100644 index 7a91044..0000000 --- a/src/routes/weather/week/+page.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { get } from 'svelte/store'; - -import { redirect } from '@sveltejs/kit'; - -import { storedLocation } from '$lib/stores/settings'; - -import { buildLocationRoute } from '$lib/utils/location'; - -import type { PageLoad } from './$types'; - -export const load = (async () => { - const location = get(storedLocation); - const locationRoute = buildLocationRoute(location); - throw redirect(303, '/weather/week/' + locationRoute); -}) satisfies PageLoad; diff --git a/src/routes/weather/week/[location]/+page.svelte b/src/routes/weather/week/[location]/+page.svelte index e6e6c14..9ea855f 100644 --- a/src/routes/weather/week/[location]/+page.svelte +++ b/src/routes/weather/week/[location]/+page.svelte @@ -12,23 +12,28 @@ import MeteogramCharts from './MeteogramCharts.svelte'; import ModelSelector from './ModelSelector.svelte'; - import type { GeoLocation } from '$lib/stores/settings'; + import type { PageData } from './$types'; import type { FetchedDaily, FetchedHourly } from './types'; + let { data }: { data: PageData } = $props(); + let params = $state({ - latitude: [$storedLocation.latitude], - longitude: [$storedLocation.longitude], models: ['best_match'], ...defaultParameters }); - let location = $state($storedLocation); - storedLocation.subscribe((value) => { - location = value; + // the URL is the source of truth: location comes from the load function, + // which is also correct on hydrated prerendered pages. The persisted store + // only mirrors it so the header and bare /weather/* redirects follow along. + let location = $derived(data.location); + $effect(() => { + storedLocation.set(data.location); }); let mounted = $state(false); let loading = $state(true); + let loadError = $state(null); + let requestVersion = 0; const selectedDay = new SvelteDate(); @@ -52,40 +57,47 @@ if (!mounted || !loc || !modelList?.length) return; - const loadData = async () => { - loading = true; + // versioned so a slow stale response can never overwrite a newer one + const version = ++requestVersion; + loading = true; + loadError = null; - const result: WeekForecastResult = await fetchWeekForecast({ - latitude: loc.latitude!, - longitude: loc.longitude!, - model: modelList[0], - temperature_unit: params.temperature_unit as 'celsius' | 'fahrenheit', - wind_speed_unit: params.wind_speed_unit as 'kmh' | 'ms' | 'mph' | 'kn', - precipitation_unit: params.precipitation_unit as 'mm' | 'inch', - forecast_days: 7, - past_days: 0, - timezone: loc.timezone + fetchWeekForecast({ + latitude: loc.latitude!, + longitude: loc.longitude!, + model: modelList[0], + temperature_unit: params.temperature_unit as 'celsius' | 'fahrenheit', + wind_speed_unit: params.wind_speed_unit as 'kmh' | 'ms' | 'mph' | 'kn', + precipitation_unit: params.precipitation_unit as 'mm' | 'inch', + forecast_days: 7, + past_days: 0, + timezone: loc.timezone + }) + .then((result: WeekForecastResult) => { + if (version !== requestVersion) return; + + fetchedHourly = { + hourly: result.hourly, + utc_offset_seconds: result.utcOffsetSeconds, + timezone: result.timezone, + timestamps: result.hourlyTimestamps, + hourlyDates: result.hourlyDates, + markAreas: result.markAreas + }; + + fetchedDaily = { + daily: result.daily, + timezone: result.timezone, + dailyDates: result.dailyDates + }; + + loading = false; + }) + .catch((err: unknown) => { + if (version !== requestVersion) return; + loadError = err instanceof Error ? err.message : String(err); + loading = false; }); - - fetchedHourly = { - hourly: result.hourly, - utc_offset_seconds: result.utcOffsetSeconds, - timezone: result.timezone, - timestamps: result.hourlyTimestamps, - hourlyDates: result.hourlyDates, - markAreas: result.markAreas - }; - - fetchedDaily = { - daily: result.daily, - timezone: result.timezone, - dailyDates: result.dailyDates - }; - - loading = false; - }; - - loadData(); }); @@ -97,6 +109,14 @@
+ {#if loadError} +
+ Failed to load weather data: {loadError} +
+ {/if} + {#if fetchedHourly && fetchedDaily} diff --git a/src/routes/weather/week/[location]/MeteogramCharts.svelte b/src/routes/weather/week/[location]/MeteogramCharts.svelte index bbcb480..58c5850 100644 --- a/src/routes/weather/week/[location]/MeteogramCharts.svelte +++ b/src/routes/weather/week/[location]/MeteogramCharts.svelte @@ -1,12 +1,11 @@ - -{#if daily && sunrise && sunset} -
-
- - - - {formatZoned(sunrise, daily.timezone, 'HH:mm')} -
-
- - - - {formatZoned(sunset, daily.timezone, 'HH:mm')} -
-
-{/if} - -