10 changed files with 559 additions and 199 deletions
@ -0,0 +1,185 @@
@@ -0,0 +1,185 @@
|
||||
import { useState } from 'react'; |
||||
import { useNavigate, Link } from 'react-router-dom'; |
||||
import { useAuth } from '../hooks/useAuth'; |
||||
|
||||
export function RegisterPage() { |
||||
const [username, setUsername] = useState(''); |
||||
const [password, setPassword] = useState(''); |
||||
const [confirmPassword, setConfirmPassword] = useState(''); |
||||
const [dateOfBirth, setDateOfBirth] = useState(''); |
||||
const [error, setError] = useState<string | null>(null); |
||||
const [loading, setLoading] = useState(false); |
||||
|
||||
const { login } = useAuth(); |
||||
const navigate = useNavigate(); |
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => { |
||||
e.preventDefault(); |
||||
setError(null); |
||||
|
||||
// Validation
|
||||
if (username.length < 3) { |
||||
setError('Username must be at least 3 characters long'); |
||||
return; |
||||
} |
||||
|
||||
if (password.length < 8) { |
||||
setError('Password must be at least 8 characters long'); |
||||
return; |
||||
} |
||||
|
||||
if (password !== confirmPassword) { |
||||
setError('Passwords do not match'); |
||||
return; |
||||
} |
||||
|
||||
if (!dateOfBirth) { |
||||
setError('Date of birth is required'); |
||||
return; |
||||
} |
||||
|
||||
// Validate age on frontend as well
|
||||
const birthDate = new Date(dateOfBirth); |
||||
const today = new Date(); |
||||
const age = today.getFullYear() - birthDate.getFullYear(); |
||||
const monthDiff = today.getMonth() - birthDate.getMonth(); |
||||
const dayDiff = today.getDate() - birthDate.getDate(); |
||||
|
||||
let actualAge = age; |
||||
if (monthDiff < 0 || (monthDiff === 0 && dayDiff < 0)) { |
||||
actualAge--; |
||||
} |
||||
|
||||
if (actualAge < 18) { |
||||
setError('You must be at least 18 years old to register'); |
||||
return; |
||||
} |
||||
|
||||
setLoading(true); |
||||
|
||||
try { |
||||
const { authApi } = await import('../services/apiClient'); |
||||
const response: any = await authApi.register(username, password, dateOfBirth); |
||||
|
||||
// Registration endpoint returns tokens and user data, same as login
|
||||
// Use login function to set user and token in auth context
|
||||
// This ensures consistent state management
|
||||
await login(username, password); |
||||
|
||||
// Navigate to home page
|
||||
navigate('/'); |
||||
} catch (err: any) { |
||||
setError(err.error?.message || 'Registration failed. Please try again.'); |
||||
} finally { |
||||
setLoading(false); |
||||
} |
||||
}; |
||||
|
||||
return ( |
||||
<div className="flex items-start justify-center bg-background px-4 pt-12 pb-8"> |
||||
<div className="w-full max-w-md bg-card rounded-3xl shadow-lg overflow-hidden border border-border"> |
||||
<div className="px-8 pt-8 pb-6 text-center border-b border-border"> |
||||
<h1 className="text-2xl font-bold text-foreground mb-2">Create Account</h1> |
||||
<p className="text-sm text-muted-foreground">Sign up to get started</p> |
||||
</div> |
||||
|
||||
<form onSubmit={handleSubmit} className="px-8 py-8"> |
||||
{error && ( |
||||
<div className="mb-6 p-3 bg-destructive/10 text-destructive border border-destructive/20 rounded-xl text-sm"> |
||||
{error} |
||||
</div> |
||||
)} |
||||
|
||||
<div className="mb-5"> |
||||
<label htmlFor="username" className="block mb-2 text-sm font-semibold text-foreground"> |
||||
Username |
||||
</label> |
||||
<input |
||||
id="username" |
||||
type="text" |
||||
value={username} |
||||
onChange={(e) => setUsername(e.target.value)} |
||||
disabled={loading} |
||||
required |
||||
minLength={3} |
||||
maxLength={50} |
||||
autoFocus |
||||
className="w-full px-4 py-3 border border-border rounded-xl bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent disabled:opacity-50" |
||||
/> |
||||
<p className="mt-1 text-xs text-muted-foreground">Must be at least 3 characters</p> |
||||
</div> |
||||
|
||||
<div className="mb-5"> |
||||
<label htmlFor="password" className="block mb-2 text-sm font-semibold text-foreground"> |
||||
Password |
||||
</label> |
||||
<input |
||||
id="password" |
||||
type="password" |
||||
value={password} |
||||
onChange={(e) => setPassword(e.target.value)} |
||||
disabled={loading} |
||||
required |
||||
minLength={8} |
||||
className="w-full px-4 py-3 border border-border rounded-xl bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent disabled:opacity-50" |
||||
/> |
||||
<p className="mt-1 text-xs text-muted-foreground">Must be at least 8 characters</p> |
||||
</div> |
||||
|
||||
<div className="mb-5"> |
||||
<label htmlFor="confirmPassword" className="block mb-2 text-sm font-semibold text-foreground"> |
||||
Confirm Password |
||||
</label> |
||||
<input |
||||
id="confirmPassword" |
||||
type="password" |
||||
value={confirmPassword} |
||||
onChange={(e) => setConfirmPassword(e.target.value)} |
||||
disabled={loading} |
||||
required |
||||
minLength={8} |
||||
className="w-full px-4 py-3 border border-border rounded-xl bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent disabled:opacity-50" |
||||
/> |
||||
</div> |
||||
|
||||
<div className="mb-6"> |
||||
<label htmlFor="dateOfBirth" className="block mb-2 text-sm font-semibold text-foreground"> |
||||
Date of Birth |
||||
</label> |
||||
<input |
||||
id="dateOfBirth" |
||||
type="date" |
||||
value={dateOfBirth} |
||||
onChange={(e) => setDateOfBirth(e.target.value)} |
||||
disabled={loading} |
||||
required |
||||
max={new Date(new Date().setFullYear(new Date().getFullYear() - 18)).toISOString().split('T')[0]} |
||||
className="w-full px-4 py-3 border border-border rounded-xl bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent disabled:opacity-50" |
||||
/> |
||||
<p className="mt-1 text-xs text-muted-foreground">You must be at least 18 years old to register</p> |
||||
</div> |
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full px-4 py-3 bg-primary text-primary-foreground rounded-xl font-semibold text-sm hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed shadow-md mb-4" |
||||
> |
||||
{loading ? 'Creating account...' : 'Create Account'} |
||||
</button> |
||||
|
||||
<div className="text-center"> |
||||
<p className="text-sm text-muted-foreground"> |
||||
Have an account?{' '} |
||||
<Link
|
||||
to="/login"
|
||||
className="text-primary hover:underline font-semibold" |
||||
> |
||||
Sign in here |
||||
</Link> |
||||
</p> |
||||
</div> |
||||
</form> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
Loading…
Reference in new issue