feat: add global weather support
All checks were successful
CI / Lint, typecheck and build (push) Successful in 9m54s

This commit is contained in:
zv
2026-06-14 15:53:34 +02:00
parent d572c9cc53
commit 2182297adc
45 changed files with 739 additions and 212 deletions

View File

@@ -5,7 +5,7 @@ import { ExternalLink, LoaderCircle, LocateFixed, MapPinned, ShieldAlert, X } fr
import { Button } from "@/components/ui/button";
import { useI18n } from "@/lib/i18n";
import { fetchReverseLocation } from "@/lib/location-api";
import { findNearestSynopStation, type LocatedSynopStation } from "@/lib/location-utils";
import { createSelectedLocation, type LocatedSynopStation } from "@/lib/location-utils";
import { useWeatherStore } from "@/lib/store";
const GPS_PROMPT_SEEN_KEY = "wtr:gps-prompt-seen";
@@ -39,11 +39,6 @@ export function CurrentLocationControl({ stations }: { stations: LocatedSynopSta
setMessage(t("location.gpsUnavailable"));
return;
}
if (!stations.length) {
setMessage(t("location.gpsStationsPending"));
return;
}
setIsLocating(true);
navigator.geolocation.getCurrentPosition(
async ({ coords }) => {
@@ -54,14 +49,16 @@ export function CurrentLocationControl({ stations }: { stations: LocatedSynopSta
name: t("location.gpsFallbackName"),
province: null,
district: null,
country: null,
countryCode: null,
admin1: null,
admin2: null,
timezone: null,
region: "GLOBAL" as const,
}));
const nearest = findNearestSynopStation({ ...place, latitude, longitude }, stations);
if (!nearest) {
setMessage(t("location.gpsStationsPending"));
return;
}
selectLocation(nearest);
setMessage(t("location.gpsSelected", { location: nearest.name }));
const selected = createSelectedLocation({ ...place, latitude, longitude }, stations);
selectLocation(selected);
setMessage(t("location.gpsSelected", { location: selected.name }));
} catch {
setMessage(t("location.gpsPositionUnavailable"));
} finally {
@@ -89,13 +86,13 @@ export function CurrentLocationControl({ stations }: { stations: LocatedSynopSta
}, []);
useEffect(() => {
if (!window.isSecureContext || !stations.length || autoLocated.current || !navigator.permissions?.query) return;
if (!window.isSecureContext || autoLocated.current || !navigator.permissions?.query) return;
navigator.permissions.query({ name: "geolocation" }).then((permission) => {
if (permission.state !== "granted" || autoLocated.current) return;
autoLocated.current = true;
locate();
}).catch(() => undefined);
}, [locate, stations.length]);
}, [locate]);
return (
<div className="mt-3 space-y-2">
@@ -108,7 +105,7 @@ export function CurrentLocationControl({ stations }: { stations: LocatedSynopSta
<p className="mt-1 text-xs leading-5 text-muted">{t("location.gpsPromptDescription")}</p>
{!isSecureContext && <p className="mt-2 flex items-start gap-1.5 text-xs leading-5 text-warning"><ShieldAlert className="mt-0.5 size-3.5 shrink-0" />{t("location.gpsSecureContext")}</p>}
<div className="mt-3 flex flex-wrap gap-2">
<Button type="button" onClick={locate} disabled={isLocating || !stations.length}>
<Button type="button" onClick={locate} disabled={isLocating}>
{isLocating ? <LoaderCircle className="size-4 animate-spin" /> : <LocateFixed className="size-4" />}
{isLocating ? t("location.gpsLocating") : t("location.gpsAllow")}
</Button>
@@ -120,7 +117,7 @@ export function CurrentLocationControl({ stations }: { stations: LocatedSynopSta
)}
{!showPrompt && (
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
<Button type="button" variant="glass" onClick={locate} disabled={isLocating || !stations.length}>
<Button type="button" variant="glass" onClick={locate} disabled={isLocating}>
{isLocating ? <LoaderCircle className="size-4 animate-spin" /> : <LocateFixed className="size-4" />}
{isLocating ? t("location.gpsLocating") : t("location.gpsUse")}
</Button>