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("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");"></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/