Django

⌘K
  1. Home
  2. Django
  3. Htmx
  4. Project Based
  5. Project Setup

Project Setup

আপনি একদম ঠিক বলেছেন! HTMX দিয়ে SPA (Single Page Application) তৈরি করলে অনেক দ্রুত হবে। আমি আপনার জন্য একটি সম্পূর্ণ HTMX SPA তৈরি করব যেখানে পুরো পেজ রিলোড হবে না।

ধাপ ১: প্রজেক্ট সেটআপ (SPA Style)

# ভার্চুয়াল এনভায়রনমেন্ট তৈরি
python -m venv venv
venv\Scripts\activate

# প্রয়োজনীয় প্যাকেজ ইনস্টল
pip install django python-decouple gunicorn whitenoise

# প্রজেক্ট তৈরি
django-admin startproject config .
python manage.py startapp permission

# ফোল্ডার স্ট্রাকচার
mkdir static static/css static/js static/images media templates templates/partials

ধাপ ২: Settings.py (SPA অপটিমাইজড)

from pathlib import Path
from decouple import config
import os

BASE_DIR = Path(__file__).resolve().parent.parent

SECRET_KEY = config('SECRET_KEY', default='django-secret-key-change-in-production')
DEBUG = config('DEBUG', default=True, cast=bool)
ALLOWED_HOSTS = ['*']

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'permission',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'config.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}
# CSRF Settings
CSRF_COOKIE_HTTPONLY = False  # JavaScript থেকে CSRF টোকেন অ্যাক্সেস করার জন্য
CSRF_COOKIE_SAMESITE = 'Lax'
CSRF_TRUSTED_ORIGINS = [
    'http://127.0.0.1:8000',
    'http://localhost:8000',
]

# HTMX এর জন্য CSRF হেডার
CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN'
# স্ট্যাটিক ফাইল (SPA অপটিমাইজড)
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [BASE_DIR / 'static']

# WhiteNoise ক্যাশিং (দ্রুততার জন্য)
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
WHITENOISE_MAX_AGE = 31536000

MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'

LOGIN_URL = '/'
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/'

LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'Asia/Dhaka'
USE_I18N = True
USE_TZ = True

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

ধাপ ৩: URLs (SPA Style)

from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('permission.urls')),
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

ধাপ ১০: ডাটাবেস ও সুপারইউজার

# ডাটাবেস মাইগ্রেশন
python manage.py makemigrations
python manage.py migrate

# সুপারইউজার তৈরি
python manage.py createsuperuser
# ইউজারনেম: admin
# পাসওয়ার্ড: admin

ধাপ ১১: .env ফাইল

SECRET_KEY=your-secret-key-here-change-in-production
DEBUG=True

ধাপ ১২: প্রজেক্ট চালানো

# ডেভেলপমেন্ট সার্ভার চালান
python manage.py runserver

ধাপ ১৩: Requirements.txt

asgiref==3.9.1
Django==5.2.4
gunicorn==23.0.0
packaging==25.0
python-decouple==3.8
sqlparse==0.5.3
tzdata==2025.2
whitenoise==6.9.0

htmx-project
├─ .env
├─ config
  ├─ asgi.py
  ├─ settings.py
  ├─ urls.py
  ├─ wsgi.py
  ├─ __init__.py
  └─ __pycache__
     ├─ settings.cpython-313.pyc
     ├─ urls.cpython-313.pyc
     ├─ wsgi.cpython-313.pyc
     └─ __init__.cpython-313.pyc
├─ db.sqlite3
├─ manage.py
├─ media
├─ permission
  ├─ admin.py
  ├─ apps.py
  ├─ migrations
    └─ __init__.py
  ├─ models.py
  ├─ tests.py
  ├─ urls.py
  ├─ views.py
  └─ __init__.py
├─ project-hierarchy.txt
├─ requirements.txt
├─ static
  ├─ css
  ├─ images
  └─ js
└─ templates
   ├─ dashboard.html
   ├─ login.html
   ├─ partials
   ├─ spa_app.html
   └─ users.html

urls.py in permission

from django.urls import path
from . import views

app_name = 'permission'

urlpatterns = [
    path('', views.spa_app, name='spa_app'),
    path('login/', views.login_view, name='login'),
    path('dashboard/', views.dashboard_view, name='dashboard'),
    path('users/', views.users_view, name='users'),
    path('logout/', views.logout_view, name='logout'),
]

ধাপ ৪: Views (SPA অপটিমাইজড)

from django.shortcuts import render, redirect
from django.contrib.auth import authenticate, login as auth_login, logout as auth_logout
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.http import HttpResponse

def spa_app(request):
    """Main SPA application view"""
    return render(request, 'spa_app.html')

def login_view(request):
    """Login view - returns login form or processes login"""
    if request.method == 'POST':
        username = request.POST.get('username')
        password = request.POST.get('password')
        
        user = authenticate(request, username=username, password=password)
        if user is not None:
            auth_login(request, user)
            # Return the main SPA with sidebar and header
            return render(request, 'spa_app.html')
        else:
            messages.error(request, 'Invalid username or password')
            return render(request, 'login.html')
    
    # GET request - return login form
    return render(request, 'login.html')

@login_required
def dashboard_view(request):
    """Dashboard view - returns only dashboard content"""
    return render(request, 'dashboard.html')

@login_required
def users_view(request):
    """Users view - returns only users table content"""
    return render(request, 'users.html')

@login_required
def logout_view(request):
    """Logout view"""
    auth_logout(request)
    return render(request, 'login.html')
