From e7123ee5db5d44ee73c1a589f54cbec8d48d5eb5 Mon Sep 17 00:00:00 2001 From: owain Date: Fri, 12 Jun 2026 20:52:10 +0100 Subject: [PATCH] Mobile-responsive UI: bottom tab nav, stacked dashboard, page fixes for phones - Layout: sidebar hidden below md; mobile top header + bottom tab bar (Dashboard/Activities/Health/Routes) with a More slide-up sheet holding Records/Import/Profile/Users, Garmin sync progress and sign-out - Dashboard: single-column widget stack on phones ordered from the saved desktop layout (stat cards pair into 2-col grid); drag-edit stays desktop-only; grid conditionally mounted so WidthProvider never measures a hidden container - New useMediaQuery/useIsMobile hook (matchMedia, md breakpoint) - ActivityDetail: 2-col stats on phones, wrapping map toolbar, Height control hidden on small screens - Activities: compact distance/time/pace line on mobile rows - Records/Users: horizontally scrollable tables; route records hide Pace/Date columns below sm - Profile forms single-column on phones; Routes expanded card stacks; Health flex-wrap fixes; p-4 md:p-6 page padding; h-dvh for mobile browser chrome - vite.config: VITE_PROXY_TARGET override for host-side dev Co-Authored-By: Claude Fable 5 --- frontend/src/components/ui/Layout.jsx | 139 +++++++++++++++++++--- frontend/src/hooks/useMediaQuery.js | 16 +++ frontend/src/pages/ActivitiesPage.jsx | 10 +- frontend/src/pages/ActivityDetailPage.jsx | 40 ++++--- frontend/src/pages/DashboardPage.jsx | 59 +++++++-- frontend/src/pages/HealthPage.jsx | 6 +- frontend/src/pages/LoginPage.jsx | 2 +- frontend/src/pages/ProfilePage.jsx | 6 +- frontend/src/pages/RecordsPage.jsx | 18 ++- frontend/src/pages/RoutesPage.jsx | 8 +- frontend/src/pages/UploadPage.jsx | 2 +- frontend/src/pages/UsersPage.jsx | 4 +- frontend/vite.config.js | 3 +- 13 files changed, 245 insertions(+), 68 deletions(-) create mode 100644 frontend/src/hooks/useMediaQuery.js diff --git a/frontend/src/components/ui/Layout.jsx b/frontend/src/components/ui/Layout.jsx index 8ce11da..0b03099 100644 --- a/frontend/src/components/ui/Layout.jsx +++ b/frontend/src/components/ui/Layout.jsx @@ -1,13 +1,13 @@ import { useEffect, useState } from 'react' -import { Outlet, NavLink, useNavigate } from 'react-router-dom' +import { Outlet, NavLink, useNavigate, useLocation } from 'react-router-dom' import { useAuthStore } from '../../hooks/useAuth' import { useSyncStore, syncProgressPct } from '../../hooks/useSync' const nav = [ - { to: '/', label: 'Dashboard', icon: 'πŸ“Š', exact: true }, - { to: '/activities', label: 'Activities', icon: 'πŸƒ' }, - { to: '/health', label: 'Health', icon: '❀️' }, - { to: '/routes', label: 'Routes', icon: 'πŸ—ΊοΈ' }, + { to: '/', label: 'Dashboard', icon: 'πŸ“Š', exact: true, mobilePrimary: true }, + { to: '/activities', label: 'Activities', icon: 'πŸƒ', mobilePrimary: true }, + { to: '/health', label: 'Health', icon: '❀️', mobilePrimary: true }, + { to: '/routes', label: 'Routes', icon: 'πŸ—ΊοΈ', mobilePrimary: true }, { to: '/records', label: 'Records', icon: 'πŸ†' }, { to: '/upload', label: 'Import', icon: '⬆️' }, { to: '/profile', label: 'Profile', icon: 'βš™οΈ' }, @@ -17,14 +17,19 @@ const nav = [ export default function Layout() { const { user, logout } = useAuthStore() const navigate = useNavigate() + const location = useLocation() const { inProgress, status, startPolling, stopPolling } = useSyncStore() const [collapsed, setCollapsed] = useState(() => localStorage.getItem('navCollapsed') === '1') + const [moreOpen, setMoreOpen] = useState(false) useEffect(() => { startPolling() return () => stopPolling() }, []) + // Close the mobile "More" sheet on navigation + useEffect(() => { setMoreOpen(false) }, [location.pathname]) + const toggleCollapsed = () => { setCollapsed(c => { const next = !c @@ -39,10 +44,28 @@ export default function Layout() { } const role = user?.is_admin ? 'Administrator' : 'Member' + const visibleNav = nav.filter(({ adminOnly }) => !adminOnly || user?.is_admin) + const primaryNav = visibleNav.filter(i => i.mobilePrimary) + const moreNav = visibleNav.filter(i => !i.mobilePrimary) + const moreActive = moreNav.some(i => location.pathname.startsWith(i.to)) + + const syncProgress = ( +
+
+ + Garmin sync +
+
+
+
+

{status || 'Starting sync…'}

+
+ ) return ( -
-