diff --git a/README.md b/README.md index 02ac795..f65a2e4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **Pogoda z danych IMGW. Prosto. Pięknie. Aktualnie.** -`wtr.` to nowoczesna pogodowa PWA dla Polski oparta o publiczne dane IMGW i jawnie oznaczoną prognozę modelową Open-Meteo. Aplikacja prezentuje bieżące odczyty synoptyczne, prognozę godzinową i 7-dniową, stacje hydrologiczne oraz ostrzeżenia w spokojnym, mobilnym interfejsie z gradientami, kartami glassmorphism i subtelnymi animacjami. Dashboard pokazuje rozszerzony desktopowy podgląd godzin, a także wykresy temperatury i opadu dla bieżącego dnia. Każdy dzień prognozy można także otworzyć w animowanym widoku szczegółowym z przebiegiem godzinowym oraz wykresami. +`wtr.` to nowoczesna pogodowa PWA dla Polski oparta o publiczne dane IMGW i jawnie oznaczoną prognozę modelową Open-Meteo. Aplikacja prezentuje bieżące odczyty synoptyczne, prognozę godzinową i 7-dniową, stacje hydrologiczne oraz ostrzeżenia w spokojnym, mobilnym interfejsie z gradientami, kartami glassmorphism i subtelnymi animacjami. Dashboard pokazuje rozszerzony desktopowy podgląd godzin z podsumowaniem najbliższej doby, a także wykresy temperatury i opadu dla bieżącego dnia. Każdy dzień prognozy można także otworzyć w animowanym widoku szczegółowym z przebiegiem godzinowym oraz wykresami. Interfejs jest dostępny po polsku i angielsku. Wybrany język jest zapisywany lokalnie w przeglądarce. Oryginalne treści ostrzeżeń oraz nazwy stacji pochodzą bezpośrednio z API IMGW i nie są automatycznie tłumaczone. diff --git a/components/forecast/forecast-panel.tsx b/components/forecast/forecast-panel.tsx index e21d5ed..a31a6fe 100644 --- a/components/forecast/forecast-panel.tsx +++ b/components/forecast/forecast-panel.tsx @@ -2,7 +2,7 @@ import { useCallback, useState } from "react"; import { motion } from "framer-motion"; -import { CalendarDays, ChevronRight, Clock3, CloudRain, CloudSun, Droplets, ExternalLink, RefreshCw, ThermometerSun, Wind } from "lucide-react"; +import { CalendarDays, ChevronRight, Clock3, CloudRain, CloudSun, Droplets, ExternalLink, RefreshCw, ThermometerSun, Wind, type LucideIcon } from "lucide-react"; import { DayForecastCharts } from "@/components/charts/day-forecast-charts"; import { DayForecastModal } from "@/components/forecast/day-forecast-modal"; import { ForecastIcon } from "@/components/forecast/forecast-icon"; @@ -20,7 +20,7 @@ import { getHourlyForecastForDay, getUpcomingHourlyForecast, } from "@/lib/forecast-utils"; -import type { DailyForecast } from "@/types/forecast"; +import type { DailyForecast, HourlyForecast } from "@/types/forecast"; function formatHour(value: string) { return value.slice(11, 16); @@ -31,6 +31,55 @@ function formatDay(value: string, locale: string, todayLabel: string, index: num return new Intl.DateTimeFormat(locale, { weekday: "short", timeZone: "UTC" }).format(new Date(`${value}T12:00:00Z`)); } +function getMaximum(values: Array) { + const availableValues = values.filter((value): value is number => value !== null); + return availableValues.length ? Math.max(...availableValues) : null; +} + +function getMinimum(values: Array) { + const availableValues = values.filter((value): value is number => value !== null); + return availableValues.length ? Math.min(...availableValues) : null; +} + +function getTotal(values: Array) { + const availableValues = values.filter((value): value is number => value !== null); + return availableValues.length ? availableValues.reduce((total, value) => total + value, 0) : null; +} + +function HourlySummaryMetric({ icon: Icon, label, value }: { icon: LucideIcon; label: string; value: string }) { + return ( +
+ +

{label}

+

{value}

+
+ ); +} + +function HourlyForecastSummary({ hours }: { hours: HourlyForecast[] }) { + const { language, t } = useI18n(); + const minimumTemperature = getMinimum(hours.map((hour) => hour.temperature)); + const maximumTemperature = getMaximum(hours.map((hour) => hour.temperature)); + const maximumWind = getMaximum(hours.map((hour) => hour.windSpeed)); + const rainfallTotal = getTotal(hours.map((hour) => hour.precipitation)); + const maximumRainfallProbability = getMaximum(hours.map((hour) => hour.precipitationProbability)); + const temperatureRange = minimumTemperature === null || maximumTemperature === null + ? "—" + : `${formatForecastTemperature(minimumTemperature, language)} / ${formatForecastTemperature(maximumTemperature, language)}`; + + return ( +
+

{t("forecast.nextHoursOverview")}

+
+ + + + +
+
+ ); +} + function DailyForecastRow({ day, index, onSelect }: { day: DailyForecast; index: number; onSelect: (day: DailyForecast) => void }) { const { language, locale, t } = useI18n(); const label = formatDay(day.date, locale, t("forecast.today"), index); @@ -95,7 +144,7 @@ export function ForecastPanel({ latitude, longitude, locationName }: { latitude?

{t("forecast.hourly")}

-
+
    {upcomingHours.map((hour, index) => (
+

{t("forecast.daily")}

diff --git a/lib/i18n.tsx b/lib/i18n.tsx index f379ad6..851355a 100644 --- a/lib/i18n.tsx +++ b/lib/i18n.tsx @@ -100,6 +100,10 @@ const translations = { "forecast.sunrise": "Wschód słońca", "forecast.sunset": "Zachód słońca", "forecast.maxWind": "Maks. wiatr", + "forecast.nextHoursOverview": "Najbliższe 24 godziny w skrócie", + "forecast.temperatureRange": "Zakres temperatur", + "forecast.rainfallTotal": "Suma opadu", + "forecast.maxProbability": "Maks. szansa opadu", "forecast.pastHour": "Miniona godzina", "forecast.source": "Źródło prognozy:", "forecast.error": "Nie udało się pobrać prognozy Open-Meteo.", @@ -258,6 +262,10 @@ const translations = { "forecast.sunrise": "Sunrise", "forecast.sunset": "Sunset", "forecast.maxWind": "Max. wind", + "forecast.nextHoursOverview": "Next 24 hours at a glance", + "forecast.temperatureRange": "Temperature range", + "forecast.rainfallTotal": "Rainfall total", + "forecast.maxProbability": "Max. rain chance", "forecast.pastHour": "Past hour", "forecast.source": "Forecast source:", "forecast.error": "Unable to load the Open-Meteo forecast.",