diff --git a/web/static/css/style.css b/web/static/css/style.css index 9ca10da..94454d7 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -75,6 +75,209 @@ body { min-height: 100vh; } +/* Login Page */ +.login-page { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: var(--space-4); +} + +.login-container { + background: var(--white); + border-radius: var(--radius-xl); + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + padding: var(--space-10); + width: 100%; + max-width: 450px; + animation: fadeInUp 0.5s ease-out; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.login-header { + text-align: center; + margin-bottom: var(--space-8); +} + +.login-logo { + width: 80px; + height: 80px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto var(--space-4); + box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3); +} + +.login-logo i { + font-size: var(--font-size-3xl); + color: var(--white); +} + +.login-header h1 { + font-size: var(--font-size-2xl); + font-weight: 700; + color: var(--gray-900); + margin-bottom: var(--space-2); +} + +.login-header p { + color: var(--gray-600); + font-size: var(--font-size-base); +} + +.login-form { + margin-bottom: var(--space-6); +} + +.login-error { + display: flex; + align-items: center; + gap: var(--space-3); + padding: var(--space-4); + background: #fef2f2; + border: 2px solid #fecaca; + border-radius: var(--radius-lg); + color: #991b1b; + margin-bottom: var(--space-5); + animation: shake 0.5s ease-in-out; +} + +.login-error i { + font-size: var(--font-size-lg); + color: #ef4444; +} + +.login-error span { + font-weight: 500; + font-size: var(--font-size-sm); +} + +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 10%, 30%, 50%, 70%, 90% { transform: translateX(-10px); } + 20%, 40%, 60%, 80% { transform: translateX(10px); } +} + +.login-form .form-group { + margin-bottom: var(--space-5); +} + +.login-form label { + display: flex; + align-items: center; + gap: var(--space-2); + margin-bottom: var(--space-2); + font-weight: 600; + color: var(--gray-700); + font-size: var(--font-size-sm); +} + +.login-form label i { + color: #667eea; + font-size: var(--font-size-base); +} + +.login-form input { + width: 100%; + padding: var(--space-4); + border: 2px solid var(--gray-200); + border-radius: var(--radius-lg); + font-size: var(--font-size-base); + transition: var(--transition); + background: var(--white); +} + +.login-form input:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1); +} + +.login-form input.error { + border-color: #ef4444; + background: #fef2f2; +} + +.login-form input.error:focus { + border-color: #ef4444; + box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.1); +} + +.login-form input::placeholder { + color: var(--gray-400); +} + +.btn-block { + width: 100%; + justify-content: center; + padding: var(--space-4); + font-size: var(--font-size-base); + font-weight: 600; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: none; + margin-top: var(--space-6); + position: relative; + overflow: hidden; +} + +.btn-block::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: rgba(255, 255, 255, 0.2); + transition: left 0.5s ease; +} + +.btn-block:hover::before { + left: 100%; +} + +.btn-block:hover:not(:disabled) { + background: linear-gradient(135deg, #5568d3 0%, #6a3f8f 100%); + transform: translateY(-2px); + box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3); +} + +.btn-block:active:not(:disabled) { + transform: translateY(0); +} + +.login-footer { + text-align: center; + padding-top: var(--space-4); + border-top: 1px solid var(--gray-200); +} + +.login-footer p { + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-2); + margin: 0; +} + +.login-footer i { + color: var(--success-color); +} + .container { max-width: 1200px; margin: 0 auto; @@ -694,6 +897,24 @@ body { /* Responsive Design */ @media (max-width: 768px) { + .login-container { + padding: var(--space-6); + max-width: 100%; + } + + .login-logo { + width: 60px; + height: 60px; + } + + .login-logo i { + font-size: var(--font-size-2xl); + } + + .login-header h1 { + font-size: var(--font-size-xl); + } + .container { padding: 0 var(--space-3); } diff --git a/web/static/js/app.js b/web/static/js/app.js index d234f3a..11c7d8c 100644 --- a/web/static/js/app.js +++ b/web/static/js/app.js @@ -33,28 +33,25 @@ class MailingListApp { * Setup event listeners */ setupEventListeners() { - // Login/logout - document.getElementById('loginBtn').addEventListener('click', () => { + // Login form submission + document.getElementById('loginForm').addEventListener('submit', (e) => { + e.preventDefault(); this.handleLogin(); }); + // Clear error when user starts typing + document.getElementById('username').addEventListener('input', () => { + this.clearLoginError(); + }); + + document.getElementById('password').addEventListener('input', () => { + this.clearLoginError(); + }); + document.getElementById('logoutBtn').addEventListener('click', () => { this.logout(); }); - // Enter key in login inputs - document.getElementById('username').addEventListener('keypress', (e) => { - if (e.key === 'Enter') { - this.handleLogin(); - } - }); - - document.getElementById('password').addEventListener('keypress', (e) => { - if (e.key === 'Enter') { - this.handleLogin(); - } - }); - // Bulk import button document.getElementById('showBulkImportBtn').addEventListener('click', () => { uiManager.showBulkImportModal(); @@ -91,14 +88,53 @@ class MailingListApp { const username = usernameInput.value.trim(); const password = passwordInput.value.trim(); + // Clear previous error states + this.clearLoginError(); + if (!username || !password) { - uiManager.showNotification('Please enter both username and password', 'error'); + this.showLoginError('Please enter both username and password'); return; } await this.login(username, password); } + /** + * Show login error message + */ + showLoginError(message) { + const errorDiv = document.getElementById('loginError'); + const errorMessage = document.getElementById('loginErrorMessage'); + const usernameInput = document.getElementById('username'); + const passwordInput = document.getElementById('password'); + + errorMessage.textContent = message; + errorDiv.style.display = 'flex'; + + // Add error class to inputs + usernameInput.classList.add('error'); + passwordInput.classList.add('error'); + + // Remove animation class and re-add to trigger animation + errorDiv.style.animation = 'none'; + setTimeout(() => { + errorDiv.style.animation = ''; + }, 10); + } + + /** + * Clear login error message + */ + clearLoginError() { + const errorDiv = document.getElementById('loginError'); + const usernameInput = document.getElementById('username'); + const passwordInput = document.getElementById('password'); + + errorDiv.style.display = 'none'; + usernameInput.classList.remove('error'); + passwordInput.classList.remove('error'); + } + /** * Authenticate with API */ @@ -124,7 +160,20 @@ class MailingListApp { apiClient.clearToken(); localStorage.removeItem('authToken'); - uiManager.handleError(error, 'Login failed'); + // Show error on login form + let errorMessage = 'Login failed'; + if (error.message) { + if (error.message.includes('401') || error.message.includes('Unauthorized') || + error.message.includes('Invalid') || error.message.includes('credentials')) { + errorMessage = 'Invalid username or password'; + } else if (error.message.includes('network') || error.message.includes('fetch')) { + errorMessage = 'Unable to connect to server'; + } else { + errorMessage = error.message; + } + } + + this.showLoginError(errorMessage); } finally { uiManager.setLoading(false); } @@ -153,8 +202,8 @@ class MailingListApp { * Show authenticated UI */ showAuthenticatedUI() { - document.getElementById('authControls').style.display = 'none'; - document.getElementById('userInfo').style.display = 'flex'; + document.getElementById('loginPage').style.display = 'none'; + document.getElementById('mainHeader').style.display = 'block'; document.getElementById('mainContent').style.display = 'block'; // Clear login inputs @@ -180,8 +229,8 @@ class MailingListApp { * Show unauthenticated UI */ showUnauthenticatedUI() { - document.getElementById('authControls').style.display = 'flex'; - document.getElementById('userInfo').style.display = 'none'; + document.getElementById('loginPage').style.display = 'flex'; + document.getElementById('mainHeader').style.display = 'none'; document.getElementById('mainContent').style.display = 'none'; }