diff --git a/src/app/(agent)/agent/edit/components/DynamicField.tsx b/src/app/(agent)/agent/edit/components/DynamicField.tsx new file mode 100644 index 0000000..e028a9b --- /dev/null +++ b/src/app/(agent)/agent/edit/components/DynamicField.tsx @@ -0,0 +1,345 @@ +'use client'; + +import React from 'react'; +import { ProfileField } from '@/services/profile-sections.service'; +import { FormInput } from './FormInput'; +import { FormCheckbox } from './FormCheckbox'; +import { FormTextarea } from './FormTextarea'; +import { FormSelect } from './FormSelect'; +import { TagInput } from './TagInput'; + +interface DynamicFieldProps { + field: ProfileField; + value: unknown; + onChange: (fieldSlug: string, value: unknown) => void; + error?: string; +} + +export default function DynamicField({ field, value, onChange, error }: DynamicFieldProps) { + const handleChange = (newValue: unknown) => { + onChange(field.slug, newValue); + }; + + const renderField = () => { + switch (field.fieldType) { + case 'TEXT': + return ( + handleChange(val)} + required={field.isRequired} + error={error} + /> + ); + + case 'TEXTAREA': + return ( + handleChange(val)} + required={field.isRequired} + rows={4} + /> + ); + + case 'NUMBER': + return ( + handleChange(val)} + required={field.isRequired} + error={error} + min={field.validation?.min} + max={field.validation?.max} + /> + ); + + case 'DATE': + return ( + handleChange(val)} + required={field.isRequired} + error={error} + /> + ); + + case 'CHECKBOX': + return ( + handleChange(checked)} + /> + ); + + case 'CHECKBOX_GROUP': + return ( + + ); + + case 'RADIO': + return ( + + ); + + case 'SELECT': + return ( + handleChange(val)} + options={field.options?.map(opt => ({ value: opt.value, label: opt.label })) || []} + placeholder={field.placeholder || `Select ${field.name.toLowerCase()}`} + /> + ); + + case 'MULTI_SELECT': + return ( + + ); + + case 'RANGE': + return ( + + ); + + case 'TAG_INPUT': + return ( +
+ + handleChange(tags)} + placeholder={field.placeholder || `Add ${field.name.toLowerCase()}`} + /> +
+ ); + + default: + return ( + handleChange(val)} + required={field.isRequired} + error={error} + /> + ); + } + }; + + return ( +
+ {renderField()} +
+ ); +} + +// Checkbox Group Component +interface CheckboxGroupProps { + field: ProfileField; + value: string[]; + onChange: (value: string[]) => void; +} + +function CheckboxGroup({ field, value, onChange }: CheckboxGroupProps) { + const handleToggle = (optionValue: string) => { + const newValue = value.includes(optionValue) + ? value.filter(v => v !== optionValue) + : [...value, optionValue]; + onChange(newValue); + }; + + const columns = field.uiConfig?.columns || 2; + + return ( +
+ +
+ {field.options?.map((option) => ( + + ))} +
+
+ ); +} + +// Radio Group Component +interface RadioGroupProps { + field: ProfileField; + value: string; + onChange: (value: string) => void; +} + +function RadioGroup({ field, value, onChange }: RadioGroupProps) { + return ( +
+ +
+ {field.options?.map((option) => ( + + ))} +
+
+ ); +} + +// Multi-Select Component +interface MultiSelectProps { + field: ProfileField; + value: string[]; + onChange: (value: string[]) => void; +} + +function MultiSelect({ field, value, onChange }: MultiSelectProps) { + const handleToggle = (optionValue: string) => { + const newValue = value.includes(optionValue) + ? value.filter(v => v !== optionValue) + : [...value, optionValue]; + onChange(newValue); + }; + + return ( +
+ +
+ {field.options?.map((option) => ( + + ))} +
+ {value.length > 0 && ( +
+ {value.map((v) => { + const option = field.options?.find(o => o.value === v); + return ( + + {option?.label || v} + + + ); + })} +
+ )} +
+ ); +} + +// Range Slider Component +interface RangeSliderProps { + field: ProfileField; + value: number; + onChange: (value: number) => void; +} + +function RangeSlider({ field, value, onChange }: RangeSliderProps) { + const min = field.rangeConfig?.min || 0; + const max = field.rangeConfig?.max || 100; + const step = field.rangeConfig?.step || 1; + const unit = field.rangeConfig?.unit || ''; + + return ( +
+ +
+ onChange(Number(e.target.value))} + className="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-[#E58625]" + /> + + {value}{unit} + +
+
+ {min}{unit} + {max}{unit} +
+
+ ); +} diff --git a/src/app/(agent)/agent/edit/components/DynamicSection.tsx b/src/app/(agent)/agent/edit/components/DynamicSection.tsx new file mode 100644 index 0000000..6f954a4 --- /dev/null +++ b/src/app/(agent)/agent/edit/components/DynamicSection.tsx @@ -0,0 +1,140 @@ +'use client'; + +import React from 'react'; +import Image from 'next/image'; +import { ProfileSection } from '@/services/profile-sections.service'; +import DynamicField from './DynamicField'; + +// Map section slugs to icon paths +const sectionIcons: Record = { + 'basic-information': '/assets/icons/basic-information-icon.svg', + 'contact-information': '/assets/icons/email-orange-icon.svg', + 'location': '/assets/icons/location-orange-icon.svg', + 'professional-info': '/assets/icons/certification-icon.svg', + 'specialization': '/assets/icons/home-icon.svg', + 'experience': '/assets/icons/star-icon.svg', + 'availability': '/assets/icons/availability-clock-icon.svg', + 'biography': '/assets/icons/bio-icon.svg', + 'attached-documents': '/assets/icons/attachment-icon.svg', + 'hobbies': '/assets/icons/heart-icon.svg', + 'testimonials': '/assets/icons/quote-icon.svg', + 'certification': '/assets/icons/certification-icon.svg', +}; + +// Map icon names (from database) to icon paths +const iconNameToPath: Record = { + 'star': '/assets/icons/star-icon.svg', + 'home': '/assets/icons/home-icon.svg', + 'location': '/assets/icons/location-orange-icon.svg', + 'heart': '/assets/icons/heart-icon.svg', + 'clock': '/assets/icons/availability-clock-icon.svg', + 'user': '/assets/icons/basic-information-icon.svg', + 'email': '/assets/icons/email-orange-icon.svg', + 'phone': '/assets/icons/phone-orange-icon.svg', + 'document': '/assets/icons/attachment-icon.svg', + 'certificate': '/assets/icons/certification-icon.svg', + 'certification': '/assets/icons/certification-icon.svg', + 'quote': '/assets/icons/quote-icon.svg', + 'bio': '/assets/icons/bio-icon.svg', + 'professional': '/assets/icons/professional-icon.svg', + 'wallet': '/assets/icons/wallet-icon.svg', + 'chart': '/assets/icons/chart-icon.svg', + 'dollar': '/assets/icons/dollar-icon.svg', + 'calendar': '/assets/icons/calendar-icon.svg', +}; + +// Default icon for sections without a specific icon +const defaultIcon = '/assets/icons/professional-icon.svg'; + +// Helper to check if a string is an emoji +const isEmojiString = (str: string): boolean => { + // Emoji regex pattern - matches most common emojis + const emojiRegex = /^[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{1F600}-\u{1F64F}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]/u; + return emojiRegex.test(str); +}; + +interface DynamicSectionProps { + section: ProfileSection; + formData: Record; + onChange: (fieldSlug: string, value: unknown) => void; + errors?: Record; +} + +export default function DynamicSection({ section, formData, onChange, errors }: DynamicSectionProps) { + // Determine icon source + let iconPath: string = defaultIcon; + let isEmoji = false; + + if (section.icon) { + if (section.icon.startsWith('/') || section.icon.startsWith('http')) { + // It's a valid URL path + iconPath = section.icon; + } else if (isEmojiString(section.icon)) { + // It's an emoji + isEmoji = true; + } else if (iconNameToPath[section.icon.toLowerCase()]) { + // It's an icon name like "star", "home", etc. + iconPath = iconNameToPath[section.icon.toLowerCase()]; + } else { + // Fallback to slug mapping or default + iconPath = sectionIcons[section.slug] || defaultIcon; + } + } else { + // No icon set, use slug mapping or default + iconPath = sectionIcons[section.slug] || defaultIcon; + } + + // Sort fields by sortOrder + const sortedFields = [...(section.fields || [])].sort((a, b) => a.sortOrder - b.sortOrder); + const activeFields = sortedFields.filter(f => f.isActive && !f.isSearchableOnly); + + if (activeFields.length === 0) { + return null; + } + + return ( +
+ {/* Section Header */} +
+
+ {isEmoji ? ( + {section.icon} + ) : ( + {section.name} + )} +
+
+

