My-Weekly-ToDo-List/src/components/TaskItem.tsx
mARTin 83d0d0b920 fix: implement custom CSS sync spinner and update all components
- Replaced broken animate-spin Tailwind class with custom weekly-spinner CSS

- Standardized loading indicators across Auth, Tasks, and Calendar settings

- Fixed JSX syntax errors in several components
2026-02-23 22:04:16 +01:00

160 lines
6.4 KiB
TypeScript

import { useState } from 'react';
interface Task {
id: string;
title: string;
description?: string;
completed: boolean;
isRolling?: boolean;
createdAt: Date;
updatedAt: Date;
}
interface TaskItemProps {
task: Task;
onToggleComplete: (id: string) => void;
onDelete: (id: string) => void;
onEdit: (task: Task) => void;
onToggleRolling: (id: string) => void;
variant?: 'default' | 'minimal';
}
export default function TaskItem({ task, onToggleComplete, onDelete, onEdit, onToggleRolling, variant = 'default' }: TaskItemProps) {
const [isEditing, setIsEditing] = useState(false);
const [editedTitle, setEditedTitle] = useState(task.title);
const [editedDescription, setEditedDescription] = useState(task.description || '');
const [isDeleting, setIsDeleting] = useState(false);
const handleSave = () => {
onEdit({ ...task, title: editedTitle, description: editedDescription });
setIsEditing(false);
};
const handleCancel = () => {
setEditedTitle(task.title);
setEditedDescription(task.description || '');
setIsEditing(false);
};
const handleDelete = () => {
setIsDeleting(true);
// Add a small delay to allow the UI to update
setTimeout(() => {
onDelete(task.id);
setIsDeleting(false);
}, 100);
};
if (isEditing) {
return (
<div className="border border-gray-200 rounded-lg p-4 mb-2 bg-white shadow-sm transition-all duration-200">
<div className="mb-3">
<input
type="text"
value={editedTitle}
onChange={(e) => setEditedTitle(e.target.value)}
className="w-full p-2 border border-gray-300 rounded-md text-lg font-semibold focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Task title"
/>
</div>
<div className="mb-4">
<textarea
value={editedDescription}
onChange={(e) => setEditedDescription(e.target.value)}
className="w-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
rows={2}
placeholder="Task description"
/>
</div>
<div className="flex justify-end space-x-2">
<button
onClick={handleCancel}
className="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition-colors"
>
Cancel
</button>
<button
onClick={handleSave}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
Save
</button>
</div>
</div>
);
}
const containerClass = variant === 'minimal'
? `flex items-center group py-1 border-b border-gray-100 hover:bg-gray-50 transition-colors ${isDeleting ? 'opacity-50' : ''}`
: `border border-gray-200 rounded-lg p-4 mb-2 shadow-sm transition-all duration-200 ${isDeleting ? 'opacity-50' : 'hover:shadow-md'}`;
return (
<div className={containerClass}>
<div className={`flex items-start ${variant === 'minimal' ? 'w-full' : ''}`}>
<div className={`flex items-center mr-3 ${variant === 'minimal' ? '' : 'mt-1'} space-x-2`}>
{/* Rolling Toggle - hide in minimal unless hovered or active? Or keep it? App usually has verify simple. Keep it for now. */}
<button
onClick={() => onToggleRolling(task.id)}
className={`p-1 rounded-full hover:bg-gray-100 transition-colors ${task.isRolling ? 'text-blue-600' : 'text-gray-300'}`}
title={task.isRolling ? "Disable rolling" : "Enable rolling"}
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21.5 2v6h-6M21.34 15.57a10 10 0 1 1-.57-8.38" />
</svg>
</button>
<input
type="checkbox"
checked={task.completed}
onChange={() => onToggleComplete(task.id)}
className="h-5 w-5 text-blue-600 rounded focus:ring-blue-500"
aria-label={task.completed ? "Mark task as incomplete" : "Mark task as complete"}
/>
</div>
<div className="flex-1">
<div className={`font-semibold ${variant === 'minimal' ? 'text-xs' : 'text-lg'} ${task.completed ? 'line-through text-gray-500' : 'text-gray-800'}`}>
{task.title}
</div>
{task.description && (
<p className={`mt-2 ${task.completed ? 'line-through text-gray-500' : 'text-gray-700'}`}>
{task.description}
</p>
)}
<div className="flex items-center mt-3 text-xs text-gray-500">
<span>Created: {new Date(task.createdAt).toLocaleDateString()}</span>
{task.updatedAt && task.updatedAt.getTime() !== task.createdAt.getTime() && (
<span className="ml-3">Updated: {new Date(task.updatedAt).toLocaleDateString()}</span>
)}
</div>
</div>
<div className="flex space-x-2 ml-2">
<button
onClick={() => {
setIsEditing(true);
}}
className="p-2 text-gray-500 hover:text-blue-600 rounded-full hover:bg-gray-100 transition-colors"
aria-label="Edit task"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
</svg>
</button>
<button
onClick={handleDelete}
disabled={isDeleting}
className="p-2 text-gray-500 hover:text-red-600 rounded-full hover:bg-gray-100 transition-colors"
aria-label="Delete task"
>
{isDeleting ? (
<div className="weekly-spinner"></div>
) : (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
)}
</button>
</div>
</div>
</div>
);
}