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. *, *::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. .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}
  15. .cart-body{max-width:1280px;margin:0 auto;padding:32px 24px}
  16. .cart-title{font-size:28px;font-weight:800;color:var(--gray-900);letter-spacing:-0.02em;margin-bottom:6px}
  17. .cart-subtitle{font-size:14px;color:var(--gray-400);font-weight:500;margin-bottom:28px}
  18. .cart-layout{display:grid;grid-template-columns:1fr 360px;gap:24px;align-items:start}
  19. .cart-items{display:flex;flex-direction:column;gap:18px}
  20. .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)}
  21. .cart-empty-icon{font-size:64px;margin-bottom:16px}
  22. .cart-empty-title{font-size:20px;font-weight:800;color:var(--gray-900);margin-bottom:8px}
  23. .cart-empty-sub{font-size:14px;color:var(--gray-400);margin-bottom:24px}
  24. /* Address picker */
  25. .addr-card{background:var(--white);border:1.5px solid var(--gray-100);border-radius:var(--rlg);padding:18px 20px;box-shadow:var(--shadow-sm)}
  26. .addr-card-title{font-size:13px;font-weight:800;color:var(--gray-700);text-transform:uppercase;letter-spacing:0.05em;margin-bottom:10px;display:flex;align-items:center;gap:7px}
  27. .addr-select{width:100%;height:46px;border:1.5px solid var(--gray-200);border-radius:var(--rsm);padding:0 14px;font-family:var(--font);font-size:14px;font-weight:600;color:var(--gray-800);background:var(--white);outline:none;cursor:pointer}
  28. .addr-select:focus{border-color:var(--brand)}
  29. .addr-zone{font-size:12px;font-weight:600;margin-top:9px;color:var(--gray-500)}
  30. .addr-zone b{color:var(--brand)}
  31. .addr-warn{background:#fef9c3;border:1.5px solid #fde047;border-radius:var(--rsm);padding:11px 14px;font-size:12.5px;font-weight:600;color:#854d0e;margin-top:10px;line-height:1.45}
  32. .addr-warn a{color:#854d0e;font-weight:800;text-decoration:underline}
  33. .addr-link{display:inline-block;margin-top:10px;font-size:13px;font-weight:700;color:var(--brand);text-decoration:none}
  34. .addr-link:hover{text-decoration:underline}
  35. /* Shop group */
  36. .shop-group{background:var(--white);border:1.5px solid var(--gray-100);border-radius:var(--rlg);box-shadow:var(--shadow-sm);overflow:hidden}
  37. .shop-group.undeliverable{border-color:#fecaca}
  38. .shop-group-header{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:14px 20px;border-bottom:1px solid var(--gray-100);background:var(--gray-50);flex-wrap:wrap}
  39. .shop-group-name{font-size:14px;font-weight:800;color:var(--gray-900);display:flex;align-items:center;gap:7px}
  40. .shop-badge{font-size:11px;font-weight:800;padding:5px 11px;border-radius:999px;white-space:nowrap}
  41. .shop-badge.ok{background:#dcfce7;color:#16a34a}
  42. .shop-badge.no{background:#fee2e2;color:#dc2626}
  43. .shop-badge.unknown{background:var(--gray-100);color:var(--gray-500)}
  44. .shop-items{display:flex;flex-direction:column}
  45. .cart-item{padding:18px 20px;display:flex;align-items:center;gap:16px;border-bottom:1px solid var(--gray-100);transition:opacity .2s}
  46. .cart-item:last-child{border-bottom:none}
  47. .cart-item-img{width:74px;height:74px;background:var(--gray-50);border-radius:var(--rsm);overflow:hidden;flex-shrink:0;display:flex;align-items:center;justify-content:center}
  48. .cart-item-img img{width:100%;height:100%;object-fit:contain}
  49. .cart-item-info{flex:1;min-width:0}
  50. .cart-item-cat{font-size:10px;font-weight:700;color:var(--brand);text-transform:uppercase;letter-spacing:0.05em;margin-bottom:3px}
  51. .cart-item-name{font-size:15px;font-weight:700;color:var(--gray-900);margin-bottom:4px;overflow-wrap:anywhere;line-height:1.3}
  52. .cart-item-variant{font-size:12px;color:var(--gray-400);font-weight:500}
  53. .cart-item-right{display:flex;flex-direction:column;align-items:flex-end;gap:10px;flex-shrink:0}
  54. .cart-item-price{font-size:19px;font-weight:800;color:var(--gray-900);letter-spacing:-0.02em}
  55. .cart-item-price sup{font-size:12px;font-weight:700;vertical-align:super}
  56. .cart-item-subtotal{font-size:11px;color:var(--gray-400);font-weight:500}
  57. .cart-qty{display:flex;align-items:center;border:1.5px solid var(--gray-200);border-radius:var(--rsm);overflow:hidden}
  58. .qty-btn{width:36px;height:36px;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;}
  59. .qty-btn:hover{background:var(--brand-light);color:var(--brand)}
  60. .qty-num{width:40px;height:36px;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}
  61. .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:1.5px solid transparent;font-family:var(--font);padding:8px 12px;border-radius:var(--rsm);min-height:36px}
  62. .remove-btn:hover{color:#ef4444;border-color:#fecaca;background:#fef2f2}
  63. .shop-delivery-line{display:flex;justify-content:space-between;align-items:center;padding:12px 20px;background:var(--gray-50);font-size:13px;font-weight:600}
  64. .shop-delivery-line .lbl{color:var(--gray-500)}
  65. .shop-delivery-line .val{font-weight:800;color:var(--gray-800)}
  66. .shop-delivery-line .val.free{color:#16a34a}
  67. .shop-delivery-line .val.no{color:#dc2626}
  68. .shop-undeliver-note{padding:12px 20px;background:#fef2f2;border-top:1px solid #fecaca;font-size:12.5px;font-weight:600;color:#991b1b;line-height:1.45}
  69. /* Summary */
  70. .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}
  71. .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)}
  72. .summary-row{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;font-size:14px}
  73. .summary-row .label{color:var(--gray-500);font-weight:500}
  74. .summary-row .value{font-weight:700;color:var(--gray-800)}
  75. .summary-row.sub-ship{font-size:12.5px;margin-bottom:8px}
  76. .summary-row.sub-ship .label{color:var(--gray-400);padding-left:10px}
  77. .summary-row.sub-ship .value{color:var(--gray-500);font-weight:600}
  78. .summary-row.sub-ship .value.no{color:#dc2626}
  79. .summary-divider{border:none;border-top:1px solid var(--gray-100);margin:16px 0}
  80. .summary-total{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}
  81. .summary-total .label{font-size:16px;font-weight:700;color:var(--gray-900)}
  82. .summary-total .value{font-size:22px;font-weight:800;color:var(--brand)}
  83. .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}
  84. .btn-checkout:hover{background:var(--brand-dark)}
  85. .btn-checkout.disabled{background:var(--gray-300);cursor:not-allowed;pointer-events:none}
  86. .checkout-block-note{background:#fef2f2;border:1.5px solid #fecaca;border-radius:var(--rsm);padding:11px 14px;font-size:12.5px;font-weight:600;color:#991b1b;margin-bottom:12px;line-height:1.45}
  87. .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}
  88. .btn-continue:hover{border-color:var(--brand);color:var(--brand);background:var(--brand-light)}
  89. .trust-chips{display:flex;gap:12px;flex-wrap:wrap;margin-top:16px;padding-top:16px;border-top:1px solid var(--gray-100)}
  90. .trust-chip{display:flex;align-items:center;gap:5px;font-size:11px;font-weight:600;color:var(--gray-500)}
  91. .updating{opacity:0.5;pointer-events:none}
  92. @media(max-width:768px){
  93.   .cart-layout{grid-template-columns:1fr}
  94.   .cart-nav { padding: 0 14px !important; height: 60px !important; }
  95.   .cart-nav img { height: 38px !important; }
  96.   .cart-body { padding: 20px 14px !important; }
  97.   .cart-title { font-size: 22px !important; }
  98.   .cart-summary { position: static !important; top: auto !important; padding:20px !important; }
  99. }
  100. @media (max-width: 480px) {
  101.   .cart-body { padding: 16px 10px !important; }
  102.   .cart-item { padding: 14px !important; gap: 12px !important; flex-wrap: wrap !important; align-items: flex-start !important; }
  103.   .cart-item-img { width: 64px !important; height: 64px !important; }
  104.   .cart-item-info { flex: 1 1 calc(100% - 80px) !important; min-width: 0 !important; }
  105.   .cart-item-right { flex: 1 1 100% !important; flex-direction: row !important; flex-wrap: wrap !important; align-items: center !important; justify-content: space-between !important; gap: 8px !important; margin-top: 6px !important; padding-top: 12px !important; border-top: 1px solid var(--gray-100) !important; }
  106.   .cart-item-subtotal { order: 3 !important; flex-basis: 100% !important; text-align: right !important; }
  107.   .qty-btn { width: 40px !important; height: 40px !important; font-size: 18px !important; }
  108.   .qty-num { width: 44px !important; height: 40px !important; font-size: 15px !important; }
  109. }
  110. </style>
  111. </head>
  112. <body>
  113. <nav class="cart-nav">
  114.   <a href="{{ path('app_home') }}">
  115.     <img src="/images/julico-logo.png" alt="Julico" style="height:48px;object-fit:contain">
  116.   </a>
  117.   <div style="font-size:18px;font-weight:800;color:var(--gray-900)">๐Ÿ›’ My Cart</div>
  118.   <a href="{{ path('app_home') }}" style="font-size:13px;font-weight:600;color:var(--gray-500);text-decoration:none">โ† Continue Shopping</a>
  119. </nav>
  120. <div class="cart-body">
  121.   <div class="cart-title">Shopping Cart</div>
  122.   <div class="cart-subtitle">
  123.     {% if itemCount > 0 %}{{ itemCount }} item{% if itemCount > 1 %}s{% endif %} in your cart{% else %}Your cart is empty{% endif %}
  124.   </div>
  125.   <div class="cart-layout">
  126.     <div class="cart-items">
  127.       {% if itemCount > 0 %}
  128.         {# โ”€โ”€ Address picker โ”€โ”€ #}
  129.         {% if isLoggedIn %}
  130.           <div class="addr-card">
  131.             <div class="addr-card-title">๐Ÿ“ Deliver to</div>
  132.             {% if addresses|length > 0 %}
  133.               <select class="addr-select" onchange="window.location.href='{{ path('app_panier') }}?addr=' + encodeURIComponent(this.value)">
  134.                 {% for a in addresses %}
  135.                   <option value="{{ a.id }}" {{ a.id == selectedAddressId ? 'selected' : '' }}>
  136.                     {{ a.label ?? 'Address' }} โ€” {{ a.fullAddresse ?? a.ville ?? 'Saved address' }}{% if a.region %} ({{ a.region.name }}){% endif %}
  137.                   </option>
  138.                 {% endfor %}
  139.               </select>
  140.               {% if selectedRegionName %}
  141.                 <div class="addr-zone">Delivery zone: <b>{{ selectedRegionName }}</b></div>
  142.               {% else %}
  143.                 <div class="addr-warn">
  144.                   โš ๏ธ This address has no delivery zone set, so we can't calculate delivery fees.
  145.                   <a href="{{ path('add_addresse') }}">Add a new address</a> (drop a map pin to auto-set the zone), or edit this one.
  146.                 </div>
  147.               {% endif %}
  148.             {% else %}
  149.               <div class="addr-warn">
  150.                 โš ๏ธ You have no saved addresses. <a href="{{ path('add_addresse') }}">Add one</a> to calculate delivery and check out.
  151.               </div>
  152.             {% endif %}
  153.           </div>
  154.         {% else %}
  155.           <div class="addr-card">
  156.             <div class="addr-card-title">๐Ÿ“ Deliver to</div>
  157.             <div class="addr-warn">โš ๏ธ <a href="/login">Sign in</a> to choose a delivery address and see shipping fees.</div>
  158.           </div>
  159.         {% endif %}
  160.         {# โ”€โ”€ Shop groups โ”€โ”€ #}
  161.         {% for g in shopGroups %}
  162.           <div class="shop-group {{ (g.canDeliver is same as(false)) ? 'undeliverable' : '' }}">
  163.             <div class="shop-group-header">
  164.               <div class="shop-group-name">๐Ÿช {{ g.shopName }}</div>
  165.               {% if g.canDeliver is null %}
  166.                 <span class="shop-badge unknown">Select an address</span>
  167.               {% elseif g.canDeliver %}
  168.                 <span class="shop-badge ok">โœ“ Delivers here</span>
  169.               {% else %}
  170.                 <span class="shop-badge no">โœ— No delivery</span>
  171.               {% endif %}
  172.             </div>
  173.             <div class="shop-items">
  174.               {% for item in g.items %}
  175.                 {% set variantId = (item.productVariant and item.productVariant.id is defined) ? item.productVariant.id : 'notdefined' %}
  176.                 <div class="cart-item" id="cart-item-{{ item.product.id }}-{{ variantId }}"
  177.                      data-id="{{ item.product.id }}"
  178.                      data-variant="{{ variantId }}"
  179.                      data-shop="{{ g.shopId }}"
  180.                      data-price="{{ item.unitPrice }}"
  181.                      data-csrf="{{ csrf_token('authenticate') }}"
  182.                      data-add-url="{{ path('panier_add',    {'id': item.product.id, 'idVariante': variantId, 'option': 'continue'}) }}"
  183.                      data-minus-url="{{ path('panier_minus', {'id': item.product.id, 'idVariante': variantId, 'option': 'continue'}) }}"
  184.                      data-remove-url="{{ path('panier_remove',{'id': item.product.id, 'idVariante': variantId}) }}">
  185.                   <div class="cart-item-img">
  186.                     {% if item.product.images|length > 0 %}
  187.                       <img src="{{ asset('assets/uploads/products/' ~ item.product.images[0].fileName) }}"
  188.                            alt="{{ item.product.productName }}"
  189.                            onerror="this.onerror=null;this.parentNode.innerHTML='<span style=font-size:32px;opacity:0.3>๐Ÿ“ฆ</span>'">
  190.                     {% else %}
  191.                       <span style="font-size:32px;opacity:0.3">๐Ÿ“ฆ</span>
  192.                     {% endif %}
  193.                   </div>
  194.                   <div class="cart-item-info">
  195.                     <div class="cart-item-cat">
  196.                       {% if item.product.category is defined and item.product.category %}{{ item.product.category.nom }}{% endif %}
  197.                     </div>
  198.                     <a href="{{ path('show_produit', {'id': item.product.id}) }}" style="text-decoration:none">
  199.                       <div class="cart-item-name">{{ item.product.productName }}</div>
  200.                     </a>
  201.                     {% if item.productVariant %}
  202.                       <div class="cart-item-variant">
  203.                         {% set cvals = [] %}
  204.                         {% if item.productVariant.optionLabel %}{% set cvals = cvals|merge([item.productVariant.optionLabel]) %}{% endif %}
  205.                         {% if item.productVariant.optionLabel2 %}{% set cvals = cvals|merge([item.productVariant.optionLabel2]) %}{% endif %}
  206.                         {% if item.productVariant.optionLabel3 %}{% set cvals = cvals|merge([item.productVariant.optionLabel3]) %}{% endif %}
  207.                         {% if cvals|length > 0 %}
  208.                           {{ cvals|join(' ยท ') }}
  209.                         {% else %}
  210.                           {% if item.productVariant.paramColor is defined and item.productVariant.paramColor %}Color: {{ item.productVariant.paramColor.colorName }}{% endif %}
  211.                           {% if item.productVariant.paramSize is defined and item.productVariant.paramSize %} ยท Size: {{ item.productVariant.paramSize.sizeAbreviation }}{% endif %}
  212.                         {% endif %}
  213.                       </div>
  214.                     {% endif %}
  215.                   </div>
  216.                   <div class="cart-item-right">
  217.                     <div class="cart-item-price"><sup>$</sup>{{ item.unitPrice|number_format(2) }}</div>
  218.                     <div class="cart-item-subtotal" id="subtotal-{{ item.product.id }}-{{ variantId }}">
  219.                       ร— {{ item.quantity }} = ${{ item.lineTotal|number_format(2) }}
  220.                     </div>
  221.                     <div class="cart-qty">
  222.                       <button type="button" class="qty-btn btn-minus" title="Remove one">โˆ’</button>
  223.                       <input type="number" class="qty-num" value="{{ item.quantity }}" readonly>
  224.                       <button type="button" class="qty-btn btn-plus" title="Add one">+</button>
  225.                     </div>
  226.                     <button type="button" class="remove-btn btn-remove">๐Ÿ—‘ Remove</button>
  227.                   </div>
  228.                 </div>
  229.               {% endfor %}
  230.             </div>
  231.             {# Per-shop delivery line #}
  232.             <div class="shop-delivery-line">
  233.               <span class="lbl">Items: <span id="shopsub-{{ g.shopId }}">${{ g.subtotal|number_format(2) }}</span></span>
  234.               {% if g.canDeliver is null %}
  235.                 <span class="val">Delivery: โ€”</span>
  236.               {% elseif g.canDeliver %}
  237.                 <span class="val {{ g.deliveryFee == 0 ? 'free' : '' }}">
  238.                   Delivery: {% if g.deliveryFee == 0 %}Free{% else %}${{ g.deliveryFee|number_format(2) }}{% endif %}
  239.                 </span>
  240.               {% else %}
  241.                 <span class="val no">Delivery: unavailable</span>
  242.               {% endif %}
  243.             </div>
  244.             {% if g.canDeliver is same as(false) %}
  245.               <div class="shop-undeliver-note">
  246.                 โŒ {{ g.shopName }} doesn't deliver to {{ selectedRegionName ?? 'this area' }}. Remove these items or choose a different delivery address to continue.
  247.               </div>
  248.             {% endif %}
  249.           </div>
  250.         {% endfor %}
  251.       {% else %}
  252.         <div class="cart-empty">
  253.           <div class="cart-empty-icon">๐Ÿ›’</div>
  254.           <div class="cart-empty-title">Your cart is empty</div>
  255.           <div class="cart-empty-sub">Looks like you haven't added anything yet.</div>
  256.           <a href="{{ path('app_home') }}" class="btn-checkout" style="max-width:220px;margin:0 auto">Browse Products โ†’</a>
  257.         </div>
  258.       {% endif %}
  259.     </div>
  260.     {% if itemCount > 0 %}
  261.     <div class="cart-summary">
  262.       <div class="summary-title">Order Summary</div>
  263.       <div class="summary-row">
  264.         <span class="label">Subtotal</span>
  265.         <span class="value" id="summary-subtotal">${{ subtotal|number_format(2) }}</span>
  266.       </div>
  267.       {# Per-shop delivery breakdown #}
  268.       {% if deliveryResolvable %}
  269.         {% for g in shopGroups %}
  270.           <div class="summary-row sub-ship">
  271.             <span class="label">๐Ÿšš {{ g.shopName }}</span>
  272.             {% if g.canDeliver %}
  273.               <span class="value">{% if g.deliveryFee == 0 %}Free{% else %}${{ g.deliveryFee|number_format(2) }}{% endif %}</span>
  274.             {% else %}
  275.               <span class="value no">Unavailable</span>
  276.             {% endif %}
  277.           </div>
  278.         {% endfor %}
  279.         <div class="summary-row">
  280.           <span class="label">Total delivery</span>
  281.           <span class="value">{% if totalDelivery == 0 %}Free{% else %}${{ totalDelivery|number_format(2) }}{% endif %}</span>
  282.         </div>
  283.       {% else %}
  284.         <div class="summary-row">
  285.           <span class="label">Delivery</span>
  286.           <span class="value" style="color:var(--gray-400)">Select address</span>
  287.         </div>
  288.       {% endif %}
  289.       <hr class="summary-divider">
  290.       <div class="summary-total">
  291.         <span class="label">Total</span>
  292.         <span class="value" id="summary-total">${{ grandTotal|number_format(2) }}</span>
  293.       </div>
  294.       {% if not isLoggedIn %}
  295.         <a href="/login" class="btn-checkout">Sign In to Checkout โ†’</a>
  296.       {% elseif not deliveryResolvable %}
  297.         <div class="checkout-block-note">๐Ÿ“ Select a delivery address with a zone to continue.</div>
  298.         <span class="btn-checkout disabled">Proceed to Checkout โ†’</span>
  299.       {% elseif hasUndeliverable %}
  300.         <div class="checkout-block-note">โš ๏ธ One or more shops can't deliver to {{ selectedRegionName }}. Remove those items or change your address.</div>
  301.         <span class="btn-checkout disabled">Proceed to Checkout โ†’</span>
  302.       {% else %}
  303.         <form action="{{ path('panier_validationcmd', {'option': 'goprofile', 'addresseid': 'notdefined', 'cmdid': 'notdefined'}) }}" method="POST">
  304.           <input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
  305.           <button type="submit" class="btn-checkout">Proceed to Checkout โ†’</button>
  306.         </form>
  307.       {% endif %}
  308.       <a href="{{ path('app_home') }}" class="btn-continue">โ† Continue Shopping</a>
  309.       <div class="trust-chips">
  310.         <div class="trust-chip">๐Ÿ”’ Secure checkout</div>
  311.         <div class="trust-chip">๐Ÿšš Per-shop delivery</div>
  312.         <div class="trust-chip">โ†ฉ๏ธ Easy returns</div>
  313.       </div>
  314.     </div>
  315.     {% endif %}
  316.   </div>
  317. </div>
  318. <script>
  319. document.addEventListener('DOMContentLoaded', function() {
  320.   document.querySelectorAll('.cart-item').forEach(function(itemEl) {
  321.     var id        = itemEl.dataset.id;
  322.     var variant   = itemEl.dataset.variant;
  323.     var shopId    = itemEl.dataset.shop;
  324.     var csrf      = itemEl.dataset.csrf;
  325.     var addUrl    = itemEl.dataset.addUrl;
  326.     var minusUrl  = itemEl.dataset.minusUrl;
  327.     var removeUrl = itemEl.dataset.removeUrl;
  328.     var price     = parseFloat(itemEl.dataset.price);
  329.     var qtyInput  = itemEl.querySelector('.qty-num');
  330.     var subtotal  = document.getElementById('subtotal-' + id + '-' + variant);
  331.     var plusBtn   = itemEl.querySelector('.btn-plus');
  332.     var minusBtn  = itemEl.querySelector('.btn-minus');
  333.     var removeBtn = itemEl.querySelector('.btn-remove');
  334.     plusBtn.addEventListener('click', function() {
  335.       ajaxQty(addUrl, csrf, price, 1, itemEl, qtyInput, subtotal, shopId);
  336.     });
  337.     minusBtn.addEventListener('click', function() {
  338.       var qty = parseInt(qtyInput.value, 10);
  339.       if (isNaN(qty) || qty <= 1) {
  340.         ajaxRemove(removeUrl, csrf, itemEl);
  341.       } else {
  342.         ajaxQty(minusUrl, csrf, price, -1, itemEl, qtyInput, subtotal, shopId);
  343.       }
  344.     });
  345.     removeBtn.addEventListener('click', function() {
  346.       ajaxRemove(removeUrl, csrf, itemEl);
  347.     });
  348.   });
  349. });
  350. function ajaxQty(url, csrf, price, delta, itemEl, qtyInput, subtotalEl, shopId) {
  351.   itemEl.classList.add('updating');
  352.   var fd = new FormData();
  353.   fd.append('_csrf_token', csrf);
  354.   fd.append('qttprdid', '1');
  355.   var xhr = new XMLHttpRequest();
  356.   xhr.open('POST', url);
  357.   xhr.withCredentials = true;
  358.   xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
  359.   xhr.setRequestHeader('Accept', 'application/json');
  360.   xhr.onload = function() {
  361.     itemEl.classList.remove('updating');
  362.     if (xhr.status === 200) {
  363.       var newQty = Math.max(0, parseInt(qtyInput.value) + delta);
  364.       qtyInput.value = newQty;
  365.       if (subtotalEl) subtotalEl.textContent = 'ร— ' + newQty + ' = $' + (price * newQty).toFixed(2);
  366.       adjustMoney(price * delta, shopId);
  367.     }
  368.   };
  369.   xhr.onerror = function() { itemEl.classList.remove('updating'); };
  370.   xhr.send(fd);
  371. }
  372. function ajaxRemove(url, csrf, itemEl) {
  373.   itemEl.classList.add('updating');
  374.   var fd = new FormData();
  375.   fd.append('_csrf_token', csrf);
  376.   var xhr = new XMLHttpRequest();
  377.   xhr.open('POST', url);
  378.   xhr.withCredentials = true;
  379.   xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
  380.   xhr.setRequestHeader('Accept', 'application/json');
  381.   xhr.onload = function() {
  382.     window.location.reload();
  383.   };
  384.   xhr.onerror = function() { itemEl.classList.remove('updating'); };
  385.   xhr.send(fd);
  386. }
  387. function adjustMoney(delta, shopId) {
  388.   var shopSub = document.getElementById('shopsub-' + shopId);
  389.   if (shopSub) {
  390.     var v = parseFloat(shopSub.textContent.replace('$','').replace(',','')) + delta;
  391.     shopSub.textContent = '$' + Math.max(0, v).toFixed(2);
  392.   }
  393.   var subEl = document.getElementById('summary-subtotal');
  394.   var totEl = document.getElementById('summary-total');
  395.   if (subEl) {
  396.     var s = parseFloat(subEl.textContent.replace('$','').replace(',','')) + delta;
  397.     subEl.textContent = '$' + Math.max(0, s).toFixed(2);
  398.   }
  399.   if (totEl) {
  400.     var t = parseFloat(totEl.textContent.replace('$','').replace(',','')) + delta;
  401.     totEl.textContent = '$' + Math.max(0, t).toFixed(2);
  402.   }
  403. }
  404. </script>
  405. </body>
  406. </html>