# Store Admin Frontend - Alpine.js/Jinja2 Page Template Guide ## 📋 Overview This guide provides complete templates for creating new store admin pages using the established Alpine.js + Jinja2 architecture. Follow these patterns to ensure consistency across the store portal. --- ## 🎯 Quick Reference ### File Structure for New Page ``` app/ ├── templates/store/ │ └── [page-name].html # Jinja2 template ├── static/store/js/ │ └── [page-name].js # Alpine.js component └── routes/ └── store_pages.py # Route registration ``` ### Checklist for New Page - [ ] Create Jinja2 template extending base.html - [ ] Create Alpine.js JavaScript component - [ ] Register route in store_pages.py - [ ] Add navigation link to sidebar.html - [ ] Test authentication - [ ] Test data loading - [ ] Test responsive design --- ## 📄 Template Structure ### 1. Jinja2 Template **File:** `app/templates/store/[page-name].html` ```jinja2 {# app/templates/store/[page-name].html #} {% extends "store/base.html" %} {# Page title for browser tab #} {% block title %}[Page Name]{% endblock %} {# Alpine.js component name - use data() for simple pages or store[PageName]() for complex pages #} {% block alpine_data %}store[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/store/js/[page-name].js` ```javascript // app/static/store/js/[page-name].js /** * [Page Name] page logic * Handles data loading, filtering, CRUD operations */ // ✅ Create dedicated logger for this page const store[PageName]Log = window.LogConfig.loggers.[pagename]; function store[PageName]() { return { // ═══════════════════════════════════════════════════════════ // INHERIT BASE STATE (from init-alpine.js) // ═══════════════════════════════════════════════════════════ // This provides: storeCode, currentUser, store, dark mode, menu states ...data(), // ✅ Set page identifier (for sidebar highlighting) currentPage: '[page-name]', // ═══════════════════════════════════════════════════════════ // PAGE-SPECIFIC 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() { // Guard against multiple initialization if (window._store[PageName]Initialized) { return; } window._store[PageName]Initialized = true; // IMPORTANT: Call parent init first to set storeCode from URL const parentInit = data().init; if (parentInit) { await parentInit.call(this); } store[PageName]Log.info('[PageName] page initializing...'); await this.loadData(); store[PageName]Log.info('[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 // NOTE: apiClient prepends /api/v1, and store context middleware handles store detection // So we just call /store/[endpoint] → becomes /api/v1/store/[endpoint] const response = await apiClient.get( `/store/[endpoint]?${params}` ); // Update state this.items = response.items || []; this.updatePagination(response); store[PageName]Log.info('[PageName] data loaded', { items: this.items.length, total: this.pagination.total }); } catch (error) { store[PageName]Log.error('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 = `/store/${this.storeCode}/[endpoint]/${id}`; }, async editItem(id) { try { // Load item data const item = await apiClient.get( `/store/[endpoint]/${id}` ); this.modalMode = 'edit'; this.modalTitle = 'Edit Item'; this.formData = { ...item }; this.showModal = true; } catch (error) { store[PageName]Log.error('Failed to load item', error); alert('Failed to load item details'); } }, async saveItem() { this.saving = true; try { if (this.modalMode === 'create') { await apiClient.post( `/store/[endpoint]`, this.formData ); store[PageName]Log.info('Item created successfully'); } else { await apiClient.put( `/store/[endpoint]/${this.formData.id}`, this.formData ); store[PageName]Log.info('Item updated successfully'); } this.closeModal(); await this.loadData(); } catch (error) { store[PageName]Log.error('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( `/store/[endpoint]/${id}` ); store[PageName]Log.info('Item deleted successfully'); await this.loadData(); } catch (error) { store[PageName]Log.error('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.store[PageName] = store[PageName]; ``` --- ### 3. Route Registration **File:** `app/routes/store_pages.py` ```python @router.get("/{store_code}/[page-name]", response_class=HTMLResponse, include_in_schema=False) async def store_[page_name]_page( request: Request, store_code: str = Path(..., description="Store code"), current_user: User = Depends(get_current_store_from_cookie_or_header) ): """ Render [page name] page. JavaScript loads data via API. """ return templates.TemplateResponse( "store/[page-name].html", { "request": request, "user": current_user, "store_code": store_code, } ) ``` --- ### 4. Sidebar Navigation **File:** `app/templates/store/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() { // Call parent init first const parentInit = data().init; if (parentInit) { await parentInit.call(this); } await this.loadData(); } async loadData() { this.loading = true; try { const response = await apiClient.get(`/store/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() { // Call parent init first const parentInit = data().init; if (parentInit) { await parentInit.call(this); } await Promise.all([ this.loadStats(), this.loadRecentActivity() ]); } async loadStats() { const stats = await apiClient.get(`/store/stats`); this.stats = stats; } ``` ### Pattern 3: Detail Page Use for: Product detail, order detail ```javascript async init() { // Call parent init first const parentInit = data().init; if (parentInit) { await parentInit.call(this); } await this.loadItem(); } async loadItem() { const id = this.getItemIdFromUrl(); this.item = await apiClient.get(`/store/items/${id}`); } ``` ### Pattern 4: Simple Page (No Custom JavaScript) Use for: Coming soon pages, static pages, pages under development **Template:** `app/templates/store/[page-name].html` ```jinja2 {# app/templates/store/products.html #} {% extends "store/base.html" %} {% block title %}Products{% endblock %} {# Use base data() directly - no custom JavaScript needed #} {% block alpine_data %}data(){% endblock %} {% block content %}

    Products

    📦

    Products Management Coming Soon

    This page is under development.

    Back to Dashboard
    {% endblock %} ``` **No JavaScript file needed!** The page inherits all functionality from `init-alpine.js`. ### Pattern 5: 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(`/store/settings`, this.formData); } ``` --- ## 🔧 Best Practices ### 1. Error Handling ```javascript try { await apiClient.get('/endpoint'); } catch (error) { // Use dedicated page logger storePageLog.error('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 ``` ### 6. Inherited State from Base (init-alpine.js) All store pages automatically inherit these properties from the base `data()` function: ```javascript // ✅ Available in all pages via ...data() spread { // Store context (set by parent init) storeCode: '', // Extracted from URL path store: null, // Loaded from API currentUser: {}, // Loaded from localStorage // UI state dark: false, // Dark mode toggle isSideMenuOpen: false, isNotificationsMenuOpen: false, isProfileMenuOpen: false, currentPage: '', // Override this in your component // Methods init() { ... }, // MUST call this via parent init pattern loadStoreInfo() { ... }, handleLogout() { ... } } ``` **Important:** Always call parent `init()` before your page logic: ```javascript async init() { const parentInit = data().init; if (parentInit) { await parentInit.call(this); } // Now storeCode and store are available await this.loadData(); } ``` --- ## 📱 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/store/products.html touch app/static/store/js/products.js # Copy templates cp template.html app/templates/store/products.html cp template.js app/static/store/js/products.js # Update files with your page name # Register route in store_pages.py # Add sidebar link # Test! ``` --- ## 📚 Additional Resources ### Helpers and Utilities - **Icons**: Use `$icon('icon-name', 'classes')` helper from `shared/js/icons.js` - **API Client**: Automatically handles auth tokens, prepends `/api/v1` to paths - **Logging**: Create dedicated logger per page: `const myPageLog = window.LogConfig.loggers.pagename;` - **Date Formatting**: Use `formatDate()` helper (available in your component) - **Currency**: Use `formatCurrency()` helper (available in your component) ### Reusable Partials You can include reusable template partials in your pages: ```jinja2 {# Display store information card #} {% include 'store/partials/store_info.html' %} {# Already included in base.html #} {% include 'store/partials/sidebar.html' %} {% include 'store/partials/header.html' %} ``` ### API Endpoint Pattern All store API calls follow this pattern: - **JavaScript**: `apiClient.get('/store/endpoint')` - **Becomes**: `/api/v1/store/endpoint` - **Middleware**: Automatically detects store from cookie/header context - **No need** to include `storeCode` in API path ### Script Loading Order (from base.html) The base template loads scripts in this specific order: 1. Log Configuration (`log-config.js`) 2. Icons (`icons.js`) 3. Alpine Base Data (`init-alpine.js`) - provides `data()` function 4. Utils (`utils.js`) 5. API Client (`api-client.js`) 6. Alpine.js library (deferred) 7. Page-specific scripts (your custom JS) --- This template provides a complete, production-ready pattern for building store admin pages with consistent structure, error handling, and user experience. --- ## 🎯 Real-World Example: Marketplace Import Page The marketplace import page is a comprehensive real-world implementation demonstrating all best practices. ### Implementation Files **Template**: `app/templates/store/marketplace.html` **JavaScript**: `static/store/js/marketplace.js` **Route**: `app/routes/store_pages.py` - `store_marketplace_page()` ### Key Features Demonstrated #### 1. Complete Form Handling ```javascript // Import form with validation importForm: { csv_url: '', marketplace: 'Letzshop', language: 'fr', batch_size: 1000 }, async startImport() { if (!this.importForm.csv_url) { this.error = 'Please enter a CSV URL'; return; } this.importing = true; try { const response = await apiClient.post('/store/marketplace/import', { source_url: this.importForm.csv_url, marketplace: this.importForm.marketplace, batch_size: this.importForm.batch_size }); this.successMessage = `Import job #${response.job_id} started!`; await this.loadJobs(); // Refresh list } catch (error) { this.error = error.message; } finally { this.importing = false; } } ``` #### 2. Auto-Refresh for Active Jobs ```javascript startAutoRefresh() { this.autoRefreshInterval = setInterval(async () => { const hasActiveJobs = this.jobs.some(job => job.status === 'pending' || job.status === 'processing' ); if (hasActiveJobs) { await this.loadJobs(); } }, 10000); // Every 10 seconds } ``` #### 3. Quick Fill from Settings ```javascript // Load store settings async loadStoreSettings() { const response = await apiClient.get('/store/settings'); this.storeSettings = { letzshop_csv_url_fr: response.letzshop_csv_url_fr || '', letzshop_csv_url_en: response.letzshop_csv_url_en || '', letzshop_csv_url_de: response.letzshop_csv_url_de || '' }; } // Quick fill function quickFill(language) { const urlMap = { 'fr': this.storeSettings.letzshop_csv_url_fr, 'en': this.storeSettings.letzshop_csv_url_en, 'de': this.storeSettings.letzshop_csv_url_de }; if (urlMap[language]) { this.importForm.csv_url = urlMap[language]; this.importForm.language = language; } } ``` #### 4. Job Details Modal ```javascript async viewJobDetails(jobId) { try { const response = await apiClient.get(`/store/marketplace/imports/${jobId}`); this.selectedJob = response; this.showJobModal = true; } catch (error) { this.error = error.message; } } ``` #### 5. Pagination ```javascript async nextPage() { if (this.page * this.limit < this.totalJobs) { this.page++; await this.loadJobs(); } } ``` #### 6. Utility Functions ```javascript formatDate(dateString) { if (!dateString) return 'N/A'; const date = new Date(dateString); return date.toLocaleString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } calculateDuration(job) { if (!job.started_at) return 'Not started'; const start = new Date(job.started_at); const end = job.completed_at ? new Date(job.completed_at) : new Date(); const durationMs = end - start; const seconds = Math.floor(durationMs / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); if (hours > 0) { return `${hours}h ${minutes % 60}m`; } else if (minutes > 0) { return `${minutes}m ${seconds % 60}s`; } return `${seconds}s`; } ``` ### Template Features #### Dynamic Status Badges ```html ``` #### Conditional Display ```html ``` #### Progress Metrics ```html
    imported, updated
    errors
    ``` --- ## 📚 Related Documentation - [Marketplace Integration Guide](../../guides/marketplace-integration.md) - Complete marketplace system documentation - [Admin Page Templates](../admin/page-templates.md) - Admin page patterns - [Icons Guide](../../development/icons-guide.md) - Available icons