Document reusable UI components and patterns: - Page layout structure - Page header component - Stats cards - Search and filters bar - Data table structure - Pagination component Provides reference for consistent UI development. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
12 KiB
12 KiB
UI Components
This document describes the reusable UI components and patterns used in the Wizamart admin panel.
Page Layout Structure
All admin list pages follow a consistent structure:
┌─────────────────────────────────────────────────────┐
│ Page Header (Title + Action Button) │
├─────────────────────────────────────────────────────┤
│ Stats Cards (4 columns on desktop) │
├─────────────────────────────────────────────────────┤
│ Search & Filters Bar │
├─────────────────────────────────────────────────────┤
│ Data Table │
│ ├── Table Header │
│ ├── Table Rows │
│ └── Pagination │
└─────────────────────────────────────────────────────┘
Page Header
The page header contains the page title and primary action button.
<div class="flex items-center justify-between my-6">
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">
Page Title
</h2>
<a href="/admin/resource/create"
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>
Create Resource
</a>
</div>
Stats Cards
Stats cards display key metrics in a 4-column grid layout.
Structure
<div class="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
<!-- Card Template -->
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-{color}-500 bg-{color}-100 rounded-full dark:text-{color}-100 dark:bg-{color}-500">
<span x-html="$icon('icon-name', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Label
</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="value">
0
</p>
</div>
</div>
</div>
Color Options
| Color | Use Case | Example |
|---|---|---|
blue |
Total counts | Total Users, Total Companies |
green |
Positive status | Active, Verified |
red |
Negative status | Inactive, Errors |
orange |
Special/Admin | Admin users, Warnings |
purple |
Primary/Vendors | Active vendors, Main metrics |
Icon Style
- Icons should be inside a circular container (
rounded-full) - Icon size:
w-5 h-5 - Container padding:
p-3
Search & Filters Bar
The search and filters bar provides filtering capabilities for list pages.
Structure
<div class="mb-6 px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<!-- Search Input -->
<div class="flex-1 max-w-md">
<div class="relative">
<input
type="text"
x-model="filters.search"
@input="debouncedSearch()"
placeholder="Search..."
class="w-full pl-10 pr-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:bg-gray-700 dark:text-gray-300"
>
<div class="absolute inset-y-0 left-0 flex items-center pl-3">
<span x-html="$icon('search', 'w-5 h-5 text-gray-400')"></span>
</div>
</div>
</div>
<!-- Filter Dropdowns -->
<div class="flex gap-3">
<select
x-model="filters.filterName"
@change="loadData()"
class="px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300"
>
<option value="">All Options</option>
<option value="value1">Option 1</option>
<option value="value2">Option 2</option>
</select>
</div>
</div>
</div>
JavaScript Implementation
// State
filters: {
search: '',
status: '',
type: ''
},
// Debounced search function
debouncedSearch() {
if (this._searchTimeout) {
clearTimeout(this._searchTimeout);
}
this._searchTimeout = setTimeout(() => {
this.pagination.page = 1; // Reset to first page
this.loadData();
}, 300);
},
// Load data with filters
async loadData() {
const params = new URLSearchParams();
params.append('page', this.pagination.page);
params.append('per_page', this.pagination.per_page);
if (this.filters.search) {
params.append('search', this.filters.search);
}
if (this.filters.status) {
params.append('status', this.filters.status);
}
const response = await apiClient.get(`/admin/resource?${params}`);
// Handle response...
}
Backend API Support
The API endpoint should support these query parameters:
| Parameter | Type | Description |
|---|---|---|
page |
int | Page number (1-based) |
per_page |
int | Items per page |
search |
string | Search term (searches multiple fields) |
{filter} |
string | Filter by specific field |
Example API implementation:
@router.get("", response_model=ListResponse)
def get_all(
page: int = Query(1, ge=1),
per_page: int = Query(10, ge=1, le=100),
search: str = Query("", description="Search term"),
status: str = Query("", description="Filter by status"),
db: Session = Depends(get_db),
):
query = db.query(Model)
# Apply search
if search:
search_term = f"%{search.lower()}%"
query = query.filter(
(Model.name.ilike(search_term)) |
(Model.email.ilike(search_term))
)
# Apply filters
if status:
query = query.filter(Model.status == status)
# Pagination
total = query.count()
items = query.offset((page - 1) * per_page).limit(per_page).all()
return ListResponse(items=items, total=total, page=page, ...)
Data Table
Structure
<div 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">Column</th>
<!-- More columns -->
<th class="px-4 py-3">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
<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">
<!-- Columns -->
</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 dark:text-gray-400 dark:bg-gray-800">
<!-- Pagination controls -->
</div>
</div>
Action Buttons
Standard action buttons for table rows:
<div class="flex items-center space-x-2 text-sm">
<!-- View -->
<a :href="'/admin/resource/' + item.id"
class="flex items-center justify-center p-2 text-blue-600 rounded-lg hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-gray-700"
title="View">
<span x-html="$icon('eye', 'w-5 h-5')"></span>
</a>
<!-- Edit -->
<a :href="'/admin/resource/' + item.id + '/edit'"
class="flex items-center justify-center p-2 text-purple-600 rounded-lg hover:bg-purple-50 dark:text-purple-400 dark:hover:bg-gray-700"
title="Edit">
<span x-html="$icon('edit', 'w-5 h-5')"></span>
</a>
<!-- Delete -->
<button @click="deleteItem(item)"
class="flex items-center justify-center p-2 text-red-600 rounded-lg hover:bg-red-50 dark:text-red-400 dark:hover:bg-gray-700"
title="Delete">
<span x-html="$icon('delete', 'w-5 h-5')"></span>
</button>
</div>
Status Badges
<!-- Active/Inactive -->
<span class="px-2 py-1 font-semibold leading-tight rounded-full text-xs"
:class="item.is_active
? 'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100'
: 'text-red-700 bg-red-100 dark:bg-red-700 dark:text-red-100'"
x-text="item.is_active ? 'Active' : 'Inactive'">
</span>
<!-- Role Badge -->
<span class="px-2 py-1 font-semibold leading-tight rounded-full text-xs capitalize"
:class="{
'text-orange-700 bg-orange-100 dark:bg-orange-700 dark:text-orange-100': item.role === 'admin',
'text-purple-700 bg-purple-100 dark:bg-purple-700 dark:text-purple-100': item.role === 'vendor'
}"
x-text="item.role">
</span>
Loading & Error States
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...</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 loading data</p>
<p class="text-sm" x-text="error"></p>
</div>
</div>
Empty State
<template x-if="items.length === 0">
<tr>
<td colspan="7" 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 text-gray-400 mb-4')"></span>
<p class="text-lg font-medium">No items found</p>
<p class="text-sm" x-text="filters.search ? 'Try adjusting your search' : 'Create your first item'"></p>
</div>
</td>
</tr>
</template>
Pages Using These Components
| Page | Stats | Search | Filters |
|---|---|---|---|
/admin/users |
Total, Active, Inactive, Admins | Name, Email, Username | Role, Status |
/admin/companies |
Total, Verified, Active, Vendors | Name, Email, Owner | Status |
/admin/vendors |
Total, Verified, Active, Products | Name, Code, Subdomain | Status, Company |
JavaScript Module Structure
Each list page follows this pattern:
function adminResourceList() {
return {
// Inherit base layout
...data(),
// Page identifier (for sidebar highlighting)
currentPage: 'resource-name',
// State
items: [],
loading: false,
error: null,
filters: { search: '', status: '' },
stats: {},
pagination: { page: 1, per_page: 10, total: 0, pages: 0 },
// Initialize
async init() {
await this.loadItems();
await this.loadStats();
},
// Format helpers
formatDate(dateString) {
return dateString ? Utils.formatDate(dateString) : '-';
},
// Data loading
async loadItems() { /* ... */ },
async loadStats() { /* ... */ },
// Search & filters
debouncedSearch() { /* ... */ },
// Pagination
nextPage() { /* ... */ },
previousPage() { /* ... */ },
// Actions
async deleteItem(item) { /* ... */ }
};
}