Files
orion/static/shop/cart.html

489 lines
15 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>