From b2c8108c8cb7c013664b625088ce9bc5ba5329a1 Mon Sep 17 00:00:00 2001 From: Vincent van der Wal Date: Sat, 1 Aug 2026 17:24:00 +0200 Subject: [PATCH] small bug fixes --- messages/de.json | 6 +- messages/en.json | 6 +- messages/es.json | 6 +- messages/fr.json | 6 +- messages/it.json | 6 +- .../components/navigation/weather-nav.svelte | 30 ++++++- src/routes/layout.css | 10 +-- src/routes/weather/options.ts | 34 ++++++++ .../weather/week/[location]/+page.svelte | 79 ++++++++++++++++--- 9 files changed, 161 insertions(+), 22 deletions(-) diff --git a/messages/de.json b/messages/de.json index ac3f4f8..93e31f9 100644 --- a/messages/de.json +++ b/messages/de.json @@ -203,5 +203,9 @@ "var_visibility_short": "Sicht", "var_cape": "CAPE", "var_cape_short": "CAPE", - "var_time": "Zeit" + "var_time": "Zeit", + "no_data_title": "Für dieses Modell gibt es hier keine Daten", + "no_data_body": "Das gewählte Wettermodell deckt {location} nicht ab - regionale Modelle liefern nur Daten in ihrem eigenen Gebiet.", + "no_data_try_city": "Ort auf {city} ändern", + "no_data_best_match": "Zu „Best match“ wechseln" } diff --git a/messages/en.json b/messages/en.json index 9d9bfe3..3743a22 100644 --- a/messages/en.json +++ b/messages/en.json @@ -203,5 +203,9 @@ "var_visibility_short": "Vis", "var_cape": "CAPE", "var_cape_short": "CAPE", - "var_time": "Time" + "var_time": "Time", + "no_data_title": "No forecast data for this model here", + "no_data_body": "The selected weather model doesn't cover {location} - regional models only provide data inside their own area.", + "no_data_try_city": "Change location to {city}", + "no_data_best_match": "Switch to Best match" } diff --git a/messages/es.json b/messages/es.json index 0afb764..dc58924 100644 --- a/messages/es.json +++ b/messages/es.json @@ -203,5 +203,9 @@ "var_visibility_short": "Vis.", "var_cape": "CAPE", "var_cape_short": "CAPE", - "var_time": "Hora" + "var_time": "Hora", + "no_data_title": "Este modelo no tiene datos aquí", + "no_data_body": "El modelo seleccionado no cubre {location}: los modelos regionales solo ofrecen datos dentro de su propia área.", + "no_data_try_city": "Cambiar la ubicación a {city}", + "no_data_best_match": "Cambiar a «Best match»" } diff --git a/messages/fr.json b/messages/fr.json index 5d504a2..613f5a4 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -203,5 +203,9 @@ "var_visibility_short": "Vis.", "var_cape": "CAPE", "var_cape_short": "CAPE", - "var_time": "Heure" + "var_time": "Heure", + "no_data_title": "Ce modèle n'a pas de données ici", + "no_data_body": "Le modèle sélectionné ne couvre pas {location} : les modèles régionaux ne fournissent des données que dans leur propre zone.", + "no_data_try_city": "Changer de lieu pour {city}", + "no_data_best_match": "Passer à « Best match »" } diff --git a/messages/it.json b/messages/it.json index 1498c91..c285940 100644 --- a/messages/it.json +++ b/messages/it.json @@ -203,5 +203,9 @@ "var_visibility_short": "Vis.", "var_cape": "CAPE", "var_cape_short": "CAPE", - "var_time": "Ora" + "var_time": "Ora", + "no_data_title": "Nessun dato per questo modello qui", + "no_data_body": "Il modello selezionato non copre {location}: i modelli regionali forniscono dati solo nella propria area.", + "no_data_try_city": "Cambia località in {city}", + "no_data_best_match": "Passa a «Best match»" } diff --git a/src/lib/components/navigation/weather-nav.svelte b/src/lib/components/navigation/weather-nav.svelte index d09f374..e30633c 100644 --- a/src/lib/components/navigation/weather-nav.svelte +++ b/src/lib/components/navigation/weather-nav.svelte @@ -139,7 +139,35 @@ {/each} - + + {#if onMobileClose} + + {/if} {#if onToggle} diff --git a/src/routes/layout.css b/src/routes/layout.css index 24108f4..bf2d613 100644 --- a/src/routes/layout.css +++ b/src/routes/layout.css @@ -155,13 +155,13 @@ in the cross-fade. Everything else keeps its pixels: the `day-switch` class cancels the root animation, so the strip, header and page chrome do not so much as flicker while the table, summary and charts swap over. */ - .day-region-table { + :root.day-switch .day-region-table { view-transition-name: day-table; } - .day-region-summary { + :root.day-switch .day-region-summary { view-transition-name: day-summary; } - .day-region-charts { + :root.day-switch .day-region-charts { view-transition-name: day-charts; } @@ -169,7 +169,7 @@ captured too: view-transition snapshots all paint in one layer above the page, so without its own group (and a higher z-index in that layer) the fading table and charts would slide over the top of it. */ - .daystrip { + :root.day-switch .daystrip { view-transition-name: daystrip; } ::view-transition-group(daystrip) { @@ -178,7 +178,7 @@ /* Same for the topbar: a captured region's snapshot is painted at its layout position, including the part normally scrolled up behind the chrome. */ - .topbar { + :root.day-switch .topbar { view-transition-name: topbar; } ::view-transition-group(topbar) { diff --git a/src/routes/weather/options.ts b/src/routes/weather/options.ts index 0197874..b384cfe 100644 --- a/src/routes/weather/options.ts +++ b/src/routes/weather/options.ts @@ -479,3 +479,37 @@ export const ensembleModels: WeatherModel[] = ensembleModelGroups.flatMap((group export const findEnsembleModel = (value: string): WeatherModel | undefined => ensembleModels.find((model) => model.value === value); + +/** + * A city inside each regional model's domain, used to offer a way out when the + * chosen model has no data for the current place. Keyed by the model-id prefix + * so new variants of a domain are covered without another entry. + */ +const IN_DOMAIN_CITY: [string, { slug: string; label: string }][] = [ + ['meteoswiss', { slug: 'zurich', label: 'Zürich' }], + ['italia_meteo', { slug: 'rome', label: 'Rome' }], + ['icon', { slug: 'berlin', label: 'Berlin' }], + ['dwd', { slug: 'berlin', label: 'Berlin' }], + ['meteofrance', { slug: 'paris', label: 'Paris' }], + ['arpege', { slug: 'paris', label: 'Paris' }], + ['arome', { slug: 'paris', label: 'Paris' }], + ['ukmo', { slug: 'london', label: 'London' }], + ['knmi', { slug: 'amsterdam', label: 'Amsterdam' }], + ['dmi', { slug: 'copenhagen', label: 'Copenhagen' }], + ['metno', { slug: 'oslo', label: 'Oslo' }], + ['ncep', { slug: 'new-york', label: 'New York' }], + ['gfs', { slug: 'new-york', label: 'New York' }], + ['hrrr', { slug: 'new-york', label: 'New York' }], + ['nbm', { slug: 'new-york', label: 'New York' }], + ['gem', { slug: 'toronto', label: 'Toronto' }], + ['jma', { slug: 'tokyo', label: 'Tokyo' }], + ['kma', { slug: 'seoul', label: 'Seoul' }], + ['cma', { slug: 'beijing', label: 'Beijing' }], + ['bom', { slug: 'sydney', label: 'Sydney' }] +]; + +/** Where this model definitely has data, or null for a global model. */ +export function inDomainCity(model: string): { slug: string; label: string } | null { + const hit = IN_DOMAIN_CITY.find(([prefix]) => model.startsWith(prefix)); + return hit ? hit[1] : null; +} diff --git a/src/routes/weather/week/[location]/+page.svelte b/src/routes/weather/week/[location]/+page.svelte index c1a4145..e48c041 100644 --- a/src/routes/weather/week/[location]/+page.svelte +++ b/src/routes/weather/week/[location]/+page.svelte @@ -22,6 +22,8 @@ import { ChartContainer } from '$lib/components/charts'; + import { href } from '$lib/i18n'; + import * as m from '$lib/paraglide/messages'; import { type FriendlyWeatherError, type WeekForecastResult, @@ -30,7 +32,7 @@ } from '$lib/services/weather'; import { useHeroActions } from '../../hero.svelte'; - import { defaultParameters } from '../../options'; + import { defaultParameters, inDomainCity } from '../../options'; import { computeDayNightWeatherCodes } from '../../utils/weather-codes'; import DailyStripSticky from './DailyStripSticky.svelte'; import DaySummary from './DaySummary.svelte'; @@ -114,6 +116,14 @@ // for the same place once their range is exhausted. let locationRoute = $derived(buildLocationRoute(location)); + // When a regional model has no data here, point at a place it does cover + // rather than only offering to abandon the model. + let suggestedCity = $derived.by(() => { + const city = inDomainCity(params.models?.[0] ?? ''); + // pointless to offer the place we are already on + return city && city.slug !== locationRoute ? city : null; + }); + let mounted = $state(false); let loading = $state(true); let loadError = $state(null); @@ -157,6 +167,36 @@ return fd ? formatZoned(selectedDay, fd.timezone, 'yyyy-MM-dd') : ''; }); + // A model swap can leave the open day outside what the new model covers (a + // short-range model after a 15-day one, say). Rather than showing an empty + // day, fall back to the nearest day that does have data - searching forward + // first, then back. + $effect(() => { + const fd = fetchedDaily; + if (!fd || wantedDay) return; + const days = fd.dailyDates; + if (days.length === 0) return; + + const covered = (i: number) => { + const max = fd.daily.temperature_2m_max[i]; + return max != null && Number.isFinite(max); + }; + const current = days.findIndex( + (d) => formatZoned(d, fd.timezone, 'yyyy-MM-dd') === selectedDayKey + ); + if (current >= 0 && covered(current)) return; + + const from = current >= 0 ? current : 0; + for (let step = 0; step < days.length; step++) { + for (const i of [from + step, from - step]) { + if (i >= 0 && i < days.length && covered(i)) { + selectedDay.setTime(days[i].getTime()); + return; + } + } + } + }); + // Charts intentionally keep their current range: they show the full week // unless the user narrows it via the range presets or Ctrl+scroll. const switchDay = (date: Date) => { @@ -324,12 +364,20 @@ > Try again + {#if suggestedCity} + + {m.no_data_try_city({ city: suggestedCity.label })} + + {/if} {#if params.models?.[0] !== 'best_match'} {/if} @@ -362,18 +410,27 @@ />
-

No forecast data for this model here

+

{m.no_data_title()}

- The selected weather model doesn't cover {location.name} — regional models only provide data - inside their own area. + {m.no_data_body({ location: location.name ?? '' })}

- +
+ {#if suggestedCity} + + {m.no_data_try_city({ city: suggestedCity.label })} + + {/if} + +
{/if}