refactor: rename shopLayoutData to storefrontLayoutData
Some checks failed
Some checks failed
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:
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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: '',
|
||||||
|
|||||||
@@ -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: [],
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -318,7 +318,7 @@
|
|||||||
<script>
|
<script>
|
||||||
function addressesPage() {
|
function addressesPage() {
|
||||||
return {
|
return {
|
||||||
...shopLayoutData(),
|
...storefrontLayoutData(),
|
||||||
|
|
||||||
// State
|
// State
|
||||||
loading: true,
|
loading: true,
|
||||||
|
|||||||
@@ -163,7 +163,7 @@
|
|||||||
<script>
|
<script>
|
||||||
function accountDashboard() {
|
function accountDashboard() {
|
||||||
return {
|
return {
|
||||||
...shopLayoutData(),
|
...storefrontLayoutData(),
|
||||||
showLogoutModal: false,
|
showLogoutModal: false,
|
||||||
|
|
||||||
confirmLogout() {
|
confirmLogout() {
|
||||||
|
|||||||
@@ -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 '-';
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
function customerLoyaltyDashboard() {
|
function customerLoyaltyDashboard() {
|
||||||
return {
|
return {
|
||||||
...shopLayoutData(),
|
...storefrontLayoutData(),
|
||||||
|
|
||||||
// Data
|
// Data
|
||||||
card: null,
|
card: null,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
function customerLoyaltyEnroll() {
|
function customerLoyaltyEnroll() {
|
||||||
return {
|
return {
|
||||||
...shopLayoutData(),
|
...storefrontLayoutData(),
|
||||||
|
|
||||||
// Program info
|
// Program info
|
||||||
program: null,
|
program: null,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
function customerLoyaltyHistory() {
|
function customerLoyaltyHistory() {
|
||||||
return {
|
return {
|
||||||
...shopLayoutData(),
|
...storefrontLayoutData(),
|
||||||
|
|
||||||
// Data
|
// Data
|
||||||
card: null,
|
card: null,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -253,7 +253,7 @@
|
|||||||
<script>
|
<script>
|
||||||
function shopMessages() {
|
function shopMessages() {
|
||||||
return {
|
return {
|
||||||
...shopLayoutData(),
|
...storefrontLayoutData(),
|
||||||
loading: true,
|
loading: true,
|
||||||
conversations: [],
|
conversations: [],
|
||||||
selectedConversation: null,
|
selectedConversation: null,
|
||||||
|
|||||||
@@ -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 '-';
|
||||||
|
|||||||
@@ -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 '-';
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user