Files
resolutionflow/frontend/src/pages/LoginPage.tsx
Michael Chihlas cd10ecd42c Complete Phase 2: Frontend implementation with React + TypeScript
Frontend Features:
- React 18 + Vite + TypeScript + Tailwind CSS + Zustand
- JWT authentication with automatic token refresh
- Tree library with search and category filtering
- Full tree navigation (decision/action/solution nodes)
- Session management with notes and completion
- Session history with export (Markdown/Text/HTML)
- ErrorBoundary for graceful error handling

Backend Fixes:
- CORS: Added port 5174 to allowed origins
- Sessions: Fixed JSONB datetime serialization (mode='json')

Documentation:
- Updated PROGRESS.md with Phase 2 completion
- Updated 03-DEVELOPMENT-ROADMAP.md with checked items
- Added PHASE-2.5-PERSONAL-BRANCHING.md spec

Seed Data:
- Added backend/scripts/seed_data.py with Password Reset tree

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 22:42:22 -05:00

119 lines
4.1 KiB
TypeScript

import { useState } from 'react'
import { Link, useNavigate, useLocation } from 'react-router-dom'
import { useAuthStore } from '@/store/authStore'
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 || '/trees'
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 })
navigate(from, { replace: true })
} catch {
// Error is set in the store
}
}
return (
<div className="flex min-h-screen items-center justify-center bg-background px-4">
<div className="w-full max-w-md space-y-8">
<div className="text-center">
<h1 className="text-3xl font-bold tracking-tight text-foreground">Apoklisis</h1>
<p className="mt-2 text-muted-foreground">Sign in to your account</p>
</div>
<form onSubmit={handleSubmit} className="mt-8 space-y-6">
<div className="space-y-4 rounded-lg border border-border bg-card p-6 shadow-sm">
{(error || localError) && (
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
{localError || error}
</div>
)}
<div>
<label htmlFor="email" className="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(
'mt-1 block w-full rounded-md border border-input bg-background px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
)}
placeholder="you@example.com"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-foreground">
Password
</label>
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
className={cn(
'mt-1 block w-full rounded-md border border-input bg-background px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
)}
placeholder="••••••••••"
/>
</div>
<button
type="submit"
disabled={isLoading}
className={cn(
'w-full rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
'hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2',
'disabled:cursor-not-allowed disabled:opacity-50'
)}
>
{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-primary hover:text-primary/90">
Register
</Link>
</p>
</form>
</div>
</div>
)
}
export default LoginPage