176 lines
6.2 KiB
TypeScript
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
|