migrating vendor frontend to new architecture

This commit is contained in:
2025-10-30 19:11:51 +01:00
parent cd5097fc04
commit 9420483ae6
33 changed files with 8194 additions and 36 deletions

10
app/templates/vendor/base.html vendored Normal file
View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
</html>

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
</html>

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
</html>

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
</html>

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
</html>

View File

@@ -89,40 +89,5 @@ E:\FASTAPI-MULTITENANT-ECOMMERCE\STATIC
| api-client.js
| icons.js
| utils.js
|
+---shop
| | cart.html
| | checkout.html
| | home.html
| | product.html
| | products.html
| | search.html
| |
| \---account
| addresses.html
| login.html
| orders.html
| profile.html
| register.html
|
\---vendor
| dashboard.html
| login.html
|
\---admin
| customers.html
| inventory.html
| media.html
| notifications.html
| orders.html
| payments.html
| products.html
| settings.html
| teams.html
|
\---marketplace
browse.html
config.html
imports.html
selected.html

View File

@@ -0,0 +1,828 @@
╔══════════════════════════════════════════════════════════════════╗
║ ADMIN FRONTEND ARCHITECTURE OVERVIEW ║
║ Alpine.js + Jinja2 + Tailwind CSS ║
╚══════════════════════════════════════════════════════════════════╝
📦 WHAT IS THIS?
═════════════════════════════════════════════════════════════════
Admin frontend provides platform administrators with complete control
over the marketplace. Built with:
✅ Jinja2 Templates (server-side rendering)
✅ Alpine.js (client-side reactivity)
✅ Tailwind CSS (utility-first styling)
✅ Windmill Dashboard UI (dark mode ready)
✅ FastAPI (backend routes)
🎯 KEY PRINCIPLES
═════════════════════════════════════════════════════════════════
1. Minimal Server-Side Rendering
• Routes handle authentication + template rendering
• NO database queries in route handlers
• ALL data loaded client-side via JavaScript
2. Component-Based Architecture
• Base template with shared layout
• Reusable partials (header, sidebar, etc.)
• Page-specific templates extend base
3. Progressive Enhancement
• Works without JavaScript (basic HTML)
• JavaScript adds interactivity
• Graceful degradation
4. API-First Data Loading
• All data from REST APIs
• Client-side state management
• Real-time updates possible
5. Centralized State & Logging
• Base layout provides shared state
• Centralized logging system
• Consistent error handling
📁 FILE STRUCTURE
═════════════════════════════════════════════════════════════════
app/
├── templates/admin/
│ ├── base.html ← Base template (layout)
│ ├── login.html ← Admin login page
│ ├── dashboard.html ← Dashboard overview
│ ├── vendors.html ← Vendor management
│ ├── vendor-edit.html ← Single vendor edit
│ ├── users.html ← User management
│ ├── products.html ← Product management
│ ├── orders.html ← Order management
│ ├── import-jobs.html ← Import job tracking
│ ├── audit-logs.html ← Audit log viewer
│ ├── settings.html ← System settings
│ └── partials/ ← Reusable components
│ ├── header.html ← Top navigation
│ ├── sidebar.html ← Main navigation
│ ├── notifications.html ← Toast notifications
│ ├── modal.html ← Modal template
│ └── table.html ← Data table template
├── static/admin/
│ ├── css/
│ │ ├── tailwind.output.css ← Generated Tailwind
│ │ └── admin.css ← Custom admin styles
│ ├── js/
│ │ ├── init-alpine.js ← Base Alpine.js data
│ │ ├── dashboard.js ← Dashboard logic
│ │ ├── vendors.js ← Vendor management logic
│ │ ├── vendor-edit.js ← Vendor edit logic
│ │ ├── vendor-theme.js ← Theme customization
│ │ ├── users.js ← User management logic
│ │ ├── products.js ← Product management logic
│ │ ├── orders.js ← Order management logic
│ │ ├── import-jobs.js ← Import tracking logic
│ │ ├── audit-logs.js ← Audit log logic
│ │ └── settings.js ← Settings logic
│ └── img/
│ ├── login-office.jpeg
│ └── login-office-dark.jpeg
├── static/shared/ ← Shared across all areas
│ ├── js/
│ │ ├── log-config.js ← Centralized logging
│ │ ├── icons.js ← Icon registry
│ │ ├── utils.js ← Utility functions
│ │ └── api-client.js ← API wrapper
│ └── css/
│ └── base.css ← Global styles
└── api/v1/admin/
├── pages.py ← Route handlers (templates)
├── vendors.py ← Vendor API endpoints
├── users.py ← User API endpoints
├── products.py ← Product API endpoints
├── orders.py ← Order API endpoints
└── settings.py ← Settings API endpoints
🏗️ ARCHITECTURE LAYERS
═════════════════════════════════════════════════════════════════
Layer 1: Routes (FastAPI)
Layer 2: Templates (Jinja2)
Layer 3: JavaScript (Alpine.js)
Layer 4: API (REST endpoints)
Layer 5: Database
Layer 1: ROUTES (FastAPI)
──────────────────────────────────────────────────────────────────
Purpose: Authentication + Template Rendering
Location: app/api/v1/admin/pages.py
Example:
@router.get("/admin/dashboard")
async def admin_dashboard_page(
request: Request,
current_user: User = Depends(get_current_admin_user)
):
return templates.TemplateResponse(
"admin/dashboard.html",
{
"request": request,
"user": current_user,
}
)
Responsibilities:
✅ Verify authentication (admin role required)
✅ Extract route parameters
✅ Render template with minimal context
❌ NO database queries
❌ NO business logic
Layer 2: TEMPLATES (Jinja2)
──────────────────────────────────────────────────────────────────
Purpose: HTML Structure + Server-Side Data
Location: app/templates/admin/
Template Hierarchy:
base.html (layout + shared components)
dashboard.html (page content)
partials/sidebar.html (navigation)
Example:
{% extends "admin/base.html" %}
{% block title %}Dashboard{% endblock %}
{% block alpine_data %}adminDashboard(){% endblock %}
{% block content %}
<div x-show="loading">Loading...</div>
<div x-show="!loading">
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
<template x-for="stat in stats" :key="stat.label">
<!-- Stats card -->
</template>
</div>
</div>
{% endblock %}
Key Features:
✅ Template inheritance
✅ Server-side variables (user info)
✅ Include partials
✅ Block overrides
✅ Alpine.js integration
Layer 3: JAVASCRIPT (Alpine.js)
──────────────────────────────────────────────────────────────────
Purpose: Client-Side Interactivity + Data Loading
Location: app/static/admin/js/
Component Structure:
function adminDashboard() {
return {
// ✅ CRITICAL: Inherit base layout state
...data(),
// ✅ CRITICAL: Set page identifier
currentPage: 'dashboard',
// Page-specific state
loading: false,
error: null,
stats: [],
// Initialization
async init() {
// Guard against multiple initialization
if (window._dashboardInitialized) {
dashLog.warn('Already initialized');
return;
}
window._dashboardInitialized = true;
await this.loadStats();
},
// Data loading
async loadStats() {
this.loading = true;
try {
this.stats = await apiClient.get(
'/api/v1/admin/stats'
);
} catch (error) {
this.error = error.message;
} finally {
this.loading = false;
}
}
};
}
Responsibilities:
✅ Load data from API
✅ Manage UI state
✅ Handle user interactions
✅ Update DOM reactively
✅ Inherit base layout functionality
Layer 4: API (REST)
──────────────────────────────────────────────────────────────────
Purpose: Business Logic + Data Access
Location: app/api/v1/admin/*.py (not pages.py)
Example Endpoints:
GET /api/v1/admin/stats
GET /api/v1/admin/vendors
POST /api/v1/admin/vendors
PUT /api/v1/admin/vendors/{id}
DELETE /api/v1/admin/vendors/{id}
GET /api/v1/admin/users
GET /api/v1/admin/audit-logs
🔄 DATA FLOW
═════════════════════════════════════════════════════════════════
Page Load Flow:
──────────────────────────────────────────────────────────────────
1. Admin → GET /admin/dashboard
2. FastAPI → Check authentication (admin role)
3. FastAPI → Render template with minimal context
4. Browser → Load HTML + CSS + JS
5. Alpine.js → init() executes
6. JavaScript → Check initialization guard
7. JavaScript → API call for dashboard stats
8. API → Return JSON data
9. Alpine.js → Update reactive state
10. Browser → DOM updates automatically
User Interaction Flow:
──────────────────────────────────────────────────────────────────
1. Admin → Click "Delete Vendor"
2. Alpine.js → Show confirmation dialog
3. Admin → Confirms deletion
4. Alpine.js → DELETE to API
5. API → Delete vendor + return success
6. Alpine.js → Update local state (remove from list)
7. Browser → DOM updates automatically
8. Alpine.js → Show success toast notification
💡 BASE LAYOUT INHERITANCE
═════════════════════════════════════════════════════════════════
The ...data() Spread:
──────────────────────────────────────────────────────────────────
Every page component MUST start with ...data() to inherit
base layout functionality:
init-alpine.js provides:
function data() {
return {
// Theme state
dark: localStorage.getItem('theme') === 'dark',
toggleTheme() { /* ... */ },
// Side menu state
isSideMenuOpen: false,
toggleSideMenu() { /* ... */ },
closeSideMenu() { /* ... */ },
// Profile menu state
isProfileMenuOpen: false,
toggleProfileMenu() { /* ... */ },
closeProfileMenu() { /* ... */ },
// Notifications menu state
isNotificationsMenuOpen: false,
toggleNotificationsMenu() { /* ... */ },
closeNotificationsMenu() { /* ... */ },
// Page identifier (override in each page)
currentPage: ''
};
}
Your page inherits ALL of this:
function adminVendors() {
return {
...data(), // ← Spreads all base functionality
currentPage: 'vendors', // ← Override page identifier
// Your page-specific state
vendors: [],
loading: false
};
}
Benefits:
✅ Automatic dark mode support
✅ Menu states work automatically
✅ No duplicate code
✅ Consistent behavior across pages
🎨 STYLING SYSTEM
═════════════════════════════════════════════════════════════════
Tailwind CSS Utility Classes:
• Responsive: sm:, md:, lg:, xl:
• Dark mode: dark:bg-gray-800
• Hover: hover:bg-purple-700
• Focus: focus:outline-none
• Transitions: transition-colors duration-150
Custom CSS Variables (admin/css/admin.css):
--color-primary: #7c3aed (purple-600)
--color-accent: #ec4899 (pink-500)
--color-success: #10b981 (green-500)
--color-warning: #f59e0b (yellow-500)
--color-danger: #ef4444 (red-500)
Windmill Dashboard Theme:
• Professional admin UI
• Dark mode ready
• Consistent components
• Accessible by default
🔐 AUTHENTICATION
═════════════════════════════════════════════════════════════════
Auth Flow:
1. Login → POST /api/v1/admin/auth/login
2. API → Verify credentials + check admin role
3. API → Return JWT token
4. JavaScript → Store in localStorage
5. API Client → Add to all requests
6. Routes → Verify with get_current_admin_user
Protected Routes:
• All /admin/* routes
• Require valid JWT token
• Require admin role (is_admin=True)
• Redirect to login if unauthorized
Public Routes:
• /admin/login
• No authentication required
Role-Based Access:
• Admin: Full platform access
• Vendor: Limited to own shop (vendor portal)
• Customer: No admin access
📱 RESPONSIVE DESIGN
═════════════════════════════════════════════════════════════════
Breakpoints (Tailwind):
• sm: 640px (mobile landscape)
• md: 768px (tablet)
• lg: 1024px (desktop)
• xl: 1280px (large desktop)
Mobile-First Approach:
• Base styles for mobile
• Add complexity for larger screens
• Collapsible sidebar on mobile
• Stack cards vertically on mobile
Example:
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4">
<!-- 1 column mobile, 2 tablet, 4 desktop -->
</div>
🌙 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: persist preference
Toggle Flow:
1. User clicks dark mode button
2. toggleTheme() called (from base data)
3. dark state toggled
4. Saved to localStorage
5. HTML class updates
6. Tailwind dark: variants activate
Example Usage:
<div class="bg-white dark:bg-gray-800">
<p class="text-gray-900 dark:text-white">Content</p>
</div>
🔒 INITIALIZATION GUARDS
═════════════════════════════════════════════════════════════════
Purpose: Prevent Multiple Initialization
Alpine.js can sometimes initialize components multiple times.
Use a guard to prevent duplicate API calls and setup:
Pattern:
async init() {
// Check if already initialized
if (window._dashboardInitialized) {
dashLog.warn('Already initialized, skipping...');
return; // Exit early
}
// Set flag BEFORE async operations
window._dashboardInitialized = true;
// Safe to proceed
await this.loadData();
}
Naming Convention:
• Dashboard: window._dashboardInitialized
• Vendors: window._vendorsInitialized
• Products: window._productsInitialized
• etc.
Benefits:
✅ Prevents duplicate API calls
✅ Prevents duplicate event listeners
✅ Improves performance
✅ Avoids state conflicts
🪵 CENTRALIZED LOGGING
═════════════════════════════════════════════════════════════════
Location: app/static/shared/js/log-config.js
Pre-configured Loggers:
window.LogConfig.loggers.dashboard
window.LogConfig.loggers.vendors
window.LogConfig.loggers.vendorTheme
window.LogConfig.loggers.users
window.LogConfig.loggers.products
window.LogConfig.loggers.orders
window.LogConfig.loggers.imports
window.LogConfig.loggers.audit
Usage:
// Use pre-configured logger
const dashLog = window.LogConfig.loggers.dashboard;
dashLog.info('Dashboard loading...');
dashLog.error('Failed to load stats', error);
dashLog.debug('Stats data:', statsData);
dashLog.warn('API response slow');
Advanced Features:
// Grouped logs
dashLog.group('Loading Dashboard Data');
dashLog.info('Fetching stats...');
dashLog.info('Fetching activity...');
dashLog.groupEnd();
// API call logging
window.LogConfig.logApiCall('GET', url, data, 'response');
// Performance logging
window.LogConfig.logPerformance('Load Stats', duration);
// Error logging
window.LogConfig.logError(error, 'Load Stats');
Benefits:
✅ One line instead of 15+ lines per file
✅ Consistent logging format
✅ Environment-aware (dev/prod)
✅ Frontend-aware (admin/vendor/shop)
✅ Advanced features (groups, perf, API)
📡 API CLIENT
═════════════════════════════════════════════════════════════════
Location: app/static/shared/js/api-client.js
CRITICAL: Always use lowercase 'apiClient'
✅ apiClient.get()
❌ ApiClient.get()
❌ API_CLIENT.get()
Usage:
const data = await apiClient.get('/api/v1/admin/vendors');
await apiClient.post('/api/v1/admin/vendors', {
name: 'New Vendor',
code: 'NEWVENDOR'
});
await apiClient.put('/api/v1/admin/vendors/123', {
name: 'Updated Name'
});
await apiClient.delete('/api/v1/admin/vendors/123');
Features:
✅ Automatic auth headers
✅ Error handling
✅ JSON parsing
✅ Request/response logging
🎭 ICONS
═════════════════════════════════════════════════════════════════
Location: app/static/shared/js/icons.js
Usage:
<span x-html="$icon('home', 'w-5 h-5')"></span>
<span x-html="$icon('users', 'w-4 h-4 text-blue-500')"></span>
Available Icons:
• home, dashboard, settings
• user, users, user-group
• shopping-bag, shopping-cart
• cube, download, upload
• plus, minus, x
• pencil, trash, eye
• check, exclamation
• chevron-left, chevron-right
• spinner (for loading)
🚀 PERFORMANCE
═════════════════════════════════════════════════════════════════
Optimization Techniques:
1. Template Caching
• Base template cached by FastAPI
• Reduces rendering time
2. Lazy Loading
• Data loaded after page render
• Progressive content display
3. Debouncing
• Search inputs debounced
• Reduces API calls
4. Pagination
• Server-side pagination
• Load only needed data
5. CDN Assets
• Tailwind CSS from CDN
• Alpine.js from CDN
6. Initialization Guards
• Prevent duplicate setups
• Reduce unnecessary operations
📊 PAGE-BY-PAGE BREAKDOWN
═════════════════════════════════════════════════════════════════
/admin/dashboard
──────────────────────────────────────────────────────────────────
Purpose: Overview of platform operations
Components:
• Stats cards (vendors, users, orders, revenue)
• Recent activity feed
• Quick actions panel
• System health indicators
Data Sources:
• GET /api/v1/admin/stats
• GET /api/v1/admin/recent-activity
/admin/vendors
──────────────────────────────────────────────────────────────────
Purpose: Manage marketplace vendors
Components:
• Vendor list table
• Search and filters
• Create/Edit modal
• Status management
• Verification controls
Data Sources:
• GET /api/v1/admin/vendors
• POST /api/v1/admin/vendors
• PUT /api/v1/admin/vendors/{id}
• DELETE /api/v1/admin/vendors/{id}
/admin/vendors/{code}/edit
──────────────────────────────────────────────────────────────────
Purpose: Edit single vendor details
Components:
• Vendor information form
• Status controls
• Contact information
• Business details
Data Sources:
• GET /api/v1/admin/vendors/{code}
• PUT /api/v1/admin/vendors/{code}
/admin/vendors/{code}/theme
──────────────────────────────────────────────────────────────────
Purpose: Customize vendor's shop theme
Components:
• Color picker
• Font selector
• Logo uploader
• Layout options
• Custom CSS editor
• Theme presets
Data Sources:
• GET /api/v1/admin/vendor-themes/{code}
• PUT /api/v1/admin/vendor-themes/{code}
/admin/users
──────────────────────────────────────────────────────────────────
Purpose: Manage platform users
Components:
• User list table
• Search and filters
• Role management
• Status controls
Data Sources:
• GET /api/v1/admin/users
• PUT /api/v1/admin/users/{id}
• DELETE /api/v1/admin/users/{id}
/admin/products
──────────────────────────────────────────────────────────────────
Purpose: View all marketplace products
Components:
• Product list table
• Search and filters
• Vendor filter
• Bulk actions
Data Sources:
• GET /api/v1/admin/products
/admin/orders
──────────────────────────────────────────────────────────────────
Purpose: View all marketplace orders
Components:
• Order list table
• Status filters
• Vendor filter
• Order detail modal
Data Sources:
• GET /api/v1/admin/orders
• GET /api/v1/admin/orders/{id}
/admin/import-jobs
──────────────────────────────────────────────────────────────────
Purpose: Monitor marketplace import operations
Components:
• Import job list
• Status indicators
• Progress tracking
• Error logs
Data Sources:
• GET /api/v1/admin/import-jobs
• GET /api/v1/admin/import-jobs/{id}
/admin/audit-logs
──────────────────────────────────────────────────────────────────
Purpose: Track all system actions
Components:
• Audit log table
• Filters (user, action, date)
• Export functionality
Data Sources:
• GET /api/v1/admin/audit-logs
/admin/settings
──────────────────────────────────────────────────────────────────
Purpose: Configure platform settings
Components:
• Settings tabs
• Configuration forms
• Feature toggles
Data Sources:
• GET /api/v1/admin/settings
• PUT /api/v1/admin/settings
🎓 LEARNING PATH
═════════════════════════════════════════════════════════════════
For New Developers:
1. Understand Architecture (1 hour)
→ Read this document
→ Review file structure
→ Examine base template
→ Understand ...data() inheritance
2. Study Existing Page (2 hours)
→ Open dashboard.html
→ Open dashboard.js
→ Trace data flow
→ Understand init guard pattern
3. Create Simple Page (4 hours)
→ Copy templates from dashboard
→ Modify for new feature
→ Test initialization guard
→ Verify dark mode works
4. Add Complex Feature (1 day)
→ Forms with validation
→ Modal dialogs
→ API integration
→ Error handling
5. Master Patterns (1 week)
→ All common patterns
→ Centralized logging
→ Performance optimization
→ Best practices
🔄 DEPLOYMENT CHECKLIST
═════════════════════════════════════════════════════════════════
Before Deploying:
□ Build Tailwind CSS
□ Minify JavaScript
□ Test all routes
□ Verify authentication
□ Check role-based access
□ Verify initialization guards work
□ Check mobile responsive
□ Test dark mode
□ Validate API endpoints
□ Review error handling
□ Test logging in production mode
□ Check console for errors
□ Verify no duplicate initializations
🔒 SECURITY
═════════════════════════════════════════════════════════════════
Best Practices:
1. Authentication
✅ JWT tokens
✅ Token expiration
✅ Secure storage (httpOnly cookies option)
2. Authorization
✅ Route-level checks (admin role required)
✅ API-level validation
✅ Role-based permissions
3. Input Validation
✅ Client-side validation
✅ Server-side validation
✅ XSS prevention
4. CSRF Protection
✅ Token-based
✅ SameSite cookies
5. Admin-Specific
✅ Audit logging for all actions
✅ Strong password requirements
✅ Two-factor authentication (optional)
📚 REFERENCE LINKS
═════════════════════════════════════════════════════════════════
Documentation:
• Alpine.js: https://alpinejs.dev/
• Tailwind CSS: https://tailwindcss.com/
• Jinja2: https://jinja.palletsprojects.com/
• FastAPI: https://fastapi.tiangolo.com/
• Windmill Dashboard: https://windmill-dashboard.vercel.app/
Internal Docs:
• Page Template Guide: FRONTEND_ADMIN_ALPINE_PAGE_TEMPLATE.md
• API Documentation: API_REFERENCE.md
• Database Schema: DATABASE_SCHEMA.md
══════════════════════════════════════════════════════════════════
ADMIN FRONTEND ARCHITECTURE
Powerful, Secure, and Maintainable Platform Control
══════════════════════════════════════════════════════════════════

View 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.

View 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
══════════════════════════════════════════════════════════════════

View File

@@ -0,0 +1,837 @@
# Vendor Admin Frontend - Alpine.js/Jinja2 Page Template Guide
## 📋 Overview
This guide provides complete templates for creating new vendor admin pages using the established Alpine.js + Jinja2 architecture. Follow these patterns to ensure consistency across the vendor portal.
---
## 🎯 Quick Reference
### File Structure for New Page
```
app/
├── templates/vendor/admin/
│ └── [page-name].html # Jinja2 template
├── static/vendor/js/
│ └── [page-name].js # Alpine.js component
└── api/v1/vendor/
└── pages.py # Route registration
```
### Checklist for New Page
- [ ] Create Jinja2 template extending base.html
- [ ] Create Alpine.js JavaScript component
- [ ] Register route in pages.py
- [ ] Add navigation link to sidebar.html
- [ ] Test authentication
- [ ] Test data loading
- [ ] Test responsive design
---
## 📄 Template Structure
### 1. Jinja2 Template
**File:** `app/templates/vendor/admin/[page-name].html`
```jinja2
{# app/templates/vendor/admin/[page-name].html #}
{% extends "vendor/base.html" %}
{# Page title for browser tab #}
{% block title %}[Page Name]{% endblock %}
{# Alpine.js component name #}
{% block alpine_data %}vendor[PageName](){% endblock %}
{# Page content #}
{% block content %}
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- PAGE HEADER -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div class="flex items-center justify-between my-6">
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">
[Page Name]
</h2>
<!-- Action Buttons -->
<div class="flex items-center space-x-3">
<button
@click="refresh()"
:disabled="loading"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50"
>
<span x-show="!loading" x-html="$icon('refresh', 'w-4 h-4 mr-2')"></span>
<span x-show="loading" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
<span x-text="loading ? 'Loading...' : 'Refresh'"></span>
</button>
<button
@click="openCreateModal()"
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple"
>
<span x-html="$icon('plus', 'w-4 h-4 mr-2')"></span>
<span>Add New</span>
</button>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- LOADING STATE -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div x-show="loading" class="text-center py-12">
<span x-html="$icon('spinner', 'inline w-8 h-8 text-purple-600')"></span>
<p class="mt-2 text-gray-600 dark:text-gray-400">Loading data...</p>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- ERROR STATE -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div x-show="error && !loading"
class="mb-6 p-4 bg-red-100 border border-red-400 text-red-700 rounded-lg flex items-start">
<span x-html="$icon('exclamation', 'w-5 h-5 mr-3 mt-0.5 flex-shrink-0')"></span>
<div>
<p class="font-semibold">Error</p>
<p class="text-sm" x-text="error"></p>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- FILTERS & SEARCH -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div x-show="!loading" class="mb-6 bg-white rounded-lg shadow-xs dark:bg-gray-800 p-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<!-- Search -->
<div>
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400">Search</span>
<input
x-model="filters.search"
@input.debounce.300ms="applyFilters()"
class="block w-full mt-1 text-sm dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:text-gray-300 dark:focus:shadow-outline-gray form-input"
placeholder="Search..."
/>
</label>
</div>
<!-- Status Filter -->
<div>
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400">Status</span>
<select
x-model="filters.status"
@change="applyFilters()"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 form-select focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray"
>
<option value="">All</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</label>
</div>
<!-- Sort -->
<div>
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400">Sort By</span>
<select
x-model="filters.sortBy"
@change="applyFilters()"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 form-select focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray"
>
<option value="created_at:desc">Newest First</option>
<option value="created_at:asc">Oldest First</option>
<option value="name:asc">Name (A-Z)</option>
<option value="name:desc">Name (Z-A)</option>
</select>
</label>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- DATA TABLE -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div x-show="!loading" class="w-full overflow-hidden rounded-lg shadow-xs">
<div class="w-full overflow-x-auto">
<table class="w-full whitespace-no-wrap">
<thead>
<tr class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50 dark:text-gray-400 dark:bg-gray-800">
<th class="px-4 py-3">Name</th>
<th class="px-4 py-3">Status</th>
<th class="px-4 py-3">Date</th>
<th class="px-4 py-3">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
<!-- Empty State -->
<template x-if="items.length === 0">
<tr>
<td colspan="4" class="px-4 py-8 text-center text-gray-600 dark:text-gray-400">
<div class="flex flex-col items-center">
<span x-html="$icon('inbox', 'w-12 h-12 mb-2 text-gray-300')"></span>
<p>No items found.</p>
</div>
</td>
</tr>
</template>
<!-- Data Rows -->
<template x-for="item in items" :key="item.id">
<tr class="text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
<td class="px-4 py-3">
<div class="flex items-center text-sm">
<div>
<p class="font-semibold" x-text="item.name"></p>
<p class="text-xs text-gray-600 dark:text-gray-400"
x-text="item.description"></p>
</div>
</div>
</td>
<td class="px-4 py-3 text-xs">
<span class="px-2 py-1 font-semibold leading-tight rounded-full"
:class="item.status === 'active'
? 'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100'
: 'text-gray-700 bg-gray-100 dark:text-gray-100 dark:bg-gray-700'"
x-text="item.status"></span>
</td>
<td class="px-4 py-3 text-sm" x-text="formatDate(item.created_at)">
</td>
<td class="px-4 py-3">
<div class="flex items-center space-x-2 text-sm">
<button
@click="viewItem(item.id)"
class="flex items-center justify-center p-2 text-purple-600 rounded-lg hover:bg-purple-50 dark:text-gray-400 dark:hover:bg-gray-700"
title="View"
>
<span x-html="$icon('eye', 'w-5 h-5')"></span>
</button>
<button
@click="editItem(item.id)"
class="flex items-center justify-center p-2 text-blue-600 rounded-lg hover:bg-blue-50 dark:text-gray-400 dark:hover:bg-gray-700"
title="Edit"
>
<span x-html="$icon('pencil', 'w-5 h-5')"></span>
</button>
<button
@click="deleteItem(item.id)"
class="flex items-center justify-center p-2 text-red-600 rounded-lg hover:bg-red-50 dark:text-gray-400 dark:hover:bg-gray-700"
title="Delete"
>
<span x-html="$icon('trash', 'w-5 h-5')"></span>
</button>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="px-4 py-3 text-xs font-semibold tracking-wide text-gray-500 uppercase border-t dark:border-gray-700 bg-gray-50 sm:grid-cols-9 dark:text-gray-400 dark:bg-gray-800">
<span class="flex items-center col-span-3">
Showing <span class="mx-1 font-semibold" x-text="pagination.from"></span>-<span class="mx-1 font-semibold" x-text="pagination.to"></span> of <span class="mx-1 font-semibold" x-text="pagination.total"></span>
</span>
<span class="col-span-2"></span>
<span class="flex col-span-4 mt-2 sm:mt-auto sm:justify-end">
<nav aria-label="Table navigation">
<ul class="inline-flex items-center">
<li>
<button
@click="previousPage()"
:disabled="!pagination.hasPrevious"
class="px-3 py-1 rounded-md rounded-l-lg focus:outline-none focus:shadow-outline-purple"
:class="pagination.hasPrevious ? 'hover:bg-gray-100 dark:hover:bg-gray-700' : 'opacity-50 cursor-not-allowed'"
>
<span x-html="$icon('chevron-left', 'w-4 h-4')"></span>
</button>
</li>
<li>
<span class="px-3 py-1" x-text="`Page ${pagination.currentPage} of ${pagination.totalPages}`"></span>
</li>
<li>
<button
@click="nextPage()"
:disabled="!pagination.hasNext"
class="px-3 py-1 rounded-md rounded-r-lg focus:outline-none focus:shadow-outline-purple"
:class="pagination.hasNext ? 'hover:bg-gray-100 dark:hover:bg-gray-700' : 'opacity-50 cursor-not-allowed'"
>
<span x-html="$icon('chevron-right', 'w-4 h-4')"></span>
</button>
</li>
</ul>
</nav>
</span>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- MODALS (if needed) -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- Create/Edit Modal -->
<div x-show="showModal"
x-cloak
class="fixed inset-0 z-50 flex items-center justify-center overflow-auto bg-black bg-opacity-50"
@click.self="closeModal()">
<div class="relative w-full max-w-lg p-6 mx-auto bg-white rounded-lg shadow-xl dark:bg-gray-800">
<!-- Modal Header -->
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200"
x-text="modalTitle"></h3>
<button @click="closeModal()"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
<span x-html="$icon('x', 'w-6 h-6')"></span>
</button>
</div>
<!-- Modal Body -->
<form @submit.prevent="saveItem()">
<div class="space-y-4">
<!-- Form fields here -->
<div>
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400">Name</span>
<input
x-model="formData.name"
type="text"
class="block w-full mt-1 text-sm dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:text-gray-300 dark:focus:shadow-outline-gray form-input"
required
/>
</label>
</div>
</div>
<!-- Modal Footer -->
<div class="flex justify-end mt-6 space-x-3">
<button
type="button"
@click="closeModal()"
class="px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition-colors duration-150 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 focus:outline-none focus:shadow-outline-gray dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600"
>
Cancel
</button>
<button
type="submit"
:disabled="saving"
class="px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50"
>
<span x-show="!saving">Save</span>
<span x-show="saving">Saving...</span>
</button>
</div>
</form>
</div>
</div>
{% endblock %}
{# Page-specific JavaScript #}
{% block extra_scripts %}
<script src="{{ url_for('static', path='vendor/js/[page-name].js') }}"></script>
{% endblock %}
```
---
### 2. Alpine.js Component
**File:** `app/static/vendor/js/[page-name].js`
```javascript
// app/static/vendor/js/[page-name].js
/**
* [Page Name] page logic
* Handles data loading, filtering, CRUD operations
*/
function vendor[PageName]() {
return {
// ═══════════════════════════════════════════════════════════
// STATE
// ═══════════════════════════════════════════════════════════
loading: false,
error: '',
items: [],
// Filters
filters: {
search: '',
status: '',
sortBy: 'created_at:desc'
},
// Pagination
pagination: {
currentPage: 1,
perPage: 10,
total: 0,
totalPages: 0,
from: 0,
to: 0,
hasPrevious: false,
hasNext: false
},
// Modal state
showModal: false,
modalTitle: '',
modalMode: 'create', // 'create' or 'edit'
formData: {},
saving: false,
// ═══════════════════════════════════════════════════════════
// LIFECYCLE
// ═══════════════════════════════════════════════════════════
async init() {
logInfo('[PageName] page initializing...');
await this.loadData();
logInfo('[PageName] page initialized');
},
// ═══════════════════════════════════════════════════════════
// DATA LOADING
// ═══════════════════════════════════════════════════════════
async loadData() {
this.loading = true;
this.error = '';
try {
// Build query params
const params = new URLSearchParams({
page: this.pagination.currentPage,
per_page: this.pagination.perPage,
...this.filters
});
// API call
const response = await apiClient.get(
`/api/v1/vendors/${this.vendorCode}/[endpoint]?${params}`
);
// Update state
this.items = response.items || [];
this.updatePagination(response);
logInfo('[PageName] data loaded', {
items: this.items.length,
total: this.pagination.total
});
} catch (error) {
logError('Failed to load [page] data', error);
this.error = error.message || 'Failed to load data';
} finally {
this.loading = false;
}
},
async refresh() {
await this.loadData();
},
updatePagination(response) {
this.pagination = {
currentPage: response.page || 1,
perPage: response.per_page || 10,
total: response.total || 0,
totalPages: response.pages || 0,
from: ((response.page - 1) * response.per_page) + 1,
to: Math.min(response.page * response.per_page, response.total),
hasPrevious: response.page > 1,
hasNext: response.page < response.pages
};
},
// ═══════════════════════════════════════════════════════════
// FILTERING & PAGINATION
// ═══════════════════════════════════════════════════════════
async applyFilters() {
this.pagination.currentPage = 1; // Reset to first page
await this.loadData();
},
async previousPage() {
if (this.pagination.hasPrevious) {
this.pagination.currentPage--;
await this.loadData();
}
},
async nextPage() {
if (this.pagination.hasNext) {
this.pagination.currentPage++;
await this.loadData();
}
},
// ═══════════════════════════════════════════════════════════
// CRUD OPERATIONS
// ═══════════════════════════════════════════════════════════
openCreateModal() {
this.modalMode = 'create';
this.modalTitle = 'Create New Item';
this.formData = {
name: '',
description: '',
status: 'active'
};
this.showModal = true;
},
async viewItem(id) {
// Navigate to detail page or open view modal
window.location.href = `/vendor/${this.vendorCode}/[endpoint]/${id}`;
},
async editItem(id) {
try {
// Load item data
const item = await apiClient.get(
`/api/v1/vendors/${this.vendorCode}/[endpoint]/${id}`
);
this.modalMode = 'edit';
this.modalTitle = 'Edit Item';
this.formData = { ...item };
this.showModal = true;
} catch (error) {
logError('Failed to load item', error);
alert('Failed to load item details');
}
},
async saveItem() {
this.saving = true;
try {
if (this.modalMode === 'create') {
await apiClient.post(
`/api/v1/vendors/${this.vendorCode}/[endpoint]`,
this.formData
);
logInfo('Item created successfully');
} else {
await apiClient.put(
`/api/v1/vendors/${this.vendorCode}/[endpoint]/${this.formData.id}`,
this.formData
);
logInfo('Item updated successfully');
}
this.closeModal();
await this.loadData();
} catch (error) {
logError('Failed to save item', error);
alert(error.message || 'Failed to save item');
} finally {
this.saving = false;
}
},
async deleteItem(id) {
if (!confirm('Are you sure you want to delete this item?')) {
return;
}
try {
await apiClient.delete(
`/api/v1/vendors/${this.vendorCode}/[endpoint]/${id}`
);
logInfo('Item deleted successfully');
await this.loadData();
} catch (error) {
logError('Failed to delete item', error);
alert(error.message || 'Failed to delete item');
}
},
closeModal() {
this.showModal = false;
this.formData = {};
},
// ═══════════════════════════════════════════════════════════
// UTILITIES
// ═══════════════════════════════════════════════════════════
formatDate(dateString) {
if (!dateString) return '-';
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
},
formatCurrency(amount) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR'
}).format(amount || 0);
}
};
}
// Make available globally
window.vendor[PageName] = vendor[PageName];
```
---
### 3. Route Registration
**File:** `app/api/v1/vendor/pages.py`
```python
@router.get("/vendor/{vendor_code}/[page-name]", response_class=HTMLResponse, include_in_schema=False)
async def vendor_[page_name]_page(
request: Request,
vendor_code: str = Path(..., description="Vendor code"),
current_user: User = Depends(get_current_vendor_user)
):
"""
Render [page name] page.
JavaScript loads data via API.
"""
return templates.TemplateResponse(
"vendor/admin/[page-name].html",
{
"request": request,
"user": current_user,
"vendor_code": vendor_code,
}
)
```
---
### 4. Sidebar Navigation
**File:** `app/templates/vendor/partials/sidebar.html`
```jinja2
<li class="relative px-6 py-3">
<span x-show="currentPage === '[page-name]'"
class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg"
aria-hidden="true"></span>
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
:class="currentPage === '[page-name]' ? 'text-gray-800 dark:text-gray-100' : ''"
:href="`/vendor/${vendorCode}/[page-name]`">
<span x-html="$icon('[icon-name]', 'w-5 h-5')"></span>
<span class="ml-4">[Page Display Name]</span>
</a>
</li>
```
---
## 🎨 Common Patterns
### Pattern 1: Simple Data List
Use for: Product list, order list, customer list
```javascript
async init() {
await this.loadData();
}
async loadData() {
this.loading = true;
try {
const response = await apiClient.get(`/api/v1/vendors/${this.vendorCode}/items`);
this.items = response.items || [];
} catch (error) {
this.error = error.message;
} finally {
this.loading = false;
}
}
```
### Pattern 2: Dashboard with Stats
Use for: Dashboard, analytics pages
```javascript
async init() {
await Promise.all([
this.loadStats(),
this.loadRecentActivity()
]);
}
async loadStats() {
const stats = await apiClient.get(`/api/v1/vendors/${this.vendorCode}/stats`);
this.stats = stats;
}
```
### Pattern 3: Detail Page
Use for: Product detail, order detail
```javascript
async init() {
await this.loadItem();
}
async loadItem() {
const id = this.getItemIdFromUrl();
this.item = await apiClient.get(`/api/v1/vendors/${this.vendorCode}/items/${id}`);
}
```
### Pattern 4: Form with Validation
Use for: Settings, profile edit
```javascript
formData: {
name: '',
email: ''
},
errors: {},
validateForm() {
this.errors = {};
if (!this.formData.name) this.errors.name = 'Name is required';
if (!this.formData.email) this.errors.email = 'Email is required';
return Object.keys(this.errors).length === 0;
},
async saveForm() {
if (!this.validateForm()) return;
await apiClient.put(`/api/v1/vendors/${this.vendorCode}/settings`, this.formData);
}
```
---
## 🔧 Best Practices
### 1. Error Handling
```javascript
try {
await apiClient.get('/endpoint');
} catch (error) {
logError('Operation failed', error);
this.error = error.message || 'An error occurred';
// Don't throw - let UI handle gracefully
}
```
### 2. Loading States
```javascript
// Always set loading at start and end
this.loading = true;
try {
// ... operations
} finally {
this.loading = false; // Always executes
}
```
### 3. Data Refresh
```javascript
async refresh() {
// Clear error before refresh
this.error = '';
await this.loadData();
}
```
### 4. Modal Management
```javascript
openModal() {
this.showModal = true;
// Reset form
this.formData = {};
this.errors = {};
}
closeModal() {
this.showModal = false;
// Clean up
this.formData = {};
}
```
### 5. Debouncing
```html
<!-- Debounce search input -->
<input
x-model="filters.search"
@input.debounce.300ms="applyFilters()"
/>
```
---
## 📱 Responsive Design Checklist
- [ ] Table scrolls horizontally on mobile
- [ ] Modal is scrollable on small screens
- [ ] Filters stack vertically on mobile
- [ ] Action buttons adapt to screen size
- [ ] Text truncates appropriately
- [ ] Icons remain visible
---
## ✅ Testing Checklist
- [ ] 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
- [ ] Create operation works
- [ ] Edit operation works
- [ ] Delete operation works
- [ ] Modal opens/closes
- [ ] Form validation works
- [ ] Dark mode works
- [ ] Mobile responsive
---
## 🚀 Quick Start Commands
```bash
# Create new page files
touch app/templates/vendor/admin/products.html
touch app/static/vendor/js/products.js
# Copy templates
cp template.html app/templates/vendor/admin/products.html
cp template.js app/static/vendor/js/products.js
# Update files with your page name
# Register route in pages.py
# Add sidebar link
# Test!
```
---
## 📚 Additional Resources
- **Icons**: Use `$icon('icon-name', 'classes')` helper
- **API Client**: Automatically handles auth tokens
- **Logging**: Use logInfo, logError, logDebug
- **Date Formatting**: Use formatDate() helper
- **Currency**: Use formatCurrency() helper
---
This template provides a complete, production-ready pattern for building vendor admin pages with consistent structure, error handling, and user experience.

View File

@@ -0,0 +1,695 @@
╔══════════════════════════════════════════════════════════════════╗
║ VENDOR ADMIN FRONTEND ARCHITECTURE OVERVIEW ║
║ Alpine.js + Jinja2 + Tailwind CSS ║
╚══════════════════════════════════════════════════════════════════╝
📦 WHAT IS THIS?
═════════════════════════════════════════════════════════════════
Vendor admin frontend provides vendors with a complete management
interface for their store. Built with:
✅ Jinja2 Templates (server-side rendering)
✅ Alpine.js (client-side reactivity)
✅ Tailwind CSS (utility-first styling)
✅ FastAPI (backend routes)
🎯 KEY PRINCIPLES
═════════════════════════════════════════════════════════════════
1. Minimal Server-Side Rendering
• Routes handle authentication + template rendering
• NO database queries in route handlers
• ALL data loaded client-side via JavaScript
2. Component-Based Architecture
• Base template with shared layout
• Reusable partials (header, sidebar, etc.)
• Page-specific templates extend base
3. Progressive Enhancement
• Works without JavaScript (basic HTML)
• JavaScript adds interactivity
• Graceful degradation
4. API-First Data Loading
• All data from REST APIs
• Client-side state management
• Real-time updates possible
📁 FILE STRUCTURE
═════════════════════════════════════════════════════════════════
app/
├── templates/vendor/
│ ├── base.html ← Base template (layout)
│ ├── login.html ← Public login page
│ ├── admin/ ← Authenticated pages
│ │ ├── dashboard.html
│ │ ├── products.html
│ │ ├── orders.html
│ │ ├── customers.html
│ │ ├── inventory.html
│ │ ├── marketplace.html
│ │ ├── team.html
│ │ └── settings.html
│ └── partials/ ← Reusable components
│ ├── header.html ← Top navigation
│ ├── sidebar.html ← Main navigation
│ ├── vendor_info.html ← Vendor details card
│ └── notifications.html ← Toast notifications
├── static/vendor/
│ ├── css/
│ │ ├── tailwind.output.css ← Generated Tailwind
│ │ └── vendor.css ← Custom styles
│ ├── js/
│ │ ├── init-alpine.js ← Alpine.js base data
│ │ ├── dashboard.js ← Dashboard logic
│ │ ├── products.js ← Products page logic
│ │ ├── orders.js ← Orders page logic
│ │ ├── customers.js ← Customers page logic
│ │ ├── inventory.js ← Inventory page logic
│ │ ├── marketplace.js ← Marketplace page logic
│ │ ├── team.js ← Team page logic
│ │ └── settings.js ← Settings page logic
│ └── img/
│ ├── login-office.jpeg
│ └── login-office-dark.jpeg
├── 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/vendor/
└── pages.py ← Route handlers
🏗️ ARCHITECTURE LAYERS
═════════════════════════════════════════════════════════════════
Layer 1: Routes (FastAPI)
Layer 2: Templates (Jinja2)
Layer 3: JavaScript (Alpine.js)
Layer 4: API (REST endpoints)
Layer 5: Database
Layer 1: ROUTES (FastAPI)
──────────────────────────────────────────────────────────────────
Purpose: Authentication + Template Rendering
Location: app/api/v1/vendor/pages.py
Example:
@router.get("/vendor/{vendor_code}/dashboard")
async def vendor_dashboard_page(
request: Request,
vendor_code: str,
current_user: User = Depends(get_current_vendor_user)
):
return templates.TemplateResponse(
"vendor/admin/dashboard.html",
{
"request": request,
"user": current_user,
"vendor_code": vendor_code
}
)
Responsibilities:
✅ Verify authentication
✅ Extract route parameters
✅ Render template
❌ NO database queries
❌ NO business logic
Layer 2: TEMPLATES (Jinja2)
──────────────────────────────────────────────────────────────────
Purpose: HTML Structure + Server-Side Data
Location: app/templates/vendor/
Template Hierarchy:
base.html (layout)
admin/dashboard.html (page)
partials/sidebar.html (components)
Example:
{% extends "vendor/base.html" %}
{% block title %}Dashboard{% endblock %}
{% block alpine_data %}vendorDashboard(){% endblock %}
{% block content %}
<div x-show="loading">Loading...</div>
<div x-show="!loading" x-text="stats.products_count"></div>
{% endblock %}
Key Features:
✅ Template inheritance
✅ Server-side variables (user, vendor_code)
✅ Include partials
✅ Block overrides
Layer 3: JAVASCRIPT (Alpine.js)
──────────────────────────────────────────────────────────────────
Purpose: Client-Side Interactivity + Data Loading
Location: app/static/vendor/js/
Example:
function vendorDashboard() {
return {
loading: false,
stats: {},
async init() {
await this.loadStats();
},
async loadStats() {
this.loading = true;
try {
this.stats = await apiClient.get(
`/api/v1/vendors/${this.vendorCode}/stats`
);
} finally {
this.loading = false;
}
}
};
}
Responsibilities:
✅ Load data from API
✅ Manage UI state
✅ Handle user interactions
✅ Update DOM reactively
Layer 4: API (REST)
──────────────────────────────────────────────────────────────────
Purpose: Business Logic + Data Access
Location: app/api/v1/vendor/*.py (not pages.py)
Example Endpoints:
GET /api/v1/vendors/{code}/stats
GET /api/v1/vendors/{code}/products
POST /api/v1/vendors/{code}/products
PUT /api/v1/vendors/{code}/products/{id}
DELETE /api/v1/vendors/{code}/products/{id}
🔄 DATA FLOW
═════════════════════════════════════════════════════════════════
Page Load Flow:
──────────────────────────────────────────────────────────────────
1. User → GET /vendor/ACME/dashboard
2. FastAPI → Check authentication
3. FastAPI → Render template with minimal context
4. Browser → Load HTML + CSS + JS
5. Alpine.js → init() executes
6. JavaScript → API call for data
7. API → Return JSON data
8. Alpine.js → Update reactive state
9. Browser → DOM updates automatically
User Interaction Flow:
──────────────────────────────────────────────────────────────────
1. User → Click "Add Product"
2. Alpine.js → openCreateModal()
3. Alpine.js → Show modal
4. User → Fill form + submit
5. Alpine.js → POST to API
6. API → Create product + return
7. Alpine.js → Update local state
8. Browser → DOM updates automatically
9. Alpine.js → Close modal
🎨 STYLING SYSTEM
═════════════════════════════════════════════════════════════════
Tailwind CSS Utility Classes:
• Responsive: sm:, md:, lg:, xl:
• Dark mode: dark:bg-gray-800
• Hover: hover:bg-purple-700
• Focus: focus:outline-none
• Transitions: transition-colors duration-150
Custom CSS Variables (vendor/css/vendor.css):
--color-primary: #7c3aed (purple-600)
--color-accent: #ec4899 (pink-500)
--color-success: #10b981 (green-500)
--color-warning: #f59e0b (yellow-500)
--color-danger: #ef4444 (red-500)
🔐 AUTHENTICATION
═════════════════════════════════════════════════════════════════
Auth Flow:
1. Login → POST /api/v1/vendor/auth/login
2. API → Return JWT token
3. JavaScript → Store in localStorage
4. API Client → Add to all requests
5. Routes → Verify with get_current_vendor_user
Protected Routes:
• All /vendor/{code}/admin/* routes
• Require valid JWT token
• Redirect to login if unauthorized
Public Routes:
• /vendor/{code}/login
• No authentication required
📱 RESPONSIVE DESIGN
═════════════════════════════════════════════════════════════════
Breakpoints (Tailwind):
• sm: 640px (mobile landscape)
• md: 768px (tablet)
• lg: 1024px (desktop)
• xl: 1280px (large desktop)
Mobile-First Approach:
• Base styles for mobile
• Add complexity for larger screens
• Hide sidebar on mobile
• Stack cards vertically
Example:
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4">
<!-- 1 column mobile, 2 tablet, 4 desktop -->
</div>
🌙 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: persist preference
Toggle:
toggleTheme() {
this.dark = !this.dark;
localStorage.setItem('theme', this.dark ? 'dark' : 'light');
}
🔧 COMPONENT PATTERNS
═════════════════════════════════════════════════════════════════
Pattern 1: DATA TABLE
──────────────────────────────────────────────────────────────────
Use For: Lists (products, orders, customers)
Structure:
• Loading state
• Error state
• Empty state
• Data rows
• Actions column
• Pagination
Key Features:
✅ Server-side pagination
✅ Filtering
✅ Sorting
✅ Responsive
Pattern 2: DASHBOARD
──────────────────────────────────────────────────────────────────
Use For: Overview pages with stats
Structure:
• Stats cards grid
• Recent activity list
• Quick actions
Key Features:
✅ Real-time stats
✅ Charts (optional)
✅ Refresh button
Pattern 3: FORM MODAL
──────────────────────────────────────────────────────────────────
Use For: Create/Edit operations
Structure:
• Modal overlay
• Form fields
• Validation
• Save/Cancel buttons
Key Features:
✅ Client-side validation
✅ Error messages
✅ Loading state
✅ Escape to close
Pattern 4: DETAIL PAGE
──────────────────────────────────────────────────────────────────
Use For: Single item view (product detail, order detail)
Structure:
• Header with actions
• Info cards
• Related data tabs
Key Features:
✅ Edit inline
✅ Related items
✅ Status badges
🔄 STATE MANAGEMENT
═════════════════════════════════════════════════════════════════
Alpine.js Reactive State:
Global State (init-alpine.js):
• dark mode
• current user
• vendor info
• menu state
Page State (e.g., products.js):
• items array
• loading boolean
• error string
• filters object
• pagination object
State Updates:
• Direct assignment: this.items = []
• Alpine auto-updates DOM
• No manual DOM manipulation
📡 API CLIENT
═════════════════════════════════════════════════════════════════
Location: app/static/shared/js/api-client.js
Usage:
const data = await apiClient.get('/endpoint');
await apiClient.post('/endpoint', { data });
await apiClient.put('/endpoint/{id}', { data });
await apiClient.delete('/endpoint/{id}');
Features:
✅ Automatic auth headers
✅ Error handling
✅ JSON parsing
✅ Retry logic
🐛 LOGGING
═════════════════════════════════════════════════════════════════
Location: app/static/shared/js/log-config.js
Usage:
logInfo('Operation completed', data);
logError('Operation failed', error);
logDebug('Debug info', context);
logWarn('Warning message');
Levels:
• INFO: General information
• ERROR: Errors and failures
• DEBUG: Detailed debugging
• WARN: Warnings
🎭 ICONS
═════════════════════════════════════════════════════════════════
Location: app/static/shared/js/icons.js
Usage:
<span x-html="$icon('home', 'w-5 h-5')"></span>
<span x-html="$icon('user', 'w-4 h-4 text-blue-500')"></span>
Available Icons:
• home, dashboard, settings
• user, users, user-group
• shopping-bag, shopping-cart
• cube, download, upload
• plus, minus, x
• pencil, trash, eye
• check, exclamation
• chevron-left, chevron-right
• spinner (for loading)
🚀 PERFORMANCE
═════════════════════════════════════════════════════════════════
Optimization Techniques:
1. Template Caching
• Base template cached by FastAPI
• Reduces rendering time
2. Lazy Loading
• Data loaded after page render
• Progressive content display
3. Debouncing
• Search inputs debounced
• Reduces API calls
4. Pagination
• Server-side pagination
• Load only needed data
5. CDN Assets
• Tailwind CSS from CDN
• Alpine.js from CDN
🧪 TESTING APPROACH
═════════════════════════════════════════════════════════════════
Unit Tests:
• Route handlers (Python)
• API endpoints (Python)
• Utility functions (JavaScript)
Integration Tests:
• Full page load
• Data fetching
• Form submission
• Authentication flow
Manual Testing:
• Visual regression
• Cross-browser
• Mobile devices
• Dark mode
🔒 SECURITY
═════════════════════════════════════════════════════════════════
Best Practices:
1. Authentication
✅ JWT tokens
✅ HttpOnly cookies option
✅ Token expiration
2. Authorization
✅ Route-level checks
✅ API-level validation
✅ Vendor-scoped data
3. Input Validation
✅ Client-side validation
✅ Server-side validation
✅ XSS prevention
4. CSRF Protection
✅ Token-based
✅ SameSite cookies
📊 PAGE-BY-PAGE BREAKDOWN
═════════════════════════════════════════════════════════════════
/vendor/{code}/dashboard
──────────────────────────────────────────────────────────────────
Purpose: Overview of vendor operations
Components:
• Stats cards (products, orders, revenue)
• Recent orders table
• Quick actions
Data Sources:
• GET /api/v1/vendors/{code}/stats
• GET /api/v1/vendors/{code}/orders?limit=5
/vendor/{code}/products
──────────────────────────────────────────────────────────────────
Purpose: Manage product catalog
Components:
• Product list table
• Search and filters
• Create/Edit modal
Data Sources:
• GET /api/v1/vendors/{code}/products
• POST /api/v1/vendors/{code}/products
• PUT /api/v1/vendors/{code}/products/{id}
/vendor/{code}/orders
──────────────────────────────────────────────────────────────────
Purpose: View and manage orders
Components:
• Orders table
• Status filters
• Order detail modal
Data Sources:
• GET /api/v1/vendors/{code}/orders
• PUT /api/v1/vendors/{code}/orders/{id}
/vendor/{code}/customers
──────────────────────────────────────────────────────────────────
Purpose: Customer management
Components:
• Customer list
• Search functionality
• Customer detail view
Data Sources:
• GET /api/v1/vendors/{code}/customers
/vendor/{code}/inventory
──────────────────────────────────────────────────────────────────
Purpose: Track stock levels
Components:
• Inventory table
• Stock adjustment modal
• Low stock alerts
Data Sources:
• GET /api/v1/vendors/{code}/inventory
• PUT /api/v1/vendors/{code}/inventory/{id}
/vendor/{code}/marketplace
──────────────────────────────────────────────────────────────────
Purpose: Import products from marketplace
Components:
• Import job list
• Product browser
• Import wizard
Data Sources:
• GET /api/v1/vendors/{code}/marketplace/jobs
• POST /api/v1/vendors/{code}/marketplace/import
/vendor/{code}/team
──────────────────────────────────────────────────────────────────
Purpose: Manage team members
Components:
• Team member list
• Role management
• Invitation form
Data Sources:
• GET /api/v1/vendors/{code}/team
• POST /api/v1/vendors/{code}/team/invite
/vendor/{code}/settings
──────────────────────────────────────────────────────────────────
Purpose: Configure vendor settings
Components:
• Settings tabs
• Form sections
• Save buttons
Data Sources:
• GET /api/v1/vendors/{code}/settings
• PUT /api/v1/vendors/{code}/settings
🎓 LEARNING PATH
═════════════════════════════════════════════════════════════════
For New Developers:
1. Understand Architecture (1 hour)
→ Read this document
→ Review file structure
→ Examine base template
2. Study Existing Page (2 hours)
→ Open dashboard.html
→ Open dashboard.js
→ Trace data flow
3. Create Simple Page (4 hours)
→ Copy templates
→ Modify for new feature
→ Test thoroughly
4. Add Complex Feature (1 day)
→ Forms with validation
→ Modal dialogs
→ API integration
5. Master Patterns (1 week)
→ All common patterns
→ Error handling
→ Performance optimization
🔄 DEPLOYMENT CHECKLIST
═════════════════════════════════════════════════════════════════
Before Deploying:
□ Build Tailwind CSS
□ Minify JavaScript
□ Test all routes
□ Verify authentication
□ Check mobile responsive
□ Test dark mode
□ Validate API endpoints
□ Review error handling
□ Check console for errors
□ Test in production mode
📚 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_VENDOR_ALPINE_PAGE_TEMPLATE.md
• API Documentation: API_REFERENCE.md
• Database Schema: DATABASE_SCHEMA.md
══════════════════════════════════════════════════════════════════
VENDOR ADMIN ARCHITECTURE
Modern, Maintainable, and Developer-Friendly
══════════════════════════════════════════════════════════════════

File diff suppressed because it is too large Load Diff

View File

View File

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

1
static/vendor/css/tailwind.output.css vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

BIN
static/vendor/img/login-office-dark.jpeg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
static/vendor/img/login-office.jpeg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB