(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
? ` `
: 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
? ` `
: 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 `
${dateStr}${timeStr ? ', '+timeStr : ''}
${locStr}
`;
}).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 ? '
' : '')
+ '
'
+ '
' + 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 => `${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}
Size
${sizeLabels[capturedSize]||capturedSize}
Color
${colorSwatch}${capturedColor}
${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();
})();