<!DOCTYPE html>
<html lang="en" class="light">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Premium ShadCN Dashboard</title>
  
  <!-- CSRF Token for HTMX -->
  <meta name="csrf-token" content="{{ csrf_token }}">
  
  <!-- HTMX -->
  <script src="https://unpkg.com/htmx.org@1.9.10"></script>
  
  <script src="https://cdn.tailwindcss.com"></script>
  <script>
    tailwind.config = {
      darkMode: 'class',
      theme: {
        extend: {
          colors: {
            sidebar: {
              DEFAULT: 'var(--sidebar-bg)',
              foreground: 'var(--sidebar-fg)',
              border: 'var(--sidebar-border)',
              accent: 'var(--sidebar-accent)',
              'accent-foreground': 'var(--sidebar-accent-fg)',
              primary: 'var(--sidebar-primary)',
              'primary-foreground': 'var(--sidebar-primary-fg)',
              ring: 'var(--sidebar-ring)',
            },
            background: 'var(--background)',
            foreground: 'var(--foreground)',
            muted: 'var(--muted)',
            'muted-foreground': 'var(--muted-foreground)',
            border: 'var(--border)',
            accent: 'var(--accent)',
            'accent-foreground': 'var(--accent-foreground)',
            card: 'var(--card)',
            'card-foreground': 'var(--card-foreground)',
            primary: 'var(--primary)',
            'primary-foreground': 'var(--primary-foreground)',
            secondary: 'var(--secondary)',
            'secondary-foreground': 'var(--secondary-foreground)',
            destructive: 'var(--destructive)',
            'destructive-foreground': 'var(--destructive-foreground)',
            ring: 'var(--ring)',
            success: 'var(--success)',
            warning: 'var(--warning)',
            info: 'var(--info)',
          },
          fontFamily: {
            sans: ['Inter', 'sans-serif'],
          },
          borderRadius: {
            lg: 'var(--radius)',
            md: 'calc(var(--radius) - 2px)',
            sm: 'calc(var(--radius) - 4px)',
          },
          keyframes: {
            'fade-in-up': {
              '0%': { transform: 'translateY(10px)', opacity: '0' },
              '100%': { transform: 'translateY(0)', opacity: '1' },
            },
            'pulse-ring': {
              '0%': { transform: 'scale(0.8)', opacity: '0.8' },
              '100%': { transform: 'scale(2)', opacity: '0' },
            },
          },
          animation: {
            'fade-in-up': 'fade-in-up 0.4s ease-out',
            'pulse-ring': 'pulse-ring 1.5s cubic-bezier(0.24, 0, 0.38, 1) infinite',
          },
        }
      }
    };
  </script>
  
  <style>
    @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');

    :root {
      --radius: 0.5rem;
      --sidebar-width: 16rem;
      --sidebar-width-icon: 4rem;
      --sidebar-bg: #ffffff;
      --sidebar-fg: #09090b;
      --sidebar-border: #e2e8f0;
      --sidebar-accent: #f1f5f9;
      --sidebar-accent-fg: #09090b;
      --sidebar-primary: #f1f5f9;
      --sidebar-primary-fg: #09090b;
      --sidebar-ring: rgba(0, 0, 0, 0.1);
      --background: #ffffff;
      --foreground: #09090b;
      --muted: #f1f5f9;
      --muted-foreground: #64748b;
      --border: #e2e8f0;
      --accent: #f1f5f9;
      --accent-foreground: #09090b;
      --card: #ffffff;
      --card-foreground: #09090b;
      --primary: #0f172a;
      --primary-foreground: #f8fafc;
      --primary-rgb: 15, 23, 42;
      --secondary: #f1f5f9;
      --secondary-foreground: #0f172a;
      --destructive: #ef4444;
      --destructive-foreground: #f8fafc;
      --ring: rgba(0, 0, 0, 0.1);
      --success: #10b981;
      --warning: #f59e0b;
      --info: #3b82f6;
      --spacing: 0.25rem;
    }

    .dark {
      --sidebar-bg: #09090b;
      --sidebar-fg: #f8fafc;
      --sidebar-border: #1e293b;
      --sidebar-accent: #1e293b;
      --sidebar-accent-fg: #f8fafc;
      --sidebar-primary: #1e293b;
      --sidebar-primary-fg: #f8fafc;
      --sidebar-ring: rgba(255, 255, 255, 0.1);
      --background: #09090b;
      --foreground: #f8fafc;
      --muted: #1e293b;
      --muted-foreground: #94a3b8;
      --border: #1e293b;
      --accent: #1e293b;
      --accent-foreground: #f8fafc;
      --card: #09090b;
      --card-foreground: #f8fafc;
      --primary: #f8fafc;
      --primary-foreground: #0f172a;
      --primary-rgb: 248, 250, 252;
      --secondary: #1e293b;
      --secondary-foreground: #f8fafc;
      --destructive: #ef4444;
      --destructive-foreground: #f8fafc;
      --ring: rgba(255, 255, 255, 0.1);
      --success: #10b981;
      --warning: #f59e0b;
      --info: #3b82f6;
    }

    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    body {
      font-family: 'Inter', sans-serif;
      overflow-x: hidden;
    }

    .sidebar-expanded {
      width: var(--sidebar-width);
      transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
    }

    .sidebar-collapsed {
      width: var(--sidebar-width-icon);
      transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
    }

    .sidebar-menu-item-collapsed span,
    .sidebar-menu-item-collapsed .sidebar-group-label,
    .sidebar-menu-item-collapsed .menu-action,
    .sidebar-menu-item-collapsed .menu-sub {
      display: none;
    }

    .sidebar-menu-item-collapsed svg:not(.chevron-icon) {
      margin-left: auto;
      margin-right: auto;
    }

    .menu-sub {
      max-height: 0;
      overflow: hidden;
      transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1);
      opacity: 0;
    }

    .menu-sub.open {
      max-height: 500px;
      opacity: 1;
    }

    .dropdown-menu {
      opacity: 0;
      transform: translateY(10px);
      visibility: hidden;
      transition: opacity 0.2s ease, transform 0.2s ease, visibility 0s linear 0.2s;
    }

    .dropdown-menu.open {
      opacity: 1;
      transform: translateY(0);
      visibility: visible;
      transition: opacity 0.2s ease, transform 0.2s ease, visibility 0s linear 0s;
    }

    @media (max-width: 768px) {
      .sidebar {
        transform: translateX(-100%);
        position: fixed;
        z-index: 50;
        height: 100vh;
        transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
      }
      
      .sidebar.open {
        transform: translateX(0);
      }
      
      .sidebar-overlay {
        display: none;
        transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
        opacity: 0;
      }
      
      .sidebar-overlay.active {
        display: block;
        opacity: 1;
      }
    }

    /* Custom scrollbar */
    ::-webkit-scrollbar {
      width: 6px;
      height: 6px;
    }

    ::-webkit-scrollbar-track {
      background: transparent;
    }

    ::-webkit-scrollbar-thumb {
      background: var(--muted-foreground);
      opacity: 0.5;
      border-radius: 3px;
    }

    ::-webkit-scrollbar-thumb:hover {
      background: var(--muted-foreground);
      opacity: 0.8;
    }

    /* Tooltip */
    .tooltip {
      position: relative;
    }

    .tooltip::after {
      content: attr(data-tooltip);
      position: absolute;
      left: 100%;
      top: 50%;
      transform: translateY(-50%);
      background: var(--foreground);
      color: var(--background);
      padding: 0.25rem 0.5rem;
      border-radius: 0.25rem;
      font-size: 0.75rem;
      white-space: nowrap;
      opacity: 0;
      pointer-events: none;
      transition: opacity 0.2s ease, transform 0.2s ease;
      margin-left: 0.5rem;
      z-index: 100;
      box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
    }

    .tooltip:hover::after {
      opacity: 1;
    }

    .sidebar-collapsed .tooltip:hover::after {
      opacity: 1;
    }

    /* Notification badge */
    .notification-badge {
      position: absolute;
      top: -2px;
      right: -2px;
      width: 16px;
      height: 16px;
      border-radius: 50%;
      background-color: var(--destructive);
      color: var(--destructive-foreground);
      font-size: 10px;
      display: flex;
      align-items: center;
      justify-content: center;
      font-weight: 600;
    }

    /* Pulse animation for notification dot */
    .pulse-dot {
      position: relative;
    }

    .pulse-dot::before {
      content: '';
      position: absolute;
      top: 0;
      right: 0;
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background-color: var(--success);
      z-index: 1;
    }

    .pulse-dot::after {
      content: '';
      position: absolute;
      top: 0;
      right: 0;
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background-color: var(--success);
      animation: pulse-ring 1.5s cubic-bezier(0.24, 0, 0.38, 1) infinite;
      z-index: 0;
    }

    /* Glass morphism effect */
    .glass-effect {
      background: rgba(255, 255, 255, 0.1);
      backdrop-filter: blur(10px);
      -webkit-backdrop-filter: blur(10px);
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .dark .glass-effect {
      background: rgba(0, 0, 0, 0.2);
      border: 1px solid rgba(255, 255, 255, 0.05);
    }

    /* Gradient text */
    .gradient-text {
      background: linear-gradient(90deg, #3b82f6, #10b981);
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      color: transparent;
    }

    .dark .gradient-text {
      background: linear-gradient(90deg, #60a5fa, #34d399);
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
      color: transparent;
    }

    /* Glow effect */
    .glow-effect {
      box-shadow: 0 0 10px rgba(var(--primary-rgb), 0.3);
      transition: box-shadow 0.3s ease;
    }

    .glow-effect:hover {
      box-shadow: 0 0 20px rgba(var(--primary-rgb), 0.5);
    }

    /* Hover card effect */
    .hover-card {
      transition: transform 0.2s ease, box-shadow 0.2s ease;
    }

    .hover-card:hover {
      transform: translateY(-5px);
      box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
    }

    .dark .hover-card:hover {
      box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 10px 10px -5px rgba(0, 0, 0, 0.2);
    }

    /* Progress bar animation */
    @keyframes progress {
      0% { width: 0%; }
      100% { width: 100%; }
    }

    .animate-progress {
      animation: progress 2s cubic-bezier(0.4, 0, 0.2, 1);
    }

    /* Shimmer loading effect */
    @keyframes shimmer {
      0% { background-position: -1000px 0; }
      100% { background-position: 1000px 0; }
    }

    .shimmer {
      background: linear-gradient(90deg, var(--card) 0%, var(--muted) 50%, var(--card) 100%);
      background-size: 1000px 100%;
      animation: shimmer 2s infinite linear;
    }

    /* Sidebar rail handle */
    .sidebar-rail {
      position: absolute;
      right: -4px;
      top: 50%;
      transform: translateY(-50%);
      width: 8px;
      height: 50px;
      background-color: var(--sidebar-bg);
      border: 1px solid var(--sidebar-border);
      border-radius: 4px;
      cursor: ew-resize;
      display: flex;
      align-items: center;
      justify-content: center;
      z-index: 20;
      transition: background-color 0.2s ease;
    }

    .sidebar-rail:hover {
      background-color: var(--sidebar-accent);
    }

    .sidebar-rail::before {
      content: '';
      width: 2px;
      height: 20px;
      background-color: var(--sidebar-border);
      border-radius: 1px;
    }

    /* Sidebar footer gradient separator */
    .footer-separator {
      height: 1px;
      background: linear-gradient(90deg, transparent, var(--sidebar-border), transparent);
      margin: 8px 0;
    }

    /* Notification bell animation */
    @keyframes bell-ring {
      0%, 100% { transform: rotate(0); }
      20%, 60% { transform: rotate(8deg); }
      40%, 80% { transform: rotate(-8deg); }
    }

    .animate-bell {
      animation: bell-ring 0.8s ease;
    }

    /* Sidebar menu item active indicator */
    .menu-item-active-indicator {
      position: absolute;
      left: 0;
      top: 50%;
      transform: translateY(-50%);
      width: 3px;
      height: 60%;
      background-color: var(--primary);
      border-radius: 0 3px 3px 0;
      opacity: 0;
      transition: opacity 0.2s ease;
    }

    .menu-item-active .menu-item-active-indicator {
      opacity: 1;
    }

    /* Sidebar menu item hover effect */
    .menu-item-hover-effect {
      position: relative;
      overflow: hidden;
    }

    .menu-item-hover-effect::after {
      content: '';
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background-color: var(--sidebar-accent);
      opacity: 0;
      transform: scale(0.8);
      transition: opacity 0.2s ease, transform 0.2s ease;
      border-radius: var(--radius);
      z-index: -1;
    }

    .menu-item-hover-effect:hover::after {
      opacity: 1;
      transform: scale(1);
    }

    /* HTMX Loading States */
    .htmx-request {
      opacity: 0.7;
      transition: opacity 0.3s ease;
    }

    .loading-spinner {
      width: 20px;
      height: 20px;
      border: 2px solid #f3f3f3;
      border-top: 2px solid var(--primary);
      border-radius: 50%;
      animation: spin 1s linear infinite;
    }

    @keyframes spin {
      0% { transform: rotate(0deg); }
      100% { transform: rotate(360deg); }
    }

    /* Page Transitions */
    .fade-in {
      animation: fadeIn 0.3s ease-in-out;
    }

    @keyframes fadeIn {
      from { opacity: 0; transform: translateY(10px); }
      to { opacity: 1; transform: translateY(0); }
    }
  </style>
</head>
<body class="bg-background text-foreground antialiased">
  <!-- SPA Container -->
  <div id="spa-container">
    {% if user.is_authenticated %}
      <!-- Dashboard Layout with Sidebar + Header -->
      <div class="flex min-h-screen">
        <!-- Sidebar Overlay (Mobile) -->
        <div id="sidebar-overlay" class="fixed inset-0 bg-black/50 backdrop-blur-sm z-40 hidden md:hidden"></div>
        
        <!-- Sidebar -->
        <aside id="sidebar" class="sidebar sidebar-expanded bg-sidebar text-sidebar-foreground border-r border-sidebar-border h-screen transition-all duration-300 ease-in-out z-30">
          <!-- Sidebar Header -->
          <div class="p-3 flex flex-col gap-2">
            <ul class="flex w-full min-w-0 flex-col gap-1">
              <li class="group relative">
                <button class="flex w-full items-center gap-3 overflow-hidden rounded-md p-2.5 text-left h-14 text-sm hover:bg-sidebar-accent hover:text-sidebar-accent-foreground transition-all duration-200 glow-effect">
                  <div class="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-9 items-center justify-center rounded-lg shadow-sm">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-5">
                      <path d="M7 2h10"></path>
                      <path d="M5 6h14"></path>
                      <rect width="18" height="12" x="3" y="10" rx="2"></rect>
                    </svg>
                  </div>
                  <div class="grid flex-1 text-left text-sm leading-tight">
                    <span class="truncate font-semibold text-base">Acme Inc</span>
                    <span class="truncate text-xs text-sidebar-foreground/70">Enterprise Plan</span>
                  </div>
                </button>
              </li>
            </ul>
          </div>
          
          <!-- Search Bar -->
          <div class="px-3 mb-2">
            <div class="relative">
              <input type="text" placeholder="Search..." class="w-full h-9 px-3 py-2 bg-sidebar-accent/50 border border-sidebar-border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all duration-200 pl-9">
              <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="absolute left-3 top-1/2 transform -translate-y-1/2 size-4 text-sidebar-foreground/50">
                <circle cx="11" cy="11" r="8"></circle>
                <path d="m21 21-4.3-4.3"></path>
              </svg>
            </div>
          </div>
          
          <!-- Sidebar Content -->
          <div class="flex min-h-0 flex-1 flex-col gap-2 overflow-auto px-2">
            <div class="relative flex w-full min-w-0 flex-col p-1">
              <div class="sidebar-group-label text-sidebar-foreground/70 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium">
                <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-3.5 mr-1.5">
                  <rect width="18" height="18" x="3" y="3" rx="2"></rect>
                  <path d="M7 7h10"></path>
                  <path d="M7 12h10"></path>
                  <path d="M7 17h10"></path>
                </svg>
                Platform
              </div>
              <ul class="flex w-full min-w-0 flex-col gap-1 mt-1">
                <!-- Dashboard Item -->
                <li class="group relative">
                  <a href="#" 
                     hx-get="{% url 'permission:dashboard' %}" 
                     hx-target="#main-content" 
                     hx-swap="innerHTML"
                     class="sidebar-menu-item menu-item-hover-effect flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left h-9 text-sm hover:text-sidebar-accent-foreground transition-colors"
                     onclick="setActiveMenu(this)">
                    <div class="menu-item-active-indicator"></div>
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4">
                      <rect width="7" height="9" x="3" y="3" rx="1"></rect>
                      <rect width="7" height="5" x="14" y="3" rx="1"></rect>
                      <rect width="7" height="9" x="14" y="12" rx="1"></rect>
                      <rect width="7" height="5" x="3" y="16" rx="1"></rect>
                    </svg>
                    <span>Dashboard</span>
                  </a>
                </li>
                
                <!-- Users Item -->
                <li class="group relative">
                  <a href="#" 
                     hx-get="{% url 'permission:users' %}" 
                     hx-target="#main-content" 
                     hx-swap="innerHTML"
                     class="sidebar-menu-item menu-item-hover-effect flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left h-9 text-sm hover:text-sidebar-accent-foreground transition-colors"
                     onclick="setActiveMenu(this)">
                    <div class="menu-item-active-indicator"></div>
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4">
                      <path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path>
                      <circle cx="9" cy="7" r="4"></circle>
                      <path d="M22 21v-2a4 4 0 0 0-3-3.87"></path>
                      <path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
                    </svg>
                    <span>Users</span>
                  </a>
                </li>
              </ul>
            </div>
          </div>
          
          <!-- Sidebar Footer -->
          <div class="flex flex-col gap-2 p-3 mt-auto">
            <div class="footer-separator"></div>
            
            <!-- Theme Toggle -->
            <div class="flex items-center justify-between px-2 py-1">
              <span class="text-xs font-medium text-sidebar-foreground/70">Theme</span>
              <button onclick="toggleTheme()" class="relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent bg-sidebar-accent transition-colors duration-200 ease-in-out focus:outline-none">
                <span class="sr-only">Toggle theme</span>
                <span id="theme-toggle-dot" class="pointer-events-none relative inline-block h-4 w-4 transform rounded-full bg-sidebar-foreground shadow ring-0 transition duration-200 ease-in-out translate-x-0 dark:translate-x-4">
                  <span class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity dark:opacity-0">
                    <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 text-sidebar-accent-foreground" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                      <circle cx="12" cy="12" r="4"></circle>
                      <path d="M12 2v2"></path>
                      <path d="M12 20v2"></path>
                      <path d="m4.93 4.93 1.41 1.41"></path>
                      <path d="m17.66 17.66 1.41 1.41"></path>
                      <path d="M2 12h2"></path>
                      <path d="M20 12h2"></path>
                      <path d="m6.34 17.66-1.41 1.41"></path>
                      <path d="m19.07 4.93-1.41 1.41"></path>
                    </svg>
                  </span>
                  <span class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity opacity-0 dark:opacity-100">
                    <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 text-sidebar-accent-foreground" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                      <path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"></path>
                    </svg>
                  </span>
                </span>
              </button>
            </div>
            
            <!-- User Profile -->
            <ul class="flex w-full min-w-0 flex-col gap-1">
              <li class="group relative">
                <button class="sidebar-menu-item flex w-full items-center gap-3 overflow-hidden rounded-md p-2.5 text-left h-14 text-sm hover:bg-sidebar-accent hover:text-sidebar-accent-foreground transition-all duration-200 glow-effect">
                  <span class="relative flex size-9 shrink-0 overflow-hidden h-9 w-9 rounded-lg">
                    <img class="aspect-square size-full object-cover" alt="User" src="https://ui.shadcn.com/avatars/shadcn.jpg">
                    <span class="absolute bottom-0 right-0 h-2.5 w-2.5 rounded-full bg-success border-2 border-sidebar-bg"></span>
                  </span>
                  <div class="grid flex-1 text-left text-sm leading-tight">
                    <span class="truncate font-semibold">{{ user.username }}</span>
                    <span class="truncate text-xs text-sidebar-foreground/70">{{ user.email }}</span>
                  </div>
                </button>
              </li>
            </ul>
          </div>
          
          <!-- Sidebar Toggle Button -->
          <div id="sidebar-rail" class="sidebar-rail" onclick="toggleSidebarCollapse()">
            <span class="sr-only">Resize Sidebar</span>
          </div>
        </aside>
        
        <!-- Main Content -->
        <main class="flex-1 bg-background">
          <!-- Header -->
          <header class="flex h-16 shrink-0 items-center gap-2 border-b border-border px-4 shadow-sm">
            <button onclick="toggleMobileSidebar()" id="mobile-sidebar-toggle" class="md:hidden inline-flex items-center justify-center rounded-md text-sm font-medium h-9 w-9 text-foreground hover:bg-accent hover:text-accent-foreground transition-colors">
              <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-5">
                <rect width="18" height="18" x="3" y="3" rx="2"></rect>
                <path d="M9 3v18"></path>
              </svg>
              <span class="sr-only">Toggle Sidebar</span>
            </button>
            
            <div class="h-4 w-px bg-border mx-2 hidden md:block"></div>
            
            <nav aria-label="breadcrumb">
              <ol class="flex flex-wrap items-center gap-1.5 text-sm text-muted-foreground">
                <li class="inline-flex items-center gap-1.5">
                  <span id="breadcrumb-title" class="text-foreground font-medium">Dashboard</span>
                </li>
              </ol>
            </nav>
            
            <div class="ml-auto flex items-center gap-3">
              <!-- Theme Toggle -->
              <button onclick="toggleTheme()" id="theme-toggle" class="inline-flex items-center justify-center rounded-md text-sm font-medium h-9 w-9 text-foreground hover:bg-accent hover:text-accent-foreground transition-colors">
                <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-5 dark:hidden">
                  <circle cx="12" cy="12" r="4"></circle>
                  <path d="M12 2v2"></path>
                  <path d="M12 20v2"></path>
                  <path d="m4.93 4.93 1.41 1.41"></path>
                  <path d="m17.66 17.66 1.41 1.41"></path>
                  <path d="M2 12h2"></path>
                  <path d="M20 12h2"></path>
                  <path d="m6.34 17.66-1.41 1.41"></path>
                  <path d="m19.07 4.93-1.41 1.41"></path>
                </svg>
                <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-5 hidden dark:block">
                  <path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"></path>
                </svg>
                <span class="sr-only">Toggle theme</span>
              </button>
              
              <!-- Logout Button -->
              <button 
                hx-post="{% url 'permission:logout' %}" 
                hx-target="#spa-container" 
                hx-swap="innerHTML"
                class="inline-flex items-center justify-center rounded-md text-sm font-medium h-9 px-4 py-2 border border-border bg-background shadow-sm hover:bg-accent transition-colors"
              >
                <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 mr-2">
                  <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
                  <polyline points="16 17 21 12 16 7"></polyline>
                  <line x1="21" x2="9" y1="12" y2="12"></line>
                </svg>
                Logout
              </button>
            </div>
          </header>
          
          <!-- Dynamic Content Area -->
          <div id="main-content" class="flex flex-1 flex-col">
            <!-- Load Dashboard by default -->
            <div hx-get="{% url 'permission:dashboard' %}" hx-trigger="load" hx-target="this" hx-swap="outerHTML">
              <div class="p-6">
                <div class="text-center">
                  <div class="loading-spinner mx-auto mb-4"></div>
                  <p class="text-muted-foreground">Loading Dashboard...</p>
                </div>
              </div>
            </div>
          </div>
        </main>
      </div>
    {% else %}
      <!-- Load Login Form -->
      <div hx-get="{% url 'permission:login' %}" hx-trigger="load" hx-target="#spa-container" hx-swap="innerHTML">
        <div class="min-h-screen flex items-center justify-center">
          <div class="text-center">
            <div class="loading-spinner mx-auto mb-4"></div>
            <p class="text-muted-foreground">Loading...</p>
          </div>
        </div>
      </div>
    {% endif %}
  </div>

  <script>
    // Global Functions
    window.toggleTheme = function() {
      document.documentElement.classList.toggle('dark');
      localStorage.setItem('theme', 
        document.documentElement.classList.contains('dark') ? 'dark' : 'light'
      );
    }

    window.toggleMobileSidebar = function() {
      const sidebar = document.getElementById('sidebar');
      const overlay = document.getElementById('sidebar-overlay');
      if (sidebar && overlay) {
        sidebar.classList.toggle('open');
        overlay.classList.toggle('active');
      }
    }

    window.toggleSidebarCollapse = function() {
      const sidebar = document.getElementById('sidebar');
      if (!sidebar) return;
      
      const isExpanded = sidebar.classList.contains('sidebar-expanded');
      
      if (isExpanded) {
        sidebar.classList.remove('sidebar-expanded');
        sidebar.classList.add('sidebar-collapsed');
        
        const menuItems = document.querySelectorAll('.sidebar-menu-item');
        menuItems.forEach(item => {
          item.classList.add('sidebar-menu-item-collapsed');
          
          if (!item.hasAttribute('data-tooltip') && item.querySelector('span')) {
            const tooltipText = item.querySelector('span').textContent;
            item.setAttribute('data-tooltip', tooltipText);
            item.classList.add('tooltip');
          }
        });
      } else {
        sidebar.classList.remove('sidebar-collapsed');
        sidebar.classList.add('sidebar-expanded');
        
        const menuItems = document.querySelectorAll('.sidebar-menu-item');
        menuItems.forEach(item => {
          item.classList.remove('sidebar-menu-item-collapsed');
        });
      }
    }

    window.setActiveMenu = function(element) {
      // Remove active class from all menu items
      document.querySelectorAll('.sidebar-menu-item').forEach(item => {
        item.classList.remove('menu-item-active');
      });
      
      // Add active class to clicked item
      element.classList.add('menu-item-active');
      
      // Update breadcrumb
      const breadcrumb = document.getElementById('breadcrumb-title');
      const menuText = element.querySelector('span').textContent;
      if (breadcrumb) {
        breadcrumb.textContent = menuText;
      }
    }

    // Load theme
    if (localStorage.getItem('theme') === 'dark') {
      document.documentElement.classList.add('dark');
    }
    
    // HTMX Configuration
    document.addEventListener('DOMContentLoaded', function() {
      document.body.addEventListener('htmx:configRequest', function(event) {
        const token = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
        event.detail.headers['X-CSRFToken'] = token;
      });
      
      // Set Dashboard as active by default
      const dashboardLink = document.querySelector('a[hx-get*="dashboard"]');
      if (dashboardLink) {
        dashboardLink.classList.add('menu-item-active');
      }
    });
    
    // HTMX Event Listeners
    document.addEventListener('htmx:afterSwap', function(event) {
      event.detail.target.classList.add('fade-in');
    });
    
    // Error handling
    document.addEventListener('htmx:responseError', function(event) {
      let errorMessage = 'Something went wrong. Please try again.';
      
      if (event.detail.xhr.status === 403) {
        errorMessage = 'Permission denied. Please reload the page.';
        setTimeout(() => window.location.reload(), 2000);
      }
      
      const errorDiv = document.createElement('div');
      errorDiv.className = 'fixed top-4 right-4 bg-destructive/10 border border-destructive/20 text-destructive px-4 py-3 rounded-lg z-50 max-w-sm shadow-lg';
      errorDiv.innerHTML = `
        <div class="flex items-center">
          <span class="text-sm">${errorMessage}</span>
          <button onclick="this.parentElement.parentElement.remove()" class="ml-4 text-destructive hover:text-destructive/80">✕</button>
        </div>
      `;
      document.body.appendChild(errorDiv);
      
      setTimeout(() => {
        if (errorDiv.parentNode) {
          errorDiv.remove();
        }
      }, 5000);
    });
  </script>
</body>
</html>
<div class="min-h-screen flex items-center justify-center bg-background p-4">
  <div class="w-full max-w-md">
    <div class="bg-card text-card-foreground shadow-lg border border-border rounded-2xl overflow-hidden">
      <!-- Header -->
      <div class="px-8 py-6 bg-gradient-to-r from-primary/5 to-primary/10 border-b border-border">
        <div class="text-center">
          <h1 class="text-2xl font-bold gradient-text mb-2">Welcome Back</h1>
          <p class="text-sm text-muted-foreground">Sign in to your account</p>
        </div>
      </div>
      
      <!-- Form -->
      <div class="px-8 py-6">
        <form hx-post="{% url 'permission:login' %}" hx-target="#spa-container" hx-swap="innerHTML">
          {% csrf_token %}
          
          {% if messages %}
            {% for message in messages %}
              <div class="mb-4 p-3 rounded-lg bg-destructive/10 border border-destructive/20 text-destructive text-sm">
                {{ message }}
              </div>
            {% endfor %}
          {% endif %}
          
          <div class="space-y-4">
            <div>
              <label for="username" class="block text-sm font-medium text-foreground mb-2">Username</label>
              <input 
                type="text" 
                id="username" 
                name="username" 
                required
                class="w-full h-10 px-3 py-2 bg-background border border-border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all"
                placeholder="Enter your username"
              />
            </div>
            
            <div>
              <label for="password" class="block text-sm font-medium text-foreground mb-2">Password</label>
              <input 
                type="password" 
                id="password" 
                name="password" 
                required
                class="w-full h-10 px-3 py-2 bg-background border border-border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all"
                placeholder="Enter your password"
              />
            </div>
            
            <button 
              type="submit" 
              class="w-full h-10 bg-primary text-primary-foreground rounded-md font-medium hover:bg-primary/90 transition-colors flex items-center justify-center"
            >
              <span class="htmx-indicator loading-spinner mr-2" style="display: none;"></span>
              Sign In
            </button>
          </div>
        </form>
      </div>
    </div>
  </div>
</div>
<div class="flex flex-1 flex-col gap-6 p-6">
  <!-- Welcome Section -->
  <div class="bg-card text-card-foreground shadow-sm border border-border rounded-lg p-6 animate-fade-in-up">
    <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
      <div>
        <h1 class="text-2xl font-bold tracking-tight">Welcome back, <span class="gradient-text">{{ user.username }}</span>!</h1>
        <p class="text-muted-foreground mt-1">Here's what's happening with your projects today.</p>
      </div>
      <div class="flex items-center gap-2">
        <button class="inline-flex items-center justify-center rounded-md text-sm font-medium h-9 px-4 py-2 bg-primary text-primary-foreground shadow hover:bg-primary/90 transition-colors">
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 mr-2">
            <path d="M5 12h14"></path>
            <path d="M12 5v14"></path>
          </svg>
          <span>New Project</span>
        </button>
      </div>
    </div>
  </div>
  
  <!-- Stats Cards -->
  <div class="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
    <div class="bg-card text-card-foreground shadow-sm border border-border rounded-lg p-6 hover-card animate-fade-in-up" style="animation-delay: 0.1s;">
      <div class="flex items-center justify-between">
        <div>
          <p class="text-sm font-medium text-muted-foreground">Total Users</p>
          <div class="flex items-baseline gap-1">
            <h3 class="text-2xl font-bold">1,234</h3>
            <p class="text-xs text-success">+20.1%</p>
          </div>
        </div>
        <div class="bg-primary/10 text-primary rounded-full p-2">
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-5">
            <path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path>
            <circle cx="9" cy="7" r="4"></circle>
            <path d="M22 21v-2a4 4 0 0 0-3-3.87"></path>
            <path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
          </svg>
        </div>
      </div>
    </div>
    
    <div class="bg-card text-card-foreground shadow-sm border border-border rounded-lg p-6 hover-card animate-fade-in-up" style="animation-delay: 0.2s;">
      <div class="flex items-center justify-between">
        <div>
          <p class="text-sm font-medium text-muted-foreground">Active Sessions</p>
          <div class="flex items-baseline gap-1">
            <h3 class="text-2xl font-bold">567</h3>
            <p class="text-xs text-success">+18.2%</p>
          </div>
        </div>
        <div class="bg-primary/10 text-primary rounded-full p-2">
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-5">
            <path d="M3 3v18h18"></path>
            <path d="m19 9-5 5-4-4-3 3"></path>
          </svg>
        </div>
      </div>
    </div>
    
    <div class="bg-card text-card-foreground shadow-sm border border-border rounded-lg p-6 hover-card animate-fade-in-up" style="animation-delay: 0.3s;">
      <div class="flex items-center justify-between">
        <div>
          <p class="text-sm font-medium text-muted-foreground">Total Revenue</p>
          <div class="flex items-baseline gap-1">
            <h3 class="text-2xl font-bold">$12,234</h3>
            <p class="text-xs text-destructive">-2.5%</p>
          </div>
        </div>
        <div class="bg-primary/10 text-primary rounded-full p-2">
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-5">
            <path d="M12 2v20"></path>
            <path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>
          </svg>
        </div>
      </div>
    </div>
    
    <div class="bg-card text-card-foreground shadow-sm border border-border rounded-lg p-6 hover-card animate-fade-in-up" style="animation-delay: 0.4s;">
      <div class="flex items-center justify-between">
        <div>
          <p class="text-sm font-medium text-muted-foreground">Growth Rate</p>
          <div class="flex items-baseline gap-1">
            <h3 class="text-2xl font-bold">4.3%</h3>
            <p class="text-xs text-success">+12.2%</p>
          </div>
        </div>
        <div class="bg-primary/10 text-primary rounded-full p-2">
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-5">
            <path d="M12 22V8"></path>
            <path d="m5 12 7-4 7 4"></path>
            <path d="M5 16l7-4 7 4"></path>
            <path d="M5 20l7-4 7 4"></path>
          </svg>
        </div>
      </div>
    </div>
  </div>
  
  <!-- Recent Activity Card -->
  <div class="bg-card text-card-foreground shadow-sm border border-border rounded-lg p-6 animate-fade-in-up" style="animation-delay: 0.5s;">
    <div class="flex items-center justify-between mb-4">
      <h2 class="text-xl font-semibold">Recent Activity</h2>
      <button class="text-sm text-primary hover:underline">View all</button>
    </div>
    <div class="space-y-4">
      <div class="flex items-center gap-3">
        <div class="bg-primary/10 text-primary rounded-full p-2 h-8 w-8 flex items-center justify-center shrink-0">
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4">
            <path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path>
            <polyline points="14 2 14 8 20 8"></polyline>
          </svg>
        </div>
        <div class="flex-1 min-w-0">
          <p class="text-sm font-medium">New report generated</p>
          <p class="text-xs text-muted-foreground">2 hours ago</p>
        </div>
      </div>
      <div class="flex items-center gap-3">
        <div class="bg-primary/10 text-primary rounded-full p-2 h-8 w-8 flex items-center justify-center shrink-0">
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4">
            <path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path>
            <circle cx="9" cy="7" r="4"></circle>
            <path d="M22 21v-2a4 4 0 0 0-3-3.87"></path>
            <path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
          </svg>
        </div>
        <div class="flex-1 min-w-0">
          <p class="text-sm font-medium">New team members added</p>
          <p class="text-xs text-muted-foreground">5 hours ago</p>
        </div>
      </div>
      <div class="flex items-center gap-3">
        <div class="bg-primary/10 text-primary rounded-full p-2 h-8 w-8 flex items-center justify-center shrink-0">
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4">
            <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10"></path>
          </svg>
        </div>
        <div class="flex-1 min-w-0">
          <p class="text-sm font-medium">Security update completed</p>
          <p class="text-xs text-muted-foreground">1 day ago</p>
        </div>
      </div>
    </div>
  </div>
</div>
<div class="flex flex-1 flex-col gap-6 p-6">
  <!-- Users Table -->
  <div class="bg-card text-card-foreground shadow-sm border border-border rounded-lg animate-fade-in-up">
    <div class="flex items-center justify-between p-6 border-b border-border">
      <h2 class="text-xl font-semibold">Users Management</h2>
      <div class="flex items-center gap-2">
        <div class="relative">
          <input type="text" placeholder="Search users..." class="h-9 w-64 rounded-md border border-border bg-background px-3 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all pl-9">
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="absolute left-3 top-1/2 transform -translate-y-1/2 size-4 text-muted-foreground">
            <circle cx="11" cy="11" r="8"></circle>
            <path d="m21 21-4.3-4.3"></path>
          </svg>
        </div>
        <button class="inline-flex items-center justify-center rounded-md text-sm font-medium h-9 px-4 py-2 bg-primary text-primary-foreground shadow hover:bg-primary/90 transition-colors">
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 mr-2">
            <path d="M5 12h14"></path>
            <path d="M12 5v14"></path>
          </svg>
          <span>Add User</span>
        </button>
      </div>
    </div>
    <div class="overflow-x-auto">
      <table class="w-full">
        <thead>
          <tr class="border-b border-border bg-muted/50">
            <th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">User</th>
            <th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Email</th>
            <th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Status</th>
            <th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Role</th>
            <th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Last Login</th>
            <th class="px-6 py-3 text-right text-xs font-medium text-muted-foreground uppercase tracking-wider">Actions</th>
          </tr>
        </thead>
        <tbody class="divide-y divide-border">
          <tr class="hover:bg-muted/30 transition-colors">
            <td class="px-6 py-4 whitespace-nowrap">
              <div class="flex items-center">
                <div class="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center mr-3">
                  <span class="text-sm font-medium text-primary">JD</span>
                </div>
                <div>
                  <div class="font-medium">John Doe</div>
                  <div class="text-sm text-muted-foreground">@johndoe</div>
                </div>
              </div>
            </td>
            <td class="px-6 py-4 whitespace-nowrap text-sm">john.doe@example.com</td>
            <td class="px-6 py-4 whitespace-nowrap">
              <span class="px-2 py-1 text-xs rounded-full bg-success/20 text-success">Active</span>
            </td>
            <td class="px-6 py-4 whitespace-nowrap text-sm">Admin</td>
            <td class="px-6 py-4 whitespace-nowrap text-sm">2 hours ago</td>
            <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
              <button class="text-primary hover:text-primary/80 transition-colors mr-2">Edit</button>
              <button class="text-destructive hover:text-destructive/80 transition-colors">Delete</button>
            </td>
          </tr>
          <tr class="hover:bg-muted/30 transition-colors">
            <td class="px-6 py-4 whitespace-nowrap">
              <div class="flex items-center">
                <div class="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center mr-3">
                  <span class="text-sm font-medium text-primary">JS</span>
                </div>
                <div>
                  <div class="font-medium">Jane Smith</div>
                  <div class="text-sm text-muted-foreground">@janesmith</div>
                </div>
              </div>
            </td>
            <td class="px-6 py-4 whitespace-nowrap text-sm">jane.smith@example.com</td>
            <td class="px-6 py-4 whitespace-nowrap">
              <span class="px-2 py-1 text-xs rounded-full bg-success/20 text-success">Active</span>
            </td>
            <td class="px-6 py-4 whitespace-nowrap text-sm">User</td>
            <td class="px-6 py-4 whitespace-nowrap text-sm">1 day ago</td>
            <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
              <button class="text-primary hover:text-primary/80 transition-colors mr-2">Edit</button>
              <button class="text-destructive hover:text-destructive/80 transition-colors">Delete</button>
            </td>
          </tr>
          <tr class="hover:bg-muted/30 transition-colors">
            <td class="px-6 py-4 whitespace-nowrap">
              <div class="flex items-center">
                <div class="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center mr-3">
                  <span class="text-sm font-medium text-primary">MB</span>
                </div>
                <div>
                  <div class="font-medium">Mike Brown</div>
                  <div class="text-sm text-muted-foreground">@mikebrown</div>
                </div>
              </div>
            </td>
            <td class="px-6 py-4 whitespace-nowrap text-sm">mike.brown@example.com</td>
            <td class="px-6 py-4 whitespace-nowrap">
              <span class="px-2 py-1 text-xs rounded-full bg-warning/20 text-warning">Inactive</span>
            </td>
            <td class="px-6 py-4 whitespace-nowrap text-sm">User</td>
            <td class="px-6 py-4 whitespace-nowrap text-sm">1 week ago</td>
            <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
              <button class="text-primary hover:text-primary/80 transition-colors mr-2">Edit</button>
              <button class="text-destructive hover:text-destructive/80 transition-colors">Delete</button>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
    <div class="flex items-center justify-between px-6 py-3 border-t border-border">
      <div class="text-sm text-muted-foreground">
        Showing <span class="font-medium">1</span> to <span class="font-medium">3</span> of <span class="font-medium">3</span> results
      </div>
      <div class="flex items-center gap-2">
        <button class="inline-flex items-center justify-center rounded-md text-sm font-medium h-8 w-8 border border-border bg-background shadow-sm hover:bg-accent transition-colors disabled:opacity-50 disabled:pointer-events-none" disabled>
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4">
            <path d="m15 18-6-6 6-6"></path>
          </svg>
          <span class="sr-only">Previous</span>
        </button>
        <button class="inline-flex items-center justify-center rounded-md text-sm font-medium h-8 min-w-8 bg-primary text-primary-foreground shadow hover:bg-primary/90 transition-colors px-3">1</button>
        <button class="inline-flex items-center justify-center rounded-md text-sm font-medium h-8 w-8 border border-border bg-background shadow-sm hover:bg-accent transition-colors disabled:opacity-50 disabled:pointer-events-none" disabled>
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4">
            <path d="m9 18 6-6-6-6"></path>
          </svg>
          <span class="sr-only">Next</span>
        </button>
      </div>
    </div>
  </div>
</div>

SPA এর মূল বৈশিষ্ট্য:

একবার পেজ লোড – শুধু প্রথমবার HTML লোড হবে✅ HTMX API Routes – শুধু প্রয়োজনীয় অংশ লোড✅ দ্রুত নেভিগেশন – কোন পেজ রিফ্রেশ নেই✅ Loading States – ইউজার ফ্রেন্ডলি লোডিং✅ Smooth Transitions – CSS অ্যানিমেশন✅ Mobile Responsive – সব ডিভাইসে কাজ করবে✅ Error Handling – HTMX এরর হ্যান্ডলিং✅ Auto-hide Messages – মেসেজ অটো লুকানো

কিভাবে কাজ করে:

  1. প্রথম লোড: spa_app.html একবার লোড হয়
  2. লগইন: HTMX দিয়ে /api/login/ এ POST রিকুয়েস্ট
  3. নেভিগেশন: সাইডবার বাটনে HTMX GET রিকুয়েস্ট
  4. কন্টেন্ট আপডেট: শুধু #spa-container এর ভিতরে কন্টেন্ট পরিবর্তন

এই SPA অ্যাপ্রোচে:

  • পেজ রিলোড নেই
  • দ্রুত লোডিং
  • কম ব্যান্ডউইথ ব্যবহার
  • বেটার ইউজার এক্সপেরিয়েন্স

এখন http://127.0.0.1:8000 এ গিয়ে দেখুন। এটি একটি সম্পূর্ণ SPA হিসেবে কাজ করবে!

How can we help?