feat: allow dashboard section reordering

This commit is contained in:
zv
2026-07-04 20:31:56 +02:00
parent 91acdb39b8
commit c7da9e0d94
7 changed files with 209 additions and 34 deletions

View File

@@ -19,6 +19,11 @@ import { locateSynopStations } from "@/lib/location-utils";
import { DashboardWarnings } from "@/components/warnings/dashboard-warnings";
import { WeatherBriefCard } from "@/components/dashboard/weather-brief-card";
import type { SynopStation } from "@/types/imgw";
import {
normalizeDashboardSectionOrder,
normalizeDashboardSectionVisibility,
type DashboardSectionId,
} from "@/lib/dashboard-sections";
export function DashboardPage() {
const { t } = useI18n();
@@ -26,7 +31,8 @@ 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 dashboardSections = normalizeDashboardSectionVisibility(useWeatherStore((state) => state.dashboardSections));
const dashboardSectionOrder = normalizeDashboardSectionOrder(useWeatherStore((state) => state.dashboardSectionOrder));
const selectedStation =
stations?.find((station) => station.id === selectedStationId) ??
stations?.find((station) => station.name === DEFAULT_STATION_NAME) ??
@@ -73,6 +79,34 @@ export function DashboardPage() {
}
: selectedStation;
const renderDashboardSection = (sectionId: DashboardSectionId) => {
if (!dashboardSections[sectionId]) return null;
if (sectionId === "warnings") return <DashboardWarnings key={sectionId} />;
if (sectionId === "weatherBrief")
return (
<WeatherBriefCard
key={sectionId}
latitude={forecastLatitude}
longitude={forecastLongitude}
region={activeRegion}
locationName={forecastLocationName ?? selectedStation.name}
/>
);
if (sectionId === "forecast")
return (
<ForecastPanel
key={sectionId}
latitude={forecastLatitude}
longitude={forecastLongitude}
region={activeRegion}
locationName={forecastLocationName ?? selectedStation.name}
/>
);
if (sectionId === "favorites") return <FavoritesSection key={sectionId} stations={stations} />;
if (sectionId === "featuredStations") return <FeaturedStationsSection key={sectionId} stations={stations} />;
return null;
};
return (
<div className="space-y-10">
<LocationSearch stations={stations} positions={positions} />
@@ -84,23 +118,7 @@ export function DashboardPage() {
locationName={activeLocation?.name}
distanceKm={activeLocation?.distanceKm}
/>
<DashboardWarnings />
{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} />
{dashboardSections.featuredStations && <FeaturedStationsSection stations={stations} />}
{dashboardSectionOrder.map(renderDashboardSection)}
</div>
);
}

View File

@@ -2,6 +2,8 @@
import { useEffect, useMemo, useState } from "react";
import {
ArrowDown,
ArrowUp,
Bell,
BellRing,
ChevronDown,
@@ -27,7 +29,11 @@ import { useMeteoStationPositions } from "@/hooks/use-meteo-stations";
import { useWeatherStations } from "@/hooks/use-weather-stations";
import { APP_SECTION_SETTINGS } from "@/lib/app-sections";
import { DEFAULT_STATION_ID } from "@/lib/constants";
import { DASHBOARD_SECTION_SETTINGS } from "@/lib/dashboard-sections";
import {
DASHBOARD_SECTION_SETTINGS,
normalizeDashboardSectionOrder,
normalizeDashboardSectionVisibility,
} from "@/lib/dashboard-sections";
import { useI18n } from "@/lib/i18n";
import { locateSynopStations } from "@/lib/location-utils";
import {
@@ -230,7 +236,8 @@ export function SettingsPage() {
const precipitationUnit = useWeatherStore((state) => state.precipitationUnit);
const pressureUnit = useWeatherStore((state) => state.pressureUnit);
const distanceUnit = useWeatherStore((state) => state.distanceUnit);
const dashboardSections = useWeatherStore((state) => state.dashboardSections);
const dashboardSections = normalizeDashboardSectionVisibility(useWeatherStore((state) => state.dashboardSections));
const dashboardSectionOrder = normalizeDashboardSectionOrder(useWeatherStore((state) => state.dashboardSectionOrder));
const appSections = useWeatherStore((state) => state.appSections);
const setNotificationsEnabled = useWeatherStore((state) => state.setWarningNotificationsEnabled);
const setMorningBriefEnabled = useWeatherStore((state) => state.setMorningBriefNotificationsEnabled);
@@ -243,6 +250,7 @@ export function SettingsPage() {
const setPressureUnit = useWeatherStore((state) => state.setPressureUnit);
const setDistanceUnit = useWeatherStore((state) => state.setDistanceUnit);
const setDashboardSectionVisible = useWeatherStore((state) => state.setDashboardSectionVisible);
const moveDashboardSection = useWeatherStore((state) => state.moveDashboardSection);
const setAppSectionVisible = useWeatherStore((state) => state.setAppSectionVisible);
const selectedStation =
@@ -277,6 +285,9 @@ export function SettingsPage() {
? formatProvinceName(effectiveProvince, language)
: t("settings.notifications.noProvince");
const manualProvinceValue = manualProvince ?? selectedProvince ?? "mazowieckie";
const orderedDashboardSections = dashboardSectionOrder
.map((sectionId) => DASHBOARD_SECTION_SETTINGS.find((section) => section.id === sectionId))
.filter((section): section is (typeof DASHBOARD_SECTION_SETTINGS)[number] => Boolean(section));
useEffect(() => {
const abortController = new AbortController();
@@ -714,17 +725,58 @@ export function SettingsPage() {
</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 className="mt-4 space-y-2">
{orderedDashboardSections.map((section, index) => {
const sectionTitle = t(section.titleKey);
const checked = dashboardSections[section.id];
return (
<div
key={section.id}
className={`rounded-card border px-3 py-3 transition-colors ${
checked ? "border-accent/35 bg-accent/5" : "border-border/60 bg-surface"
}`}
>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<label className="flex min-w-0 flex-1 cursor-pointer items-center justify-between gap-4">
<span className="min-w-0">
<span className="block text-sm font-semibold">{sectionTitle}</span>
<span className="mt-1 block text-sm leading-6 text-muted">{t(section.descriptionKey)}</span>
</span>
<input
type="checkbox"
checked={checked}
onChange={(event) => setDashboardSectionVisible(section.id, event.target.checked)}
className="sr-only"
/>
<SwitchIndicator checked={checked} />
</label>
<div className="flex items-center gap-2 self-end sm:self-center" aria-label={sectionTitle}>
<Button
type="button"
variant="icon"
className="size-9"
disabled={index === 0}
aria-label={t("settings.dashboardSections.moveUp", { section: sectionTitle })}
onClick={() => moveDashboardSection(section.id, -1)}
>
<ArrowUp className="size-4" aria-hidden="true" />
</Button>
<Button
type="button"
variant="icon"
className="size-9"
disabled={index === orderedDashboardSections.length - 1}
aria-label={t("settings.dashboardSections.moveDown", { section: sectionTitle })}
onClick={() => moveDashboardSection(section.id, 1)}
>
<ArrowDown className="size-4" aria-hidden="true" />
</Button>
</div>
</div>
</div>
);
})}
</div>
</Card>