Driverthing/templates/index.html
Claude 0431308cb4
Persist GPS coords in sessionStorage; fall back to raw coords on geocode failure
iOS Safari can discard page JS state when backgrounding to open the camera.
Saving gpsCoords to sessionStorage ensures the coords survive the round-trip
and are still available when handleUpload runs after the photo is taken.

Also change the server-side geocode fallback from empty string to raw lat/lon
so the location column in the Excel sheet is never silently empty.

https://claude.ai/code/session_015myTTMs6yDsAGarATe5ePZ
2026-03-18 20:36:32 +00:00

343 lines
11 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Driverthing</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f0f2f5;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 24px 16px;
}
h1 { font-size: 1.4rem; color: #1a1a2e; margin-bottom: 4px; }
.subtitle { color: #999; font-size: 0.85rem; margin-bottom: 24px; }
.card {
background: white;
border-radius: 16px;
padding: 24px;
width: 100%;
max-width: 420px;
box-shadow: 0 2px 16px rgba(0,0,0,0.08);
}
.progress {
display: flex;
gap: 6px;
margin-bottom: 22px;
}
.dot {
flex: 1;
height: 4px;
border-radius: 2px;
background: #e0e0e0;
transition: background 0.3s;
}
.dot.active { background: #4f8ef7; }
.dot.done { background: #34c759; }
.step-label {
font-size: 0.72rem;
color: #aaa;
text-transform: uppercase;
letter-spacing: 0.06em;
margin-bottom: 4px;
}
.step-title {
font-size: 1.05rem;
font-weight: 600;
color: #1a1a2e;
margin-bottom: 14px;
}
.info-box {
background: #f8f9ff;
border: 1px solid #e0e7ff;
border-radius: 10px;
padding: 11px 14px;
font-size: 0.85rem;
color: #4a5580;
margin-bottom: 14px;
}
.reading {
font-size: 2.2rem;
font-weight: 700;
color: #4f8ef7;
text-align: center;
margin: 10px 0 4px;
}
.gps-tag {
display: block;
text-align: center;
font-size: 0.78rem;
color: #388e3c;
background: #e8f5e9;
border-radius: 6px;
padding: 3px 10px;
margin: 0 auto 14px;
width: fit-content;
}
label.cam-btn {
display: block;
background: #4f8ef7;
color: white;
text-align: center;
padding: 14px;
border-radius: 12px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background 0.15s;
margin-top: 6px;
user-select: none;
}
label.cam-btn:active { background: #3a7be0; }
input[type="file"] { display: none; }
.btn {
display: block;
width: 100%;
padding: 14px;
border-radius: 12px;
font-size: 1rem;
font-weight: 600;
border: none;
cursor: pointer;
text-align: center;
text-decoration: none;
margin-top: 10px;
transition: opacity 0.15s;
}
.btn:active { opacity: 0.8; }
.btn-green { background: #34c759; color: white; }
.btn-gray { background: #ebebeb; color: #555; }
.spinner {
border: 3px solid #eee;
border-top: 3px solid #4f8ef7;
border-radius: 50%;
width: 26px;
height: 26px;
animation: spin 0.8s linear infinite;
margin: 14px auto;
}
@keyframes spin { to { transform: rotate(360deg); } }
.error {
color: #e53e3e;
font-size: 0.88rem;
text-align: center;
margin-top: 10px;
}
.divider { height: 1px; background: #f0f0f0; margin: 14px 0; }
.summary-row {
display: flex;
justify-content: space-between;
padding: 7px 0;
font-size: 0.9rem;
border-bottom: 1px solid #f5f5f5;
}
.summary-row:last-child { border-bottom: none; }
.summary-label { color: #888; }
.summary-value { font-weight: 600; color: #1a1a2e; }
.hidden { display: none; }
</style>
</head>
<body>
<h1>Driverthing</h1>
<p class="subtitle">Mileage tracker</p>
<div class="card">
<div class="progress">
<div class="dot" id="d1"></div>
<div class="dot" id="d2"></div>
<div class="dot" id="d3"></div>
</div>
<div id="content"></div>
</div>
<script>
// Restore coords if iOS discarded the page while camera was open
let gpsCoords = (() => {
try { return JSON.parse(sessionStorage.getItem('gpsCoords')); } catch { return null; }
})();
function getLocation() {
const el = document.getElementById('loc-status');
if (!navigator.geolocation) {
el.textContent = 'Location not supported by this browser.';
return;
}
el.innerHTML = '<span style="color:#888">Getting location…</span>';
navigator.geolocation.getCurrentPosition(
pos => {
gpsCoords = { lat: pos.coords.latitude, lon: pos.coords.longitude };
sessionStorage.setItem('gpsCoords', JSON.stringify(gpsCoords));
el.innerHTML = '<span style="color:#388e3c">📍 Location captured</span>';
},
err => {
el.innerHTML = `<span style="color:#e53e3e">Error ${err.code}: ${err.message}</span>`;
},
{ timeout: 15000 }
);
}
function dots(active, doneUpTo) {
[1, 2, 3].forEach(i => {
const el = document.getElementById('d' + i);
el.className = 'dot';
if (i <= doneUpTo) el.classList.add('done');
else if (i === active) el.classList.add('active');
});
}
function render(html) {
document.getElementById('content').innerHTML = html;
}
// ── Step screens ────────────────────────────────────────────────────────
function showStep(n) {
const labels = {
1: 'Start of ride 1',
2: 'End of ride 1 / Start of ride 2',
3: 'End of ride 2',
};
dots(n, n - 1);
const locBlock = n === 1 ? `
<button class="btn btn-gray" style="margin-bottom:10px" onclick="getLocation()">📍 Share location (optional)</button>
<div id="loc-status" style="font-size:0.85rem;text-align:center;min-height:1.2em;margin-bottom:6px"></div>` : '';
render(`
<div class="step-label">Step ${n} of 3</div>
<div class="step-title">${labels[n]}</div>
<div class="info-box">Point your camera at the odometer and take a photo.</div>
${locBlock}
<label class="cam-btn" for="img">
📸 Open camera
<input type="file" id="img" accept="image/*" capture="environment"
onchange="handleUpload(this, ${n})">
</label>
<div id="status"></div>
`);
}
// ── Upload ───────────────────────────────────────────────────────────────
async function handleUpload(input, step) {
const file = input.files[0];
if (!file) return;
document.getElementById('status').innerHTML = '<div class="spinner"></div>';
const form = new FormData();
form.append('image', file);
if (step === 1 && gpsCoords) {
form.append('lat', gpsCoords.lat);
form.append('lon', gpsCoords.lon);
}
try {
const resp = await fetch('/upload?step=' + step, { method: 'POST', body: form });
if (!resp.ok) throw new Error(await resp.text());
const data = await resp.json();
if (step === 1) afterStep1(data);
else if (step === 2) afterStep2(data);
else afterStep3(data);
} catch (e) {
document.getElementById('status').innerHTML =
`<div class="error">⚠ ${e.message}<br><small>Try taking the photo again.</small></div>`;
// Re-enable the input so the user can retry
input.value = '';
}
}
// ── After step 1: show reading + GPS, then offer step 2 ─────────────────
function afterStep1(data) {
dots(2, 1);
render(`
<div class="step-label">Step 1 done ✓</div>
<div class="step-title">Odometer read</div>
<div class="reading">${data.reading.toLocaleString()} km</div>
${data.location ? `<span class="gps-tag">📍 ${data.location}</span>` : ''}
<div class="divider"></div>
<div class="step-label">Step 2 of 3</div>
<div class="step-title">End of ride 1 / Start of ride 2</div>
<div class="info-box">Take a photo when you have arrived and are about to start ride 2.</div>
<label class="cam-btn" for="img">
📸 Open camera
<input type="file" id="img" accept="image/*" capture="environment"
onchange="handleUpload(this, 2)">
</label>
<div id="status"></div>
`);
}
// ── After step 2: show ride 1 result + offer step 3 ─────────────────────
function afterStep2(data) {
dots(3, 2);
render(`
<div class="step-label">Step 2 done ✓</div>
<div class="step-title">Ride 1: ${data.ride1_km} km</div>
<div class="reading">${data.reading.toLocaleString()} km</div>
<div class="divider"></div>
<div class="step-label">Step 3 of 3</div>
<div class="step-title">End of ride 2</div>
<div class="info-box">Take a photo of the odometer at the end of your second ride.</div>
<label class="cam-btn" for="img">
📸 Open camera
<input type="file" id="img" accept="image/*" capture="environment"
onchange="handleUpload(this, 3)">
</label>
<div id="status"></div>
`);
}
// ── After step 3: summary + download ────────────────────────────────────
function afterStep3(data) {
dots(3, 3);
const total = data.ride1_km + data.ride2_km;
render(`
<div class="step-label">All done ✓</div>
<div class="step-title">Both rides saved</div>
<div style="height:8px"></div>
<div class="summary-row">
<span class="summary-label">Ride 1</span>
<span class="summary-value">${data.ride1_km} km</span>
</div>
<div class="summary-row">
<span class="summary-label">Ride 2</span>
<span class="summary-value">${data.ride2_km} km</span>
</div>
<div class="summary-row">
<span class="summary-label">Total today</span>
<span class="summary-value">${total} km</span>
</div>
${data.location ? `
<div class="summary-row">
<span class="summary-label">Location</span>
<span class="summary-value">${data.location}</span>
</div>` : ''}
<div style="height:10px"></div>
<a class="btn btn-green" href="/download">⬇ Download Excel</a>
<button class="btn btn-gray" onclick="startOver()">Start new session</button>
`);
}
// ── Reset ────────────────────────────────────────────────────────────────
async function startOver() {
await fetch('/reset', { method: 'POST' });
gpsCoords = null;
sessionStorage.removeItem('gpsCoords');
showStep(1);
}
// ── Boot ─────────────────────────────────────────────────────────────────
showStep(1);
</script>
</body>
</html>