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

@@ -5,9 +5,15 @@ import { Card } from "@/components/ui/card";
import { ChartTooltip } from "@/components/charts/chart-tooltip";
import { CHART_COLORS } from "@/lib/chart-theme";
import { useI18n } from "@/lib/i18n";
import { formatForecastRainfall, formatForecastTemperature } from "@/lib/forecast-utils";
import { formatForecastTemperature } from "@/lib/forecast-utils";
import { useWeatherStore } from "@/lib/store";
import { convertTemperature, formatTemperatureValue, getTemperatureUnitLabel } from "@/lib/weather-utils";
import {
convertPrecipitation,
convertTemperature,
formatTemperatureValue,
getPrecipitationUnitLabel,
getTemperatureUnitLabel,
} from "@/lib/weather-utils";
import type { HourlyForecast } from "@/types/forecast";
const INITIAL_CHART_DIMENSION = { width: 1, height: 1 };
@@ -22,6 +28,15 @@ function formatHour(value: string) {
return value.slice(11, 16);
}
function formatChartPrecipitation(value: number | null, language: "pl" | "en", unit: string) {
if (value === null) return "—";
const formattedValue = new Intl.NumberFormat(language === "pl" ? "pl-PL" : "en-GB", {
minimumFractionDigits: unit === "in" ? 2 : 0,
maximumFractionDigits: unit === "mm" ? (value < 1 ? 2 : 1) : 2,
}).format(value);
return `${formattedValue} ${unit}`;
}
function ChartLegend({ items }: { items: ChartLegendItem[] }) {
return (
<div className="mt-2 flex flex-wrap items-center gap-x-4 gap-y-2 text-[0.72rem] font-medium text-muted">
@@ -46,14 +61,16 @@ function ChartLegend({ items }: { items: ChartLegendItem[] }) {
export function DayForecastCharts({ hours }: { hours: HourlyForecast[] }) {
const { language, t } = useI18n();
const temperatureUnit = useWeatherStore((state) => state.temperatureUnit);
const precipitationUnit = useWeatherStore((state) => state.precipitationUnit);
const rows = hours.map((hour) => ({
time: formatHour(hour.time),
temperature: hour.temperature === null ? null : convertTemperature(hour.temperature, temperatureUnit),
feelsLike: hour.feelsLike === null ? null : convertTemperature(hour.feelsLike, temperatureUnit),
precipitation: hour.precipitation,
precipitation: hour.precipitation === null ? null : convertPrecipitation(hour.precipitation, precipitationUnit),
precipitationProbability: hour.precipitationProbability,
}));
const temperatureUnitLabel = getTemperatureUnitLabel(temperatureUnit);
const precipitationUnitLabel = getPrecipitationUnitLabel(precipitationUnit);
const temperatureLegendItems: ChartLegendItem[] = [
{ color: CHART_COLORS.temperature, label: t("forecast.temperature"), marker: "line" },
{ color: CHART_COLORS.feelsLike, label: t("forecast.apparentTemperature"), marker: "dashed-line" },
@@ -153,7 +170,7 @@ export function DayForecastCharts({ hours }: { hours: HourlyForecast[] }) {
axisLine={false}
tickLine={false}
tick={{ fill: "currentColor", fontSize: 11 }}
unit=" mm"
unit={` ${precipitationUnitLabel}`}
/>
<YAxis
yAxisId="probability"
@@ -170,7 +187,11 @@ export function DayForecastCharts({ hours }: { hours: HourlyForecast[] }) {
<ChartTooltip
valueFormatter={(entry) =>
entry.dataKey === "precipitation"
? formatForecastRainfall(typeof entry.value === "number" ? entry.value : null, language)
? formatChartPrecipitation(
typeof entry.value === "number" ? entry.value : null,
language,
precipitationUnitLabel,
)
: `${typeof entry.value === "number" ? entry.value : "—"}%`
}
/>

View File

@@ -6,7 +6,13 @@ import { ChartTooltip } from "@/components/charts/chart-tooltip";
import { Card } from "@/components/ui/card";
import { useI18n } from "@/lib/i18n";
import { useWeatherStore } from "@/lib/store";
import { convertWindSpeed, getWindSpeedUnitLabel } from "@/lib/weather-utils";
import {
convertPrecipitation,
convertWindSpeed,
convertWindSpeedToBeaufort,
getPrecipitationUnitLabel,
getWindSpeedUnitLabel,
} from "@/lib/weather-utils";
const INITIAL_CHART_DIMENSION = { width: 1, height: 1 };
const SNAPSHOT_COLORS = ["hsl(var(--chart-temperature))", "hsl(var(--chart-feels-like))", "hsl(var(--chart-rainfall))"];
@@ -14,10 +20,17 @@ const SNAPSHOT_COLORS = ["hsl(var(--chart-temperature))", "hsl(var(--chart-feels
export function SnapshotChart({ station }: { station: SynopStation }) {
const { t } = useI18n();
const windSpeedUnit = useWeatherStore((state) => state.windSpeedUnit);
const precipitationUnit = useWeatherStore((state) => state.precipitationUnit);
const windDigits = windSpeedUnit === "ms" ? 1 : 0;
const windSpeed =
station.windSpeed === null ? null : Number(convertWindSpeed(station.windSpeed, windSpeedUnit).toFixed(windDigits));
const windMax = windSpeedUnit === "ms" ? 20 : windSpeedUnit === "kmh" ? 72 : 45;
station.windSpeed === null
? null
: windSpeedUnit === "bft"
? convertWindSpeedToBeaufort(station.windSpeed)
: Number(convertWindSpeed(station.windSpeed, windSpeedUnit).toFixed(windDigits));
const windMax = windSpeedUnit === "ms" ? 20 : windSpeedUnit === "kmh" ? 72 : windSpeedUnit === "bft" ? 12 : 45;
const rainfall =
station.rainfall === null ? null : Number(convertPrecipitation(station.rainfall, precipitationUnit).toFixed(2));
const rows = [
{ name: t("weather.humidity"), value: station.humidity, unit: "%", max: 100, color: SNAPSHOT_COLORS[0] },
{
@@ -27,7 +40,13 @@ export function SnapshotChart({ station }: { station: SynopStation }) {
max: windMax,
color: SNAPSHOT_COLORS[1],
},
{ name: t("weather.rainfall"), value: station.rainfall, unit: "mm", max: 30, color: SNAPSHOT_COLORS[2] },
{
name: t("weather.rainfall"),
value: rainfall,
unit: getPrecipitationUnitLabel(precipitationUnit),
max: convertPrecipitation(30, precipitationUnit),
color: SNAPSHOT_COLORS[2],
},
]
.filter((row) => row.value !== null)
.map((row) => ({ ...row, normalized: Math.min(100, ((row.value ?? 0) / row.max) * 100) }));

View File

@@ -33,6 +33,7 @@ export function WeatherBriefCard({
const selectedLocation = useWeatherStore((state) => state.selectedLocation);
const temperatureUnit = useWeatherStore((state) => state.temperatureUnit);
const windSpeedUnit = useWeatherStore((state) => state.windSpeedUnit);
const precipitationUnit = useWeatherStore((state) => state.precipitationUnit);
const isGlobalLocation = region === "GLOBAL";
const province = isGlobalLocation
? null
@@ -50,6 +51,7 @@ export function WeatherBriefCard({
language,
temperatureUnit,
windSpeedUnit,
precipitationUnit,
});
}, [
forecast,
@@ -61,6 +63,7 @@ export function WeatherBriefCard({
selectedLocation?.countyTeryt,
temperatureUnit,
windSpeedUnit,
precipitationUnit,
]);
const tomorrowBrief = useMemo(() => {
if (!forecast) return null;
@@ -73,6 +76,7 @@ export function WeatherBriefCard({
language,
temperatureUnit,
windSpeedUnit,
precipitationUnit,
});
}, [
forecast,
@@ -84,6 +88,7 @@ export function WeatherBriefCard({
selectedLocation?.countyTeryt,
temperatureUnit,
windSpeedUnit,
precipitationUnit,
]);
if (!Number.isFinite(latitude) || !Number.isFinite(longitude) || isPending) {

View File

@@ -58,6 +58,7 @@ export function DayForecastModal({
const { language, locale, t } = useI18n();
const temperatureUnit = useWeatherStore((state) => state.temperatureUnit);
const windSpeedUnit = useWeatherStore((state) => state.windSpeedUnit);
const precipitationUnit = useWeatherStore((state) => state.precipitationUnit);
const closeButtonRef = useRef<HTMLButtonElement>(null);
const portalRoot = typeof document === "undefined" ? null : document.body;
const maximumWind = useMemo(() => getMaximumWind(hours), [hours]);
@@ -150,7 +151,7 @@ export function DayForecastModal({
<DayMetric
icon={Droplets}
label={t("forecast.precipitation")}
value={formatForecastRainfall(day.precipitation, language)}
value={formatForecastRainfall(day.precipitation, language, precipitationUnit)}
/>
<DayMetric
icon={Wind}

View File

@@ -77,6 +77,7 @@ function HourlyForecastSummary({ hours }: { hours: HourlyForecast[] }) {
const { language, t } = useI18n();
const temperatureUnit = useWeatherStore((state) => state.temperatureUnit);
const windSpeedUnit = useWeatherStore((state) => state.windSpeedUnit);
const precipitationUnit = useWeatherStore((state) => state.precipitationUnit);
const minimumTemperature = getMinimum(hours.map((hour) => hour.temperature));
const maximumTemperature = getMaximum(hours.map((hour) => hour.temperature));
const maximumWind = getMaximum(hours.map((hour) => hour.windSpeed));
@@ -102,7 +103,7 @@ function HourlyForecastSummary({ hours }: { hours: HourlyForecast[] }) {
<HourlySummaryMetric
icon={CloudRain}
label={t("forecast.rainfallTotal")}
value={formatForecastRainfall(rainfallTotal, language)}
value={formatForecastRainfall(rainfallTotal, language, precipitationUnit)}
/>
<HourlySummaryMetric
icon={Droplets}
@@ -125,6 +126,7 @@ function DailyForecastRow({
}) {
const { language, locale, t } = useI18n();
const temperatureUnit = useWeatherStore((state) => state.temperatureUnit);
const precipitationUnit = useWeatherStore((state) => state.precipitationUnit);
const label = formatDay(day.date, locale, t("forecast.today"), index);
return (
<motion.li
@@ -144,7 +146,8 @@ function DailyForecastRow({
<div className="flex min-w-0 items-center gap-2">
<ForecastIcon code={day.weatherCode} className="size-6 shrink-0 text-accent" />
<span className="truncate text-xs text-muted">
{getForecastCondition(day.weatherCode, language)} · {formatForecastRainfall(day.precipitation, language)}
{getForecastCondition(day.weatherCode, language)} ·{" "}
{formatForecastRainfall(day.precipitation, language, precipitationUnit)}
</span>
</div>
<span className="hidden items-center gap-1 text-xs text-accent sm:flex">
@@ -177,6 +180,7 @@ export function ForecastPanel({
const { language, t } = useI18n();
const temperatureUnit = useWeatherStore((state) => state.temperatureUnit);
const windSpeedUnit = useWeatherStore((state) => state.windSpeedUnit);
const precipitationUnit = useWeatherStore((state) => state.precipitationUnit);
const { data: forecast, isPending, isError, refetch } = useForecast(latitude, longitude, region);
const [selectedDay, setSelectedDay] = useState<DailyForecast | null>(null);
const closeDayDetails = useCallback(() => setSelectedDay(null), []);
@@ -261,7 +265,7 @@ export function ForecastPanel({
</p>
<p className="flex items-center justify-center gap-1" title={t("forecast.precipitation")}>
<CloudRain className="size-3 text-accent" />
{formatForecastRainfall(hour.precipitation, language)}
{formatForecastRainfall(hour.precipitation, language, precipitationUnit)}
</p>
</div>
</motion.li>

View File

@@ -6,10 +6,13 @@ import {
BellRing,
ChevronDown,
Clock3,
CloudRain,
Gauge,
Languages,
LayoutDashboard,
MapPinned,
Palette,
Ruler,
Settings,
Smartphone,
Thermometer,
@@ -36,8 +39,14 @@ import {
} from "@/lib/notification-api";
import { formatProvinceName, getProvinceForSelection, PROVINCES } from "@/lib/provinces";
import { useWeatherStore } from "@/lib/store";
import { TEMPERATURE_UNITS, WIND_SPEED_UNITS } from "@/lib/weather-utils";
import type { TemperatureUnit, WindSpeedUnit } from "@/types/units";
import {
DISTANCE_UNITS,
PRECIPITATION_UNITS,
PRESSURE_UNITS,
TEMPERATURE_UNITS,
WIND_SPEED_UNITS,
} from "@/lib/weather-utils";
import type { DistanceUnit, PrecipitationUnit, PressureUnit, TemperatureUnit, WindSpeedUnit } from "@/types/units";
type NotificationSupportStatus =
| "checking"
@@ -154,11 +163,17 @@ function NotificationSwitchLabel({
const windSpeedUnitKeys: Record<
WindSpeedUnit,
"settings.units.wind.ms" | "settings.units.wind.kmh" | "settings.units.wind.mph"
| "settings.units.wind.ms"
| "settings.units.wind.kmh"
| "settings.units.wind.mph"
| "settings.units.wind.kt"
| "settings.units.wind.bft"
> = {
ms: "settings.units.wind.ms",
kmh: "settings.units.wind.kmh",
mph: "settings.units.wind.mph",
kt: "settings.units.wind.kt",
bft: "settings.units.wind.bft",
};
const temperatureUnitKeys: Record<TemperatureUnit, "settings.units.temperature.c" | "settings.units.temperature.f"> = {
@@ -166,6 +181,33 @@ const temperatureUnitKeys: Record<TemperatureUnit, "settings.units.temperature.c
f: "settings.units.temperature.f",
};
const precipitationUnitKeys: Record<
PrecipitationUnit,
"settings.units.precipitation.mm" | "settings.units.precipitation.cm" | "settings.units.precipitation.in"
> = {
mm: "settings.units.precipitation.mm",
cm: "settings.units.precipitation.cm",
in: "settings.units.precipitation.in",
};
const pressureUnitKeys: Record<
PressureUnit,
| "settings.units.pressure.hpa"
| "settings.units.pressure.kpa"
| "settings.units.pressure.inhg"
| "settings.units.pressure.mmhg"
> = {
hpa: "settings.units.pressure.hpa",
kpa: "settings.units.pressure.kpa",
inhg: "settings.units.pressure.inhg",
mmhg: "settings.units.pressure.mmhg",
};
const distanceUnitKeys: Record<DistanceUnit, "settings.units.distance.km" | "settings.units.distance.mi"> = {
km: "settings.units.distance.km",
mi: "settings.units.distance.mi",
};
export function SettingsPage() {
const { language, t } = useI18n();
const [notificationStatus, setNotificationStatus] = useState<NotificationSupportStatus>("checking");
@@ -185,6 +227,9 @@ export function SettingsPage() {
const manualProvince = useWeatherStore((state) => state.warningNotificationProvince);
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 dashboardSections = useWeatherStore((state) => state.dashboardSections);
const appSections = useWeatherStore((state) => state.appSections);
const setNotificationsEnabled = useWeatherStore((state) => state.setWarningNotificationsEnabled);
@@ -194,6 +239,9 @@ export function SettingsPage() {
const setManualProvince = useWeatherStore((state) => state.setWarningNotificationProvince);
const setTemperatureUnit = useWeatherStore((state) => state.setTemperatureUnit);
const setWindSpeedUnit = useWeatherStore((state) => state.setWindSpeedUnit);
const setPrecipitationUnit = useWeatherStore((state) => state.setPrecipitationUnit);
const setPressureUnit = useWeatherStore((state) => state.setPressureUnit);
const setDistanceUnit = useWeatherStore((state) => state.setDistanceUnit);
const setDashboardSectionVisible = useWeatherStore((state) => state.setDashboardSectionVisible);
const setAppSectionVisible = useWeatherStore((state) => state.setAppSectionVisible);
@@ -268,6 +316,9 @@ export function SettingsPage() {
countyTeryt: notificationCountyTeryt,
temperatureUnit,
windSpeedUnit,
precipitationUnit,
pressureUnit,
distanceUnit,
}).catch(() => undefined);
}
@@ -289,6 +340,9 @@ export function SettingsPage() {
notificationTimezone,
selectedRegion,
temperatureUnit,
precipitationUnit,
pressureUnit,
distanceUnit,
tomorrowBriefEnabled,
vapidPublicKey,
windSpeedUnit,
@@ -376,6 +430,9 @@ export function SettingsPage() {
countyTeryt: notificationCountyTeryt,
temperatureUnit,
windSpeedUnit,
precipitationUnit,
pressureUnit,
distanceUnit,
});
setNotificationsEnabled(true);
setNotificationMessage(t("settings.notifications.saveSuccess"));
@@ -444,6 +501,9 @@ export function SettingsPage() {
countyTeryt: notificationCountyTeryt,
temperatureUnit,
windSpeedUnit,
precipitationUnit,
pressureUnit,
distanceUnit,
});
} catch {
setNotificationMessage(t("settings.notifications.saveError"));
@@ -470,6 +530,9 @@ export function SettingsPage() {
countyTeryt: notificationCountyTeryt,
temperatureUnit,
windSpeedUnit,
precipitationUnit,
pressureUnit,
distanceUnit,
});
} catch {
setNotificationMessage(t("settings.notifications.saveError"));
@@ -554,6 +617,90 @@ export function SettingsPage() {
))}
</div>
</SettingsRow>
<SettingsRow
icon={CloudRain}
title={t("settings.units.precipitation.title")}
description={t("settings.units.precipitation.description")}
>
<div
className="surface-control inline-flex rounded-full p-1"
role="radiogroup"
aria-label={t("settings.units.precipitation.label")}
>
{PRECIPITATION_UNITS.map((unit) => (
<button
key={unit}
type="button"
role="radio"
aria-checked={precipitationUnit === unit}
className={`min-w-14 rounded-full border px-3.5 py-1.5 text-sm font-medium transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent ${
precipitationUnit === unit
? "border-accent/45 bg-accent/10 text-accent shadow-soft"
: "border-transparent text-muted hover:bg-surface-muted/70 hover:text-foreground"
}`}
onClick={() => setPrecipitationUnit(unit)}
>
{t(precipitationUnitKeys[unit])}
</button>
))}
</div>
</SettingsRow>
<SettingsRow
icon={Gauge}
title={t("settings.units.pressure.title")}
description={t("settings.units.pressure.description")}
>
<div
className="surface-control inline-flex rounded-full p-1"
role="radiogroup"
aria-label={t("settings.units.pressure.label")}
>
{PRESSURE_UNITS.map((unit) => (
<button
key={unit}
type="button"
role="radio"
aria-checked={pressureUnit === unit}
className={`min-w-14 rounded-full border px-3.5 py-1.5 text-sm font-medium transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent ${
pressureUnit === unit
? "border-accent/45 bg-accent/10 text-accent shadow-soft"
: "border-transparent text-muted hover:bg-surface-muted/70 hover:text-foreground"
}`}
onClick={() => setPressureUnit(unit)}
>
{t(pressureUnitKeys[unit])}
</button>
))}
</div>
</SettingsRow>
<SettingsRow
icon={Ruler}
title={t("settings.units.distance.title")}
description={t("settings.units.distance.description")}
>
<div
className="surface-control inline-flex rounded-full p-1"
role="radiogroup"
aria-label={t("settings.units.distance.label")}
>
{DISTANCE_UNITS.map((unit) => (
<button
key={unit}
type="button"
role="radio"
aria-checked={distanceUnit === unit}
className={`min-w-14 rounded-full border px-3.5 py-1.5 text-sm font-medium transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent ${
distanceUnit === unit
? "border-accent/45 bg-accent/10 text-accent shadow-soft"
: "border-transparent text-muted hover:bg-surface-muted/70 hover:text-foreground"
}`}
onClick={() => setDistanceUnit(unit)}
>
{t(distanceUnitKeys[unit])}
</button>
))}
</div>
</SettingsRow>
</Card>
<Card className="p-4 sm:p-5">

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">