feat: redesign Login/Register with HUD aesthetic and Google Auth integration
This commit is contained in:
@ -3,10 +3,9 @@ import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { authService } from '@/services/authService'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { signInWithGoogle } from '@/firebaseConfig'
|
||||
|
||||
defineProps<{
|
||||
onToggle: () => void
|
||||
}>()
|
||||
const emit = defineEmits(['toggle'])
|
||||
|
||||
const email = ref('')
|
||||
const password = ref('')
|
||||
@ -31,177 +30,128 @@ const handleLogin = async () => {
|
||||
|
||||
// Redirect based on role or home
|
||||
const role = response.role.toUpperCase()
|
||||
if (role === 'ADMIN') {
|
||||
router.push('/admin')
|
||||
} else if (role === 'DRIVER') {
|
||||
router.push('/driver')
|
||||
} else if (role === 'PROMOTER') {
|
||||
router.push('/promoter')
|
||||
} else {
|
||||
router.push('/map')
|
||||
}
|
||||
if (role === 'ADMIN') router.push('/admin')
|
||||
else if (role === 'DRIVER') router.push('/driver')
|
||||
else if (role === 'PROMOTER') router.push('/promoter')
|
||||
else router.push('/map')
|
||||
|
||||
} catch (error: any) {
|
||||
if (!error.response) {
|
||||
errorMessage.value = `Error de conexión: No se pudo contactar con el servidor en ${authService.getApiUrl()}. Revisa si el backend está encendido y accesible desde este dispositivo.`
|
||||
errorMessage.value = 'Error de conexión. Verifica tu internet.'
|
||||
} else if (error.response.status === 401) {
|
||||
errorMessage.value = 'Correo o contraseña incorrectos.'
|
||||
errorMessage.value = 'Credenciales inválidas.'
|
||||
} else {
|
||||
errorMessage.value = error.response?.data?.detail || 'Error interno del servidor. Inténtalo más tarde.'
|
||||
errorMessage.value = error.response?.data?.detail || 'Error en el servidor.'
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleGoogleLogin = async () => {
|
||||
isLoading.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
const { token } = await signInWithGoogle()
|
||||
const response = await authService.googleLogin(token)
|
||||
|
||||
authStore.login(response.access_token, response.role, response.full_name)
|
||||
|
||||
const role = response.role.toUpperCase()
|
||||
if (role === 'ADMIN') router.push('/admin')
|
||||
else if (role === 'DRIVER') router.push('/driver')
|
||||
else if (role === 'PROMOTER') router.push('/promoter')
|
||||
else router.push('/map')
|
||||
|
||||
} catch (error: any) {
|
||||
errorMessage.value = 'Error al iniciar sesión con Google.'
|
||||
console.error(error)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-form">
|
||||
<h2 class="auth-title">Iniciar Sesión</h2>
|
||||
|
||||
<form @submit.prevent="handleLogin" class="form-container">
|
||||
<div class="form-group">
|
||||
<label for="email">Correo Electrónico</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
v-model="email"
|
||||
placeholder="ejemplo@correo.com"
|
||||
required
|
||||
/>
|
||||
<div class="login-hud">
|
||||
<!-- Google Login Button (Featured) -->
|
||||
<button
|
||||
@click="handleGoogleLogin"
|
||||
class="w-full mb-8 h-14 bg-white dark:bg-zinc-800 rounded-2xl flex items-center justify-center gap-3 border border-slate-200 dark:border-white/10 shadow-sm active:scale-95 transition-all group"
|
||||
>
|
||||
<img src="https://www.gstatic.com/firebasejs/ui/2.0.0/images/auth/google.svg" class="size-6" alt="Google">
|
||||
<span class="text-sm font-black text-slate-700 dark:text-gray-200 uppercase tracking-tight">Continuar con Google</span>
|
||||
<div class="absolute inset-0 rounded-2xl border-2 border-primary opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
</button>
|
||||
|
||||
<div class="flex items-center gap-4 mb-8">
|
||||
<div class="h-px flex-1 bg-white/5"></div>
|
||||
<span class="text-[10px] font-black text-slate-500 uppercase tracking-widest">O entrar con email</span>
|
||||
<div class="h-px flex-1 bg-white/5"></div>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleLogin" class="space-y-4">
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-[10px] font-black text-primary uppercase tracking-widest ml-1">Terminal de Correo</label>
|
||||
<div class="relative group">
|
||||
<span class="material-icons absolute left-4 top-1/2 -translate-y-1/2 text-primary/40 text-sm">alternate_email</span>
|
||||
<input
|
||||
type="email"
|
||||
v-model="email"
|
||||
placeholder="ACCESS_ID@SIBU.COM"
|
||||
required
|
||||
class="w-full pl-12 pr-4 h-14 bg-white/5 border border-white/10 rounded-2xl text-white font-bold placeholder:text-slate-600 focus:border-primary focus:ring-0 transition-all uppercase text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Contraseña</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
v-model="password"
|
||||
placeholder="********"
|
||||
required
|
||||
/>
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-[10px] font-black text-primary uppercase tracking-widest ml-1">Código de Acceso</label>
|
||||
<div class="relative group">
|
||||
<span class="material-icons absolute left-4 top-1/2 -translate-y-1/2 text-primary/40 text-sm">lock_open</span>
|
||||
<input
|
||||
type="password"
|
||||
v-model="password"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
class="w-full pl-12 pr-4 h-14 bg-white/5 border border-white/10 rounded-2xl text-white font-bold focus:border-primary focus:ring-0 transition-all text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-options">
|
||||
<label class="checkbox-container">
|
||||
<input type="checkbox" v-model="keepSession" />
|
||||
<span class="checkmark"></span>
|
||||
Mantener sesión
|
||||
<div class="flex items-center justify-between pt-2">
|
||||
<label class="flex items-center gap-2 cursor-pointer group">
|
||||
<input type="checkbox" v-model="keepSession" class="hidden" />
|
||||
<div class="size-5 rounded-lg border-2 border-white/10 flex items-center justify-center transition-colors" :class="keepSession ? 'bg-primary border-primary' : ''">
|
||||
<span class="material-icons text-slate-900 text-xs" v-if="keepSession">check</span>
|
||||
</div>
|
||||
<span class="text-[10px] font-bold text-slate-500 uppercase">Mantener sesión</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<p v-if="errorMessage" class="error-text">{{ errorMessage }}</p>
|
||||
<p v-if="errorMessage" class="text-xs font-bold text-red-500 uppercase tracking-tight text-center">{{ errorMessage }}</p>
|
||||
|
||||
<button type="submit" class="auth-button" :disabled="isLoading">
|
||||
<span v-if="isLoading">Cargando...</span>
|
||||
<span v-else>Entrar</span>
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full h-14 bg-primary text-slate-900 rounded-2xl font-black uppercase tracking-[0.2em] text-xs shadow-xl shadow-primary/20 active:scale-95 transition-all mt-4"
|
||||
:disabled="isLoading"
|
||||
>
|
||||
{{ isLoading ? 'Validando...' : 'Acceder al Sistema' }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="auth-footer">
|
||||
<p>¿No tienes cuenta? <a @click.prevent="onToggle" href="#">Regístrate aquí</a></p>
|
||||
<div class="mt-8 text-center">
|
||||
<p class="text-[11px] font-bold text-slate-500 uppercase">
|
||||
¿Nuevo en la red?
|
||||
<button @click="emit('toggle')" class="text-primary hover:underline ml-1">Crear Registro</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.login-form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 24px;
|
||||
color: var(--text-primary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
input[type="email"],
|
||||
input[type="password"] {
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
font-size: 16px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.form-options {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.checkbox-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.auth-button {
|
||||
margin-top: 12px;
|
||||
padding: 14px;
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, background 0.2s;
|
||||
}
|
||||
|
||||
.auth-button:hover:not(:disabled) {
|
||||
background: var(--accent-hover);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.auth-button:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: #ef5350;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.auth-footer {
|
||||
margin-top: 24px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.auth-footer a {
|
||||
color: var(--accent-color);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
input::placeholder {
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,17 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { authService } from '@/services/authService'
|
||||
import { analyticsService } from '@/services/analyticsService'
|
||||
|
||||
const { onToggle, onSuccess } = defineProps<{
|
||||
onToggle: () => void,
|
||||
onSuccess: () => void
|
||||
}>()
|
||||
const emit = defineEmits(['toggle', 'success'])
|
||||
|
||||
// Form data
|
||||
const fullName = ref('')
|
||||
const email = ref('')
|
||||
const password = ref('')
|
||||
|
||||
const isLoading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const successMessage = ref('')
|
||||
@ -27,12 +23,17 @@ const handleRegister = async () => {
|
||||
password: password.value
|
||||
})
|
||||
|
||||
successMessage.value = 'Registro exitoso. Ya puedes iniciar sesión.'
|
||||
analyticsService.logEvent({
|
||||
event_name: 'sign_up',
|
||||
properties: { method: 'email' }
|
||||
})
|
||||
|
||||
successMessage.value = 'Registro exitoso. Ahora puedes entrar.'
|
||||
setTimeout(() => {
|
||||
onSuccess() // Back to login
|
||||
emit('success')
|
||||
}, 2000)
|
||||
} catch (error: any) {
|
||||
errorMessage.value = error.response?.data?.detail || 'Error al registrarse'
|
||||
errorMessage.value = error.response?.data?.detail || 'Error al crear la cuenta.'
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
@ -40,216 +41,73 @@ const handleRegister = async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="register-form">
|
||||
<h2 class="auth-title">Registrarse</h2>
|
||||
|
||||
<div class="form-scroll-container">
|
||||
<form @submit.prevent="handleRegister" class="form-container">
|
||||
<!-- Common Fields -->
|
||||
<div class="form-group">
|
||||
<label>Nombre Completo</label>
|
||||
<input type="text" v-model="fullName" placeholder="Juan Pérez" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Correo Electrónico</label>
|
||||
<input type="email" v-model="email" placeholder="juan@correo.com" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Contraseña</label>
|
||||
<input type="password" v-model="password" placeholder="********" required />
|
||||
</div>
|
||||
|
||||
<p v-if="errorMessage" class="error-text">{{ errorMessage }}</p>
|
||||
<p v-if="successMessage" class="success-text">{{ successMessage }}</p>
|
||||
|
||||
<button type="submit" class="auth-button" :disabled="isLoading">
|
||||
<span v-if="isLoading">Cargando...</span>
|
||||
<span v-else>Registrarse</span>
|
||||
</button>
|
||||
</form>
|
||||
<div class="register-hud">
|
||||
<div v-if="successMessage" class="bg-primary/10 border border-primary/50 p-6 rounded-3xl text-center mb-6">
|
||||
<span class="material-icons text-primary text-4xl mb-2">verified_user</span>
|
||||
<h3 class="text-white font-black uppercase text-sm mb-1">Acceso Concedido</h3>
|
||||
<p class="text-xs text-primary/80 font-bold uppercase">{{ successMessage }}</p>
|
||||
</div>
|
||||
|
||||
<form v-else @submit.prevent="handleRegister" class="space-y-4">
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-[10px] font-black text-primary uppercase tracking-widest ml-1">Identidad de Usuario</label>
|
||||
<div class="relative group">
|
||||
<span class="material-icons absolute left-4 top-1/2 -translate-y-1/2 text-primary/40 text-sm">badge</span>
|
||||
<input
|
||||
type="text"
|
||||
v-model="fullName"
|
||||
placeholder="NOMBRE COMPLETO"
|
||||
required
|
||||
class="w-full pl-12 pr-4 h-14 bg-white/5 border border-white/10 rounded-2xl text-white font-bold placeholder:text-slate-600 focus:border-primary focus:ring-0 transition-all uppercase text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-[10px] font-black text-primary uppercase tracking-widest ml-1">Terminal de Correo</label>
|
||||
<div class="relative group">
|
||||
<span class="material-icons absolute left-4 top-1/2 -translate-y-1/2 text-primary/40 text-sm">alternate_email</span>
|
||||
<input
|
||||
type="email"
|
||||
v-model="email"
|
||||
placeholder="USUARIO@SIBU.COM"
|
||||
required
|
||||
class="w-full pl-12 pr-4 h-14 bg-white/5 border border-white/10 rounded-2xl text-white font-bold placeholder:text-slate-600 focus:border-primary focus:ring-0 transition-all uppercase text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-[10px] font-black text-primary uppercase tracking-widest ml-1">Definir Código de Acceso</label>
|
||||
<div class="relative group">
|
||||
<span class="material-icons absolute left-4 top-1/2 -translate-y-1/2 text-primary/40 text-sm">security</span>
|
||||
<input
|
||||
type="password"
|
||||
v-model="password"
|
||||
placeholder="MÍNIMO 8 CARACTERES"
|
||||
required
|
||||
minlength="8"
|
||||
class="w-full pl-12 pr-4 h-14 bg-white/5 border border-white/10 rounded-2xl text-white font-bold placeholder:text-slate-600 focus:border-primary focus:ring-0 transition-all text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="errorMessage" class="text-xs font-bold text-red-500 uppercase tracking-tight text-center">{{ errorMessage }}</p>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full h-14 bg-primary text-slate-900 rounded-2xl font-black uppercase tracking-[0.2em] text-xs shadow-xl shadow-primary/20 active:scale-95 transition-all mt-6"
|
||||
:disabled="isLoading"
|
||||
>
|
||||
{{ isLoading ? 'Procesando Datos...' : 'Registrar Credenciales' }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="auth-footer">
|
||||
<p>¿Ya tienes cuenta? <a @click.prevent="onToggle" href="#">Inicia sesión</a></p>
|
||||
<div class="mt-8 text-center">
|
||||
<p class="text-[11px] font-bold text-slate-500 uppercase">
|
||||
¿Ya tienes autorización?
|
||||
<button @click="emit('toggle')" class="text-primary hover:underline ml-1">Regresar al Login</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.register-form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 24px;
|
||||
color: var(--text-primary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.role-selection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.selection-detail {
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.role-cards {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.role-card {
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.role-card:hover {
|
||||
border-color: var(--accent-color);
|
||||
transform: translateY(-4px);
|
||||
background: var(--hover-bg);
|
||||
}
|
||||
|
||||
.role-card .material-icons {
|
||||
font-size: 32px;
|
||||
color: var(--accent-color);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.role-card h3 {
|
||||
font-size: 16px;
|
||||
margin: 4px 0;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.role-card p {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.form-scroll-container {
|
||||
max-height: 450px;
|
||||
overflow-y: auto;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
.form-scroll-container::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
.form-scroll-container::-webkit-scrollbar-thumb {
|
||||
background: var(--border-color);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.vehicle-tabs {
|
||||
display: flex;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.vehicle-tabs button {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.vehicle-tabs button.active {
|
||||
background: var(--card-bg);
|
||||
color: var(--accent-color);
|
||||
box-shadow: 0 2px 4px var(--shadow);
|
||||
}
|
||||
|
||||
.form-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
font-size: 12px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.auth-button {
|
||||
margin-top: 12px;
|
||||
padding: 14px;
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.error-text { color: #ef5350; font-size: 14px; }
|
||||
.success-text { color: #4caf50; font-size: 14px; font-weight: 600; }
|
||||
|
||||
.auth-footer {
|
||||
margin-top: 24px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.auth-footer a {
|
||||
color: var(--accent-color);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user