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> <script>
document.addEventListener('alpine:init', () => { document.addEventListener('alpine:init', () => {
Alpine.data('shoppingCart', () => { Alpine.data('shoppingCart', () => {
const baseData = shopLayoutData(); const baseData = storefrontLayoutData();
return { return {
...baseData, ...baseData,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -288,7 +288,7 @@
<script> <script>
function shopProfilePage() { function shopProfilePage() {
return { return {
...shopLayoutData(), ...storefrontLayoutData(),
// State // State
profile: null, 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) { formatDate(dateStr) {
if (!dateStr) return '-'; if (!dateStr) return '-';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -340,7 +340,7 @@
<script> <script>
function shopOrderDetailPage() { function shopOrderDetailPage() {
return { return {
...shopLayoutData(), ...storefrontLayoutData(),
// State // State
order: null, 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'; 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) { formatDateTime(dateStr) {
if (!dateStr) return '-'; if (!dateStr) return '-';

View File

@@ -134,7 +134,7 @@
<script> <script>
function shopOrdersPage() { function shopOrdersPage() {
return { return {
...shopLayoutData(), ...storefrontLayoutData(),
// State // State
orders: [], 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'; 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) { formatDate(dateStr) {
if (!dateStr) return '-'; if (!dateStr) return '-';

View File

@@ -1,7 +1,7 @@
{# app/templates/storefront/base.html #} {# app/templates/storefront/base.html #}
{# Base template for store shop frontend with theme support #} {# Base template for store shop frontend with theme support #}
<!DOCTYPE html> <!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> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <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> <script>
function componentName() { function componentName() {
return { return {
...shopLayoutData(), ...storefrontLayoutData(),
// Component-specific data/methods // Component-specific data/methods
} }
} }
@@ -487,7 +487,7 @@ Custom Tailwind CSS modal for logout confirmation instead of browser's native `c
```javascript ```javascript
function accountDashboard() { function accountDashboard() {
return { return {
...shopLayoutData(), ...storefrontLayoutData(),
showLogoutModal: false, // Modal state showLogoutModal: false, // Modal state
confirmLogout() { confirmLogout() {

View File

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

View File

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

View File

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