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. body{background:var(--gray-50);min-height:100vh}
  11. .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}
  12. .prod-body{max-width:1280px;margin:0 auto;padding:28px 24px}
  13. .breadcrumb{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--gray-400);margin-bottom:24px;flex-wrap:wrap}
  14. .breadcrumb a{color:var(--gray-400);text-decoration:none;transition:color .2s}
  15. .breadcrumb a:hover{color:var(--brand)}
  16. .breadcrumb span{color:var(--gray-800);font-weight:600}
  17. .prod-layout{display:grid;grid-template-columns:1fr 1fr;gap:40px;align-items:start}
  18. .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}
  19. .main-image img{width:80%;height:80%;object-fit:contain;transition:transform .3s}
  20. .main-image:hover img{transform:scale(1.05)}
  21. .main-image .no-img{font-size:80px;opacity:0.2}
  22. .thumb-grid{display:flex;gap:10px;overflow-x:auto;scrollbar-width:none}
  23. .thumb-grid::-webkit-scrollbar{display:none}
  24. .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}
  25. .thumb:hover,.thumb.active{border-color:var(--brand)}
  26. .thumb img{width:100%;height:100%;object-fit:contain;pointer-events:none}
  27. .prod-info{}
  28. .prod-badges{display:flex;gap:8px;margin-bottom:14px;flex-wrap:wrap}
  29. .prod-badge{font-size:11px;font-weight:700;padding:4px 10px;border-radius:100px}
  30. .badge-promo{background:#fef3c7;color:#b45309}
  31. .badge-new{background:#dcfce7;color:#166534}
  32. .badge-b2b{background:var(--brand-light);color:var(--brand-dark)}
  33. .badge-oos{background:#fee2e2;color:#991b1b}
  34. .prod-name{font-size:28px;font-weight:800;color:var(--gray-900);letter-spacing:-0.02em;line-height:1.2;margin-bottom:8px}
  35. .prod-cat{font-size:13px;font-weight:600;color:var(--brand);margin-bottom:16px}
  36. .prod-desc{font-size:14px;color:var(--gray-500);line-height:1.7;margin-bottom:20px}
  37. .prod-price-box{background:var(--brand-light);border:1.5px solid var(--brand-mid);border-radius:var(--r);padding:20px;margin-bottom:20px}
  38. .prod-price{font-size:36px;font-weight:800;color:var(--brand);letter-spacing:-0.02em;line-height:1}
  39. .prod-price sup{font-size:18px;font-weight:700;vertical-align:super}
  40. .prod-old-price{font-size:16px;color:var(--gray-400);text-decoration:line-through;font-weight:500;margin-top:4px}
  41. .prod-tax{font-size:12px;color:var(--gray-400);margin-top:6px}
  42. .prod-stock{display:flex;align-items:center;gap:6px;font-size:13px;font-weight:600;margin-bottom:20px}
  43. .stock-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
  44. .stock-in{background:#22c55e}
  45. .stock-out{background:#ef4444}
  46. .variants-section{margin-bottom:20px}
  47. .variants-label{font-size:13px;font-weight:700;color:var(--gray-700);margin-bottom:10px}
  48. .color-options{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:14px}
  49. .color-opt{width:34px;height:34px;border-radius:50%;border:3px solid transparent;cursor:pointer;transition:all .2s;outline:2px solid var(--gray-200)}
  50. .color-opt:hover,.color-opt.active{outline-color:var(--brand);outline-offset:2px}
  51. .size-options{display:flex;gap:8px;flex-wrap:wrap}
  52. .size-opt{padding:8px 16px;border:1.5px solid var(--gray-200);border-radius:var(--rsm);font-size:13px;font-weight:600;color:var(--gray-700);cursor:pointer;transition:all .2s;background:var(--white);font-family:var(--font)}
  53. .size-opt:hover,.size-opt.active{border-color:var(--brand);color:var(--brand);background:var(--brand-light)}
  54. .prod-actions{display:flex;gap:12px;align-items:center;margin-bottom:20px}
  55. .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)}
  56. .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)}
  57. .qty-btn-large:hover{background:var(--gray-100)}
  58. .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}
  59. .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}
  60. .btn-add-cart:hover{background:var(--brand-dark)}
  61. .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}
  62. .btn-checkout-now:hover{background:var(--gray-700)}
  63. .info-cards{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:20px}
  64. .info-card{background:var(--white);border:1.5px solid var(--gray-100);border-radius:var(--r);padding:14px;text-align:center;box-shadow:var(--shadow-sm)}
  65. .info-card-icon{font-size:22px;margin-bottom:6px}
  66. .info-card-title{font-size:12px;font-weight:700;color:var(--gray-700)}
  67. .info-card-sub{font-size:11px;color:var(--gray-400);margin-top:2px}
  68. .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}
  69. .cart-toast.show{opacity:1;transform:translateX(-50%) translateY(0);pointer-events:auto}
  70. .cart-toast a{color:var(--brand);font-weight:700;text-decoration:none;margin-left:4px}
  71. @media(max-width:900px){.prod-layout{grid-template-columns:1fr}.prod-actions{flex-wrap:wrap}}
  72. </style>
  73. </head>
  74. <body>
  75. <nav class="prod-nav">
  76.   <a href="{{ path('app_home') }}">
  77.     <img src="/images/julico-logo.png" alt="Julico" style="height:48px;object-fit:contain">
  78.   </a>
  79.   <div style="display:flex;align-items:center;gap:12px">
  80.     <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)">
  81.       ๐Ÿ›’ Cart
  82.     </a>
  83.     {% if app.user %}
  84.       <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>
  85.       <a href="{{ path('app_profile') }}" style="font-size:13px;font-weight:600;color:var(--brand);text-decoration:none">{{ app.user.pseudo }}</a>
  86.     {% else %}
  87.       <a href="/login" style="font-size:13px;font-weight:600;color:var(--brand);text-decoration:none">Sign In</a>
  88.     {% endif %}
  89.   </div>
  90. </nav>
  91. <div class="prod-body">
  92.   <div class="breadcrumb">
  93.     <a href="{{ path('app_home') }}">Home</a> โ€บ
  94.     {% if produit.category is defined and produit.category %}
  95.       <a href="{{ path('app_home') }}">{{ produit.category.nom }}</a> โ€บ
  96.     {% endif %}
  97.     <span>{{ produit.productName }}</span>
  98.   </div>
  99.   <div class="prod-layout">
  100.     <!-- IMAGES -->
  101.     <div class="prod-images">
  102.       <div class="main-image">
  103.         {% if produit.images|length > 0 %}
  104.           {# โ”€โ”€ Main image โ€” src set by JS on thumb click โ”€โ”€ #}
  105.           <img id="main-img"
  106.                src="{{ asset('assets/uploads/products/' ~ produit.images|first.fileName) }}"
  107.                alt="{{ produit.productName }}"
  108.                onerror="this.onerror=null;this.parentNode.innerHTML='<div class=no-img>๐Ÿ“ฆ</div>'">
  109.         {% else %}
  110.           <div class="no-img">๐Ÿ“ฆ</div>
  111.         {% endif %}
  112.       </div>
  113.       {% if produit.images|length > 1 %}
  114.       <div class="thumb-grid" id="thumb-grid">
  115.         {% for image in produit.images %}
  116.         <div class="thumb {% if loop.first %}active{% endif %}"
  117.              data-src="{{ asset('assets/uploads/products/' ~ image.fileName) }}"
  118.              data-fallback="/assets/uploads/products/{{ image.fileName }}">
  119.           <img src="{{ asset('assets/uploads/products/' ~ image.fileName) }}"
  120.                alt="{{ loop.index }}"
  121.                onerror="this.onerror=null;this.src='/assets/uploads/products/{{ image.fileName }}'">
  122.         </div>
  123.         {% endfor %}
  124.       </div>
  125.       {% endif %}
  126.     </div>
  127.     <!-- INFO -->
  128.     <div class="prod-info">
  129.       <div class="prod-badges">
  130.         {% if produit.promo %}<span class="prod-badge badge-promo">๐Ÿ”ฅ On Promo</span>{% endif %}
  131.         {% if produit.qtt > 0 %}
  132.           <span class="prod-badge badge-new">โœ… In Stock</span>
  133.         {% else %}
  134.           <span class="prod-badge badge-oos">โŒ Out of Stock</span>
  135.         {% endif %}
  136.         {% if produit.magasin is defined and produit.magasin %}
  137.           <span class="prod-badge badge-b2b">{{ produit.magasin.nom }}</span>
  138.         {% endif %}
  139.       </div>
  140.       <div class="prod-name">{{ produit.productName }}</div>
  141.       <div class="prod-cat">
  142.         {% if produit.category is defined and produit.category %}{{ produit.category.nom }}{% endif %}
  143.         {% if produit.magasin is defined and produit.magasin %} ยท {{ produit.magasin.nom }}{% endif %}
  144.       </div>
  145.       <div class="prod-desc">{{ produit.description }}</div>
  146.       <div class="prod-price-box">
  147.         {% if produit.promo and produit.salePrice and produit.salePrice > 0 and produit.salePrice < produit.prix %}
  148.           <div class="prod-price"><sup>$</sup>{{ produit.salePrice|number_format(2) }}</div>
  149.           <div class="prod-old-price">${{ produit.prix|number_format(2) }}</div>
  150.         {% else %}
  151.           <div class="prod-price"><sup>$</sup>{{ produit.prix|number_format(2) }}</div>
  152.         {% endif %}
  153.         <div class="prod-tax">Price includes applicable taxes</div>
  154.       </div>
  155.       <div class="prod-stock">
  156.         <span class="stock-dot {{ produit.qtt > 0 ? 'stock-in' : 'stock-out' }}"></span>
  157.         {% if produit.qtt > 0 %}
  158.           <span style="color:#16a34a">In Stock ({{ produit.qtt }} available)</span>
  159.         {% else %}
  160.           <span style="color:#dc2626">Out of Stock</span>
  161.         {% endif %}
  162.       </div>
  163.       {% if groupedVariantscolor|length > 0 %}
  164.       <div class="variants-section">
  165.         <div class="variants-label">Color:</div>
  166.         <div class="color-options">
  167.           {% for color, variants in groupedVariantscolor %}
  168.           <div class="color-opt" data-color="{{ color }}"
  169.                style="background-color:{{ color }}" title="{{ color }}"></div>
  170.           {% endfor %}
  171.         </div>
  172.       </div>
  173.       {% endif %}
  174.       {% if groupedVariantssize|length > 0 %}
  175.       <div class="variants-section">
  176.         <div class="variants-label">Size:</div>
  177.         <div class="size-options">
  178.           {% for size, variants in groupedVariantssize %}
  179.           <button type="button" class="size-opt" data-size="{{ size }}">{{ size }}</button>
  180.           {% endfor %}
  181.         </div>
  182.       </div>
  183.       {% endif %}
  184.       {% if produit.qtt > 0 %}
  185.       <form method="post" id="cart-form">
  186.         <input type="hidden" id="chosenColor" name="chosenColor">
  187.         <input type="hidden" id="chosenSize"  name="chosenSize">
  188.         <input type="hidden" name="productid"   value="{{ produit.id }}">
  189.         <input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
  190.         <div class="prod-actions">
  191.           <div class="qty-wrap">
  192.             <button type="button" class="qty-btn-large" id="qty-minus">โˆ’</button>
  193.             <input type="number" class="qty-input" id="qttprdid" name="qttprdid"
  194.                    min="1" max="{{ produit.qtt }}" value="1">
  195.             <button type="button" class="qty-btn-large" id="qty-plus">+</button>
  196.           </div>
  197.           <button type="button" class="btn-add-cart" id="btn-add-cart"
  198.                   data-add-url="{{ path('panier_add', {'id': produit.id, 'idVariante': 'notdefined', 'option': 'continue'}) }}">
  199.             ๐Ÿ›’ Add to Cart
  200.           </button>
  201.           <button type="submit" class="btn-checkout-now"
  202.                   formaction="{{ path('panier_add', {'id': produit.id, 'idVariante': 'notdefined', 'option': 'done'}) }}">
  203.             โšก Buy Now
  204.           </button>
  205.         </div>
  206.       </form>
  207.       {% else %}
  208.       <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">
  209.         โŒ This product is currently out of stock
  210.       </div>
  211.       {% endif %}
  212.       <div class="info-cards">
  213.         <div class="info-card"><div class="info-card-icon">๐Ÿšš</div><div class="info-card-title">Fast Delivery</div><div class="info-card-sub">Next-day dispatch</div></div>
  214.         <div class="info-card"><div class="info-card-icon">๐Ÿ”’</div><div class="info-card-title">Secure Payment</div><div class="info-card-sub">100% protected</div></div>
  215.         <div class="info-card"><div class="info-card-icon">โ†ฉ๏ธ</div><div class="info-card-title">Easy Returns</div><div class="info-card-sub">Hassle-free policy</div></div>
  216.       </div>
  217.     </div>
  218.   </div>
  219. </div>
  220. <div class="cart-toast" id="cart-toast">
  221.   <span>โœ“ Added to cart!</span>
  222.   <a href="{{ path('app_panier') }}">View Cart โ†’</a>
  223. </div>
  224. <script>
  225. // โ”€โ”€ IMAGE GALLERY โ€” data-src approach, no inline onclick โ”€โ”€
  226. document.addEventListener('DOMContentLoaded', function() {
  227.   // Thumbnail click โ€” switch main image
  228.   var thumbGrid = document.getElementById('thumb-grid');
  229.   if (thumbGrid) {
  230.     thumbGrid.addEventListener('click', function(e) {
  231.       var thumb = e.target.closest('.thumb');
  232.       if (!thumb) return;
  233.       var src      = thumb.dataset.src;
  234.       var fallback = thumb.dataset.fallback;
  235.       var mainImg  = document.getElementById('main-img');
  236.       if (mainImg && src) {
  237.         mainImg.src = src;
  238.         mainImg.onerror = function() {
  239.           this.onerror = null;
  240.           this.src = fallback;
  241.         };
  242.       }
  243.       // Update active state
  244.       document.querySelectorAll('.thumb').forEach(function(t) {
  245.         t.classList.remove('active');
  246.       });
  247.       thumb.classList.add('active');
  248.     });
  249.   }
  250.   // โ”€โ”€ Qty buttons โ”€โ”€
  251.   var qtyInput = document.getElementById('qttprdid');
  252.   var qtyMinus = document.getElementById('qty-minus');
  253.   var qtyPlus  = document.getElementById('qty-plus');
  254.   if (qtyMinus && qtyInput) {
  255.     qtyMinus.addEventListener('click', function() {
  256.       var val = parseInt(qtyInput.value) - 1;
  257.       qtyInput.value = Math.max(1, val);
  258.     });
  259.   }
  260.   if (qtyPlus && qtyInput) {
  261.     qtyPlus.addEventListener('click', function() {
  262.       var val = parseInt(qtyInput.value) + 1;
  263.       var max = parseInt(qtyInput.max) || 999;
  264.       qtyInput.value = Math.min(max, val);
  265.     });
  266.   }
  267.   // โ”€โ”€ Color selection โ”€โ”€
  268.   document.querySelectorAll('.color-opt').forEach(function(el) {
  269.     el.addEventListener('click', function() {
  270.       document.querySelectorAll('.color-opt').forEach(function(c) { c.classList.remove('active'); });
  271.       el.classList.add('active');
  272.       var chosenColor = document.getElementById('chosenColor');
  273.       if (chosenColor) chosenColor.value = el.dataset.color;
  274.     });
  275.   });
  276.   // โ”€โ”€ Size selection โ”€โ”€
  277.   document.querySelectorAll('.size-opt').forEach(function(el) {
  278.     el.addEventListener('click', function() {
  279.       document.querySelectorAll('.size-opt').forEach(function(s) { s.classList.remove('active'); });
  280.       el.classList.add('active');
  281.       var chosenSize = document.getElementById('chosenSize');
  282.       if (chosenSize) chosenSize.value = el.dataset.size;
  283.     });
  284.   });
  285.   // โ”€โ”€ AJAX Add to Cart โ”€โ”€
  286.   var addBtn = document.getElementById('btn-add-cart');
  287.   if (addBtn) {
  288.     addBtn.addEventListener('click', function() {
  289.       var form    = document.getElementById('cart-form');
  290.       var addUrl  = addBtn.dataset.addUrl;
  291.       var origTxt = addBtn.innerHTML;
  292.       addBtn.innerHTML  = 'โ€ฆ';
  293.       addBtn.style.opacity = '0.7';
  294.       addBtn.disabled   = true;
  295.       var formData = new FormData(form);
  296.       var xhr = new XMLHttpRequest();
  297.       xhr.open('POST', addUrl);
  298.       xhr.withCredentials = true;
  299.       xhr.onload = function() {
  300.         // Show toast
  301.         var toast = document.getElementById('cart-toast');
  302.         if (toast) {
  303.           toast.classList.add('show');
  304.           setTimeout(function() { toast.classList.remove('show'); }, 4000);
  305.         }
  306.         addBtn.innerHTML = 'โœ“ Added!';
  307.         addBtn.style.background = '#16a34a';
  308.         addBtn.style.opacity = '1';
  309.         setTimeout(function() {
  310.           addBtn.innerHTML = origTxt;
  311.           addBtn.style.background = '';
  312.           addBtn.disabled = false;
  313.         }, 1500);
  314.       };
  315.       xhr.onerror = function() {
  316.         // Fallback: normal submit
  317.         addBtn.innerHTML = origTxt;
  318.         addBtn.style.opacity = '1';
  319.         addBtn.disabled = false;
  320.         form.action = addUrl;
  321.         form.submit();
  322.       };
  323.       xhr.send(formData);
  324.     });
  325.   }
  326. });
  327. </script>
  328. </body>
  329. </html>