|
|
| (9 intermediate revisions by the same user not shown) |
| Line 1: |
Line 1: |
| <html>
| | Upload your in-game Treeverse selfies onto the map below. |
| <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
| |
|
| |
|
| <style>
| | Map Credit: TvdB & Jimmy |
| .mw-parser-output, .mw-body, .mw-body-content { overflow: visible !important; }
| |
|
| |
|
| .tv-wrap{position:relative}
| | <html> |
| #map{height:80vh;width:100%;border:1px solid #ddd;border-radius:10px; overflow:visible}
| | <iframe src="/map-selfie/selfies.html" style="width:100%;height:85vh;border:none"></iframe> |
| | |
| .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:320px; 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:8px 10px;border:1px solid #ccc;border-radius:6px;font-size:15px}
| |
| .tv-btn{display:block;width:100%;margin-top:8px;padding:12px;border:none;border-radius:6px;
| |
| font-weight:bold;cursor:pointer}
| |
| .btn-add{background:#27ae60;color:#fff} .btn-add:hover{background:#1f8b4d}
| |
| .btn-del{background:#bdc3c7;color:#2c3e50;transition:background .2s,color .2s}
| |
| .btn-del.on{background:#e74c3c;color:#fff}
| |
| | |
| #m-photo{padding:8px;border:1px solid #ccc;border-radius:6px;font-size:15px;background:#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}
| |
| | |
| .tv-popup .leaflet-popup-content{margin:10px 12px; width:380px !important; max-width:380px !important}
| |
| .tv-popup .leaflet-popup-content img{display:block;width:50%;height:auto;border-radius:6px;margin-top:6px}
| |
| @media (max-width:520px){
| |
| .tv-popup .leaflet-popup-content{width:72vw !important; max-width:72vw !important}
| |
| }
| |
| </style> | |
| | |
| <div class="tv-wrap">
| |
| <div id="map"></div>
| |
| | |
| <div class="tv-panel" id="tv-panel">
| |
| <h3>Upload Your Selfie</h3>
| |
| | |
| <input id="m-title" class="tv-input tv-text" placeholder="Enter Player Username *" required>
| |
| <textarea id="m-desc" class="tv-input tv-text" rows="2" placeholder="Add description"></textarea>
| |
| <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-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 on camera pin icons to view selfies.<br>
| |
| Right-click on camera pin icon on map 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>
| |
| 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 ? `<img src="${imgUrl}" alt="">` : '';
| |
| return `<b>${t||'Untitled'}</b>${d?`<div style="margin-top:6px">${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 popupOpts = { className:'tv-popup', maxWidth:600, minWidth:220,
| |
| autoPan:true, keepInView:true, autoPanPaddingTopLeft:[0,80], autoPanPaddingBottomRight:[0,40] };
| |
| | |
| const selfies = L.layerGroup().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), popupOpts);
| |
| 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)); }
| |
| | |
| const tI=document.getElementById('m-title'),
| |
| dI=document.getElementById('m-desc'),
| |
| pI=document.getElementById('m-photo'),
| |
| addBtn=document.getElementById('m-add');
| |
| | |
| function validateInputs(){
| |
| const name = tI.value.trim();
| |
| if(!name){ alert('Please enter a player username.'); tI.focus(); return false; }
| |
| const f = pI.files[0];
| |
| if(f){
| |
| 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;
| |
| }
| |
| | |
| let placing=null;
| |
| addBtn.onclick = () => {
| |
| if(!validateInputs()) 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();
| |
| 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=''; }
| |
| });
| |
| };
| |
| | |
| (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> | | </html> |