All checks were successful
Build & Push Docker Image / build (push) Successful in 6m38s
217 lines
8.5 KiB
JavaScript
217 lines
8.5 KiB
JavaScript
import { useState } from 'react';
|
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
|
import { useAuth } from '../contexts/AuthContext';
|
|
import { useLanguage } from '../contexts/LanguageContext';
|
|
import { useBranding } from '../contexts/BrandingContext';
|
|
import { Mail, Lock, User, ArrowRight, Loader2, CheckCircle, ShieldAlert } from 'lucide-react';
|
|
import BrandLogo from '../components/BrandLogo';
|
|
import toast from 'react-hot-toast';
|
|
|
|
export default function Register() {
|
|
const [searchParams] = useSearchParams();
|
|
const inviteToken = searchParams.get('invite') || '';
|
|
const [username, setUsername] = useState('');
|
|
const [displayName, setDisplayName] = useState('');
|
|
const [email, setEmail] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [confirmPassword, setConfirmPassword] = useState('');
|
|
const [loading, setLoading] = useState(false);
|
|
const [needsVerification, setNeedsVerification] = useState(false);
|
|
const { register } = useAuth();
|
|
const { t } = useLanguage();
|
|
const { registrationMode } = useBranding();
|
|
const navigate = useNavigate();
|
|
|
|
// Invite-only mode without a token → show blocked message
|
|
const isBlocked = registrationMode === 'invite' && !inviteToken;
|
|
|
|
const handleSubmit = async (e) => {
|
|
e.preventDefault();
|
|
|
|
if (password !== confirmPassword) {
|
|
toast.error(t('auth.passwordMismatch'));
|
|
return;
|
|
}
|
|
|
|
if (password.length < 8) {
|
|
toast.error(t('auth.passwordTooShort'));
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
try {
|
|
const result = await register(username, displayName, email, password, inviteToken);
|
|
if (result?.needsVerification) {
|
|
setNeedsVerification(true);
|
|
toast.success(t('auth.verificationSent'));
|
|
} else {
|
|
toast.success(t('auth.registerSuccess'));
|
|
navigate('/dashboard');
|
|
}
|
|
} catch (err) {
|
|
toast.error(err.response?.data?.error || t('auth.registerFailed'));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center p-6 relative overflow-hidden">
|
|
{/* Animated background */}
|
|
<div className="absolute inset-0 bg-th-bg">
|
|
<div className="absolute inset-0 opacity-30">
|
|
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-th-accent rounded-full blur-[128px] animate-pulse" />
|
|
<div className="absolute bottom-1/4 right-1/4 w-80 h-80 bg-purple-500 rounded-full blur-[128px] animate-pulse" style={{ animationDelay: '2s' }} />
|
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-64 h-64 bg-pink-500 rounded-full blur-[128px] animate-pulse" style={{ animationDelay: '4s' }} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Register card */}
|
|
<div className="relative w-full max-w-md">
|
|
<div className="card p-8 backdrop-blur-xl bg-th-card/80 border border-th-border shadow-2xl rounded-2xl">
|
|
{/* Logo */}
|
|
<div className="flex justify-center mb-8">
|
|
<BrandLogo size="lg" />
|
|
</div>
|
|
|
|
{needsVerification ? (
|
|
<div className="text-center space-y-4">
|
|
<CheckCircle size={48} className="mx-auto text-green-400" />
|
|
<h2 className="text-2xl font-bold text-th-text">{t('auth.checkYourEmail')}</h2>
|
|
<p className="text-th-text-s">{t('auth.verificationSentDesc')}</p>
|
|
<p className="text-sm text-th-text-s font-medium">{email}</p>
|
|
<Link to="/login" className="btn-primary inline-flex items-center gap-2 mt-4">
|
|
{t('auth.login')}
|
|
</Link>
|
|
</div>
|
|
) : isBlocked ? (
|
|
<div className="text-center space-y-4">
|
|
<ShieldAlert size={48} className="mx-auto text-amber-400" />
|
|
<h2 className="text-2xl font-bold text-th-text">{t('auth.inviteOnly')}</h2>
|
|
<p className="text-th-text-s">{t('auth.inviteOnlyDesc')}</p>
|
|
<Link to="/login" className="btn-primary inline-flex items-center gap-2 mt-4">
|
|
{t('auth.login')}
|
|
</Link>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="mb-8">
|
|
<h2 className="text-2xl font-bold text-th-text mb-2">{t('auth.createAccount')}</h2>
|
|
<p className="text-th-text-s">
|
|
{t('auth.registerSubtitle')}
|
|
</p>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-5">
|
|
<div>
|
|
<label className="block text-sm font-medium text-th-text mb-1.5">{t('auth.username')}</label>
|
|
<div className="relative">
|
|
<User size={18} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-th-text-s" />
|
|
<input
|
|
type="text"
|
|
value={username}
|
|
onChange={e => setUsername(e.target.value)}
|
|
className="input-field pl-11"
|
|
placeholder={t('auth.usernamePlaceholder')}
|
|
required
|
|
/>
|
|
</div>
|
|
<p className="text-xs text-th-text-s mt-1">{t('auth.usernameHint')}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-th-text mb-1.5">{t('auth.displayName')}</label>
|
|
<div className="relative">
|
|
<User size={18} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-th-text-s" />
|
|
<input
|
|
type="text"
|
|
value={displayName}
|
|
onChange={e => setDisplayName(e.target.value)}
|
|
className="input-field pl-11"
|
|
placeholder={t('auth.displayNamePlaceholder')}
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-th-text mb-1.5">{t('auth.email')}</label>
|
|
<div className="relative">
|
|
<Mail size={18} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-th-text-s" />
|
|
<input
|
|
type="email"
|
|
value={email}
|
|
onChange={e => setEmail(e.target.value)}
|
|
className="input-field pl-11"
|
|
placeholder={t('auth.emailPlaceholder')}
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-th-text mb-1.5">{t('auth.password')}</label>
|
|
<div className="relative">
|
|
<Lock size={18} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-th-text-s" />
|
|
<input
|
|
type="password"
|
|
value={password}
|
|
onChange={e => setPassword(e.target.value)}
|
|
className="input-field pl-11"
|
|
placeholder={t('auth.minPassword')}
|
|
required
|
|
minLength={6}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-th-text mb-1.5">{t('auth.confirmPassword')}</label>
|
|
<div className="relative">
|
|
<Lock size={18} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-th-text-s" />
|
|
<input
|
|
type="password"
|
|
value={confirmPassword}
|
|
onChange={e => setConfirmPassword(e.target.value)}
|
|
className="input-field pl-11"
|
|
placeholder={t('auth.repeatPassword')}
|
|
required
|
|
minLength={6}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="btn-primary w-full py-3"
|
|
>
|
|
{loading ? (
|
|
<Loader2 size={18} className="animate-spin" />
|
|
) : (
|
|
<>
|
|
{t('auth.register')}
|
|
<ArrowRight size={18} />
|
|
</>
|
|
)}
|
|
</button>
|
|
</form>
|
|
|
|
<p className="mt-6 text-center text-sm text-th-text-s">
|
|
{t('auth.hasAccount')}{' '}
|
|
<Link to="/login" className="text-th-accent hover:underline font-medium">
|
|
{t('auth.signInNow')}
|
|
</Link>
|
|
</p>
|
|
|
|
<Link to="/" className="block mt-4 text-center text-sm text-th-text-s hover:text-th-text transition-colors">
|
|
{t('auth.backToHome')}
|
|
</Link>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|