templates/panier/index.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>My Cart โ€” 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. .cart-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. .cart-body{max-width:1280px;margin:0 auto;padding:32px 24px}
  13. .cart-title{font-size:28px;font-weight:800;color:var(--gray-900);letter-spacing:-0.02em;margin-bottom:6px}
  14. .cart-subtitle{font-size:14px;color:var(--gray-400);font-weight:500;margin-bottom:28px}
  15. .cart-layout{display:grid;grid-template-columns:1fr 360px;gap:24px;align-items:start}
  16. .cart-items{display:flex;flex-direction:column;gap:14px}
  17. .cart-empty{background:var(--white);border:1.5px solid var(--gray-100);border-radius:var(--rlg);padding:60px 24px;text-align:center;box-shadow:var(--shadow-sm)}
  18. .cart-empty-icon{font-size:64px;margin-bottom:16px}
  19. .cart-empty-title{font-size:20px;font-weight:800;color:var(--gray-900);margin-bottom:8px}
  20. .cart-empty-sub{font-size:14px;color:var(--gray-400);margin-bottom:24px}
  21. .cart-item{background:var(--white);border:1.5px solid var(--gray-100);border-radius:var(--rlg);padding:20px;display:flex;align-items:center;gap:16px;box-shadow:var(--shadow-sm);transition:box-shadow .2s}
  22. .cart-item:hover{box-shadow:var(--shadow-md)}
  23. .cart-item-img{width:80px;height:80px;background:var(--gray-50);border-radius:var(--rsm);overflow:hidden;flex-shrink:0;display:flex;align-items:center;justify-content:center}
  24. .cart-item-img img{width:100%;height:100%;object-fit:contain}
  25. .cart-item-info{flex:1}
  26. .cart-item-cat{font-size:10px;font-weight:700;color:var(--brand);text-transform:uppercase;letter-spacing:0.05em;margin-bottom:3px}
  27. .cart-item-name{font-size:15px;font-weight:700;color:var(--gray-900);margin-bottom:4px}
  28. .cart-item-variant{font-size:12px;color:var(--gray-400);font-weight:500}
  29. .cart-item-right{display:flex;flex-direction:column;align-items:flex-end;gap:10px;flex-shrink:0}
  30. .cart-item-price{font-size:20px;font-weight:800;color:var(--gray-900);letter-spacing:-0.02em}
  31. .cart-item-price sup{font-size:12px;font-weight:700;vertical-align:super}
  32. .cart-item-subtotal{font-size:11px;color:var(--gray-400);font-weight:500}
  33. .cart-qty{display:flex;align-items:center;border:1.5px solid var(--gray-200);border-radius:var(--rsm);overflow:hidden}
  34. .qty-btn{width:32px;height:32px;background:var(--white);border:none;color:var(--gray-700);font-size:16px;font-weight:600;cursor:pointer;transition:background .2s;font-family:var(--font);display:flex;align-items:center;justify-content:center;touch-action:manipulation;}
  35. .qty-btn:hover{background:var(--brand-light);color:var(--brand)}
  36. .qty-btn:disabled{opacity:0.4;cursor:not-allowed}
  37. .qty-num{width:36px;height:32px;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:14px;font-weight:700;color:var(--gray-900);background:var(--white);outline:none}
  38. .remove-btn{color:var(--gray-400);cursor:pointer;font-size:13px;font-weight:600;text-decoration:none;display:inline-flex;align-items:center;gap:4px;transition:color .2s;background:none;border:none;font-family:var(--font);padding:0}
  39. .remove-btn:hover{color:#ef4444}
  40. .cart-summary{background:var(--white);border:1.5px solid var(--gray-100);border-radius:var(--rlg);padding:24px;box-shadow:var(--shadow-sm);position:sticky;top:88px}
  41. .summary-title{font-size:18px;font-weight:800;color:var(--gray-900);margin-bottom:20px;padding-bottom:16px;border-bottom:1px solid var(--gray-100)}
  42. .summary-row{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;font-size:14px}
  43. .summary-row .label{color:var(--gray-500);font-weight:500}
  44. .summary-row .value{font-weight:700;color:var(--gray-800)}
  45. .summary-divider{border:none;border-top:1px solid var(--gray-100);margin:16px 0}
  46. .summary-total{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}
  47. .summary-total .label{font-size:16px;font-weight:700;color:var(--gray-900)}
  48. .summary-total .value{font-size:22px;font-weight:800;color:var(--brand)}
  49. .btn-checkout{width:100%;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;margin-bottom:12px;display:flex;align-items:center;justify-content:center;gap:8px;text-decoration:none}
  50. .btn-checkout:hover{background:var(--brand-dark)}
  51. .btn-continue{width:100%;height:44px;background:var(--white);color:var(--gray-700);border:1.5px solid var(--gray-200);border-radius:var(--rsm);font-family:var(--font);font-size:14px;font-weight:600;cursor:pointer;transition:all .2s;text-decoration:none;display:flex;align-items:center;justify-content:center}
  52. .btn-continue:hover{border-color:var(--brand);color:var(--brand);background:var(--brand-light)}
  53. .trust-chips{display:flex;gap:12px;flex-wrap:wrap;margin-top:16px;padding-top:16px;border-top:1px solid var(--gray-100)}
  54. .trust-chip{display:flex;align-items:center;gap:5px;font-size:11px;font-weight:600;color:var(--gray-500)}
  55. .updating{opacity:0.5;pointer-events:none}
  56. @media(max-width:768px){.cart-layout{grid-template-columns:1fr}.cart-item{flex-wrap:wrap}}
  57. </style>
  58. </head>
  59. <body>
  60. <nav class="cart-nav">
  61.   <a href="{{ path('app_home') }}">
  62.     <img src="/images/julico-logo.png" alt="Julico" style="height:48px;object-fit:contain">
  63.   </a>
  64.   <div style="font-size:18px;font-weight:800;color:var(--gray-900)">๐Ÿ›’ My Cart</div>
  65.   <a href="{{ path('app_home') }}" style="font-size:13px;font-weight:600;color:var(--gray-500);text-decoration:none">โ† Continue Shopping</a>
  66. </nav>
  67. <div class="cart-body">
  68.   <div class="cart-title">Shopping Cart</div>
  69.   <div class="cart-subtitle" id="cart-subtitle">
  70.     {% if items|length > 0 %}
  71.       {{ items|length }} item{% if items|length > 1 %}s{% endif %} in your cart
  72.     {% else %}
  73.       Your cart is empty
  74.     {% endif %}
  75.   </div>
  76.   <div class="cart-layout">
  77.     <div class="cart-items">
  78.       {% if items|length > 0 %}
  79.         {% for item in items %}
  80.           {% set unitPrice = item.product.prix %}
  81.           {% if item.product.salePrice is defined and item.product.salePrice is not null and item.product.salePrice > 0 %}
  82.             {% set unitPrice = item.product.salePrice %}
  83.           {% endif %}
  84.           {% set variantId = 'notdefined' %}
  85.           <div class="cart-item" id="cart-item-{{ item.product.id }}"
  86.                data-id="{{ item.product.id }}"
  87.                data-variant="{{ variantId }}"
  88.                data-price="{{ unitPrice }}"
  89.                data-qty="{{ item.quantity }}"
  90.                data-csrf="{{ csrf_token('authenticate') }}"
  91.                data-add-url="{{ path('panier_add',   {'id': item.product.id, 'idVariante': variantId, 'option': 'continue'}) }}"
  92.                data-minus-url="{{ path('panier_minus', {'id': item.product.id, 'idVariante': variantId, 'option': 'continue'}) }}"
  93.                data-remove-url="{{ path('panier_remove', {'id': item.product.id, 'idVariante': variantId}) }}">
  94.             {# Image โ€” fixed path #}
  95.             <div class="cart-item-img">
  96.               {% if item.product.images|length > 0 %}
  97.                 <img src="{{ asset('assets/uploads/products/' ~ item.product.images[0].fileName) }}"
  98.                      alt="{{ item.product.productName }}"
  99.                      onerror="this.onerror=null;this.parentNode.innerHTML='<span style=font-size:32px;opacity:0.3>๐Ÿ“ฆ</span>'">
  100.               {% else %}
  101.                 <span style="font-size:32px;opacity:0.3">๐Ÿ“ฆ</span>
  102.               {% endif %}
  103.             </div>
  104.             <div class="cart-item-info">
  105.               <div class="cart-item-cat">
  106.                 {% if item.product.category is defined and item.product.category %}{{ item.product.category.nom }}{% endif %}
  107.                 {% if item.product.magasin is defined and item.product.magasin %} ยท {{ item.product.magasin.nom }}{% endif %}
  108.               </div>
  109.               <a href="{{ path('show_produit', {'id': item.product.id}) }}" style="text-decoration:none">
  110.                 <div class="cart-item-name">{{ item.product.productName }}</div>
  111.               </a>
  112.               {% if item.productVariant is defined and item.productVariant %}
  113.                 <div class="cart-item-variant">
  114.                   {% if item.productVariant.paramColor is defined and item.productVariant.paramColor %}Color: {{ item.productVariant.paramColor.colorName }}{% endif %}
  115.                   {% if item.productVariant.paramSize is defined and item.productVariant.paramSize %} ยท Size: {{ item.productVariant.paramSize.sizeAbreviation }}{% endif %}
  116.                 </div>
  117.               {% endif %}
  118.             </div>
  119.             <div class="cart-item-right">
  120.               <div class="cart-item-price"><sup>$</sup>{{ unitPrice|number_format(2) }}</div>
  121.               <div class="cart-item-subtotal" id="subtotal-{{ item.product.id }}">
  122.                 ร— {{ item.quantity }} = ${{ (unitPrice * item.quantity)|number_format(2) }}
  123.               </div>
  124.               {# Qty controls โ€” AJAX, no page reload #}
  125.               <div class="cart-qty">
  126.                 <button type="button" class="qty-btn btn-minus" data-action="minus" title="Remove one">โˆ’</button>
  127.                 <input type="number" class="qty-num" value="{{ item.quantity }}" readonly>
  128.                 <button type="button" class="qty-btn btn-plus" data-action="plus" title="Add one">+</button>
  129.               </div>
  130.               <button type="button" class="remove-btn btn-remove">๐Ÿ—‘ Remove</button>
  131.             </div>
  132.           </div>
  133.         {% endfor %}
  134.       {% else %}
  135.         <div class="cart-empty">
  136.           <div class="cart-empty-icon">๐Ÿ›’</div>
  137.           <div class="cart-empty-title">Your cart is empty</div>
  138.           <div class="cart-empty-sub">Looks like you haven't added anything yet.</div>
  139.           <a href="{{ path('app_home') }}" class="btn-checkout" style="max-width:220px;margin:0 auto">Browse Products โ†’</a>
  140.         </div>
  141.       {% endif %}
  142.     </div>
  143.     {% if items|length > 0 %}
  144.     <div class="cart-summary">
  145.       <div class="summary-title">Order Summary</div>
  146.       <div class="summary-row">
  147.         <span class="label">Subtotal</span>
  148.         <span class="value" id="summary-subtotal">${{ (total - frais)|number_format(2) }}</span>
  149.       </div>
  150.       <div class="summary-row">
  151.         <span class="label">Delivery</span>
  152.         <span class="value" style="color:var(--green)">
  153.           {% if frais == 0 %}Free{% else %}${{ frais|number_format(2) }}{% endif %}
  154.         </span>
  155.       </div>
  156.       <hr class="summary-divider">
  157.       <div class="summary-total">
  158.         <span class="label">Total</span>
  159.         <span class="value" id="summary-total">${{ total|number_format(2) }}</span>
  160.       </div>
  161.       {% if app.user %}
  162.         <form action="{{ path('panier_validationcmd', {'option': 'goprofile', 'addresseid': 'notdefined', 'cmdid': 'notdefined'}) }}" method="POST">
  163.           <input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
  164.           <button type="submit" class="btn-checkout">Proceed to Checkout โ†’</button>
  165.         </form>
  166.       {% else %}
  167.         <a href="/login" class="btn-checkout">Sign In to Checkout โ†’</a>
  168.       {% endif %}
  169.       <a href="{{ path('app_home') }}" class="btn-continue">โ† Continue Shopping</a>
  170.       <div class="trust-chips">
  171.         <div class="trust-chip">๐Ÿ”’ Secure checkout</div>
  172.         <div class="trust-chip">๐Ÿšš Fast delivery</div>
  173.         <div class="trust-chip">โ†ฉ๏ธ Easy returns</div>
  174.       </div>
  175.     </div>
  176.     {% endif %}
  177.   </div>
  178. </div>
  179. <script>
  180. // โ”€โ”€ Cart item AJAX handlers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  181. var frais = {{ frais }};
  182. document.addEventListener('DOMContentLoaded', function() {
  183.   document.querySelectorAll('.cart-item').forEach(function(itemEl) {
  184.     var id        = itemEl.dataset.id;
  185.     var csrf      = itemEl.dataset.csrf;
  186.     var addUrl    = itemEl.dataset.addUrl;
  187.     var minusUrl  = itemEl.dataset.minusUrl;
  188.     var removeUrl = itemEl.dataset.removeUrl;
  189.     var price     = parseFloat(itemEl.dataset.price);
  190.     var qtyInput  = itemEl.querySelector('.qty-num');
  191.     var subtotal  = document.getElementById('subtotal-' + id);
  192.     var plusBtn   = itemEl.querySelector('.btn-plus');
  193.     var minusBtn  = itemEl.querySelector('.btn-minus');
  194.     var removeBtn = itemEl.querySelector('.btn-remove');
  195.     // โ”€โ”€ + button โ”€โ”€
  196.     plusBtn.addEventListener('click', function() {
  197.       ajaxQty(addUrl, id, csrf, price, 1, itemEl, qtyInput, subtotal);
  198.     });
  199.     // โ”€โ”€ โˆ’ button โ”€โ”€
  200.     minusBtn.addEventListener('click', function() {
  201.       var qty = parseInt(qtyInput.value);
  202.       if (qty <= 1) {
  203.         // Remove item
  204.         ajaxRemove(removeUrl, id, itemEl);
  205.       } else {
  206.         ajaxQty(minusUrl, id, csrf, price, -1, itemEl, qtyInput, subtotal);
  207.       }
  208.     });
  209.     // โ”€โ”€ Remove button โ”€โ”€
  210.     removeBtn.addEventListener('click', function() {
  211.       ajaxRemove(removeUrl, id, itemEl);
  212.     });
  213.   });
  214. });
  215. function ajaxQty(url, id, csrf, price, delta, itemEl, qtyInput, subtotalEl) {
  216.   itemEl.classList.add('updating');
  217.   var formData = new FormData();
  218.   formData.append('_csrf_token', csrf);
  219.   formData.append('productid',   id);
  220.   formData.append('qttprdid',    '1');
  221.   var xhr = new XMLHttpRequest();
  222.   xhr.open('POST', url);
  223.   xhr.withCredentials = true;
  224.   xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
  225.   xhr.setRequestHeader('Accept', 'application/json');
  226.   xhr.onload = function() {
  227.     itemEl.classList.remove('updating');
  228.     if (xhr.status === 200) {
  229.       try {
  230.         var data = JSON.parse(xhr.responseText);
  231.         if (data.success) {
  232.           var newQty = parseInt(qtyInput.value) + delta;
  233.           qtyInput.value = newQty;
  234.           if (subtotalEl) {
  235.             var sub = price * newQty;
  236.             subtotalEl.textContent = 'ร— ' + newQty + ' = $' + sub.toFixed(2);
  237.           }
  238.           updateSummary(price * delta);
  239.         }
  240.       } catch(e) {
  241.         // JSON parse failed โ€” don't reload, just ignore
  242.         var newQty = parseInt(qtyInput.value) + delta;
  243.         qtyInput.value = Math.max(0, newQty);
  244.         if (subtotalEl && newQty > 0) {
  245.           subtotalEl.textContent = 'ร— ' + newQty + ' = $' + (price * newQty).toFixed(2);
  246.         }
  247.         updateSummary(price * delta);
  248.       }
  249.     }
  250.   };
  251.   xhr.onerror = function() {
  252.     itemEl.classList.remove('updating');
  253.   };
  254.   xhr.send(formData);
  255. }
  256. function ajaxRemove(url, id, itemEl) {
  257.   itemEl.classList.add('updating');
  258.   // Check if this is the last item BEFORE removing
  259.   var remaining = document.querySelectorAll('.cart-item');
  260.   var isLast = remaining.length === 1;
  261.   var xhr = new XMLHttpRequest();
  262.   xhr.open('GET', url);
  263.   xhr.withCredentials = true;
  264.   xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
  265.   xhr.setRequestHeader('Accept', 'application/json');
  266.   xhr.onload = function() {
  267.     if (isLast) {
  268.       // Last item โ€” reload immediately to show empty cart
  269.       window.location.reload();
  270.       return;
  271.     }
  272.     // Animate removal
  273.     itemEl.style.transition = 'all 0.3s';
  274.     itemEl.style.opacity = '0';
  275.     itemEl.style.height = itemEl.offsetHeight + 'px';
  276.     setTimeout(function() {
  277.       itemEl.style.height = '0';
  278.       itemEl.style.padding = '0';
  279.       itemEl.style.margin = '0';
  280.       itemEl.style.overflow = 'hidden';
  281.       setTimeout(function() { itemEl.remove(); }, 300);
  282.     }, 300);
  283.     // Update summary
  284.     var price = parseFloat(itemEl.dataset.price);
  285.     var qty   = parseInt(itemEl.querySelector('.qty-num').value);
  286.     updateSummary(-(price * qty));
  287.   };
  288.   xhr.onerror = function() { window.location.href = url; };
  289.   xhr.send();
  290. }
  291. function updateSummary(priceDelta) {
  292.   var subtotalEl = document.getElementById('summary-subtotal');
  293.   var totalEl    = document.getElementById('summary-total');
  294.   if (subtotalEl) {
  295.     var sub = parseFloat(subtotalEl.textContent.replace('$','').replace(',','')) + priceDelta;
  296.     subtotalEl.textContent = '$' + Math.max(0, sub).toFixed(2);
  297.   }
  298.   if (totalEl) {
  299.     var tot = parseFloat(totalEl.textContent.replace('$','').replace(',','')) + priceDelta;
  300.     totalEl.textContent = '$' + Math.max(0, tot + frais).toFixed(2);
  301.   }
  302. }
  303. </script>
  304. </body>
  305. </html>