Compare commits
2 Commits
d5152609b6
...
theme-prov
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7eef62560e | ||
|
|
f70a133e18 |
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -9,12 +9,77 @@ import {
|
||||
} from './ui/dialog';
|
||||
import { Button } from './ui/button';
|
||||
import { Card } from './ui/card';
|
||||
import { Checkbox } from './ui/checkbox';
|
||||
import { Input } from './ui/input';
|
||||
import { Label } from './ui/label';
|
||||
import { Textarea } from './ui/textarea';
|
||||
import { User, Mail, Phone, Calendar, UserCheck, Clock, FileText } from 'lucide-react';
|
||||
import StatusBadge from './StatusBadge';
|
||||
import api from '../utils/api';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
const ViewRegistrationDialog = ({ open, onOpenChange, user }) => {
|
||||
if (!user) return null;
|
||||
|
||||
const [formData, setFormData] = useState(null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
||||
const autoSaveTimeoutRef = useRef(null);
|
||||
const pendingSaveRef = useRef(false);
|
||||
|
||||
const leadSourceOptions = [
|
||||
'Current member',
|
||||
'Friend',
|
||||
'OutSmart Magazine',
|
||||
'Search engine (Google etc.)',
|
||||
"I've known about LOAF for a long time",
|
||||
'Other'
|
||||
];
|
||||
|
||||
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'
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !user) return;
|
||||
const nextFormData = {
|
||||
lead_sources: Array.isArray(user.lead_sources) ? user.lead_sources : [],
|
||||
partner_first_name: user.partner_first_name || '',
|
||||
partner_last_name: user.partner_last_name || '',
|
||||
partner_is_member: Boolean(user.partner_is_member),
|
||||
partner_plan_to_become_member: Boolean(user.partner_plan_to_become_member),
|
||||
newsletter_publish_name: Boolean(user.newsletter_publish_name),
|
||||
newsletter_publish_photo: Boolean(user.newsletter_publish_photo),
|
||||
newsletter_publish_birthday: Boolean(user.newsletter_publish_birthday),
|
||||
newsletter_publish_none: Boolean(user.newsletter_publish_none),
|
||||
referred_by_member_name: user.referred_by_member_name || '',
|
||||
volunteer_interests: Array.isArray(user.volunteer_interests) ? user.volunteer_interests : [],
|
||||
scholarship_requested: Boolean(user.scholarship_requested),
|
||||
scholarship_reason: user.scholarship_reason || ''
|
||||
};
|
||||
setFormData(nextFormData);
|
||||
setHasUnsavedChanges(false);
|
||||
}, [open, user]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (autoSaveTimeoutRef.current) {
|
||||
clearTimeout(autoSaveTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return '—';
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
@@ -44,6 +109,91 @@ const ViewRegistrationDialog = ({ open, onOpenChange, user }) => {
|
||||
return phone;
|
||||
};
|
||||
|
||||
const saveProfile = async (showToast = true) => {
|
||||
if (!formData) return;
|
||||
if (isSaving) {
|
||||
pendingSaveRef.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await api.put('/users/profile', {
|
||||
lead_sources: formData.lead_sources,
|
||||
partner_first_name: formData.partner_first_name,
|
||||
partner_last_name: formData.partner_last_name,
|
||||
partner_is_member: formData.partner_is_member,
|
||||
partner_plan_to_become_member: formData.partner_plan_to_become_member,
|
||||
newsletter_publish_name: formData.newsletter_publish_name,
|
||||
newsletter_publish_photo: formData.newsletter_publish_photo,
|
||||
newsletter_publish_birthday: formData.newsletter_publish_birthday,
|
||||
newsletter_publish_none: formData.newsletter_publish_none,
|
||||
referred_by_member_name: formData.referred_by_member_name,
|
||||
volunteer_interests: formData.volunteer_interests,
|
||||
scholarship_requested: formData.scholarship_requested,
|
||||
scholarship_reason: formData.scholarship_reason
|
||||
});
|
||||
setHasUnsavedChanges(false);
|
||||
if (showToast) {
|
||||
toast.success('Registration details saved');
|
||||
}
|
||||
} catch (error) {
|
||||
if (showToast) {
|
||||
toast.error(error.response?.data?.detail || 'Failed to save registration details');
|
||||
}
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
if (pendingSaveRef.current) {
|
||||
pendingSaveRef.current = false;
|
||||
saveProfile(showToast);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const scheduleAutoSave = () => {
|
||||
setHasUnsavedChanges(true);
|
||||
if (autoSaveTimeoutRef.current) {
|
||||
clearTimeout(autoSaveTimeoutRef.current);
|
||||
}
|
||||
autoSaveTimeoutRef.current = setTimeout(() => {
|
||||
saveProfile(false);
|
||||
}, 800);
|
||||
};
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => {
|
||||
const next = { ...prev, [name]: value };
|
||||
return next;
|
||||
});
|
||||
scheduleAutoSave();
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (name, checked) => {
|
||||
setFormData(prev => ({ ...prev, [name]: checked }));
|
||||
scheduleAutoSave();
|
||||
};
|
||||
|
||||
const handleLeadSourceChange = (source) => {
|
||||
setFormData(prev => {
|
||||
const sources = prev.lead_sources.includes(source)
|
||||
? prev.lead_sources.filter((item) => item !== source)
|
||||
: [...prev.lead_sources, source];
|
||||
return { ...prev, lead_sources: sources };
|
||||
});
|
||||
scheduleAutoSave();
|
||||
};
|
||||
|
||||
const handleVolunteerChange = (option) => {
|
||||
setFormData(prev => {
|
||||
const interests = prev.volunteer_interests.includes(option)
|
||||
? prev.volunteer_interests.filter((item) => item !== option)
|
||||
: [...prev.volunteer_interests, option];
|
||||
return { ...prev, volunteer_interests: interests };
|
||||
});
|
||||
scheduleAutoSave();
|
||||
};
|
||||
|
||||
const InfoRow = ({ icon: Icon, label, value }) => (
|
||||
<div className="flex items-start gap-3 py-3 border-b border-[var(--neutral-800)] last:border-b-0">
|
||||
<div className="h-10 w-10 rounded-lg bg-[var(--lavender-400)] flex items-center justify-center flex-shrink-0">
|
||||
@@ -109,10 +259,214 @@ const ViewRegistrationDialog = ({ open, onOpenChange, user }) => {
|
||||
Registration Details
|
||||
</h3>
|
||||
<InfoRow icon={Calendar} label="Registration Date" value={formatDate(user.created_at)} />
|
||||
<InfoRow icon={UserCheck} label="Referred By" value={user.referred_by_member_name} />
|
||||
<InfoRow icon={UserCheck} label="Referred By" value={formData?.referred_by_member_name} />
|
||||
<InfoRow icon={Clock} label="Email Verification Expires" value={formatDateTime(user.email_verification_expires_at)} />
|
||||
</Card>
|
||||
|
||||
{formData && (
|
||||
<>
|
||||
{/* How Did You Hear About Us */}
|
||||
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
|
||||
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
How Did You Hear About Us? *
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{leadSourceOptions.map((source) => (
|
||||
<div key={source} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={`lead_${source}`}
|
||||
checked={formData.lead_sources.includes(source)}
|
||||
onCheckedChange={() => handleLeadSourceChange(source)}
|
||||
/>
|
||||
<Label htmlFor={`lead_${source}`} className="text-base cursor-pointer">
|
||||
{source}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Partner Information */}
|
||||
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
|
||||
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Partner Information (Optional)
|
||||
</h3>
|
||||
<div className="grid md:grid-cols-2 gap-4 mb-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-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
|
||||
/>
|
||||
</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-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="partner_is_member"
|
||||
checked={formData.partner_is_member}
|
||||
onCheckedChange={(checked) => handleCheckboxChange('partner_is_member', checked)}
|
||||
/>
|
||||
<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"
|
||||
checked={formData.partner_plan_to_become_member}
|
||||
onCheckedChange={(checked) => handleCheckboxChange('partner_plan_to_become_member', checked)}
|
||||
/>
|
||||
<Label htmlFor="partner_plan_to_become_member" className="text-base cursor-pointer">
|
||||
Does your partner plan to become a member?
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Newsletter Preferences */}
|
||||
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
|
||||
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Newsletter Publication Preferences *
|
||||
</h3>
|
||||
<p className="text-brand-purple mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
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) => handleCheckboxChange('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) => handleCheckboxChange('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) => handleCheckboxChange('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) => handleCheckboxChange('newsletter_publish_none', checked)}
|
||||
/>
|
||||
<Label htmlFor="newsletter_publish_none" className="text-base cursor-pointer">
|
||||
Do not publish any of my information
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Referral */}
|
||||
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
|
||||
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Referral
|
||||
</h3>
|
||||
<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-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
|
||||
/>
|
||||
<p className="text-sm text-brand-purple mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
If referred by a current member, you may skip the event attendance requirement.
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Volunteer Interests */}
|
||||
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
|
||||
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
Volunteer Interests (Optional)
|
||||
</h3>
|
||||
<p className="text-brand-purple mb-4" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
I may at some time be interested in volunteering with LOAF in the following ways (training is provided)
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-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>
|
||||
</Card>
|
||||
|
||||
{/* Scholarship Request */}
|
||||
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="scholarship_requested"
|
||||
checked={formData.scholarship_requested}
|
||||
onCheckedChange={(checked) => handleCheckboxChange('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-brand-purple mt-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
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-[var(--neutral-800)] focus:border-brand-purple"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Additional Information (if available) */}
|
||||
{(user.address || user.city || user.state || user.zip_code) && (
|
||||
<Card className="p-4 border border-[var(--neutral-800)] rounded-xl">
|
||||
@@ -156,6 +510,19 @@ const ViewRegistrationDialog = ({ open, onOpenChange, user }) => {
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<div className="flex-1 text-sm text-brand-purple" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
{isSaving && 'Saving changes...'}
|
||||
{!isSaving && hasUnsavedChanges && 'Unsaved changes'}
|
||||
{!isSaving && !hasUnsavedChanges && 'All changes saved'}
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => saveProfile(true)}
|
||||
disabled={!hasUnsavedChanges || isSaving}
|
||||
className="rounded-xl border-2 border-[var(--neutral-800)] bg-white text-[var(--purple-ink)] hover:bg-[var(--lavender-300)]"
|
||||
>
|
||||
Save All
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => onOpenChange(false)}
|
||||
|
||||
1048
src/pages/Profile.js
1048
src/pages/Profile.js
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user