larryhudson / vite-todo-list

Example Vite project
0 stars 0 forks source link

Fix issue #54: feat: Implement drag and drop between 'today', 'tomorrow', and 'upcoming' groups #55

Closed larryhudson-aider-github[bot] closed 2 months ago

larryhudson-aider-github[bot] commented 2 months ago

This PR addresses the changes requested in issue #54

Thank you for providing the content of src/App.tsx. To implement the drag and drop functionality between 'today', 'tomorrow', and 'upcoming' groups, we need to make several changes to the file. I'll explain the changes and then provide the necessary SEARCH/REPLACE blocks.

  1. Update the DraggableItem component to accept todos from all groups.
  2. Modify the moveItem function to handle cross-group movements.
  3. Update the toggleTodo function to handle due date changes when moving between groups.
  4. Adjust the rendering of todo items to use a single DraggableItem component for all groups.

Here are the SEARCH/REPLACE blocks to implement these changes:

src/App.tsx

<<<<<<< SEARCH
interface DraggableItemProps {
  id: string;
  index: number;
  moveItem: (dragIndex: number, hoverIndex: number) => void;
  children: React.ReactNode;
  group: 'today' | 'tomorrow' | 'upcoming';
}

const DraggableItem: React.FC<DraggableItemProps> = ({ id, index, moveItem, children, group }) => {
  const ref = useRef<HTMLLIElement>(null)

  const [, drop] = useDrop({
    accept: `TODO_${group.toUpperCase()}`,
    hover(item: { id: string; index: number }, monitor) {
      if (!ref.current) {
        return
      }
      const dragIndex = item.index
      const hoverIndex = index
      if (dragIndex === hoverIndex) {
        return
      }
      moveItem(dragIndex, hoverIndex)
      item.index = hoverIndex
    },
  })

  const [{ isDragging }, drag, preview] = useDrag({
    type: `TODO_${group.toUpperCase()}`,
    item: () => ({ id, index }),
    collect: (monitor) => ({
      isDragging: monitor.isDragging(),
    }),
  })

  drag(drop(ref))

  return (
    <li ref={preview} style={{ opacity: isDragging ? 0.5 : 1 }}>
      <span ref={ref} className="drag-handle">☰</span>
      {children}
    </li>
  )
}
=======
interface DraggableItemProps {
  id: string;
  index: number;
  moveItem: (dragIndex: number, hoverIndex: number, fromGroup: string, toGroup: string) => void;
  children: React.ReactNode;
  group: 'today' | 'tomorrow' | 'upcoming';
}

const DraggableItem: React.FC<DraggableItemProps> = ({ id, index, moveItem, children, group }) => {
  const ref = useRef<HTMLLIElement>(null)

  const [, drop] = useDrop({
    accept: ['TODO_TODAY', 'TODO_TOMORROW', 'TODO_UPCOMING'],
    hover(item: { id: string; index: number; group: string }, monitor) {
      if (!ref.current) {
        return
      }
      const dragIndex = item.index
      const hoverIndex = index
      const fromGroup = item.group
      const toGroup = group

      if (dragIndex === hoverIndex && fromGroup === toGroup) {
        return
      }

      moveItem(dragIndex, hoverIndex, fromGroup, toGroup)
      item.index = hoverIndex
      item.group = toGroup
    },
  })

  const [{ isDragging }, drag, preview] = useDrag({
    type: `TODO_${group.toUpperCase()}`,
    item: () => ({ id, index, group }),
    collect: (monitor) => ({
      isDragging: monitor.isDragging(),
    }),
  })

  drag(drop(ref))

  return (
    <li ref={preview} style={{ opacity: isDragging ? 0.5 : 1 }}>
      <span ref={ref} className="drag-handle">☰</span>
      {children}
    </li>
  )
}
>>>>>>> REPLACE

Now, let's update the moveItem function:

src/App.tsx

