# Admin Frontend - Alpine.js/Jinja2 Page Template Guide ## 📋 Overview This guide provides complete templates for creating new admin pages using the established Alpine.js + Jinja2 architecture. Follow these patterns to ensure consistency, proper initialization, and optimal performance across the admin portal. --- ## 🎯 Quick Reference ### File Structure for New Page ``` app/ ├── templates/admin/ │ └── [page-name].html # Jinja2 template ├── static/admin/js/ │ └── [page-name].js # Alpine.js component └── api/v1/admin/ └── pages.py # Route registration ``` ### Checklist for New Page - [ ] Create Jinja2 template extending admin/base.html - [ ] Create Alpine.js JavaScript component - [ ] Use centralized logger (one line!) - [ ] Add ...data() for base inheritance - [ ] Set currentPage identifier - [ ] Add initialization guard - [ ] Use lowercase apiClient - [ ] Register route in pages.py - [ ] Add sidebar navigation link - [ ] Test authentication - [ ] Test dark mode - [ ] Verify no duplicate initialization --- ## 📄 Template Structure ### 1. Jinja2 Template **File:** `app/templates/admin/[page-name].html` ```jinja2 {# app/templates/admin/[page-name].html #} {% extends "admin/base.html" %} {# Page title for browser tab #} {% block title %}[Page Name]{% endblock %} {# ✅ CRITICAL: Alpine.js component name #} {% block alpine_data %}admin[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/admin/js/[page-name].js` ```javascript // static/admin/js/[page-name].js /** * [Page Name] Component * Handles [describe functionality] */ // ✅ CRITICAL: Use centralized logger (ONE LINE!) const pageLog = window.LogConfig.loggers.[pageName]; // OR create custom logger if not pre-configured: // const pageLog = window.LogConfig.createLogger('[PAGE-NAME]', window.LogConfig.logLevel); /** * Main Alpine.js component for [page name] */ function admin[PageName]() { return { // ───────────────────────────────────────────────────── // ✅ CRITICAL: INHERIT BASE LAYOUT // ───────────────────────────────────────────────────── ...data(), // ✅ CRITICAL: SET PAGE IDENTIFIER currentPage: '[page-name]', // ───────────────────────────────────────────────────── // STATE // ───────────────────────────────────────────────────── // Loading states loading: false, saving: false, // Error handling error: null, errors: {}, // Data items: [], currentItem: null, // Pagination pagination: { currentPage: 1, totalPages: 1, perPage: 10, total: 0, from: 0, to: 0 }, // Filters filters: { search: '', status: '', sortBy: 'created_at:desc' }, // Modal showModal: false, modalMode: 'create', // 'create' or 'edit' formData: { name: '', description: '', is_active: true }, // ───────────────────────────────────────────────────── // LIFECYCLE // ───────────────────────────────────────────────────── /** * ✅ CRITICAL: Initialize component with guard */ async init() { pageLog.info('=== [PAGE NAME] INITIALIZING ==='); // ✅ CRITICAL: Initialization guard if (window._[pageName]Initialized) { pageLog.warn('Already initialized, skipping...'); return; } window._[pageName]Initialized = true; // Track performance const startTime = performance.now(); try { // Load initial data await this.loadData(); // Log performance const duration = performance.now() - startTime; window.LogConfig.logPerformance('[Page Name] Init', duration); pageLog.info('=== [PAGE NAME] INITIALIZATION COMPLETE ==='); } catch (error) { window.LogConfig.logError(error, '[Page Name] Init'); this.error = 'Failed to initialize page'; } }, // ───────────────────────────────────────────────────── // DATA LOADING // ───────────────────────────────────────────────────── /** * Load main data from API */ async loadData() { pageLog.info('Loading data...'); this.loading = true; this.error = null; try { const startTime = performance.now(); // Build query params const params = new URLSearchParams({ page: this.pagination.currentPage, per_page: this.pagination.perPage, search: this.filters.search, status: this.filters.status, sort_by: this.filters.sortBy }); const url = `/api/v1/admin/items?${params}`; // Log API request window.LogConfig.logApiCall('GET', url, null, 'request'); // ✅ CRITICAL: Use lowercase apiClient const response = await apiClient.get(url); // Log API response window.LogConfig.logApiCall('GET', url, response, 'response'); // Update state this.items = response.items || []; this.pagination.total = response.total || 0; this.pagination.totalPages = Math.ceil( this.pagination.total / this.pagination.perPage ); this.pagination.from = (this.pagination.currentPage - 1) * this.pagination.perPage + 1; this.pagination.to = Math.min( this.pagination.currentPage * this.pagination.perPage, this.pagination.total ); // Log performance const duration = performance.now() - startTime; window.LogConfig.logPerformance('Load Data', duration); pageLog.info(`Data loaded successfully`, { count: this.items.length, duration: `${duration}ms`, page: this.pagination.currentPage }); } catch (error) { window.LogConfig.logError(error, 'Load Data'); this.error = error.message || 'Failed to load data'; Utils.showToast('Failed to load data', 'error'); } finally { this.loading = false; } }, /** * Refresh data */ async refresh() { pageLog.info('Refreshing data...'); this.error = null; await this.loadData(); Utils.showToast('Data refreshed', 'success'); }, // ───────────────────────────────────────────────────── // 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: '', status: '', 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(); }, /** * Go to previous page */ async previousPage() { await this.goToPage(this.pagination.currentPage - 1); }, /** * Go to next page */ async nextPage() { await this.goToPage(this.pagination.currentPage + 1); }, /** * Get pagination range for display */ get paginationRange() { const current = this.pagination.currentPage; const total = this.pagination.totalPages; const range = []; // Show max 5 page numbers let start = Math.max(1, current - 2); let end = Math.min(total, start + 4); // Adjust start if we're near the end if (end - start < 4) { start = Math.max(1, end - 4); } for (let i = start; i <= end; i++) { range.push(i); } return range; }, // ───────────────────────────────────────────────────── // CRUD OPERATIONS // ───────────────────────────────────────────────────── /** * Open create modal */ openCreateModal() { pageLog.info('Opening create modal'); this.modalMode = 'create'; this.formData = { name: '', description: '', is_active: true }; this.errors = {}; this.showModal = true; }, /** * Open edit modal */ async editItem(itemId) { pageLog.info('Opening edit modal for item:', itemId); try { this.modalMode = 'edit'; // Load item details const url = `/api/v1/admin/items/${itemId}`; window.LogConfig.logApiCall('GET', url, null, 'request'); const item = await apiClient.get(url); window.LogConfig.logApiCall('GET', url, item, 'response'); // Populate form this.formData = { ...item }; this.currentItem = item; this.errors = {}; this.showModal = true; } catch (error) { window.LogConfig.logError(error, 'Edit Item'); Utils.showToast('Failed to load item', 'error'); } }, /** * View item details */ async viewItem(itemId) { pageLog.info('Viewing item:', itemId); // Navigate to detail page or open view modal window.location.href = `/admin/items/${itemId}`; }, /** * Save item (create or update) */ async saveItem() { pageLog.info('Saving item...'); // Validate form if (!this.validateForm()) { pageLog.warn('Form validation failed'); return; } this.saving = true; this.errors = {}; try { let response; if (this.modalMode === 'create') { // Create new item const url = '/api/v1/admin/items'; window.LogConfig.logApiCall('POST', url, this.formData, 'request'); response = await apiClient.post(url, this.formData); window.LogConfig.logApiCall('POST', url, response, 'response'); pageLog.info('Item created successfully'); Utils.showToast('Item created successfully', 'success'); } else { // Update existing item const url = `/api/v1/admin/items/${this.currentItem.id}`; window.LogConfig.logApiCall('PUT', url, this.formData, 'request'); response = await apiClient.put(url, this.formData); window.LogConfig.logApiCall('PUT', url, response, 'response'); pageLog.info('Item updated successfully'); Utils.showToast('Item updated successfully', 'success'); } // Close modal and reload data this.closeModal(); await this.loadData(); } catch (error) { window.LogConfig.logError(error, 'Save Item'); // Handle validation errors if (error.details && error.details.validation_errors) { this.errors = error.details.validation_errors; } else { Utils.showToast('Failed to save item', 'error'); } } finally { this.saving = false; } }, /** * Delete item */ async deleteItem(itemId) { pageLog.info('Deleting item:', itemId); // Confirm deletion if (!confirm('Are you sure you want to delete this item?')) { return; } try { const url = `/api/v1/admin/items/${itemId}`; window.LogConfig.logApiCall('DELETE', url, null, 'request'); await apiClient.delete(url); window.LogConfig.logApiCall('DELETE', url, null, 'response'); pageLog.info('Item deleted successfully'); Utils.showToast('Item deleted successfully', 'success'); // Reload data await this.loadData(); } catch (error) { window.LogConfig.logError(error, 'Delete Item'); Utils.showToast('Failed to delete item', 'error'); } }, /** * Close modal */ closeModal() { this.showModal = false; this.formData = {}; this.errors = {}; this.currentItem = null; }, // ───────────────────────────────────────────────────── // VALIDATION // ───────────────────────────────────────────────────── /** * Validate form data */ validateForm() { this.errors = {}; if (!this.formData.name || this.formData.name.trim() === '') { this.errors.name = 'Name is required'; } // Add more validation rules as needed return Object.keys(this.errors).length === 0; }, // ───────────────────────────────────────────────────── // HELPERS // ───────────────────────────────────────────────────── /** * Format date for display */ formatDate(dateString) { if (!dateString) return '-'; return Utils.formatDate(dateString); }, /** * Truncate text */ truncate(text, length = 50) { if (!text) return ''; if (text.length <= length) return text; return text.substring(0, length) + '...'; } }; } // Make available globally window.admin[PageName] = admin[PageName]; pageLog.info('[Page Name] module loaded'); ``` --- ### 3. Route Registration **File:** `app/api/v1/admin/pages.py` ```python from fastapi import APIRouter, Request, Depends from app.api.deps import get_current_admin_from_cookie_or_header from app.models.database.user import User router = APIRouter() @router.get("/admin/[page-route]") async def [page_name]_page( request: Request, current_user: User = Depends(get_current_admin_from_cookie_or_header) ): """ [Page Name] page Displays [description] Requires admin authentication. """ return templates.TemplateResponse( "admin/[page-name].html", { "request": request, "user": current_user, } ) ``` --- ### 4. Sidebar Navigation **File:** `app/templates/admin/partials/sidebar.html` ```jinja2
  • [Page Display Name]
  • ``` --- ## 🎨 Common Page Patterns ### Pattern 1: Simple Data List (GET) **Use for:** Store list, user list, product list ```javascript async init() { if (window._myPageInitialized) return; window._myPageInitialized = true; await this.loadData(); } async loadData() { this.loading = true; try { const response = await apiClient.get('/api/v1/admin/items'); this.items = response.items || []; } finally { this.loading = false; } } ``` --- ### Pattern 2: Dashboard with Stats **Use for:** Dashboard, analytics pages ```javascript async init() { if (window._dashboardInitialized) return; window._dashboardInitialized = true; await Promise.all([ this.loadStats(), this.loadRecentActivity() ]); } async loadStats() { this.stats = await apiClient.get('/api/v1/admin/stats'); } async loadRecentActivity() { this.activity = await apiClient.get('/api/v1/admin/recent-activity'); } ``` --- ### Pattern 3: Single Item Detail/Edit **Use for:** Store edit, user edit ```javascript async init() { if (window._editPageInitialized) return; window._editPageInitialized = true; const itemId = this.getItemIdFromUrl(); await this.loadItem(itemId); } async loadItem(id) { this.item = await apiClient.get(`/api/v1/admin/items/${id}`); // Populate form with item data this.formData = { ...this.item }; } async saveItem() { if (!this.validateForm()) return; await apiClient.put( `/api/v1/admin/items/${this.item.id}`, this.formData ); Utils.showToast('Saved successfully', 'success'); } ``` --- ### Pattern 4: With Filters and Pagination **Use for:** Large lists with filtering ```javascript filters: { search: '', status: '', sortBy: 'created_at:desc' }, pagination: { currentPage: 1, totalPages: 1, perPage: 10 }, async loadData() { const params = new URLSearchParams({ page: this.pagination.currentPage, per_page: this.pagination.perPage, ...this.filters }); const response = await apiClient.get( `/api/v1/admin/items?${params}` ); this.items = response.items; this.pagination.total = response.total; this.pagination.totalPages = Math.ceil( response.total / this.pagination.perPage ); } async applyFilters() { this.pagination.currentPage = 1; // Reset to page 1 await this.loadData(); } ``` --- ## 🔧 Best Practices ### 1. Base Layout Inheritance **✅ ALWAYS include ...data():** ```javascript function adminMyPage() { return { ...data(), // ← Must be first! currentPage: 'my-page', // Your state here }; } ``` ### 2. Initialization Guards **✅ ALWAYS use initialization guard:** ```javascript async init() { if (window._myPageInitialized) { log.warn('Already initialized'); return; } window._myPageInitialized = true; await this.loadData(); } ``` ### 3. API Client Usage **✅ Use lowercase apiClient:** ```javascript // ✅ CORRECT await apiClient.get('/endpoint'); // ❌ WRONG await ApiClient.get('/endpoint'); await API_CLIENT.get('/endpoint'); ``` ### 4. Centralized Logging **✅ Use pre-configured logger:** ```javascript // ✅ CORRECT - One line! const myLog = window.LogConfig.loggers.myPage; // ❌ WRONG - 15+ lines const myLog = { error: (...args) => console.error(...), // ... etc }; ``` ### 5. Error Handling **✅ Handle errors gracefully:** ```javascript try { await this.loadData(); } catch (error) { window.LogConfig.logError(error, 'Load Data'); this.error = error.message; Utils.showToast('Failed to load data', 'error'); // Don't throw - let UI handle it } ``` ### 6. Loading States **✅ Always set loading state:** ```javascript this.loading = true; try { // ... operations } finally { this.loading = false; // Always executes } ``` ### 7. Performance Tracking **✅ Track performance for slow operations:** ```javascript const startTime = performance.now(); await this.loadData(); const duration = performance.now() - startTime; window.LogConfig.logPerformance('Load Data', duration); ``` --- ## 📱 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 - [ ] Sidebar collapses on mobile - [ ] Stats cards stack 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 - [ ] Create operation works - [ ] Edit operation works - [ ] Delete operation works - [ ] Modal opens/closes - [ ] Form validation works ### Architecture - [ ] Initialization guard works (no duplicate init) - [ ] ...data() properly inherited - [ ] currentPage set correctly - [ ] Lowercase apiClient used - [ ] Centralized logging used - [ ] No console errors - [ ] Dark mode works - [ ] Mobile responsive --- ## 🚀 Quick Start Commands ```bash # Create new page files touch app/templates/admin/new-page.html touch app/static/admin/js/new-page.js # Copy templates (start from dashboard.js as base) cp app/static/admin/js/dashboard.js app/static/admin/js/new-page.js # Update files: # 1. Change logger: dashLog → newPageLog # 2. Change function: adminDashboard() → adminNewPage() # 3. Change init flag: _dashboardInitialized → _newPageInitialized # 4. Change currentPage: 'dashboard' → 'new-page' # 5. Replace data loading logic # 6. Update HTML template # 7. Add route in pages.py # 8. Add sidebar link # 9. Test! ``` --- ## 📚 Additional Resources ### Pre-configured Loggers ```javascript window.LogConfig.loggers.dashboard window.LogConfig.loggers.merchants window.LogConfig.loggers.stores window.LogConfig.loggers.storeTheme window.LogConfig.loggers.users window.LogConfig.loggers.customers window.LogConfig.loggers.products window.LogConfig.loggers.orders window.LogConfig.loggers.imports window.LogConfig.loggers.audit ``` ### From Base Data (via ...data()) - `this.dark` - Dark mode state - `this.toggleTheme()` - Toggle theme - `this.isSideMenuOpen` - Side menu state - `this.toggleSideMenu()` - Toggle side menu - `this.isProfileMenuOpen` - Profile menu state - `this.toggleProfileMenu()` - Toggle profile menu ### Global Utilities - `Utils.showToast(message, type, duration)` - `Utils.formatDate(dateString)` - `apiClient.get(url)` - `apiClient.post(url, data)` - `apiClient.put(url, data)` - `apiClient.delete(url)` ### Icon Helper ```html ``` --- ## ⚠️ Common Mistakes to Avoid ### 1. Missing ...data() ```javascript // ❌ WRONG - No base inheritance function myPage() { return { items: [] }; } // ✅ CORRECT function myPage() { return { ...data(), // Must be first! items: [] }; } ``` ### 2. Wrong API Client Case ```javascript // ❌ WRONG await ApiClient.get('/url'); // ✅ CORRECT await apiClient.get('/url'); ``` ### 3. Missing Init Guard ```javascript // ❌ WRONG async init() { await this.loadData(); } // ✅ CORRECT async init() { if (window._myPageInitialized) return; window._myPageInitialized = true; await this.loadData(); } ``` ### 4. Old Logging Pattern ```javascript // ❌ WRONG - 15+ lines const log = { error: (...args) => console.error(...), // ... }; // ✅ CORRECT - 1 line const log = window.LogConfig.loggers.myPage; ``` ### 5. Missing currentPage ```javascript // ❌ WRONG return { ...data(), items: [] }; // ✅ CORRECT return { ...data(), currentPage: 'my-page', // Must set! items: [] }; ``` --- This template provides a complete, production-ready pattern for building admin pages with consistent structure, proper initialization, comprehensive logging, and excellent maintainability. --- ## 🎯 Real-World Examples: Marketplace Import Pages The marketplace import system provides two comprehensive real-world implementations demonstrating all best practices. ### 1. Self-Service Import (`/admin/marketplace`) **Purpose**: Admin tool for triggering imports for any store **Files**: - **Template**: `app/templates/admin/marketplace.html` - **JavaScript**: `static/admin/js/marketplace.js` - **Route**: `app/routes/admin_pages.py` - `admin_marketplace_page()` #### Key Features ##### Store Selection with Auto-Load ```javascript // Load all stores async loadStores() { const response = await apiClient.get('/admin/stores?limit=1000'); this.stores = response.items || []; } // Handle store selection change onStoreChange() { const storeId = parseInt(this.importForm.store_id); this.selectedStore = this.stores.find(v => v.id === storeId) || null; } // Quick fill from selected store's settings quickFill(language) { if (!this.selectedStore) return; const urlMap = { 'fr': this.selectedStore.letzshop_csv_url_fr, 'en': this.selectedStore.letzshop_csv_url_en, 'de': this.selectedStore.letzshop_csv_url_de }; if (urlMap[language]) { this.importForm.csv_url = urlMap[language]; this.importForm.language = language; } } ``` ##### Filter by Current User ```javascript async loadJobs() { const params = new URLSearchParams({ page: this.page, limit: this.limit, created_by_me: 'true' // Only show jobs I triggered }); const response = await apiClient.get( `/admin/marketplace-import-jobs?${params.toString()}` ); this.jobs = response.items || []; } ``` ##### Store Name Helper ```javascript getStoreName(storeId) { const store = this.stores.find(v => v.id === storeId); return store ? `${store.name} (${store.store_code})` : `Store #${storeId}`; } ``` --- ### 2. Platform Monitoring (`/admin/imports`) **Purpose**: System-wide oversight of all import jobs **Files**: - **Template**: `app/templates/admin/imports.html` - **JavaScript**: `static/admin/js/imports.js` - **Route**: `app/routes/admin_pages.py` - `admin_imports_page()` #### Key Features ##### Statistics Dashboard ```javascript async loadStats() { const response = await apiClient.get('/admin/marketplace-import-jobs/stats'); this.stats = { total: response.total || 0, active: (response.pending || 0) + (response.processing || 0), completed: response.completed || 0, failed: response.failed || 0 }; } ``` **Template**: ```html

    Total Jobs

    0

    ``` ##### Advanced Filtering ```javascript filters: { store_id: '', status: '', marketplace: '', created_by: '' // 'me' or empty for all }, async applyFilters() { this.page = 1; // Reset to first page const params = new URLSearchParams({ page: this.page, limit: this.limit }); // Add filters if (this.filters.store_id) { params.append('store_id', this.filters.store_id); } if (this.filters.status) { params.append('status', this.filters.status); } if (this.filters.created_by === 'me') { params.append('created_by_me', 'true'); } await this.loadJobs(); await this.loadStats(); // Update stats based on filters } ``` **Template**: ```html
    ``` ##### Enhanced Job Table ```html
    Job ID Store Status Progress Created By Actions
    ``` --- ## 🔄 Comparison: Two Admin Interfaces | Feature | Self-Service (`/marketplace`) | Platform Monitoring (`/imports`) | |---------|-------------------------------|----------------------------------| | **Purpose** | Import products for stores | Monitor all system imports | | **Scope** | Personal (my jobs) | System-wide (all jobs) | | **Primary Action** | Trigger new imports | View and analyze | | **Jobs Shown** | Only jobs I triggered | All jobs (with filtering) | | **Store Selection** | Required (select store to import for) | Optional (filter view) | | **Statistics** | No | Yes (dashboard cards) | | **Auto-Refresh** | 10 seconds | 15 seconds | | **Filter Options** | Store, Status, Marketplace | Store, Status, Marketplace, Creator | | **Use Case** | "I need to import for Store X" | "What's happening system-wide?" | --- ## 📋 Navigation Structure ### Sidebar Organization ```javascript // Admin sidebar sections { "Dashboard": [ "Dashboard" ], "Platform Administration": [ "Merchants", "Stores", "Users", "Customers", "Marketplace" ], "Content Management": [ "Platform Homepage", "Content Pages", "Store Themes" ], "Developer Tools": [ "Components", "Icons", "Testing Hub", "Code Quality" ], "Platform Monitoring": [ "Import Jobs", "Application Logs" ], "Settings": [ "Settings" ] } ``` ### Setting currentPage ```javascript // marketplace.js return { ...data(), currentPage: 'marketplace', // Highlights "Marketplace" in sidebar // ... }; // imports.js return { ...data(), currentPage: 'imports', // Highlights "Import Jobs" in sidebar // ... }; ``` ### Collapsible Sections Sidebar sections are collapsible with state persisted to localStorage: ```javascript // Section keys used in openSections state { platformAdmin: true, // Platform Administration (default open) contentMgmt: false, // Content Management devTools: false, // Developer Tools monitoring: false // Platform Monitoring } // Toggle a section toggleSection('devTools'); // Check if section is open if (openSections.devTools) { ... } ``` See [Sidebar Navigation](../shared/sidebar.md) for full documentation. --- ## 🎨 UI Patterns ### Success/Error Messages ```html

    Error

    ``` ### Empty States ```html

    You haven't triggered any imports yet

    Start a new import using the form above

    ``` ### Loading States with Spinners ```html

    Loading import jobs...

    ``` ### Modal Dialogs ```html

    Import Job Details

    ``` --- ## 📚 Related Documentation - [Marketplace Integration Guide](../../guides/marketplace-integration.md) - Complete marketplace system documentation - [Store Page Templates](../store/page-templates.md) - Store page patterns - [Icons Guide](../../development/icons-guide.md) - Available icons - [Admin Integration Guide](../../backend/admin-integration-guide.md) - Backend integration