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:
+71
-1
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user