This commit is contained in:
Vincent van der Wal
2026-08-01 17:58:03 +02:00
parent b2c8108c8c
commit 01cc6225aa
430 changed files with 3150 additions and 441 deletions
+8 -30
View File
@@ -1,33 +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>
<!-- TODO(vincent): replace the [bracketed] placeholders with your real postal
address and country before deploying. An imprint without a serviceable
address does not satisfy the imprint/contact requirements. -->
<ProsePage title="Imprint" subtitle="Legal notice / provider identification.">
<h2>Service provider</h2>
<address>
Vincent van der Wal<br />
Tschalungasse 2<br />
CH-6442 Gersau<br />
Switzerland
</address>
<p>
Email: <a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
</p>
<p>Drizz.li is operated as a personal, independent project.</p>
<h2>Disclaimer</h2>
<p>
Weather data shown on this site is provided for general information only. Forecasts are
inherently uncertain; do not rely on them where life, health or property is at stake - consult
official warnings from your national weather service instead.
</p>
<p>
Weather and geodata are retrieved from
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>, which is not
affiliated with this site. External links are provided in good faith; their content is the
responsibility of the respective operators.
</p>
</ProsePage>
<LocalizedContent variants={{ en, de, es, fr, it }} />
@@ -0,0 +1,32 @@
<script lang="ts">
import ProsePage from '$lib/components/prose-page.svelte';
</script>
<ProsePage title="Impressum" subtitle="Anbieterkennzeichnung.">
<h2>Diensteanbieter</h2>
<address>
Vincent van der Wal<br />
Tschalungasse 2<br />
CH-6442 Gersau<br />
Switzerland
</address>
<p>
E-Mail: <a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
</p>
<p>Drizz.li wird als persönliches, unabhängiges Projekt betrieben.</p>
<h2>Haftungsausschluss</h2>
<p>
Die hier gezeigten Wetterdaten dienen ausschliesslich der allgemeinen Information. Vorhersagen
sind naturgemäss unsicher; verlassen Sie sich nicht auf sie, wenn Leben, Gesundheit oder
Sachwerte auf dem Spiel stehen - massgeblich sind die amtlichen Warnungen Ihres nationalen
Wetterdienstes.
</p>
<p>
Wetter- und Geodaten stammen von
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>, das nicht mit
dieser Seite verbunden ist. Externe Links werden nach bestem Wissen gesetzt; für deren Inhalte
sind die jeweiligen Betreiber verantwortlich.
</p>
<p class="text-muted-foreground">Massgeblich ist die englische Fassung dieses Impressums.</p>
</ProsePage>
@@ -0,0 +1,30 @@
<script lang="ts">
import ProsePage from '$lib/components/prose-page.svelte';
</script>
<ProsePage title="Imprint" subtitle="Legal notice / provider identification.">
<h2>Service provider</h2>
<address>
Vincent van der Wal<br />
Tschalungasse 2<br />
CH-6442 Gersau<br />
Switzerland
</address>
<p>
Email: <a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
</p>
<p>Drizz.li is operated as a personal, independent project.</p>
<h2>Disclaimer</h2>
<p>
Weather data shown on this site is provided for general information only. Forecasts are
inherently uncertain; do not rely on them where life, health or property is at stake - consult
official warnings from your national weather service instead.
</p>
<p>
Weather and geodata are retrieved from
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>, which is not
affiliated with this site. External links are provided in good faith; their content is the
responsibility of the respective operators.
</p>
</ProsePage>
@@ -0,0 +1,33 @@
<script lang="ts">
import ProsePage from '$lib/components/prose-page.svelte';
</script>
<ProsePage title="Aviso legal" subtitle="Identificación del prestador del servicio.">
<h2>Prestador del servicio</h2>
<address>
Vincent van der Wal<br />
Tschalungasse 2<br />
CH-6442 Gersau<br />
Switzerland
</address>
<p>
Correo electrónico: <a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
</p>
<p>Drizz.li se gestiona como un proyecto personal e independiente.</p>
<h2>Descargo de responsabilidad</h2>
<p>
Los datos meteorológicos de este sitio son solo informativos. Los pronósticos son
intrínsecamente inciertos; no confíes en ellos cuando estén en juego la vida, la salud o los
bienes: consulta los avisos oficiales de tu servicio meteorológico nacional.
</p>
<p>
Los datos meteorológicos y geográficos proceden de
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>, que no está
afiliado a este sitio. Los enlaces externos se ofrecen de buena fe; su contenido es
responsabilidad de sus respectivos operadores.
</p>
<p class="text-muted-foreground">
En caso de discrepancia prevalece la versión inglesa de este aviso legal.
</p>
</ProsePage>
@@ -0,0 +1,33 @@
<script lang="ts">
import ProsePage from '$lib/components/prose-page.svelte';
</script>
<ProsePage title="Mentions légales" subtitle="Identification de l'éditeur.">
<h2>Éditeur du service</h2>
<address>
Vincent van der Wal<br />
Tschalungasse 2<br />
CH-6442 Gersau<br />
Switzerland
</address>
<p>
E-mail: <a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
</p>
<p>Drizz.li est exploité comme un projet personnel et indépendant.</p>
<h2>Avertissement</h2>
<p>
Les données météo présentées ici sont fournies à titre d'information générale. Les prévisions
sont par nature incertaines ; ne vous y fiez pas lorsque des vies, la santé ou des biens sont en
jeu - consultez les alertes officielles de votre service météorologique national.
</p>
<p>
Les données météo et géographiques proviennent d'
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>, qui n'est pas
affilié à ce site. Les liens externes sont fournis de bonne foi ; leur contenu relève de la
responsabilité de leurs exploitants.
</p>
<p class="text-muted-foreground">
En cas de divergence, la version anglaise de ces mentions fait foi.
</p>
</ProsePage>
@@ -0,0 +1,33 @@
<script lang="ts">
import ProsePage from '$lib/components/prose-page.svelte';
</script>
<ProsePage title="Note legali" subtitle="Identificazione del fornitore del servizio.">
<h2>Fornitore del servizio</h2>
<address>
Vincent van der Wal<br />
Tschalungasse 2<br />
CH-6442 Gersau<br />
Switzerland
</address>
<p>
E-mail: <a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
</p>
<p>Drizz.li è gestito come progetto personale e indipendente.</p>
<h2>Avvertenza</h2>
<p>
I dati meteo mostrati su questo sito hanno finalità puramente informative. Le previsioni sono
per natura incerte: non farvi affidamento quando sono in gioco la vita, la salute o i beni -
consulta gli avvisi ufficiali del tuo servizio meteorologico nazionale.
</p>
<p>
I dati meteo e geografici provengono da
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>, che non è
affiliato a questo sito. I link esterni sono forniti in buona fede; i contenuti sono
responsabilità dei rispettivi gestori.
</p>
<p class="text-muted-foreground">
In caso di discrepanza prevale la versione inglese di queste note legali.
</p>
</ProsePage>
+8 -144
View File
@@ -1,147 +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>
<!-- TODO(vincent): fill in the [bracketed] placeholders (address, hosting
provider, supervisory authority) before deploying. -->
<ProsePage title="Privacy policy" subtitle="Last updated: 1 August 2026">
<p>
Drizz.li is built to need as little of your data as possible: there are no user accounts, no
cookies, no advertising and no analytics or tracking scripts. This page explains what little
processing does happen - when you browse the site, and when you support the project with a
contribution.
</p>
<h2>1. Controller</h2>
<address>
Vincent van der Wal<br />
Tschalungasse 2, CH-6442 Gersau, Switzerland <br />
Email:
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
</address>
<p>
This policy covers the websites drizz.li (the weather app) and support.drizz.li (the supporter
signup page).
</p>
<h2>2. Browsing the site</h2>
<h3>Weather data requests</h3>
<p>
Drizz.li is a static site: when you open a forecast, your browser fetches the weather data
directly from the open-data APIs of
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>
(api.open-meteo.com, geocoding-api.open-meteo.com, archive-api.open-meteo.com, ensemble-api.open-meteo.com,
and map tiles from maps.open-meteo.com / map-tiles.open-meteo.com / map-assets.open-meteo.com). Like
any web request, this transmits your IP address and the requested location or search term to Open-Meteo.
We do not receive or store any of this. See
<a href="https://open-meteo.com/en/terms" target="_blank" rel="noopener"
>Open-Meteo's terms and privacy information</a
>. Legal basis: our legitimate interest in delivering the content you request (Art. 6(1)(f)
GDPR).
</p>
<h3>Hosting</h3>
<p>
The static site is served by [hosting provider, location]. The hosting infrastructure may keep
short-lived technical server logs (IP address, requested URL, timestamp) for security and
operations. Legal basis: legitimate interest in a secure, reliable service (Art. 6(1)(f) GDPR).
</p>
<h3>Settings on your device (local storage)</h3>
<p>
Your preferences - theme, measurement units, last searched location, and (for supporters) your
access key and its last verification result - are stored only in your browser's local storage.
They never leave your device except as described below for key verification, and you can clear
them at any time via your browser.
</p>
<h2>3. Supporting the project (contributions)</h2>
<p>
Supporter contributions unlock the supporter extras, so legally they are a paid agreement, not a
pure gift - and this is what we process to handle them:
</p>
<h3>Signup form</h3>
<ul>
<li>
<strong>Email address</strong> (required) - to send you the transfer details and, after your contribution
arrives, your access key.
</li>
<li><strong>Name</strong> (optional) - to help match your bank transfer.</li>
<li>
<strong>Currency and amount</strong> - shown based on your browser's locale/timezone, detected on
your device (no geolocation request is made).
</li>
<li>
<strong>IP address and technical anti-bot signals</strong> - kept with the signup request to prevent
abuse and spam of the form.
</li>
<li>
<strong>Payment reference</strong> (e.g. DRZ-XXXXXX) - generated per request to match your transfer
to your signup.
</li>
</ul>
<p>
The transfer details (bank account and reference) are sent to you by email rather than shown on
the page. Legal bases: performance of the agreement (Art. 6(1)(b) GDPR) and, for the anti-abuse
measures, legitimate interest (Art. 6(1)(f) GDPR).
</p>
<h3>Bank transfer</h3>
<p>
Contributions are paid by ordinary bank transfer. Your bank and ours process the transfer data
under their own responsibility; on our bank statement we see the usual transfer details (your
name, account number, amount, reference). We use them only to match your contribution and are
required to retain accounting records under statutory bookkeeping and tax law (Art. 6(1)(c)
GDPR).
</p>
<h3>Access key and verification</h3>
<p>
After your transfer is matched (manually, usually within 24 hours), you receive an access key by
email. When you paste it into Drizz.li, your browser sends the key to our verification endpoint
(support.drizz.li) to check whether it is active; the response contains only the validity, tier
and expiry. The subscriber list (key, email, expiry) is stored on our server for as long as your
access is active.
</p>
<h3>Email</h3>
<p>
Transactional emails (transfer details, access key, renewals) are sent through our email
provider, Strato (Strato AG, Germany), acting as a processor. We send no newsletters or
marketing email.
</p>
<h2>4. Retention</h2>
<ul>
<li>Signup requests that are never paid are deleted after 6 months at the latest.</li>
<li>
Supporter records (key, email, expiry) are kept while your access is active and deleted within
12 months after it expires, unless you ask us to delete them sooner.
</li>
<li>
Accounting records (bank statements showing your transfer) are kept for the statutory
retention period (up to 10 years, depending on jurisdiction).
</li>
</ul>
<h2>5. Recipients</h2>
<p>
We do not sell or share personal data with third parties for their own purposes. Recipients are
limited to: Open-Meteo (weather requests made directly by your browser), our hosting provider,
our email provider (Strato), and the banks involved in your transfer.
</p>
<h2>6. Your rights</h2>
<p>
Under the GDPR you have the right to access, rectification, erasure, restriction of processing,
data portability, and to object to processing based on legitimate interest. Where processing is
based on consent, you may withdraw it at any time. To exercise any of these rights, email
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>. You also have the
right to lodge a complaint with a supervisory authority, in particular in the EU member state of
your residence, or with [competent supervisory authority of the controller].
</p>
<p>
For visitors from Switzerland: the corresponding rights under the Swiss Federal Act on Data
Protection (FADP) apply equivalently.
</p>
<h2>7. Changes</h2>
<p>
We may update this policy when the service changes; the date above reflects the latest revision.
</p>
</ProsePage>
<LocalizedContent variants={{ en, de, es, fr, it }} />
+150
View File
@@ -0,0 +1,150 @@
<script lang="ts">
import ProsePage from '$lib/components/prose-page.svelte';
</script>
<ProsePage title="Datenschutzerklärung" subtitle="Zuletzt aktualisiert: 1. August 2026">
<p>
Drizz.li ist so gebaut, dass möglichst wenige Ihrer Daten nötig sind: keine Benutzerkonten,
keine Cookies, keine Werbung, keine Analyse- oder Tracking-Skripte. Diese Seite erklärt, welche
wenige Verarbeitung dennoch stattfindet - beim Besuch der Seite und wenn Sie das Projekt mit
einem Beitrag unterstützen.
</p>
<h2>1. Verantwortlicher</h2>
<address>
Vincent van der Wal<br />
Tschalungasse 2, CH-6442 Gersau, Switzerland <br />
E-Mail:
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
</address>
<p>
Diese Erklärung gilt für die Websites drizz.li (die Wetter-App) und support.drizz.li (die
Unterstützer-Anmeldung).
</p>
<h2>2. Besuch der Website</h2>
<h3>Abruf der Wetterdaten</h3>
<p>
Drizz.li ist eine statische Seite: Wenn Sie eine Vorhersage öffnen, ruft Ihr Browser die
Wetterdaten direkt bei den Open-Data-Schnittstellen von
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>. Wie bei jeder
Webanfrage werden dabei Ihre IP-Adresse und der gewünschte Ort bzw. Suchbegriff an Open-Meteo
übertragen. Wir erhalten und speichern davon nichts. Siehe
<a href="https://open-meteo.com/en/terms" target="_blank" rel="noopener"
>Nutzungs- und Datenschutzhinweise von Open-Meteo</a
>. Rechtsgrundlage: unser berechtigtes Interesse an der Auslieferung der angeforderten Inhalte
(Art. 6 Abs. 1 lit. f DSGVO).
</p>
<h3>Hosting</h3>
<p>
Die statische Seite wird von [Hosting-Anbieter, Standort] ausgeliefert. Die
Hosting-Infrastruktur kann kurzlebige technische Server-Logs (IP-Adresse, angefragte URL,
Zeitstempel) zu Sicherheits- und Betriebszwecken speichern. Rechtsgrundlage: berechtigtes
Interesse an einem sicheren, zuverlässigen Dienst (Art. 6 Abs. 1 lit. f DSGVO).
</p>
<h3>Einstellungen auf Ihrem Gerät (Local Storage)</h3>
<p>
Ihre Einstellungen - Design, Masseinheiten, zuletzt gesuchter Ort und (für Unterstützer) Ihr
Zugangsschlüssel samt letztem Prüfergebnis - werden ausschliesslich im Local Storage Ihres
Browsers gespeichert. Sie verlassen Ihr Gerät nicht, ausser wie unten für die Schlüsselprüfung
beschrieben, und Sie können sie jederzeit über Ihren Browser löschen.
</p>
<h2>3. Das Projekt unterstützen (Beiträge)</h2>
<p>
Unterstützerbeiträge schalten die Extras frei, rechtlich sind sie daher eine entgeltliche
Vereinbarung und keine reine Schenkung - das verarbeiten wir zur Abwicklung:
</p>
<h3>Anmeldeformular</h3>
<ul>
<li>
<strong>E-Mail-Adresse</strong> (erforderlich) - um Ihnen die Überweisungsdaten und nach Eingang
Ihres Beitrags den Zugangsschlüssel zu senden.
</li>
<li><strong>Name</strong> (optional) - zur leichteren Zuordnung Ihrer Überweisung.</li>
<li>
<strong>Währung und Betrag</strong> - anhand der Spracheinstellung/Zeitzone Ihres Browsers auf Ihrem
Gerät ermittelt (keine Standortabfrage).
</li>
<li>
<strong>IP-Adresse und technische Anti-Bot-Signale</strong> - zusammen mit der Anfrage gespeichert,
um Missbrauch und Spam zu verhindern.
</li>
<li>
<strong>Zahlungsreferenz</strong> (z. B. DRZ-XXXXXX) - je Anfrage erzeugt, um Ihre Überweisung Ihrer
Anmeldung zuzuordnen.
</li>
</ul>
<p>
Die Überweisungsdaten (Bankverbindung und Referenz) senden wir Ihnen per E-Mail statt sie auf
der Seite anzuzeigen. Rechtsgrundlagen: Erfüllung der Vereinbarung (Art. 6 Abs. 1 lit. b DSGVO)
und für die Missbrauchsabwehr berechtigtes Interesse (Art. 6 Abs. 1 lit. f DSGVO).
</p>
<h3>Banküberweisung</h3>
<p>
Beiträge werden per gewöhnlicher Banküberweisung gezahlt. Ihre und unsere Bank verarbeiten die
Überweisungsdaten in eigener Verantwortung; auf unserem Kontoauszug sehen wir die üblichen
Angaben (Name, Kontonummer, Betrag, Referenz). Wir nutzen sie nur zur Zuordnung Ihres Beitrags
und sind nach handels- und steuerrechtlichen Vorschriften zur Aufbewahrung verpflichtet (Art. 6
Abs. 1 lit. c DSGVO).
</p>
<h3>Zugangsschlüssel und Prüfung</h3>
<p>
Nach Zuordnung Ihrer Überweisung (manuell, meist innerhalb von 24 Stunden) erhalten Sie den
Zugangsschlüssel per E-Mail. Wenn Sie ihn in Drizz.li einfügen, sendet Ihr Browser den Schlüssel
an unseren Prüfendpunkt (support.drizz.li), um seine Gültigkeit zu prüfen; die Antwort enthält
nur Gültigkeit, Stufe und Ablauf. Die Abonnentenliste (Schlüssel, E-Mail, Ablauf) liegt auf
unserem Server, solange Ihr Zugang aktiv ist.
</p>
<h3>E-Mail</h3>
<p>
Transaktions-E-Mails (Überweisungsdaten, Zugangsschlüssel, Verlängerungen) versenden wir über
unseren E-Mail-Anbieter Strato (Strato AG, Deutschland) als Auftragsverarbeiter. Newsletter oder
Werbe-E-Mails versenden wir nicht.
</p>
<h2>4. Speicherdauer</h2>
<ul>
<li>Nicht bezahlte Anmeldungen werden spätestens nach 6 Monaten gelöscht.</li>
<li>
Unterstützerdaten (Schlüssel, E-Mail, Ablauf) werden für die Dauer Ihres Zugangs aufbewahrt
und innerhalb von 12 Monaten nach dessen Ablauf gelöscht, sofern Sie keine frühere Löschung
wünschen.
</li>
<li>
Buchhaltungsunterlagen (Kontoauszüge mit Ihrer Überweisung) werden für die gesetzliche
Aufbewahrungsfrist gespeichert (je nach Rechtsordnung bis zu 10 Jahre).
</li>
</ul>
<h2>5. Empfänger</h2>
<p>
Wir verkaufen personenbezogene Daten nicht und geben sie nicht für eigene Zwecke Dritter weiter.
Empfänger sind ausschliesslich: Open-Meteo (Wetterabrufe direkt durch Ihren Browser), unser
Hosting-Anbieter, unser E-Mail-Anbieter (Strato) und die an der Überweisung beteiligten Banken.
</p>
<h2>6. Ihre Rechte</h2>
<p>
Nach der DSGVO haben Sie das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der
Verarbeitung, Datenübertragbarkeit sowie das Recht, einer auf berechtigtem Interesse beruhenden
Verarbeitung zu widersprechen. Beruht eine Verarbeitung auf Einwilligung, können Sie diese
jederzeit widerrufen. Zur Ausübung schreiben Sie an
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>. Ausserdem haben
Sie das Recht, sich bei einer Aufsichtsbehörde zu beschweren, insbesondere im EU-Mitgliedstaat
Ihres Wohnsitzes, oder bei [zuständige Aufsichtsbehörde des Verantwortlichen].
</p>
<p>
Für Besucherinnen und Besucher aus der Schweiz gelten die entsprechenden Rechte nach dem
Bundesgesetz über den Datenschutz (DSG) sinngemäss.
</p>
<h2>7. Änderungen</h2>
<p>
Wir aktualisieren diese Erklärung, wenn sich der Dienst ändert; das Datum oben nennt die letzte
Fassung.
</p>
<p class="text-muted-foreground">
Massgeblich ist die englische Fassung dieser Datenschutzerklärung.
</p>
</ProsePage>
+145
View File
@@ -0,0 +1,145 @@
<script lang="ts">
import ProsePage from '$lib/components/prose-page.svelte';
</script>
<ProsePage title="Privacy policy" subtitle="Last updated: 1 August 2026">
<p>
Drizz.li is built to need as little of your data as possible: there are no user accounts, no
cookies, no advertising and no analytics or tracking scripts. This page explains what little
processing does happen - when you browse the site, and when you support the project with a
contribution.
</p>
<h2>1. Controller</h2>
<address>
Vincent van der Wal<br />
Tschalungasse 2, CH-6442 Gersau, Switzerland <br />
Email:
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
</address>
<p>
This policy covers the websites drizz.li (the weather app) and support.drizz.li (the supporter
signup page).
</p>
<h2>2. Browsing the site</h2>
<h3>Weather data requests</h3>
<p>
Drizz.li is a static site: when you open a forecast, your browser fetches the weather data
directly from the open-data APIs of
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>
(api.open-meteo.com, geocoding-api.open-meteo.com, archive-api.open-meteo.com, ensemble-api.open-meteo.com,
and map tiles from maps.open-meteo.com / map-tiles.open-meteo.com / map-assets.open-meteo.com). Like
any web request, this transmits your IP address and the requested location or search term to Open-Meteo.
We do not receive or store any of this. See
<a href="https://open-meteo.com/en/terms" target="_blank" rel="noopener"
>Open-Meteo's terms and privacy information</a
>. Legal basis: our legitimate interest in delivering the content you request (Art. 6(1)(f)
GDPR).
</p>
<h3>Hosting</h3>
<p>
The static site is served by [hosting provider, location]. The hosting infrastructure may keep
short-lived technical server logs (IP address, requested URL, timestamp) for security and
operations. Legal basis: legitimate interest in a secure, reliable service (Art. 6(1)(f) GDPR).
</p>
<h3>Settings on your device (local storage)</h3>
<p>
Your preferences - theme, measurement units, last searched location, and (for supporters) your
access key and its last verification result - are stored only in your browser's local storage.
They never leave your device except as described below for key verification, and you can clear
them at any time via your browser.
</p>
<h2>3. Supporting the project (contributions)</h2>
<p>
Supporter contributions unlock the supporter extras, so legally they are a paid agreement, not a
pure gift - and this is what we process to handle them:
</p>
<h3>Signup form</h3>
<ul>
<li>
<strong>Email address</strong> (required) - to send you the transfer details and, after your contribution
arrives, your access key.
</li>
<li><strong>Name</strong> (optional) - to help match your bank transfer.</li>
<li>
<strong>Currency and amount</strong> - shown based on your browser's locale/timezone, detected on
your device (no geolocation request is made).
</li>
<li>
<strong>IP address and technical anti-bot signals</strong> - kept with the signup request to prevent
abuse and spam of the form.
</li>
<li>
<strong>Payment reference</strong> (e.g. DRZ-XXXXXX) - generated per request to match your transfer
to your signup.
</li>
</ul>
<p>
The transfer details (bank account and reference) are sent to you by email rather than shown on
the page. Legal bases: performance of the agreement (Art. 6(1)(b) GDPR) and, for the anti-abuse
measures, legitimate interest (Art. 6(1)(f) GDPR).
</p>
<h3>Bank transfer</h3>
<p>
Contributions are paid by ordinary bank transfer. Your bank and ours process the transfer data
under their own responsibility; on our bank statement we see the usual transfer details (your
name, account number, amount, reference). We use them only to match your contribution and are
required to retain accounting records under statutory bookkeeping and tax law (Art. 6(1)(c)
GDPR).
</p>
<h3>Access key and verification</h3>
<p>
After your transfer is matched (manually, usually within 24 hours), you receive an access key by
email. When you paste it into Drizz.li, your browser sends the key to our verification endpoint
(support.drizz.li) to check whether it is active; the response contains only the validity, tier
and expiry. The subscriber list (key, email, expiry) is stored on our server for as long as your
access is active.
</p>
<h3>Email</h3>
<p>
Transactional emails (transfer details, access key, renewals) are sent through our email
provider, Strato (Strato AG, Germany), acting as a processor. We send no newsletters or
marketing email.
</p>
<h2>4. Retention</h2>
<ul>
<li>Signup requests that are never paid are deleted after 6 months at the latest.</li>
<li>
Supporter records (key, email, expiry) are kept while your access is active and deleted within
12 months after it expires, unless you ask us to delete them sooner.
</li>
<li>
Accounting records (bank statements showing your transfer) are kept for the statutory
retention period (up to 10 years, depending on jurisdiction).
</li>
</ul>
<h2>5. Recipients</h2>
<p>
We do not sell or share personal data with third parties for their own purposes. Recipients are
limited to: Open-Meteo (weather requests made directly by your browser), our hosting provider,
our email provider (Strato), and the banks involved in your transfer.
</p>
<h2>6. Your rights</h2>
<p>
Under the GDPR you have the right to access, rectification, erasure, restriction of processing,
data portability, and to object to processing based on legitimate interest. Where processing is
based on consent, you may withdraw it at any time. To exercise any of these rights, email
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>. You also have the
right to lodge a complaint with a supervisory authority, in particular in the EU member state of
your residence, or with [competent supervisory authority of the controller].
</p>
<p>
For visitors from Switzerland: the corresponding rights under the Swiss Federal Act on Data
Protection (FADP) apply equivalently.
</p>
<h2>7. Changes</h2>
<p>
We may update this policy when the service changes; the date above reflects the latest revision.
</p>
</ProsePage>
+149
View File
@@ -0,0 +1,149 @@
<script lang="ts">
import ProsePage from '$lib/components/prose-page.svelte';
</script>
<ProsePage title="Política de privacidad" subtitle="Última actualización: 1 de agosto de 2026">
<p>
Drizz.li está hecho para necesitar los mínimos datos posibles: sin cuentas, sin cookies, sin
publicidad y sin scripts de analítica ni de seguimiento. Esta página explica el poco tratamiento
que sí ocurre: al navegar por el sitio y al apoyar el proyecto con una aportación.
</p>
<h2>1. Responsable</h2>
<address>
Vincent van der Wal<br />
Tschalungasse 2, CH-6442 Gersau, Switzerland <br />
Correo electrónico:
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
</address>
<p>
Esta política cubre los sitios drizz.li (la app meteorológica) y support.drizz.li (la página de
registro de colaboradores).
</p>
<h2>2. Navegación por el sitio</h2>
<h3>Peticiones de datos meteorológicos</h3>
<p>
Drizz.li es un sitio estático: cuando abres un pronóstico, tu navegador obtiene los datos
directamente de las API abiertas de
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>. Como en
cualquier petición web, esto transmite tu dirección IP y la ubicación o término buscado a
Open-Meteo. Nosotros no recibimos ni almacenamos nada de eso. Consulta
<a href="https://open-meteo.com/en/terms" target="_blank" rel="noopener"
>las condiciones e información de privacidad de Open-Meteo</a
>. Base jurídica: nuestro interés legítimo en entregar el contenido que solicitas (art. 6.1.f
del RGPD).
</p>
<h3>Alojamiento</h3>
<p>
El sitio estático lo sirve [proveedor de alojamiento, ubicación]. La infraestructura puede
conservar registros técnicos de corta duración (dirección IP, URL solicitada, marca de tiempo)
por seguridad y operación. Base jurídica: interés legítimo en un servicio seguro y fiable (art.
6.1.f del RGPD).
</p>
<h3>Ajustes en tu dispositivo (almacenamiento local)</h3>
<p>
Tus preferencias - tema, unidades, última ubicación buscada y (para colaboradores) tu clave de
acceso y su última verificación - se guardan únicamente en el almacenamiento local de tu
navegador. No salen de tu dispositivo salvo como se describe abajo para verificar la clave, y
puedes borrarlas cuando quieras.
</p>
<h2>3. Apoyar el proyecto (aportaciones)</h2>
<p>
Las aportaciones desbloquean los extras, así que legalmente son un acuerdo remunerado y no una
donación pura; esto es lo que tratamos para gestionarlas:
</p>
<h3>Formulario de registro</h3>
<ul>
<li>
<strong>Dirección de correo</strong> (obligatoria) - para enviarte los datos de la transferencia
y, cuando llegue tu aportación, tu clave de acceso.
</li>
<li><strong>Nombre</strong> (opcional) - para ayudar a casar tu transferencia.</li>
<li>
<strong>Moneda e importe</strong> - determinados por la configuración regional/zona horaria de tu
navegador, en tu dispositivo (no se solicita geolocalización).
</li>
<li>
<strong>Dirección IP y señales técnicas anti-bots</strong> - guardadas con la solicitud para evitar
abusos y spam.
</li>
<li>
<strong>Referencia de pago</strong> (p. ej. DRZ-XXXXXX) - generada por solicitud para casar tu transferencia
con tu registro.
</li>
</ul>
<p>
Los datos de la transferencia (cuenta y referencia) se te envían por correo en lugar de
mostrarse en la página. Bases jurídicas: ejecución del contrato (art. 6.1.b del RGPD) y, para
las medidas antiabuso, interés legítimo (art. 6.1.f del RGPD).
</p>
<h3>Transferencia bancaria</h3>
<p>
Las aportaciones se pagan por transferencia bancaria ordinaria. Tu banco y el nuestro tratan los
datos bajo su propia responsabilidad; en nuestro extracto vemos los datos habituales (nombre,
número de cuenta, importe, referencia). Solo los usamos para casar tu aportación y debemos
conservar los registros contables por obligación legal (art. 6.1.c del RGPD).
</p>
<h3>Clave de acceso y verificación</h3>
<p>
Una vez casada tu transferencia (manualmente, normalmente en 24 horas), recibes una clave de
acceso por correo. Al pegarla en Drizz.li, tu navegador envía la clave a nuestro punto de
verificación (support.drizz.li) para comprobar si está activa; la respuesta contiene solo
validez, nivel y caducidad. La lista de suscriptores (clave, correo, caducidad) se guarda en
nuestro servidor mientras tu acceso esté activo.
</p>
<h3>Correo electrónico</h3>
<p>
Los correos transaccionales (datos de transferencia, clave de acceso, renovaciones) se envían a
través de nuestro proveedor Strato (Strato AG, Alemania), que actúa como encargado del
tratamiento. No enviamos boletines ni correo comercial.
</p>
<h2>4. Conservación</h2>
<ul>
<li>Las solicitudes de registro que nunca se pagan se eliminan como máximo a los 6 meses.</li>
<li>
Los registros de colaborador (clave, correo, caducidad) se conservan mientras tu acceso esté
activo y se eliminan en los 12 meses siguientes a su expiración, salvo que pidas borrarlos
antes.
</li>
<li>
Los registros contables (extractos que muestran tu transferencia) se conservan durante el
plazo legal (hasta 10 años según la jurisdicción).
</li>
</ul>
<h2>5. Destinatarios</h2>
<p>
No vendemos ni compartimos datos personales con terceros para sus propios fines. Los
destinatarios se limitan a: Open-Meteo (peticiones meteorológicas hechas directamente por tu
navegador), nuestro proveedor de alojamiento, nuestro proveedor de correo (Strato) y los bancos
implicados en tu transferencia.
</p>
<h2>6. Tus derechos</h2>
<p>
Conforme al RGPD tienes derecho de acceso, rectificación, supresión, limitación del tratamiento,
portabilidad y oposición al tratamiento basado en el interés legítimo. Cuando el tratamiento se
base en el consentimiento, puedes retirarlo en cualquier momento. Para ejercer estos derechos,
escribe a
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>. También tienes
derecho a presentar una reclamación ante una autoridad de control, en particular en el Estado
miembro de tu residencia, o ante [autoridad de control competente].
</p>
<p>
Para visitantes desde Suiza, se aplican de forma equivalente los derechos correspondientes de la
Ley Federal de Protección de Datos (LPD).
</p>
<h2>7. Cambios</h2>
<p>
Podemos actualizar esta política cuando cambie el servicio; la fecha de arriba refleja la última
revisión.
</p>
<p class="text-muted-foreground">
En caso de discrepancia prevalece la versión inglesa de esta política.
</p>
</ProsePage>
+151
View File
@@ -0,0 +1,151 @@
<script lang="ts">
import ProsePage from '$lib/components/prose-page.svelte';
</script>
<ProsePage title="Politique de confidentialité" subtitle="Dernière mise à jour : 1er août 2026">
<p>
Drizz.li est conçu pour avoir besoin du minimum de vos données : aucun compte, aucun cookie,
aucune publicité, aucun script d'analyse ou de suivi. Cette page explique le peu de traitement
qui a lieu - lorsque vous consultez le site et lorsque vous soutenez le projet par une
contribution.
</p>
<h2>1. Responsable du traitement</h2>
<address>
Vincent van der Wal<br />
Tschalungasse 2, CH-6442 Gersau, Switzerland <br />
E-mail:
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
</address>
<p>
Cette politique couvre les sites drizz.li (l'application météo) et support.drizz.li (la page
d'inscription contributeur).
</p>
<h2>2. Navigation sur le site</h2>
<h3>Requêtes de données météo</h3>
<p>
Drizz.li est un site statique : lorsque vous ouvrez une prévision, votre navigateur récupère les
données directement auprès des API ouvertes d'
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>. Comme pour toute
requête web, cela transmet votre adresse IP et le lieu ou le terme recherché à Open-Meteo. Nous
n'en recevons ni n'en conservons rien. Voir
<a href="https://open-meteo.com/en/terms" target="_blank" rel="noopener"
>les conditions et informations de confidentialité d'Open-Meteo</a
>. Base légale : notre intérêt légitime à fournir le contenu demandé (art. 6, par. 1, point f)
du RGPD).
</p>
<h3>Hébergement</h3>
<p>
Le site statique est servi par [hébergeur, localisation]. L'infrastructure d'hébergement peut
conserver de brefs journaux techniques (adresse IP, URL demandée, horodatage) à des fins de
sécurité et d'exploitation. Base légale : intérêt légitime à un service sûr et fiable (art. 6,
par. 1, point f) du RGPD).
</p>
<h3>Réglages sur votre appareil (stockage local)</h3>
<p>
Vos préférences - thème, unités, dernier lieu recherché et (pour les contributeurs) votre clé
d'accès et son dernier résultat de vérification - sont stockées uniquement dans le stockage
local de votre navigateur. Elles ne quittent pas votre appareil, sauf comme décrit ci-dessous
pour la vérification de la clé, et vous pouvez les effacer à tout moment.
</p>
<h2>3. Soutenir le projet (contributions)</h2>
<p>
Les contributions débloquent les bonus : juridiquement il s'agit donc d'un accord payant et non
d'un simple don - voici ce que nous traitons pour les gérer :
</p>
<h3>Formulaire d'inscription</h3>
<ul>
<li>
<strong>Adresse e-mail</strong> (obligatoire) - pour vous envoyer les coordonnées bancaires puis,
à réception de votre contribution, votre clé d'accès.
</li>
<li><strong>Nom</strong> (facultatif) - pour faciliter le rapprochement de votre virement.</li>
<li>
<strong>Devise et montant</strong> - déterminés d'après la langue/le fuseau horaire de votre navigateur,
sur votre appareil (aucune demande de géolocalisation).
</li>
<li>
<strong>Adresse IP et signaux anti-robots techniques</strong> - conservés avec la demande pour prévenir
les abus et le spam.
</li>
<li>
<strong>Référence de paiement</strong> (p. ex. DRZ-XXXXXX) - générée par demande pour rapprocher
votre virement de votre inscription.
</li>
</ul>
<p>
Les coordonnées bancaires (compte et référence) vous sont envoyées par e-mail plutôt
qu'affichées sur la page. Bases légales : exécution du contrat (art. 6, par. 1, point b) du
RGPD) et, pour les mesures anti-abus, intérêt légitime (art. 6, par. 1, point f) du RGPD).
</p>
<h3>Virement bancaire</h3>
<p>
Les contributions sont réglées par virement bancaire ordinaire. Votre banque et la nôtre
traitent les données du virement sous leur propre responsabilité ; sur notre relevé figurent les
informations habituelles (nom, numéro de compte, montant, référence). Nous ne les utilisons que
pour rapprocher votre contribution et devons conserver les pièces comptables au titre des
obligations légales (art. 6, par. 1, point c) du RGPD).
</p>
<h3>Clé d'accès et vérification</h3>
<p>
Une fois votre virement rapproché (manuellement, en général sous 24 heures), vous recevez une
clé d'accès par e-mail. Lorsque vous la collez dans Drizz.li, votre navigateur envoie la clé à
notre point de vérification (support.drizz.li) pour savoir si elle est active ; la réponse ne
contient que la validité, le niveau et l'échéance. La liste des abonnés (clé, e-mail, échéance)
est conservée sur notre serveur tant que votre accès est actif.
</p>
<h3>E-mail</h3>
<p>
Les e-mails transactionnels (coordonnées bancaires, clé d'accès, renouvellements) sont envoyés
via notre prestataire Strato (Strato AG, Allemagne), agissant comme sous-traitant. Nous
n'envoyons ni newsletter ni e-mail marketing.
</p>
<h2>4. Conservation</h2>
<ul>
<li>Les demandes d'inscription jamais payées sont supprimées au plus tard après 6 mois.</li>
<li>
Les données contributeur (clé, e-mail, échéance) sont conservées tant que votre accès est
actif et supprimées dans les 12 mois suivant son expiration, sauf demande de suppression
anticipée.
</li>
<li>
Les pièces comptables (relevés bancaires mentionnant votre virement) sont conservées pendant
la durée légale (jusqu'à 10 ans selon la juridiction).
</li>
</ul>
<h2>5. Destinataires</h2>
<p>
Nous ne vendons ni ne partageons de données personnelles avec des tiers pour leurs propres
finalités. Les destinataires se limitent à : Open-Meteo (requêtes météo effectuées directement
par votre navigateur), notre hébergeur, notre prestataire e-mail (Strato) et les banques
impliquées dans votre virement.
</p>
<h2>6. Vos droits</h2>
<p>
En vertu du RGPD, vous disposez d'un droit d'accès, de rectification, d'effacement, de
limitation du traitement, de portabilité, et d'opposition au traitement fondé sur l'intérêt
légitime. Lorsque le traitement repose sur le consentement, vous pouvez le retirer à tout
moment. Pour exercer ces droits, écrivez à
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>. Vous avez
également le droit d'introduire une réclamation auprès d'une autorité de contrôle, notamment
dans l'État membre de votre résidence, ou auprès de [autorité de contrôle compétente].
</p>
<p>
Pour les visiteurs suisses, les droits correspondants de la loi fédérale sur la protection des
données (LPD) s'appliquent de manière équivalente.
</p>
<h2>7. Modifications</h2>
<p>
Nous pouvons mettre à jour cette politique lorsque le service évolue ; la date ci-dessus indique
la dernière révision.
</p>
<p class="text-muted-foreground">
En cas de divergence, la version anglaise de cette politique fait foi.
</p>
</ProsePage>
+149
View File
@@ -0,0 +1,149 @@
<script lang="ts">
import ProsePage from '$lib/components/prose-page.svelte';
</script>
<ProsePage title="Informativa sulla privacy" subtitle="Ultimo aggiornamento: 1 agosto 2026">
<p>
Drizz.li è costruito per aver bisogno del minimo dei tuoi dati: nessun account, nessun cookie,
nessuna pubblicità, nessuno script di analisi o tracciamento. Questa pagina spiega il poco
trattamento che avviene comunque: quando navighi sul sito e quando sostieni il progetto con un
contributo.
</p>
<h2>1. Titolare del trattamento</h2>
<address>
Vincent van der Wal<br />
Tschalungasse 2, CH-6442 Gersau, Switzerland <br />
E-mail:
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
</address>
<p>
Questa informativa copre i siti drizz.li (l'app meteo) e support.drizz.li (la pagina di
iscrizione per i sostenitori).
</p>
<h2>2. Navigazione sul sito</h2>
<h3>Richieste di dati meteo</h3>
<p>
Drizz.li è un sito statico: quando apri una previsione, il tuo browser scarica i dati
direttamente dalle API aperte di
<a href="https://open-meteo.com" target="_blank" rel="noopener">Open-Meteo</a>. Come per
qualsiasi richiesta web, questo trasmette il tuo indirizzo IP e la località o il termine cercato
a Open-Meteo. Noi non riceviamo né conserviamo nulla di tutto ciò. Vedi
<a href="https://open-meteo.com/en/terms" target="_blank" rel="noopener"
>le condizioni e le informazioni sulla privacy di Open-Meteo</a
>. Base giuridica: il nostro legittimo interesse a fornire il contenuto richiesto (art. 6, par.
1, lett. f del GDPR).
</p>
<h3>Hosting</h3>
<p>
Il sito statico è servito da [fornitore di hosting, sede]. L'infrastruttura può conservare brevi
log tecnici del server (indirizzo IP, URL richiesto, data e ora) per sicurezza e gestione. Base
giuridica: legittimo interesse a un servizio sicuro e affidabile (art. 6, par. 1, lett. f del
GDPR).
</p>
<h3>Impostazioni sul tuo dispositivo (archiviazione locale)</h3>
<p>
Le tue preferenze - tema, unità di misura, ultima località cercata e (per i sostenitori) la
chiave di accesso e l'ultimo esito della verifica - sono salvate solo nell'archiviazione locale
del browser. Non lasciano il tuo dispositivo, salvo quanto descritto sotto per la verifica della
chiave, e puoi cancellarle quando vuoi.
</p>
<h2>3. Sostenere il progetto (contributi)</h2>
<p>
I contributi sbloccano gli extra, quindi giuridicamente sono un accordo a pagamento e non una
pura donazione: ecco cosa trattiamo per gestirli:
</p>
<h3>Modulo di iscrizione</h3>
<ul>
<li>
<strong>Indirizzo e-mail</strong> (obbligatorio) - per inviarti i dati del bonifico e, all'arrivo
del contributo, la chiave di accesso.
</li>
<li><strong>Nome</strong> (facoltativo) - per facilitare l'abbinamento del bonifico.</li>
<li>
<strong>Valuta e importo</strong> - determinati dalle impostazioni locali/fuso orario del browser,
sul tuo dispositivo (nessuna richiesta di geolocalizzazione).
</li>
<li>
<strong>Indirizzo IP e segnali tecnici anti-bot</strong> - conservati con la richiesta per prevenire
abusi e spam.
</li>
<li>
<strong>Riferimento di pagamento</strong> (es. DRZ-XXXXXX) - generato per ogni richiesta per abbinare
il bonifico all'iscrizione.
</li>
</ul>
<p>
I dati del bonifico (conto e riferimento) ti vengono inviati via e-mail anziché mostrati sulla
pagina. Basi giuridiche: esecuzione del contratto (art. 6, par. 1, lett. b del GDPR) e, per le
misure anti-abuso, legittimo interesse (art. 6, par. 1, lett. f del GDPR).
</p>
<h3>Bonifico bancario</h3>
<p>
I contributi si pagano con un normale bonifico. La tua banca e la nostra trattano i dati del
bonifico sotto la propria responsabilità; sul nostro estratto conto vediamo i dati consueti
(nome, numero di conto, importo, riferimento). Li usiamo solo per abbinare il contributo e siamo
tenuti a conservare le scritture contabili per obbligo di legge (art. 6, par. 1, lett. c del
GDPR).
</p>
<h3>Chiave di accesso e verifica</h3>
<p>
Dopo l'abbinamento del bonifico (manuale, di solito entro 24 ore) ricevi una chiave di accesso
via e-mail. Quando la incolli in Drizz.li, il browser invia la chiave al nostro endpoint di
verifica (support.drizz.li) per controllare se è attiva; la risposta contiene solo validità,
livello e scadenza. L'elenco dei sostenitori (chiave, e-mail, scadenza) resta sul nostro server
finché il tuo accesso è attivo.
</p>
<h3>E-mail</h3>
<p>
Le e-mail transazionali (dati del bonifico, chiave di accesso, rinnovi) sono inviate tramite il
nostro fornitore Strato (Strato AG, Germania), che agisce come responsabile del trattamento. Non
inviamo newsletter né e-mail di marketing.
</p>
<h2>4. Conservazione</h2>
<ul>
<li>Le richieste di iscrizione mai pagate sono cancellate al più tardi dopo 6 mesi.</li>
<li>
I dati dei sostenitori (chiave, e-mail, scadenza) sono conservati finché l'accesso è attivo e
cancellati entro 12 mesi dalla scadenza, salvo richiesta di cancellazione anticipata.
</li>
<li>
Le scritture contabili (estratti conto con il tuo bonifico) sono conservate per il periodo di
legge (fino a 10 anni a seconda della giurisdizione).
</li>
</ul>
<h2>5. Destinatari</h2>
<p>
Non vendiamo né condividiamo dati personali con terzi per finalità proprie. I destinatari si
limitano a: Open-Meteo (richieste meteo fatte direttamente dal tuo browser), il nostro fornitore
di hosting, il nostro fornitore e-mail (Strato) e le banche coinvolte nel bonifico.
</p>
<h2>6. I tuoi diritti</h2>
<p>
In base al GDPR hai diritto di accesso, rettifica, cancellazione, limitazione del trattamento,
portabilità dei dati e opposizione al trattamento fondato sul legittimo interesse. Se il
trattamento si basa sul consenso, puoi revocarlo in qualsiasi momento. Per esercitare questi
diritti scrivi a
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>. Hai inoltre il
diritto di proporre reclamo a un'autorità di controllo, in particolare nello Stato membro di
residenza, o a [autorità di controllo competente].
</p>
<p>
Per i visitatori dalla Svizzera valgono in modo equivalente i corrispondenti diritti previsti
dalla Legge federale sulla protezione dei dati (LPD).
</p>
<h2>7. Modifiche</h2>
<p>
Possiamo aggiornare questa informativa quando il servizio cambia; la data in alto indica
l'ultima revisione.
</p>
<p class="text-muted-foreground">
In caso di discrepanza prevale la versione inglese di questa informativa.
</p>
</ProsePage>
+8 -79
View File
@@ -1,82 +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>
<!-- TODO(vincent): confirm the governing-law country in section 8. -->
<ProsePage title="Supporter terms" subtitle="Last updated: 1 August 2026">
<p>
Drizz.li is free to use. These terms cover the optional supporter contribution: a small payment
that helps keep the project running and, as a thank-you, unlocks the supporter extras. Although
we call it a contribution, features are unlocked in return.
</p>
<h2>1. What you get</h2>
<p>
A supporter contribution unlocks the supporter extras for the paid period: currently historical
weather with climate-normal comparisons, plus new supporter features as they land. The free
parts of Drizz.li stay free for everyone.
</p>
<h2>2. How it works</h2>
<ul>
<li>
You request the transfer details on the signup page; we email them to you together with a
personal payment reference.
</li>
<li>
You transfer the amount (from €3 / $3 / CHF 3 per month) by ordinary bank transfer, including
the reference.
</li>
<li>
Transfers are matched manually. Once your contribution arrives - usually within 24 hours, at
most a few days - you receive a personal access key by email.
</li>
<li>
You paste the key into Drizz.li once; it is stored on your device and verified automatically.
</li>
</ul>
<h2>3. No auto-renewal</h2>
<p>
Contributions are one-off and prepaid: nothing renews automatically and we never charge you.
When your period ends, the extras simply lock again; contributing again with the same email
extends your existing key.
</p>
<h2>4. Withdrawal and refunds</h2>
<p>
If you are a consumer in the EU/EEA, you have a statutory 14-day right of withdrawal. Beyond
that, we keep it simple: if you are unhappy for any reason within 14 days of receiving your
access key, email
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a> and we will refund your
contribution in full.
</p>
<h2>5. Fair use</h2>
<p>
The access key is personal. Please don't publish or share it; keys that are clearly abused (e.g.
shared publicly) may be revoked. If your key is revoked without cause, you are entitled to a
pro-rata refund.
</p>
<h2>6. Service and availability</h2>
<p>
Drizz.li is a personal open-source project, provided as-is. We work to keep it available and
accurate, but we cannot guarantee uninterrupted availability or the correctness of forecasts -
weather data is informational only (see the disclaimer in the imprint). If the supporter extras
become permanently unavailable during a period you paid for, we will refund the remaining period
on request.
</p>
<h2>7. Data</h2>
<p>
How we handle your data (email, payment reference, bank transfer details) is described in the
<a href="/legal/privacy/">privacy policy</a>.
</p>
<h2>8. Governing law</h2>
<p>
These terms are governed by the law of [Switzerland], without prejudice to mandatory consumer
protection provisions of your country of residence.
</p>
</ProsePage>
<LocalizedContent variants={{ en, de, es, fr, it }} />
+87
View File
@@ -0,0 +1,87 @@
<script lang="ts">
import ProsePage from '$lib/components/prose-page.svelte';
import { href } from '$lib/i18n';
</script>
<ProsePage title="Unterstützerbedingungen" subtitle="Zuletzt aktualisiert: 1. August 2026">
<p>
Drizz.li ist kostenlos nutzbar. Diese Bedingungen betreffen den freiwilligen
Unterstützerbeitrag: eine kleine Zahlung, die das Projekt am Laufen hält und als Dankeschön die
Unterstützer-Extras freischaltet. Auch wenn wir von einem Beitrag sprechen, werden dafür
Funktionen freigeschaltet.
</p>
<h2>1. Was Sie erhalten</h2>
<p>
Ein Unterstützerbeitrag schaltet die Extras für den bezahlten Zeitraum frei: derzeit
historisches Wetter mit Vergleich zu den Klimanormalen sowie neue Funktionen, sobald sie
erscheinen. Die kostenlosen Teile von Drizz.li bleiben für alle frei.
</p>
<h2>2. Ablauf</h2>
<ul>
<li>
Sie fordern die Überweisungsdaten auf der Anmeldeseite an; wir senden sie Ihnen zusammen mit
einer persönlichen Zahlungsreferenz per E-Mail.
</li>
<li>
Sie überweisen den Betrag (ab 3 € / 3 $ / 3 CHF pro Monat) per gewöhnlicher Banküberweisung
unter Angabe der Referenz.
</li>
<li>
Überweisungen werden manuell zugeordnet. Sobald Ihr Beitrag eingeht - meist innerhalb von 24
Stunden, höchstens einige Tage - erhalten Sie per E-Mail einen persönlichen Zugangsschlüssel.
</li>
<li>
Sie fügen den Schlüssel einmal in Drizz.li ein; er wird auf Ihrem Gerät gespeichert und
automatisch überprüft.
</li>
</ul>
<h2>3. Keine automatische Verlängerung</h2>
<p>
Beiträge sind einmalig und im Voraus bezahlt: Nichts verlängert sich automatisch und wir
belasten Sie nie. Läuft Ihr Zeitraum ab, sperren sich die Extras einfach wieder; ein erneuter
Beitrag mit derselben E-Mail verlängert Ihren bestehenden Schlüssel.
</p>
<h2>4. Widerruf und Erstattung</h2>
<p>
Als Verbraucherin oder Verbraucher in der EU/im EWR haben Sie ein gesetzliches 14-tägiges
Widerrufsrecht. Darüber hinaus halten wir es einfach: Sind Sie innerhalb von 14 Tagen nach
Erhalt Ihres Zugangsschlüssels aus irgendeinem Grund unzufrieden, schreiben Sie an
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
und wir erstatten Ihren Beitrag vollständig.
</p>
<h2>5. Faire Nutzung</h2>
<p>
Der Zugangsschlüssel ist persönlich. Bitte veröffentlichen oder teilen Sie ihn nicht; eindeutig
missbrauchte Schlüssel (z. B. öffentlich geteilt) können gesperrt werden. Wird Ihr Schlüssel
ohne Grund gesperrt, haben Sie Anspruch auf eine anteilige Erstattung.
</p>
<h2>6. Dienst und Verfügbarkeit</h2>
<p>
Drizz.li ist ein persönliches Open-Source-Projekt und wird ohne Gewähr bereitgestellt. Wir
bemühen uns um Verfügbarkeit und Genauigkeit, können aber weder unterbrechungsfreien Betrieb
noch die Richtigkeit der Vorhersagen garantieren - Wetterdaten sind rein informativ (siehe
Haftungsausschluss im Impressum). Sollten die Unterstützer-Extras während eines bezahlten
Zeitraums dauerhaft ausfallen, erstatten wir auf Anfrage den Restzeitraum.
</p>
<h2>7. Daten</h2>
<p>
Wie wir Ihre Daten (E-Mail, Zahlungsreferenz, Überweisungsdaten) verarbeiten, steht in der
<a href={href('/legal/privacy')}>Datenschutzerklärung</a>.
</p>
<h2>8. Anwendbares Recht</h2>
<p>
Für diese Bedingungen gilt das Recht der [Schweiz], unbeschadet zwingender
Verbraucherschutzvorschriften Ihres Wohnsitzlandes.
</p>
<p class="text-muted-foreground">Massgeblich ist die englische Fassung dieser Bedingungen.</p>
</ProsePage>
+83
View File
@@ -0,0 +1,83 @@
<script lang="ts">
import ProsePage from '$lib/components/prose-page.svelte';
import { href } from '$lib/i18n';
</script>
<ProsePage title="Supporter terms" subtitle="Last updated: 1 August 2026">
<p>
Drizz.li is free to use. These terms cover the optional supporter contribution: a small payment
that helps keep the project running and, as a thank-you, unlocks the supporter extras. Although
we call it a contribution, features are unlocked in return.
</p>
<h2>1. What you get</h2>
<p>
A supporter contribution unlocks the supporter extras for the paid period: currently historical
weather with climate-normal comparisons, plus new supporter features as they land. The free
parts of Drizz.li stay free for everyone.
</p>
<h2>2. How it works</h2>
<ul>
<li>
You request the transfer details on the signup page; we email them to you together with a
personal payment reference.
</li>
<li>
You transfer the amount (from €3 / $3 / CHF 3 per month) by ordinary bank transfer, including
the reference.
</li>
<li>
Transfers are matched manually. Once your contribution arrives - usually within 24 hours, at
most a few days - you receive a personal access key by email.
</li>
<li>
You paste the key into Drizz.li once; it is stored on your device and verified automatically.
</li>
</ul>
<h2>3. No auto-renewal</h2>
<p>
Contributions are one-off and prepaid: nothing renews automatically and we never charge you.
When your period ends, the extras simply lock again; contributing again with the same email
extends your existing key.
</p>
<h2>4. Withdrawal and refunds</h2>
<p>
If you are a consumer in the EU/EEA, you have a statutory 14-day right of withdrawal. Beyond
that, we keep it simple: if you are unhappy for any reason within 14 days of receiving your
access key, email
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a> and we will refund your
contribution in full.
</p>
<h2>5. Fair use</h2>
<p>
The access key is personal. Please don't publish or share it; keys that are clearly abused (e.g.
shared publicly) may be revoked. If your key is revoked without cause, you are entitled to a
pro-rata refund.
</p>
<h2>6. Service and availability</h2>
<p>
Drizz.li is a personal open-source project, provided as-is. We work to keep it available and
accurate, but we cannot guarantee uninterrupted availability or the correctness of forecasts -
weather data is informational only (see the disclaimer in the imprint). If the supporter extras
become permanently unavailable during a period you paid for, we will refund the remaining period
on request.
</p>
<h2>7. Data</h2>
<p>
How we handle your data (email, payment reference, bank transfer details) is described in the
<a href={href('/legal/privacy')}>privacy policy</a>.
</p>
<h2>8. Governing law</h2>
<p>
These terms are governed by the law of [Switzerland], without prejudice to mandatory consumer
protection provisions of your country of residence.
</p>
</ProsePage>
+91
View File
@@ -0,0 +1,91 @@
<script lang="ts">
import ProsePage from '$lib/components/prose-page.svelte';
import { href } from '$lib/i18n';
</script>
<ProsePage
title="Condiciones para colaboradores"
subtitle="Última actualización: 1 de agosto de 2026"
>
<p>
Drizz.li es gratuito. Estas condiciones cubren la aportación voluntaria de colaborador: un
pequeño pago que ayuda a mantener el proyecto y que, como agradecimiento, desbloquea los extras.
Aunque lo llamamos aportación, a cambio se desbloquean funciones.
</p>
<h2>1. Qué obtienes</h2>
<p>
Una aportación desbloquea los extras durante el periodo pagado: actualmente el clima histórico
con comparación frente a las normales climáticas, además de las nuevas funciones que vayan
llegando. Las partes gratuitas de Drizz.li siguen siendo gratuitas para todos.
</p>
<h2>2. Cómo funciona</h2>
<ul>
<li>
Solicitas los datos de la transferencia en la página de registro; te los enviamos por correo
junto con una referencia de pago personal.
</li>
<li>
Transfieres el importe (desde 3 € / 3 $ / 3 CHF al mes) mediante una transferencia bancaria
ordinaria, incluyendo la referencia.
</li>
<li>
Las transferencias se concilian manualmente. Cuando llega tu aportación - normalmente en 24
horas, como mucho unos días - recibes por correo una clave de acceso personal.
</li>
<li>
Pegas la clave en Drizz.li una vez; se guarda en tu dispositivo y se verifica automáticamente.
</li>
</ul>
<h2>3. Sin renovación automática</h2>
<p>
Las aportaciones son únicas y prepagadas: nada se renueva solo y nunca te cobramos. Cuando
termina tu periodo, los extras simplemente se bloquean de nuevo; volver a aportar con el mismo
correo amplía tu clave existente.
</p>
<h2>4. Desistimiento y reembolsos</h2>
<p>
Si eres consumidor en la UE/EEE, dispones de un derecho legal de desistimiento de 14 días. Más
allá de eso, lo mantenemos sencillo: si no estás satisfecho por cualquier motivo en los 14 días
siguientes a recibir tu clave, escribe a
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
y te reembolsaremos la aportación íntegra.
</p>
<h2>5. Uso razonable</h2>
<p>
La clave de acceso es personal. No la publiques ni la compartas; las claves claramente abusadas
(por ejemplo, compartidas públicamente) pueden revocarse. Si tu clave se revoca sin causa,
tienes derecho a un reembolso proporcional.
</p>
<h2>6. Servicio y disponibilidad</h2>
<p>
Drizz.li es un proyecto personal de código abierto, ofrecido tal cual. Trabajamos para
mantenerlo disponible y preciso, pero no podemos garantizar disponibilidad ininterrumpida ni la
exactitud de los pronósticos: los datos meteorológicos son solo informativos (véase el descargo
del aviso legal). Si los extras dejan de estar disponibles de forma permanente durante un
periodo que has pagado, reembolsaremos el periodo restante a petición.
</p>
<h2>7. Datos</h2>
<p>
Cómo tratamos tus datos (correo, referencia de pago, datos de la transferencia) se describe en
la
<a href={href('/legal/privacy')}>política de privacidad</a>.
</p>
<h2>8. Legislación aplicable</h2>
<p>
Estas condiciones se rigen por la legislación de [Suiza], sin perjuicio de las disposiciones
imperativas de protección al consumidor de tu país de residencia.
</p>
<p class="text-muted-foreground">
En caso de discrepancia prevalece la versión inglesa de estas condiciones.
</p>
</ProsePage>
+89
View File
@@ -0,0 +1,89 @@
<script lang="ts">
import ProsePage from '$lib/components/prose-page.svelte';
import { href } from '$lib/i18n';
</script>
<ProsePage title="Conditions contributeur" subtitle="Dernière mise à jour : 1er août 2026">
<p>
Drizz.li est gratuit. Ces conditions portent sur la contribution facultative : un petit paiement
qui aide à faire vivre le projet et qui, en remerciement, débloque les bonus. Même si nous
parlons de contribution, des fonctionnalités sont débloquées en retour.
</p>
<h2>1. Ce que vous obtenez</h2>
<p>
Une contribution débloque les bonus pour la période payée : actuellement la météo historique
avec comparaison aux normales climatiques, ainsi que les nouvelles fonctions à venir. Les
parties gratuites de Drizz.li le restent pour tout le monde.
</p>
<h2>2. Fonctionnement</h2>
<ul>
<li>
Vous demandez les coordonnées bancaires sur la page d'inscription ; nous vous les envoyons par
e-mail avec une référence de paiement personnelle.
</li>
<li>
Vous virez le montant (à partir de 3 € / 3 $ / 3 CHF par mois) par virement bancaire
ordinaire, en indiquant la référence.
</li>
<li>
Les virements sont rapprochés manuellement. Dès réception de votre contribution - en général
sous 24 heures, au plus quelques jours - vous recevez une clé d'accès personnelle par e-mail.
</li>
<li>
Vous collez la clé dans Drizz.li une seule fois ; elle est stockée sur votre appareil et
vérifiée automatiquement.
</li>
</ul>
<h2>3. Pas de reconduction automatique</h2>
<p>
Les contributions sont ponctuelles et prépayées : rien ne se reconduit et nous ne vous prélevons
jamais. À la fin de votre période, les bonus se reverrouillent simplement ; contribuer à nouveau
avec la même adresse prolonge votre clé existante.
</p>
<h2>4. Rétractation et remboursement</h2>
<p>
Si vous êtes consommateur dans l'UE/EEE, vous disposez d'un droit légal de rétractation de 14
jours. Au-delà, nous faisons simple : si vous n'êtes pas satisfait, pour quelque raison que ce
soit, dans les 14 jours suivant la réception de votre clé, écrivez à
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
et nous vous remboursons intégralement.
</p>
<h2>5. Usage loyal</h2>
<p>
La clé d'accès est personnelle. Merci de ne pas la publier ni la partager ; les clés
manifestement détournées (par exemple partagées publiquement) peuvent être révoquées. Si votre
clé est révoquée sans motif, vous avez droit à un remboursement au prorata.
</p>
<h2>6. Service et disponibilité</h2>
<p>
Drizz.li est un projet personnel open source, fourni en l'état. Nous nous efforçons de le
maintenir disponible et exact, mais nous ne pouvons garantir ni une disponibilité ininterrompue
ni l'exactitude des prévisions - les données météo sont purement informatives (voir
l'avertissement dans les mentions légales). Si les bonus devenaient définitivement indisponibles
pendant une période payée, nous rembourserions la période restante sur demande.
</p>
<h2>7. Données</h2>
<p>
La façon dont nous traitons vos données (e-mail, référence de paiement, informations de
virement) est décrite dans la
<a href={href('/legal/privacy')}>politique de confidentialité</a>.
</p>
<h2>8. Droit applicable</h2>
<p>
Ces conditions sont régies par le droit de la [Suisse], sans préjudice des dispositions
impératives de protection des consommateurs de votre pays de résidence.
</p>
<p class="text-muted-foreground">
En cas de divergence, la version anglaise de ces conditions fait foi.
</p>
</ProsePage>
+89
View File
@@ -0,0 +1,89 @@
<script lang="ts">
import ProsePage from '$lib/components/prose-page.svelte';
import { href } from '$lib/i18n';
</script>
<ProsePage title="Condizioni per i sostenitori" subtitle="Ultimo aggiornamento: 1 agosto 2026">
<p>
Drizz.li è gratuito. Queste condizioni riguardano il contributo facoltativo di sostegno: un
piccolo pagamento che aiuta a mantenere vivo il progetto e che, come ringraziamento, sblocca gli
extra. Anche se lo chiamiamo contributo, in cambio vengono sbloccate delle funzioni.
</p>
<h2>1. Cosa ottieni</h2>
<p>
Un contributo sblocca gli extra per il periodo pagato: attualmente il meteo storico con
confronto rispetto alle normali climatiche, oltre alle nuove funzioni man mano che arrivano. Le
parti gratuite di Drizz.li restano gratuite per tutti.
</p>
<h2>2. Come funziona</h2>
<ul>
<li>
Richiedi i dati per il bonifico nella pagina di iscrizione; te li inviamo via e-mail insieme a
un riferimento di pagamento personale.
</li>
<li>
Effettui il bonifico (da 3 € / 3 $ / 3 CHF al mese) con un normale bonifico bancario,
indicando il riferimento.
</li>
<li>
I bonifici vengono abbinati manualmente. Appena il contributo arriva - di solito entro 24 ore,
al massimo pochi giorni - ricevi via e-mail una chiave di accesso personale.
</li>
<li>
Incolli la chiave in Drizz.li una volta sola; viene salvata sul tuo dispositivo e verificata
automaticamente.
</li>
</ul>
<h2>3. Nessun rinnovo automatico</h2>
<p>
I contributi sono una tantum e prepagati: nulla si rinnova da solo e non ti addebitiamo mai
nulla. Alla fine del periodo gli extra si bloccano semplicemente di nuovo; un nuovo contributo
con la stessa e-mail estende la chiave esistente.
</p>
<h2>4. Recesso e rimborsi</h2>
<p>
Se sei un consumatore nell'UE/SEE, hai un diritto di recesso legale di 14 giorni. Oltre a
questo, la teniamo semplice: se per qualsiasi motivo non sei soddisfatto entro 14 giorni dalla
ricezione della chiave, scrivi a
<a href="mailto:drizzli@vincentvanderwal.nl">drizzli@vincentvanderwal.nl</a>
e ti rimborseremo l'intero contributo.
</p>
<h2>5. Uso corretto</h2>
<p>
La chiave di accesso è personale. Non pubblicarla né condividerla; le chiavi chiaramente abusate
(ad esempio condivise pubblicamente) possono essere revocate. Se la tua chiave viene revocata
senza motivo, hai diritto a un rimborso proporzionale.
</p>
<h2>6. Servizio e disponibilità</h2>
<p>
Drizz.li è un progetto personale open source, fornito così com'è. Lavoriamo per mantenerlo
disponibile e accurato, ma non possiamo garantire né la continuità del servizio né la
correttezza delle previsioni: i dati meteo sono solo informativi (vedi l'avvertenza nelle note
legali). Se gli extra dovessero diventare permanentemente non disponibili durante un periodo
pagato, rimborseremo su richiesta il periodo residuo.
</p>
<h2>7. Dati</h2>
<p>
Il trattamento dei tuoi dati (e-mail, riferimento di pagamento, dati del bonifico) è descritto
nell'
<a href={href('/legal/privacy')}>informativa sulla privacy</a>.
</p>
<h2>8. Legge applicabile</h2>
<p>
Queste condizioni sono regolate dal diritto della [Svizzera], fatte salve le disposizioni
imperative di tutela del consumatore del tuo paese di residenza.
</p>
<p class="text-muted-foreground">
In caso di discrepanza prevale la versione inglese di queste condizioni.
</p>
</ProsePage>
@@ -327,8 +327,7 @@
/>
</svg>
<span>
This model's ensemble only reaches about <strong>{validDays} days</strong> ahead, the spread is
trimmed to its available range.
{m.ensemble_trimmed({ days: validDays })}
</span>
</div>
{/if}
@@ -385,7 +384,9 @@
{#snippet controls()}
<div class="flex items-center gap-2">
<Switch id="show_legend" name="Show legend" bind:checked={showLegend} />
<Label for="show_legend" class="cursor-pointer text-base leading-none">Show legend</Label>
<Label for="show_legend" class="cursor-pointer text-base leading-none"
>{m.legend_show()}</Label
>
</div>
{/snippet}
</ChartToolbar>
@@ -26,6 +26,7 @@
groupRange,
isColumnUnit
} from '$lib/charts';
import * as m from '$lib/paraglide/messages';
import {
type DaylightBand,
type ModelCompareResult,
@@ -315,10 +316,10 @@
<!-- Range / zoom controls, aligned with the title like the other pages -->
<div class="lg:absolute lg:right-0 lg:top-20 z-40 flex flex-wrap items-center gap-3">
<span class="hidden text-xs text-muted-foreground lg:inline">
drag or
{m.meteograms_zoom_hint()}
<kbd class="rounded border border-border bg-muted px-1 py-0.5 font-sans text-[10px]">Ctrl</kbd
>
+ scroll to zoom
{m.meteograms_zoom_hint_end()}
</span>
{#if zoomActive}
<button
@@ -336,13 +337,13 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v6h6M20 20v-6h-6" />
<path stroke-linecap="round" d="M20 10a8 8 0 0 0-14-4M4 14a8 8 0 0 0 14 4" />
</svg>
Reset zoom
{m.reset_zoom()}
</button>
{/if}
<div
class="inline-flex items-center rounded-lg bg-muted p-0.5 text-xs font-semibold"
role="group"
aria-label="Chart time range"
aria-label={m.range_group_aria()}
>
{#each rangePresets as preset (preset.label)}
<button
@@ -426,7 +427,9 @@
{#snippet controls()}
<div class="flex items-center gap-2">
<Switch id="show_legend" name="Show legend" bind:checked={showLegend} />
<Label for="show_legend" class="cursor-pointer text-base leading-none">Show legend</Label>
<Label for="show_legend" class="cursor-pointer text-base leading-none"
>{m.legend_show()}</Label
>
</div>
{/snippet}
</ChartToolbar>
@@ -436,7 +439,9 @@
<div class="mt-4 md:mt-8">
<div class="flex">
<a href="#models"><h2 id="models" class="text-2xl md:text-3xl">Models</h2></a>
<a href="#models"
><h2 id="models" class="text-2xl md:text-3xl">{m.compare_models_heading()}</h2></a
>
{#if params.models && params.models.length > 0}
<div transition:fade={{ duration: 200 }} class="relative mt-1.25">
<div
@@ -487,7 +492,7 @@
<div class="flex">
<a href="#hourly_weather_variables"
><h2 id="hourly_weather_variables" class="text-2xl md:text-3xl">
Hourly Weather Variables
{m.compare_variables_heading()}
</h2></a
>
{#if params.hourly && params.hourly.length > 0}
@@ -1,6 +1,8 @@
<script lang="ts">
import { formatZoned, getZonedHour } from '$lib/utils/date';
import * as m from '$lib/paraglide/messages';
import { getWeatherIconName } from '../../utils/weather-codes';
interface Props {
@@ -69,14 +71,14 @@
{#if displayModels.length > 0}
<div class="mt-8">
<div class="mb-4 flex items-center justify-between">
<h3 class="text-xl font-bold">Model Comparison Timeline</h3>
<h3 class="text-xl font-bold">{m.compare_timeline_title()}</h3>
<div class="flex items-center gap-1.5 text-[13px] font-semibold">
<span class="select-none text-muted-foreground">3h</span>
<button
class="relative h-6 w-11 cursor-pointer rounded-full border transition-colors
{hourlyInterval === 1 ? 'border-primary/40 bg-primary' : 'border-border bg-muted'}"
onclick={() => (hourlyInterval = hourlyInterval === 1 ? 3 : 1)}
title="Toggle between 1-hour and 3-hour intervals"
title={m.interval_toggle()}
>
<span
class="absolute top-0.75 size-4.5 rounded-full bg-white shadow-sm transition-[left] duration-200
@@ -96,7 +98,7 @@
<th
class="sticky left-0 z-20 w-32 border-b border-r border-border bg-muted/95 p-2 text-left text-xs font-bold"
>
Model
{m.compare_model_label()}
</th>
{#each filteredIndices as idx, i (idx)}
{@const ts = timestamps[idx]}
@@ -1,10 +1,12 @@
<script lang="ts">
import { onMount } from 'svelte';
import { SvelteDate } from 'svelte/reactivity';
import { get } from 'svelte/store';
import { fade } from 'svelte/transition';
import { reportPageReady } from '$lib/stores/page-transition.svelte';
import {
storedArchiveModel,
storedChartLayout,
storedLocation,
storedUnits,
@@ -13,6 +15,7 @@
import { ChartContainer } from '$lib/components/charts';
import * as m from '$lib/paraglide/messages';
import PaywallGate from '$lib/paywall/PaywallGate.svelte';
import { isSupporter } from '$lib/paywall/supporter';
import {
@@ -22,8 +25,10 @@
fetchHistoricalWeather
} from '$lib/services/weather';
import { defaultParameters } from '../../options';
import { useHeroActions } from '../../hero.svelte';
import { archiveModelGroups, defaultParameters } from '../../options';
import HourlyTable from '../../week/[location]/HourlyTable.svelte';
import ModelSelector from '../../week/[location]/ModelSelector.svelte';
import { neededHourlyApiVars } from '../../week/[location]/variables';
import DateRangeControls from './DateRangeControls.svelte';
import HistoricalDaily from './HistoricalDaily.svelte';
@@ -37,6 +42,8 @@
// the page cross-fade waits for this before revealing the new page
reportPageReady(() => result != null);
useHeroActions(heroActions);
let location = $derived(data.location);
$effect(() => {
storedLocation.set(data.location);
@@ -79,6 +86,11 @@
mounted = true;
});
let archiveModel = $state('best_match');
onMount(() => {
archiveModel = get(storedArchiveModel);
});
function onRangeChange(s: string, e: string) {
startDate = s;
endDate = e;
@@ -101,6 +113,8 @@
const s = startDate;
const e = endDate;
const vars = hourlyVars;
const model = archiveModel;
void model;
if (!mounted || !$isSupporter || !loc || !s || !e) return;
const version = ++requestVersion;
@@ -203,12 +217,27 @@
}
</script>
<!-- the reanalysis picker rides in the layout's location row -->
{#snippet heroActions()}
<div class="lg:absolute lg:right-0 lg:top-0 flex w-full min-w-0 items-center gap-3 sm:w-auto">
<ModelSelector
selectedModel={archiveModel}
groups={archiveModelGroups}
label={m.model_archive()}
onModelChange={(model) => {
archiveModel = model;
storedArchiveModel.set(model);
}}
/>
</div>
{/snippet}
<svelte:head>
<title>Drizz.li | Historical weather</title>
<title>Drizz.li | {m.page_historical_subtitle()}</title>
<meta name="description" content="Past weather and climate-normal comparisons for any location" />
</svelte:head>
<PaywallGate feature="Historical weather">
<PaywallGate feature={m.page_historical_subtitle()}>
<DateRangeControls
start={startDate}
end={endDate}
@@ -1,4 +1,6 @@
<script lang="ts">
import * as m from '$lib/paraglide/messages';
interface Props {
start: string;
end: string;
@@ -34,10 +36,10 @@
}
const presets = [
{ label: '7 days', apply: () => applyLastDays(7) },
{ label: '30 days', apply: () => applyLastDays(30) },
{ label: '90 days', apply: () => applyLastDays(90) },
{ label: 'Month, last year', apply: applyThisMonthLastYear }
{ label: () => m.historical_last_days({ days: 7 }), apply: () => applyLastDays(7) },
{ label: () => m.historical_last_days({ days: 30 }), apply: () => applyLastDays(30) },
{ label: () => m.historical_last_days({ days: 90 }), apply: () => applyLastDays(90) },
{ label: () => m.historical_month_last_year(), apply: applyThisMonthLastYear }
];
// ─── Manual inputs ────────────────────────────────────────────────────────────
@@ -65,7 +67,9 @@
>
<div class="flex flex-wrap items-end gap-3">
<div class="grid gap-1">
<label for="hist-start" class="text-xs font-semibold text-muted-foreground">From</label>
<label for="hist-start" class="text-xs font-semibold text-muted-foreground"
>{m.historical_from()}</label
>
<input
id="hist-start"
type="date"
@@ -77,7 +81,9 @@
/>
</div>
<div class="grid gap-1">
<label for="hist-end" class="text-xs font-semibold text-muted-foreground">To</label>
<label for="hist-end" class="text-xs font-semibold text-muted-foreground"
>{m.historical_to()}</label
>
<input
id="hist-end"
type="date"
@@ -93,15 +99,15 @@
<div
class="inline-flex flex-wrap items-center gap-0.5 rounded-lg bg-muted p-0.5 text-xs font-semibold"
role="group"
aria-label="Quick ranges"
aria-label={m.historical_quick_ranges()}
>
{#each presets as preset (preset.label)}
{#each presets as preset (preset.label())}
<button
type="button"
class="cursor-pointer rounded-md px-2.5 py-1.5 whitespace-nowrap text-muted-foreground transition-colors hover:bg-background hover:text-foreground hover:shadow-sm"
onclick={preset.apply}
>
{preset.label}
{preset.label()}
</button>
{/each}
</div>
@@ -1,6 +1,7 @@
<script lang="ts">
import { formatZoned, isSameDayInZone } from '$lib/utils/date';
import * as m from '$lib/paraglide/messages';
import {
type ClimateNormals,
type HistoricalDailyData,
@@ -104,7 +105,7 @@
<!-- ─── KPI tiles ────────────────────────────────────────────────────────────── -->
<div class="grid grid-cols-2 gap-2.5 lg:grid-cols-4">
<div class="rounded-xl border border-border/70 bg-card p-3.5 shadow-sm">
<p class="text-xs font-medium text-muted-foreground">Average temperature</p>
<p class="text-xs font-medium text-muted-foreground">{m.stat_avg_temperature()}</p>
<p class="mt-1 text-2xl font-bold tabular-nums">
{fmtTemp(stats.avg)}<span class="text-base font-semibold text-muted-foreground"
>{tempUnit.replace('°', '')}</span
@@ -118,15 +119,15 @@
class:dark:text-red-400={stats.tempAnomaly >= 0}
class:dark:text-blue-400={stats.tempAnomaly < 0}
>
{fmtSigned(stats.tempAnomaly)} vs normal
{m.anomaly_vs_normal({ value: fmtSigned(stats.tempAnomaly) })}
</p>
{:else}
<p class="mt-0.5 text-xs text-muted-foreground">19912020 normal loading…</p>
<p class="mt-0.5 text-xs text-muted-foreground">{m.normals_loading_period()}</p>
{/if}
</div>
<div class="rounded-xl border border-border/70 bg-card p-3.5 shadow-sm">
<p class="text-xs font-medium text-muted-foreground">Total precipitation</p>
<p class="text-xs font-medium text-muted-foreground">{m.stat_total_precipitation()}</p>
<p class="mt-1 text-2xl font-bold tabular-nums">{fmtPrecip(stats.totalPrecip)}</p>
{#if stats.normalPrecip != null}
<p class="mt-0.5 text-xs font-semibold text-muted-foreground">
@@ -140,7 +141,7 @@
</div>
<div class="rounded-xl border border-border/70 bg-card p-3.5 shadow-sm">
<p class="text-xs font-medium text-muted-foreground">Warmest day</p>
<p class="text-xs font-medium text-muted-foreground">{m.stat_warmest_day()}</p>
<p class="mt-1 text-2xl font-bold tabular-nums">{fmtTemp(stats.warm.t)}</p>
{#if stats.warm.i >= 0}
<p class="mt-0.5 text-xs font-semibold text-muted-foreground">
@@ -150,7 +151,7 @@
</div>
<div class="rounded-xl border border-border/70 bg-card p-3.5 shadow-sm">
<p class="text-xs font-medium text-muted-foreground">Coldest day</p>
<p class="text-xs font-medium text-muted-foreground">{m.stat_coldest_day()}</p>
<p class="mt-1 text-2xl font-bold tabular-nums">{fmtTemp(stats.cold.t)}</p>
{#if stats.cold.i >= 0}
<p class="mt-0.5 text-xs font-semibold text-muted-foreground">
@@ -166,12 +167,12 @@
>
<div class="flex items-center justify-between border-b border-border/70 bg-muted/40 px-4 py-2.5">
<h3 class="text-base font-bold">
Daily <span class="font-semibold text-muted-foreground"> select a day for hourly detail</span
>
{m.historical_daily_heading()}
<span class="font-semibold text-muted-foreground">{m.historical_daily_hint()}</span>
</h3>
{#if normals}
<span class="hidden text-xs text-muted-foreground sm:inline">
normal band = 19912020 mean
{m.normal_band_legend()}
</span>
{/if}
</div>
@@ -242,7 +243,7 @@
<span
class="mt-0.5 inline-block h-1.5 w-6 rounded-full"
style="background:{anomalyColor(anomaly)}"
title="{fmtSigned(anomaly)} vs normal"
title={m.anomaly_vs_normal({ value: fmtSigned(anomaly) })}
></span>
{/if}
@@ -8,6 +8,7 @@
import { ChartContainer, downloadChartsPng } from '$lib/components/charts';
import { CanvasChart, groupRange } from '$lib/charts';
import * as m from '$lib/paraglide/messages';
import { getWeatherIconName } from '../../utils/weather-codes';
import { type FetchedHourly, type WeatherUnits } from '../../week/[location]/types';
@@ -121,22 +122,23 @@
<section class="mt-8" transition:fade={{ duration: 200 }}>
<div class="mb-3 flex flex-wrap items-center justify-between gap-2">
<h3 class="text-lg font-bold">
Meteograms <span class="font-semibold text-muted-foreground"> full range</span>
{m.meteograms_heading()}
<span class="font-semibold text-muted-foreground">{m.historical_full_range()}</span>
</h3>
<div class="flex flex-wrap items-center gap-3">
<span class="hidden text-xs text-muted-foreground md:inline">
drag or
{m.meteograms_zoom_hint()}
<kbd class="rounded border border-border bg-muted px-1 py-0.5 font-sans text-[10px]"
>Ctrl</kbd
>
+ scroll to zoom
{m.meteograms_zoom_hint_end()}
</span>
<button
type="button"
class="flex cursor-pointer items-center gap-1.5 rounded-lg border border-border bg-card px-3 py-1.5 text-xs font-semibold text-muted-foreground transition-colors hover:border-primary/50 hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
disabled={liveCharts.length === 0 || downloadingPng}
onclick={downloadPng}
title="Download meteogram as PNG image"
title={m.chart_download()}
>
{#if downloadingPng}Rendering…{:else}PNG{/if}
</button>
@@ -148,7 +150,7 @@
class="flex cursor-pointer items-center gap-1.5 rounded-lg border border-primary/50 bg-primary/10 px-2.5 py-1 text-xs font-semibold text-primary transition-colors hover:bg-primary/15"
onclick={resetZoom}
>
Reset zoom
{m.reset_zoom()}
</button>
{/if}
</div>
@@ -158,7 +160,7 @@
<div
class="rounded-xl border border-dashed border-border px-4 py-10 text-center text-sm text-muted-foreground"
>
No meteograms configured. Add variables from the 7-day forecast page.
{m.meteograms_none_historical()}
</div>
{:else}
<div class="-mx-3 border-y border-border/70 bg-card shadow-sm lg:mx-0 lg:rounded-2xl lg:border">
+4 -2
View File
@@ -5,6 +5,8 @@
import { mapsDomainForModel } from '$lib/utils/maps-domain';
import * as m from '$lib/paraglide/messages';
// Hash piping, both directions:
// - inbound: a #zoom/lat/lng(/bearing/pitch) hash on OUR url seeds the
// map, so positions can be bookmarked/shared via drizzli links
@@ -74,7 +76,7 @@
</script>
<svelte:head>
<title>Weather Map | Open-Meteo.com</title>
<title>{m.page_maps_title()} | Open-Meteo.com</title>
<link rel="canonical" href="https://open-meteo.com/weather/maps" />
<meta name="description" content="Interactive weather map powered by Open-Meteo" />
</svelte:head>
@@ -88,7 +90,7 @@
<iframe
bind:this={iframeEl}
src={iframeSrc}
title="Open-Meteo Interactive Map"
title={m.maps_iframe_title()}
loading="lazy"
allowfullscreen
allow="cross-origin-isolated"
+45
View File
@@ -513,3 +513,48 @@ export function inDomainCity(model: string): { slug: string; label: string } | n
const hit = IN_DOMAIN_CITY.find(([prefix]) => model.startsWith(prefix));
return hit ? hit[1] : null;
}
/** Reanalyses the archive API accepts (verified against archive-api.open-meteo.com). */
export const archiveModelGroups: WeatherModelGroup[] = [
{
value: 'auto',
label: 'Automatic',
models: [{ value: 'best_match', label: 'Best match', resolution: 'varies', update: 'daily' }]
},
{
value: 'era5',
label: 'ECMWF reanalysis',
models: [
{ value: 'era5_seamless', label: 'ERA5 seamless', resolution: '9-25 km', update: 'daily' },
{ value: 'era5', label: 'ERA5', resolution: '25 km', update: 'daily' },
{ value: 'era5_land', label: 'ERA5-Land', resolution: '9 km', update: 'daily' },
{ value: 'ecmwf_ifs', label: 'ECMWF IFS', resolution: '9 km', update: 'daily' }
]
},
{
value: 'regional',
label: 'Regional reanalysis',
models: [{ value: 'cerra', label: 'CERRA (Europe)', resolution: '5 km', update: 'daily' }]
}
];
/** Seasonal models the seasonal API accepts. */
export const seasonalModelGroups: WeatherModelGroup[] = [
{
value: 'auto',
label: 'Automatic',
models: [{ value: 'best_match', label: 'Best match', resolution: 'varies', update: 'monthly' }]
},
{
value: 'ecmwf',
label: 'ECMWF',
models: [
{
value: 'ecmwf_seasonal_seamless',
label: 'ECMWF SEAS5',
resolution: '36 km',
update: 'monthly'
}
]
}
];
@@ -1,14 +1,16 @@
<script lang="ts">
import { onMount } from 'svelte';
import { get } from 'svelte/store';
import { fade } from 'svelte/transition';
import { reportPageReady } from '$lib/stores/page-transition.svelte';
import { storedLocation, storedUnits } from '$lib/stores/settings';
import { storedLocation, storedSeasonalModel, storedUnits } from '$lib/stores/settings';
import { ChartContainer, ChartToolbar } from '$lib/components/charts';
import { Label } from '$lib/components/ui/label';
import { Switch } from '$lib/components/ui/switch';
import * as m from '$lib/paraglide/messages';
import PaywallGate from '$lib/paywall/PaywallGate.svelte';
import { isSupporter } from '$lib/paywall/supporter';
import {
@@ -19,7 +21,8 @@
} from '$lib/services/weather';
import { useHeroActions } from '../../hero.svelte';
import { defaultParameters } from '../../options';
import { defaultParameters, seasonalModelGroups } from '../../options';
import ModelSelector from '../../week/[location]/ModelSelector.svelte';
import { getPrecipUnit } from '../../week/[location]/types';
import SeasonalCharts from './SeasonalCharts.svelte';
import SeasonalMonths from './SeasonalMonths.svelte';
@@ -79,7 +82,9 @@
let result = $state<SeasonalForecastResult | null>(null);
let normals = $state<ClimateNormals | null>(null);
let seasonalModel = $state('best_match');
onMount(() => {
seasonalModel = get(storedSeasonalModel);
mounted = true;
});
@@ -89,6 +94,7 @@
const loc = location;
const tempUnit = params.temperature_unit;
const precipUnit = params.precipitation_unit;
const model = seasonalModel;
if (!mounted || !$isSupporter || !loc) return;
const version = ++requestVersion;
@@ -99,6 +105,7 @@
latitude: loc.latitude!,
longitude: loc.longitude!,
dailyVariables: SEASONAL_VARS,
model,
temperature_unit: tempUnit as 'celsius' | 'fahrenheit',
wind_speed_unit: params.wind_speed_unit as 'kmh' | 'ms' | 'mph' | 'kn',
precipitation_unit: precipUnit as 'mm' | 'inch',
@@ -173,7 +180,7 @@
</script>
<svelte:head>
<title>Drizz.li | Seasonal forecast</title>
<title>Drizz.li | {m.page_seasonal_subtitle()}</title>
<meta
name="description"
content="Multi-month seasonal outlook: monthly temperature and precipitation trends against the 1991-2020 climate normal"
@@ -182,32 +189,43 @@
<!-- the range buttons ride in the layout's location row (see weather/+layout) -->
{#snippet heroActions()}
{#if result}
<!-- range buttons reslice the already-fetched horizon (no refetch) -->
<div
class="flex w-full gap-1 rounded-lg border border-border bg-card p-1 sm:w-auto"
role="group"
aria-label="Outlook range"
>
{#each RANGES as range, i (range.label)}
{@const disabled = range.days !== Infinity && range.days > horizonDays}
<button
type="button"
class="flex-1 cursor-pointer rounded-md px-3 py-1.5 text-xs font-semibold whitespace-nowrap transition-colors sm:flex-none {rangeIndex ===
i
? 'bg-primary text-primary-foreground'
: 'text-muted-foreground hover:bg-muted hover:text-foreground'} {disabled
? 'cursor-not-allowed opacity-40'
: ''}"
aria-pressed={rangeIndex === i}
{disabled}
onclick={() => (rangeIndex = i)}
>
{range.label}
</button>
{/each}
</div>
{/if}
<div class="flex w-full flex-wrap items-center gap-3 sm:w-auto">
{#if result}
<!-- range buttons reslice the already-fetched horizon (no refetch) -->
<div
class="flex w-full gap-1 rounded-lg border border-border bg-card p-1 sm:w-auto"
role="group"
aria-label={m.seasonal_range_aria()}
>
{#each RANGES as range, i (range.label)}
{@const disabled = range.days !== Infinity && range.days > horizonDays}
<button
type="button"
class="flex-1 cursor-pointer rounded-md px-3 py-1.5 text-xs font-semibold whitespace-nowrap transition-colors sm:flex-none {rangeIndex ===
i
? 'bg-primary text-primary-foreground'
: 'text-muted-foreground hover:bg-muted hover:text-foreground'} {disabled
? 'cursor-not-allowed opacity-40'
: ''}"
aria-pressed={rangeIndex === i}
{disabled}
onclick={() => (rangeIndex = i)}
>
{range.label}
</button>
{/each}
</div>
{/if}
<ModelSelector
selectedModel={seasonalModel}
groups={seasonalModelGroups}
label={m.model_seasonal()}
onModelChange={(model) => {
seasonalModel = model;
storedSeasonalModel.set(model);
}}
/>
</div>
{/snippet}
<!-- What a seasonal forecast is (and is not): without this the daily-looking
@@ -226,17 +244,16 @@
<circle cx="12" cy="12" r="9" />
</svg>
<p class="text-muted-foreground">
A seasonal forecast shows how a whole month is likely to
<strong class="font-semibold text-foreground">depart from its climate normal</strong> - not the
weather on any given day. Read the monthly trend and the ensemble agreement, not the daily
wiggles.
{m.seasonal_explainer_before()}
<strong class="font-semibold text-foreground">{m.seasonal_explainer_strong()}</strong>
{m.seasonal_explainer_after()}
{#if lastDayLabel}
This outlook runs to <strong class="font-semibold text-foreground">{lastDayLabel}</strong>.
{m.seasonal_runs_to({ date: lastDayLabel })}
{/if}
</p>
</div>
<PaywallGate feature="The seasonal outlook">
<PaywallGate feature={m.page_seasonal_subtitle()}>
{#if loadError}
<div
class="mb-4 rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive"
@@ -265,7 +282,7 @@
<div class="flex items-center gap-2">
<Switch id="show_legend" name="Show legend" bind:checked={showLegend} />
<Label for="show_legend" class="cursor-pointer text-base leading-none"
>Show legend</Label
>{m.legend_show()}</Label
>
</div>
{/snippet}
@@ -1,4 +1,6 @@
<script lang="ts">
import * as m from '$lib/paraglide/messages';
import { getColor } from '../../utils/colors';
import { type WeatherUnits, getPrecipUnit, getTempUnit } from '../../week/[location]/types';
@@ -50,7 +52,7 @@
<!-- headline: mean temperature and its departure from the 1991-2020 normal -->
<div class="mt-2.5 flex items-end gap-3">
<div>
<p class="text-xs font-medium text-muted-foreground">Mean temperature</p>
<p class="text-xs font-medium text-muted-foreground">{m.stat_mean_temperature()}</p>
<p class="text-2xl leading-tight font-bold tabular-nums">
{fmtTemp(month.tMean)}<span class="text-base font-semibold text-muted-foreground"
>{tempUnit.replace('°', '')}</span
@@ -70,9 +72,9 @@
</p>
<p class="text-[11px] text-muted-foreground">{anomalyLabel(month.anomaly)}</p>
{:else if normals}
<p class="text-[11px] text-muted-foreground">no normal for this month</p>
<p class="text-[11px] text-muted-foreground">{m.seasonal_no_normal()}</p>
{:else}
<p class="text-[11px] text-muted-foreground">normal loading…</p>
<p class="text-[11px] text-muted-foreground">{m.normals_loading()}</p>
{/if}
</div>
</div>
@@ -85,7 +87,7 @@
style="background:{getColor(month.tMax, units.temperature_unit)}"
></span>
<span class="font-semibold tabular-nums">{fmtTemp(month.tMax)}</span>
<span class="text-muted-foreground">day</span>
<span class="text-muted-foreground">{m.label_day()}</span>
</span>
<span class="inline-flex items-center gap-1.5">
<span
@@ -93,14 +95,14 @@
style="background:{getColor(month.tMin, units.temperature_unit)}"
></span>
<span class="font-semibold tabular-nums">{fmtTemp(month.tMin)}</span>
<span class="text-muted-foreground">night</span>
<span class="text-muted-foreground">{m.label_night()}</span>
</span>
</div>
<!-- precipitation against its own normal, as a share bar -->
<div class="mt-3 border-t border-border/60 pt-2.5">
<div class="flex items-baseline justify-between gap-2 text-xs">
<span class="font-medium text-muted-foreground">Precipitation</span>
<span class="font-medium text-muted-foreground">{m.var_precipitation()}</span>
<span class="font-semibold tabular-nums">
{fmtPrecip(month.precip)}
{precipUnit}
@@ -39,6 +39,7 @@
import HourlyTable from './HourlyTable.svelte';
import MeteogramCharts from './MeteogramCharts.svelte';
import ModelSelector from './ModelSelector.svelte';
import NearbyCities from './NearbyCities.svelte';
import VariableSidebar from './VariableSidebar.svelte';
import { neededHourlyApiVars } from './variables';
@@ -326,7 +327,7 @@
</script>
<svelte:head>
<title>Drizz.li | Weather</title>
<title>Drizz.li | {m.page_week_title()}</title>
<link rel="canonical" href="https://drizz.li/weather/week" />
<meta name="description" content="7-day weather forecast with detailed hourly data" />
</svelte:head>
@@ -362,7 +363,7 @@
class="cursor-pointer rounded-md border border-destructive/40 bg-background px-3 py-1 text-xs font-semibold text-destructive transition-colors hover:bg-destructive/10"
onclick={() => retryNonce++}
>
Try again
{m.action_try_again()}
</button>
{#if suggestedCity}
<a
@@ -383,7 +384,7 @@
</div>
{#if loadError.detail}
<details class="mt-2 text-xs text-destructive/70">
<summary class="cursor-pointer select-none">Technical details</summary>
<summary class="cursor-pointer select-none">{m.error_technical_details()}</summary>
<p class="mt-1 font-mono break-all">{loadError.detail}</p>
</details>
{/if}
@@ -535,6 +536,15 @@
<ChartContainer loading chartCount={enabledChartCount || 1} {chartHeight} />
</section>
{/if}
{#if selectedDayKey}
<NearbyCities
latitude={location.latitude}
longitude={location.longitude}
{selectedDayKey}
units={$storedUnits}
/>
{/if}
</div>
</div>
</div>
@@ -152,15 +152,15 @@
>
<div class="flex items-center justify-between border-b border-border px-5 py-4">
<div>
<h2 class="text-base font-bold">Customize meteograms</h2>
<h2 class="text-base font-bold">{m.customize_meteograms()}</h2>
<p class="text-xs text-muted-foreground">
Drag variables between charts to build your own layout.
{m.customizer_intro()}
</p>
</div>
<button
class="flex h-8 w-8 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
onclick={onClose}
aria-label="Close"
aria-label={m.action_close()}
>
<svg
class="h-4.5 w-4.5"
@@ -212,12 +212,12 @@
>
<div class="mb-2 flex items-center justify-between">
<span class="text-[11px] font-bold tracking-wider text-primary uppercase"
>Chart {i + 1}</span
>{m.customizer_chart_n({ number: i + 1 })}</span
>
<button
class="cursor-pointer rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-destructive"
onclick={() => deletePanel(panel.id)}
aria-label="Delete chart {i + 1}"
aria-label={m.customizer_delete_chart({ number: i + 1 })}
>
<svg
class="h-4 w-4"
@@ -238,7 +238,7 @@
data-chip
role="button"
tabindex="0"
aria-label="Drag {def.label}"
aria-label={m.customizer_drag({ variable: def.label })}
class="flex cursor-grab touch-none items-center gap-1.5 rounded-lg border border-border bg-background py-1.5 pr-1 pl-2.5 text-sm shadow-sm select-none active:cursor-grabbing {dragKey ===
key
? 'opacity-30'
@@ -257,7 +257,7 @@
class="ml-0.5 flex h-5 w-5 cursor-pointer items-center justify-center rounded text-muted-foreground hover:bg-muted hover:text-foreground"
onpointerdown={(e) => e.stopPropagation()}
onclick={() => removeVar(key)}
aria-label="Remove {def.label}"
aria-label={m.customizer_remove({ variable: def.label })}
>
<svg
class="h-3.5 w-3.5"
@@ -274,7 +274,7 @@
{/each}
{#if panel.variables.length === 0}
<span class="self-center text-xs text-muted-foreground italic"
>Drop variables here</span
>{m.customizer_drop_here()}</span
>
{/if}
</div>
@@ -285,12 +285,12 @@
class="w-full cursor-pointer rounded-xl border-2 border-dashed border-border py-2.5 text-sm font-semibold text-muted-foreground transition-colors hover:border-primary/50 hover:text-foreground"
onclick={addPanel}
>
+ Add chart
{m.customizer_add_chart()}
</button>
<div>
<h3 class="mb-2 text-[11px] font-bold tracking-wider text-muted-foreground uppercase">
Available variables
{m.customizer_available()}
</h3>
<div
data-zone="pool"
@@ -304,7 +304,7 @@
data-chip
role="button"
tabindex="0"
aria-label="Drag {def.label}"
aria-label={m.customizer_drag({ variable: def.label })}
class="flex cursor-grab touch-none items-center gap-1.5 rounded-lg border border-border bg-background px-2.5 py-1.5 text-sm text-muted-foreground shadow-sm select-none active:cursor-grabbing {dragKey ===
def.key
? 'opacity-30'
@@ -321,7 +321,7 @@
{/each}
{#if availableVars.length === 0}
<span class="self-center text-xs text-muted-foreground italic"
>All variables are in use</span
>{m.customizer_all_in_use()}</span
>
{/if}
</div>
@@ -333,13 +333,13 @@
class="cursor-pointer text-xs font-medium text-muted-foreground underline-offset-2 transition-colors hover:text-foreground hover:underline"
onclick={resetLayout}
>
Reset to defaults
{m.action_reset_defaults()}
</button>
<button
class="cursor-pointer rounded-lg bg-primary px-4 py-1.5 text-sm font-semibold text-primary-foreground transition-opacity hover:opacity-90"
onclick={onClose}
>
Done
{m.action_done()}
</button>
</div>
</div>
@@ -3,6 +3,8 @@
import { formatZoned, getRelativeDayLabel, isSameDayInZone } from '$lib/utils/date';
import * as m from '$lib/paraglide/messages';
import { getTempStyle } from '../../utils/colors';
import { getWeatherIconName } from '../../utils/weather-codes';
import { precipIsSignificant, sunIsSignificant, windIsSignificant } from './significance';
@@ -118,7 +120,7 @@
type="button"
class="mr-4 flex w-24 shrink-0 cursor-pointer flex-col items-center justify-center gap-2 rounded-2xl border border-dashed border-border/70 bg-card/40 px-2 text-muted-foreground transition-colors hover:border-primary/50 hover:bg-primary/5 hover:text-foreground"
onclick={onExtendPast}
aria-label="Load recent past days"
aria-label={m.strip_past_aria()}
>
<svg
class="h-6 w-6"
@@ -135,7 +137,7 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M14 13l-3 3 3 3" />
</svg>
<span class="text-center text-[11px] leading-tight font-semibold">
Past<br />3 days
{m.daycards_past()}
</span>
</button>
{/if}
@@ -317,7 +319,7 @@
type="button"
class="day-card-btn flex w-24 shrink-0 cursor-pointer flex-col items-center justify-center gap-2 self-stretch rounded-2xl border border-dashed border-border/70 bg-card/40 px-2 text-muted-foreground transition-colors hover:border-primary/50 hover:bg-primary/5 hover:text-foreground md:hidden"
onclick={onExtend}
aria-label="Load the longer-range forecast"
aria-label={m.strip_extend_aria()}
>
<svg
class="h-6 w-6"
@@ -334,7 +336,7 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M12 14v4m-2-2h4" />
</svg>
<span class="text-center text-[11px] leading-tight font-semibold">
Load<br />15 days
{m.daycards_load_15()}
</span>
</button>
{/if}
@@ -349,7 +351,7 @@
type="button"
class="mt-2 mb-3 hidden w-20 shrink-0 cursor-pointer flex-col items-center justify-center gap-2 self-stretch rounded-2xl border border-dashed border-border/70 bg-card/40 px-2 text-muted-foreground transition-colors hover:border-primary/50 hover:bg-primary/5 hover:text-foreground md:mt-5 md:mb-11 md:flex md:w-24"
onclick={onExtend}
aria-label="Load the longer-range forecast"
aria-label={m.strip_extend_aria()}
>
<svg
class="h-6 w-6"
@@ -365,7 +367,8 @@
/>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 14v4m-2-2h4" />
</svg>
<span class="text-center text-[11px] leading-tight font-semibold"> Load<br />15 days </span>
<span class="text-center text-[11px] leading-tight font-semibold">{m.daycards_load_15()}</span
>
</button>
{/if}
</div>
@@ -451,7 +451,7 @@
<button
class="inline-flex h-8 cursor-pointer items-center gap-1.5 rounded-lg border border-border bg-background px-2.5 text-[13px] font-semibold text-muted-foreground transition-colors hover:border-primary/50 hover:text-foreground"
onclick={onCustomize}
aria-label="Customize variables"
aria-label={m.table_customize()}
>
<!-- sliders icon -->
<svg
@@ -466,13 +466,13 @@
d="M4 6h9m5 0h2M4 12h2m5 0h9M4 18h9m5 0h2M13 4.5v3M6 10.5v3M18 16.5v3"
/>
</svg>
<span class="hidden sm:inline">Variables</span>
<span class="hidden sm:inline">{m.hourly_variables()}</span>
</button>
{/if}
<div
class="inline-flex items-center rounded-lg bg-muted p-0.5 text-[13px] font-semibold"
role="group"
aria-label="Hourly interval"
aria-label={m.table_interval_aria()}
>
{#each [3, 1] as interval (interval)}
<button
@@ -584,7 +584,7 @@
class="absolute bottom-0.5 z-15 -translate-x-1/2 rounded-full bg-red-500 px-1.5 py-px text-[9px] font-bold tracking-wide whitespace-nowrap text-white uppercase shadow-sm"
style="left:{nowPercent}%"
>
Now
{m.table_now()}
</span>
{/if}
<!-- Hour labels -->
@@ -231,7 +231,7 @@
</h3>
<div class="flex flex-wrap items-center gap-3">
<span class="hidden text-xs text-muted-foreground md:inline">
drag or
{m.meteograms_zoom_hint()}
<kbd class="rounded border border-border bg-muted px-1 py-0.5 font-sans text-[10px]"
>Ctrl</kbd
>
@@ -273,7 +273,7 @@
class="flex cursor-pointer items-center gap-1.5 rounded-lg border border-border bg-card px-3 py-1.5 text-xs font-semibold text-muted-foreground transition-colors hover:border-primary/50 hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
disabled={liveCharts.length === 0 || downloadingPng}
onclick={downloadPng}
title="Download meteogram as PNG image"
title={m.chart_download()}
>
{#if downloadingPng}
<svg
@@ -330,9 +330,10 @@
<div
class="rounded-xl border border-dashed border-border px-4 py-10 text-center text-sm text-muted-foreground"
>
No meteograms configured, <button
{m.meteograms_none_before()}
<button
class="cursor-pointer font-semibold text-primary underline-offset-2 hover:underline"
onclick={() => (customizerOpen = true)}>add some variables</button
onclick={() => (customizerOpen = true)}>{m.meteograms_none_action()}</button
>.
</div>
{:else}
@@ -43,7 +43,7 @@
}}
>
<Select.Trigger
aria-label="{label} selection"
aria-label={m.model_selector_aria({ label })}
class="group h-auto min-h-12 min-w-0 flex-1 cursor-pointer gap-2.5 rounded-xl border-2 border-primary/35 bg-card py-1.5 ps-2.5 shadow-sm transition-colors hover:border-primary/70 hover:shadow-md data-[size=default]:h-auto data-[state=open]:border-primary sm:min-h-14 sm:gap-3 sm:py-2 sm:min-w-72 sm:flex-none"
>
<div
@@ -99,7 +99,9 @@
{mo.resolution}{mo.update ? ` · updated ${mo.update}` : ''}
</span>
{:else if mo.value === 'best_match'}
<span class="text-[11px] text-muted-foreground">Automatic selection</span>
<span class="text-[11px] text-muted-foreground"
>{m.model_automatic_selection()}</span
>
{/if}
</div>
</Select.Item>
@@ -0,0 +1,133 @@
<script lang="ts">
import { fade } from 'svelte/transition';
import { buildLocationRoute } from '$lib/utils/location';
import { href } from '$lib/i18n';
import * as m from '$lib/paraglide/messages';
import { getLocale } from '$lib/paraglide/runtime';
import { type NearbyCity, findNearbyCities } from '$lib/services/nearby-cities';
import { type NearbyDaily, fetchNearbyDaily } from '$lib/services/weather';
import { getWeatherIconName } from '../../utils/weather-codes';
import type { UnitPrefs } from '$lib/stores/settings';
interface Props {
latitude: number;
longitude: number;
/** "yyyy-MM-dd" of the day the rest of the page is showing */
selectedDayKey: string;
units: UnitPrefs;
}
let { latitude, longitude, selectedDayKey, units }: Props = $props();
const COUNT = 10;
let cities = $state<NearbyCity[]>([]);
let daily = $state<(NearbyDaily | null)[]>([]);
let failed = $state(false);
// Country names come free and localized from the platform; the flag images
// are the same set the header uses.
let countryNames = $derived(new Intl.DisplayNames([getLocale()], { type: 'region' }));
const countryName = (code: string) => {
try {
return countryNames.of(code) ?? code;
} catch {
return code;
}
};
// One request covers the whole strip's date range, so clicking through the
// days re-reads what is already here instead of refetching ten cities.
$effect(() => {
const lat = latitude;
const lon = longitude;
const unitPrefs = { ...units };
let cancelled = false;
cities = [];
daily = [];
failed = false;
(async () => {
try {
const found = await findNearbyCities(lat, lon, COUNT);
if (cancelled) return;
cities = found;
const snapshots = await fetchNearbyDaily({ points: found, ...unitPrefs });
if (cancelled) return;
daily = snapshots;
} catch {
if (!cancelled) failed = true;
}
})();
return () => {
cancelled = true;
};
});
const dayFor = (index: number) => daily[index]?.byDate[selectedDayKey];
const temp = (value: number | undefined) =>
value == null || !Number.isFinite(value) ? '' : `${Math.round(value)}°`;
const distance = (km: number) =>
units.wind_speed_unit === 'mph' ? `${Math.round(km * 0.621371)} mi` : `${Math.round(km)} km`;
</script>
{#if cities.length > 0 && !failed}
<section class="mt-8" in:fade={{ duration: 200 }}>
<div class="mb-3 flex flex-wrap items-baseline justify-between gap-2">
<h2 class="text-lg font-semibold">{m.nearby_cities_title()}</h2>
<p class="text-xs text-muted-foreground">{m.nearby_cities_subtitle()}</p>
</div>
<div
class="-mx-3 grid grid-cols-2 gap-px overflow-hidden border-y border-border/70 bg-border/70 sm:grid-cols-3 md:mx-0 md:rounded-2xl md:border lg:grid-cols-5"
>
{#each cities as city, i (city.id)}
{@const day = dayFor(i)}
<a
href={href('/weather/week/[location]', {
location: buildLocationRoute({
id: city.id,
name: city.name,
latitude: city.latitude,
longitude: city.longitude,
population: city.population,
feature_code: 'PPL'
})
})}
class="group flex items-center gap-2 bg-card px-3 py-2.5 transition-colors hover:bg-muted/60"
title="{city.name}, {countryName(city.countryCode)}"
>
<svg class="shrink-0 fill-foreground/80" width="34px" height="34px">
{#if day}
<use
xlink:href="/images/weather-icons/{getWeatherIconName(
day.weatherCode,
true
)}.svg#Layer_1"
></use>
{/if}
</svg>
<div class="min-w-0 flex-1">
<div class="truncate text-sm font-medium group-hover:underline">{city.name}</div>
<div class="text-[11px] text-muted-foreground">
{distance(city.distanceKm)} · {countryName(city.countryCode)}
</div>
</div>
<div class="text-right text-sm tabular-nums">
<div class="font-semibold">{temp(day?.max)}</div>
<div class="text-[11px] text-muted-foreground">{temp(day?.min)}</div>
</div>
</a>
{/each}
</div>
</section>
{/if}
@@ -12,6 +12,8 @@
import { Checkbox } from '$lib/components/ui/checkbox';
import { Label } from '$lib/components/ui/label';
import * as m from '$lib/paraglide/messages';
interface Props {
open: boolean;
onClose: () => void;
@@ -81,16 +83,16 @@
<aside
class="absolute inset-y-0 right-0 flex w-80 max-w-[90vw] flex-col overflow-y-auto border-l border-border bg-card shadow-xl"
transition:fly={{ x: 320, duration: 200, opacity: 1 }}
aria-label="Variable selection"
aria-label={m.variables_aria()}
>
<div
class="sticky top-0 flex items-center justify-between border-b border-border bg-card px-5 py-4"
>
<h2 class="text-base font-bold">Variables</h2>
<h2 class="text-base font-bold">{m.hourly_variables()}</h2>
<button
class="flex h-8 w-8 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
onclick={onClose}
aria-label="Close variable selection"
aria-label={m.variables_close()}
>
<svg
class="h-4.5 w-4.5"
@@ -107,7 +109,7 @@
<div class="flex flex-1 flex-col gap-6 px-5 py-4">
<section>
<h3 class="mb-2 text-[11px] font-bold tracking-wider text-primary uppercase">
Hourly table
{m.variables_table_section()}
</h3>
<div class="flex flex-col gap-1">
{#each tableRowOrder as key, i (key)}
@@ -129,7 +131,7 @@
class="flex h-6 w-6 cursor-pointer items-center justify-center rounded text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:pointer-events-none disabled:opacity-30"
onclick={() => moveRow(key, -1)}
disabled={i === 0}
aria-label="Move {tableVariableLabels[key] ?? key} up"
aria-label={m.variables_move_up({ variable: tableVariableLabels[key] ?? key })}
>
<svg
class="h-3.5 w-3.5"
@@ -145,7 +147,9 @@
class="flex h-6 w-6 cursor-pointer items-center justify-center rounded text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:pointer-events-none disabled:opacity-30"
onclick={() => moveRow(key, 1)}
disabled={i === tableRowOrder.length - 1}
aria-label="Move {tableVariableLabels[key] ?? key} down"
aria-label={m.variables_move_down({
variable: tableVariableLabels[key] ?? key
})}
>
<svg
class="h-3.5 w-3.5"
@@ -164,8 +168,9 @@
</section>
<p class="text-xs text-muted-foreground">
Meteogram variables are configured with the <span class="font-semibold">Customize</span>
button above the charts.
{m.variables_charts_hint_before()}
<span class="font-semibold">{m.meteograms_customize()}</span>
{m.variables_charts_hint_after()}
</p>
</div>
@@ -174,7 +179,7 @@
class="cursor-pointer text-xs font-medium text-muted-foreground underline-offset-2 transition-colors hover:text-foreground hover:underline"
onclick={resetDefaults}
>
Reset to defaults
{m.action_reset_defaults()}
</button>
</div>
</aside>