Add mobile-friendly tab layout with bottom nav

Three-panel desktop layout becomes a tab-based single-panel view on mobile
with Rides/Map/Details bottom nav. Tapping a ride auto-navigates to the map.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-08-28 10:52:21 -04:00
parent 77061b7cc6
commit 40b685e6d0
+71 -1
View File
@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>bikeslop</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
@@ -143,6 +143,50 @@
.blinking { display: inline-block; width: 2px; height: 12px; background: var(--accent); animation: blink 0.8s step-end infinite; margin-left: 2px; vertical-align: middle; }
@keyframes blink { 50% { opacity: 0; } }
.no-ride-msg { color: var(--muted); font-size: 12px; text-align: center; padding: 24px 16px; }
/* Mobile */
#mobile-nav { display: none; }
@media (max-width: 768px) {
#sidebar, #main, #details {
position: fixed;
top: 0; left: 0; right: 0;
bottom: calc(48px + env(safe-area-inset-bottom));
width: 100%;
display: none;
}
#sidebar.mobile-active, #main.mobile-active, #details.mobile-active { display: flex; }
#sidebar { border-right: none; }
#details { border-left: none; }
#mobile-nav {
display: flex;
position: fixed;
bottom: 0; left: 0; right: 0;
height: calc(48px + env(safe-area-inset-bottom));
padding-bottom: env(safe-area-inset-bottom);
background: var(--surface);
border-top: 1px solid var(--border);
z-index: 100;
}
.mobile-nav-btn {
flex: 1;
background: none;
border: none;
color: var(--muted);
font-size: 10px;
font-family: inherit;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3px;
transition: color 0.15s;
padding: 0;
-webkit-tap-highlight-color: transparent;
}
.mobile-nav-btn.active { color: var(--accent); }
.mobile-nav-btn svg { width: 20px; height: 20px; stroke: currentColor; fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
}
</style>
</head>
<body>
@@ -251,6 +295,21 @@
</div>
</div>
<nav id="mobile-nav">
<button class="mobile-nav-btn active" data-tab="list" onclick="setMobileTab('list')">
<svg viewBox="0 0 24 24"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
Rides
</button>
<button class="mobile-nav-btn" data-tab="map" onclick="setMobileTab('map')">
<svg viewBox="0 0 24 24"><polygon points="3,6 9,3 15,6 21,3 21,18 15,21 9,18 3,21"/><line x1="9" y1="3" x2="9" y2="18"/><line x1="15" y1="6" x2="15" y2="21"/></svg>
Map
</button>
<button class="mobile-nav-btn" data-tab="details" onclick="setMobileTab('details')">
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
Details
</button>
</nav>
<script>
const map = L.map('map', { zoomControl: true }).setView([37.7749, -122.4194], 11);
L.tileLayer('https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.png', {
@@ -408,6 +467,7 @@ async function selectRide(id) {
currentRide = await res.json();
renderDetails(currentRide);
setMobileTab('map');
if (currentRide.has_gps) {
// Fetch points
@@ -812,6 +872,16 @@ document.getElementById('unit-toggle').addEventListener('click', () => {
if (currentPoints && currentPoints.length) renderCharts(currentPoints);
});
function setMobileTab(tab) {
if (!window.matchMedia('(max-width: 768px)').matches) return;
document.getElementById('sidebar').classList.toggle('mobile-active', tab === 'list');
document.getElementById('main').classList.toggle('mobile-active', tab === 'map');
document.getElementById('details').classList.toggle('mobile-active', tab === 'details');
document.querySelectorAll('.mobile-nav-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.tab === tab));
if (tab === 'map') setTimeout(() => map.invalidateSize(), 50);
}
setMobileTab('list');
async function initAuth() {
const res = await fetch('/api/me');
if (res.status === 401) {