Driverthing/templates/index.html
Claude 89f60e894b
Fix geolocation on iOS Safari: explicit button + error feedback
Replace silent on-load getCurrentPosition with an opt-in button on
step 1. iOS Safari requires a user gesture for the permission prompt
to appear; calling it on page load suppresses the dialog. The button
triggers the request on tap, shows "Getting location…" while waiting,
and surfaces permission-denied / timeout errors with clear messages.

https://claude.ai/code/session_015myTTMs6yDsAGarATe5ePZ
2026-03-18 20:16:16 +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>
let gpsCoords = 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 };
el.innerHTML = '<span style="color:#388e3c">📍 Location captured</span>';
},
err => {
const msgs = {
1: 'Permission denied — allow location in Safari settings.',
2: 'Position unavailable.',
3: 'Location request timed out.',
};
el.innerHTML = `<span style="color:#e53e3e">${msgs[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;
showStep(1);
}
// ── Boot ─────────────────────────────────────────────────────────────────
showStep(1);
</script>
</body>
</html>