- Initialize SvelteKit project with Tailwind CSS and DaisyUI - Configure API base URL and environment variables - Create base API client with JWT token handling (src/lib/api/client.js) - Implement login/logout flow (src/lib/api/auth.js, src/routes/login/+page.svelte) - Create root layout with navigation (src/routes/+layout.svelte) - Set up protected route group with auth checks (src/routes/(app)/+layout.svelte) - Create dashboard homepage (src/routes/(app)/dashboard/+page.svelte) - Add auth state store with localStorage persistence (src/lib/stores/auth.js) All Phase 0 foundation items completed per implementation plan.
265 lines
10 KiB
HTML
265 lines
10 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en" data-theme="forest">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>CLQMS - Login</title>
|
|
<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>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
|
<link rel="stylesheet" href="shared.css">
|
|
<style>
|
|
@keyframes gradient-shift {
|
|
0%, 100% { background-position: 0% 50%; }
|
|
50% { background-position: 100% 50%; }
|
|
}
|
|
.animated-bg {
|
|
background: linear-gradient(-45deg, #57534e, #78716c, #a8a29e, #d6d3d1);
|
|
background-size: 400% 400%;
|
|
animation: gradient-shift 15s ease infinite;
|
|
}
|
|
.input-focus:focus-within {
|
|
border-color: #10b981;
|
|
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.2);
|
|
}
|
|
.error-shake {
|
|
animation: shake 0.5s ease-in-out;
|
|
}
|
|
@keyframes shake {
|
|
0%, 100% { transform: translateX(0); }
|
|
25% { transform: translateX(-5px); }
|
|
75% { transform: translateX(5px); }
|
|
}
|
|
.spinner {
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
@keyframes spin {
|
|
from { transform: rotate(0deg); }
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="min-h-screen bg-white flex items-center justify-center">
|
|
<div class="hero min-h-screen">
|
|
<div class="hero-content flex-col w-full max-w-lg">
|
|
<!-- Logo Section -->
|
|
<div class="text-center mb-6">
|
|
<div class="w-20 h-20 mx-auto rounded-2xl bg-emerald-100 flex items-center justify-center mb-4 shadow-lg border-2 border-emerald-200">
|
|
<svg class="w-12 h-12 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"/>
|
|
</svg>
|
|
</div>
|
|
<h1 class="text-4xl font-bold text-emerald-600 mb-2">CLQMS</h1>
|
|
<p class="text-base-content/60">Clinical Laboratory Quality Management System</p>
|
|
</div>
|
|
|
|
<!-- Login Card -->
|
|
<div class="card bg-base-200 w-full max-w-xl shadow-2xl border-t-4 border-emerald-500">
|
|
<div class="card-body p-8">
|
|
<h2 class="text-2xl font-bold text-center text-emerald-700 mb-6">Welcome Back</h2>
|
|
|
|
<form id="loginForm">
|
|
<!-- Username Field -->
|
|
<div class="mb-4">
|
|
<label class="input w-full input-focus transition-all duration-200">
|
|
<span class="label"><i class="fa-solid fa-user text-emerald-500"></i></span>
|
|
<input type="text" id="username" name="username" placeholder="Username" autocomplete="username" />
|
|
</label>
|
|
<p id="usernameError" class="text-red-500 text-sm mt-1 hidden"></p>
|
|
</div>
|
|
|
|
<!-- Password Field -->
|
|
<div class="mb-4">
|
|
<label class="input w-full input-focus transition-all duration-200">
|
|
<span class="label"><i class="fa-solid fa-lock text-emerald-500"></i></span>
|
|
<input type="password" id="password" name="password" placeholder="Password" autocomplete="current-password" />
|
|
<button type="button" id="togglePassword" class="btn btn-ghost btn-sm btn-circle">
|
|
<i class="fa-solid fa-eye text-emerald-500"></i>
|
|
</button>
|
|
</label>
|
|
<p id="passwordError" class="text-red-500 text-sm mt-1 hidden"></p>
|
|
</div>
|
|
|
|
<!-- Remember Me & Forgot Password -->
|
|
<div class="flex items-center justify-between mb-6">
|
|
<label class="label cursor-pointer flex items-center gap-2">
|
|
<input type="checkbox" id="rememberMe" class="checkbox checkbox-sm checkbox-emerald" />
|
|
<span class="label-text text-sm">Remember me</span>
|
|
</label>
|
|
<a href="#" class="text-sm text-emerald-600 hover:text-emerald-700 hover:underline">Forgot password?</a>
|
|
</div>
|
|
|
|
<!-- Login Button -->
|
|
<div class="form-control">
|
|
<button type="submit" id="loginBtn" class="btn bg-emerald-600 hover:bg-emerald-700 text-white shadow-lg hover:shadow-xl transition-all w-full">
|
|
<span id="btnText">
|
|
<i class="fa-solid fa-right-to-bracket mr-2"></i>
|
|
Sign In
|
|
</span>
|
|
<span id="btnSpinner" class="hidden">
|
|
<i class="fa-solid fa-circle-notch spinner mr-2"></i>
|
|
Signing in...
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
|
|
<!-- Divider -->
|
|
<div class="divider text-sm text-base-content/40">or</div>
|
|
|
|
<!-- Help Text -->
|
|
<p class="text-center text-sm text-base-content/60">
|
|
Need help? Contact your system administrator
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div class="mt-8 text-center">
|
|
<p class="text-sm text-base-content/40">© 2026 CLQMS. All rights reserved.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// DOM Elements
|
|
const form = document.getElementById('loginForm');
|
|
const usernameInput = document.getElementById('username');
|
|
const passwordInput = document.getElementById('password');
|
|
const usernameError = document.getElementById('usernameError');
|
|
const passwordError = document.getElementById('passwordError');
|
|
const togglePassword = document.getElementById('togglePassword');
|
|
const rememberMe = document.getElementById('rememberMe');
|
|
const loginBtn = document.getElementById('loginBtn');
|
|
const btnText = document.getElementById('btnText');
|
|
const btnSpinner = document.getElementById('btnSpinner');
|
|
|
|
// Load saved credentials
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
const savedUsername = localStorage.getItem('clqms_username');
|
|
const savedRemember = localStorage.getItem('clqms_remember') === 'true';
|
|
|
|
if (savedRemember && savedUsername) {
|
|
usernameInput.value = savedUsername;
|
|
rememberMe.checked = true;
|
|
}
|
|
});
|
|
|
|
// Toggle password visibility
|
|
togglePassword.addEventListener('click', () => {
|
|
const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password';
|
|
passwordInput.setAttribute('type', type);
|
|
|
|
const icon = togglePassword.querySelector('i');
|
|
icon.classList.toggle('fa-eye');
|
|
icon.classList.toggle('fa-eye-slash');
|
|
});
|
|
|
|
// Form validation
|
|
function validateForm() {
|
|
let isValid = true;
|
|
|
|
// Reset errors
|
|
usernameError.classList.add('hidden');
|
|
passwordError.classList.add('hidden');
|
|
usernameInput.parentElement.classList.remove('border-red-500', 'error-shake');
|
|
passwordInput.parentElement.classList.remove('border-red-500', 'error-shake');
|
|
|
|
// Validate username
|
|
if (!usernameInput.value.trim()) {
|
|
usernameError.textContent = 'Username is required';
|
|
usernameError.classList.remove('hidden');
|
|
usernameInput.parentElement.classList.add('border-red-500', 'error-shake');
|
|
isValid = false;
|
|
} else if (usernameInput.value.length < 3) {
|
|
usernameError.textContent = 'Username must be at least 3 characters';
|
|
usernameError.classList.remove('hidden');
|
|
usernameInput.parentElement.classList.add('border-red-500', 'error-shake');
|
|
isValid = false;
|
|
}
|
|
|
|
// Validate password
|
|
if (!passwordInput.value) {
|
|
passwordError.textContent = 'Password is required';
|
|
passwordError.classList.remove('hidden');
|
|
passwordInput.parentElement.classList.add('border-red-500', 'error-shake');
|
|
isValid = false;
|
|
} else if (passwordInput.value.length < 6) {
|
|
passwordError.textContent = 'Password must be at least 6 characters';
|
|
passwordError.classList.remove('hidden');
|
|
passwordInput.parentElement.classList.add('border-red-500', 'error-shake');
|
|
isValid = false;
|
|
}
|
|
|
|
// Remove shake animation after it completes
|
|
setTimeout(() => {
|
|
usernameInput.parentElement.classList.remove('error-shake');
|
|
passwordInput.parentElement.classList.remove('error-shake');
|
|
}, 500);
|
|
|
|
return isValid;
|
|
}
|
|
|
|
// Handle form submission
|
|
form.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
if (!validateForm()) return;
|
|
|
|
// Show loading state
|
|
loginBtn.disabled = true;
|
|
btnText.classList.add('hidden');
|
|
btnSpinner.classList.remove('hidden');
|
|
|
|
// Save/Remove credentials based on Remember Me
|
|
if (rememberMe.checked) {
|
|
localStorage.setItem('clqms_username', usernameInput.value);
|
|
localStorage.setItem('clqms_remember', 'true');
|
|
} else {
|
|
localStorage.removeItem('clqms_username');
|
|
localStorage.setItem('clqms_remember', 'false');
|
|
}
|
|
|
|
// Simulate API call (replace with actual login logic)
|
|
try {
|
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
|
|
// Simulate successful login
|
|
console.log('Login successful:', {
|
|
username: usernameInput.value,
|
|
rememberMe: rememberMe.checked
|
|
});
|
|
|
|
// Reset form
|
|
form.reset();
|
|
|
|
// Here you would typically redirect
|
|
// window.location.href = '/dashboard';
|
|
|
|
} catch (error) {
|
|
console.error('Login error:', error);
|
|
} finally {
|
|
// Reset button state
|
|
loginBtn.disabled = false;
|
|
btnText.classList.remove('hidden');
|
|
btnSpinner.classList.add('hidden');
|
|
}
|
|
});
|
|
|
|
// Real-time validation on input
|
|
usernameInput.addEventListener('input', () => {
|
|
if (usernameInput.value.trim()) {
|
|
usernameError.classList.add('hidden');
|
|
usernameInput.parentElement.classList.remove('border-red-500');
|
|
}
|
|
});
|
|
|
|
passwordInput.addEventListener('input', () => {
|
|
if (passwordInput.value) {
|
|
passwordError.classList.add('hidden');
|
|
passwordInput.parentElement.classList.remove('border-red-500');
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |