{"id":3443,"date":"2025-11-10T12:12:34","date_gmt":"2025-11-10T12:12:34","guid":{"rendered":"https:\/\/dev.sticker4u.dk\/?page_id=3443"},"modified":"2026-04-09T13:56:05","modified_gmt":"2026-04-09T13:56:05","slug":"file-upload","status":"publish","type":"page","link":"https:\/\/dev.sticker4u.dk\/en\/file-upload\/","title":{"rendered":"File Upload"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"3443\" class=\"elementor elementor-3443\" data-elementor-post-type=\"page\">\n\t\t\t\t<div class=\"elementor-element elementor-element-2f44689 e-con-full e-flex e-con e-parent\" data-id=\"2f44689\" data-element_type=\"container\">\n\t\t\t\t<div class=\"elementor-element elementor-element-0de30c4 elementor-widget elementor-widget-heading\" data-id=\"0de30c4\" data-element_type=\"widget\" data-widget_type=\"heading.default\">\n\t\t\t\t\t<h2 class=\"elementor-heading-title elementor-size-default\">Review Your Order &amp; Upload Your Design<\/h2>\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-16a7e09 elementor-widget elementor-widget-text-editor\" data-id=\"16a7e09\" data-element_type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t\t\t\t\t\t<p>Confirm your details on the left and upload your design on the right to complete your print setup.<\/p>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-2d3792e e-con-full cart-wrapper e-flex e-con e-parent\" data-id=\"2d3792e\" data-element_type=\"container\">\n\t\t<div class=\"elementor-element elementor-element-c8b40b0 e-con-full e-flex e-con e-child\" data-id=\"c8b40b0\" data-element_type=\"container\">\n\t\t\t\t<div class=\"elementor-element elementor-element-5e220dd elementor-widget elementor-widget-shortcode\" data-id=\"5e220dd\" data-element_type=\"widget\" data-widget_type=\"shortcode.default\">\n\t\t\t\t\t\t\t<div class=\"elementor-shortcode\"><p>Your cart is empty.<\/p><\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-6b49b88 elementor-widget elementor-widget-html\" data-id=\"6b49b88\" data-element_type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t\t<style>\r\n\/* ========================== *\/\r\n\/* CPS Cards & Layout CSS *\/\r\n\/* ========================== *\/\r\n\r\n.cps-card {\r\n  cursor: pointer;\r\n  background: #fff;\r\n  border: 1px solid #e1e1e1;\r\n  border-radius: 10px;\r\n  padding: 14px 16px;\r\n  margin-bottom: 12px;\r\n  box-shadow: 0 1px 2px rgba(0,0,0,0.05);\r\n  display: flex;\r\n  justify-content: space-between;\r\n  align-items: flex-start;\r\n  transition: all 0.2s ease;\r\n}\r\n\r\n.cps-card.active {\r\n  border: 2px solid #000000;\r\n  background-color: #ffffff;\r\n  transition: all 0.3s ease;\r\n}\r\n\r\n.cps-left { display: flex; gap: 12px; flex: 1; align-items: flex-start; }\r\n.cps-img img { width: 60px; border-radius: 6px; }\r\n.cps-info { display: flex; flex-direction: column; gap: 5px; }\r\n.cps-name-row { display: flex; align-items: center; gap: 8px; position: relative; }\r\n.cps-name { font-weight: 600; font-size: 15px; color: #222; }\r\n.cps-qty { font-size: 13px; color: #666; }\r\n.cps-right { text-align: right; min-width: 85px; }\r\n.cps-price { font-weight: 600; color: #111; font-size: 16px; }\r\n.cps-subtotal { font-size: 12px; color: #777; }\r\n\r\n\/* Info icon *\/\r\n.cps-info-icon {\r\n  display: inline-flex;\r\n  justify-content: center;\r\n  align-items: center;\r\n  width: 16px;\r\n  height: 16px;\r\n  border-radius: 50%;\r\n  background: #fff;\r\n  border: 1px solid #c8c8c8;\r\n  color: #222;\r\n  font-size: 10px;\r\n  font-weight: 700;\r\n  cursor: pointer;\r\n  padding: 0;\r\n  line-height: 1;\r\n  text-align: center;\r\n  box-sizing: border-box;\r\n  vertical-align: middle;\r\n  transition: all 0.25s ease;\r\n  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);\r\n}\r\n\r\n.cps-info-icon:hover {\r\n  background: #111;\r\n  color: #fff;\r\n  border-color: #111;\r\n  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);\r\n  transform: scale(1.05);\r\n}\r\n\r\n@media (max-width: 768px) {\r\n  .cps-card { flex-direction: column; align-items: flex-start; }\r\n  .cps-right { text-align: left; margin-top: 6px; }\r\n}\r\n\r\n\/* ========================== *\/\r\n\/* CPS Modal CSS *\/\r\n\/* ========================== *\/\r\n.cps-modal {\r\n  display: none;\r\n  position: fixed;\r\n  top: 0; left: 0;\r\n  width: 100%; height: 100%;\r\n  background: rgba(0, 0, 0, 0.6);\r\n  z-index: 9999;\r\n  justify-content: center;\r\n  align-items: center;\r\n  padding: 20px;\r\n}\r\n\r\n.cps-modal-content {\r\n  background: #fff;\r\n  border-radius: 10px;\r\n  max-width: 420px;\r\n  width: 90%;\r\n  padding: 24px;\r\n  position: relative;\r\n  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);\r\n  animation: fadeInScale 0.25s ease;\r\n}\r\n@keyframes fadeInScale {\r\n  from { opacity: 0; transform: scale(0.9); }\r\n  to { opacity: 1; transform: scale(1); }\r\n}\r\n\r\n.cps-modal-close {\r\n  position: absolute;\r\n  top: 10px;\r\n  right: 12px;\r\n  background: none;\r\n  border: none;\r\n  font-size: 24px;\r\n  line-height: 1;\r\n  cursor: pointer;\r\n  color: #000000;\r\n  transition: color 0.2s ease;\r\n}\r\n.cps-modal-close:hover { color: #fff;\r\nbackground-color: #00000080;}\r\n\r\n.cps-modal-title {\r\n  font-size: 18px;\r\n  font-weight: 600;\r\n  margin-bottom: 12px;\r\n  color: #111;\r\n}\r\n.cps-modal-body {\r\n  font-size: 14px;\r\n  color: #444;\r\n}\r\n.cps-modal-body .cps-attr {\r\n  margin-bottom: 8px;\r\n  border-bottom: 1px solid #eee;\r\n  padding-bottom: 6px;\r\n}\r\n.cps-modal-body .cps-attr:last-child { border-bottom: none; }\r\n\r\n.cps-remove-text{\r\n  font-size: 12px;\r\n  font-weight: 600;\r\n  color: #9aa1aa;\r\n}\r\n\r\n.cps-remove-text:hover{\r\n  color: #dc2626;\r\n}\r\n\/* ========================== *\/\r\n\/* File Upload Section CSS *\/\r\n\/* ========================== *\/\r\n.upload-card,\r\n.preview-card {\r\n  display: none; \/* initially hide *\/\r\n  flex-direction: row;\r\n  transition: all 0.3s ease;\r\n}\r\n\r\n.upload-card.active,\r\n.preview-card.active {\r\n  display: flex; \/* show when active *\/\r\n}\r\n<\/style>\r\n\r\n<script>\r\ndocument.addEventListener('DOMContentLoaded', function () {\r\n  const cards = Array.from(document.querySelectorAll('.cps-card'));\r\n  const fileSection = document.querySelector('.file-section');\r\n\r\n  \/* =========================================================\r\n     MODAL OPEN \/ CLOSE\r\n  ========================================================= *\/\r\n  const infoButtons = document.querySelectorAll('.cps-info-icon');\r\n  const modals = document.querySelectorAll('.cps-modal');\r\n\r\n  infoButtons.forEach(btn => {\r\n    btn.addEventListener('click', function (e) {\r\n      e.stopPropagation();\r\n      const target = btn.getAttribute('data-modal-target');\r\n      const modal = document.getElementById(target);\r\n      if (modal) {\r\n        modal.style.display = 'flex';\r\n        document.body.style.overflow = 'hidden';\r\n      }\r\n    });\r\n  });\r\n\r\n  modals.forEach(modal => {\r\n    const closeBtn = modal.querySelector('.cps-modal-close');\r\n\r\n    if (closeBtn) {\r\n      closeBtn.addEventListener('click', function () {\r\n        modal.style.display = 'none';\r\n        document.body.style.overflow = '';\r\n      });\r\n    }\r\n\r\n    modal.addEventListener('click', function (e) {\r\n      if (e.target === modal) {\r\n        modal.style.display = 'none';\r\n        document.body.style.overflow = '';\r\n      }\r\n    });\r\n  });\r\n\r\n  \/* =========================================================\r\n     CARD SELECTION\r\n  ========================================================= *\/\r\n  let activeIndex = null;\r\n\r\n  function showFileSection() {\r\n    if (fileSection) fileSection.classList.add('visible');\r\n  }\r\n\r\n  function activateCard(index) {\r\n    if (index < 0 || index >= cards.length) return;\r\n\r\n    cards.forEach(card => card.classList.remove('active', 'is-active', 'selected'));\r\n\r\n    const card = cards[index];\r\n    card.classList.add('active', 'is-active', 'selected');\r\n    activeIndex = index;\r\n\r\n    showFileSection();\r\n  }\r\n\r\n  function isInfoClick(target) {\r\n    return !!target.closest('.cps-info-icon, .cps-modal, .cps-modal-close');\r\n  }\r\n\r\n  cards.forEach((card, index) => {\r\n    card.addEventListener('click', function (e) {\r\n      if (isInfoClick(e.target)) return;\r\n      activateCard(index);\r\n    });\r\n  });\r\n\r\n  \/* =========================================================\r\n     AUTO SELECT FIRST CARD\r\n  ========================================================= *\/\r\n  if (cards.length) {\r\n    setTimeout(function () {\r\n      const hasActive = cards.some(card =>\r\n        card.classList.contains('active') ||\r\n        card.classList.contains('is-active') ||\r\n        card.classList.contains('selected')\r\n      );\r\n\r\n      if (!hasActive) {\r\n        activateCard(0);\r\n      }\r\n    }, 200);\r\n  }\r\n\r\n  \/* =========================================================\r\n     REMOVE CART ITEM\r\n     Requires:\r\n     - .cps-remove-text inside each .cps-card\r\n     - card has data-cart-item-key\r\n     - customCartAjax.ajax_url\r\n     - customCartAjax.nonce\r\n  ========================================================= *\/\r\n  cards.forEach(card => {\r\n    const removeBtn = card.querySelector('.cps-remove-text');\r\n    if (!removeBtn) return;\r\n\r\n    removeBtn.addEventListener('click', function (e) {\r\n      e.preventDefault();\r\n      e.stopPropagation();\r\n\r\n      const cartItemKey =\r\n        removeBtn.getAttribute('data-cart-item-key') ||\r\n        card.getAttribute('data-cart-item-key');\r\n\r\n      if (!cartItemKey) return;\r\n\r\n      const originalText = removeBtn.textContent;\r\n      removeBtn.textContent = 'Removing...';\r\n      removeBtn.style.pointerEvents = 'none';\r\n      removeBtn.style.opacity = '0.7';\r\n\r\n      fetch(customCartAjax.ajax_url, {\r\n        method: 'POST',\r\n        headers: {\r\n          'Content-Type': 'application\/x-www-form-urlencoded; charset=UTF-8'\r\n        },\r\n        body: new URLSearchParams({\r\n          action: 'custom_remove_cart_item',\r\n          cart_item_key: cartItemKey,\r\n          nonce: customCartAjax.nonce\r\n        }).toString()\r\n      })\r\n      .then(response => response.json())\r\n      .then(data => {\r\n        if (data && data.success) {\r\n          window.location.reload();\r\n        } else {\r\n          removeBtn.textContent = originalText;\r\n          removeBtn.style.pointerEvents = '';\r\n          removeBtn.style.opacity = '';\r\n          alert((data && data.data && data.data.message) ? data.data.message : 'Error removing item');\r\n        }\r\n      })\r\n      .catch(error => {\r\n        console.error('Remove item error:', error);\r\n        removeBtn.textContent = originalText;\r\n        removeBtn.style.pointerEvents = '';\r\n        removeBtn.style.opacity = '';\r\n        alert('Failed to remove item');\r\n      });\r\n    });\r\n  });\r\n});\r\n<\/script>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-2b389dc e-flex e-con-boxed e-con e-child\" data-id=\"2b389dc\" data-element_type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-1ffb124 elementor-widget elementor-widget-html\" data-id=\"1ffb124\" data-element_type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t\t<div class=\"s4u-upload-panel\" id=\"s4u-upload-panel\">\r\n\r\n  <div class=\"s4u-upload-inner\">\r\n\r\n    <div class=\"s4u-upload-header\">\r\n      <h2 class=\"s4u-upload-title\">Upload Files<\/h2>\r\n      <div class=\"s4u-upload-specs\">\r\n        Net format: <span id=\"s4u-net-format\">85 \u00d7 55 mm<\/span>\r\n        <span class=\"s4u-dot-sep\">\u2022<\/span>\r\n        Gross format: <span id=\"s4u-gross-format\">88 \u00d7 58 mm<\/span>\r\n      <\/div>\r\n    <\/div>\r\n\r\n    <div class=\"s4u-upload-slots\" id=\"s4u-upload-slots\">\r\n\r\n      <!-- SLOT: FRONT \/ SINGLE -->\r\n      <div class=\"s4u-upload-slot\"\r\n           data-slot=\"front\"\r\n           data-slot-label=\"Front Design\">\r\n\r\n        <div class=\"s4u-upload-slot-head\">\r\n          <div class=\"s4u-upload-slot-title\" id=\"s4u-front-slot-title\">\r\n            Upload Front Design\r\n          <\/div>\r\n        <\/div>\r\n\r\n        <div class=\"s4u-upload-dropzone\" data-dropzone-for=\"front\">\r\n          <input type=\"file\" id=\"s4u-file-front\" accept=\".jpg,.jpeg,.png,.webp,image\/jpeg,image\/png,image\/webp\" hidden>\r\n\r\n          <button type=\"button\"\r\n                  class=\"s4u-upload-btn\"\r\n                  data-trigger-upload=\"front\">\r\n            <span class=\"s4u-upload-btn-icon\">\r\n              <img decoding=\"async\" src=\"https:\/\/dev.sticker4u.dk\/wp-content\/uploads\/2026\/03\/hugeicons_folder-upload.svg\" alt=\"\" \/>\r\n            <\/span>\r\n            <span>Upload<\/span>\r\n          <\/button>\r\n\r\n          <div class=\"s4u-upload-hint\">\r\n            Max file size: 32 MB. Allowed formats:\r\n            <span>JPG<\/span>\r\n            <span>JPEG<\/span>\r\n            <span>PNG<\/span>\r\n            <span>WEBP<\/span>\r\n          <\/div>\r\n        <\/div>\r\n\r\n        <div class=\"s4u-upload-warning\" data-warning-for=\"front\" style=\"display:none;\"><\/div>\r\n\r\n        <div class=\"s4u-file-list\" data-file-list=\"front\">\r\n          <div class=\"s4u-file-card\">\r\n            <div class=\"s4u-file-left\">\r\n              <div class=\"s4u-file-icon\">\r\n                <img decoding=\"async\" src=\"https:\/\/dev.sticker4u.dk\/wp-content\/uploads\/2026\/03\/hugeicons_file-01.svg\" alt=\"File icon\" \/>\r\n              <\/div>\r\n\r\n              <div class=\"s4u-file-meta\">\r\n                <div class=\"s4u-file-name\">front-file.jpg<\/div>\r\n                <div class=\"s4u-file-sub\">\r\n                  <span>3.2 MB<\/span>\r\n                  <span class=\"s4u-meta-dot\"><\/span>\r\n                  <span>1 Page<\/span>\r\n                  <span class=\"s4u-meta-dot\"><\/span>\r\n                  <span>2 sec left<\/span>\r\n                <\/div>\r\n              <\/div>\r\n            <\/div>\r\n\r\n            <div class=\"s4u-file-right\">\r\n              <button type=\"button\" class=\"s4u-file-delete\" aria-label=\"Delete file\">\r\n                <img decoding=\"async\" src=\"https:\/\/dev.sticker4u.dk\/wp-content\/uploads\/2026\/03\/hugeicons_delete-02.svg\" alt=\"\" \/>\r\n              <\/button>\r\n              <div class=\"s4u-file-status\">65%<\/div>\r\n            <\/div>\r\n\r\n            <div class=\"s4u-progress\">\r\n              <div class=\"s4u-progress-bar\" style=\"width:65%;\"><\/div>\r\n            <\/div>\r\n          <\/div>\r\n        <\/div>\r\n      <\/div>\r\n\r\n      <!-- SLOT: BACK -->\r\n      <div class=\"s4u-upload-slot\"\r\n           data-slot=\"back\"\r\n           data-slot-label=\"Back Design\">\r\n\r\n        <div class=\"s4u-upload-slot-head\">\r\n          <div class=\"s4u-upload-slot-title\" id=\"s4u-back-slot-title\">\r\n            Upload Back Design\r\n          <\/div>\r\n        <\/div>\r\n\r\n        <div class=\"s4u-upload-dropzone\" data-dropzone-for=\"back\">\r\n          <input type=\"file\" id=\"s4u-file-back\" accept=\".jpg,.jpeg,.png,.webp,image\/jpeg,image\/png,image\/webp\" hidden>\r\n\r\n          <button type=\"button\"\r\n                  class=\"s4u-upload-btn\"\r\n                  data-trigger-upload=\"back\">\r\n            <span class=\"s4u-upload-btn-icon\">\r\n              <img decoding=\"async\" src=\"https:\/\/dev.sticker4u.dk\/wp-content\/uploads\/2026\/03\/hugeicons_folder-upload.svg\" alt=\"\" \/>\r\n            <\/span>\r\n            <span>Upload<\/span>\r\n          <\/button>\r\n\r\n          <div class=\"s4u-upload-hint\">\r\n            Max file size: 32 MB. Allowed formats:\r\n            <span>JPG<\/span>\r\n            <span>JPEG<\/span>\r\n            <span>PNG<\/span>\r\n            <span>WEBP<\/span>\r\n          <\/div>\r\n        <\/div>\r\n\r\n        <div class=\"s4u-upload-warning\" data-warning-for=\"back\" style=\"display:none;\"><\/div>\r\n\r\n        <div class=\"s4u-file-list\" data-file-list=\"back\">\r\n          <div class=\"s4u-file-card is-complete\">\r\n            <div class=\"s4u-file-left\">\r\n              <div class=\"s4u-file-icon\">\r\n                <img decoding=\"async\" src=\"https:\/\/dev.sticker4u.dk\/wp-content\/uploads\/2026\/03\/hugeicons_file-01.svg\" alt=\"File icon\" \/>\r\n              <\/div>\r\n\r\n              <div class=\"s4u-file-meta\">\r\n                <div class=\"s4u-file-name\">back-file.jpg<\/div>\r\n                <div class=\"s4u-file-sub\">\r\n                  <span>2.1 MB<\/span>\r\n                  <span class=\"s4u-meta-dot\"><\/span>\r\n                  <span>1 Page<\/span>\r\n                <\/div>\r\n              <\/div>\r\n            <\/div>\r\n\r\n            <div class=\"s4u-file-right\">\r\n              <button type=\"button\" class=\"s4u-file-delete\" aria-label=\"Delete file\">\r\n                <img decoding=\"async\" src=\"https:\/\/dev.sticker4u.dk\/wp-content\/uploads\/2026\/03\/hugeicons_delete-02.svg\" alt=\"\" \/>\r\n              <\/button>\r\n              <div class=\"s4u-file-status\">Uploaded<\/div>\r\n            <\/div>\r\n\r\n            <div class=\"s4u-progress\">\r\n              <div class=\"s4u-progress-bar\" style=\"width:100%;\"><\/div>\r\n            <\/div>\r\n          <\/div>\r\n        <\/div>\r\n      <\/div>\r\n\r\n      <div class=\"s4u-preview-wrap\" id=\"s4u-preview-wrap\">\r\n        <div class=\"s4u-preview-head\">\r\n          <h3 class=\"s4u-preview-title\">3D Preview<\/h3>\r\n        <\/div>\r\n\r\n        <div class=\"s4u-preview-stage\" id=\"s4u-preview-stage\">\r\n          <div class=\"s4u-preview-controls\">\r\n            <button type=\"button\" id=\"s4u-zoom-in\" aria-label=\"Zoom in\">+<\/button>\r\n            <button type=\"button\" id=\"s4u-zoom-out\" aria-label=\"Zoom out\">\u2212<\/button>\r\n            <button type=\"button\" id=\"s4u-rotate-left\" aria-label=\"Rotate left\">\u27f2<\/button>\r\n            <button type=\"button\" id=\"s4u-rotate-right\" aria-label=\"Rotate right\">\u27f3<\/button>\r\n            <button type=\"button\" id=\"s4u-tilt-up\" aria-label=\"Tilt up\">\u21ba<\/button>\r\n            <button type=\"button\" id=\"s4u-tilt-down\" aria-label=\"Tilt down\">\u21bb<\/button>\r\n            <button type=\"button\" id=\"s4u-fold-toggle\" aria-label=\"Fold preview\" style=\"display:none;\">Fold<\/button>\r\n          <\/div>\r\n        <\/div>\r\n      <\/div>\r\n\r\n    <\/div>\r\n\r\n  <\/div>\r\n<\/div>\r\n\r\n<style>\r\n.s4u-upload-warning {\r\n  display: none;\r\n  margin-top: 12px;\r\n  margin-bottom: 10px;\r\n  padding: 12px 14px;\r\n  border-radius: 10px;\r\n  background: #fff4e5;\r\n  border: 1px solid #f3c98b;\r\n  color: #8a5a00;\r\n  font-size: 13px;\r\n  line-height: 1.4;\r\n}\r\n\r\n.s4u-upload-warning.is-error {\r\n  background: #fff3f3;\r\n  border-color: #f5c2c7;\r\n  color: #b42318;\r\n}\r\n\r\n.s4u-upload-btn[disabled] {\r\n  opacity: 0.6;\r\n  cursor: not-allowed;\r\n}\r\n\r\n.s4u-upload-slot.is-uploading .s4u-upload-btn {\r\n  opacity: 0.6;\r\n  pointer-events: none;\r\n}\r\n<\/style>\r\n\r\n<script>\r\njQuery(function ($) {\r\n\r\n  function autoSelectFirstCard() {\r\n    const $cards = $('.cps-card');\r\n    if (!$cards.length) return;\r\n\r\n    const $alreadyActive = $cards.filter('.is-active, .active, .selected');\r\n    if ($alreadyActive.length) return;\r\n\r\n    const $first = $cards.first();\r\n    $first.trigger('click');\r\n  }\r\n\r\n  setTimeout(autoSelectFirstCard, 200);\r\n  setTimeout(autoSelectFirstCard, 600);\r\n\r\n});\r\n<\/script>\r\n\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-847c203 elementor-widget elementor-widget-html\" data-id=\"847c203\" data-element_type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t\t<script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/three.js\/r126\/three.min.js\"><\/script>\r\n<script src=\"https:\/\/cdn.jsdelivr.net\/npm\/three@0.126.0\/examples\/js\/controls\/OrbitControls.js\"><\/script>\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-03c0e9f elementor-widget elementor-widget-html\" data-id=\"03c0e9f\" data-element_type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t\t    <script>\n    window.s4uUploadData = {\n      ajaxUrl: \"https:\/\/dev.sticker4u.dk\/wp-admin\/admin-ajax.php\",\n      cartUploads: []    };\n    <\/script>\n    \r\n<script>\r\njQuery(function ($) {\r\n  if (typeof THREE === 'undefined' || typeof THREE.OrbitControls === 'undefined') {\r\n    console.warn('Three.js \/ OrbitControls not loaded.');\r\n    return;\r\n  }\r\n\r\n  const $panel        = $('#s4u-upload-panel');\r\n  const $slotsWrap    = $('#s4u-upload-slots');\r\n  const $frontSlot    = $slotsWrap.find('[data-slot=\"front\"]');\r\n  const $backSlot     = $slotsWrap.find('[data-slot=\"back\"]');\r\n  const $frontTitle   = $('#s4u-front-slot-title');\r\n  const $backTitle    = $('#s4u-back-slot-title');\r\n  const $previewStage = $('#s4u-preview-stage');\r\n\r\n  if (!$panel.length || !$slotsWrap.length || !$previewStage.length) return;\r\n\r\n  let activeCartKey   = null;\r\n  let forcedFoldType  = null;\r\n  let isUploadingFile = false;\r\n\r\n  const DPI    = 300;\r\n  const MAX_MB = 32;\r\n\r\n  const DIMENSION_MAP = {\r\n    \"85x55mm\":{\"w\":85,\"h\":55},\"90x50mm\":{\"w\":90,\"h\":50},\"85x25mm\":{\"w\":85,\"h\":25},\r\n    \"65x65mm\":{\"w\":65,\"h\":65},\"135x55mm\":{\"w\":135,\"h\":55},\"170x55mm\":{\"w\":170,\"h\":55},\r\n    \"105x297\":{\"w\":105,\"h\":297},\"198x210\":{\"w\":198,\"h\":210},\r\n    \"a3\":{\"w\":297,\"h\":420},\"a4\":{\"w\":210,\"h\":297},\"a4-3x-perforated\":{\"w\":210,\"h\":297},\r\n    \"a4-perforated-strips\":{\"w\":210,\"h\":297},\"a5\":{\"w\":148,\"h\":210},\r\n    \"a6\":{\"w\":105,\"h\":148},\"a7\":{\"w\":74,\"h\":105},\"dl\":{\"w\":99,\"h\":210}\r\n  };\r\n\r\n  const FOLD_PRODUCT_IDS = [701, 32516];\r\n  const uploadState = new Map();\r\n\r\n  function mmToPx(mm) { return Math.round((mm \/ 25.4) * DPI); }\r\n  function normalizeValue(val) { return String(val || '').trim().toLowerCase(); }\r\n\r\n  function showUploadWarning(side, message, type = 'warning') {\r\n    const $warning = $('[data-warning-for=\"' + side + '\"]');\r\n    if (!$warning.length) return;\r\n    $warning.removeClass('is-error');\r\n    if (type === 'error') $warning.addClass('is-error');\r\n    $warning.text(message).show();\r\n  }\r\n\r\n  function clearUploadWarning(side) {\r\n    const $warning = $('[data-warning-for=\"' + side + '\"]');\r\n    if (!$warning.length) return;\r\n    $warning.removeClass('is-error').text('').hide();\r\n  }\r\n\r\n  function setSlotUploading(side, uploading) {\r\n    const $slot = $slotsWrap.find('[data-slot=\"' + side + '\"]');\r\n    const $btn = $slot.find('.s4u-upload-btn');\r\n\r\n    if (uploading) {\r\n      $slot.addClass('is-uploading');\r\n      $btn.prop('disabled', true);\r\n    } else {\r\n      $slot.removeClass('is-uploading');\r\n      $btn.prop('disabled', false);\r\n    }\r\n  }\r\n\r\n  function canonicalFoldSlug(val) {\r\n    const s = normalizeValue(val).replace(\/\\s+\/g,'-').replace(\/_\/g,'-');\r\n    if (!s) return '';\r\n    if (['z','z-fold','zfold'].includes(s)) return 'z';\r\n    if (['c','c-fold','cfold'].includes(s)) return 'c';\r\n    if (['zigzag','zig-zag','zig-zag-fold','accordion'].includes(s)) return 'zigzag';\r\n    if (['in-two','intwo','in-2','half','bi-fold','bifold','fold-in-two'].includes(s)) return 'in-two';\r\n    if (['in-four','infour','in-4','quarter-fold','fold-in-four'].includes(s)) return 'in-four';\r\n    if (['altar-fold','altarfold','altar'].includes(s)) return 'altar-fold';\r\n    return s;\r\n  }\r\n\r\n  function getActiveCard() {\r\n    return $('.cps-card.is-active, .cps-card.active, .cps-card.selected').first();\r\n  }\r\n\r\n  function extractFoldTypeFromCard($card) {\r\n    if (!$card || !$card.length) return '';\r\n\r\n    const directAttrs = [\r\n      $card.attr('data-fold-type'), $card.attr('data-folded-type'),\r\n      $card.attr('data-fold'),      $card.attr('data-fold_type'),\r\n      $card.data('fold-type'),      $card.data('folded-type'),\r\n      $card.data('fold'),           $card.data('fold_type')\r\n    ];\r\n\r\n    for (const v of directAttrs) {\r\n      const slug = canonicalFoldSlug(v);\r\n      if (slug) return slug;\r\n    }\r\n\r\n    const cartKey     = ($card.attr('data-cart-item-key') || '').trim();\r\n    const modalTarget = $card.find('[data-modal-target]').first().attr('data-modal-target') || '';\r\n\r\n    let $roots = $card;\r\n\r\n    if (modalTarget) {\r\n      try { $roots = $roots.add($(`#${CSS.escape(modalTarget)}`)); } catch(e) {}\r\n      try { $roots = $roots.add($(`[data-modal-id=\"${CSS.escape(modalTarget)}\"]`)); } catch(e) {}\r\n    }\r\n\r\n    if (cartKey) {\r\n      try { $roots = $roots.add($(`.cps-modal[data-cart-item-key=\"${CSS.escape(cartKey)}\"]`)); } catch(e) {}\r\n      try { $roots = $roots.add($(`.cps-info-modal[data-cart-item-key=\"${CSS.escape(cartKey)}\"]`)); } catch(e) {}\r\n      try { $roots = $roots.add($(`.cps-config-modal[data-cart-item-key=\"${CSS.escape(cartKey)}\"]`)); } catch(e) {}\r\n      try { $roots = $roots.add($(`[data-modal-target=\"${CSS.escape(cartKey)}\"]`).closest('[class*=\"modal\"]')); } catch(e) {}\r\n    }\r\n\r\n    let found = '';\r\n    $roots.each(function () {\r\n      if (found) return false;\r\n      $(this).find('.cps-attr').each(function () {\r\n        if (found) return false;\r\n        const $row  = $(this);\r\n        const label = normalizeValue($row.find('strong, b').first().text());\r\n        if (!label.includes('fold')) return;\r\n        const slug  = canonicalFoldSlug($row.find('span').last().text());\r\n        if (slug) { found = slug; return false; }\r\n      });\r\n    });\r\n    if (found) return found;\r\n\r\n    $roots.each(function () {\r\n      if (found) return false;\r\n      $(this).find('strong, b, th, dt').each(function () {\r\n        if (found) return false;\r\n        if (!normalizeValue($(this).text()).includes('fold')) return;\r\n        const slug = canonicalFoldSlug($(this).next('span, td, dd').text());\r\n        if (slug) { found = slug; return false; }\r\n      });\r\n    });\r\n\r\n    return found;\r\n  }\r\n\r\n  function getSelectedFoldTypeFromUI() {\r\n    if (forcedFoldType) return forcedFoldType;\r\n\r\n    const tileSelectors = [\r\n      '.flyer-fold-type-img.active.selected:not(.fold-disabled)',\r\n      '.flyer-fold-type-img.active:not(.fold-disabled)',\r\n      '.flyer-fold-type-img.selected:not(.fold-disabled)',\r\n      '.flyer-fold-type-img.is-active:not(.fold-disabled)',\r\n      '.fold-type-card.active:not(.fold-disabled)',\r\n      '.fold-type-card.selected:not(.fold-disabled)'\r\n    ];\r\n\r\n    for (const sel of tileSelectors) {\r\n      const $el = $(sel).first();\r\n      if (!$el.length) continue;\r\n      const slug = canonicalFoldSlug($el.attr('data-fold'))\r\n                || canonicalFoldSlug($el.attr('data-fold-type'))\r\n                || canonicalFoldSlug($el.attr('data-value'))\r\n                || canonicalFoldSlug($el.data('fold'))\r\n                || canonicalFoldSlug($el.find('figcaption,.widget-image-caption').first().text());\r\n      if (slug) return slug;\r\n    }\r\n\r\n    const selects = [\r\n      'select[name=\"attribute_pa_fold-type\"]','select[name=\"attribute_fold-type\"]',\r\n      'select[name=\"attribute_pa_folded-type\"]','select[name=\"attribute_folded-type\"]',\r\n      'select[name=\"attribute_pa_fold_type\"]','select[name=\"attribute_fold_type\"]'\r\n    ];\r\n\r\n    for (const sel of selects) {\r\n      const $el = $(sel).first();\r\n      if (!$el.length) continue;\r\n      const slug = canonicalFoldSlug($el.val())\r\n                || canonicalFoldSlug($el.find('option:selected').text());\r\n      if (slug) return slug;\r\n    }\r\n\r\n    return '';\r\n  }\r\n\r\n  function getSelectedDimensionFromUI() {\r\n    const selects = [\r\n      'select[name=\"attribute_pa_dimension\"]','select[name=\"attribute_dimension\"]',\r\n      'select[name=\"attribute_pa_folded-size\"]','select[name=\"attribute_folded-size\"]',\r\n      'select[name=\"attribute_pa_flyer-size\"]','select[name=\"attribute_flyer-size\"]'\r\n    ];\r\n\r\n    for (const sel of selects) {\r\n      const v = normalizeValue($(sel).val());\r\n      if (v) return v;\r\n    }\r\n\r\n    const tiles = [\r\n      '.dimension-img.active:not(.fold-disabled)',\r\n      '.dimension-img.selected:not(.fold-disabled)',\r\n      '.flyer-dimension-img.active:not(.fold-disabled)',\r\n      '.flyer-dimension-img.selected:not(.fold-disabled)'\r\n    ];\r\n\r\n    for (const sel of tiles) {\r\n      const $el = $(sel).first();\r\n      if (!$el.length) continue;\r\n      for (const v of [\r\n        $el.attr('data-value'), $el.attr('data-dimension'), $el.attr('data-size'),\r\n        $el.data('value'), $el.data('dimension'), $el.data('size')\r\n      ]) {\r\n        const val = normalizeValue(v);\r\n        if (val) return val;\r\n      }\r\n    }\r\n\r\n    return '';\r\n  }\r\n\r\n  function getSelectedPrintingModeFromUI() {\r\n    const selects = [\r\n      'select[name=\"attribute_pa_printing-sides\"]','select[name=\"attribute_printing-sides\"]',\r\n      'select[name=\"attribute_pa_print-color\"]','select[name=\"attribute_print-color\"]',\r\n      'select[name=\"attribute_pa_tearoff-color\"]','select[name=\"attribute_tearoff-color\"]'\r\n    ];\r\n\r\n    for (const sel of selects) {\r\n      const v = normalizeValue($(sel).val());\r\n      if (v) return v;\r\n    }\r\n\r\n    const tiles = [\r\n      '.printing-card.active','.printing-card.selected',\r\n      '.flyer-printing-card.active','.flyer-printing-card.selected',\r\n      '.tearoff-color-card.active','.tearoff-color-card.selected'\r\n    ];\r\n\r\n    for (const sel of tiles) {\r\n      const $el = $(sel).first();\r\n      if (!$el.length) continue;\r\n      for (const v of [\r\n        $el.attr('data-value'), $el.attr('data-printing-sides'),\r\n        $el.attr('data-print-color'), $el.attr('data-tearoff-color'),\r\n        $el.data('value'), $el.data('printing-sides'),\r\n        $el.data('print-color'), $el.data('tearoff-color')\r\n      ]) {\r\n        const val = normalizeValue(v);\r\n        if (val) return val;\r\n      }\r\n    }\r\n\r\n    return '';\r\n  }\r\n\r\n  function getFoldPreviewConfig(productId, dimension, foldType) {\r\n    const dim  = normalizeValue(dimension);\r\n    const fold = canonicalFoldSlug(foldType);\r\n\r\n    if (productId === 701) {\r\n      if (dim === '135x55mm' || dim === '135x55') {\r\n        return { mode:'fold', type:'gate-offset-25-75', panels:[0.25, 0.75] };\r\n      }\r\n      return { mode:'fold', type:'half', panels:[0.5, 0.5] };\r\n    }\r\n\r\n    switch (fold) {\r\n      case 'in-two':     return { mode:'fold', type:'half',    panels:[0.5,  0.5] };\r\n      case 'z':          return { mode:'fold', type:'z',       panels:[1\/3,  1\/3,  1\/3] };\r\n      case 'c':          return { mode:'fold', type:'c',       panels:[1\/3,  1\/3,  1\/3] };\r\n      case 'zigzag':     return { mode:'fold', type:'zigzag',  panels:[0.25, 0.25, 0.25, 0.25] };\r\n      case 'in-four':    return { mode:'fold', type:'in-four', panels:[0.25, 0.25, 0.25, 0.25] };\r\n      case 'altar-fold': return { mode:'flat', type:'flat',    panels:[1] };\r\n      default:           return { mode:'fold', type:'half',    panels:[0.5, 0.5] };\r\n    }\r\n  }\r\n\r\n  function getCardData($card) {\r\n    const productId    = parseInt($card.attr('data-product-id') || '0', 10) || 0;\r\n    const liveMode     = getSelectedPrintingModeFromUI();\r\n    const cardPS       = normalizeValue($card.attr('data-printing-sides'));\r\n    const cardPC       = normalizeValue($card.attr('data-print-color'));\r\n    const cardTC       = normalizeValue($card.attr('data-tearoff-color'));\r\n    const resolvedMode = liveMode || cardPS || cardPC || cardTC;\r\n    const liveDim      = getSelectedDimensionFromUI();\r\n    const resolvedDim  = liveDim || normalizeValue($card.attr('data-dimension'));\r\n\r\n    const isFoldProduct = FOLD_PRODUCT_IDS.includes(productId);\r\n    let resolvedFold = '';\r\n    let foldPreview  = { mode:'flat', type:'flat', panels:[1] };\r\n\r\n    if (isFoldProduct) {\r\n      const liveFold  = getSelectedFoldTypeFromUI();\r\n      const cardFold  = extractFoldTypeFromCard($card);\r\n      resolvedFold    = liveFold || cardFold;\r\n      foldPreview     = getFoldPreviewConfig(productId, resolvedDim, resolvedFold);\r\n    }\r\n\r\n    return {\r\n      cartKey: ($card.attr('data-cart-item-key') || '').trim(),\r\n      productId,\r\n      dimension: resolvedDim,\r\n      printingSides: liveMode || cardPS,\r\n      printColor: liveMode || cardPC,\r\n      tearoffColor: liveMode || cardTC,\r\n      resolvedMode,\r\n      foldType: resolvedFold,\r\n      foldPreview,\r\n      folded: isFoldProduct && foldPreview.mode === 'fold'\r\n    };\r\n  }\r\n\r\n  function needsBackFile(data) {\r\n    const vals = [data.resolvedMode, data.printingSides, data.printColor, data.tearoffColor]\r\n      .map(normalizeValue)\r\n      .filter(Boolean);\r\n\r\n    const dbl = [\r\n      'both-sides','both sides','black-both','black both','4-4','1-1',\r\n      'color-both-sides-4-4','colour-both-sides-4-4','black-both-1-1'\r\n    ];\r\n\r\n    const sgl = [\r\n      'one-side','one side','single-side','single side','black-one-side',\r\n      'black one side','4-0','1-0','black-one-side-1-0',\r\n      'color-one-side-4-0','colour-one-side-4-0'\r\n    ];\r\n\r\n    if (vals.some(v => dbl.includes(v))) return true;\r\n    if (vals.some(v => sgl.includes(v))) return false;\r\n    return false;\r\n  }\r\n\r\n  function getOrCreateState(cartKey) {\r\n    if (!uploadState.has(cartKey)) {\r\n      uploadState.set(cartKey, {\r\n        front: null,\r\n        back: null,\r\n        frontFile: null,\r\n        backFile: null,\r\n        mode: 'single',\r\n        folded: false,\r\n        foldConfig: { mode:'flat', type:'flat', panels:[1] }\r\n      });\r\n    }\r\n    return uploadState.get(cartKey);\r\n  }\r\n\r\n  function hydrateStateFromServer(cartKey) {\r\n    const existing = window.s4uUploadData?.cartUploads?.[cartKey]?.uploaded_files || {};\r\n    const state = getOrCreateState(cartKey);\r\n\r\n    ['front', 'back'].forEach(side => {\r\n      if (!existing[side]) return;\r\n\r\n      state[side] = {\r\n        name: existing[side].name || `${side}.jpg`,\r\n        sizeText: formatBytes(existing[side].size || 0),\r\n        pagesText: '1 File',\r\n        timeLeftText: '',\r\n        progress: 100,\r\n        status: 'uploaded',\r\n        url: existing[side].url || ''\r\n      };\r\n    });\r\n  }\r\n\r\n  function showSingleMode() {\r\n    $panel.addClass('s4u-mode-single');\r\n    $backSlot.addClass('is-hidden').hide();\r\n    $frontSlot.removeClass('is-hidden').show();\r\n    $frontTitle.text('Upload design');\r\n  }\r\n\r\n  function showDoubleMode() {\r\n    $panel.removeClass('s4u-mode-single');\r\n    $frontSlot.removeClass('is-hidden').show();\r\n    $backSlot.removeClass('is-hidden').show();\r\n    $frontTitle.text('Upload front design');\r\n    $backTitle.text('Upload back design');\r\n  }\r\n\r\n  function formatBytes(bytes) {\r\n    if (!bytes || isNaN(bytes)) return '';\r\n    return `${(bytes \/ (1024 * 1024)).toFixed(1)} MB`;\r\n  }\r\n\r\n  function escapeHtml(str) {\r\n    return String(str)\r\n      .replaceAll('&','&amp;')\r\n      .replaceAll('<','&lt;')\r\n      .replaceAll('>','&gt;')\r\n      .replaceAll('\"','&quot;')\r\n      .replaceAll(\"'\",'&#039;');\r\n  }\r\n\r\n  function renderFileCard(fileObj, side) {\r\n    if (!fileObj) return '';\r\n    const st = fileObj.status === 'uploaded' ? 'Uploaded' : (fileObj.progress || 0) + '%';\r\n    const pw = fileObj.status === 'uploaded' ? 100 : (fileObj.progress || 0);\r\n    const sub = [];\r\n\r\n    if (fileObj.sizeText)  sub.push(`<span>${fileObj.sizeText}<\/span>`);\r\n    if (fileObj.pagesText) sub.push(`<span class=\"s4u-meta-dot\"><\/span><span>${fileObj.pagesText}<\/span>`);\r\n    if (fileObj.timeLeftText && fileObj.status !== 'uploaded') {\r\n      sub.push(`<span class=\"s4u-meta-dot\"><\/span><span>${fileObj.timeLeftText}<\/span>`);\r\n    }\r\n\r\n    return `\r\n      <div class=\"s4u-file-card ${fileObj.status === 'uploaded' ? 'is-complete' : ''}\" data-side=\"${side}\">\r\n        <div class=\"s4u-file-left\">\r\n          <div class=\"s4u-file-icon\">\r\n            <img decoding=\"async\" src=\"https:\/\/dev.sticker4u.dk\/wp-content\/uploads\/2026\/03\/hugeicons_file-01.svg\" alt=\"File icon\"\/>\r\n          <\/div>\r\n          <div class=\"s4u-file-meta\">\r\n            <div class=\"s4u-file-name\">${escapeHtml(fileObj.name || 'file')}<\/div>\r\n            <div class=\"s4u-file-sub\">${sub.join('')}<\/div>\r\n          <\/div>\r\n        <\/div>\r\n        <div class=\"s4u-file-right\">\r\n          <button type=\"button\" class=\"s4u-file-delete\" data-delete-side=\"${side}\" aria-label=\"Delete file\">\r\n            <img decoding=\"async\" src=\"https:\/\/dev.sticker4u.dk\/wp-content\/uploads\/2026\/03\/hugeicons_delete-02.svg\" alt=\"\"\/>\r\n          <\/button>\r\n          <div class=\"s4u-file-status\">${st}<\/div>\r\n        <\/div>\r\n        <div class=\"s4u-progress\">\r\n          <div class=\"s4u-progress-bar\" style=\"width:${pw}%;\"><\/div>\r\n        <\/div>\r\n      <\/div>`;\r\n  }\r\n\r\n  function renderSlotFiles(cartKey) {\r\n    const s = getOrCreateState(cartKey);\r\n    $slotsWrap.find('[data-file-list=\"front\"]').html(s.front ? renderFileCard(s.front, 'front') : '');\r\n    $slotsWrap.find('[data-file-list=\"back\"]').html(s.back ? renderFileCard(s.back, 'back') : '');\r\n  }\r\n\r\n  function validateUploadedFile(file, side, onValid) {\r\n    clearUploadWarning(side);\r\n\r\n    const $a = getActiveCard();\r\n    if (!$a.length) {\r\n      showUploadWarning(side, 'Please select a product first.', 'error');\r\n      return;\r\n    }\r\n\r\n    const data = getCardData($a);\r\n\r\n    if (file.size > MAX_MB * 1024 * 1024) {\r\n      showUploadWarning(side, `File exceeds ${MAX_MB} MB limit.`, 'error');\r\n      return;\r\n    }\r\n\r\n    const allowedTypes = ['image\/jpeg', 'image\/jpg', 'image\/png', 'image\/webp'];\r\n    const fileType = (file.type || '').toLowerCase();\r\n\r\n    if (!allowedTypes.includes(fileType)) {\r\n      showUploadWarning(side, 'Only JPG, JPEG, PNG, and WEBP files are supported for preview.', 'error');\r\n      return;\r\n    }\r\n\r\n    const dimKey = normalizeValue(data.dimension);\r\n    const dim = DIMENSION_MAP[dimKey];\r\n\r\n    if (!dim) {\r\n      console.warn('Could not determine product size for quality check:', data);\r\n      onValid(file);\r\n      return;\r\n    }\r\n\r\n    const requiredW = mmToPx(dim.w);\r\n    const requiredH = mmToPx(dim.h);\r\n\r\n    const reader = new FileReader();\r\n    reader.onload = e => {\r\n      const img = new Image();\r\n\r\n      img.onload = () => {\r\n        const validNormal  = img.width >= requiredW && img.height >= requiredH;\r\n        const validRotated = img.width >= requiredH && img.height >= requiredW;\r\n\r\n        if (!validNormal && !validRotated) {\r\n          showUploadWarning(\r\n            side,\r\n            `Image too small. Minimum required: ${requiredW} \u00d7 ${requiredH}px at 300 DPI.`,\r\n            'error'\r\n          );\r\n          return;\r\n        }\r\n\r\n        const softLimitW = Math.round(requiredW * 1.15);\r\n        const softLimitH = Math.round(requiredH * 1.15);\r\n\r\n        if (\r\n          (img.width < softLimitW || img.height < softLimitH) &&\r\n          (img.width < softLimitH || img.height < softLimitW)\r\n        ) {\r\n          showUploadWarning(\r\n            side,\r\n            'The uploaded file meets the minimum size, but quality may be low for best print output.',\r\n            'warning'\r\n          );\r\n        } else {\r\n          clearUploadWarning(side);\r\n        }\r\n\r\n        onValid(file);\r\n      };\r\n\r\n      img.onerror = () => {\r\n        showUploadWarning(side, 'Could not read image.', 'error');\r\n      };\r\n\r\n      img.src = e.target.result;\r\n    };\r\n\r\n    reader.onerror = () => {\r\n      showUploadWarning(side, 'Could not process file.', 'error');\r\n    };\r\n\r\n    reader.readAsDataURL(file);\r\n  }\r\n\r\n  const preview3D = {\r\n    scene:null, camera:null, renderer:null, controls:null,\r\n    root:null, flatBase:null, frontPlane:null, backPlane:null,\r\n    segments:[], mode:'flat',\r\n    currentFoldConfig:{mode:'flat',type:'flat',panels:[1]},\r\n    targetFold:0, foldProgress:0, isFoldedOpen:false,\r\n\r\n    init() {\r\n      this.scene = new THREE.Scene();\r\n      this.camera = new THREE.PerspectiveCamera(45, 1, 0.1, 1000);\r\n      this.camera.position.set(0, 0, 8);\r\n\r\n      this.renderer = new THREE.WebGLRenderer({antialias:true, alpha:false});\r\n      this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\r\n      this.renderer.setClearColor(0xf4f4f4, 1);\r\n      $previewStage.append(this.renderer.domElement);\r\n\r\n      this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);\r\n      this.controls.enableDamping = true;\r\n      this.controls.dampingFactor = 0.08;\r\n      this.controls.enablePan = false;\r\n      this.controls.minDistance = 4;\r\n      this.controls.maxDistance = 20;\r\n\r\n      const dl = new THREE.DirectionalLight(0xffffff, 1);\r\n      dl.position.set(5, 10, 7);\r\n      this.scene.add(dl);\r\n      this.scene.add(new THREE.AmbientLight(0xffffff, 0.85));\r\n\r\n      this.root = new THREE.Group();\r\n      this.scene.add(this.root);\r\n\r\n      this.setMode('flat', {mode:'flat',type:'flat',panels:[1]});\r\n      this.resize();\r\n      this.animate();\r\n\r\n      $('#s4u-zoom-in').off('click.s4u3d').on('click.s4u3d', () => {\r\n        this.camera.position.z = Math.max(4, this.camera.position.z - 0.8);\r\n      });\r\n\r\n      $('#s4u-zoom-out').off('click.s4u3d').on('click.s4u3d', () => {\r\n        this.camera.position.z = Math.min(20, this.camera.position.z + 0.8);\r\n      });\r\n\r\n      $('#s4u-rotate-left').off('click.s4u3d').on('click.s4u3d', () => {\r\n        this.root.rotation.y -= 0.35;\r\n      });\r\n\r\n      $('#s4u-rotate-right').off('click.s4u3d').on('click.s4u3d', () => {\r\n        this.root.rotation.y += 0.35;\r\n      });\r\n\r\n      $('#s4u-tilt-up').off('click.s4u3d').on('click.s4u3d', () => {\r\n        this.root.rotation.x -= 0.22;\r\n      });\r\n\r\n      $('#s4u-tilt-down').off('click.s4u3d').on('click.s4u3d', () => {\r\n        this.root.rotation.x += 0.22;\r\n      });\r\n\r\n      $(document).off('click.s4u3dFold', '#s4u-fold-toggle')\r\n        .on('click.s4u3dFold', '#s4u-fold-toggle', () => {\r\n          if (this.currentFoldConfig.mode !== 'fold') return;\r\n          this.isFoldedOpen = !this.isFoldedOpen;\r\n          this.targetFold = this.isFoldedOpen ? 1 : 0;\r\n          $('#s4u-fold-toggle').text(this.isFoldedOpen ? 'Unfold' : 'Fold');\r\n        });\r\n\r\n      $(window).on('resize.s4uPreview3d', () => this.resize());\r\n    },\r\n\r\n    resize() {\r\n      const w = $previewStage.innerWidth() || 900;\r\n      const h = $previewStage.innerHeight() || 560;\r\n      this.camera.aspect = w \/ h;\r\n      this.camera.updateProjectionMatrix();\r\n      this.renderer.setSize(w, h);\r\n    },\r\n\r\n    clearSceneObjects() {\r\n      while (this.root.children.length) this.root.remove(this.root.children[0]);\r\n      this.flatBase = this.frontPlane = this.backPlane = null;\r\n      this.segments = [];\r\n    },\r\n\r\n    setMode(mode, foldConfig) {\r\n      this.mode = mode === 'fold' ? 'fold' : 'flat';\r\n      this.currentFoldConfig = foldConfig || {mode:'flat',type:'flat',panels:[1]};\r\n      this.clearSceneObjects();\r\n      this.targetFold = this.foldProgress = 0;\r\n      this.isFoldedOpen = false;\r\n\r\n      if (this.mode === 'fold') {\r\n        this.buildFoldStructure(this.currentFoldConfig);\r\n        $('#s4u-fold-toggle').text('Fold').show();\r\n      } else {\r\n        this.buildFlatCard();\r\n        $('#s4u-fold-toggle').hide();\r\n      }\r\n    },\r\n\r\n    buildFlatCard() {\r\n      const mat = () => new THREE.MeshBasicMaterial({color:0xffffff, side:THREE.DoubleSide});\r\n      const geo = new THREE.PlaneGeometry(3.96, 2.46);\r\n\r\n      this.flatBase = new THREE.Mesh(\r\n        new THREE.BoxGeometry(4, 2.5, 0.06),\r\n        new THREE.MeshStandardMaterial({color:0xffffff})\r\n      );\r\n      this.root.add(this.flatBase);\r\n\r\n      this.frontPlane = new THREE.Mesh(geo, mat());\r\n      this.frontPlane.position.z = 0.031;\r\n      this.root.add(this.frontPlane);\r\n\r\n      this.backPlane = new THREE.Mesh(geo, mat());\r\n      this.backPlane.rotation.y = Math.PI;\r\n      this.backPlane.position.z = -0.031;\r\n      this.root.add(this.backPlane);\r\n    },\r\n\r\n    buildFoldStructure(foldConfig) {\r\n      const totalW = 4, totalH = 2.5, depth = 0.06;\r\n      const raw = (Array.isArray(foldConfig.panels) && foldConfig.panels.length) ? foldConfig.panels : [0.5, 0.5];\r\n      const total = raw.reduce((a, b) => a + b, 0) || 1;\r\n      const norm = raw.map(p => p \/ total);\r\n      const widths = norm.map(p => totalW * p);\r\n\r\n      const mkF = () => new THREE.MeshBasicMaterial({color:0xffffff, side:THREE.DoubleSide});\r\n      const mkB = () => new THREE.MeshBasicMaterial({color:0xffffff, side:THREE.DoubleSide});\r\n      const mkS = () => new THREE.MeshStandardMaterial({color:0xffffff});\r\n\r\n      const fw = widths[0], fg = new THREE.Group();\r\n      fg.position.x = -totalW\/2 + fw\/2;\r\n      this.root.add(fg);\r\n\r\n      const ff = new THREE.Mesh(new THREE.PlaneGeometry(Math.max(fw - 0.04, 0.08), 2.46), mkF());\r\n      ff.position.z = 0.031;\r\n      const fb = new THREE.Mesh(new THREE.PlaneGeometry(Math.max(fw - 0.04, 0.08), 2.46), mkB());\r\n      fb.rotation.y = Math.PI;\r\n      fb.position.z = -0.031;\r\n\r\n      fg.add(new THREE.Mesh(new THREE.BoxGeometry(fw, totalH, depth), mkS()));\r\n      fg.add(ff);\r\n      fg.add(fb);\r\n\r\n      this.segments.push({group:fg, hinge:null, front:ff, back:fb, width:fw, ratio:norm[0], start:0, end:norm[0]});\r\n\r\n      let cumul = norm[0], prevG = fg, prevW = fw;\r\n\r\n      for (let i = 1; i < norm.length; i++) {\r\n        const w = widths[i], r = norm[i];\r\n        const hinge = new THREE.Group();\r\n        hinge.position.x = prevW \/ 2;\r\n        prevG.add(hinge);\r\n\r\n        const sg = new THREE.Group();\r\n        sg.position.x = w \/ 2;\r\n        hinge.add(sg);\r\n\r\n        const pf = new THREE.Mesh(new THREE.PlaneGeometry(Math.max(w - 0.04, 0.08), 2.46), mkF());\r\n        pf.position.z = 0.031;\r\n        const pb = new THREE.Mesh(new THREE.PlaneGeometry(Math.max(w - 0.04, 0.08), 2.46), mkB());\r\n        pb.rotation.y = Math.PI;\r\n        pb.position.z = -0.031;\r\n\r\n        sg.add(new THREE.Mesh(new THREE.BoxGeometry(w, totalH, depth), mkS()));\r\n        sg.add(pf);\r\n        sg.add(pb);\r\n\r\n        this.segments.push({group:sg, hinge, front:pf, back:pb, width:w, ratio:r, start:cumul, end:cumul+r});\r\n        cumul += r;\r\n        prevG = sg;\r\n        prevW = w;\r\n      }\r\n    },\r\n\r\n    getHingeAngles(progress) {\r\n      const type = this.currentFoldConfig?.type || 'half';\r\n      const p = Math.max(0, Math.min(1, progress));\r\n      const f = Math.PI * p;\r\n      const n = this.segments.length - 1;\r\n\r\n      switch (type) {\r\n        case 'half':\r\n        case 'gate-offset-25-75': return [-f];\r\n        case 'z':                 return [-f, f];\r\n        case 'c':                 return [-f, -f];\r\n        case 'zigzag':            return Array.from({length:n}, (_,i) => i % 2 === 0 ? -f : f);\r\n        case 'in-four':           return [-f, -f, -f];\r\n        default:                  return Array.from({length:n}, () => -f);\r\n      }\r\n    },\r\n\r\n    loadTextureFromFile(file, done) {\r\n      if (!file) return done(null);\r\n\r\n      const allowedTypes = ['image\/jpeg', 'image\/jpg', 'image\/png', 'image\/webp'];\r\n      const fileType = (file.type || '').toLowerCase();\r\n\r\n      if (!allowedTypes.includes(fileType)) {\r\n        return done(null);\r\n      }\r\n\r\n      const reader = new FileReader();\r\n\r\n      reader.onload = e => {\r\n        const img = new Image();\r\n\r\n        img.onload = () => {\r\n          const tex = new THREE.TextureLoader().load(img.src, () => {\r\n            tex.needsUpdate = true;\r\n\r\n            if ('colorSpace' in tex) tex.colorSpace = THREE.SRGBColorSpace;\r\n            else if ('encoding' in tex) tex.encoding = THREE.sRGBEncoding;\r\n\r\n            tex.minFilter = THREE.LinearFilter;\r\n            tex.magFilter = THREE.LinearFilter;\r\n            tex.wrapS = THREE.ClampToEdgeWrapping;\r\n            tex.wrapT = THREE.ClampToEdgeWrapping;\r\n\r\n            done(tex);\r\n          }, undefined, () => done(null));\r\n        };\r\n\r\n        img.onerror = () => done(null);\r\n        img.src = e.target.result;\r\n      };\r\n\r\n      reader.onerror = () => done(null);\r\n      reader.readAsDataURL(file);\r\n    },\r\n\r\n    loadTextureFromUrl(url, done) {\r\n      if (!url) return done(null);\r\n\r\n      const tex = new THREE.TextureLoader().load(url, () => {\r\n        tex.needsUpdate = true;\r\n\r\n        if ('colorSpace' in tex) tex.colorSpace = THREE.SRGBColorSpace;\r\n        else if ('encoding' in tex) tex.encoding = THREE.sRGBEncoding;\r\n\r\n        tex.minFilter = THREE.LinearFilter;\r\n        tex.magFilter = THREE.LinearFilter;\r\n        tex.wrapS = THREE.ClampToEdgeWrapping;\r\n        tex.wrapT = THREE.ClampToEdgeWrapping;\r\n\r\n        done(tex);\r\n      }, undefined, () => done(null));\r\n    },\r\n\r\n    makeTextureSlice(tex, start, end, isBack) {\r\n      if (!tex) return null;\r\n      const c = tex.clone();\r\n      c.needsUpdate = true;\r\n      c.wrapS = c.wrapT = THREE.ClampToEdgeWrapping;\r\n      const w = Math.max(end - start, 0.0001);\r\n\r\n      if (isBack) {\r\n        c.repeat.set(-w, 1);\r\n        c.offset.set(end, 0);\r\n      } else {\r\n        c.repeat.set(w, 1);\r\n        c.offset.set(start, 0);\r\n      }\r\n\r\n      return c;\r\n    },\r\n\r\n    disposeMap(mesh) {\r\n      if (!mesh?.material?.map) return;\r\n      mesh.material.map.dispose();\r\n      mesh.material.map = null;\r\n      mesh.material.needsUpdate = true;\r\n    },\r\n\r\n    applySliced(tex, side) {\r\n      this.segments.forEach(seg => {\r\n        const mesh = side === 'back' ? seg.back : seg.front;\r\n        if (!mesh) return;\r\n        this.disposeMap(mesh);\r\n        mesh.material.map = this.makeTextureSlice(tex, seg.start, seg.end, side === 'back');\r\n        mesh.material.needsUpdate = true;\r\n      });\r\n    },\r\n\r\n    applyFrontFile(file) {\r\n      this.loadTextureFromFile(file, tex => {\r\n        if (this.mode === 'fold') {\r\n          this.applySliced(tex, 'front');\r\n          return;\r\n        }\r\n        if (!this.frontPlane) return;\r\n        this.disposeMap(this.frontPlane);\r\n        this.frontPlane.material.map = tex || null;\r\n        this.frontPlane.material.needsUpdate = true;\r\n      });\r\n    },\r\n\r\n    applyBackFile(file) {\r\n      this.loadTextureFromFile(file, tex => {\r\n        if (this.mode === 'fold') {\r\n          this.applySliced(tex, 'back');\r\n          return;\r\n        }\r\n        if (!this.backPlane) return;\r\n        this.disposeMap(this.backPlane);\r\n        this.backPlane.material.map = tex || null;\r\n        this.backPlane.material.needsUpdate = true;\r\n      });\r\n    },\r\n\r\n    applyFrontUrl(url) {\r\n      this.loadTextureFromUrl(url, tex => {\r\n        if (this.mode === 'fold') {\r\n          this.applySliced(tex, 'front');\r\n          return;\r\n        }\r\n        if (!this.frontPlane) return;\r\n        this.disposeMap(this.frontPlane);\r\n        this.frontPlane.material.map = tex || null;\r\n        this.frontPlane.material.needsUpdate = true;\r\n      });\r\n    },\r\n\r\n    applyBackUrl(url) {\r\n      this.loadTextureFromUrl(url, tex => {\r\n        if (this.mode === 'fold') {\r\n          this.applySliced(tex, 'back');\r\n          return;\r\n        }\r\n        if (!this.backPlane) return;\r\n        this.disposeMap(this.backPlane);\r\n        this.backPlane.material.map = tex || null;\r\n        this.backPlane.material.needsUpdate = true;\r\n      });\r\n    },\r\n\r\n    clearFront() {\r\n      if (this.mode === 'fold') {\r\n        this.segments.forEach(s => this.disposeMap(s.front));\r\n        return;\r\n      }\r\n      this.disposeMap(this.frontPlane);\r\n    },\r\n\r\n    clearBack() {\r\n      if (this.mode === 'fold') {\r\n        this.segments.forEach(s => this.disposeMap(s.back));\r\n        return;\r\n      }\r\n      this.disposeMap(this.backPlane);\r\n    },\r\n\r\n    syncFromState(state) {\r\n      if (!state) return;\r\n\r\n      const fc = state.foldConfig || {mode:'flat',type:'flat',panels:[1]};\r\n      const nextMode = (state.folded && fc.mode === 'fold') ? 'fold' : 'flat';\r\n      const nextConfig = nextMode === 'fold' ? fc : {mode:'flat',type:'flat',panels:[1]};\r\n\r\n      const modeChanged =\r\n        this.mode !== nextMode ||\r\n        JSON.stringify(this.currentFoldConfig) !== JSON.stringify(nextConfig);\r\n\r\n      if (modeChanged) {\r\n        this.setMode(nextMode, nextConfig);\r\n      }\r\n\r\n      if (state.frontFile) {\r\n        this.applyFrontFile(state.frontFile);\r\n      } else if (state.front && state.front.url) {\r\n        this.applyFrontUrl(state.front.url);\r\n      } else {\r\n        this.clearFront();\r\n      }\r\n\r\n      if (state.mode === 'double') {\r\n        if (state.backFile) {\r\n          this.applyBackFile(state.backFile);\r\n        } else if (state.back && state.back.url) {\r\n          this.applyBackUrl(state.back.url);\r\n        } else {\r\n          this.clearBack();\r\n        }\r\n      } else {\r\n        this.clearBack();\r\n      }\r\n    },\r\n\r\n    animate() {\r\n      const loop = () => {\r\n        requestAnimationFrame(loop);\r\n\r\n        if (this.currentFoldConfig.mode === 'fold' && this.segments.length > 1) {\r\n          this.foldProgress += (this.targetFold - this.foldProgress) * 0.03;\r\n          const angles = this.getHingeAngles(this.foldProgress);\r\n          for (let i = 1; i < this.segments.length; i++) {\r\n            if (this.segments[i].hinge) this.segments[i].hinge.rotation.y = angles[i - 1] || 0;\r\n          }\r\n        }\r\n\r\n        this.controls.update();\r\n        this.renderer.render(this.scene, this.camera);\r\n      };\r\n\r\n      loop();\r\n    }\r\n  };\r\n\r\n  preview3D.init();\r\n\r\n  function uploadFileToServer(side, file) {\r\n    if (!activeCartKey) {\r\n      showUploadWarning(side, 'Please select a product first.', 'error');\r\n      return;\r\n    }\r\n\r\n    isUploadingFile = true;\r\n    setSlotUploading(side, true);\r\n\r\n    const state = getOrCreateState(activeCartKey);\r\n\r\n    state[side] = {\r\n      name: file.name,\r\n      sizeText: formatBytes(file.size),\r\n      pagesText: '1 File',\r\n      timeLeftText: 'Uploading...',\r\n      progress: 10,\r\n      status: 'uploading'\r\n    };\r\n\r\n    if (side === 'front') {\r\n      state.frontFile = file;\r\n    } else {\r\n      state.backFile = file;\r\n    }\r\n\r\n    renderSlotFiles(activeCartKey);\r\n\r\n    const formData = new FormData();\r\n    formData.append('action', 'custom_upload_file');\r\n    formData.append('cart_item_key', activeCartKey);\r\n    formData.append('side', side);\r\n    formData.append('file', file);\r\n\r\n    const xhr = new XMLHttpRequest();\r\n    xhr.open('POST', window.s4uUploadData.ajaxUrl, true);\r\n\r\n    xhr.upload.addEventListener('progress', function (e) {\r\n      if (!e.lengthComputable) return;\r\n\r\n      const percent = Math.max(10, Math.round((e.loaded \/ e.total) * 100));\r\n      const currentState = getOrCreateState(activeCartKey);\r\n\r\n      if (!currentState[side]) return;\r\n\r\n      currentState[side].progress = percent;\r\n      currentState[side].timeLeftText = percent >= 100 ? '' : 'Uploading...';\r\n      renderSlotFiles(activeCartKey);\r\n    });\r\n\r\n    xhr.onload = function () {\r\n      isUploadingFile = false;\r\n      setSlotUploading(side, false);\r\n\r\n      let response = null;\r\n\r\n      try {\r\n        response = JSON.parse(xhr.responseText);\r\n      } catch (e) {\r\n        response = null;\r\n      }\r\n\r\n      if (!response || !response.success) {\r\n        showUploadWarning(side, response?.data?.message || 'Upload failed.', 'error');\r\n\r\n        const currentState = getOrCreateState(activeCartKey);\r\n        currentState[side] = null;\r\n\r\n        if (side === 'front') {\r\n          currentState.frontFile = null;\r\n          preview3D.clearFront();\r\n        } else {\r\n          currentState.backFile = null;\r\n          preview3D.clearBack();\r\n        }\r\n\r\n        renderSlotFiles(activeCartKey);\r\n        return;\r\n      }\r\n\r\n      const uploaded = response.data.file;\r\n      const currentState = getOrCreateState(activeCartKey);\r\n\r\n      currentState[side] = {\r\n        name: uploaded.name || file.name,\r\n        sizeText: formatBytes(uploaded.size || file.size),\r\n        pagesText: '1 File',\r\n        timeLeftText: '',\r\n        progress: 100,\r\n        status: 'uploaded',\r\n        url: uploaded.url || ''\r\n      };\r\n\r\n      if (side === 'front') {\r\n        currentState.frontFile = null;\r\n      } else {\r\n        currentState.backFile = null;\r\n      }\r\n\r\n      if (window.s4uUploadData?.cartUploads?.[activeCartKey]) {\r\n        if (!window.s4uUploadData.cartUploads[activeCartKey].uploaded_files) {\r\n          window.s4uUploadData.cartUploads[activeCartKey].uploaded_files = {};\r\n        }\r\n        window.s4uUploadData.cartUploads[activeCartKey].uploaded_files[side] = uploaded;\r\n      }\r\n\r\n      renderSlotFiles(activeCartKey);\r\n      preview3D.syncFromState(currentState);\r\n    };\r\n\r\n    xhr.onerror = function () {\r\n      isUploadingFile = false;\r\n      setSlotUploading(side, false);\r\n\r\n      showUploadWarning(side, 'Upload failed.', 'error');\r\n\r\n      const currentState = getOrCreateState(activeCartKey);\r\n      currentState[side] = null;\r\n\r\n      if (side === 'front') {\r\n        currentState.frontFile = null;\r\n        preview3D.clearFront();\r\n      } else {\r\n        currentState.backFile = null;\r\n        preview3D.clearBack();\r\n      }\r\n\r\n      renderSlotFiles(activeCartKey);\r\n    };\r\n\r\n    xhr.send(formData);\r\n  }\r\n\r\n  function renderPanelForCard($card) {\r\n    const data = getCardData($card);\r\n    if (!data.cartKey) return;\r\n\r\n    activeCartKey = data.cartKey;\r\n    $('.cps-card').removeClass('is-active active selected');\r\n    $card.addClass('is-active active selected');\r\n\r\n    const state = getOrCreateState(data.cartKey);\r\n    hydrateStateFromServer(data.cartKey);\r\n\r\n    const doubleMode = needsBackFile(data);\r\n    state.mode = doubleMode ? 'double' : 'single';\r\n    state.foldConfig = data.foldPreview;\r\n    state.folded = data.folded;\r\n\r\n    if (doubleMode) {\r\n      showDoubleMode();\r\n    } else {\r\n      showSingleMode();\r\n      state.back = null;\r\n      state.backFile = null;\r\n      clearUploadWarning('back');\r\n      preview3D.clearBack();\r\n    }\r\n\r\n    renderSlotFiles(data.cartKey);\r\n    preview3D.syncFromState(state);\r\n  }\r\n\r\n  function refreshUploadPanelFromActiveCard() {\r\n    if (isUploadingFile) return;\r\n    const $a = getActiveCard();\r\n    if ($a.length) renderPanelForCard($a);\r\n  }\r\n\r\n  function ensureInitialSelection() {\r\n    let $a = getActiveCard();\r\n    if (!$a.length) $a = $('.cps-card').first();\r\n    if ($a.length) renderPanelForCard($a);\r\n  }\r\n\r\n  $(document).off('click.s4uCard','.cps-card').on('click.s4uCard','.cps-card',function(e){\r\n    if (isUploadingFile) return;\r\n    if ($(e.target).closest('.cps-info-icon,.cps-remove-text,.cps-modal,.cps-modal-close').length) return;\r\n    renderPanelForCard($(this));\r\n  });\r\n\r\n  $(document).off('click.s4uInfo','.cps-info-icon').on('click.s4uInfo','.cps-info-icon',function(){\r\n    if (isUploadingFile) return;\r\n    const $card = $(this).closest('.cps-card');\r\n    if ($card.length) {\r\n      $('.cps-card').removeClass('is-active active selected');\r\n      $card.addClass('is-active active selected');\r\n    }\r\n  });\r\n\r\n  $(document).off('click.s4uModalClose','.cps-modal-close').on('click.s4uModalClose','.cps-modal-close',function(){\r\n    if (isUploadingFile) return;\r\n    setTimeout(() => {\r\n      const $a = getActiveCard();\r\n      if ($a.length) renderPanelForCard($a);\r\n    }, 100);\r\n  });\r\n\r\n  $(document).off('click.s4uUpload','[data-trigger-upload]').on('click.s4uUpload','[data-trigger-upload]',function(){\r\n    const side = ($(this).attr('data-trigger-upload') || '').trim();\r\n    $(side === 'back' ? '#s4u-file-back' : '#s4u-file-front').trigger('click');\r\n  });\r\n\r\n  $('#s4u-file-front').off('change.s4u').on('change.s4u', function(){\r\n    const f = this.files && this.files[0];\r\n    if (!f) return;\r\n    validateUploadedFile(f, 'front', v => uploadFileToServer('front', v));\r\n    this.value = '';\r\n  });\r\n\r\n  $('#s4u-file-back').off('change.s4u').on('change.s4u', function(){\r\n    const f = this.files && this.files[0];\r\n    if (!f) return;\r\n    validateUploadedFile(f, 'back', v => uploadFileToServer('back', v));\r\n    this.value = '';\r\n  });\r\n\r\n  $(document).off('click.s4uDelete', '[data-delete-side]').on('click.s4uDelete', '[data-delete-side]', function(){\r\n    const side = ($(this).attr('data-delete-side') || '').trim();\r\n    if (!activeCartKey || !side || isUploadingFile) return;\r\n\r\n    const formData = new FormData();\r\n    formData.append('action', 'custom_delete_uploaded_file');\r\n    formData.append('cart_item_key', activeCartKey);\r\n    formData.append('side', side);\r\n\r\n    fetch(window.s4uUploadData.ajaxUrl, {\r\n      method: 'POST',\r\n      body: formData\r\n    })\r\n    .then(r => r.json())\r\n    .then(res => {\r\n      if (!res || !res.success) {\r\n        showUploadWarning(side, res?.data?.message || 'Could not delete file.', 'error');\r\n        return;\r\n      }\r\n\r\n      clearUploadWarning(side);\r\n\r\n      const state = getOrCreateState(activeCartKey);\r\n      state[side] = null;\r\n\r\n      if (side === 'front') {\r\n        state.frontFile = null;\r\n        preview3D.clearFront();\r\n      } else {\r\n        state.backFile = null;\r\n        preview3D.clearBack();\r\n      }\r\n\r\n      if (window.s4uUploadData?.cartUploads?.[activeCartKey]?.uploaded_files) {\r\n        delete window.s4uUploadData.cartUploads[activeCartKey].uploaded_files[side];\r\n      }\r\n\r\n      renderSlotFiles(activeCartKey);\r\n    })\r\n    .catch(() => {\r\n      showUploadWarning(side, 'Could not delete file.', 'error');\r\n    });\r\n  });\r\n\r\n  $(document).off('change.s4uSelects','select').on('change.s4uSelects','select',\r\n    () => setTimeout(refreshUploadPanelFromActiveCard, 80)\r\n  );\r\n\r\n  $(document).off('click.s4uFoldTile','.flyer-fold-type-img,.fold-type-card')\r\n    .on('click.s4uFoldTile','.flyer-fold-type-img,.fold-type-card',function(){\r\n      if (isUploadingFile) return;\r\n      const $el = $(this);\r\n      const slug = canonicalFoldSlug(\r\n        $el.attr('data-fold') || $el.attr('data-fold-type') || $el.attr('data-value') ||\r\n        $el.data('fold') || $el.find('figcaption,.widget-image-caption').first().text()\r\n      );\r\n      if (!slug) return;\r\n      forcedFoldType = slug;\r\n      const $a = getActiveCard();\r\n      if ($a.length) renderPanelForCard($a);\r\n      forcedFoldType = null;\r\n    });\r\n\r\n  $(document).off('click.s4uOtherTiles','.printing-card,.flyer-printing-card,.tearoff-color-card,.dimension-img,.flyer-dimension-img')\r\n    .on('click.s4uOtherTiles','.printing-card,.flyer-printing-card,.tearoff-color-card,.dimension-img,.flyer-dimension-img',\r\n      () => setTimeout(refreshUploadPanelFromActiveCard, 80)\r\n    );\r\n\r\n  function injectContinueButton() {\r\n    if ($('#s4u-continue-checkout').length) return;\r\n    const html = `<div class=\"s4u-continue-wrap\"><button type=\"button\" id=\"s4u-continue-checkout\" class=\"s4u-continue-btn\">Continue to checkout<\/button><\/div>`;\r\n    ($('#s4u-preview-wrap').length ? $('#s4u-preview-wrap') : $slotsWrap).after(html);\r\n  }\r\n  injectContinueButton();\r\n\r\n  function validateBeforeContinue() {\r\n    const $a = getActiveCard();\r\n    if (!$a.length) {\r\n      alert('Please select a product first.');\r\n      return false;\r\n    }\r\n\r\n    const data = getCardData($a);\r\n    const state = getOrCreateState(data.cartKey);\r\n\r\n    if (!state.front) {\r\n      showUploadWarning('front', 'Please upload the design file before continuing.', 'error');\r\n      return false;\r\n    }\r\n\r\n    if (needsBackFile(data) && !state.back) {\r\n      showUploadWarning('back', 'Please upload the back design file before continuing.', 'error');\r\n      return false;\r\n    }\r\n\r\n    return true;\r\n  }\r\n\r\n  function getCheckoutUrl() {\r\n    return (typeof wc_checkout_params !== 'undefined' && wc_checkout_params.checkout_url)\r\n      ? wc_checkout_params.checkout_url\r\n      : '\/checkout\/';\r\n  }\r\n\r\n  $(document).off('click.s4uContinue','#s4u-continue-checkout').on('click.s4uContinue','#s4u-continue-checkout',function(){\r\n    if (!validateBeforeContinue()) return;\r\n    $(this).prop('disabled', true).text('Proceeding...');\r\n    setTimeout(() => { window.location.href = getCheckoutUrl(); }, 200);\r\n  });\r\n\r\n  setTimeout(() => { ensureInitialSelection(); preview3D.resize(); }, 250);\r\n  setTimeout(() => { ensureInitialSelection(); preview3D.resize(); }, 700);\r\n});\r\n<\/script>\r\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Review Your Order &amp; Upload Your Design Confirm your details on the left and upload your design on the right to complete your print setup. Upload Files Net format: 85 \u00d7 55 mm \u2022 Gross format: 88 \u00d7 58 mm Upload Front Design Upload Max file size: 32 MB. Allowed formats: JPG JPEG PNG WEBP [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_acf_changed":false,"footnotes":""},"class_list":["post-3443","page","type-page","status-publish","hentry"],"acf":[],"_links":{"self":[{"href":"https:\/\/dev.sticker4u.dk\/en\/wp-json\/wp\/v2\/pages\/3443","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/dev.sticker4u.dk\/en\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/dev.sticker4u.dk\/en\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/dev.sticker4u.dk\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/dev.sticker4u.dk\/en\/wp-json\/wp\/v2\/comments?post=3443"}],"version-history":[{"count":652,"href":"https:\/\/dev.sticker4u.dk\/en\/wp-json\/wp\/v2\/pages\/3443\/revisions"}],"predecessor-version":[{"id":46457,"href":"https:\/\/dev.sticker4u.dk\/en\/wp-json\/wp\/v2\/pages\/3443\/revisions\/46457"}],"wp:attachment":[{"href":"https:\/\/dev.sticker4u.dk\/en\/wp-json\/wp\/v2\/media?parent=3443"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}