Files
futur-web-app/web/frontend/src/pages/work/components/TodoForm.tsx
2026-01-24 14:59:05 +02:00

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>
</>
);
}