fix: Improve robustness and error handling for dynamic form fields and sections with safe data access and type coercion.

This commit is contained in:
pradeepkumar
2026-02-24 21:43:05 +05:30
parent 6d467cc8c0
commit c8f9045a38
4 changed files with 234 additions and 119 deletions

View File

@@ -29,6 +29,18 @@ export default function DynamicField({ field, value, onChange, error }: DynamicF
onChange(field.slug, newValue); onChange(field.slug, newValue);
}; };
// Safe accessors for field properties that might be null/undefined
const fieldName = field.name || 'Field';
const fieldSlug = field.slug || '';
const fieldPlaceholder = field.placeholder || '';
const safeOptions = Array.isArray(field.options) ? field.options : [];
// Safe value coercion helpers
const safeString = (v: unknown): string => (typeof v === 'string' ? v : '');
const safeStringArray = (v: unknown): string[] => (Array.isArray(v) ? v.filter((item): item is string => typeof item === 'string') : []);
const safeNumber = (v: unknown, fallback: number): number => (typeof v === 'number' ? v : fallback);
const safeFileArray = (v: unknown): UploadedFile[] => (Array.isArray(v) ? v : []);
const renderField = () => { const renderField = () => {
switch (field.fieldType) { switch (field.fieldType) {
case 'REPEATER': case 'REPEATER':
@@ -38,9 +50,9 @@ export default function DynamicField({ field, value, onChange, error }: DynamicF
case 'TEXT': case 'TEXT':
return ( return (
<FormInput <FormInput
label={field.name} label={fieldName}
placeholder={field.placeholder || `Enter ${field.name.toLowerCase()}`} placeholder={fieldPlaceholder || `Enter ${fieldName.toLowerCase()}`}
value={(value as string) || ''} value={safeString(value)}
onChange={(val: string) => handleChange(val)} onChange={(val: string) => handleChange(val)}
required={field.isRequired} required={field.isRequired}
error={error} error={error}
@@ -50,9 +62,9 @@ export default function DynamicField({ field, value, onChange, error }: DynamicF
case 'TEXTAREA': case 'TEXTAREA':
return ( return (
<FormTextarea <FormTextarea
label={field.name} label={fieldName}
placeholder={field.placeholder || `Enter ${field.name.toLowerCase()}`} placeholder={fieldPlaceholder || `Enter ${fieldName.toLowerCase()}`}
value={(value as string) || ''} value={safeString(value)}
onChange={(val: string) => handleChange(val)} onChange={(val: string) => handleChange(val)}
required={field.isRequired} required={field.isRequired}
rows={4} rows={4}
@@ -62,10 +74,10 @@ export default function DynamicField({ field, value, onChange, error }: DynamicF
case 'NUMBER': case 'NUMBER':
return ( return (
<FormInput <FormInput
label={field.name} label={fieldName}
type="number" type="number"
placeholder={field.placeholder || `Enter ${field.name.toLowerCase()}`} placeholder={fieldPlaceholder || `Enter ${fieldName.toLowerCase()}`}
value={(value as string) || ''} value={safeString(value)}
onChange={(val: string) => handleChange(val)} onChange={(val: string) => handleChange(val)}
required={field.isRequired} required={field.isRequired}
error={error} error={error}
@@ -77,9 +89,9 @@ export default function DynamicField({ field, value, onChange, error }: DynamicF
case 'DATE': case 'DATE':
return ( return (
<FormInput <FormInput
label={field.name} label={fieldName}
type="date" type="date"
value={(value as string) || ''} value={safeString(value)}
onChange={(val: string) => handleChange(val)} onChange={(val: string) => handleChange(val)}
required={field.isRequired} required={field.isRequired}
error={error} error={error}
@@ -89,7 +101,7 @@ export default function DynamicField({ field, value, onChange, error }: DynamicF
case 'CHECKBOX': case 'CHECKBOX':
return ( return (
<FormCheckbox <FormCheckbox
label={field.name} label={fieldName}
checked={Boolean(value)} checked={Boolean(value)}
onChange={(checked: boolean) => handleChange(checked)} onChange={(checked: boolean) => handleChange(checked)}
/> />
@@ -99,7 +111,7 @@ export default function DynamicField({ field, value, onChange, error }: DynamicF
return ( return (
<CheckboxGroup <CheckboxGroup
field={field} field={field}
value={(value as string[]) || []} value={safeStringArray(value)}
onChange={handleChange} onChange={handleChange}
/> />
); );
@@ -108,7 +120,7 @@ export default function DynamicField({ field, value, onChange, error }: DynamicF
return ( return (
<RadioGroup <RadioGroup
field={field} field={field}
value={(value as string) || ''} value={safeString(value)}
onChange={handleChange} onChange={handleChange}
/> />
); );
@@ -116,11 +128,11 @@ export default function DynamicField({ field, value, onChange, error }: DynamicF
case 'SELECT': case 'SELECT':
return ( return (
<FormSelect <FormSelect
label={field.name} label={fieldName}
value={(value as string) || ''} value={safeString(value)}
onChange={(val: string) => handleChange(val)} onChange={(val: string) => handleChange(val)}
options={field.options?.map(opt => ({ value: opt.value, label: opt.label })) || []} options={safeOptions.map(opt => ({ value: opt.value, label: opt.label }))}
placeholder={field.placeholder || `Select ${field.name.toLowerCase()}`} placeholder={fieldPlaceholder || `Select ${fieldName.toLowerCase()}`}
/> />
); );
@@ -128,7 +140,7 @@ export default function DynamicField({ field, value, onChange, error }: DynamicF
return ( return (
<MultiSelect <MultiSelect
field={field} field={field}
value={(value as string[]) || []} value={safeStringArray(value)}
onChange={handleChange} onChange={handleChange}
/> />
); );
@@ -137,7 +149,7 @@ export default function DynamicField({ field, value, onChange, error }: DynamicF
return ( return (
<RangeSlider <RangeSlider
field={field} field={field}
value={(value as number) || field.rangeConfig?.min || 0} value={safeNumber(value, field.rangeConfig?.min || 0)}
onChange={handleChange} onChange={handleChange}
/> />
); );
@@ -146,13 +158,13 @@ export default function DynamicField({ field, value, onChange, error }: DynamicF
return ( return (
<div className="mb-4"> <div className="mb-4">
<label className="block text-[14px] font-bold font-serif text-[#00293D] mb-2"> <label className="block text-[14px] font-bold font-serif text-[#00293D] mb-2">
{field.name} {fieldName}
{field.isRequired && <span className="text-red-500 ml-1">*</span>} {field.isRequired && <span className="text-red-500 ml-1">*</span>}
</label> </label>
<TagInput <TagInput
tags={(value as string[]) || []} tags={safeStringArray(value)}
onChange={(tags: string[]) => handleChange(tags)} onChange={(tags: string[]) => handleChange(tags)}
placeholder={field.placeholder || `Add ${field.name.toLowerCase()}`} placeholder={fieldPlaceholder || `Add ${fieldName.toLowerCase()}`}
/> />
</div> </div>
); );
@@ -162,8 +174,8 @@ export default function DynamicField({ field, value, onChange, error }: DynamicF
return ( return (
<FileUpload <FileUpload
field={field} field={field}
fieldSlug={field.slug} fieldSlug={fieldSlug}
value={(value as UploadedFile[]) || []} value={safeFileArray(value)}
onChange={handleChange} onChange={handleChange}
/> />
); );
@@ -171,9 +183,9 @@ export default function DynamicField({ field, value, onChange, error }: DynamicF
default: default:
return ( return (
<FormInput <FormInput
label={field.name} label={fieldName}
placeholder={field.placeholder || `Enter ${field.name.toLowerCase()}`} placeholder={fieldPlaceholder || `Enter ${fieldName.toLowerCase()}`}
value={(value as string) || ''} value={safeString(value)}
onChange={(val: string) => handleChange(val)} onChange={(val: string) => handleChange(val)}
required={field.isRequired} required={field.isRequired}
error={error} error={error}
@@ -182,6 +194,8 @@ export default function DynamicField({ field, value, onChange, error }: DynamicF
} }
}; };
// Wrap in try-catch to prevent individual field errors from crashing the entire page
try {
const fieldContent = renderField(); const fieldContent = renderField();
// Don't render anything for null fields (like REPEATER) // Don't render anything for null fields (like REPEATER)
@@ -190,10 +204,18 @@ export default function DynamicField({ field, value, onChange, error }: DynamicF
} }
return ( return (
<div className="dynamic-field" data-field-slug={field.slug}> <div className="dynamic-field" data-field-slug={fieldSlug}>
{fieldContent} {fieldContent}
</div> </div>
); );
} catch (err) {
console.error(`Error rendering field "${fieldName}" (${field.fieldType}):`, err);
return (
<div className="dynamic-field p-3 bg-red-50 border border-red-200 rounded-lg" data-field-slug={fieldSlug}>
<p className="text-sm text-red-600">Failed to render field: {fieldName}</p>
</div>
);
}
} }
// Checkbox Group Component // Checkbox Group Component
@@ -204,10 +226,13 @@ interface CheckboxGroupProps {
} }
function CheckboxGroup({ field, value, onChange }: CheckboxGroupProps) { function CheckboxGroup({ field, value, onChange }: CheckboxGroupProps) {
const safeValue = Array.isArray(value) ? value : [];
const safeOptions = Array.isArray(field.options) ? field.options : [];
const handleToggle = (optionValue: string) => { const handleToggle = (optionValue: string) => {
const newValue = value.includes(optionValue) const newValue = safeValue.includes(optionValue)
? value.filter(v => v !== optionValue) ? safeValue.filter(v => v !== optionValue)
: [...value, optionValue]; : [...safeValue, optionValue];
onChange(newValue); onChange(newValue);
}; };
@@ -216,15 +241,15 @@ function CheckboxGroup({ field, value, onChange }: CheckboxGroupProps) {
return ( return (
<div className="mb-4"> <div className="mb-4">
<label className="block text-[14px] font-bold font-serif text-[#00293D] mb-2"> <label className="block text-[14px] font-bold font-serif text-[#00293D] mb-2">
{field.name} {field.name || 'Field'}
{field.isRequired && <span className="text-red-500 ml-1">*</span>} {field.isRequired && <span className="text-red-500 ml-1">*</span>}
</label> </label>
<div <div
className="grid gap-2" className="grid gap-2"
style={{ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` }} style={{ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` }}
> >
{field.options?.map((option) => { {safeOptions.map((option) => {
const isChecked = value.includes(option.value); const isChecked = safeValue.includes(option.value);
return ( return (
<label key={option.value} className="flex items-center gap-2 cursor-pointer"> <label key={option.value} className="flex items-center gap-2 cursor-pointer">
<div className="relative"> <div className="relative">
@@ -263,14 +288,16 @@ interface RadioGroupProps {
} }
function RadioGroup({ field, value, onChange }: RadioGroupProps) { function RadioGroup({ field, value, onChange }: RadioGroupProps) {
const safeOptions = Array.isArray(field.options) ? field.options : [];
return ( return (
<div className="mb-4"> <div className="mb-4">
<label className="block text-[14px] font-bold font-serif text-[#00293D] mb-2"> <label className="block text-[14px] font-bold font-serif text-[#00293D] mb-2">
{field.name} {field.name || 'Field'}
{field.isRequired && <span className="text-red-500 ml-1">*</span>} {field.isRequired && <span className="text-red-500 ml-1">*</span>}
</label> </label>
<div className="flex flex-wrap gap-4"> <div className="flex flex-wrap gap-4">
{field.options?.map((option) => ( {safeOptions.map((option) => (
<label key={option.value} className="flex items-center gap-2 cursor-pointer"> <label key={option.value} className="flex items-center gap-2 cursor-pointer">
<input <input
type="radio" type="radio"
@@ -364,14 +391,17 @@ const multiSelectStyles: StylesConfig<SelectOption, true> = {
}; };
function MultiSelect({ field, value, onChange }: MultiSelectProps) { function MultiSelect({ field, value, onChange }: MultiSelectProps) {
const safeFieldOptions = Array.isArray(field.options) ? field.options : [];
const safeValue = Array.isArray(value) ? value : [];
// Convert field options to react-select format // Convert field options to react-select format
const options: SelectOption[] = field.options?.map(opt => ({ const options: SelectOption[] = safeFieldOptions.map(opt => ({
value: opt.value, value: opt.value,
label: opt.label, label: opt.label,
})) || []; }));
// Convert current value to react-select format // Convert current value to react-select format
const selectedOptions = options.filter(opt => value.includes(opt.value)); const selectedOptions = options.filter(opt => safeValue.includes(opt.value));
const handleChange = (newValue: MultiValue<SelectOption>) => { const handleChange = (newValue: MultiValue<SelectOption>) => {
onChange(newValue.map(opt => opt.value)); onChange(newValue.map(opt => opt.value));
@@ -380,7 +410,7 @@ function MultiSelect({ field, value, onChange }: MultiSelectProps) {
return ( return (
<div className="mb-4"> <div className="mb-4">
<label className="block text-[14px] font-bold font-serif text-[#00293D] mb-2"> <label className="block text-[14px] font-bold font-serif text-[#00293D] mb-2">
{field.name} {field.name || 'Field'}
{field.isRequired && <span className="text-red-500 ml-1">*</span>} {field.isRequired && <span className="text-red-500 ml-1">*</span>}
</label> </label>
<Select<SelectOption, true> <Select<SelectOption, true>
@@ -388,7 +418,7 @@ function MultiSelect({ field, value, onChange }: MultiSelectProps) {
options={options} options={options}
value={selectedOptions} value={selectedOptions}
onChange={handleChange} onChange={handleChange}
placeholder={field.placeholder || `Select ${field.name.toLowerCase()}...`} placeholder={field.placeholder || `Select ${(field.name || 'option').toLowerCase()}...`}
styles={multiSelectStyles} styles={multiSelectStyles}
classNamePrefix="react-select" classNamePrefix="react-select"
isClearable isClearable
@@ -408,15 +438,16 @@ interface RangeSliderProps {
} }
function RangeSlider({ field, value, onChange }: RangeSliderProps) { function RangeSlider({ field, value, onChange }: RangeSliderProps) {
const min = field.rangeConfig?.min || 0; const rangeConfig = field.rangeConfig || { min: 0, max: 100, step: 1 };
const max = field.rangeConfig?.max || 100; const min = rangeConfig.min ?? 0;
const step = field.rangeConfig?.step || 1; const max = rangeConfig.max ?? 100;
const unit = field.rangeConfig?.unit || ''; const step = rangeConfig.step ?? 1;
const unit = rangeConfig.unit ?? '';
return ( return (
<div className="mb-4"> <div className="mb-4">
<label className="block text-[14px] font-bold font-serif text-[#00293D] mb-2"> <label className="block text-[14px] font-bold font-serif text-[#00293D] mb-2">
{field.name} {field.name || 'Field'}
{field.isRequired && <span className="text-red-500 ml-1">*</span>} {field.isRequired && <span className="text-red-500 ml-1">*</span>}
</label> </label>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
@@ -482,8 +513,9 @@ function FileUpload({ field, fieldSlug, value, onChange }: FileUploadProps) {
const [loadingUrls, setLoadingUrls] = useState<Record<string, boolean>>({}); const [loadingUrls, setLoadingUrls] = useState<Record<string, boolean>>({});
// Get allowed formats from field config or use defaults // Get allowed formats from field config or use defaults
const allowedFormats = field.validation?.allowedFormats || ['PDF', 'DOCX', 'JPG', 'PNG']; const validation = field.validation && typeof field.validation === 'object' ? field.validation : {};
const maxFileSize = field.validation?.maxFileSize || 10; // MB const allowedFormats = Array.isArray(validation.allowedFormats) ? validation.allowedFormats : ['PDF', 'DOCX', 'JPG', 'PNG'];
const maxFileSize = typeof validation.maxFileSize === 'number' ? validation.maxFileSize : 10; // MB
const acceptString = allowedFormats.map(f => `.${f.toLowerCase()}`).join(','); const acceptString = allowedFormats.map(f => `.${f.toLowerCase()}`).join(',');
const formatFileSize = (bytes: number): string => { const formatFileSize = (bytes: number): string => {
@@ -648,7 +680,7 @@ function FileUpload({ field, fieldSlug, value, onChange }: FileUploadProps) {
} }
// Get the key - either from file.key, file.id, or file.url if it's an S3 key // Get the key - either from file.key, file.id, or file.url if it's an S3 key
const fileKey = file.key || file.id || file.url; const fileKey = file.key || file.id || file.url || '';
// If the URL is already a full URL (http/https), just open it directly // If the URL is already a full URL (http/https), just open it directly
if (fileKey.startsWith('http://') || fileKey.startsWith('https://')) { if (fileKey.startsWith('http://') || fileKey.startsWith('https://')) {

View File

@@ -5,6 +5,40 @@ import Image from 'next/image';
import { ProfileSection } from '@/services/profile-sections.service'; import { ProfileSection } from '@/services/profile-sections.service';
import DynamicField from './DynamicField'; import DynamicField from './DynamicField';
// Error boundary to catch rendering errors in individual sections
class SectionErrorBoundary extends React.Component<
{ sectionName: string; children: React.ReactNode },
{ hasError: boolean; error: Error | null }
> {
constructor(props: { sectionName: string; children: React.ReactNode }) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error(`Error rendering section "${this.props.sectionName}":`, error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="bg-white rounded-[20px] p-6 shadow-[0px_10px_20px_rgba(217,217,217,0.5)]">
<div className="p-4 bg-red-50 border border-red-200 rounded-lg">
<p className="text-sm text-red-600">
Failed to load section: {this.props.sectionName}
</p>
</div>
</div>
);
}
return this.props.children;
}
}
// Map section slugs to icon paths // Map section slugs to icon paths
const sectionIcons: Record<string, string> = { const sectionIcons: Record<string, string> = {
'basic-information': '/assets/icons/basic-information-icon.svg', 'basic-information': '/assets/icons/basic-information-icon.svg',
@@ -65,27 +99,32 @@ export default function DynamicSection({ section, formData, onChange, errors }:
let iconPath: string = defaultIcon; let iconPath: string = defaultIcon;
let isEmoji = false; let isEmoji = false;
if (section.icon) { const sectionName = section.name || 'Section';
if (section.icon.startsWith('/') || section.icon.startsWith('http')) { const sectionSlug = section.slug || '';
const sectionIcon = section.icon || '';
if (sectionIcon) {
if (sectionIcon.startsWith('/') || sectionIcon.startsWith('http')) {
// It's a valid URL path // It's a valid URL path
iconPath = section.icon; iconPath = sectionIcon;
} else if (isEmojiString(section.icon)) { } else if (isEmojiString(sectionIcon)) {
// It's an emoji // It's an emoji
isEmoji = true; isEmoji = true;
} else if (iconNameToPath[section.icon.toLowerCase()]) { } else if (iconNameToPath[sectionIcon.toLowerCase()]) {
// It's an icon name like "star", "home", etc. // It's an icon name like "star", "home", etc.
iconPath = iconNameToPath[section.icon.toLowerCase()]; iconPath = iconNameToPath[sectionIcon.toLowerCase()];
} else { } else {
// Fallback to slug mapping or default // Fallback to slug mapping or default
iconPath = sectionIcons[section.slug] || defaultIcon; iconPath = sectionIcons[sectionSlug] || defaultIcon;
} }
} else { } else {
// No icon set, use slug mapping or default // No icon set, use slug mapping or default
iconPath = sectionIcons[section.slug] || defaultIcon; iconPath = sectionIcons[sectionSlug] || defaultIcon;
} }
// Sort fields by sortOrder // Sort fields by sortOrder - safely handle missing/null fields
const sortedFields = [...(section.fields || [])].sort((a, b) => a.sortOrder - b.sortOrder); const fields = Array.isArray(section.fields) ? section.fields : [];
const sortedFields = [...fields].sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
const activeFields = sortedFields.filter(f => f.isActive && !f.isSearchableOnly); const activeFields = sortedFields.filter(f => f.isActive && !f.isSearchableOnly);
if (activeFields.length === 0) { if (activeFields.length === 0) {
@@ -93,19 +132,20 @@ export default function DynamicSection({ section, formData, onChange, errors }:
} }
return ( return (
<SectionErrorBoundary sectionName={sectionName}>
<div <div
id={section.slug} id={sectionSlug}
className="bg-white rounded-[20px] p-6 shadow-[0px_10px_20px_rgba(217,217,217,0.5)] scroll-mt-[100px]" className="bg-white rounded-[20px] p-6 shadow-[0px_10px_20px_rgba(217,217,217,0.5)] scroll-mt-[100px]"
> >
{/* Section Header */} {/* Section Header */}
<div className="flex items-center gap-3 mb-6"> <div className="flex items-center gap-3 mb-6">
<div className="w-[40px] h-[40px] flex items-center justify-center"> <div className="w-[40px] h-[40px] flex items-center justify-center">
{isEmoji ? ( {isEmoji ? (
<span className="text-2xl">{section.icon}</span> <span className="text-2xl">{sectionIcon}</span>
) : ( ) : (
<Image <Image
src={iconPath} src={iconPath}
alt={section.name} alt={sectionName}
width={24} width={24}
height={24} height={24}
className="object-contain" className="object-contain"
@@ -114,7 +154,7 @@ export default function DynamicSection({ section, formData, onChange, errors }:
</div> </div>
<div> <div>
<h2 className="text-[18px] font-bold font-serif text-[#00293D]"> <h2 className="text-[18px] font-bold font-serif text-[#00293D]">
{section.name} {sectionName}
{section.isRequired && <span className="text-red-500 ml-1">*</span>} {section.isRequired && <span className="text-red-500 ml-1">*</span>}
</h2> </h2>
{section.description && ( {section.description && (
@@ -136,5 +176,6 @@ export default function DynamicSection({ section, formData, onChange, errors }:
))} ))}
</div> </div>
</div> </div>
</SectionErrorBoundary>
); );
} }

View File

@@ -5,6 +5,40 @@ import Image from 'next/image';
import { ProfileSection } from '@/services/profile-sections.service'; import { ProfileSection } from '@/services/profile-sections.service';
import DynamicField from './DynamicField'; import DynamicField from './DynamicField';
// Error boundary to catch rendering errors in repeatable sections
class RepeatableSectionErrorBoundary extends React.Component<
{ sectionName: string; children: React.ReactNode },
{ hasError: boolean }
> {
constructor(props: { sectionName: string; children: React.ReactNode }) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error(`Error rendering repeatable section "${this.props.sectionName}":`, error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="bg-white rounded-[20px] p-6 shadow-[0px_10px_20px_rgba(217,217,217,0.5)]">
<div className="p-4 bg-red-50 border border-red-200 rounded-lg">
<p className="text-sm text-red-600">
Failed to load section: {this.props.sectionName}
</p>
</div>
</div>
);
}
return this.props.children;
}
}
// Map section slugs to icon paths // Map section slugs to icon paths
const sectionIcons: Record<string, string> = { const sectionIcons: Record<string, string> = {
'basic-information': '/assets/icons/basic-information-icon.svg', 'basic-information': '/assets/icons/basic-information-icon.svg',
@@ -88,8 +122,9 @@ export default function RepeatableSection({
iconPath = sectionIcons[section.slug] || defaultIcon; iconPath = sectionIcons[section.slug] || defaultIcon;
} }
// Sort fields by sortOrder and filter active, non-search-only fields // Sort fields by sortOrder and filter active, non-search-only fields - safely handle missing/null fields
const sortedFields = [...(section.fields || [])].sort((a, b) => a.sortOrder - b.sortOrder); const fields = Array.isArray(section.fields) ? section.fields : [];
const sortedFields = [...fields].sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
const activeFields = sortedFields.filter(f => f.isActive && !f.isSearchableOnly); const activeFields = sortedFields.filter(f => f.isActive && !f.isSearchableOnly);
if (activeFields.length === 0) { if (activeFields.length === 0) {
@@ -152,7 +187,10 @@ export default function RepeatableSection({
return `${section.name} ${index + 1}`; return `${section.name} ${index + 1}`;
}; };
const sectionName = section.name || 'Section';
return ( return (
<RepeatableSectionErrorBoundary sectionName={sectionName}>
<div <div
id={section.slug} id={section.slug}
className="bg-white rounded-[20px] p-6 shadow-[0px_10px_20px_rgba(217,217,217,0.5)] scroll-mt-[100px]" className="bg-white rounded-[20px] p-6 shadow-[0px_10px_20px_rgba(217,217,217,0.5)] scroll-mt-[100px]"
@@ -166,7 +204,7 @@ export default function RepeatableSection({
) : ( ) : (
<Image <Image
src={iconPath} src={iconPath}
alt={section.name} alt={sectionName}
width={24} width={24}
height={24} height={24}
className="object-contain" className="object-contain"
@@ -175,7 +213,7 @@ export default function RepeatableSection({
</div> </div>
<div> <div>
<h2 className="text-[18px] font-bold font-serif text-[#00293D]"> <h2 className="text-[18px] font-bold font-serif text-[#00293D]">
{section.name} {sectionName}
{section.isRequired && <span className="text-red-500 ml-1">*</span>} {section.isRequired && <span className="text-red-500 ml-1">*</span>}
</h2> </h2>
{section.description && ( {section.description && (
@@ -268,8 +306,9 @@ export default function RepeatableSection({
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg> </svg>
Add Another {section.name} Add Another {sectionName}
</button> </button>
</div> </div>
</RepeatableSectionErrorBoundary>
); );
} }

View File

@@ -73,7 +73,9 @@ export default function EditProfilePage() {
// Then apply field default values where no existing data // Then apply field default values where no existing data
sortedSections.forEach(section => { sortedSections.forEach(section => {
section.fields?.forEach(field => { const sectionFields = Array.isArray(section.fields) ? section.fields : [];
sectionFields.forEach(field => {
if (!field || !field.slug) return; // Skip malformed fields
if (field.defaultValue && initialData[field.slug] === undefined) { if (field.defaultValue && initialData[field.slug] === undefined) {
// Parse default value based on field type // Parse default value based on field type
switch (field.fieldType) { switch (field.fieldType) {
@@ -186,12 +188,13 @@ export default function EditProfilePage() {
const repErrors: Record<string, Record<string, Record<string, string>>> = {}; const repErrors: Record<string, Record<string, Record<string, string>>> = {};
sections.forEach(section => { sections.forEach(section => {
const sectionFields = Array.isArray(section.fields) ? section.fields : [];
if (section.isRepeatable) { if (section.isRepeatable) {
// Validate repeatable section entries // Validate repeatable section entries
const entries = repeatableData[section.slug] || [{}]; const entries = repeatableData[section.slug] || [{}];
entries.forEach((entry, entryIndex) => { entries.forEach((entry, entryIndex) => {
section.fields?.forEach(field => { sectionFields.forEach(field => {
if (field.isRequired && field.isActive && !field.isSearchableOnly) { if (field && field.isRequired && field.isActive && !field.isSearchableOnly) {
const value = entry[field.slug]; const value = entry[field.slug];
const isEmpty = const isEmpty =
value === undefined || value === undefined ||
@@ -209,8 +212,8 @@ export default function EditProfilePage() {
}); });
} else { } else {
// Validate regular section fields // Validate regular section fields
section.fields?.forEach(field => { sectionFields.forEach(field => {
if (field.isRequired && field.isActive && !field.isSearchableOnly) { if (field && field.isRequired && field.isActive && !field.isSearchableOnly) {
const value = formData[field.slug]; const value = formData[field.slug];
const isEmpty = const isEmpty =
value === undefined || value === undefined ||