- Extended Task type with recurring, category, reminder, and priority properties - Improved TaskCard component with new UI elements for advanced features - Enhanced TaskForm component with inputs for advanced features - Updated CalendarGrid component to handle improved task rendering - Updated taskPersistence with handling for new properties SUMMARY: .planning/phases/02-user-experience-advanced-features/02-01-SUMMARY.md
212 lines
8.0 KiB
TypeScript
212 lines
8.0 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
||
import { Task } from '../types/task';
|
||
|
||
interface TaskFormProps {
|
||
task?: Task;
|
||
onSave: (task: Task) => void;
|
||
onCancel: () => void;
|
||
}
|
||
|
||
const TaskForm: React.FC<TaskFormProps> = ({ task, onSave, onCancel }) => {
|
||
const [title, setTitle] = useState(task?.title || '');
|
||
const [description, setDescription] = useState(task?.description || '');
|
||
const [date, setDate] = useState(task?.date ? new Date(task.date).toISOString().split('T')[0] : '');
|
||
const [completed, setCompleted] = useState(task?.completed || false);
|
||
const [category, setCategory] = useState(task?.category || '');
|
||
const [priority, setPriority] = useState<Task['priority']>(task?.priority || 'medium');
|
||
const [recurring, setRecurring] = useState<Task['recurring']>(task?.recurring || undefined);
|
||
const [reminder, setReminder] = useState(task?.reminder ? new Date(task.reminder).toISOString().split('T')[0] : '');
|
||
const [tags, setTags] = useState<string[]>(task?.tags || []);
|
||
|
||
const handleSubmit = (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
|
||
const newTask: Task = {
|
||
id: task?.id || Date.now().toString(),
|
||
title,
|
||
description,
|
||
date: new Date(date),
|
||
completed,
|
||
category,
|
||
priority,
|
||
recurring,
|
||
reminder: reminder ? new Date(reminder) : undefined,
|
||
tags
|
||
};
|
||
|
||
onSave(newTask);
|
||
};
|
||
|
||
const handleTagAdd = (e: React.KeyboardEvent) => {
|
||
if (e.key === 'Enter' && (e.target as HTMLInputElement).value.trim() !== '') {
|
||
const tagValue = (e.target as HTMLInputElement).value.trim();
|
||
if (!tags.includes(tagValue)) {
|
||
setTags([...tags, tagValue]);
|
||
}
|
||
(e.target as HTMLInputElement).value = '';
|
||
}
|
||
};
|
||
|
||
const handleTagRemove = (index: number) => {
|
||
setTags(tags.filter((_, i) => i !== index));
|
||
};
|
||
|
||
return (
|
||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||
<div className="bg-white rounded-lg shadow-xl w-full max-w-md">
|
||
<div className="p-6">
|
||
<h2 className="text-xl font-bold text-gray-900 mb-4">
|
||
{task ? 'Edit Task' : 'Add New Task'}
|
||
</h2>
|
||
|
||
<form onSubmit={handleSubmit}>
|
||
<div className="mb-4">
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Title *
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={title}
|
||
onChange={(e) => setTitle(e.target.value)}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div className="mb-4">
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Description
|
||
</label>
|
||
<textarea
|
||
value={description}
|
||
onChange={(e) => setDescription(e.target.value)}
|
||
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"
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Date *
|
||
</label>
|
||
<input
|
||
type="date"
|
||
value={date}
|
||
onChange={(e) => setDate(e.target.value)}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Priority
|
||
</label>
|
||
<select
|
||
value={priority}
|
||
onChange={(e) => setPriority(e.target.value as Task['priority'])}
|
||
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="low">Low</option>
|
||
<option value="medium">Medium</option>
|
||
<option value="high">High</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mb-4">
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Category
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={category}
|
||
onChange={(e) => setCategory(e.target.value)}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
placeholder="Work, Personal, etc."
|
||
/>
|
||
</div>
|
||
|
||
<div className="mb-4">
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Tags
|
||
</label>
|
||
<div className="flex flex-wrap gap-2 mb-2">
|
||
{tags.map((tag, index) => (
|
||
<span
|
||
key={index}
|
||
className="bg-indigo-100 text-indigo-800 text-xs px-2 py-1 rounded flex items-center"
|
||
>
|
||
{tag}
|
||
<button
|
||
type="button"
|
||
onClick={() => handleTagRemove(index)}
|
||
className="ml-1 text-indigo-600 hover:text-indigo-800"
|
||
>
|
||
×
|
||
</button>
|
||
</span>
|
||
))}
|
||
</div>
|
||
<input
|
||
type="text"
|
||
placeholder="Press Enter to add tag"
|
||
onKeyDown={handleTagAdd}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Recurring
|
||
</label>
|
||
<select
|
||
value={recurring || ''}
|
||
onChange={(e) => setRecurring(e.target.value ? e.target.value as Task['recurring'] : undefined)}
|
||
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="">None</option>
|
||
<option value="daily">Daily</option>
|
||
<option value="weekly">Weekly</option>
|
||
<option value="monthly">Monthly</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Reminder Date
|
||
</label>
|
||
<input
|
||
type="date"
|
||
value={reminder}
|
||
onChange={(e) => setReminder(e.target.value)}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex justify-end space-x-3">
|
||
<button
|
||
type="button"
|
||
onClick={onCancel}
|
||
className="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50"
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button
|
||
type="submit"
|
||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
>
|
||
{task ? 'Update Task' : 'Add Task'}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default TaskForm; |