'use client' import { useState, useEffect, Suspense } from 'react' import { useRouter, useSearchParams } from 'next/navigation' import Link from 'next/link' import Image from 'next/image' import { useStore } from '@/lib/store' import { api } from '@/lib/api' import { Loader2, ArrowRight, Eye, EyeOff, CheckCircle } from 'lucide-react' // Logo Component function Logo() { return ( ) } // OAuth Icons function GoogleIcon({ className }: { className?: string }) { return ( ) } function GitHubIcon({ className }: { className?: string }) { return ( ) } function LoginForm() { const router = useRouter() const searchParams = useSearchParams() const { login } = useStore() const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [showPassword, setShowPassword] = useState(false) const [error, setError] = useState(null) const [loading, setLoading] = useState(false) const [oauthProviders, setOauthProviders] = useState({ google_enabled: false, github_enabled: false }) const [verified, setVerified] = useState(false) const sanitizeRedirect = (value: string | null | undefined): string => { const fallback = '/terminal/radar' if (!value) return fallback const v = value.trim() if (!v.startsWith('/')) return fallback if (v.startsWith('//')) return fallback if (v.includes('://')) return fallback if (v.includes('\\')) return fallback if (v.length > 2048) return fallback return v } // Get redirect URL from query params or localStorage (set during registration) const paramRedirect = searchParams.get('redirect') const [redirectTo, setRedirectTo] = useState(sanitizeRedirect(paramRedirect)) // Check localStorage for redirect (set during registration before email verification) useEffect(() => { const storedRedirect = localStorage.getItem('pounce_redirect_after_login') if (storedRedirect && !paramRedirect) { setRedirectTo(sanitizeRedirect(storedRedirect)) } }, [paramRedirect]) // Check for verified status useEffect(() => { if (searchParams.get('verified') === 'true') { setVerified(true) } if (searchParams.get('error')) { setError(searchParams.get('error') === 'oauth_failed' ? 'OAuth authentication failed. Please try again.' : 'Authentication failed') } }, [searchParams]) // Load OAuth providers useEffect(() => { api.getOAuthProviders().then(setOauthProviders).catch(() => {}) }, []) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() setError(null) setLoading(true) try { await login(email, password) // Check if email is verified const user = await api.getMe() if (!user.is_verified) { // Redirect to verify-email page if not verified router.push(`/verify-email?email=${encodeURIComponent(email)}`) return } // Clear stored redirect (was set during registration) localStorage.removeItem('pounce_redirect_after_login') // Redirect to intended destination or dashboard router.push(sanitizeRedirect(redirectTo)) } catch (err: unknown) { console.error('Login error:', err) if (err instanceof Error) { setError(err.message || 'Authentication failed') } else if (typeof err === 'object' && err !== null) { if ('detail' in err) { setError(String((err as { detail: unknown }).detail)) } else if ('message' in err) { setError(String((err as { message: unknown }).message)) } else { setError('Authentication failed. Please try again.') } } else if (typeof err === 'string') { setError(err) } else { setError('Authentication failed. Please try again.') } } finally { setLoading(false) } } // Generate register link with redirect preserved const registerLink = redirectTo !== '/terminal/radar' ? `/register?redirect=${encodeURIComponent(redirectTo)}` : '/register' return ( {/* Logo */} {/* Header */} Back to the hunt. Sign in to your account {/* Verified Message */} {verified && ( Email verified successfully! You can now sign in. )} {/* Form */} {error && ( {error} )} setEmail(e.target.value)} placeholder="Email address" required autoComplete="email" className="input-elegant text-body-sm sm:text-body" /> setPassword(e.target.value)} placeholder="Password" required minLength={8} autoComplete="current-password" className="input-elegant text-body-sm sm:text-body pr-12" /> setShowPassword(!showPassword)} className="absolute right-3 sm:right-4 top-1/2 -translate-y-1/2 text-foreground-muted hover:text-foreground transition-colors duration-200" aria-label={showPassword ? 'Hide password' : 'Show password'} > {showPassword ? ( ) : ( )} Forgot password? {loading ? ( ) : ( <> Continue > )} {/* OAuth Buttons */} {(oauthProviders.google_enabled || oauthProviders.github_enabled) && ( {/* Divider */} or continue with {oauthProviders.google_enabled && ( Continue with Google )} {oauthProviders.github_enabled && ( Continue with GitHub )} )} {/* Register Link */} Don't have an account?{' '} Create one ) } export default function LoginPage() { return ( {/* Ambient glow */} }> ) }
Sign in to your account
Email verified successfully! You can now sign in.
{error}
Don't have an account?{' '} Create one