frontend_nyla/src/components/UiComponents/Popup/EditForm.tsx
2025-12-01 17:01:25 +01:00

312 lines
No EOL
9.6 KiB
TypeScript

import { useState, useEffect } from 'react';
import styles from './EditForm.module.css';
// Field configuration interface (moved from EditPopup)
export interface EditFieldConfig {
key: string;
label: string;
type: 'string' | 'email' | 'date' | 'enum' | 'boolean' | 'readonly' | 'textarea';
editable: boolean;
required?: boolean;
options?: string[]; // For enum types
formatter?: (value: any) => string; // For display formatting
validator?: (value: any) => string | null; // Returns error message or null
placeholder?: string;
minRows?: number; // For textarea types
maxRows?: number; // For textarea types
}
// EditForm props
export interface EditFormProps<T = any> {
data: T;
fields: EditFieldConfig[];
onSave: (updatedData: T) => void;
onCancel?: () => void;
saveButtonText?: string;
cancelButtonText?: string;
showButtons?: boolean;
className?: string;
}
// EditForm component - handles form logic
export function EditForm<T extends Record<string, any>>({
data,
fields,
onSave,
onCancel,
saveButtonText = 'Save',
cancelButtonText = 'Cancel',
showButtons = true,
className = ''
}: EditFormProps<T>) {
const [editedData, setEditedData] = useState<T>(data);
const [errors, setErrors] = useState<Record<string, string>>({});
const [fieldFocused, setFieldFocused] = useState<Record<string, boolean>>({});
// Reset data when data changes
useEffect(() => {
setEditedData({ ...data });
setErrors({});
setFieldFocused({});
// Initialize textarea heights for textarea fields
setTimeout(() => {
fields.forEach(field => {
if (field.type === 'textarea') {
const textarea = document.querySelector(`textarea[name="${field.key}"]`) as HTMLTextAreaElement;
if (textarea) {
const minRows = field.minRows || 4;
const maxRows = field.maxRows || 8;
textarea.style.height = 'auto';
const newHeight = Math.max(
minRows * 1.5 * 16,
Math.min(
textarea.scrollHeight,
maxRows * 1.5 * 16
)
);
textarea.style.height = `${newHeight}px`;
}
}
});
}, 0);
}, [data, fields]);
// Handle field focus
const handleFieldFocus = (fieldKey: string, focused: boolean) => {
setFieldFocused(prev => ({
...prev,
[fieldKey]: focused
}));
};
// Handle field value changes
const handleFieldChange = (fieldKey: string, value: any) => {
setEditedData(prev => ({
...prev,
[fieldKey]: value
}));
// Clear error for this field when user starts typing
if (errors[fieldKey]) {
setErrors(prev => {
const newErrors = { ...prev };
delete newErrors[fieldKey];
return newErrors;
});
}
};
// Validate all fields
const validateFields = (): boolean => {
const newErrors: Record<string, string> = {};
fields.forEach(field => {
const value = editedData[field.key];
// Check required fields
if (field.required && (!value || value.toString().trim() === '')) {
newErrors[field.key] = `${field.label} is required`;
return;
}
// Run custom validator
if (field.validator && value) {
const error = field.validator(value);
if (error) {
newErrors[field.key] = error;
}
}
// Basic email validation
if (field.type === 'email' && value && !/\S+@\S+\.\S+/.test(value)) {
newErrors[field.key] = 'Invalid email format';
}
});
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
// Handle save
const handleSave = () => {
if (validateFields()) {
onSave(editedData);
}
};
// Handle cancel
const handleCancel = () => {
setEditedData({ ...data });
setErrors({});
onCancel?.();
};
// Helper function to get label class
const getLabelClass = (fieldKey: string, value: any) => {
const isFocused = fieldFocused[fieldKey];
const hasValue = value && value.toString().trim() !== '';
if (isFocused) {
return styles.activeFocusedLabel; // Secondary color when actively focused
} else if (hasValue) {
return styles.focusedLabel; // Primary color when has value but not focused
} else {
return styles.label; // Regular position when empty and not focused
}
};
// Render field based on its type
const renderField = (field: EditFieldConfig) => {
const value = editedData[field.key];
const hasError = errors[field.key];
if (field.type === 'readonly' || !field.editable) {
return (
<div className={styles.floatingLabelInput} key={field.key}>
<div className={styles.readonlyField}>
{field.formatter ? field.formatter(value) : (value || 'N/A')}
</div>
<label className={styles.focusedLabel}>
{field.label}
</label>
</div>
);
}
if (field.type === 'enum') {
return (
<div className={styles.floatingLabelInput} key={field.key}>
<select
value={value || ''}
onChange={(e) => handleFieldChange(field.key, e.target.value)}
onFocus={() => handleFieldFocus(field.key, true)}
onBlur={() => handleFieldFocus(field.key, false)}
className={`${styles.fieldInput} ${hasError ? styles.fieldError : ''}`}
>
<option value="" disabled hidden></option>
{field.options?.map(option => (
<option key={option} value={option}>{option}</option>
))}
</select>
<label className={getLabelClass(field.key, value)}>
{field.label}
{field.required && <span className={styles.required}>*</span>}
</label>
{hasError && <span className={styles.errorText}>{hasError}</span>}
</div>
);
}
if (field.type === 'boolean') {
return (
<div className={styles.fieldGroup} key={field.key}>
<label className={styles.checkboxLabel}>
<input
type="checkbox"
checked={!!value}
onChange={(e) => handleFieldChange(field.key, e.target.checked)}
onFocus={() => handleFieldFocus(field.key, true)}
onBlur={() => handleFieldFocus(field.key, false)}
className={styles.checkboxInput}
/>
{field.label}
{field.required && <span className={styles.required}>*</span>}
</label>
{hasError && <span className={styles.errorText}>{hasError}</span>}
</div>
);
}
// Handle textarea type
if (field.type === 'textarea') {
const minRows = field.minRows || 4;
const maxRows = field.maxRows || 8;
return (
<div className={styles.floatingLabelInput} key={field.key}>
<textarea
name={field.key}
value={value || ''}
onChange={(e) => {
handleFieldChange(field.key, e.target.value);
// Auto-resize textarea
const textarea = e.target;
textarea.style.height = 'auto';
const newHeight = Math.max(
minRows * 1.5 * 16, // minRows * line-height * font-size
Math.min(
textarea.scrollHeight,
maxRows * 1.5 * 16 // maxRows * line-height * font-size
)
);
textarea.style.height = `${newHeight}px`;
}}
onFocus={() => handleFieldFocus(field.key, true)}
onBlur={() => handleFieldFocus(field.key, false)}
className={`${styles.fieldTextarea} ${hasError ? styles.fieldError : ''}`}
rows={minRows}
/>
<label className={getLabelClass(field.key, value)}>
{field.label}
{field.required && <span className={styles.required}>*</span>}
</label>
{hasError && <span className={styles.errorText}>{hasError}</span>}
</div>
);
}
// Default to text input for string, email, date types
const inputType = field.type === 'email' ? 'email' :
field.type === 'date' ? 'date' : 'text';
return (
<div className={styles.floatingLabelInput} key={field.key}>
<input
type={inputType}
value={value || ''}
onChange={(e) => handleFieldChange(field.key, e.target.value)}
onFocus={() => handleFieldFocus(field.key, true)}
onBlur={() => handleFieldFocus(field.key, false)}
className={`${styles.fieldInput} ${hasError ? styles.fieldError : ''}`}
/>
<label className={getLabelClass(field.key, value)}>
{field.label}
{field.required && <span className={styles.required}>*</span>}
</label>
{hasError && <span className={styles.errorText}>{hasError}</span>}
</div>
);
};
return (
<div className={`${styles.editForm} ${className}`}>
<form onSubmit={(e) => { e.preventDefault(); handleSave(); }}>
{fields.map(field => renderField(field))}
</form>
{showButtons && (
<div className={styles.buttonGroup}>
{onCancel && (
<button
type="button"
className={styles.cancelButton}
onClick={handleCancel}
>
{cancelButtonText}
</button>
)}
<button
type="button"
className={styles.saveButton}
onClick={handleSave}
>
{saveButtonText}
</button>
</div>
)}
</div>
);
}
export default EditForm;