- 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
160 lines
6.4 KiB
TypeScript
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>
|
|
);
|
|
} |