fix: refactor product detail page to extend base template and use correct paths

Issues resolved:
- Product detail page was standalone HTML without proper styling
- Referenced non-existent CSS files (/static/css/shared/base.css, /static/css/vendor/vendor.css)
- Used wrong image path (/static/images/ instead of /static/shop/img/)
- 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)
- Update image paths to /static/shop/img/placeholder.jpg
- Integrate with shopLayoutData() for cart and toast functionality
- Add proper breadcrumbs with landing page navigation
- Use Tailwind classes consistent with other shop pages
- Use theme CSS variables from base template

The product detail 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:
2025-11-23 06:23:25 +01:00
parent b7bf505a61
commit 1df4f12e92

View File

@@ -1,150 +1,143 @@
<!DOCTYPE html> {# app/templates/shop/product.html #}
<html lang="en"> {% extends "shop/base.html" %}
<head>
<meta charset="UTF-8"> {% block title %}{{ product.name if product else 'Product' }}{% endblock %}
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ product.name if product else 'Product' }} - {{ vendor.name }}</title> {# Alpine.js component #}
<link rel="stylesheet" href="/static/css/shared/base.css"> {% block alpine_data %}productDetail(){% endblock %}
<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> {% block content %}
</head> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"
<body>
<div x-data="productDetail()"
x-init="loadProduct()"
data-vendor-id="{{ vendor.id }}" data-vendor-id="{{ vendor.id }}"
data-product-id="{{ product_id }}" data-product-id="{{ product_id }}"
> >
<!-- Header --> {# Breadcrumbs #}
<header class="header"> <div class="breadcrumb mb-6">
<div class="header-left"> <a href="{{ base_url }}" class="hover:text-primary">Home</a>
<a href="{{ base_url }}shop/products" class="btn-back">← Back to Products</a> <span>/</span>
<h1>{{ vendor.name }}</h1> <a href="{{ base_url }}shop/products" class="hover:text-primary">Products</a>
</div> <span>/</span>
<div class="header-right"> <span class="text-gray-900 dark:text-gray-200 font-medium" x-text="product?.marketplace_product?.title || 'Product'">Product</span>
<a href="{{ base_url }}shop/cart" class="btn-primary">
🛒 Cart (<span x-text="cartCount"></span>)
</a>
</div>
</header>
<!-- Loading State -->
<div x-show="loading" class="container">
<div class="loading">
<div class="loading-spinner-lg"></div>
<p>Loading product...</p>
</div>
</div> </div>
<!-- Product Detail --> {# Loading State #}
<div x-show="!loading && product" class="container"> <div x-show="loading" class="flex justify-center items-center py-12">
<div class="product-detail-container"> <div class="spinner"></div>
<!-- Product Images --> </div>
{# Product Detail #}
<div x-show="!loading && product" class="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12">
{# Product Images #}
<div class="product-images"> <div class="product-images">
<div class="main-image"> <div class="main-image bg-white dark:bg-gray-800 rounded-lg overflow-hidden mb-4">
<img <img
:src="selectedImage || '/static/images/placeholder.png'" :src="selectedImage || '/static/shop/img/placeholder.jpg'"
:alt="product?.marketplace_product?.title" :alt="product?.marketplace_product?.title"
class="product-main-image" class="w-full h-auto object-contain"
style="max-height: 600px;"
> >
</div> </div>
<!-- Thumbnail Gallery --> {# Thumbnail Gallery #}
<div class="image-gallery" x-show="product?.marketplace_product?.images?.length > 1"> <div x-show="product?.marketplace_product?.images?.length > 1" class="grid grid-cols-4 gap-2">
<template x-for="(image, index) in product?.marketplace_product?.images" :key="index"> <template x-for="(image, index) in product?.marketplace_product?.images" :key="index">
<img <img
:src="image" :src="image"
:alt="`Product image ${index + 1}`" :alt="`Product image ${index + 1}`"
class="thumbnail" class="w-full aspect-square object-cover rounded-lg cursor-pointer border-2 transition-all"
:class="{ 'active': selectedImage === image }" :class="selectedImage === image ? 'border-primary' : 'border-transparent hover:border-gray-300'"
@click="selectedImage = image" @click="selectedImage = image"
> >
</template> </template>
</div> </div>
</div> </div>
<!-- Product Info --> {# Product Info #}
<div class="product-info-detail"> <div class="product-info-detail">
<h1 x-text="product?.marketplace_product?.title" class="product-title-detail"></h1> <h1 x-text="product?.marketplace_product?.title" class="text-3xl md:text-4xl font-bold text-gray-800 dark:text-gray-200 mb-4">
Product
</h1>
<!-- Brand & Category --> {# Brand & Category #}
<div class="product-meta"> <div class="flex flex-wrap gap-4 mb-6 pb-6 border-b border-gray-200 dark:border-gray-700">
<span x-show="product?.marketplace_product?.brand" class="meta-item"> <span x-show="product?.marketplace_product?.brand" class="text-sm text-gray-600 dark:text-gray-400">
<strong>Brand:</strong> <span x-text="product?.marketplace_product?.brand"></span> <strong>Brand:</strong> <span x-text="product?.marketplace_product?.brand"></span>
</span> </span>
<span x-show="product?.marketplace_product?.google_product_category" class="meta-item"> <span x-show="product?.marketplace_product?.google_product_category" class="text-sm text-gray-600 dark:text-gray-400">
<strong>Category:</strong> <span x-text="product?.marketplace_product?.google_product_category"></span> <strong>Category:</strong> <span x-text="product?.marketplace_product?.google_product_category"></span>
</span> </span>
<span class="meta-item"> <span class="text-sm text-gray-600 dark:text-gray-400">
<strong>SKU:</strong> <span x-text="product?.product_id || product?.marketplace_product?.mpn"></span> <strong>SKU:</strong> <span x-text="product?.product_id || product?.marketplace_product?.mpn"></span>
</span> </span>
</div> </div>
<!-- Price --> {# Price #}
<div class="product-pricing"> <div class="mb-6">
<div x-show="product?.sale_price && product?.sale_price < product?.price"> <div x-show="product?.sale_price && product?.sale_price < product?.price">
<span class="price-original"><span x-text="parseFloat(product?.price).toFixed(2)"></span></span> <span class="text-xl text-gray-500 line-through mr-3"><span x-text="parseFloat(product?.price).toFixed(2)"></span></span>
<span class="price-sale"><span x-text="parseFloat(product?.sale_price).toFixed(2)"></span></span> <span class="text-4xl font-bold text-red-600"><span x-text="parseFloat(product?.sale_price).toFixed(2)"></span></span>
<span class="price-badge">SALE</span> <span class="ml-2 inline-block bg-red-600 text-white px-3 py-1 rounded-full text-sm font-semibold">SALE</span>
</div> </div>
<div x-show="!product?.sale_price || product?.sale_price >= product?.price"> <div x-show="!product?.sale_price || product?.sale_price >= product?.price">
<span class="price-current"><span x-text="parseFloat(product?.price || 0).toFixed(2)"></span></span> <span class="text-4xl font-bold text-gray-800 dark:text-gray-200"><span x-text="parseFloat(product?.price || 0).toFixed(2)"></span></span>
</div> </div>
</div> </div>
<!-- Availability --> {# Availability #}
<div class="product-availability"> <div class="mb-6">
<span <span
x-show="product?.available_inventory > 0" x-show="product?.available_inventory > 0"
class="availability-badge in-stock" class="inline-block px-4 py-2 bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 rounded-lg font-semibold"
> >
✓ In Stock (<span x-text="product?.available_inventory"></span> available) ✓ In Stock (<span x-text="product?.available_inventory"></span> available)
</span> </span>
<span <span
x-show="!product?.available_inventory || product?.available_inventory <= 0" x-show="!product?.available_inventory || product?.available_inventory <= 0"
class="availability-badge out-of-stock" class="inline-block px-4 py-2 bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200 rounded-lg font-semibold"
> >
✗ Out of Stock ✗ Out of Stock
</span> </span>
</div> </div>
<!-- Description --> {# Description #}
<div class="product-description"> <div class="mb-6 p-6 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
<h3>Description</h3> <h3 class="text-xl font-semibold mb-3">Description</h3>
<p x-text="product?.marketplace_product?.description || 'No description available'"></p> <p class="text-gray-600 dark:text-gray-400 leading-relaxed" x-text="product?.marketplace_product?.description || 'No description available'"></p>
</div> </div>
<!-- Additional Details --> {# Additional Details #}
<div class="product-details" x-show="hasAdditionalDetails"> <div x-show="hasAdditionalDetails" class="mb-6 p-6 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
<h3>Product Details</h3> <h3 class="text-xl font-semibold mb-3">Product Details</h3>
<ul> <ul class="space-y-2">
<li x-show="product?.marketplace_product?.gtin"> <li x-show="product?.marketplace_product?.gtin" class="text-sm text-gray-600 dark:text-gray-400">
<strong>GTIN:</strong> <span x-text="product?.marketplace_product?.gtin"></span> <strong>GTIN:</strong> <span x-text="product?.marketplace_product?.gtin"></span>
</li> </li>
<li x-show="product?.condition"> <li x-show="product?.condition" class="text-sm text-gray-600 dark:text-gray-400">
<strong>Condition:</strong> <span x-text="product?.condition"></span> <strong>Condition:</strong> <span x-text="product?.condition"></span>
</li> </li>
<li x-show="product?.marketplace_product?.color"> <li x-show="product?.marketplace_product?.color" class="text-sm text-gray-600 dark:text-gray-400">
<strong>Color:</strong> <span x-text="product?.marketplace_product?.color"></span> <strong>Color:</strong> <span x-text="product?.marketplace_product?.color"></span>
</li> </li>
<li x-show="product?.marketplace_product?.size"> <li x-show="product?.marketplace_product?.size" class="text-sm text-gray-600 dark:text-gray-400">
<strong>Size:</strong> <span x-text="product?.marketplace_product?.size"></span> <strong>Size:</strong> <span x-text="product?.marketplace_product?.size"></span>
</li> </li>
<li x-show="product?.marketplace_product?.material"> <li x-show="product?.marketplace_product?.material" class="text-sm text-gray-600 dark:text-gray-400">
<strong>Material:</strong> <span x-text="product?.marketplace_product?.material"></span> <strong>Material:</strong> <span x-text="product?.marketplace_product?.material"></span>
</li> </li>
</ul> </ul>
</div> </div>
<!-- Add to Cart Section --> {# Add to Cart Section #}
<div class="add-to-cart-section"> <div class="p-6 bg-white dark:bg-gray-800 rounded-lg border-2 border-primary">
<!-- Quantity Selector --> {# Quantity Selector #}
<div class="quantity-selector"> <div class="mb-4">
<label>Quantity:</label> <label class="block font-semibold text-lg mb-2">Quantity:</label>
<div class="quantity-controls"> <div class="flex items-center gap-2">
<button <button
@click="decreaseQuantity()" @click="decreaseQuantity()"
:disabled="quantity <= (product?.min_quantity || 1)" :disabled="quantity <= (product?.min_quantity || 1)"
class="btn-quantity" class="w-10 h-10 flex items-center justify-center border-2 border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
> >
</button> </button>
@@ -153,60 +146,59 @@
x-model.number="quantity" x-model.number="quantity"
:min="product?.min_quantity || 1" :min="product?.min_quantity || 1"
:max="product?.max_quantity || product?.available_inventory" :max="product?.max_quantity || product?.available_inventory"
class="quantity-input" class="w-20 text-center px-3 py-2 border-2 border-gray-300 dark:border-gray-600 rounded-lg font-semibold dark:bg-gray-700 dark:text-white"
@change="validateQuantity()" @change="validateQuantity()"
> >
<button <button
@click="increaseQuantity()" @click="increaseQuantity()"
:disabled="quantity >= (product?.max_quantity || product?.available_inventory)" :disabled="quantity >= (product?.max_quantity || product?.available_inventory)"
class="btn-quantity" class="w-10 h-10 flex items-center justify-center border-2 border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
> >
+ +
</button> </button>
</div> </div>
</div> </div>
<!-- Add to Cart Button --> {# Add to Cart Button #}
<button <button
@click="addToCart()" @click="addToCart()"
:disabled="!canAddToCart || addingToCart" :disabled="!canAddToCart || addingToCart"
class="btn-add-to-cart" class="w-full px-6 py-4 bg-primary text-white rounded-lg font-semibold text-lg hover:bg-primary-dark transition-all disabled:opacity-50 disabled:cursor-not-allowed"
> >
<span x-show="!addingToCart"> <span x-show="!addingToCart">
🛒 Add to Cart 🛒 Add to Cart
</span> </span>
<span x-show="addingToCart"> <span x-show="addingToCart">
<span class="loading-spinner"></span> Adding... <span class="inline-block animate-spin"></span> Adding...
</span> </span>
</button> </button>
<!-- Total Price --> {# Total Price #}
<div class="total-price"> <div class="mt-4 p-4 bg-gray-50 dark:bg-gray-900 rounded-lg text-center">
<strong>Total:</strong><span x-text="totalPrice.toFixed(2)"></span> <strong class="text-xl">Total:</strong> <span class="text-2xl font-bold"><span x-text="totalPrice.toFixed(2)"></span></span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Related Products / You May Also Like --> {# Related Products #}
<div class="related-products" x-show="relatedProducts.length > 0"> <div x-show="relatedProducts.length > 0" class="mt-12 pt-12 border-t-2 border-gray-200 dark:border-gray-700">
<h2>You May Also Like</h2> <h2 class="text-3xl font-bold mb-6">You May Also Like</h2>
<div class="product-grid"> <div class="product-grid">
<template x-for="related in relatedProducts" :key="related.id"> <template x-for="related in relatedProducts" :key="related.id">
<div class="product-card"> <div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm hover:shadow-md transition-shadow overflow-hidden cursor-pointer">
<a :href="`{{ base_url }}shop/products/${related.id}`">
<img <img
:src="related.image_url || '/static/images/placeholder.png'" :src="related.marketplace_product?.image_link || '/static/shop/img/placeholder.jpg'"
:alt="related.name" :alt="related.marketplace_product?.title"
class="product-image" class="w-full h-48 object-cover"
@click="viewProduct(related.id)"
> >
<div class="product-info"> </a>
<h3 <div class="p-4">
class="product-title" <a :href="`{{ base_url }}shop/products/${related.id}`">
@click="viewProduct(related.id)" <h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-2 line-clamp-2" x-text="related.marketplace_product?.title"></h3>
x-text="related.name" </a>
></h3> <p class="text-2xl font-bold text-primary">
<p class="product-price">
<span x-text="parseFloat(related.price).toFixed(2)"></span> <span x-text="parseFloat(related.price).toFixed(2)"></span>
</p> </p>
</div> </div>
@@ -214,20 +206,16 @@
</template> </template>
</div> </div>
</div> </div>
</div>
<!-- Toast Notification --> </div>
<div {% endblock %}
x-show="toast.show"
x-transition {% block extra_scripts %}
:class="'toast toast-' + toast.type" <script>
x-text="toast.message" document.addEventListener('alpine:init', () => {
></div> Alpine.data('productDetail', () => ({
</div> ...shopLayoutData(),
<script>
function productDetail() {
return {
// Data // Data
product: null, product: null,
relatedProducts: [], relatedProducts: [],
@@ -235,17 +223,8 @@
addingToCart: false, addingToCart: false,
quantity: 1, quantity: 1,
selectedImage: null, selectedImage: null,
cartCount: 0,
vendorId: null, vendorId: null,
productId: null, productId: null,
sessionId: null,
// Toast notification
toast: {
show: false,
type: 'success',
message: ''
},
// Computed properties // Computed properties
get canAddToCart() { get canAddToCart() {
@@ -269,21 +248,11 @@
}, },
// Initialize // Initialize
init() { async init() {
console.log('[SHOP] Product detail page initializing...');
this.vendorId = this.$el.dataset.vendorId; this.vendorId = this.$el.dataset.vendorId;
this.productId = this.$el.dataset.productId; this.productId = this.$el.dataset.productId;
this.sessionId = this.getOrCreateSessionId(); await this.loadProduct();
this.loadCartCount();
},
// 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 product details // Load product details
@@ -291,15 +260,15 @@
this.loading = true; this.loading = true;
try { try {
const response = await fetch( console.log(`[SHOP] Loading product ${this.productId}...`);
`/api/v1/shop/products/${this.productId}` const response = await fetch(`/api/v1/shop/products/${this.productId}`);
);
if (!response.ok) { if (!response.ok) {
throw new Error('Product not found'); throw new Error('Product not found');
} }
this.product = await response.json(); this.product = await response.json();
console.log('[SHOP] Product loaded:', this.product);
// Set default image // Set default image
if (this.product?.marketplace_product?.image_link) { if (this.product?.marketplace_product?.image_link) {
@@ -309,27 +278,25 @@
// Set initial quantity // Set initial quantity
this.quantity = this.product?.min_quantity || 1; this.quantity = this.product?.min_quantity || 1;
// Load related products (optional) // Load related products
await this.loadRelatedProducts(); await this.loadRelatedProducts();
} catch (error) { } catch (error) {
console.error('Failed to load product:', error); console.error('[SHOP] Failed to load product:', error);
this.showToast('Failed to load product', 'error'); this.showToast('Failed to load product', 'error');
// Redirect back to products after error // Redirect back to products after error
setTimeout(() => { setTimeout(() => {
window.location.href = '/shop/products'; window.location.href = '{{ base_url }}shop/products';
}, 2000); }, 2000);
} finally { } finally {
this.loading = false; this.loading = false;
} }
}, },
// Load related products (same category or brand) // Load related products
async loadRelatedProducts() { async loadRelatedProducts() {
try { try {
const response = await fetch( const response = await fetch(`/api/v1/shop/products?limit=4`);
`/api/v1/shop/products?limit=4`
);
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
@@ -337,27 +304,11 @@
this.relatedProducts = data.products this.relatedProducts = data.products
.filter(p => p.id !== parseInt(this.productId)) .filter(p => p.id !== parseInt(this.productId))
.slice(0, 4); .slice(0, 4);
console.log('[SHOP] Loaded related products:', this.relatedProducts.length);
} }
} catch (error) { } catch (error) {
console.error('Failed to load related products:', error); console.error('[SHOP] Failed to load related products:', error);
}
},
// Load cart count
async loadCartCount() {
try {
const response = await fetch(
`/api/v1/shop/cart/${this.sessionId}`
);
if (response.ok) {
const data = await response.json();
this.cartCount = (data.items || []).reduce((sum, item) =>
sum + item.quantity, 0
);
}
} catch (error) {
console.error('Failed to load cart count:', error);
} }
}, },
@@ -394,6 +345,8 @@
this.addingToCart = true; this.addingToCart = true;
try { try {
console.log('[SHOP] Adding to cart:', { productId: this.productId, quantity: this.quantity });
const response = await fetch( const response = await fetch(
`/api/v1/shop/cart/${this.sessionId}/items`, `/api/v1/shop/cart/${this.sessionId}/items`,
{ {
@@ -422,350 +375,13 @@
throw new Error(error.detail || 'Failed to add to cart'); throw new Error(error.detail || 'Failed to add to cart');
} }
} catch (error) { } catch (error) {
console.error('Add to cart error:', error); console.error('[SHOP] Add to cart error:', error);
this.showToast(error.message || 'Failed to add to cart', 'error'); this.showToast(error.message || 'Failed to add to cart', 'error');
} finally { } finally {
this.addingToCart = false; this.addingToCart = false;
} }
},
// View other product
viewProduct(productId) {
window.location.href = `/shop/products/${productId}`;
},
// Show toast notification
showToast(message, type = 'success') {
this.toast = {
show: true,
type: type,
message: message
};
setTimeout(() => {
this.toast.show = false;
}, 3000);
} }
} }));
} });
</script> </script>
{% endblock %}
<style>
/* Product Detail Styles */
.product-detail-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-2xl);
margin-top: var(--spacing-xl);
margin-bottom: var(--spacing-2xl);
}
.product-images {
position: sticky;
top: var(--spacing-lg);
height: fit-content;
}
.main-image {
width: 100%;
aspect-ratio: 1;
border-radius: var(--radius-lg);
overflow: hidden;
background: var(--gray-50);
margin-bottom: var(--spacing-md);
}
.product-main-image {
width: 100%;
height: 100%;
object-fit: contain;
}
.image-gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
gap: var(--spacing-sm);
}
.thumbnail {
width: 100%;
aspect-ratio: 1;
object-fit: cover;
border-radius: var(--radius-md);
cursor: pointer;
border: 2px solid transparent;
transition: all var(--transition-base);
}
.thumbnail:hover {
border-color: var(--primary-color);
}
.thumbnail.active {
border-color: var(--primary-color);
box-shadow: var(--shadow-md);
}
.product-info-detail {
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
}
.product-title-detail {
font-size: var(--font-4xl);
font-weight: 700;
color: var(--text-primary);
margin: 0;
}
.product-meta {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-md);
padding-bottom: var(--spacing-md);
border-bottom: 1px solid var(--border-color);
}
.meta-item {
font-size: var(--font-sm);
color: var(--text-secondary);
}
.product-pricing {
padding: var(--spacing-lg);
background: var(--gray-50);
border-radius: var(--radius-lg);
}
.price-original {
font-size: var(--font-xl);
color: var(--text-muted);
text-decoration: line-through;
margin-right: var(--spacing-md);
}
.price-sale {
font-size: var(--font-4xl);
font-weight: 700;
color: var(--danger-color);
}
.price-current {
font-size: var(--font-4xl);
font-weight: 700;
color: var(--text-primary);
}
.price-badge {
display: inline-block;
background: var(--danger-color);
color: white;
padding: 4px 12px;
border-radius: var(--radius-full);
font-size: var(--font-sm);
font-weight: 600;
margin-left: var(--spacing-sm);
}
.product-availability {
padding: var(--spacing-md);
background: white;
border-radius: var(--radius-md);
}
.availability-badge {
display: inline-block;
padding: 8px 16px;
border-radius: var(--radius-md);
font-weight: 600;
font-size: var(--font-base);
}
.availability-badge.in-stock {
background: #d4edda;
color: #155724;
}
.availability-badge.out-of-stock {
background: #f8d7da;
color: #721c24;
}
.product-description {
padding: var(--spacing-lg);
background: white;
border-radius: var(--radius-lg);
border: 1px solid var(--border-color);
}
.product-description h3 {
margin-bottom: var(--spacing-md);
font-size: var(--font-xl);
}
.product-description p {
line-height: 1.6;
color: var(--text-secondary);
}
.product-details {
padding: var(--spacing-lg);
background: white;
border-radius: var(--radius-lg);
border: 1px solid var(--border-color);
}
.product-details h3 {
margin-bottom: var(--spacing-md);
font-size: var(--font-xl);
}
.product-details ul {
list-style: none;
padding: 0;
margin: 0;
}
.product-details li {
padding: var(--spacing-sm) 0;
border-bottom: 1px solid var(--border-color);
}
.product-details li:last-child {
border-bottom: none;
}
.add-to-cart-section {
padding: var(--spacing-xl);
background: white;
border-radius: var(--radius-lg);
border: 2px solid var(--primary-color);
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
}
.quantity-selector {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.quantity-selector label {
font-weight: 600;
font-size: var(--font-lg);
}
.quantity-controls {
display: flex;
gap: var(--spacing-sm);
align-items: center;
}
.btn-quantity {
width: 40px;
height: 40px;
border: 2px solid var(--border-color);
background: white;
border-radius: var(--radius-md);
cursor: pointer;
font-size: var(--font-xl);
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
transition: all var(--transition-base);
}
.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: 80px;
text-align: center;
padding: 10px;
border: 2px solid var(--border-color);
border-radius: var(--radius-md);
font-size: var(--font-lg);
font-weight: 600;
}
.btn-add-to-cart {
width: 100%;
padding: 16px 32px;
background: var(--primary-color);
color: white;
border: none;
border-radius: var(--radius-lg);
font-size: var(--font-xl);
font-weight: 600;
cursor: pointer;
transition: all var(--transition-base);
}
.btn-add-to-cart:hover:not(:disabled) {
background: var(--primary-dark);
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
.btn-add-to-cart:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.total-price {
padding: var(--spacing-md);
background: var(--gray-50);
border-radius: var(--radius-md);
text-align: center;
font-size: var(--font-2xl);
}
.related-products {
margin-top: var(--spacing-2xl);
padding-top: var(--spacing-2xl);
border-top: 2px solid var(--border-color);
}
.related-products h2 {
margin-bottom: var(--spacing-xl);
font-size: var(--font-3xl);
}
/* Responsive */
@media (max-width: 1024px) {
.product-detail-container {
grid-template-columns: 1fr;
}
.product-images {
position: static;
}
}
@media (max-width: 768px) {
.product-title-detail {
font-size: var(--font-2xl);
}
.price-current,
.price-sale {
font-size: var(--font-3xl);
}
.add-to-cart-section {
padding: var(--spacing-lg);
}
}
</style>
</body>
</html>