Files
wallabicher/web/frontend/src/views/Login.vue
2026-01-20 18:15:49 +01:00

301 lines
12 KiB
Vue

<template>
<div class="min-h-screen bg-white dark:bg-gray-900 flex">
<!-- Left Panel - Branding -->
<div class="hidden lg:flex lg:w-1/2 bg-gradient-to-br from-primary-600 via-primary-700 to-primary-800 relative overflow-hidden">
<!-- Decorative Elements -->
<div class="absolute inset-0 opacity-10">
<div class="absolute top-0 left-0 w-96 h-96 bg-white rounded-full -translate-x-1/2 -translate-y-1/2 blur-3xl"></div>
<div class="absolute bottom-0 right-0 w-96 h-96 bg-primary-300 rounded-full translate-x-1/2 translate-y-1/2 blur-3xl"></div>
<div class="absolute top-1/2 left-1/2 w-64 h-64 bg-primary-400 rounded-full -translate-x-1/2 -translate-y-1/2 blur-3xl"></div>
</div>
<!-- Content -->
<div class="relative z-10 flex flex-col justify-between p-12 text-white">
<div>
<div class="flex items-center space-x-3 mb-8">
<div class="w-14 h-14 bg-white/20 backdrop-blur-sm rounded-xl overflow-hidden ring-2 ring-white/30 shadow-xl">
<img
src="/logo.jpg"
alt="Wallabicher Logo"
class="w-full h-full object-cover"
/>
</div>
<div>
<h1 class="text-2xl font-bold">Wallabicher</h1>
<p class="text-sm text-white/80">Admin Panel</p>
</div>
</div>
</div>
<div class="space-y-6">
<div>
<h2 class="text-4xl font-bold mb-4 leading-tight">
Bienvenido de vuelta
</h2>
<p class="text-lg text-white/90 leading-relaxed">
Gestiona y monitoriza tus búsquedas de productos con nuestro panel de administración profesional.
</p>
</div>
<!-- Features -->
<div class="space-y-4 pt-4 border-t border-white/20">
<div class="flex items-start space-x-3">
<div class="flex-shrink-0 w-6 h-6 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center mt-0.5">
<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"></path>
</svg>
</div>
<div>
<p class="font-semibold text-white">Monitoreo en tiempo real</p>
<p class="text-sm text-white/80">Recibe notificaciones instantáneas de nuevos artículos</p>
</div>
</div>
<div class="flex items-start space-x-3">
<div class="flex-shrink-0 w-6 h-6 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center mt-0.5">
<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"></path>
</svg>
</div>
<div>
<p class="font-semibold text-white">Gestión avanzada</p>
<p class="text-sm text-white/80">Control total sobre workers, usuarios y configuraciones</p>
</div>
</div>
<div class="flex items-start space-x-3">
<div class="flex-shrink-0 w-6 h-6 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center mt-0.5">
<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"></path>
</svg>
</div>
<div>
<p class="font-semibold text-white">Seguridad empresarial</p>
<p class="text-sm text-white/80">Autenticación robusta y gestión de permisos</p>
</div>
</div>
</div>
</div>
<div class="text-sm text-white/70">
© {{ new Date().getFullYear() }} Wallabicher. Todos los derechos reservados.
</div>
</div>
</div>
<!-- Right Panel - Login Form -->
<div class="flex-1 flex items-center justify-center p-8 bg-gray-50 dark:bg-gray-900">
<div class="w-full max-w-md">
<!-- Mobile Logo -->
<div class="lg:hidden flex items-center justify-center space-x-3 mb-8">
<div class="w-12 h-12 bg-primary-600 rounded-xl overflow-hidden ring-2 ring-primary-500/50 shadow-lg">
<img
src="/logo.jpg"
alt="Wallabicher Logo"
class="w-full h-full object-cover"
/>
</div>
<div>
<h1 class="text-xl font-bold text-gray-900 dark:text-white">Wallabicher</h1>
<p class="text-xs text-gray-500 dark:text-gray-400">Admin Panel</p>
</div>
</div>
<!-- Login Card -->
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl border border-gray-200 dark:border-gray-700 p-8">
<div class="mb-8">
<h2 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">
Iniciar Sesión
</h2>
<p class="text-gray-600 dark:text-gray-400">
Ingresa tus credenciales para acceder al panel
</p>
</div>
<!-- Error Message -->
<div
v-if="loginError"
class="mb-6 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-xl p-4 animate-shake"
>
<div class="flex items-start space-x-3">
<div class="flex-shrink-0">
<svg class="w-5 h-5 text-red-600 dark:text-red-400 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path>
</svg>
</div>
<div class="flex-1">
<p class="text-sm font-medium text-red-800 dark:text-red-300">
{{ loginError }}
</p>
</div>
</div>
</div>
<!-- Login Form -->
<form @submit.prevent="handleLogin" class="space-y-6">
<!-- Username Field -->
<div>
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
Usuario
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
</svg>
</div>
<input
v-model="loginForm.username"
type="text"
class="input w-full pl-12 pr-4 py-3 text-base"
placeholder="Ingresa tu usuario"
required
autocomplete="username"
:disabled="loginLoading"
/>
</div>
</div>
<!-- Password Field -->
<div>
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
Contraseña
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
</svg>
</div>
<input
v-model="loginForm.password"
type="password"
class="input w-full pl-12 pr-4 py-3 text-base"
placeholder="Ingresa tu contraseña"
required
autocomplete="current-password"
:disabled="loginLoading"
/>
</div>
</div>
<!-- Remember Me & Forgot Password -->
<div class="flex items-center justify-between">
<div class="flex items-center">
<input
v-model="loginForm.remember"
type="checkbox"
id="remember"
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 transition-colors cursor-pointer"
:disabled="loginLoading"
/>
<label for="remember" class="ml-2 block text-sm text-gray-700 dark:text-gray-300 cursor-pointer select-none">
Recordar sesión
</label>
</div>
</div>
<!-- Submit Button -->
<button
type="submit"
class="w-full btn btn-primary py-3.5 text-base font-semibold shadow-lg hover:shadow-xl transition-all duration-200 transform hover:scale-[1.02] active:scale-[0.98]"
:disabled="loginLoading"
>
<span v-if="!loginLoading" class="flex items-center justify-center">
<span>Iniciar Sesión</span>
<svg class="ml-2 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path>
</svg>
</span>
<span v-else class="flex items-center justify-center">
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" 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 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Iniciando sesión...
</span>
</button>
</form>
<!-- Footer -->
<div class="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700">
<p class="text-xs text-center text-gray-500 dark:text-gray-400">
¿Necesitas ayuda? Contacta con el administrador del sistema
</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import authService from '../services/auth';
const router = useRouter();
const loginError = ref('');
const loginLoading = ref(false);
const loginForm = ref({
username: '',
password: '',
remember: true,
});
async function handleLogin() {
loginError.value = '';
loginLoading.value = true;
if (!loginForm.value.username || !loginForm.value.password) {
loginError.value = 'Usuario y contraseña son requeridos';
loginLoading.value = false;
return;
}
try {
await authService.login(
loginForm.value.username,
loginForm.value.password
);
// Si llegamos aquí, el login fue exitoso
// Redirigir al dashboard
router.push('/');
// Disparar evento para que App.vue se actualice
window.dispatchEvent(new CustomEvent('auth-login'));
} catch (error) {
console.error('Error en login:', error);
loginError.value = error.message || 'Usuario o contraseña incorrectos';
authService.clearSession();
} finally {
loginLoading.value = false;
}
}
onMounted(() => {
// Si ya está autenticado, redirigir al dashboard
if (authService.hasCredentials()) {
router.push('/');
return;
}
// Cargar username guardado si existe
const username = authService.getUsername();
if (username) {
loginForm.value.username = username;
}
});
</script>
<style scoped>
@keyframes shake {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
20%, 40%, 60%, 80% { transform: translateX(5px); }
}
.animate-shake {
animation: shake 0.5s ease-in-out;
}
</style>