diff --git a/README.md b/README.md
index 016314f..1e1d1bc 100644
--- a/README.md
+++ b/README.md
@@ -115,6 +115,7 @@ components/dashboard dashboard aplikacji
components/weather/ hero, stacje, metryki i szczegóły
components/warnings/ alerty meteo i hydro
components/hydro/ lista stacji hydrologicznych
+components/settings/ ustawienia języka, motywu i preferencji alertów
components/ui/ bazowe komponenty interfejsu
components/states/ loading, empty i error states
hooks/ zapytania TanStack Query
@@ -127,6 +128,8 @@ public/ manifest, ikony i service worker
Manifest znajduje się w `public/manifest.json`, a service worker w `public/sw.js`. Rejestracja service workera działa w buildzie produkcyjnym. Powłoka aplikacji ma podstawowy offline fallback. Odpowiedzi API mogą być dostępne z pamięci urządzenia przy braku sieci, ale UI nadal prezentuje czas pomiaru i status świeżości.
+Widok `/settings` zawiera pierwszy etap konfiguracji powiadomień o ostrzeżeniach meteorologicznych IMGW: wybór województwa, preferencję alertów i sprawdzenie zgody Web Push na urządzeniu. Na iOS powiadomienia webowe wymagają dodania PWA do ekranu początkowego i uruchomienia aplikacji z ikony. Realna wysyłka powiadomień wymaga jeszcze backendowego zapisu subskrypcji Web Push, kluczy VAPID oraz cyklicznego sprawdzania endpointu IMGW `warningsmeteo`; obecny UI nie wysyła jeszcze alertów.
+
## Wdrożenie na Vercel
1. Umieść repozytorium w serwisie Git.
diff --git a/app/settings/page.tsx b/app/settings/page.tsx
new file mode 100644
index 0000000..5b99841
--- /dev/null
+++ b/app/settings/page.tsx
@@ -0,0 +1,8 @@
+import type { Metadata } from "next";
+import { SettingsPage } from "@/components/settings/settings-page";
+
+export const metadata: Metadata = { title: "Ustawienia / Settings" };
+
+export default function Page() {
+ return ;
+}
diff --git a/components/layout/app-shell.tsx b/components/layout/app-shell.tsx
index 2f6cdb3..9ff6aa5 100644
--- a/components/layout/app-shell.tsx
+++ b/components/layout/app-shell.tsx
@@ -2,15 +2,13 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
-import { CloudSun, Droplets, TriangleAlert } from "lucide-react";
+import { CloudSun, Droplets, Settings, TriangleAlert } from "lucide-react";
import { NAV_ITEMS } from "@/lib/constants";
import { cn } from "@/lib/utils";
import { InstallPWAButton } from "@/components/ui/install-pwa-button";
-import { ThemeToggle } from "@/components/ui/theme-toggle";
-import { LanguageToggle } from "@/components/ui/language-toggle";
import { useI18n } from "@/lib/i18n";
-const icons = [CloudSun, TriangleAlert, Droplets];
+const icons = [CloudSun, TriangleAlert, Droplets, Settings];
export function AppShell({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
@@ -34,8 +32,6 @@ export function AppShell({ children }: { children: React.ReactNode }) {
-
-
@@ -45,9 +41,9 @@ export function AppShell({ children }: { children: React.ReactNode }) {
const Icon = icons[index];
const active = item.href === "/" ? pathname === "/" : pathname.startsWith(item.href);
return (
-
+
- {t(item.labelKey)}
+ {t(item.labelKey)}
);
})}
diff --git a/components/settings/settings-page.tsx b/components/settings/settings-page.tsx
new file mode 100644
index 0000000..b71e23f
--- /dev/null
+++ b/components/settings/settings-page.tsx
@@ -0,0 +1,218 @@
+"use client";
+
+import { useEffect, useMemo, useState } from "react";
+import { Bell, BellRing, Languages, MapPinned, Palette, Settings, Smartphone } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Card } from "@/components/ui/card";
+import { LanguageToggle } from "@/components/ui/language-toggle";
+import { ThemeToggle } from "@/components/ui/theme-toggle";
+import { DEFAULT_STATION_ID } from "@/lib/constants";
+import { useI18n } from "@/lib/i18n";
+import { formatProvinceName, getProvinceForSelection, PROVINCES } from "@/lib/provinces";
+import { useWeatherStore } from "@/lib/store";
+import type { Province } from "@/types/province";
+
+type NotificationSupportStatus = "checking" | "unsupported" | "needs-install" | "default" | "denied" | "granted";
+
+function isStandaloneApp() {
+ if (typeof window === "undefined") return false;
+ const navigatorWithStandalone = window.navigator as Navigator & { standalone?: boolean };
+ return window.matchMedia("(display-mode: standalone)").matches || navigatorWithStandalone.standalone === true;
+}
+
+function getNotificationSupportStatus(): NotificationSupportStatus {
+ if (typeof window === "undefined") return "checking";
+ if (!("Notification" in window) || !("serviceWorker" in navigator) || !("PushManager" in window)) return "unsupported";
+ if (!isStandaloneApp()) return "needs-install";
+ return Notification.permission;
+}
+
+function SettingsRow({ icon: Icon, title, description, children }: { icon: typeof Settings; title: string; description: string; children: React.ReactNode }) {
+ return (
+