diff --git a/components/layout/app-shell.tsx b/components/layout/app-shell.tsx index 3ef55f1..47c14e3 100644 --- a/components/layout/app-shell.tsx +++ b/components/layout/app-shell.tsx @@ -3,16 +3,27 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; import { CloudSun, Droplets, Settings, TriangleAlert } from "lucide-react"; +import type { LucideIcon } from "lucide-react"; +import { isAppNavItemVisible } from "@/lib/app-sections"; import { NAV_ITEMS } from "@/lib/constants"; import { cn } from "@/lib/utils"; import { InstallPWAButton } from "@/components/ui/install-pwa-button"; import { useI18n } from "@/lib/i18n"; +import { useWeatherStore } from "@/lib/store"; -const icons = [CloudSun, TriangleAlert, Droplets, Settings]; +const iconsByHref: Record = { + "/": CloudSun, + "/warnings": TriangleAlert, + "/hydro": Droplets, + "/settings": Settings, +}; export function AppShell({ children }: { children: React.ReactNode }) { const pathname = usePathname(); const { t } = useI18n(); + const appSections = useWeatherStore((state) => state.appSections); + const visibleNavItems = NAV_ITEMS.filter((item) => isAppNavItemVisible(item.href, appSections)); + return (
@@ -21,7 +32,7 @@ export function AppShell({ children }: { children: React.ReactNode }) { wtr.
{children}
+ +
+
+
+
+

{t("settings.appSections.title")}

+

{t("settings.appSections.description")}

+
+
+ +
+ {APP_SECTION_SETTINGS.map((section) => ( + setAppSectionVisible(section.id, checked)} + /> + ))} +
+
+
diff --git a/docs/architecture.md b/docs/architecture.md index 84dc0d3..db3dcdb 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -22,7 +22,7 @@ Najważniejsze widoki: - `/` - dashboard pogody, wyszukiwarka lokalizacji, hero, prognoza, briefy i ostrzeżenia regionalne, - `/warnings` - pełny widok ostrzeżeń meteo i hydro, - `/hydro` - stacje hydrologiczne, -- `/settings` - język, motyw, widoczność sekcji dashboardu, lokalizacja powiadomień i Web Push, +- `/settings` - język, motyw, widoczność sekcji aplikacji i dashboardu, lokalizacja powiadomień i Web Push, - `/station/[id]` - szczegóły stacji, - `/offline` - fallback offline. @@ -43,7 +43,8 @@ Zustand w `lib/store.ts` przechowuje trwałe preferencje użytkownika w `localSt - wybraną stację albo lokalizację, - ustawienia powiadomień, - tryb wyboru województwa dla alertów, -- widoczność opcjonalnych sekcji dashboardu. +- widoczność opcjonalnych sekcji dashboardu, +- widoczność opcjonalnych sekcji aplikacji w nawigacji. Język interfejsu jest przechowywany osobno w `localStorage` pod kluczem `wtr:language`. diff --git a/lib/app-sections.ts b/lib/app-sections.ts new file mode 100644 index 0000000..13e5106 --- /dev/null +++ b/lib/app-sections.ts @@ -0,0 +1,46 @@ +import type { TranslationKey } from "@/lib/i18n"; + +export const APP_SECTION_SETTINGS = [ + { + id: "warnings", + href: "/warnings", + titleKey: "settings.appSections.warnings.title", + descriptionKey: "settings.appSections.warnings.description", + defaultVisible: true, + }, + { + id: "hydro", + href: "/hydro", + titleKey: "settings.appSections.hydro.title", + descriptionKey: "settings.appSections.hydro.description", + defaultVisible: false, + }, +] as const satisfies ReadonlyArray<{ + id: string; + href: string; + titleKey: TranslationKey; + descriptionKey: TranslationKey; + defaultVisible: boolean; +}>; + +export type AppSectionId = typeof APP_SECTION_SETTINGS[number]["id"]; + +export type AppSectionVisibility = Record; + +export const DEFAULT_APP_SECTION_VISIBILITY = APP_SECTION_SETTINGS.reduce((visibility, section) => ({ + ...visibility, + [section.id]: section.defaultVisible, +}), {} as AppSectionVisibility); + +export function normalizeAppSectionVisibility(value: unknown): AppSectionVisibility { + const stored = value && typeof value === "object" ? value as Partial> : {}; + return APP_SECTION_SETTINGS.reduce((visibility, section) => ({ + ...visibility, + [section.id]: typeof stored[section.id] === "boolean" ? stored[section.id] : section.defaultVisible, + }), {} as AppSectionVisibility); +} + +export function isAppNavItemVisible(href: string, visibility: AppSectionVisibility) { + const section = APP_SECTION_SETTINGS.find((candidate) => candidate.href === href); + return section ? visibility[section.id] : true; +} diff --git a/lib/i18n.tsx b/lib/i18n.tsx index 6b2e2d9..be1005b 100644 --- a/lib/i18n.tsx +++ b/lib/i18n.tsx @@ -32,6 +32,12 @@ const translations = { "settings.dashboardSections.weatherBrief.description": "Pokazuje deterministyczne podsumowanie dnia i krótką prognozę na jutro.", "settings.dashboardSections.featuredStations.title": "Szybki wybór", "settings.dashboardSections.featuredStations.description": "Pokazuje listę popularnych lokalizacji na dole strony głównej.", + "settings.appSections.title": "Sekcje aplikacji", + "settings.appSections.description": "Wybierz, które dodatkowe widoki mają być widoczne w nawigacji.", + "settings.appSections.warnings.title": "Ostrzeżenia", + "settings.appSections.warnings.description": "Pokazuje widok oficjalnych ostrzeżeń IMGW w głównej nawigacji.", + "settings.appSections.hydro.title": "Hydro", + "settings.appSections.hydro.description": "Pokazuje widok monitoringu hydrologicznego IMGW w głównej nawigacji.", "settings.units.temperature.title": "Jednostka temperatury", "settings.units.temperature.description": "Stosowana w prognozie, briefach, powiadomieniach i bieżących pomiarach na tym urządzeniu.", "settings.units.temperature.label": "Wybierz jednostkę temperatury", @@ -295,6 +301,12 @@ const translations = { "settings.dashboardSections.weatherBrief.description": "Shows the deterministic daily summary and a short forecast for tomorrow.", "settings.dashboardSections.featuredStations.title": "Quick select", "settings.dashboardSections.featuredStations.description": "Shows the popular locations list at the bottom of the home screen.", + "settings.appSections.title": "App sections", + "settings.appSections.description": "Choose which additional views are visible in navigation.", + "settings.appSections.warnings.title": "Warnings", + "settings.appSections.warnings.description": "Shows the official IMGW warnings view in main navigation.", + "settings.appSections.hydro.title": "Hydro", + "settings.appSections.hydro.description": "Shows the IMGW hydrological monitoring view in main navigation.", "settings.units.temperature.title": "Temperature unit", "settings.units.temperature.description": "Used in forecasts, briefs, notifications and current readings on this device.", "settings.units.temperature.label": "Choose temperature unit", diff --git a/lib/store.ts b/lib/store.ts index b78dc13..2441e07 100644 --- a/lib/store.ts +++ b/lib/store.ts @@ -5,6 +5,7 @@ import { persist } from "zustand/middleware"; import type { SelectedLocation } from "@/types/location"; import type { Province } from "@/types/province"; import type { TemperatureUnit, WindSpeedUnit } from "@/types/units"; +import { DEFAULT_APP_SECTION_VISIBILITY, normalizeAppSectionVisibility, type AppSectionId, type AppSectionVisibility } from "@/lib/app-sections"; import { DEFAULT_DASHBOARD_SECTION_VISIBILITY, normalizeDashboardSectionVisibility, type DashboardSectionId, type DashboardSectionVisibility } from "@/lib/dashboard-sections"; import { DEFAULT_TEMPERATURE_UNIT, DEFAULT_WIND_SPEED_UNIT } from "@/lib/weather-utils"; import type { WeatherRegion } from "@/types/weather-region"; @@ -24,6 +25,7 @@ interface WeatherStore { temperatureUnit: TemperatureUnit; windSpeedUnit: WindSpeedUnit; dashboardSections: DashboardSectionVisibility; + appSections: AppSectionVisibility; toggleFavorite: (id: string) => void; selectStation: (id: string) => void; selectLocation: (location: SelectedLocation) => void; @@ -35,6 +37,7 @@ interface WeatherStore { setTemperatureUnit: (unit: TemperatureUnit) => void; setWindSpeedUnit: (unit: WindSpeedUnit) => void; setDashboardSectionVisible: (section: DashboardSectionId, visible: boolean) => void; + setAppSectionVisible: (section: AppSectionId, visible: boolean) => void; } export const useWeatherStore = create()( @@ -51,6 +54,7 @@ export const useWeatherStore = create()( temperatureUnit: DEFAULT_TEMPERATURE_UNIT, windSpeedUnit: DEFAULT_WIND_SPEED_UNIT, dashboardSections: DEFAULT_DASHBOARD_SECTION_VISIBILITY, + appSections: DEFAULT_APP_SECTION_VISIBILITY, toggleFavorite: (id) => set((state) => ({ favorites: state.favorites.includes(id) @@ -69,6 +73,9 @@ export const useWeatherStore = create()( setDashboardSectionVisible: (section, visible) => set((state) => ({ dashboardSections: { ...state.dashboardSections, [section]: visible }, })), + setAppSectionVisible: (section, visible) => set((state) => ({ + appSections: { ...state.appSections, [section]: visible }, + })), }), { name: "wtr:preferences", @@ -77,12 +84,14 @@ export const useWeatherStore = create()( const location = state?.selectedLocation as (Partial & { region?: WeatherRegion }) | null | undefined; if (!state) return persisted; const dashboardSections = normalizeDashboardSectionVisibility(state.dashboardSections); - if (!location) return { ...state, dashboardSections }; + const appSections = normalizeAppSectionVisibility(state.appSections); + if (!location) return { ...state, dashboardSections, appSections }; const countryCode = location.countryCode ?? (location.province ? "PL" : null); const region = location.region ?? getWeatherRegionForCountryCode(countryCode); return { ...state, dashboardSections, + appSections, selectedLocation: { ...location, country: location.country ?? (countryCode === "PL" ? "Polska" : null),