more translations
This commit is contained in:
+16
-3
@@ -1,3 +1,16 @@
|
||||
<svelte:head>
|
||||
<title>Drizzli</title>
|
||||
</svelte:head>
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import { localizeHref } from '$lib/paraglide/runtime';
|
||||
|
||||
// Deliberately a client-side redirect, not one from `load`: this page is
|
||||
// prerendered, so a redirect resolved at build time would bake in the base
|
||||
// locale and every visitor would land on English. Deciding it here lets the
|
||||
// locale strategy read the URL, the cookie and finally the browser's own
|
||||
// languages before choosing.
|
||||
onMount(() => {
|
||||
goto(localizeHref('/weather/week/'), { replaceState: true });
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
import { localizeHref } from '$lib/paraglide/runtime';
|
||||
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async () => {
|
||||
// bare `/` lands in the locale the strategy resolved (URL, cookie, then the
|
||||
// browser's own languages), not always English
|
||||
throw redirect(303, localizeHref('/weather/week/'));
|
||||
}) satisfies PageLoad;
|
||||
@@ -1,41 +1,11 @@
|
||||
<script lang="ts">
|
||||
import ProsePage from '$lib/components/prose-page.svelte';
|
||||
import LocalizedContent from '$lib/components/localized-content.svelte';
|
||||
|
||||
import de from './content/de.svelte';
|
||||
import en from './content/en.svelte';
|
||||
import es from './content/es.svelte';
|
||||
import fr from './content/fr.svelte';
|
||||
import it from './content/it.svelte';
|
||||
</script>
|
||||
|
||||
<ProsePage title="About Drizz.li" subtitle="A small, open-source weather site.">
|
||||
<p>
|
||||
Drizz.li is an independent weather site. It shows multi-model forecasts, model comparisons,
|
||||
14-day outlooks, weather maps and historical weather - without accounts, ads or trackers.
|
||||
</p>
|
||||
|
||||
<h2>How it works</h2>
|
||||
<p>
|
||||
All weather data comes from the excellent open-data APIs of
|
||||
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>, which aggregates
|
||||
national weather services (DWD, NOAA, Météo-France, MeteoSwiss and more). Drizz.li is a static
|
||||
site: forecasts are fetched directly by your browser and rendered on your device.
|
||||
</p>
|
||||
<p>
|
||||
The site is open source. Anyone can read the code, report issues or contribute improvements.
|
||||
</p>
|
||||
|
||||
<h2>Supporting the project</h2>
|
||||
<p>
|
||||
Drizz.li is free to use. If you find it useful, you can support it with a small monthly
|
||||
contribution (from €3 / month) - it helps cover the domain, hosting and development time. As a
|
||||
thank-you, supporters unlock the extras: historical weather with climate-normal comparisons, and
|
||||
new supporter features as they land.
|
||||
</p>
|
||||
<p>
|
||||
Contributions are handled by a simple bank transfer - no payment processor, no stored card
|
||||
details. The details of how that works are in the
|
||||
<a href="/legal/terms/">supporter terms</a> and the
|
||||
<a href="/legal/privacy/">privacy policy</a>.
|
||||
</p>
|
||||
|
||||
<h2>Contact</h2>
|
||||
<p>
|
||||
Questions, feedback or bug reports:
|
||||
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
|
||||
</p>
|
||||
</ProsePage>
|
||||
<LocalizedContent variants={{ en, de, es, fr, it }} />
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
import ProsePage from '$lib/components/prose-page.svelte';
|
||||
|
||||
import { href } from '$lib/i18n';
|
||||
</script>
|
||||
|
||||
<ProsePage title="Über Drizz.li" subtitle="Eine kleine, quelloffene Wetterseite.">
|
||||
<p>
|
||||
Drizz.li ist eine unabhängige Wetterseite. Sie zeigt Vorhersagen mehrerer Modelle,
|
||||
Modellvergleiche, 14-Tage-Aussichten, Wetterkarten und historisches Wetter - ohne Konten,
|
||||
Werbung oder Tracker.
|
||||
</p>
|
||||
|
||||
<h2>So funktioniert es</h2>
|
||||
<p>
|
||||
Alle Wetterdaten stammen aus den hervorragenden Open-Data-Schnittstellen von
|
||||
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>, die nationale
|
||||
Wetterdienste bündeln (DWD, NOAA, Météo-France, MeteoSchweiz und weitere). Drizz.li ist eine
|
||||
statische Seite: Die Vorhersagen werden direkt von Ihrem Browser abgerufen und auf Ihrem Gerät
|
||||
dargestellt.
|
||||
</p>
|
||||
<p>
|
||||
Die Seite ist quelloffen. Jede und jeder kann den Code lesen, Fehler melden oder Verbesserungen
|
||||
beitragen.
|
||||
</p>
|
||||
|
||||
<h2>Das Projekt unterstützen</h2>
|
||||
<p>
|
||||
Drizz.li ist kostenlos nutzbar. Wenn es Ihnen nützt, können Sie es mit einem kleinen monatlichen
|
||||
Beitrag unterstützen (ab 3 € / Monat) - das hilft, Domain, Hosting und Entwicklungszeit zu
|
||||
decken. Als Dankeschön schalten Unterstützerinnen und Unterstützer die Extras frei: historisches
|
||||
Wetter mit Vergleich zu den Klimanormalen sowie neue Funktionen, sobald sie erscheinen.
|
||||
</p>
|
||||
<p>
|
||||
Beiträge laufen über eine einfache Banküberweisung - kein Zahlungsdienstleister, keine
|
||||
gespeicherten Kartendaten. Wie das genau abläuft, steht in den
|
||||
<a href={href('/legal/terms')}>Unterstützerbedingungen</a> und in der
|
||||
<a href={href('/legal/privacy')}>Datenschutzerklärung</a>.
|
||||
</p>
|
||||
|
||||
<h2>Kontakt</h2>
|
||||
<p>
|
||||
Fragen, Rückmeldungen oder Fehlerberichte:
|
||||
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
|
||||
</p>
|
||||
</ProsePage>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import ProsePage from '$lib/components/prose-page.svelte';
|
||||
|
||||
import { href } from '$lib/i18n';
|
||||
</script>
|
||||
|
||||
<ProsePage title="About Drizz.li" subtitle="A small, open-source weather site.">
|
||||
<p>
|
||||
Drizz.li is an independent weather site. It shows multi-model forecasts, model comparisons,
|
||||
14-day outlooks, weather maps and historical weather - without accounts, ads or trackers.
|
||||
</p>
|
||||
|
||||
<h2>How it works</h2>
|
||||
<p>
|
||||
All weather data comes from the excellent open-data APIs of
|
||||
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>, which aggregates
|
||||
national weather services (DWD, NOAA, Météo-France, MeteoSwiss and more). Drizz.li is a static
|
||||
site: forecasts are fetched directly by your browser and rendered on your device.
|
||||
</p>
|
||||
<p>
|
||||
The site is open source. Anyone can read the code, report issues or contribute improvements.
|
||||
</p>
|
||||
|
||||
<h2>Supporting the project</h2>
|
||||
<p>
|
||||
Drizz.li is free to use. If you find it useful, you can support it with a small monthly
|
||||
contribution (from €3 / month) - it helps cover the domain, hosting and development time. As a
|
||||
thank-you, supporters unlock the extras: historical weather with climate-normal comparisons, and
|
||||
new supporter features as they land.
|
||||
</p>
|
||||
<p>
|
||||
Contributions are handled by a simple bank transfer - no payment processor, no stored card
|
||||
details. The details of how that works are in the
|
||||
<a href={href('/legal/terms')}>supporter terms</a> and the
|
||||
<a href={href('/legal/privacy')}>privacy policy</a>.
|
||||
</p>
|
||||
|
||||
<h2>Contact</h2>
|
||||
<p>
|
||||
Questions, feedback or bug reports:
|
||||
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
|
||||
</p>
|
||||
</ProsePage>
|
||||
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
import ProsePage from '$lib/components/prose-page.svelte';
|
||||
|
||||
import { href } from '$lib/i18n';
|
||||
</script>
|
||||
|
||||
<ProsePage title="Sobre Drizz.li" subtitle="Un pequeño sitio meteorológico de código abierto.">
|
||||
<p>
|
||||
Drizz.li es un sitio meteorológico independiente. Muestra pronósticos de varios modelos,
|
||||
comparaciones entre modelos, previsiones a 14 días, mapas meteorológicos y datos históricos, sin
|
||||
cuentas, sin anuncios y sin rastreadores.
|
||||
</p>
|
||||
|
||||
<h2>Cómo funciona</h2>
|
||||
<p>
|
||||
Todos los datos proceden de las excelentes API de datos abiertos de
|
||||
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>, que agrega los
|
||||
servicios meteorológicos nacionales (DWD, NOAA, Météo-France, MeteoSwiss y más). Drizz.li es un
|
||||
sitio estático: tu navegador descarga los pronósticos directamente y los representa en tu
|
||||
dispositivo.
|
||||
</p>
|
||||
<p>
|
||||
El sitio es de código abierto. Cualquiera puede leer el código, informar de errores o contribuir
|
||||
con mejoras.
|
||||
</p>
|
||||
|
||||
<h2>Apoyar el proyecto</h2>
|
||||
<p>
|
||||
Drizz.li es gratuito. Si te resulta útil, puedes apoyarlo con una pequeña aportación mensual
|
||||
(desde 3 € al mes): ayuda a cubrir el dominio, el alojamiento y el tiempo de desarrollo. Como
|
||||
agradecimiento, quienes colaboran desbloquean los extras: clima histórico con comparación frente
|
||||
a las normales climáticas y las nuevas funciones que vayan llegando.
|
||||
</p>
|
||||
<p>
|
||||
Las aportaciones se gestionan mediante una simple transferencia bancaria: sin pasarela de pago y
|
||||
sin guardar datos de tarjetas. Los detalles están en las
|
||||
<a href={href('/legal/terms')}>condiciones para colaboradores</a> y en la
|
||||
<a href={href('/legal/privacy')}>política de privacidad</a>.
|
||||
</p>
|
||||
|
||||
<h2>Contacto</h2>
|
||||
<p>
|
||||
Dudas, comentarios o informes de errores:
|
||||
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
|
||||
</p>
|
||||
</ProsePage>
|
||||
@@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import ProsePage from '$lib/components/prose-page.svelte';
|
||||
|
||||
import { href } from '$lib/i18n';
|
||||
</script>
|
||||
|
||||
<ProsePage title="À propos de Drizz.li" subtitle="Un petit site météo open source.">
|
||||
<p>
|
||||
Drizz.li est un site météo indépendant. Il propose des prévisions multi-modèles, des
|
||||
comparaisons de modèles, des tendances à 14 jours, des cartes météo et des données historiques,
|
||||
sans compte, sans publicité et sans traceurs.
|
||||
</p>
|
||||
|
||||
<h2>Comment ça marche</h2>
|
||||
<p>
|
||||
Toutes les données proviennent des excellentes API ouvertes d'<a
|
||||
href="https://open-meteo.com"
|
||||
target="_blank"
|
||||
rel="noopener">Open-Meteo</a
|
||||
>, qui agrègent les services météorologiques nationaux (DWD, NOAA, Météo-France, MeteoSwiss et
|
||||
d'autres). Drizz.li est un site statique : les prévisions sont récupérées directement par votre
|
||||
navigateur et affichées sur votre appareil.
|
||||
</p>
|
||||
<p>
|
||||
Le site est open source. Chacun peut lire le code, signaler un problème ou proposer des
|
||||
améliorations.
|
||||
</p>
|
||||
|
||||
<h2>Soutenir le projet</h2>
|
||||
<p>
|
||||
Drizz.li est gratuit. S'il vous est utile, vous pouvez le soutenir par une petite contribution
|
||||
mensuelle (à partir de 3 € par mois) : elle aide à couvrir le nom de domaine, l'hébergement et
|
||||
le temps de développement. En remerciement, les contributeurs débloquent les bonus : la météo
|
||||
historique avec comparaison aux normales climatiques, ainsi que les nouvelles fonctions à venir.
|
||||
</p>
|
||||
<p>
|
||||
Les contributions passent par un simple virement bancaire : aucun prestataire de paiement,
|
||||
aucune donnée de carte conservée. Le détail figure dans les
|
||||
<a href={href('/legal/terms')}>conditions contributeur</a> et dans la
|
||||
<a href={href('/legal/privacy')}>politique de confidentialité</a>.
|
||||
</p>
|
||||
|
||||
<h2>Contact</h2>
|
||||
<p>
|
||||
Questions, retours ou signalements de bugs :
|
||||
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
|
||||
</p>
|
||||
</ProsePage>
|
||||
@@ -0,0 +1,47 @@
|
||||
<script lang="ts">
|
||||
import ProsePage from '$lib/components/prose-page.svelte';
|
||||
|
||||
import { href } from '$lib/i18n';
|
||||
</script>
|
||||
|
||||
<ProsePage title="Informazioni su Drizz.li" subtitle="Un piccolo sito meteo open source.">
|
||||
<p>
|
||||
Drizz.li è un sito meteo indipendente. Offre previsioni multi-modello, confronti tra modelli,
|
||||
tendenze a 14 giorni, mappe meteo e dati storici, senza account, senza pubblicità e senza
|
||||
tracciatori.
|
||||
</p>
|
||||
|
||||
<h2>Come funziona</h2>
|
||||
<p>
|
||||
Tutti i dati provengono dalle ottime API aperte di
|
||||
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>, che aggregano i
|
||||
servizi meteorologici nazionali (DWD, NOAA, Météo-France, MeteoSwiss e altri). Drizz.li è un
|
||||
sito statico: le previsioni vengono scaricate direttamente dal tuo browser e disegnate sul tuo
|
||||
dispositivo.
|
||||
</p>
|
||||
<p>
|
||||
Il sito è open source. Chiunque può leggere il codice, segnalare problemi o contribuire con
|
||||
miglioramenti.
|
||||
</p>
|
||||
|
||||
<h2>Sostenere il progetto</h2>
|
||||
<p>
|
||||
Drizz.li è gratuito. Se lo trovi utile, puoi sostenerlo con un piccolo contributo mensile (da 3
|
||||
€ al mese): aiuta a coprire dominio, hosting e tempo di sviluppo. Come ringraziamento, chi
|
||||
sostiene il progetto sblocca gli extra: meteo storico con confronto rispetto alle normali
|
||||
climatiche e le nuove funzioni man mano che arrivano.
|
||||
</p>
|
||||
<p>
|
||||
I contributi avvengono con un semplice bonifico bancario: nessun gestore di pagamenti, nessun
|
||||
dato della carta conservato. I dettagli sono nelle
|
||||
<a href={href('/legal/terms')}>condizioni per i sostenitori</a> e nell'<a
|
||||
href={href('/legal/privacy')}>informativa sulla privacy</a
|
||||
>.
|
||||
</p>
|
||||
|
||||
<h2>Contatti</h2>
|
||||
<p>
|
||||
Domande, riscontri o segnalazioni di bug:
|
||||
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
|
||||
</p>
|
||||
</ProsePage>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import { localizeHref } from '$lib/paraglide/runtime';
|
||||
|
||||
// Same reasoning as the root redirect: resolved in the browser so the
|
||||
// visitor's language is known (see routes/+page.svelte).
|
||||
onMount(() => {
|
||||
goto(localizeHref('/weather/week/'), { replaceState: true });
|
||||
});
|
||||
</script>
|
||||
@@ -1,10 +0,0 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
import { localizeHref } from '$lib/paraglide/runtime';
|
||||
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async () => {
|
||||
// keep the visitor in their language when the bare /weather path is hit
|
||||
throw redirect(303, localizeHref('/weather/week/'));
|
||||
}) satisfies PageLoad;
|
||||
@@ -173,15 +173,38 @@
|
||||
|
||||
// Apply ?day= once the forecast is in: the parameter is a plain calendar date,
|
||||
// which only means something against the location's own timezone.
|
||||
let appliedDayParam = false;
|
||||
// A `?day=` outside what this page can ever show (more than 3 days back, or
|
||||
// beyond the 15-day horizon) is dropped and we open on today. A day that IS
|
||||
// reachable but sits outside the default 7-day window widens the request up
|
||||
// front, so the link loads straight into the right day instead of showing
|
||||
// today and making the visitor click.
|
||||
let wantedDay: string | null = $state(null);
|
||||
onMount(() => {
|
||||
const param = get(page).url.searchParams.get('day');
|
||||
if (!param || !/^\d{4}-\d{2}-\d{2}$/.test(param)) return;
|
||||
|
||||
const target = Date.parse(`${param}T12:00:00Z`);
|
||||
if (Number.isNaN(target)) return;
|
||||
const today = new Date();
|
||||
today.setUTCHours(12, 0, 0, 0);
|
||||
const offset = Math.round((target - today.getTime()) / 86_400_000);
|
||||
|
||||
if (offset < -3 || offset > 15) return; // out of reach: stay on today
|
||||
wantedDay = param;
|
||||
if (offset < 0) pastDays = 3;
|
||||
if (offset >= 7) forecastDays = 15;
|
||||
});
|
||||
|
||||
// Select it as soon as a fetch arrives that actually covers the day.
|
||||
$effect(() => {
|
||||
const fd = fetchedDaily;
|
||||
if (appliedDayParam || !fd) return;
|
||||
appliedDayParam = true;
|
||||
const wanted = get(page).url.searchParams.get('day');
|
||||
if (!wanted) return;
|
||||
const wanted = wantedDay;
|
||||
if (!fd || !wanted) return;
|
||||
const match = fd.dailyDates.find((d) => formatZoned(d, fd.timezone, 'yyyy-MM-dd') === wanted);
|
||||
if (match) selectedDay.setTime(match.getTime());
|
||||
if (match) {
|
||||
selectedDay.setTime(match.getTime());
|
||||
wantedDay = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Mirror the open day and the plotted model back into the URL.
|
||||
@@ -189,8 +212,11 @@
|
||||
const dayKey = selectedDayKey;
|
||||
const model = params.models?.[0];
|
||||
if (!mounted || !dayKey) return;
|
||||
const isToday = fetchedDaily
|
||||
? dayKey === formatZoned(new Date(), fetchedDaily.timezone, 'yyyy-MM-dd')
|
||||
: false;
|
||||
syncSearchParams($page.url, {
|
||||
day: dayKey,
|
||||
day: isToday ? null : dayKey,
|
||||
model: model && model !== 'best_match' ? model : null
|
||||
});
|
||||
});
|
||||
|
||||
@@ -235,12 +235,15 @@
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
/** A value the model actually produced (NaN pads everything past its horizon). */
|
||||
const finite = (v: number | null | undefined): v is number => v != null && Number.isFinite(v);
|
||||
|
||||
function formatTemp(temp: number | null): string {
|
||||
return temp != null ? `${temp.toFixed(0)}°` : '-';
|
||||
return finite(temp) ? `${temp.toFixed(0)}°` : '–';
|
||||
}
|
||||
|
||||
function formatValue(val: number | null): string {
|
||||
return val != null ? val.toFixed(0) : '-';
|
||||
return finite(val) ? val.toFixed(0) : '–';
|
||||
}
|
||||
|
||||
let dayIdx = $derived(getDayIndices(data.hourlyDates, selectedDay));
|
||||
@@ -254,15 +257,48 @@
|
||||
const isNow =
|
||||
formatZoned(date, tz, 'yyyy-MM-dd HH') === formatZoned(today, tz, 'yyyy-MM-dd HH');
|
||||
|
||||
// a column past the model's horizon: nothing to show in any row
|
||||
const hasData = [
|
||||
data.hourly.temperature_2m,
|
||||
data.hourly.weather_code,
|
||||
data.hourly.windspeed_10m,
|
||||
data.hourly.relative_humidity_2m,
|
||||
data.hourly.cloud_cover,
|
||||
data.hourly.precipitation
|
||||
].some((arr) => arr != null && Number.isFinite(arr[idx]));
|
||||
|
||||
return {
|
||||
idx,
|
||||
date,
|
||||
isNow,
|
||||
isDaytime: daytimeFlags[i]
|
||||
isDaytime: daytimeFlags[i],
|
||||
hasData
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
// A model that doesn't carry a variable returns an empty or all-NaN array.
|
||||
// Those rows are dimmed rather than shown as a wall of dashes.
|
||||
let rowHasData = $derived.by((): Record<string, boolean> => {
|
||||
const has = (arr: number[] | undefined) =>
|
||||
arr != null && cellData.some((c) => finite(arr[c.idx]));
|
||||
return {
|
||||
icons: has(data.hourly.weather_code),
|
||||
temperature: has(data.hourly.temperature_2m),
|
||||
feels: has(data.hourly.apparent_temperature),
|
||||
dew_point: has(data.hourly.dew_point_2m),
|
||||
wind: has(data.hourly.windspeed_10m),
|
||||
gusts: has(data.hourly.wind_gusts_10m),
|
||||
humidity: has(data.hourly.relative_humidity_2m),
|
||||
clouds: has(data.hourly.cloud_cover),
|
||||
pressure: has(data.hourly.pressure_msl),
|
||||
uv: has(data.hourly.uv_index),
|
||||
visibility: has(data.hourly.visibility),
|
||||
precipitation: has(data.hourly.precipitation),
|
||||
snowfall: has(data.hourly.snowfall)
|
||||
};
|
||||
});
|
||||
|
||||
let sunrisePercent = $derived(
|
||||
sunTimes && cellData.length > 0 ? timeToFraction(sunTimes.sunrise) * 100 : null
|
||||
);
|
||||
@@ -475,7 +511,7 @@
|
||||
<!-- Time + Daylight bar (merged) -->
|
||||
<tr class="row">
|
||||
<th class="hdr" scope="row" bind:clientWidth={headerColWidth}>
|
||||
<span class="text-[10px] font-semibold text-muted-foreground">Time</span>
|
||||
<span class="text-[10px] font-semibold text-muted-foreground">{m.var_time()}</span>
|
||||
</th>
|
||||
<td
|
||||
colspan={cellData.length}
|
||||
@@ -556,6 +592,7 @@
|
||||
{@const leftPct = (i / cellData.length) * 100}
|
||||
{@const widthPct = 100 / cellData.length}
|
||||
<span
|
||||
class:col-empty={!cell.hasData}
|
||||
class="absolute top-0 flex items-start pl-1 font-bold {is3h
|
||||
? 'pt-2 text-sm'
|
||||
: 'pt-2.5'}
|
||||
@@ -591,12 +628,14 @@
|
||||
<!-- Weather Icons -->
|
||||
{#each rowOrder as rowKey (rowKey)}
|
||||
{#if rowKey === 'icons' && showRow('icons')}
|
||||
<tr class="row">
|
||||
<tr class="row" class:row-empty={rowHasData[rowKey] === false}>
|
||||
{@render rowHeader('wi-day-cloudy')}
|
||||
{#each cellData as cell, i (cell.idx)}
|
||||
{@const wCode = hourly.weather_code[cell.idx]}
|
||||
<td
|
||||
class:col-empty={!cell.hasData}
|
||||
class="cell h-12 leading-0"
|
||||
class:cell-empty={!finite(wCode)}
|
||||
class:icon-day={cell.isDaytime}
|
||||
class:icon-night={!cell.isDaytime}
|
||||
class:icon-dawn={!cell.isDaytime && cellData[i + 1]?.isDaytime}
|
||||
@@ -604,20 +643,23 @@
|
||||
cellData[i + 1] &&
|
||||
!cellData[i + 1].isDaytime}
|
||||
>
|
||||
{@render weatherIcon(getWeatherIconName(wCode, cell.isDaytime), iconPx)}
|
||||
{#if finite(wCode)}
|
||||
{@render weatherIcon(getWeatherIconName(wCode, cell.isDaytime), iconPx)}
|
||||
{/if}
|
||||
</td>
|
||||
{/each}
|
||||
</tr>
|
||||
<!-- Temperature -->
|
||||
{:else if rowKey === 'temperature' && showRow('temperature')}
|
||||
<tr class="row">
|
||||
{@render rowHeader('wi-thermometer', tempUnit, 'Temp')}
|
||||
<tr class="row" class:row-empty={rowHasData[rowKey] === false}>
|
||||
{@render rowHeader('wi-thermometer', tempUnit, m.var_temperature_short())}
|
||||
{#each cellData as cell (cell.idx)}
|
||||
{@const temp = hourly.temperature_2m[cell.idx]}
|
||||
{@const style = getTempStyle(temp, String(units.temperature_unit))}
|
||||
<td
|
||||
class:col-empty={!cell.hasData}
|
||||
class="cell h-12 font-bold {is3h ? 'text-lg' : 'text-[15px]'}"
|
||||
style="background-color:{style.bg};color:{style.fg}"
|
||||
style={finite(temp) ? `background-color:${style.bg};color:${style.fg}` : ''}
|
||||
>
|
||||
{formatTemp(temp)}
|
||||
</td>
|
||||
@@ -625,11 +667,12 @@
|
||||
</tr>
|
||||
<!-- Feels Like (short row) -->
|
||||
{:else if rowKey === 'feels' && showRow('feels')}
|
||||
<tr class="row">
|
||||
{@render rowHeader(undefined, tempUnit, 'Feels')}
|
||||
<tr class="row" class:row-empty={rowHasData[rowKey] === false}>
|
||||
{@render rowHeader(undefined, tempUnit, m.var_apparent_short())}
|
||||
{#each cellData as cell (cell.idx)}
|
||||
{@const temp = hourly.apparent_temperature[cell.idx]}
|
||||
<td
|
||||
class:col-empty={!cell.hasData}
|
||||
class="cell h-12 text-muted-foreground {is3h ? 'text-[13px]' : 'text-[11px]'}"
|
||||
>
|
||||
{formatTemp(temp)}
|
||||
@@ -638,11 +681,12 @@
|
||||
</tr>
|
||||
<!-- Dew Point (short row; off by default) -->
|
||||
{:else if rowKey === 'dew_point' && showRow('dew_point')}
|
||||
<tr class="row">
|
||||
{@render rowHeader('wi-raindrop', tempUnit, 'Dew')}
|
||||
<tr class="row" class:row-empty={rowHasData[rowKey] === false}>
|
||||
{@render rowHeader('wi-raindrop', tempUnit, m.var_dew_point_short())}
|
||||
{#each cellData as cell (cell.idx)}
|
||||
{@const temp = hourly.dew_point_2m?.[cell.idx]}
|
||||
<td
|
||||
class:col-empty={!cell.hasData}
|
||||
class="cell h-12 text-muted-foreground {is3h ? 'text-[13px]' : 'text-[11px]'}"
|
||||
>
|
||||
{formatTemp(temp)}
|
||||
@@ -651,12 +695,15 @@
|
||||
</tr>
|
||||
<!-- Wind -->
|
||||
{:else if rowKey === 'wind' && showRow('wind')}
|
||||
<tr class="row">
|
||||
{@render rowHeader('wi-strong-wind', windUnit, 'Wind')}
|
||||
<tr class="row" class:row-empty={rowHasData[rowKey] === false}>
|
||||
{@render rowHeader('wi-strong-wind', windUnit, m.var_wind_short())}
|
||||
{#each cellData as cell (cell.idx)}
|
||||
{@const wind = hourly.windspeed_10m[cell.idx]}
|
||||
{@const windDir = hourly.winddirection_10m[cell.idx]}
|
||||
<td class="relative cell h-12 align-middle leading-none">
|
||||
<td
|
||||
class:col-empty={!cell.hasData}
|
||||
class="relative cell h-12 align-middle leading-none"
|
||||
>
|
||||
{#if windDir != null && !isNaN(windDir)}
|
||||
<span
|
||||
class="absolute top-0 left-1/2 inline-block origin-center leading-0"
|
||||
@@ -673,11 +720,12 @@
|
||||
</tr>
|
||||
<!-- Wind Gusts (off by default) -->
|
||||
{:else if rowKey === 'gusts' && showRow('gusts')}
|
||||
<tr class="row">
|
||||
{@render rowHeader('wi-strong-wind', windUnit, 'Gusts')}
|
||||
<tr class="row" class:row-empty={rowHasData[rowKey] === false}>
|
||||
{@render rowHeader('wi-strong-wind', windUnit, m.var_gusts_short())}
|
||||
{#each cellData as cell (cell.idx)}
|
||||
{@const v = hourly.wind_gusts_10m?.[cell.idx]}
|
||||
<td
|
||||
class:col-empty={!cell.hasData}
|
||||
class="cell h-12 font-semibold text-foreground/80 {is3h
|
||||
? 'text-sm'
|
||||
: 'text-xs'}"
|
||||
@@ -688,24 +736,31 @@
|
||||
</tr>
|
||||
<!-- Humidity (short row) -->
|
||||
{:else if rowKey === 'humidity' && showRow('humidity')}
|
||||
<tr class="row">
|
||||
{@render rowHeader('wi-humidity', '%', 'Humidity')}
|
||||
<tr class="row" class:row-empty={rowHasData[rowKey] === false}>
|
||||
{@render rowHeader('wi-humidity', '%', m.var_humidity())}
|
||||
{#each cellData as cell (cell.idx)}
|
||||
{@const hum = hourly.relative_humidity_2m[cell.idx]}
|
||||
<td class="cell h-12" style="background:{getHumidityBg(hum ?? 0)}">
|
||||
<td
|
||||
class:col-empty={!cell.hasData}
|
||||
class="cell h-12"
|
||||
style={finite(hum) ? `background:${getHumidityBg(hum)}` : ''}
|
||||
>
|
||||
{formatValue(hum)}
|
||||
</td>
|
||||
{/each}
|
||||
</tr>
|
||||
<!-- Cloud Cover -->
|
||||
{:else if rowKey === 'clouds' && showRow('clouds')}
|
||||
<tr class="row">
|
||||
{@render rowHeader('wi-cloud', '%', 'Clouds')}
|
||||
<tr class="row" class:row-empty={rowHasData[rowKey] === false}>
|
||||
{@render rowHeader('wi-cloud', '%', m.var_cloud_short())}
|
||||
{#each cellData as cell (cell.idx)}
|
||||
{@const cloud = hourly.cloud_cover[cell.idx]}
|
||||
<td
|
||||
class:col-empty={!cell.hasData}
|
||||
class="cell h-12"
|
||||
style="background:rgba(140,160,180,{getCloudOpacity(cloud ?? 0)})"
|
||||
style={finite(cloud)
|
||||
? `background:rgba(140,160,180,${getCloudOpacity(cloud)})`
|
||||
: ''}
|
||||
>
|
||||
{formatValue(cloud)}
|
||||
</td>
|
||||
@@ -713,11 +768,12 @@
|
||||
</tr>
|
||||
<!-- Pressure (off by default) -->
|
||||
{:else if rowKey === 'pressure' && showRow('pressure')}
|
||||
<tr class="row">
|
||||
{@render rowHeader('wi-barometer', 'hPa', 'Pressure')}
|
||||
<tr class="row" class:row-empty={rowHasData[rowKey] === false}>
|
||||
{@render rowHeader('wi-barometer', 'hPa', m.var_pressure_short())}
|
||||
{#each cellData as cell (cell.idx)}
|
||||
{@const v = hourly.pressure_msl?.[cell.idx]}
|
||||
<td
|
||||
class:col-empty={!cell.hasData}
|
||||
class="cell h-12 {is3h ? 'text-sm' : 'text-xs'}"
|
||||
style="background:{getPressureBg(v ?? null)}"
|
||||
>
|
||||
@@ -727,11 +783,12 @@
|
||||
</tr>
|
||||
<!-- UV Index (off by default) -->
|
||||
{:else if rowKey === 'uv' && showRow('uv')}
|
||||
<tr class="row">
|
||||
{@render rowHeader('wi-day-sunny', 'UV', 'UV')}
|
||||
<tr class="row" class:row-empty={rowHasData[rowKey] === false}>
|
||||
{@render rowHeader('wi-day-sunny', 'UV', m.var_uv_short())}
|
||||
{#each cellData as cell (cell.idx)}
|
||||
{@const v = hourly.uv_index?.[cell.idx]}
|
||||
<td
|
||||
class:col-empty={!cell.hasData}
|
||||
class="cell h-12 font-semibold {is3h ? 'text-sm' : 'text-xs'}"
|
||||
style="background:{getUvBg(v ?? null)}"
|
||||
>
|
||||
@@ -741,23 +798,27 @@
|
||||
</tr>
|
||||
<!-- Visibility (off by default) -->
|
||||
{:else if rowKey === 'visibility' && showRow('visibility')}
|
||||
<tr class="row">
|
||||
{@render rowHeader('wi-fog', 'km', 'Visibility')}
|
||||
<tr class="row" class:row-empty={rowHasData[rowKey] === false}>
|
||||
{@render rowHeader('wi-fog', 'km', m.var_visibility_short())}
|
||||
{#each cellData as cell (cell.idx)}
|
||||
{@const v = hourly.visibility?.[cell.idx]}
|
||||
<td class="cell h-12 {is3h ? 'text-sm' : 'text-xs'}">
|
||||
<td
|
||||
class:col-empty={!cell.hasData}
|
||||
class="cell h-12 {is3h ? 'text-sm' : 'text-xs'}"
|
||||
>
|
||||
{v != null && !isNaN(v) ? (v / 1000).toFixed(0) : '-'}
|
||||
</td>
|
||||
{/each}
|
||||
</tr>
|
||||
<!-- Precipitation -->
|
||||
{:else if rowKey === 'precipitation' && showRow('precipitation')}
|
||||
<tr class="row">
|
||||
{@render rowHeader('wi-raindrop', precipUnit, 'Precip')}
|
||||
<tr class="row" class:row-empty={rowHasData[rowKey] === false}>
|
||||
{@render rowHeader('wi-raindrop', precipUnit, m.var_precipitation_short())}
|
||||
{#each cellData as cell (cell.idx)}
|
||||
{@const precip = hourly.precipitation[cell.idx]}
|
||||
{@const prob = hourly.precipitation_probability[cell.idx]}
|
||||
<td
|
||||
class:col-empty={!cell.hasData}
|
||||
class="cell precip-cell h-12"
|
||||
style="background:{getPrecipProbBg(prob ?? 0)}"
|
||||
title={formatPrecipTooltip(precip, prob)}
|
||||
@@ -773,11 +834,14 @@
|
||||
</tr>
|
||||
<!-- Snowfall (off by default) -->
|
||||
{:else if rowKey === 'snowfall' && showRow('snowfall')}
|
||||
<tr class="row">
|
||||
{@render rowHeader('wi-snow', 'cm', 'Snow')}
|
||||
<tr class="row" class:row-empty={rowHasData[rowKey] === false}>
|
||||
{@render rowHeader('wi-snow', 'cm', m.var_snowfall_short())}
|
||||
{#each cellData as cell (cell.idx)}
|
||||
{@const v = hourly.snowfall?.[cell.idx]}
|
||||
<td class="cell h-12 {is3h ? 'text-sm' : 'text-xs'}">
|
||||
<td
|
||||
class:col-empty={!cell.hasData}
|
||||
class="cell h-12 {is3h ? 'text-sm' : 'text-xs'}"
|
||||
>
|
||||
{v != null && !isNaN(v) && v > 0 ? v.toFixed(1) : '-'}
|
||||
</td>
|
||||
{/each}
|
||||
@@ -853,6 +917,24 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* A row the selected model carries no data for: legible, clearly inactive,
|
||||
and not competing with the rows that do have values. */
|
||||
.row-empty {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
/* An individual cell past the point where the model stopped: drop the
|
||||
day/night tinting so the gap reads as "no data" rather than clear sky. */
|
||||
.cell-empty {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
/* A whole column the model never reached - dimmed end to end, hour label
|
||||
included, so the point where the forecast runs out is obvious. */
|
||||
.col-empty {
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
/* ── Precipitation ──────────────────────────────────────────── */
|
||||
.precip-cell {
|
||||
position: relative;
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
*/
|
||||
import { defaultVariablePrefs } from '$lib/stores/settings';
|
||||
|
||||
import * as m from '$lib/paraglide/messages';
|
||||
|
||||
import { getColor } from '../../utils/colors';
|
||||
import {
|
||||
type WeatherUnits,
|
||||
@@ -69,8 +71,8 @@ export interface ChartVariableDef {
|
||||
export const CHART_VARIABLES: ChartVariableDef[] = [
|
||||
{
|
||||
key: 'temperature',
|
||||
label: 'Temperature',
|
||||
short: 'Temp',
|
||||
label: m.var_temperature(),
|
||||
short: m.var_temperature_short(),
|
||||
field: 'temperature_2m',
|
||||
type: 'line',
|
||||
kind: 'temp',
|
||||
@@ -83,8 +85,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
||||
},
|
||||
{
|
||||
key: 'weather_icons',
|
||||
label: 'Weather icons',
|
||||
short: 'Icons',
|
||||
label: m.var_icons(),
|
||||
short: m.var_icons_short(),
|
||||
field: 'weather_code',
|
||||
type: 'line',
|
||||
kind: 'temp',
|
||||
@@ -94,8 +96,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
||||
},
|
||||
{
|
||||
key: 'apparent_temperature',
|
||||
label: 'Apparent Temp',
|
||||
short: 'Feels',
|
||||
label: m.var_apparent(),
|
||||
short: m.var_apparent_short(),
|
||||
field: 'apparent_temperature',
|
||||
type: 'line',
|
||||
kind: 'temp',
|
||||
@@ -105,8 +107,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
||||
},
|
||||
{
|
||||
key: 'dew_point',
|
||||
label: 'Dew Point',
|
||||
short: 'Dew',
|
||||
label: m.var_dew_point(),
|
||||
short: m.var_dew_point_short(),
|
||||
field: 'dew_point_2m',
|
||||
type: 'line',
|
||||
kind: 'temp',
|
||||
@@ -115,8 +117,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
||||
},
|
||||
{
|
||||
key: 'cloud_cover',
|
||||
label: 'Cloud Cover',
|
||||
short: 'Cloud',
|
||||
label: m.var_cloud(),
|
||||
short: m.var_cloud_short(),
|
||||
field: 'cloud_cover',
|
||||
type: 'line',
|
||||
kind: 'percent',
|
||||
@@ -128,8 +130,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
||||
// the way the layers actually look from below.
|
||||
{
|
||||
key: 'cloud_cover_low',
|
||||
label: 'Cloud Cover Low',
|
||||
short: 'Low',
|
||||
label: m.var_cloud_low(),
|
||||
short: m.var_cloud_low_short(),
|
||||
field: 'cloud_cover_low',
|
||||
type: 'line',
|
||||
kind: 'percent',
|
||||
@@ -139,8 +141,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
||||
},
|
||||
{
|
||||
key: 'cloud_cover_mid',
|
||||
label: 'Cloud Cover Mid',
|
||||
short: 'Mid',
|
||||
label: m.var_cloud_mid(),
|
||||
short: m.var_cloud_mid_short(),
|
||||
field: 'cloud_cover_mid',
|
||||
type: 'line',
|
||||
kind: 'percent',
|
||||
@@ -150,8 +152,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
||||
},
|
||||
{
|
||||
key: 'cloud_cover_high',
|
||||
label: 'Cloud Cover High',
|
||||
short: 'High',
|
||||
label: m.var_cloud_high(),
|
||||
short: m.var_cloud_high_short(),
|
||||
field: 'cloud_cover_high',
|
||||
type: 'line',
|
||||
kind: 'percent',
|
||||
@@ -161,8 +163,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
||||
},
|
||||
{
|
||||
key: 'precipitation',
|
||||
label: 'Precipitation',
|
||||
short: 'Precip',
|
||||
label: m.var_precipitation(),
|
||||
short: m.var_precipitation_short(),
|
||||
field: 'precipitation',
|
||||
type: 'bar',
|
||||
kind: 'precip',
|
||||
@@ -170,8 +172,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
||||
},
|
||||
{
|
||||
key: 'precipitation_probability',
|
||||
label: 'Precip. Probability',
|
||||
short: 'PoP',
|
||||
label: m.var_pop(),
|
||||
short: m.var_pop_short(),
|
||||
field: 'precipitation_probability',
|
||||
type: 'line',
|
||||
kind: 'percent',
|
||||
@@ -181,8 +183,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
||||
},
|
||||
{
|
||||
key: 'rain',
|
||||
label: 'Rain',
|
||||
short: 'Rain',
|
||||
label: m.var_rain(),
|
||||
short: m.var_rain_short(),
|
||||
field: 'rain',
|
||||
type: 'bar',
|
||||
kind: 'precip',
|
||||
@@ -190,8 +192,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
||||
},
|
||||
{
|
||||
key: 'showers',
|
||||
label: 'Showers',
|
||||
short: 'Shwr',
|
||||
label: m.var_showers(),
|
||||
short: m.var_showers_short(),
|
||||
field: 'showers',
|
||||
type: 'bar',
|
||||
kind: 'precip',
|
||||
@@ -199,8 +201,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
||||
},
|
||||
{
|
||||
key: 'snowfall',
|
||||
label: 'Snowfall',
|
||||
short: 'Snow',
|
||||
label: m.var_snowfall(),
|
||||
short: m.var_snowfall_short(),
|
||||
field: 'snowfall',
|
||||
type: 'bar',
|
||||
kind: 'snow',
|
||||
@@ -208,8 +210,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
||||
},
|
||||
{
|
||||
key: 'wind',
|
||||
label: 'Wind Speed',
|
||||
short: 'Wind',
|
||||
label: m.var_wind(),
|
||||
short: m.var_wind_short(),
|
||||
field: 'windspeed_10m',
|
||||
api: 'wind_speed_10m',
|
||||
type: 'line',
|
||||
@@ -221,8 +223,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
||||
},
|
||||
{
|
||||
key: 'wind_direction',
|
||||
label: 'Wind direction',
|
||||
short: 'Dir',
|
||||
label: m.var_wind_dir(),
|
||||
short: m.var_wind_dir_short(),
|
||||
field: 'winddirection_10m',
|
||||
api: 'wind_direction_10m',
|
||||
type: 'line',
|
||||
@@ -233,8 +235,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
||||
},
|
||||
{
|
||||
key: 'wind_gusts',
|
||||
label: 'Wind Gusts',
|
||||
short: 'Gusts',
|
||||
label: m.var_gusts(),
|
||||
short: m.var_gusts_short(),
|
||||
field: 'wind_gusts_10m',
|
||||
type: 'line',
|
||||
kind: 'wind',
|
||||
@@ -244,8 +246,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
||||
},
|
||||
{
|
||||
key: 'humidity',
|
||||
label: 'Humidity',
|
||||
short: 'RH',
|
||||
label: m.var_humidity(),
|
||||
short: m.var_humidity_short(),
|
||||
field: 'relative_humidity_2m',
|
||||
type: 'line',
|
||||
kind: 'percent',
|
||||
@@ -255,8 +257,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
||||
},
|
||||
{
|
||||
key: 'pressure_msl',
|
||||
label: 'Pressure (MSL)',
|
||||
short: 'MSLP',
|
||||
label: m.var_pressure(),
|
||||
short: m.var_pressure_short(),
|
||||
field: 'pressure_msl',
|
||||
type: 'line',
|
||||
kind: 'pressure',
|
||||
@@ -265,8 +267,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
||||
},
|
||||
{
|
||||
key: 'surface_pressure',
|
||||
label: 'Surface Pressure',
|
||||
short: 'Psfc',
|
||||
label: m.var_surface_pressure(),
|
||||
short: m.var_surface_pressure_short(),
|
||||
field: 'surface_pressure',
|
||||
type: 'line',
|
||||
kind: 'pressure',
|
||||
@@ -276,8 +278,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
||||
},
|
||||
{
|
||||
key: 'uv_index',
|
||||
label: 'UV Index',
|
||||
short: 'UV',
|
||||
label: m.var_uv(),
|
||||
short: m.var_uv_short(),
|
||||
field: 'uv_index',
|
||||
type: 'line',
|
||||
kind: 'uv',
|
||||
@@ -288,8 +290,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
||||
},
|
||||
{
|
||||
key: 'visibility',
|
||||
label: 'Visibility',
|
||||
short: 'Vis',
|
||||
label: m.var_visibility(),
|
||||
short: m.var_visibility_short(),
|
||||
field: 'visibility',
|
||||
type: 'line',
|
||||
kind: 'distance',
|
||||
@@ -299,8 +301,8 @@ export const CHART_VARIABLES: ChartVariableDef[] = [
|
||||
},
|
||||
{
|
||||
key: 'cape',
|
||||
label: 'CAPE',
|
||||
short: 'CAPE',
|
||||
label: m.var_cape(),
|
||||
short: m.var_cape_short(),
|
||||
field: 'cape',
|
||||
type: 'line',
|
||||
kind: 'energy',
|
||||
|
||||
Reference in New Issue
Block a user