import React, { useState, useEffect } from 'react' import { useNavigate } from 'react-router-dom' import { useAuthStore } from '../store/authStore' import { useSettingsStore } from '../store/settingsStore' import { useTranslation } from '../i18n' import { authApi } from '../api/client' import { Plane, Eye, EyeOff, Mail, Lock, MapPin, Calendar, Package, User, Globe, Zap, Users, Wallet, Map, CheckSquare, BookMarked, FolderOpen, Route, Shield } from 'lucide-react' export default function LoginPage() { const { t, language } = useTranslation() const [mode, setMode] = useState('login') // 'login' | 'register' const [username, setUsername] = useState('') const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [showPassword, setShowPassword] = useState(false) const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState('') const [appConfig, setAppConfig] = useState(null) const { login, register, demoLogin } = useAuthStore() const { setLanguageLocal } = useSettingsStore() const navigate = useNavigate() useEffect(() => { authApi.getAppConfig?.().catch(() => null).then(config => { if (config) { setAppConfig(config) if (!config.has_users) setMode('register') } }) // Handle OIDC callback token (via URL fragment to avoid logging) const hash = window.location.hash.substring(1) const hashParams = new URLSearchParams(hash) const token = hashParams.get('token') const params = new URLSearchParams(window.location.search) const oidcError = params.get('oidc_error') if (token) { localStorage.setItem('auth_token', token) window.history.replaceState({}, '', '/login') login.__fromOidc = true navigate('/dashboard') window.location.reload() } if (oidcError) { const errorMessages = { registration_disabled: language === 'de' ? 'Registrierung ist deaktiviert. Kontaktiere den Administrator.' : 'Registration is disabled. Contact your administrator.', no_email: language === 'de' ? 'Keine E-Mail vom Provider erhalten.' : 'No email received from provider.', token_failed: language === 'de' ? 'Authentifizierung fehlgeschlagen.' : 'Authentication failed.', invalid_state: language === 'de' ? 'Ungueltige Sitzung. Bitte erneut versuchen.' : 'Invalid session. Please try again.', } setError(errorMessages[oidcError] || oidcError) window.history.replaceState({}, '', '/login') } }, []) const handleDemoLogin = async () => { setError('') setIsLoading(true) try { await demoLogin() setShowTakeoff(true) setTimeout(() => navigate('/dashboard'), 2600) } catch (err) { setError(err.message || 'Demo-Login fehlgeschlagen') } finally { setIsLoading(false) } } const [showTakeoff, setShowTakeoff] = useState(false) const handleSubmit = async (e) => { e.preventDefault() setError('') setIsLoading(true) try { if (mode === 'register') { if (!username.trim()) { setError('Username is required'); setIsLoading(false); return } if (password.length < 6) { setError('Password must be at least 6 characters'); setIsLoading(false); return } await register(username, email, password) } else { await login(email, password) } setShowTakeoff(true) setTimeout(() => navigate('/dashboard'), 2600) } catch (err) { setError(err.message || t('login.error')) setIsLoading(false) } } const showRegisterOption = appConfig?.allow_registration || !appConfig?.has_users const inputBase = { width: '100%', padding: '11px 12px 11px 40px', border: '1px solid #e5e7eb', borderRadius: 12, fontSize: 14, fontFamily: 'inherit', outline: 'none', color: '#111827', background: 'white', boxSizing: 'border-box', transition: 'border-color 0.15s', } if (showTakeoff) { return (
{/* Sky gradient */}
{/* Stars */} {Array.from({ length: 60 }, (_, i) => (
0.7 ? 3 : 1.5, height: Math.random() > 0.7 ? 3 : 1.5, borderRadius: '50%', background: 'white', top: `${Math.random() * 100}%`, left: `${Math.random() * 100}%`, animationDelay: `${0.3 + Math.random() * 0.5}s, ${Math.random() * 1}s`, }} /> ))} {/* Clouds rushing past */} {[0, 1, 2, 3, 4].map(i => (
))} {/* Speed lines */} {Array.from({ length: 12 }, (_, i) => (
))} {/* Plane */}
{/* Contrail */}
{/* Logo fade in + burst */}
NOMAD

