major refactoring adding vendor and customer features
This commit is contained in:
489
static/shop/cart.html
Normal file
489
static/shop/cart.html
Normal file
@@ -0,0 +1,489 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user