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

@@ -0,0 +1,235 @@
import React from 'react';
import { Label } from '../ui/label';
import { Input } from '../ui/input';
import { Checkbox } from '../ui/checkbox';
const RegistrationStep1 = ({ formData, setFormData, handleInputChange }) => {
const leadSourceOptions = [
'Current member',
'Friend',
'OutSmart Magazine',
'Search engine (Google etc.)',
"I've known about LOAF for a long time",
'Other'
];
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 };
});
};
return (
<div className="space-y-8">
{/* Personal Information */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold fraunces text-[#3D405B]">
Personal Information
</h2>
{/* First Name, Last Name */}
<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>
{/* Phone, Date of Birth */}
<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>
{/* Address */}
<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>
{/* City, State, Zipcode */}
<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>
</div>
);
};
export default RegistrationStep1;

View File

@@ -0,0 +1,186 @@
import React from 'react';
import { Label } from '../ui/label';
import { Input } from '../ui/input';
import { Textarea } from '../ui/textarea';
import { Checkbox } from '../ui/checkbox';
const RegistrationStep2 = ({ formData, setFormData, handleInputChange }) => {
const volunteerOptions = [
'Welcoming new members at events',
'Sending out birthday cards',
'Care Team Calls',
'Sharing ideas for events',
'Researching grants',
'Applying for grants',
'Assisting with TeatherLOAFers',
'Assisting with ActiveLOAFers',
'Assisting with weekday Lunch Bunch',
'Uploading Photos to the Website',
'Assisting with eNewsletter',
'Other administrative task'
];
const handleVolunteerChange = (option) => {
setFormData(prev => {
const interests = prev.volunteer_interests.includes(option)
? prev.volunteer_interests.filter(i => i !== option)
: [...prev.volunteer_interests, option];
return { ...prev, volunteer_interests: interests };
});
};
return (
<div className="space-y-8">
{/* Newsletter Publication Preferences */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold fraunces text-[#3D405B]">
Newsletter Publication Preferences *
</h2>
<p className="text-[#6B708D]">
Please check what information may be published in LOAF Newsletter
</p>
<div className="space-y-3">
<div className="flex items-center space-x-2">
<Checkbox
id="newsletter_publish_name"
checked={formData.newsletter_publish_name}
onCheckedChange={(checked) =>
setFormData(prev => ({ ...prev, newsletter_publish_name: checked }))
}
/>
<Label htmlFor="newsletter_publish_name" className="text-base cursor-pointer">
Name
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="newsletter_publish_photo"
checked={formData.newsletter_publish_photo}
onCheckedChange={(checked) =>
setFormData(prev => ({ ...prev, newsletter_publish_photo: checked }))
}
/>
<Label htmlFor="newsletter_publish_photo" className="text-base cursor-pointer">
Photo (added later in profile)
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="newsletter_publish_birthday"
checked={formData.newsletter_publish_birthday}
onCheckedChange={(checked) =>
setFormData(prev => ({ ...prev, newsletter_publish_birthday: checked }))
}
/>
<Label htmlFor="newsletter_publish_birthday" className="text-base cursor-pointer">
Birthday
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="newsletter_publish_none"
checked={formData.newsletter_publish_none}
onCheckedChange={(checked) =>
setFormData(prev => ({ ...prev, newsletter_publish_none: checked }))
}
/>
<Label htmlFor="newsletter_publish_none" className="text-base cursor-pointer">
Do not publish any of my information
</Label>
</div>
</div>
</div>
{/* Referral */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold fraunces text-[#3D405B]">
Referral
</h2>
<div>
<Label htmlFor="referred_by_member_name">
Name of a LOAF Member who already knows you
</Label>
<Input
id="referred_by_member_name"
name="referred_by_member_name"
value={formData.referred_by_member_name}
onChange={handleInputChange}
placeholder="Enter member 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>
{/* Volunteer Interests */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold fraunces text-[#3D405B]">
Volunteer Interests (Optional)
</h2>
<p className="text-[#6B708D]">
I may at some time be interested in volunteering with LOAF in the following ways (training is provided)
</p>
<div className="space-y-3">
{volunteerOptions.map((option) => (
<div key={option} className="flex items-center space-x-2">
<Checkbox
id={`volunteer_${option}`}
checked={formData.volunteer_interests.includes(option)}
onCheckedChange={() => handleVolunteerChange(option)}
/>
<Label htmlFor={`volunteer_${option}`} className="text-base cursor-pointer">
{option}
</Label>
</div>
))}
</div>
</div>
{/* Scholarship Request */}
<div className="space-y-4">
<div className="flex items-center space-x-2">
<Checkbox
id="scholarship_requested"
checked={formData.scholarship_requested}
onCheckedChange={(checked) =>
setFormData(prev => ({ ...prev, scholarship_requested: checked }))
}
/>
<Label htmlFor="scholarship_requested" className="text-base cursor-pointer font-semibold">
I am requesting for scholarship
</Label>
</div>
<p className="text-sm text-[#6B708D]">
Scholarship information is kept confidential
</p>
{formData.scholarship_requested && (
<div className="mt-4">
<Label htmlFor="scholarship_reason">
Please explain your situation *
</Label>
<Textarea
id="scholarship_reason"
name="scholarship_reason"
value={formData.scholarship_reason}
onChange={handleInputChange}
placeholder="Tell us why you're requesting a scholarship..."
rows={4}
className="rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
/>
</div>
)}
</div>
</div>
);
};
export default RegistrationStep2;

View File

@@ -0,0 +1,174 @@
import React, { useEffect } from 'react';
import { Label } from '../ui/label';
import { Input } from '../ui/input';
import { Textarea } from '../ui/textarea';
const RegistrationStep3 = ({ formData, setFormData, handleInputChange }) => {
// Pre-fill directory fields when user opts in
useEffect(() => {
if (formData.show_in_directory && !formData.directory_email) {
setFormData(prev => ({
...prev,
directory_email: prev.email || '',
directory_address: prev.address || '',
directory_phone: prev.phone || '',
directory_dob: prev.date_of_birth || '',
directory_partner_name: prev.partner_first_name && prev.partner_last_name
? `${prev.partner_first_name} ${prev.partner_last_name}`
: ''
}));
}
}, [formData.show_in_directory]);
return (
<div className="space-y-6">
<div className="space-y-4">
<h2 className="text-2xl font-semibold fraunces text-[#3D405B]">
Members Directory
</h2>
<p className="text-[#6B708D]">
Would you like to be displayed on our private members directory? (optional and you can change the answer later)
</p>
{/* Directory Opt-in Radio Buttons */}
<div className="space-y-3">
<div
className={`
p-4 rounded-xl border-2 cursor-pointer transition-all
${formData.show_in_directory
? 'border-[#E07A5F] bg-[#E07A5F]/5'
: 'border-[#EAE0D5] hover:border-[#6B708D]'
}
`}
onClick={() => setFormData(prev => ({ ...prev, show_in_directory: true }))}
>
<div className="flex items-center space-x-3">
<div className={`
w-5 h-5 rounded-full border-2 flex items-center justify-center
${formData.show_in_directory ? 'border-[#E07A5F]' : 'border-[#EAE0D5]'}
`}>
{formData.show_in_directory && (
<div className="w-3 h-3 rounded-full bg-[#E07A5F]" />
)}
</div>
<span className="font-medium text-[#3D405B]">
Yes, include me in the Members Directory
</span>
</div>
</div>
<div
className={`
p-4 rounded-xl border-2 cursor-pointer transition-all
${!formData.show_in_directory
? 'border-[#E07A5F] bg-[#E07A5F]/5'
: 'border-[#EAE0D5] hover:border-[#6B708D]'
}
`}
onClick={() => setFormData(prev => ({ ...prev, show_in_directory: false }))}
>
<div className="flex items-center space-x-3">
<div className={`
w-5 h-5 rounded-full border-2 flex items-center justify-center
${!formData.show_in_directory ? 'border-[#E07A5F]' : 'border-[#EAE0D5]'}
`}>
{!formData.show_in_directory && (
<div className="w-3 h-3 rounded-full bg-[#E07A5F]" />
)}
</div>
<span className="font-medium text-[#3D405B]">
No, don't include me in the Members Directory
</span>
</div>
</div>
</div>
</div>
{/* Conditional Directory Fields */}
{formData.show_in_directory && (
<div className="space-y-4 mt-6 p-6 bg-[#FDFCF8] rounded-xl border border-[#EAE0D5]">
<p className="text-[#6B708D] text-sm">
Below, choose what information you would like include in the Members Only Directory.
(If you ever want to update this information, remember the Directory Section and Account Section are separate)
</p>
<div>
<Label htmlFor="directory_email">Your preferred contact email</Label>
<Input
id="directory_email"
name="directory_email"
type="email"
value={formData.directory_email}
onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
/>
</div>
<div>
<Label htmlFor="directory_bio">Bio for my Member Profile</Label>
<Textarea
id="directory_bio"
name="directory_bio"
value={formData.directory_bio}
onChange={handleInputChange}
placeholder="Tell other members about yourself..."
rows={4}
className="rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
/>
</div>
<div>
<Label htmlFor="directory_address">Preferred address</Label>
<Input
id="directory_address"
name="directory_address"
value={formData.directory_address}
onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
/>
</div>
<div>
<Label htmlFor="directory_phone">Preferred contact phone number</Label>
<Input
id="directory_phone"
name="directory_phone"
type="tel"
value={formData.directory_phone}
onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
/>
</div>
<div>
<Label htmlFor="directory_dob">Date of birth</Label>
<Input
id="directory_dob"
name="directory_dob"
type="date"
value={formData.directory_dob}
onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
/>
</div>
<div>
<Label htmlFor="directory_partner_name">
Enter your partner's name as you'd like it to appear in the Members Directory
</Label>
<Input
id="directory_partner_name"
name="directory_partner_name"
value={formData.directory_partner_name}
onChange={handleInputChange}
className="h-14 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
/>
</div>
</div>
)}
</div>
);
};
export default RegistrationStep3;

View File

@@ -0,0 +1,78 @@
import React from 'react';
import { Label } from '../ui/label';
import { Input } from '../ui/input';
const RegistrationStep4 = ({ formData, handleInputChange }) => {
return (
<div className="space-y-6">
<div className="space-y-4">
<h2 className="text-2xl font-semibold fraunces text-[#3D405B]">
Account Credentials
</h2>
<p className="text-[#6B708D]">
Your email is also your username that you can use to login.
Please note you can only login after your application is approved.
</p>
</div>
<div className="space-y-4">
<div>
<Label htmlFor="email">Email *</Label>
<Input
id="email"
name="email"
type="email"
required
value={formData.email}
onChange={handleInputChange}
placeholder="your.email@example.com"
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}
placeholder="At least 6 characters"
className="h-14 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
data-testid="password-input"
/>
<p className="text-sm text-[#6B708D] mt-2">
Must be at least 6 characters long
</p>
</div>
<div>
<Label htmlFor="confirmPassword">Repeat Password *</Label>
<Input
id="confirmPassword"
name="confirmPassword"
type="password"
required
value={formData.confirmPassword}
onChange={handleInputChange}
placeholder="Re-enter your password"
className="h-14 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
data-testid="confirm-password-input"
/>
{formData.confirmPassword && formData.password !== formData.confirmPassword && (
<p className="text-sm text-red-500 mt-2">
Passwords do not match
</p>
)}
</div>
</div>
</div>
);
};
export default RegistrationStep4;

View File

@@ -0,0 +1,62 @@
import React from 'react';
const RegistrationStepIndicator = ({ currentStep, totalSteps = 4 }) => {
const steps = [
{ number: 1, title: 'Personal Info' },
{ number: 2, title: 'Preferences' },
{ number: 3, title: 'Directory' },
{ number: 4, title: 'Account' }
];
return (
<div className="mb-8">
{/* Progress Bar */}
<div className="flex items-center justify-between relative">
{steps.map((step, index) => (
<React.Fragment key={step.number}>
{/* Step Circle */}
<div className="flex flex-col items-center relative z-10">
<div className={`
w-12 h-12 rounded-full flex items-center justify-center font-semibold text-lg
transition-all duration-300
${currentStep === step.number
? 'bg-[#E07A5F] text-white scale-110 shadow-lg'
: currentStep > step.number
? 'bg-[#81B29A] text-white'
: 'bg-[#EAE0D5] text-[#6B708D]'
}
`}>
{currentStep > step.number ? '✓' : step.number}
</div>
<span className={`
text-sm mt-2 font-medium transition-colors
${currentStep === step.number ? 'text-[#E07A5F]' : 'text-[#6B708D]'}
`}>
{step.title}
</span>
</div>
{/* Connecting Line */}
{index < steps.length - 1 && (
<div className="flex-1 h-1 mx-2 relative -top-6 bg-[#EAE0D5]">
<div
className={`
h-full transition-all duration-500
${currentStep > step.number ? 'bg-[#81B29A] w-full' : 'bg-transparent w-0'}
`}
/>
</div>
)}
</React.Fragment>
))}
</div>
{/* Step Counter */}
<p className="text-center text-[#6B708D] mt-6 text-lg">
Step <span className="font-semibold text-[#E07A5F]">{currentStep}</span> of {totalSteps}
</p>
</div>
);
};
export default RegistrationStepIndicator;

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>