{"id":1136,"date":"2025-12-30T17:56:31","date_gmt":"2025-12-30T16:56:31","guid":{"rendered":"https:\/\/lesbalconsdeladrome.fr\/archives\/?post_type=jetpack-portfolio&#038;p=1136"},"modified":"2026-03-22T17:57:19","modified_gmt":"2026-03-22T16:57:19","slug":"trail-2025","status":"publish","type":"jetpack-portfolio","link":"https:\/\/lesbalconsdeladrome.fr\/archives\/portfolio\/trail-2025\/","title":{"rendered":"Trails 2025"},"content":{"rendered":"\n<p class=\"has-text-align-center has-medium-font-size wp-block-paragraph\"><strong>Trails 2025<\/strong><\/p>\n\n\n\n<!-- DEBUT WIDGET RECHERCHE DOSSARD -->\n<meta charset=\"UTF-8\">\n<style>\n    body {\n        background: #000;\n        color: #fff;\n    }\n\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: #000;\n        border-radius: 8px;\n        border: 1px solid #333;\n    }\n\n    .search-controls {\n        display: flex;\n        gap: 10px;\n        margin-bottom: 20px;\n        align-items: center;\n        flex-wrap: wrap;\n    }\n\n    .year-select {\n        padding: 10px;\n        border-radius: 6px;\n        border: 1px solid #555;\n        background: #111;\n        color: #fff;\n    }\n\n    .search-input {\n        width: 200px;\n        padding: 12px;\n        border: 1px solid #555;\n        border-radius: 6px;\n        font-size: 16px;\n        background: #111;\n        color: #fff;\n    }\n\n    #results-grid {\n        display: grid;\n        grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));\n        gap: 16px;\n        min-height: 200px;\n    }\n\n    .photo-card {\n        background: #111;\n        border-radius: 8px;\n        overflow: hidden;\n        border: 1px solid #333;\n        text-align: center;\n        display: flex;\n        flex-direction: column;\n        position: relative;\n        box-shadow: 0 1px 3px rgba(255, 255, 255, 0.08);\n    }\n\n    .img-container {\n        display: block;\n        width: 100%;\n        aspect-ratio: 20 \/ 27;\n        overflow: hidden;\n        position: relative;\n        background: #111;\n    }\n\n    .img-container img {\n        width: 100%;\n        height: 100%;\n        object-fit: cover;\n        transition: transform 0.2s;\n    }\n\n    .img-container img:hover {\n        transform: scale(1.05);\n    }\n\n    .photo-info {\n        padding: 10px;\n        flex-grow: 1;\n        display: flex;\n        flex-direction: column;\n        justify-content: center;\n        align-items: center;\n        gap: 8px;\n        background: #111;\n        z-index: 10;\n    }\n\n    .photo-info:empty {\n        display: none;\n    }\n\n    .photo-name {\n        font-size: 11px;\n        color: #fff;\n        word-break: break-all;\n        min-height: 1.2em;\n        display: block;\n    }\n\n    .edit-btn {\n        background: #fffbeb;\n        color: #92400e;\n        border: 2px solid #fde68a;\n        border-radius: 4px;\n        padding: 5px 10px;\n        font-size: 12px;\n        cursor: pointer;\n        font-weight: bold;\n        position: relative;\n        z-index: 20;\n    }\n\n    .edit-btn:hover {\n        background: #fef3c7;\n        border-color: #fbbf24;\n    }\n\n    .edit-btn:active {\n        transform: translateY(1px);\n        background: #fde68a;\n    }\n\n    .edit-input {\n        width: 90%;\n        font-size: 11px;\n        text-align: center;\n        padding: 4px;\n        border: 2px solid #fbbf24;\n        background: #111;\n        color: #fff;\n    }\n\n    .download-link {\n        display: inline-block;\n        padding: 8px 16px;\n        background-color: #2563eb;\n        color: white !important;\n        text-decoration: none !important;\n        border-radius: 4px;\n        font-size: 13px;\n        margin: 10px;\n        font-weight: 600;\n    }\n\n    #status-msg {\n        margin-bottom: 15px;\n        font-weight: 500;\n        color: #fff;\n    }\n\n    #typingIndicator {\n        font-size: 12px;\n        color: #d1d5db;\n        visibility: hidden;\n    }\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:#fff; display:block; margin-bottom:10px;\">\n        Saisir le dossard, \u00ab\u00a00\u00a0\u00bb ou une lettre pour tout voir.<br>\u00ab\u00a0000\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, a, 0, 000...\"\n            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 = \"\";\n    let BASE_URL = \"\";\n    let allPhotos = [];\n    let isIndexLoaded = false;\n    let typingTimer;\n    let serverConnected = false;\n\n    const LOCAL_SERVER_CANDIDATE_PORTS = [5000, 5001, 5002, 5003, 5004, 5005];\n\n    \/\/ Configuration WordPress visible pour l'op\u00e9rateur.\n    \/\/ Adaptez facilement ces valeurs si l'index JSON change de dossier sur le site.\n    const WORDPRESS_INDEX_YEAR = \"2026\";\n    const WORDPRESS_INDEX_MONTH = \"03\";\n    const WORDPRESS_INDEX_FILENAME = \"index_photos.json\";\n    const WORDPRESS_ROOT_CANDIDATES = [\n        window.location.origin + \"\/archives\", \/\/Sous-site du site principal\n        window.location.origin,\n    ];\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 connectToLocalServer(candidateUrls) {\n        for (const candidateUrl of candidateUrls) {\n            try {\n                const resp = await tryFetch(`${candidateUrl}\/index`);\n                if (resp && resp.ok) {\n                    allPhotos = await resp.json();\n                    isIndexLoaded = true;\n                    serverConnected = true;\n                    LOCAL_SERVER_URL = candidateUrl;\n                    return true;\n                }\n            } catch (e) { }\n        }\n        return false;\n    }\n\n    async function setup() {\n        const statusMsg = document.getElementById('status-msg');\n        const runtimeOrigin = window.location.origin || \"\";\n        const candidateUrls = [];\n\n        if (window.location.hostname === \"localhost\" || window.location.hostname === \"127.0.0.1\") {\n            candidateUrls.push(runtimeOrigin);\n        }\n\n        for (const port of LOCAL_SERVER_CANDIDATE_PORTS) {\n            const candidate = `http:\/\/127.0.0.1:${port}`;\n            if (!candidateUrls.includes(candidate)) {\n                candidateUrls.push(candidate);\n            }\n        }\n\n        \/\/ Si on est servi par un serveur local, on tente d'abord l'origine courante.\n        if (window.location.hostname === \"localhost\" || window.location.hostname === \"127.0.0.1\") {\n            statusMsg.innerHTML = \"Connexion au service local...\";\n            if (await connectToLocalServer(candidateUrls)) {\n                statusMsg.innerHTML = \"<span style='color:green;'>\u2705 Mode \u00c9dition activ\u00e9 (Serveur Local)<\/span>\";\n                return;\n            }\n        }\n\n        if (isLocal) {\n            statusMsg.innerHTML = `Connexion au serveur Python local...`;\n            if (await connectToLocalServer(candidateUrls)) {\n                statusMsg.innerHTML = \"<span style='color:green;'>\u2705 Mode \u00c9dition activ\u00e9 (Local)<\/span>\";\n            } else {\n                statusMsg.innerHTML = `<div style=\"color:orange; border:1px solid orange; padding:10px; border-radius:5px;\">Mode consultation (Serveur 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\n        statusMsg.innerHTML = \"Recherche de l'index WordPress configur\u00e9...\";\n\n        const uniqueRoots = [...new Set(WORDPRESS_ROOT_CANDIDATES)];\n        const configuredYear = WORDPRESS_INDEX_YEAR;\n        const configuredMonth = WORDPRESS_INDEX_MONTH;\n\n        select.innerHTML = \"\";\n\n        for (const root of uniqueRoots) {\n            const testPath = `${root}\/wp-content\/uploads\/${configuredYear}\/${configuredMonth}\/${WORDPRESS_INDEX_FILENAME}`;\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 = configuredYear;\n                    opt.text = configuredYear;\n                    opt.dataset.path = `${root}\/wp-content\/uploads\/${configuredYear}\/${configuredMonth}\/`;\n                    select.appendChild(opt);\n                    await initYearSelection();\n                    return;\n                }\n            } catch (e) { }\n        }\n\n        statusMsg.innerHTML = `<span style='color:red;'>Erreur : aucun fichier '${WORDPRESS_INDEX_FILENAME}' trouv\u00e9 \u00e0 l'adresse configur\u00e9e \/uploads\/${configuredYear}\/${configuredMonth}\/.<\/span>`;\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 + WORDPRESS_INDEX_FILENAME + \"?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}\/${WORDPRESS_INDEX_MONTH} 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 = \"\";\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        let filtered = allPhotos.filter(f => {\n            \/\/ 1. Tout voir (0)\n            if (queryInput === \"0\") return true;\n\n            \/\/ 2. Toute saisie contenant au moins une lettre affiche toutes les images.\n            if (isNaN(queryInput)) {\n                return true;\n            }\n\n            \/\/ 3. 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            const isLocalEditMode = isLocal && serverConnected;\n            const showFilename = isLocalEditMode;\n            const photoInfoMarkup = showFilename ? `\n            <div class=\"photo-info\">\n                <span class=\"photo-name\">${filename}<\/span>\n            <\/div>` : '';\n            const downloadMarkup = isLocalEditMode\n                ? `<a href=\"${url}\" download=\"${filename}\" class=\"download-link\">T\u00e9l\u00e9charger<\/a>`\n                : '';\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            ${photoInfoMarkup}\n            ${downloadMarkup}\n        `;\n\n            if (isLocal && serverConnected) {\n                const infoDiv = card.querySelector('.photo-info');\n                const nameSpan = infoDiv.querySelector('.photo-name');\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\">&nbsp;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Trails 2025 Trouvez vos photos Saisir le dossard, \u00ab\u00a00\u00a0\u00bb ou une lettre pour tout voir.\u00ab\u00a0000\u00a0\u00bb pour les photos sans dossard. [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","template":"","format":"gallery","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"jetpack-portfolio-type":[17],"jetpack-portfolio-tag":[19],"class_list":["post-1136","jetpack-portfolio","type-jetpack-portfolio","status-publish","format-gallery","hentry","post_format-post-format-gallery","jetpack-portfolio-type-trail","jetpack-portfolio-tag-19"],"jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/lesbalconsdeladrome.fr\/archives\/wp-json\/wp\/v2\/jetpack-portfolio\/1136","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/lesbalconsdeladrome.fr\/archives\/wp-json\/wp\/v2\/jetpack-portfolio"}],"about":[{"href":"https:\/\/lesbalconsdeladrome.fr\/archives\/wp-json\/wp\/v2\/types\/jetpack-portfolio"}],"author":[{"embeddable":true,"href":"https:\/\/lesbalconsdeladrome.fr\/archives\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/lesbalconsdeladrome.fr\/archives\/wp-json\/wp\/v2\/comments?post=1136"}],"version-history":[{"count":13,"href":"https:\/\/lesbalconsdeladrome.fr\/archives\/wp-json\/wp\/v2\/jetpack-portfolio\/1136\/revisions"}],"predecessor-version":[{"id":9509,"href":"https:\/\/lesbalconsdeladrome.fr\/archives\/wp-json\/wp\/v2\/jetpack-portfolio\/1136\/revisions\/9509"}],"wp:attachment":[{"href":"https:\/\/lesbalconsdeladrome.fr\/archives\/wp-json\/wp\/v2\/media?parent=1136"}],"wp:term":[{"taxonomy":"jetpack-portfolio-type","embeddable":true,"href":"https:\/\/lesbalconsdeladrome.fr\/archives\/wp-json\/wp\/v2\/jetpack-portfolio-type?post=1136"},{"taxonomy":"jetpack-portfolio-tag","embeddable":true,"href":"https:\/\/lesbalconsdeladrome.fr\/archives\/wp-json\/wp\/v2\/jetpack-portfolio-tag?post=1136"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}