(function () { 'use strict'; // ── Onboarding ──────────────────────────────────────────────────────────── const splashEl = document.getElementById('screen-splash'); const obTrack = document.getElementById('ob-track'); const obDots = document.querySelectorAll('.ob-dot'); const obNext = document.getElementById('ob-next'); const obSkip = document.getElementById('ob-skip'); let obStep = 0; const OB_TOTAL = 4; if (localStorage.getItem('pawmap_onboarding_v2')) { splashEl.style.display = 'none'; } function obGoTo(i) { obStep = i; obTrack.style.transform = `translateX(${-i * 100}%)`; obDots.forEach((d, idx) => d.classList.toggle('active', idx === i)); obNext.textContent = i === OB_TOTAL - 1 ? 'Get Started' : 'Next →'; } function obDismiss() { localStorage.setItem('pawmap_onboarding_v2', '1'); splashEl.classList.add('splash-out'); setTimeout(() => { splashEl.style.display = 'none'; }, 350); } function obShow() { splashEl.style.display = 'flex'; // force reflow so the transition replays when re-opening void splashEl.offsetWidth; splashEl.classList.remove('splash-out'); obGoTo(0); } obNext.addEventListener('click', () => { if (obStep < OB_TOTAL - 1) obGoTo(obStep + 1); else obDismiss(); }); obSkip.addEventListener('click', obDismiss); const menuBtn = document.getElementById('menu-btn'); if (menuBtn) menuBtn.addEventListener('click', obShow); // Swipe support let obTouchX = null; obTrack.addEventListener('touchstart', e => { obTouchX = e.touches[0].clientX; }, { passive: true }); obTrack.addEventListener('touchend', e => { if (obTouchX === null) return; const dx = e.changedTouches[0].clientX - obTouchX; obTouchX = null; if (dx < -40 && obStep < OB_TOTAL - 1) obGoTo(obStep + 1); if (dx > 40 && obStep > 0) obGoTo(obStep - 1); }, { passive: true }); // ── Icon helper ─────────────────────────────────────────────────────────── function svgIcon(name, size=20, color='currentColor') { const ic = (typeof lucide !== 'undefined') && lucide.icons && lucide.icons[name]; if (!ic) return ''; const attrs = `xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"`; const [,, children] = ic; const paths = children.map(([tag, a]) => { const attrStr = Object.entries(a).map(([k,v])=>`${k}="${v}"`).join(' '); return `<${tag} ${attrStr}/>`; }).join(''); return `${paths}`; } // ── State ────────────────────────────────────────────────────────────────── let currentSpecies = 'all'; let currentTimeframe = 'all'; let map, markersLayer; let trajectoryMap = null; let activeAnimal = null; let selectedFile = null; let gpsCoords = null; let sessionId = null; const header = document.getElementById('header'); const filterRow = document.getElementById('filter-row'); const bottomNav = document.getElementById('bottom-nav'); const photoInput= document.getElementById('photo-input'); const FLOW_SCREENS = ['analysis', 'confirm', 'profile', 'help-proof']; // ── Navigation ───────────────────────────────────────────────────────────── function showScreen(name) { document.querySelectorAll('.screen').forEach(s => s.classList.remove('active')); const el = document.getElementById('screen-' + name); if (el) el.classList.add('active'); const isFlow = FLOW_SCREENS.includes(name); header.classList.toggle('hidden', isFlow); bottomNav.classList.toggle('hidden', isFlow); filterRow.style.display = name === 'map' ? 'flex' : 'none'; document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active')); const navBtn = document.querySelector(`.nav-btn[data-screen="${name}"]`); if (navBtn) navBtn.classList.add('active'); if (name === 'map' && map) setTimeout(() => map.invalidateSize(), 60); if (name === 'sightings') loadAnimals(); } document.querySelectorAll('.nav-btn').forEach(btn => btn.addEventListener('click', () => showScreen(btn.dataset.screen)) ); // ── MAP ──────────────────────────────────────────────────────────────────── function initMap() { map = L.map('map', { zoomControl: false }).setView([-15.7801, -47.9292], 13); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OSM', maxZoom: 19 }).addTo(map); markersLayer = L.layerGroup().addTo(map); L.control.zoom({ position: 'bottomright' }).addTo(map); map.on('click', hideCard); } function makeIcon(a) { const isDog = a.species === 'dog'; const urgent = a.days_since > 30; const color = urgent ? '#E53935' : isDog ? '#388C59' : '#FB8C00'; const badge = a.count > 1 ? `${a.count}` : ''; const fallbackSvg = svgIcon(isDog ? 'dog' : 'cat', 22, '#fff'); const inner = a.photo_url ? `` : fallbackSvg; return L.divIcon({ html: `
${inner}${badge}
`, className:'', iconSize:[42,42], iconAnchor:[21,21], popupAnchor:[0,-26] }); } function renderMarkers(data) { markersLayer.clearLayers(); const pts = data.filter(a => a.lat && a.lng); document.getElementById('map-empty').style.display = pts.length ? 'none' : 'block'; if (!pts.length) { hideCard(); return; } pts.forEach(a => { const m = L.marker([a.lat, a.lng], { icon: makeIcon(a) }); m.on('click', e => { e.originalEvent.stopPropagation(); showCard(a); }); markersLayer.addLayer(m); }); if (typeof lucide !== 'undefined') lucide.createIcons(); showCard(pts[0]); } function showCard(a) { activeAnimal = a; const isDog = a.species === 'dog'; const urgent = a.days_since > 30; const photo = document.getElementById('card-photo'); const animalIcon = svgIcon(isDog ? 'dog' : 'cat', 26, isDog ? '#388C59' : '#FB8C00'); photo.innerHTML = a.photo_url ? `photo` : animalIcon; const badge = document.getElementById('card-badge'); badge.textContent = isDog ? 'Dog' : 'Cat'; badge.className = 'badge' + (urgent ? ' urgent' : (!isDog ? ' orange' : '')); document.getElementById('card-time').textContent = a.days_since === 0 ? 'Today' : a.days_since === 1 ? 'Yesterday' : `${a.days_since}d ago`; document.getElementById('card-location').textContent = a.desc || (isDog ? 'Dog spotted' : 'Cat spotted'); document.getElementById('card-sub').textContent = `${a.count} sighting${a.count!==1?'s':''} · last: ${a.last_seen}`; document.getElementById('sighting-card').classList.remove('hidden'); } function hideCard() { document.getElementById('sighting-card').classList.add('hidden'); activeAnimal = null; } document.getElementById('card-btn').addEventListener('click', () => { if (activeAnimal) openProfile(activeAnimal.id); }); async function loadMapData() { try { const data = await fetch(`/api/map-data?species=${currentSpecies}&timeframe=${currentTimeframe}`).then(r => r.json()); renderMarkers(data); } catch(e) { console.error(e); } } document.querySelectorAll('.chip[data-species]').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.chip[data-species]').forEach(b => b.classList.remove('active')); btn.classList.add('active'); currentSpecies = btn.dataset.species; loadMapData(); }); }); document.querySelectorAll('.chip[data-timeframe]').forEach(btn => { btn.addEventListener('click', () => { const was = btn.classList.contains('active'); document.querySelectorAll('.chip[data-timeframe]').forEach(b => b.classList.remove('active')); currentTimeframe = was ? 'all' : btn.dataset.timeframe; if (!was) btn.classList.add('active'); loadMapData(); }); }); // ── SIGHTINGS LIST ───────────────────────────────────────────────────────── let animalsCache = null; async function loadAnimals(force = false) { if (animalsCache && !force) { renderAnimalList(animalsCache); return; } document.getElementById('animals-list').innerHTML = `
Loading...
`; try { const data = await fetch('/api/animals').then(r => r.json()); animalsCache = data; renderAnimalList(data); } catch(e) { document.getElementById('animals-list').innerHTML = `
Failed to load. Try again.
`; } } function renderAnimalList(animals) { const list = document.getElementById('animals-list'); const count = document.getElementById('sightings-count'); count.textContent = `${animals.length} animal${animals.length !== 1 ? 's' : ''} on record`; if (!animals.length) { list.innerHTML = `

No animals on record yet.
Go to the Report tab to get started!

`; return; } list.innerHTML = animals.map(a => { const isDog = a.species === 'dog'; const urgent = (a.days_since || 0) > 30; const em = svgIcon(isDog ? 'dog' : 'cat', 28, isDog ? '#388C59' : '#FB8C00'); const name = isDog ? 'Dog' : 'Cat'; let desc = ''; try { const d = JSON.parse(a.description || '{}'); desc = [d.breed_estimate, d.primary_color].filter(Boolean).join(' · '); } catch(e){} const photoHtml = a.photo_url ? `photo` : em; return `
${photoHtml}
${name} #${a.id}
${desc || 'Unknown breed'}
${urgent ? svgIcon('triangle-alert',13,'#E53935')+' Not seen for '+a.days_since+'d' : 'Seen '+formatDays(a.days_since)+' · '+a.sighting_count+'x spotted'}
${a.sighting_count}x
`; }).join(''); if (typeof lucide !== 'undefined') lucide.createIcons(); list.querySelectorAll('.animal-item').forEach(el => { el.addEventListener('click', () => openProfile(parseInt(el.dataset.id))); }); } function formatDays(d) { if (!d || d === 0) return 'today'; if (d === 1) return 'yesterday'; return `${d} days ago`; } document.getElementById('refresh-btn').addEventListener('click', () => loadAnimals(true)); // ── PROFILE / FICHA ──────────────────────────────────────────────────────── let profileAnimalId = null; async function openProfile(id) { profileAnimalId = id; window.profileAnimalId = id; showScreen('profile'); // Reset const heroImgEl = document.getElementById('profile-hero-img'); heroImgEl.src = ''; heroImgEl.style.display = 'block'; document.getElementById('profile-title').textContent = 'Loading...'; document.getElementById('profile-status-text').textContent = ''; document.getElementById('profile-badge-text').textContent = '...'; document.getElementById('profile-gallery').innerHTML = ''; document.getElementById('prof-species').textContent = '—'; document.getElementById('prof-breed').textContent = '—'; document.getElementById('prof-size').textContent = '—'; document.getElementById('prof-color').innerHTML = '—'; if (trajectoryMap) { trajectoryMap.remove(); trajectoryMap = null; } try { const data = await fetch(`/api/animal/${id}`).then(r => r.json()); const animal = data.animal; const sightings = data.sightings || []; const helpEvents = data.help_events || []; let desc = {}; try { desc = JSON.parse(animal.description || '{}'); } catch(e){} const isDog = animal.species === 'dog'; const breed = desc.breed_estimate || 'Unknown'; const color = desc.primary_color || '—'; const size = desc.size || '—'; const count = animal.sighting_count || sightings.length; const daysSince = animal.days_since || 0; // Hero image const heroPhoto = sightings.find(s => s.photo_url)?.photo_url || ''; if (heroPhoto) { const img = document.getElementById('profile-hero-img'); img.src = heroPhoto; img.onerror = () => { img.style.display = 'none'; }; } // Badge document.getElementById('profile-badge-text').textContent = `${count} sighting${count !== 1 ? 's' : ''}`; // Title & status — prefer user-given name const colorCap = color.charAt(0).toUpperCase() + color.slice(1); const autoName = `${isDog ? 'Dog' : 'Cat'} ${colorCap}`.trim(); const name = (animal.name && animal.name !== 'null') ? animal.name : autoName; document.getElementById('profile-title').textContent = name; const statusText = daysSince === 0 ? 'Seen today' : daysSince === 1 ? 'Seen yesterday' : `Seen ${daysSince} days ago`; document.getElementById('profile-status-text').textContent = statusText; // Identification document.getElementById('prof-species').innerHTML = `${svgIcon(isDog?'dog':'cat',16)} ${isDog ? 'Dog' : 'Cat'}`; document.getElementById('prof-breed').textContent = breed; document.getElementById('prof-size').textContent = mapSizeLabel(size); const colorDot = colorToHex(color); document.getElementById('prof-color').innerHTML = colorDot ? `${colorCap}` : colorCap; // AI description callout const aiDescEl = document.getElementById('profile-ai-desc'); const aiTextEl = document.getElementById('profile-ai-text'); const aiText = desc.description_text || ''; const condition = desc.condition || ''; const marks = (desc.distinctive_marks || []).filter(Boolean); if (aiText) { let aiContent = aiText.charAt(0).toUpperCase() + aiText.slice(1); if (condition) aiContent += ` · Condition: ${condition}`; if (marks.length) aiContent += ` · Marks: ${marks.join(', ')}`; aiTextEl.textContent = aiContent; aiDescEl.style.display = 'flex'; } else { aiDescEl.style.display = 'none'; } // Helped banner const helpedBanner = document.getElementById('profile-helped-banner'); helpedBanner.style.display = helpEvents.length ? 'flex' : 'none'; // Gallery const gallery = document.getElementById('profile-gallery'); const photosWithUrl = sightings.filter(s => s.photo_url); if (photosWithUrl.length) { gallery.innerHTML = photosWithUrl.map(s => { const dt = s.created_at ? new Date(s.created_at) : null; const dateStr = dt ? dt.toLocaleDateString('en-US', {day:'2-digit', month:'2-digit'}) : ''; const timeStr = dt ? dt.toLocaleTimeString('en-US', {hour:'2-digit', minute:'2-digit'}) : ''; const locStr = (s.latitude && s.longitude) ? `${s.latitude.toFixed(3)}, ${s.longitude.toFixed(3)}` : 'Location not recorded'; return ` `; }).join(''); if (typeof lucide !== 'undefined') lucide.createIcons(); } else { gallery.innerHTML = `
No photos recorded.
`; } // Community Help section const helpSection = document.getElementById('profile-help-section'); const helpList = document.getElementById('profile-help-list'); const helpTypeLabels = { fed: ' Fed', vet: ' Took to vet', adopted: ' Adopted', rescued: ' Rescued', other: ' Helped', }; if (helpEvents.length) { helpSection.style.display = ''; helpList.innerHTML = helpEvents.map(h => { const dt = h.created_at ? new Date(h.created_at) : null; const dateStr = dt ? dt.toLocaleDateString('en-US', {day:'2-digit', month:'short', year:'numeric'}) : ''; const label = helpTypeLabels[h.help_type] || ' Helped'; return '
' + (h.photo_url ? 'help proof' : '') + '
' + '
' + label + '
' + (h.notes ? '
' + h.notes + '
' : '') + (dateStr ? '
' + dateStr + '
' : '') + '
'; }).join(''); if (typeof lucide !== 'undefined') lucide.createIcons(); } else { helpSection.style.display = 'none'; } // Trajectory mini-map const pts = sightings.filter(s => s.latitude && s.longitude); initTrajectoryMap(pts); if (typeof lucide !== 'undefined') lucide.createIcons(); } catch(e) { console.error(e); document.getElementById('profile-title').textContent = 'Failed to load'; } } function initTrajectoryMap(points) { const el = document.getElementById('trajectory-map'); if (trajectoryMap) { trajectoryMap.remove(); trajectoryMap = null; } if (!points.length) { el.innerHTML = `
No locations recorded
`; return; } el.innerHTML = ''; el.style.height = '180px'; const center = [points[0].latitude, points[0].longitude]; trajectoryMap = L.map(el, { zoomControl: false, attributionControl: false, dragging: false, scrollWheelZoom: false, doubleClickZoom: false, touchZoom: false }).setView(center, 14); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(trajectoryMap); const latlngs = points.map(p => [p.latitude, p.longitude]); if (latlngs.length > 1) { L.polyline(latlngs, { color: '#388C59', weight: 2.5, opacity: .7, dashArray: '6,4' }).addTo(trajectoryMap); } points.forEach((p, i) => { const isLast = i === 0; const icon = L.divIcon({ html: `
`, className:'', iconSize:[isLast?16:10, isLast?16:10], iconAnchor:[isLast?8:5, isLast?8:5] }); L.marker([p.latitude, p.longitude], { icon }).addTo(trajectoryMap); }); if (latlngs.length > 1) { trajectoryMap.fitBounds(latlngs, { padding: [20, 20] }); } setTimeout(() => trajectoryMap && trajectoryMap.invalidateSize(), 100); } function mapSizeLabel(s) { if (!s) return '—'; const l = s.toLowerCase(); if (l.includes('small') || l.includes('pequen')) return '↕ Small'; if (l.includes('large') || l.includes('grand')) return '↕ Large'; if (l.includes('médio') || l.includes('medio') || l.includes('medium')) return '↕ Medium'; return '↕ ' + s; } function colorToHex(color) { const map = { caramelo:'#D2691E', preto:'#222', branco:'#f0f0f0', cinza:'#999', marrom:'#8B4513', amarelo:'#F5C542', mesclado:'linear-gradient(135deg,#888 50%,#D2691E 50%)' }; return map[(color||'').toLowerCase()] || null; } document.getElementById('profile-back-btn').addEventListener('click', () => { if (trajectoryMap) { trajectoryMap.remove(); trajectoryMap = null; } showScreen('sightings'); }); // ── REGISTER ─────────────────────────────────────────────────────────────── document.getElementById('shutter-btn').addEventListener('click', () => photoInput.click()); photoInput.addEventListener('change', e => { const file = e.target.files[0]; if (!file) return; selectedFile = file; const url = URL.createObjectURL(file); document.getElementById('photo-preview').src = url; document.getElementById('photo-preview').style.display = 'block'; document.getElementById('camera-placeholder').style.display = 'none'; updateSubmitBtn(); }); document.getElementById('gps-pill').addEventListener('click', requestGPS); document.getElementById('gps-btn').addEventListener('click', requestGPS); function requestGPS() { if (!navigator.geolocation) { document.getElementById('gps-text').textContent = 'GPS not available'; return; } const txt = document.getElementById('gps-text'); const btn = document.getElementById('gps-btn'); txt.textContent = 'Detecting...'; txt.classList.remove('located'); btn.disabled = true; navigator.geolocation.getCurrentPosition( pos => { gpsCoords = { lat: pos.coords.latitude, lng: pos.coords.longitude }; txt.textContent = `${gpsCoords.lat.toFixed(4)}, ${gpsCoords.lng.toFixed(4)}`; txt.classList.add('located'); btn.innerHTML = ` Location Added`; btn.classList.add('located'); btn.disabled = false; updateSubmitBtn(); }, err => { const msgs = {1:'Permission denied',2:'Location unavailable',3:'GPS timeout'}; txt.textContent = msgs[err.code] || 'GPS error'; btn.disabled = false; }, { enableHighAccuracy: true, timeout: 15000, maximumAge: 0 } ); } function updateSubmitBtn() { document.getElementById('submit-reg-btn').disabled = !selectedFile; } document.getElementById('submit-reg-btn').addEventListener('click', async () => { if (!selectedFile) return; showScreen('analysis'); await runAnalysis(); }); // ── GRADIO CLIENT ────────────────────────────────────────────────────────── let _client = null, _handleFile = null; async function getClient() { if (!_client) { const mod = await import('https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js'); _handleFile = mod.handle_file; _client = await mod.Client.connect(window.location.origin); } return { client: _client, handleFile: _handleFile }; } // ── ANALYSIS ─────────────────────────────────────────────────────────────── async function runAnalysis() { document.getElementById('analysis-photo').src = URL.createObjectURL(selectedFile); const aiBadge = document.getElementById('ai-badge'); aiBadge.className = ''; document.getElementById('ai-badge-text').textContent = 'AI Analyzing...'; document.getElementById('animal-result-badge').classList.remove('visible'); const confirmBtn = document.getElementById('confirm-btn'); confirmBtn.disabled = true; confirmBtn.innerHTML = '
Analyzing...'; try { const { client, handleFile } = await getClient(); const res = await client.predict('/analyze_image', { image_path: handleFile(selectedFile) }); const data = res.data[0]; if (data.error) { aiBadge.className = 'error'; document.getElementById('ai-badge-text').textContent = 'No animal found'; document.getElementById('result-badge-text').textContent = data.error; document.getElementById('animal-result-badge').classList.add('visible', 'error'); confirmBtn.style.display = 'none'; document.getElementById('discard-btn').textContent = '← Take another photo'; return; } sessionId = data.session_id; const desc = data.description || {}; setSelectVal('sel-species', desc.species === 'cat' ? 'cat' : 'dog'); setSelectVal('sel-breed', desc.breed_estimate || 'SRD'); setSelectVal('sel-color', desc.primary_color || 'Caramelo'); setSelectVal('sel-size', mapSizeOpt(desc.size)); aiBadge.className = 'done'; document.getElementById('ai-badge-text').textContent = 'AI Done'; const sp = desc.species === 'cat' ? 'Cat' : 'Dog'; const co = (desc.primary_color || '').charAt(0).toUpperCase() + (desc.primary_color || '').slice(1); document.getElementById('result-badge-text').textContent = `${sp} ${co}`.trim(); document.getElementById('animal-result-badge').classList.add('visible'); renderSimilar(data.similar || []); confirmBtn.disabled = false; confirmBtn.innerHTML = 'Confirm & Report →'; } catch(err) { console.error(err); document.getElementById('ai-badge-text').textContent = 'Analysis error'; confirmBtn.disabled = false; confirmBtn.innerHTML = 'Confirm without AI →'; } } // ── Breed lists ─────────────────────────────────────────────────────────── const DOG_BREEDS = [ 'SRD', 'Labrador Retriever', 'Golden Retriever', 'Pitbull', 'Poodle', 'Shih Tzu', 'Rottweiler', 'German Shepherd', 'Bulldog', 'Dachshund', 'Chihuahua', 'Siberian Husky', 'Border Collie', 'Beagle', 'Boxer', 'Maltese', 'Chow Chow', 'Akita', 'Dalmatian', 'Doberman', 'Other', ]; const CAT_BREEDS = [ 'Domestic Shorthair', 'Domestic Longhair', 'Siamese', 'Persian', 'Maine Coon', 'Bengal', 'British Shorthair', 'Ragdoll', 'Scottish Fold', 'Turkish Angora', 'Sphynx', 'Abyssinian', 'Other', ]; function populateBreeds(species) { const sel = document.getElementById('sel-breed'); if (!sel) return; const list = species === 'cat' ? CAT_BREEDS : DOG_BREEDS; const current = sel.value; sel.innerHTML = list.map(b => ``).join(''); // Keep current selection if it still exists if (list.includes(current)) sel.value = current; } // Repopulate breeds when species changes document.getElementById('sel-species').addEventListener('change', e => { populateBreeds(e.target.value); }); // Init with dogs populateBreeds('dog'); function setSelectVal(id, val) { const sel = document.getElementById(id); if (!sel) return; // If setting species, repopulate breeds first if (id === 'sel-species') populateBreeds(val); for (const opt of sel.options) { if (opt.value.toLowerCase() === (val||'').toLowerCase()) { sel.value = opt.value; return; } } // If breed not found in list, select "Other" if (id === 'sel-breed') { sel.value = 'Other'; } } function mapSizeOpt(s) { if (!s) return 'Médio'; const l = s.toLowerCase(); if (l.includes('small')||l.includes('pequen')) return 'Pequeno'; if (l.includes('large')||l.includes('grand')) return 'Grande'; return 'Médio'; } function renderSimilar(similar) { const section = document.getElementById('similar-section'); const scroll = document.getElementById('similar-scroll'); if (!similar.length) { section.style.display = 'none'; return; } section.style.display = ''; scroll.innerHTML = similar.map(m => `
${m.photo_url ? `` : svgIcon('paw-print',36,'#ccc')} ${m.score_pct}%
${m.days_ago ? m.days_ago+' days ago' : 'Registered'}
Animal #${m.id}
`).join(''); } document.querySelectorAll('.cond-chip').forEach(b => b.addEventListener('click', () => b.classList.toggle('active'))); document.getElementById('analysis-back').addEventListener('click', () => showScreen('register')); document.getElementById('discard-btn').addEventListener('click', () => { const confirmBtn = document.getElementById('confirm-btn'); confirmBtn.style.display = ''; confirmBtn.disabled = false; confirmBtn.innerHTML = 'Confirm & Report →'; document.getElementById('discard-btn').textContent = 'Discard Photo'; document.getElementById('animal-result-badge').classList.remove('visible','error'); resetRegister(); showScreen('register'); }); // ── CONFIRM ──────────────────────────────────────────────────────────────── document.getElementById('confirm-btn').addEventListener('click', async () => { const btn = document.getElementById('confirm-btn'); btn.disabled = true; btn.innerHTML = '
Saving...'; const capturedSpecies = document.getElementById('sel-species').value; const capturedBreed = document.getElementById('sel-breed').value; const capturedColor = document.getElementById('sel-color').value; const capturedSize = document.getElementById('sel-size').value; const capturedCond = [...document.querySelectorAll('.cond-chip.active')].map(c => c.dataset.val).join(', '); try { const { client } = await getClient(); const gpsJson = gpsCoords ? JSON.stringify(gpsCoords) : ''; const notes = document.getElementById('notes-input').value.trim(); const animalName = (document.getElementById('animal-name-input').value || '').trim(); const res = await client.predict('/confirm_sighting', { session_id: sessionId||'', gps_json: gpsJson, notes, condition: capturedCond, animal_name: animalName }); const data = res.data[0]; if (data.error) { alert(data.error); btn.disabled=false; btn.innerHTML='Confirm & Report →'; return; } document.getElementById('confirm-animal').textContent = data.name || '—'; document.getElementById('confirm-local').textContent = data.location || '—'; document.getElementById('confirm-hora').textContent = data.time || '—'; const photoWrap = document.getElementById('confirm-photo'); const specIcon = svgIcon(data.species === 'dog' ? 'dog' : 'cat', 72, data.species === 'dog' ? '#388C59' : '#FB8C00'); photoWrap.innerHTML = data.photo_url ? `` : specIcon; const specLabel = capturedSpecies === 'dog' ? 'Dog' : 'Cat'; const sizeLabels = { Pequeno:'↕ Small', Médio:'↕ Medium', Grande:'↕ Large' }; const colorDot = colorToHex(capturedColor); const colorSwatch = colorDot ? `` : ''; document.getElementById('confirm-id-grid').innerHTML = `
AI Identification
Species
${svgIcon(capturedSpecies==='dog'?'dog':'cat',13)} ${specLabel}
Breed
${capturedBreed}
Size
${sizeLabels[capturedSize]||capturedSize}
Color
${colorSwatch}${capturedColor}
${capturedCond ? `
Condition
${capturedCond}
` : ''}
`; animalsCache = null; showScreen('confirm'); loadMapData(); } catch(err) { console.error(err); alert('Error saving. Please try again.'); btn.disabled=false; btn.innerHTML='Confirm & Report →'; } }); document.getElementById('register-another-btn').addEventListener('click', () => { resetRegister(); showScreen('register'); }); document.getElementById('go-map-btn').addEventListener('click', () => showScreen('map')); function resetRegister() { selectedFile = null; gpsCoords = null; sessionId = null; document.getElementById('photo-preview').style.display = 'none'; document.getElementById('photo-preview').src = ''; document.getElementById('camera-placeholder').style.display = 'flex'; document.getElementById('photo-input').value = ''; document.getElementById('notes-input').value = ''; document.getElementById('animal-name-input').value = ''; document.getElementById('gps-text').textContent = 'Tap to detect location'; document.getElementById('gps-text').classList.remove('located'); const btn = document.getElementById('gps-btn'); btn.className = 'btn-secondary'; btn.innerHTML = ` Add Location`; btn.disabled = false; document.querySelectorAll('.cond-chip').forEach(c => c.classList.remove('active')); updateSubmitBtn(); } // ── PHOTO CONTEXT MENU ──────────────────────────────────────────────────── let photoMenuUrl = null; const photoMenu = document.getElementById('photo-menu'); function openPhotoMenu(btn) { photoMenuUrl = btn.dataset.url; const rect = btn.getBoundingClientRect(); const appRect = document.getElementById('app').getBoundingClientRect(); photoMenu.classList.remove('hidden'); // Position below the button, clamped inside app let top = rect.bottom - appRect.top + 4; let left = rect.right - appRect.left - photoMenu.offsetWidth; if (left < 8) left = 8; photoMenu.style.top = top + 'px'; photoMenu.style.left = left + 'px'; } function closePhotoMenu() { photoMenu.classList.add('hidden'); photoMenuUrl = null; } document.getElementById('app').addEventListener('click', e => { const btn = e.target.closest('.gallery-card-menu-btn'); if (btn) { e.stopPropagation(); openPhotoMenu(btn); return; } if (!photoMenu.contains(e.target)) closePhotoMenu(); }); document.getElementById('photo-menu-download').addEventListener('click', () => { if (!photoMenuUrl) return; const a = document.createElement('a'); a.href = photoMenuUrl; a.download = 'pawmap-photo.jpg'; a.target = '_blank'; document.body.appendChild(a); a.click(); document.body.removeChild(a); closePhotoMenu(); }); // ── HELP SHEET ──────────────────────────────────────────────────────────── let helpAnimal = null; function openHelpSheet(animalData) { helpAnimal = animalData; const isDog = animalData.species === 'dog'; // Populate header const photoEl = document.getElementById('help-animal-photo'); const heroPhoto = (animalData.sightings || []).find(s => s.photo_url)?.photo_url || animalData.photo_url || ''; photoEl.innerHTML = heroPhoto ? `` : `${isDog ? 'Dog' : 'Cat'}`; let desc = {}; try { desc = JSON.parse(animalData.description || '{}'); } catch(e) {} const color = (desc.primary_color || '').charAt(0).toUpperCase() + (desc.primary_color || '').slice(1); document.getElementById('help-animal-name').textContent = `${isDog ? 'Dog' : 'Cat'} ${color}`.trim() || `Animal #${animalData.id}`; document.getElementById('help-animal-sub').textContent = `${animalData.sighting_count || 1} sighting${(animalData.sighting_count||1)!==1?'s':''} · last seen ${animalData.last_seen || 'recently'}`; document.getElementById('help-overlay').classList.remove('hidden'); document.getElementById('help-sheet').classList.remove('hidden'); requestAnimationFrame(() => document.getElementById('help-sheet').classList.add('open')); } function closeHelpSheet() { const sheet = document.getElementById('help-sheet'); sheet.classList.remove('open'); setTimeout(() => { sheet.classList.add('hidden'); document.getElementById('help-overlay').classList.add('hidden'); }, 300); } document.getElementById('btn-adopt').addEventListener('click', () => { // Pass current profile animal data fetch(`/api/animal/${profileAnimalId}`).then(r => r.json()).then(data => { openHelpSheet({ ...data.animal, sightings: data.sightings }); }); }); document.getElementById('help-cancel').addEventListener('click', closeHelpSheet); document.getElementById('help-overlay').addEventListener('click', closeHelpSheet); // Share document.getElementById('help-share').addEventListener('click', () => { const url = `${window.location.origin}/#animal/${profileAnimalId}`; const text = `Help this stray animal find a home! Track their location on PawMap: ${url}`; if (navigator.share) { navigator.share({ title: 'PawMap', text, url }).catch(() => {}); } else { const wa = `https://wa.me/?text=${encodeURIComponent(text)}`; window.open(wa, '_blank'); } closeHelpSheet(); }); // Directions document.getElementById('help-directions').addEventListener('click', () => { if (!helpAnimal) return; const sightings = helpAnimal.sightings || []; const last = sightings.find(s => s.latitude && s.longitude); if (last) { window.open(`https://www.google.com/maps/dir/?api=1&destination=${last.latitude},${last.longitude}`, '_blank'); } else { alert('No location recorded for this animal yet.'); } closeHelpSheet(); }); // I helped → nova tela document.getElementById('help-helped').addEventListener('click', () => { closeHelpSheet(); window.openHelpProofScreen(); }); document.getElementById('helped-ok').addEventListener('click', () => { const confirm = document.getElementById('helped-confirm'); confirm.classList.remove('open'); setTimeout(() => confirm.classList.add('hidden'), 300); }); // ── Photo lightbox ──────────────────────────────────────────────────────── const lightbox = document.getElementById('photo-lightbox'); const lightboxImg = document.getElementById('lightbox-img'); function openLightbox(src) { lightboxImg.src = src; lightbox.classList.remove('hidden'); document.body.style.overflow = 'hidden'; } function closeLightbox() { lightbox.classList.add('hidden'); lightboxImg.src = ''; document.body.style.overflow = ''; } document.getElementById('lightbox-close').addEventListener('click', closeLightbox); document.querySelector('.lightbox-backdrop').addEventListener('click', closeLightbox); document.addEventListener('keydown', e => { if (e.key === 'Escape') closeLightbox(); }); document.getElementById('profile-help-list').addEventListener('click', e => { const img = e.target.closest('.help-event-photo'); if (img) openLightbox(img.src); }); // Expor para help-proof.js window.showScreen = showScreen; window.openProfile = openProfile; window.getGradioClient = getClient; // ── BOOT ────────────────────────────────────────────────────────────────── initMap(); loadMapData(); if (typeof lucide !== 'undefined') lucide.createIcons(); })();