forked from andika/membership-fe
Update registration Step
This commit is contained in:
235
src/components/registration/RegistrationStep1.js
Normal file
235
src/components/registration/RegistrationStep1.js
Normal 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;
|
||||||
186
src/components/registration/RegistrationStep2.js
Normal file
186
src/components/registration/RegistrationStep2.js
Normal 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;
|
||||||
174
src/components/registration/RegistrationStep3.js
Normal file
174
src/components/registration/RegistrationStep3.js
Normal 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;
|
||||||
78
src/components/registration/RegistrationStep4.js
Normal file
78
src/components/registration/RegistrationStep4.js
Normal 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;
|
||||||
62
src/components/registration/RegistrationStepIndicator.js
Normal file
62
src/components/registration/RegistrationStepIndicator.js
Normal 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;
|
||||||
@@ -2,45 +2,61 @@ import React, { useState } from 'react';
|
|||||||
import { useNavigate, Link } from 'react-router-dom';
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
import { Button } from '../components/ui/button';
|
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 { Card } from '../components/ui/card';
|
||||||
import { Checkbox } from '../components/ui/checkbox';
|
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import Navbar from '../components/Navbar';
|
import Navbar from '../components/Navbar';
|
||||||
import { ArrowRight, ArrowLeft } from 'lucide-react';
|
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 Register = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { register } = useAuth();
|
const { register } = useAuth();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [currentStep, setCurrentStep] = useState(1);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
email: '',
|
// Step 1: Personal & Partner Information
|
||||||
password: '',
|
|
||||||
first_name: '',
|
first_name: '',
|
||||||
last_name: '',
|
last_name: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
|
date_of_birth: '',
|
||||||
address: '',
|
address: '',
|
||||||
city: '',
|
city: '',
|
||||||
state: '',
|
state: '',
|
||||||
zipcode: '',
|
zipcode: '',
|
||||||
date_of_birth: '',
|
|
||||||
lead_sources: [],
|
lead_sources: [],
|
||||||
partner_first_name: '',
|
partner_first_name: '',
|
||||||
partner_last_name: '',
|
partner_last_name: '',
|
||||||
partner_is_member: false,
|
partner_is_member: false,
|
||||||
partner_plan_to_become_member: false,
|
partner_plan_to_become_member: false,
|
||||||
referred_by_member_name: ''
|
|
||||||
});
|
|
||||||
|
|
||||||
const leadSourceOptions = [
|
// Step 2: Newsletter, Volunteer & Scholarship
|
||||||
'Current member',
|
referred_by_member_name: '',
|
||||||
'Friend',
|
newsletter_publish_name: false,
|
||||||
'OutSmart Magazine',
|
newsletter_publish_photo: false,
|
||||||
'Search engine (Google etc.)',
|
newsletter_publish_birthday: false,
|
||||||
"I've known about LOAF for a long time",
|
newsletter_publish_none: false,
|
||||||
'Other'
|
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 handleInputChange = (e) => {
|
||||||
const { name, value, type, checked } = e.target;
|
const { name, value, type, checked } = e.target;
|
||||||
@@ -50,28 +66,113 @@ const Register = () => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLeadSourceChange = (source) => {
|
const validateStep1 = () => {
|
||||||
setFormData(prev => {
|
const required = ['first_name', 'last_name', 'phone', 'date_of_birth',
|
||||||
const sources = prev.lead_sources.includes(source)
|
'address', 'city', 'state', 'zipcode'];
|
||||||
? prev.lead_sources.filter(s => s !== source)
|
for (const field of required) {
|
||||||
: [...prev.lead_sources, source];
|
if (!formData[field]?.trim()) {
|
||||||
return { ...prev, lead_sources: sources };
|
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) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Final validation
|
||||||
|
if (!validateStep4()) return;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Convert date to ISO format
|
// Remove confirmPassword (client-side only)
|
||||||
const dataToSubmit = {
|
const { confirmPassword, ...dataToSubmit } = formData;
|
||||||
...formData,
|
|
||||||
date_of_birth: new Date(formData.date_of_birth).toISOString()
|
// 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);
|
await register(submitData);
|
||||||
toast.success('Registration successful! Please check your email to verify your account.');
|
toast.success('Please check your email for a confirmation email.');
|
||||||
navigate('/login');
|
navigate('/login');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(error.response?.data?.detail || 'Registration failed. Please try again.');
|
toast.error(error.response?.data?.detail || 'Registration failed. Please try again.');
|
||||||
@@ -103,281 +204,83 @@ const Register = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-8" data-testid="register-form">
|
<form onSubmit={handleSubmit} className="space-y-8" data-testid="register-form">
|
||||||
{/* Account Information */}
|
<RegistrationStepIndicator currentStep={currentStep} />
|
||||||
<div className="space-y-4">
|
|
||||||
<h2 className="text-2xl font-semibold fraunces text-[#3D405B]">
|
{currentStep === 1 && (
|
||||||
Account Information
|
<RegistrationStep1
|
||||||
</h2>
|
formData={formData}
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
setFormData={setFormData}
|
||||||
<div>
|
handleInputChange={handleInputChange}
|
||||||
<Label htmlFor="email">Email *</Label>
|
/>
|
||||||
<Input
|
)}
|
||||||
id="email"
|
|
||||||
name="email"
|
{currentStep === 2 && (
|
||||||
type="email"
|
<RegistrationStep2
|
||||||
required
|
formData={formData}
|
||||||
value={formData.email}
|
setFormData={setFormData}
|
||||||
onChange={handleInputChange}
|
handleInputChange={handleInputChange}
|
||||||
className="h-14 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
|
/>
|
||||||
data-testid="email-input"
|
)}
|
||||||
/>
|
|
||||||
</div>
|
{currentStep === 3 && (
|
||||||
<div>
|
<RegistrationStep3
|
||||||
<Label htmlFor="password">Password *</Label>
|
formData={formData}
|
||||||
<Input
|
setFormData={setFormData}
|
||||||
id="password"
|
handleInputChange={handleInputChange}
|
||||||
name="password"
|
/>
|
||||||
type="password"
|
)}
|
||||||
required
|
|
||||||
minLength={6}
|
{currentStep === 4 && (
|
||||||
value={formData.password}
|
<RegistrationStep4
|
||||||
onChange={handleInputChange}
|
formData={formData}
|
||||||
className="h-14 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
|
handleInputChange={handleInputChange}
|
||||||
data-testid="password-input"
|
/>
|
||||||
/>
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
{/* 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>
|
</div>
|
||||||
|
|
||||||
{/* Personal Information */}
|
<p className="text-center text-[#6B708D] mt-4">
|
||||||
<div className="space-y-4">
|
Already have an account?{' '}
|
||||||
<h2 className="text-2xl font-semibold fraunces text-[#3D405B]">
|
<Link to="/login" className="text-[#E07A5F] hover:underline font-medium">
|
||||||
Personal Information
|
Login here
|
||||||
</h2>
|
</Link>
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
</p>
|
||||||
<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>
|
|
||||||
</form>
|
</form>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user