Spaces:
Sleeping
Sleeping
| --- | |
| import Layout from '../layouts/Layout.astro'; | |
| const title = "Đăng nhập | DPTXA - Thiên đường điện ảnh"; | |
| const description = "Đăng nhập vào tài khoản DPTXA để trải nghiệm kho phim khổng lồ và lưu lại những bộ phim yêu thích của bạn."; | |
| --- | |
| <Layout title={title} description={description}> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" /> | |
| <div class="relative min-h-[calc(100vh-64px)] w-full overflow-hidden flex items-center justify-center py-12 px-4"> | |
| <div class="absolute inset-0 z-0"> | |
| <img | |
| src="/images/auth/bg.png" | |
| alt="Background" | |
| class="h-full w-full object-cover scale-105 blur-[2px]" | |
| /> | |
| <div class="absolute inset-0 bg-gradient-to-b from-[#0f0f0f]/80 via-[#0f0f0f]/60 to-[#0f0f0f] z-10"></div> | |
| </div> | |
| <div class="relative z-20 w-full max-w-[450px] glass rounded-[32px] p-8 md:p-12 shadow-[0_0_50px_rgba(0,0,0,0.5)] ring-1 ring-white/10 backdrop-blur-2xl animate-in fade-in zoom-in duration-700"> | |
| <div class="owl-container mb-6 flex justify-center"> | |
| <div class="owl relative w-24 h-24 bg-[#1a1a1a] rounded-full border-2 border-white/10 flex items-center justify-center shadow-lg"> | |
| <div class="owl-ears absolute top-[-5px] left-0 right-0 flex justify-between px-3"> | |
| <div class="w-4 h-4 bg-[#1a1a1a] rotate-45 rounded-tl-md border-l-2 border-t-2 border-white/10"></div> | |
| <div class="w-4 h-4 bg-[#1a1a1a] rotate-45 rounded-tr-md border-r-2 border-t-2 border-white/10"></div> | |
| </div> | |
| <div class="owl-eyes flex gap-4 z-10 transition-transform duration-300"> | |
| <div class="eye w-7 h-7 bg-white rounded-full flex items-center justify-center relative overflow-hidden"> | |
| <div class="pupil w-3 h-3 bg-black rounded-full absolute transition-all duration-100"></div> | |
| </div> | |
| <div class="eye w-7 h-7 bg-white rounded-full flex items-center justify-center relative overflow-hidden"> | |
| <div class="pupil w-3 h-3 bg-black rounded-full absolute transition-all duration-100"></div> | |
| </div> | |
| </div> | |
| <div class="owl-wings absolute inset-0 flex justify-between px-2 items-center pointer-events-none z-20"> | |
| <div class="wing-left w-6 h-12 bg-[#2a2a2a] rounded-full border border-white/10 origin-bottom transition-all duration-300 translate-y-12 opacity-0"></div> | |
| <div class="wing-right w-6 h-12 bg-[#2a2a2a] rounded-full border border-white/10 origin-bottom transition-all duration-300 translate-y-12 opacity-0"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="text-center mb-8"> | |
| <h1 class="text-3xl font-black tracking-tighter text-white mb-2">Mừng bạn trở lại!</h1> | |
| <p class="text-gray-400 text-sm font-medium">Đăng nhập để tiếp tục xem phim nào</p> | |
| </div> | |
| <form id="login-form" class="space-y-5" novalidate> | |
| <div class="space-y-1"> | |
| <label for="identity" class="text-[10px] font-black uppercase tracking-widest text-gray-500 ml-1">Tài khoản hoặc Email</label> | |
| <div class="relative group input-container"> | |
| <i class="fas fa-user absolute left-5 top-1/2 -translate-y-1/2 text-gray-500 group-focus-within:text-primary transition-colors z-10"></i> | |
| <input | |
| type="text" | |
| id="identity" | |
| name="identity" | |
| placeholder="Nhập tên tài khoản hoặc email..." | |
| class="w-full bg-white/5 border border-white/10 rounded-2xl py-4 pl-12 pr-10 text-sm text-white placeholder:text-gray-600 focus:outline-none focus:ring-2 focus:ring-primary/50 transition-all z-0" | |
| required | |
| /> | |
| <i class="validation-icon fas absolute right-4 top-1/2 -translate-y-1/2 opacity-0 transition-opacity pointer-events-none"></i> | |
| </div> | |
| <p class="error-msg text-[11px] text-red-500 font-medium ml-1 h-4 opacity-0 transition-opacity"></p> | |
| </div> | |
| <div class="space-y-1"> | |
| <div class="flex items-center justify-between ml-1"> | |
| <label for="password" class="text-[10px] font-black uppercase tracking-widest text-gray-500">Mật khẩu</label> | |
| <a href="#" class="text-[10px] font-black uppercase tracking-widest text-primary hover:underline transition-all">Quên mật khẩu?</a> | |
| </div> | |
| <div class="relative group input-container"> | |
| <i class="fas fa-lock absolute left-5 top-1/2 -translate-y-1/2 text-gray-500 group-focus-within:text-primary transition-colors z-10"></i> | |
| <input | |
| type="password" | |
| id="password" | |
| name="password" | |
| placeholder="••••••••" | |
| class="w-full bg-white/5 border border-white/10 rounded-2xl py-4 pl-12 pr-24 text-sm text-white placeholder:text-gray-600 focus:outline-none focus:ring-2 focus:ring-primary/50 transition-all z-0" | |
| required | |
| /> | |
| <div class="absolute right-4 top-1/2 -translate-y-1/2 flex items-center gap-3 z-10"> | |
| <i class="validation-icon fas opacity-0 transition-opacity pointer-events-none"></i> | |
| <button | |
| type="button" | |
| id="toggle-password" | |
| class="text-gray-500 hover:text-white transition-colors" | |
| > | |
| <i class="fas fa-eye"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <p class="error-msg text-[11px] text-red-500 font-medium ml-1 h-4 opacity-0 transition-opacity"></p> | |
| </div> | |
| <label class="flex items-center gap-3 cursor-pointer group w-fit pt-1"> | |
| <div class="relative flex items-center justify-center shrink-0"> | |
| <input type="checkbox" class="peer sr-only" /> | |
| <div class="h-5 w-5 rounded-md border border-white/10 bg-white/5 transition-all peer-checked:bg-primary peer-checked:border-primary"></div> | |
| <i class="fas fa-check absolute text-[10px] text-white opacity-0 peer-checked:opacity-100 transition-opacity"></i> | |
| </div> | |
| <span class="text-xs font-medium text-gray-400 group-hover:text-gray-200 transition-colors select-none">Ghi nhớ đăng nhập</span> | |
| </label> | |
| <button | |
| type="submit" | |
| class="w-full bg-primary hover:bg-red-700 text-white font-black uppercase tracking-widest py-4 rounded-2xl shadow-[0_0_20px_rgba(229,9,20,0.3)] hover:shadow-[0_0_30px_rgba(229,9,20,0.5)] transition-all active:scale-95 flex items-center justify-center gap-3 mt-2" | |
| > | |
| <span>Đăng nhập ngay</span> | |
| <i class="fas fa-arrow-right text-xs"></i> | |
| </button> | |
| <div class="relative flex items-center py-2"> | |
| <div class="flex-grow border-t border-white/5"></div> | |
| <span class="flex-shrink mx-4 text-[10px] font-black uppercase tracking-widest text-gray-600">Hoặc tiếp tục với</span> | |
| <div class="flex-grow border-t border-white/5"></div> | |
| </div> | |
| <div class="grid grid-cols-2 gap-4"> | |
| <button | |
| type="button" | |
| id="google-login-btn" | |
| data-txatooltip="Đăng nhập với Google" | |
| class="flex items-center justify-center gap-3 bg-white/5 hover:bg-white/10 border border-white/10 py-3 rounded-2xl transition-all group" | |
| > | |
| <i class="fab fa-google text-red-500 group-hover:scale-110 transition-transform"></i> | |
| <span class="text-xs font-black uppercase tracking-widest text-white">Google</span> | |
| </button> | |
| <button | |
| type="button" | |
| id="facebook-login-btn" | |
| data-txatooltip="Đăng nhập với Facebook" | |
| class="flex items-center justify-center gap-3 bg-white/5 hover:bg-white/10 border border-white/10 py-3 rounded-2xl transition-all group" | |
| > | |
| <i class="fab fa-facebook text-blue-500 group-hover:scale-110 transition-transform"></i> | |
| <span class="text-xs font-black uppercase tracking-widest text-white">Facebook</span> | |
| </button> | |
| </div> | |
| </form> | |
| <div class="mt-8 text-center"> | |
| <p class="text-gray-500 text-xs font-medium"> | |
| Chưa có tài khoản? | |
| <a href="/dang-ky" class="text-primary font-black uppercase tracking-widest ml-1 hover:underline">Đăng ký tại đây</a> | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| </Layout> | |
| <script is:inline> | |
| function initLoginPage() { | |
| const form = document.getElementById('login-form'); | |
| const togglePass = document.getElementById('toggle-password'); | |
| const passInput = document.getElementById('password'); | |
| const identityInput = document.getElementById('identity'); | |
| const pupils = document.querySelectorAll('.pupil'); | |
| const wingLeft = document.querySelector('.wing-left'); | |
| const wingRight = document.querySelector('.wing-right'); | |
| // --- OWL ADAPTIVE TRACKING ANIMATION --- | |
| const setupTracking = (input) => { | |
| if (!input) return; | |
| input.addEventListener('input', (e) => { | |
| const length = e.target.value.length; | |
| const moveX = Math.min(Math.max(length * 0.5 - 5, -6), 6); | |
| pupils.forEach(pupil => { | |
| pupil.style.transform = `translate(${moveX}px, 3px)`; | |
| }); | |
| }); | |
| input.addEventListener('focus', () => { | |
| const length = input.value.length; | |
| const moveX = Math.min(Math.max(length * 0.5 - 5, -6), 6); | |
| pupils.forEach(pupil => pupil.style.transform = `translate(${moveX}px, 3px)`); | |
| }); | |
| input.addEventListener('blur', () => { | |
| pupils.forEach(pupil => pupil.style.transform = 'translate(0px, 0px)'); | |
| }); | |
| }; | |
| setupTracking(identityInput); | |
| const setupPasswordOwlEffect = (input) => { | |
| if (!input) return; | |
| input.addEventListener('focus', () => { | |
| wingLeft.style.opacity = '1'; | |
| wingRight.style.opacity = '1'; | |
| wingLeft.style.transform = 'translateY(-35px) scaleY(-1) rotate(15deg)'; | |
| wingRight.style.transform = 'translateY(-35px) scaleY(-1) rotate(-15deg)'; | |
| pupils.forEach(pupil => pupil.style.transform = 'translate(0px, -10px)'); | |
| }); | |
| input.addEventListener('blur', () => { | |
| if (passInput.getAttribute('type') === 'password') { | |
| wingLeft.style.opacity = '0'; | |
| wingRight.style.opacity = '0'; | |
| wingLeft.style.transform = 'translateY(48px)'; | |
| wingRight.style.transform = 'translateY(48px)'; | |
| } | |
| pupils.forEach(pupil => pupil.style.transform = 'translate(0px, 0px)'); | |
| }); | |
| }; | |
| setupPasswordOwlEffect(passInput); | |
| // Toggle Password Visibility | |
| if (togglePass && passInput) { | |
| togglePass.onclick = () => { | |
| const isShowing = passInput.getAttribute('type') === 'text'; | |
| const type = isShowing ? 'password' : 'text'; | |
| passInput.setAttribute('type', type); | |
| togglePass.querySelector('i').classList.toggle('fa-eye'); | |
| togglePass.querySelector('i').classList.toggle('fa-eye-slash'); | |
| if (!isShowing) { // Now showing text | |
| wingLeft.style.opacity = '0'; | |
| wingRight.style.opacity = '0'; | |
| wingLeft.style.transform = 'translateY(48px)'; | |
| wingRight.style.transform = 'translateY(48px)'; | |
| pupils.forEach(pupil => pupil.style.transform = 'translate(0px, 0px)'); | |
| } else { // Now hidden | |
| wingLeft.style.opacity = '1'; | |
| wingRight.style.opacity = '1'; | |
| wingLeft.style.transform = 'translateY(-35px) scaleY(-1) rotate(15deg)'; | |
| wingRight.style.transform = 'translateY(-35px) scaleY(-1) rotate(-15deg)'; | |
| pupils.forEach(pupil => pupil.style.transform = 'translate(0px, -10px)'); | |
| } | |
| }; | |
| } | |
| // --- REAL-TIME INPUT VALIDATION --- | |
| const validateInput = (input, rules) => { | |
| if (!input) return true; | |
| const container = input.closest('.input-container'); | |
| const icon = container.querySelector('.validation-icon'); | |
| const errorMsg = container.parentElement.querySelector('.error-msg'); | |
| let isValid = true; | |
| let message = ''; | |
| const val = input.value.trim(); | |
| for (const rule of rules) { | |
| const check = rule(val); | |
| if (check !== true) { | |
| isValid = false; | |
| message = check; | |
| break; | |
| } | |
| } | |
| if (!val && input.hasAttribute('required')) { | |
| isValid = false; | |
| message = 'Trường này không được để trống nhé!'; | |
| } | |
| if (isValid && val) { | |
| input.style.borderColor = 'rgba(16, 185, 129, 0.4)'; | |
| if (icon) { | |
| icon.className = 'validation-icon fas fa-check-circle absolute text-emerald-500 opacity-100 transition-all pointer-events-none scale-110'; | |
| if (input.id === 'identity') icon.style.right = '16px'; | |
| if (input.id === 'password') icon.style.right = '48px'; | |
| icon.style.top = '50%'; | |
| icon.style.transform = 'translateY(-50%)'; | |
| } | |
| if (errorMsg) { | |
| errorMsg.style.opacity = '0'; | |
| errorMsg.textContent = ''; | |
| } | |
| } else if (!isValid) { | |
| input.style.borderColor = 'rgba(239, 68, 68, 0.4)'; | |
| if (icon) { | |
| icon.className = 'validation-icon fas fa-exclamation-circle absolute text-red-500 opacity-100 transition-all pointer-events-none scale-110'; | |
| if (input.id === 'identity') icon.style.right = '16px'; | |
| if (input.id === 'password') icon.style.right = '48px'; | |
| icon.style.top = '50%'; | |
| icon.style.transform = 'translateY(-50%)'; | |
| } | |
| if (errorMsg) { | |
| errorMsg.textContent = message; | |
| errorMsg.style.opacity = '1'; | |
| } | |
| } else { | |
| input.style.borderColor = ''; | |
| if (icon) icon.style.opacity = '0'; | |
| if (errorMsg) errorMsg.style.opacity = '0'; | |
| } | |
| return isValid; | |
| }; | |
| [identityInput, passInput].forEach(el => { | |
| if (!el) return; | |
| el.addEventListener('input', () => { | |
| if (el === identityInput) { | |
| validateInput(el, [v => v.length >= 3 || 'Tối thiểu 3 ký tự!']); | |
| } else if (el === passInput) { | |
| validateInput(el, [v => v.length >= 6 || 'Tối thiểu 6 ký tự!']); | |
| } | |
| }); | |
| }); | |
| // Form Handle Submit | |
| if (form) { | |
| form.onsubmit = async (e) => { | |
| e.preventDefault(); | |
| const { txatoast } = window; | |
| const isIdentityValid = validateInput(identityInput, [v => v.length >= 3]); | |
| const isPasswordValid = validateInput(passInput, [v => v.length >= 6]); | |
| if (!isIdentityValid || !isPasswordValid) { | |
| if (txatoast) txatoast.error('Kiểm tra lại thông tin đăng nhập!'); | |
| return; | |
| } | |
| const submitBtn = form.querySelector('button[type="submit"]'); | |
| const originalBtnContent = submitBtn.innerHTML; | |
| try { | |
| const formData = new FormData(form); | |
| const data = Object.fromEntries(formData.entries()); | |
| submitBtn.disabled = true; | |
| submitBtn.style.opacity = '0.5'; | |
| submitBtn.innerHTML = '<i class="fas fa-circle-notch fa-spin"></i> <span>Đang xác thực...</span>'; | |
| if (txatoast) txatoast.info('Đang kiểm tra...', 2000, 'top-center'); | |
| // Tạo độ trễ 800ms tối thiểu để nút xoay mượt mà đậm chất điện ảnh | |
| const [response] = await Promise.all([ | |
| fetch('/api/auth/login', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(data) | |
| }), | |
| new Promise(resolve => setTimeout(resolve, 800)) | |
| ]); | |
| const result = await response.json(); | |
| if (!response.ok) { | |
| if (txatoast) txatoast.error(result.error || 'Sai tài khoản hoặc mật khẩu!'); | |
| submitBtn.disabled = false; | |
| submitBtn.style.opacity = '1'; | |
| submitBtn.innerHTML = originalBtnContent; | |
| return; | |
| } | |
| if (txatoast) txatoast.success('Đăng nhập thành công! Đang chuyển hướng...', 2000); | |
| setTimeout(() => window.location.href = '/', 1000); | |
| } catch (error) { | |
| if (txatoast) txatoast.error('Lỗi kết nối!'); | |
| submitBtn.disabled = false; | |
| submitBtn.style.opacity = '1'; | |
| submitBtn.innerHTML = originalBtnContent; | |
| } | |
| }; | |
| } | |
| // Social Logins Click | |
| const googleBtn = document.getElementById('google-login-btn'); | |
| const facebookBtn = document.getElementById('facebook-login-btn'); | |
| if (googleBtn) { | |
| googleBtn.onclick = () => { | |
| if (window.txatoast) { | |
| window.txatoast.info('Tính năng đăng nhập qua Google đang được phát triển!'); | |
| } | |
| }; | |
| } | |
| if (facebookBtn) { | |
| facebookBtn.onclick = () => { | |
| if (window.txatoast) { | |
| window.txatoast.info('Tính năng đăng nhập qua Facebook đang được phát triển!'); | |
| } | |
| }; | |
| } | |
| } | |
| document.addEventListener('astro:page-load', initLoginPage); | |
| </script> | |
| <style> | |
| .glass { | |
| background: rgba(255, 255, 255, 0.02); | |
| backdrop-filter: blur(25px); | |
| -webkit-backdrop-filter: blur(25px); | |
| border: 1px solid rgba(255, 255, 255, 0.08); | |
| } | |
| .text-primary { | |
| color: #e50914; | |
| } | |
| .bg-primary { | |
| background-color: #e50914; | |
| } | |
| input:focus { | |
| box-shadow: 0 0 20px rgba(229, 9, 20, 0.15); | |
| } | |
| </style> |