diff --git a/AGENTS.md b/AGENTS.md
index 2bca59b..801dfcf 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -39,6 +39,7 @@ Repozytorium nie ma obecnie skryptu testów, osobnego skryptu type-check ani for
- Traktuj IMGW jako źródło bieżących pomiarów, hydro i ostrzeżeń. Prognozę Open-Meteo pokazuj oddzielnie jako prognozę modelową. Nie generuj fikcyjnych danych ani nie przedstawiaj prognozy jako pomiaru IMGW.
- Route handler prognozy pobiera godzinowe dane Open-Meteo dla pełnych 7 dni. Dashboard pokazuje najbliższe 24 przyszłe godziny oraz wykresy pełnego bieżącego dnia, a widok szczegółowy dnia korzysta z pełnego zestawu godzin dla wybranej daty.
- `synop.suma_opadu` jest akumulowaną sumą opadu. Nie używaj jej jako sygnału, że pada w tej chwili, ani do sterowania animacją deszczu.
+- Ostrzeżenia hydro zawierają jawne województwa, a ostrzeżenia meteo kody powiatów TERYT. Normalizuj oba warianty przez `lib/provinces.ts`; nie filtruj ostrzeżeń wyłącznie po opisach tekstowych.
- GPS wymaga świadomej zgody użytkownika i HTTPS. Zaokrąglaj współrzędne przed użyciem i utrzymuj widoczną atrybucję OpenStreetMap dla reverse geocodingu Nominatim.
- Normalizuj zewnętrzne odpowiedzi i obsługuj `null`, puste pola oraz błędne wartości. Brak danych pokazuj jawnie zamiast uzupełniać estymacją.
- Dla pobierania danych używaj TanStack Query z sensownym `queryKey`, cache i retry. W UI zachowuj loading, error, retry oraz empty states.
diff --git a/README.md b/README.md
index f65a2e4..ec5a98f 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,8 @@ Wyszukiwarka na stronie głównej obsługuje miejscowości w całej Polsce. Nazw
Użytkownik może opcjonalnie udostępnić położenie GPS. Pozycja jest zaokrąglana do trzech miejsc po przecinku, czyli około 100 metrów, a nazwa miejscowości jest ustalana przez Nominatim / OpenStreetMap. Po zgodzie aplikacja wybiera lokalizację, najbliższą stację IMGW i prognozę. Geolocation API wymaga bezpiecznego kontekstu HTTPS. Wyjątkiem jest `localhost`; wejście z iPhone przez lokalny adres typu `http://192.168.x.x:3000` nie uruchomi systemowego pytania Safari.
+Widok ostrzeżeń priorytetyzuje komunikaty dla województwa wynikającego z miejscowości lub stacji wybranej w pogodzie. Ostrzeżenia meteorologiczne IMGW przypisuje do regionów na podstawie kodów TERYT, a hydrologiczne na podstawie jawnych pól województwa z API. Pozostałe aktywne komunikaty są wyświetlane niżej.
+
## Stack
- Next.js z App Router i TypeScript
diff --git a/components/warnings/warnings-panel.tsx b/components/warnings/warnings-panel.tsx
index befcbaf..d77b3fb 100644
--- a/components/warnings/warnings-panel.tsx
+++ b/components/warnings/warnings-panel.tsx
@@ -1,17 +1,64 @@
"use client";
+import { Map, MapPinned } from "lucide-react";
import { useWarnings } from "@/hooks/use-warnings";
import { WarningCard } from "@/components/warnings/warning-card";
import { PageLoadingSkeleton } from "@/components/states/loading-skeleton";
import { EmptyState } from "@/components/states/empty-state";
import { ErrorState } from "@/components/states/error-state";
+import { DEFAULT_STATION_ID } from "@/lib/constants";
import { useI18n } from "@/lib/i18n";
+import { formatProvinceName, getProvinceForStation, normalizeProvinceName } from "@/lib/provinces";
+import { useWeatherStore } from "@/lib/store";
+import type { WeatherWarning } from "@/types/imgw";
+
+function WarningGrid({ warnings, indexOffset = 0 }: { warnings: WeatherWarning[]; indexOffset?: number }) {
+ return (
+
+ {warnings.map((warning, index) => )}
+
+ );
+}
export function WarningsPanel() {
- const { t } = useI18n();
+ const { language, t } = useI18n();
const { data: warnings, isPending, isError, refetch } = useWarnings();
+ const selectedStationId = useWeatherStore((state) => state.selectedStationId);
+ const selectedLocation = useWeatherStore((state) => state.selectedLocation);
if (isPending) return ;
if (isError) return refetch()} description={t("warnings.error")} />;
if (!warnings?.length) return ;
- return {warnings.map((warning, index) => )}
;
+
+ const province = normalizeProvinceName(selectedLocation?.province)
+ ?? getProvinceForStation(selectedStationId ?? DEFAULT_STATION_ID);
+ if (!province) return ;
+
+ const provinceLabel = formatProvinceName(province, language);
+ const localWarnings = warnings.filter((warning) => warning.provinces.includes(province));
+ const otherWarnings = warnings.filter((warning) => !warning.provinces.includes(province));
+
+ return (
+
+
+
+
{t("warnings.myProvince")}
+
{provinceLabel}
+
{t("warnings.myProvinceDescription", { province: provinceLabel })}
+
+ {localWarnings.length
+ ?
+ : }
+
+
+ {otherWarnings.length > 0 && (
+
+
+
{t("warnings.otherRegions")}
+
{t("warnings.otherRegionsDescription")}
+
+
+
+ )}
+
+ );
}
diff --git a/lib/constants.ts b/lib/constants.ts
index 75e7c94..50e1612 100644
--- a/lib/constants.ts
+++ b/lib/constants.ts
@@ -1,4 +1,5 @@
export const DEFAULT_STATION_NAME = "Warszawa";
+export const DEFAULT_STATION_ID = "12375";
export const APP_NAME = "wtr.";
export const APP_TAGLINE = "Pogoda z danych IMGW. Prosto. Pięknie. Aktualnie.";
diff --git a/lib/i18n.tsx b/lib/i18n.tsx
index 45df055..a30bfca 100644
--- a/lib/i18n.tsx
+++ b/lib/i18n.tsx
@@ -135,6 +135,12 @@ const translations = {
"warnings.section": "Komunikaty IMGW",
"warnings.title": "Ostrzeżenia",
"warnings.description": "Aktualne ostrzeżenia meteorologiczne i hydrologiczne publikowane przez IMGW. Szczegóły obszaru i czasu obowiązywania pochodzą bezpośrednio z API.",
+ "warnings.myProvince": "Moje województwo",
+ "warnings.myProvinceDescription": "Najpierw pokazujemy komunikaty IMGW dotyczące województwa {province}, zgodnie z lokalizacją wybraną w pogodzie.",
+ "warnings.myProvinceEmptyTitle": "Brak ostrzeżeń dla Twojego województwa",
+ "warnings.myProvinceEmptyDescription": "IMGW nie publikuje obecnie aktywnych ostrzeżeń dotyczących województwa {province}.",
+ "warnings.otherRegions": "Pozostałe regiony",
+ "warnings.otherRegionsDescription": "Aktywne komunikaty IMGW dla pozostałych obszarów Polski.",
"warnings.error": "Nie udało się pobrać ostrzeżeń meteorologicznych ani hydrologicznych.",
"warnings.emptyTitle": "Brak aktywnych ostrzeżeń",
"warnings.emptyDescription": "IMGW nie publikuje obecnie ostrzeżeń meteorologicznych ani hydrologicznych.",
@@ -296,6 +302,12 @@ const translations = {
"warnings.section": "IMGW notices",
"warnings.title": "Warnings",
"warnings.description": "Current meteorological and hydrological warnings published by IMGW. Area and validity details come directly from the API.",
+ "warnings.myProvince": "My province",
+ "warnings.myProvinceDescription": "IMGW notices for the {province} province are shown first, based on the location selected in weather.",
+ "warnings.myProvinceEmptyTitle": "No warnings for your province",
+ "warnings.myProvinceEmptyDescription": "IMGW is not currently publishing active warnings for the {province} province.",
+ "warnings.otherRegions": "Other regions",
+ "warnings.otherRegionsDescription": "Active IMGW notices for the remaining areas of Poland.",
"warnings.error": "Unable to load meteorological or hydrological warnings.",
"warnings.emptyTitle": "No active warnings",
"warnings.emptyDescription": "IMGW is not currently publishing any meteorological or hydrological warnings.",
diff --git a/lib/provinces.ts b/lib/provinces.ts
new file mode 100644
index 0000000..d7849dd
--- /dev/null
+++ b/lib/provinces.ts
@@ -0,0 +1,133 @@
+import type { Language } from "@/lib/i18n";
+import type { Province } from "@/types/province";
+
+const provinceByTerytPrefix: Record = {
+ "02": "dolnośląskie",
+ "04": "kujawsko-pomorskie",
+ "06": "lubelskie",
+ "08": "lubuskie",
+ "10": "łódzkie",
+ "12": "małopolskie",
+ "14": "mazowieckie",
+ "16": "opolskie",
+ "18": "podkarpackie",
+ "20": "podlaskie",
+ "22": "pomorskie",
+ "24": "śląskie",
+ "26": "świętokrzyskie",
+ "28": "warmińsko-mazurskie",
+ "30": "wielkopolskie",
+ "32": "zachodniopomorskie",
+};
+
+const provinceByStationId: Record = {
+ "12100": "zachodniopomorskie",
+ "12105": "zachodniopomorskie",
+ "12115": "pomorskie",
+ "12120": "pomorskie",
+ "12125": "pomorskie",
+ "12135": "pomorskie",
+ "12155": "pomorskie",
+ "12160": "warmińsko-mazurskie",
+ "12185": "warmińsko-mazurskie",
+ "12195": "podlaskie",
+ "12200": "zachodniopomorskie",
+ "12205": "zachodniopomorskie",
+ "12210": "zachodniopomorskie",
+ "12215": "zachodniopomorskie",
+ "12230": "wielkopolskie",
+ "12235": "pomorskie",
+ "12250": "kujawsko-pomorskie",
+ "12270": "mazowieckie",
+ "12272": "warmińsko-mazurskie",
+ "12280": "warmińsko-mazurskie",
+ "12285": "mazowieckie",
+ "12295": "podlaskie",
+ "12300": "lubuskie",
+ "12310": "lubuskie",
+ "12330": "wielkopolskie",
+ "12345": "wielkopolskie",
+ "12360": "mazowieckie",
+ "12375": "mazowieckie",
+ "12385": "mazowieckie",
+ "12399": "lubelskie",
+ "12400": "lubuskie",
+ "12415": "dolnośląskie",
+ "12418": "wielkopolskie",
+ "12424": "dolnośląskie",
+ "12435": "wielkopolskie",
+ "12455": "łódzkie",
+ "12465": "łódzkie",
+ "12469": "łódzkie",
+ "12488": "mazowieckie",
+ "12495": "lubelskie",
+ "12497": "lubelskie",
+ "12500": "dolnośląskie",
+ "12510": "dolnośląskie",
+ "12520": "dolnośląskie",
+ "12530": "opolskie",
+ "12540": "śląskie",
+ "12550": "śląskie",
+ "12560": "śląskie",
+ "12566": "małopolskie",
+ "12570": "świętokrzyskie",
+ "12575": "małopolskie",
+ "12580": "podkarpackie",
+ "12585": "świętokrzyskie",
+ "12595": "lubelskie",
+ "12600": "śląskie",
+ "12625": "małopolskie",
+ "12650": "małopolskie",
+ "12660": "małopolskie",
+ "12670": "podkarpackie",
+ "12690": "podkarpackie",
+ "12695": "podkarpackie",
+};
+
+const provinceLabels: Record> = {
+ "dolnośląskie": { pl: "dolnośląskie", en: "Lower Silesian" },
+ "kujawsko-pomorskie": { pl: "kujawsko-pomorskie", en: "Kuyavian-Pomeranian" },
+ "lubelskie": { pl: "lubelskie", en: "Lublin" },
+ "lubuskie": { pl: "lubuskie", en: "Lubusz" },
+ "łódzkie": { pl: "łódzkie", en: "Łódź" },
+ "małopolskie": { pl: "małopolskie", en: "Lesser Poland" },
+ "mazowieckie": { pl: "mazowieckie", en: "Masovian" },
+ "opolskie": { pl: "opolskie", en: "Opole" },
+ "podkarpackie": { pl: "podkarpackie", en: "Subcarpathian" },
+ "podlaskie": { pl: "podlaskie", en: "Podlaskie" },
+ "pomorskie": { pl: "pomorskie", en: "Pomeranian" },
+ "śląskie": { pl: "śląskie", en: "Silesian" },
+ "świętokrzyskie": { pl: "świętokrzyskie", en: "Świętokrzyskie" },
+ "warmińsko-mazurskie": { pl: "warmińsko-mazurskie", en: "Warmian-Masurian" },
+ "wielkopolskie": { pl: "wielkopolskie", en: "Greater Poland" },
+ "zachodniopomorskie": { pl: "zachodniopomorskie", en: "West Pomeranian" },
+};
+
+function simplifyProvinceName(value: string) {
+ return value
+ .normalize("NFD")
+ .replace(/\p{Diacritic}/gu, "")
+ .toLocaleLowerCase("pl-PL")
+ .replace(/^wojewodztwo\s+/, "")
+ .trim();
+}
+
+const provinceBySimplifiedName = Object.fromEntries(
+ Object.keys(provinceLabels).map((province) => [simplifyProvinceName(province), province]),
+) as Record;
+
+export function getProvinceFromTeryt(code: string) {
+ return provinceByTerytPrefix[code.trim().slice(0, 2)] ?? null;
+}
+
+export function getProvinceForStation(stationId: string | null) {
+ return stationId ? provinceByStationId[stationId] ?? null : null;
+}
+
+export function normalizeProvinceName(value: string | null | undefined) {
+ return value ? provinceBySimplifiedName[simplifyProvinceName(value)] ?? null : null;
+}
+
+export function formatProvinceName(province: Province, language: Language) {
+ return provinceLabels[province][language];
+}
diff --git a/lib/weather-utils.ts b/lib/weather-utils.ts
index 2f1b4b7..9b71760 100644
--- a/lib/weather-utils.ts
+++ b/lib/weather-utils.ts
@@ -9,6 +9,7 @@ import type {
WarningKind,
} from "@/types/imgw";
import { translate, type Language } from "@/lib/i18n";
+import { getProvinceFromTeryt, normalizeProvinceName } from "@/lib/provinces";
const locales: Record = { pl: "pl-PL", en: "en-GB" };
@@ -67,10 +68,14 @@ export function normalizeHydroStation(raw: RawHydroStation): HydroStation | null
}
export function normalizeWarning(raw: RawWarning, kind: WarningKind, index: number): WeatherWarning {
+ const provinces = [...new Set([
+ ...(raw.obszary ?? []).map((area) => normalizeProvinceName(area.wojewodztwo)),
+ ...(raw.teryt ?? []).map(getProvinceFromTeryt),
+ ].filter((province) => province !== null))];
const describedAreas = (raw.obszary ?? [])
.map((area) => area.opis?.trim() || area.wojewodztwo?.trim())
.filter((area): area is string => Boolean(area));
- const areas = describedAreas.length ? describedAreas : (raw.teryt ?? []).map((code) => `TERYT ${code}`);
+ const areas = describedAreas.length ? describedAreas : provinces;
const title = raw.zdarzenie?.trim() || raw.nazwa_zdarzenia?.trim() || "";
return {
id: `${kind}-${raw.id ?? raw.numer ?? index}-${raw.data_od ?? raw.obowiazuje_od ?? "unknown"}`,
@@ -84,6 +89,7 @@ export function normalizeWarning(raw: RawWarning, kind: WarningKind, index: numb
publishedAt: normalizeDate(raw.opublikowano),
probability: toNumber(raw.prawdopodobienstwo),
areas,
+ provinces,
office: raw.biuro?.trim() || null,
};
}
diff --git a/types/imgw.ts b/types/imgw.ts
index 5976a12..edb3415 100644
--- a/types/imgw.ts
+++ b/types/imgw.ts
@@ -112,7 +112,9 @@ export interface WeatherWarning {
publishedAt: string | null;
probability: number | null;
areas: string[];
+ provinces: Province[];
office: string | null;
}
export type WeatherMood = "warm" | "cloudy" | "wind" | "cold" | "night" | "mild";
+import type { Province } from "@/types/province";
diff --git a/types/province.ts b/types/province.ts
new file mode 100644
index 0000000..1f08302
--- /dev/null
+++ b/types/province.ts
@@ -0,0 +1,17 @@
+export type Province =
+ | "dolnośląskie"
+ | "kujawsko-pomorskie"
+ | "lubelskie"
+ | "lubuskie"
+ | "łódzkie"
+ | "małopolskie"
+ | "mazowieckie"
+ | "opolskie"
+ | "podkarpackie"
+ | "podlaskie"
+ | "pomorskie"
+ | "śląskie"
+ | "świętokrzyskie"
+ | "warmińsko-mazurskie"
+ | "wielkopolskie"
+ | "zachodniopomorskie";