{"id":3443,"date":"2025-11-10T12:12:34","date_gmt":"2025-11-10T12:12:34","guid":{"rendered":"https:\/\/test.sticker4u.dk\/?page_id=3443"},"modified":"2026-03-25T14:34:04","modified_gmt":"2026-03-25T14:34:04","slug":"file-upload","status":"publish","type":"page","link":"https:\/\/test.sticker4u.dk\/da\/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    <!-- Dynamic upload slots wrapper -->\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\" 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:\/\/test.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>JPEG<\/span>\r\n            <span>TIFF<\/span>\r\n            <span>PDF<\/span>\r\n          <\/div>\r\n        <\/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:\/\/test.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.pdf<\/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:\/\/test.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\" 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:\/\/test.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>JPEG<\/span>\r\n            <span>TIFF<\/span>\r\n            <span>PDF<\/span>\r\n          <\/div>\r\n        <\/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:\/\/test.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.pdf<\/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:\/\/test.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<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    <\/div>\r\n\r\n  <\/div>\r\n<\/div>\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; \/\/ don't override manual selection\r\n\r\n    const $first = $cards.first();\r\n\r\n    \/\/ trigger your existing click logic (important)\r\n    $first.trigger('click');\r\n  }\r\n\r\n  \/* run after everything is rendered *\/\r\n  setTimeout(autoSelectFirstCard, 200);\r\n  setTimeout(autoSelectFirstCard, 600);\r\n\r\n});\r\n<\/script>\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>\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  const $foldBtn      = $('#s4u-fold-toggle');\r\n\r\n  if (!$panel.length || !$slotsWrap.length || !$previewStage.length) return;\r\n\r\n  let activeCartKey = null;\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 },\r\n    \"90x50mm\": { w: 90, h: 50 },\r\n    \"85x25mm\": { w: 85, h: 25 },\r\n    \"65x65mm\": { w: 65, h: 65 },\r\n    \"135x55mm\": { w: 135, h: 55 },\r\n    \"170x55mm\": { w: 170, h: 55 },\r\n    \"105x297\": { w: 105, h: 297 },\r\n    \"198x210\": { w: 198, h: 210 },\r\n    \"a3\": { w: 297, h: 420 },\r\n    \"a4\": { w: 210, h: 297 },\r\n    \"a4-3x-perforated\": { w: 210, h: 297 },\r\n    \"a4-perforated-strips\": { w: 210, h: 297 },\r\n    \"a5\": { w: 148, h: 210 },\r\n    \"a6\": { w: 105, h: 148 },\r\n    \"a7\": { w: 74, h: 105 },\r\n    \"dl\": { w: 99, h: 210 }\r\n  };\r\n\r\n  const FOLD_PREVIEW_PRODUCT_IDS = [701, 32516];\r\n  const uploadState = new Map();\r\n\r\n  function mmToPx(mm) {\r\n    return Math.round((mm \/ 25.4) * DPI);\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 getCardData($card) {\r\n    return {\r\n      cartKey: ($card.attr('data-cart-item-key') || '').trim(),\r\n      productId: parseInt($card.attr('data-product-id') || '0', 10) || 0,\r\n      dimension: (($card.attr('data-dimension') || '') + '').trim().toLowerCase(),\r\n      printingSides: (($card.attr('data-printing-sides') || '') + '').trim().toLowerCase(),\r\n      printColor: (($card.attr('data-print-color') || '') + '').trim().toLowerCase(),\r\n      tearoffColor: (($card.attr('data-tearoff-color') || '') + '').trim().toLowerCase(),\r\n      folded: FOLD_PREVIEW_PRODUCT_IDS.includes(parseInt($card.attr('data-product-id') || '0', 10) || 0)\r\n    };\r\n  }\r\n\r\n  function needsBackFile(data) {\r\n    const sides = data.printingSides;\r\n    const printColor = data.printColor;\r\n    const tearoffColor = data.tearoffColor;\r\n\r\n    if (sides === 'both-sides') return true;\r\n    if (sides === 'one-side') return false;\r\n\r\n    if (['4-4', '1-1'].includes(printColor)) return true;\r\n    if (['4-0', '1-0'].includes(printColor)) return false;\r\n\r\n    if (['black-both', 'both-sides', 'color-both-sides-4-4'].includes(tearoffColor)) return true;\r\n    if (['black-one-side-1-0', 'color-one-side-4-0', 'one-side'].includes(tearoffColor)) return false;\r\n\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      });\r\n    }\r\n    return uploadState.get(cartKey);\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    const mb = bytes \/ (1024 * 1024);\r\n    return `${mb.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\r\n    const statusText = fileObj.status === 'uploaded'\r\n      ? 'Uploaded'\r\n      : (fileObj.progress || 0) + '%';\r\n\r\n    const progressWidth = fileObj.status === 'uploaded'\r\n      ? 100\r\n      : (fileObj.progress || 0);\r\n\r\n    const subParts = [];\r\n    if (fileObj.sizeText) subParts.push(`<span>${fileObj.sizeText}<\/span>`);\r\n    if (fileObj.pagesText) subParts.push(`<span class=\"s4u-meta-dot\"><\/span><span>${fileObj.pagesText}<\/span>`);\r\n    if (fileObj.timeLeftText && fileObj.status !== 'uploaded') {\r\n      subParts.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:\/\/test.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\">${subParts.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:\/\/test.sticker4u.dk\/wp-content\/uploads\/2026\/03\/hugeicons_delete-02.svg\" alt=\"\" \/>\r\n          <\/button>\r\n          <div class=\"s4u-file-status\">${statusText}<\/div>\r\n        <\/div>\r\n        <div class=\"s4u-progress\">\r\n          <div class=\"s4u-progress-bar\" style=\"width:${progressWidth}%;\"><\/div>\r\n        <\/div>\r\n      <\/div>\r\n    `;\r\n  }\r\n\r\n  function renderSlotFiles(cartKey) {\r\n    const state = getOrCreateState(cartKey);\r\n    $slotsWrap.find('[data-file-list=\"front\"]').html(state.front ? renderFileCard(state.front, 'front') : '');\r\n    $slotsWrap.find('[data-file-list=\"back\"]').html(state.back ? renderFileCard(state.back, 'back') : '');\r\n  }\r\n\r\n  function validateUploadedFile(file, side, onValid) {\r\n    const $activeCard = getActiveCard();\r\n    if (!$activeCard.length) {\r\n      alert('Please select a product first.');\r\n      return;\r\n    }\r\n\r\n    const data = getCardData($activeCard);\r\n    const maxBytes = MAX_MB * 1024 * 1024;\r\n\r\n    if (file.size > maxBytes) {\r\n      alert(`This file exceeds the maximum allowed size of ${MAX_MB} MB. Please upload a smaller file.`);\r\n      return;\r\n    }\r\n\r\n    const dim = DIMENSION_MAP[data.dimension];\r\n    if (!dim) {\r\n      onValid(file);\r\n      return;\r\n    }\r\n\r\n    const type = (file.type || '').toLowerCase();\r\n    const isImage = ['image\/jpeg', 'image\/jpg', 'image\/png', 'image\/webp'].includes(type);\r\n\r\n    if (!isImage) {\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 = function (e) {\r\n      const img = new Image();\r\n\r\n      img.onload = function () {\r\n        const actualW = img.width;\r\n        const actualH = img.height;\r\n\r\n        const passNormal = actualW >= requiredW && actualH >= requiredH;\r\n        const passRotated = actualW >= requiredH && actualH >= requiredW;\r\n\r\n        if (!passNormal && !passRotated) {\r\n          alert(\r\n            'This image is too small for high-quality printing.\\n\\n' +\r\n            'Minimum recommended size for this format is ' +\r\n            requiredW + ' \u00d7 ' + requiredH + ' px at 300 DPI.\\n\\n' +\r\n            'Please upload a higher-resolution file to continue.'\r\n          );\r\n          return;\r\n        }\r\n\r\n        onValid(file);\r\n      };\r\n\r\n      img.onerror = function () {\r\n        alert('We could not read this image. Please upload a valid JPG, PNG, or WEBP file.');\r\n      };\r\n\r\n      img.src = e.target.result;\r\n    };\r\n\r\n    reader.onerror = function () {\r\n      alert('We could not process this file. Please try again.');\r\n    };\r\n\r\n    reader.readAsDataURL(file);\r\n  }\r\n\r\n  const preview3D = {\r\n    scene: null,\r\n    camera: null,\r\n    renderer: null,\r\n    controls: null,\r\n    root: null,\r\n    flatBase: null,\r\n    frontPlane: null,\r\n    backPlane: null,\r\n    leftBase: null,\r\n    rightHinge: null,\r\n    rightBase: null,\r\n    frontPlaneL: null,\r\n    frontPlaneR: null,\r\n    backPlaneL: null,\r\n    backPlaneR: null,\r\n    mode: 'flat',\r\n    targetFold: 0,\r\n    foldProgress: 0,\r\n    isFoldedOpen: false,\r\n\r\n    init() {\r\n      this.scene = new THREE.Scene();\r\n\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\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 light = new THREE.DirectionalLight(0xffffff, 1);\r\n      light.position.set(5, 10, 7);\r\n      this.scene.add(light);\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');\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      $foldBtn.off('click.s4u3d').on('click.s4u3d', () => {\r\n        if (this.mode !== 'fold') return;\r\n        this.isFoldedOpen = !this.isFoldedOpen;\r\n        this.targetFold = this.isFoldedOpen ? 1 : 0;\r\n      });\r\n\r\n      $(window).on('resize.s4uPreview3d', () => this.resize());\r\n    },\r\n\r\n    resize() {\r\n      const width = $previewStage.innerWidth() || 900;\r\n      const height = $previewStage.innerHeight() || 560;\r\n      this.camera.aspect = width \/ height;\r\n      this.camera.updateProjectionMatrix();\r\n      this.renderer.setSize(width, height);\r\n    },\r\n\r\n    clearSceneObjects() {\r\n      while (this.root.children.length) {\r\n        const child = this.root.children[0];\r\n        this.root.remove(child);\r\n      }\r\n      this.flatBase = null;\r\n      this.frontPlane = null;\r\n      this.backPlane = null;\r\n      this.leftBase = null;\r\n      this.rightHinge = null;\r\n      this.rightBase = null;\r\n      this.frontPlaneL = null;\r\n      this.frontPlaneR = null;\r\n      this.backPlaneL = null;\r\n      this.backPlaneR = null;\r\n    },\r\n\r\n    setMode(mode) {\r\n      this.mode = mode === 'fold' ? 'fold' : 'flat';\r\n      this.clearSceneObjects();\r\n\r\n      if (this.mode === 'fold') {\r\n        this.buildFoldCard();\r\n        $foldBtn.show();\r\n      } else {\r\n        this.buildFlatCard();\r\n        $foldBtn.hide();\r\n        this.targetFold = 0;\r\n        this.foldProgress = 0;\r\n        this.isFoldedOpen = false;\r\n      }\r\n    },\r\n\r\n    buildFlatCard() {\r\n      const baseGeo = new THREE.BoxGeometry(4, 2.5, 0.06);\r\n      const baseMat = new THREE.MeshStandardMaterial({ color: 0xffffff });\r\n      this.flatBase = new THREE.Mesh(baseGeo, baseMat);\r\n      this.root.add(this.flatBase);\r\n\r\n      const planeGeo = new THREE.PlaneGeometry(3.96, 2.46);\r\n\r\n      const frontMat = new THREE.MeshBasicMaterial({\r\n        color: 0xffffff,\r\n        transparent: false,\r\n        side: THREE.FrontSide\r\n      });\r\n      this.frontPlane = new THREE.Mesh(planeGeo, frontMat);\r\n      this.frontPlane.position.z = 0.031;\r\n      this.root.add(this.frontPlane);\r\n\r\n      const backMat = new THREE.MeshBasicMaterial({\r\n        color: 0xffffff,\r\n        transparent: false,\r\n        side: THREE.FrontSide\r\n      });\r\n      this.backPlane = new THREE.Mesh(planeGeo, backMat);\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    buildFoldCard() {\r\n      const panelW = 2;\r\n      const panelH = 2.5;\r\n      const depth = 0.06;\r\n      const planeGeo = new THREE.PlaneGeometry(1.96, 2.46);\r\n      const baseMat = new THREE.MeshStandardMaterial({ color: 0xffffff });\r\n\r\n      this.leftBase = new THREE.Mesh(new THREE.BoxGeometry(panelW, panelH, depth), baseMat.clone());\r\n      this.leftBase.position.x = -1;\r\n      this.root.add(this.leftBase);\r\n\r\n      this.rightHinge = new THREE.Group();\r\n      this.rightHinge.position.x = 0;\r\n      this.root.add(this.rightHinge);\r\n\r\n      this.rightBase = new THREE.Mesh(new THREE.BoxGeometry(panelW, panelH, depth), baseMat.clone());\r\n      this.rightBase.position.x = 1;\r\n      this.rightHinge.add(this.rightBase);\r\n\r\n      this.frontPlaneL = new THREE.Mesh(planeGeo, new THREE.MeshBasicMaterial({ color: 0xffffff, side: THREE.FrontSide }));\r\n      this.frontPlaneL.position.set(-1, 0, 0.031);\r\n      this.root.add(this.frontPlaneL);\r\n\r\n      this.backPlaneL = new THREE.Mesh(planeGeo, new THREE.MeshBasicMaterial({ color: 0xffffff, side: THREE.FrontSide }));\r\n      this.backPlaneL.rotation.y = Math.PI;\r\n      this.backPlaneL.position.set(-1, 0, -0.031);\r\n      this.root.add(this.backPlaneL);\r\n\r\n      this.frontPlaneR = new THREE.Mesh(planeGeo, new THREE.MeshBasicMaterial({ color: 0xffffff, side: THREE.FrontSide }));\r\n      this.frontPlaneR.position.set(1, 0, 0.031);\r\n      this.rightHinge.add(this.frontPlaneR);\r\n\r\n      this.backPlaneR = new THREE.Mesh(planeGeo, new THREE.MeshBasicMaterial({ color: 0xffffff, side: THREE.FrontSide }));\r\n      this.backPlaneR.rotation.y = Math.PI;\r\n      this.backPlaneR.position.set(1, 0, -0.031);\r\n      this.rightHinge.add(this.backPlaneR);\r\n    },\r\n\r\n    loadTextureFromFile(file, done) {\r\n      if (!file) {\r\n        done(null);\r\n        return;\r\n      }\r\n\r\n      const okTypes = ['image\/jpeg', 'image\/jpg', 'image\/png', 'image\/webp'];\r\n      if (!okTypes.includes((file.type || '').toLowerCase())) {\r\n        done(null);\r\n        return;\r\n      }\r\n\r\n      const reader = new FileReader();\r\n      reader.onload = function (e) {\r\n        const img = new Image();\r\n        img.onload = function () {\r\n          const texture = new THREE.TextureLoader().load(img.src, () => {\r\n            texture.needsUpdate = true;\r\n            if ('colorSpace' in texture) {\r\n              texture.colorSpace = THREE.SRGBColorSpace;\r\n            } else if ('encoding' in texture) {\r\n              texture.encoding = THREE.sRGBEncoding;\r\n            }\r\n            texture.minFilter = THREE.LinearFilter;\r\n            texture.magFilter = THREE.LinearFilter;\r\n            done(texture);\r\n          });\r\n        };\r\n        img.onerror = function () {\r\n          done(null);\r\n        };\r\n        img.src = e.target.result;\r\n      };\r\n      reader.onerror = function () {\r\n        done(null);\r\n      };\r\n      reader.readAsDataURL(file);\r\n    },\r\n\r\n    applyFrontFile(file) {\r\n      this.loadTextureFromFile(file, (texture) => {\r\n        if (this.mode === 'fold') {\r\n          if (this.frontPlaneL && this.frontPlaneL.material.map) this.frontPlaneL.material.map.dispose();\r\n          if (this.frontPlaneR && this.frontPlaneR.material.map) this.frontPlaneR.material.map.dispose();\r\n          if (this.frontPlaneL) {\r\n            this.frontPlaneL.material.map = texture || null;\r\n            this.frontPlaneL.material.needsUpdate = true;\r\n          }\r\n          if (this.frontPlaneR) {\r\n            this.frontPlaneR.material.map = texture || null;\r\n            this.frontPlaneR.material.needsUpdate = true;\r\n          }\r\n          return;\r\n        }\r\n\r\n        if (!this.frontPlane) return;\r\n        if (this.frontPlane.material.map) this.frontPlane.material.map.dispose();\r\n        this.frontPlane.material.map = texture || null;\r\n        this.frontPlane.material.needsUpdate = true;\r\n      });\r\n    },\r\n\r\n    applyBackFile(file) {\r\n      this.loadTextureFromFile(file, (texture) => {\r\n        if (this.mode === 'fold') {\r\n          if (this.backPlaneL && this.backPlaneL.material.map) this.backPlaneL.material.map.dispose();\r\n          if (this.backPlaneR && this.backPlaneR.material.map) this.backPlaneR.material.map.dispose();\r\n          if (this.backPlaneL) {\r\n            this.backPlaneL.material.map = texture || null;\r\n            this.backPlaneL.material.needsUpdate = true;\r\n          }\r\n          if (this.backPlaneR) {\r\n            this.backPlaneR.material.map = texture || null;\r\n            this.backPlaneR.material.needsUpdate = true;\r\n          }\r\n          return;\r\n        }\r\n\r\n        if (!this.backPlane) return;\r\n        if (this.backPlane.material.map) this.backPlane.material.map.dispose();\r\n        this.backPlane.material.map = texture || 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        if (this.frontPlaneL && this.frontPlaneL.material.map) this.frontPlaneL.material.map.dispose();\r\n        if (this.frontPlaneR && this.frontPlaneR.material.map) this.frontPlaneR.material.map.dispose();\r\n        if (this.frontPlaneL) {\r\n          this.frontPlaneL.material.map = null;\r\n          this.frontPlaneL.material.needsUpdate = true;\r\n        }\r\n        if (this.frontPlaneR) {\r\n          this.frontPlaneR.material.map = null;\r\n          this.frontPlaneR.material.needsUpdate = true;\r\n        }\r\n        return;\r\n      }\r\n\r\n      if (!this.frontPlane) return;\r\n      if (this.frontPlane.material.map) this.frontPlane.material.map.dispose();\r\n      this.frontPlane.material.map = null;\r\n      this.frontPlane.material.needsUpdate = true;\r\n    },\r\n\r\n    clearBack() {\r\n      if (this.mode === 'fold') {\r\n        if (this.backPlaneL && this.backPlaneL.material.map) this.backPlaneL.material.map.dispose();\r\n        if (this.backPlaneR && this.backPlaneR.material.map) this.backPlaneR.material.map.dispose();\r\n        if (this.backPlaneL) {\r\n          this.backPlaneL.material.map = null;\r\n          this.backPlaneL.material.needsUpdate = true;\r\n        }\r\n        if (this.backPlaneR) {\r\n          this.backPlaneR.material.map = null;\r\n          this.backPlaneR.material.needsUpdate = true;\r\n        }\r\n        return;\r\n      }\r\n\r\n      if (!this.backPlane) return;\r\n      if (this.backPlane.material.map) this.backPlane.material.map.dispose();\r\n      this.backPlane.material.map = null;\r\n      this.backPlane.material.needsUpdate = true;\r\n    },\r\n\r\n    syncFromState(state) {\r\n      if (!state) return;\r\n\r\n      this.setMode(state.folded ? 'fold' : 'flat');\r\n\r\n      if (state.frontFile) this.applyFrontFile(state.frontFile);\r\n      else this.clearFront();\r\n\r\n      if (state.mode === 'double' && state.backFile) this.applyBackFile(state.backFile);\r\n      else this.clearBack();\r\n    },\r\n\r\n    animate() {\r\n      const loop = () => {\r\n        requestAnimationFrame(loop);\r\n\r\n        if (this.mode === 'fold' && this.rightHinge) {\r\n          this.foldProgress += (this.targetFold - this.foldProgress) * 0.08;\r\n          this.rightHinge.rotation.y = -Math.PI * this.foldProgress;\r\n        }\r\n\r\n        this.controls.update();\r\n        this.renderer.render(this.scene, this.camera);\r\n      };\r\n      loop();\r\n    }\r\n  };\r\n\r\n  preview3D.init();\r\n\r\n  function attachMockUpload(side, file) {\r\n    if (!activeCartKey) return;\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 Page',\r\n      timeLeftText: 'Uploading...',\r\n      progress: 15,\r\n      status: 'uploading'\r\n    };\r\n\r\n    if (side === 'front') {\r\n      state.frontFile = file;\r\n      preview3D.applyFrontFile(file);\r\n    } else {\r\n      state.backFile = file;\r\n      preview3D.applyBackFile(file);\r\n    }\r\n\r\n    renderSlotFiles(activeCartKey);\r\n\r\n    let progress = 15;\r\n    const thisCartKey = activeCartKey;\r\n\r\n    const timer = setInterval(() => {\r\n      if (!uploadState.has(thisCartKey)) {\r\n        clearInterval(timer);\r\n        return;\r\n      }\r\n\r\n      const current = uploadState.get(thisCartKey);\r\n      if (!current[side]) {\r\n        clearInterval(timer);\r\n        return;\r\n      }\r\n\r\n      progress += 17;\r\n\r\n      if (progress >= 100) {\r\n        current[side].progress = 100;\r\n        current[side].status = 'uploaded';\r\n        current[side].timeLeftText = '';\r\n        if (activeCartKey === thisCartKey) renderSlotFiles(thisCartKey);\r\n        clearInterval(timer);\r\n        return;\r\n      }\r\n\r\n      current[side].progress = progress;\r\n      current[side].timeLeftText = '2 sec left';\r\n      if (activeCartKey === thisCartKey) renderSlotFiles(thisCartKey);\r\n    }, 350);\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\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    const doubleMode = needsBackFile(data);\r\n\r\n    state.mode = doubleMode ? 'double' : 'single';\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      preview3D.clearBack();\r\n    }\r\n\r\n    renderSlotFiles(data.cartKey);\r\n    preview3D.syncFromState(state);\r\n  }\r\n\r\n  function ensureInitialSelection() {\r\n    let $active = getActiveCard();\r\n    if (!$active.length) {\r\n      $active = $('.cps-card').first();\r\n      if ($active.length) {\r\n        renderPanelForCard($active);\r\n      }\r\n    } else {\r\n      renderPanelForCard($active);\r\n    }\r\n  }\r\n\r\n  $(document)\r\n    .off('click.s4uCardMerged', '.cps-card')\r\n    .on('click.s4uCardMerged', '.cps-card', function (e) {\r\n      if ($(e.target).closest('.cps-info-icon, .cps-remove-text, .cps-modal, .cps-modal-close').length) {\r\n        return;\r\n      }\r\n      renderPanelForCard($(this));\r\n    });\r\n\r\n  $(document)\r\n    .off('click.s4uInfoMerged', '.cps-info-icon')\r\n    .on('click.s4uInfoMerged', '.cps-info-icon', function () {\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)\r\n    .off('click.s4uModalCloseMerged', '.cps-modal-close')\r\n    .on('click.s4uModalCloseMerged', '.cps-modal-close', function () {\r\n      setTimeout(function () {\r\n        const $active = getActiveCard();\r\n        if ($active.length) renderPanelForCard($active);\r\n      }, 100);\r\n    });\r\n\r\n  $(document)\r\n    .off('click.s4uUploadMerged', '[data-trigger-upload]')\r\n    .on('click.s4uUploadMerged', '[data-trigger-upload]', function () {\r\n      const side = ($(this).attr('data-trigger-upload') || '').trim();\r\n      const inputId = side === 'back' ? '#s4u-file-back' : '#s4u-file-front';\r\n      const $input = $(inputId);\r\n      if ($input.length) $input.trigger('click');\r\n    });\r\n\r\n  $('#s4u-file-front')\r\n    .off('change.s4u')\r\n    .on('change.s4u', function () {\r\n      const file = this.files && this.files[0];\r\n      const input = this;\r\n      if (!file) return;\r\n\r\n      validateUploadedFile(file, 'front', function (validFile) {\r\n        attachMockUpload('front', validFile);\r\n      });\r\n\r\n      input.value = '';\r\n    });\r\n\r\n  $('#s4u-file-back')\r\n    .off('change.s4u')\r\n    .on('change.s4u', function () {\r\n      const file = this.files && this.files[0];\r\n      const input = this;\r\n      if (!file) return;\r\n\r\n      validateUploadedFile(file, 'back', function (validFile) {\r\n        attachMockUpload('back', validFile);\r\n      });\r\n\r\n      input.value = '';\r\n    });\r\n\r\n  $(document)\r\n    .off('click.s4uDeleteMerged', '[data-delete-side]')\r\n    .on('click.s4uDeleteMerged', '[data-delete-side]', function () {\r\n      const side = ($(this).attr('data-delete-side') || '').trim();\r\n      if (!activeCartKey || !side) return;\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      renderSlotFiles(activeCartKey);\r\n    });\r\n\r\n  function injectContinueButton() {\r\n    if ($('#s4u-continue-checkout').length) return;\r\n\r\n    const html = `\r\n      <div class=\"s4u-continue-wrap\">\r\n        <button type=\"button\" id=\"s4u-continue-checkout\" class=\"s4u-continue-btn\">\r\n          Continue to checkout\r\n        <\/button>\r\n      <\/div>\r\n    `;\r\n\r\n    if ($('#s4u-preview-wrap').length) {\r\n      $('#s4u-preview-wrap').after(html);\r\n    } else {\r\n      $slotsWrap.after(html);\r\n    }\r\n  }\r\n\r\n  injectContinueButton();\r\n\r\n  function validateBeforeContinue() {\r\n    const $active = getActiveCard();\r\n    if (!$active.length) {\r\n      alert('Please select a product first.');\r\n      return false;\r\n    }\r\n\r\n    const data = getCardData($active);\r\n    const isDouble = needsBackFile(data);\r\n    const state = getOrCreateState(data.cartKey);\r\n\r\n    const hasFront = !!state.frontFile;\r\n    const hasBack = !!state.backFile;\r\n\r\n    if (!hasFront) {\r\n      alert('Please upload the design file before continuing.');\r\n      return false;\r\n    }\r\n\r\n    if (isDouble && !hasBack) {\r\n      alert('Please upload the back design file before continuing.');\r\n      return false;\r\n    }\r\n\r\n    return true;\r\n  }\r\n\r\n  function getCheckoutUrl() {\r\n    if (typeof wc_checkout_params !== 'undefined' && wc_checkout_params.checkout_url) {\r\n      return wc_checkout_params.checkout_url;\r\n    }\r\n    return '\/checkout\/';\r\n  }\r\n\r\n  $(document)\r\n    .off('click.s4uContinueMerged', '#s4u-continue-checkout')\r\n    .on('click.s4uContinueMerged', '#s4u-continue-checkout', function () {\r\n      if (!validateBeforeContinue()) return;\r\n\r\n      const $btn = $(this);\r\n      $btn.prop('disabled', true).text('Proceeding...');\r\n\r\n      setTimeout(function () {\r\n        window.location.href = getCheckoutUrl();\r\n      }, 200);\r\n    });\r\n\r\n  setTimeout(function () {\r\n    ensureInitialSelection();\r\n    preview3D.resize();\r\n  }, 250);\r\n\r\n  setTimeout(function () {\r\n    ensureInitialSelection();\r\n    preview3D.resize();\r\n  }, 700);\r\n});\r\n<\/script>\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: JPEG TIFF PDF front-file.pdf [&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:\/\/test.sticker4u.dk\/da\/wp-json\/wp\/v2\/pages\/3443","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/test.sticker4u.dk\/da\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/test.sticker4u.dk\/da\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/test.sticker4u.dk\/da\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/test.sticker4u.dk\/da\/wp-json\/wp\/v2\/comments?post=3443"}],"version-history":[{"count":520,"href":"https:\/\/test.sticker4u.dk\/da\/wp-json\/wp\/v2\/pages\/3443\/revisions"}],"predecessor-version":[{"id":43555,"href":"https:\/\/test.sticker4u.dk\/da\/wp-json\/wp\/v2\/pages\/3443\/revisions\/43555"}],"wp:attachment":[{"href":"https:\/\/test.sticker4u.dk\/da\/wp-json\/wp\/v2\/media?parent=3443"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}