Files
thetool/public/mobile/components/LoginScreen.js
2026-01-13 12:44:45 +01:00

640 lines
33 KiB
JavaScript

/**
* LoginScreen Component
*
* Displays the login form for the PWA with 2FA support.
* Features:
* - Username/password authentication
* - 2FA verification with OTP auto-detection (Web OTP API for Android, autocomplete for iOS)
* - Remember me option
*/
import { login, verify2FA, resend2FA } from '/mobile/shared/auth.js';
export default {
name: 'LoginScreen',
emits: ['login', 'set-theme'],
props: {
theme: {
type: String,
default: 'system'
}
},
setup(props, { emit }) {
const { ref, onMounted, onUnmounted, nextTick } = Vue;
// Login form state
const username = ref('');
const password = ref('');
const rememberMe = ref(true);
const showPassword = ref(false);
// 2FA state
const show2FA = ref(false);
const otpCode = ref('');
const otpDigits = ref(['', '', '', '', '']);
const deliveryMethod = ref('');
const maskedTarget = ref('');
const resendCooldown = ref(0);
// General state
const error = ref('');
const success = ref('');
const loading = ref(false);
const showThemePicker = ref(!localStorage.getItem('theme'));
// OTP input refs
let otpInputRefs = [];
let otpAbortController = null;
let resendTimer = null;
// Handle login form submission
const handleSubmit = async () => {
if (!username.value || !password.value) {
error.value = 'Bitte Benutzername und Passwort eingeben';
return;
}
loading.value = true;
error.value = '';
try {
// Call login API directly
const result = await login({
username: username.value,
password: password.value,
rememberMe: rememberMe.value
});
if (result.requires2FA) {
// Show 2FA verification screen
show2FA.value = true;
deliveryMethod.value = result.deliveryMethod;
maskedTarget.value = result.maskedTarget;
success.value = result.message;
error.value = '';
// Start resend cooldown
startResendCooldown();
// Focus first OTP input after render
await nextTick();
focusOtpInput(0);
// Try Web OTP API for SMS
if (result.deliveryMethod === 'sms') {
startWebOTP();
}
} else if (result.success) {
// Direct login success (no 2FA) - notify parent
emit('login', { _2faSuccess: true, user: result.user });
} else {
error.value = result.message || 'Login fehlgeschlagen';
}
} catch (e) {
error.value = 'Ein Fehler ist aufgetreten';
} finally {
loading.value = false;
}
};
// Handle 2FA verification
const handleVerify2FA = async () => {
const code = otpDigits.value.join('');
if (code.length !== 5) {
error.value = 'Bitte gib den 5-stelligen Code ein';
return;
}
loading.value = true;
error.value = '';
success.value = '';
try {
const result = await verify2FA(code);
if (result.success) {
// Emit the successful result to parent (which handles navigation)
emit('login', { _2faSuccess: true, user: result.user });
} else {
error.value = result.message || 'Ungültiger Code';
if (result.expired || result.codeExpired) {
// Session or code expired - go back to login
resetTo2FA();
}
}
} catch (e) {
error.value = 'Ein Fehler ist aufgetreten';
} finally {
loading.value = false;
}
};
// Handle resend 2FA code
const handleResend = async () => {
if (resendCooldown.value > 0) return;
loading.value = true;
error.value = '';
try {
const result = await resend2FA();
if (result.success) {
success.value = result.message || 'Neuer Code wurde gesendet';
startResendCooldown();
// Clear OTP inputs
otpDigits.value = ['', '', '', '', ''];
focusOtpInput(0);
// Restart Web OTP if SMS
if (deliveryMethod.value === 'sms') {
startWebOTP();
}
} else {
error.value = result.message || 'Code konnte nicht gesendet werden';
if (result.expired) {
resetTo2FA();
}
}
} catch (e) {
error.value = 'Ein Fehler ist aufgetreten';
} finally {
loading.value = false;
}
};
// Go back to login form
const backToLogin = () => {
show2FA.value = false;
otpDigits.value = ['', '', '', '', ''];
error.value = '';
success.value = '';
abortWebOTP();
};
// Reset after session expired
const resetTo2FA = () => {
show2FA.value = false;
password.value = '';
otpDigits.value = ['', '', '', '', ''];
error.value = 'Sitzung abgelaufen. Bitte erneut anmelden.';
};
// Start resend cooldown (30 seconds)
const startResendCooldown = () => {
resendCooldown.value = 30;
if (resendTimer) clearInterval(resendTimer);
resendTimer = setInterval(() => {
resendCooldown.value--;
if (resendCooldown.value <= 0) {
clearInterval(resendTimer);
}
}, 1000);
};
// OTP input handlers
const focusOtpInput = (index) => {
const inputs = document.querySelectorAll('.otp-input');
if (inputs[index]) {
inputs[index].focus();
}
};
const handleOtpInput = (index, event) => {
const value = event.target.value;
// Only allow digits
if (!/^\d*$/.test(value)) {
event.target.value = otpDigits.value[index];
return;
}
// Handle paste of full code
if (value.length > 1) {
const digits = value.replace(/\D/g, '').slice(0, 5).split('');
digits.forEach((digit, i) => {
if (i < 5) otpDigits.value[i] = digit;
});
focusOtpInput(Math.min(digits.length, 4));
// Auto-submit if complete
if (otpDigits.value.join('').length === 5) {
handleVerify2FA();
}
return;
}
otpDigits.value[index] = value;
// Move to next input
if (value && index < 4) {
focusOtpInput(index + 1);
}
// Auto-submit when complete
if (otpDigits.value.join('').length === 5) {
handleVerify2FA();
}
};
const handleOtpKeydown = (index, event) => {
// Handle backspace
if (event.key === 'Backspace' && !otpDigits.value[index] && index > 0) {
focusOtpInput(index - 1);
}
};
const handleOtpPaste = (event) => {
event.preventDefault();
const pastedData = event.clipboardData.getData('text');
const digits = pastedData.replace(/\D/g, '').slice(0, 5).split('');
digits.forEach((digit, i) => {
if (i < 5) otpDigits.value[i] = digit;
});
focusOtpInput(Math.min(digits.length, 4));
// Auto-submit if complete
if (otpDigits.value.join('').length === 5) {
handleVerify2FA();
}
};
// Web OTP API for automatic SMS code detection (Android)
const startWebOTP = async () => {
if (!('OTPCredential' in window)) {
console.log('Web OTP API not supported');
return;
}
abortWebOTP();
otpAbortController = new AbortController();
try {
const otp = await navigator.credentials.get({
otp: { transport: ['sms'] },
signal: otpAbortController.signal
});
if (otp && otp.code) {
// Extract 5-digit code from SMS
const code = otp.code.replace(/\D/g, '').slice(0, 5);
if (code.length === 5) {
code.split('').forEach((digit, i) => {
otpDigits.value[i] = digit;
});
// Auto-submit
handleVerify2FA();
}
}
} catch (err) {
if (err.name !== 'AbortError') {
console.log('Web OTP error:', err);
}
}
};
const abortWebOTP = () => {
if (otpAbortController) {
otpAbortController.abort();
otpAbortController = null;
}
};
// Theme picker
const selectTheme = (newTheme) => {
emit('set-theme', newTheme);
showThemePicker.value = false;
};
// Cleanup
onUnmounted(() => {
abortWebOTP();
if (resendTimer) clearInterval(resendTimer);
});
return {
// Login state
username,
password,
rememberMe,
showPassword,
// 2FA state
show2FA,
otpDigits,
deliveryMethod,
maskedTarget,
resendCooldown,
// General state
error,
success,
loading,
showThemePicker,
// Methods
handleSubmit,
handleVerify2FA,
handleResend,
backToLogin,
handleOtpInput,
handleOtpKeydown,
handleOtpPaste,
selectTheme
};
},
template: `
<div class="min-h-screen flex items-center justify-center p-4 relative overflow-hidden">
<!-- Animated Network Background -->
<div class="absolute inset-0 bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900">
<!-- Fiber grid pattern -->
<div class="absolute inset-0 opacity-40" style="background-image:
linear-gradient(to right, rgba(0, 83, 132, 0.15) 1px, transparent 1px),
linear-gradient(to bottom, rgba(0, 83, 132, 0.15) 1px, transparent 1px);
background-size: 50px 50px;"></div>
<!-- Glowing nodes with enhanced animation -->
<div class="absolute w-3 h-3 bg-cyan-400 rounded-full network-node" style="top: 12%; left: 18%; box-shadow: 0 0 25px 8px rgba(34, 211, 238, 0.6);"></div>
<div class="absolute w-2.5 h-2.5 bg-blue-400 rounded-full network-node" style="top: 22%; left: 78%; animation-delay: 0.5s; box-shadow: 0 0 20px 6px rgba(96, 165, 250, 0.6);"></div>
<div class="absolute w-3 h-3 bg-cyan-300 rounded-full network-node-slow" style="top: 72%; left: 12%; animation-delay: 1s; box-shadow: 0 0 25px 8px rgba(103, 232, 249, 0.6);"></div>
<div class="absolute w-2.5 h-2.5 bg-blue-300 rounded-full network-node" style="top: 82%; left: 85%; animation-delay: 0.3s; box-shadow: 0 0 20px 6px rgba(147, 197, 253, 0.6);"></div>
<div class="absolute w-2 h-2 bg-cyan-400 rounded-full network-node-slow" style="top: 42%; left: 8%; animation-delay: 0.7s; box-shadow: 0 0 15px 5px rgba(34, 211, 238, 0.5);"></div>
<div class="absolute w-2.5 h-2.5 bg-blue-400 rounded-full network-node" style="top: 32%; left: 92%; animation-delay: 1.2s; box-shadow: 0 0 20px 6px rgba(96, 165, 250, 0.6);"></div>
<div class="absolute w-2 h-2 bg-cyan-300 rounded-full network-node" style="top: 58%; left: 88%; animation-delay: 0.9s; box-shadow: 0 0 15px 5px rgba(103, 232, 249, 0.5);"></div>
<div class="absolute w-2 h-2 bg-blue-300 rounded-full network-node-slow" style="top: 88%; left: 45%; animation-delay: 0.4s; box-shadow: 0 0 15px 5px rgba(147, 197, 253, 0.5);"></div>
<div class="absolute w-1.5 h-1.5 bg-cyan-400 rounded-full network-node" style="top: 5%; left: 55%; animation-delay: 1.5s; box-shadow: 0 0 12px 4px rgba(34, 211, 238, 0.5);"></div>
<div class="absolute w-1.5 h-1.5 bg-blue-400 rounded-full network-node-slow" style="top: 95%; left: 25%; animation-delay: 0.8s; box-shadow: 0 0 12px 4px rgba(96, 165, 250, 0.5);"></div>
<!-- Connection lines (SVG) with animations -->
<svg class="absolute inset-0 w-full h-full network-lines" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none">
<defs>
<linearGradient id="lineGrad1" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:rgb(34, 211, 238);stop-opacity:0" />
<stop offset="50%" style="stop-color:rgb(34, 211, 238);stop-opacity:1" />
<stop offset="100%" style="stop-color:rgb(34, 211, 238);stop-opacity:0" />
</linearGradient>
<linearGradient id="lineGrad2" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:rgb(96, 165, 250);stop-opacity:0" />
<stop offset="50%" style="stop-color:rgb(96, 165, 250);stop-opacity:1" />
<stop offset="100%" style="stop-color:rgb(96, 165, 250);stop-opacity:0" />
</linearGradient>
<linearGradient id="lineGrad3" x1="0%" y1="100%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:rgb(103, 232, 249);stop-opacity:0" />
<stop offset="50%" style="stop-color:rgb(103, 232, 249);stop-opacity:1" />
<stop offset="100%" style="stop-color:rgb(103, 232, 249);stop-opacity:0" />
</linearGradient>
</defs>
<!-- Main network connections -->
<line x1="18%" y1="12%" x2="78%" y2="22%" stroke="url(#lineGrad1)" stroke-width="1.5"/>
<line x1="78%" y1="22%" x2="85%" y2="82%" stroke="url(#lineGrad2)" stroke-width="1.5"/>
<line x1="18%" y1="12%" x2="12%" y2="72%" stroke="url(#lineGrad1)" stroke-width="1.5"/>
<line x1="12%" y1="72%" x2="85%" y2="82%" stroke="url(#lineGrad2)" stroke-width="1.5"/>
<line x1="8%" y1="42%" x2="18%" y2="12%" stroke="url(#lineGrad3)" stroke-width="1.5"/>
<line x1="92%" y1="32%" x2="78%" y2="22%" stroke="url(#lineGrad2)" stroke-width="1.5"/>
<!-- Additional connections -->
<line x1="12%" y1="72%" x2="45%" y2="88%" stroke="url(#lineGrad1)" stroke-width="1.5"/>
<line x1="45%" y1="88%" x2="85%" y2="82%" stroke="url(#lineGrad3)" stroke-width="1.5"/>
<line x1="88%" y1="58%" x2="92%" y2="32%" stroke="url(#lineGrad2)" stroke-width="1.5"/>
<line x1="88%" y1="58%" x2="85%" y2="82%" stroke="url(#lineGrad1)" stroke-width="1.5"/>
<line x1="8%" y1="42%" x2="12%" y2="72%" stroke="url(#lineGrad3)" stroke-width="1.5"/>
<line x1="55%" y1="5%" x2="78%" y2="22%" stroke="url(#lineGrad1)" stroke-width="1.5"/>
<line x1="55%" y1="5%" x2="18%" y2="12%" stroke="url(#lineGrad2)" stroke-width="1.5"/>
<line x1="25%" y1="95%" x2="45%" y2="88%" stroke="url(#lineGrad3)" stroke-width="1.5"/>
<line x1="25%" y1="95%" x2="12%" y2="72%" stroke="url(#lineGrad1)" stroke-width="1.5"/>
<!-- Cross connections -->
<line x1="18%" y1="12%" x2="88%" y2="58%" stroke="url(#lineGrad2)" stroke-width="1"/>
<line x1="8%" y1="42%" x2="78%" y2="22%" stroke="url(#lineGrad3)" stroke-width="1"/>
<line x1="12%" y1="72%" x2="92%" y2="32%" stroke="url(#lineGrad1)" stroke-width="1"/>
</svg>
<!-- Flowing data lines overlay -->
<svg class="absolute inset-0 w-full h-full opacity-50" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none">
<line x1="18%" y1="12%" x2="78%" y2="22%" stroke="rgb(34, 211, 238)" stroke-width="2" class="network-line-flow"/>
<line x1="12%" y1="72%" x2="85%" y2="82%" stroke="rgb(96, 165, 250)" stroke-width="2" class="network-line-flow" style="animation-delay: 2s;"/>
<line x1="8%" y1="42%" x2="18%" y2="12%" stroke="rgb(103, 232, 249)" stroke-width="2" class="network-line-flow" style="animation-delay: 4s;"/>
<line x1="55%" y1="5%" x2="78%" y2="22%" stroke="rgb(34, 211, 238)" stroke-width="2" class="network-line-flow" style="animation-delay: 1s;"/>
<line x1="45%" y1="88%" x2="85%" y2="82%" stroke="rgb(96, 165, 250)" stroke-width="2" class="network-line-flow" style="animation-delay: 3s;"/>
</svg>
<!-- Subtle radial glow -->
<div class="absolute inset-0" style="background: radial-gradient(ellipse at 30% 20%, rgba(0, 83, 132, 0.2) 0%, transparent 50%);"></div>
<div class="absolute inset-0" style="background: radial-gradient(ellipse at 70% 80%, rgba(34, 211, 238, 0.15) 0%, transparent 40%);"></div>
<div class="absolute inset-0" style="background: radial-gradient(ellipse at 90% 40%, rgba(96, 165, 250, 0.1) 0%, transparent 35%);"></div>
</div>
<!-- Theme Picker Modal -->
<transition name="fade">
<div v-if="showThemePicker" class="fixed inset-0 bg-black bg-opacity-60 z-50 flex items-center justify-center p-4">
<div class="bg-white dark:bg-slate-800 rounded-2xl p-6 w-full max-w-xs text-center shadow-2xl border border-slate-200 dark:border-slate-700">
<h3 class="font-bold text-lg mb-2 text-slate-800 dark:text-white">Willkommen!</h3>
<p class="text-sm text-slate-600 dark:text-slate-300 mb-6">Wähle dein bevorzugtes Farbschema.</p>
<div class="flex flex-col space-y-3">
<button @click="selectTheme('light')" class="w-full px-4 py-3 bg-slate-100 text-slate-800 font-semibold rounded-xl hover:bg-slate-200 transition flex items-center justify-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
Hell
</button>
<button @click="selectTheme('dark')" class="w-full px-4 py-3 bg-slate-700 text-white font-semibold rounded-xl hover:bg-slate-600 transition flex items-center justify-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
Dunkel
</button>
<button @click="selectTheme('system')" class="w-full mt-1 text-sm text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-300 transition">
Automatisch (System)
</button>
</div>
</div>
</div>
</transition>
<!-- Login/2FA Form Container -->
<div class="relative bg-white/95 dark:bg-slate-800/95 backdrop-blur-sm rounded-2xl shadow-2xl p-6 w-full max-w-sm border border-white/20 dark:border-slate-700/50">
<div class="mb-5">
<img src="/assets/images/xinon-full-transparent.png" class="h-14 mx-auto dark:hidden" alt="Logo">
<img src="/assets/images/xinon-full-transparent-white.png" class="h-14 mx-auto hidden dark:block" alt="Logo">
</div>
<!-- 2FA Verification Screen -->
<template v-if="show2FA">
<div class="text-center mb-6">
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
<svg v-if="deliveryMethod === 'sms'" xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<h2 class="text-xl font-bold text-slate-800 dark:text-white">
Verifizierung
</h2>
<p class="text-sm text-slate-500 dark:text-slate-400 mt-2">
Code wurde gesendet an<br>
<span class="font-medium text-slate-700 dark:text-slate-300">{{ maskedTarget }}</span>
</p>
</div>
<!-- OTP Input -->
<div class="mb-6">
<div class="flex justify-center space-x-2">
<input
v-for="(digit, index) in otpDigits"
:key="index"
type="text"
inputmode="numeric"
:autocomplete="index === 0 ? 'one-time-code' : 'off'"
maxlength="5"
class="otp-input w-12 h-14 text-center text-2xl font-bold border-2 border-slate-300 rounded-lg focus:border-primary focus:ring-2 focus:ring-primary/30 transition dark:bg-slate-700 dark:border-slate-600 dark:text-white"
:value="digit"
@input="handleOtpInput(index, $event)"
@keydown="handleOtpKeydown(index, $event)"
@paste="handleOtpPaste"
>
</div>
<p class="text-xs text-slate-400 dark:text-slate-500 text-center mt-3">
Code ist 5 Minuten gültig
</p>
</div>
<!-- Success Message -->
<div v-if="success" class="mb-4 p-3 bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-800 rounded-lg">
<p class="text-sm text-green-600 dark:text-green-400">{{ success }}</p>
</div>
<!-- Error Message -->
<div v-if="error" class="mb-4 p-3 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-lg">
<p class="text-sm text-red-600 dark:text-red-400">{{ error }}</p>
</div>
<!-- Verify Button -->
<button
@click="handleVerify2FA"
:disabled="loading || otpDigits.join('').length !== 5"
class="w-full py-3 px-4 bg-primary text-white font-bold rounded-lg hover:bg-primary/90 focus:ring-4 focus:ring-primary/30 transition disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
>
<svg v-if="loading" class="animate-spin -ml-1 mr-2 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" 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>
{{ loading ? 'Wird verifiziert...' : 'Verifizieren' }}
</button>
<!-- Resend and Back buttons -->
<div class="mt-4 flex flex-col items-center space-y-3">
<button
@click="handleResend"
:disabled="resendCooldown > 0 || loading"
class="text-sm text-primary hover:underline disabled:text-slate-400 disabled:no-underline"
>
{{ resendCooldown > 0 ? 'Neuer Code in ' + resendCooldown + 's' : 'Neuen Code senden' }}
</button>
<button
@click="backToLogin"
class="text-sm text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-300"
>
Zurück zur Anmeldung
</button>
</div>
</template>
<!-- Login Form -->
<template v-else>
<form @submit.prevent="handleSubmit" class="space-y-4">
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Benutzername
</label>
<input
v-model="username"
type="text"
autocomplete="username"
autocapitalize="none"
class="w-full p-3 border border-slate-300 rounded-xl focus:ring-2 focus:ring-primary focus:border-primary transition dark:bg-slate-700 dark:border-slate-600 dark:text-white"
placeholder="Benutzername eingeben"
>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Passwort
</label>
<div class="relative">
<input
v-model="password"
:type="showPassword ? 'text' : 'password'"
autocomplete="current-password"
class="w-full p-3 pr-12 border border-slate-300 rounded-xl focus:ring-2 focus:ring-primary focus:border-primary transition dark:bg-slate-700 dark:border-slate-600 dark:text-white"
placeholder="Passwort eingeben"
>
<button
type="button"
@click="showPassword = !showPassword"
class="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300"
>
<svg v-if="!showPassword" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
</svg>
</button>
</div>
</div>
<!-- Beautiful Toggle Switch -->
<div class="flex items-center justify-between py-1">
<span class="text-sm text-slate-600 dark:text-slate-300">Angemeldet bleiben</span>
<button
type="button"
@click="rememberMe = !rememberMe"
:class="[
'relative w-11 h-6 rounded-full transition-colors duration-200',
rememberMe ? 'bg-primary' : 'bg-slate-300 dark:bg-slate-600'
]"
>
<span :class="[
'absolute top-0.5 w-5 h-5 bg-white rounded-full shadow-md transition-transform duration-200',
rememberMe ? 'left-5' : 'left-0.5'
]"></span>
</button>
</div>
<div v-if="error" class="p-3 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-xl">
<p class="text-sm text-red-600 dark:text-red-400">{{ error }}</p>
</div>
<button
type="submit"
:disabled="loading"
class="w-full py-3 px-4 bg-primary text-white font-bold rounded-xl hover:bg-primary/90 focus:ring-4 focus:ring-primary/30 transition disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
>
<svg v-if="loading" class="animate-spin -ml-1 mr-2 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" 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>
{{ loading ? 'Wird angemeldet...' : 'Anmelden' }}
</button>
</form>
</template>
<div class="mt-3 pt-3 border-t border-slate-100 dark:border-slate-700 text-center">
<p class="text-xs text-slate-400 dark:text-slate-500">
powered by <span class="font-semibold">XINON</span>
</p>
</div>
</div>
</div>
`
};