Receta Autenticación Formularios UX

Receta: Login con validación

Pantalla de acceso con formulario de autenticación, validación JS, estado de carga y credenciales de demo. Lista para copiar y adaptar a tu proyecto.

Código fuente del template

template.html
{% extends "showcase/base_empty.html" %}
{% load components_ui %}

{% block title %}Receta: Login — Django Components UI{% endblock %}

{% block content %}
<div class="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-900 via-primary-950 to-gray-900 p-4">
    <!-- Background pattern -->
    <div class="absolute inset-0 opacity-[0.03]"
         style="background-image: url(&quot;data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='1'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E&quot;);"></div>

    <div class="relative w-full max-w-[420px]">
        <!-- Card -->
        <div class="bg-white rounded-3xl shadow-2xl shadow-black/30 overflow-hidden">

            <!-- Header band -->
            <div class="bg-gradient-to-r from-primary-600 to-primary-700 px-8 pt-8 pb-10 text-white">
                <div class="flex items-center gap-3 mb-6">
                    <div class="w-9 h-9 bg-white/20 rounded-xl flex items-center justify-center backdrop-blur-sm">
                        {% comp_icon name="CpuChipIcon" size="w-5 h-5 text-white" %}
                    </div>
                    <span class="font-black text-lg tracking-tight">Mi Aplicación</span>
                </div>
                <h1 class="text-2xl font-black tracking-tight">Bienvenido de nuevo</h1>
                <p class="text-primary-200 text-sm mt-1 font-medium">Introduce tus credenciales para continuar</p>
            </div>

            <!-- Form -->
            <div class="-mt-4 bg-white rounded-t-3xl px-8 pt-8 pb-8 space-y-5">

                <!-- Error banner (hidden by default) -->
                <div id="login-error"
                     class="hidden flex items-center gap-3 p-4 bg-error-50 border border-error-200 text-error-700 rounded-2xl text-sm font-medium">
                    {% comp_icon name="ExclamationCircleIcon" size="w-4 h-4 text-error-500" %}
                    <span>Credenciales incorrectas. Por favor, inténtalo de nuevo.</span>
                </div>

                <form id="login-form" novalidate class="space-y-5" onsubmit="handleLogin(event)">
                    {% comp_input_text name="email" label="Correo electrónico" type="email" placeholder="tu@empresa.com" icon="UserIcon" required=True %}

                    <div>
                        {% comp_input_password name="password" label="Contraseña" placeholder="••••••••" required=True %}
                        <div class="flex justify-end mt-1.5">
                            <a href="#" class="text-xs text-primary-600 hover:text-primary-700 font-semibold transition-colors">
                                ¿Olvidaste tu contraseña?
                            </a>
                        </div>
                    </div>

                    {% comp_toggle name="remember" label="Recordar este dispositivo" checked=False %}

                    <div class="pt-2">
                        {% comp_button text="Iniciar sesión" type="submit" color="primary" icon="ArrowRightEndOnRectangleIcon" size="lg" classes="w-full justify-center" id="btn-login" %}
                    </div>
                </form>

                <!-- Demo credentials hint -->
                <div class="border-t border-gray-100 pt-5">
                    <p class="text-center text-xs text-gray-400 font-medium mb-3">Credenciales de demo</p>
                    <div class="flex gap-2">
                        <button onclick="fillDemo()"
                                class="flex-1 py-2 px-3 text-xs font-bold text-primary-600 bg-primary-50 hover:bg-primary-100 rounded-xl transition-colors border border-primary-100">
                            admin@demo.com / demo1234
                        </button>
                    </div>
                </div>
            </div>
        </div>

        <!-- Footer link -->
        <p class="text-center text-xs text-gray-500 mt-6 font-medium">
            ¿No tienes cuenta?
            <a href="#" class="text-primary-400 hover:text-primary-300 font-bold transition-colors ml-1">Solicitar acceso →</a>
        </p>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script>
function fillDemo() {
    document.getElementById('email').value = 'admin@demo.com';
    document.getElementById('password').value = 'demo1234';
}

function handleLogin(e) {
    e.preventDefault();
    const email    = document.getElementById('email').value.trim();
    const password = document.getElementById('password').value;
    const error    = document.getElementById('login-error');
    const btn      = document.getElementById('btn-login');

    // Basic validation
    if (!email || !password) {
        error.classList.remove('hidden');
        error.querySelector('span').textContent = 'Por favor, completa todos los campos.';
        return;
    }

    // Loading state
    btn.disabled = true;
    btn.querySelector('span, [data-btn-text]')?.textContent;
    const originalHTML = btn.innerHTML;
    btn.innerHTML = `<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
        <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
        <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
    </svg><span class="ml-2">Verificando...</span>`;

    setTimeout(() => {
        // Demo: accept only the demo credentials
        if (email === 'admin@demo.com' && password === 'demo1234') {
            error.classList.add('hidden');
            btn.innerHTML = `<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
            </svg><span class="ml-2">¡Acceso concedido!</span>`;
            btn.classList.add('bg-success-600');
            btn.classList.remove('bg-primary-600');
            Toast.show('Sesión iniciada correctamente', 'success', 3000, '¡Bienvenido!');
        } else {
            error.classList.remove('hidden');
            error.querySelector('span').textContent = 'Credenciales incorrectas. Usa las credenciales de demo.';
            btn.innerHTML = originalHTML;
            btn.disabled = false;
        }
    }, 1200);
}
</script>
{% endblock %}

Vista previa

Pantalla completa
/recipes/login/