<<<<<<< SEARCH
  const moveItem = useCallback((group: 'today' | 'tomorrow' | 'upcoming', dragIndex: number, hoverIndex: number) => {
    setTodos((prevTodos) => {
      const newTodos = [...prevTodos];
      let groupTodos: Todo[];
      if (group === 'today') {
        groupTodos = newTodos.filter(isDueOrOverdue);
      } else if (group === 'tomorrow') {
        groupTodos = newTodos.filter(todo => isTomorrow(todo.dueDate));
      } else {
        groupTodos = newTodos.filter(todo => !isDueOrOverdue(todo) && !isTomorrow(todo.dueDate));
      }

      const [draggedItem] = groupTodos.splice(dragIndex, 1);
      groupTodos.splice(hoverIndex, 0, draggedItem);

      // Create a map of todo IDs to their new positions within the group
      const todoPositions = new Map(groupTodos.map((todo, index) => [todo.id, index]));

      // Update the order of todos in the original array
      newTodos.sort((a, b) => {
        const posA = todoPositions.get(a.id);
        const posB = todoPositions.get(b.id);
        if (posA !== undefined && posB !== undefined) {
          return posA - posB;
        }
        return 0;
      });

      return newTodos;
    });
  }, []);
=======
  const moveItem = useCallback((dragIndex: number, hoverIndex: number, fromGroup: string, toGroup: string) => {
    setTodos((prevTodos) => {
      const newTodos = [...prevTodos];
      let fromGroupTodos: Todo[];
      let toGroupTodos: Todo[];

      if (fromGroup === 'today') {
        fromGroupTodos = newTodos.filter(isDueOrOverdue);
      } else if (fromGroup === 'tomorrow') {
        fromGroupTodos = newTodos.filter(todo => isTomorrow(todo.dueDate));
      } else {
        fromGroupTodos = newTodos.filter(todo => !isDueOrOverdue(todo) && !isTomorrow(todo.dueDate));
      }

      if (toGroup === 'today') {
        toGroupTodos = newTodos.filter(isDueOrOverdue);
      } else if (toGroup === 'tomorrow') {
        toGroupTodos = newTodos.filter(todo => isTomorrow(todo.dueDate));
      } else {
        toGroupTodos = newTodos.filter(todo => !isDueOrOverdue(todo) && !isTomorrow(todo.dueDate));
      }

      const [draggedItem] = fromGroupTodos.splice(dragIndex, 1);

      // Update the due date based on the new group
      const today = new Date();
      if (toGroup === 'today') {
        draggedItem.dueDate = today;
      } else if (toGroup === 'tomorrow') {
        const tomorrow = new Date(today);
        tomorrow.setDate(tomorrow.getDate() + 1);
        draggedItem.dueDate = tomorrow;
      } else if (toGroup === 'upcoming') {
        const nextWeek = new Date(today);
        nextWeek.setDate(nextWeek.getDate() + 7);
        draggedItem.dueDate = nextWeek;
      }

      toGroupTodos.splice(hoverIndex, 0, draggedItem);

      // Create a map of todo IDs to their new positions within the group
      const todoPositions = new Map(toGroupTodos.map((todo, index) => [todo.id, index]));

      // Update the order of todos in the original array
      newTodos.sort((a, b) => {
        const posA = todoPositions.get(a.id);
        const posB = todoPositions.get(b.id);
        if (posA !== undefined && posB !== undefined) {
          return posA - posB;
        }
        return 0;
      });

      return newTodos;
    });
  }, []);
>>>>>>> REPLACE

Now, let's update the rendering of todo items to use a single DraggableItem component for all groups:

src/App.tsx

