Update registration Step

This commit is contained in:
Koncept Kit
2025-12-06 13:47:40 +07:00
parent 7bdb910a67
commit 7b8ee6442a
6 changed files with 940 additions and 302 deletions

View File

@@ -2,45 +2,61 @@ import React, { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import { Button } from '../components/ui/button';
import { Input } from '../components/ui/input';
import { Label } from '../components/ui/label';
import { Card } from '../components/ui/card';
import { Checkbox } from '../components/ui/checkbox';
import { toast } from 'sonner';
import Navbar from '../components/Navbar';
import { ArrowRight, ArrowLeft } from 'lucide-react';
import RegistrationStepIndicator from '../components/registration/RegistrationStepIndicator';
import RegistrationStep1 from '../components/registration/RegistrationStep1';
import RegistrationStep2 from '../components/registration/RegistrationStep2';
import RegistrationStep3 from '../components/registration/RegistrationStep3';
import RegistrationStep4 from '../components/registration/RegistrationStep4';
const Register = () => {
const navigate = useNavigate();
const { register } = useAuth();
const [loading, setLoading] = useState(false);
const [currentStep, setCurrentStep] = useState(1);
const [formData, setFormData] = useState({
email: '',
password: '',
// Step 1: Personal & Partner Information
first_name: '',
last_name: '',
phone: '',
date_of_birth: '',
address: '',
city: '',
state: '',
zipcode: '',
date_of_birth: '',
lead_sources: [],
partner_first_name: '',
partner_last_name: '',
partner_is_member: false,
partner_plan_to_become_member: false,
referred_by_member_name: ''
});
const leadSourceOptions = [
'Current member',
'Friend',
'OutSmart Magazine',
'Search engine (Google etc.)',
"I've known about LOAF for a long time",
'Other'
];
// Step 2: Newsletter, Volunteer & Scholarship
referred_by_member_name: '',
newsletter_publish_name: false,
newsletter_publish_photo: false,
newsletter_publish_birthday: false,
newsletter_publish_none: false,
volunteer_interests: [],
scholarship_requested: false,
scholarship_reason: '',
// Step 3: Directory Settings
show_in_directory: false,
directory_email: '',
directory_bio: '',
directory_address: '',
directory_phone: '',
directory_dob: '',
directory_partner_name: '',
// Step 4: Account Credentials
email: '',
password: '',
confirmPassword: ''
});
const handleInputChange = (e) => {
const { name, value, type, checked } = e.target;
@@ -50,28 +66,113 @@ const Register = () => {
}));
};
const handleLeadSourceChange = (source) => {
setFormData(prev => {
const sources = prev.lead_sources.includes(source)
? prev.lead_sources.filter(s => s !== source)
: [...prev.lead_sources, source];
return { ...prev, lead_sources: sources };
});
const validateStep1 = () => {
const required = ['first_name', 'last_name', 'phone', 'date_of_birth',
'address', 'city', 'state', 'zipcode'];
for (const field of required) {
if (!formData[field]?.trim()) {
toast.error('Please fill in all required fields');
return false;
}
}
if (formData.lead_sources.length === 0) {
toast.error('Please select at least one option for how you heard about us');
return false;
}
return true;
};
const validateStep2 = () => {
const { newsletter_publish_name, newsletter_publish_photo,
newsletter_publish_birthday, newsletter_publish_none } = formData;
if (!newsletter_publish_name && !newsletter_publish_photo &&
!newsletter_publish_birthday && !newsletter_publish_none) {
toast.error('Please select at least one newsletter publication preference');
return false;
}
if (formData.scholarship_requested && !formData.scholarship_reason?.trim()) {
toast.error('Please explain your scholarship request');
return false;
}
return true;
};
const validateStep3 = () => {
return true; // No required fields
};
const validateStep4 = () => {
if (!formData.email || !formData.password || !formData.confirmPassword) {
toast.error('Please fill in all account fields');
return false;
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(formData.email)) {
toast.error('Please enter a valid email address');
return false;
}
if (formData.password.length < 6) {
toast.error('Password must be at least 6 characters');
return false;
}
if (formData.password !== formData.confirmPassword) {
toast.error('Passwords do not match');
return false;
}
return true;
};
const handleNext = () => {
let isValid = false;
switch (currentStep) {
case 1: isValid = validateStep1(); break;
case 2: isValid = validateStep2(); break;
case 3: isValid = validateStep3(); break;
default: isValid = false;
}
if (isValid) {
setCurrentStep(prev => Math.min(prev + 1, 4));
window.scrollTo({ top: 0, behavior: 'smooth' });
}
};
const handleBack = () => {
setCurrentStep(prev => Math.max(prev - 1, 1));
window.scrollTo({ top: 0, behavior: 'smooth' });
};
const handleSubmit = async (e) => {
e.preventDefault();
// Final validation
if (!validateStep4()) return;
setLoading(true);
try {
// Convert date to ISO format
const dataToSubmit = {
...formData,
date_of_birth: new Date(formData.date_of_birth).toISOString()
// Remove confirmPassword (client-side only)
const { confirmPassword, ...dataToSubmit } = formData;
// Convert date fields to ISO format
const submitData = {
...dataToSubmit,
date_of_birth: new Date(dataToSubmit.date_of_birth).toISOString(),
directory_dob: dataToSubmit.directory_dob
? new Date(dataToSubmit.directory_dob).toISOString()
: null
};
await register(dataToSubmit);
toast.success('Registration successful! Please check your email to verify your account.');
await register(submitData);
toast.success('Please check your email for a confirmation email.');
navigate('/login');
} catch (error) {
toast.error(error.response?.data?.detail || 'Registration failed. Please try again.');
@@ -103,281 +204,83 @@ const Register = () => {
</div>
<form onSubmit={handleSubmit} className="space-y-8" data-testid="register-form">
{/* Account Information */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold fraunces text-[#3D405B]">
Account Information
</h2>
<div className="grid md:grid-cols-2 gap-4">
<div>
<Label htmlFor="email">Email *</Label>
<Input
id="email"
name="email"
type="email"
required
value={formData.email}
onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
data-testid="email-input"
/>
</div>
<div>
<Label htmlFor="password">Password *</Label>
<Input
id="password"
name="password"
type="password"
required
minLength={6}
value={formData.password}
onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
data-testid="password-input"
/>
</div>
</div>
<RegistrationStepIndicator currentStep={currentStep} />
{currentStep === 1 && (
<RegistrationStep1
formData={formData}
setFormData={setFormData}
handleInputChange={handleInputChange}
/>
)}
{currentStep === 2 && (
<RegistrationStep2
formData={formData}
setFormData={setFormData}
handleInputChange={handleInputChange}
/>
)}
{currentStep === 3 && (
<RegistrationStep3
formData={formData}
setFormData={setFormData}
handleInputChange={handleInputChange}
/>
)}
{currentStep === 4 && (
<RegistrationStep4
formData={formData}
handleInputChange={handleInputChange}
/>
)}
{/* Navigation Buttons */}
<div className="flex justify-between items-center pt-6">
{currentStep > 1 ? (
<Button
type="button"
onClick={handleBack}
variant="outline"
className="rounded-full px-6 py-6 text-lg border-2 border-[#EAE0D5] hover:border-[#6B708D] text-[#3D405B]"
>
<ArrowLeft className="mr-2 h-5 w-5" />
Back
</Button>
) : (
<div></div>
)}
{currentStep < 4 ? (
<Button
type="button"
onClick={handleNext}
className="bg-[#E07A5F] text-white hover:bg-[#D0694E] rounded-full px-6 py-6 text-lg font-medium shadow-lg hover:scale-105 transition-transform"
>
Next
<ArrowRight className="ml-2 h-5 w-5" />
</Button>
) : (
<Button
type="submit"
disabled={loading}
className="bg-[#E07A5F] text-white hover:bg-[#D0694E] rounded-full px-6 py-6 text-lg font-medium shadow-lg hover:scale-105 transition-transform disabled:opacity-50 disabled:cursor-not-allowed"
data-testid="submit-register-button"
>
{loading ? 'Creating Account...' : 'Create Account'}
<ArrowRight className="ml-2 h-5 w-5" />
</Button>
)}
</div>
{/* Personal Information */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold fraunces text-[#3D405B]">
Personal Information
</h2>
<div className="grid md:grid-cols-2 gap-4">
<div>
<Label htmlFor="first_name">First Name *</Label>
<Input
id="first_name"
name="first_name"
required
value={formData.first_name}
onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
data-testid="first-name-input"
/>
</div>
<div>
<Label htmlFor="last_name">Last Name *</Label>
<Input
id="last_name"
name="last_name"
required
value={formData.last_name}
onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
data-testid="last-name-input"
/>
</div>
</div>
<div className="grid md:grid-cols-2 gap-4">
<div>
<Label htmlFor="phone">Phone *</Label>
<Input
id="phone"
name="phone"
type="tel"
required
value={formData.phone}
onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
data-testid="phone-input"
/>
</div>
<div>
<Label htmlFor="date_of_birth">Date of Birth *</Label>
<Input
id="date_of_birth"
name="date_of_birth"
type="date"
required
value={formData.date_of_birth}
onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
data-testid="dob-input"
/>
</div>
</div>
<div>
<Label htmlFor="address">Address *</Label>
<Input
id="address"
name="address"
required
value={formData.address}
onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
data-testid="address-input"
/>
</div>
<div className="grid md:grid-cols-3 gap-4">
<div>
<Label htmlFor="city">City *</Label>
<Input
id="city"
name="city"
required
value={formData.city}
onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
data-testid="city-input"
/>
</div>
<div>
<Label htmlFor="state">State *</Label>
<Input
id="state"
name="state"
required
value={formData.state}
onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
data-testid="state-input"
/>
</div>
<div>
<Label htmlFor="zipcode">Zipcode *</Label>
<Input
id="zipcode"
name="zipcode"
required
value={formData.zipcode}
onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
data-testid="zipcode-input"
/>
</div>
</div>
</div>
{/* How Did You Hear About Us */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold fraunces text-[#3D405B]">
How Did You Hear About Us? *
</h2>
<div className="space-y-3">
{leadSourceOptions.map((source) => (
<div key={source} className="flex items-center space-x-2">
<Checkbox
id={source}
checked={formData.lead_sources.includes(source)}
onCheckedChange={() => handleLeadSourceChange(source)}
data-testid={`lead-source-${source.toLowerCase().replace(/\s+/g, '-')}`}
/>
<Label htmlFor={source} className="text-base cursor-pointer">
{source}
</Label>
</div>
))}
</div>
</div>
{/* Partner Information */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold fraunces text-[#3D405B]">
Partner Information (Optional)
</h2>
<div className="grid md:grid-cols-2 gap-4">
<div>
<Label htmlFor="partner_first_name">Partner First Name</Label>
<Input
id="partner_first_name"
name="partner_first_name"
value={formData.partner_first_name}
onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
data-testid="partner-first-name-input"
/>
</div>
<div>
<Label htmlFor="partner_last_name">Partner Last Name</Label>
<Input
id="partner_last_name"
name="partner_last_name"
value={formData.partner_last_name}
onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
data-testid="partner-last-name-input"
/>
</div>
</div>
<div className="space-y-3">
<div className="flex items-center space-x-2">
<Checkbox
id="partner_is_member"
name="partner_is_member"
checked={formData.partner_is_member}
onCheckedChange={(checked) =>
setFormData(prev => ({ ...prev, partner_is_member: checked }))
}
data-testid="partner-is-member-checkbox"
/>
<Label htmlFor="partner_is_member" className="text-base cursor-pointer">
Is your partner already a member?
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="partner_plan_to_become_member"
name="partner_plan_to_become_member"
checked={formData.partner_plan_to_become_member}
onCheckedChange={(checked) =>
setFormData(prev => ({ ...prev, partner_plan_to_become_member: checked }))
}
data-testid="partner-plan-member-checkbox"
/>
<Label htmlFor="partner_plan_to_become_member" className="text-base cursor-pointer">
Does your partner plan to become a member?
</Label>
</div>
</div>
</div>
{/* Referral */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold fraunces text-[#3D405B]">
Referral (Optional)
</h2>
<div>
<Label htmlFor="referred_by_member_name">Referred by Member (Name or Email)</Label>
<Input
id="referred_by_member_name"
name="referred_by_member_name"
value={formData.referred_by_member_name}
onChange={handleInputChange}
placeholder="If a current member referred you, enter their name or email"
className="h-14 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
data-testid="referral-input"
/>
<p className="text-sm text-[#6B708D] mt-2">
If referred by a current member, you may skip the event attendance requirement.
</p>
</div>
</div>
{/* Submit Button */}
<div className="pt-6">
<Button
type="submit"
disabled={loading || formData.lead_sources.length === 0}
className="w-full bg-[#E07A5F] text-white hover:bg-[#D0694E] rounded-full py-6 text-lg font-medium shadow-lg hover:scale-105 transition-transform disabled:opacity-50 disabled:cursor-not-allowed"
data-testid="submit-register-button"
>
{loading ? 'Creating Account...' : 'Create Account'}
<ArrowRight className="ml-2 h-5 w-5" />
</Button>
<p className="text-center text-[#6B708D] mt-4">
Already have an account?{' '}
<Link to="/login" className="text-[#E07A5F] hover:underline font-medium">
Login here
</Link>
</p>
</div>
<p className="text-center text-[#6B708D] mt-4">
Already have an account?{' '}
<Link to="/login" className="text-[#E07A5F] hover:underline font-medium">
Login here
</Link>
</p>
</form>
</Card>
</div>