# Shop Frontend - Alpine.js/Jinja2 Page Template Guide

## 📋 Overview

This guide provides complete templates for creating new customer-facing shop pages using the established Alpine.js + Jinja2 + Multi-Theme architecture. Follow these patterns to ensure consistency across all vendor shops while maintaining unique branding.

---

## 🎯 Quick Reference

### File Structure for New Page
```
app/
├── templates/shop/
│   └── [page-name].html          # Jinja2 template
├── static/shop/js/
│   └── [page-name].js            # Alpine.js component
└── api/v1/shop/
    └── pages.py                  # Route registration
```

### Checklist for New Page
- [ ] Create Jinja2 template extending shop/base.html
- [ ] Create Alpine.js JavaScript component
- [ ] Register route in pages.py
- [ ] Test with multiple vendor themes
- [ ] Test responsive design (mobile/tablet/desktop)
- [ ] Test dark mode
- [ ] Test cart integration (if applicable)
- [ ] Verify theme CSS variables work
- [ ] Check image optimization

---

## 📄 Template Structure

### 1. Jinja2 Template

**File:** `app/templates/shop/[page-name].html`

```jinja2
{# app/templates/shop/[page-name].html #}
{% extends "shop/base.html" %}

{# Page title for browser tab - includes vendor name #}
{% block title %}[Page Name] - {{ vendor.name }}{% endblock %}

{# Meta description for SEO #}
{% block meta_description %}[Page description for SEO]{% endblock %}

{# Alpine.js component name #}
{% block alpine_data %}shop[PageName](){% endblock %}

{# Page content #}
{% block content %}
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- 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">
            <li class="inline-flex items-center">
                <a href="/" class="text-gray-700 hover:text-primary dark:text-gray-400">
                    Home
                </a>
            </li>
            <li>
                <div class="flex items-center">
                    <svg class="w-4 h-4 text-gray-400 mx-1" fill="currentColor" viewBox="0 0 20 20">
                        <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path>
                    </svg>
                    <span class="text-gray-500 dark:text-gray-400">[Page Name]</span>
                </div>
            </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()"
            class="px-6 py-2 text-white rounded-lg transition-colors"
            style="background-color: var(--color-primary)"
            :style="{ 'background-color': 'var(--color-primary)' }"
        >
            Action
        </button>
    </div>
    
    <!-- ═══════════════════════════════════════════════════════════════ -->
    <!-- LOADING STATE                                                    -->
    <!-- ═══════════════════════════════════════════════════════════════ -->
    <div x-show="loading" class="text-center py-20">
        <div class="inline-block animate-spin rounded-full h-12 w-12 border-4 border-gray-200"
             :style="{ 'border-top-color': 'var(--color-primary)' }">
        </div>
        <p class="mt-4 text-gray-600 dark:text-gray-400">Loading...</p>
    </div>
    
    <!-- ═══════════════════════════════════════════════════════════════ -->
    <!-- ERROR STATE                                                      -->
    <!-- ═══════════════════════════════════════════════════════════════ -->
    <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" 
                 fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <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>
                <h3 class="text-red-800 dark:text-red-200 font-semibold">Error</h3>
                <p class="text-red-700 dark:text-red-300 text-sm mt-1" x-text="error"></p>
            </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" 
                 fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <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">
                No items found
            </h3>
            <p class="text-gray-500 dark:text-gray-400">
                Try adjusting your filters or check back later.
            </p>
        </div>
        
        <!-- Grid Layout (for products, items, etc.) -->
        <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'" 
                             :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)' }">
                                Add
                            </button>
                        </div>
                    </div>
                </div>
            </template>
        </div>
        
        <!-- ═════════════════════════════════════════════════════════════ -->
        <!-- PAGINATION                                                      -->
        <!-- ═════════════════════════════════════════════════════════════ -->
        <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)"
                :disabled="pagination.currentPage === 1"
                class="px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
            >
                Previous
            </button>
            
            <!-- Page Numbers -->
            <template x-for="page in paginationRange" :key="page">
                <button
                    @click="goToPage(page)"
                    :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)"
                :disabled="pagination.currentPage === pagination.totalPages"
                class="px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
            >
                Next
            </button>
        </div>
    </div>
</div>
{% endblock %}

{# Page-specific JavaScript #}
{% block extra_scripts %}
<script src="{{ url_for('static', path='shop/js/[page-name].js') }}"></script>
{% endblock %}
```

---

### 2. Alpine.js Component

**File:** `app/static/shop/js/[page-name].js`

```javascript
// static/shop/js/[page-name].js
/**
 * [Page Name] Component
 * Handles [describe functionality]
 */

const pageLog = {
    info: (...args) => console.info('🛍️ [PAGE]', ...args),
    warn: (...args) => console.warn('⚠️ [PAGE]', ...args),
    error: (...args) => console.error('❌ [PAGE]', ...args),
    debug: (...args) => console.log('🔍 [PAGE]', ...args)
};

/**
 * Main Alpine.js component for [page name]
 */
function shop[PageName]() {
    return {
        // ─────────────────────────────────────────────────────
        // STATE
        // ─────────────────────────────────────────────────────
        loading: false,
        error: '',
        items: [],
        
        // Pagination
        pagination: {
            currentPage: 1,
            totalPages: 1,
            perPage: 12,
            total: 0
        },
        
        // Filters
        filters: {
            search: '',
            category: '',
            sortBy: 'created_at:desc'
        },
        
        // Vendor info (from template)
        vendorCode: '{{ vendor.code }}',
        
        // ─────────────────────────────────────────────────────
        // LIFECYCLE
        // ─────────────────────────────────────────────────────
        
        /**
         * Initialize component
         */
        async init() {
            pageLog.info('[PageName] initializing...');
            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.vendorCode}/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';
            } finally {
                this.loading = false;
            }
        },
        
        /**
         * Refresh data
         */
        async refresh() {
            pageLog.info('Refreshing data...');
            this.error = '';
            await this.loadData();
        },
        
        // ─────────────────────────────────────────────────────
        // FILTERS & SEARCH
        // ─────────────────────────────────────────────────────
        
        /**
         * Apply filters and reload data
         */
        async applyFilters() {
            pageLog.debug('Applying filters:', this.filters);
            this.pagination.currentPage = 1; // Reset to first page
            await this.loadData();
        },
        
        /**
         * Reset filters to default
         */
        async resetFilters() {
            this.filters = {
                search: '',
                category: '',
                sortBy: 'created_at:desc'
            };
            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
         */
        get paginationRange() {
            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();
            
            if (shopLayout && typeof shopLayout.addToCart === 'function') {
                shopLayout.addToCart(item, quantity);
                this.showToast(`${item.name} added to cart`, 'success');
            } else {
                pageLog.error('Shop layout not available');
            }
        },
        
        // ─────────────────────────────────────────────────────
        // UI HELPERS
        // ─────────────────────────────────────────────────────
        
        /**
         * Show toast notification
         */
        showToast(message, type = 'info') {
            const shopLayout = Alpine.store('shop') || window.shopLayoutData();
            if (shopLayout && typeof shopLayout.showToast === 'function') {
                shopLayout.showToast(message, type);
            }
        },
        
        /**
         * Format price as currency
         */
        formatPrice(price) {
            return new Intl.NumberFormat('en-US', {
                style: 'currency',
                currency: 'USD'
            }).format(price);
        },
        
        /**
         * Format date
         */
        formatDate(dateString) {
            if (!dateString) return '-';
            const date = new Date(dateString);
            return date.toLocaleDateString('en-US', {
                year: 'numeric',
                month: 'short',
                day: 'numeric'
            });
        },
        
        /**
         * Truncate text
         */
        truncate(text, length = 100) {
            if (!text) return '';
            if (text.length <= length) return text;
            return text.substring(0, length) + '...';
        }
    };
}

// Make available globally
window.shop[PageName] = shop[PageName];

pageLog.info('[PageName] module loaded');
```

---

### 3. Route Registration

**File:** `app/api/v1/shop/pages.py`

```python
from fastapi import APIRouter, Request, Depends
from sqlalchemy.orm import Session
from app.core.database import get_db

router = APIRouter()

@router.get("/[page-route]")
async def [page_name]_page(
    request: Request,
    db: Session = Depends(get_db)
):
    """
    [Page Name] page
    Displays [description]
    """
    # Vendor and theme come from middleware
    vendor = request.state.vendor
    theme = request.state.theme
    
    return templates.TemplateResponse(
        "shop/[page-name].html",
        {
            "request": request,
            "vendor": vendor,
            "theme": theme,
        }
    )
```

---

## 🎨 Common Page Patterns

