From 99a594ed162f1a4aaaf95b53d5c302c675a3ae43 Mon Sep 17 00:00:00 2001 From: pradeepkumar Date: Wed, 15 Apr 2026 15:44:51 +0530 Subject: [PATCH] feat: implement comprehensive field validation logic and update UI components to enforce validation constraints --- .../agent/edit/components/DynamicField.tsx | 5 + .../agent/edit/components/FormInput.tsx | 8 +- .../agent/edit/components/FormTextarea.tsx | 8 +- src/app/(agent)/agent/edit/page.tsx | 61 +++++++----- src/components/home/TestimonialsSection.tsx | 17 ++-- src/services/profile-sections.service.ts | 2 + src/utils/profileDataMapper.ts | 21 +---- src/utils/validateFieldValue.ts | 92 +++++++++++++++++++ 8 files changed, 159 insertions(+), 55 deletions(-) create mode 100644 src/utils/validateFieldValue.ts diff --git a/src/app/(agent)/agent/edit/components/DynamicField.tsx b/src/app/(agent)/agent/edit/components/DynamicField.tsx index 0279051..e566b43 100644 --- a/src/app/(agent)/agent/edit/components/DynamicField.tsx +++ b/src/app/(agent)/agent/edit/components/DynamicField.tsx @@ -56,6 +56,7 @@ export default function DynamicField({ field, value, onChange, error }: DynamicF onChange={(val: string) => handleChange(val)} required={field.isRequired} error={error} + maxLength={field.validation?.maxLength} /> ); @@ -68,6 +69,8 @@ export default function DynamicField({ field, value, onChange, error }: DynamicF onChange={(val: string) => handleChange(val)} required={field.isRequired} rows={4} + maxLength={field.validation?.maxLength} + error={error} /> ); @@ -95,6 +98,8 @@ export default function DynamicField({ field, value, onChange, error }: DynamicF onChange={(val: string) => handleChange(val)} required={field.isRequired} error={error} + min={field.validation?.minDate} + max={field.validation?.maxDate} /> ); diff --git a/src/app/(agent)/agent/edit/components/FormInput.tsx b/src/app/(agent)/agent/edit/components/FormInput.tsx index 90ec3f1..80ac951 100644 --- a/src/app/(agent)/agent/edit/components/FormInput.tsx +++ b/src/app/(agent)/agent/edit/components/FormInput.tsx @@ -9,11 +9,12 @@ interface FormInputProps { disabled?: boolean; required?: boolean; error?: string; - min?: number; - max?: number; + min?: number | string; + max?: number | string; + maxLength?: number; } -export function FormInput({ label, value, onChange, placeholder, type = 'text', disabled, required, error, min, max }: FormInputProps) { +export function FormInput({ label, value, onChange, placeholder, type = 'text', disabled, required, error, min, max, maxLength }: FormInputProps) { return (
{/* Stats Section */} -
+
{data.stats.map((stat, index) => ( -
+
-
- - {stat.boldText} - -
- - {stat.normalText} - -
+ + {stat.boldText}{' '} + {stat.normalText} +
))}
diff --git a/src/services/profile-sections.service.ts b/src/services/profile-sections.service.ts index 207401e..26470ba 100644 --- a/src/services/profile-sections.service.ts +++ b/src/services/profile-sections.service.ts @@ -36,6 +36,8 @@ export interface FieldValidation { minLength?: number; maxLength?: number; pattern?: string; + minDate?: string; // ISO date (YYYY-MM-DD) for DATE fields + maxDate?: string; // ISO date (YYYY-MM-DD) for DATE fields allowedFormats?: string[]; maxFileSize?: number; } diff --git a/src/utils/profileDataMapper.ts b/src/utils/profileDataMapper.ts index 0e1ee5f..42dfd0f 100644 --- a/src/utils/profileDataMapper.ts +++ b/src/utils/profileDataMapper.ts @@ -291,22 +291,11 @@ export function mapFieldValuesToAvailability(fieldValues: FieldValueResponse[]): return trimmed; }; - // Drop stale/orphan options. Backend returns `valueLabel[i]` equal to the raw `value[i]` - // when no option match is found (admin renamed/removed the option). Those are orphans — - // skip them entirely so old selections don't linger on the public profile. - const resolvedLabels: string[] = []; - rawValues.forEach((val, i) => { - const raw = String(val || '').trim(); - if (!raw) return; - const fromBackend = labelValues[i]; - const hasResolvedLabel = - fromBackend && - typeof fromBackend === 'string' && - fromBackend.trim() && - fromBackend !== raw; // backend-resolved label must DIFFER from raw value - if (!hasResolvedLabel) return; // orphan — drop - resolvedLabels.push(formatIfSnakeCase(fromBackend)); - }); + // Backend now strips orphan entries and returns only resolvable option labels, + // so we trust valueLabel as-is. + const resolvedLabels: string[] = labelValues + .filter((l): l is string => typeof l === 'string' && l.trim().length > 0) + .map(formatIfSnakeCase); // Deduplicate by normalized comparison (case/whitespace insensitive) const seen = new Set(); diff --git a/src/utils/validateFieldValue.ts b/src/utils/validateFieldValue.ts new file mode 100644 index 0000000..adb51ce --- /dev/null +++ b/src/utils/validateFieldValue.ts @@ -0,0 +1,92 @@ +import type { ProfileField } from '@/services/profile-sections.service'; + +/** + * Returns an error message if the given value fails validation for the field, + * or null if it passes. Enforces: + * - required + * - TEXT/TEXTAREA: minLength, maxLength, pattern + * - NUMBER/RANGE: min, max + * - DATE: minDate, maxDate + * - CHECKBOX_GROUP / MULTI_SELECT / TAG_INPUT / RADIO / SELECT: required non-empty + */ +export function validateFieldValue(field: ProfileField, value: unknown): string | null { + const v = field.validation ?? {}; + const fieldName = field.name || 'This field'; + + const isEmpty = + value === undefined || + value === null || + value === '' || + (Array.isArray(value) && value.length === 0); + + if (field.isRequired && isEmpty) { + return `${fieldName} is required`; + } + if (isEmpty) return null; // optional + empty → OK + + switch (field.fieldType) { + case 'TEXT': + case 'TEXTAREA': { + const str = String(value); + if (typeof v.minLength === 'number' && str.length < v.minLength) { + return `${fieldName} must be at least ${v.minLength} characters`; + } + if (typeof v.maxLength === 'number' && str.length > v.maxLength) { + return `${fieldName} must be no more than ${v.maxLength} characters`; + } + if (v.pattern && typeof v.pattern === 'string') { + try { + if (!new RegExp(v.pattern).test(str)) { + return `${fieldName} has an invalid format`; + } + } catch { + // Invalid regex in admin config — silently skip + } + } + return null; + } + + case 'NUMBER': + case 'RANGE': { + const n = typeof value === 'number' ? value : parseFloat(String(value)); + if (!isFinite(n)) return `${fieldName} must be a number`; + if (typeof v.min === 'number' && n < v.min) { + return `${fieldName} must be at least ${v.min}`; + } + if (typeof v.max === 'number' && n > v.max) { + return `${fieldName} must be no more than ${v.max}`; + } + return null; + } + + case 'DATE': { + const str = String(value).slice(0, 10); // YYYY-MM-DD + if (!/^\d{4}-\d{2}-\d{2}$/.test(str)) { + return `${fieldName} must be a valid date`; + } + if (v.minDate && str < v.minDate) { + return `${fieldName} must be on or after ${v.minDate}`; + } + if (v.maxDate && str > v.maxDate) { + return `${fieldName} must be on or before ${v.maxDate}`; + } + return null; + } + + case 'TAG_INPUT': { + // string items — apply maxLength per item if defined + if (Array.isArray(value) && typeof v.maxLength === 'number') { + const tooLong = (value as unknown[]).find( + (item) => typeof item === 'string' && item.length > v.maxLength!, + ); + if (tooLong) { + return `Each ${fieldName.toLowerCase()} entry must be no more than ${v.maxLength} characters`; + } + } + return null; + } + + default: + return null; + } +}