revamping documentation
This commit is contained in:
695
docs/frontend/vendor/architecture.md
vendored
Normal file
695
docs/frontend/vendor/architecture.md
vendored
Normal 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
|
||||
══════════════════════════════════════════════════════════════════
|
||||
837
docs/frontend/vendor/page-templates.md
vendored
Normal file
837
docs/frontend/vendor/page-templates.md
vendored
Normal 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.
|
||||
Reference in New Issue
Block a user