From a996c2623d556f2567df001d0687cc2d92b52dca Mon Sep 17 00:00:00 2001 From: hanif salafi Date: Sun, 13 Jul 2025 21:58:07 +0700 Subject: [PATCH] add : starting improvement, add component refactoring --- components/auth/profile-form.tsx | 2 +- components/form/shared/form-checkbox.tsx | 244 ++++++ .../form/shared/form-components-demo.tsx | 280 +++++++ components/form/shared/form-date-picker.tsx | 231 ++++++ components/form/shared/form-field.tsx | 228 ++++++ components/form/shared/form-grid.tsx | 271 +++++++ components/form/shared/form-radio.tsx | 210 ++++++ components/form/shared/form-section.tsx | 184 +++++ components/form/shared/form-select.tsx | 215 ++++++ components/form/shared/index.ts | 32 + docs/DESIGN_SYSTEM.md | 494 +++++++++++++ docs/IMPROVEMENT_PLAN.md | 699 ++++++++++++++++++ docs/PHASE_1_SUMMARY.md | 281 +++++++ docs/PHASE_2_COMPONENT_REFACTORING.md | 338 +++++++++ docs/PHASE_2_SUMMARY.md | 109 +++ 15 files changed, 3817 insertions(+), 1 deletion(-) create mode 100644 components/form/shared/form-checkbox.tsx create mode 100644 components/form/shared/form-components-demo.tsx create mode 100644 components/form/shared/form-date-picker.tsx create mode 100644 components/form/shared/form-field.tsx create mode 100644 components/form/shared/form-grid.tsx create mode 100644 components/form/shared/form-radio.tsx create mode 100644 components/form/shared/form-section.tsx create mode 100644 components/form/shared/form-select.tsx create mode 100644 components/form/shared/index.ts create mode 100644 docs/DESIGN_SYSTEM.md create mode 100644 docs/IMPROVEMENT_PLAN.md create mode 100644 docs/PHASE_1_SUMMARY.md create mode 100644 docs/PHASE_2_COMPONENT_REFACTORING.md create mode 100644 docs/PHASE_2_SUMMARY.md diff --git a/components/auth/profile-form.tsx b/components/auth/profile-form.tsx index e5c48afe..f4959adb 100644 --- a/components/auth/profile-form.tsx +++ b/components/auth/profile-form.tsx @@ -159,7 +159,7 @@ export const ProfileForm: React.FC = ({ - {institutes.map((institute) => ( + {institutes?.map((institute) => ( diff --git a/components/form/shared/form-checkbox.tsx b/components/form/shared/form-checkbox.tsx new file mode 100644 index 00000000..736556df --- /dev/null +++ b/components/form/shared/form-checkbox.tsx @@ -0,0 +1,244 @@ +/** + * FormCheckbox Component + * + * A reusable checkbox component that handles: + * - Single checkbox + * - Checkbox groups + * - Label + * - Controller (react-hook-form) + * - Error handling + * + * This component reduces code duplication for checkbox fields across forms. + */ + +import React from 'react'; +import { Control, Controller, FieldError, FieldPath, FieldValues } from 'react-hook-form'; +import { Label } from '@/components/ui/label'; +import { Checkbox } from '@/components/ui/checkbox'; +import { cn } from '@/lib/utils'; + +// ============================================================================= +// TYPES +// ============================================================================= + +export interface CheckboxOption { + value: string | number; + label: string; + disabled?: boolean; +} + +export interface FormCheckboxProps< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> { + // Form control + control: Control; + name: TName; + + // Field configuration + label?: string; + required?: boolean; + + // Checkbox configuration + single?: boolean; // Single checkbox vs checkbox group + options?: CheckboxOption[]; // For checkbox groups + + // Styling + className?: string; + labelClassName?: string; + checkboxClassName?: string; + errorClassName?: string; + groupClassName?: string; + + // Layout + layout?: 'horizontal' | 'vertical'; + columns?: number; // For grid layout + + // Validation + validation?: { + required?: boolean | string; + }; + + // Additional props + disabled?: boolean; + size?: 'sm' | 'md' | 'lg'; + + // Custom render functions + renderOption?: (option: CheckboxOption, checked: boolean, onChange: (checked: boolean) => void) => React.ReactElement; +} + +// ============================================================================= +// COMPONENT +// ============================================================================= + +export function FormCheckbox< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + control, + name, + label, + required = false, + single = false, + options = [], + className = '', + labelClassName = '', + checkboxClassName = '', + errorClassName = '', + groupClassName = '', + layout = 'horizontal', + columns = 1, + validation, + disabled = false, + size = 'md', + renderOption, +}: FormCheckboxProps) { + + // ============================================================================= + // HELPER FUNCTIONS + // ============================================================================= + + const getCheckboxSize = (size: 'sm' | 'md' | 'lg') => { + switch (size) { + case 'sm': + return 'h-4 w-4'; + case 'lg': + return 'h-6 w-6'; + default: + return 'h-5 w-5'; + } + }; + + const getGroupLayout = () => { + if (layout === 'vertical') { + return 'flex flex-col gap-2'; + } + + if (columns > 1) { + return `grid grid-cols-${columns} gap-2`; + } + + return 'flex flex-wrap gap-4'; + }; + + // ============================================================================= + // RENDER FUNCTIONS + // ============================================================================= + + const renderSingleCheckbox = (field: any, fieldState: { error?: FieldError }) => ( +
+ + +
+ ); + + const renderCheckboxGroup = (field: any, fieldState: { error?: FieldError }) => { + const selectedValues = field.value || []; + + const handleOptionChange = (optionValue: string | number, checked: boolean) => { + const newValues = checked + ? [...selectedValues, optionValue] + : selectedValues.filter((value: any) => value !== optionValue); + field.onChange(newValues); + }; + + const renderDefaultOption = (option: CheckboxOption) => { + const checked = selectedValues.includes(option.value); + + if (renderOption) { + return renderOption(option, checked, (checked) => handleOptionChange(option.value, checked)); + } + + return ( +
+ handleOptionChange(option.value, checked as boolean)} + disabled={disabled || option.disabled} + className={getCheckboxSize(size)} + /> + +
+ ); + }; + + return ( +
+ {options.map(renderDefaultOption)} +
+ ); + }; + + // ============================================================================= + // MAIN RENDER + // ============================================================================= + + return ( +
+ {/* Group Label */} + {label && !single && ( + + )} + + {/* Controller */} + ( +
+ {single ? + renderSingleCheckbox(field, fieldState) : + renderCheckboxGroup(field, fieldState) + } + + {/* Error Message */} + {fieldState.error && ( +

+ {fieldState.error.message} +

+ )} +
+ )} + /> +
+ ); +} + +export default FormCheckbox; \ No newline at end of file diff --git a/components/form/shared/form-components-demo.tsx b/components/form/shared/form-components-demo.tsx new file mode 100644 index 00000000..8981d0a1 --- /dev/null +++ b/components/form/shared/form-components-demo.tsx @@ -0,0 +1,280 @@ +/** + * Form Components Demo + * + * This component demonstrates how to use the new reusable form components. + * It shows examples of all the form field types and layout components. + */ + +import React from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import * as z from 'zod'; +import { Button } from '@/components/ui/button'; +import { Card } from '@/components/ui/card'; +import { + FormField, + FormSelect, + FormCheckbox, + FormRadio, + FormDatePicker, + FormSection, + FormGrid, + FormGridItem, + SelectOption, + CheckboxOption, + RadioOption, +} from './index'; + +// ============================================================================= +// SCHEMA +// ============================================================================= + +const demoFormSchema = z.object({ + title: z.string().min(1, 'Title is required'), + description: z.string().min(10, 'Description must be at least 10 characters'), + category: z.string().min(1, 'Category is required'), + priority: z.string().min(1, 'Priority is required'), + tags: z.array(z.string()).min(1, 'At least one tag is required'), + status: z.string().min(1, 'Status is required'), + dueDate: z.date().optional(), + isPublic: z.boolean().default(false), + notifications: z.array(z.string()).default([]), +}); + +type DemoFormData = z.infer; + +// ============================================================================= +// OPTIONS DATA +// ============================================================================= + +const categoryOptions: SelectOption[] = [ + { value: 'feature', label: 'Feature Request' }, + { value: 'bug', label: 'Bug Report' }, + { value: 'improvement', label: 'Improvement' }, + { value: 'documentation', label: 'Documentation' }, +]; + +const priorityOptions: RadioOption[] = [ + { value: 'low', label: 'Low' }, + { value: 'medium', label: 'Medium' }, + { value: 'high', label: 'High' }, + { value: 'urgent', label: 'Urgent' }, +]; + +const tagOptions: CheckboxOption[] = [ + { value: 'frontend', label: 'Frontend' }, + { value: 'backend', label: 'Backend' }, + { value: 'ui', label: 'UI/UX' }, + { value: 'database', label: 'Database' }, + { value: 'security', label: 'Security' }, + { value: 'performance', label: 'Performance' }, +]; + +const statusOptions: SelectOption[] = [ + { value: 'draft', label: 'Draft' }, + { value: 'in-progress', label: 'In Progress' }, + { value: 'review', label: 'Under Review' }, + { value: 'completed', label: 'Completed' }, +]; + +const notificationOptions: CheckboxOption[] = [ + { value: 'email', label: 'Email Notifications' }, + { value: 'push', label: 'Push Notifications' }, + { value: 'sms', label: 'SMS Notifications' }, +]; + +// ============================================================================= +// COMPONENT +// ============================================================================= + +export function FormComponentsDemo() { + const form = useForm({ + resolver: zodResolver(demoFormSchema), + defaultValues: { + title: '', + description: '', + category: '', + priority: 'medium', + tags: [], + status: 'draft', + dueDate: undefined, + isPublic: false, + notifications: ['email'], + }, + }); + + const onSubmit = (data: DemoFormData) => { + console.log('Form submitted:', data); + alert('Form submitted! Check console for data.'); + }; + + return ( +
+
+

Form Components Demo

+

+ Examples of using the new reusable form components +

+
+ +
+ {/* Basic Information Section */} + + + + + + + + + + + + + + + + + {/* Priority and Status Section */} + + + + + + + + + + + + + {/* Tags and Settings Section */} + + + + + + + + + +
+ +
+ +
+ +
+
+
+
+ + {/* Form Actions */} + +
+ + +
+
+
+ + {/* Form Data Display */} + +

Current Form Data

+
+          {JSON.stringify(form.watch(), null, 2)}
+        
+
+
+ ); +} + +export default FormComponentsDemo; \ No newline at end of file diff --git a/components/form/shared/form-date-picker.tsx b/components/form/shared/form-date-picker.tsx new file mode 100644 index 00000000..fd6c9acc --- /dev/null +++ b/components/form/shared/form-date-picker.tsx @@ -0,0 +1,231 @@ +/** + * FormDatePicker Component + * + * A reusable date picker component that handles: + * - Single date selection + * - Date range selection + * - Label + * - Controller (react-hook-form) + * - Error handling + * + * This component reduces code duplication for date picker fields across forms. + */ + +import React from 'react'; +import { Control, Controller, FieldError, FieldPath, FieldValues } from 'react-hook-form'; +import { Label } from '@/components/ui/label'; +import { Button } from '@/components/ui/button'; +import { Calendar } from '@/components/ui/calendar'; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover'; +import { CalendarIcon } from 'lucide-react'; +import { format } from 'date-fns'; +import { DateRange } from 'react-day-picker'; +import { cn } from '@/lib/utils'; + +// ============================================================================= +// TYPES +// ============================================================================= + +export interface FormDatePickerProps< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> { + // Form control + control: Control; + name: TName; + + // Field configuration + label?: string; + placeholder?: string; + required?: boolean; + + // Date picker configuration + mode?: 'single' | 'range'; + numberOfMonths?: number; + + // Styling + className?: string; + labelClassName?: string; + buttonClassName?: string; + errorClassName?: string; + + // Validation + validation?: { + required?: boolean | string; + }; + + // Additional props + disabled?: boolean; + size?: 'sm' | 'md' | 'lg'; + + // Date formatting + dateFormat?: string; + + // Custom render functions + renderTrigger?: (props: { + field: any; + fieldState: { error?: FieldError }; + selectedDate: Date | DateRange | undefined; + placeholder: string; + }) => React.ReactElement; +} + +// ============================================================================= +// COMPONENT +// ============================================================================= + +export function FormDatePicker< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + control, + name, + label, + placeholder = 'Pick a date', + required = false, + mode = 'single', + numberOfMonths = 1, + className = '', + labelClassName = '', + buttonClassName = '', + errorClassName = '', + validation, + disabled = false, + size = 'md', + dateFormat = 'LLL dd, y', + renderTrigger, +}: FormDatePickerProps) { + + // ============================================================================= + // HELPER FUNCTIONS + // ============================================================================= + + const getButtonSize = (size: 'sm' | 'md' | 'lg') => { + switch (size) { + case 'sm': + return 'h-8 text-sm'; + case 'lg': + return 'h-12 text-base'; + default: + return 'h-10 text-sm'; + } + }; + + const formatDate = (date: Date | DateRange | undefined): string => { + if (!date) return ''; + + if (mode === 'range' && 'from' in date) { + if (date.from) { + if (date.to) { + return `${format(date.from, dateFormat)} - ${format(date.to, dateFormat)}`; + } + return format(date.from, dateFormat); + } + } else if (date instanceof Date) { + return format(date, dateFormat); + } + + return ''; + }; + + // ============================================================================= + // RENDER FUNCTIONS + // ============================================================================= + + const renderDefaultTrigger = ( + field: any, + fieldState: { error?: FieldError }, + selectedDate: Date | DateRange | undefined, + placeholder: string + ) => ( + + ); + + // ============================================================================= + // MAIN RENDER + // ============================================================================= + + return ( +
+ {/* Label */} + {label && ( + + )} + + {/* Controller */} + { + const selectedDate = field.value; + + return ( +
+ + + {renderTrigger ? + renderTrigger({ field, fieldState, selectedDate, placeholder }) : + renderDefaultTrigger(field, fieldState, selectedDate, placeholder) + } + + + + + + + + {/* Error Message */} + {fieldState.error && ( +

+ {fieldState.error.message} +

+ )} +
+ ); + }} + /> +
+ ); +} + +export default FormDatePicker; \ No newline at end of file diff --git a/components/form/shared/form-field.tsx b/components/form/shared/form-field.tsx new file mode 100644 index 00000000..f81aecf1 --- /dev/null +++ b/components/form/shared/form-field.tsx @@ -0,0 +1,228 @@ +/** + * FormField Component + * + * A reusable form field component that handles the common pattern of: + * - Label + * - Controller (react-hook-form) + * - Input/Select/Textarea + * - Error handling + * + * This component reduces code duplication across all form components. + */ + +import React from 'react'; +import { Control, Controller, FieldError, FieldPath, FieldValues } from 'react-hook-form'; +import { Label } from '@/components/ui/label'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { cn } from '@/lib/utils'; + +// ============================================================================= +// TYPES +// ============================================================================= + +export interface FormFieldProps< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> { + // Form control + control: Control; + name: TName; + + // Field configuration + label?: string; + placeholder?: string; + type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url'; + required?: boolean; + + // Field type + fieldType?: 'input' | 'textarea' | 'select'; + + // Styling + className?: string; + labelClassName?: string; + inputClassName?: string; + errorClassName?: string; + + // Validation + validation?: { + required?: boolean | string; + minLength?: { value: number; message: string }; + maxLength?: { value: number; message: string }; + pattern?: { value: RegExp; message: string }; + }; + + // Additional props + disabled?: boolean; + readOnly?: boolean; + autoComplete?: string; + size?: 'sm' | 'md' | 'lg'; + + // Custom render function + render?: (props: { + field: any; + fieldState: { error?: FieldError }; + }) => React.ReactElement; +} + +// ============================================================================= +// COMPONENT +// ============================================================================= + +export function FormField< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + control, + name, + label, + placeholder, + type = 'text', + required = false, + fieldType = 'input', + className = '', + labelClassName = '', + inputClassName = '', + errorClassName = '', + validation, + disabled = false, + readOnly = false, + autoComplete, + size = 'md', + render, +}: FormFieldProps) { + + // ============================================================================= + // HELPER FUNCTIONS + // ============================================================================= + + const getInputSize = (size: 'sm' | 'md' | 'lg') => { + switch (size) { + case 'sm': + return 'h-8 text-sm'; + case 'lg': + return 'h-12 text-base'; + default: + return 'h-10 text-sm'; + } + }; + + const getTextareaSize = (size: 'sm' | 'md' | 'lg') => { + switch (size) { + case 'sm': + return 'min-h-[80px] text-sm'; + case 'lg': + return 'min-h-[120px] text-base'; + default: + return 'min-h-[100px] text-sm'; + } + }; + + // ============================================================================= + // RENDER FUNCTIONS + // ============================================================================= + + const renderInput = (field: any, fieldState: { error?: FieldError }) => { + const inputProps = { + ...field, + type, + placeholder, + disabled, + readOnly, + autoComplete, + className: cn( + 'w-full transition-colors', + getInputSize(size), + fieldState.error && 'border-destructive focus:border-destructive', + inputClassName + ), + }; + + return ; + }; + + const renderTextarea = (field: any, fieldState: { error?: FieldError }) => { + const textareaProps = { + ...field, + placeholder, + disabled, + readOnly, + autoComplete, + className: cn( + 'w-full resize-none transition-colors', + getTextareaSize(size), + fieldState.error && 'border-destructive focus:border-destructive', + inputClassName + ), + }; + + return