import React, { useCallback, useEffect, useMemo } from "react";
import {
useForm,
Controller,
useWatch,
Control,
FieldValues,
} from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { debounce } from "lodash";
import {
colorSchema,
urlSchema,
emailSchema,
numberRangeSchema,
stringLengthSchema,
validateFormField,
ValidationError,
} from "./validation";
import { useCMSEditor } from "./store";
// Form field interfaces
interface BaseFormFieldProps {
name: string;
label: string;
description?: string;
required?: boolean;
disabled?: boolean;
className?: string;
}
interface TextFieldProps extends BaseFormFieldProps {
type?: "text" | "email" | "url" | "tel" | "password";
placeholder?: string;
maxLength?: number;
minLength?: number;
}
interface NumberFieldProps extends BaseFormFieldProps {
min?: number;
max?: number;
step?: number;
placeholder?: string;
}
interface SelectFieldProps extends BaseFormFieldProps {
options: Array<{ value: string | number; label: string; disabled?: boolean }>;
placeholder?: string;
}
interface ColorFieldProps extends BaseFormFieldProps {
allowTransparent?: boolean;
}
interface ToggleFieldProps extends BaseFormFieldProps {
size?: "small" | "medium" | "large";
}
interface RangeFieldProps extends BaseFormFieldProps {
min: number;
max: number;
step?: number;
showValue?: boolean;
formatValue?: (value: number) => string;
}
// Error display component
const FieldError: React.FC<{ error?: string }> = ({ error }) => {
if (!error) return null;
return (
);
};
// Success indicator
const FieldSuccess: React.FC<{ show: boolean }> = ({ show }) => {
if (!show) return null;
return (
);
};
// Text input component
export const TextField: React.FC<
TextFieldProps & { control: Control }
> = ({
name,
label,
description,
type = "text",
placeholder,
required = false,
disabled = false,
maxLength,
minLength,
className = "",
control,
}) => {
return (
{description &&
{description}
}
(
)}
/>
);
};
// Number input component
export const NumberField: React.FC<
NumberFieldProps & { control: Control }
> = ({
name,
label,
description,
min,
max,
step = 1,
placeholder,
required = false,
disabled = false,
className = "",
control,
}) => {
return (
{description &&
{description}
}
(
field.onChange(parseFloat(e.target.value) || 0)}
className={`
w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
disabled:bg-gray-50 disabled:text-gray-500
${
invalid
? "border-red-300 focus:ring-red-500 focus:border-red-500"
: ""
}
${
!invalid && field.value !== undefined
? "border-green-300"
: ""
}
`}
/>
)}
/>
);
};
// Select dropdown component
export const SelectField: React.FC<
SelectFieldProps & { control: Control }
> = ({
name,
label,
description,
options,
placeholder,
required = false,
disabled = false,
className = "",
control,
}) => {
return (
{description &&
{description}
}
(
)}
/>
);
};
// Color picker component
export const ColorField: React.FC = ({
name,
label,
description,
required = false,
disabled = false,
allowTransparent = false,
className = "",
control,
}) => {
return (
{description &&
{description}
}
(
)}
/>
);
};
// Toggle switch component
export const ToggleField: React.FC = ({
name,
label,
description,
required = false,
disabled = false,
size = "medium",
className = "",
control,
}) => {
const sizeClasses = {
small: "w-8 h-4",
medium: "w-10 h-5",
large: "w-12 h-6",
};
const thumbSizeClasses = {
small: "w-3 h-3",
medium: "w-4 h-4",
large: "w-5 h-5",
};
return (
(
{description && (
{description}
)}
)}
/>
);
};
// Range slider component
export const RangeField: React.FC = ({
name,
label,
description,
min,
max,
step = 1,
showValue = true,
formatValue,
required = false,
disabled = false,
className = "",
control,
}) => {
return (
{showValue && (
(
{formatValue ? formatValue(field.value) : field.value}
)}
/>
)}
{description &&
{description}
}
(
)}
/>
);
};
// Optimistic form wrapper
interface OptimisticFormProps {
sectionId: string;
schema: z.ZodSchema;
defaultValues: any;
onSubmit: (data: any) => Promise;
children: (props: {
control: any;
formState: any;
handleSubmit: any;
}) => React.ReactNode;
className?: string;
}
export const OptimisticForm: React.FC = ({
sectionId,
schema,
defaultValues,
onSubmit,
children,
className = "",
}) => {
const {
updateSection,
startOptimisticUpdate,
finishOptimisticUpdate,
rollbackOptimisticUpdate,
} = useCMSEditor();
const form = useForm({
resolver: zodResolver(schema),
defaultValues,
mode: "onChange",
});
const { control, handleSubmit, formState, watch, getValues } = form;
// Watch for changes and apply optimistic updates
const watchedValues = useWatch({ control });
const debouncedOptimisticUpdate = useMemo(
() =>
debounce((values: any) => {
startOptimisticUpdate(sectionId);
updateSection(sectionId, values);
}, 300),
[sectionId, startOptimisticUpdate, updateSection]
);
useEffect(() => {
if (formState.isValid && !formState.isSubmitting) {
debouncedOptimisticUpdate(watchedValues);
}
return () => {
debouncedOptimisticUpdate.cancel();
};
}, [
watchedValues,
formState.isValid,
formState.isSubmitting,
debouncedOptimisticUpdate,
]);
const handleFormSubmit = useCallback(
async (data: any) => {
try {
startOptimisticUpdate(sectionId);
await onSubmit(data);
finishOptimisticUpdate(sectionId, true);
} catch (error) {
console.error("Form submission failed:", error);
rollbackOptimisticUpdate(sectionId);
finishOptimisticUpdate(sectionId, false);
}
},
[
sectionId,
onSubmit,
startOptimisticUpdate,
finishOptimisticUpdate,
rollbackOptimisticUpdate,
]
);
return (
);
};
// Real-time validation indicator
export const ValidationIndicator: React.FC<{
sectionId: string;
className?: string;
}> = ({ sectionId, className = "" }) => {
const { validationErrors, pendingUpdates } = useCMSEditor();
const errors = validationErrors[sectionId] || [];
const isPending = pendingUpdates.has(sectionId);
if (isPending) {
return (
);
}
if (errors.length > 0) {
return (
);
}
return (
);
};