feat: add weather unit preferences
Some checks failed
CI / Lint, test, typecheck and build (push) Has been cancelled

This commit is contained in:
zv
2026-07-04 20:16:11 +02:00
parent ab6b7b414f
commit 91acdb39b8
27 changed files with 623 additions and 76 deletions

View File

@@ -19,6 +19,8 @@ export function CurrentConditionsCard({ station }: { station: SynopStation }) {
const { language, t } = useI18n();
const temperatureUnit = useWeatherStore((state) => state.temperatureUnit);
const windSpeedUnit = useWeatherStore((state) => state.windSpeedUnit);
const precipitationUnit = useWeatherStore((state) => state.precipitationUnit);
const pressureUnit = useWeatherStore((state) => state.pressureUnit);
const feelsLike = calculateFeelsLike(station.temperature, station.humidity, station.windSpeed);
const metrics = [
{
@@ -36,7 +38,7 @@ export function CurrentConditionsCard({ station }: { station: SynopStation }) {
{
icon: Gauge,
label: t("weather.pressure"),
value: formatPressure(station.pressure, language),
value: formatPressure(station.pressure, language, pressureUnit),
detail: t("weather.pressureDetail"),
},
{
@@ -57,7 +59,7 @@ export function CurrentConditionsCard({ station }: { station: SynopStation }) {
{
icon: Umbrella,
label: t("weather.rainfallTotal"),
value: formatRainfall(station.rainfall, language),
value: formatRainfall(station.rainfall, language, precipitationUnit),
detail: t("weather.rainfallDetail"),
},
{

View File

@@ -7,6 +7,7 @@ import { useLocationSearch } from "@/hooks/use-location-search";
import { useI18n } from "@/lib/i18n";
import { createSelectedLocation, locateSynopStations } from "@/lib/location-utils";
import { useWeatherStore } from "@/lib/store";
import { formatDistance } from "@/lib/weather-utils";
import type { MeteoStationPosition, SynopStation } from "@/types/imgw";
import type { SelectedLocation } from "@/types/location";
@@ -21,6 +22,7 @@ export function LocationSearch({
const [query, setQuery] = useState("");
const [isFocused, setIsFocused] = useState(false);
const selectedLocation = useWeatherStore((state) => state.selectedLocation);
const distanceUnit = useWeatherStore((state) => state.distanceUnit);
const selectLocation = useWeatherStore((state) => state.selectLocation);
const { data: locations, isFetching, isError } = useLocationSearch(query, language);
const locatedStations = useMemo(() => locateSynopStations(stations, positions), [positions, stations]);
@@ -112,7 +114,7 @@ export function LocationSearch({
{t("location.nearest")}
<br />
<strong className="font-semibold text-foreground">
{selected.stationName} · {selected.distanceKm} km
{selected.stationName} · {formatDistance(selected.distanceKm, language, distanceUnit)}
</strong>
</span>
) : (
@@ -135,7 +137,7 @@ export function LocationSearch({
? t("location.currentSource", {
location: selectedLocation.name,
station: selectedLocation.stationName,
distance: selectedLocation.distanceKm,
distance: formatDistance(selectedLocation.distanceKm, language, distanceUnit),
})
: t("location.currentSourceGlobal", { location: selectedLocation.name })}
</p>

View File

@@ -25,6 +25,7 @@ export function StationCard({ station, index = 0 }: { station: SynopStation; ind
const selectStation = useWeatherStore((state) => state.selectStation);
const temperatureUnit = useWeatherStore((state) => state.temperatureUnit);
const windSpeedUnit = useWeatherStore((state) => state.windSpeedUnit);
const pressureUnit = useWeatherStore((state) => state.pressureUnit);
const favorite = favorites.includes(station.id);
const mood = getWeatherMoodFromData(station);
const compactWind = station.windSpeed === null ? "—" : formatWind(station.windSpeed, null, language, windSpeedUnit);
@@ -77,7 +78,7 @@ export function StationCard({ station, index = 0 }: { station: SynopStation; ind
</span>
<span className="flex items-center gap-1">
<Gauge className="size-3" />
{station.pressure === null ? "—" : formatPressure(station.pressure, language).split(" ")[0]}
{station.pressure === null ? "—" : formatPressure(station.pressure, language, pressureUnit)}
</span>
</Link>
</Card>

View File

@@ -6,6 +6,7 @@ import { AlertTriangle, Droplets, Gauge, MapPin, Navigation, Umbrella, Wind } fr
import {
calculateFeelsLike,
formatDateTime,
formatDistance,
formatHumidity,
formatPressure,
formatRainfall,
@@ -49,6 +50,9 @@ export function WeatherHero({
const { language, t } = useI18n();
const temperatureUnit = useWeatherStore((state) => state.temperatureUnit);
const windSpeedUnit = useWeatherStore((state) => state.windSpeedUnit);
const precipitationUnit = useWeatherStore((state) => state.precipitationUnit);
const pressureUnit = useWeatherStore((state) => state.pressureUnit);
const distanceUnit = useWeatherStore((state) => state.distanceUnit);
const [devEffectOverride, setDevEffectOverride] = useState<DevWeatherEffectOverride | null>(null);
const displayedLocationName = locationName ?? station.name;
const hasGlobalModel = currentWeather?.coverage === "global-model";
@@ -61,6 +65,7 @@ export function WeatherHero({
distanceKm !== undefined &&
distanceKm !== null &&
distanceKm >= 30;
const formattedDistance = formatDistance(distanceKm ?? null, language, distanceUnit);
const displayedStation = currentWeather
? {
...station,
@@ -91,9 +96,13 @@ export function WeatherHero({
: currentWeather?.precipitation10m !== null && currentWeather?.precipitation10m !== undefined
? t("weather.rainfall10m")
: t("weather.rainfallTotal"),
value: formatRainfall(displayedStation.rainfall, language),
value: formatRainfall(displayedStation.rainfall, language, precipitationUnit),
},
{
icon: Gauge,
label: t("weather.pressure"),
value: formatPressure(displayedStation.pressure, language, pressureUnit),
},
{ icon: Gauge, label: t("weather.pressure"), value: formatPressure(displayedStation.pressure, language) },
];
const effectPrecipitation10m =
devEffectOverride === "rain" || devEffectOverride === "thunderstorm"
@@ -149,16 +158,16 @@ export function WeatherHero({
: hasFullHybridAnalysis
? t("location.heroHybridSource", { location: displayedLocationName })
: hasPartialHybridAnalysis
? t("location.heroHybridPartial", { station: station.name, distance: distanceKm ?? 0 })
? t("location.heroHybridPartial", { station: station.name, distance: formattedDistance })
: locationName
? t("location.heroStationFallbackWithDistance", {
station: station.name,
distance: distanceKm ?? 0,
distance: formattedDistance,
})
: t("location.heroStationFallback", { station: station.name })}
</p>
{hasFullHybridAnalysis && locationName && (
<p>{t("location.heroNearestStation", { station: station.name, distance: distanceKm ?? 0 })}</p>
<p>{t("location.heroNearestStation", { station: station.name, distance: formattedDistance })}</p>
)}
{hasDistantFallback && (
<p className="flex items-start gap-1.5 text-warning">