templates/produit/show.html.twig line 1

Open in your IDE?
  1. <!DOCTYPE html>
  2. <html lang="{{ app.session.get('_locale') ?? 'en' }}">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>{{ produit.productName }} โ€” Julico</title>
  7. <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
  8. <link rel="stylesheet" href="/css/julico-home.css">
  9. <style>
  10. *, *::before, *::after { box-sizing: border-box; }
  11. html, body { max-width: 100%; overflow-x: hidden; }
  12. img, video, iframe { max-width: 100%; height: auto; }
  13. body{background:var(--gray-50);min-height:100vh}
  14. .prod-nav{background:var(--white);border-bottom:1px solid var(--gray-200);padding:0 24px;height:68px;display:flex;align-items:center;justify-content:space-between;box-shadow:var(--shadow-sm);position:sticky;top:0;z-index:100}
  15. .prod-body{max-width:1280px;margin:0 auto;padding:28px 24px}
  16. .breadcrumb{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--gray-400);margin-bottom:24px;flex-wrap:wrap}
  17. .breadcrumb a{color:var(--gray-400);text-decoration:none;transition:color .2s}
  18. .breadcrumb a:hover{color:var(--brand)}
  19. .breadcrumb span{color:var(--gray-800);font-weight:600}
  20. .prod-layout{display:grid;grid-template-columns:1fr 1fr;gap:40px;align-items:start}
  21. .main-image{background:var(--white);border:1.5px solid var(--gray-100);border-radius:var(--rlg);overflow:hidden;aspect-ratio:1;display:flex;align-items:center;justify-content:center;margin-bottom:12px;box-shadow:var(--shadow-sm);position:relative}
  22. .main-image img{width:80%;height:80%;object-fit:contain;transition:transform .3s}
  23. .main-image:hover img{transform:scale(1.05)}
  24. .main-image .no-img{font-size:80px;opacity:0.2}
  25. .thumb-grid{display:flex;gap:10px;overflow-x:auto;scrollbar-width:none}
  26. .thumb-grid::-webkit-scrollbar{display:none}
  27. .thumb{width:72px;height:72px;background:var(--white);border:2px solid var(--gray-200);border-radius:var(--rsm);overflow:hidden;cursor:pointer;flex-shrink:0;display:flex;align-items:center;justify-content:center;transition:border-color .2s}
  28. .thumb:hover,.thumb.active{border-color:var(--brand)}
  29. .thumb img{width:100%;height:100%;object-fit:contain;pointer-events:none}
  30. .prod-info{}
  31. .prod-badges{display:flex;gap:8px;margin-bottom:14px;flex-wrap:wrap}
  32. .prod-badge{font-size:11px;font-weight:700;padding:4px 10px;border-radius:100px}
  33. .badge-promo{background:#fef3c7;color:#b45309}
  34. .badge-new{background:#dcfce7;color:#166534}
  35. .badge-b2b{background:var(--brand-light);color:var(--brand-dark)}
  36. .badge-oos{background:#fee2e2;color:#991b1b}
  37. .prod-name{font-size:28px;font-weight:800;color:var(--gray-900);letter-spacing:-0.02em;line-height:1.2;margin-bottom:8px}
  38. .prod-cat{font-size:13px;font-weight:600;color:var(--brand);margin-bottom:16px}
  39. .prod-desc{font-size:14px;color:var(--gray-500);line-height:1.7;margin-bottom:20px}
  40. .prod-price-box{background:var(--brand-light);border:1.5px solid var(--brand-mid);border-radius:var(--r);padding:20px;margin-bottom:14px}
  41. .prod-price{font-size:36px;font-weight:800;color:var(--brand);letter-spacing:-0.02em;line-height:1}
  42. .prod-price sup{font-size:18px;font-weight:700;vertical-align:super}
  43. .prod-old-price{font-size:16px;color:var(--gray-400);text-decoration:line-through;font-weight:500;margin-top:4px}
  44. .prod-tax{font-size:12px;color:var(--gray-400);margin-top:6px}
  45. .delivery-card{background:var(--white);border:1.5px solid var(--gray-100);border-radius:var(--r);padding:14px 16px;margin-bottom:18px;display:flex;align-items:flex-start;gap:12px}
  46. .delivery-card.ok{background:linear-gradient(135deg,#f0fdf4,#ecfdf5);border-color:#86efac}
  47. .delivery-card.no{background:linear-gradient(135deg,#fef2f2,#fee2e2);border-color:#fca5a5}
  48. .delivery-card.warn{background:linear-gradient(135deg,#fffbeb,#fef3c7);border-color:#fcd34d}
  49. .delivery-card .dc-icon{font-size:22px;line-height:1;flex:0 0 22px}
  50. .delivery-card .dc-body{flex:1;font-size:13px;line-height:1.5}
  51. .delivery-card .dc-title{font-weight:800;color:var(--gray-900);margin-bottom:2px}
  52. .delivery-card.ok .dc-title{color:#166534}
  53. .delivery-card.no .dc-title{color:#991b1b}
  54. .delivery-card.warn .dc-title{color:#92400e}
  55. .delivery-card .dc-sub{color:var(--gray-500);font-size:12px}
  56. .delivery-card .dc-sub a{color:var(--brand);font-weight:700;text-decoration:none}
  57. .delivery-card .dc-fee{font-weight:800;color:var(--gray-900);font-size:14px}
  58. .prod-stock{display:flex;align-items:center;gap:6px;font-size:13px;font-weight:600;margin-bottom:20px}
  59. .stock-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
  60. .stock-in{background:#22c55e}
  61. .stock-out{background:#ef4444}
  62. .variants-section{margin-bottom:20px}
  63. .variants-label{font-size:13px;font-weight:700;color:var(--gray-700);margin-bottom:10px}
  64. .color-options{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:14px}
  65. .color-opt{width:44px;height:44px;border-radius:50%;border:3px solid transparent;cursor:pointer;transition:all .2s;outline:2px solid var(--gray-200);touch-action:manipulation;-webkit-tap-highlight-color:transparent}
  66. .color-opt:hover,.color-opt.active{outline-color:var(--brand);outline-offset:2px}
  67. .size-options{display:flex;gap:8px;flex-wrap:wrap}
  68. .size-opt{min-height:44px;padding:10px 18px;border:1.5px solid var(--gray-200);border-radius:var(--rsm);font-size:14px;font-weight:600;color:var(--gray-700);cursor:pointer;transition:all .2s;background:var(--white);font-family:var(--font);touch-action:manipulation;-webkit-tap-highlight-color:transparent}
  69. .size-opt:hover,.size-opt.active{border-color:var(--brand);color:var(--brand);background:var(--brand-light)}
  70. .variant-list{display:flex;flex-direction:column;gap:10px;margin-bottom:6px}
  71. .variant-card{display:flex;flex-direction:row;align-items:center;gap:12px;width:100%;text-align:left;padding:10px 12px;border:1.5px solid var(--gray-200);border-radius:var(--rsm);background:var(--white);cursor:pointer;font-family:var(--font);transition:all .2s;-webkit-tap-highlight-color:transparent}
  72. .variant-card:hover{border-color:var(--brand);background:var(--brand-light)}
  73. .variant-card.active{border-color:var(--brand);background:var(--brand-light);box-shadow:0 0 0 3px rgba(0,167,181,.12)}
  74. .vc-thumb{width:56px;height:56px;flex-shrink:0;border-radius:var(--rsm);overflow:hidden;background:var(--gray-50);border:1px solid var(--gray-200);display:flex;align-items:center;justify-content:center}
  75. .vc-thumb img{width:100%;height:100%;object-fit:contain}
  76. .vc-thumb .vc-noimg{font-size:24px;opacity:0.3}
  77. .vc-text{display:flex;flex-direction:column;gap:4px;min-width:0}
  78. .vc-label{font-size:14px;font-weight:700;color:var(--gray-900);line-height:1.3;overflow-wrap:anywhere}
  79. .vc-meta{font-size:12px;font-weight:600;color:var(--gray-500)}
  80. .variant-card.active .vc-meta{color:var(--brand-dark)}
  81. .prod-actions{display:flex;gap:12px;align-items:center;margin-bottom:20px}
  82. .qty-wrap{display:flex;align-items:center;gap:0;border:1.5px solid var(--gray-200);border-radius:var(--rsm);overflow:hidden;background:var(--white)}
  83. .qty-btn-large{width:44px;height:50px;background:var(--white);border:none;color:var(--gray-700);font-size:20px;font-weight:600;cursor:pointer;transition:background .2s;font-family:var(--font)}
  84. .qty-btn-large:hover{background:var(--gray-100)}
  85. .qty-input{width:60px;height:50px;border:none;border-left:1px solid var(--gray-200);border-right:1px solid var(--gray-200);text-align:center;font-family:var(--font);font-size:16px;font-weight:700;color:var(--gray-900);outline:none}
  86. .btn-add-cart{flex:1;height:50px;background:var(--brand);color:white;border:none;border-radius:var(--rsm);font-family:var(--font);font-size:15px;font-weight:700;cursor:pointer;transition:background .2s;display:flex;align-items:center;justify-content:center;gap:8px;touch-action:manipulation}
  87. .btn-add-cart:hover{background:var(--brand-dark)}
  88. .btn-checkout-now{flex:1;height:50px;background:var(--gray-900);color:white;border:none;border-radius:var(--rsm);font-family:var(--font);font-size:15px;font-weight:700;cursor:pointer;transition:background .2s;display:flex;align-items:center;justify-content:center;gap:8px;touch-action:manipulation}
  89. .btn-checkout-now:hover{background:var(--gray-700)}
  90. .platform-note{background:var(--white);border:1.5px solid var(--gray-100);border-radius:var(--r);padding:14px 16px;margin-top:20px;display:flex;align-items:flex-start;gap:10px;box-shadow:var(--shadow-sm)}
  91. .platform-note-icon{font-size:18px;flex-shrink:0;margin-top:1px}
  92. .platform-note-text{font-size:12px;color:var(--gray-500);line-height:1.55}
  93. .platform-note-text strong{color:var(--gray-800);font-weight:700}
  94. .cart-toast{position:fixed;bottom:30px;left:50%;transform:translateX(-50%) translateY(20px);background:var(--gray-900);color:white;padding:14px 24px;border-radius:100px;font-size:13px;font-weight:600;display:flex;align-items:center;gap:12px;box-shadow:0 8px 32px rgba(0,0,0,0.25);z-index:400;opacity:0;pointer-events:none;transition:all .3s;white-space:nowrap}
  95. .cart-toast.show{opacity:1;transform:translateX(-50%) translateY(0);pointer-events:auto}
  96. .cart-toast a{color:var(--brand);font-weight:700;text-decoration:none;margin-left:4px}
  97. @media(max-width:900px){.prod-layout{grid-template-columns:1fr}.prod-actions{flex-wrap:wrap}}
  98. @media (max-width: 768px) {
  99.   .prod-nav { padding: 0 14px !important; height: 60px !important; }
  100.   .prod-nav img { height: 38px !important; }
  101.   .prod-body { padding: 20px 14px !important; }
  102.   .breadcrumb { font-size: 12px !important; margin-bottom: 16px !important; }
  103.   .prod-layout { gap: 22px !important; }
  104.   .prod-name { font-size: 22px !important; }
  105.   .prod-cat { font-size: 12px !important; margin-bottom: 12px !important; }
  106.   .prod-desc { font-size: 13px !important; }
  107.   .prod-price { font-size: 30px !important; }
  108.   .prod-price sup { font-size: 15px !important; }
  109.   .prod-price-box { padding: 16px !important; }
  110.   .delivery-card { padding: 12px 14px !important; }
  111.   .delivery-card .dc-body { font-size: 12px !important; }
  112.   .delivery-card .dc-icon { font-size: 20px !important; }
  113.   .cart-toast {
  114.     left: 16px !important; right: 16px !important; width: auto !important;
  115.     max-width: calc(100vw - 32px) !important; transform: translateY(20px) !important;
  116.     white-space: normal !important; justify-content: center !important; text-align: center !important;
  117.     box-sizing: border-box !important; padding: 12px 18px !important;
  118.   }
  119.   .cart-toast.show { transform: translateY(0) !important; }
  120. }
  121. @media (max-width: 480px) {
  122.   .prod-body { padding: 16px 12px !important; }
  123.   .prod-name { font-size: 20px !important; }
  124.   .prod-price { font-size: 26px !important; }
  125.   .prod-actions { flex-direction: column !important; gap: 10px !important; align-items: stretch !important; }
  126.   .qty-wrap { width: 100% !important; justify-content: center !important; }
  127.   .qty-btn-large { flex: 1 !important; max-width: 80px !important; }
  128.   .qty-input { flex: 1 !important; max-width: 100px !important; }
  129.   .btn-add-cart, .btn-checkout-now { width: 100% !important; flex: none !important; font-size: 14px !important; }
  130.   .platform-note { padding: 12px 14px !important; }
  131.   .platform-note-text { font-size: 11px !important; }
  132.   .cart-toast { bottom: 16px !important; font-size: 12px !important; }
  133. }
  134. @media (max-width: 380px) {
  135.   .prod-nav { padding: 0 10px !important; }
  136.   .prod-nav img { height: 32px !important; }
  137.   .prod-body { padding: 14px 10px !important; }
  138.   .prod-name { font-size: 18px !important; }
  139.   .prod-price { font-size: 24px !important; }
  140. }
  141. </style>
  142. </head>
  143. <body>
  144. <nav class="prod-nav">
  145.   <a href="{{ path('app_home') }}">
  146.     <img src="/images/julico-logo.png" alt="Julico" style="height:48px;object-fit:contain">
  147.   </a>
  148.   <div style="display:flex;align-items:center;gap:12px">
  149.     <a href="{{ path('app_panier') }}" style="display:flex;align-items:center;gap:6px;font-size:13px;font-weight:600;color:var(--gray-600);text-decoration:none;padding:8px 14px;border:1.5px solid var(--gray-200);border-radius:var(--rsm)">
  150.       ๐Ÿ›’ Cart
  151.     </a>
  152.     {% if app.user %}
  153.       <a href="{{ path('app_orders') }}" style="font-size:13px;font-weight:600;color:var(--gray-600);text-decoration:none;padding:8px 14px;border:1.5px solid var(--gray-200);border-radius:var(--rsm)">๐Ÿ“ฆ Orders</a>
  154.       <a href="{{ path('app_profile') }}" style="font-size:13px;font-weight:600;color:var(--brand);text-decoration:none">{{ app.user.pseudo }}</a>
  155.     {% else %}
  156.       <a href="/login" style="font-size:13px;font-weight:600;color:var(--brand);text-decoration:none">Sign In</a>
  157.     {% endif %}
  158.   </div>
  159. </nav>
  160. <div class="prod-body">
  161.   <div class="breadcrumb">
  162.     <a href="{{ path('app_home') }}">Home</a> โ€บ
  163.     {% if produit.category is defined and produit.category %}
  164.       <a href="{{ path('app_home') }}">{{ produit.category.nom }}</a> โ€บ
  165.     {% endif %}
  166.     <span>{{ produit.productName }}</span>
  167.   </div>
  168.   <div class="prod-layout">
  169.     <!-- IMAGES -->
  170.     <div class="prod-images">
  171.       <div class="main-image">
  172.         {% if produit.images|length > 0 %}
  173.           <img id="main-img"
  174.                src="{{ asset('assets/uploads/products/' ~ produit.images|first.fileName) }}"
  175.                alt="{{ produit.productName }}"
  176.                onerror="this.onerror=null;this.parentNode.innerHTML='<div class=no-img>๐Ÿ“ฆ</div>'">
  177.         {% else %}
  178.           <div class="no-img">๐Ÿ“ฆ</div>
  179.         {% endif %}
  180.       </div>
  181.       {% if produit.images|length > 1 %}
  182.       <div class="thumb-grid" id="thumb-grid">
  183.         {% for image in produit.images %}
  184.         <div class="thumb {% if loop.first %}active{% endif %}"
  185.              data-src="{{ asset('assets/uploads/products/' ~ image.fileName) }}"
  186.              data-fallback="/assets/uploads/products/{{ image.fileName }}"
  187.              data-vid="{% if image.produitVariant %}{{ image.produitVariant.id }}{% endif %}">
  188.           <img src="{{ asset('assets/uploads/products/' ~ image.fileName) }}"
  189.                alt="{{ loop.index }}"
  190.                onerror="this.onerror=null;this.src='/assets/uploads/products/{{ image.fileName }}'">
  191.         </div>
  192.         {% endfor %}
  193.       </div>
  194.       {% endif %}
  195.     </div>
  196.     <!-- INFO -->
  197.     <div class="prod-info">
  198.       {% set isFlex = produit.produitVariants|length > 0 and groupedVariantscolor|length == 0 and groupedVariantssize|length == 0 %}
  199.       <div class="prod-badges">
  200.         {% if produit.promo %}<span class="prod-badge badge-promo">๐Ÿ”ฅ On Promo</span>{% endif %}
  201.         {% if produit.qtt > 0 %}
  202.           <span class="prod-badge badge-new">โœ… In Stock</span>
  203.         {% else %}
  204.           <span class="prod-badge badge-oos">โŒ Out of Stock</span>
  205.         {% endif %}
  206.         {% if produit.magasin is defined and produit.magasin %}
  207.           <span class="prod-badge badge-b2b">{{ produit.magasin.nom }}</span>
  208.         {% endif %}
  209.       </div>
  210.       <div class="prod-name">{{ produit.productName }}</div>
  211.       <div class="prod-cat">
  212.         {% if produit.category is defined and produit.category %}{{ produit.category.nom }}{% endif %}
  213.         {% if produit.magasin is defined and produit.magasin %} ยท Sold by {{ produit.magasin.nom }}{% endif %}
  214.       </div>
  215.       <div class="prod-desc">{{ produit.description }}</div>
  216.       <div class="prod-price-box">
  217.         {% if isFlex %}
  218.           <div class="prod-price"><sup>$</sup><span id="disp-price">{{ produit.prix|number_format(2) }}</span></div>
  219.           <div class="prod-tax" id="disp-price-note">Select an option below</div>
  220.         {% elseif produit.promo and produit.salePrice and produit.salePrice > 0 and produit.salePrice < produit.prix %}
  221.           <div class="prod-price"><sup>$</sup>{{ produit.salePrice|number_format(2) }}</div>
  222.           <div class="prod-old-price">${{ produit.prix|number_format(2) }}</div>
  223.           <div class="prod-tax">Price set by seller</div>
  224.         {% else %}
  225.           <div class="prod-price"><sup>$</sup>{{ produit.prix|number_format(2) }}</div>
  226.           <div class="prod-tax">Price set by seller</div>
  227.         {% endif %}
  228.       </div>
  229.       {# โ”€โ”€โ”€โ”€โ”€โ”€ Delivery preview card (Phase 3) โ”€โ”€โ”€โ”€โ”€โ”€ #}
  230.       {% if deliveryInfo is defined and deliveryInfo.shop_id %}
  231.         {% if not app.user %}
  232.           <div class="delivery-card">
  233.             <div class="dc-icon">๐Ÿšš</div>
  234.             <div class="dc-body">
  235.               <div class="dc-title">Sign in to see delivery cost</div>
  236.               <div class="dc-sub"><a href="/login">Sign in</a> and set your delivery address to see if <strong>{{ deliveryInfo.shop_name }}</strong> delivers to your region.</div>
  237.             </div>
  238.           </div>
  239.         {% elseif not deliveryInfo.address_set %}
  240.           <div class="delivery-card warn">
  241.             <div class="dc-icon">๐Ÿ“</div>
  242.             <div class="dc-body">
  243.               <div class="dc-title">Add a delivery address</div>
  244.               <div class="dc-sub"><a href="/addresse/add">Add an address</a> to see delivery availability and fees from <strong>{{ deliveryInfo.shop_name }}</strong>.</div>
  245.             </div>
  246.           </div>
  247.         {% elseif not deliveryInfo.has_region %}
  248.           <div class="delivery-card warn">
  249.             <div class="dc-icon">๐Ÿ“</div>
  250.             <div class="dc-body">
  251.               <div class="dc-title">Set your region</div>
  252.               <div class="dc-sub">Your address has no region set. <a href="{{ path('app_profile') }}">Update your profile</a> to see delivery fees.</div>
  253.             </div>
  254.           </div>
  255.         {% elseif deliveryInfo.can_deliver is null %}
  256.           <div class="delivery-card warn">
  257.             <div class="dc-icon">๐Ÿšš</div>
  258.             <div class="dc-body">
  259.               <div class="dc-title">Delivery info unavailable</div>
  260.               <div class="dc-sub"><strong>{{ deliveryInfo.shop_name }}</strong> has not configured delivery for <strong>{{ deliveryInfo.region_name }}</strong> yet.</div>
  261.             </div>
  262.           </div>
  263.         {% elseif not deliveryInfo.can_deliver %}
  264.           <div class="delivery-card no">
  265.             <div class="dc-icon">โŒ</div>
  266.             <div class="dc-body">
  267.               <div class="dc-title">Cannot deliver to {{ deliveryInfo.region_name }}</div>
  268.               <div class="dc-sub"><strong>{{ deliveryInfo.shop_name }}</strong> does not currently deliver to your region.</div>
  269.             </div>
  270.           </div>
  271.         {% else %}
  272.           <div class="delivery-card ok">
  273.             <div class="dc-icon">โœ…</div>
  274.             <div class="dc-body">
  275.               <div class="dc-title">Delivers to {{ deliveryInfo.region_name }}</div>
  276.               <div class="dc-sub">Delivery fee: <span class="dc-fee">${{ deliveryInfo.fee|number_format(2) }}</span> set by {{ deliveryInfo.shop_name }}</div>
  277.             </div>
  278.           </div>
  279.         {% endif %}
  280.       {% endif %}
  281.       <div class="prod-stock">
  282.         <span class="stock-dot {{ produit.qtt > 0 ? 'stock-in' : 'stock-out' }}"></span>
  283.         <span id="disp-stock" style="color:{{ produit.qtt > 0 ? '#16a34a' : '#dc2626' }}">
  284.           {% if produit.qtt > 0 %}In Stock ({{ produit.qtt }} available){% else %}Out of Stock{% endif %}
  285.         </span>
  286.       </div>
  287.       {# โ”€โ”€ OLD color/size variants (legacy products) โ”€โ”€ #}
  288.       {% if groupedVariantscolor|length > 0 %}
  289.       <div class="variants-section">
  290.         <div class="variants-label">Color:</div>
  291.         <div class="color-options">
  292.           {% for color, variants in groupedVariantscolor %}
  293.           <div class="color-opt" data-color="{{ color }}"
  294.                style="background-color:{{ color }}" title="{{ color }}"></div>
  295.           {% endfor %}
  296.         </div>
  297.       </div>
  298.       {% endif %}
  299.       {% if groupedVariantssize|length > 0 %}
  300.       <div class="variants-section">
  301.         <div class="variants-label">Size:</div>
  302.         <div class="size-options">
  303.           {% for size, variants in groupedVariantssize %}
  304.           <button type="button" class="size-opt" data-size="{{ size }}">{{ size }}</button>
  305.           {% endfor %}
  306.         </div>
  307.       </div>
  308.       {% endif %}
  309.       {# โ”€โ”€ NEW flexible variants (Scent/Size/etc. typed by the vendor) โ”€โ”€ #}
  310.       {% if isFlex %}
  311.       <div class="variants-section">
  312.         <div class="variants-label">
  313.           {% set dimNames = [] %}
  314.           {% if produit.variantType %}{% set dimNames = dimNames|merge([produit.variantType]) %}{% endif %}
  315.           {% if produit.variantType2 %}{% set dimNames = dimNames|merge([produit.variantType2]) %}{% endif %}
  316.           {% if produit.variantType3 %}{% set dimNames = dimNames|merge([produit.variantType3]) %}{% endif %}
  317.           Choose {% if dimNames|length > 0 %}{{ dimNames|join(' / ') }}{% else %}an option{% endif %}:
  318.         </div>
  319.         <div class="variant-list" id="variant-list">
  320.           {% for v in produit.produitVariants %}
  321.             {% set vals = [] %}
  322.             {% if v.optionLabel %}{% set vals = vals|merge([v.optionLabel]) %}{% endif %}
  323.             {% if v.optionLabel2 %}{% set vals = vals|merge([v.optionLabel2]) %}{% endif %}
  324.             {% if v.optionLabel3 %}{% set vals = vals|merge([v.optionLabel3]) %}{% endif %}
  325.             {% if vals|length > 0 %}
  326.               {% set vimg = v.images|length > 0 ? asset('assets/uploads/products/' ~ v.images|first.fileName) : '' %}
  327.               <button type="button" class="variant-card {% if loop.first %}active{% endif %}"
  328.                       data-vid="{{ v.id }}"
  329.                       data-price="{{ v.prix }}"
  330.                       data-stock="{{ v.qtt }}"
  331.                       data-img="{{ vimg }}">
  332.                 <span class="vc-thumb">
  333.                   {% if vimg %}
  334.                     <img src="{{ vimg }}" alt="{{ vals|join(' ') }}"
  335.                          onerror="this.onerror=null;this.style.display='none';this.parentNode.innerHTML='<span class=vc-noimg>๐Ÿ“ฆ</span>'">
  336.                   {% else %}
  337.                     <span class="vc-noimg">๐Ÿ“ฆ</span>
  338.                   {% endif %}
  339.                 </span>
  340.                 <span class="vc-text">
  341.                   <span class="vc-label">{{ vals|join(' ยท ') }}</span>
  342.                   <span class="vc-meta">${{ v.prix|number_format(2) }} ยท {{ v.qtt|number_format(0) }} in stock</span>
  343.                 </span>
  344.               </button>
  345.             {% endif %}
  346.           {% endfor %}
  347.         </div>
  348.       </div>
  349.       {% endif %}
  350.       {% if produit.qtt > 0 %}
  351.       <form method="post" id="cart-form">
  352.         <input type="hidden" id="chosenColor" name="chosenColor">
  353.         <input type="hidden" id="chosenSize"  name="chosenSize">
  354.         <input type="hidden" name="productid"   value="{{ produit.id }}">
  355.         <input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
  356.         <div class="prod-actions">
  357.           <div class="qty-wrap">
  358.             <button type="button" class="qty-btn-large" id="qty-minus">โˆ’</button>
  359.             <input type="number" class="qty-input" id="qttprdid" name="qttprdid"
  360.                    min="1" max="{{ produit.qtt }}" value="1">
  361.             <button type="button" class="qty-btn-large" id="qty-plus">+</button>
  362.           </div>
  363.           <button type="button" class="btn-add-cart" id="btn-add-cart"
  364.                   data-add-url="{{ path('panier_add', {'id': produit.id, 'idVariante': 'notdefined', 'option': 'continue'}) }}"
  365.                   data-add-base="{{ path('panier_add', {'id': produit.id, 'idVariante': '__VID__', 'option': 'continue'}) }}">
  366.             ๐Ÿ›’ Add to Cart
  367.           </button>
  368.           <button type="submit" class="btn-checkout-now" id="btn-buy-now"
  369.                   formaction="{{ path('panier_add', {'id': produit.id, 'idVariante': 'notdefined', 'option': 'done'}) }}"
  370.                   data-buy-base="{{ path('panier_add', {'id': produit.id, 'idVariante': '__VID__', 'option': 'done'}) }}">
  371.             โšก Buy Now
  372.           </button>
  373.         </div>
  374.       </form>
  375.       {% else %}
  376.       <div style="background:#fee2e2;border:1.5px solid #fecaca;border-radius:var(--rsm);padding:16px;text-align:center;color:#dc2626;font-weight:600;margin-bottom:20px">
  377.         โŒ This product is currently out of stock
  378.       </div>
  379.       {% endif %}
  380.       <div class="platform-note">
  381.         <span class="platform-note-icon">โ„น๏ธ</span>
  382.         <div class="platform-note-text">
  383.           <strong>Julico is a marketplace platform.</strong>
  384.           Pricing, delivery, and transactions are handled directly between you and the seller. Julico is not responsible for shipping, payments, or returns.
  385.         </div>
  386.       </div>
  387.     </div>
  388.   </div>
  389. </div>
  390. <div class="cart-toast" id="cart-toast">
  391.   <span>โœ“ Added to cart!</span>
  392.   <a href="{{ path('app_panier') }}">View Cart โ†’</a>
  393. </div>
  394. <script>
  395. document.addEventListener('DOMContentLoaded', function() {
  396.   var thumbGrid = document.getElementById('thumb-grid');
  397.   var mainImgEl = document.getElementById('main-img');
  398.   var variantList = document.getElementById('variant-list');
  399.   var dispPrice = document.getElementById('disp-price');
  400.   var dispNote  = document.getElementById('disp-price-note');
  401.   var dispStock = document.getElementById('disp-stock');
  402.   var addBtnEl  = document.getElementById('btn-add-cart');
  403.   var buyBtnEl  = document.getElementById('btn-buy-now');
  404.   var qtyEl     = document.getElementById('qttprdid');
  405.   function activateThumbForVid(vid) {
  406.     if (!thumbGrid) return;
  407.     var thumbs = thumbGrid.querySelectorAll('.thumb');
  408.     var found = false;
  409.     for (var i = 0; i < thumbs.length; i++) {
  410.       if (thumbs[i].dataset.vid && thumbs[i].dataset.vid === String(vid)) {
  411.         thumbs.forEach(function(t){ t.classList.remove('active'); });
  412.         thumbs[i].classList.add('active');
  413.         found = true;
  414.         break;
  415.       }
  416.     }
  417.     return found;
  418.   }
  419.   function selectVariant(card) {
  420.     if (!card) return;
  421.     if (variantList) {
  422.       variantList.querySelectorAll('.variant-card').forEach(function(c) { c.classList.remove('active'); });
  423.     }
  424.     card.classList.add('active');
  425.     var vid   = card.dataset.vid;
  426.     var price = parseFloat(card.dataset.price || '0');
  427.     var stock = parseInt(card.dataset.stock || '0', 10);
  428.     var img   = card.dataset.img;
  429.     if (dispPrice) dispPrice.textContent = price.toFixed(2);
  430.     if (dispNote)  dispNote.textContent  = 'Price set by seller';
  431.     if (dispStock) dispStock.textContent = stock > 0 ? ('In Stock (' + stock + ' available)') : 'Out of Stock';
  432.     if (qtyEl) {
  433.       qtyEl.max = stock > 0 ? stock : 1;
  434.       if (parseInt(qtyEl.value, 10) > stock) { qtyEl.value = Math.max(1, stock); }
  435.     }
  436.     if (img && mainImgEl) { mainImgEl.src = img; }
  437.     if (addBtnEl && addBtnEl.dataset.addBase) {
  438.       addBtnEl.dataset.addUrl = addBtnEl.dataset.addBase.replace('__VID__', vid);
  439.     }
  440.     if (buyBtnEl && buyBtnEl.dataset.buyBase) {
  441.       buyBtnEl.setAttribute('formaction', buyBtnEl.dataset.buyBase.replace('__VID__', vid));
  442.     }
  443.     activateThumbForVid(vid);
  444.   }
  445.   function selectVariantById(vid) {
  446.     if (!variantList) return false;
  447.     var cards = variantList.querySelectorAll('.variant-card');
  448.     for (var i = 0; i < cards.length; i++) {
  449.       if (cards[i].dataset.vid === String(vid)) {
  450.         selectVariant(cards[i]);
  451.         return true;
  452.       }
  453.     }
  454.     return false;
  455.   }
  456.   if (thumbGrid) {
  457.     thumbGrid.addEventListener('click', function(e) {
  458.       var thumb = e.target.closest('.thumb');
  459.       if (!thumb) return;
  460.       var vid = thumb.dataset.vid;
  461.       if (vid && selectVariantById(vid)) {
  462.         return;
  463.       }
  464.       var src      = thumb.dataset.src;
  465.       var fallback = thumb.dataset.fallback;
  466.       if (mainImgEl && src) {
  467.         mainImgEl.src = src;
  468.         mainImgEl.onerror = function() { this.onerror = null; this.src = fallback; };
  469.       }
  470.       thumbGrid.querySelectorAll('.thumb').forEach(function(t) { t.classList.remove('active'); });
  471.       thumb.classList.add('active');
  472.     });
  473.   }
  474.   var qtyMinus = document.getElementById('qty-minus');
  475.   var qtyPlus  = document.getElementById('qty-plus');
  476.   if (qtyMinus && qtyEl) {
  477.     qtyMinus.addEventListener('click', function() {
  478.       var val = parseInt(qtyEl.value) - 1;
  479.       qtyEl.value = Math.max(1, val);
  480.     });
  481.   }
  482.   if (qtyPlus && qtyEl) {
  483.     qtyPlus.addEventListener('click', function() {
  484.       var val = parseInt(qtyEl.value) + 1;
  485.       var max = parseInt(qtyEl.max) || 999;
  486.       qtyEl.value = Math.min(max, val);
  487.     });
  488.   }
  489.   document.querySelectorAll('.color-opt').forEach(function(el) {
  490.     el.addEventListener('click', function() {
  491.       document.querySelectorAll('.color-opt').forEach(function(c) { c.classList.remove('active'); });
  492.       el.classList.add('active');
  493.       var chosenColor = document.getElementById('chosenColor');
  494.       if (chosenColor) chosenColor.value = el.dataset.color;
  495.     });
  496.   });
  497.   document.querySelectorAll('.size-opt').forEach(function(el) {
  498.     el.addEventListener('click', function() {
  499.       document.querySelectorAll('.size-opt').forEach(function(s) { s.classList.remove('active'); });
  500.       el.classList.add('active');
  501.       var chosenSize = document.getElementById('chosenSize');
  502.       if (chosenSize) chosenSize.value = el.dataset.size;
  503.     });
  504.   });
  505.   if (variantList) {
  506.     variantList.addEventListener('click', function(e) {
  507.       var card = e.target.closest('.variant-card');
  508.       if (card) selectVariant(card);
  509.     });
  510.     var firstCard = variantList.querySelector('.variant-card');
  511.     if (firstCard) selectVariant(firstCard);
  512.   }
  513.   var addBtn = document.getElementById('btn-add-cart');
  514.   if (addBtn) {
  515.     addBtn.addEventListener('click', function() {
  516.       var form    = document.getElementById('cart-form');
  517.       var addUrl  = addBtn.dataset.addUrl;
  518.       var origTxt = addBtn.innerHTML;
  519.       addBtn.innerHTML  = 'โ€ฆ';
  520.       addBtn.style.opacity = '0.7';
  521.       addBtn.disabled   = true;
  522.       var formData = new FormData(form);
  523.       var xhr = new XMLHttpRequest();
  524.       xhr.open('POST', addUrl);
  525.       xhr.withCredentials = true;
  526.       xhr.onload = function() {
  527.         var toast = document.getElementById('cart-toast');
  528.         if (toast) {
  529.           toast.classList.add('show');
  530.           setTimeout(function() { toast.classList.remove('show'); }, 4000);
  531.         }
  532.         addBtn.innerHTML = 'โœ“ Added!';
  533.         addBtn.style.background = '#16a34a';
  534.         addBtn.style.opacity = '1';
  535.         setTimeout(function() {
  536.           addBtn.innerHTML = origTxt;
  537.           addBtn.style.background = '';
  538.           addBtn.disabled = false;
  539.         }, 1500);
  540.       };
  541.       xhr.onerror = function() {
  542.         addBtn.innerHTML = origTxt;
  543.         addBtn.style.opacity = '1';
  544.         addBtn.disabled = false;
  545.         form.action = addUrl;
  546.         form.submit();
  547.       };
  548.       xhr.send(formData);
  549.     });
  550.   }
  551. });
  552. </script>
  553. </body>
  554. </html>