Selfies: Difference between revisions
From Treeverse
More actions
Created blank page |
No edit summary |
||
| Line 1: | Line 1: | ||
<html> | |||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"> | |||
<style> | |||
.tv-wrap{position:relative} | |||
#map{height:80vh;width:100%;border:1px solid #ddd;border-radius:10px} | |||
.tv-panel{ | |||
position:absolute; top:10px; right:10px; z-index:1000; | |||
background:#f4f8f4; padding:10px; border-radius:12px; | |||
box-shadow:0 6px 20px rgba(0,0,0,.25); | |||
width:300px; font:14px/1.3 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; | |||
border:2px solid #3b5d3f; | |||
} | |||
.tv-panel h3{margin:.2rem 0} | |||
.tv-icon{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:50%; | |||
box-shadow:0 0 0 2px #fff,0 2px 6px rgba(0,0,0,.25); font-size:18px; line-height:1; background:#16a085} | |||
.tv-input{display:block;width:100%;margin:6px 0;box-sizing:border-box} | |||
.tv-text{padding:6px 8px;border:1px solid #ccc;border-radius:6px;font-size:14px} | |||
.tv-select{padding:8px;border:1px solid #ccc;border-radius:6px;font-size:15px} | |||
.tv-btn{display:block;width:100%;margin-top:6px;padding:10px;border:none;border-radius:6px;font-weight:bold;cursor:pointer} | |||
.btn-add{background:#27ae60;color:#fff} .btn-add:hover{background:#1f8b4d} | |||
.btn-exp{background:#3498db;color:#fff} .btn-exp:hover{background:#2980b9} | |||
.btn-del{background:#bdc3c7;color:#2c3e50} .btn-del.on{background:#e74c3c;color:#fff} | |||
#toast{position:fixed;top:10px;left:50%;transform:translateX(-50%);background:#2ecc71;color:#fff;padding:6px 12px;border-radius:6px;font-size:13px;opacity:0;transition:opacity .3s;pointer-events:none;z-index:2000} | |||
#toast.show{opacity:1} | |||
</style> | |||
<div class="tv-wrap"> | |||
<div id="map"></div> | |||
<div class="tv-panel" id="tv-panel"> | |||
<h3>Map Tools</h3> | |||
<select id="m-type" class="tv-input tv-select"> | |||
<option value="selfie">Selfie 📸</option> | |||
</select> | |||
<input id="m-title" class="tv-input tv-text" placeholder="Type Name..."> | |||
<textarea id="m-desc" class="tv-input tv-text" rows="2" placeholder="Add short notes..."></textarea> | |||
<!-- allow jpg/jpeg/png/webp (≤10MB checked in JS + PHP) --> | |||
<input type="file" id="m-photo" class="tv-input" accept="image/jpeg,image/png,image/webp"> | |||
<button id="m-add" class="tv-btn btn-add">Add to Map</button> | |||
<button id="m-export" class="tv-btn btn-exp">Export Map</button> | |||
<button id="m-delmode" class="tv-btn btn-del">Delete mode: OFF</button> | |||
<div style="font-size:12px;opacity:.75;margin-top:6px;line-height:1.4"> | |||
👉 Click a map icon to view details.<br> | |||
🖱️ Right click on map icon to delete (if Delete mode is ON). | |||
</div> | |||
</div> | |||
</div> | |||
<div id="toast">Saved ✅</div> | |||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> | |||
<script src="https://unpkg.com/html2canvas@1.4.1/dist/html2canvas.min.js"></script> | |||
<script> | |||
// --- CONFIG (endpoints) --- | |||
const MAP_IMAGE_URL = 'https://treeverse.wiki/images/5/50/Selfie.JPEG'; | |||
const DATA_URL = '/map-selfie/map-data-selfie.json'; | |||
const SAVE_URL = '/map-selfie/save-selfie.php'; | |||
const UPLOAD_URL = '/map-selfie/upload-selfie.php'; | |||
// -------------------------- | |||
function toast(msg="Saved ✅"){ const t=document.getElementById('toast'); t.textContent=msg; t.classList.add('show'); setTimeout(()=>t.classList.remove('show'),1500); } | |||
function icon(){ return L.divIcon({ className:'tv-icon', html:'<span>📸</span>', iconSize:[30,30], iconAnchor:[15,30], popupAnchor:[0,-24] }); } | |||
function popupHtml(title,desc,imgUrl){ | |||
const t=(title||'').trim(); | |||
const d=(desc||'').trim().replace(/</g,'<').replace(/>/g,'>'); | |||
const img = imgUrl ? `<div style="margin-top:6px"><img src="${imgUrl}" alt="" style="max-width:220px;border-radius:6px"></div>` : ''; | |||
return `<b>${t||'Untitled'}</b>${d?`<div style="margin-top:4px">${d}</div>`:''}${img}`; | |||
} | |||
const img = new Image(); | |||
img.onload = () => { | |||
const w = img.naturalWidth, h = img.naturalHeight; | |||
const map = L.map('map', { crs:L.CRS.Simple, minZoom:-2, maxZoom:5, zoomControl:true }); | |||
const bounds = [[0,0],[h,w]]; | |||
L.imageOverlay(MAP_IMAGE_URL, bounds, { crossOrigin:'anonymous' }).addTo(map); | |||
map.fitBounds(bounds); | |||
const selfies = L.layerGroup().addTo(map); | |||
L.control.layers(null, { 'Selfies 📸': selfies }, { collapsed:false, position:'topleft' }).addTo(map); | |||
// Delete mode | |||
let deleteMode=false; | |||
const delBtn=document.getElementById('m-delmode'); | |||
delBtn.onclick=()=>{ deleteMode=!deleteMode; delBtn.textContent='Delete mode: '+(deleteMode?'ON':'OFF'); delBtn.classList.toggle('on', deleteMode); }; | |||
function addMarker(latlng, title, desc, imgUrl){ | |||
const m = L.marker(latlng,{icon:icon(), title:title||'Selfie', desc:desc||'', imgUrl:imgUrl||null}).addTo(selfies); | |||
m.bindPopup(popupHtml(title,desc,imgUrl)); | |||
m.on('click', () => m.openPopup()); | |||
m.on('contextmenu', () => { if(deleteMode){ selfies.removeLayer(m); saveShared().catch(console.warn); } }); | |||
return m; | |||
} | |||
function collectMarkers(){ | |||
const all=[]; selfies.eachLayer(l=>{ const ll=l.getLatLng(); | |||
all.push({type:'selfie', y:ll.lat, x:ll.lng, title:l.options.title||'Selfie', desc:l.options.desc||'', img:l.options.imgUrl||null}); | |||
}); return all; | |||
} | |||
async function saveShared(){ | |||
const payload = JSON.stringify(collectMarkers()); | |||
const res = await fetch(SAVE_URL,{ method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ data: payload }) }); | |||
if(!res.ok) throw new Error(await res.text()); | |||
toast('Saved ✅'); | |||
} | |||
function applyMarkers(arr){ arr.forEach(m => addMarker([m.y,m.x], m.title||'Selfie', m.desc||'', m.img||null)); } | |||
// UI refs | |||
const tI=document.getElementById('m-title'), dI=document.getElementById('m-desc'), pI=document.getElementById('m-photo'), addBtn=document.getElementById('m-add'); | |||
// Client-side file checks (jpg/jpeg/png/webp ≤ 10MB) | |||
function validatePhoto(){ | |||
const f = pI.files[0]; if(!f) return true; | |||
const ok = ['image/jpeg','image/png','image/webp']; | |||
if(!ok.includes(f.type)){ alert('Only JPG/JPEG, PNG, or WEBP allowed.'); pI.value=''; return false; } | |||
if(f.size > 10*1024*1024){ alert('Image is over 10MB.'); pI.value=''; return false; } | |||
return true; | |||
} | |||
async function uploadPhotoIfAny(){ | |||
const f = pI.files[0]; if(!f) return null; | |||
const fd = new FormData(); fd.append('photo', f); | |||
const res = await fetch(UPLOAD_URL, { method:'POST', body: fd }); | |||
if(!res.ok) throw new Error(await res.text()); | |||
const j = await res.json(); return j.url; | |||
} | |||
// Add -> click map | |||
let placing=null; | |||
addBtn.onclick = () => { | |||
if(!validatePhoto()) return; | |||
placing = { title: tI.value, desc: dI.value }; | |||
addBtn.textContent='Click map…'; addBtn.disabled=true; map.getContainer().style.cursor='crosshair'; | |||
function cancel(){ placing=null; addBtn.textContent='Add to Map'; addBtn.disabled=false; map.getContainer().style.cursor=''; document.removeEventListener('keydown',esc); } | |||
function esc(e){ if(e.key==='Escape') cancel(); } document.addEventListener('keydown',esc); | |||
map.once('click', async e => { | |||
try{ | |||
const imgUrl = await uploadPhotoIfAny(); // may be null | |||
addMarker([e.latlng.lat,e.latlng.lng], placing.title, placing.desc, imgUrl); | |||
await saveShared(); | |||
}catch(err){ alert('Could not add selfie: ' + err.message); } | |||
finally{ cancel(); tI.value=''; dI.value=''; pI.value=''; } | |||
}); | |||
}; | |||
// Export PNG (hides panel for clean capture) | |||
document.getElementById('m-export').onclick = async () => { | |||
try{ | |||
const panel=document.getElementById('tv-panel'); const prev=panel.style.display; panel.style.display='none'; | |||
await new Promise(r=>setTimeout(r,50)); | |||
const canvas = await html2canvas(document.getElementById('map'), { useCORS:true, backgroundColor:null, scale:2 }); | |||
panel.style.display = prev || ''; | |||
const a=document.createElement('a'); a.href=canvas.toDataURL('image/png'); a.download='selfies-map.png'; document.body.appendChild(a); a.click(); document.body.removeChild(a); | |||
}catch(e){ alert('Export failed'); console.error(e); } | |||
}; | |||
// Initial load | |||
(async function(){ | |||
try{ | |||
const res = await fetch(DATA_URL,{cache:'no-store'}); if(!res.ok) throw new Error('HTTP '+res.status); | |||
const arr = await res.json(); selfies.clearLayers(); applyMarkers(arr); | |||
}catch(e){ console.warn('Could not load shared data:', e); } | |||
})(); | |||
}; | |||
img.src = MAP_IMAGE_URL; | |||
</script> | |||
</html> | |||