Files
backerup-website/frontend/app/login/LoginForm.tsx
T

142 lines
4.6 KiB
TypeScript

'use client'
import { useState, useRef } from 'react'
import { useRouter } from 'next/navigation'
interface Props {
from: string
}
export default function LoginForm({ from }: Props) {
const router = useRouter()
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const usernameRef = useRef<HTMLInputElement>(null)
const passwordRef = useRef<HTMLInputElement>(null)
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
setError('')
setLoading(true)
const username = usernameRef.current?.value.trim() ?? ''
const password = passwordRef.current?.value ?? ''
try {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
})
const data = (await res.json()) as { error?: string }
if (!res.ok) {
setError(data.error ?? 'Login failed')
setLoading(false)
return
}
// Navigate to intended destination — fresh page load picks up the session cookie
router.push(from || '/browse')
} catch {
setError('Network error — please try again')
setLoading(false)
}
}
return (
<form onSubmit={handleSubmit} className="space-y-5" noValidate>
{error && (
<div
className="flex items-center gap-2.5 px-4 py-3 rounded-xl text-sm border"
style={{
background: 'rgba(239,68,68,0.08)',
borderColor: 'rgba(239,68,68,0.3)',
color: '#fca5a5',
}}
role="alert"
>
<svg viewBox="0 0 20 20" fill="currentColor" className="w-4 h-4 shrink-0" aria-hidden="true">
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z"
clipRule="evenodd"
/>
</svg>
{error}
</div>
)}
<div className="space-y-1.5">
<label htmlFor="username" className="block text-sm font-medium text-slate-300">
Username
</label>
<input
id="username"
ref={usernameRef}
type="text"
autoComplete="username"
required
autoFocus
className="w-full rounded-xl px-4 py-3 text-sm outline-none transition-all"
style={{
background: 'rgba(255,255,255,0.04)',
border: '1px solid rgba(255,255,255,0.1)',
color: '#e2e8f0',
}}
onFocus={(e) =>
(e.currentTarget.style.borderColor = 'rgba(34,211,238,0.5)')
}
onBlur={(e) =>
(e.currentTarget.style.borderColor = 'rgba(255,255,255,0.1)')
}
placeholder="Enter your username"
/>
</div>
<div className="space-y-1.5">
<label htmlFor="password" className="block text-sm font-medium text-slate-300">
Password
</label>
<input
id="password"
ref={passwordRef}
type="password"
autoComplete="current-password"
required
className="w-full rounded-xl px-4 py-3 text-sm outline-none transition-all"
style={{
background: 'rgba(255,255,255,0.04)',
border: '1px solid rgba(255,255,255,0.1)',
color: '#e2e8f0',
}}
onFocus={(e) =>
(e.currentTarget.style.borderColor = 'rgba(34,211,238,0.5)')
}
onBlur={(e) =>
(e.currentTarget.style.borderColor = 'rgba(255,255,255,0.1)')
}
placeholder="Enter your password"
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full py-3 rounded-xl font-semibold text-sm text-slate-900 transition-all hover:brightness-110 active:scale-[0.98] disabled:opacity-60 disabled:cursor-not-allowed"
style={{ background: 'linear-gradient(135deg, #22d3ee, #06b6d4)' }}
>
{loading ? (
<span className="flex items-center justify-center gap-2">
<svg className="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24" aria-hidden="true">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
Signing in
</span>
) : (
'Sign in'
)}
</button>
</form>
)
}