This commit is contained in:
2025-12-31 19:55:43 +01:00
parent 8260bf7baf
commit 167e82a52b
66 changed files with 5124 additions and 228 deletions

View File

@@ -32,7 +32,8 @@ import Heading from '@/components/ui/Heading';
import { useAuth } from '@/lib/auth/AuthContext';
interface FormErrors {
displayName?: string;
firstName?: string;
lastName?: string;
email?: string;
password?: string;
confirmPassword?: string;
@@ -101,7 +102,8 @@ export default function SignupPage() {
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [errors, setErrors] = useState<FormErrors>({});
const [formData, setFormData] = useState({
displayName: '',
firstName: '',
lastName: '',
email: '',
password: '',
confirmPassword: '',
@@ -138,10 +140,32 @@ export default function SignupPage() {
const validateForm = (): boolean => {
const newErrors: FormErrors = {};
if (!formData.displayName.trim()) {
newErrors.displayName = 'Display name is required';
} else if (formData.displayName.trim().length < 3) {
newErrors.displayName = 'Display name must be at least 3 characters';
// First name validation
const firstName = formData.firstName.trim();
if (!firstName) {
newErrors.firstName = 'First name is required';
} else if (firstName.length < 2) {
newErrors.firstName = 'First name must be at least 2 characters';
} else if (firstName.length > 25) {
newErrors.firstName = 'First name must be no more than 25 characters';
} else if (!/^[A-Za-z\-']+$/.test(firstName)) {
newErrors.firstName = 'First name can only contain letters, hyphens, and apostrophes';
} else if (/^(user|test|demo|guest|player)/i.test(firstName)) {
newErrors.firstName = 'Please use your real first name, not a nickname';
}
// Last name validation
const lastName = formData.lastName.trim();
if (!lastName) {
newErrors.lastName = 'Last name is required';
} else if (lastName.length < 2) {
newErrors.lastName = 'Last name must be at least 2 characters';
} else if (lastName.length > 25) {
newErrors.lastName = 'Last name must be no more than 25 characters';
} else if (!/^[A-Za-z\-']+$/.test(lastName)) {
newErrors.lastName = 'Last name can only contain letters, hyphens, and apostrophes';
} else if (/^(user|test|demo|guest|player)/i.test(lastName)) {
newErrors.lastName = 'Please use your real last name, not a nickname';
}
if (!formData.email.trim()) {
@@ -150,10 +174,13 @@ export default function SignupPage() {
newErrors.email = 'Invalid email format';
}
// Password strength validation
if (!formData.password) {
newErrors.password = 'Password is required';
} else if (formData.password.length < 8) {
newErrors.password = 'Password must be at least 8 characters';
} else if (!/[a-z]/.test(formData.password) || !/[A-Z]/.test(formData.password) || !/\d/.test(formData.password)) {
newErrors.password = 'Password must contain uppercase, lowercase, and number';
}
if (!formData.confirmPassword) {
@@ -176,21 +203,18 @@ export default function SignupPage() {
setErrors({});
try {
const response = await fetch('/api/auth/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: formData.email,
password: formData.password,
displayName: formData.displayName,
}),
const { ServiceFactory } = await import('@/lib/services/ServiceFactory');
const serviceFactory = new ServiceFactory(process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001');
const authService = serviceFactory.createAuthService();
// Combine first and last name into display name
const displayName = `${formData.firstName} ${formData.lastName}`.trim();
await authService.signup({
email: formData.email,
password: formData.password,
displayName,
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Signup failed');
}
// Refresh session in context so header updates immediately
await refreshSession();
@@ -206,8 +230,12 @@ export default function SignupPage() {
const handleDemoLogin = async () => {
setLoading(true);
try {
// Demo: Set cookie to indicate driver mode (works without OAuth)
document.cookie = 'gridpilot_demo_mode=driver; path=/; max-age=86400';
const { ServiceFactory } = await import('@/lib/services/ServiceFactory');
const serviceFactory = new ServiceFactory(process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001');
const authService = serviceFactory.createAuthService();
await authService.demoLogin({ role: 'driver' });
await new Promise(resolve => setTimeout(resolve, 500));
router.push(returnTo === '/onboarding' ? '/dashboard' : returnTo);
} catch {
@@ -337,27 +365,57 @@ export default function SignupPage() {
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-bl from-primary-blue/10 to-transparent rounded-bl-full" />
<form onSubmit={handleSubmit} className="relative space-y-4">
{/* Display Name */}
{/* First Name */}
<div>
<label htmlFor="displayName" className="block text-sm font-medium text-gray-300 mb-2">
Display Name
<label htmlFor="firstName" className="block text-sm font-medium text-gray-300 mb-2">
First Name
</label>
<div className="relative">
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
<Input
id="displayName"
id="firstName"
type="text"
value={formData.displayName}
onChange={(e: ChangeEvent<HTMLInputElement>) => setFormData({ ...formData, displayName: e.target.value })}
error={!!errors.displayName}
errorMessage={errors.displayName}
placeholder="SpeedyRacer42"
value={formData.firstName}
onChange={(e: ChangeEvent<HTMLInputElement>) => setFormData({ ...formData, firstName: e.target.value })}
error={!!errors.firstName}
errorMessage={errors.firstName}
placeholder="John"
disabled={loading}
className="pl-10"
autoComplete="username"
autoComplete="given-name"
/>
</div>
<p className="mt-1 text-xs text-gray-500">This is how other drivers will see you</p>
</div>
{/* Last Name */}
<div>
<label htmlFor="lastName" className="block text-sm font-medium text-gray-300 mb-2">
Last Name
</label>
<div className="relative">
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
<Input
id="lastName"
type="text"
value={formData.lastName}
onChange={(e: ChangeEvent<HTMLInputElement>) => setFormData({ ...formData, lastName: e.target.value })}
error={!!errors.lastName}
errorMessage={errors.lastName}
placeholder="Smith"
disabled={loading}
className="pl-10"
autoComplete="family-name"
/>
</div>
<p className="mt-1 text-xs text-gray-500">Your name will be used as-is and cannot be changed later</p>
</div>
{/* Name Immutability Warning */}
<div className="flex items-start gap-3 p-3 rounded-lg bg-warning-amber/10 border border-warning-amber/30">
<AlertCircle className="w-5 h-5 text-warning-amber flex-shrink-0 mt-0.5" />
<div className="text-sm text-warning-amber">
<strong>Important:</strong> Your name cannot be changed after signup. Please ensure it's correct.
</div>
</div>
{/* Email */}
@@ -529,7 +587,7 @@ export default function SignupPage() {
</div>
</div>
{/* iRacing Signup */}
{/* Demo Login */}
<motion.button
type="button"
onClick={handleDemoLogin}
@@ -539,7 +597,7 @@ export default function SignupPage() {
className="w-full flex items-center justify-center gap-3 px-4 py-3 rounded-lg bg-gradient-to-r from-deep-graphite to-iron-gray border border-charcoal-outline text-gray-300 hover:border-primary-blue/30 transition-all disabled:opacity-50 group"
>
<Gamepad2 className="w-5 h-5 text-primary-blue" />
<span>Demo Login (iRacing)</span>
<span>Demo Login</span>
<ChevronRight className="w-4 h-4 text-gray-500 group-hover:translate-x-0.5 transition-transform" />
</motion.button>