╔══════════════════════════════════════════════════════════════════╗ ║ 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 + 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 │ │ ├── register.html │ │ ├── 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 (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/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="/vendors/{vendor_code}/shop", ...) # Path-based This means routes defined WITHOUT /shop prefix in shop_pages.py: @router.get("/products") → /shop/products OR /vendors/{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. Vendor 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 vendor/theme from middleware""" vendor = getattr(request.state, 'vendor', None) theme = getattr(request.state, 'theme', None) clean_path = getattr(request.state, 'clean_path', request.url.path) vendor_context = getattr(request.state, 'vendor_context', None) # Get detection method (domain, subdomain, or path) access_method = vendor_context.get('detection_method', 'unknown') if vendor_context else 'unknown' # Calculate base URL for links # - Domain/subdomain: base_url = "/" # - Path-based: base_url = "/vendor/{vendor_code}/" base_url = "/" if access_method == "path" and vendor: full_prefix = vendor_context.get('full_prefix', '/vendor/') base_url = f"{full_prefix}{vendor.subdomain}/" return { "request": request, "vendor": vendor, "theme": theme, "clean_path": clean_path, "access_method": access_method, "base_url": base_url, # ⭐ Used for all links in templates **extra_context } Responsibilities: ✅ Access vendor from middleware (request.state.vendor) ✅ 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 - Vendor has their own domain - base_url = "/" - Links: /shop/products, /shop/about, /shop/contact 2. **Subdomain** (Production) URL: https://wizamart.letzshop.com/shop/products - Vendor uses platform subdomain - base_url = "/" - Links: /shop/products, /shop/about, /shop/contact 3. **Path-Based** (Development/Testing) URL: http://localhost:8000/vendors/wizamart/shop/products - Vendor accessed via path prefix - base_url = "/vendors/wizamart/" - Links: /vendors/wizamart/shop/products, /vendors/wizamart/shop/about ⚠️ CRITICAL: All template links MUST use {{ base_url }}shop/ prefix Example: ❌ BAD: Products ❌ BAD: Products ✅ GOOD: Products Note: The router is mounted at /shop prefix in main.py, so all links need shop/ after base_url How It Works: 1. VendorContextMiddleware detects access method 2. Sets request.state.vendor_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: Vendor & Theme Identification Two middleware components work together: 1. Vendor Context Middleware (middleware/vendor_context.py) • Detects vendor from domain/subdomain/path • Sets request.state.vendor • Sets request.state.vendor_context (includes detection_method) • Sets request.state.clean_path (path without vendor prefix) • Returns 404 if vendor not found 2. Theme Context Middleware (middleware/theme_context.py) • Loads theme for detected vendor • Sets request.state.theme • Falls back to default theme Order matters: vendor_context_middleware → theme_context_middleware Detection Methods: - custom_domain: Vendor has custom domain - subdomain: Vendor uses platform subdomain - path: Vendor accessed via /vendor/{code}/ or /vendors/{code}/ 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 %}
Loading products...
{% 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/ ⚠️ 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: 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 #} {# 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 vendor context. NO vendor_id or vendor_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 Vendor Context Works: 1. Browser makes API call from shop page (e.g., /vendors/wizamart/shop/products) 2. Browser automatically sends Referer header: http://localhost:8000/vendors/wizamart/shop/products 3. VendorContextMiddleware extracts vendor from Referer header 4. Middleware sets request.state.vendor = 5. API endpoint accesses vendor: vendor = request.state.vendor 6. No vendor_id needed in URL! 🔄 DATA FLOW ═════════════════════════════════════════════════════════════════ Page Load Flow: ────────────────────────────────────────────────────────────────── 1. Customer → visits acme-shop.com (or /vendors/acme/shop/products) 2. Vendor Middleware → Identifies "ACME" vendor 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 vendor from Referer, injects into request.state 9. API → Returns product list JSON for ACME vendor 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 vendor from Referer 6. API → Creates order + payment intent for vendor 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 vendor has a theme record • Stores colors, fonts, logos, layout prefs • Custom CSS per vendor 2. CSS Variables Injection • base.html injects variables in