diff --git a/README.md b/README.md
index a3296b9..a8d16c5 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. Aplikacja prezentuje bieżące odczyty synoptyczne, stacje hydrologiczne oraz ostrzeżenia w spokojnym, mobilnym interfejsie z gradientami, kartami glassmorphism i subtelnymi animacjami.
+`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.
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.
@@ -38,9 +38,9 @@ npm run build
npm run start
```
-## Dane IMGW
+## Źródła danych
-Aplikacja korzysta wyłącznie z rzeczywistych publicznych danych IMGW:
+Bieżące pomiary i komunikaty pochodzą z rzeczywistych publicznych danych IMGW:
- dane synoptyczne: `https://danepubliczne.imgw.pl/api/data/synop`
- pojedyncza stacja synoptyczna: `https://danepubliczne.imgw.pl/api/data/synop/id/{id}`
@@ -50,20 +50,23 @@ Aplikacja korzysta wyłącznie z rzeczywistych publicznych danych IMGW:
- dane meteorologiczne: `https://danepubliczne.imgw.pl/api/data/meteo/`
- lista produktów: `https://danepubliczne.imgw.pl/api/data/product`
-Do wyszukiwania nazw miejscowości, bez pobierania danych pogodowych, używany jest endpoint `https://geocoding-api.open-meteo.com/v1/search`. Przed wdrożeniem komercyjnym należy sprawdzić aktualne warunki korzystania z Open-Meteo lub zastąpić geokoder własnym dostawcą.
+Prognoza godzinowa i 7-dniowa pochodzi z Open-Meteo Forecast API: `https://api.open-meteo.com/v1/forecast`. Jest prezentowana oddzielnie od bieżących pomiarów IMGW i podpisana w interfejsie jako prognoza modelowa.
-Przeglądarka pobiera dane przez whitelistowane proxy w `app/api/imgw/[...path]/route.ts`. Pozwala to ujednolicić cache, błędy API i bezpiecznie obsłużyć hydro bez mixed content.
+Do wyszukiwania nazw miejscowości używany jest endpoint `https://geocoding-api.open-meteo.com/v1/search`. Przed wdrożeniem komercyjnym należy sprawdzić aktualne warunki korzystania z Open-Meteo lub zastąpić usługę własnym dostawcą.
+
+Przeglądarka pobiera dane przez route handlery Next.js. Proxy IMGW w `app/api/imgw/[...path]/route.ts` pozwala ujednolicić cache, błędy API i bezpiecznie obsłużyć hydro bez mixed content. Prognozę obsługuje `app/api/forecast/route.ts`.
## Ograniczenia API
-Publiczny endpoint synoptyczny IMGW udostępnia najnowszy pomiar, a nie pełną prognozę godzinową lub wielodniową i nie historię odczytów. `wtr.` nie generuje fikcyjnych prognoz. Widok stacji prezentuje aktualne parametry i jawnie opisany snapshot pomiarowy. Brakujące wartości są oznaczane jako `Brak danych`.
+Publiczny endpoint synoptyczny IMGW udostępnia najnowszy pomiar, a nie pełną prognozę godzinową lub wielodniową i nie historię odczytów. Dlatego prognoza pochodzi z Open-Meteo i jest wyraźnie oddzielona od pomiarów IMGW. `wtr.` nie generuje fikcyjnych prognoz. Widok stacji prezentuje aktualne parametry i jawnie opisany snapshot pomiarowy. Brakujące wartości są oznaczane jako `Brak danych`.
Czas aktualizacji parametrów hydrologicznych może się różnić. Interfejs pokazuje czas pomiaru, aby starsze odczyty nie wyglądały na bieżące.
## Struktura projektu
```text
-app/ routing, layout, proxy IMGW, offline fallback
+app/ routing, layout, proxy danych, offline fallback
+components/forecast/ prognoza godzinowa i dzienna Open-Meteo
components/dashboard dashboard aplikacji
components/weather/ hero, stacje, metryki i szczegóły
components/warnings/ alerty meteo i hydro
diff --git a/app/api/forecast/route.ts b/app/api/forecast/route.ts
new file mode 100644
index 0000000..0a03a0d
--- /dev/null
+++ b/app/api/forecast/route.ts
@@ -0,0 +1,39 @@
+import { NextResponse } from "next/server";
+
+const FORECAST_URL = "https://api.open-meteo.com/v1/forecast";
+
+function parseCoordinate(value: string | null, min: number, max: number) {
+ if (!value?.trim()) return null;
+ const coordinate = Number(value);
+ return Number.isFinite(coordinate) && coordinate >= min && coordinate <= max ? coordinate : null;
+}
+
+export async function GET(request: Request) {
+ const { searchParams } = new URL(request.url);
+ const latitude = parseCoordinate(searchParams.get("latitude"), -90, 90);
+ const longitude = parseCoordinate(searchParams.get("longitude"), -180, 180);
+ if (latitude === null || longitude === null) {
+ return NextResponse.json({ error: "Invalid coordinates." }, { status: 400 });
+ }
+
+ const params = new URLSearchParams({
+ latitude: String(latitude),
+ longitude: String(longitude),
+ hourly: "temperature_2m,apparent_temperature,precipitation_probability,precipitation,weather_code,wind_speed_10m",
+ daily: "weather_code,temperature_2m_max,temperature_2m_min,precipitation_probability_max,precipitation_sum,sunrise,sunset",
+ timezone: "Europe/Warsaw",
+ forecast_hours: "24",
+ forecast_days: "7",
+ wind_speed_unit: "ms",
+ });
+
+ try {
+ const response = await fetch(`${FORECAST_URL}?${params}`, { next: { revalidate: 900 } });
+ if (!response.ok) return NextResponse.json({ error: "Forecast service is unavailable." }, { status: 502 });
+ return NextResponse.json(await response.json(), {
+ headers: { "Cache-Control": "public, s-maxage=900, stale-while-revalidate=1800" },
+ });
+ } catch {
+ return NextResponse.json({ error: "Forecast service is unavailable." }, { status: 502 });
+ }
+}
diff --git a/components/dashboard/dashboard-page.tsx b/components/dashboard/dashboard-page.tsx
index aa8aa65..a649940 100644
--- a/components/dashboard/dashboard-page.tsx
+++ b/components/dashboard/dashboard-page.tsx
@@ -11,6 +11,8 @@ import { PageLoadingSkeleton } from "@/components/states/loading-skeleton";
import { ErrorState } from "@/components/states/error-state";
import { useI18n } from "@/lib/i18n";
import { useMeteoStationPositions } from "@/hooks/use-meteo-stations";
+import { ForecastPanel } from "@/components/forecast/forecast-panel";
+import { locateSynopStations } from "@/lib/location-utils";
export function DashboardPage() {
const { t } = useI18n();
@@ -24,11 +26,17 @@ export function DashboardPage() {
?? stations.find((station) => station.name === DEFAULT_STATION_NAME)
?? stations[0];
const activeLocation = selectedLocation?.stationId === selectedStation.id ? selectedLocation : null;
+ const stationPosition = locateSynopStations(stations, positions).find((station) => station.id === selectedStation.id);
+ const hasActiveLocationCoordinates = Number.isFinite(activeLocation?.latitude) && Number.isFinite(activeLocation?.longitude);
+ const forecastLatitude = hasActiveLocationCoordinates ? activeLocation?.latitude : stationPosition?.latitude;
+ const forecastLongitude = hasActiveLocationCoordinates ? activeLocation?.longitude : stationPosition?.longitude;
+ const forecastLocationName = hasActiveLocationCoordinates ? activeLocation?.name ?? selectedStation.name : selectedStation.name;
return (