Spaces:
Runtime error
Runtime error
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>TreeTrack - Sign In</title> | |
| <link rel="icon" type="image/png" href="/static/image/icons8-tree-96.png"> | |
| <link rel="apple-touch-icon" href="/static/image/icons8-tree-96.png"> | |
| <link rel="stylesheet" href="/static/css/design-system.css"> | |
| <style> | |
| body { | |
| min-height: 100vh; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: var(--space-4); | |
| position: relative; | |
| overflow: hidden; | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; | |
| } | |
| /* Static Background Image */ | |
| .forest-background { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100vw; | |
| height: 100vh; | |
| z-index: -1; | |
| background-image: url('https://images.unsplash.com/photo-1441974231531-c6227db76b6e?q=80&w=2560&auto=format&fit=crop&ixlib=rb-4.0.3'); | |
| background-position: center center; | |
| background-size: cover; | |
| background-repeat: no-repeat; | |
| filter: brightness(0.9); | |
| } | |
| /* Elegant Login Container */ | |
| .tt-login-container { | |
| background: rgba(255, 255, 255, 0.95); | |
| backdrop-filter: blur(20px); | |
| border-radius: var(--radius-3xl); | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.1); | |
| padding: var(--space-8) var(--space-10); | |
| width: 100%; | |
| max-width: 420px; | |
| position: relative; | |
| /* Remove animation to prevent upward drift on refresh */ | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| /* Optional: Add animation only on first visit */ | |
| .tt-login-container.animate-in { | |
| animation: slideUp 0.6s ease-out; | |
| } | |
| @keyframes slideUp { | |
| from { | |
| opacity: 0; | |
| transform: translateY(30px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| /* Logo Section */ | |
| .logo-section { | |
| text-align: center; | |
| margin-bottom: var(--space-8); | |
| } | |
| .logo { | |
| font-size: var(--text-4xl); | |
| font-weight: var(--font-bold); | |
| background: linear-gradient(135deg, var(--primary-600), var(--primary-800)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| margin-bottom: var(--space-2); | |
| letter-spacing: -0.02em; | |
| } | |
| .logo-subtitle { | |
| color: var(--gray-600); | |
| font-size: var(--text-sm); | |
| font-weight: var(--font-medium); | |
| margin-bottom: var(--space-2); | |
| } | |
| /* Form Styling */ | |
| .login-form { | |
| display: flex; | |
| flex-direction: column; | |
| gap: var(--space-5); | |
| } | |
| .tt-form-input { | |
| padding: var(--space-4) var(--space-5); | |
| font-size: var(--text-base); | |
| border: 2px solid var(--gray-200); | |
| border-radius: var(--radius-xl); | |
| transition: all 0.2s ease; | |
| background: rgba(255, 255, 255, 0.8); | |
| } | |
| .tt-form-input:focus { | |
| border-color: var(--primary-500); | |
| box-shadow: 0 0 0 4px rgba(138, 176, 112, 0.1); | |
| background: white; | |
| } | |
| .tt-form-label { | |
| font-weight: var(--font-semibold); | |
| color: var(--gray-700); | |
| margin-bottom: var(--space-2); | |
| } | |
| /* Button Styling */ | |
| .login-button { | |
| margin-top: var(--space-2); | |
| position: relative; | |
| overflow: hidden; | |
| border-radius: var(--radius-xl); | |
| padding: var(--space-4) var(--space-6); | |
| font-size: var(--text-base); | |
| font-weight: var(--font-semibold); | |
| background: var(--gradient-primary); | |
| border: none; | |
| color: white; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| } | |
| .login-button:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15); | |
| } | |
| .login-button.loading::after { | |
| content: ''; | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| width: 20px; | |
| height: 20px; | |
| margin: -10px 0 0 -10px; | |
| border: 2px solid transparent; | |
| border-top: 2px solid #ffffff; | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| /* Demo Accounts Section - Minimized */ | |
| .demo-accounts { | |
| margin-top: var(--space-6); | |
| position: relative; | |
| } | |
| .demo-toggle { | |
| background: none; | |
| border: none; | |
| color: var(--primary-600); | |
| font-size: var(--text-sm); | |
| font-weight: var(--font-medium); | |
| cursor: pointer; | |
| padding: var(--space-3) 0; | |
| width: 100%; | |
| text-align: center; | |
| border-radius: var(--radius-lg); | |
| transition: all 0.2s ease; | |
| border: 1px solid transparent; | |
| } | |
| .demo-toggle:hover { | |
| background: var(--primary-50); | |
| border-color: var(--primary-200); | |
| } | |
| .accounts-dropdown { | |
| position: absolute; | |
| bottom: 100%; /* open upwards to avoid being cut off at the bottom */ | |
| left: 0; | |
| right: 0; | |
| background: white; | |
| border: 1px solid var(--gray-200); | |
| border-radius: var(--radius-xl); | |
| box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); | |
| z-index: 20; | |
| overflow: hidden; | |
| opacity: 0; | |
| transform: translateY(10px); /* animate from below upwards */ | |
| transition: all 0.2s ease; | |
| pointer-events: none; | |
| } | |
| .accounts-dropdown.show { | |
| opacity: 1; | |
| transform: translateY(0); | |
| pointer-events: auto; | |
| } | |
| .account-item { | |
| background: transparent; | |
| padding: var(--space-4) var(--space-5); | |
| border: none; | |
| width: 100%; | |
| text-align: left; | |
| cursor: pointer; | |
| transition: all 0.15s ease; | |
| border-bottom: 1px solid var(--gray-100); | |
| } | |
| .account-item:last-child { | |
| border-bottom: none; | |
| } | |
| .account-item:hover { | |
| background: var(--primary-50); | |
| } | |
| .account-role { | |
| font-weight: var(--font-semibold); | |
| color: var(--gray-900); | |
| font-size: var(--text-sm); | |
| margin-bottom: var(--space-1); | |
| } | |
| .account-username { | |
| color: var(--gray-600); | |
| font-size: var(--text-xs); | |
| } | |
| /* Messages */ | |
| .tt-message { | |
| padding: var(--space-3) var(--space-4); | |
| border-radius: var(--radius-lg); | |
| font-size: var(--text-sm); | |
| font-weight: var(--font-medium); | |
| margin-bottom: var(--space-4); | |
| } | |
| .tt-message-error { | |
| background: var(--error-50); | |
| color: var(--error-700); | |
| border: 1px solid var(--error-200); | |
| } | |
| .tt-message-success { | |
| background: var(--success-50); | |
| color: var(--success-700); | |
| border: 1px solid var(--success-200); | |
| } | |
| /* Footer */ | |
| .footer { | |
| text-align: center; | |
| margin-top: var(--space-6); | |
| color: var(--gray-500); | |
| font-size: var(--text-xs); | |
| } | |
| /* Mobile Responsiveness */ | |
| @media (max-width: 640px) { | |
| body { | |
| padding: var(--space-2); | |
| } | |
| .tt-login-container { | |
| padding: var(--space-6) var(--space-6); | |
| max-width: 100%; | |
| margin: 0; | |
| } | |
| .logo { | |
| font-size: var(--text-3xl); | |
| } | |
| } | |
| @media (max-width: 480px) { | |
| .tt-login-container { | |
| padding: var(--space-5); | |
| } | |
| .logo { | |
| font-size: var(--text-2xl); | |
| } | |
| } | |
| /* Demo Account Styling */ | |
| .account-item.demo-account { | |
| background: linear-gradient(135deg, var(--primary-50), var(--primary-100)); | |
| border: 2px solid var(--primary-200); | |
| margin-bottom: var(--space-2); | |
| } | |
| .account-item.demo-account:hover { | |
| background: linear-gradient(135deg, var(--primary-100), var(--primary-150)); | |
| border-color: var(--primary-300); | |
| transform: translateY(-1px); | |
| } | |
| .account-item.demo-account .account-role { | |
| color: var(--primary-700); | |
| font-weight: var(--font-semibold); | |
| } | |
| .account-item.demo-account .account-username { | |
| color: var(--primary-600); | |
| } | |
| /* Focus improvements */ | |
| .demo-toggle:focus, | |
| .account-item:focus { | |
| outline: 2px solid var(--primary-500); | |
| outline-offset: 2px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Forest Background --> | |
| <div class="forest-background"></div> | |
| <div class="tt-login-container"> | |
| <div class="logo-section"> | |
| <div class="logo">TreeTrack</div> | |
| <div class="logo-subtitle">Secure Field Research Access</div> | |
| </div> | |
| <form class="login-form" id="loginForm"> | |
| <div id="message" class="tt-message" style="display: none;"></div> | |
| <div class="tt-form-group"> | |
| <label class="tt-form-label" for="username">Username</label> | |
| <input class="tt-form-input" type="text" id="username" name="username" required autocomplete="username"> | |
| </div> | |
| <div class="tt-form-group"> | |
| <label class="tt-form-label" for="password">Password</label> | |
| <input class="tt-form-input" type="password" id="password" name="password" required autocomplete="current-password"> | |
| </div> | |
| <button class="tt-btn tt-btn-primary tt-btn-lg login-button" type="submit" id="loginButton"> | |
| <span id="buttonText">Sign In to TreeTrack</span> | |
| </button> | |
| </form> | |
| <div class="demo-accounts"> | |
| <button class="demo-toggle" id="demoToggle" onclick="toggleDemoAccounts()"> | |
| Choose Account ↓ | |
| </button> | |
| <div class="accounts-dropdown" id="accountsDropdown"> | |
| <button class="account-item demo-account" onclick="selectAccount('demo_user', event)"> | |
| <div class="account-role">Demo Account</div> | |
| <div class="account-username">demo_user (requires password)</div> | |
| </button> | |
| <button class="account-item" onclick="selectAccount('aalekh', event)"> | |
| <div class="account-role">Aalekh (Admin)</div> | |
| <div class="account-username">Full system access</div> | |
| </button> | |
| <button class="account-item" onclick="selectAccount('admin', event)"> | |
| <div class="account-role">System Admin</div> | |
| <div class="account-username">Administrative access</div> | |
| </button> | |
| <button class="account-item" onclick="selectAccount('ishita', event)"> | |
| <div class="account-role">Ishita</div> | |
| <div class="account-username">Tree research & documentation</div> | |
| </button> | |
| <button class="account-item" onclick="selectAccount('jeeb', event)"> | |
| <div class="account-role">Jeeb</div> | |
| <div class="account-username">Tree research & documentation</div> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="footer"> | |
| © 2025 TreeTrack - Secure Field Research Platform | |
| </div> | |
| </div> | |
| <script> | |
| function toggleDemoAccounts() { | |
| const dropdown = document.getElementById('accountsDropdown'); | |
| const toggle = document.getElementById('demoToggle'); | |
| if (dropdown.classList.contains('show')) { | |
| dropdown.classList.remove('show'); | |
| toggle.textContent = 'Choose Account ↓'; | |
| } else { | |
| dropdown.classList.add('show'); | |
| toggle.textContent = 'Hide Accounts ↑'; | |
| } | |
| } | |
| function selectAccount(username, event) { | |
| document.getElementById('username').value = username; | |
| document.getElementById('password').value = ''; | |
| document.getElementById('password').focus(); | |
| // Hide dropdown after selection | |
| const dropdown = document.getElementById('accountsDropdown'); | |
| const toggle = document.getElementById('demoToggle'); | |
| dropdown.classList.remove('show'); | |
| toggle.textContent = 'Choose Account ↓'; | |
| // Visual feedback on selection | |
| const button = event.target.closest('.account-item'); | |
| if (button) { | |
| button.style.background = 'var(--primary-100)'; | |
| setTimeout(() => { | |
| button.style.background = 'transparent'; | |
| }, 300); | |
| } | |
| } | |
| // Legacy function for backward compatibility | |
| function fillCredentials(username, event = null) { | |
| selectAccount(username, event || { target: document.body }); | |
| } | |
| // Close dropdown when clicking outside | |
| document.addEventListener('click', function(event) { | |
| const demoSection = document.querySelector('.demo-accounts'); | |
| const dropdown = document.getElementById('accountsDropdown'); | |
| if (!demoSection.contains(event.target) && dropdown.classList.contains('show')) { | |
| dropdown.classList.remove('show'); | |
| document.getElementById('demoToggle').textContent = 'Choose Account ↓'; | |
| } | |
| }); | |
| function showMessage(message, type = 'error') { | |
| const messageEl = document.getElementById('message'); | |
| messageEl.textContent = message; | |
| messageEl.className = `tt-message tt-message-${type} tt-fade-in`; | |
| messageEl.style.display = 'block'; | |
| if (type === 'success') { | |
| setTimeout(() => { | |
| messageEl.style.display = 'none'; | |
| }, 3000); | |
| } | |
| } | |
| function setLoading(loading) { | |
| const button = document.getElementById('loginButton'); | |
| const buttonText = document.getElementById('buttonText'); | |
| if (loading) { | |
| button.disabled = true; | |
| button.classList.add('loading'); | |
| buttonText.textContent = 'Signing In...'; | |
| } else { | |
| button.disabled = false; | |
| button.classList.remove('loading'); | |
| buttonText.textContent = 'Sign In to TreeTrack'; | |
| } | |
| } | |
| document.getElementById('loginForm').addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const username = document.getElementById('username').value.trim(); | |
| const password = document.getElementById('password').value; | |
| if (!username || !password) { | |
| showMessage('Please enter both username and password'); | |
| return; | |
| } | |
| setLoading(true); | |
| try { | |
| const response = await fetch('/api/auth/login', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ username, password }) | |
| }); | |
| const result = await response.json(); | |
| if (response.ok) { | |
| // Store authentication token | |
| localStorage.setItem('auth_token', result.token); | |
| localStorage.setItem('user_info', JSON.stringify(result.user)); | |
| showMessage('Login successful! Redirecting...', 'success'); | |
| // Redirect to main application | |
| setTimeout(() => { | |
| window.location.href = '/'; | |
| }, 1500); | |
| } else { | |
| showMessage(result.detail || 'Login failed. Please check your credentials.'); | |
| } | |
| } catch (error) { | |
| console.error('Login error:', error); | |
| showMessage('Network error. Please try again.'); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }); | |
| // Check if already logged in | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const token = localStorage.getItem('auth_token'); | |
| if (token) { | |
| // Validate token | |
| fetch('/api/auth/validate', { | |
| headers: { | |
| 'Authorization': `Bearer ${token}` | |
| } | |
| }) | |
| .then(response => response.ok ? window.location.href = '/' : null) | |
| .catch(() => { | |
| // Token invalid, remove it | |
| localStorage.removeItem('auth_token'); | |
| localStorage.removeItem('user_info'); | |
| }); | |
| } | |
| }); | |
| // Auto-fill demo username on page load for development | |
| // Auto-select ishita account for easy testing (password still needs to be entered) | |
| document.addEventListener('DOMContentLoaded', () => { | |
| setTimeout(() => { | |
| fillCredentials('ishita'); | |
| }, 1000); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |