revamping documentation
This commit is contained in:
808
docs/frontend/shop/architecture.md
Normal file
808
docs/frontend/shop/architecture.md
Normal file
@@ -0,0 +1,808 @@
|
||||
╔══════════════════════════════════════════════════════════════════╗
|
||||
║ SHOP FRONTEND ARCHITECTURE OVERVIEW ║
|
||||
║ Alpine.js + Jinja2 + Tailwind CSS + Multi-Theme ║
|
||||
╚══════════════════════════════════════════════════════════════════╝
|
||||
|
||||
📦 WHAT IS THIS?
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Customer-facing shop frontend provides visitors with a branded
|
||||
e-commerce experience unique to each vendor. Built with:
|
||||
✅ Jinja2 Templates (server-side rendering)
|
||||
✅ Alpine.js (client-side reactivity)
|
||||
✅ Tailwind CSS (utility-first styling)
|
||||
✅ Multi-Theme System (vendor branding)
|
||||
✅ FastAPI (backend routes)
|
||||
|
||||
|
||||
🎯 KEY PRINCIPLES
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
1. Theme-First Design
|
||||
• Each vendor has unique colors, fonts, logos
|
||||
• CSS variables for dynamic theming
|
||||
• Custom CSS support per vendor
|
||||
• Dark mode with vendor colors
|
||||
|
||||
2. Progressive Enhancement
|
||||
• Works without JavaScript (basic HTML)
|
||||
• JavaScript adds cart, search, filters
|
||||
• Graceful degradation for older browsers
|
||||
|
||||
3. API-First Data Loading
|
||||
• All products from REST APIs
|
||||
• Client-side cart management
|
||||
• Real-time stock updates
|
||||
• Search and filtering
|
||||
|
||||
4. Responsive & Mobile-First
|
||||
• Mobile-first Tailwind approach
|
||||
• Touch-friendly interactions
|
||||
• Optimized images
|
||||
• Fast page loads
|
||||
|
||||
|
||||
📁 FILE STRUCTURE
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
app/
|
||||
├── templates/shop/
|
||||
│ ├── base.html ← Base template (layout)
|
||||
│ ├── home.html ← Homepage / product grid
|
||||
│ ├── product-detail.html ← Single product page
|
||||
│ ├── cart.html ← Shopping cart
|
||||
│ ├── checkout.html ← Checkout flow
|
||||
│ ├── search.html ← Search results
|
||||
│ ├── category.html ← Category browse
|
||||
│ ├── about.html ← About the shop
|
||||
│ ├── contact.html ← Contact form
|
||||
│ └── partials/ ← Reusable components
|
||||
│ ├── product-card.html ← Product display card
|
||||
│ ├── cart-item.html ← Cart item row
|
||||
│ ├── search-modal.html ← Search overlay
|
||||
│ └── filters.html ← Product filters
|
||||
│
|
||||
├── static/shop/
|
||||
│ ├── css/
|
||||
│ │ ├── shop.css ← Shop-specific styles
|
||||
│ │ └── themes/ ← Optional theme stylesheets
|
||||
│ │ ├── modern.css
|
||||
│ │ ├── minimal.css
|
||||
│ │ └── elegant.css
|
||||
│ ├── js/
|
||||
│ │ ├── shop-layout.js ← Base shop functionality
|
||||
│ │ ├── home.js ← Homepage logic
|
||||
│ │ ├── product-detail.js ← Product page logic
|
||||
│ │ ├── cart.js ← Cart management
|
||||
│ │ ├── checkout.js ← Checkout flow
|
||||
│ │ ├── search.js ← Search functionality
|
||||
│ │ └── filters.js ← Product filtering
|
||||
│ └── img/
|
||||
│ ├── placeholder-product.png
|
||||
│ └── empty-cart.svg
|
||||
│
|
||||
├── static/shared/ ← Shared across all areas
|
||||
│ ├── js/
|
||||
│ │ ├── log-config.js ← Logging setup
|
||||
│ │ ├── icons.js ← Icon registry
|
||||
│ │ ├── utils.js ← Utility functions
|
||||
│ │ └── api-client.js ← API wrapper
|
||||
│ └── css/
|
||||
│ └── base.css ← Global styles
|
||||
│
|
||||
└── api/v1/shop/
|
||||
└── pages.py ← Route handlers
|
||||
|
||||
|
||||
🏗️ ARCHITECTURE LAYERS
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Layer 1: Routes (FastAPI)
|
||||
↓
|
||||
Layer 2: Middleware (Vendor + Theme Detection)
|
||||
↓
|
||||
Layer 3: Templates (Jinja2)
|
||||
↓
|
||||
Layer 4: JavaScript (Alpine.js)
|
||||
↓
|
||||
Layer 5: API (REST endpoints)
|
||||
↓
|
||||
Layer 6: Database
|
||||
|
||||
|
||||
Layer 1: ROUTES (FastAPI)
|
||||
──────────────────────────────────────────────────────────────────
|
||||
Purpose: Vendor Detection + Template Rendering
|
||||
Location: app/api/v1/shop/pages.py
|
||||
|
||||
Example:
|
||||
@router.get("/")
|
||||
async def shop_home(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
vendor = request.state.vendor # From middleware
|
||||
theme = request.state.theme # From middleware
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"shop/home.html",
|
||||
{
|
||||
"request": request,
|
||||
"vendor": vendor,
|
||||
"theme": theme,
|
||||
}
|
||||
)
|
||||
|
||||
Responsibilities:
|
||||
✅ Access vendor from middleware
|
||||
✅ Access theme from middleware
|
||||
✅ Render template
|
||||
❌ NO database queries (data loaded client-side)
|
||||
❌ NO business logic
|
||||
|
||||
|
||||
Layer 2: MIDDLEWARE
|
||||
──────────────────────────────────────────────────────────────────
|
||||
Purpose: Vendor & Theme Identification
|
||||
|
||||
Two middleware components work together:
|
||||
|
||||
1. Vendor Context Middleware
|
||||
• Detects vendor from domain/subdomain
|
||||
• Sets request.state.vendor
|
||||
• Returns 404 if vendor not found
|
||||
|
||||
2. Theme Context Middleware
|
||||
• Loads theme for detected vendor
|
||||
• Sets request.state.theme
|
||||
• Falls back to default theme
|
||||
|
||||
Order matters:
|
||||
vendor_context_middleware → theme_context_middleware
|
||||
|
||||
|
||||
Layer 3: TEMPLATES (Jinja2)
|
||||
──────────────────────────────────────────────────────────────────
|
||||
Purpose: HTML Structure + Vendor Branding
|
||||
Location: app/templates/shop/
|
||||
|
||||
Template Hierarchy:
|
||||
base.html (layout + theme injection)
|
||||
↓
|
||||
home.html (product grid)
|
||||
↓
|
||||
partials/product-card.html (components)
|
||||
|
||||
Example:
|
||||
{% extends "shop/base.html" %}
|
||||
{% block title %}{{ vendor.name }}{% endblock %}
|
||||
{% block alpine_data %}shopHome(){% endblock %}
|
||||
{% block content %}
|
||||
<div x-show="loading">Loading products...</div>
|
||||
<div x-show="!loading" class="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<template x-for="product in products" :key="product.id">
|
||||
<!-- Product card -->
|
||||
</template>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
Key Features:
|
||||
✅ Theme CSS variables injection
|
||||
✅ Vendor logo (light/dark mode)
|
||||
✅ Custom CSS from theme
|
||||
✅ Social links from theme
|
||||
✅ Dynamic favicon
|
||||
|
||||
|
||||
Layer 4: JAVASCRIPT (Alpine.js)
|
||||
──────────────────────────────────────────────────────────────────
|
||||
Purpose: Client-Side Interactivity + Cart + Search
|
||||
Location: app/static/shop/js/
|
||||
|
||||
Example (shop-layout.js):
|
||||
function shopLayoutData() {
|
||||
return {
|
||||
dark: false,
|
||||
cartCount: 0,
|
||||
cart: [],
|
||||
|
||||
init() {
|
||||
this.loadCart();
|
||||
this.loadThemePreference();
|
||||
},
|
||||
|
||||
addToCart(product, quantity) {
|
||||
// Add to cart logic
|
||||
this.cart.push({ ...product, quantity });
|
||||
this.saveCart();
|
||||
},
|
||||
|
||||
toggleTheme() {
|
||||
this.dark = !this.dark;
|
||||
localStorage.setItem('shop-theme',
|
||||
this.dark ? 'dark' : 'light');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Responsibilities:
|
||||
✅ Load products from API
|
||||
✅ Manage cart in localStorage
|
||||
✅ Handle search and filters
|
||||
✅ Update DOM reactively
|
||||
✅ Theme toggling
|
||||
|
||||
|
||||
Layer 5: API (REST)
|
||||
──────────────────────────────────────────────────────────────────
|
||||
Purpose: Product Data + Cart + Orders
|
||||
Location: app/api/v1/shop/*.py (not pages.py)
|
||||
|
||||
Example Endpoints:
|
||||
GET /api/v1/shop/{vendor_code}/products
|
||||
GET /api/v1/shop/{vendor_code}/products/{id}
|
||||
GET /api/v1/shop/{vendor_code}/categories
|
||||
POST /api/v1/shop/{vendor_code}/search
|
||||
POST /api/v1/shop/{vendor_code}/cart/checkout
|
||||
|
||||
|
||||
🔄 DATA FLOW
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Page Load Flow:
|
||||
──────────────────────────────────────────────────────────────────
|
||||
1. Customer → visits acme-shop.com
|
||||
2. Vendor Middleware → Identifies "ACME" vendor
|
||||
3. Theme Middleware → Loads ACME's theme config
|
||||
4. FastAPI → Renders shop/home.html
|
||||
5. Browser → Receives HTML with theme CSS variables
|
||||
6. Alpine.js → init() executes
|
||||
7. JavaScript → GET /api/v1/shop/ACME/products
|
||||
8. API → Returns product list JSON
|
||||
9. Alpine.js → Updates products array
|
||||
10. Browser → DOM updates with product cards
|
||||
|
||||
Add to Cart Flow:
|
||||
──────────────────────────────────────────────────────────────────
|
||||
1. Customer → Clicks "Add to Cart"
|
||||
2. Alpine.js → addToCart(product, quantity)
|
||||
3. Alpine.js → Updates cart array
|
||||
4. Alpine.js → Saves to localStorage
|
||||
5. Alpine.js → Updates cartCount badge
|
||||
6. Alpine.js → Shows toast notification
|
||||
7. Browser → Cart icon updates automatically
|
||||
|
||||
Checkout Flow:
|
||||
──────────────────────────────────────────────────────────────────
|
||||
1. Customer → Goes to /cart
|
||||
2. Page → Loads cart from localStorage
|
||||
3. Customer → Fills checkout form
|
||||
4. Alpine.js → POST /api/v1/shop/ACME/cart/checkout
|
||||
5. API → Creates order + payment intent
|
||||
6. Alpine.js → Redirects to payment
|
||||
7. Payment → Completes
|
||||
8. Redirect → /order/{order_id}/confirmation
|
||||
|
||||
|
||||
🎨 MULTI-THEME SYSTEM
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
How Themes Work:
|
||||
|
||||
1. Database Storage
|
||||
• Each vendor has a theme record
|
||||
• Stores colors, fonts, logos, layout prefs
|
||||
• Custom CSS per vendor
|
||||
|
||||
2. CSS Variables Injection
|
||||
• base.html injects variables in <style> tag
|
||||
• Variables available throughout page
|
||||
• Example:
|
||||
:root {
|
||||
--color-primary: #6366f1;
|
||||
--color-secondary: #8b5cf6;
|
||||
--color-accent: #ec4899;
|
||||
--color-background: #ffffff;
|
||||
--color-text: #1f2937;
|
||||
--font-heading: Inter, sans-serif;
|
||||
--font-body: Inter, sans-serif;
|
||||
}
|
||||
|
||||
3. Usage in Templates
|
||||
<button style="background-color: var(--color-primary)">
|
||||
Buy Now
|
||||
</button>
|
||||
|
||||
4. Dark Mode
|
||||
• Vendor colors adjust for dark mode
|
||||
• Saved in localStorage
|
||||
• Applied via :class="{ 'dark': dark }"
|
||||
• Uses dark: variants in Tailwind
|
||||
|
||||
|
||||
Theme Configuration Example:
|
||||
──────────────────────────────────────────────────────────────────
|
||||
{
|
||||
"theme_name": "modern",
|
||||
"colors": {
|
||||
"primary": "#6366f1",
|
||||
"secondary": "#8b5cf6",
|
||||
"accent": "#ec4899",
|
||||
"background": "#ffffff",
|
||||
"text": "#1f2937"
|
||||
},
|
||||
"fonts": {
|
||||
"heading": "Inter, sans-serif",
|
||||
"body": "Inter, sans-serif"
|
||||
},
|
||||
"branding": {
|
||||
"logo": "/media/vendors/acme/logo.png",
|
||||
"logo_dark": "/media/vendors/acme/logo-dark.png",
|
||||
"favicon": "/media/vendors/acme/favicon.ico"
|
||||
},
|
||||
"layout": {
|
||||
"header": "fixed",
|
||||
"style": "grid"
|
||||
},
|
||||
"social_links": {
|
||||
"facebook": "https://facebook.com/acme",
|
||||
"instagram": "https://instagram.com/acme"
|
||||
},
|
||||
"custom_css": ".product-card { border-radius: 12px; }"
|
||||
}
|
||||
|
||||
|
||||
🛒 CART SYSTEM
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Client-Side Cart Management:
|
||||
|
||||
Storage: localStorage
|
||||
Format: JSON array of cart items
|
||||
|
||||
Cart Item Structure:
|
||||
{
|
||||
"id": "product-123",
|
||||
"name": "Product Name",
|
||||
"price": 29.99,
|
||||
"quantity": 2,
|
||||
"image": "/media/products/image.jpg",
|
||||
"vendor_code": "ACME"
|
||||
}
|
||||
|
||||
Key Functions:
|
||||
• loadCart(): Load from localStorage
|
||||
• saveCart(): Persist to localStorage
|
||||
• addToCart(product, quantity): Add item
|
||||
• updateCartItem(id, quantity): Update quantity
|
||||
• removeFromCart(id): Remove item
|
||||
• clearCart(): Empty cart
|
||||
• cartTotal: Computed total price
|
||||
|
||||
Cart Persistence:
|
||||
• Survives page refresh
|
||||
• Shared across shop pages
|
||||
• Cleared on checkout completion
|
||||
• Synced across tabs (optional)
|
||||
|
||||
|
||||
🔍 SEARCH & FILTERS
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Search System:
|
||||
|
||||
1. Search Modal
|
||||
• Overlay with input
|
||||
• Live search as you type
|
||||
• Keyboard shortcuts (Cmd+K)
|
||||
|
||||
2. Search API
|
||||
POST /api/v1/shop/{vendor_code}/search
|
||||
{
|
||||
"query": "laptop",
|
||||
"category": "electronics",
|
||||
"min_price": 100,
|
||||
"max_price": 1000,
|
||||
"sort": "price:asc"
|
||||
}
|
||||
|
||||
3. Client-Side Filtering
|
||||
• Filter products array
|
||||
• No API call needed for basic filters
|
||||
• Use for in-memory datasets
|
||||
|
||||
Filter Components:
|
||||
• Category dropdown
|
||||
• Price range slider
|
||||
• Sort options
|
||||
• Availability toggle
|
||||
• Brand checkboxes
|
||||
|
||||
|
||||
📱 RESPONSIVE DESIGN
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Breakpoints (Tailwind):
|
||||
• sm: 640px (mobile landscape)
|
||||
• md: 768px (tablet)
|
||||
• lg: 1024px (desktop)
|
||||
• xl: 1280px (large desktop)
|
||||
|
||||
Product Grid Responsive:
|
||||
• Mobile: 1 column
|
||||
• Tablet: 2 columns
|
||||
• Desktop: 4 columns
|
||||
|
||||
Example:
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<!-- Product cards -->
|
||||
</div>
|
||||
|
||||
Touch Optimization:
|
||||
• Larger touch targets (min 44x44px)
|
||||
• Swipeable carousels
|
||||
• Touch-friendly buttons
|
||||
• Tap to zoom images
|
||||
|
||||
|
||||
🌙 DARK MODE
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Implementation:
|
||||
1. Alpine.js state: dark: boolean
|
||||
2. HTML class binding: :class="{ 'dark': dark }"
|
||||
3. Tailwind variants: dark:bg-gray-800
|
||||
4. LocalStorage persistence
|
||||
5. Vendor colors adapt to dark mode
|
||||
|
||||
Toggle Button:
|
||||
<button @click="toggleTheme()">
|
||||
<svg x-show="!dark"><!-- Sun icon --></svg>
|
||||
<svg x-show="dark"><!-- Moon icon --></svg>
|
||||
</button>
|
||||
|
||||
Dark Mode Colors:
|
||||
• Background: dark:bg-gray-900
|
||||
• Text: dark:text-gray-100
|
||||
• Borders: dark:border-gray-700
|
||||
• Cards: dark:bg-gray-800
|
||||
|
||||
Vendor Colors:
|
||||
• Primary color adjusts brightness
|
||||
• Maintains brand identity
|
||||
• Readable in both modes
|
||||
|
||||
|
||||
🔐 CUSTOMER AUTHENTICATION
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Optional Auth System:
|
||||
|
||||
Guest Checkout:
|
||||
✅ No account required
|
||||
✅ Email for order updates
|
||||
✅ Quick checkout
|
||||
|
||||
Account Features:
|
||||
✅ Order history
|
||||
✅ Saved addresses
|
||||
✅ Wishlist
|
||||
✅ Profile management
|
||||
|
||||
Auth Flow:
|
||||
1. Login/Register → POST /api/v1/shop/auth/login
|
||||
2. API → Return JWT token
|
||||
3. JavaScript → Store in localStorage
|
||||
4. API Client → Add to authenticated requests
|
||||
5. Optional → Use account features
|
||||
|
||||
|
||||
📡 API CLIENT
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Location: app/static/shared/js/api-client.js
|
||||
|
||||
Usage:
|
||||
const products = await apiClient.get(
|
||||
`/api/v1/shop/${vendorCode}/products`
|
||||
);
|
||||
|
||||
const order = await apiClient.post(
|
||||
`/api/v1/shop/${vendorCode}/checkout`,
|
||||
orderData
|
||||
);
|
||||
|
||||
Features:
|
||||
✅ Automatic error handling
|
||||
✅ JSON parsing
|
||||
✅ Loading states
|
||||
✅ Auth token injection (if logged in)
|
||||
|
||||
|
||||
🐛 LOGGING
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Location: app/static/shared/js/log-config.js
|
||||
|
||||
Shop-Specific Logging:
|
||||
shopLog.info('Product added to cart', product);
|
||||
shopLog.error('Checkout failed', error);
|
||||
shopLog.debug('Search query', { query, results });
|
||||
|
||||
Levels:
|
||||
• INFO: User actions
|
||||
• ERROR: Failures and exceptions
|
||||
• DEBUG: Development debugging
|
||||
• WARN: Warnings
|
||||
|
||||
|
||||
🎭 ICONS
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Location: app/static/shared/js/icons.js
|
||||
|
||||
Usage:
|
||||
<span x-html="$icon('shopping-cart', 'w-5 h-5')"></span>
|
||||
<span x-html="$icon('search', 'w-6 h-6 text-primary')"></span>
|
||||
|
||||
Shop Icons:
|
||||
• shopping-cart, shopping-bag
|
||||
• heart (wishlist)
|
||||
• search, filter
|
||||
• eye (view)
|
||||
• star (rating)
|
||||
• truck (shipping)
|
||||
• check (success)
|
||||
• x (close)
|
||||
• chevron-left, chevron-right
|
||||
• spinner (loading)
|
||||
|
||||
|
||||
🚀 PERFORMANCE
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Optimization Techniques:
|
||||
|
||||
1. Image Optimization
|
||||
• WebP format
|
||||
• Lazy loading
|
||||
• Responsive images (srcset)
|
||||
• CDN delivery
|
||||
|
||||
2. Code Splitting
|
||||
• Base layout loads first
|
||||
• Page-specific JS loads after
|
||||
• Deferred non-critical CSS
|
||||
|
||||
3. Caching
|
||||
• Browser cache for assets
|
||||
• LocalStorage for cart
|
||||
• Service worker (optional)
|
||||
|
||||
4. Lazy Loading
|
||||
• Products load as you scroll
|
||||
• Images load when visible
|
||||
• Infinite scroll for large catalogs
|
||||
|
||||
5. CDN Assets
|
||||
• Tailwind from CDN
|
||||
• Alpine.js from CDN
|
||||
• Vendor assets from CDN
|
||||
|
||||
|
||||
📊 PAGE-BY-PAGE BREAKDOWN
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
/ (Homepage)
|
||||
──────────────────────────────────────────────────────────────────
|
||||
Purpose: Display featured products, hero section
|
||||
Components:
|
||||
• Hero banner with CTA
|
||||
• Featured products grid
|
||||
• Category cards
|
||||
• About vendor section
|
||||
Data Sources:
|
||||
• GET /api/v1/shop/{code}/products?featured=true
|
||||
• GET /api/v1/shop/{code}/categories
|
||||
|
||||
/products
|
||||
──────────────────────────────────────────────────────────────────
|
||||
Purpose: Browse all products with filters
|
||||
Components:
|
||||
• Product grid
|
||||
• Filter sidebar
|
||||
• Sort dropdown
|
||||
• Pagination
|
||||
Data Sources:
|
||||
• GET /api/v1/shop/{code}/products
|
||||
• Filters applied client-side or server-side
|
||||
|
||||
/products/{product_id}
|
||||
──────────────────────────────────────────────────────────────────
|
||||
Purpose: Single product detail page
|
||||
Components:
|
||||
• Image gallery
|
||||
• Product info
|
||||
• Add to cart form
|
||||
• Related products
|
||||
• Reviews (optional)
|
||||
Data Sources:
|
||||
• GET /api/v1/shop/{code}/products/{id}
|
||||
• GET /api/v1/shop/{code}/products/{id}/related
|
||||
|
||||
/cart
|
||||
──────────────────────────────────────────────────────────────────
|
||||
Purpose: Review cart contents before checkout
|
||||
Components:
|
||||
• Cart items list
|
||||
• Quantity adjusters
|
||||
• Remove buttons
|
||||
• Cart total
|
||||
• Checkout button
|
||||
Data Sources:
|
||||
• LocalStorage cart
|
||||
• No API call needed
|
||||
|
||||
/checkout
|
||||
──────────────────────────────────────────────────────────────────
|
||||
Purpose: Complete purchase
|
||||
Components:
|
||||
• Shipping form
|
||||
• Payment form
|
||||
• Order summary
|
||||
• Submit button
|
||||
Data Sources:
|
||||
• POST /api/v1/shop/{code}/checkout
|
||||
• Stripe/PayPal integration
|
||||
|
||||
/search
|
||||
──────────────────────────────────────────────────────────────────
|
||||
Purpose: Search results page
|
||||
Components:
|
||||
• Search input
|
||||
• Results grid
|
||||
• Filter options
|
||||
• Sort options
|
||||
Data Sources:
|
||||
• POST /api/v1/shop/{code}/search
|
||||
|
||||
/category/{category_slug}
|
||||
──────────────────────────────────────────────────────────────────
|
||||
Purpose: Browse products by category
|
||||
Components:
|
||||
• Breadcrumbs
|
||||
• Product grid
|
||||
• Subcategories
|
||||
• Filters
|
||||
Data Sources:
|
||||
• GET /api/v1/shop/{code}/categories/{slug}/products
|
||||
|
||||
/about
|
||||
──────────────────────────────────────────────────────────────────
|
||||
Purpose: About the vendor
|
||||
Components:
|
||||
• Vendor story
|
||||
• Team photos
|
||||
• Values/mission
|
||||
• Contact info
|
||||
Data Sources:
|
||||
• Vendor info from middleware
|
||||
• Static content
|
||||
|
||||
/contact
|
||||
──────────────────────────────────────────────────────────────────
|
||||
Purpose: Contact form
|
||||
Components:
|
||||
• Contact form
|
||||
• Map (optional)
|
||||
• Business hours
|
||||
• Social links
|
||||
Data Sources:
|
||||
• POST /api/v1/shop/{code}/contact
|
||||
|
||||
|
||||
🎓 LEARNING PATH
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
For New Developers:
|
||||
|
||||
1. Understand Architecture (1 hour)
|
||||
→ Read this document
|
||||
→ Review file structure
|
||||
→ Examine base template
|
||||
→ Understand theme system
|
||||
|
||||
2. Study Existing Page (2 hours)
|
||||
→ Open home.html
|
||||
→ Open shop-layout.js
|
||||
→ Trace product loading flow
|
||||
→ Examine cart management
|
||||
|
||||
3. Create Simple Page (4 hours)
|
||||
→ Copy templates
|
||||
→ Modify for new feature
|
||||
→ Test with different vendor themes
|
||||
→ Verify responsive design
|
||||
|
||||
4. Add Complex Feature (1 day)
|
||||
→ Product filters
|
||||
→ Cart operations
|
||||
→ API integration
|
||||
→ Search functionality
|
||||
|
||||
5. Master Patterns (1 week)
|
||||
→ All common patterns
|
||||
→ Theme customization
|
||||
→ Performance optimization
|
||||
→ Mobile responsiveness
|
||||
|
||||
|
||||
🔄 DEPLOYMENT CHECKLIST
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Before Deploying:
|
||||
□ Build Tailwind CSS
|
||||
□ Minify JavaScript
|
||||
□ Test all routes
|
||||
□ Test with multiple vendor themes
|
||||
□ Verify cart persistence
|
||||
□ Check mobile responsive
|
||||
□ Test dark mode
|
||||
□ Validate product display
|
||||
□ Test checkout flow
|
||||
□ Check image optimization
|
||||
□ Test search functionality
|
||||
□ Verify social links
|
||||
□ Test across browsers
|
||||
□ Check console for errors
|
||||
□ Test in production mode
|
||||
|
||||
|
||||
🔒 SECURITY
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Best Practices:
|
||||
|
||||
1. Input Validation
|
||||
✅ Validate all form inputs
|
||||
✅ Sanitize user content
|
||||
✅ XSS prevention
|
||||
|
||||
2. Cart Security
|
||||
✅ Validate cart on server
|
||||
✅ Check stock availability
|
||||
✅ Verify prices server-side
|
||||
✅ Never trust client cart
|
||||
|
||||
3. Payment Security
|
||||
✅ Use trusted payment providers
|
||||
✅ Never store card details
|
||||
✅ HTTPS only
|
||||
✅ PCI compliance
|
||||
|
||||
4. Rate Limiting
|
||||
✅ Search endpoint throttling
|
||||
✅ Contact form limits
|
||||
✅ Checkout attempt limits
|
||||
|
||||
|
||||
📚 REFERENCE LINKS
|
||||
═════════════════════════════════════════════════════════════════
|
||||
|
||||
Documentation:
|
||||
• Alpine.js: https://alpinejs.dev/
|
||||
• Tailwind CSS: https://tailwindcss.com/
|
||||
• Jinja2: https://jinja.palletsprojects.com/
|
||||
• FastAPI: https://fastapi.tiangolo.com/
|
||||
|
||||
Internal Docs:
|
||||
• Page Template Guide: FRONTEND_SHOP_ALPINE_PAGE_TEMPLATE.md
|
||||
• Multi-Theme Guide: MULTI_THEME_SHOP_GUIDE.md
|
||||
• API Documentation: API_REFERENCE.md
|
||||
• Database Schema: DATABASE_SCHEMA.md
|
||||
|
||||
|
||||
══════════════════════════════════════════════════════════════════
|
||||
SHOP FRONTEND ARCHITECTURE
|
||||
Theme-Driven, Customer-Focused, Brand-Consistent
|
||||
══════════════════════════════════════════════════════════════════
|
||||
972
docs/frontend/shop/page-templates.md
Normal file
972
docs/frontend/shop/page-templates.md
Normal file
@@ -0,0 +1,972 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user