- Implement JWT authentication with HTTP-only cookies - Create /v2/* namespace to avoid conflicts with existing frontend - Upgrade to DaisyUI 5 + Tailwind CSS 4 - Add light/dark theme toggle with smooth transitions - Build login page, dashboard, and patient list UI - Protect V2 routes with auth middleware - Add comprehensive documentation No breaking changes - all new features under /v2/* namespace
324 lines
10 KiB
PHP
324 lines
10 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="en" data-theme="corporate">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Login - CLQMS</title>
|
|
|
|
<!-- TailwindCSS 4 + DaisyUI 5 CDN -->
|
|
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
|
|
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
|
|
|
<!-- FontAwesome -->
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/js/all.min.js"></script>
|
|
|
|
<!-- Alpine.js -->
|
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
|
|
<style>
|
|
[x-cloak] { display: none !important; }
|
|
|
|
/* Smooth theme transition */
|
|
* {
|
|
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
|
}
|
|
|
|
/* Animated gradient background */
|
|
.gradient-bg {
|
|
background: linear-gradient(-45deg, #0ea5e9, #3b82f6, #6366f1, #8b5cf6);
|
|
background-size: 400% 400%;
|
|
animation: gradient 15s ease infinite;
|
|
}
|
|
|
|
@keyframes gradient {
|
|
0% { background-position: 0% 50%; }
|
|
50% { background-position: 100% 50%; }
|
|
100% { background-position: 0% 50%; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="min-h-screen flex items-center justify-center gradient-bg" x-data="loginApp()">
|
|
|
|
<!-- Login Card -->
|
|
<div class="w-full max-w-md p-4">
|
|
<div class="card bg-base-100 shadow-2xl">
|
|
<div class="card-body">
|
|
|
|
<!-- Logo & Title -->
|
|
<div class="text-center mb-6">
|
|
<div class="w-20 h-20 bg-primary/20 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
|
<i class="fa-solid fa-flask text-primary text-4xl"></i>
|
|
</div>
|
|
<h1 class="text-3xl font-bold text-base-content">CLQMS</h1>
|
|
<p class="text-base-content/60 mt-2">Clinical Laboratory Queue Management System</p>
|
|
</div>
|
|
|
|
<!-- Alert Messages -->
|
|
<div x-show="errorMessage" x-cloak class="alert alert-error mb-4">
|
|
<i class="fa-solid fa-exclamation-circle"></i>
|
|
<span x-text="errorMessage"></span>
|
|
</div>
|
|
|
|
<div x-show="successMessage" x-cloak class="alert alert-success mb-4">
|
|
<i class="fa-solid fa-check-circle"></i>
|
|
<span x-text="successMessage"></span>
|
|
</div>
|
|
|
|
<!-- Login Form -->
|
|
<form @submit.prevent="login">
|
|
|
|
<!-- Username -->
|
|
<div class="form-control mb-4">
|
|
<label class="label">
|
|
<span class="label-text font-medium">Username</span>
|
|
</label>
|
|
<div class="relative">
|
|
<span class="absolute inset-y-0 left-0 flex items-center pl-3">
|
|
<i class="fa-solid fa-user text-base-content/40"></i>
|
|
</span>
|
|
<input
|
|
type="text"
|
|
placeholder="Enter your username"
|
|
class="input input-bordered w-full pl-10"
|
|
x-model="form.username"
|
|
required
|
|
:disabled="loading"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Password -->
|
|
<div class="form-control mb-6">
|
|
<label class="label">
|
|
<span class="label-text font-medium">Password</span>
|
|
</label>
|
|
<div class="relative">
|
|
<span class="absolute inset-y-0 left-0 flex items-center pl-3">
|
|
<i class="fa-solid fa-lock text-base-content/40"></i>
|
|
</span>
|
|
<input
|
|
:type="showPassword ? 'text' : 'password'"
|
|
placeholder="Enter your password"
|
|
class="input input-bordered w-full pl-10 pr-10"
|
|
x-model="form.password"
|
|
required
|
|
:disabled="loading"
|
|
/>
|
|
<button
|
|
type="button"
|
|
@click="showPassword = !showPassword"
|
|
class="absolute inset-y-0 right-0 flex items-center pr-3"
|
|
tabindex="-1"
|
|
>
|
|
<i :class="showPassword ? 'fa-solid fa-eye-slash' : 'fa-solid fa-eye'" class="text-base-content/40"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Remember Me -->
|
|
<div class="form-control mb-6">
|
|
<label class="label cursor-pointer justify-start gap-3">
|
|
<input type="checkbox" class="checkbox checkbox-primary checkbox-sm" x-model="form.remember" />
|
|
<span class="label-text">Remember me</span>
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Submit Button -->
|
|
<button
|
|
type="submit"
|
|
class="btn btn-primary w-full"
|
|
:disabled="loading"
|
|
>
|
|
<span x-show="loading" class="loading loading-spinner loading-sm"></span>
|
|
<span x-show="!loading">
|
|
<i class="fa-solid fa-sign-in-alt mr-2"></i>
|
|
Login
|
|
</span>
|
|
</button>
|
|
|
|
</form>
|
|
|
|
<!-- Footer -->
|
|
<div class="divider">OR</div>
|
|
|
|
<div class="text-center">
|
|
<p class="text-sm text-base-content/60">
|
|
Don't have an account?
|
|
<button @click="showRegister = true" class="link link-primary">Register here</button>
|
|
</p>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Copyright -->
|
|
<div class="text-center mt-6 text-white/80">
|
|
<p class="text-sm">© 2025 5Panda. All rights reserved.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Register Modal -->
|
|
<dialog class="modal" :class="showRegister && 'modal-open'">
|
|
<div class="modal-box">
|
|
<h3 class="font-bold text-lg mb-4">
|
|
<i class="fa-solid fa-user-plus mr-2 text-primary"></i>
|
|
Create Account
|
|
</h3>
|
|
|
|
<form @submit.prevent="register">
|
|
<!-- Username -->
|
|
<div class="form-control mb-4">
|
|
<label class="label">
|
|
<span class="label-text font-medium">Username</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
placeholder="Choose a username"
|
|
class="input input-bordered w-full"
|
|
x-model="registerForm.username"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<!-- Password -->
|
|
<div class="form-control mb-4">
|
|
<label class="label">
|
|
<span class="label-text font-medium">Password</span>
|
|
</label>
|
|
<input
|
|
type="password"
|
|
placeholder="Choose a password"
|
|
class="input input-bordered w-full"
|
|
x-model="registerForm.password"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<!-- Confirm Password -->
|
|
<div class="form-control mb-6">
|
|
<label class="label">
|
|
<span class="label-text font-medium">Confirm Password</span>
|
|
</label>
|
|
<input
|
|
type="password"
|
|
placeholder="Confirm your password"
|
|
class="input input-bordered w-full"
|
|
x-model="registerForm.confirmPassword"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div class="modal-action">
|
|
<button type="button" class="btn btn-ghost" @click="showRegister = false">Cancel</button>
|
|
<button type="submit" class="btn btn-primary" :disabled="registering">
|
|
<span x-show="registering" class="loading loading-spinner loading-sm"></span>
|
|
<span x-show="!registering">Register</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-backdrop bg-black/50" @click="showRegister = false"></div>
|
|
</dialog>
|
|
|
|
<!-- Scripts -->
|
|
<script>
|
|
window.BASEURL = "<?= base_url() ?>";
|
|
|
|
function loginApp() {
|
|
return {
|
|
loading: false,
|
|
registering: false,
|
|
showPassword: false,
|
|
showRegister: false,
|
|
errorMessage: '',
|
|
successMessage: '',
|
|
|
|
form: {
|
|
username: '',
|
|
password: '',
|
|
remember: false
|
|
},
|
|
|
|
registerForm: {
|
|
username: '',
|
|
password: '',
|
|
confirmPassword: ''
|
|
},
|
|
|
|
async login() {
|
|
this.errorMessage = '';
|
|
this.successMessage = '';
|
|
this.loading = true;
|
|
|
|
try {
|
|
const res = await fetch(`${BASEURL}api/auth/login`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: new URLSearchParams({
|
|
username: this.form.username,
|
|
password: this.form.password
|
|
})
|
|
});
|
|
|
|
const data = await res.json();
|
|
|
|
if (res.ok && data.status === 'success') {
|
|
this.successMessage = 'Login successful! Redirecting...';
|
|
setTimeout(() => {
|
|
window.location.href = `${BASEURL}v2/`;
|
|
}, 1000);
|
|
} else {
|
|
this.errorMessage = data.message || 'Login failed. Please try again.';
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
this.errorMessage = 'Network error. Please try again.';
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
async register() {
|
|
this.errorMessage = '';
|
|
this.successMessage = '';
|
|
|
|
if (this.registerForm.password !== this.registerForm.confirmPassword) {
|
|
this.errorMessage = 'Passwords do not match!';
|
|
return;
|
|
}
|
|
|
|
this.registering = true;
|
|
|
|
try {
|
|
const res = await fetch(`${BASEURL}api/auth/register`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
username: this.registerForm.username,
|
|
password: this.registerForm.password
|
|
})
|
|
});
|
|
|
|
const data = await res.json();
|
|
|
|
if (res.ok && data.status === 'success') {
|
|
this.successMessage = 'Registration successful! You can now login.';
|
|
this.showRegister = false;
|
|
this.registerForm = { username: '', password: '', confirmPassword: '' };
|
|
} else {
|
|
this.errorMessage = data.message || 'Registration failed. Please try again.';
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
this.errorMessage = 'Network error. Please try again.';
|
|
} finally {
|
|
this.registering = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|