- Improved calendar event display with better visual distinction - Enhanced drag-and-drop feedback and visual cues - Added loading states and error handling - Improved responsive design and accessibility - Updated styling for consistency across components - Refined calendar settings UI with clearer visual feedback - Added better sync status indicators - Improved connection management with confirmation dialogs - Enhanced error handling and user feedback - Standardized task form styling with better validation - Improved form accessibility and user feedback - Added loading states for task operations - Enhanced task item styling with better visual hierarchy - Improved edit/delete functionality with icons - Added better hover states and transitions - Refined task list with better empty state messaging - Improved accessibility for screen readers - Enhanced loading indicators
221 lines
7.0 KiB
TypeScript
221 lines
7.0 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState } from 'react';
|
|
|
|
interface TaskFormData {
|
|
title: string;
|
|
description?: string;
|
|
startTime?: string;
|
|
endTime?: string;
|
|
dayOfWeek?: number;
|
|
}
|
|
|
|
interface TaskFormProps {
|
|
onSubmit: (task: TaskFormData) => void;
|
|
isLoading?: boolean;
|
|
}
|
|
|
|
const TaskForm: React.FC<TaskFormProps> = ({ onSubmit, isLoading = false }) => {
|
|
const [formData, setFormData] = useState<TaskFormData>({
|
|
title: '',
|
|
description: '',
|
|
startTime: '',
|
|
endTime: '',
|
|
dayOfWeek: 1 // Default to Monday
|
|
});
|
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
|
|
const validateForm = (): boolean => {
|
|
const newErrors: Record<string, string> = {};
|
|
|
|
if (!formData.title.trim()) {
|
|
newErrors.title = 'Title is required';
|
|
}
|
|
|
|
// Validate time format if provided
|
|
if (formData.startTime && !isValidTime(formData.startTime)) {
|
|
newErrors.startTime = 'Invalid time format';
|
|
}
|
|
|
|
if (formData.endTime && !isValidTime(formData.endTime)) {
|
|
newErrors.endTime = 'Invalid time format';
|
|
}
|
|
|
|
// Check if start time is before end time if both are provided
|
|
if (formData.startTime && formData.endTime) {
|
|
const start = parseTime(formData.startTime);
|
|
const end = parseTime(formData.endTime);
|
|
if (start >= end) {
|
|
newErrors.endTime = 'End time must be after start time';
|
|
}
|
|
}
|
|
|
|
setErrors(newErrors);
|
|
return Object.keys(newErrors).length === 0;
|
|
};
|
|
|
|
const isValidTime = (time: string): boolean => {
|
|
const timeRegex = /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/;
|
|
return timeRegex.test(time);
|
|
};
|
|
|
|
const parseTime = (time: string): number => {
|
|
const [hours, minutes] = time.split(':').map(Number);
|
|
return hours * 60 + minutes;
|
|
};
|
|
|
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
|
const { name, value } = e.target;
|
|
setFormData({
|
|
...formData,
|
|
[name]: value
|
|
});
|
|
|
|
// Clear error when user starts typing
|
|
if (errors[name]) {
|
|
setErrors(prev => {
|
|
const newErrors = { ...prev };
|
|
delete newErrors[name];
|
|
return newErrors;
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
if (validateForm()) {
|
|
onSubmit(formData);
|
|
// Reset form
|
|
setFormData({
|
|
title: '',
|
|
description: '',
|
|
startTime: '',
|
|
endTime: '',
|
|
dayOfWeek: 1
|
|
});
|
|
setErrors({});
|
|
}
|
|
};
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit} className="task-form bg-white rounded-lg shadow-md p-6 mb-6">
|
|
<h3 className="text-lg font-semibold text-gray-800 mb-4">Add New Task</h3>
|
|
|
|
<div className="form-group mb-4">
|
|
<label htmlFor="title" className="block text-sm font-medium text-gray-700 mb-1">
|
|
Task Title *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="title"
|
|
name="title"
|
|
value={formData.title}
|
|
onChange={handleChange}
|
|
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
|
errors.title ? 'border-red-500' : 'border-gray-300'
|
|
}`}
|
|
placeholder="What needs to be done?"
|
|
required
|
|
/>
|
|
{errors.title && <p className="mt-1 text-sm text-red-600">{errors.title}</p>}
|
|
</div>
|
|
|
|
<div className="form-group mb-4">
|
|
<label htmlFor="description" className="block text-sm font-medium text-gray-700 mb-1">
|
|
Description
|
|
</label>
|
|
<textarea
|
|
id="description"
|
|
name="description"
|
|
value={formData.description}
|
|
onChange={handleChange}
|
|
rows={3}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
placeholder="Add details..."
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
|
<div className="form-group">
|
|
<label htmlFor="startTime" className="block text-sm font-medium text-gray-700 mb-1">
|
|
Start Time
|
|
</label>
|
|
<input
|
|
type="time"
|
|
id="startTime"
|
|
name="startTime"
|
|
value={formData.startTime}
|
|
onChange={handleChange}
|
|
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
|
errors.startTime ? 'border-red-500' : 'border-gray-300'
|
|
}`}
|
|
/>
|
|
{errors.startTime && <p className="mt-1 text-sm text-red-600">{errors.startTime}</p>}
|
|
</div>
|
|
|
|
<div className="form-group">
|
|
<label htmlFor="endTime" className="block text-sm font-medium text-gray-700 mb-1">
|
|
End Time
|
|
</label>
|
|
<input
|
|
type="time"
|
|
id="endTime"
|
|
name="endTime"
|
|
value={formData.endTime}
|
|
onChange={handleChange}
|
|
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
|
errors.endTime ? 'border-red-500' : 'border-gray-300'
|
|
}`}
|
|
/>
|
|
{errors.endTime && <p className="mt-1 text-sm text-red-600">{errors.endTime}</p>}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="form-group mb-4">
|
|
<label htmlFor="dayOfWeek" className="block text-sm font-medium text-gray-700 mb-1">
|
|
Day of Week
|
|
</label>
|
|
<select
|
|
id="dayOfWeek"
|
|
name="dayOfWeek"
|
|
value={formData.dayOfWeek}
|
|
onChange={handleChange}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
>
|
|
<option value={0}>Sunday</option>
|
|
<option value={1}>Monday</option>
|
|
<option value={2}>Tuesday</option>
|
|
<option value={3}>Wednesday</option>
|
|
<option value={4}>Thursday</option>
|
|
<option value={5}>Friday</option>
|
|
<option value={6}>Saturday</option>
|
|
</select>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={isLoading}
|
|
className={`w-full py-2 px-4 rounded-md text-white font-medium transition-colors ${
|
|
isLoading
|
|
? 'bg-blue-400 cursor-not-allowed'
|
|
: 'bg-blue-600 hover:bg-blue-700'
|
|
}`}
|
|
>
|
|
{isLoading ? (
|
|
<span className="flex items-center justify-center">
|
|
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
Adding Task...
|
|
</span>
|
|
) : (
|
|
'Add Task'
|
|
)}
|
|
</button>
|
|
</form>
|
|
);
|
|
};
|
|
|
|
export default TaskForm; |