fix: refactor cart page to extend base template and fix styling
Issue:
- Cart page was standalone HTML without proper styling
- Referenced non-existent CSS files (/static/css/shared/base.css, /static/css/vendor/vendor.css)
- Missing header, footer, and navigation
- Not integrated with shop layout system
Changes:
- Extend shop/base.html for consistent styling and layout
- Remove hardcoded CSS references (now inherited from base)
- Add proper breadcrumbs with landing page navigation
- Integrate with shopLayoutData() for cart and toast functionality
- Use Tailwind classes consistent with other shop pages
- Add @error handler for broken product images
- Update all URLs to use {{ base_url }} for multi-tenant support
- Modernize layout with responsive grid system
The cart page now has proper styling, navigation, and integrates
seamlessly with the rest of the shop frontend.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,489 +1,308 @@
|
||||
<!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="{{ base_url }}shop/" class="btn-secondary">← Continue Shopping</a>
|
||||
</div>
|
||||
</header>
|
||||
{# app/templates/shop/cart.html #}
|
||||
{% extends "shop/base.html" %}
|
||||
|
||||
<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>
|
||||
{% block title %}Shopping Cart{% endblock %}
|
||||
|
||||
<!-- 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="{{ base_url }}shop/products" class="btn-primary">Browse Products</a>
|
||||
</div>
|
||||
{# Alpine.js component #}
|
||||
{% block alpine_data %}shoppingCart(){% endblock %}
|
||||
|
||||
<!-- 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/shop/img/placeholder.svg'"
|
||||
:alt="item.name">
|
||||
{% block content %}
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
|
||||
{# Breadcrumbs #}
|
||||
<div class="breadcrumb mb-6">
|
||||
<a href="{{ base_url }}" class="hover:text-primary">Home</a>
|
||||
<span>/</span>
|
||||
<a href="{{ base_url }}shop/products" class="hover:text-primary">Products</a>
|
||||
<span>/</span>
|
||||
<span class="text-gray-900 dark:text-gray-200 font-medium">Cart</span>
|
||||
</div>
|
||||
|
||||
{# Page Header #}
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl md:text-4xl font-bold text-gray-800 dark:text-gray-200 mb-2">
|
||||
Shopping Cart
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{# Loading State #}
|
||||
<div x-show="loading && items.length === 0" class="flex justify-center items-center py-12">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
|
||||
{# Empty Cart #}
|
||||
<div x-show="!loading && items.length === 0" class="text-center py-12 bg-white dark:bg-gray-800 rounded-lg border-2 border-dashed border-gray-300 dark:border-gray-600">
|
||||
<div class="text-6xl mb-4">🛒</div>
|
||||
<h3 class="text-2xl font-semibold text-gray-700 dark:text-gray-200 mb-2">
|
||||
Your cart is empty
|
||||
</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-6">
|
||||
Add some products to get started!
|
||||
</p>
|
||||
<a href="{{ base_url }}shop/products" class="inline-block px-6 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors">
|
||||
Browse Products
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{# Cart Items #}
|
||||
<div x-show="items.length > 0" class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
|
||||
{# Cart Items List #}
|
||||
<div class="lg:col-span-2 space-y-4">
|
||||
<template x-for="item in items" :key="item.product_id">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-6">
|
||||
<div class="flex gap-6">
|
||||
{# Item Image #}
|
||||
<div class="flex-shrink-0">
|
||||
<img
|
||||
:src="item.image_url || '/static/shop/img/placeholder.svg'"
|
||||
@error="$el.src = '/static/shop/img/placeholder.svg'"
|
||||
:alt="item.name"
|
||||
class="w-24 h-24 object-cover rounded-lg"
|
||||
>
|
||||
</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">
|
||||
{# Item Details #}
|
||||
<div class="flex-1">
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-1" x-text="item.name"></h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mb-2" x-text="'SKU: ' + item.sku"></p>
|
||||
<p class="text-xl font-bold text-primary mb-4">
|
||||
€<span x-text="parseFloat(item.price).toFixed(2)"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="item-quantity">
|
||||
<label>Quantity:</label>
|
||||
<div class="quantity-controls">
|
||||
<button
|
||||
{# Quantity Controls #}
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
@click="updateQuantity(item.product_id, item.quantity - 1)"
|
||||
:disabled="item.quantity <= 1 || updating"
|
||||
class="btn-quantity"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<input
|
||||
class="w-8 h-8 flex items-center justify-center border-2 border-gray-300 dark:border-gray-600 rounded hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
−
|
||||
</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
|
||||
class="w-16 text-center px-2 py-1 border-2 border-gray-300 dark:border-gray-600 rounded font-semibold dark:bg-gray-700 dark:text-white"
|
||||
>
|
||||
<button
|
||||
@click="updateQuantity(item.product_id, item.quantity + 1)"
|
||||
:disabled="updating"
|
||||
class="btn-quantity"
|
||||
class="w-8 h-8 flex items-center justify-center border-2 border-gray-300 dark:border-gray-600 rounded hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 text-right">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Subtotal</p>
|
||||
<p class="text-xl font-bold text-gray-800 dark:text-gray-200">
|
||||
€<span x-text="(parseFloat(item.price) * item.quantity).toFixed(2)"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="removeItem(item.product_id)"
|
||||
:disabled="updating"
|
||||
class="w-10 h-10 flex items-center justify-center border border-gray-300 dark:border-gray-600 rounded hover:bg-red-600 hover:border-red-600 hover:text-white transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title="Remove from cart"
|
||||
>
|
||||
+
|
||||
🗑️
|
||||
</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="{{ base_url }}shop/products" class="btn-outline">
|
||||
Continue Shopping
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
{# Cart Summary #}
|
||||
<div class="lg:col-span-1">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-6 sticky top-4">
|
||||
<h3 class="text-xl font-semibold mb-6 pb-4 border-b-2 border-gray-200 dark:border-gray-700">
|
||||
Order Summary
|
||||
</h3>
|
||||
|
||||
<div class="space-y-3 mb-6">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Subtotal (<span x-text="totalItems"></span> items):</span>
|
||||
<span class="font-semibold">€<span x-text="subtotal.toFixed(2)"></span></span>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600 dark:text-gray-400">Shipping:</span>
|
||||
<span class="font-semibold" x-text="shipping > 0 ? '€' + shipping.toFixed(2) : 'FREE'"></span>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between text-lg font-bold pt-3 border-t-2 border-gray-200 dark:border-gray-700">
|
||||
<span>Total:</span>
|
||||
<span class="text-primary text-2xl">€<span x-text="total.toFixed(2)"></span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="proceedToCheckout()"
|
||||
:disabled="updating || items.length === 0"
|
||||
class="w-full px-6 py-3 bg-primary text-white rounded-lg font-semibold hover:bg-primary-dark transition-colors disabled:opacity-50 disabled:cursor-not-allowed mb-3"
|
||||
>
|
||||
Proceed to Checkout
|
||||
</button>
|
||||
|
||||
<a href="{{ base_url }}shop/products" class="block w-full px-6 py-3 text-center border-2 border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
Continue Shopping
|
||||
</a>
|
||||
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-4 text-center">
|
||||
Free shipping on orders over €50
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
function shoppingCart() {
|
||||
return {
|
||||
items: [],
|
||||
loading: false,
|
||||
updating: false,
|
||||
vendorId: null,
|
||||
sessionId: null,
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('shoppingCart', () => ({
|
||||
...shopLayoutData(),
|
||||
|
||||
// Computed properties
|
||||
get totalItems() {
|
||||
return this.items.reduce((sum, item) => sum + item.quantity, 0);
|
||||
},
|
||||
items: [],
|
||||
loading: false,
|
||||
updating: false,
|
||||
|
||||
get subtotal() {
|
||||
return this.items.reduce((sum, item) =>
|
||||
sum + (parseFloat(item.price) * item.quantity), 0
|
||||
// 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
|
||||
async init() {
|
||||
console.log('[SHOP] Cart page initializing...');
|
||||
await this.loadCart();
|
||||
},
|
||||
|
||||
// Load cart from API
|
||||
async loadCart() {
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
console.log(`[SHOP] Loading cart for session ${this.sessionId}...`);
|
||||
const response = await fetch(`/api/v1/shop/cart/${this.sessionId}`);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
this.items = data.items || [];
|
||||
this.cartCount = this.totalItems;
|
||||
console.log('[SHOP] Cart loaded:', this.items.length, 'items');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[SHOP] Failed to load cart:', error);
|
||||
this.showToast('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 {
|
||||
console.log('[SHOP] Updating quantity:', productId, newQuantity);
|
||||
const response = await fetch(
|
||||
`/api/v1/shop/cart/${this.sessionId}/items/${productId}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ quantity: newQuantity })
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
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/shop/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/shop/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/shop/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';
|
||||
if (response.ok) {
|
||||
await this.loadCart();
|
||||
this.showToast('Quantity updated', 'success');
|
||||
} else {
|
||||
window.location.href = '/shop/checkout';
|
||||
throw new Error('Failed to update quantity');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[SHOP] Update quantity error:', error);
|
||||
this.showToast('Failed to update quantity', 'error');
|
||||
} finally {
|
||||
this.updating = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Remove item from cart
|
||||
async removeItem(productId) {
|
||||
if (!confirm('Remove this item from your cart?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updating = true;
|
||||
|
||||
try {
|
||||
console.log('[SHOP] Removing item:', productId);
|
||||
const response = await fetch(
|
||||
`/api/v1/shop/cart/${this.sessionId}/items/${productId}`,
|
||||
{
|
||||
method: 'DELETE'
|
||||
}
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
await this.loadCart();
|
||||
this.showToast('Item removed from cart', 'success');
|
||||
} else {
|
||||
throw new Error('Failed to remove item');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[SHOP] Remove item error:', error);
|
||||
this.showToast('Failed to remove item', 'error');
|
||||
} 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 = '{{ base_url }}shop/account/login?return={{ base_url }}shop/checkout';
|
||||
} else {
|
||||
window.location.href = '{{ base_url }}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>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user