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">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<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>
|
<title>bikeslop</title>
|
||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
|
<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>
|
<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; }
|
.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; } }
|
@keyframes blink { 50% { opacity: 0; } }
|
||||||
.no-ride-msg { color: var(--muted); font-size: 12px; text-align: center; padding: 24px 16px; }
|
.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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -251,6 +295,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
<script>
|
||||||
const map = L.map('map', { zoomControl: true }).setView([37.7749, -122.4194], 11);
|
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', {
|
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();
|
currentRide = await res.json();
|
||||||
|
|
||||||
renderDetails(currentRide);
|
renderDetails(currentRide);
|
||||||
|
setMobileTab('map');
|
||||||
|
|
||||||
if (currentRide.has_gps) {
|
if (currentRide.has_gps) {
|
||||||
// Fetch points
|
// Fetch points
|
||||||
@@ -812,6 +872,16 @@ document.getElementById('unit-toggle').addEventListener('click', () => {
|
|||||||
if (currentPoints && currentPoints.length) renderCharts(currentPoints);
|
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() {
|
async function initAuth() {
|
||||||
const res = await fetch('/api/me');
|
const res = await fetch('/api/me');
|
||||||
if (res.status === 401) {
|
if (res.status === 401) {
|
||||||
|
|||||||
Reference in New Issue
Block a user