103 lines
3.7 KiB
React
103 lines
3.7 KiB
React
import { useState } from 'react'
|
|
import { useNavigate } from 'react-router-dom'
|
|
import { useAuthStore } from '../hooks/useAuth'
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import api from '../utils/api'
|
|
|
|
export default function LoginPage() {
|
|
const [username, setUsername] = useState('')
|
|
const [password, setPassword] = useState('')
|
|
const [error, setError] = useState('')
|
|
const { login, isLoading } = useAuthStore()
|
|
const navigate = useNavigate()
|
|
|
|
const { data: pocketidData } = useQuery({
|
|
queryKey: ['pocketid-available'],
|
|
queryFn: () => api.get('/auth/pocketid/available').then(r => r.data),
|
|
})
|
|
|
|
const handleSubmit = async (e) => {
|
|
e.preventDefault()
|
|
setError('')
|
|
try {
|
|
await login(username, password)
|
|
navigate('/')
|
|
} catch (err) {
|
|
setError(err.response?.data?.detail || 'Login failed')
|
|
}
|
|
}
|
|
|
|
const handlePocketID = async () => {
|
|
const { data } = await api.get('/auth/pocketid/login-url')
|
|
window.location.href = data.url
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-950 flex items-center justify-center px-4">
|
|
<div className="w-full max-w-sm">
|
|
<div className="text-center mb-8">
|
|
<h1 className="text-3xl font-bold text-white">
|
|
<span className="text-blue-400">Mile</span>Vault
|
|
</h1>
|
|
<p className="text-gray-500 mt-2 text-sm">Your personal fitness dashboard</p>
|
|
</div>
|
|
|
|
<div className="bg-gray-900 rounded-2xl p-6 border border-gray-800">
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div>
|
|
<label className="block text-xs text-gray-400 mb-1">Username</label>
|
|
<input
|
|
type="text"
|
|
value={username}
|
|
onChange={e => setUsername(e.target.value)}
|
|
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2.5 text-sm text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
autoComplete="username"
|
|
required
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs text-gray-400 mb-1">Password</label>
|
|
<input
|
|
type="password"
|
|
value={password}
|
|
onChange={e => setPassword(e.target.value)}
|
|
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2.5 text-sm text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
autoComplete="current-password"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
{error && (
|
|
<p className="text-red-400 text-xs">{error}</p>
|
|
)}
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={isLoading}
|
|
className="w-full bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white font-medium py-2.5 rounded-lg text-sm transition-colors"
|
|
>
|
|
{isLoading ? 'Signing in…' : 'Sign in'}
|
|
</button>
|
|
</form>
|
|
|
|
{pocketidData?.available && (
|
|
<>
|
|
<div className="flex items-center gap-3 my-4">
|
|
<div className="flex-1 h-px bg-gray-800" />
|
|
<span className="text-xs text-gray-600">or</span>
|
|
<div className="flex-1 h-px bg-gray-800" />
|
|
</div>
|
|
<button
|
|
onClick={handlePocketID}
|
|
className="w-full bg-gray-800 hover:bg-gray-700 text-gray-300 font-medium py-2.5 rounded-lg text-sm transition-colors flex items-center justify-center gap-2"
|
|
>
|
|
🔑 Sign in with passkey
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|