{"id":177,"date":"2026-02-17T18:47:38","date_gmt":"2026-02-17T17:47:38","guid":{"rendered":"https:\/\/lesbalconsdeladrome.fr\/test-balcons\/?page_id=177"},"modified":"2026-02-17T22:19:22","modified_gmt":"2026-02-17T21:19:22","slug":"dossards","status":"publish","type":"page","link":"https:\/\/lesbalconsdeladrome.fr\/test-balcons\/dossards\/","title":{"rendered":"Dossards"},"content":{"rendered":"\n<!-- DEBUT WIDGET RECHERCHE DOSSARD -->\n<meta charset=\"UTF-8\">\n<style>\n  #search-container {\n    font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n    max-width: 1200px;\n    margin: 20px auto;\n    padding: 20px;\n    background: #f9fafb;\n    border-radius: 8px;\n    border: 1px solid #e5e7eb;\n  }\n  .search-controls { display: flex; gap: 10px; margin-bottom: 20px; align-items: center; flex-wrap: wrap; }\n  .year-select { padding: 10px; border-radius: 6px; border: 1px solid #d1d5db; background: white; }\n  .search-input { width: 200px; padding: 12px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 16px; }\n  \n  #results-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 16px; min-height: 200px; }\n  .photo-card { background: white; border-radius: 8px; overflow: hidden; border: 1px solid #eee; text-align: center; display: flex; flex-direction: column; position: relative; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }\n  \n  .img-container { display: block; width: 100%; height: 200px; overflow: hidden; position: relative; background: #eee; }\n  .img-container img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.2s; }\n  .img-container img:hover { transform: scale(1.05); }\n  \n  .photo-info { padding: 10px; flex-grow: 1; display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 8px; background: #fff; z-index: 10; }\n  .photo-name { font-size: 11px; color: #6b7280; word-break: break-all; min-height: 1.2em; display: block; }\n  \n  .edit-btn { \n    background: #fffbeb; color: #92400e; border: 2px solid #fde68a; \n    border-radius: 4px; padding: 5px 10px; font-size: 12px; cursor: pointer; \n    font-weight: bold; position: relative; z-index: 20;\n  }\n  .edit-btn:hover { background: #fef3c7; border-color: #fbbf24; }\n  .edit-btn:active { transform: translateY(1px); background: #fde68a; }\n  \n  .edit-input { width: 90%; font-size: 11px; text-align: center; padding: 4px; border: 2px solid #fbbf24; background: white; color: black; }\n  \n  .download-link {\n    display: inline-block; padding: 8px 16px; background-color: #2563eb; color: white !important;\n    text-decoration: none !important; border-radius: 4px; font-size: 13px; margin: 10px; font-weight: 600;\n  }\n  #status-msg { margin-bottom: 15px; font-weight: 500; color: #374151; }\n  #typingIndicator { font-size: 12px; color: #9ca3af; visibility: hidden; }\n<\/style>\n\n<div id=\"search-container\">\n  <h3 style=\"margin-top:0;\">Trouvez vos photos<\/h3>\n  <span style=\"font-size:14px; color:#4b5563; display:block; margin-bottom:10px;\">\n    Saisir le dossard ou \u00ab\u00a00\u00a0\u00bb pour tout voir.<br>\u00ab\u00a0S\u00a0\u00bb pour les photos sans dossard.\n  <\/span>\n  \n  <div class=\"search-controls\">\n    <select id=\"yearSelect\" class=\"year-select\" style=\"display:none;\" onchange=\"initYearSelection()\"><\/select>\n    <input type=\"text\" id=\"dossardInput\" class=\"search-input\" placeholder=\"Ex: 12, s, 0...\" oninput=\"handleTyping()\" \/>\n    <span id=\"typingIndicator\">Recherche en cours&#8230;<\/span>\n  <\/div>\n  \n  <div id=\"status-msg\">Initialisation&#8230;<\/div>\n  <div id=\"results-grid\"><\/div>\n<\/div>\n\n<script>\n  let LOCAL_SERVER_URL = \"http:\/\/127.0.0.1:5000\"; \n  let BASE_URL = \"\";\n  let allPhotos = [];\n  let isIndexLoaded = false;\n  let typingTimer;\n  let serverConnected = false;\n\n  const isLocal = window.location.hostname === \"localhost\" || \n                  window.location.hostname === \"127.0.0.1\" || \n                  window.location.protocol === \"file:\";\n\n  async function setup() {\n    const statusMsg = document.getElementById('status-msg');\n    \n    if (isLocal) {\n        statusMsg.innerHTML = `Connexion au serveur Python local...`;\n        try {\n            let resp = await tryFetch(\"http:\/\/127.0.0.1:5000\/index\");\n            if (!resp) {\n                LOCAL_SERVER_URL = \"http:\/\/localhost:5000\";\n                resp = await tryFetch(`${LOCAL_SERVER_URL}\/index`);\n            }\n            if (resp && resp.ok) {\n                allPhotos = await resp.json();\n                isIndexLoaded = true;\n                serverConnected = true;\n                statusMsg.innerHTML = \"<span style='color:green;'>\u2705 Mode \u00c9dition activ\u00e9 (Local)<\/span>\";\n            } else { throw new Error(); }\n        } catch(e) {\n            statusMsg.innerHTML = `<div style=\"color:orange; border:1px solid orange; padding:10px; border-radius:5px;\">Mode consultation (Serveur local non d\u00e9tect\u00e9).<\/div>`;\n            await loadAvailableYears();\n        }\n    } else {\n        await loadAvailableYears();\n    }\n  }\n\n  async function loadAvailableYears() {\n    const statusMsg = document.getElementById('status-msg');\n    const select = document.getElementById('yearSelect');\n    const currentYear = new Date().getFullYear();\n    let yearsFound = 0;\n\n    statusMsg.innerHTML = \"Recherche des index...\";\n\n    const possibleRoots = [\n        window.location.origin + \"\/test-balcons\", \n        window.location.origin\n    ];\n\n    const uniqueRoots = [...new Set(possibleRoots)];\n\n    \/\/ On boucle sur les ann\u00e9es\n    for (let y = currentYear; y >= 2024; y--) {\n        let foundForYear = false;\n        \n        \/\/ On teste les dossiers de mois de 01 \u00e0 12 (car ton serveur de test est en 02, prod en 06)\n        const months = [\"02\"];\n        \n        for (let root of uniqueRoots) {\n            if (foundForYear) break;\n            for (let m of months) {\n                if (foundForYear) break;\n                const testPath = `${root}\/wp-content\/uploads\/${y}\/${m}\/index_photos.json`;\n                try {\n                    const resp = await fetch(testPath, { method: 'HEAD', cache: 'no-cache' });\n                    if (resp.ok) {\n                        let opt = document.createElement('option');\n                        opt.value = y;\n                        opt.text = y;\n                        opt.dataset.path = `${root}\/wp-content\/uploads\/${y}\/${m}\/`;\n                        select.appendChild(opt);\n                        yearsFound++;\n                        foundForYear = true;\n                    }\n                } catch (e) {}\n            }\n        }\n    }\n\n    if (yearsFound > 0) {\n        select.style.display = \"inline-block\";\n        await initYearSelection();\n    } else {\n        statusMsg.innerHTML = `<span style='color:red;'>Erreur : Aucun fichier 'index_photos.json' trouv\u00e9 dans \/uploads\/YYYY\/MM\/.<\/span>`;\n    }\n  }\n\n  async function tryFetch(url) {\n      try {\n          const controller = new AbortController();\n          const id = setTimeout(() => controller.abort(), 1500);\n          const r = await fetch(url, { mode: 'cors', signal: controller.signal });\n          clearTimeout(id);\n          return r;\n      } catch (e) { return null; }\n  }\n\n  async function initYearSelection() {\n    const select = document.getElementById('yearSelect');\n    const selectedOption = select.options[select.selectedIndex];\n    if (!selectedOption) return;\n\n    BASE_URL = selectedOption.dataset.path;\n    const year = selectedOption.value;\n    const statusMsg = document.getElementById('status-msg');\n    const jsonUrl = BASE_URL + \"index_photos.json?t=\" + Date.now();\n    \n    statusMsg.innerHTML = `Chargement de l'index ${year}...`;\n    \n    try {\n        const resp = await fetch(jsonUrl);\n        if (resp.ok) {\n            allPhotos = await resp.json();\n            isIndexLoaded = true;\n            statusMsg.innerHTML = `<span style='color:green;'>Index ${year} charg\u00e9 (${allPhotos.length} photos).<\/span>`;\n            if (document.getElementById('dossardInput').value) rechercherPhotos();\n        } else {\n            statusMsg.innerHTML = `<span style='color:red;'>Erreur ${resp.status} sur le fichier JSON.<\/span>`;\n        }\n    } catch(e) {\n        statusMsg.innerHTML = `<span style='color:red;'>Erreur r\u00e9seau.<\/span>`;\n    }\n  }\n\n  function handleTyping() {\n    clearTimeout(typingTimer);\n    const input = document.getElementById('dossardInput').value.trim();\n    if (input !== \"\") {\n        document.getElementById('typingIndicator').style.visibility = \"visible\";\n        typingTimer = setTimeout(() => {\n            document.getElementById('typingIndicator').style.visibility = \"hidden\";\n            rechercherPhotos();\n        }, 300); \n    } else {\n        document.getElementById('results-grid').innerHTML = '';\n        document.getElementById('status-msg').innerHTML = \"Saisissez un num\u00e9ro pour commencer.\";\n    }\n  }\n\n  function rechercherPhotos() {\n    const queryInput = document.getElementById('dossardInput').value.trim();\n    const grid = document.getElementById('results-grid');\n    if (!isIndexLoaded || !queryInput) return;\n    \n    const lowerQuery = queryInput.toLowerCase();\n    \n    let filtered = allPhotos.filter(f => {\n        \/\/ 1. Photos retouch\u00e9es (R)\n        if (lowerQuery === 'r') return f.toUpperCase().includes('_R');\n        \n        \/\/ 2. Tout voir (0)\n        if (queryInput === \"0\") return true;\n        \n        \/\/ 3. Sans dossard (S ou n'importe quelle autre lettre) -> Cherche _E\n        if (isNaN(queryInput)) {\n            return f.toUpperCase().includes('_E');\n        }\n        \n        \/\/ 4. Recherche par dossard num\u00e9rique\n        const dossard = parseInt(queryInput, 10).toString();\n        \/\/ Regex robuste g\u00e9rant _, . et le nouveau s\u00e9parateur -\n        const regex = new RegExp(`(^|_)0*${dossard}(_|-|\\\\.)`);\n        return regex.test(f);\n    });\n\n    document.getElementById('status-msg').innerHTML = `<b>${filtered.length}<\/b> photo(s) trouv\u00e9e(s).`;\n    grid.innerHTML = '';\n\n    filtered.forEach(filename => {\n        const url = (isLocal && serverConnected ? \"\" : BASE_URL) + filename;\n        const displayUrl = isLocal && serverConnected ? `${filename}?t=${Date.now()}` : url;\n        \n        const card = document.createElement('div');\n        card.className = 'photo-card';\n        card.innerHTML = `\n            <a href=\"${url}\" target=\"_blank\" class=\"img-container\">\n                <img decoding=\"async\" src=\"${displayUrl}\" loading=\"lazy\" onerror=\"this.src='data:image\/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZWVlIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtc2l6ZT0iMTQiIGZpbGw9IiM5OTkiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj5JbWFnZSBpbnRyb3V2YWJsZTwvdGV4dD48L3N2Zz4='\">\n            <\/a>\n            <div class=\"photo-info\">\n                <span class=\"photo-name\">${filename}<\/span>\n            <\/div>\n            <a href=\"${url}\" download=\"${filename}\" class=\"download-link\">T\u00e9l\u00e9charger<\/a>\n        `;\n        \n        const infoDiv = card.querySelector('.photo-info');\n        const nameSpan = infoDiv.querySelector('.photo-name');\n\n        if (isLocal && serverConnected) {\n            const btn = document.createElement('button');\n            btn.className = 'edit-btn';\n            btn.innerText = '\u270e \u00c9diter le nom';\n            btn.onclick = (e) => {\n                e.preventDefault();\n                editerNom(nameSpan, filename, btn);\n            };\n            infoDiv.appendChild(btn);\n        }\n        grid.appendChild(card);\n    });\n  }\n\n  async function editerNom(container, oldName, btn) {\n    if (container.querySelector('input')) return;\n    const input = document.createElement('input');\n    input.className = \"edit-input\";\n    input.value = oldName;\n    const originalText = container.innerText;\n    container.innerHTML = '';\n    container.appendChild(input);\n    btn.style.display = 'none';\n    input.focus();\n    input.select();\n\n    const terminerEdition = async () => {\n        const newName = input.value.trim();\n        if (newName && newName !== oldName) {\n            try {\n                const resp = await fetch(`${LOCAL_SERVER_URL}\/rename`, {\n                    method: 'POST',\n                    headers: {'Content-Type': 'application\/json'},\n                    body: JSON.stringify({oldName, newName})\n                });\n                if (resp.ok) {\n                    const idx = allPhotos.indexOf(oldName);\n                    if (idx !== -1) allPhotos[idx] = newName;\n                    rechercherPhotos();\n                } else {\n                    const err = await resp.json();\n                    alert(\"Erreur : \" + err.message);\n                    container.innerText = originalText;\n                }\n            } catch(e) { \n                alert(\"Erreur de connexion serveur\"); \n                container.innerText = originalText;\n            }\n        } else { container.innerText = originalText; }\n        btn.style.display = 'inline-block';\n    };\n\n    input.onblur = terminerEdition;\n    input.onkeydown = (e) => { \n        if(e.key === \"Enter\") { e.preventDefault(); input.blur(); }\n        if(e.key === \"Escape\") { input.value = oldName; input.blur(); } \n    };\n  }\n\n  document.addEventListener('DOMContentLoaded', setup);\n<\/script>\n<!-- FIN WIDGET RECHERCHE DOSSARD -->\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Trouvez vos photos Saisir le dossard ou \u00ab\u00a00\u00a0\u00bb pour tout voir.\u00ab\u00a0S\u00a0\u00bb pour les photos sans dossard. Recherche en cours&#8230; Initialisation&#8230;<\/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-177","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/lesbalconsdeladrome.fr\/test-balcons\/wp-json\/wp\/v2\/pages\/177","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/lesbalconsdeladrome.fr\/test-balcons\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/lesbalconsdeladrome.fr\/test-balcons\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/lesbalconsdeladrome.fr\/test-balcons\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/lesbalconsdeladrome.fr\/test-balcons\/wp-json\/wp\/v2\/comments?post=177"}],"version-history":[{"count":26,"href":"https:\/\/lesbalconsdeladrome.fr\/test-balcons\/wp-json\/wp\/v2\/pages\/177\/revisions"}],"predecessor-version":[{"id":1028,"href":"https:\/\/lesbalconsdeladrome.fr\/test-balcons\/wp-json\/wp\/v2\/pages\/177\/revisions\/1028"}],"wp:attachment":[{"href":"https:\/\/lesbalconsdeladrome.fr\/test-balcons\/wp-json\/wp\/v2\/media?parent=177"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}