{"id":702,"date":"2025-09-10T11:01:09","date_gmt":"2025-09-10T09:01:09","guid":{"rendered":"https:\/\/modelos.castorsunglasses.es\/?page_id=702"},"modified":"2025-09-10T12:48:30","modified_gmt":"2025-09-10T10:48:30","slug":"fotos-de-productos","status":"publish","type":"page","link":"https:\/\/modelos.castorsunglasses.es\/mr\/fotos-de-productos\/","title":{"rendered":"Fotos de productos"},"content":{"rendered":"<div class=\"nbr-container\">\n            <div class=\"nbr-steps\">\n                <div class=\"nbr-step nbr-step-active\" data-step=\"1\">\n                    <div class=\"nbr-step-number\">1<\/div>\n                    <div class=\"nbr-step-title\">Modelo<\/div>\n                <\/div>\n                <div class=\"nbr-step\" data-step=\"2\">\n                    <div class=\"nbr-step-number\">2<\/div>\n                    <div class=\"nbr-step-title\">Resultados<\/div>\n                <\/div>\n            <\/div>\n\n            <!-- Paso 1: MODELO -->\n            <div class=\"nbr-panel nbr-panel-active\" id=\"nbrStep1\">\n                <div class=\"nbr-dropzone\" id=\"nbrDropzone\">\n                    <input type=\"file\" id=\"nbrFileInput\" accept=\"image\/*\" multiple style=\"position:absolute; left:-9999px;\">\n                    <p id=\"nbrDropText\">Arrastra y suelta im\u00e1genes aqu\u00ed o haz clic para seleccionar<\/p>\n                    <button class=\"nbr-btn\" id=\"nbrChooseBtn\" type=\"button\">Seleccionar im\u00e1genes<\/button>\n\n                    <div id=\"nbrPreview\" class=\"nbr-preview\"><\/div>\n\n                    <div class=\"nbr-progress-wrap\" id=\"nbrProgressWrap\" hidden>\n                        <div class=\"nbr-progress-bar\" id=\"nbrProgressBar\" style=\"width:0%;\"><\/div>\n                        <div class=\"nbr-progress-text\" id=\"nbrProgressText\">0%<\/div>\n                    <\/div>\n\n                    <p class=\"nbr-note\">* M\u00e1ximo 10 im\u00e1genes. No podr\u00e1s continuar hasta que la subida llegue al 100% (salvo que ya exista modelo precargado por ACF).<\/p>\n\n                    <div class=\"nbr-preloaded\" id=\"nbrPreloaded\" style=\"margin-top:10px; display:none;\">\n                        <strong>Modelo precargado:<\/strong>\n                        <img decoding=\"async\" id=\"nbrPreloadedImg\" src=\"\" alt=\"Modelo\" class=\"nbr-thumb\" style=\"margin-left:8px;\">\n                    <\/div>\n                <\/div>\n\n                <div class=\"nbr-actions-center\">\n                    <button class=\"nbr-btn nbr-btn-primary\" id=\"nbrContinueBtn\" type=\"button\" disabled>\u0938\u0941\u0930\u0942 \u0920\u0947\u0935\u093e<\/button>\n                <\/div>\n            <\/div>\n\n            <!-- Paso 2: SOLO RESULTADOS (prompt oculto v\u00eda ACF y 2 outputs fijos) -->\n            <div class=\"nbr-panel\" id=\"nbrStep2\">\n                <div class=\"nbr-controls-row\" style=\"justify-content:center;\">\n                    <button id=\"nbrGenerateBtn\" class=\"nbr-btn nbr-btn-primary\" type=\"button\">Generar<\/button>\n                <\/div>\n                <div id=\"nbrStatus\" class=\"nbr-status\"><\/div>\n                <div id=\"nbrResults\" class=\"nbr-results-grid\" aria-live=\"polite\"><\/div>\n\n                <div class=\"nbr-actions-row\">\n                    <button class=\"nbr-btn\" id=\"nbrBackToStep1\" type=\"button\">Volver al modelo<\/button>\n                <\/div>\n            <\/div>\n        <\/div>\n\n        <!-- Modal para Ampliar -->\n        <div class=\"nbr-modal\" id=\"nbrModal\" hidden>\n            <div class=\"nbr-modal-backdrop\" id=\"nbrModalBackdrop\"><\/div>\n            <div class=\"nbr-modal-content\">\n                <button class=\"nbr-modal-close\" id=\"nbrModalClose\" aria-label=\"Cerrar\">\u00d7<\/button>\n                <img decoding=\"async\" id=\"nbrModalImg\" src=\"\" alt=\"Vista ampliada\" \/>\n            <\/div>\n        <\/div>\n\n        <style>\n            .nbr-container { max-width: 980px; margin: 24px auto; font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; padding: 0 16px; }\n            .nbr-steps { display:flex; justify-content:center; gap:20px; margin-bottom:18px; flex-wrap:wrap; }\n            .nbr-step { display:flex; align-items:center; gap:8px; opacity:.2; transition:opacity .2s; }\n            .nbr-step-active { opacity:1; }\n            .nbr-step-number { width:28px; height:28px; border-radius:50%; background:var(--color-primary) !important; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:600; }\n            .nbr-step-title { font-weight:600; }\n            .nbr-panel { display:none; }\n            .nbr-panel-active { display:block; }\n            .nbr-dropzone { border:2px dashed #bbb; border-radius:16px; padding:24px; text-align:center; background:var(--text-color); position:relative; }\n            .nbr-dropzone.dragover { background:#eef6ff; border-color:#66a3ff; }\n            .nbr-btn { background:#222; color:#fff; border:none; padding:10px 16px; border-radius:12px; cursor:pointer; }\n            .nbr-btn-primary { background:#111; box-shadow:0 6px 20px rgba(0,0,0,.15); }\n            .nbr-btn:disabled { opacity:.5; cursor:not-allowed; }\n            .nbr-actions-center, .nbr-actions-row { display:flex; justify-content:center; gap:10px; margin-top:16px; }\n            .nbr-preview { display:flex; flex-wrap:wrap; gap:10px; margin-top:12px; justify-content:center; }\n            .nbr-thumb { width:140px; height:140px; object-fit:cover; border-radius:14px; box-shadow:0 2px 8px rgba(0,0,0,.08); }\n            .nbr-progress-wrap { margin-top:14px; position:relative; width:100%; height:14px; background:#eee; border-radius:8px; overflow:hidden; }\n            .nbr-progress-bar { height:100%; background:var(--color-primary) !important; transition:width .2s; }\n            .nbr-progress-text { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; font-size:12px; color:var(--text-color); }\n            .nbr-note { margin-top:8px; color:#666; font-size:12px; }\n            .nbr-controls-row { display:flex; align-items:center; gap:10px; justify-content:center; margin:12px 0; flex-wrap:wrap; }\n            .nbr-status { text-align:center; margin-top:8px; color:#444; min-height:22px; }\n            .nbr-results-grid { margin-top:16px; display:grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap:14px; }\n            .nbr-card { background:#fff; border:1px solid #eee; border-radius:16px; padding:10px; box-shadow:0 6px 24px rgba(0,0,0,.08); display:flex; flex-direction:column; align-items:center; }\n            .nbr-card img { width:100%; aspect-ratio:1\/1; object-fit:cover; border-radius:12px; }\n            .nbr-card-actions { display:flex; gap:8px; margin-top:10px; width:100%; }\n            .nbr-card-actions .nbr-btn { flex:1; }\n            .nbr-modal[hidden] { display:none; }\n            .nbr-modal { position:fixed; inset:0; z-index:9999; }\n            .nbr-modal-backdrop { position:absolute; inset:0; background:rgba(0,0,0,.65); }\n            .nbr-modal-content { position:absolute; inset:40px 20px; display:flex; align-items:center; justify-content:center; }\n            .nbr-modal img { max-width:90vw; max-height:80vh; border-radius:18px; box-shadow:0 20px 60px rgba(0,0,0,.45); }\n            .nbr-modal-close { position:absolute; top:14px; right:14px; width:42px; height:42px; border-radius:50%; background:#fff; border:none; font-size:24px; line-height:1; cursor:pointer; }\n            @media (prefers-color-scheme: dark) {\n                .nbr-container { color:var(--text-color); }\n                .nbr-dropzone { background:var(--body-bg-color); border-color:#333; }\n                .nbr-progress-wrap { background:#222; }\n                .nbr-card { background:#0f0f0f; border-color:#222; }\n                .nbr-select { background:#0f0f0f; color:#eaeaea; border-color:#222; }\n                .nbr-btn { background:var(--color-grey-700); }\n            }\n\n            \/* ---- PLACEHOLDERS ---- *\/\n            .nbr-results-grid.is-fixed-1 { grid-template-columns: repeat(1, 1fr) !important; }\n            .nbr-results-grid.is-fixed-2 { grid-template-columns: repeat(2, 1fr) !important; } \/* 2 resultados lado a lado *\/\n            .nbr-results-grid.is-fixed-3 { grid-template-columns: repeat(3, 1fr) !important; }\n            .nbr-results-grid.is-fixed-4 { grid-template-columns: repeat(2, 1fr) !important; } \/* 2x2 *\/\n\n            .nbr-card.placeholder {\n              position: relative;\n              align-items: center;\n              justify-content: center;\n              min-height: 220px;\n              gap: 10px;\n            }\n            .nbr-card.placeholder .nbr-loading-img {\n              width: 64px; height: 64px; object-fit: contain; opacity:.85;\n            }\n            .nbr-card.placeholder .nbr-loading-text {\n              font-weight: 600; font-size: 14px; color:#666;\n            }\n            .nbr-card.placeholder.error .nbr-loading-text { color:#b23; }\n            .nbr-card.placeholder img { display:block; }\n        <\/style>\n\n        <script>\n        (function(){\n            const ajaxUrl = \"https:\\\/\\\/modelos.castorsunglasses.es\\\/wp-admin\\\/admin-ajax.php\";\n            const nonce   = \"8498c73064\";\n            const currentPostId = 702;\n\n            const $steps = Array.from(document.querySelectorAll('.nbr-step'));\n            const $panels = {\n                1: document.getElementById('nbrStep1'),\n                2: document.getElementById('nbrStep2'),\n            };\n\n            function goStep(n){\n                $steps.forEach(s => s.classList.toggle('nbr-step-active', s.getAttribute('data-step') == n));\n                Object.values($panels).forEach(p => p.classList.remove('nbr-panel-active'));\n                $panels[n].classList.add('nbr-panel-active');\n                window.scrollTo({top: $panels[n].offsetTop - 10, behavior: 'smooth'});\n            }\n\n            \/\/ Paso 1\n            const drop = document.getElementById('nbrDropzone');\n            const input = document.getElementById('nbrFileInput');\n            const chooseBtn = document.getElementById('nbrChooseBtn');\n            const preview = document.getElementById('nbrPreview');\n            const progressWrap = document.getElementById('nbrProgressWrap');\n            const progressBar = document.getElementById('nbrProgressBar');\n            const progressText = document.getElementById('nbrProgressText');\n            const continueBtn = document.getElementById('nbrContinueBtn');\n            const preloadedWrap = document.getElementById('nbrPreloaded');\n            const preloadedImg  = document.getElementById('nbrPreloadedImg');\n\n            \/\/ Paso 2\n            const generateBtn = document.getElementById('nbrGenerateBtn');\n            const statusEl = document.getElementById('nbrStatus');\n            const resultsGrid = document.getElementById('nbrResults');\n            const backTo1 = document.getElementById('nbrBackToStep1');\n\n            \/\/ Modal\n            const modal = document.getElementById('nbrModal');\n            const modalImg = document.getElementById('nbrModalImg');\n            const modalClose = document.getElementById('nbrModalClose');\n            const modalBackdrop = document.getElementById('nbrModalBackdrop');\n\n            \/\/ Estado\n            const MAX_IMAGES = 10;\n            let uploadedUrls = [];\n            let pendingFiles = [];\n            let isUploading = false;\n            let uploadDone = false;\n            let genUrls = [];\n            const preloadedBase = \"\";\n\n            \/\/ Imagen de carga (c\u00e1mbiala por una subida a tu WP)\n            const LOADING_IMG = 'URL_DE_TU_IMAGEN_DE_CARGA.png';\n\n            function fixGridColumns(gridEl, n){\n              gridEl.classList.remove('is-fixed-1','is-fixed-2','is-fixed-3','is-fixed-4');\n              const cls = 'is-fixed-' + Math.min(4, Math.max(1, n));\n              gridEl.classList.add(cls);\n            }\n\n            function renderPlaceholders(gridEl, n){\n              fixGridColumns(gridEl, n);\n              gridEl.innerHTML = '';\n              const frag = document.createDocumentFragment();\n              for (let i=0; i<n; i++){\n                const card = document.createElement('div');\n                card.className = 'nbr-card placeholder';\n                card.dataset.slot = String(i);\n\n                const img = document.createElement('img');\n                img.className = 'nbr-loading-img';\n                img.alt = 'Cargando';\n                img.src = LOADING_IMG;\n\n                const txt = document.createElement('div');\n                txt.className = 'nbr-loading-text';\n                txt.textContent = 'Generando\u2026';\n\n                card.appendChild(img);\n                card.appendChild(txt);\n                frag.appendChild(card);\n              }\n              gridEl.appendChild(frag);\n            }\n\n            function replacePlaceholderWithResult(gridEl, url, idx){\n              const slot = gridEl.querySelector(`.nbr-card.placeholder[data-slot=\"${idx}\"]`);\n              const final = renderResultCard(url, idx);\n              if (slot) slot.replaceWith(final);\n            }\n\n            function markPlaceholderError(gridEl, idx, message){\n              const slot = gridEl.querySelector(`.nbr-card.placeholder[data-slot=\"${idx}\"]`);\n              if (slot){\n                slot.classList.add('error');\n                const t = slot.querySelector('.nbr-loading-text');\n                if (t) t.textContent = message || 'No se gener\u00f3 esta variaci\u00f3n.';\n              }\n            }\n\n            function totalSelectedCount(){\n                return uploadedUrls.length + pendingFiles.length;\n            }\n\n            \/\/ Modelo ACF precargado\n            if (preloadedBase) {\n                drop.dataset.baseUrl = preloadedBase;\n                preloadedWrap.style.display = 'block';\n                preloadedImg.src = preloadedBase;\n                continueBtn.disabled = false; \/\/ permitir continuar con solo el modelo ACF\n            }\n\n            function setProgress(pct){\n                progressWrap.hidden = false;\n                const v = Math.max(0, Math.min(100, Math.round(pct)));\n                progressBar.style.width = v + '%';\n                progressText.textContent = v + '%';\n            }\n\n            function resetProgress(){\n                progressWrap.hidden = true;\n                setProgress(0);\n            }\n\n            function renderThumbFromFile(file){\n                const reader = new FileReader();\n                reader.onload = function(e){\n                    const img = document.createElement('img');\n                    img.src = e.target.result;\n                    img.className = 'nbr-thumb';\n                    preview.appendChild(img);\n                };\n                reader.readAsDataURL(file);\n            }\n\n            function queueFiles(files){\n                const arr = Array.from(files);\n                const remaining = Math.max(0, MAX_IMAGES - totalSelectedCount());\n                const toAdd = arr.slice(0, remaining);\n                toAdd.forEach(f => {\n                    pendingFiles.push(f);\n                    renderThumbFromFile(f);\n                });\n                if (arr.length > toAdd.length){\n                    alert(`M\u00e1ximo ${MAX_IMAGES} im\u00e1genes. Se ignoraron ${arr.length - toAdd.length}.`);\n                }\n                continueBtn.disabled = (totalSelectedCount() === 0 && uploadedUrls.length === 0 && !preloadedBase);\n                return toAdd;\n            }\n\n            function uploadFiles(files){\n                if (!files || !files.length) return;\n                isUploading = true;\n                uploadDone = false;\n                continueBtn.disabled = true;\n\n                const form = new FormData();\n                form.append('action', 'nbr_upload_images');\n                form.append('_ajax_nonce', nonce);\n                Array.from(files).forEach(f => form.append('images[]', f, f.name));\n\n                const xhr = new XMLHttpRequest();\n                xhr.open('POST', ajaxUrl, true);\n\n                xhr.upload.addEventListener('progress', (e) => {\n                    if (e.lengthComputable) {\n                        const pct = (e.loaded \/ e.total) * 100;\n                        setProgress(pct);\n                    }\n                });\n\n                xhr.onreadystatechange = function(){\n                    if (xhr.readyState === 4) {\n                        isUploading = false;\n                        try {\n                            const resp = JSON.parse(xhr.responseText);\n                            if (resp && resp.success && resp.data && resp.data.urls) {\n                                uploadedUrls = uploadedUrls.concat(resp.data.urls);\n                                pendingFiles = [];\n                                setProgress(100);\n                                uploadDone = true;\n                                continueBtn.disabled = (uploadedUrls.length === 0 && !preloadedBase);\n                            } else {\n                                uploadDone = false;\n                                alert('Error subiendo im\u00e1genes: ' + (resp && resp.data && resp.data.message ? resp.data.message : 'Desconocido'));\n                                continueBtn.disabled = (uploadedUrls.length === 0 && !preloadedBase);\n                            }\n                        } catch (err) {\n                            uploadDone = false;\n                            alert('Error de red o respuesta inv\u00e1lida.');\n                            continueBtn.disabled = (uploadedUrls.length === 0 && !preloadedBase);\n                        }\n                    }\n                };\n\n                resetProgress();\n                xhr.send(form);\n            }\n\n            document.getElementById('nbrChooseBtn').addEventListener('click', (e)=>{ e.preventDefault(); input.click(); });\n            input.addEventListener('change', (e)=>{\n                const added = queueFiles(e.target.files);\n                if (added.length) uploadFiles(added);\n                e.target.value = '';\n            });\n            drop.addEventListener('dragover', (e)=>{ e.preventDefault(); drop.classList.add('dragover'); });\n            ['dragleave','dragend','drop'].forEach(ev=>{\n                drop.addEventListener(ev, (e)=>{ e.preventDefault(); drop.classList.remove('dragover'); });\n            });\n            drop.addEventListener('drop', (e)=>{\n                const dt = e.dataTransfer;\n                if (dt && dt.files && dt.files.length) {\n                    const added = queueFiles(dt.files);\n                    if (added.length) uploadFiles(added);\n                }\n            });\n\n            continueBtn.addEventListener('click', ()=>{\n                const hasBase = !!drop.dataset.baseUrl;\n                if (!hasBase && (!uploadDone || uploadedUrls.length === 0)) {\n                    alert('Debes subir al menos una imagen o disponer de un modelo precargado.');\n                    return;\n                }\n                goStep(2);\n            });\n\n            backTo1.addEventListener('click', ()=> goStep(1));\n\n            function renderResultCard(url, idx){\n                const card = document.createElement('div');\n                card.className = 'nbr-card';\n\n                const img = document.createElement('img');\n                img.src = url;\n                img.alt = 'Resultado ' + (idx+1);\n                card.appendChild(img);\n\n                const actions = document.createElement('div');\n                actions.className = 'nbr-card-actions';\n\n                const btnZoom = document.createElement('button');\n                btnZoom.className = 'nbr-btn';\n                btnZoom.type = 'button';\n                btnZoom.textContent = 'Ampliar';\n                btnZoom.addEventListener('click', ()=>{\n                    modalImg.src = url;\n                    modal.removeAttribute('hidden');\n                });\n\n                const linkDown = document.createElement('a');\n                linkDown.className = 'nbr-btn';\n                linkDown.href = url;\n                linkDown.download = 'imagen-' + (idx+1) + '.jpg';\n                linkDown.textContent = 'Descargar';\n\n                actions.appendChild(btnZoom);\n                actions.appendChild(linkDown);\n                card.appendChild(actions);\n                return card;\n            }\n\n            \/\/ GENERAR (sin prompt visible; se toma del ACF \"promt\" en el servidor)\n            generateBtn.addEventListener('click', ()=>{\n              const urls = [];\n              if (drop.dataset.baseUrl) urls.push(drop.dataset.baseUrl);\n              uploadedUrls.forEach(u => urls.push(u));\n\n              \/\/ Fijamos 2 resultados\n              const NUM_OUTPUTS = 2;\n\n              generateBtn.disabled = true;\n              statusEl.textContent = 'Generando im\u00e1genes\u2026';\n              renderPlaceholders(resultsGrid, NUM_OUTPUTS);\n\n              const body = new FormData();\n              body.append('action', 'nbr_generate');\n              body.append('_ajax_nonce', nonce);\n              body.append('post_id', String(currentPostId));\n              body.append('images', JSON.stringify(urls));\n\n              fetch(ajaxUrl, { method:'POST', body })\n                .then(r => r.json())\n                .then(data => {\n                  generateBtn.disabled = false;\n\n                  if (!data || !data.success) {\n                    statusEl.textContent = 'Error: ' + (data?.data?.message || 'No se pudo generar.');\n                    for (let i=0; i<NUM_OUTPUTS; i++) markPlaceholderError(resultsGrid, i, 'Error en la generaci\u00f3n.');\n                    return;\n                  }\n\n                  const out = Array.isArray(data.data.output_urls) ? data.data.output_urls : [];\n                  for (let i=0; i<NUM_OUTPUTS; i++){\n                    const url = out[i];\n                    if (url) replacePlaceholderWithResult(resultsGrid, url, i);\n                    else markPlaceholderError(resultsGrid, i, 'Sin salida.');\n                  }\n\n                  statusEl.textContent = 'Listo.';\n                })\n                .catch(()=>{\n                  generateBtn.disabled = false;\n                  statusEl.textContent = 'Error de red generando im\u00e1genes.';\n                  for (let i=0; i<2; i++) markPlaceholderError(resultsGrid, i, 'Error de red.');\n                });\n            });\n\n            function closeModal(){ modal.setAttribute('hidden','hidden'); modalImg.src = ''; }\n            modalClose.addEventListener('click', closeModal);\n            modalBackdrop.addEventListener('click', closeModal);\n            document.addEventListener('keydown', (e)=>{ if (e.key === 'Escape' && !modal.hasAttribute('hidden')) closeModal(); });\n        })();\n        <\/script>","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_acf_changed":false,"type":"","auto_type":false,"post":"","stream":"","stream_url":"","waveform_data":[],"duration":0,"bpm":0,"downloadable":false,"download_url":"","purchase_title":"","purchase_url":"","post-count-all":0,"like_count":0,"download_count":0,"editor_note":"","copyright":"","captions":[],"sources":[],"footnotes":""},"acf":[],"_links":{"self":[{"href":"https:\/\/modelos.castorsunglasses.es\/mr\/wp-json\/wp\/v2\/pages\/702"}],"collection":[{"href":"https:\/\/modelos.castorsunglasses.es\/mr\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/modelos.castorsunglasses.es\/mr\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/modelos.castorsunglasses.es\/mr\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/modelos.castorsunglasses.es\/mr\/wp-json\/wp\/v2\/comments?post=702"}],"version-history":[{"count":2,"href":"https:\/\/modelos.castorsunglasses.es\/mr\/wp-json\/wp\/v2\/pages\/702\/revisions"}],"predecessor-version":[{"id":726,"href":"https:\/\/modelos.castorsunglasses.es\/mr\/wp-json\/wp\/v2\/pages\/702\/revisions\/726"}],"wp:attachment":[{"href":"https:\/\/modelos.castorsunglasses.es\/mr\/wp-json\/wp\/v2\/media?parent=702"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}