forked from andika/membership-fe
Update registration Step
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user