<<<<<<< SEARCH
        {filterTodos(todayTodos).length > 0 && (
          <>
            <h2>Today</h2>
            <ul>
              {filterTodos(todayTodos).map((todo, index) => (
                <DraggableItem key={todo.id} id={todo.id} index={index} moveItem={(dragIndex, hoverIndex) => moveItem('today', dragIndex, hoverIndex)} group="today">
                  <input
                    type="checkbox"
                    checked={todo.completed}
                    onChange={() => toggleTodo(todo.id)}
                  />
                  <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
                    {todo.text} (Due: {todo.dueDate.toLocaleDateString()})
                  </span>
                  {completedTodoId === todo.id && (
                    <span className="completion-message" style={{ textDecoration: 'none' }}>Good job!</span>
                  )}
                  <button onClick={() => setEditingTodo(todo)}>Edit</button>
                  <button onClick={() => deleteTodo(todo.id)}>Delete</button>
                </DraggableItem>
              ))}
            </ul>
          </>
        )}
        {filterTodos(tomorrowTodos).length > 0 && (
          <>
            <h2>Tomorrow</h2>
            <ul>
              {filterTodos(tomorrowTodos).map((todo, index) => (
                <DraggableItem key={todo.id} id={todo.id} index={index} moveItem={(dragIndex, hoverIndex) => moveItem('tomorrow', dragIndex, hoverIndex)} group="tomorrow">
                  <input
                    type="checkbox"
                    checked={todo.completed}
                    onChange={() => toggleTodo(todo.id)}
                  />
                  <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
                    {todo.text} (Due: {todo.dueDate.toLocaleDateString()})
                  </span>
                  <button onClick={() => deleteTodo(todo.id)}>Delete</button>
                </DraggableItem>
              ))}
            </ul>
          </>
        )}
        {filterTodos(upcomingTodos).length > 0 && (
          <>
            <h2>Upcoming</h2>
            <ul>
              {filterTodos(upcomingTodos).map((todo, index) => (
                <DraggableItem key={todo.id} id={todo.id} index={index} moveItem={(dragIndex, hoverIndex) => moveItem('upcoming', dragIndex, hoverIndex)} group="upcoming">
                  <input
                    type="checkbox"
                    checked={todo.completed}
                    onChange={() => toggleTodo(todo.id)}
                  />
                  <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
                    {todo.text} (Due: {todo.dueDate.toLocaleDateString()})
                  </span>
                  <button onClick={() => deleteTodo(todo.id)}>Delete</button>
                </DraggableItem>
              ))}
            </ul>
          </>
        )}
=======
        {['today', 'tomorrow', 'upcoming'].map((group) => {
          const todos = group === 'today' ? todayTodos :
                        group === 'tomorrow' ? tomorrowTodos : upcomingTodos;
          return filterTodos(todos).length > 0 && (
            <React.Fragment key={group}>
              <h2>{group.charAt(0).toUpperCase() + group.slice(1)}</h2>
              <ul>
                {filterTodos(todos).map((todo, index) => (
                  <DraggableItem 
                    key={todo.id} 
                    id={todo.id} 
                    index={index} 
                    moveItem={(dragIndex, hoverIndex, fromGroup, toGroup) => moveItem(dragIndex, hoverIndex, fromGroup, toGroup)} 
                    group={group as 'today' | 'tomorrow' | 'upcoming'}
                  >
                    <input
                      type="checkbox"
                      checked={todo.completed}
                      onChange={() => toggleTodo(todo.id)}
                    />
                    <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
                      {todo.text} (Due: {todo.dueDate.toLocaleDateString()})
                    </span>
                    {completedTodoId === todo.id && (
                      <span className="completion-message" style={{ textDecoration: 'none' }}>Good job!</span>
                    )}
                    <button onClick={() => setEditingTodo(todo)}>Edit</button>
                    <button onClick={() => deleteTodo(todo.id)}>Delete</button>
                  </DraggableItem>
                ))}
              </ul>
            </React.Fragment>
          );
        })}
>>>>>>> REPLACE

These changes will allow dragging and dropping todo items between the 'today', 'tomorrow', and 'upcoming' groups. The due dates will be updated accordingly when items are moved between groups. The 'today' group will set the due date to today, 'tomorrow' will set it to tomorrow, and 'upcoming' will set it to 7 days from today.