<!DOCTYPE html>
<html lang="{{ app.session.get('_locale') ?? 'en' }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Cart โ Julico</title>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/julico-home.css">
<style>
*, *::before, *::after { box-sizing: border-box; }
html, body { max-width: 100%; overflow-x: hidden; }
img, video, iframe { max-width: 100%; height: auto; }
body{background:var(--gray-50);min-height:100vh}
.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}
.cart-body{max-width:1280px;margin:0 auto;padding:32px 24px}
.cart-title{font-size:28px;font-weight:800;color:var(--gray-900);letter-spacing:-0.02em;margin-bottom:6px}
.cart-subtitle{font-size:14px;color:var(--gray-400);font-weight:500;margin-bottom:28px}
.cart-layout{display:grid;grid-template-columns:1fr 360px;gap:24px;align-items:start}
.cart-items{display:flex;flex-direction:column;gap:18px}
.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)}
.cart-empty-icon{font-size:64px;margin-bottom:16px}
.cart-empty-title{font-size:20px;font-weight:800;color:var(--gray-900);margin-bottom:8px}
.cart-empty-sub{font-size:14px;color:var(--gray-400);margin-bottom:24px}
/* Address picker */
.addr-card{background:var(--white);border:1.5px solid var(--gray-100);border-radius:var(--rlg);padding:18px 20px;box-shadow:var(--shadow-sm)}
.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}
.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}
.addr-select:focus{border-color:var(--brand)}
.addr-zone{font-size:12px;font-weight:600;margin-top:9px;color:var(--gray-500)}
.addr-zone b{color:var(--brand)}
.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}
.addr-warn a{color:#854d0e;font-weight:800;text-decoration:underline}
.addr-link{display:inline-block;margin-top:10px;font-size:13px;font-weight:700;color:var(--brand);text-decoration:none}
.addr-link:hover{text-decoration:underline}
/* Shop group */
.shop-group{background:var(--white);border:1.5px solid var(--gray-100);border-radius:var(--rlg);box-shadow:var(--shadow-sm);overflow:hidden}
.shop-group.undeliverable{border-color:#fecaca}
.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}
.shop-group-name{font-size:14px;font-weight:800;color:var(--gray-900);display:flex;align-items:center;gap:7px}
.shop-badge{font-size:11px;font-weight:800;padding:5px 11px;border-radius:999px;white-space:nowrap}
.shop-badge.ok{background:#dcfce7;color:#16a34a}
.shop-badge.no{background:#fee2e2;color:#dc2626}
.shop-badge.unknown{background:var(--gray-100);color:var(--gray-500)}
.shop-items{display:flex;flex-direction:column}
.cart-item{padding:18px 20px;display:flex;align-items:center;gap:16px;border-bottom:1px solid var(--gray-100);transition:opacity .2s}
.cart-item:last-child{border-bottom:none}
.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}
.cart-item-img img{width:100%;height:100%;object-fit:contain}
.cart-item-info{flex:1;min-width:0}
.cart-item-cat{font-size:10px;font-weight:700;color:var(--brand);text-transform:uppercase;letter-spacing:0.05em;margin-bottom:3px}
.cart-item-name{font-size:15px;font-weight:700;color:var(--gray-900);margin-bottom:4px;overflow-wrap:anywhere;line-height:1.3}
.cart-item-variant{font-size:12px;color:var(--gray-400);font-weight:500}
.cart-item-right{display:flex;flex-direction:column;align-items:flex-end;gap:10px;flex-shrink:0}
.cart-item-price{font-size:19px;font-weight:800;color:var(--gray-900);letter-spacing:-0.02em}
.cart-item-price sup{font-size:12px;font-weight:700;vertical-align:super}
.cart-item-subtotal{font-size:11px;color:var(--gray-400);font-weight:500}
.cart-qty{display:flex;align-items:center;border:1.5px solid var(--gray-200);border-radius:var(--rsm);overflow:hidden}
.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;}
.qty-btn:hover{background:var(--brand-light);color:var(--brand)}
.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}
.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}
.remove-btn:hover{color:#ef4444;border-color:#fecaca;background:#fef2f2}
.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}
.shop-delivery-line .lbl{color:var(--gray-500)}
.shop-delivery-line .val{font-weight:800;color:var(--gray-800)}
.shop-delivery-line .val.free{color:#16a34a}
.shop-delivery-line .val.no{color:#dc2626}
.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}
/* Summary */
.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}
.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)}
.summary-row{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;font-size:14px}
.summary-row .label{color:var(--gray-500);font-weight:500}
.summary-row .value{font-weight:700;color:var(--gray-800)}
.summary-row.sub-ship{font-size:12.5px;margin-bottom:8px}
.summary-row.sub-ship .label{color:var(--gray-400);padding-left:10px}
.summary-row.sub-ship .value{color:var(--gray-500);font-weight:600}
.summary-row.sub-ship .value.no{color:#dc2626}
.summary-divider{border:none;border-top:1px solid var(--gray-100);margin:16px 0}
.summary-total{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}
.summary-total .label{font-size:16px;font-weight:700;color:var(--gray-900)}
.summary-total .value{font-size:22px;font-weight:800;color:var(--brand)}
.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}
.btn-checkout:hover{background:var(--brand-dark)}
.btn-checkout.disabled{background:var(--gray-300);cursor:not-allowed;pointer-events:none}
.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}
.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}
.btn-continue:hover{border-color:var(--brand);color:var(--brand);background:var(--brand-light)}
.trust-chips{display:flex;gap:12px;flex-wrap:wrap;margin-top:16px;padding-top:16px;border-top:1px solid var(--gray-100)}
.trust-chip{display:flex;align-items:center;gap:5px;font-size:11px;font-weight:600;color:var(--gray-500)}
.updating{opacity:0.5;pointer-events:none}
@media(max-width:768px){
.cart-layout{grid-template-columns:1fr}
.cart-nav { padding: 0 14px !important; height: 60px !important; }
.cart-nav img { height: 38px !important; }
.cart-body { padding: 20px 14px !important; }
.cart-title { font-size: 22px !important; }
.cart-summary { position: static !important; top: auto !important; padding:20px !important; }
}
@media (max-width: 480px) {
.cart-body { padding: 16px 10px !important; }
.cart-item { padding: 14px !important; gap: 12px !important; flex-wrap: wrap !important; align-items: flex-start !important; }
.cart-item-img { width: 64px !important; height: 64px !important; }
.cart-item-info { flex: 1 1 calc(100% - 80px) !important; min-width: 0 !important; }
.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; }
.cart-item-subtotal { order: 3 !important; flex-basis: 100% !important; text-align: right !important; }
.qty-btn { width: 40px !important; height: 40px !important; font-size: 18px !important; }
.qty-num { width: 44px !important; height: 40px !important; font-size: 15px !important; }
}
</style>
</head>
<body>
<nav class="cart-nav">
<a href="{{ path('app_home') }}">
<img src="/images/julico-logo.png" alt="Julico" style="height:48px;object-fit:contain">
</a>
<div style="font-size:18px;font-weight:800;color:var(--gray-900)">๐ My Cart</div>
<a href="{{ path('app_home') }}" style="font-size:13px;font-weight:600;color:var(--gray-500);text-decoration:none">โ Continue Shopping</a>
</nav>
<div class="cart-body">
<div class="cart-title">Shopping Cart</div>
<div class="cart-subtitle">
{% if itemCount > 0 %}{{ itemCount }} item{% if itemCount > 1 %}s{% endif %} in your cart{% else %}Your cart is empty{% endif %}
</div>
<div class="cart-layout">
<div class="cart-items">
{% if itemCount > 0 %}
{# โโ Address picker โโ #}
{% if isLoggedIn %}
<div class="addr-card">
<div class="addr-card-title">๐ Deliver to</div>
{% if addresses|length > 0 %}
<select class="addr-select" onchange="window.location.href='{{ path('app_panier') }}?addr=' + encodeURIComponent(this.value)">
{% for a in addresses %}
<option value="{{ a.id }}" {{ a.id == selectedAddressId ? 'selected' : '' }}>
{{ a.label ?? 'Address' }} โ {{ a.fullAddresse ?? a.ville ?? 'Saved address' }}{% if a.region %} ({{ a.region.name }}){% endif %}
</option>
{% endfor %}
</select>
{% if selectedRegionName %}
<div class="addr-zone">Delivery zone: <b>{{ selectedRegionName }}</b></div>
{% else %}
<div class="addr-warn">
โ ๏ธ This address has no delivery zone set, so we can't calculate delivery fees.
<a href="{{ path('add_addresse') }}">Add a new address</a> (drop a map pin to auto-set the zone), or edit this one.
</div>
{% endif %}
{% else %}
<div class="addr-warn">
โ ๏ธ You have no saved addresses. <a href="{{ path('add_addresse') }}">Add one</a> to calculate delivery and check out.
</div>
{% endif %}
</div>
{% else %}
<div class="addr-card">
<div class="addr-card-title">๐ Deliver to</div>
<div class="addr-warn">โ ๏ธ <a href="/login">Sign in</a> to choose a delivery address and see shipping fees.</div>
</div>
{% endif %}
{# โโ Shop groups โโ #}
{% for g in shopGroups %}
<div class="shop-group {{ (g.canDeliver is same as(false)) ? 'undeliverable' : '' }}">
<div class="shop-group-header">
<div class="shop-group-name">๐ช {{ g.shopName }}</div>
{% if g.canDeliver is null %}
<span class="shop-badge unknown">Select an address</span>
{% elseif g.canDeliver %}
<span class="shop-badge ok">โ Delivers here</span>
{% else %}
<span class="shop-badge no">โ No delivery</span>
{% endif %}
</div>
<div class="shop-items">
{% for item in g.items %}
{% set variantId = (item.productVariant and item.productVariant.id is defined) ? item.productVariant.id : 'notdefined' %}
<div class="cart-item" id="cart-item-{{ item.product.id }}-{{ variantId }}"
data-id="{{ item.product.id }}"
data-variant="{{ variantId }}"
data-shop="{{ g.shopId }}"
data-price="{{ item.unitPrice }}"
data-csrf="{{ csrf_token('authenticate') }}"
data-add-url="{{ path('panier_add', {'id': item.product.id, 'idVariante': variantId, 'option': 'continue'}) }}"
data-minus-url="{{ path('panier_minus', {'id': item.product.id, 'idVariante': variantId, 'option': 'continue'}) }}"
data-remove-url="{{ path('panier_remove',{'id': item.product.id, 'idVariante': variantId}) }}">
<div class="cart-item-img">
{% if item.product.images|length > 0 %}
<img src="{{ asset('assets/uploads/products/' ~ item.product.images[0].fileName) }}"
alt="{{ item.product.productName }}"
onerror="this.onerror=null;this.parentNode.innerHTML='<span style=font-size:32px;opacity:0.3>๐ฆ</span>'">
{% else %}
<span style="font-size:32px;opacity:0.3">๐ฆ</span>
{% endif %}
</div>
<div class="cart-item-info">
<div class="cart-item-cat">
{% if item.product.category is defined and item.product.category %}{{ item.product.category.nom }}{% endif %}
</div>
<a href="{{ path('show_produit', {'id': item.product.id}) }}" style="text-decoration:none">
<div class="cart-item-name">{{ item.product.productName }}</div>
</a>
{% if item.productVariant %}
<div class="cart-item-variant">
{% set cvals = [] %}
{% if item.productVariant.optionLabel %}{% set cvals = cvals|merge([item.productVariant.optionLabel]) %}{% endif %}
{% if item.productVariant.optionLabel2 %}{% set cvals = cvals|merge([item.productVariant.optionLabel2]) %}{% endif %}
{% if item.productVariant.optionLabel3 %}{% set cvals = cvals|merge([item.productVariant.optionLabel3]) %}{% endif %}
{% if cvals|length > 0 %}
{{ cvals|join(' ยท ') }}
{% else %}
{% if item.productVariant.paramColor is defined and item.productVariant.paramColor %}Color: {{ item.productVariant.paramColor.colorName }}{% endif %}
{% if item.productVariant.paramSize is defined and item.productVariant.paramSize %} ยท Size: {{ item.productVariant.paramSize.sizeAbreviation }}{% endif %}
{% endif %}
</div>
{% endif %}
</div>
<div class="cart-item-right">
<div class="cart-item-price"><sup>$</sup>{{ item.unitPrice|number_format(2) }}</div>
<div class="cart-item-subtotal" id="subtotal-{{ item.product.id }}-{{ variantId }}">
ร {{ item.quantity }} = ${{ item.lineTotal|number_format(2) }}
</div>
<div class="cart-qty">
<button type="button" class="qty-btn btn-minus" title="Remove one">โ</button>
<input type="number" class="qty-num" value="{{ item.quantity }}" readonly>
<button type="button" class="qty-btn btn-plus" title="Add one">+</button>
</div>
<button type="button" class="remove-btn btn-remove">๐ Remove</button>
</div>
</div>
{% endfor %}
</div>
{# Per-shop delivery line #}
<div class="shop-delivery-line">
<span class="lbl">Items: <span id="shopsub-{{ g.shopId }}">${{ g.subtotal|number_format(2) }}</span></span>
{% if g.canDeliver is null %}
<span class="val">Delivery: โ</span>
{% elseif g.canDeliver %}
<span class="val {{ g.deliveryFee == 0 ? 'free' : '' }}">
Delivery: {% if g.deliveryFee == 0 %}Free{% else %}${{ g.deliveryFee|number_format(2) }}{% endif %}
</span>
{% else %}
<span class="val no">Delivery: unavailable</span>
{% endif %}
</div>
{% if g.canDeliver is same as(false) %}
<div class="shop-undeliver-note">
โ {{ g.shopName }} doesn't deliver to {{ selectedRegionName ?? 'this area' }}. Remove these items or choose a different delivery address to continue.
</div>
{% endif %}
</div>
{% endfor %}
{% else %}
<div class="cart-empty">
<div class="cart-empty-icon">๐</div>
<div class="cart-empty-title">Your cart is empty</div>
<div class="cart-empty-sub">Looks like you haven't added anything yet.</div>
<a href="{{ path('app_home') }}" class="btn-checkout" style="max-width:220px;margin:0 auto">Browse Products โ</a>
</div>
{% endif %}
</div>
{% if itemCount > 0 %}
<div class="cart-summary">
<div class="summary-title">Order Summary</div>
<div class="summary-row">
<span class="label">Subtotal</span>
<span class="value" id="summary-subtotal">${{ subtotal|number_format(2) }}</span>
</div>
{# Per-shop delivery breakdown #}
{% if deliveryResolvable %}
{% for g in shopGroups %}
<div class="summary-row sub-ship">
<span class="label">๐ {{ g.shopName }}</span>
{% if g.canDeliver %}
<span class="value">{% if g.deliveryFee == 0 %}Free{% else %}${{ g.deliveryFee|number_format(2) }}{% endif %}</span>
{% else %}
<span class="value no">Unavailable</span>
{% endif %}
</div>
{% endfor %}
<div class="summary-row">
<span class="label">Total delivery</span>
<span class="value">{% if totalDelivery == 0 %}Free{% else %}${{ totalDelivery|number_format(2) }}{% endif %}</span>
</div>
{% else %}
<div class="summary-row">
<span class="label">Delivery</span>
<span class="value" style="color:var(--gray-400)">Select address</span>
</div>
{% endif %}
<hr class="summary-divider">
<div class="summary-total">
<span class="label">Total</span>
<span class="value" id="summary-total">${{ grandTotal|number_format(2) }}</span>
</div>
{% if not isLoggedIn %}
<a href="/login" class="btn-checkout">Sign In to Checkout โ</a>
{% elseif not deliveryResolvable %}
<div class="checkout-block-note">๐ Select a delivery address with a zone to continue.</div>
<span class="btn-checkout disabled">Proceed to Checkout โ</span>
{% elseif hasUndeliverable %}
<div class="checkout-block-note">โ ๏ธ One or more shops can't deliver to {{ selectedRegionName }}. Remove those items or change your address.</div>
<span class="btn-checkout disabled">Proceed to Checkout โ</span>
{% else %}
<form action="{{ path('panier_validationcmd', {'option': 'goprofile', 'addresseid': 'notdefined', 'cmdid': 'notdefined'}) }}" method="POST">
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
<button type="submit" class="btn-checkout">Proceed to Checkout โ</button>
</form>
{% endif %}
<a href="{{ path('app_home') }}" class="btn-continue">โ Continue Shopping</a>
<div class="trust-chips">
<div class="trust-chip">๐ Secure checkout</div>
<div class="trust-chip">๐ Per-shop delivery</div>
<div class="trust-chip">โฉ๏ธ Easy returns</div>
</div>
</div>
{% endif %}
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.cart-item').forEach(function(itemEl) {
var id = itemEl.dataset.id;
var variant = itemEl.dataset.variant;
var shopId = itemEl.dataset.shop;
var csrf = itemEl.dataset.csrf;
var addUrl = itemEl.dataset.addUrl;
var minusUrl = itemEl.dataset.minusUrl;
var removeUrl = itemEl.dataset.removeUrl;
var price = parseFloat(itemEl.dataset.price);
var qtyInput = itemEl.querySelector('.qty-num');
var subtotal = document.getElementById('subtotal-' + id + '-' + variant);
var plusBtn = itemEl.querySelector('.btn-plus');
var minusBtn = itemEl.querySelector('.btn-minus');
var removeBtn = itemEl.querySelector('.btn-remove');
plusBtn.addEventListener('click', function() {
ajaxQty(addUrl, csrf, price, 1, itemEl, qtyInput, subtotal, shopId);
});
minusBtn.addEventListener('click', function() {
var qty = parseInt(qtyInput.value, 10);
if (isNaN(qty) || qty <= 1) {
ajaxRemove(removeUrl, csrf, itemEl);
} else {
ajaxQty(minusUrl, csrf, price, -1, itemEl, qtyInput, subtotal, shopId);
}
});
removeBtn.addEventListener('click', function() {
ajaxRemove(removeUrl, csrf, itemEl);
});
});
});
function ajaxQty(url, csrf, price, delta, itemEl, qtyInput, subtotalEl, shopId) {
itemEl.classList.add('updating');
var fd = new FormData();
fd.append('_csrf_token', csrf);
fd.append('qttprdid', '1');
var xhr = new XMLHttpRequest();
xhr.open('POST', url);
xhr.withCredentials = true;
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.setRequestHeader('Accept', 'application/json');
xhr.onload = function() {
itemEl.classList.remove('updating');
if (xhr.status === 200) {
var newQty = Math.max(0, parseInt(qtyInput.value) + delta);
qtyInput.value = newQty;
if (subtotalEl) subtotalEl.textContent = 'ร ' + newQty + ' = $' + (price * newQty).toFixed(2);
adjustMoney(price * delta, shopId);
}
};
xhr.onerror = function() { itemEl.classList.remove('updating'); };
xhr.send(fd);
}
function ajaxRemove(url, csrf, itemEl) {
itemEl.classList.add('updating');
var fd = new FormData();
fd.append('_csrf_token', csrf);
var xhr = new XMLHttpRequest();
xhr.open('POST', url);
xhr.withCredentials = true;
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.setRequestHeader('Accept', 'application/json');
xhr.onload = function() {
window.location.reload();
};
xhr.onerror = function() { itemEl.classList.remove('updating'); };
xhr.send(fd);
}
function adjustMoney(delta, shopId) {
var shopSub = document.getElementById('shopsub-' + shopId);
if (shopSub) {
var v = parseFloat(shopSub.textContent.replace('$','').replace(',','')) + delta;
shopSub.textContent = '$' + Math.max(0, v).toFixed(2);
}
var subEl = document.getElementById('summary-subtotal');
var totEl = document.getElementById('summary-total');
if (subEl) {
var s = parseFloat(subEl.textContent.replace('$','').replace(',','')) + delta;
subEl.textContent = '$' + Math.max(0, s).toFixed(2);
}
if (totEl) {
var t = parseFloat(totEl.textContent.replace('$','').replace(',','')) + delta;
totEl.textContent = '$' + Math.max(0, t).toFixed(2);
}
}
</script>
</body>
</html>