193 lines
5.4 KiB
TypeScript
193 lines
5.4 KiB
TypeScript
import { object, string, Schema } from 'yup';
|
|
import { useFormContext } from 'react-hook-form';
|
|
import { TodoWithStats, CreateTodoRequest, UpdateTodoRequest } from '../../../api/controller/todo';
|
|
import Button from '@greatness/components/src/Button';
|
|
import Modal from '@greatness/components/src/Modal';
|
|
import Form from '@greatness/components/src/form/Form';
|
|
import TextInput from '@greatness/components/src/form/text/TextInput';
|
|
|
|
interface TodoFormProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onSubmit: (data: CreateTodoRequest | UpdateTodoRequest) => Promise<void>;
|
|
todo?: TodoWithStats; // If provided, we're editing
|
|
isLoading?: boolean;
|
|
}
|
|
|
|
const DEFAULT_COLORS = [
|
|
'#3B82F6', // Blue
|
|
'#EF4444', // Red
|
|
'#10B981', // Green
|
|
'#F59E0B', // Yellow
|
|
'#8B5CF6', // Purple
|
|
'#EC4899', // Pink
|
|
'#06B6D4', // Cyan
|
|
'#84CC16', // Lime
|
|
'#F97316', // Orange
|
|
'#6366F1', // Indigo
|
|
];
|
|
|
|
const todoValidation: Schema<CreateTodoRequest> = object({
|
|
title: string()
|
|
.required('Title is required')
|
|
.max(200, 'Title must be less than 200 characters')
|
|
.trim(),
|
|
description: string()
|
|
.max(1000, 'Description must be less than 1000 characters')
|
|
.default(''),
|
|
color: string()
|
|
.required('Please select a color')
|
|
.matches(/^#[0-9A-F]{6}$/i, 'Please select a valid color'),
|
|
});
|
|
|
|
export default function TodoForm({ isOpen, onClose, onSubmit, todo, isLoading = false }: TodoFormProps) {
|
|
const defaultValues: CreateTodoRequest = {
|
|
title: todo?.title || '',
|
|
description: todo?.description || '',
|
|
color: todo?.color || DEFAULT_COLORS[0],
|
|
};
|
|
|
|
const handleFormSubmit = async (data: CreateTodoRequest) => {
|
|
try {
|
|
await onSubmit(data);
|
|
onClose();
|
|
} catch (error) {
|
|
console.error('Failed to save todo:', error);
|
|
throw error; // Let form handle the error
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Modal
|
|
isOpen={isOpen}
|
|
handleClose={onClose}
|
|
>
|
|
<div className="p-6">
|
|
<h2 className="text-xl font-semibold mb-4">
|
|
{todo ? 'Edit Todo' : 'Create New Todo'}
|
|
</h2>
|
|
|
|
<Form
|
|
defaultValues={defaultValues}
|
|
validation={todoValidation}
|
|
handleSubmit={handleFormSubmit}
|
|
className="space-y-4"
|
|
>
|
|
<TodoFormContent
|
|
isLoading={isLoading}
|
|
onClose={onClose}
|
|
todo={todo}
|
|
/>
|
|
</Form>
|
|
</div>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
// Separate component for form content to access form context
|
|
interface TodoFormContentProps {
|
|
isLoading: boolean;
|
|
onClose: () => void;
|
|
todo?: TodoWithStats;
|
|
}
|
|
|
|
function TodoFormContent({ isLoading, onClose, todo }: TodoFormContentProps) {
|
|
const { watch, setValue } = useFormContext<CreateTodoRequest>();
|
|
const watchedValues = watch();
|
|
|
|
return (
|
|
<>
|
|
{/* Title */}
|
|
<div>
|
|
<TextInput
|
|
name="title"
|
|
label="Title *"
|
|
/>
|
|
</div>
|
|
|
|
{/* Description */}
|
|
<div>
|
|
<TextInput
|
|
name="description"
|
|
label="Description"
|
|
type="textarea"
|
|
/>
|
|
</div>
|
|
|
|
{/* Color Selection */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-white mb-2">
|
|
Color *
|
|
</label>
|
|
<div className="flex flex-wrap gap-2">
|
|
{DEFAULT_COLORS.map((color) => (
|
|
<button
|
|
key={color}
|
|
type="button"
|
|
onClick={() => setValue('color', color)}
|
|
className={`w-8 h-8 rounded-full border-2 transition-all ${
|
|
watchedValues.color === color
|
|
? 'border-gray-800 scale-110'
|
|
: 'border-gray-300 hover:border-gray-500'
|
|
}`}
|
|
style={{ backgroundColor: color }}
|
|
title={color}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
{/* Custom color input */}
|
|
<div className="mt-2">
|
|
<input
|
|
type="color"
|
|
value={watchedValues.color || DEFAULT_COLORS[0]}
|
|
onChange={(e) => setValue('color', e.target.value)}
|
|
className="w-16 h-8 border border-gray-300 rounded cursor-pointer"
|
|
title="Choose custom color"
|
|
/>
|
|
<span className="ml-2 text-sm text-gray-600">{watchedValues.color}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Preview */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-white mb-2">
|
|
Preview
|
|
</label>
|
|
<div
|
|
className="p-3 rounded-lg border-2"
|
|
style={{
|
|
borderColor: watchedValues.color || DEFAULT_COLORS[0],
|
|
backgroundColor: `${watchedValues.color || DEFAULT_COLORS[0]}10`
|
|
}}
|
|
>
|
|
<div
|
|
className="w-full h-1 rounded-t-lg mb-2"
|
|
style={{ backgroundColor: watchedValues.color || DEFAULT_COLORS[0] }}
|
|
/>
|
|
<h4 className="font-semibold text-white">
|
|
{watchedValues.title || 'Todo Title'}
|
|
</h4>
|
|
{watchedValues.description && (
|
|
<p className="text-sm text-white mt-1">{watchedValues.description}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Form Actions */}
|
|
<div className="flex gap-3 pt-4">
|
|
<Button
|
|
type="submit"
|
|
isDisabled={isLoading}
|
|
label={isLoading ? 'Saving...' : (todo ? 'Update Todo' : 'Create Todo')}
|
|
/>
|
|
<Button
|
|
type="button"
|
|
onClick={onClose}
|
|
label="Cancel"
|
|
/>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|