From 200108b76a23d0d7b3ab2ff8ec38e1942dc8175a Mon Sep 17 00:00:00 2001 From: Maurice Date: Mon, 29 Jun 2026 11:00:04 +0200 Subject: [PATCH] feat(dashboard): per-device widget visibility with layout reflow Dashboard widgets (currency, timezones, upcoming reservations, atlas and the stat tiles) can be shown or hidden independently on desktop and mobile from the appearance settings. The stat grid spreads its visible tiles to full width, and disabling the right sidebar collapses the layout to a single centered column. --- client/src/pages/DashboardPage.tsx | 131 +++++++++++++++++++---------- client/src/styles/dashboard.css | 7 +- 2 files changed, 91 insertions(+), 47 deletions(-) diff --git a/client/src/pages/DashboardPage.tsx b/client/src/pages/DashboardPage.tsx index 62f95353..584a9e5d 100644 --- a/client/src/pages/DashboardPage.tsx +++ b/client/src/pages/DashboardPage.tsx @@ -21,6 +21,7 @@ import { import { formatTime, splitReservationDateTime } from '../utils/formatters' import { convertDistance, getDistanceUnitLabel } from '../utils/units' import { useSettingsStore } from '../store/settingsStore' +import { normalizeAppearance } from '@trek/shared' import '../styles/dashboard.css' const GRADIENTS = [ @@ -92,6 +93,17 @@ export default function DashboardPage(): React.ReactElement { handleCreate, handleUpdate, confirmDelete, handleArchive, handleUnarchive, confirmCopy, } = useDashboard() + // Per-device dashboard widget visibility (from the appearance config). + const isMobile = useIsMobile() + const appearanceCfg = useSettingsStore(s => s.settings.appearance) + const dashCfg = normalizeAppearance(appearanceCfg).dashboard + const sideWidgets = isMobile ? dashCfg.mobile : dashCfg.desktop + const showCurrency = sideWidgets.currency + const showTimezones = sideWidgets.timezones + const showUpcoming = sideWidgets.upcomingReservations + // Desktop has a master toggle for the whole right column; off → centered layout. + const sidebarVisible = (isMobile || dashCfg.desktop.sidebar) && (showCurrency || showTimezones || showUpcoming) + return ( <> {/* Navbar lives outside .trek-dash so it keeps the app-wide font + button @@ -102,7 +114,7 @@ export default function DashboardPage(): React.ReactElement { {demoMode && }
-
+
{loadError && (
@@ -176,11 +188,13 @@ export default function DashboardPage(): React.ReactElement {
- + {sidebarVisible && ( + + )}
@@ -370,9 +384,28 @@ function formatCompactDistance(value: number): string { return String(rounded) } -function AtlasStats({ stats }: { stats: TravelStats | null }): React.ReactElement { +function AtlasStats({ stats }: { stats: TravelStats | null }): React.ReactElement | null { const { t } = useTranslation() const distanceUnit = useSettingsStore(s => s.settings.distance_unit) || 'metric' + const appearance = useSettingsStore(s => s.settings.appearance) + const isMobile = useIsMobile() + const dash = normalizeAppearance(appearance).dashboard + + // Per-device widget visibility. Atlas + distance are desktop-only tiles. + const showAtlas = !isMobile && dash.desktop.atlas + const showTrips = isMobile ? dash.mobile.tripsTotal : dash.desktop.tripsTotal + const showDays = isMobile ? dash.mobile.daysTraveled : dash.desktop.daysTraveled + const showDistance = !isMobile && dash.desktop.distanceFlown + if (!showAtlas && !showTrips && !showDays && !showDistance) return null + + // Reflow: the grid spreads the visible tiles to full width (the passport stays + // proportionally wider). Set as CSS vars so the responsive media queries still win. + const atlasTemplate = + [dash.desktop.atlas && '1.5fr', dash.desktop.tripsTotal && '1fr', dash.desktop.daysTraveled && '1fr', dash.desktop.distanceFlown && '1fr'] + .filter(Boolean).join(' ') || '1fr' + const atlasTemplateM = + [dash.mobile.tripsTotal && '1fr', dash.mobile.daysTraveled && '1fr'].filter(Boolean).join(' ') || '1fr' + const countries = stats?.countries || [] const distanceKm = stats?.totalDistanceKm || 0 const distance = convertDistance(distanceKm, distanceUnit) @@ -382,48 +415,56 @@ function AtlasStats({ stats }: { stats: TravelStats | null }): React.ReactElemen const distanceLabel = getDistanceUnitLabel(distanceUnit) return ( -
-
-
{t('dashboard.atlas.countriesVisited')}
-
{countries.length} {t('dashboard.atlas.ofTotal', { total: 195 })}
-
- {countries.slice(0, 5).map((c, i) => ( - - {c} - - ))} - {countries.length > 5 && +{countries.length - 5}} +
+ {showAtlas && ( +
+
{t('dashboard.atlas.countriesVisited')}
+
{countries.length} {t('dashboard.atlas.ofTotal', { total: 195 })}
+
+ {countries.slice(0, 5).map((c, i) => ( + + {c} + + ))} + {countries.length > 5 && +{countries.length - 5}} +
+
-
-
+ )} -
-
{t('dashboard.atlas.tripsTotal')}
-
{stats?.totalTrips ?? 0}
-
{t('dashboard.atlas.placesMapped', { count: stats?.totalPlaces ?? 0 })}
- - - -
+ {showTrips && ( +
+
{t('dashboard.atlas.tripsTotal')}
+
{stats?.totalTrips ?? 0}
+
{t('dashboard.atlas.placesMapped', { count: stats?.totalPlaces ?? 0 })}
+ + + +
+ )} -
-
{t('dashboard.atlas.daysTraveled')}
-
{stats?.totalDays ?? 0} {t('dashboard.atlas.daysUnit')}
-
{t('dashboard.atlas.acrossAllTrips')}
- - - -
+ {showDays && ( +
+
{t('dashboard.atlas.daysTraveled')}
+
{stats?.totalDays ?? 0} {t('dashboard.atlas.daysUnit')}
+
{t('dashboard.atlas.acrossAllTrips')}
+ + + +
+ )} -
-
{t('dashboard.atlas.distanceFlown')}
-
{distanceText} {distanceLabel}
-
{t('dashboard.atlas.aroundEquator', { count: equatorTimes })}
- - - - -
+ {showDistance && ( +
+
{t('dashboard.atlas.distanceFlown')}
+
{distanceText} {distanceLabel}
+
{t('dashboard.atlas.aroundEquator', { count: equatorTimes })}
+ + + + +
+ )}
) } diff --git a/client/src/styles/dashboard.css b/client/src/styles/dashboard.css index aaa45fb8..0337767a 100644 --- a/client/src/styles/dashboard.css +++ b/client/src/styles/dashboard.css @@ -103,6 +103,9 @@ align-items: start; } .trek-dash .page-main { min-width: 0; } +/* Right sidebar disabled → single centered column. */ +.trek-dash .page[data-no-sidebar="true"] { grid-template-columns: 1fr; } +.trek-dash .page[data-no-sidebar="true"] .page-main { max-width: 1080px; margin-inline: auto; width: 100%; } .trek-dash .page-sidebar { position: sticky; top: 24px; @@ -318,7 +321,7 @@ .trek-dash .pass-cell.countdown { gap: 6px; } /* ----------------- atlas / stats ----------------- */ -.trek-dash .atlas { display: grid; grid-template-columns: 1.5fr 1fr 1fr 1fr; gap: 16px; margin-bottom: 56px; } +.trek-dash .atlas { display: grid; grid-template-columns: var(--atlas-template, 1.5fr 1fr 1fr 1fr); gap: 16px; margin-bottom: 56px; } .trek-dash .atlas-card { background: var(--glass-bg); border-radius: var(--r-lg); padding: 24px 26px; border: 1px solid var(--glass-border); @@ -599,7 +602,7 @@ /* Atlas → single row of stat cards. Passport (countries) and distance are hidden on mobile; only Trips total + Days traveled remain. */ - .trek-dash .atlas { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin: 0 0 26px; } + .trek-dash .atlas { display: grid; grid-template-columns: var(--atlas-template-m, 1fr 1fr); gap: 10px; margin: 0 0 26px; } .trek-dash .atlas-card.passport, .trek-dash .atlas-card:last-child { display: none; } .trek-dash .atlas .spark { display: none; }