feat: prioritize warnings for selected province
This commit is contained in:
@@ -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.
|
- 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.
|
- 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.
|
- `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.
|
- 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ą.
|
- 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.
|
- Dla pobierania danych używaj TanStack Query z sensownym `queryKey`, cache i retry. W UI zachowuj loading, error, retry oraz empty states.
|
||||||
|
|||||||
@@ -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.
|
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
|
## Stack
|
||||||
|
|
||||||
- Next.js z App Router i TypeScript
|
- Next.js z App Router i TypeScript
|
||||||
|
|||||||
@@ -1,17 +1,64 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { Map, MapPinned } from "lucide-react";
|
||||||
import { useWarnings } from "@/hooks/use-warnings";
|
import { useWarnings } from "@/hooks/use-warnings";
|
||||||
import { WarningCard } from "@/components/warnings/warning-card";
|
import { WarningCard } from "@/components/warnings/warning-card";
|
||||||
import { PageLoadingSkeleton } from "@/components/states/loading-skeleton";
|
import { PageLoadingSkeleton } from "@/components/states/loading-skeleton";
|
||||||
import { EmptyState } from "@/components/states/empty-state";
|
import { EmptyState } from "@/components/states/empty-state";
|
||||||
import { ErrorState } from "@/components/states/error-state";
|
import { ErrorState } from "@/components/states/error-state";
|
||||||
|
import { DEFAULT_STATION_ID } from "@/lib/constants";
|
||||||
import { useI18n } from "@/lib/i18n";
|
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 (
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||||
|
{warnings.map((warning, index) => <WarningCard key={warning.id} warning={warning} index={index + indexOffset} />)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function WarningsPanel() {
|
export function WarningsPanel() {
|
||||||
const { t } = useI18n();
|
const { language, t } = useI18n();
|
||||||
const { data: warnings, isPending, isError, refetch } = useWarnings();
|
const { data: warnings, isPending, isError, refetch } = useWarnings();
|
||||||
|
const selectedStationId = useWeatherStore((state) => state.selectedStationId);
|
||||||
|
const selectedLocation = useWeatherStore((state) => state.selectedLocation);
|
||||||
if (isPending) return <PageLoadingSkeleton />;
|
if (isPending) return <PageLoadingSkeleton />;
|
||||||
if (isError) return <ErrorState onRetry={() => refetch()} description={t("warnings.error")} />;
|
if (isError) return <ErrorState onRetry={() => refetch()} description={t("warnings.error")} />;
|
||||||
if (!warnings?.length) return <EmptyState title={t("warnings.emptyTitle")} description={t("warnings.emptyDescription")} />;
|
if (!warnings?.length) return <EmptyState title={t("warnings.emptyTitle")} description={t("warnings.emptyDescription")} />;
|
||||||
return <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">{warnings.map((warning, index) => <WarningCard key={warning.id} warning={warning} index={index} />)}</div>;
|
|
||||||
|
const province = normalizeProvinceName(selectedLocation?.province)
|
||||||
|
?? getProvinceForStation(selectedStationId ?? DEFAULT_STATION_ID);
|
||||||
|
if (!province) return <WarningGrid warnings={warnings} />;
|
||||||
|
|
||||||
|
const provinceLabel = formatProvinceName(province, language);
|
||||||
|
const localWarnings = warnings.filter((warning) => warning.provinces.includes(province));
|
||||||
|
const otherWarnings = warnings.filter((warning) => !warning.provinces.includes(province));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-9">
|
||||||
|
<section className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<p className="flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.16em] text-sky-700 dark:text-sky-300"><MapPinned className="size-4" />{t("warnings.myProvince")}</p>
|
||||||
|
<h2 className="mt-2 text-2xl font-semibold capitalize tracking-tight">{provinceLabel}</h2>
|
||||||
|
<p className="mt-1 max-w-2xl text-sm leading-6 text-slate-600 dark:text-slate-300">{t("warnings.myProvinceDescription", { province: provinceLabel })}</p>
|
||||||
|
</div>
|
||||||
|
{localWarnings.length
|
||||||
|
? <WarningGrid warnings={localWarnings} />
|
||||||
|
: <EmptyState title={t("warnings.myProvinceEmptyTitle")} description={t("warnings.myProvinceEmptyDescription", { province: provinceLabel })} />}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{otherWarnings.length > 0 && (
|
||||||
|
<section className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<p className="flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.16em] text-slate-500 dark:text-slate-400"><Map className="size-4" />{t("warnings.otherRegions")}</p>
|
||||||
|
<p className="mt-1 max-w-2xl text-sm leading-6 text-slate-600 dark:text-slate-300">{t("warnings.otherRegionsDescription")}</p>
|
||||||
|
</div>
|
||||||
|
<WarningGrid warnings={otherWarnings} indexOffset={localWarnings.length} />
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const DEFAULT_STATION_NAME = "Warszawa";
|
export const DEFAULT_STATION_NAME = "Warszawa";
|
||||||
|
export const DEFAULT_STATION_ID = "12375";
|
||||||
export const APP_NAME = "wtr.";
|
export const APP_NAME = "wtr.";
|
||||||
export const APP_TAGLINE = "Pogoda z danych IMGW. Prosto. Pięknie. Aktualnie.";
|
export const APP_TAGLINE = "Pogoda z danych IMGW. Prosto. Pięknie. Aktualnie.";
|
||||||
|
|
||||||
|
|||||||
12
lib/i18n.tsx
12
lib/i18n.tsx
@@ -135,6 +135,12 @@ const translations = {
|
|||||||
"warnings.section": "Komunikaty IMGW",
|
"warnings.section": "Komunikaty IMGW",
|
||||||
"warnings.title": "Ostrzeżenia",
|
"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.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.error": "Nie udało się pobrać ostrzeżeń meteorologicznych ani hydrologicznych.",
|
||||||
"warnings.emptyTitle": "Brak aktywnych ostrzeżeń",
|
"warnings.emptyTitle": "Brak aktywnych ostrzeżeń",
|
||||||
"warnings.emptyDescription": "IMGW nie publikuje obecnie ostrzeżeń meteorologicznych ani hydrologicznych.",
|
"warnings.emptyDescription": "IMGW nie publikuje obecnie ostrzeżeń meteorologicznych ani hydrologicznych.",
|
||||||
@@ -296,6 +302,12 @@ const translations = {
|
|||||||
"warnings.section": "IMGW notices",
|
"warnings.section": "IMGW notices",
|
||||||
"warnings.title": "Warnings",
|
"warnings.title": "Warnings",
|
||||||
"warnings.description": "Current meteorological and hydrological warnings published by IMGW. Area and validity details come directly from the API.",
|
"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.error": "Unable to load meteorological or hydrological warnings.",
|
||||||
"warnings.emptyTitle": "No active warnings",
|
"warnings.emptyTitle": "No active warnings",
|
||||||
"warnings.emptyDescription": "IMGW is not currently publishing any meteorological or hydrological warnings.",
|
"warnings.emptyDescription": "IMGW is not currently publishing any meteorological or hydrological warnings.",
|
||||||
|
|||||||
133
lib/provinces.ts
Normal file
133
lib/provinces.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import type { Language } from "@/lib/i18n";
|
||||||
|
import type { Province } from "@/types/province";
|
||||||
|
|
||||||
|
const provinceByTerytPrefix: Record<string, Province> = {
|
||||||
|
"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<string, Province> = {
|
||||||
|
"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<Province, Record<Language, string>> = {
|
||||||
|
"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<string, Province>;
|
||||||
|
|
||||||
|
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];
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import type {
|
|||||||
WarningKind,
|
WarningKind,
|
||||||
} from "@/types/imgw";
|
} from "@/types/imgw";
|
||||||
import { translate, type Language } from "@/lib/i18n";
|
import { translate, type Language } from "@/lib/i18n";
|
||||||
|
import { getProvinceFromTeryt, normalizeProvinceName } from "@/lib/provinces";
|
||||||
|
|
||||||
const locales: Record<Language, string> = { pl: "pl-PL", en: "en-GB" };
|
const locales: Record<Language, string> = { 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 {
|
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 ?? [])
|
const describedAreas = (raw.obszary ?? [])
|
||||||
.map((area) => area.opis?.trim() || area.wojewodztwo?.trim())
|
.map((area) => area.opis?.trim() || area.wojewodztwo?.trim())
|
||||||
.filter((area): area is string => Boolean(area));
|
.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() || "";
|
const title = raw.zdarzenie?.trim() || raw.nazwa_zdarzenia?.trim() || "";
|
||||||
return {
|
return {
|
||||||
id: `${kind}-${raw.id ?? raw.numer ?? index}-${raw.data_od ?? raw.obowiazuje_od ?? "unknown"}`,
|
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),
|
publishedAt: normalizeDate(raw.opublikowano),
|
||||||
probability: toNumber(raw.prawdopodobienstwo),
|
probability: toNumber(raw.prawdopodobienstwo),
|
||||||
areas,
|
areas,
|
||||||
|
provinces,
|
||||||
office: raw.biuro?.trim() || null,
|
office: raw.biuro?.trim() || null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,7 +112,9 @@ export interface WeatherWarning {
|
|||||||
publishedAt: string | null;
|
publishedAt: string | null;
|
||||||
probability: number | null;
|
probability: number | null;
|
||||||
areas: string[];
|
areas: string[];
|
||||||
|
provinces: Province[];
|
||||||
office: string | null;
|
office: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WeatherMood = "warm" | "cloudy" | "wind" | "cold" | "night" | "mild";
|
export type WeatherMood = "warm" | "cloudy" | "wind" | "cold" | "night" | "mild";
|
||||||
|
import type { Province } from "@/types/province";
|
||||||
|
|||||||
17
types/province.ts
Normal file
17
types/province.ts
Normal file
@@ -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";
|
||||||
Reference in New Issue
Block a user