121 lines
3.8 KiB
TypeScript
121 lines
3.8 KiB
TypeScript
|
|
import { useState } from 'react'
|
||
|
|
import { useNavigate } from 'react-router-dom'
|
||
|
|
import { motion } from 'framer-motion'
|
||
|
|
import { Eye, EyeOff, Mail, Lock } from 'lucide-react'
|
||
|
|
import { Input } from '@/components/ui/Input'
|
||
|
|
import { Button } from '@/components/ui/Button'
|
||
|
|
import { useAuthStore } from '@/stores/authStore'
|
||
|
|
import { authApi } from '@/lib/api'
|
||
|
|
|
||
|
|
export function LoginPage() {
|
||
|
|
const navigate = useNavigate()
|
||
|
|
const { login } = useAuthStore()
|
||
|
|
const [email, setEmail] = useState('')
|
||
|
|
const [password, setPassword] = useState('')
|
||
|
|
const [showPassword, setShowPassword] = useState(false)
|
||
|
|
const [error, setError] = useState('')
|
||
|
|
const [loading, setLoading] = useState(false)
|
||
|
|
|
||
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
||
|
|
e.preventDefault()
|
||
|
|
setError('')
|
||
|
|
setLoading(true)
|
||
|
|
|
||
|
|
try {
|
||
|
|
const res = await authApi.login(email, password)
|
||
|
|
if (!res.ok) {
|
||
|
|
setError('Invalid credentials')
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// Save token FIRST so authApi.me() can use it
|
||
|
|
localStorage.setItem('amos_token', res.token)
|
||
|
|
|
||
|
|
// Fetch user info
|
||
|
|
const me = await authApi.me()
|
||
|
|
|
||
|
|
login(res.token, {
|
||
|
|
id: me.user.sub,
|
||
|
|
email: me.user.email,
|
||
|
|
name: 'Erik Svensson',
|
||
|
|
role: me.user.roles?.[0] || 'user',
|
||
|
|
})
|
||
|
|
navigate('/')
|
||
|
|
} catch (err) {
|
||
|
|
setError(err instanceof Error ? err.message : 'Login failed')
|
||
|
|
} finally {
|
||
|
|
setLoading(false)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="min-h-screen bg-bg flex items-center justify-center p-4">
|
||
|
|
<motion.div
|
||
|
|
initial={{ opacity: 0, y: 16 }}
|
||
|
|
animate={{ opacity: 1, y: 0 }}
|
||
|
|
transition={{ duration: 0.3, ease: 'easeOut' }}
|
||
|
|
className="w-full max-w-[400px]"
|
||
|
|
>
|
||
|
|
{/* Logo */}
|
||
|
|
<div className="flex items-center justify-center mb-10">
|
||
|
|
<div className="w-12 h-12 rounded-xl bg-primary flex items-center justify-center">
|
||
|
|
<svg width="24" height="24" viewBox="0 0 32 32" fill="none">
|
||
|
|
<path d="M10 22L16 10L22 22H10Z" stroke="white" strokeWidth="2.5" strokeLinejoin="round" />
|
||
|
|
</svg>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="text-center mb-8">
|
||
|
|
<h1 className="text-2xl font-semibold text-text-primary">Welcome back</h1>
|
||
|
|
<p className="text-sm text-text-secondary mt-1.5">Sign in to your AMOS account</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||
|
|
<Input
|
||
|
|
label="Email"
|
||
|
|
type="email"
|
||
|
|
placeholder="you@company.com"
|
||
|
|
value={email}
|
||
|
|
onChange={(e) => setEmail(e.target.value)}
|
||
|
|
icon={<Mail size={16} />}
|
||
|
|
required
|
||
|
|
/>
|
||
|
|
|
||
|
|
<div className="relative">
|
||
|
|
<Input
|
||
|
|
label="Password"
|
||
|
|
type={showPassword ? 'text' : 'password'}
|
||
|
|
placeholder="Enter your password"
|
||
|
|
value={password}
|
||
|
|
onChange={(e) => setPassword(e.target.value)}
|
||
|
|
icon={<Lock size={16} />}
|
||
|
|
required
|
||
|
|
/>
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={() => setShowPassword(!showPassword)}
|
||
|
|
className="absolute right-3.5 top-[38px] text-text-secondary hover:text-text-primary transition-colors"
|
||
|
|
>
|
||
|
|
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{error && (
|
||
|
|
<motion.p
|
||
|
|
initial={{ opacity: 0, y: -4 }}
|
||
|
|
animate={{ opacity: 1, y: 0 }}
|
||
|
|
className="text-sm text-danger"
|
||
|
|
>
|
||
|
|
{error}
|
||
|
|
</motion.p>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<Button type="submit" size="lg" loading={loading} className="w-full">
|
||
|
|
Sign in
|
||
|
|
</Button>
|
||
|
|
</form>
|
||
|
|
</motion.div>
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|