### Pattern 1: Product Grid Page (Homepage, Category)

**Use for:** Homepage, category pages, search results

```javascript
async init() {
    await this.loadProducts();
}

async loadProducts() {
    this.loading = true;
    try {
        const response = await fetch(
            `/api/v1/shop/${this.vendorCode}/products?category=${this.category}`
        );
        const data = await response.json();
        this.products = data.products || [];
    } catch (error) {
        this.error = error.message;
    } finally {
        this.loading = false;
    }
}
```

**Template:**
```html
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
    <template x-for="product in products" :key="product.id">
        {% include 'shop/partials/product-card.html' %}
    </template>
</div>
```

---

### Pattern 2: Product Detail Page

**Use for:** Single product pages

```javascript
async init() {
    const productId = this.getProductIdFromUrl();
    await this.loadProduct(productId);
    await this.loadRelatedProducts(productId);
}

async loadProduct(id) {
    const product = await fetch(
        `/api/v1/shop/${this.vendorCode}/products/${id}`
    ).then(r => r.json());
    
    this.product = product;
    this.selectedImage = product.images[0];
}

addToCartWithQuantity() {
    const shopLayout = window.shopLayoutData();
    shopLayout.addToCart(this.product, this.quantity);
}
```

**Template:**
```html
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12">
    <!-- Image Gallery -->
    <div>
        <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" 
                     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" 
           :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"
                :style="{ 'background-color': 'var(--color-primary)' }">
            Add to Cart
        </button>
    </div>
</div>
```

---

### Pattern 3: Cart Page

**Use for:** Shopping cart

```javascript
async init() {
    this.loadCart();
}

loadCart() {
    const shopLayout = window.shopLayoutData();
    this.cart = shopLayout.cart;
    this.calculateTotals();
}

updateQuantity(productId, quantity) {
    const shopLayout = window.shopLayoutData();
    shopLayout.updateCartItem(productId, quantity);
    this.loadCart();
}

removeItem(productId) {
    const shopLayout = window.shopLayoutData();
    shopLayout.removeFromCart(productId);
    this.loadCart();
}

get subtotal() {
    return this.cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
}

get shipping() {
    return this.subtotal > 50 ? 0 : 9.99;
}

get total() {
    return this.subtotal + this.shipping;
}
```

---

### Pattern 4: Search & Filter Page

**Use for:** Search results, filtered product lists

```javascript
filters: {
    search: '',
    category: '',
    minPrice: 0,
    maxPrice: 1000,
    sortBy: 'relevance',
    inStock: true
},

async performSearch() {
    this.loading = true;
    try {
        const response = await fetch(
            `/api/v1/shop/${this.vendorCode}/search`,
            {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(this.filters)
            }
        );
        const data = await response.json();
        this.results = data.results || [];
    } finally {
        this.loading = false;
    }
}
```

---

## 🔧 Best Practices

### 1. Theme Integration

Always use CSS variables for vendor colors:

```html
<!-- ✅ GOOD: Uses theme variable -->
<button :style="{ 'background-color': 'var(--color-primary)' }">
    Buy Now
</button>

<!-- ❌ BAD: Hardcoded color -->
<button class="bg-blue-500">
    Buy Now
</button>
```

### 2. Cart Integration

Always use the shop layout's cart methods:

```javascript
// ✅ GOOD: Uses shop layout
const shopLayout = window.shopLayoutData();
shopLayout.addToCart(product, quantity);

// ❌ BAD: Direct localStorage manipulation
localStorage.setItem('cart', JSON.stringify(cart));
```

### 3. Loading States

Always show loading indicators:

```javascript
this.loading = true;
try {
    // ... async operation
} finally {
    this.loading = false; // Always executes
}
```

### 4. Error Handling

Always handle errors gracefully:

```javascript
try {
    await this.loadData();
} catch (error) {
    console.error('Load failed:', error);
    this.error = 'Unable to load products. Please try again.';
    // Don't throw - let UI handle gracefully
}
```

### 5. Responsive Images

Use lazy loading and responsive images:

```html
<img :src="product.image" 
     :alt="product.name"
     loading="lazy"
     class="w-full h-full object-cover">
```

### 6. Dark Mode

Support both light and dark modes:

```html
<div class="bg-white dark:bg-gray-800 text-gray-900 dark:text-white">
    Content
</div>
```

### 7. Accessibility

