{"id":137,"date":"2026-08-14T18:35:53","date_gmt":"2026-08-14T18:35:53","guid":{"rendered":"http:\/\/kokes71.com\/?page_id=137"},"modified":"2026-08-14T18:35:53","modified_gmt":"2026-08-14T18:35:53","slug":"fondue-test","status":"publish","type":"page","link":"https:\/\/kokes71.com\/index.php\/fondue-test\/","title":{"rendered":"fondue-test"},"content":{"rendered":"\n<!DOCTYPE html>\n<html lang=\"fr\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, viewer-fit=cover\">\n<title>Fondue Radar<\/title>\n<link rel=\"stylesheet\" href=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/leaflet\/1.9.4\/leaflet.min.css\">\n<link rel=\"preconnect\" href=\"https:\/\/fonts.googleapis.com\">\n<link href=\"https:\/\/fonts.googleapis.com\/css2?family=Fraunces:opsz,wght@9..144,500;9..144,700;9..144,900&#038;family=Space+Grotesk:wght@400;500;700&#038;display=swap\" rel=\"stylesheet\">\n<style>\n  :root{\n    --cheese: #E8A93B;\n    --amber: #C97A2B;\n    --wine: #6B1E23;\n    --cream: #F7EDE0;\n    --choco: #2B1A14;\n    --night: #1B2A3A;\n  }\n  *{box-sizing:border-box; -webkit-tap-highlight-color: transparent;}\n  html,body{margin:0; height:100%; overflow:hidden; font-family:'Space Grotesk', sans-serif; background:var(--choco); color:var(--cream);}\n \n  #app{position:relative; width:100vw; height:100vh; height:100dvh;}\n \n  \/* Map *\/\n  #map{position:absolute; inset:0; z-index:0; filter: sepia(8%) saturate(85%);}\n  .leaflet-control-attribution{font-size:9px; opacity:0.6;}\n \n  \/* Header *\/\n  header{\n    position:absolute; top:0; left:0; right:0; z-index:10;\n    padding: env(safe-area-inset-top, 14px) 16px 10px;\n    background: linear-gradient(180deg, rgba(27,42,58,0.92) 0%, rgba(27,42,58,0.55) 70%, rgba(27,42,58,0) 100%);\n    pointer-events:none;\n  }\n  header h1{\n    font-family:'Fraunces', serif; font-weight:900; font-size:22px; margin:0;\n    color:var(--cheese); letter-spacing:0.2px;\n    text-shadow: 0 2px 10px rgba(0,0,0,0.4);\n  }\n  header p{margin:2px 0 0; font-size:12px; color:var(--cream); opacity:0.75;}\n \n  \/* Pot counter card *\/\n  #pot{\n    position:absolute; top: calc(env(safe-area-inset-top, 14px) + 54px); right:12px; z-index:10;\n    width:118px;\n    background: radial-gradient(circle at 30% 20%, #7a2a30, var(--wine) 65%);\n    border-radius: 16px;\n    padding:10px 8px 12px;\n    text-align:center;\n    box-shadow: 0 6px 18px rgba(0,0,0,0.45), inset 0 0 0 1px rgba(232,169,59,0.35);\n  }\n  #pot .flame{font-size:16px; display:block; margin-bottom:2px; animation: flicker 1.6s infinite ease-in-out;}\n  @keyframes flicker{0%,100%{transform:scale(1) rotate(-2deg);} 50%{transform:scale(1.12) rotate(2deg);}}\n  #pot .label{font-size:9px; text-transform:uppercase; letter-spacing:0.08em; color:var(--cheese); opacity:0.85;}\n  #pot .count{font-family:'Fraunces', serif; font-weight:900; font-size:30px; line-height:1; color:var(--cream); margin:2px 0;}\n  #pot .record{font-size:10px; color:var(--cheese); opacity:0.9; margin-top:4px; border-top:1px solid rgba(232,169,59,0.3); padding-top:5px;}\n  #pot .record b{color:var(--cream);}\n \n  \/* Bottom action bar *\/\n  #actionbar{\n    position:absolute; left:0; right:0; bottom:0; z-index:10;\n    padding: 14px 16px calc(env(safe-area-inset-bottom, 14px) + 14px);\n    background: linear-gradient(0deg, rgba(27,42,58,0.95) 30%, rgba(27,42,58,0) 100%);\n  }\n  #statusText{font-size:12px; text-align:center; margin:0 0 10px; opacity:0.85; min-height:14px;}\n  #mainBtn{\n    display:flex; align-items:center; justify-content:center; gap:8px;\n    width:100%; border:none; border-radius:999px; padding:15px;\n    font-family:'Space Grotesk', sans-serif; font-weight:700; font-size:15.5px;\n    background: linear-gradient(135deg, var(--cheese), var(--amber));\n    color: var(--choco);\n    box-shadow: 0 8px 20px rgba(201,122,43,0.4);\n    transition: transform 0.15s ease;\n  }\n  #mainBtn:active{transform: scale(0.97);}\n  #mainBtn.active{\n    background: linear-gradient(135deg, #4a5f6f, var(--night));\n    color: var(--cream);\n    box-shadow: 0 8px 20px rgba(0,0,0,0.4);\n  }\n  #mainBtn:disabled{opacity:0.5;}\n \n  \/* Marker popup *\/\n  .fondue-marker{\n    font-size:26px;\n    filter: drop-shadow(0 3px 4px rgba(0,0,0,0.5));\n    animation: bob 2.4s infinite ease-in-out;\n  }\n  @keyframes bob{0%,100%{transform:translateY(0);} 50%{transform:translateY(-4px);}}\n  .leaflet-popup-content-wrapper{background:var(--cream); color:var(--choco); border-radius:12px;}\n  .leaflet-popup-content{font-family:'Space Grotesk', sans-serif; font-size:12.5px; margin:8px 10px;}\n  .leaflet-popup-tip{background:var(--cream);}\n \n  #nameModal{\n    position:absolute; inset:0; z-index:30; display:none;\n    background:rgba(27,42,58,0.85); align-items:center; justify-content:center; padding:24px;\n  }\n  #nameModal.show{display:flex;}\n  #nameCard{\n    background:var(--cream); color:var(--choco); border-radius:18px; padding:22px 20px;\n    width:100%; max-width:320px; text-align:center;\n  }\n  #nameCard h2{font-family:'Fraunces', serif; font-weight:700; font-size:19px; margin:0 0 6px;}\n  #nameCard p{font-size:12.5px; opacity:0.75; margin:0 0 14px;}\n  #nameCard input{\n    width:100%; padding:11px 12px; border-radius:10px; border:1.5px solid #ddd0bd;\n    font-size:14px; font-family:inherit; margin-bottom:12px;\n  }\n  #nameCard .row{display:flex; gap:8px;}\n  #nameCard button{\n    flex:1; border:none; border-radius:10px; padding:12px; font-weight:700; font-size:13.5px;\n    font-family:inherit; cursor:pointer;\n  }\n  #confirmBtn{background:linear-gradient(135deg, var(--cheese), var(--amber)); color:var(--choco);}\n  #skipBtn{background:#eee2cf; color:var(--choco);}\n<\/style>\n<\/head>\n<body>\n<div id=\"app\">\n  <div id=\"map\"><\/div>\n \n  <header>\n    <h1>\ud83e\uded5 Fondue Radar<\/h1>\n    <p>Qui mange une fondue dans le monde, l\u00e0, maintenant ?<\/p>\n  <\/header>\n \n  <div id=\"pot\">\n    <span class=\"flame\">\ud83d\udd25<\/span>\n    <div class=\"label\">En ce moment<\/div>\n    <div class=\"count\" id=\"liveCount\">\u2013<\/div>\n    <div class=\"record\">Record : <b id=\"recordCount\">\u2013<\/b><\/div>\n  <\/div>\n \n  <div id=\"actionbar\">\n    <p id=\"statusText\"><\/p>\n    <button id=\"mainBtn\">\ud83e\uded5 Je mange une fondue<\/button>\n  <\/div>\n \n  <div id=\"nameModal\">\n    <div id=\"nameCard\">\n      <h2>Petit d\u00e9tail<\/h2>\n      <p>Un pseudo \u00e0 afficher sur la carte ? (facultatif)<\/p>\n      <input id=\"nameInput\" type=\"text\" maxlength=\"20\" placeholder=\"Ex. Marie, ou laisse vide\">\n      <div class=\"row\">\n        <button id=\"skipBtn\">Sans pseudo<\/button>\n        <button id=\"confirmBtn\">Confirmer \ud83e\uded5<\/button>\n      <\/div>\n    <\/div>\n  <\/div>\n<\/div>\n \n<script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/leaflet\/1.9.4\/leaflet.min.js\"><\/script>\n<script src=\"https:\/\/www.gstatic.com\/firebasejs\/10.14.1\/firebase-app-compat.js\"><\/script>\n<script src=\"https:\/\/www.gstatic.com\/firebasejs\/10.14.1\/firebase-database-compat.js\"><\/script>\n<script>\n\/\/ \u26a0\ufe0f COLLE ICI ta configuration Firebase (copi\u00e9e depuis la console Firebase)\nconst firebaseConfig = {\n  apiKey: \"AIzaSyCirMYrfFYPsK7HIAsfknxoDxOZEkT20aY\",\n  authDomain: \"fondue-radar.firebaseapp.com\",\n  databaseURL: \"https:\/\/fondue-radar-default-rtdb.firebaseio.com\",\n  projectId: \"fondue-radar\",\n  storageBucket: \"fondue-radar.firebasestorage.app\",\n  messagingSenderId: \"337505442066\",\n  appId: \"1:337505442066:web:d430e25861dd6f63cb7bcb\"\n};\nfirebase.initializeApp(firebaseConfig);\nconst db = firebase.database();\n \n(async function(){\n  const EXPIRY_MS = 3 * 60 * 60 * 1000; \/\/ une \"session fondue\" dure 3h max\n  const myId = 'me-' + Math.random().toString(36).slice(2, 11);\n  let myEntry = null; \/\/ {lat,lng,name,ts} if active\n  let myMarker = null;\n  const markers = {};\n \n  const map = L.map('map', { zoomControl:false, attributionControl:true }).setView([30, 10], 2);\n  L.control.zoom({ position:'bottomleft' }).addTo(map);\n  L.tileLayer('https:\/\/{s}.tile.openstreetmap.org\/{z}\/{x}\/{y}.png', {\n    maxZoom: 18,\n    attribution: '&copy; OpenStreetMap'\n  }).addTo(map);\n \n  const fondueIcon = L.divIcon({\n    className:'', html:'<div class=\"fondue-marker\">\ud83e\uded5<\/div>',\n    iconSize:[30,30], iconAnchor:[15,20], popupAnchor:[0,-18]\n  });\n \n  const mainBtn = document.getElementById('mainBtn');\n  const statusText = document.getElementById('statusText');\n  const liveCountEl = document.getElementById('liveCount');\n  const recordCountEl = document.getElementById('recordCount');\n  const modal = document.getElementById('nameModal');\n  const nameInput = document.getElementById('nameInput');\n \n  function timeAgo(ts){\n    const min = Math.max(0, Math.floor((Date.now()-ts)\/60000));\n    if(min < 1) return \"\u00e0 l'instant\";\n    if(min < 60) return `depuis ${min} min`;\n    return `depuis ${Math.floor(min\/60)} h`;\n  }\n \n  function getPosition(){\n    return new Promise((resolve, reject) => {\n      if(!navigator.geolocation){ reject(new Error('no-geo')); return; }\n      navigator.geolocation.getCurrentPosition(\n        pos => resolve({lat: pos.coords.latitude, lng: pos.coords.longitude}),\n        err => reject(err),\n        { enableHighAccuracy:false, timeout:8000 }\n      );\n    });\n  }\n \n  async function saveRecordIfBeaten(current){\n    try{\n      await db.ref('fondue\/record').transaction(curr => {\n        curr = curr || 0;\n        return current > curr ? current : curr;\n      });\n      const snap = await db.ref('fondue\/record').get();\n      return snap.exists() ? snap.val() : current;\n    }catch(e){ return current; }\n  }\n \n  async function fetchActiveEntries(){\n    const now = Date.now();\n    const entries = [];\n    try{\n      const snap = await db.ref('fondue\/eaters').get();\n      if(snap.exists()){\n        const all = snap.val();\n        for(const key in all){\n          const data = all[key];\n          if(now - data.ts < EXPIRY_MS){\n            entries.push({ key, ...data });\n          } else {\n            db.ref('fondue\/eaters\/' + key).remove().catch(()=>{});\n          }\n        }\n      }\n    }catch(e){ \/* Firebase indisponible ou config manquante *\/ }\n    return entries;\n  }\n \n  function renderMarkers(entries){\n    const seen = new Set();\n    entries.forEach(e => {\n      seen.add(e.key);\n      const popupHtml = `<b>${e.name ? escapeHtml(e.name) : \"Quelqu'un\"}<\/b> mange une fondue<br><span style=\"opacity:0.65\">${timeAgo(e.ts)}<\/span>`;\n      if(markers[e.key]){\n        markers[e.key].setLatLng([e.lat, e.lng]);\n        markers[e.key].setPopupContent(popupHtml);\n      } else {\n        markers[e.key] = L.marker([e.lat, e.lng], {icon: fondueIcon})\n          .addTo(map)\n          .bindPopup(popupHtml);\n      }\n    });\n    Object.keys(markers).forEach(k => {\n      if(!seen.has(k)){ map.removeLayer(markers[k]); delete markers[k]; }\n    });\n  }\n \n  function escapeHtml(s){\n    return s.replace(\/[&<>\"']\/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c]));\n  }\n \n  async function refresh(){\n    const entries = await fetchActiveEntries();\n    renderMarkers(entries);\n    liveCountEl.textContent = entries.length;\n    const record = await saveRecordIfBeaten(entries.length);\n    recordCountEl.textContent = record;\n  }\n \n  function setButtonState(active){\n    mainBtn.classList.toggle('active', active);\n    mainBtn.textContent = active ? '\u2705 Terminer ma fondue' : '\ud83e\uded5 Je mange une fondue';\n  }\n \n  async function startFondue(name){\n    mainBtn.disabled = true;\n    statusText.textContent = 'Localisation en cours\u2026';\n    try{\n      const pos = await getPosition();\n      const data = { lat: pos.lat, lng: pos.lng, name: name || '', ts: Date.now() };\n      await db.ref('fondue\/eaters\/' + myId).set(data);\n      myEntry = { key: myId, ...data };\n      setButtonState(true);\n      statusText.textContent = 'Bon app\u00e9tit ! Ton statut expire apr\u00e8s 3h.';\n      map.setView([pos.lat, pos.lng], 6, { animate:true });\n      await refresh();\n    }catch(e){\n      statusText.textContent = \"Impossible d'obtenir ta position. V\u00e9rifie l'autorisation de localisation.\";\n    }finally{\n      mainBtn.disabled = false;\n    }\n  }\n \n  async function stopFondue(){\n    if(!myEntry) return;\n    mainBtn.disabled = true;\n    try{\n      await db.ref('fondue\/eaters\/' + myEntry.key).remove();\n    }catch(e){}\n    myEntry = null;\n    setButtonState(false);\n    statusText.textContent = \"\u00c0 la prochaine fondue !\";\n    mainBtn.disabled = false;\n    await refresh();\n  }\n \n  mainBtn.addEventListener('click', () => {\n    if(myEntry){ stopFondue(); }\n    else { modal.classList.add('show'); nameInput.focus(); }\n  });\n \n  document.getElementById('confirmBtn').addEventListener('click', () => {\n    modal.classList.remove('show');\n    startFondue(nameInput.value.trim());\n  });\n  document.getElementById('skipBtn').addEventListener('click', () => {\n    modal.classList.remove('show');\n    startFondue('');\n  });\n \n  await refresh();\n  setInterval(refresh, 10000);\n})();\n<\/script>\n<\/body>\n<\/html>\n \n","protected":false},"excerpt":{"rendered":"<p>Fondue Radar \ud83e\uded5 Fondue Radar Qui mange une fondue dans le monde, l\u00e0,&hellip;<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-137","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/kokes71.com\/index.php\/wp-json\/wp\/v2\/pages\/137","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/kokes71.com\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/kokes71.com\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/kokes71.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/kokes71.com\/index.php\/wp-json\/wp\/v2\/comments?post=137"}],"version-history":[{"count":1,"href":"https:\/\/kokes71.com\/index.php\/wp-json\/wp\/v2\/pages\/137\/revisions"}],"predecessor-version":[{"id":139,"href":"https:\/\/kokes71.com\/index.php\/wp-json\/wp\/v2\/pages\/137\/revisions\/139"}],"wp:attachment":[{"href":"https:\/\/kokes71.com\/index.php\/wp-json\/wp\/v2\/media?parent=137"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}