updates
This commit is contained in:
192
web/frontend/src/pages/work/components/TodoForm.tsx
Normal file
192
web/frontend/src/pages/work/components/TodoForm.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
import { object, string, Schema } from 'yup';
|
||||
import { useFormContext, Controller } 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user