Files
resolutionflow/frontend/src/pages/LoginPage.tsx
2026-03-16 02:29:22 -04:00

176 lines
6.2 KiB
TypeScript

import { useState } from 'react'
import { Link, useNavigate, useLocation } from 'react-router-dom'
import { useAuthStore } from '@/store/authStore'
import { BrandLogo } from '@/components/common/BrandLogo'
import { PasswordInput } from '@/components/common/PasswordInput'
import { PageMeta } from '@/components/common/PageMeta'
import { cn } from '@/lib/utils'
export function LoginPage() {
const navigate = useNavigate()
const location = useLocation()
const { login, isLoading, error, clearError } = useAuthStore()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [localError, setLocalError] = useState('')
const from = (location.state as { from?: { pathname: string } })?.from?.pathname || '/'
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLocalError('')
clearError()
if (!email || !password) {
setLocalError('Please enter both email and password')
return
}
try {
await login({ email, password })
const user = useAuthStore.getState().user
if (user?.must_change_password) {
navigate('/change-password', { replace: true })
} else {
navigate(from, { replace: true })
}
} catch {
// Error is set in the store
}
}
return (
<>
<PageMeta title="Sign In" description="Sign in to your ResolutionFlow account" />
<div className="flex min-h-screen items-center justify-center bg-background px-4">
{/* Atmosphere orbs */}
<div
className="pointer-events-none fixed z-0"
style={{
top: '-120px',
right: '-80px',
width: '600px',
height: '600px',
borderRadius: '50%',
background: 'radial-gradient(circle, rgba(6, 182, 212, 0.15) 0%, rgba(6, 182, 212, 0.04) 40%, transparent 70%)',
filter: 'blur(60px)',
}}
/>
<div
className="pointer-events-none fixed z-0"
style={{
bottom: '-100px',
left: '-60px',
width: '500px',
height: '500px',
borderRadius: '50%',
background: 'radial-gradient(circle, rgba(139, 92, 246, 0.08) 0%, rgba(139, 92, 246, 0.02) 40%, transparent 70%)',
filter: 'blur(60px)',
}}
/>
<div className="relative z-10 w-full max-w-md space-y-8">
<div className="text-center">
<div className="mb-4 flex justify-center sm:mb-6">
<BrandLogo size="lg" />
</div>
<h1 className="text-3xl font-bold font-heading text-foreground tracking-tight">
<span>Resolution</span><span className="text-gradient-brand">Flow</span>
</h1>
<p className="mt-2 text-base font-medium text-muted-foreground sm:mt-3 sm:text-lg">
Decision Tree Platform
</p>
<p className="mt-1 text-sm text-muted-foreground/70 sm:mt-2">
Sign in to your account
</p>
</div>
<form onSubmit={handleSubmit} className="mt-8 space-y-6" data-testid="login-form">
<div className="glass-card-static p-6 space-y-4">
{(error || localError) && (
<div className="rounded-[10px] border border-rose-500/20 bg-rose-500/10 p-3 text-sm text-rose-400">
{localError || error}
</div>
)}
<div>
<label htmlFor="email" className="mb-1.5 block text-sm font-medium text-foreground">
Email address
</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className={cn(
'block w-full rounded-[10px] border border-border bg-card px-3 py-2.5',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary/30 focus:outline-hidden focus:ring-1 focus:ring-primary/20',
'transition-colors'
)}
placeholder="you@example.com"
/>
</div>
<div>
<label htmlFor="password" className="mb-1.5 block text-sm font-medium text-foreground">
Password
</label>
<PasswordInput
id="password"
name="password"
autoComplete="current-password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
className={cn(
'block w-full rounded-[10px] border border-border bg-card px-3 py-2.5',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary/30 focus:outline-hidden focus:ring-1 focus:ring-primary/20',
'transition-colors'
)}
placeholder="••••••••••"
/>
</div>
<div className="text-right">
<Link to="/forgot-password" className="text-xs text-muted-foreground hover:text-foreground transition-colors">
Forgot password?
</Link>
</div>
<button
type="submit"
disabled={isLoading}
data-testid="login-submit"
className={cn(
'w-full rounded-[10px] px-4 py-2.5 text-sm font-semibold',
'bg-gradient-brand text-brand-dark shadow-lg shadow-primary/20 hover:opacity-90 active:scale-[0.97]',
'focus:outline-hidden focus:ring-2 focus:ring-primary/30 focus:ring-offset-2 focus:ring-offset-background',
'disabled:cursor-not-allowed disabled:opacity-50',
'transition-all'
)}
>
{isLoading ? 'Signing in...' : 'Sign in'}
</button>
</div>
<p className="text-center text-sm text-muted-foreground">
Don't have an account?{' '}
<Link to="/register" className="font-medium text-foreground hover:underline">
Register
</Link>
</p>
</form>
</div>
</div>
</>
)
}
export default LoginPage