489 lines
15 KiB
HTML
489 lines
15 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Shopping Cart - {{ vendor.name }}</title>
|
||
<link rel="stylesheet" href="/static/css/shared/base.css">
|
||
<link rel="stylesheet" href="/static/css/vendor/vendor.css">
|
||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||
</head>
|
||
<body>
|
||
<div x-data="shoppingCart()"
|
||
x-init="loadCart()"
|
||
data-vendor-id="{{ vendor.id }}"
|
||
>
|
||
<!-- Header -->
|
||
<header class="header">
|
||
<div class="header-left">
|
||
<h1>🛒 Shopping Cart</h1>
|
||
</div>
|
||
<div class="header-right">
|
||
<a href="/shop" class="btn-secondary">← Continue Shopping</a>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="container">
|
||
<!-- Loading State -->
|
||
<div x-show="loading && items.length === 0" class="loading">
|
||
<div class="loading-spinner-lg"></div>
|
||
<p>Loading your cart...</p>
|
||
</div>
|
||
|
||
<!-- Empty Cart -->
|
||
<div x-show="!loading && items.length === 0" class="empty-state">
|
||
<div class="empty-state-icon">🛒</div>
|
||
<h3>Your cart is empty</h3>
|
||
<p>Add some products to get started!</p>
|
||
<a href="/shop/products" class="btn-primary">Browse Products</a>
|
||
</div>
|
||
|
||
<!-- Cart Items -->
|
||
<div x-show="items.length > 0" class="cart-content">
|
||
<!-- Cart Items List -->
|
||
<div class="cart-items">
|
||
<template x-for="item in items" :key="item.product_id">
|
||
<div class="cart-item-card">
|
||
<div class="item-image">
|
||
<img :src="item.image_url || '/static/images/placeholder.png'"
|
||
:alt="item.name">
|
||
</div>
|
||
|
||
<div class="item-details">
|
||
<h3 class="item-name" x-text="item.name"></h3>
|
||
<p class="item-sku" x-text="'SKU: ' + item.sku"></p>
|
||
<p class="item-price">
|
||
€<span x-text="parseFloat(item.price).toFixed(2)"></span>
|
||
</p>
|
||
</div>
|
||
|
||
<div class="item-quantity">
|
||
<label>Quantity:</label>
|
||
<div class="quantity-controls">
|
||
<button
|
||
@click="updateQuantity(item.product_id, item.quantity - 1)"
|
||
:disabled="item.quantity <= 1 || updating"
|
||
class="btn-quantity"
|
||
>
|
||
−
|
||
</button>
|
||
<input
|
||
type="number"
|
||
:value="item.quantity"
|
||
@change="updateQuantity(item.product_id, $event.target.value)"
|
||
min="1"
|
||
max="99"
|
||
:disabled="updating"
|
||
class="quantity-input"
|
||
>
|
||
<button
|
||
@click="updateQuantity(item.product_id, item.quantity + 1)"
|
||
:disabled="updating"
|
||
class="btn-quantity"
|
||
>
|
||
+
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-total">
|
||
<label>Subtotal:</label>
|
||
<p class="item-total-price">
|
||
€<span x-text="(parseFloat(item.price) * item.quantity).toFixed(2)"></span>
|
||
</p>
|
||
</div>
|
||
|
||
<div class="item-actions">
|
||
<button
|
||
@click="removeItem(item.product_id)"
|
||
:disabled="updating"
|
||
class="btn-remove"
|
||
title="Remove from cart"
|
||
>
|
||
🗑️
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- Cart Summary -->
|
||
<div class="cart-summary">
|
||
<div class="summary-card">
|
||
<h3>Order Summary</h3>
|
||
|
||
<div class="summary-row">
|
||
<span>Subtotal (<span x-text="totalItems"></span> items):</span>
|
||
<span>€<span x-text="subtotal.toFixed(2)"></span></span>
|
||
</div>
|
||
|
||
<div class="summary-row">
|
||
<span>Shipping:</span>
|
||
<span x-text="shipping > 0 ? '€' + shipping.toFixed(2) : 'FREE'"></span>
|
||
</div>
|
||
|
||
<div class="summary-row summary-total">
|
||
<span>Total:</span>
|
||
<span class="total-amount">€<span x-text="total.toFixed(2)"></span></span>
|
||
</div>
|
||
|
||
<button
|
||
@click="proceedToCheckout()"
|
||
:disabled="updating || items.length === 0"
|
||
class="btn-primary btn-checkout"
|
||
>
|
||
Proceed to Checkout
|
||
</button>
|
||
|
||
<a href="/shop/products" class="btn-outline">
|
||
Continue Shopping
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
function shoppingCart() {
|
||
return {
|
||
items: [],
|
||
loading: false,
|
||
updating: false,
|
||
vendorId: null,
|
||
sessionId: null,
|
||
|
||
// Computed properties
|
||
get totalItems() {
|
||
return this.items.reduce((sum, item) => sum + item.quantity, 0);
|
||
},
|
||
|
||
get subtotal() {
|
||
return this.items.reduce((sum, item) =>
|
||
sum + (parseFloat(item.price) * item.quantity), 0
|
||
);
|
||
},
|
||
|
||
get shipping() {
|
||
// Free shipping over €50
|
||
return this.subtotal >= 50 ? 0 : 5.99;
|
||
},
|
||
|
||
get total() {
|
||
return this.subtotal + this.shipping;
|
||
},
|
||
|
||
// Initialize
|
||
init() {
|
||
this.vendorId = this.$el.dataset.vendorId;
|
||
this.sessionId = this.getOrCreateSessionId();
|
||
},
|
||
|
||
// Get or create session ID
|
||
getOrCreateSessionId() {
|
||
let sessionId = localStorage.getItem('cart_session_id');
|
||
if (!sessionId) {
|
||
sessionId = 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||
localStorage.setItem('cart_session_id', sessionId);
|
||
}
|
||
return sessionId;
|
||
},
|
||
|
||
// Load cart from API
|
||
async loadCart() {
|
||
this.loading = true;
|
||
|
||
try {
|
||
const response = await fetch(
|
||
`/api/v1/public/vendors/${this.vendorId}/cart/${this.sessionId}`
|
||
);
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
this.items = data.items || [];
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to load cart:', error);
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
},
|
||
|
||
// Update item quantity
|
||
async updateQuantity(productId, newQuantity) {
|
||
newQuantity = parseInt(newQuantity);
|
||
|
||
if (newQuantity < 1 || newQuantity > 99) return;
|
||
|
||
this.updating = true;
|
||
|
||
try {
|
||
const response = await fetch(
|
||
`/api/v1/public/vendors/${this.vendorId}/cart/${this.sessionId}/items/${productId}`,
|
||
{
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({ quantity: newQuantity })
|
||
}
|
||
);
|
||
|
||
if (response.ok) {
|
||
await this.loadCart();
|
||
} else {
|
||
throw new Error('Failed to update quantity');
|
||
}
|
||
} catch (error) {
|
||
console.error('Update quantity error:', error);
|
||
alert('Failed to update quantity. Please try again.');
|
||
} finally {
|
||
this.updating = false;
|
||
}
|
||
},
|
||
|
||
// Remove item from cart
|
||
async removeItem(productId) {
|
||
if (!confirm('Remove this item from your cart?')) {
|
||
return;
|
||
}
|
||
|
||
this.updating = true;
|
||
|
||
try {
|
||
const response = await fetch(
|
||
`/api/v1/public/vendors/${this.vendorId}/cart/${this.sessionId}/items/${productId}`,
|
||
{
|
||
method: 'DELETE'
|
||
}
|
||
);
|
||
|
||
if (response.ok) {
|
||
await this.loadCart();
|
||
} else {
|
||
throw new Error('Failed to remove item');
|
||
}
|
||
} catch (error) {
|
||
console.error('Remove item error:', error);
|
||
alert('Failed to remove item. Please try again.');
|
||
} finally {
|
||
this.updating = false;
|
||
}
|
||
},
|
||
|
||
// Proceed to checkout
|
||
proceedToCheckout() {
|
||
// Check if customer is logged in
|
||
const token = localStorage.getItem('customer_token');
|
||
|
||
if (!token) {
|
||
// Redirect to login with return URL
|
||
window.location.href = '/shop/account/login?return=/shop/checkout';
|
||
} else {
|
||
window.location.href = '/shop/checkout';
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
/* Cart-specific styles */
|
||
.cart-content {
|
||
display: grid;
|
||
grid-template-columns: 1fr 350px;
|
||
gap: var(--spacing-lg);
|
||
margin-top: var(--spacing-lg);
|
||
}
|
||
|
||
.cart-items {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--spacing-md);
|
||
}
|
||
|
||
.cart-item-card {
|
||
background: white;
|
||
border-radius: var(--radius-lg);
|
||
padding: var(--spacing-lg);
|
||
display: grid;
|
||
grid-template-columns: 100px 1fr auto auto auto;
|
||
gap: var(--spacing-lg);
|
||
align-items: center;
|
||
box-shadow: var(--shadow-md);
|
||
}
|
||
|
||
.item-image img {
|
||
width: 100px;
|
||
height: 100px;
|
||
object-fit: cover;
|
||
border-radius: var(--radius-md);
|
||
}
|
||
|
||
.item-name {
|
||
font-size: var(--font-lg);
|
||
font-weight: 600;
|
||
margin-bottom: var(--spacing-xs);
|
||
}
|
||
|
||
.item-sku {
|
||
font-size: var(--font-sm);
|
||
color: var(--text-muted);
|
||
margin-bottom: var(--spacing-xs);
|
||
}
|
||
|
||
.item-price {
|
||
font-size: var(--font-lg);
|
||
font-weight: 600;
|
||
color: var(--primary-color);
|
||
}
|
||
|
||
.quantity-controls {
|
||
display: flex;
|
||
gap: var(--spacing-sm);
|
||
align-items: center;
|
||
}
|
||
|
||
.btn-quantity {
|
||
width: 32px;
|
||
height: 32px;
|
||
border: 1px solid var(--border-color);
|
||
background: white;
|
||
border-radius: var(--radius-sm);
|
||
cursor: pointer;
|
||
font-size: var(--font-lg);
|
||
font-weight: 600;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.btn-quantity:hover:not(:disabled) {
|
||
background: var(--gray-50);
|
||
border-color: var(--primary-color);
|
||
}
|
||
|
||
.btn-quantity:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.quantity-input {
|
||
width: 60px;
|
||
text-align: center;
|
||
padding: 8px;
|
||
border: 1px solid var(--border-color);
|
||
border-radius: var(--radius-sm);
|
||
}
|
||
|
||
.item-total-price {
|
||
font-size: var(--font-xl);
|
||
font-weight: 700;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.btn-remove {
|
||
width: 40px;
|
||
height: 40px;
|
||
border: 1px solid var(--border-color);
|
||
background: white;
|
||
border-radius: var(--radius-sm);
|
||
cursor: pointer;
|
||
font-size: var(--font-lg);
|
||
transition: all var(--transition-base);
|
||
}
|
||
|
||
.btn-remove:hover:not(:disabled) {
|
||
background: var(--danger-color);
|
||
border-color: var(--danger-color);
|
||
color: white;
|
||
}
|
||
|
||
.cart-summary {
|
||
position: sticky;
|
||
top: var(--spacing-lg);
|
||
height: fit-content;
|
||
}
|
||
|
||
.summary-card {
|
||
background: white;
|
||
border-radius: var(--radius-lg);
|
||
padding: var(--spacing-lg);
|
||
box-shadow: var(--shadow-md);
|
||
}
|
||
|
||
.summary-card h3 {
|
||
font-size: var(--font-xl);
|
||
margin-bottom: var(--spacing-lg);
|
||
padding-bottom: var(--spacing-md);
|
||
border-bottom: 2px solid var(--border-color);
|
||
}
|
||
|
||
.summary-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-bottom: var(--spacing-md);
|
||
font-size: var(--font-base);
|
||
}
|
||
|
||
.summary-total {
|
||
font-size: var(--font-lg);
|
||
font-weight: 700;
|
||
padding-top: var(--spacing-md);
|
||
border-top: 2px solid var(--border-color);
|
||
margin-top: var(--spacing-md);
|
||
}
|
||
|
||
.total-amount {
|
||
color: var(--primary-color);
|
||
font-size: var(--font-2xl);
|
||
}
|
||
|
||
.btn-checkout {
|
||
width: 100%;
|
||
margin-top: var(--spacing-lg);
|
||
margin-bottom: var(--spacing-md);
|
||
}
|
||
|
||
.btn-outline {
|
||
width: 100%;
|
||
display: block;
|
||
text-align: center;
|
||
padding: 10px 20px;
|
||
}
|
||
|
||
/* Responsive */
|
||
@media (max-width: 1024px) {
|
||
.cart-content {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.cart-summary {
|
||
position: static;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.cart-item-card {
|
||
grid-template-columns: 80px 1fr;
|
||
gap: var(--spacing-md);
|
||
}
|
||
|
||
.item-image img {
|
||
width: 80px;
|
||
height: 80px;
|
||
}
|
||
|
||
.item-quantity,
|
||
.item-total {
|
||
grid-column: 2;
|
||
}
|
||
|
||
.item-actions {
|
||
grid-column: 2;
|
||
justify-self: end;
|
||
}
|
||
}
|
||
</style>
|
||
</body>
|
||
</html> |