Restructure into full project layout
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { useAuthStore } from '@/store/authStore'
|
||||
import { Button, Input, ErrorMessage } from '@/components/ui'
|
||||
import { AxiosError } from 'axios'
|
||||
|
||||
// ─── Shared card wrapper ──────────────────────────────────────────────────────
|
||||
|
||||
function AuthCard({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="min-h-screen bg-bg-base flex items-center justify-center p-4">
|
||||
{/* Subtle background texture */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-5"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'radial-gradient(circle at 25% 25%, #7C3AED 0%, transparent 50%), radial-gradient(circle at 75% 75%, #4C1D95 0%, transparent 50%)',
|
||||
}}
|
||||
/>
|
||||
<div className="relative w-full max-w-md">
|
||||
{/* Logo */}
|
||||
<div className="flex flex-col items-center mb-8 gap-2">
|
||||
<svg viewBox="0 0 60 60" className="w-14 h-14" fill="none">
|
||||
<polygon
|
||||
points="30,3 57,21 46.5,51 13.5,51 3,21"
|
||||
stroke="#7C3AED"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
/>
|
||||
<polygon
|
||||
points="30,12 48,24 41,44 19,44 12,24"
|
||||
stroke="#9F67FF"
|
||||
strokeWidth="1"
|
||||
fill="rgba(124,58,237,0.1)"
|
||||
/>
|
||||
<circle cx="30" cy="28" r="5" fill="#7C3AED" opacity="0.8" />
|
||||
</svg>
|
||||
<h1 className="font-display text-2xl text-text-primary tracking-wide">Commander Forge</h1>
|
||||
<p className="text-text-muted text-sm">AI-powered deck building for Commander</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-bg-surface border border-bg-border rounded-xl p-8 shadow-card">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function getErrorMessage(err: unknown): string {
|
||||
if (err instanceof AxiosError) {
|
||||
const detail = err.response?.data?.detail
|
||||
if (typeof detail === 'string') return detail
|
||||
if (Array.isArray(detail)) return detail.map((d) => d.msg).join('. ')
|
||||
}
|
||||
return 'Something went wrong. Please try again.'
|
||||
}
|
||||
|
||||
// ─── Login ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export function LoginPage() {
|
||||
const navigate = useNavigate()
|
||||
const { login, loading } = useAuthStore()
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
try {
|
||||
await login(email, password)
|
||||
navigate('/decks')
|
||||
} catch (err) {
|
||||
setError(getErrorMessage(err))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthCard>
|
||||
<h2 className="font-display text-lg text-text-primary mb-6">Sign in</h2>
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
{error && <ErrorMessage message={error} />}
|
||||
<Input
|
||||
label="Email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="you@example.com"
|
||||
autoComplete="email"
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
label="Password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
/>
|
||||
<Button type="submit" loading={loading} className="w-full mt-2" size="lg">
|
||||
Sign in
|
||||
</Button>
|
||||
</form>
|
||||
<p className="text-center text-sm text-text-muted mt-6">
|
||||
No account?{' '}
|
||||
<Link to="/register" className="text-accent-violet-light hover:underline">
|
||||
Request access
|
||||
</Link>
|
||||
</p>
|
||||
</AuthCard>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Register ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export function RegisterPage() {
|
||||
const navigate = useNavigate()
|
||||
const { register, loading } = useAuthStore()
|
||||
const [email, setEmail] = useState('')
|
||||
const [displayName, setDisplayName] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [confirm, setConfirm] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
if (password !== confirm) {
|
||||
setError('Passwords do not match.')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await register(email, password, displayName || undefined)
|
||||
navigate('/pending')
|
||||
} catch (err) {
|
||||
setError(getErrorMessage(err))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthCard>
|
||||
<h2 className="font-display text-lg text-text-primary mb-1">Request access</h2>
|
||||
<p className="text-text-muted text-sm mb-6">
|
||||
Accounts require admin approval before you can start building.
|
||||
</p>
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
{error && <ErrorMessage message={error} />}
|
||||
<Input
|
||||
label="Email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="you@example.com"
|
||||
autoComplete="email"
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
label="Display name (optional)"
|
||||
type="text"
|
||||
value={displayName}
|
||||
onChange={(e) => setDisplayName(e.target.value)}
|
||||
placeholder="Your name"
|
||||
/>
|
||||
<Input
|
||||
label="Password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="At least 8 characters"
|
||||
autoComplete="new-password"
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
label="Confirm password"
|
||||
type="password"
|
||||
value={confirm}
|
||||
onChange={(e) => setConfirm(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
autoComplete="new-password"
|
||||
required
|
||||
/>
|
||||
<Button type="submit" loading={loading} className="w-full mt-2" size="lg">
|
||||
Request access
|
||||
</Button>
|
||||
</form>
|
||||
<p className="text-center text-sm text-text-muted mt-6">
|
||||
Already have an account?{' '}
|
||||
<Link to="/login" className="text-accent-violet-light hover:underline">
|
||||
Sign in
|
||||
</Link>
|
||||
</p>
|
||||
</AuthCard>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Pending ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export function PendingPage() {
|
||||
const { logout } = useAuthStore()
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<AuthCard>
|
||||
<div className="text-center flex flex-col items-center gap-4">
|
||||
<div className="w-16 h-16 rounded-full bg-accent-violet/20 border border-accent-violet/40 flex items-center justify-center">
|
||||
<svg className="w-8 h-8 text-accent-violet-light" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
|
||||
d="M12 6v6l4 2M12 2a10 10 0 100 20 10 10 0 000-20z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="font-display text-lg text-text-primary mb-2">Access pending</h2>
|
||||
<p className="text-text-secondary text-sm leading-relaxed">
|
||||
Your account is awaiting admin approval. You'll be able to sign in once approved.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => { logout(); navigate('/login') }}
|
||||
className="text-sm text-text-muted hover:text-text-secondary transition-colors"
|
||||
>
|
||||
Back to sign in
|
||||
</button>
|
||||
</div>
|
||||
</AuthCard>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user