forge/frontend/app/signup/page.tsx
DJP 7a804e896d Initial commit - FORGE AI unified platform
Features:
- Image generation (OpenAI, Gemini, Leonardo, Bria, Stability, Flux)
- Nano Banana iterative editing
- Video generation and upscaling
- Audio TTS, STT, sound effects (ElevenLabs)
- Text prompt studio and alt text
- User authentication with JWT/cookies
- Admin panel with voice management
- Job queue with Celery
- PostgreSQL + Redis backend
- Next.js 15 + FastAPI architecture

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
2025-12-09 20:39:00 -05:00

191 lines
6.3 KiB
TypeScript

'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { toast } from 'react-hot-toast';
import { UserPlus, Eye, EyeOff, Loader2, Check, X } from 'lucide-react';
import { authApi } from '@/lib/api';
import { useStore } from '@/lib/store';
export default function SignUpPage() {
const router = useRouter();
const { setUser, setToken } = useStore();
const [email, setEmail] = useState('');
const [displayName, setDisplayName] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [loading, setLoading] = useState(false);
const passwordRequirements = [
{ label: 'At least 8 characters', met: password.length >= 8 },
{ label: 'Contains a number', met: /\d/.test(password) },
{ label: 'Contains a letter', met: /[a-zA-Z]/.test(password) },
{ label: 'Passwords match', met: password === confirmPassword && password.length > 0 },
];
const isValid =
email.trim() &&
displayName.trim() &&
password.length >= 8 &&
password === confirmPassword;
const handleSignUp = async (e: React.FormEvent) => {
e.preventDefault();
if (!isValid) {
toast.error('Please fill in all fields correctly');
return;
}
setLoading(true);
try {
const response = await authApi.signup({
email,
password,
display_name: displayName,
});
const { user } = response.data;
// Store user data and set token marker (actual auth via cookie)
setUser({
id: user.id,
email: user.email,
name: user.display_name || user.email,
role: user.role,
avatar_url: user.avatar_url,
});
setToken('cookie-auth'); // Marker to indicate authenticated
toast.success('Account created successfully!');
router.push('/');
} catch (err: any) {
toast.error(err.response?.data?.detail || 'Failed to create account');
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-forge-gray flex items-center justify-center p-4">
<div className="w-full max-w-md">
{/* Logo */}
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-16 h-16 bg-forge-yellow rounded-xl mb-4">
<span className="text-2xl font-bold text-black">F</span>
</div>
<h1 className="text-2xl font-bold text-white">Create Account</h1>
<p className="text-gray-500 mt-2">Join FORGE AI and start creating</p>
</div>
{/* Sign Up Form */}
<form onSubmit={handleSignUp} className="bg-forge-dark rounded-xl border border-gray-800 p-8 space-y-6">
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Full Name
</label>
<input
type="text"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
placeholder="Your name"
className="input-field"
autoFocus
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Email Address
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
className="input-field"
autoComplete="email"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Password
</label>
<div className="relative">
<input
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Create a password"
className="input-field pr-10"
autoComplete="new-password"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-300"
>
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Confirm Password
</label>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="Confirm your password"
className="input-field"
autoComplete="new-password"
/>
</div>
{/* Password Requirements */}
{password.length > 0 && (
<div className="space-y-2">
{passwordRequirements.map((req) => (
<div key={req.label} className="flex items-center gap-2 text-sm">
{req.met ? (
<Check className="w-4 h-4 text-green-500" />
) : (
<X className="w-4 h-4 text-gray-500" />
)}
<span className={req.met ? 'text-green-500' : 'text-gray-500'}>
{req.label}
</span>
</div>
))}
</div>
)}
<button
type="submit"
disabled={loading || !isValid}
className="btn-primary w-full flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? (
<Loader2 className="w-5 h-5 animate-spin" />
) : (
<UserPlus className="w-5 h-5" />
)}
{loading ? 'Creating account...' : 'Create Account'}
</button>
<div className="text-center text-sm text-gray-500">
Already have an account?{' '}
<Link href="/login" className="text-forge-yellow hover:text-yellow-400">
Sign in
</Link>
</div>
</form>
</div>
</div>
);
}