refactor: rename shopLayoutData to storefrontLayoutData
Some checks failed
CI / ruff (push) Successful in 11s
CI / pytest (push) Failing after 46m49s
CI / validate (push) Successful in 23s
CI / dependency-scanning (push) Successful in 30s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped

Align Alpine.js base component naming with storefront terminology.
Updated across all storefront JS, templates, and documentation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 19:06:45 +01:00
parent ec888f2e94
commit a6e6d9be8e
25 changed files with 130 additions and 130 deletions

View File

@@ -175,7 +175,7 @@
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('shoppingCart', () => {
const baseData = shopLayoutData();
const baseData = storefrontLayoutData();
return {
...baseData,

View File

@@ -146,7 +146,7 @@ window.CATEGORY_SLUG = '{{ category_slug | default("") }}';
document.addEventListener('alpine:init', () => {
Alpine.data('shopCategory', () => ({
...shopLayoutData(),
...storefrontLayoutData(),
// Data
categorySlug: window.CATEGORY_SLUG,

View File

@@ -218,7 +218,7 @@ window.STORE_ID = {{ store.id }};
document.addEventListener('alpine:init', () => {
Alpine.data('productDetail', () => {
const baseData = shopLayoutData();
const baseData = storefrontLayoutData();
return {
...baseData,

View File

@@ -145,7 +145,7 @@
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('shopProducts', () => ({
...shopLayoutData(),
...storefrontLayoutData(),
products: [],
loading: true,
filters: {
@@ -205,7 +205,7 @@ document.addEventListener('alpine:init', () => {
this.loadProducts();
},
// formatPrice is inherited from shopLayoutData() via spread operator
// formatPrice is inherited from storefrontLayoutData() via spread operator
async addToCart(product) {
console.log('[SHOP] Adding to cart:', product);

View File

@@ -170,7 +170,7 @@
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('shopSearch', () => ({
...shopLayoutData(),
...storefrontLayoutData(),
// Search state
searchInput: '',

View File

@@ -135,7 +135,7 @@
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('shopWishlist', () => ({
...shopLayoutData(),
...storefrontLayoutData(),
// Data
items: [],

View File

@@ -477,7 +477,7 @@
<script>
function checkoutPage() {
return {
...shopLayoutData(),
...storefrontLayoutData(),
// State
loading: true,
@@ -595,8 +595,8 @@ function checkoutPage() {
console.log('[CHECKOUT] Initializing...');
// Initialize session
if (typeof shopLayoutData === 'function') {
const baseData = shopLayoutData();
if (typeof storefrontLayoutData === 'function') {
const baseData = storefrontLayoutData();
if (baseData.init) {
baseData.init.call(this);
}

View File

@@ -7,7 +7,7 @@
{% block meta_description %}{{ page.meta_description or store.description or store.name }}{% endblock %}
{# Alpine.js component #}
{% block alpine_data %}shopLayoutData(){% endblock %}
{% block alpine_data %}storefrontLayoutData(){% endblock %}
{% block content %}
<div class="min-h-screen">

View File

@@ -7,7 +7,7 @@
{% block meta_description %}{{ page.meta_description or store.description or store.name }}{% endblock %}
{# Alpine.js component #}
{% block alpine_data %}shopLayoutData(){% endblock %}
{% block alpine_data %}storefrontLayoutData(){% endblock %}
{% block content %}
<div class="min-h-screen">

View File

@@ -16,7 +16,7 @@ const shopLog = {
* Shop Layout Data
* Base Alpine.js component for shop pages
*/
function shopLayoutData() {
function storefrontLayoutData() {
return {
// Theme state
dark: localStorage.getItem('shop-theme') === 'dark',
@@ -243,7 +243,7 @@ function shopLayoutData() {
}
// Make available globally
window.shopLayoutData = shopLayoutData;
window.storefrontLayoutData = storefrontLayoutData;
/**
* Language Selector Component

View File

@@ -318,7 +318,7 @@
<script>
function addressesPage() {
return {
...shopLayoutData(),
...storefrontLayoutData(),
// State
loading: true,

View File

@@ -163,7 +163,7 @@
<script>
function accountDashboard() {
return {
...shopLayoutData(),
...storefrontLayoutData(),
showLogoutModal: false,
confirmLogout() {

View File

@@ -288,7 +288,7 @@
<script>
function shopProfilePage() {
return {
...shopLayoutData(),
...storefrontLayoutData(),
// State
profile: null,
@@ -508,7 +508,7 @@ function shopProfilePage() {
}
},
// formatPrice is inherited from shopLayoutData() via spread operator
// formatPrice is inherited from storefrontLayoutData() via spread operator
formatDate(dateStr) {
if (!dateStr) return '-';

View File

@@ -3,7 +3,7 @@
function customerLoyaltyDashboard() {
return {
...shopLayoutData(),
...storefrontLayoutData(),
// Data
card: null,

View File

@@ -3,7 +3,7 @@
function customerLoyaltyEnroll() {
return {
...shopLayoutData(),
...storefrontLayoutData(),
// Program info
program: null,

View File

@@ -3,7 +3,7 @@
function customerLoyaltyHistory() {
return {
...shopLayoutData(),
...storefrontLayoutData(),
// Data
card: null,

View File

@@ -86,7 +86,7 @@
<script>
function customerLoyaltyEnrollSuccess() {
return {
...shopLayoutData(),
...storefrontLayoutData(),
walletUrls: { google_wallet_url: null, apple_wallet_url: null },
async init() {
try {

View File

@@ -253,7 +253,7 @@
<script>
function shopMessages() {
return {
...shopLayoutData(),
...storefrontLayoutData(),
loading: true,
conversations: [],
selectedConversation: null,

View File

@@ -340,7 +340,7 @@
<script>
function shopOrderDetailPage() {
return {
...shopLayoutData(),
...storefrontLayoutData(),
// State
order: null,
@@ -416,7 +416,7 @@ function shopOrderDetailPage() {
return this.statuses[status]?.class || 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200';
},
// formatPrice is inherited from shopLayoutData() via spread operator
// formatPrice is inherited from storefrontLayoutData() via spread operator
formatDateTime(dateStr) {
if (!dateStr) return '-';

View File

@@ -134,7 +134,7 @@
<script>
function shopOrdersPage() {
return {
...shopLayoutData(),
...storefrontLayoutData(),
// State
orders: [],
@@ -209,7 +209,7 @@ function shopOrdersPage() {
return this.statuses[status]?.class || 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200';
},
// formatPrice is inherited from shopLayoutData() via spread operator
// formatPrice is inherited from storefrontLayoutData() via spread operator
formatDate(dateStr) {
if (!dateStr) return '-';

View File

@@ -1,7 +1,7 @@
{# app/templates/storefront/base.html #}
{# Base template for store shop frontend with theme support #}
<!DOCTYPE html>
<html lang="en" x-data="{% block alpine_data %}shopLayoutData(){% endblock %}" x-bind:class="{ 'dark': dark }">
<html lang="en" x-data="{% block alpine_data %}storefrontLayoutData(){% endblock %}" x-bind:class="{ 'dark': dark }">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">

View File

@@ -412,7 +412,7 @@ All authentication pages follow the shop template pattern:
<script>
function componentName() {
return {
...shopLayoutData(),
...storefrontLayoutData(),
// Component-specific data/methods
}
}
@@ -487,7 +487,7 @@ Custom Tailwind CSS modal for logout confirmation instead of browser's native `c
```javascript
function accountDashboard() {
return {
...shopLayoutData(),
...storefrontLayoutData(),
showLogoutModal: false, // Modal state
confirmLogout() {

View File

@@ -291,7 +291,7 @@ Scripts MUST load in this exact order (see base.html):
7. Page-specific JS ← Optional page scripts
Why This Order Matters:
• shop-layout.js defines shopLayoutData() BEFORE Alpine initializes
• shop-layout.js defines storefrontLayoutData() BEFORE Alpine initializes
• Alpine.js defers to ensure DOM is ready
• Shared utilities available to all scripts
• Icons and logging available immediately
@@ -311,7 +311,7 @@ Alpine.js Component Architecture:
Provides shared functionality for all shop pages:
function shopLayoutData() {
function storefrontLayoutData() {
return {
// Theme state
dark: localStorage.getItem('shop-theme') === 'dark',
@@ -368,16 +368,16 @@ Provides shared functionality for all shop pages:
}
// Make globally available
window.shopLayoutData = shopLayoutData;
window.storefrontLayoutData = storefrontLayoutData;
⭐ PAGE-SPECIFIC COMPONENTS:
Each page extends shopLayoutData() for page-specific functionality:
Each page extends storefrontLayoutData() for page-specific functionality:
// Example: products.html
document.addEventListener('alpine:init', () => {
Alpine.data('shopProducts', () => ({
...shopLayoutData(), // Extend base component
...storefrontLayoutData(), // Extend base component
// Page-specific state
products: [],
@@ -387,7 +387,7 @@ Each page extends shopLayoutData() for page-specific functionality:
// Override init to add page-specific initialization
async init() {
shopLog.info('Products page initializing...');
this.loadCart(); // From shopLayoutData
this.loadCart(); // From storefrontLayoutData
await this.loadProducts(); // Page-specific
},
@@ -405,18 +405,18 @@ Template Usage:
──────────────────────────────────────────────────────────────────
{# In base.html - uses block to allow override #}
<html x-data="{% block alpine_data %}shopLayoutData(){% endblock %}"
<html x-data="{% block alpine_data %}storefrontLayoutData(){% endblock %}"
x-bind:class="{ 'dark': dark }">
{# In products.html - overrides to use page-specific component #}
{% block alpine_data %}shopProducts(){% endblock %}
{# In home.html - uses default base component #}
{# No block override needed, inherits shopLayoutData() #}
{# No block override needed, inherits storefrontLayoutData() #}
⭐ COMPONENT HIERARCHY:
shopLayoutData() ← Base component (shared state & methods)
storefrontLayoutData() ← Base component (shared state & methods)
shopProducts() ← Products page (extends base + products state)
shopCart() ← Cart page (extends base + cart state)
@@ -435,11 +435,11 @@ Tradeoffs:
⚠️ Can't easily split page into independent sub-components
Best Practices:
1. Always extend shopLayoutData() in page components
1. Always extend storefrontLayoutData() in page components
2. Override init() if you need page-specific initialization
3. Call parent methods when needed (this.loadCart(), this.showToast())
4. Keep page-specific state in the page component
5. Keep shared state in shopLayoutData()
5. Keep shared state in storefrontLayoutData()
Responsibilities:
✅ Load products from API

View File

@@ -79,7 +79,7 @@ app/
<!-- PAGE HEADER -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Breadcrumb -->
<nav class="flex mb-6 text-sm" aria-label="Breadcrumb">
<ol class="inline-flex items-center space-x-1 md:space-x-3">
@@ -98,14 +98,14 @@ app/
</li>
</ol>
</nav>
<!-- Page Title -->
<div class="flex items-center justify-between mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white"
style="font-family: var(--font-heading)">
[Page Name]
</h1>
<!-- Optional action button -->
<button
@click="someAction()"
@@ -116,7 +116,7 @@ app/
Action
</button>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- LOADING STATE -->
<!-- ═══════════════════════════════════════════════════════════════ -->
@@ -126,16 +126,16 @@ app/
</div>
<p class="mt-4 text-gray-600 dark:text-gray-400">Loading...</p>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- ERROR STATE -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div x-show="error && !loading"
<div x-show="error && !loading"
class="mb-6 p-6 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<div class="flex items-start">
<svg class="w-6 h-6 text-red-600 dark:text-red-400 mr-3 flex-shrink-0"
<svg class="w-6 h-6 text-red-600 dark:text-red-400 mr-3 flex-shrink-0"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<div>
@@ -144,17 +144,17 @@ app/
</div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- MAIN CONTENT -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div x-show="!loading">
<!-- Empty State -->
<div x-show="items.length === 0" class="text-center py-20">
<svg class="w-24 h-24 mx-auto text-gray-300 dark:text-gray-600 mb-4"
<svg class="w-24 h-24 mx-auto text-gray-300 dark:text-gray-600 mb-4"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1"
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"></path>
</svg>
<h3 class="text-xl font-semibold text-gray-700 dark:text-gray-300 mb-2">
@@ -164,35 +164,35 @@ app/
Try adjusting your filters or check back later.
</p>
</div>
<!-- Grid Layout (for products, items, etc.) -->
<div x-show="items.length > 0"
<div x-show="items.length > 0"
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
<template x-for="item in items" :key="item.id">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm hover:shadow-lg transition-shadow border border-gray-200 dark:border-gray-700">
<!-- Item Image -->
<div class="aspect-w-1 aspect-h-1 w-full overflow-hidden rounded-t-lg bg-gray-100 dark:bg-gray-700">
<img :src="item.image || '/static/shop/img/placeholder-product.png'"
<img :src="item.image || '/static/shop/img/placeholder-product.png'"
:alt="item.name"
class="w-full h-full object-cover object-center hover:scale-105 transition-transform"
loading="lazy">
</div>
<!-- Item Info -->
<div class="p-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2"
x-text="item.name"></h3>
<p class="text-gray-600 dark:text-gray-400 text-sm mb-4 line-clamp-2"
x-text="item.description"></p>
<!-- Price -->
<div class="flex items-center justify-between">
<span class="text-2xl font-bold"
:style="{ color: 'var(--color-primary)' }"
x-text="formatPrice(item.price)"></span>
<button @click="addToCart(item)"
class="px-4 py-2 text-white rounded-lg hover:opacity-90 transition-opacity"
:style="{ 'background-color': 'var(--color-primary)' }">
@@ -203,13 +203,13 @@ app/
</div>
</template>
</div>
<!-- ═════════════════════════════════════════════════════════════ -->
<!-- PAGINATION -->
<!-- ═════════════════════════════════════════════════════════════ -->
<div x-show="pagination.totalPages > 1"
<div x-show="pagination.totalPages > 1"
class="flex justify-center items-center space-x-2 mt-12">
<!-- Previous Button -->
<button
@click="goToPage(pagination.currentPage - 1)"
@@ -218,20 +218,20 @@ app/
>
Previous
</button>
<!-- Page Numbers -->
<template x-for="page in paginationRange" :key="page">
<button
@click="goToPage(page)"
:class="page === pagination.currentPage
? 'text-white'
:class="page === pagination.currentPage
? 'text-white'
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700'"
:style="page === pagination.currentPage ? { 'background-color': 'var(--color-primary)' } : {}"
class="px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600"
x-text="page"
></button>
</template>
<!-- Next Button -->
<button
@click="goToPage(pagination.currentPage + 1)"
@@ -282,7 +282,7 @@ function shop[PageName]() {
loading: false,
error: '',
items: [],
// Pagination
pagination: {
currentPage: 1,
@@ -290,21 +290,21 @@ function shop[PageName]() {
perPage: 12,
total: 0
},
// Filters
filters: {
search: '',
category: '',
sortBy: 'created_at:desc'
},
// Store info (from template)
storeCode: '{{ store.code }}',
// ─────────────────────────────────────────────────────
// LIFECYCLE
// ─────────────────────────────────────────────────────
/**
* Initialize component
*/
@@ -313,44 +313,44 @@ function shop[PageName]() {
await this.loadData();
pageLog.info('[PageName] initialized');
},
// ─────────────────────────────────────────────────────
// DATA LOADING
// ─────────────────────────────────────────────────────
/**
* Load main data from API
*/
async loadData() {
this.loading = true;
this.error = '';
try {
const params = new URLSearchParams({
page: this.pagination.currentPage,
per_page: this.pagination.perPage,
...this.filters
});
const response = await fetch(
`/api/v1/shop/${this.storeCode}/items?${params}`
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
// Update state
this.items = data.items || [];
this.pagination.total = data.total || 0;
this.pagination.totalPages = Math.ceil(
this.pagination.total / this.pagination.perPage
);
pageLog.info('Data loaded:', this.items.length, 'items');
} catch (error) {
pageLog.error('Failed to load data:', error);
this.error = error.message || 'Failed to load data';
@@ -358,7 +358,7 @@ function shop[PageName]() {
this.loading = false;
}
},
/**
* Refresh data
*/
@@ -367,11 +367,11 @@ function shop[PageName]() {
this.error = '';
await this.loadData();
},
// ─────────────────────────────────────────────────────
// FILTERS & SEARCH
// ─────────────────────────────────────────────────────
/**
* Apply filters and reload data
*/
@@ -380,7 +380,7 @@ function shop[PageName]() {
this.pagination.currentPage = 1; // Reset to first page
await this.loadData();
},
/**
* Reset filters to default
*/
@@ -392,24 +392,24 @@ function shop[PageName]() {
};
await this.applyFilters();
},
// ─────────────────────────────────────────────────────
// PAGINATION
// ─────────────────────────────────────────────────────
/**
* Navigate to specific page
*/
async goToPage(page) {
if (page < 1 || page > this.pagination.totalPages) return;
this.pagination.currentPage = page;
await this.loadData();
// Scroll to top
window.scrollTo({ top: 0, behavior: 'smooth' });
},
/**
* Get pagination range for display
*/
@@ -417,36 +417,36 @@ function shop[PageName]() {
const current = this.pagination.currentPage;
const total = this.pagination.totalPages;
const range = [];
// Show max 7 page numbers
let start = Math.max(1, current - 3);
let end = Math.min(total, start + 6);
// Adjust start if we're near the end
if (end - start < 6) {
start = Math.max(1, end - 6);
}
for (let i = start; i <= end; i++) {
range.push(i);
}
return range;
},
// ─────────────────────────────────────────────────────
// CART INTEGRATION
// ─────────────────────────────────────────────────────
/**
* Add item to cart
*/
addToCart(item, quantity = 1) {
pageLog.info('Adding to cart:', item.name);
// Get cart from shop layout
const shopLayout = Alpine.store('shop') || window.shopLayoutData();
const shopLayout = Alpine.store('shop') || window.storefrontLayoutData();
if (shopLayout && typeof shopLayout.addToCart === 'function') {
shopLayout.addToCart(item, quantity);
this.showToast(`${item.name} added to cart`, 'success');
@@ -454,21 +454,21 @@ function shop[PageName]() {
pageLog.error('Shop layout not available');
}
},
// ─────────────────────────────────────────────────────
// UI HELPERS
// ─────────────────────────────────────────────────────
/**
* Show toast notification
*/
showToast(message, type = 'info') {
const shopLayout = Alpine.store('shop') || window.shopLayoutData();
const shopLayout = Alpine.store('shop') || window.storefrontLayoutData();
if (shopLayout && typeof shopLayout.showToast === 'function') {
shopLayout.showToast(message, type);
}
},
/**
* Format price as currency
*/
@@ -478,7 +478,7 @@ function shop[PageName]() {
currency: 'USD'
}).format(price);
},
/**
* Format date
*/
@@ -491,7 +491,7 @@ function shop[PageName]() {
day: 'numeric'
});
},
/**
* Truncate text
*/
@@ -534,7 +534,7 @@ async def [page_name]_page(
# Store and theme come from middleware
store = request.state.store
theme = request.state.theme
return templates.TemplateResponse(
"shop/[page-name].html",
{
@@ -600,13 +600,13 @@ async loadProduct(id) {
const product = await fetch(
`/api/v1/shop/${this.storeCode}/products/${id}`
).then(r => r.json());
this.product = product;
this.selectedImage = product.images[0];
}
addToCartWithQuantity() {
const shopLayout = window.shopLayoutData();
const shopLayout = window.storefrontLayoutData();
shopLayout.addToCart(this.product, this.quantity);
}
```
@@ -619,28 +619,28 @@ addToCartWithQuantity() {
<img :src="selectedImage" class="w-full rounded-lg">
<div class="grid grid-cols-4 gap-2 mt-4">
<template x-for="img in product.images">
<img @click="selectedImage = img"
:src="img"
<img @click="selectedImage = img"
:src="img"
class="cursor-pointer rounded border-2"
:class="selectedImage === img ? 'border-primary' : 'border-gray-200'">
</template>
</div>
</div>
<!-- Product Info -->
<div>
<h1 class="text-3xl font-bold mb-4" x-text="product.name"></h1>
<p class="text-2xl font-bold mb-6"
<p class="text-2xl font-bold mb-6"
:style="{ color: 'var(--color-primary)' }"
x-text="formatPrice(product.price)"></p>
<p class="text-gray-600 mb-8" x-text="product.description"></p>
<!-- Quantity -->
<div class="flex items-center space-x-4 mb-6">
<label>Quantity:</label>
<input type="number" x-model="quantity" min="1" class="w-20 px-3 py-2 border rounded">
</div>
<!-- Add to Cart -->
<button @click="addToCartWithQuantity()"
class="w-full py-3 text-white rounded-lg text-lg font-semibold"
@@ -663,19 +663,19 @@ async init() {
}
loadCart() {
const shopLayout = window.shopLayoutData();
const shopLayout = window.storefrontLayoutData();
this.cart = shopLayout.cart;
this.calculateTotals();
}
updateQuantity(productId, quantity) {
const shopLayout = window.shopLayoutData();
const shopLayout = window.storefrontLayoutData();
shopLayout.updateCartItem(productId, quantity);
this.loadCart();
}
removeItem(productId) {
const shopLayout = window.shopLayoutData();
const shopLayout = window.storefrontLayoutData();
shopLayout.removeFromCart(productId);
this.loadCart();
}
@@ -754,7 +754,7 @@ Always use the shop layout's cart methods:
```javascript
// ✅ GOOD: Uses shop layout
const shopLayout = window.shopLayoutData();
const shopLayout = window.storefrontLayoutData();
shopLayout.addToCart(product, quantity);
// ❌ BAD: Direct localStorage manipulation
@@ -793,7 +793,7 @@ try {
Use lazy loading and responsive images:
```html
<img :src="product.image"
<img :src="product.image"
:alt="product.name"
loading="lazy"
class="w-full h-full object-cover">
@@ -814,7 +814,7 @@ Support both light and dark modes:
Add proper ARIA labels and keyboard navigation:
```html
<button @click="addToCart(product)"
<button @click="addToCart(product)"
aria-label="Add to cart"
role="button">
Add to Cart
@@ -889,10 +889,10 @@ Create reusable components in `templates/shop/partials/`:
<img :src="product.image" :alt="product.name" class="w-full h-64 object-cover rounded-t-lg">
<div class="p-4">
<h3 class="font-semibold text-lg" x-text="product.name"></h3>
<p class="text-2xl font-bold"
<p class="text-2xl font-bold"
:style="{ color: 'var(--color-primary)' }"
x-text="formatPrice(product.price)"></p>
<button @click="addToCart(product)"
<button @click="addToCart(product)"
class="w-full mt-4 py-2 text-white rounded"
:style="{ 'background-color': 'var(--color-primary)' }">
Add to Cart
@@ -905,7 +905,7 @@ Create reusable components in `templates/shop/partials/`:
```html
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 class="font-semibold mb-4">Filters</h3>
<!-- Category -->
<div class="mb-6">
<label class="block mb-2 font-medium">Category</label>
@@ -917,11 +917,11 @@ Create reusable components in `templates/shop/partials/`:
</template>
</select>
</div>
<!-- Price Range -->
<div class="mb-6">
<label class="block mb-2 font-medium">Price Range</label>
<input type="range" x-model="filters.maxPrice"
<input type="range" x-model="filters.maxPrice"
min="0" max="1000" step="10"
class="w-full">
<div class="flex justify-between text-sm">
@@ -929,9 +929,9 @@ Create reusable components in `templates/shop/partials/`:
<span x-text="'$' + filters.maxPrice"></span>
</div>
</div>
<!-- Apply Button -->
<button @click="applyFilters()"
<button @click="applyFilters()"
class="w-full py-2 text-white rounded"
:style="{ 'background-color': 'var(--color-primary)' }">
Apply Filters

View File

@@ -56,7 +56,7 @@ Session ID not properly initialized in Alpine.js components.
**How it Works:**
- Cart uses session ID stored in localStorage as `cart_session_id`
- `shopLayoutData()` base component initializes `sessionId` in its `init()` method
- `storefrontLayoutData()` base component initializes `sessionId` in its `init()` method
- Child components (product, cart) must call parent `init()` to get `sessionId`
- If parent init isn't called, `sessionId` is `undefined`
@@ -295,7 +295,7 @@ Child component doesn't call parent `init()` method.
**Solution:**
```javascript
// Store reference to parent
const baseData = shopLayoutData();
const baseData = storefrontLayoutData();
return {
...baseData,