📸 Report a parking rate
Snap the guard-house rate board. We extract the numbers, you make KL drivers' day.
Terima kasih!
Building name *
Type *
—
Office
Condo
Mall
Hospital
Gov
Date seen *
Location *
(tap the map, or use your location)
📍 Use my location
No point selected yet.
Photo of the rate board
(strongly recommended)
+ Take / choose photo
No photo? Type the rates instead ▾
Visitor rate
Overnight penalty (Condo)
Early bird
Your handle
(for the hero wall — optional)
Submit report
// and a div before the submit button: //
// The widget injects a hidden `cf-turnstile-response` field that the form already // forwards (see the submit handler), and the Worker verifies it. // date default = today document.getElementById('seen_date').value = new Date().toISOString().slice(0,10); // map (free OSM tiles, no key) const map = L.map('map').setView([3.139, 101.6869], 12); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap' }).addTo(map); let marker = null; function setPoint(la, ln) { lat = +la.toFixed(6); lng = +ln.toFixed(6); if (marker) marker.setLatLng([lat, lng]); else marker = L.marker([lat, lng]).addTo(map); document.getElementById('coords').textContent = `📍 ${lat}, ${lng}`; } map.on('click', e => setPoint(e.latlng.lat, e.latlng.lng)); document.getElementById('geo').onclick = () => { navigator.geolocation?.getCurrentPosition(p => { map.setView([p.coords.latitude, p.coords.longitude], 17); setPoint(p.coords.latitude, p.coords.longitude); }, () => alert('Could not get location — tap the map instead.')); }; // building autocomplete (existing buildings prefill the pin) const nameEl = document.getElementById('building_name'), acEl = document.getElementById('aclist'); let acTimer; nameEl.oninput = () => { clearTimeout(acTimer); const q = nameEl.value.trim(); if (q.length < 2) { acEl.style.display = 'none'; return; } acTimer = setTimeout(async () => { try { const r = await fetch(`${API}/api/search?q=${encodeURIComponent(q)}`); const { results } = await r.json(); if (!results?.length) { acEl.style.display = 'none'; return; } acEl.innerHTML = results.map(b => `
${esc(b.name)}
${esc(b.type)} · ${esc(b.area||'')}
`).join(''); acEl.style.display = 'block'; acEl.querySelectorAll('div').forEach(d => d.onclick = () => { nameEl.value = d.dataset.name; acEl.style.display = 'none'; }); } catch { acEl.style.display = 'none'; } }, 250); }; document.addEventListener('click', e => { if (!acEl.contains(e.target) && e.target !== nameEl) acEl.style.display = 'none'; }); // photo preview const photoEl = document.getElementById('photo'); let previewUrl = null; photoEl.onchange = () => { const f = photoEl.files[0]; if (!f) return; document.getElementById('photolbl').classList.add('has'); document.getElementById('photolbl').textContent = '✓ ' + f.name; const img = document.getElementById('preview'); if (previewUrl) URL.revokeObjectURL(previewUrl); // free the previous blob previewUrl = URL.createObjectURL(f); img.src = previewUrl; img.style.display = 'block'; }; // submit document.getElementById('f').onsubmit = async (e) => { e.preventDefault(); const msg = document.getElementById('msg'), go = document.getElementById('go'); msg.style.display = 'none'; if (lat == null) { showErr('Please pick a location on the map.'); return; } const fd = new FormData(); fd.set('building_name', nameEl.value.trim()); fd.set('building_type', document.getElementById('building_type').value); fd.set('latitude', lat); fd.set('longitude', lng); fd.set('seen_date', document.getElementById('seen_date').value); for (const id of ['visitor_rate_text','overnight_penalty','early_bird_text','contributor_handle']) { const v = document.getElementById(id).value.trim(); if (v) fd.set(id, v); } if (photoEl.files[0]) fd.set('photo', photoEl.files[0]); go.disabled = true; go.textContent = 'Submitting…'; try { const r = await fetch(`${API}/api/contribute`, { method: 'POST', body: fd }); const j = await r.json(); if (r.ok) { msg.className = 'msg ok'; msg.textContent = '🎉 ' + (j.thanks || 'Thank you! Pending review.'); msg.style.display = 'block'; document.getElementById('f').reset(); document.getElementById('preview').style.display = 'none'; if (previewUrl) { URL.revokeObjectURL(previewUrl); previewUrl = null; } document.getElementById('photolbl').classList.remove('has'); document.getElementById('photolbl').textContent = '+ Take / choose photo'; } else showErr(j.error || 'Something went wrong.'); } catch { showErr('Network error — try again.'); } go.disabled = false; go.textContent = 'Submit report'; }; function showErr(t){ const m=document.getElementById('msg'); m.className='msg err'; m.textContent='⚠️ '+t; m.style.display='block'; }