feat: Implement dynamic agent profile editing by introducing new services and dynamic UI components.
This commit is contained in:
@@ -1,708 +1,288 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useRef } from 'react';
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Image from 'next/image';
|
||||
import {
|
||||
QuickLinks,
|
||||
FormSection,
|
||||
FormInput,
|
||||
FormCheckbox,
|
||||
FormTextarea,
|
||||
FormSelect,
|
||||
FileUpload,
|
||||
AttachedFile,
|
||||
TagInput,
|
||||
} from './components';
|
||||
|
||||
// Initial form data - in production this would come from API
|
||||
const initialFormData = {
|
||||
firstName: 'Brian',
|
||||
lastName: 'Neeland',
|
||||
email: 'brian@requestnetwork.com',
|
||||
phone: '+9195003837493',
|
||||
professionalTypes: {
|
||||
firstTime: true,
|
||||
solo: false,
|
||||
team: false,
|
||||
},
|
||||
attachedDocuments: ['Brian_Photo_1_Blue_Coat.pdf'],
|
||||
state: 'Colorado',
|
||||
licensedAreas: ['Colorado Springs', 'Monument'],
|
||||
expertiseYears: [
|
||||
{ area: 'Colorado', years: '10 yrs' },
|
||||
{ area: 'Falcon', years: '10 yrs' },
|
||||
],
|
||||
biography: "Brian brings eight years of hands-on experience helping clients confidently buy, sell, and invest in real estate. He combines strong analytical skills with deep market knowledge to identify the right opportunities and guide clients through every step of the process. With a data-driven approach and clear communication, Brian ensures smooth transactions and informed decisions tailored to each client's goals.",
|
||||
testimonialHighlight: "The most amazing experience I've had as a real estate professional is helping a family secure their dream home and seeing their happiness when they received the keys.",
|
||||
expertise: ['Buyers', 'Sellers', 'Divorce', 'Luxury', 'Retirement', 'Historical', 'Condo', 'Rural'],
|
||||
yearsInBusiness: {
|
||||
totalYears: '10 Years',
|
||||
totalTransactions: '100+',
|
||||
},
|
||||
contractsClosed: {
|
||||
lessThan10: false,
|
||||
between10And25: false,
|
||||
between26And50: true,
|
||||
between51And75: false,
|
||||
moreThan75: false,
|
||||
},
|
||||
pricePoint: {
|
||||
under100k: false,
|
||||
between100kAnd200k: false,
|
||||
between200kAnd400k: true,
|
||||
between400kAnd600k: true,
|
||||
between600kAnd800k: false,
|
||||
between800kAnd1m: false,
|
||||
above1m: false,
|
||||
},
|
||||
specialization: {
|
||||
clientLocalization: {
|
||||
local: true,
|
||||
national: false,
|
||||
international: false,
|
||||
},
|
||||
clientTypeWorkedWith: {
|
||||
firstTimeBuyer: true,
|
||||
firstTimeSeller: false,
|
||||
},
|
||||
propertyType: {
|
||||
sfr: true,
|
||||
condo: false,
|
||||
luxury: false,
|
||||
townhome: false,
|
||||
commercial: false,
|
||||
},
|
||||
transactionType: {
|
||||
relocation: true,
|
||||
probate: false,
|
||||
taxLien: false,
|
||||
traditional: false,
|
||||
newConstruction: false,
|
||||
},
|
||||
loanType: {
|
||||
conventional: true,
|
||||
fha: false,
|
||||
va: false,
|
||||
usda: false,
|
||||
reverse: false,
|
||||
},
|
||||
lifestyleSpecialties: {
|
||||
boating: true,
|
||||
horses: false,
|
||||
rv: false,
|
||||
automotive: false,
|
||||
aviation: false,
|
||||
},
|
||||
},
|
||||
hobbies: ['Boating', 'Horses'],
|
||||
certifications: [
|
||||
{ name: 'Certified Residential Specialist (CRS)', org: 'Residential Real Estate Council' },
|
||||
],
|
||||
availability: {
|
||||
officeHours: ['MON-FRI 8am-6pm', 'Sat 10am-2pm'],
|
||||
contactMethod: {
|
||||
email: true,
|
||||
call: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
import { QuickLinks } from './components';
|
||||
import DynamicSection from './components/DynamicSection';
|
||||
import { profileSectionsService, ProfileSection, AgentTypeSectionsResponse } from '@/services/profile-sections.service';
|
||||
import { agentsService, AgentProfile } from '@/services/agents.service';
|
||||
|
||||
export default function EditProfilePage() {
|
||||
const router = useRouter();
|
||||
const [formData, setFormData] = useState(initialFormData);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [stateInput, setStateInput] = useState('');
|
||||
const [officeHoursInput, setOfficeHoursInput] = useState('');
|
||||
|
||||
// State for agent profile and dynamic sections
|
||||
const [agentProfile, setAgentProfile] = useState<AgentProfile | null>(null);
|
||||
const [sections, setSections] = useState<ProfileSection[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Form data keyed by field slug
|
||||
const [formData, setFormData] = useState<Record<string, unknown>>({});
|
||||
const [formErrors, setFormErrors] = useState<Record<string, string>>({});
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
// Fetch agent profile and sections on mount
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// First, fetch the agent's profile to get their agentTypeId
|
||||
const profile = await agentsService.getMyProfile();
|
||||
setAgentProfile(profile);
|
||||
|
||||
if (!profile.agentTypeId) {
|
||||
setError('Your profile does not have an agent type assigned. Please contact support.');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Now fetch the sections for this agent type
|
||||
const response: AgentTypeSectionsResponse = await profileSectionsService.getSectionsForAgentType(profile.agentTypeId);
|
||||
|
||||
// Sort sections by effectiveSortOrder
|
||||
const sortedSections = [...response.sections].sort(
|
||||
(a, b) => (a.effectiveSortOrder || a.sortOrder) - (b.effectiveSortOrder || b.sortOrder)
|
||||
);
|
||||
|
||||
setSections(sortedSections);
|
||||
|
||||
// Initialize form data with existing profile data + default values from fields
|
||||
const initialData: Record<string, unknown> = {};
|
||||
|
||||
// Pre-fill with existing profile data
|
||||
if (profile.firstName) initialData['first-name'] = profile.firstName;
|
||||
if (profile.lastName) initialData['last-name'] = profile.lastName;
|
||||
if (profile.email) initialData['email'] = profile.email;
|
||||
if (profile.phone) initialData['phone'] = profile.phone;
|
||||
if (profile.bio) initialData['bio'] = profile.bio;
|
||||
if (profile.licenseNumber) initialData['license-number'] = profile.licenseNumber;
|
||||
if (profile.experience) initialData['experience'] = profile.experience;
|
||||
if (profile.specializations?.length) initialData['specializations'] = profile.specializations;
|
||||
if (profile.languages?.length) initialData['languages'] = profile.languages;
|
||||
if (profile.serviceAreas?.length) initialData['service-areas'] = profile.serviceAreas;
|
||||
|
||||
// Then apply field default values where no existing data
|
||||
sortedSections.forEach(section => {
|
||||
section.fields?.forEach(field => {
|
||||
if (field.defaultValue && initialData[field.slug] === undefined) {
|
||||
// Parse default value based on field type
|
||||
switch (field.fieldType) {
|
||||
case 'CHECKBOX':
|
||||
initialData[field.slug] = field.defaultValue === 'true';
|
||||
break;
|
||||
case 'NUMBER':
|
||||
case 'RANGE':
|
||||
initialData[field.slug] = parseFloat(field.defaultValue) || 0;
|
||||
break;
|
||||
case 'CHECKBOX_GROUP':
|
||||
case 'MULTI_SELECT':
|
||||
case 'TAG_INPUT':
|
||||
try {
|
||||
initialData[field.slug] = JSON.parse(field.defaultValue);
|
||||
} catch {
|
||||
initialData[field.slug] = [];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
initialData[field.slug] = field.defaultValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
setFormData(initialData);
|
||||
|
||||
} catch (err: unknown) {
|
||||
console.error('Failed to fetch data:', err);
|
||||
|
||||
// Check for specific error types
|
||||
const error = err as { response?: { status?: number } };
|
||||
if (error.response?.status === 401) {
|
||||
setError('You are not logged in or your session has expired. Please log in again.');
|
||||
} else if (error.response?.status === 404) {
|
||||
setError('Agent profile not found. Please complete your registration first.');
|
||||
} else {
|
||||
setError('Failed to load profile. Please try again.');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const handleCancel = () => {
|
||||
router.push('/agent/dashboard');
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
// In production, this would save to API
|
||||
console.log('Saving form data:', formData);
|
||||
router.push('/agent/dashboard');
|
||||
};
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
setIsSaving(true);
|
||||
setFormErrors({});
|
||||
|
||||
const updateField = (field: string, value: any) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
// Validate required fields
|
||||
const errors: Record<string, string> = {};
|
||||
sections.forEach(section => {
|
||||
section.fields?.forEach(field => {
|
||||
if (field.isRequired && field.isActive && !field.isSearchableOnly) {
|
||||
const value = formData[field.slug];
|
||||
const isEmpty =
|
||||
value === undefined ||
|
||||
value === null ||
|
||||
value === '' ||
|
||||
(Array.isArray(value) && value.length === 0);
|
||||
|
||||
const handleAddLicensedArea = (value: string) => {
|
||||
const trimmedValue = value.trim();
|
||||
if (trimmedValue && !formData.licensedAreas.includes(trimmedValue)) {
|
||||
updateField('licensedAreas', [...formData.licensedAreas, trimmedValue]);
|
||||
}
|
||||
setStateInput('');
|
||||
};
|
||||
if (isEmpty) {
|
||||
errors[field.slug] = `${field.name} is required`;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const handleStateInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter' || e.key === ',') {
|
||||
e.preventDefault();
|
||||
handleAddLicensedArea(stateInput);
|
||||
if (Object.keys(errors).length > 0) {
|
||||
setFormErrors(errors);
|
||||
// Scroll to first error
|
||||
const firstErrorField = Object.keys(errors)[0];
|
||||
const errorElement = document.querySelector(`[data-field-slug="${firstErrorField}"]`);
|
||||
errorElement?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Call API to save form data
|
||||
console.log('Saving form data:', formData);
|
||||
|
||||
// Simulate save
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
router.push('/agent/dashboard');
|
||||
} catch (err) {
|
||||
console.error('Failed to save:', err);
|
||||
setError('Failed to save profile. Please try again.');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddOfficeHours = (value: string) => {
|
||||
const trimmedValue = value.trim();
|
||||
if (trimmedValue && !formData.availability.officeHours.includes(trimmedValue)) {
|
||||
updateField('availability', {
|
||||
...formData.availability,
|
||||
officeHours: [...formData.availability.officeHours, trimmedValue],
|
||||
const handleFieldChange = (fieldSlug: string, value: unknown) => {
|
||||
setFormData(prev => ({ ...prev, [fieldSlug]: value }));
|
||||
// Clear error when field is modified
|
||||
if (formErrors[fieldSlug]) {
|
||||
setFormErrors(prev => {
|
||||
const newErrors = { ...prev };
|
||||
delete newErrors[fieldSlug];
|
||||
return newErrors;
|
||||
});
|
||||
}
|
||||
setOfficeHoursInput('');
|
||||
};
|
||||
|
||||
const handleOfficeHoursKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter' || e.key === ',') {
|
||||
e.preventDefault();
|
||||
handleAddOfficeHours(officeHoursInput);
|
||||
}
|
||||
};
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-[calc(100vh-180px)]">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#E58625]"></div>
|
||||
<p className="text-[14px] font-serif text-[#00293D]/70">Loading profile sections...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleRemoveOfficeHours = (hour: string) => {
|
||||
updateField('availability', {
|
||||
...formData.availability,
|
||||
officeHours: formData.availability.officeHours.filter((h) => h !== hour),
|
||||
});
|
||||
};
|
||||
if (error && sections.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-[calc(100vh-180px)]">
|
||||
<div className="bg-white rounded-[20px] p-8 shadow-[0px_10px_20px_rgba(217,217,217,0.5)] max-w-md text-center">
|
||||
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg className="w-8 h-8 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-[18px] font-bold font-serif text-[#00293D] mb-2">Unable to Load Profile</h2>
|
||||
<p className="text-[14px] font-serif text-[#00293D]/70 mb-4">{error}</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="px-6 py-2 bg-[#E58625] rounded-full text-[14px] font-semibold font-serif text-white hover:bg-[#E58625]/90 transition-colors"
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex gap-6 h-[calc(100vh-180px)]">
|
||||
{/* Left Sidebar - Quick Links */}
|
||||
<div className="w-[220px] flex-shrink-0 hidden lg:block h-full overflow-y-auto scrollbar-thin">
|
||||
<QuickLinks scrollContainerRef={scrollContainerRef} />
|
||||
<QuickLinks scrollContainerRef={scrollContainerRef} sections={sections} />
|
||||
</div>
|
||||
|
||||
{/* Main Content - Scrollable */}
|
||||
<div ref={scrollContainerRef} className="flex-1 space-y-6 overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent">
|
||||
{/* Basic Information */}
|
||||
<div className="bg-white rounded-[15px] border border-[#00293D]/20 shadow-[0px_10px_20px_#D9D9D9] p-6">
|
||||
<FormSection id="basic-info" icon="/assets/icons/basic-information-icon.svg" title="Basic Information">
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
<FormInput
|
||||
label="First Name"
|
||||
value={formData.firstName}
|
||||
onChange={(value) => updateField('firstName', value)}
|
||||
placeholder="Enter first name"
|
||||
/>
|
||||
<FormInput
|
||||
label="Last Name"
|
||||
value={formData.lastName}
|
||||
onChange={(value) => updateField('lastName', value)}
|
||||
placeholder="Enter last name"
|
||||
/>
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent"
|
||||
>
|
||||
{/* Inner container with padding for shadow visibility */}
|
||||
<div className="space-y-6 p-2">
|
||||
{/* Error Banner */}
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 rounded-[15px] p-4 flex items-center gap-3">
|
||||
<svg className="w-5 h-5 text-red-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<p className="text-[14px] font-serif text-red-700">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dynamic Sections */}
|
||||
{sections.map((section) => (
|
||||
<DynamicSection
|
||||
key={section.id}
|
||||
section={section}
|
||||
formData={formData}
|
||||
onChange={handleFieldChange}
|
||||
errors={formErrors}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Empty State */}
|
||||
{sections.length === 0 && !loading && (
|
||||
<div className="bg-white rounded-[20px] p-8 shadow-[0px_10px_20px_rgba(217,217,217,0.5)] text-center">
|
||||
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg className="w-8 h-8 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
|
||||
{/* Contact Information */}
|
||||
<div className="bg-white rounded-[15px] border border-[#00293D]/20 shadow-[0px_10px_20px_#D9D9D9] p-6">
|
||||
<FormSection id="contact-info" icon="/assets/icons/contact-icon.svg" title="Contact Information">
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
<FormInput
|
||||
label="Email Address"
|
||||
value={formData.email}
|
||||
onChange={(value) => updateField('email', value)}
|
||||
placeholder="Enter email"
|
||||
type="email"
|
||||
/>
|
||||
<FormInput
|
||||
label="Phone Number"
|
||||
value={formData.phone}
|
||||
onChange={(value) => updateField('phone', value)}
|
||||
placeholder="Enter phone"
|
||||
type="tel"
|
||||
/>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
|
||||
{/* Professional Types */}
|
||||
<div className="bg-white rounded-[15px] border border-[#00293D]/20 shadow-[0px_10px_20px_#D9D9D9] p-6">
|
||||
<FormSection id="professional-types" icon="/assets/icons/professional-icon.svg" title="Professional Types">
|
||||
<div className="flex flex-wrap gap-6">
|
||||
<FormCheckbox
|
||||
label="First Time"
|
||||
checked={formData.professionalTypes.firstTime}
|
||||
onChange={(checked) =>
|
||||
updateField('professionalTypes', { ...formData.professionalTypes, firstTime: checked })
|
||||
}
|
||||
/>
|
||||
<FormCheckbox
|
||||
label="Solo"
|
||||
checked={formData.professionalTypes.solo}
|
||||
onChange={(checked) =>
|
||||
updateField('professionalTypes', { ...formData.professionalTypes, solo: checked })
|
||||
}
|
||||
/>
|
||||
<FormCheckbox
|
||||
label="Team"
|
||||
checked={formData.professionalTypes.team}
|
||||
onChange={(checked) =>
|
||||
updateField('professionalTypes', { ...formData.professionalTypes, team: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
|
||||
{/* Upload Documents */}
|
||||
<div className="bg-white rounded-[15px] border border-[#00293D]/20 shadow-[0px_10px_20px_#D9D9D9] p-6">
|
||||
<FormSection id="upload-documents" icon="/assets/icons/upload-icon.svg" title="Upload Documents">
|
||||
<FileUpload onFileSelect={(files) => console.log('Files selected:', files)} />
|
||||
|
||||
{/* Attached Documents */}
|
||||
{formData.attachedDocuments.length > 0 && (
|
||||
<div className="mt-6">
|
||||
<h4 className="text-[14px] font-semibold text-[#00293D] font-fractul mb-3 flex items-center gap-2">
|
||||
<Image src="/assets/icons/attachment-icon.svg" alt="Attached" width={16} height={16} />
|
||||
Attached Documents
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{formData.attachedDocuments.map((doc, idx) => (
|
||||
<AttachedFile
|
||||
key={idx}
|
||||
fileName={doc}
|
||||
onRemove={() => {
|
||||
const newDocs = [...formData.attachedDocuments];
|
||||
newDocs.splice(idx, 1);
|
||||
updateField('attachedDocuments', newDocs);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</FormSection>
|
||||
</div>
|
||||
|
||||
{/* Licensing & Areas */}
|
||||
<div className="bg-white rounded-[15px] border border-[#00293D]/20 shadow-[0px_10px_20px_#D9D9D9] p-6">
|
||||
<FormSection id="licensing-areas" icon="/assets/icons/professional-icon.svg" title="Licensing & Areas">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-[14px] font-semibold text-[#00293D] font-serif mb-2">
|
||||
What state(s) are you licensed in?
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="absolute left-4 top-1/2 -translate-y-1/2">
|
||||
<Image
|
||||
src="/assets/icons/location-icon.svg"
|
||||
alt="Location"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={stateInput}
|
||||
onChange={(e) => setStateInput(e.target.value)}
|
||||
onKeyDown={handleStateInputKeyDown}
|
||||
placeholder="Enter state"
|
||||
className="w-full h-[40px] pl-10 pr-4 border border-[#00293D]/20 rounded-[15px] text-[14px] font-serif text-[#00293D] placeholder:text-[#00293D]/40 focus:outline-none focus:border-[#E58625]"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[12px] text-[#00293D]/60 font-serif mt-2">
|
||||
Press Enter or comma to add a state.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{formData.licensedAreas.map((area) => (
|
||||
<span
|
||||
key={area}
|
||||
className="inline-flex items-center gap-2 px-3 h-[32px] rounded-[15px] text-[14px] font-medium font-serif border border-[#00293d] text-[#00293d]"
|
||||
>
|
||||
{area}
|
||||
<button
|
||||
onClick={() => {
|
||||
const newAreas = formData.licensedAreas.filter((a) => a !== area);
|
||||
updateField('licensedAreas', newAreas);
|
||||
}}
|
||||
className="text-[#00293d] hover:text-[#E58625] transition-colors"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
|
||||
{/* Biography */}
|
||||
<div className="bg-white rounded-[15px] border border-[#00293D]/20 shadow-[0px_10px_20px_#D9D9D9] p-6">
|
||||
<FormSection id="biography" icon="/assets/icons/bio-icon.svg" title="Biography">
|
||||
<div className="space-y-4">
|
||||
<FormTextarea
|
||||
value={formData.biography}
|
||||
onChange={(value) => updateField('biography', value)}
|
||||
placeholder="Write your biography..."
|
||||
rows={5}
|
||||
maxLength={500}
|
||||
/>
|
||||
<FormTextarea
|
||||
label="Write a few sentences that explain yourself and also a good memorable thought."
|
||||
value={formData.testimonialHighlight}
|
||||
onChange={(value) => updateField('testimonialHighlight', value)}
|
||||
placeholder="Write your memorable experience..."
|
||||
rows={3}
|
||||
maxLength={250}
|
||||
/>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
|
||||
{/* Expertise */}
|
||||
<div className="bg-white rounded-[15px] border border-[#00293D]/20 shadow-[0px_10px_20px_#D9D9D9] p-6">
|
||||
<FormSection id="expertise" icon="/assets/icons/professional-icon.svg" title="Expertise">
|
||||
<TagInput
|
||||
tags={formData.expertise}
|
||||
onChange={(tags) => updateField('expertise', tags)}
|
||||
placeholder="Add expertise..."
|
||||
/>
|
||||
</FormSection>
|
||||
</div>
|
||||
|
||||
{/* Years In Business */}
|
||||
<div className="bg-white rounded-[15px] border border-[#00293D]/20 shadow-[0px_10px_20px_#D9D9D9] p-6">
|
||||
<FormSection id="years-business" icon="/assets/icons/professional-icon.svg" title="Years In Business">
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
<FormInput
|
||||
label="Total Years"
|
||||
value={formData.yearsInBusiness.totalYears}
|
||||
onChange={(value) =>
|
||||
updateField('yearsInBusiness', { ...formData.yearsInBusiness, totalYears: value })
|
||||
}
|
||||
/>
|
||||
<FormInput
|
||||
label="Total Transactions"
|
||||
value={formData.yearsInBusiness.totalTransactions}
|
||||
onChange={(value) =>
|
||||
updateField('yearsInBusiness', { ...formData.yearsInBusiness, totalTransactions: value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
|
||||
{/* Areas in expertise & Years */}
|
||||
<div className="bg-white rounded-[15px] border border-[#00293D]/20 shadow-[0px_10px_20px_#D9D9D9] p-6">
|
||||
<FormSection
|
||||
id="areas-expertise-years"
|
||||
icon="/assets/icons/basic-information-icon.svg"
|
||||
title="Areas in expertise & Years"
|
||||
showEdit
|
||||
onEdit={() => console.log('Edit areas')}
|
||||
>
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
<FormSelect
|
||||
label="Select State"
|
||||
value={formData.state}
|
||||
onChange={(value) => updateField('state', value)}
|
||||
options={[
|
||||
{ value: 'Colorado', label: 'Colorado' },
|
||||
{ value: 'Texas', label: 'Texas' },
|
||||
]}
|
||||
/>
|
||||
<FormSelect
|
||||
label="Select Years"
|
||||
value="10"
|
||||
onChange={() => {}}
|
||||
options={[
|
||||
{ value: '5', label: '5 Years' },
|
||||
{ value: '10', label: '10 Years' },
|
||||
{ value: '15', label: '15 Years' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
|
||||
{/* Contracts Closed */}
|
||||
<div className="bg-white rounded-[15px] border border-[#00293D]/20 shadow-[0px_10px_20px_#D9D9D9] p-6">
|
||||
<FormSection id="contracts-closed" icon="/assets/icons/professional-icon.svg" title="Contracts Closed">
|
||||
<div className="space-y-3">
|
||||
<FormCheckbox
|
||||
label="< 10"
|
||||
checked={formData.contractsClosed.lessThan10}
|
||||
onChange={(checked) =>
|
||||
updateField('contractsClosed', { ...formData.contractsClosed, lessThan10: checked })
|
||||
}
|
||||
/>
|
||||
<FormCheckbox
|
||||
label="10 to 25"
|
||||
checked={formData.contractsClosed.between10And25}
|
||||
onChange={(checked) =>
|
||||
updateField('contractsClosed', { ...formData.contractsClosed, between10And25: checked })
|
||||
}
|
||||
/>
|
||||
<FormCheckbox
|
||||
label="26 to 50"
|
||||
checked={formData.contractsClosed.between26And50}
|
||||
onChange={(checked) =>
|
||||
updateField('contractsClosed', { ...formData.contractsClosed, between26And50: checked })
|
||||
}
|
||||
/>
|
||||
<FormCheckbox
|
||||
label="51 to 75"
|
||||
checked={formData.contractsClosed.between51And75}
|
||||
onChange={(checked) =>
|
||||
updateField('contractsClosed', { ...formData.contractsClosed, between51And75: checked })
|
||||
}
|
||||
/>
|
||||
<FormCheckbox
|
||||
label="75+"
|
||||
checked={formData.contractsClosed.moreThan75}
|
||||
onChange={(checked) =>
|
||||
updateField('contractsClosed', { ...formData.contractsClosed, moreThan75: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
|
||||
{/* Price Point */}
|
||||
<div className="bg-white rounded-[15px] border border-[#00293D]/20 shadow-[0px_10px_20px_#D9D9D9] p-6">
|
||||
<FormSection id="price-point" icon="/assets/icons/basic-information-icon.svg" title="Price Point">
|
||||
<div className="space-y-3">
|
||||
<FormCheckbox
|
||||
label="Under $100K"
|
||||
checked={formData.pricePoint.under100k}
|
||||
onChange={(checked) =>
|
||||
updateField('pricePoint', { ...formData.pricePoint, under100k: checked })
|
||||
}
|
||||
/>
|
||||
<FormCheckbox
|
||||
label="$100K – $200K"
|
||||
checked={formData.pricePoint.between100kAnd200k}
|
||||
onChange={(checked) =>
|
||||
updateField('pricePoint', { ...formData.pricePoint, between100kAnd200k: checked })
|
||||
}
|
||||
/>
|
||||
<FormCheckbox
|
||||
label="$200K – $400K"
|
||||
checked={formData.pricePoint.between200kAnd400k}
|
||||
onChange={(checked) =>
|
||||
updateField('pricePoint', { ...formData.pricePoint, between200kAnd400k: checked })
|
||||
}
|
||||
/>
|
||||
<FormCheckbox
|
||||
label="$400K – $600K"
|
||||
checked={formData.pricePoint.between400kAnd600k}
|
||||
onChange={(checked) =>
|
||||
updateField('pricePoint', { ...formData.pricePoint, between400kAnd600k: checked })
|
||||
}
|
||||
/>
|
||||
<FormCheckbox
|
||||
label="$600K – $800K"
|
||||
checked={formData.pricePoint.between600kAnd800k}
|
||||
onChange={(checked) =>
|
||||
updateField('pricePoint', { ...formData.pricePoint, between600kAnd800k: checked })
|
||||
}
|
||||
/>
|
||||
<FormCheckbox
|
||||
label="$800K – $1M"
|
||||
checked={formData.pricePoint.between800kAnd1m}
|
||||
onChange={(checked) =>
|
||||
updateField('pricePoint', { ...formData.pricePoint, between800kAnd1m: checked })
|
||||
}
|
||||
/>
|
||||
<FormCheckbox
|
||||
label="$1M and above"
|
||||
checked={formData.pricePoint.above1m}
|
||||
onChange={(checked) =>
|
||||
updateField('pricePoint', { ...formData.pricePoint, above1m: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
|
||||
{/* Specialization */}
|
||||
<div className="bg-white rounded-[15px] border border-[#00293D]/20 shadow-[0px_10px_20px_#D9D9D9] p-6">
|
||||
<FormSection id="specialization" icon="/assets/icons/basic-information-icon.svg" title="Specialization">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{/* Client Localization */}
|
||||
<div>
|
||||
<h4 className="text-[14px] font-semibold text-[#00293D] font-fractul mb-3">Client Localization</h4>
|
||||
<div className="space-y-2">
|
||||
<FormCheckbox label="Local" checked={formData.specialization.clientLocalization.local} onChange={() => {}} />
|
||||
<FormCheckbox label="National" checked={formData.specialization.clientLocalization.national} onChange={() => {}} />
|
||||
<FormCheckbox label="International" checked={formData.specialization.clientLocalization.international} onChange={() => {}} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Client Type Worked With */}
|
||||
<div>
|
||||
<h4 className="text-[14px] font-semibold text-[#00293D] font-fractul mb-3">Client Type Worked With</h4>
|
||||
<div className="space-y-2">
|
||||
<FormCheckbox label="First Time Buyer" checked={formData.specialization.clientTypeWorkedWith.firstTimeBuyer} onChange={() => {}} />
|
||||
<FormCheckbox label="First Time Seller" checked={formData.specialization.clientTypeWorkedWith.firstTimeSeller} onChange={() => {}} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Property Type */}
|
||||
<div>
|
||||
<h4 className="text-[14px] font-semibold text-[#00293D] font-fractul mb-3">Property Type</h4>
|
||||
<div className="space-y-2">
|
||||
<FormCheckbox label="SFR" checked={formData.specialization.propertyType.sfr} onChange={() => {}} />
|
||||
<FormCheckbox label="Condo" checked={formData.specialization.propertyType.condo} onChange={() => {}} />
|
||||
<FormCheckbox label="Luxury" checked={formData.specialization.propertyType.luxury} onChange={() => {}} />
|
||||
<FormCheckbox label="Townhome" checked={formData.specialization.propertyType.townhome} onChange={() => {}} />
|
||||
<FormCheckbox label="Commercial" checked={formData.specialization.propertyType.commercial} onChange={() => {}} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Transaction Type */}
|
||||
<div>
|
||||
<h4 className="text-[14px] font-semibold text-[#00293D] font-fractul mb-3">Transaction Type</h4>
|
||||
<div className="space-y-2">
|
||||
<FormCheckbox label="Relocation" checked={formData.specialization.transactionType.relocation} onChange={() => {}} />
|
||||
<FormCheckbox label="Probate" checked={formData.specialization.transactionType.probate} onChange={() => {}} />
|
||||
<FormCheckbox label="Tax Lien" checked={formData.specialization.transactionType.taxLien} onChange={() => {}} />
|
||||
<FormCheckbox label="Traditional" checked={formData.specialization.transactionType.traditional} onChange={() => {}} />
|
||||
<FormCheckbox label="New Construction" checked={formData.specialization.transactionType.newConstruction} onChange={() => {}} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Loan Type */}
|
||||
<div>
|
||||
<h4 className="text-[14px] font-semibold text-[#00293D] font-fractul mb-3">Loan Type</h4>
|
||||
<div className="space-y-2">
|
||||
<FormCheckbox label="Conventional" checked={formData.specialization.loanType.conventional} onChange={() => {}} />
|
||||
<FormCheckbox label="FHA" checked={formData.specialization.loanType.fha} onChange={() => {}} />
|
||||
<FormCheckbox label="VA" checked={formData.specialization.loanType.va} onChange={() => {}} />
|
||||
<FormCheckbox label="USDA" checked={formData.specialization.loanType.usda} onChange={() => {}} />
|
||||
<FormCheckbox label="Reverse" checked={formData.specialization.loanType.reverse} onChange={() => {}} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lifestyle Specialties */}
|
||||
<div>
|
||||
<h4 className="text-[14px] font-semibold text-[#00293D] font-fractul mb-3">Lifestyle Specialties</h4>
|
||||
<div className="space-y-2">
|
||||
<FormCheckbox label="Boating" checked={formData.specialization.lifestyleSpecialties.boating} onChange={() => {}} />
|
||||
<FormCheckbox label="Horses" checked={formData.specialization.lifestyleSpecialties.horses} onChange={() => {}} />
|
||||
<FormCheckbox label="RV" checked={formData.specialization.lifestyleSpecialties.rv} onChange={() => {}} />
|
||||
<FormCheckbox label="Automotive" checked={formData.specialization.lifestyleSpecialties.automotive} onChange={() => {}} />
|
||||
<FormCheckbox label="Aviation" checked={formData.specialization.lifestyleSpecialties.aviation} onChange={() => {}} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
|
||||
{/* Special Interests & Hobbies */}
|
||||
<div className="bg-white rounded-[15px] border border-[#00293D]/20 shadow-[0px_10px_20px_#D9D9D9] p-6">
|
||||
<FormSection id="hobbies" icon="/assets/icons/hobbies-icon.svg" title="Special Interests & Hobbies">
|
||||
<TagInput
|
||||
tags={formData.hobbies}
|
||||
onChange={(tags) => updateField('hobbies', tags)}
|
||||
placeholder="Add hobby..."
|
||||
/>
|
||||
</FormSection>
|
||||
</div>
|
||||
|
||||
{/* Certifications */}
|
||||
<div className="bg-white rounded-[15px] border border-[#00293D]/20 shadow-[0px_10px_20px_#D9D9D9] p-6">
|
||||
<FormSection id="certifications" icon="/assets/icons/certification-icon.svg" title="Professional Info">
|
||||
<p className="text-[12px] font-medium text-[#00293D]/70 font-serif mb-3">Certifications / Designations</p>
|
||||
<div className="space-y-2">
|
||||
{formData.certifications.map((cert, idx) => (
|
||||
<div key={idx} className="p-3 bg-gray-50 rounded-[10px]">
|
||||
<p className="text-[14px] font-semibold text-[#00293D] font-serif">{cert.name}</p>
|
||||
<p className="text-[12px] text-[#00293D]/70 font-serif">{cert.org}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
|
||||
{/* Availability */}
|
||||
<div className="bg-white rounded-[15px] border border-[#00293D]/20 shadow-[0px_10px_20px_#D9D9D9] p-6">
|
||||
<FormSection id="availability" icon="/assets/icons/availability-clock-icon.svg" title="Availability">
|
||||
<div className="flex flex-col lg:flex-row gap-6">
|
||||
{/* Typical Office Hours */}
|
||||
<div className="flex-1">
|
||||
<label className="block text-[14px] font-semibold text-[#00293D] font-serif mb-2">
|
||||
Typical Office Hours
|
||||
</label>
|
||||
<div className="flex items-center gap-2 h-[40px] px-4 border border-[#00293D]/20 rounded-[15px]">
|
||||
{formData.availability.officeHours.map((hour) => (
|
||||
<span
|
||||
key={hour}
|
||||
className="inline-flex items-center gap-1 text-[14px] font-semibold font-serif text-[#00293D]"
|
||||
>
|
||||
{hour}
|
||||
<button
|
||||
onClick={() => handleRemoveOfficeHours(hour)}
|
||||
className="text-[#00293d]/50 hover:text-[#E58625] transition-colors ml-1"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
<input
|
||||
type="text"
|
||||
value={officeHoursInput}
|
||||
onChange={(e) => setOfficeHoursInput(e.target.value)}
|
||||
onKeyDown={handleOfficeHoursKeyDown}
|
||||
placeholder={formData.availability.officeHours.length === 0 ? "Add office hours" : ""}
|
||||
className="flex-1 h-full text-[14px] font-serif text-[#00293D] placeholder:text-[#00293D]/40 focus:outline-none bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preferred Contact Method */}
|
||||
<div className="lg:w-[250px]">
|
||||
<label className="block text-[14px] font-semibold text-[#00293D] font-serif mb-2">
|
||||
Preferred Contact Method
|
||||
</label>
|
||||
<div className="flex items-center gap-6 h-[40px]">
|
||||
<FormCheckbox
|
||||
label="Email"
|
||||
checked={formData.availability.contactMethod.email}
|
||||
onChange={(checked) =>
|
||||
updateField('availability', {
|
||||
...formData.availability,
|
||||
contactMethod: { ...formData.availability.contactMethod, email: checked },
|
||||
})
|
||||
}
|
||||
/>
|
||||
<FormCheckbox
|
||||
label="Call"
|
||||
checked={formData.availability.contactMethod.call}
|
||||
onChange={(checked) =>
|
||||
updateField('availability', {
|
||||
...formData.availability,
|
||||
contactMethod: { ...formData.availability.contactMethod, call: checked },
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
<h2 className="text-[18px] font-bold font-serif text-[#00293D] mb-2">No Profile Sections Available</h2>
|
||||
<p className="text-[14px] font-serif text-[#00293D]/70">
|
||||
Profile sections have not been configured for your agent type yet.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex justify-end gap-4 pb-6">
|
||||
<button
|
||||
onClick={handleCancel}
|
||||
className="px-8 py-3 border border-[#00293D]/20 rounded-full text-[14px] font-semibold font-serif text-[#00293D] hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="px-8 py-3 bg-[#E58625] rounded-full text-[14px] font-semibold font-serif text-white hover:bg-[#E58625]/90 transition-colors"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
{sections.length > 0 && (
|
||||
<div className="flex justify-end gap-4 pb-6">
|
||||
<button
|
||||
onClick={handleCancel}
|
||||
disabled={isSaving}
|
||||
className="px-8 py-3 border border-[#00293D]/20 rounded-full text-[14px] font-semibold font-serif text-[#00293D] hover:bg-gray-50 transition-colors disabled:opacity-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
className="px-8 py-3 bg-[#E58625] rounded-full text-[14px] font-semibold font-serif text-white hover:bg-[#E58625]/90 transition-colors disabled:opacity-50 flex items-center gap-2"
|
||||
>
|
||||
{isSaving && (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
)}
|
||||
{isSaving ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user