Files
orion/docs/frontend/ui-components.md
Samir Boulahtit a33cf1e95d docs: add UI components documentation
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>
2025-12-03 21:38:06 +01:00

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) { /* ... */ }
    };
}