# 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 Name]

Loading data...

Error

Name Status Date Actions
Showing - of

{% endblock %} {# Page-specific JavaScript #} {% block extra_scripts %} {% 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
  • [Page Display Name]
  • ``` --- ## 🎨 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 ``` --- ## 📱 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.