{t('login.tagline')}

) } return (
{/* Sprach-Toggle oben rechts */} {/* Left — branding */}
{/* Stars */}
{Array.from({ length: 40 }, (_, i) => (
0.7 ? 2 : 1, height: Math.random() > 0.7 ? 2 : 1, borderRadius: '50%', background: 'white', opacity: 0.15 + Math.random() * 0.25, top: `${Math.random() * 70}%`, left: `${Math.random() * 100}%`, animationDelay: `${Math.random() * 4}s`, }} /> ))}
{/* Animated glow orbs */}
{/* Animated planes — realistic silhouettes at different sizes/speeds */}
{/* Plane 1 — large, slow, foreground */} {/* Plane 2 — small, faster, higher */} {/* Plane 3 — medium, mid-speed */} {/* Plane 4 — tiny, fast, high */} {/* Plane 5 — medium, right to left, lower */} {/* Plane 6 — tiny distant */}
{/* Logo */}
NOMAD

{t('login.tagline')}

{t('login.description')}

{[ { Icon: Map, label: t('login.features.maps'), desc: t('login.features.mapsDesc') }, { Icon: Zap, label: t('login.features.realtime'), desc: t('login.features.realtimeDesc') }, { Icon: Wallet, label: t('login.features.budget'), desc: t('login.features.budgetDesc') }, { Icon: Users, label: t('login.features.collab'), desc: t('login.features.collabDesc') }, { Icon: CheckSquare, label: t('login.features.packing'), desc: t('login.features.packingDesc') }, { Icon: BookMarked, label: t('login.features.bookings'), desc: t('login.features.bookingsDesc') }, { Icon: FolderOpen, label: t('login.features.files'), desc: t('login.features.filesDesc') }, { Icon: Route, label: t('login.features.routes'), desc: t('login.features.routesDesc') }, ].map(({ Icon, label, desc }) => (
{ e.currentTarget.style.background = 'rgba(255,255,255,0.08)'; e.currentTarget.style.borderColor = 'rgba(255,255,255,0.12)' }} onMouseLeave={e => { e.currentTarget.style.background = 'rgba(255,255,255,0.04)'; e.currentTarget.style.borderColor = 'rgba(255,255,255,0.06)' }}>
{label}
{desc}
))}

{t('login.selfHosted')}

{/* Right — form */}
{/* Mobile logo */}
NOMAD

{t('login.tagline')}

{mode === 'register' ? (!appConfig?.has_users ? t('login.createAdmin') : t('login.createAccount')) : t('login.title')}

{mode === 'register' ? (!appConfig?.has_users ? t('login.createAdminHint') : t('login.createAccountHint')) : t('login.subtitle')}

{error && (
{error}
)} {/* Username (register only) */} {mode === 'register' && (
setUsername(e.target.value)} required placeholder="admin" style={inputBase} onFocus={e => e.target.style.borderColor = '#111827'} onBlur={e => e.target.style.borderColor = '#e5e7eb'} />
)} {/* Email */}
setEmail(e.target.value)} required placeholder={t('login.emailPlaceholder')} style={inputBase} onFocus={e => e.target.style.borderColor = '#111827'} onBlur={e => e.target.style.borderColor = '#e5e7eb'} />
{/* Password */}
setPassword(e.target.value)} required placeholder="••••••••" style={{ ...inputBase, paddingRight: 44 }} onFocus={e => e.target.style.borderColor = '#111827'} onBlur={e => e.target.style.borderColor = '#e5e7eb'} />
{/* Toggle login/register */} {showRegisterOption && appConfig?.has_users && !appConfig?.demo_mode && (

{mode === 'login' ? t('login.noAccount') + ' ' : t('login.hasAccount') + ' '}

)}
{/* OIDC / SSO login button */} {appConfig?.oidc_configured && ( <>
{language === 'de' ? 'oder' : 'or'}
{ e.currentTarget.style.background = '#f9fafb'; e.currentTarget.style.borderColor = '#9ca3af' }} onMouseLeave={e => { e.currentTarget.style.background = 'white'; e.currentTarget.style.borderColor = '#d1d5db' }} > {language === 'de' ? `Anmelden mit ${appConfig.oidc_display_name}` : `Sign in with ${appConfig.oidc_display_name}`} )} {/* Demo login button */} {appConfig?.demo_mode && ( )}
) }