Compare commits
4 Commits
2182297adc
...
8bbd9397a1
| Author | SHA1 | Date | |
|---|---|---|---|
| 8bbd9397a1 | |||
| 9087e1215c | |||
| 6be00da027 | |||
| 5049d81bd6 |
@@ -26,6 +26,7 @@ export function DashboardPage() {
|
|||||||
const { data: positions = [] } = useMeteoStationPositions();
|
const { data: positions = [] } = useMeteoStationPositions();
|
||||||
const selectedStationId = useWeatherStore((state) => state.selectedStationId);
|
const selectedStationId = useWeatherStore((state) => state.selectedStationId);
|
||||||
const selectedLocation = useWeatherStore((state) => state.selectedLocation);
|
const selectedLocation = useWeatherStore((state) => state.selectedLocation);
|
||||||
|
const dashboardSections = useWeatherStore((state) => state.dashboardSections);
|
||||||
const selectedStation = stations?.find((station) => station.id === selectedStationId)
|
const selectedStation = stations?.find((station) => station.id === selectedStationId)
|
||||||
?? stations?.find((station) => station.name === DEFAULT_STATION_NAME)
|
?? stations?.find((station) => station.name === DEFAULT_STATION_NAME)
|
||||||
?? stations?.[0];
|
?? stations?.[0];
|
||||||
@@ -62,10 +63,10 @@ export function DashboardPage() {
|
|||||||
<LocationSearch stations={stations} positions={positions} />
|
<LocationSearch stations={stations} positions={positions} />
|
||||||
<WeatherHero station={heroStation} currentWeather={currentWeather} currentWeatherLoading={isCurrentWeatherLoading} currentForecastWeatherCode={currentForecastWeatherCode} locationName={activeLocation?.name} distanceKm={activeLocation?.distanceKm} />
|
<WeatherHero station={heroStation} currentWeather={currentWeather} currentWeatherLoading={isCurrentWeatherLoading} currentForecastWeatherCode={currentForecastWeatherCode} locationName={activeLocation?.name} distanceKm={activeLocation?.distanceKm} />
|
||||||
<DashboardWarnings />
|
<DashboardWarnings />
|
||||||
<WeatherBriefCard latitude={forecastLatitude} longitude={forecastLongitude} region={activeRegion} locationName={forecastLocationName ?? selectedStation.name} />
|
{dashboardSections.weatherBrief && <WeatherBriefCard latitude={forecastLatitude} longitude={forecastLongitude} region={activeRegion} locationName={forecastLocationName ?? selectedStation.name} />}
|
||||||
<ForecastPanel latitude={forecastLatitude} longitude={forecastLongitude} region={activeRegion} locationName={forecastLocationName ?? selectedStation.name} />
|
<ForecastPanel latitude={forecastLatitude} longitude={forecastLongitude} region={activeRegion} locationName={forecastLocationName ?? selectedStation.name} />
|
||||||
<FavoritesSection stations={stations} />
|
<FavoritesSection stations={stations} />
|
||||||
<FeaturedStationsSection stations={stations} />
|
{dashboardSections.featuredStations && <FeaturedStationsSection stations={stations} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +3,27 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { CloudSun, Droplets, Settings, TriangleAlert } from "lucide-react";
|
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 { NAV_ITEMS } from "@/lib/constants";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { InstallPWAButton } from "@/components/ui/install-pwa-button";
|
import { InstallPWAButton } from "@/components/ui/install-pwa-button";
|
||||||
import { useI18n } from "@/lib/i18n";
|
import { useI18n } from "@/lib/i18n";
|
||||||
|
import { useWeatherStore } from "@/lib/store";
|
||||||
|
|
||||||
const icons = [CloudSun, TriangleAlert, Droplets, Settings];
|
const iconsByHref: Record<string, LucideIcon> = {
|
||||||
|
"/": CloudSun,
|
||||||
|
"/warnings": TriangleAlert,
|
||||||
|
"/hydro": Droplets,
|
||||||
|
"/settings": Settings,
|
||||||
|
};
|
||||||
|
|
||||||
export function AppShell({ children }: { children: React.ReactNode }) {
|
export function AppShell({ children }: { children: React.ReactNode }) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const appSections = useWeatherStore((state) => state.appSections);
|
||||||
|
const visibleNavItems = NAV_ITEMS.filter((item) => isAppNavItemVisible(item.href, appSections));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen overflow-x-hidden bg-background">
|
<div className="min-h-screen overflow-x-hidden bg-background">
|
||||||
<header className="app-header-safe sticky top-0 z-40 border-b border-border/70 bg-surface">
|
<header className="app-header-safe sticky top-0 z-40 border-b border-border/70 bg-surface">
|
||||||
@@ -21,7 +32,7 @@ export function AppShell({ children }: { children: React.ReactNode }) {
|
|||||||
wtr<span className="text-accent">.</span>
|
wtr<span className="text-accent">.</span>
|
||||||
</Link>
|
</Link>
|
||||||
<nav aria-label={t("nav.main")} className="hidden items-center gap-1 md:flex">
|
<nav aria-label={t("nav.main")} className="hidden items-center gap-1 md:flex">
|
||||||
{NAV_ITEMS.map((item) => {
|
{visibleNavItems.map((item) => {
|
||||||
const active = item.href === "/" ? pathname === "/" : pathname.startsWith(item.href);
|
const active = item.href === "/" ? pathname === "/" : pathname.startsWith(item.href);
|
||||||
return (
|
return (
|
||||||
<Link key={item.href} href={item.href} className={cn("rounded-control px-4 py-2 text-sm font-medium transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent", active ? "bg-foreground text-background shadow-soft" : "text-muted hover:bg-surface-muted/70")}>
|
<Link key={item.href} href={item.href} className={cn("rounded-control px-4 py-2 text-sm font-medium transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent", active ? "bg-foreground text-background shadow-soft" : "text-muted hover:bg-surface-muted/70")}>
|
||||||
@@ -37,8 +48,8 @@ export function AppShell({ children }: { children: React.ReactNode }) {
|
|||||||
</header>
|
</header>
|
||||||
<main className="app-main-safe mx-auto max-w-7xl px-4 pt-5 sm:px-6 sm:pt-8 lg:px-8">{children}</main>
|
<main className="app-main-safe mx-auto max-w-7xl px-4 pt-5 sm:px-6 sm:pt-8 lg:px-8">{children}</main>
|
||||||
<nav aria-label={t("nav.mobile")} className="mobile-nav-safe fixed z-50 flex justify-around rounded-panel border border-border/70 bg-surface p-1.5 shadow-card md:hidden">
|
<nav aria-label={t("nav.mobile")} className="mobile-nav-safe fixed z-50 flex justify-around rounded-panel border border-border/70 bg-surface p-1.5 shadow-card md:hidden">
|
||||||
{NAV_ITEMS.map((item, index) => {
|
{visibleNavItems.map((item) => {
|
||||||
const Icon = icons[index];
|
const Icon = iconsByHref[item.href] ?? CloudSun;
|
||||||
const active = item.href === "/" ? pathname === "/" : pathname.startsWith(item.href);
|
const active = item.href === "/" ? pathname === "/" : pathname.startsWith(item.href);
|
||||||
return (
|
return (
|
||||||
<Link key={item.href} href={item.href} className={cn("flex min-w-0 flex-1 flex-col items-center gap-1 rounded-card px-2 py-2 text-[0.68rem] font-medium transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent", active ? "bg-foreground text-background" : "text-muted")}>
|
<Link key={item.href} href={item.href} className={cn("flex min-w-0 flex-1 flex-col items-center gap-1 rounded-card px-2 py-2 text-[0.68rem] font-medium transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent", active ? "bg-foreground text-background" : "text-muted")}>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Bell, BellRing, ChevronDown, Clock3, Languages, MapPinned, Palette, Settings, Smartphone, Thermometer, Wind } from "lucide-react";
|
import { Bell, BellRing, ChevronDown, Clock3, Languages, LayoutDashboard, MapPinned, Palette, Settings, Smartphone, Thermometer, Wind } from "lucide-react";
|
||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
@@ -9,7 +9,9 @@ import { LanguageToggle } from "@/components/ui/language-toggle";
|
|||||||
import { ThemeToggle } from "@/components/ui/theme-toggle";
|
import { ThemeToggle } from "@/components/ui/theme-toggle";
|
||||||
import { useMeteoStationPositions } from "@/hooks/use-meteo-stations";
|
import { useMeteoStationPositions } from "@/hooks/use-meteo-stations";
|
||||||
import { useWeatherStations } from "@/hooks/use-weather-stations";
|
import { useWeatherStations } from "@/hooks/use-weather-stations";
|
||||||
|
import { APP_SECTION_SETTINGS } from "@/lib/app-sections";
|
||||||
import { DEFAULT_STATION_ID } from "@/lib/constants";
|
import { DEFAULT_STATION_ID } from "@/lib/constants";
|
||||||
|
import { DASHBOARD_SECTION_SETTINGS } from "@/lib/dashboard-sections";
|
||||||
import { useI18n } from "@/lib/i18n";
|
import { useI18n } from "@/lib/i18n";
|
||||||
import { locateSynopStations } from "@/lib/location-utils";
|
import { locateSynopStations } from "@/lib/location-utils";
|
||||||
import { decodeBase64UrlKey, deletePushSubscription, fetchVapidPublicKey, savePushSubscription, sendTestPushNotification } from "@/lib/notification-api";
|
import { decodeBase64UrlKey, deletePushSubscription, fetchVapidPublicKey, savePushSubscription, sendTestPushNotification } from "@/lib/notification-api";
|
||||||
@@ -131,6 +133,8 @@ export function SettingsPage() {
|
|||||||
const manualProvince = useWeatherStore((state) => state.warningNotificationProvince);
|
const manualProvince = useWeatherStore((state) => state.warningNotificationProvince);
|
||||||
const temperatureUnit = useWeatherStore((state) => state.temperatureUnit);
|
const temperatureUnit = useWeatherStore((state) => state.temperatureUnit);
|
||||||
const windSpeedUnit = useWeatherStore((state) => state.windSpeedUnit);
|
const windSpeedUnit = useWeatherStore((state) => state.windSpeedUnit);
|
||||||
|
const dashboardSections = useWeatherStore((state) => state.dashboardSections);
|
||||||
|
const appSections = useWeatherStore((state) => state.appSections);
|
||||||
const setNotificationsEnabled = useWeatherStore((state) => state.setWarningNotificationsEnabled);
|
const setNotificationsEnabled = useWeatherStore((state) => state.setWarningNotificationsEnabled);
|
||||||
const setMorningBriefEnabled = useWeatherStore((state) => state.setMorningBriefNotificationsEnabled);
|
const setMorningBriefEnabled = useWeatherStore((state) => state.setMorningBriefNotificationsEnabled);
|
||||||
const setTomorrowBriefEnabled = useWeatherStore((state) => state.setTomorrowBriefNotificationsEnabled);
|
const setTomorrowBriefEnabled = useWeatherStore((state) => state.setTomorrowBriefNotificationsEnabled);
|
||||||
@@ -138,6 +142,8 @@ export function SettingsPage() {
|
|||||||
const setManualProvince = useWeatherStore((state) => state.setWarningNotificationProvince);
|
const setManualProvince = useWeatherStore((state) => state.setWarningNotificationProvince);
|
||||||
const setTemperatureUnit = useWeatherStore((state) => state.setTemperatureUnit);
|
const setTemperatureUnit = useWeatherStore((state) => state.setTemperatureUnit);
|
||||||
const setWindSpeedUnit = useWeatherStore((state) => state.setWindSpeedUnit);
|
const setWindSpeedUnit = useWeatherStore((state) => state.setWindSpeedUnit);
|
||||||
|
const setDashboardSectionVisible = useWeatherStore((state) => state.setDashboardSectionVisible);
|
||||||
|
const setAppSectionVisible = useWeatherStore((state) => state.setAppSectionVisible);
|
||||||
|
|
||||||
const selectedStation = stations?.find((station) => station.id === selectedStationId)
|
const selectedStation = stations?.find((station) => station.id === selectedStationId)
|
||||||
?? stations?.find((station) => station.id === DEFAULT_STATION_ID)
|
?? stations?.find((station) => station.id === DEFAULT_STATION_ID)
|
||||||
@@ -435,6 +441,56 @@ export function SettingsPage() {
|
|||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4 sm:p-5">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="rounded-control border border-border/70 bg-surface-muted/70 p-2 text-accent">
|
||||||
|
<LayoutDashboard className="size-4" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-sm font-semibold">{t("settings.dashboardSections.title")}</h2>
|
||||||
|
<p className="mt-1 max-w-2xl text-sm leading-6 text-muted">{t("settings.dashboardSections.description")}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2">
|
||||||
|
{DASHBOARD_SECTION_SETTINGS.map((section) => (
|
||||||
|
<NotificationSwitchLabel
|
||||||
|
key={section.id}
|
||||||
|
icon={LayoutDashboard}
|
||||||
|
title={t(section.titleKey)}
|
||||||
|
description={t(section.descriptionKey)}
|
||||||
|
checked={dashboardSections[section.id]}
|
||||||
|
onChange={(checked) => setDashboardSectionVisible(section.id, checked)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-4 sm:p-5">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="rounded-control border border-border/70 bg-surface-muted/70 p-2 text-accent">
|
||||||
|
<Settings className="size-4" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-sm font-semibold">{t("settings.appSections.title")}</h2>
|
||||||
|
<p className="mt-1 max-w-2xl text-sm leading-6 text-muted">{t("settings.appSections.description")}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2">
|
||||||
|
{APP_SECTION_SETTINGS.map((section) => (
|
||||||
|
<NotificationSwitchLabel
|
||||||
|
key={section.id}
|
||||||
|
icon={Settings}
|
||||||
|
title={t(section.titleKey)}
|
||||||
|
description={t(section.descriptionKey)}
|
||||||
|
checked={appSections[section.id]}
|
||||||
|
onChange={(checked) => setAppSectionVisible(section.id, checked)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card className="p-4 sm:p-5">
|
<Card className="p-4 sm:p-5">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<div className="rounded-control border border-warning/30 bg-warning/10 p-2 text-warning">
|
<div className="rounded-control border border-warning/30 bg-warning/10 p-2 text-warning">
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
getWeatherDescription,
|
getWeatherDescription,
|
||||||
getWeatherMoodFromData,
|
getWeatherMoodFromData,
|
||||||
} from "@/lib/weather-utils";
|
} from "@/lib/weather-utils";
|
||||||
import type { SynopStation, WeatherMood } from "@/types/imgw";
|
import type { SynopStation } from "@/types/imgw";
|
||||||
import type { ImgwCurrentWeather } from "@/types/imgw-current";
|
import type { ImgwCurrentWeather } from "@/types/imgw-current";
|
||||||
import { WeatherIcon } from "@/components/weather/weather-icon";
|
import { WeatherIcon } from "@/components/weather/weather-icon";
|
||||||
import { WeatherEffects } from "@/components/weather/weather-effects";
|
import { WeatherEffects } from "@/components/weather/weather-effects";
|
||||||
@@ -23,17 +23,6 @@ import { useWeatherStore } from "@/lib/store";
|
|||||||
|
|
||||||
type DevWeatherEffectOverride = "rain" | "thunderstorm" | "none";
|
type DevWeatherEffectOverride = "rain" | "thunderstorm" | "none";
|
||||||
|
|
||||||
function moodAccentClass(mood: WeatherMood) {
|
|
||||||
return {
|
|
||||||
warm: "border-accent/25 bg-accent/10 text-accent",
|
|
||||||
cloudy: "border-border/70 bg-surface-muted text-muted",
|
|
||||||
wind: "border-border/70 bg-surface-muted text-muted",
|
|
||||||
cold: "border-border/70 bg-surface-muted text-muted",
|
|
||||||
night: "border-border/70 bg-surface-muted text-muted",
|
|
||||||
mild: "border-accent/25 bg-accent/10 text-accent",
|
|
||||||
}[mood];
|
|
||||||
}
|
|
||||||
|
|
||||||
function readDevWeatherEffectOverride(): DevWeatherEffectOverride | null {
|
function readDevWeatherEffectOverride(): DevWeatherEffectOverride | null {
|
||||||
if (process.env.NODE_ENV !== "development" || typeof window === "undefined") return null;
|
if (process.env.NODE_ENV !== "development" || typeof window === "undefined") return null;
|
||||||
const effect = new URLSearchParams(window.location.search).get("effect");
|
const effect = new URLSearchParams(window.location.search).get("effect");
|
||||||
@@ -63,7 +52,6 @@ export function WeatherHero({ station, currentWeather, currentWeatherLoading = f
|
|||||||
rainfall: currentWeather.precipitation10m ?? station.rainfall,
|
rainfall: currentWeather.precipitation10m ?? station.rainfall,
|
||||||
} : station;
|
} : station;
|
||||||
const mood = getWeatherMoodFromData(displayedStation);
|
const mood = getWeatherMoodFromData(displayedStation);
|
||||||
const moodAccent = moodAccentClass(mood);
|
|
||||||
const feelsLike = currentWeather?.feelsLike ?? calculateFeelsLike(displayedStation.temperature, displayedStation.humidity, displayedStation.windSpeed);
|
const feelsLike = currentWeather?.feelsLike ?? calculateFeelsLike(displayedStation.temperature, displayedStation.humidity, displayedStation.windSpeed);
|
||||||
const metrics = [
|
const metrics = [
|
||||||
{ icon: Droplets, label: t("weather.humidity"), value: formatHumidity(displayedStation.humidity, language) },
|
{ icon: Droplets, label: t("weather.humidity"), value: formatHumidity(displayedStation.humidity, language) },
|
||||||
@@ -110,9 +98,6 @@ export function WeatherHero({ station, currentWeather, currentWeatherLoading = f
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<span className="flex items-center gap-1.5 text-sm font-medium text-muted"><MapPin className="size-4" />{displayedLocationName}</span>
|
<span className="flex items-center gap-1.5 text-sm font-medium text-muted"><MapPin className="size-4" />{displayedLocationName}</span>
|
||||||
<span className={`rounded-control border px-2.5 py-1 text-[0.68rem] font-semibold uppercase tracking-[0.14em] ${moodAccent}`}>
|
|
||||||
{weatherDescription}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 space-y-1 text-xs text-muted">
|
<div className="mt-2 space-y-1 text-xs text-muted">
|
||||||
<p>{currentWeatherLoading
|
<p>{currentWeatherLoading
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ Najważniejsze widoki:
|
|||||||
- `/` - dashboard pogody, wyszukiwarka lokalizacji, hero, prognoza, briefy i ostrzeżenia regionalne,
|
- `/` - dashboard pogody, wyszukiwarka lokalizacji, hero, prognoza, briefy i ostrzeżenia regionalne,
|
||||||
- `/warnings` - pełny widok ostrzeżeń meteo i hydro,
|
- `/warnings` - pełny widok ostrzeżeń meteo i hydro,
|
||||||
- `/hydro` - stacje hydrologiczne,
|
- `/hydro` - stacje hydrologiczne,
|
||||||
- `/settings` - język, motyw, lokalizacja powiadomień i Web Push,
|
- `/settings` - język, motyw, widoczność sekcji aplikacji i dashboardu, lokalizacja powiadomień i Web Push,
|
||||||
- `/station/[id]` - szczegóły stacji,
|
- `/station/[id]` - szczegóły stacji,
|
||||||
- `/offline` - fallback offline.
|
- `/offline` - fallback offline.
|
||||||
|
|
||||||
@@ -42,7 +42,9 @@ Zustand w `lib/store.ts` przechowuje trwałe preferencje użytkownika w `localSt
|
|||||||
- ulubione stacje,
|
- ulubione stacje,
|
||||||
- wybraną stację albo lokalizację,
|
- wybraną stację albo lokalizację,
|
||||||
- ustawienia powiadomień,
|
- ustawienia powiadomień,
|
||||||
- tryb wyboru województwa dla alertów.
|
- tryb wyboru województwa dla alertów,
|
||||||
|
- widoczność opcjonalnych sekcji dashboardu,
|
||||||
|
- widoczność opcjonalnych sekcji aplikacji w nawigacji.
|
||||||
|
|
||||||
Język interfejsu jest przechowywany osobno w `localStorage` pod kluczem `wtr:language`.
|
Język interfejsu jest przechowywany osobno w `localStorage` pod kluczem `wtr:language`.
|
||||||
|
|
||||||
|
|||||||
46
lib/app-sections.ts
Normal file
46
lib/app-sections.ts
Normal file
@@ -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<AppSectionId, boolean>;
|
||||||
|
|
||||||
|
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<Record<AppSectionId, unknown>> : {};
|
||||||
|
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;
|
||||||
|
}
|
||||||
38
lib/dashboard-sections.ts
Normal file
38
lib/dashboard-sections.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import type { TranslationKey } from "@/lib/i18n";
|
||||||
|
|
||||||
|
export const DASHBOARD_SECTION_SETTINGS = [
|
||||||
|
{
|
||||||
|
id: "weatherBrief",
|
||||||
|
titleKey: "settings.dashboardSections.weatherBrief.title",
|
||||||
|
descriptionKey: "settings.dashboardSections.weatherBrief.description",
|
||||||
|
defaultVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "featuredStations",
|
||||||
|
titleKey: "settings.dashboardSections.featuredStations.title",
|
||||||
|
descriptionKey: "settings.dashboardSections.featuredStations.description",
|
||||||
|
defaultVisible: false,
|
||||||
|
},
|
||||||
|
] as const satisfies ReadonlyArray<{
|
||||||
|
id: string;
|
||||||
|
titleKey: TranslationKey;
|
||||||
|
descriptionKey: TranslationKey;
|
||||||
|
defaultVisible: boolean;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type DashboardSectionId = typeof DASHBOARD_SECTION_SETTINGS[number]["id"];
|
||||||
|
|
||||||
|
export type DashboardSectionVisibility = Record<DashboardSectionId, boolean>;
|
||||||
|
|
||||||
|
export const DEFAULT_DASHBOARD_SECTION_VISIBILITY = DASHBOARD_SECTION_SETTINGS.reduce((visibility, section) => ({
|
||||||
|
...visibility,
|
||||||
|
[section.id]: section.defaultVisible,
|
||||||
|
}), {} as DashboardSectionVisibility);
|
||||||
|
|
||||||
|
export function normalizeDashboardSectionVisibility(value: unknown): DashboardSectionVisibility {
|
||||||
|
const stored = value && typeof value === "object" ? value as Partial<Record<DashboardSectionId, unknown>> : {};
|
||||||
|
return DASHBOARD_SECTION_SETTINGS.reduce((visibility, section) => ({
|
||||||
|
...visibility,
|
||||||
|
[section.id]: typeof stored[section.id] === "boolean" ? stored[section.id] : section.defaultVisible,
|
||||||
|
}), {} as DashboardSectionVisibility);
|
||||||
|
}
|
||||||
28
lib/i18n.tsx
28
lib/i18n.tsx
@@ -26,6 +26,18 @@ const translations = {
|
|||||||
"settings.language.description": "Zmieniaj język interfejsu. Nazwy stacji i treści IMGW pozostają bez automatycznego tłumaczenia.",
|
"settings.language.description": "Zmieniaj język interfejsu. Nazwy stacji i treści IMGW pozostają bez automatycznego tłumaczenia.",
|
||||||
"settings.theme.title": "Motyw",
|
"settings.theme.title": "Motyw",
|
||||||
"settings.theme.description": "Przełącz jasny lub ciemny wygląd aplikacji na tym urządzeniu.",
|
"settings.theme.description": "Przełącz jasny lub ciemny wygląd aplikacji na tym urządzeniu.",
|
||||||
|
"settings.dashboardSections.title": "Strona główna",
|
||||||
|
"settings.dashboardSections.description": "Wybierz, które dodatkowe sekcje mają być widoczne na dashboardzie.",
|
||||||
|
"settings.dashboardSections.weatherBrief.title": "Brief dnia",
|
||||||
|
"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.title": "Jednostka temperatury",
|
||||||
"settings.units.temperature.description": "Stosowana w prognozie, briefach, powiadomieniach i bieżących pomiarach na tym urządzeniu.",
|
"settings.units.temperature.description": "Stosowana w prognozie, briefach, powiadomieniach i bieżących pomiarach na tym urządzeniu.",
|
||||||
"settings.units.temperature.label": "Wybierz jednostkę temperatury",
|
"settings.units.temperature.label": "Wybierz jednostkę temperatury",
|
||||||
@@ -91,7 +103,7 @@ const translations = {
|
|||||||
"brief.emptyDescription": "Prognoza nie zawiera wystarczających danych dla najbliższej doby.",
|
"brief.emptyDescription": "Prognoza nie zawiera wystarczających danych dla najbliższej doby.",
|
||||||
"location.label": "Twoja lokalizacja",
|
"location.label": "Twoja lokalizacja",
|
||||||
"location.searchLabel": "Szukaj miejscowości",
|
"location.searchLabel": "Szukaj miejscowości",
|
||||||
"location.placeholder": "Wpisz miejscowość, np. Warszawa albo Rome…",
|
"location.placeholder": "Wpisz miejscowość",
|
||||||
"location.clear": "Wyczyść wyszukiwanie",
|
"location.clear": "Wyczyść wyszukiwanie",
|
||||||
"location.error": "Nie udało się wyszukać miejscowości. Spróbuj ponownie.",
|
"location.error": "Nie udało się wyszukać miejscowości. Spróbuj ponownie.",
|
||||||
"location.preparing": "Przygotowuję listę najbliższych stacji IMGW…",
|
"location.preparing": "Przygotowuję listę najbliższych stacji IMGW…",
|
||||||
@@ -283,6 +295,18 @@ const translations = {
|
|||||||
"settings.language.description": "Change the interface language. Station names and IMGW content are not translated automatically.",
|
"settings.language.description": "Change the interface language. Station names and IMGW content are not translated automatically.",
|
||||||
"settings.theme.title": "Theme",
|
"settings.theme.title": "Theme",
|
||||||
"settings.theme.description": "Switch the light or dark appearance on this device.",
|
"settings.theme.description": "Switch the light or dark appearance on this device.",
|
||||||
|
"settings.dashboardSections.title": "Home screen",
|
||||||
|
"settings.dashboardSections.description": "Choose which additional sections are visible on the dashboard.",
|
||||||
|
"settings.dashboardSections.weatherBrief.title": "Daily brief",
|
||||||
|
"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.title": "Temperature unit",
|
||||||
"settings.units.temperature.description": "Used in forecasts, briefs, notifications and current readings on this device.",
|
"settings.units.temperature.description": "Used in forecasts, briefs, notifications and current readings on this device.",
|
||||||
"settings.units.temperature.label": "Choose temperature unit",
|
"settings.units.temperature.label": "Choose temperature unit",
|
||||||
@@ -348,7 +372,7 @@ const translations = {
|
|||||||
"brief.emptyDescription": "The forecast does not contain enough data for the next day.",
|
"brief.emptyDescription": "The forecast does not contain enough data for the next day.",
|
||||||
"location.label": "Your location",
|
"location.label": "Your location",
|
||||||
"location.searchLabel": "Search places",
|
"location.searchLabel": "Search places",
|
||||||
"location.placeholder": "Enter a place, e.g. Warsaw or Rome…",
|
"location.placeholder": "Enter a place",
|
||||||
"location.clear": "Clear search",
|
"location.clear": "Clear search",
|
||||||
"location.error": "Unable to search for places. Try again.",
|
"location.error": "Unable to search for places. Try again.",
|
||||||
"location.preparing": "Preparing the nearest IMGW stations…",
|
"location.preparing": "Preparing the nearest IMGW stations…",
|
||||||
|
|||||||
21
lib/store.ts
21
lib/store.ts
@@ -5,6 +5,8 @@ import { persist } from "zustand/middleware";
|
|||||||
import type { SelectedLocation } from "@/types/location";
|
import type { SelectedLocation } from "@/types/location";
|
||||||
import type { Province } from "@/types/province";
|
import type { Province } from "@/types/province";
|
||||||
import type { TemperatureUnit, WindSpeedUnit } from "@/types/units";
|
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 { DEFAULT_TEMPERATURE_UNIT, DEFAULT_WIND_SPEED_UNIT } from "@/lib/weather-utils";
|
||||||
import type { WeatherRegion } from "@/types/weather-region";
|
import type { WeatherRegion } from "@/types/weather-region";
|
||||||
import { getWeatherRegionForCountryCode } from "@/types/weather-region";
|
import { getWeatherRegionForCountryCode } from "@/types/weather-region";
|
||||||
@@ -22,6 +24,8 @@ interface WeatherStore {
|
|||||||
warningNotificationProvince: Province | null;
|
warningNotificationProvince: Province | null;
|
||||||
temperatureUnit: TemperatureUnit;
|
temperatureUnit: TemperatureUnit;
|
||||||
windSpeedUnit: WindSpeedUnit;
|
windSpeedUnit: WindSpeedUnit;
|
||||||
|
dashboardSections: DashboardSectionVisibility;
|
||||||
|
appSections: AppSectionVisibility;
|
||||||
toggleFavorite: (id: string) => void;
|
toggleFavorite: (id: string) => void;
|
||||||
selectStation: (id: string) => void;
|
selectStation: (id: string) => void;
|
||||||
selectLocation: (location: SelectedLocation) => void;
|
selectLocation: (location: SelectedLocation) => void;
|
||||||
@@ -32,6 +36,8 @@ interface WeatherStore {
|
|||||||
setWarningNotificationProvince: (province: Province | null) => void;
|
setWarningNotificationProvince: (province: Province | null) => void;
|
||||||
setTemperatureUnit: (unit: TemperatureUnit) => void;
|
setTemperatureUnit: (unit: TemperatureUnit) => void;
|
||||||
setWindSpeedUnit: (unit: WindSpeedUnit) => void;
|
setWindSpeedUnit: (unit: WindSpeedUnit) => void;
|
||||||
|
setDashboardSectionVisible: (section: DashboardSectionId, visible: boolean) => void;
|
||||||
|
setAppSectionVisible: (section: AppSectionId, visible: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useWeatherStore = create<WeatherStore>()(
|
export const useWeatherStore = create<WeatherStore>()(
|
||||||
@@ -47,6 +53,8 @@ export const useWeatherStore = create<WeatherStore>()(
|
|||||||
warningNotificationProvince: null,
|
warningNotificationProvince: null,
|
||||||
temperatureUnit: DEFAULT_TEMPERATURE_UNIT,
|
temperatureUnit: DEFAULT_TEMPERATURE_UNIT,
|
||||||
windSpeedUnit: DEFAULT_WIND_SPEED_UNIT,
|
windSpeedUnit: DEFAULT_WIND_SPEED_UNIT,
|
||||||
|
dashboardSections: DEFAULT_DASHBOARD_SECTION_VISIBILITY,
|
||||||
|
appSections: DEFAULT_APP_SECTION_VISIBILITY,
|
||||||
toggleFavorite: (id) =>
|
toggleFavorite: (id) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
favorites: state.favorites.includes(id)
|
favorites: state.favorites.includes(id)
|
||||||
@@ -62,17 +70,28 @@ export const useWeatherStore = create<WeatherStore>()(
|
|||||||
setWarningNotificationProvince: (province) => set({ warningNotificationProvince: province }),
|
setWarningNotificationProvince: (province) => set({ warningNotificationProvince: province }),
|
||||||
setTemperatureUnit: (unit) => set({ temperatureUnit: unit }),
|
setTemperatureUnit: (unit) => set({ temperatureUnit: unit }),
|
||||||
setWindSpeedUnit: (unit) => set({ windSpeedUnit: unit }),
|
setWindSpeedUnit: (unit) => set({ windSpeedUnit: unit }),
|
||||||
|
setDashboardSectionVisible: (section, visible) => set((state) => ({
|
||||||
|
dashboardSections: { ...state.dashboardSections, [section]: visible },
|
||||||
|
})),
|
||||||
|
setAppSectionVisible: (section, visible) => set((state) => ({
|
||||||
|
appSections: { ...state.appSections, [section]: visible },
|
||||||
|
})),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "wtr:preferences",
|
name: "wtr:preferences",
|
||||||
migrate: (persisted) => {
|
migrate: (persisted) => {
|
||||||
const state = persisted as Partial<WeatherStore> | undefined;
|
const state = persisted as Partial<WeatherStore> | undefined;
|
||||||
const location = state?.selectedLocation as (Partial<SelectedLocation> & { region?: WeatherRegion }) | null | undefined;
|
const location = state?.selectedLocation as (Partial<SelectedLocation> & { region?: WeatherRegion }) | null | undefined;
|
||||||
if (!state || !location) return persisted;
|
if (!state) return persisted;
|
||||||
|
const dashboardSections = normalizeDashboardSectionVisibility(state.dashboardSections);
|
||||||
|
const appSections = normalizeAppSectionVisibility(state.appSections);
|
||||||
|
if (!location) return { ...state, dashboardSections, appSections };
|
||||||
const countryCode = location.countryCode ?? (location.province ? "PL" : null);
|
const countryCode = location.countryCode ?? (location.province ? "PL" : null);
|
||||||
const region = location.region ?? getWeatherRegionForCountryCode(countryCode);
|
const region = location.region ?? getWeatherRegionForCountryCode(countryCode);
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
dashboardSections,
|
||||||
|
appSections,
|
||||||
selectedLocation: {
|
selectedLocation: {
|
||||||
...location,
|
...location,
|
||||||
country: location.country ?? (countryCode === "PL" ? "Polska" : null),
|
country: location.country ?? (countryCode === "PL" ? "Polska" : null),
|
||||||
|
|||||||
Reference in New Issue
Block a user