tinyqc/app/Views/auth/login.php

171 lines
7.8 KiB
PHP
Raw Normal View History

<!DOCTYPE html>
<!DOCTYPE html>
<html lang="en" data-theme="autumn">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - TinyQC</title>
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
<link href="https://cdn.jsdelivr.net/npm/daisyui@5/themes.css" rel="stylesheet" type="text/css" />
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.1/dist/cdn.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.7.2/css/all.min.css">
<script>
const BASEURL = '<?= base_url(''); ?>';
</script>
</head>
<body class="bg-base-200 min-h-screen flex items-center justify-center p-4" x-data="loginData()">
<div class="w-full max-w-md">
<div class="card bg-base-100 shadow-xl border border-base-300">
<div class="card-body p-8">
<div class="text-center mb-8">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary mb-4 shadow-lg shadow-primary/20">
<i class="fa-solid fa-flask text-white text-2xl"></i>
</div>
<h1 class="text-2xl font-bold text-base-content">TinyQC</h1>
<p class="text-sm text-base-content/60 mt-1">QC Management System</p>
</div>
<form @submit.prevent="handleSubmit" class="space-y-4">
<label class="input input-bordered w-full flex items-center gap-2" :class="{ 'input-error': errors.username }">
<i class="fa-solid fa-user text-sm opacity-70"></i>
<input type="text" x-model="form.username" placeholder="Username" class="grow" required autofocus>
</label>
<span x-show="errors.username" x-text="errors.username" class="text-error text-xs mt-1"></span>
<label class="input input-bordered w-full flex items-center gap-2" :class="{ 'input-error': errors.password }">
<i class="fa-solid fa-lock text-sm opacity-70"></i>
<input :type="showPassword ? 'text' : 'password'" x-model="form.password" placeholder="Password" class="grow" required>
<button type="button" @click="showPassword = !showPassword" class="opacity-70 hover:opacity-100">
<i class="fa-solid" :class="showPassword ? 'fa-eye-slash' : 'fa-eye'"></i>
</button>
</label>
<span x-show="errors.password" x-text="errors.password" class="text-error text-xs mt-1"></span>
<div class="flex items-center justify-between">
<label class="label cursor-pointer flex items-center gap-2">
<input type="checkbox" x-model="form.remember" class="checkbox checkbox-primary checkbox-sm">
<span class="label-text text-sm">Remember me</span>
</label>
</div>
<div x-show="errorMessage" x-transition
class="alert alert-error alert-sm text-sm py-2">
<i class="fa-solid fa-circle-exclamation text-sm"></i>
<span x-text="errorMessage"></span>
</div>
<button type="submit" class="btn btn-primary w-full" :class="{ 'loading': loading }"
:disabled="loading">
<span x-show="!loading">
<i class="fa-solid fa-sign-in-alt text-sm mr-2"></i>Sign In
</span>
<span x-show="loading">Signing in...</span>
</button>
</form>
<div class="divider text-xs text-base-content/50">or</div>
<div class="text-center">
<button @click="toggleTheme()" class="btn btn-ghost btn-sm">
<i class="fa-solid fa-sun text-lg text-warning" x-show="currentTheme === 'autumn'"></i>
<i class="fa-solid fa-moon text-lg text-neutral-content" x-show="currentTheme === 'dracula'"></i>
<span class="ml-2" x-text="currentTheme === 'autumn' ? 'Dark Mode' : 'Light Mode'"></span>
</button>
</div>
</div>
</div>
<p class="text-center text-sm text-base-content/50 mt-6">
&copy; <?= date('Y') ?> TinyQC - PT.Summit
</p>
</div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('loginData', () => ({
loading: false,
showPassword: false,
errorMessage: '',
errors: {
username: '',
password: ''
},
form: {
username: '',
password: '',
remember: false
},
get currentTheme() {
return localStorage.getItem('theme') || 'autumn';
},
set currentTheme(value) {
localStorage.setItem('theme', value);
document.documentElement.setAttribute('data-theme', value);
},
init() {
document.documentElement.setAttribute('data-theme', this.currentTheme);
},
toggleTheme() {
this.currentTheme = this.currentTheme === 'autumn' ? 'dracula' : 'autumn';
},
validateForm() {
this.errors = { username: '', password: '' };
let isValid = true;
if (!this.form.username.trim()) {
this.errors.username = 'Username is required';
isValid = false;
}
if (!this.form.password) {
this.errors.password = 'Password is required';
isValid = false;
}
return isValid;
},
async handleSubmit() {
this.errorMessage = '';
if (!this.validateForm()) {
return;
}
this.loading = true;
try {
const response = await fetch(`${BASEURL}login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({
username: this.form.username,
password: this.form.password,
remember: this.form.remember
})
});
const data = await response.json();
if (data.status === 'success') {
window.location.href = data.redirect || BASEURL;
} else {
this.errorMessage = data.message || 'Invalid username or password';
}
} catch (error) {
this.errorMessage = 'An error occurred. Please try again.';
} finally {
this.loading = false;
}
}
}));
});
</script>
</body>
</html>