opcotech/elemo

View on GitHub
web/components/todos/TodoDrawer.tsx

Summary

Maintainability
A
3 hrs
Test Coverage
'use client';

import { useCallback, useEffect, useState } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import useStore from '@/store';
import { Drawer } from '@/components/blocks/Drawer';
import { Button } from '@/components/blocks/Button';
import { UpdateTodoParams } from '@/store/todoSlice';
import { ListSkeleton } from '@/components/blocks/Skeleton/ListSkeleton';
import { TodoForm } from './TodoForm';
import { TodoList } from './TodoList';

type TodoListState = {
  editing?: string;
  deleting: { id: string; timer: NodeJS.Timeout | undefined }[];
  loading: string[];
};

export function TodoDrawer() {
  const [showNewTodoForm, setShowNewTodoForm] = useState(false);

  const [todos, show, toggleDrawer, fetchingTodos, fetchTodos, updateTodo] = useStore((state) => [
    state.todos,
    state.showing.todos,
    () => state.toggleDrawer('todos'),
    state.fetchingTodos,
    state.fetchTodos,
    state.updateTodo
  ]);

  const [state, setState] = useState<TodoListState>({
    editing: undefined,
    deleting: [],
    loading: []
  });

  useEffect(() => {
    if (show && !fetchingTodos && todos === undefined) fetchTodos();
  }, [show, fetchingTodos, fetchTodos, todos]);

  const setLoading = useCallback(
    (id: string, loading: boolean) =>
      setState((state) => ({
        ...state,
        loading: loading ? [...state.loading, id] : state.loading.filter((l) => l !== id)
      })),
    []
  );

  const handleUpdate = useCallback(
    async (id: string, todo: UpdateTodoParams) => {
      setLoading(id, true);
      await updateTodo(id, todo);
      setLoading(id, false);
    },
    [setLoading, updateTodo]
  );

  const handleEdit = useCallback((id: string, editing: boolean) => {
    setShowNewTodoForm(true);
    setState((state) => ({
      ...state,
      editing: editing ? id : undefined
    }));
  }, []);

  const handleDelete = useCallback(
    (id: string, deleting: boolean, timer?: NodeJS.Timeout) => {
      setState((state) => ({
        ...state,
        deleting: deleting
          ? [...state.deleting, { id, timer }]
          : state.deleting.filter((d) => {
              if (d.id === id && d.timer) clearTimeout(d.timer);
              return d.id !== id;
            })
      }));
      setLoading(id, deleting);
    },
    [setLoading]
  );

  const handleNewTodoFormOpen = useCallback(() => {
    setShowNewTodoForm(true);
  }, []);

  const handleNewTodoFormClose = useCallback(() => {
    setShowNewTodoForm(false);
    setState((state) => ({
      ...state,
      editing: undefined
    }));
  }, []);

  return (
    <Drawer id="todos" title="Todo list" show={show} toggle={toggleDrawer}>
      <div className="mt-4 mb-8">
        <AnimatePresence>
          {showNewTodoForm ? (
            <AnimatePresence>
              <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
                <TodoForm
                  editing={todos?.find((t) => t.id === state.editing)}
                  onCancel={() => handleEdit(state.editing!, false)}
                  onHide={handleNewTodoFormClose}
                />
              </motion.div>
            </AnimatePresence>
          ) : (
            <motion.div
              initial={{ opacity: 0 }}
              animate={{ opacity: 1, transition: { delay: 0.025 } }}
              exit={{ opacity: 0 }}
              className={
                'flex px-2 py-2 cursor-pointer rounded-md text-gray-500 hover:text-gray-600 bg-gray-50 hover:bg-gray-100'
              }
              onClick={handleNewTodoFormOpen}
            >
              <Button icon={'PlusCircleIcon'} size={'sm'} />
              <p className={'ml-2'}>Add new todo</p>
            </motion.div>
          )}
        </AnimatePresence>
      </div>

      <div className="px-3">
        {fetchingTodos && todos === undefined ? (
          <ListSkeleton count={5} fullWidth />
        ) : (
          <TodoList
            todos={todos || []}
            editing={state.editing}
            deleting={state.deleting}
            loading={state.loading}
            handleEdit={handleEdit}
            handleUpdate={handleUpdate}
            handleDelete={handleDelete}
          />
        )}
      </div>
    </Drawer>
  );
}