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 <noreply@anthropic.com>
This commit is contained in:
@@ -120,20 +120,20 @@ export default function ActivityDetailPage() {
|
||||
if (!activity) return null
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="p-4 md:p-6 space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-2xl">{sportIcon(activity.sport_type)}</span>
|
||||
<h1 className="text-2xl font-bold text-white">{activity.name}</h1>
|
||||
<h1 className="text-2xl font-bold text-white break-words min-w-0">{activity.name}</h1>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500">{formatDateTime(activity.start_time)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats — all on one row */}
|
||||
<div className="grid grid-cols-5 lg:grid-cols-10 gap-3">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 lg:grid-cols-10 gap-3">
|
||||
<StatCard label="Distance" value={formatDistance(activity.distance_m)} />
|
||||
<StatCard label="Time" value={formatDuration(activity.moving_time_s ?? activity.duration_s)}
|
||||
sub={activity.moving_time_s ? 'moving' : undefined} />
|
||||
@@ -164,8 +164,8 @@ export default function ActivityDetailPage() {
|
||||
{activity.polyline && activity.distance_m > 0 ? (
|
||||
<div className="bg-gray-900 rounded-xl overflow-hidden border border-gray-800">
|
||||
{/* Map toolbar */}
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b border-gray-800">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-wrap items-center justify-between gap-y-2 px-4 py-2 border-b border-gray-800">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-xs text-gray-500">Map style:</span>
|
||||
{['dark', 'street', 'satellite'].map(t => (
|
||||
<button
|
||||
@@ -202,18 +202,20 @@ export default function ActivityDetailPage() {
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
<span className="text-xs text-gray-500 ml-2">Height:</span>
|
||||
{[280, 420, 560].map(h => (
|
||||
<button
|
||||
key={h}
|
||||
onClick={() => setMapHeight(h)}
|
||||
className={`text-xs px-2.5 py-1 rounded-full transition-colors ${
|
||||
mapHeight === h ? 'bg-blue-600 text-white' : 'text-gray-400 hover:text-white bg-gray-800'
|
||||
}`}
|
||||
>
|
||||
{h === 280 ? 'S' : h === 420 ? 'M' : 'L'}
|
||||
</button>
|
||||
))}
|
||||
<div className="hidden sm:flex items-center gap-2 ml-2">
|
||||
<span className="text-xs text-gray-500">Height:</span>
|
||||
{[280, 420, 560].map(h => (
|
||||
<button
|
||||
key={h}
|
||||
onClick={() => setMapHeight(h)}
|
||||
className={`text-xs px-2.5 py-1 rounded-full transition-colors ${
|
||||
mapHeight === h ? 'bg-blue-600 text-white' : 'text-gray-400 hover:text-white bg-gray-800'
|
||||
}`}
|
||||
>
|
||||
{h === 280 ? 'S' : h === 420 ? 'M' : 'L'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{segCreate && (
|
||||
@@ -272,7 +274,7 @@ export default function ActivityDetailPage() {
|
||||
|
||||
{/* Metric timeline */}
|
||||
<div className="bg-gray-900 rounded-xl border border-gray-800 p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 mb-4">
|
||||
<h3 className="text-sm font-medium text-gray-300">Activity Timeline</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{METRICS.filter(m => availableMetrics.has(m.key)).map(({ key, label, color }) => (
|
||||
|
||||
Reference in New Issue
Block a user