done ver1.0.0

This commit is contained in:
2026-04-02 11:16:18 +07:00
parent 58dbefa155
commit d09ba3d2ad
21 changed files with 3271 additions and 668 deletions

View File

@@ -8,75 +8,7 @@
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet"/>
<!-- Material Symbols -->
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap" rel="stylesheet"/>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
colors: {
"on-secondary-fixed-variant": "#4e5c71",
"on-secondary": "#f8f8ff",
"secondary-fixed-dim": "#c7d5ed",
"surface-variant": "#d9e4ea",
"surface-tint": "#3755c3",
"primary-container": "#dde1ff",
"primary-dim": "#2848b7",
"on-background": "#2a3439",
"surface-container-lowest": "#ffffff",
"tertiary-fixed-dim": "#d4cdee",
"on-tertiary-container": "#514d68",
"error-container": "#fe8983",
"on-secondary-container": "#455367",
"outline": "#717c82",
"on-primary": "#f8f7ff",
"on-primary-container": "#2747b6",
"inverse-primary": "#6d89fa",
"on-surface": "#2a3439",
"primary-fixed": "#dde1ff",
"on-primary-fixed": "#0732a3",
"secondary-dim": "#465468",
"surface-container-high": "#e1e9ee",
"surface-container-highest": "#d9e4ea",
"on-primary-fixed-variant": "#3352c0",
"on-error-container": "#752121",
"secondary": "#526074",
"tertiary-fixed": "#e3dbfd",
"primary": "#3755c3",
"surface-dim": "#cfdce3",
"tertiary": "#605c78",
"on-error": "#fff7f6",
"secondary-fixed": "#d5e3fc",
"error-dim": "#4e0309",
"surface-bright": "#f7f9fb",
"on-surface-variant": "#566166",
"on-tertiary": "#fcf7ff",
"tertiary-container": "#e3dbfd",
"inverse-on-surface": "#9a9d9f",
"on-tertiary-fixed-variant": "#5b5672",
"tertiary-dim": "#54506b",
"outline-variant": "#a9b4b9",
"on-secondary-fixed": "#324053",
"inverse-surface": "#0b0f10",
"on-tertiary-fixed": "#3e3a54",
"primary-fixed-dim": "#cad2ff",
"surface-container": "#e8eff3",
"secondary-container": "#d5e3fc",
"surface-container-low": "#f0f4f7",
"background": "#f7f9fb",
"error": "#9f403d",
"surface": "#f7f9fb"
},
fontFamily: {
"headline": ["Manrope"],
"body": ["Inter"],
"label": ["Inter"]
},
borderRadius: {"DEFAULT": "0.125rem", "lg": "0.25rem", "xl": "0.5rem", "full": "0.75rem"},
},
},
}
</script>
<link rel="stylesheet" href="../css/main.css" />
<style>
.material-symbols-outlined {
font-family: 'Material Symbols Outlined';
@@ -110,6 +42,11 @@
<p class="text-xs uppercase tracking-widest text-on-surface-variant font-bold mt-2">Account Management System</p>
</div>
<div class="flex gap-2 mb-6" role="tablist" aria-label="Auth switcher">
<button id="loginTab" type="button" class="flex-1 py-2.5 rounded-lg text-xs font-bold uppercase tracking-wider border border-outline-variant/30 bg-primary text-on-primary shadow-sm">Đăng nhập</button>
<button id="registerTab" type="button" class="flex-1 py-2.5 rounded-lg text-xs font-bold uppercase tracking-wider border border-outline-variant/30 bg-surface-container-low text-on-surface-variant">Đăng ký</button>
</div>
<!-- Login Form -->
<form id="loginForm" class="space-y-5">
<!-- Username/Email Input -->
@@ -172,6 +109,85 @@
<div id="errorMessage" class="hidden bg-error-container/20 text-error/80 border border-error/30 rounded-lg px-4 py-3 text-xs font-medium"></div>
</form>
<!-- Register Form -->
<form id="registerForm" class="space-y-5 hidden">
<div>
<label for="regFullname" class="block text-xs font-bold text-on-surface-variant uppercase tracking-wider mb-2">Full Name</label>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant/60">
<span class="material-symbols-outlined text-base">badge</span>
</span>
<input
type="text"
id="regFullname"
name="fullname"
placeholder="Enter your full name"
class="w-full pl-10 pr-4 py-2.5 bg-surface-container-low border border-outline-variant/30 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition-all text-sm font-medium"
/>
</div>
</div>
<div>
<label for="regEmail" class="block text-xs font-bold text-on-surface-variant uppercase tracking-wider mb-2">Email</label>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant/60">
<span class="material-symbols-outlined text-base">mail</span>
</span>
<input
type="email"
id="regEmail"
name="email"
placeholder="Enter your email"
class="w-full pl-10 pr-4 py-2.5 bg-surface-container-low border border-outline-variant/30 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition-all text-sm font-medium"
/>
</div>
</div>
<div>
<label for="regUsername" class="block text-xs font-bold text-on-surface-variant uppercase tracking-wider mb-2">Username</label>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant/60">
<span class="material-symbols-outlined text-base">person_add</span>
</span>
<input
type="text"
id="regUsername"
name="username"
placeholder="Choose a username"
required
class="w-full pl-10 pr-4 py-2.5 bg-surface-container-low border border-outline-variant/30 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition-all text-sm font-medium"
/>
</div>
</div>
<div>
<label for="regPassword" class="block text-xs font-bold text-on-surface-variant uppercase tracking-wider mb-2">Password</label>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant/60">
<span class="material-symbols-outlined text-base">lock</span>
</span>
<input
type="password"
id="regPassword"
name="password"
placeholder="Create a password"
required
class="w-full pl-10 pr-4 py-2.5 bg-surface-container-low border border-outline-variant/30 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition-all text-sm font-medium"
/>
</div>
</div>
<button
type="submit"
class="w-full bg-primary hover:bg-primary-dim text-on-primary font-bold py-2.5 px-4 rounded-lg transition-all active:scale-95 duration-100 flex items-center justify-center gap-2 mt-4"
>
<span class="material-symbols-outlined text-sm">how_to_reg</span>
<span>Tạo tài khoản</span>
</button>
<div id="registerErrorMessage" class="hidden bg-error-container/20 text-error/80 border border-error/30 rounded-lg px-4 py-3 text-xs font-medium"></div>
</form>
<!-- Footer -->
<!-- <div class="mt-8 pt-6 border-t border-outline-variant/10 text-center">
<p class="text-[10px] text-on-surface-variant/60">Default credentials for demo: admin / admin</p>
@@ -192,11 +208,35 @@
const usernameInput = document.getElementById('username');
const passwordInput = document.getElementById('password');
const rememberCheckbox = document.getElementById('remember');
const registerForm = document.getElementById('registerForm');
const registerErrorMessage = document.getElementById('registerErrorMessage');
const regFullnameInput = document.getElementById('regFullname');
const regEmailInput = document.getElementById('regEmail');
const regUsernameInput = document.getElementById('regUsername');
const regPasswordInput = document.getElementById('regPassword');
const loginTab = document.getElementById('loginTab');
const registerTab = document.getElementById('registerTab');
let currentMode = 'login';
// Demo credentials
const validCredentials = {
username: 'admin',
password: 'admin'
const setMode = (mode) => {
currentMode = mode;
const isLogin = mode === 'login';
loginForm.classList.toggle('hidden', !isLogin);
registerForm.classList.toggle('hidden', isLogin);
errorMessage.classList.add('hidden');
registerErrorMessage.classList.add('hidden');
const activate = (btn, active) => {
btn.classList.toggle('bg-primary', active);
btn.classList.toggle('text-on-primary', active);
btn.classList.toggle('shadow-sm', active);
btn.classList.toggle('bg-surface-container-low', !active);
btn.classList.toggle('text-on-surface-variant', !active);
};
activate(loginTab, isLogin);
activate(registerTab, !isLogin);
};
// Check if already logged in
@@ -206,6 +246,8 @@
window.location.href = './index.html';
}
setMode('login');
// Restore remembered username
const rememberedUsername = localStorage.getItem('rememberedUsername');
if (rememberedUsername) {
@@ -214,39 +256,96 @@
}
});
loginForm.addEventListener('submit', (e) => {
loginTab.addEventListener('click', () => setMode('login'));
registerTab.addEventListener('click', () => setMode('register'));
loginForm.addEventListener('submit', async (e) => {
e.preventDefault();
errorMessage.classList.add('hidden');
const username = usernameInput.value.trim();
const password = passwordInput.value;
// Validate credentials
if (username === validCredentials.username && password === validCredentials.password) {
// Store user info
const userData = {
username: username,
role: 'Administrator',
loginTime: new Date().toISOString()
};
try {
// Call backend login API
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
localStorage.setItem('currentUser', JSON.stringify(userData));
const data = await response.json();
// Handle remember me
if (rememberCheckbox.checked) {
localStorage.setItem('rememberedUsername', username);
if (data.success && data.user) {
// Store user info from backend
localStorage.setItem('currentUser', JSON.stringify(data.user));
// Handle remember me
if (rememberCheckbox.checked) {
localStorage.setItem('rememberedUsername', username);
} else {
localStorage.removeItem('rememberedUsername');
}
// Redirect to dashboard
window.location.href = './index.html';
} else {
localStorage.removeItem('rememberedUsername');
// Show error
errorMessage.textContent = data.message || 'Invalid username or password';
errorMessage.classList.remove('hidden');
passwordInput.value = '';
passwordInput.focus();
}
// Redirect to dashboard
window.location.href = './index.html';
} else {
// Show error
errorMessage.textContent = 'Invalid username or password. Try admin / admin';
} catch (error) {
errorMessage.textContent = 'Connection error. Try admin / admin';
errorMessage.classList.remove('hidden');
passwordInput.value = '';
passwordInput.focus();
console.error('Login error:', error);
}
});
registerForm.addEventListener('submit', async (e) => {
e.preventDefault();
registerErrorMessage.classList.add('hidden');
const payload = {
fullname: regFullnameInput.value.trim(),
email: regEmailInput.value.trim(),
username: regUsernameInput.value.trim(),
password: regPasswordInput.value
};
if (!payload.username || !payload.password) {
registerErrorMessage.textContent = 'Username and password are required';
registerErrorMessage.classList.remove('hidden');
return;
}
try {
const response = await fetch('/api/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
const isJson = response.headers.get('content-type')?.includes('application/json');
const data = isJson ? await response.json() : null;
if (response.ok && data?.success && data.user) {
localStorage.setItem('currentUser', JSON.stringify(data.user));
localStorage.setItem('rememberedUsername', payload.username);
window.location.href = './index.html';
} else {
const fallback = isJson ? (data?.message || 'Registration failed') : 'Server error (HTML response)';
registerErrorMessage.textContent = fallback;
registerErrorMessage.classList.remove('hidden');
}
} catch (error) {
registerErrorMessage.textContent = 'Connection error. Please try again.';
registerErrorMessage.classList.remove('hidden');
console.error('Register error:', error);
}
});
</script>