Compare commits

...

3 Commits

Author SHA1 Message Date
zv
3515fca0da docs: document weather hero states
All checks were successful
CI / Lint, typecheck and build (push) Successful in 9m53s
2026-06-14 11:08:39 +02:00
zv
2d424655e5 fix: prefer hybrid cloud cover in hero 2026-06-14 11:05:04 +02:00
zv
a9b0c65527 feat: describe current sky in weather hero 2026-06-14 11:00:25 +02:00
5 changed files with 68 additions and 8 deletions

View File

@@ -12,7 +12,9 @@ import { ErrorState } from "@/components/states/error-state";
import { useI18n } from "@/lib/i18n";
import { useMeteoStationPositions } from "@/hooks/use-meteo-stations";
import { useCurrentWeather } from "@/hooks/use-current-weather";
import { useForecast } from "@/hooks/use-forecast";
import { ForecastPanel } from "@/components/forecast/forecast-panel";
import { getUpcomingHourlyForecast } from "@/lib/forecast-utils";
import { locateSynopStations } from "@/lib/location-utils";
import { DashboardWarnings } from "@/components/warnings/dashboard-warnings";
import { WeatherBriefCard } from "@/components/dashboard/weather-brief-card";
@@ -35,6 +37,8 @@ export function DashboardPage() {
const forecastLongitude = hasActiveLocationCoordinates ? activeLocation?.longitude : stationPosition?.longitude;
const forecastLocationName = hasActiveLocationCoordinates ? activeLocation?.name ?? selectedStation?.name : selectedStation?.name;
const { data: currentWeather, isPending: isCurrentWeatherPending } = useCurrentWeather(forecastLatitude, forecastLongitude);
const { data: forecast } = useForecast(forecastLatitude, forecastLongitude);
const currentForecastWeatherCode = forecast ? getUpcomingHourlyForecast(forecast.hourly, 1)[0]?.weatherCode ?? null : null;
const isCurrentWeatherLoading = Number.isFinite(forecastLatitude) && Number.isFinite(forecastLongitude) && isCurrentWeatherPending;
if (isPending) return <PageLoadingSkeleton />;
if (isError || !stations?.length || !selectedStation) return <ErrorState onRetry={() => refetch()} description={t("dashboard.error")} />;
@@ -42,7 +46,7 @@ export function DashboardPage() {
return (
<div className="space-y-10">
<LocationSearch stations={stations} positions={positions} />
<WeatherHero station={selectedStation} currentWeather={currentWeather} currentWeatherLoading={isCurrentWeatherLoading} locationName={activeLocation?.name} distanceKm={activeLocation?.distanceKm} />
<WeatherHero station={selectedStation} currentWeather={currentWeather} currentWeatherLoading={isCurrentWeatherLoading} currentForecastWeatherCode={currentForecastWeatherCode} locationName={activeLocation?.name} distanceKm={activeLocation?.distanceKm} />
<DashboardWarnings />
<WeatherBriefCard latitude={forecastLatitude} longitude={forecastLongitude} locationName={forecastLocationName ?? selectedStation.name} />
<ForecastPanel latitude={forecastLatitude} longitude={forecastLongitude} locationName={forecastLocationName ?? selectedStation.name} />

View File

@@ -42,7 +42,7 @@ function readDevWeatherEffectOverride(): DevWeatherEffectOverride | null {
return null;
}
export function WeatherHero({ station, currentWeather, currentWeatherLoading = false, locationName, distanceKm }: { station: SynopStation; currentWeather?: ImgwCurrentWeather | null; currentWeatherLoading?: boolean; locationName?: string; distanceKm?: number }) {
export function WeatherHero({ station, currentWeather, currentWeatherLoading = false, currentForecastWeatherCode, locationName, distanceKm }: { station: SynopStation; currentWeather?: ImgwCurrentWeather | null; currentWeatherLoading?: boolean; currentForecastWeatherCode?: number | null; locationName?: string; distanceKm?: number }) {
const { language, t } = useI18n();
const temperatureUnit = useWeatherStore((state) => state.temperatureUnit);
const windSpeedUnit = useWeatherStore((state) => state.windSpeedUnit);
@@ -80,6 +80,14 @@ export function WeatherHero({ station, currentWeather, currentWeatherLoading = f
: devEffectOverride === "rain" || devEffectOverride === "none"
? false
: currentWeather?.condition === "thunderstorm";
const weatherDescription = getWeatherDescription(
displayedStation,
language,
currentWeather?.condition,
currentWeather?.weatherCode,
currentWeather?.cloudCover,
currentForecastWeatherCode,
);
useEffect(() => {
if (process.env.NODE_ENV !== "development") return;
@@ -102,7 +110,7 @@ export function WeatherHero({ station, currentWeather, currentWeatherLoading = f
<div className="flex flex-wrap items-center gap-2">
<span className="flex items-center gap-1.5 text-sm font-medium text-muted"><MapPin className="size-4" />{displayedLocationName}</span>
<span className={`rounded-control border px-2.5 py-1 text-[0.68rem] font-semibold uppercase tracking-[0.14em] ${moodAccent}`}>
{getWeatherDescription(displayedStation, language, currentWeather?.condition)}
{weatherDescription}
</span>
</div>
<div className="mt-2 space-y-1 text-xs text-muted">
@@ -124,7 +132,7 @@ export function WeatherHero({ station, currentWeather, currentWeatherLoading = f
<div className="text-[5.8rem] font-semibold leading-[0.85] tracking-[-0.1em] sm:text-[8rem]">
{formatTemperature(displayedStation.temperature, language, temperatureUnit)}
</div>
<p className="mt-5 text-xl font-medium tracking-tight sm:text-2xl">{getWeatherDescription(displayedStation, language, currentWeather?.condition)}</p>
<p className="mt-5 text-xl font-medium tracking-tight sm:text-2xl">{weatherDescription}</p>
<p className="mt-1 text-sm text-muted">{t("weather.feelsLike")} {formatTemperature(feelsLike, language, temperatureUnit)} · {t("weather.measurement")} {formatDateTime(displayedStation.measuredAt, language)}</p>
</div>
<WeatherIcon mood={mood} condition={currentWeather?.condition} className="mb-4 size-20 text-accent sm:size-28" />

View File

@@ -77,7 +77,22 @@ Prognoza godzinowa i dzienna rozpoznaje:
| `thunderstorm` | Burza |
| `unknown` | Brak opisu |
Bieżąca analiza IMGW Hybrid rozpoznaje bezpośrednio opad deszczu, śnieg i burzę. Gdy żadne z tych zjawisk nie występuje, hero może pokazać pomocniczy opis: `Silny wiatr`, `Wilgotne warunki` albo `Spokojne warunki`.
Bieżąca analiza IMGW Hybrid rozpoznaje bezpośrednio opad deszczu, śnieg i burzę. Dashboard hero używa opisu warunków w następującym priorytecie:
1. bieżące zjawisko z IMGW Hybrid: `Burza`, `Opady śniegu`, `Opady deszczu`,
2. silny wiatr od `8 m/s`: `Silny wiatr`,
3. bieżący kod pogody i zachmurzenie z IMGW Hybrid,
4. najbliższa godzina prognozy modelowej, jeśli Hybrid nie daje opisu nieba,
5. wysoka wilgotność od `90%`: `Wilgotno`,
6. fallback: `Pogodnie`.
Pole `Cloud` z IMGW Hybrid jest używane jako opis nieba:
| Cloud | Opis w hero |
| --- | --- |
| `>= 75` | Pochmurno |
| `>= 25` | Częściowe zachmurzenie |
| `< 25` | brak osobnego opisu zachmurzenia |
Jednostki temperatury i wiatru wybierane w `/settings` dotyczą prezentacji w UI, briefach i powiadomieniach. Dane źródłowe oraz progi logiki pogody pozostają liczone w `°C` i `m/s`.

View File

@@ -133,7 +133,7 @@ const translations = {
"weather.feelsLike": "Odczuwalna",
"weather.measurement": "pomiar",
"weather.windDirection": "Kierunek wiatru",
"weather.calm": "Spokojne warunki",
"weather.calm": "Pogodnie",
"weather.humid": "Wilgotno",
"weather.strongWind": "Silny wiatr",
"weather.currentRain": "Opady deszczu",
@@ -380,7 +380,7 @@ const translations = {
"weather.feelsLike": "Feels like",
"weather.measurement": "measurement",
"weather.windDirection": "Wind direction",
"weather.calm": "Calm conditions",
"weather.calm": "Fair",
"weather.humid": "Humid",
"weather.strongWind": "Strong wind",
"weather.currentRain": "Rain",

View File

@@ -237,11 +237,44 @@ export function getWeatherMoodFromData(station: SynopStation, date = new Date())
return "mild";
}
export function getWeatherDescription(station: SynopStation, language: Language = "pl", condition?: CurrentWeatherCondition) {
function getForecastConditionDescription(code: number | null, language: Language) {
if (code === 0) return translate(language, "forecast.condition.clear");
if (code === 1 || code === 2) return translate(language, "forecast.condition.partlyCloudy");
if (code === 3) return translate(language, "forecast.condition.cloudy");
if (code === 45 || code === 48) return translate(language, "forecast.condition.fog");
if (code !== null && code >= 51 && code <= 57) return translate(language, "forecast.condition.drizzle");
if (code !== null && ((code >= 61 && code <= 67) || (code >= 80 && code <= 82))) return translate(language, "forecast.condition.rain");
if (code !== null && ((code >= 71 && code <= 77) || code === 85 || code === 86)) return translate(language, "forecast.condition.snow");
if (code !== null && code >= 95) return translate(language, "forecast.condition.thunderstorm");
return null;
}
function getCloudCoverDescription(cloudCover: number | null | undefined, language: Language) {
if (cloudCover === null || cloudCover === undefined) return null;
if (cloudCover >= 75) return translate(language, "forecast.condition.cloudy");
if (cloudCover >= 25) return translate(language, "forecast.condition.partlyCloudy");
return null;
}
export function getWeatherDescription(
station: SynopStation,
language: Language = "pl",
condition?: CurrentWeatherCondition,
currentWeatherCode?: number | null,
currentCloudCover?: number | null,
forecastWeatherCode?: number | null,
) {
if (condition === "thunderstorm") return translate(language, "weather.thunderstorm");
if (condition === "snow") return translate(language, "weather.currentSnow");
if (condition === "rain") return translate(language, "weather.currentRain");
if ((station.windSpeed ?? 0) >= 8) return translate(language, "weather.strongWind");
const currentDescription = getForecastConditionDescription(currentWeatherCode ?? null, language);
const cloudCoverDescription = getCloudCoverDescription(currentCloudCover, language);
if (cloudCoverDescription && (currentWeatherCode === null || currentWeatherCode === undefined || currentWeatherCode === 0)) return cloudCoverDescription;
if (currentDescription) return currentDescription;
if (cloudCoverDescription) return cloudCoverDescription;
const forecastDescription = getForecastConditionDescription(forecastWeatherCode ?? null, language);
if (forecastDescription) return forecastDescription;
if ((station.humidity ?? 0) >= 90) return translate(language, "weather.humid");
return translate(language, "weather.calm");
}