feat: add dashboard section visibility settings

This commit is contained in:
zv
2026-06-14 17:34:52 +02:00
parent 6be00da027
commit 9087e1215c
6 changed files with 96 additions and 6 deletions

View File

@@ -26,6 +26,7 @@ export function DashboardPage() {
const { data: positions = [] } = useMeteoStationPositions();
const selectedStationId = useWeatherStore((state) => state.selectedStationId);
const selectedLocation = useWeatherStore((state) => state.selectedLocation);
const dashboardSections = useWeatherStore((state) => state.dashboardSections);
const selectedStation = stations?.find((station) => station.id === selectedStationId)
?? stations?.find((station) => station.name === DEFAULT_STATION_NAME)
?? stations?.[0];
@@ -62,10 +63,10 @@ export function DashboardPage() {
<LocationSearch stations={stations} positions={positions} />
<WeatherHero station={heroStation} currentWeather={currentWeather} currentWeatherLoading={isCurrentWeatherLoading} currentForecastWeatherCode={currentForecastWeatherCode} locationName={activeLocation?.name} distanceKm={activeLocation?.distanceKm} />
<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} />
<FavoritesSection stations={stations} />
<FeaturedStationsSection stations={stations} />
{dashboardSections.featuredStations && <FeaturedStationsSection stations={stations} />}
</div>
);
}

View File

@@ -1,7 +1,7 @@
"use client";
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 { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
@@ -10,6 +10,7 @@ import { ThemeToggle } from "@/components/ui/theme-toggle";
import { useMeteoStationPositions } from "@/hooks/use-meteo-stations";
import { useWeatherStations } from "@/hooks/use-weather-stations";
import { DEFAULT_STATION_ID } from "@/lib/constants";
import { DASHBOARD_SECTION_SETTINGS } from "@/lib/dashboard-sections";
import { useI18n } from "@/lib/i18n";
import { locateSynopStations } from "@/lib/location-utils";
import { decodeBase64UrlKey, deletePushSubscription, fetchVapidPublicKey, savePushSubscription, sendTestPushNotification } from "@/lib/notification-api";
@@ -131,6 +132,7 @@ export function SettingsPage() {
const manualProvince = useWeatherStore((state) => state.warningNotificationProvince);
const temperatureUnit = useWeatherStore((state) => state.temperatureUnit);
const windSpeedUnit = useWeatherStore((state) => state.windSpeedUnit);
const dashboardSections = useWeatherStore((state) => state.dashboardSections);
const setNotificationsEnabled = useWeatherStore((state) => state.setWarningNotificationsEnabled);
const setMorningBriefEnabled = useWeatherStore((state) => state.setMorningBriefNotificationsEnabled);
const setTomorrowBriefEnabled = useWeatherStore((state) => state.setTomorrowBriefNotificationsEnabled);
@@ -138,6 +140,7 @@ export function SettingsPage() {
const setManualProvince = useWeatherStore((state) => state.setWarningNotificationProvince);
const setTemperatureUnit = useWeatherStore((state) => state.setTemperatureUnit);
const setWindSpeedUnit = useWeatherStore((state) => state.setWindSpeedUnit);
const setDashboardSectionVisible = useWeatherStore((state) => state.setDashboardSectionVisible);
const selectedStation = stations?.find((station) => station.id === selectedStationId)
?? stations?.find((station) => station.id === DEFAULT_STATION_ID)
@@ -435,6 +438,31 @@ export function SettingsPage() {
</SettingsRow>
</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-warning/30 bg-warning/10 p-2 text-warning">

View File

@@ -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, lokalizacja powiadomień i Web Push,
- `/settings` - język, motyw, widoczność sekcji dashboardu, lokalizacja powiadomień i Web Push,
- `/station/[id]` - szczegóły stacji,
- `/offline` - fallback offline.
@@ -42,7 +42,8 @@ Zustand w `lib/store.ts` przechowuje trwałe preferencje użytkownika w `localSt
- ulubione stacje,
- wybraną stację albo lokalizację,
- ustawienia powiadomień,
- tryb wyboru województwa dla alertów.
- tryb wyboru województwa dla alertów,
- widoczność opcjonalnych sekcji dashboardu.
Język interfejsu jest przechowywany osobno w `localStorage` pod kluczem `wtr:language`.

38
lib/dashboard-sections.ts Normal file
View 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);
}

View File

@@ -26,6 +26,12 @@ const translations = {
"settings.language.description": "Zmieniaj język interfejsu. Nazwy stacji i treści IMGW pozostają bez automatycznego tłumaczenia.",
"settings.theme.title": "Motyw",
"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.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",
@@ -283,6 +289,12 @@ const translations = {
"settings.language.description": "Change the interface language. Station names and IMGW content are not translated automatically.",
"settings.theme.title": "Theme",
"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.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",

View File

@@ -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_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";
import { getWeatherRegionForCountryCode } from "@/types/weather-region";
@@ -22,6 +23,7 @@ interface WeatherStore {
warningNotificationProvince: Province | null;
temperatureUnit: TemperatureUnit;
windSpeedUnit: WindSpeedUnit;
dashboardSections: DashboardSectionVisibility;
toggleFavorite: (id: string) => void;
selectStation: (id: string) => void;
selectLocation: (location: SelectedLocation) => void;
@@ -32,6 +34,7 @@ interface WeatherStore {
setWarningNotificationProvince: (province: Province | null) => void;
setTemperatureUnit: (unit: TemperatureUnit) => void;
setWindSpeedUnit: (unit: WindSpeedUnit) => void;
setDashboardSectionVisible: (section: DashboardSectionId, visible: boolean) => void;
}
export const useWeatherStore = create<WeatherStore>()(
@@ -47,6 +50,7 @@ export const useWeatherStore = create<WeatherStore>()(
warningNotificationProvince: null,
temperatureUnit: DEFAULT_TEMPERATURE_UNIT,
windSpeedUnit: DEFAULT_WIND_SPEED_UNIT,
dashboardSections: DEFAULT_DASHBOARD_SECTION_VISIBILITY,
toggleFavorite: (id) =>
set((state) => ({
favorites: state.favorites.includes(id)
@@ -62,17 +66,23 @@ export const useWeatherStore = create<WeatherStore>()(
setWarningNotificationProvince: (province) => set({ warningNotificationProvince: province }),
setTemperatureUnit: (unit) => set({ temperatureUnit: unit }),
setWindSpeedUnit: (unit) => set({ windSpeedUnit: unit }),
setDashboardSectionVisible: (section, visible) => set((state) => ({
dashboardSections: { ...state.dashboardSections, [section]: visible },
})),
}),
{
name: "wtr:preferences",
migrate: (persisted) => {
const state = persisted as Partial<WeatherStore> | 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);
if (!location) return { ...state, dashboardSections };
const countryCode = location.countryCode ?? (location.province ? "PL" : null);
const region = location.region ?? getWeatherRegionForCountryCode(countryCode);
return {
...state,
dashboardSections,
selectedLocation: {
...location,
country: location.country ?? (countryCode === "PL" ? "Polska" : null),