+ {section.name} + {section.isRequired && *} +

+ {section.description && ( +

{section.description}

+ )} +
+
+ + {/* Section Fields */} +
+ {activeFields.map((field) => ( + + ))} +
+
+ ); +} diff --git a/src/app/(agent)/agent/edit/components/FormInput.tsx b/src/app/(agent)/agent/edit/components/FormInput.tsx index b4bcf64..128c14f 100644 --- a/src/app/(agent)/agent/edit/components/FormInput.tsx +++ b/src/app/(agent)/agent/edit/components/FormInput.tsx @@ -7,13 +7,18 @@ interface FormInputProps { placeholder?: string; type?: string; disabled?: boolean; + required?: boolean; + error?: string; + min?: number; + max?: number; } -export function FormInput({ label, value, onChange, placeholder, type = 'text', disabled }: FormInputProps) { +export function FormInput({ label, value, onChange, placeholder, type = 'text', disabled, required, error, min, max }: FormInputProps) { return (
onChange(e.target.value)} placeholder={placeholder} disabled={disabled} - className="w-full h-[40px] px-4 border border-[#00293D]/20 rounded-[15px] text-[14px] font-serif text-[#00293D] placeholder:text-[#00293D]/40 focus:outline-none focus:border-[#E58625] disabled:bg-gray-50 disabled:text-[#00293D]/50" + min={min} + max={max} + className={`w-full h-[40px] px-4 border rounded-[15px] text-[14px] font-serif text-[#00293D] placeholder:text-[#00293D]/40 focus:outline-none disabled:bg-gray-50 disabled:text-[#00293D]/50 ${ + error ? 'border-red-500 focus:border-red-500' : 'border-[#00293D]/20 focus:border-[#E58625]' + }`} /> + {error &&

{error}

}
); } diff --git a/src/app/(agent)/agent/edit/components/FormTextarea.tsx b/src/app/(agent)/agent/edit/components/FormTextarea.tsx index e335713..04932fb 100644 --- a/src/app/(agent)/agent/edit/components/FormTextarea.tsx +++ b/src/app/(agent)/agent/edit/components/FormTextarea.tsx @@ -7,14 +7,16 @@ interface FormTextareaProps { placeholder?: string; rows?: number; maxLength?: number; + required?: boolean; } -export function FormTextarea({ label, value, onChange, placeholder, rows = 4, maxLength }: FormTextareaProps) { +export function FormTextarea({ label, value, onChange, placeholder, rows = 4, maxLength, required }: FormTextareaProps) { return (
{label && ( )}