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>
191 lines
6.3 KiB
TypeScript
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>
|
|
);
|
|
}
|