# 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 %}
{% 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:** Vendor 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:** Vendor 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.vendors
window.LogConfig.loggers.vendorTheme
window.LogConfig.loggers.users
window.LogConfig.loggers.products
window.LogConfig.loggers.orders
window.LogConfig.loggers.imports
window.LogConfig.loggers.audit
```
### 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.