Files
boc/web-v2/src/pages/LoginPage.tsx
T

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>
)
}