Replace all ~1,086 occurrences of Wizamart/wizamart/WIZAMART/WizaMart with Orion/orion/ORION across 184 files. This includes database identifiers, email addresses, domain references, R2 bucket names, DNS prefixes, encryption salt, Celery app name, config defaults, Docker configs, CI configs, documentation, seed data, and templates. Renames homepage-wizamart.html template to homepage-orion.html. Fixes duplicate file_pattern key in api.yaml architecture rule. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1346 lines
47 KiB
Markdown
1346 lines
47 KiB
Markdown
╔══════════════════════════════════════════════════════════════════╗
|
|
║ 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 store. Built with:
|
|
✅ Jinja2 Templates (server-side rendering)
|
|
✅ Alpine.js (client-side reactivity)
|
|
✅ Tailwind CSS (utility-first styling)
|
|
✅ Multi-Theme System (store branding)
|
|
✅ FastAPI (backend routes)
|
|
|
|
|
|
🎯 KEY PRINCIPLES
|
|
═════════════════════════════════════════════════════════════════
|
|
|
|
1. Theme-First Design
|
|
• Each store has unique colors, fonts, logos
|
|
• CSS variables for dynamic theming
|
|
• Custom CSS support per store
|
|
• Dark mode with store 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 + theme)
|
|
│ ├── home.html ← ✅ Homepage / featured products
|
|
│ ├── products.html ← ✅ Product catalog with filters
|
|
│ ├── product.html ← Product detail page
|
|
│ ├── cart.html ← Shopping cart
|
|
│ ├── checkout.html ← Checkout flow
|
|
│ ├── search.html ← Search results
|
|
│ ├── account/ ← Customer account pages
|
|
│ │ ├── login.html ← ✅ Customer login (IMPLEMENTED)
|
|
│ │ ├── register.html ← ✅ Customer registration (IMPLEMENTED)
|
|
│ │ ├── forgot-password.html ← ✅ Password reset (IMPLEMENTED)
|
|
│ │ ├── dashboard.html
|
|
│ │ ├── orders.html
|
|
│ │ ├── profile.html
|
|
│ │ └── addresses.html
|
|
│ └── errors/ ← Error pages
|
|
│ ├── 400.html
|
|
│ ├── 404.html
|
|
│ └── 500.html
|
|
│
|
|
├── static/shop/
|
|
│ ├── css/
|
|
│ │ └── shop.css ← ✅ Shop-specific styles (IMPLEMENTED)
|
|
│ ├── js/
|
|
│ │ └── shop-layout.js ← ✅ Base shop functionality (IMPLEMENTED)
|
|
│ └── img/
|
|
│ └── (placeholder images)
|
|
│
|
|
├── static/shared/ ← ✅ Shared across all areas (IMPLEMENTED)
|
|
│ ├── js/
|
|
│ │ ├── log-config.js ← ✅ Logging setup
|
|
│ │ ├── icons.js ← ✅ Icon registry
|
|
│ │ ├── utils.js ← ✅ Utility functions
|
|
│ │ └── api-client.js ← ✅ API wrapper
|
|
│ └── css/
|
|
│ └── (shared styles if needed)
|
|
│
|
|
└── routes/
|
|
└── shop_pages.py ← ✅ Route handlers (IMPLEMENTED)
|
|
|
|
|
|
🏗️ ARCHITECTURE LAYERS
|
|
═════════════════════════════════════════════════════════════════
|
|
|
|
Layer 1: Routes (FastAPI)
|
|
↓
|
|
Layer 2: Middleware (Store + Theme Detection)
|
|
↓
|
|
Layer 3: Templates (Jinja2)
|
|
↓
|
|
Layer 4: JavaScript (Alpine.js)
|
|
↓
|
|
Layer 5: API (REST endpoints)
|
|
↓
|
|
Layer 6: Database
|
|
|
|
|
|
Layer 1: ROUTES (FastAPI)
|
|
──────────────────────────────────────────────────────────────────
|
|
Purpose: Store Detection + Template Rendering
|
|
Location: app/routes/shop_pages.py
|
|
|
|
⚠️ ROUTE REGISTRATION (main.py):
|
|
The shop router is mounted at TWO prefixes to support both access methods:
|
|
|
|
# main.py
|
|
app.include_router(shop_pages.router, prefix="/shop", ...) # Domain/subdomain
|
|
app.include_router(shop_pages.router, prefix="/stores/{store_code}/shop", ...) # Path-based
|
|
|
|
This means routes defined WITHOUT /shop prefix in shop_pages.py:
|
|
@router.get("/products") → /shop/products OR /stores/{code}/shop/products
|
|
|
|
❌ COMMON MISTAKE: Don't add /shop prefix in route definitions!
|
|
@router.get("/shop/products") ❌ WRONG - creates /shop/shop/products
|
|
@router.get("/products") ✅ CORRECT - creates /shop/products
|
|
|
|
Example Route Handler:
|
|
@router.get("/", response_class=HTMLResponse, include_in_schema=False)
|
|
@router.get("/products", response_class=HTMLResponse, include_in_schema=False)
|
|
async def shop_products_page(request: Request):
|
|
"""
|
|
Render shop homepage / product catalog.
|
|
Store and theme are auto-injected by middleware.
|
|
"""
|
|
return templates.TemplateResponse(
|
|
"shop/products.html",
|
|
get_shop_context(request) # Helper function
|
|
)
|
|
|
|
Helper Function:
|
|
def get_shop_context(request: Request, **extra_context) -> dict:
|
|
"""Build template context with store/theme from middleware"""
|
|
store = getattr(request.state, 'store', None)
|
|
theme = getattr(request.state, 'theme', None)
|
|
clean_path = getattr(request.state, 'clean_path', request.url.path)
|
|
store_context = getattr(request.state, 'store_context', None)
|
|
|
|
# Get detection method (domain, subdomain, or path)
|
|
access_method = store_context.get('detection_method', 'unknown') if store_context else 'unknown'
|
|
|
|
# Calculate base URL for links
|
|
# - Domain/subdomain: base_url = "/"
|
|
# - Path-based: base_url = "/store/{store_code}/"
|
|
base_url = "/"
|
|
if access_method == "path" and store:
|
|
full_prefix = store_context.get('full_prefix', '/store/')
|
|
base_url = f"{full_prefix}{store.subdomain}/"
|
|
|
|
return {
|
|
"request": request,
|
|
"store": store,
|
|
"theme": theme,
|
|
"clean_path": clean_path,
|
|
"access_method": access_method,
|
|
"base_url": base_url, # ⭐ Used for all links in templates
|
|
**extra_context
|
|
}
|
|
|
|
Responsibilities:
|
|
✅ Access store from middleware (request.state.store)
|
|
✅ Access theme from middleware (request.state.theme)
|
|
✅ Calculate base_url for routing-aware links
|
|
✅ Render template with context
|
|
❌ NO database queries (data loaded client-side via API)
|
|
❌ NO business logic (handled by API endpoints)
|
|
|
|
|
|
⭐ MULTI-ACCESS ROUTING (Domain, Subdomain, Path-Based)
|
|
──────────────────────────────────────────────────────────────────
|
|
The shop frontend supports THREE access methods:
|
|
|
|
1. **Custom Domain** (Production)
|
|
URL: https://customdomain.com/shop/products
|
|
- Store has their own domain
|
|
- base_url = "/"
|
|
- Links: /shop/products, /shop/about, /shop/contact
|
|
|
|
2. **Subdomain** (Production)
|
|
URL: https://orion.letzshop.com/shop/products
|
|
- Store uses platform subdomain
|
|
- base_url = "/"
|
|
- Links: /shop/products, /shop/about, /shop/contact
|
|
|
|
3. **Path-Based** (Development/Testing)
|
|
URL: http://localhost:8000/stores/orion/shop/products
|
|
- Store accessed via path prefix
|
|
- base_url = "/stores/orion/"
|
|
- Links: /stores/orion/shop/products, /stores/orion/shop/about
|
|
|
|
⚠️ CRITICAL: All template links MUST use {{ base_url }}shop/ prefix
|
|
|
|
Example:
|
|
❌ BAD: <a href="/products">Products</a>
|
|
❌ BAD: <a href="{{ base_url }}products">Products</a>
|
|
✅ GOOD: <a href="{{ base_url }}shop/products">Products</a>
|
|
|
|
Note: The router is mounted at /shop prefix in main.py, so all links need shop/ after base_url
|
|
|
|
How It Works:
|
|
1. StoreContextMiddleware detects access method
|
|
2. Sets request.state.store_context with detection_method
|
|
3. get_shop_context() calculates base_url from detection_method
|
|
4. Templates use {{ base_url }} for all internal links
|
|
5. Links work correctly regardless of access method
|
|
|
|
|
|
Layer 2: MIDDLEWARE
|
|
──────────────────────────────────────────────────────────────────
|
|
Purpose: Store & Theme Identification
|
|
|
|
Two middleware components work together:
|
|
|
|
1. Store Context Middleware (middleware/store_context.py)
|
|
• Detects store from domain/subdomain/path
|
|
• Sets request.state.store
|
|
• Sets request.state.store_context (includes detection_method)
|
|
• Sets request.state.clean_path (path without store prefix)
|
|
• Returns 404 if store not found
|
|
|
|
2. Theme Context Middleware (middleware/theme_context.py)
|
|
• Loads theme for detected store
|
|
• Sets request.state.theme
|
|
• Falls back to default theme
|
|
|
|
Order matters:
|
|
store_context_middleware → theme_context_middleware
|
|
|
|
Detection Methods:
|
|
- custom_domain: Store has custom domain
|
|
- subdomain: Store uses platform subdomain
|
|
- path: Store accessed via /store/{code}/ or /stores/{code}/
|
|
|
|
|
|
Layer 3: TEMPLATES (Jinja2)
|
|
──────────────────────────────────────────────────────────────────
|
|
Purpose: HTML Structure + Store 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 %}{{ store.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
|
|
✅ Store 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/
|
|
|
|
⚠️ CRITICAL: JavaScript Loading Order
|
|
──────────────────────────────────────────────────────────────────
|
|
Scripts MUST load in this exact order (see base.html):
|
|
|
|
1. log-config.js ← Logging system (loads first)
|
|
2. icons.js ← Icon registry
|
|
3. shop-layout.js ← Alpine component (before Alpine!)
|
|
4. utils.js ← Utility functions
|
|
5. api-client.js ← API wrapper
|
|
6. Alpine.js (deferred) ← Loads last
|
|
7. Page-specific JS ← Optional page scripts
|
|
|
|
Why This Order Matters:
|
|
• shop-layout.js defines shopLayoutData() BEFORE Alpine initializes
|
|
• Alpine.js defers to ensure DOM is ready
|
|
• Shared utilities available to all scripts
|
|
• Icons and logging available immediately
|
|
|
|
Example from base.html:
|
|
<script src="{{ url_for('static', path='shared/js/log-config.js') }}"></script>
|
|
<script src="{{ url_for('static', path='shared/js/icons.js') }}"></script>
|
|
<script src="{{ url_for('static', path='shop/js/shop-layout.js') }}"></script>
|
|
<script src="{{ url_for('static', path='shared/js/utils.js') }}"></script>
|
|
<script src="{{ url_for('static', path='shared/js/api-client.js') }}"></script>
|
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
|
|
Alpine.js Component Architecture:
|
|
──────────────────────────────────────────────────────────────────
|
|
|
|
⭐ BASE COMPONENT (shop-layout.js):
|
|
|
|
Provides shared functionality for all shop pages:
|
|
|
|
function shopLayoutData() {
|
|
return {
|
|
// Theme state
|
|
dark: localStorage.getItem('shop-theme') === 'dark',
|
|
|
|
// UI state
|
|
mobileMenuOpen: false,
|
|
searchOpen: false,
|
|
cartCount: 0,
|
|
|
|
// Cart state
|
|
cart: [],
|
|
|
|
init() {
|
|
shopLog.info('Shop layout initializing...');
|
|
this.loadCart();
|
|
window.addEventListener('cart-updated', () => {
|
|
this.loadCart();
|
|
});
|
|
shopLog.info('Shop layout initialized');
|
|
},
|
|
|
|
addToCart(product, quantity = 1) {
|
|
const existingIndex = this.cart.findIndex(
|
|
item => item.id === product.id
|
|
);
|
|
|
|
if (existingIndex !== -1) {
|
|
this.cart[existingIndex].quantity += quantity;
|
|
} else {
|
|
this.cart.push({
|
|
id: product.id,
|
|
name: product.name,
|
|
price: product.price,
|
|
image: product.image,
|
|
quantity: quantity
|
|
});
|
|
}
|
|
|
|
this.saveCart();
|
|
this.showToast(`${product.name} added to cart`, 'success');
|
|
},
|
|
|
|
toggleTheme() {
|
|
this.dark = !this.dark;
|
|
localStorage.setItem('shop-theme',
|
|
this.dark ? 'dark' : 'light');
|
|
shopLog.debug('Theme toggled:', this.dark ? 'dark' : 'light');
|
|
},
|
|
|
|
showToast(message, type = 'info') {
|
|
// Toast notification implementation
|
|
}
|
|
};
|
|
}
|
|
|
|
// Make globally available
|
|
window.shopLayoutData = shopLayoutData;
|
|
|
|
⭐ PAGE-SPECIFIC COMPONENTS:
|
|
|
|
Each page extends shopLayoutData() for page-specific functionality:
|
|
|
|
// Example: products.html
|
|
document.addEventListener('alpine:init', () => {
|
|
Alpine.data('shopProducts', () => ({
|
|
...shopLayoutData(), // Extend base component
|
|
|
|
// Page-specific state
|
|
products: [],
|
|
loading: true,
|
|
filters: { search: '', category: '' },
|
|
|
|
// Override init to add page-specific initialization
|
|
async init() {
|
|
shopLog.info('Products page initializing...');
|
|
this.loadCart(); // From shopLayoutData
|
|
await this.loadProducts(); // Page-specific
|
|
},
|
|
|
|
// Page-specific methods
|
|
async loadProducts() {
|
|
const response = await fetch('/api/v1/shop/products');
|
|
const data = await response.json();
|
|
this.products = data.products;
|
|
this.loading = false;
|
|
}
|
|
}));
|
|
});
|
|
|
|
Template Usage:
|
|
──────────────────────────────────────────────────────────────────
|
|
|
|
{# In base.html - uses block to allow override #}
|
|
<html x-data="{% block alpine_data %}shopLayoutData(){% endblock %}"
|
|
x-bind:class="{ 'dark': dark }">
|
|
|
|
{# In products.html - overrides to use page-specific component #}
|
|
{% block alpine_data %}shopProducts(){% endblock %}
|
|
|
|
{# In home.html - uses default base component #}
|
|
{# No block override needed, inherits shopLayoutData() #}
|
|
|
|
⭐ COMPONENT HIERARCHY:
|
|
|
|
shopLayoutData() ← Base component (shared state & methods)
|
|
↓
|
|
shopProducts() ← Products page (extends base + products state)
|
|
shopCart() ← Cart page (extends base + cart state)
|
|
shopCheckout() ← Checkout page (extends base + order state)
|
|
shopAccount() ← Account page (extends base + user state)
|
|
|
|
Benefits:
|
|
✅ Shared functionality (theme, cart, toasts) available on all pages
|
|
✅ Each page has its own state and methods
|
|
✅ DRY - base functionality defined once
|
|
✅ Flexible - pages can override init() or add new methods
|
|
|
|
Tradeoffs:
|
|
⚠️ One component per page (not multiple components)
|
|
⚠️ All page state is at root level
|
|
⚠️ Can't easily split page into independent sub-components
|
|
|
|
Best Practices:
|
|
1. Always extend shopLayoutData() in page components
|
|
2. Override init() if you need page-specific initialization
|
|
3. Call parent methods when needed (this.loadCart(), this.showToast())
|
|
4. Keep page-specific state in the page component
|
|
5. Keep shared state in shopLayoutData()
|
|
|
|
Responsibilities:
|
|
✅ Load products from API
|
|
✅ Manage cart in localStorage
|
|
✅ Handle search and filters
|
|
✅ Update DOM reactively
|
|
✅ Theme toggling (light/dark)
|
|
✅ Mobile menu management
|
|
✅ Toast notifications
|
|
|
|
|
|
Layer 5: API (REST)
|
|
──────────────────────────────────────────────────────────────────
|
|
Purpose: Product Data + Cart + Orders
|
|
Location: app/api/v1/shop/*.py
|
|
|
|
⭐ NEW API STRUCTURE (as of 2025-11-22):
|
|
All shop endpoints use middleware-based store context.
|
|
NO store_id or store_code in URLs!
|
|
|
|
Example Endpoints:
|
|
GET /api/v1/shop/products ← Product catalog
|
|
GET /api/v1/shop/products/{id} ← Product details
|
|
GET /api/v1/shop/products?search=... ← Search products
|
|
GET /api/v1/shop/cart/{session_id} ← Get cart
|
|
POST /api/v1/shop/cart/{session_id}/items ← Add to cart
|
|
PUT /api/v1/shop/cart/{session_id}/items/{product_id} ← Update item
|
|
DELETE /api/v1/shop/cart/{session_id}/items/{product_id} ← Remove item
|
|
POST /api/v1/shop/orders ← Place order (auth required)
|
|
GET /api/v1/shop/orders ← Order history (auth required)
|
|
POST /api/v1/shop/auth/login ← Customer login
|
|
POST /api/v1/shop/auth/register ← Customer registration
|
|
GET /api/v1/shop/content-pages/navigation ← CMS navigation
|
|
GET /api/v1/shop/content-pages/{slug} ← CMS page content
|
|
|
|
How Store Context Works:
|
|
1. Browser makes API call from shop page (e.g., /stores/orion/shop/products)
|
|
2. Browser automatically sends Referer header: http://localhost:8000/stores/orion/shop/products
|
|
3. StoreContextMiddleware extracts store from Referer header
|
|
4. Middleware sets request.state.store = <Store: orion>
|
|
5. API endpoint accesses store: store = request.state.store
|
|
6. No store_id needed in URL!
|
|
|
|
|
|
🔄 DATA FLOW
|
|
═════════════════════════════════════════════════════════════════
|
|
|
|
Page Load Flow:
|
|
──────────────────────────────────────────────────────────────────
|
|
1. Customer → visits acme-shop.com (or /stores/acme/shop/products)
|
|
2. Store Middleware → Identifies "ACME" store from domain/path
|
|
3. Theme Middleware → Loads ACME's theme config
|
|
4. FastAPI → Renders shop/products.html
|
|
5. Browser → Receives HTML with theme CSS variables
|
|
6. Alpine.js → init() executes
|
|
7. JavaScript → GET /api/v1/shop/products (with Referer header)
|
|
8. Middleware → Extracts store from Referer, injects into request.state
|
|
9. API → Returns product list JSON for ACME store
|
|
10. Alpine.js → Updates products array
|
|
11. 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/orders (with Referer header)
|
|
5. Middleware → Extracts store from Referer
|
|
6. API → Creates order + payment intent for store
|
|
7. Alpine.js → Redirects to payment
|
|
8. Payment → Completes
|
|
9. Redirect → /order/{order_id}/confirmation
|
|
|
|
|
|
🎨 MULTI-THEME SYSTEM
|
|
═════════════════════════════════════════════════════════════════
|
|
|
|
How Themes Work:
|
|
|
|
1. Database Storage
|
|
• Each store has a theme record
|
|
• Stores colors, fonts, logos, layout prefs
|
|
• Custom CSS per store
|
|
|
|
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
|
|
• Store 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/stores/acme/logo.png",
|
|
"logo_dark": "/media/stores/acme/logo-dark.png",
|
|
"favicon": "/media/stores/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",
|
|
"store_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/{store_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. Store 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
|
|
|
|
Store 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 (with Referer header)
|
|
2. Middleware → Extracts store from Referer
|
|
3. API → Validates credentials for store's customers
|
|
4. API → Returns JWT token + sets cookie (path=/shop)
|
|
5. JavaScript → Store token in localStorage
|
|
6. API Client → Add token to authenticated requests
|
|
7. Optional → Use account features (orders, profile, etc.)
|
|
|
|
## Authentication Pages
|
|
|
|
*Added: 2025-11-24*
|
|
|
|
All authentication pages use Tailwind CSS, Alpine.js, and theme integration
|
|
for a consistent, branded experience across all stores.
|
|
|
|
✅ Login Page (app/templates/shop/account/login.html)
|
|
──────────────────────────────────────────────────────────────────
|
|
Route: /shop/account/login
|
|
|
|
Features:
|
|
• Two-column layout (branding + form)
|
|
• Email and password fields with validation
|
|
• Password visibility toggle
|
|
• "Remember me" checkbox
|
|
• Error and success message alerts
|
|
• Loading states with animated spinner
|
|
• Links to register and forgot password
|
|
• Theme-aware colors from CSS variables
|
|
• Dark mode support
|
|
• Mobile responsive design
|
|
|
|
Alpine.js Component:
|
|
function customerLogin() {
|
|
return {
|
|
credentials: { email: '', password: '' },
|
|
rememberMe: false,
|
|
showPassword: false,
|
|
loading: false,
|
|
errors: {},
|
|
alert: { show: false, type: 'error', message: '' },
|
|
dark: false,
|
|
|
|
async handleLogin() {
|
|
// POST /api/v1/shop/auth/login
|
|
// Store token in localStorage
|
|
// Redirect to account or return URL
|
|
}
|
|
}
|
|
}
|
|
|
|
API Endpoint:
|
|
POST /api/v1/shop/auth/login
|
|
Body: { email_or_username, password }
|
|
Returns: { access_token, user }
|
|
|
|
✅ Register Page (app/templates/shop/account/register.html)
|
|
──────────────────────────────────────────────────────────────────
|
|
Route: /shop/account/register
|
|
|
|
Features:
|
|
• Two-column layout with store branding
|
|
• First name, last name, email fields
|
|
• Phone number (optional)
|
|
• Password with strength requirements
|
|
• Confirm password field
|
|
• Marketing consent checkbox
|
|
• Real-time client-side validation
|
|
• Password visibility toggle
|
|
• Theme-aware styling
|
|
• Loading states
|
|
• Redirects to login after success
|
|
|
|
Validation Rules:
|
|
• First name: required
|
|
• Last name: required
|
|
• Email: required, valid format
|
|
• Password: min 8 chars, 1 letter, 1 number
|
|
• Confirm password: must match password
|
|
|
|
Alpine.js Component:
|
|
function customerRegistration() {
|
|
return {
|
|
formData: {
|
|
first_name: '', last_name: '', email: '',
|
|
phone: '', password: '', marketing_consent: false
|
|
},
|
|
confirmPassword: '',
|
|
|
|
validateForm() {
|
|
// Validates all fields
|
|
// Returns true if valid
|
|
},
|
|
|
|
async handleRegister() {
|
|
// POST /api/v1/shop/auth/register
|
|
// Redirect to login?registered=true
|
|
}
|
|
}
|
|
}
|
|
|
|
API Endpoint:
|
|
POST /api/v1/shop/auth/register
|
|
Body: { first_name, last_name, email, phone?, password, marketing_consent }
|
|
Returns: { message }
|
|
|
|
✅ Forgot Password Page (app/templates/shop/account/forgot-password.html)
|
|
──────────────────────────────────────────────────────────────────
|
|
Route: /shop/account/forgot-password
|
|
|
|
Features:
|
|
• Two-column layout with store branding
|
|
• Email input field
|
|
• Two-state interface:
|
|
1. Form submission state
|
|
2. Success confirmation state
|
|
• Success state with checkmark icon
|
|
• Option to retry if email not received
|
|
• Theme-aware styling
|
|
• Links back to login and shop
|
|
• Dark mode support
|
|
|
|
Alpine.js Component:
|
|
function forgotPassword() {
|
|
return {
|
|
email: '',
|
|
emailSent: false,
|
|
loading: false,
|
|
|
|
async handleSubmit() {
|
|
// POST /api/v1/shop/auth/forgot-password
|
|
// Show success message
|
|
// emailSent = true
|
|
}
|
|
}
|
|
}
|
|
|
|
API Endpoint:
|
|
POST /api/v1/shop/auth/forgot-password
|
|
Body: { email }
|
|
Returns: { message }
|
|
|
|
🎨 THEME INTEGRATION
|
|
──────────────────────────────────────────────────────────────────
|
|
|
|
All authentication pages inject store theme CSS variables:
|
|
|
|
<style id="store-theme-variables">
|
|
:root {
|
|
{% for key, value in theme.css_variables.items() %}
|
|
{{ key }}: {{ value }};
|
|
{% endfor %}
|
|
}
|
|
|
|
/* Theme-aware button and focus colors */
|
|
.btn-primary-theme {
|
|
background-color: var(--color-primary);
|
|
}
|
|
.btn-primary-theme:hover:not(:disabled) {
|
|
background-color: var(--color-primary-dark, var(--color-primary));
|
|
filter: brightness(0.9);
|
|
}
|
|
.focus-primary:focus {
|
|
border-color: var(--color-primary);
|
|
box-shadow: 0 0 0 3px rgba(var(--color-primary-rgb, 124, 58, 237), 0.1);
|
|
}
|
|
</style>
|
|
|
|
Key Theme Elements:
|
|
• Left panel background: var(--color-primary)
|
|
• Submit buttons: var(--color-primary)
|
|
• Links: var(--color-primary)
|
|
• Checkboxes: var(--color-primary)
|
|
• Focus states: var(--color-primary) with transparency
|
|
• Store logo from theme.branding.logo
|
|
|
|
Benefits:
|
|
✅ Each store's auth pages match their brand
|
|
✅ Consistent with main shop design
|
|
✅ Dark mode adapts to store colors
|
|
✅ Professional, polished appearance
|
|
|
|
📱 RESPONSIVE DESIGN
|
|
──────────────────────────────────────────────────────────────────
|
|
|
|
Mobile (<640px):
|
|
• Vertical layout (image on top, form below)
|
|
• Smaller padding and spacing
|
|
• Full-width buttons
|
|
• Touch-friendly input fields
|
|
|
|
Tablet (640px-1024px):
|
|
• Side-by-side layout begins
|
|
• Balanced column widths
|
|
• Comfortable spacing
|
|
|
|
Desktop (>1024px):
|
|
• Full two-column layout
|
|
• Max width container (max-w-4xl)
|
|
• Centered on page
|
|
• Larger brand imagery
|
|
|
|
🔒 SECURITY FEATURES
|
|
──────────────────────────────────────────────────────────────────
|
|
|
|
Client-Side:
|
|
• Input validation before submission
|
|
• Password visibility toggle
|
|
• HTTPS required
|
|
• No sensitive data in URLs
|
|
• Token stored in localStorage (not cookies)
|
|
|
|
Server-Side (API handles):
|
|
• Password hashing (bcrypt)
|
|
• Email verification
|
|
• Rate limiting
|
|
• CSRF protection
|
|
• SQL injection prevention
|
|
|
|
|
|
📡 API CLIENT
|
|
═════════════════════════════════════════════════════════════════
|
|
|
|
Location: app/static/shared/js/api-client.js
|
|
|
|
⭐ NEW USAGE (as of 2025-11-22):
|
|
No store_code needed! Store extracted from Referer header automatically.
|
|
|
|
Usage:
|
|
// Product catalog
|
|
const products = await fetch('/api/v1/shop/products');
|
|
|
|
// Add to cart
|
|
const response = await fetch('/api/v1/shop/cart/session123/items', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ product_id: 1, quantity: 2 })
|
|
});
|
|
|
|
// Place order
|
|
const order = await fetch('/api/v1/shop/orders', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(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 with Fallback
|
|
• Tailwind CSS from CDN (fallback to local)
|
|
• Alpine.js from CDN (fallback to local)
|
|
• Works offline and in restricted networks
|
|
• See: [CDN Fallback Strategy](../cdn-fallback-strategy.md)
|
|
|
|
|
|
📊 PAGE-BY-PAGE BREAKDOWN
|
|
═════════════════════════════════════════════════════════════════
|
|
|
|
/ (Homepage)
|
|
──────────────────────────────────────────────────────────────────
|
|
Purpose: Display featured products, hero section
|
|
Components:
|
|
• Hero banner with CTA
|
|
• Featured products grid
|
|
• Category cards
|
|
• About store section
|
|
Data Sources:
|
|
• GET /api/v1/shop/products?is_featured=true
|
|
|
|
/products
|
|
──────────────────────────────────────────────────────────────────
|
|
Purpose: Browse all products with filters
|
|
Components:
|
|
• Product grid
|
|
• Filter sidebar
|
|
• Sort dropdown
|
|
• Pagination
|
|
Data Sources:
|
|
• GET /api/v1/shop/products?skip=0&limit=20
|
|
• GET /api/v1/shop/products?search=query
|
|
• 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/products/{id}
|
|
• GET /api/v1/shop/products?limit=4 (related products)
|
|
|
|
/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/orders
|
|
• Stripe/PayPal integration
|
|
|
|
/search
|
|
──────────────────────────────────────────────────────────────────
|
|
Purpose: Search results page
|
|
Components:
|
|
• Search input
|
|
• Results grid
|
|
• Filter options
|
|
• Sort options
|
|
Data Sources:
|
|
• GET /api/v1/shop/products?search=query
|
|
|
|
/category/{category_slug}
|
|
──────────────────────────────────────────────────────────────────
|
|
Purpose: Browse products by category
|
|
Components:
|
|
• Breadcrumbs
|
|
• Product grid
|
|
• Subcategories
|
|
• Filters
|
|
Data Sources:
|
|
• GET /api/v1/shop/products?category={slug}
|
|
|
|
/about
|
|
──────────────────────────────────────────────────────────────────
|
|
Purpose: About the store
|
|
Components:
|
|
• Store story
|
|
• Team photos
|
|
• Values/mission
|
|
• Contact info
|
|
Data Sources:
|
|
• Store info from middleware
|
|
• Static content
|
|
|
|
/contact
|
|
──────────────────────────────────────────────────────────────────
|
|
Purpose: Contact form
|
|
Components:
|
|
• Contact form
|
|
• Map (optional)
|
|
• Business hours
|
|
• Social links
|
|
Data Sources:
|
|
• CMS content page (GET /api/v1/shop/content-pages/contact)
|
|
• Form submission to store email
|
|
|
|
|
|
🎓 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 store 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 store 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
|
|
|
|
|
|
🚨 ERROR HANDLING & ERROR PAGES
|
|
═════════════════════════════════════════════════════════════════
|
|
|
|
Multi-Access Aware Error Pages:
|
|
|
|
All shop error pages (404, 500, etc.) are store-context aware and display
|
|
correct links based on the access method (domain, subdomain, or path-based).
|
|
|
|
Error Page Templates:
|
|
• app/templates/shop/errors/404.html - Not Found
|
|
• app/templates/shop/errors/400.html - Bad Request
|
|
• app/templates/shop/errors/401.html - Unauthorized
|
|
• app/templates/shop/errors/403.html - Forbidden
|
|
• app/templates/shop/errors/422.html - Validation Error
|
|
• app/templates/shop/errors/429.html - Rate Limited
|
|
• app/templates/shop/errors/500.html - Server Error
|
|
• app/templates/shop/errors/502.html - Bad Gateway
|
|
• app/templates/shop/errors/base.html - Base error template
|
|
• app/templates/shop/errors/generic.html - Generic error
|
|
|
|
Error Renderer (app/exceptions/error_renderer.py):
|
|
|
|
Calculates base_url dynamically based on store access method:
|
|
|
|
def _get_context_data(self, request: Request, ...):
|
|
store = getattr(request.state, 'store', None)
|
|
access_method = getattr(request.state, "access_method", None)
|
|
store_context = getattr(request.state, "store_context", None)
|
|
|
|
# Calculate base_url for shop links
|
|
base_url = "/"
|
|
if access_method == "path" and store:
|
|
full_prefix = store_context.get('full_prefix', '/store/')
|
|
base_url = f"{full_prefix}{store.subdomain}/"
|
|
|
|
return {
|
|
"request": request,
|
|
"store": store,
|
|
"base_url": base_url, # ⭐ Used in error templates
|
|
...
|
|
}
|
|
|
|
Error Template Usage:
|
|
|
|
All error page links use {{ base_url }} prefix for correct routing:
|
|
|
|
{# ❌ BAD - Hardcoded links #}
|
|
<a href="/">Continue Shopping</a>
|
|
<a href="/products">View Products</a>
|
|
|
|
{# ✅ GOOD - Context-aware links #}
|
|
<a href="{{ base_url or '/' }}">Continue Shopping</a>
|
|
<a href="{{ base_url or '/' }}products">View Products</a>
|
|
|
|
How It Works:
|
|
|
|
1. Error occurs (404, 500, etc.)
|
|
2. Exception handler detects shop context
|
|
3. error_renderer.py calculates base_url from store_context
|
|
4. Error template renders with correct base_url
|
|
5. Links work for all access methods:
|
|
- Domain: customshop.com → base_url = "/"
|
|
- Subdomain: orion.platform.com → base_url = "/"
|
|
- Path: localhost/stores/orion/ → base_url = "/stores/orion/"
|
|
|
|
Benefits:
|
|
✅ Error pages work correctly regardless of access method
|
|
✅ No broken links in error states
|
|
✅ Consistent user experience
|
|
✅ Store branding maintained in errors
|
|
|
|
|
|
🔒 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
|
|
══════════════════════════════════════════════════════════════════
|