Add proper ARIA labels and keyboard navigation:

```html
<button @click="addToCart(product)" 
        aria-label="Add to cart"
        role="button">
    Add to Cart
</button>
```

---

## 📱 Responsive Design Checklist

- [ ] Mobile (< 640px): Single column layout
- [ ] Tablet (640px - 1024px): 2-3 column layout
- [ ] Desktop (> 1024px): 4 column layout
- [ ] Images scale properly on all devices
- [ ] Touch targets are at least 44x44px
- [ ] Text is readable without zooming
- [ ] Navigation adapts to screen size
- [ ] Modals are scrollable on small screens
- [ ] Forms are easy to fill on mobile

---

## ✅ Testing Checklist

### Functionality
- [ ] Page loads without errors
- [ ] Data loads correctly
- [ ] Loading state displays
- [ ] Error state handles failures
- [ ] Empty state shows when no data
- [ ] Filters work correctly
- [ ] Pagination works
- [ ] Cart integration works

### Theme Integration
- [ ] Vendor colors display correctly
- [ ] Vendor logo displays
- [ ] Custom fonts load
- [ ] Custom CSS applies
- [ ] Dark mode works with vendor colors

### Responsive Design
- [ ] Mobile layout works
- [ ] Tablet layout works
- [ ] Desktop layout works
- [ ] Images are responsive
- [ ] Touch interactions work

### Performance
- [ ] Page loads quickly
- [ ] Images load progressively
- [ ] No console errors
- [ ] No memory leaks

### Accessibility
- [ ] Keyboard navigation works
- [ ] Screen reader compatible
- [ ] Color contrast sufficient
- [ ] ARIA labels present

---

## 🎯 Component Library

### Reusable Partials

Create reusable components in `templates/shop/partials/`:

**product-card.html:**
```html
<div class="product-card bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-lg">
    <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" 
           :style="{ color: 'var(--color-primary)' }"
           x-text="formatPrice(product.price)"></p>
        <button @click="addToCart(product)" 
                class="w-full mt-4 py-2 text-white rounded"
                :style="{ 'background-color': 'var(--color-primary)' }">
            Add to Cart
        </button>
    </div>
</div>
```

**filter-sidebar.html:**
```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>
        <select x-model="filters.category" @change="applyFilters()"
                class="w-full px-3 py-2 border rounded">
            <option value="">All Categories</option>
            <template x-for="cat in categories">
                <option :value="cat.id" x-text="cat.name"></option>
            </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" 
               min="0" max="1000" step="10"
               class="w-full">
        <div class="flex justify-between text-sm">
            <span>$0</span>
            <span x-text="'$' + filters.maxPrice"></span>
        </div>
    </div>
    
    <!-- Apply Button -->
    <button @click="applyFilters()" 
            class="w-full py-2 text-white rounded"
            :style="{ 'background-color': 'var(--color-primary)' }">
        Apply Filters
    </button>
</div>
```

---

## 🚀 Quick Start Commands

```bash
# Create new page files
touch app/templates/shop/new-page.html
touch app/static/shop/js/new-page.js

# Copy templates
cp template.html app/templates/shop/new-page.html
cp template.js app/static/shop/js/new-page.js

# Update placeholders:
# - Replace [page-name] with actual name
# - Replace [PageName] with PascalCase name
# - Add route in pages.py
# - Test with multiple vendor themes!
```

---

## 📚 Additional Resources

### Theme System
- **CSS Variables**: All vendor colors in `var(--color-name)` format
- **Fonts**: `var(--font-heading)` and `var(--font-body)`
- **Logo**: Available in both light and dark versions
- **Custom CSS**: Vendor-specific styles automatically injected

### Shop Layout Functions
- `addToCart(product, quantity)`: Add item to cart
- `showToast(message, type)`: Show notification
- `formatPrice(amount)`: Format as currency
- `formatDate(date)`: Format date string

### Icons
Use the global icon helper:
```html
<span x-html="$icon('shopping-cart', 'w-5 h-5')"></span>
<span x-html="$icon('heart', 'w-6 h-6 text-red-500')"></span>
```

### API Client
Shared API wrapper for authenticated requests:
```javascript
const data = await apiClient.get('/endpoint');
await apiClient.post('/endpoint', { data });
```

---

This template provides a complete, theme-aware pattern for building shop pages with consistent structure, vendor branding, cart integration, and excellent user experience across all devices.
