@@ -63,6 +70,23 @@ export function WeatherEffects({ station, mood }: { station: SynopStation; mood:
style={{ top: line.top, width: line.width }}
/>
))}
+ {isRaining && rainDrops.map((drop, index) => (
+
+ ))}
+ {thunderstorm && (
+
+ )}
{mood === "cold" && (
)}
diff --git a/components/weather/weather-hero.tsx b/components/weather/weather-hero.tsx
index 5175c8c..66ec8ad 100644
--- a/components/weather/weather-hero.tsx
+++ b/components/weather/weather-hero.tsx
@@ -15,19 +15,30 @@ import {
moodGradient,
} from "@/lib/weather-utils";
import type { SynopStation } from "@/types/imgw";
+import type { ImgwCurrentWeather } from "@/types/imgw-current";
import { WeatherIcon } from "@/components/weather/weather-icon";
import { WeatherEffects } from "@/components/weather/weather-effects";
import { useI18n } from "@/lib/i18n";
-export function WeatherHero({ station, locationName, distanceKm }: { station: SynopStation; locationName?: string; distanceKm?: number }) {
+export function WeatherHero({ station, currentWeather, locationName, distanceKm }: { station: SynopStation; currentWeather?: ImgwCurrentWeather | null; locationName?: string; distanceKm?: number }) {
const { language, t } = useI18n();
- const mood = getWeatherMoodFromData(station);
- const feelsLike = calculateFeelsLike(station.temperature, station.humidity, station.windSpeed);
+ const displayedStation = currentWeather ? {
+ ...station,
+ measuredAt: currentWeather.measuredAt,
+ temperature: currentWeather.temperature,
+ windSpeed: currentWeather.windSpeed,
+ windDirection: currentWeather.windDirection,
+ humidity: currentWeather.humidity,
+ pressure: currentWeather.pressure,
+ rainfall: currentWeather.precipitation10m,
+ } : station;
+ const mood = getWeatherMoodFromData(displayedStation);
+ const feelsLike = currentWeather?.feelsLike ?? calculateFeelsLike(displayedStation.temperature, displayedStation.humidity, displayedStation.windSpeed);
const metrics = [
- { icon: Droplets, label: t("weather.humidity"), value: formatHumidity(station.humidity, language) },
- { icon: Wind, label: t("weather.wind"), value: formatWind(station.windSpeed, null, language) },
- { icon: Umbrella, label: t("weather.rainfallTotal"), value: formatRainfall(station.rainfall, language) },
- { icon: Gauge, label: t("weather.pressure"), value: formatPressure(station.pressure, language) },
+ { icon: Droplets, label: t("weather.humidity"), value: formatHumidity(displayedStation.humidity, language) },
+ { icon: Wind, label: t("weather.wind"), value: formatWind(displayedStation.windSpeed, null, language) },
+ { icon: Umbrella, label: currentWeather ? t("weather.rainfall10m") : t("weather.rainfallTotal"), value: formatRainfall(displayedStation.rainfall, language) },
+ { icon: Gauge, label: t("weather.pressure"), value: formatPressure(displayedStation.pressure, language) },
];
return (
@@ -37,7 +48,7 @@ export function WeatherHero({ station, locationName, distanceKm }: { station: Sy
transition={{ duration: 0.55, ease: "easeOut" }}
className={`relative isolate overflow-hidden rounded-[2rem] bg-gradient-to-br ${moodGradient(mood)} px-5 py-6 text-white shadow-[0_24px_75px_rgba(15,23,42,0.24)] sm:px-8 sm:py-8 lg:px-10`}
>
-
+
@@ -48,12 +59,12 @@ export function WeatherHero({ station, locationName, distanceKm }: { station: Sy
- {formatTemperature(station.temperature, language)}
+ {formatTemperature(displayedStation.temperature, language)}
-
{getWeatherDescription(station, language)}
-
{t("weather.feelsLike")} {formatTemperature(feelsLike, language)} · {t("weather.measurement")} {formatDateTime(station.measuredAt, language)}
+
{getWeatherDescription(displayedStation, language, currentWeather?.condition)}
+
{t("weather.feelsLike")} {formatTemperature(feelsLike, language)} · {t("weather.measurement")} {formatDateTime(displayedStation.measuredAt, language)}{currentWeather ? ` · ${t("weather.hybridAnalysis")}` : ""}
-
+
{metrics.map(({ icon: Icon, label, value }) => (
@@ -63,10 +74,10 @@ export function WeatherHero({ station, locationName, distanceKm }: { station: Sy
))}
- {station.windDirection !== null && (
+ {displayedStation.windDirection !== null && (
-
- {t("weather.windDirection")}: {station.windDirection}°
+
+ {t("weather.windDirection")}: {displayedStation.windDirection}°
)}
diff --git a/components/weather/weather-icon.tsx b/components/weather/weather-icon.tsx
index 859da3f..95a2045 100644
--- a/components/weather/weather-icon.tsx
+++ b/components/weather/weather-icon.tsx
@@ -1,8 +1,9 @@
-import { Cloud, CloudSun, MoonStar, Snowflake, ThermometerSun, Wind } from "lucide-react";
+import { Cloud, CloudLightning, CloudRain, CloudSun, MoonStar, Snowflake, ThermometerSun, Wind } from "lucide-react";
import type { WeatherMood } from "@/types/imgw";
+import type { CurrentWeatherCondition } from "@/types/imgw-current";
-export function WeatherIcon({ mood, className = "" }: { mood: WeatherMood; className?: string }) {
- const Icon = {
+export function WeatherIcon({ mood, condition, className = "" }: { mood: WeatherMood; condition?: CurrentWeatherCondition; className?: string }) {
+ const Icon = condition === "thunderstorm" ? CloudLightning : condition === "rain" ? CloudRain : condition === "snow" ? Snowflake : {
warm: ThermometerSun,
cloudy: Cloud,
wind: Wind,
diff --git a/hooks/use-current-weather.ts b/hooks/use-current-weather.ts
new file mode 100644
index 0000000..a629f89
--- /dev/null
+++ b/hooks/use-current-weather.ts
@@ -0,0 +1,18 @@
+"use client";
+
+import { useQuery } from "@tanstack/react-query";
+import { fetchImgwCurrentWeather } from "@/lib/imgw-current-api";
+import { QUERY_GC_TIME } from "@/lib/constants";
+
+const CURRENT_WEATHER_STALE_TIME = 2 * 60 * 1000;
+
+export function useCurrentWeather(latitude?: number, longitude?: number) {
+ return useQuery({
+ queryKey: ["imgw-current-weather", latitude, longitude],
+ queryFn: ({ signal }) => fetchImgwCurrentWeather(latitude as number, longitude as number, signal),
+ staleTime: CURRENT_WEATHER_STALE_TIME,
+ gcTime: QUERY_GC_TIME,
+ retry: 1,
+ enabled: Number.isFinite(latitude) && Number.isFinite(longitude),
+ });
+}
diff --git a/lib/i18n.tsx b/lib/i18n.tsx
index a30bfca..f797cee 100644
--- a/lib/i18n.tsx
+++ b/lib/i18n.tsx
@@ -61,6 +61,7 @@ const translations = {
"weather.humidity": "Wilgotność",
"weather.wind": "Wiatr",
"weather.rainfall": "Suma opadu",
+ "weather.rainfall10m": "Opad 10 min",
"weather.pressure": "Ciśnienie",
"weather.feelsLike": "Odczuwalna",
"weather.measurement": "pomiar",
@@ -68,6 +69,10 @@ const translations = {
"weather.calm": "Spokojne warunki",
"weather.humid": "Wilgotno",
"weather.strongWind": "Silny wiatr",
+ "weather.currentRain": "Opady deszczu",
+ "weather.currentSnow": "Opady śniegu",
+ "weather.thunderstorm": "Burza",
+ "weather.hybridAnalysis": "analiza IMGW Hybrid",
"weather.airTemperature": "Temperatura",
"weather.windSpeed": "Prędkość wiatru",
"weather.rainfallTotal": "Suma opadu",
@@ -228,6 +233,7 @@ const translations = {
"weather.humidity": "Humidity",
"weather.wind": "Wind",
"weather.rainfall": "Rainfall total",
+ "weather.rainfall10m": "Rainfall 10 min",
"weather.pressure": "Pressure",
"weather.feelsLike": "Feels like",
"weather.measurement": "measurement",
@@ -235,6 +241,10 @@ const translations = {
"weather.calm": "Calm conditions",
"weather.humid": "Humid",
"weather.strongWind": "Strong wind",
+ "weather.currentRain": "Rain",
+ "weather.currentSnow": "Snow",
+ "weather.thunderstorm": "Thunderstorm",
+ "weather.hybridAnalysis": "IMGW Hybrid analysis",
"weather.airTemperature": "Temperature",
"weather.windSpeed": "Wind speed",
"weather.rainfallTotal": "Rainfall total",
diff --git a/lib/imgw-current-api.ts b/lib/imgw-current-api.ts
new file mode 100644
index 0000000..cb7af2c
--- /dev/null
+++ b/lib/imgw-current-api.ts
@@ -0,0 +1,77 @@
+import { toNumber } from "@/lib/weather-utils";
+import type { ImgwCurrentWeather, RawImgwHybridWeatherResponse, RawImgwHybridWeatherRow } from "@/types/imgw-current";
+
+function toCelsius(value: unknown) {
+ const temperature = toNumber(value);
+ if (temperature === null) return null;
+ return temperature > 150 ? temperature - 273.15 : temperature;
+}
+
+function toHectopascals(value: unknown) {
+ const pressure = toNumber(value);
+ if (pressure === null) return null;
+ return pressure > 2_000 ? pressure / 100 : pressure;
+}
+
+function normalizeDate(value: unknown) {
+ if (typeof value !== "string") return null;
+ const date = new Date(value);
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
+}
+
+function getWeatherCode(iconCode: unknown) {
+ if (typeof iconCode !== "string") return null;
+ const match = iconCode.match(/z(\d{2})/i);
+ return match ? Number(match[1]) : null;
+}
+
+function getCondition(weatherCode: number | null, rainfall10m: number | null, snowfall10m: number | null) {
+ if (weatherCode !== null && weatherCode >= 95) return "thunderstorm" as const;
+ if ((snowfall10m ?? 0) > 0) return "snow" as const;
+ if ((rainfall10m ?? 0) > 0) return "rain" as const;
+ return null;
+}
+
+export function normalizeImgwCurrentWeather(payload: RawImgwHybridWeatherResponse): ImgwCurrentWeather | null {
+ if (!payload.data?.Valid || !Array.isArray(payload.data.Data)) return null;
+
+ const row = payload.data.Data
+ .filter((candidate): candidate is RawImgwHybridWeatherRow => {
+ if (!candidate || typeof candidate !== "object") return false;
+ return candidate.Type === "Type_Ten_Minutes"
+ && typeof candidate.MODEL === "string"
+ && candidate.MODEL.includes("AROME")
+ && normalizeDate(candidate.Date) !== null;
+ })
+ .sort((left, right) => String(right.Date).localeCompare(String(left.Date)))[0];
+ if (!row) return null;
+
+ const measuredAt = normalizeDate(row.Date);
+ if (!measuredAt) return null;
+ const rainfall10m = toNumber(row.Rain10m);
+ const snowfall10m = toNumber(row.Snow10m);
+ const weatherCode = getWeatherCode(row.Icon10);
+
+ return {
+ measuredAt,
+ temperature: toCelsius(row.Temperature),
+ feelsLike: toCelsius(row.Chill),
+ windSpeed: toNumber(row.Wind_Speed),
+ windDirection: toNumber(row.Wind_Dir),
+ humidity: toNumber(row.Humidity),
+ pressure: toHectopascals(row.PressureMSL),
+ precipitation10m: toNumber(row.Precipitation10m),
+ rainfall10m,
+ snowfall10m,
+ cloudCover: toNumber(row.Cloud),
+ weatherCode,
+ condition: getCondition(weatherCode, rainfall10m, snowfall10m),
+ };
+}
+
+export async function fetchImgwCurrentWeather(latitude: number, longitude: number, signal?: AbortSignal) {
+ const params = new URLSearchParams({ latitude: String(latitude), longitude: String(longitude) });
+ const response = await fetch(`/api/imgw-current?${params}`, { signal });
+ if (!response.ok) throw new Error("Nie udało się pobrać bieżącej analizy IMGW Hybrid.");
+ return normalizeImgwCurrentWeather(await response.json() as RawImgwHybridWeatherResponse);
+}
diff --git a/lib/weather-utils.ts b/lib/weather-utils.ts
index 9b71760..355281c 100644
--- a/lib/weather-utils.ts
+++ b/lib/weather-utils.ts
@@ -10,6 +10,7 @@ import type {
} from "@/types/imgw";
import { translate, type Language } from "@/lib/i18n";
import { getProvinceFromTeryt, normalizeProvinceName } from "@/lib/provinces";
+import type { CurrentWeatherCondition } from "@/types/imgw-current";
const locales: Record