Creating a Drag-and-Drop Todo List in React: A Complete Tutorial
Tags: react, todolist, uidesign
Published: 2024-03-04 04:56:37

{% embed https://youtu.be/GicEFePmO4U %}

🐙 [GitHub](https://github.com/radzionc/radzionkit)  |  🎮 [Demo](https://increaser.org)

### Building a Custom Todo List Feature in Increaser: From Backend to UI Design

In this post, we explore the development of a todo list feature for the productivity app [Increaser](https://increaser.org/), covering a range of topics including backend setup, responsive UI design, drag-and-drop functionality, visualization of completed tasks, and deadline management. Our goal is to build this feature without using external component libraries, showcasing how to create a custom solution from scratch. While the [Increaser](https://increaser.org/) codebase is private, we've made all reusable components, utilities, and hooks available in the [RadzionKit](https://github.com/radzionc/radzionkit) repository. We'll provide detailed explanations of [Increaser](https://increaser.org/)-specific code throughout this blog post.

![TO-DO list](https://cdn-images-1.medium.com/proxy/1*8ccyohECND-q0qaTzG6VdA.jpeg)

### Enhancing Productivity for Remote Workers: The Strategic Addition of a Todo List to Increaser

Before delving into the technical specifics, it's essential to define the features we aim to include and the reasons behind them. [Increaser](https://increaser.org/), a productivity platform for remote workers, already offers a range of tools such as a focus timer, habit tracker, time tracker, and scheduler. Adding a todo list feature further enhances its functionality, making [Increaser](https://increaser.org/) a one-stop productivity hub. However, designing an effective todo list is not without its challenges. Instead of replicating the comprehensive features of established todo list apps like Todoist, our approach concentrates on identifying and refining core features that cater to the majority of use-cases.

Here's a concise overview of the planned features:

1. **Deadline Categorization:** Tasks are divided into four groups: today, tomorrow, this week, and next week. This approach strikes a balance between user needs and UI simplicity, avoiding the complexity of more intricate scheduling systems.
2. **Efficient Task Addition:** The "Add task" button under each deadline category allows for quick task entry. Users can add tasks sequentially by pressing the "Enter" key, facilitating rapid task addition without unnecessary clicks.
3. **Flexible Task Management:** The interface design enables easy reassignment of tasks between different time frames and prioritization within categories, enhancing organizational flexibility.
4. **Mobile Accessibility:** Basic mobile access is provided to facilitate task management on-the-go.
5. **Automatic Task Cleanup:** Completed tasks are moved to a separate section and automatically removed at the beginning of the next week, maintaining a clean interface.
6. **Streamlined Task Editing:** Users can simply click on a task to edit its name, minimizing the need for additional interactions.

### Simplifying Backend Development: DynamoDB Integration for Todo List Feature in Increaser

The backend setup, being the simplest part of our implementation, uses DynamoDB at [Increaser](https://increaser.org/). We have a `users` table, where each user's data is stored in a single item. Inside this item, the `tasks` attribute contains the todo list data, organized as a record of tasks, each with a unique ID. Using a record structure instead of a list allows for direct updates to tasks by their ID, avoiding the need to iterate through a list.

The `Task` entity has the following structure:

- `startedAt`: The timestamp of when the task was created.
- `id`: A unique identifier for the task.
- `name`: The name of the task.
- `completedAt`: The timestamp of when the task was completed, or `null` if it's incomplete.
- `deadlineAt`: The timestamp of the task's deadline.
- `order`: The order of the task within its category.

```tsx
export type Task = {
  startedAt: number
  id: string
  name: string
  completedAt?: number | null
  deadlineAt: number
  order: number
}
```

[Increaser](https://increaser.org/)'s API deviates from traditional RESTful and GraphQL paradigms. If you're interested in exploring backend development with ease, consider reading this [blog post](https://radzion.com/blog/api) for more insights. To manage tasks, we use three resolvers: `createTask`, `updateTask`, and `deleteTask`. Each resolver acts as a straightforward wrapper around the `tasksDB` module, which handles direct interactions with the database.

```tsx
import { assertUserId } from "../../auth/assertUserId"
import * as tasksDB from "@increaser/db/task"
import { ApiResolver } from "../../resolvers/ApiResolver"

export const updateTask: ApiResolver<"updateTask"> = async ({
  input,
  context,
}) => {
  const userId = assertUserId(context)

  const { id, fields } = input

  const task = tasksDB.updateTask(userId, id, fields)

  return task
}
```

For example, the `updateTask` resolver is designed to accept an `id` and a `fields` object containing the updates. Internally, the corresponding function within the `tasksDB` module retrieves the specific task from the user's item using a projection expression. It then merges the incoming fields with the existing task data before invoking the `putTask` function. This function is responsible for updating the task in the database by reassigning it with its unique ID.

```tsx
import { GetCommand, UpdateCommand } from "@aws-sdk/lib-dynamodb"
import { Task } from "@increaser/entities/Task"
import { getUserItemParams } from "./user"
import { dbDocClient } from "@lib/dynamodb/client"

export const putTask = async (userId: string, task: Task) => {
  const command = new UpdateCommand({
    ...getUserItemParams(userId),
    UpdateExpression: `set tasks.#id = :task`,
    ExpressionAttributeValues: {
      ":task": task,
    },
    ExpressionAttributeNames: {
      "#id": task.id,
    },
  })

  return dbDocClient.send(command)
}

export const getTask = async (userId: string, taskId: string) => {
  const command = new GetCommand({
    ...getUserItemParams(userId),
    ProjectionExpression: "tasks.#id",
    ExpressionAttributeNames: {
      "#id": taskId,
    },
  })
  const { Item } = await dbDocClient.send(command)

  if (!Item) {
    throw new Error(`No user with id=${userId}`)
  }

  return Item.tasks[taskId] as Task
}

export const updateTask = async (
  userId: string,
  taskId: string,
  fields: Partial<Omit<Task, "id">>
) => {
  const task = await getTask(userId, taskId)

  const newTask = {
    ...task,
    ...fields,
  }

  await putTask(userId, newTask)

  return newTask
}

export const deleteTask = async (userId: string, taskId: string) => {
  const comand = new UpdateCommand({
    ...getUserItemParams(userId),
    UpdateExpression: "REMOVE tasks.#id",
    ExpressionAttributeNames: {
      "#id": taskId,
    },
  })
  return dbDocClient.send(comand)
}
```

The `organizeTasks` function plays a crucial role in maintaining the cleanliness of the todo list by automatically filtering out tasks that were completed before the onset of the current week. If this operation results in a change in the number of tasks, the user's `tasks` field in the database is updated accordingly. To optimize resource utilization and avoid additional costs associated with running a cron job, this function is invoked on a user query request.

```tsx
import { getUser, updateUser } from "@increaser/db/user"
import { getRecordSize } from "@lib/utils/record/getRecordSize"
import { recordFilter } from "@lib/utils/record/recordFilter"
import { getWeekStartedAt } from "@lib/utils/time/getWeekStartedAt"
import { inTimeZone } from "@lib/utils/time/inTimeZone"

export const organizeTasks = async (userId: string) => {
  const { tasks: oldTasks, timeZone } = await getUser(userId, [
    "tasks",
    "timeZone",
  ])

  const weekStartedAt = inTimeZone(getWeekStartedAt(Date.now()), timeZone)

  const tasks = recordFilter(oldTasks, ({ value }) => {
    if (!value.completedAt) return true

    return value.completedAt >= weekStartedAt
  })

  if (getRecordSize(tasks) !== getRecordSize(oldTasks)) {
    await updateUser(userId, {
      tasks,
    })
  }

  return tasks
}
```

### Frontend Development for Increaser's Todo List: Enhancing User Experience with NextJS

Now, let's explore the frontend implementation details. [Increaser](https://increaser.org/) is built as a Static Site Generation (SSG) application using NextJS, and we've designated a specific page for the to-do list feature accessible via the `/tasks` route.

```tsx
import { VStack } from "@lib/ui/layout/Stack"
import { FixedWidthContent } from "../../components/reusable/fixed-width-content"
import { PageTitle } from "../../ui/PageTitle"
import { UserStateOnly } from "../../user/state/UserStateOnly"
import { TasksDone } from "./TasksDone"
import { TasksToDo } from "./TasksToDo"
import {
  RenderTasksView,
  TasksViewProvider,
  TasksViewSelector,
} from "./TasksView"
import { TasksDeadlinesOverview } from "./TasksDeadlinesOverview"

export const TasksPage = () => {
  return (
    <FixedWidthContent>
      <TasksViewProvider>
        <PageTitle documentTitle={`✅ Tasks`} title={<TasksViewSelector />} />
        <VStack gap={40} style={{ maxWidth: 520 }}>
          <UserStateOnly>
            <TasksDeadlinesOverview />
            <RenderTasksView
              todo={() => <TasksToDo />}
              done={() => <TasksDone />}
            />
          </UserStateOnly>
        </VStack>
      </TasksViewProvider>
    </FixedWidthContent>
  )
}
```

To segregate completed tasks from the active to-do list, we provide a seamless user experience by enabling users to toggle between these two views. For this purpose, we use the `getViewSetup` utility. This utility streamlines view state management by accepting the default view and a name for the view as arguments. It returns a provider, a hook, and a render function. The provider makes the view state accessible throughout the component tree, while the hook allows components to access and update the current view. The render function dynamically displays the appropriate view based on the current state, ensuring efficient rendering. This setup simplifies the management of multiple views in the code, enhancing the maintainability and readability of the view-related logic.

```tsx
import { ReactNode, createContext, useState } from "react"
import { ComponentWithChildrenProps } from "../props"
import { createContextHook } from "../state/createContextHook"

type GetViewSetupInput<T extends string | number | symbol> = {
  defaultView: T
  name: string
}

export function getViewSetup<T extends string | number | symbol>({
  defaultView,
  name,
}: GetViewSetupInput<T>) {
  interface ViewState {
    view: T
    setView: (view: T) => void
  }

  const ViewContext = createContext<ViewState | undefined>(undefined)

  const ViewProvider = ({ children }: ComponentWithChildrenProps) => {
    const [view, setView] = useState<T>(defaultView)

    return (
      <ViewContext.Provider value={{ view, setView }}>
        {children}
      </ViewContext.Provider>
    )
  }

  const useView = createContextHook(ViewContext, `${name}ViewContent`)

  const RenderView = (props: Record<T, () => ReactNode>) => {
    const { view } = useView()
    const render = props[view]

    return <>{render()}</>
  }

  return {
    ViewProvider,
    useView,
    RenderView,
  }
}
```

Our Tasks view supports two states: "todo" and "done," with "todo" set as the default view, aptly named "tasks." The `TasksViewSelector` component is responsible for rendering the view selector, enabling users to toggle between these two states. Meanwhile, the `PageTitleNavigation` component is employed to integrate the view selector into the UI.

```tsx
import { getViewSetup } from "@lib/ui/view/getViewSetup"
import { PageTitleNavigation } from "@lib/ui/navigation/PageTitleNavigation"

export const tasksViews = ["todo", "done"] as const
export type TasksView = (typeof tasksViews)[number]

export const {
  ViewProvider: TasksViewProvider,
  useView: useTasksView,
  RenderView: RenderTasksView,
} = getViewSetup<TasksView>({
  defaultView: "todo",
  name: "tasks",
})

const taskViewName: Record<TasksView, string> = {
  todo: "To Do",
  done: "Done",
}

export const TasksViewSelector = () => {
  const { view, setView } = useTasksView()

  return (
    <PageTitleNavigation
      value={view}
      options={tasksViews}
      onChange={setView}
      getOptionName={(option) => taskViewName[option]}
    />
  )
}
```

The `PageTitleNavigation` component is designed to mirror the appearance of a page title in other sections by embedding it within the `PageTitle` component. This parent component sets the text size, weight, and color, ensuring that the `PageTitleNavigation` component maintains a consistent look with titles across the application. The options within the component are displayed in a flexbox layout and separated by a line for clear visual distinction.

```tsx
import styled, { css } from "styled-components"
import { transition } from "@lib/ui/css/transition"
import { UnstyledButton } from "@lib/ui/buttons/UnstyledButton"
import { HStackSeparatedBy } from "@lib/ui/layout/StackSeparatedBy"
import { Text } from "@lib/ui/text"
import { InputProps } from "../props"

const ViewOption = styled(UnstyledButton)<{ isSelected: boolean }>`
  color: ${({ isSelected, theme }) =>
    (isSelected ? theme.colors.text : theme.colors.textShy).toCssValue()};

  ${transition}

  ${({ isSelected, theme }) =>
    !isSelected &&
    css`
      &:hover {
        color: ${theme.colors.textSupporting.toCssValue()};
      }
    `}
`

type PageTitleNavigationProps<T extends string> = InputProps<T> & {
  options: readonly T[]
  getOptionName: (option: T) => string
}

export function PageTitleNavigation<T extends string>({
  value,
  getOptionName,
  options,
  onChange,
}: PageTitleNavigationProps<T>) {
  return (
    <HStackSeparatedBy separator={<Text color="shy">|</Text>}>
      {options.map((v) => (
        <ViewOption
          onClick={() => onChange(v)}
          isSelected={v === value}
          key={value}
        >
          {getOptionName(v)}
        </ViewOption>
      ))}
    </HStackSeparatedBy>
  )
}
```

### Implementing a Responsive Todo List UI in Increaser: Flexbox, react-query, and CSS Grid Techniques

In our `TasksPage`, the content is neatly wrapped within a flexbox container that has a maximum width of 520px. This intentional design choice prevents the content from stretching across the entire screen width, thereby enhancing usability. This is particularly beneficial when buttons appear next to todo items on hover, ensuring a clean and user-friendly interface. Additionally, we integrate the `UserStateOnly` component to guarantee that content is displayed only after user data has been successfully fetched. At [Increaser](https://increaser.org/), we employ a comprehensive query to retrieve nearly all necessary user data. Although this query is blocking, users typically experience negligible delays due to the efficient caching of user data in local storage through react-query. This strategy ensures that the app's functionality is readily available upon subsequent launches, providing a seamless user experience.

```tsx
import { ComponentWithChildrenProps } from "@lib/ui/props"

import { useEffect } from "react"
import { useAuthRedirect } from "@increaser/app/auth/hooks/useAuthRedirect"
import { useAuthSession } from "@increaser/app/auth/hooks/useAuthSession"
import { useUserState } from "@increaser/ui/user/UserStateContext"

export const UserStateOnly = ({ children }: ComponentWithChildrenProps) => {
  const { state } = useUserState()
  const { toAuthenticationPage } = useAuthRedirect()

  const [authSession] = useAuthSession()

  useEffect(() => {
    if (!authSession) {
      toAuthenticationPage()
    }
  }, [authSession, toAuthenticationPage])

  return state ? <>{children}</> : null
}
```

The `TasksDeadlinesOverview` widget offers a concise summary of the current and upcoming week, showcasing the start and end dates, the current weekday, and a graphical representation of completed tasks. As tasks are marked as completed, the corresponding circles in the visualization turn green, providing an instant visual indication of progress.

```tsx
import { useStartOfWeek } from "@lib/ui/hooks/useStartOfWeek"
import { WeekDeadlinesOverview } from "./WeekDeadlinesOverview"
import { convertDuration } from "@lib/utils/time/convertDuration"
import { splitBy } from "@lib/utils/array/splitBy"
import { useAssertUserState } from "@increaser/ui/user/UserStateContext"
import styled from "styled-components"
import { UniformColumnGrid } from "@lib/ui/layout/UniformColumnGrid"
import { getColor } from "@lib/ui/theme/getters"
import { borderRadius } from "@lib/ui/css/borderRadius"

const Container = styled(UniformColumnGrid)`
  ${borderRadius.m};
  overflow: hidden;
  > * {
    padding: 20px;
    background: ${getColor("foreground")};
  }
`

export const TasksDeadlinesOverview = () => {
  const weekStartedAt = useStartOfWeek()
  const nextWeekStartsAt = weekStartedAt + convertDuration(1, "w", "ms")
  const { tasks } = useAssertUserState()
  const [thisWeekTasks, nextWeekTasks] = splitBy(Object.values(tasks), (task) =>
    task.deadlineAt > nextWeekStartsAt ? 1 : 0
  )

  return (
    <Container gap={1} minChildrenWidth={240}>
      <WeekDeadlinesOverview
        name="This week"
        tasks={thisWeekTasks}
        startedAt={weekStartedAt}
      />
      <WeekDeadlinesOverview
        name="Next week"
        tasks={nextWeekTasks}
        startedAt={nextWeekStartsAt}
        showWorkdays={false}
      />
    </Container>
  )
}
```

To enhance the responsiveness of the `TasksDeadlinesOverview`, we utilize the `UniformColumnGrid` component, which streamlines the creation of CSS grid layouts. This component ensures that all grid columns are of equal width, regardless of their content. By setting a `minChildrenWidth` property, it guarantees that grid items will wrap to a new row when there is insufficient space to accommodate them in a single line. Additionally, to visually distinguish between the two weeks, we apply backgrounds to the child elements rather than the parent container, creating a clear separation.

```tsx
import styled, { css } from "styled-components"
import { toSizeUnit } from "../css/toSizeUnit"

interface UniformColumnGridProps {
  gap: number
  minChildrenWidth?: number
  maxChildrenWidth?: number
  childrenWidth?: number
  rowHeight?: number
  fullWidth?: boolean
  maxColumns?: number
}

const getColumnMax = (maxColumns: number | undefined, gap: number) => {
  if (!maxColumns) return `0px`

  const gapCount = maxColumns - 1
  const totalGapWidth = `calc(${gapCount} * ${toSizeUnit(gap)})`

  return `calc((100% - ${totalGapWidth}) / ${maxColumns})`
}

const getColumnWidth = ({
  minChildrenWidth,
  maxChildrenWidth,
  maxColumns,
  gap,
  childrenWidth,
}: UniformColumnGridProps) => {
  if (childrenWidth !== undefined) {
    return toSizeUnit(childrenWidth)
  }

  return `
    minmax(
      max(
        ${toSizeUnit(minChildrenWidth || 0)},
        ${getColumnMax(maxColumns, gap)}
      ),
      ${maxChildrenWidth ? toSizeUnit(maxChildrenWidth) : "1fr"}
  )`
}

export const UniformColumnGrid = styled.div<UniformColumnGridProps>`
  display: grid;
  grid-template-columns: repeat(auto-fit, ${getColumnWidth});
  gap: ${({ gap }) => toSizeUnit(gap)};
  ${({ rowHeight }) =>
    rowHeight &&
    css`
      grid-auto-rows: ${toSizeUnit(rowHeight)};
    `}
  ${({ fullWidth }) =>
    fullWidth &&
    css`
      width: 100%;
    `}
`
```

To categorize tasks into two groups, we first calculate the timestamp for the start of the current week. We then add the duration of one week to determine the timestamp for the start of the next week. At [Increaser](https://increaser.org/), we often need to convert between different time units, so we use a handy utility called `convertDuration`. This utility takes a value and the units to convert from and to, making the process of time unit conversion straightforward.

```tsx
import {
  MS_IN_DAY,
  MS_IN_HOUR,
  MS_IN_MIN,
  MS_IN_SEC,
  MS_IN_WEEK,
  NS_IN_MS,
} from "."
import { DurationUnit } from "./DurationUnit"

const msInUnit: Record<DurationUnit, number> = {
  ns: 1 / NS_IN_MS,
  ms: 1,
  s: MS_IN_SEC,
  min: MS_IN_MIN,
  h: MS_IN_HOUR,
  d: MS_IN_DAY,
  w: MS_IN_WEEK,
}

export const convertDuration = (
  value: number,
  from: DurationUnit,
  to: DurationUnit
) => {
  const result = value * (msInUnit[from] / msInUnit[to])

  return result
}
```

In the `WeekDeadlinesOverview` component, which displays tasks for both the current and upcoming week, we provide details such as the week's name, its start and end dates, the current day of the week, and a visual representation of task completion. Completed tasks are indicated by green circles, while pending tasks are represented by gray circles. These circles are rendered by the `TaskStatus` styled component, where the `completed` property determines the color. The design of this component utilizes the `round` and `sameDimensions` utilities to ensure that each circle is perfectly round and uniformly sized, respectively.

```tsx
import { HStack, VStack } from "@lib/ui/layout/Stack"
import { Text } from "@lib/ui/text"
import { range } from "@lib/utils/array/range"
import { convertDuration } from "@lib/utils/time/convertDuration"
import { format } from "date-fns"
import { Task } from "@increaser/entities/Task"
import { useStartOfDay } from "@lib/ui/hooks/useStartOfDay"
import { getShortWeekday } from "@lib/utils/time"
import { order } from "@lib/utils/array/order"
import styled from "styled-components"
import { round } from "@lib/ui/css/round"
import { sameDimensions } from "@lib/ui/css/sameDimensions"
import { matchColor } from "@lib/ui/theme/getters"

type WeekDeadlinesOverviewProps = {
  startedAt: number
  name: string
  tasks: Task[]
  showWorkdays?: boolean
}

const TaskStatus = styled.div<{ completed: boolean }>`
  ${round};
  ${sameDimensions(12)};
  background: ${matchColor("completed", {
    true: "success",
    false: "mistExtra",
  })};
`

export const WeekDeadlinesOverview = ({
  startedAt,
  name,
  tasks,
}: WeekDeadlinesOverviewProps) => {
  const todayStartedAt = useStartOfDay()

  return (
    <VStack gap={12}>
      <VStack gap={4}>
        <HStack alignItems="center" gap={8}>
          <Text color="contrast" weight="semibold" size={14}>
            {name}
          </Text>
          <Text weight="semibold" size={14} color="supporting">
            {format(startedAt, "MMM d")} -{" "}
            {format(startedAt + convertDuration(1, "w", "ms"), "MMM d")}
          </Text>
        </HStack>
        <HStack alignItems="center" justifyContent="space-between">
          {range(convertDuration(1, "w", "d")).map((dayIndex) => {
            const dayStartedAt =
              startedAt + convertDuration(dayIndex, "d", "ms")

            const isToday = todayStartedAt === dayStartedAt
            return (
              <Text
                key={dayIndex}
                size={12}
                weight={isToday ? "bold" : "regular"}
                color={isToday ? "primary" : "shy"}
              >
                {getShortWeekday(dayIndex)}
              </Text>
            )
          })}
        </HStack>
      </VStack>
      <HStack alignItems="center" gap={4} wrap="wrap">
        {order(tasks, (task) => task.completedAt ?? 0, "desc").map((task) => (
          <TaskStatus completed={!!task.completedAt} key={task.id} />
        ))}
      </HStack>
    </VStack>
  )
}
```

### Adaptive UI Design for Increaser's Todo List: Handling Hover and Touch Interactions

Depending on the user's selected view, the application dynamically switches between the `TasksToDo` and `TasksDone` components. Rather than using a hook to determine the current view, we employ the `RenderTaskView` component. This approach streamlines the process, enabling `RenderTaskView` to directly render the appropriate component based on the current state, thereby improving code readability.

```tsx
import { useAssertUserState } from "@increaser/ui/user/UserStateContext"
import { useRhythmicRerender } from "@lib/ui/hooks/useRhythmicRerender"
import { groupItems } from "@lib/utils/array/groupItems"
import { convertDuration } from "@lib/utils/time/convertDuration"
import {
  DeadlineStatus,
  Task,
  deadlineName,
  deadlineStatuses,
} from "@increaser/entities/Task"
import { VStack } from "@lib/ui/layout/Stack"
import { Text } from "@lib/ui/text"
import { CurrentTaskProvider } from "./CurrentTaskProvider"
import { TaskItem } from "./TaskItem"
import { getDeadlineTypes } from "@increaser/entities-utils/task/getDeadlineTypes"
import { getDeadlineStatus } from "@increaser/entities-utils/task/getDeadlineStatus"
import { useCallback, useMemo } from "react"
import { useUpdateTaskMutation } from "../api/useUpdateTaskMutation"
import { getDeadlineAt } from "@increaser/entities-utils/task/getDeadlineAt"
import { getRecord } from "@lib/utils/record/getRecord"
import { recordMap } from "@lib/utils/record/recordMap"
import { DnDGroups, ItemChangeParams } from "./DnDGroups"
import { CreateTask } from "./CreateTask"
import { getLastItemOrder } from "@lib/utils/order/getLastItemOrder"
import { DragContainer, OnHoverDragContainer } from "./DragContainer"
import { DragHandle } from "@lib/ui/dnd/DragHandle"
import { GripVerticalIcon } from "@lib/ui/icons/GripVerticalIcon"
import { useMedia } from "react-use"

const hoverableDragHandleWidth = 36

export const TasksToDo = () => {
  const { tasks } = useAssertUserState()
  const now = useRhythmicRerender(convertDuration(1, "min", "ms"))

  const isHoverEnabled = useMedia("(hover: hover) and (pointer: fine)")

  const groups = useMemo(() => {
    return {
      ...recordMap(
        getRecord(getDeadlineTypes(now), (key) => key),
        () => [] as Task[]
      ),
      ...groupItems(
        Object.values(tasks).filter((task) => !task.completedAt),
        (task) =>
          getDeadlineStatus({
            deadlineAt: task.deadlineAt,
            now,
          })
      ),
    }
  }, [now, tasks])

  const { mutate: updateTask } = useUpdateTaskMutation()

  const onChange = useCallback(
    (id: string, { order, groupId }: ItemChangeParams<DeadlineStatus>) => {
      const fields: Partial<Omit<Task, "id">> = {
        order,
      }
      if (groupId !== "overdue") {
        fields.deadlineAt = getDeadlineAt({
          deadlineType: groupId,
          now,
        })
      } else if (
        getDeadlineStatus({ deadlineAt: tasks[id].deadlineAt, now }) !==
        "overdue"
      ) {
        return
      }

      updateTask({
        id,
        fields,
      })
    },
    [now, tasks, updateTask]
  )

  return (
    <DnDGroups
      groups={groups}
      getGroupOrder={(status) => deadlineStatuses.indexOf(status)}
      getItemId={(task) => task.id}
      getItemOrder={(task) => task.order}
      onChange={onChange}
      renderGroup={({ content, groupId, containerProps }) => (
        <VStack gap={4} key={groupId}>
          <Text
            weight="semibold"
            size={12}
            color={groupId === "overdue" ? "idle" : "supporting"}
          >
            {deadlineName[groupId].toUpperCase()}
          </Text>
          <VStack {...containerProps}>
            {content}
            {groupId !== "overdue" && (
              <CreateTask
                order={getLastItemOrder(
                  groups[groupId].map((task) => task.order)
                )}
                deadlineType={groupId}
              />
            )}
          </VStack>
        </VStack>
      )}
      renderItem={({
        item,
        draggableProps,
        dragHandleProps,
        isDragging,
        isDraggingEnabled,
      }) => {
        const content = (
          <CurrentTaskProvider value={item} key={item.id}>
            <TaskItem />
          </CurrentTaskProvider>
        )
        const dragHandle = (
          <DragHandle
            style={
              isHoverEnabled
                ? {
                    position: "absolute",
                    left: -hoverableDragHandleWidth,
                    width: hoverableDragHandleWidth,
                  }
                : {
                    width: 40,
                  }
            }
            isActive={isDragging}
            {...dragHandleProps}
          >
            <GripVerticalIcon />
          </DragHandle>
        )
        if (isHoverEnabled) {
          return (
            <OnHoverDragContainer
              isDraggingEnabled={isDraggingEnabled}
              isDragging={isDragging}
              {...draggableProps}
            >
              {dragHandle}
              {content}
            </OnHoverDragContainer>
          )
        }

        return (
          <DragContainer {...draggableProps}>
            {dragHandle}
            {content}
          </DragContainer>
        )
      }}
    />
  )
}
```

To ensure our to-do list feature is accessible on mobile devices, we must check for hover support using the `hover` and `pointer` media queries. If a device supports hover, we enable a hoverable drag handle—a small vertical bar on the left side of each task item. Additionally, buttons for changing the deadline and deleting a task are positioned on the opposite side of a to-do item. Conversely, on mobile devices that do not support hover, the drag handle remains visible constantly, and a "more" button is introduced. This button activates a slide-over panel for task management, optimizing the user interface for touch interactions.

### Deadline Management in Increaser's Todo List: Categorizing Tasks by Deadline Status

To categorize tasks based on their deadline status, our implementation relies on two key functions: `getDeadlineTypes` and `getDeadlineStatus`. These functions help in identifying the type of deadline by examining the current weekday. Specifically, on Saturdays, we exclude the `thisWeek` category from our deadline types. This adjustment is due to the fact that only `today` and `tomorrow` remain as viable deadline categories for the current week, ensuring our task grouping logic accurately reflects the time-sensitive nature of task completion.

```tsx
import { DeadlineType, deadlineTypes } from "@increaser/entities/Task"
import { without } from "@lib/utils/array/without"
import { getWeekday } from "@lib/utils/time/getWeekday"

export const getDeadlineTypes = (now: number): readonly DeadlineType[] => {
  const weekday = getWeekday(new Date(now))
  if (weekday > 4) {
    return without(deadlineTypes, "thisWeek")
  }

  return deadlineTypes
}
```

The `getDeadlineStatus` function categorizes tasks by their deadlines, taking a timestamp for the deadline and the current time as inputs. It returns the status corresponding to `DeadlineType`, with an additional `overdue` status for past deadlines. Specifically, it returns:

- `overdue` for deadlines in the past,
- `today` for deadlines set for the current day,
- `tomorrow` for deadlines set for the next day,
- `thisWeek` for deadlines within the current week, and
- `nextWeek` for deadlines falling in the following week.

```ts
import { DeadlineStatus } from "@increaser/entities/Task"
import { convertDuration } from "@lib/utils/time/convertDuration"
import { getWeekday } from "@lib/utils/time/getWeekday"
import { endOfDay } from "date-fns"

type GetDeadlineStatusInput = {
  now: number
  deadlineAt: number
}

export const getDeadlineStatus = ({
  deadlineAt,
  now,
}: GetDeadlineStatusInput): DeadlineStatus => {
  if (deadlineAt < now) return "overdue"

  const todayEndsAt = endOfDay(now).getTime()
  if (deadlineAt <= todayEndsAt) return "today"

  const tomorrowEndsAt = todayEndsAt + convertDuration(1, "d", "ms")
  if (deadlineAt <= tomorrowEndsAt) return "tomorrow"

  const weekdayIndex = getWeekday(new Date(now))
  const thisWeekEndsAt =
    todayEndsAt +
    convertDuration(
      convertDuration(1, "w", "d") - (weekdayIndex + 1),
      "d",
      "ms"
    )
  if (deadlineAt <= thisWeekEndsAt) return "thisWeek"

  return "nextWeek"
}
```

To categorize tasks by their respective deadlines, we implement a systematic approach using two utility functions. The process begins with the `getRecord` function, which converts an array of tasks into a record format. This conversion segregates tasks into distinct categories, or "buckets," based on their deadline types.

```tsx
export function getRecord<T, K extends string | number>(
  items: readonly T[],
  getKey: (item: T) => K
): Record<K, T> {
  const record = {} as Record<K, T>

  items.forEach((item) => {
    record[getKey(item)] = item
  })

  return record
}
```

Following this, we utilize the `recordMap` function to process the generated record by turning each value into an empty array.

```tsx
export const recordMap = <K extends string | number, T, V>(
  record: Record<K, T>,
  fn: (value: T) => V
): Record<K, V> => {
  return Object.fromEntries(
    Object.entries(record).map(([key, value]) => [key, fn(value as T)])
  ) as Record<K, V>
}
```

Next, we merge empty buckets with tasks organized into groups based on their deadline status. This process is facilitated by the `groupItems` function, which accepts an array of tasks and a function to extract the deadline status from each task. The function then returns a record of tasks, categorized by their deadline status.

```tsx
export const groupItems = <T, K extends string | number>(
  items: T[],
  getKey: (item: T) => K
): Record<K, T[]> => {
  const result = {} as Record<K, T[]>

  items.forEach((item) => {
    const key = getKey(item)
    if (!result[key]) {
      result[key] = []
    }
    result[key]?.push(item)
  })

  return result
}
```

### Implementing Drag-and-Drop Functionality in Increaser's Todo List with React-Beautiful-DnD

With the task groups prepared, we can now focus on implementing drag-and-drop functionality. For this, we use the `react-beautiful-dnd` library. The choice of this library is based on my previous experience and its straightforwardness for common drag-and-drop scenarios between buckets. Since `react-beautiful-dnd` is not a core library, we choose not to include it in the [RadzionKit](https://github.com/radzionc/radzionkit) library. Instead, we'll keep the drag-and-drop logic in the [Increaser](https://increaser.org/) codebase, adjacent to our `TasksToDo` component.

It's important to note that we don't use `react-beautiful-dnd` directly in the `TasksToDo` component. Instead, we utilize a custom `DnDGroups` component, which acts as a wrapper around `react-beautiful-dnd`. This approach allows us to easily replace `react-beautiful-dnd` with another library if needed, as the consumer of the `DnDGroups` component is unaware of the underlying library and simply implements the required interface.

Another reason for keeping the drag-and-drop-related code in a separate component is to maintain the readability and understandability of our `TasksToDo` component. Mixing drag-and-drop logic with the business logic of the to-do list would make the component overly complex.

```tsx
import { order } from "@lib/utils/array/order"
import { getNewOrder } from "@lib/utils/order/getNewOrder"
import { ReactNode, useCallback, useState } from "react"
import {
  DragDropContext,
  Draggable,
  Droppable,
  OnDragEndResponder,
} from "react-beautiful-dnd"
import { getRecordKeys } from "@lib/utils/record/getRecordKeys"

export type ItemChangeParams<K> = {
  order: number
  groupId: K
}

type RenderGroupParams<K> = {
  groupId: K
  content: ReactNode
  containerProps?: Record<string, any>
}

type RenderItemParams<I> = {
  item: I
  draggableProps?: Record<string, any>
  dragHandleProps?: Record<string, any> | null
  isDragging?: boolean
  isDraggingEnabled?: boolean
}

export type DnDGroupsProps<K extends string, I> = {
  groups: Record<K, I[]>
  getGroupOrder: (group: K) => number
  getItemOrder: (item: I) => number
  getItemId: (item: I) => string
  onChange: (itemId: string, params: ItemChangeParams<K>) => void
  renderGroup: (params: RenderGroupParams<K>) => ReactNode
  renderItem: (params: RenderItemParams<I>) => ReactNode
}

export function DnDGroups<K extends string, I>({
  groups,
  getItemOrder,
  getItemId,
  onChange,
  renderGroup,
  renderItem,
  getGroupOrder,
}: DnDGroupsProps<K, I>) {
  const [currentItemId, setCurrentItemId] = useState<string | null>(null)

  const handleDragEnd: OnDragEndResponder = useCallback(
    ({ destination, source, draggableId }) => {
      setCurrentItemId(null)
      if (!destination) {
        return
      }

      const isSameGroup = destination.droppableId === source.droppableId

      if (isSameGroup && destination.index === source.index) {
        return
      }

      const groupId = destination.droppableId as K

      const items = groups[groupId] || []

      onChange(draggableId, {
        order: getNewOrder({
          orders: items.map(getItemOrder),
          sourceIndex: isSameGroup ? source.index : null,
          destinationIndex: destination.index,
        }),
        groupId,
      })
    },
    [getItemOrder, groups, onChange]
  )

  const groupKeys = order(getRecordKeys(groups), getGroupOrder, "asc")

  return (
    <DragDropContext
      onDragStart={({ draggableId }) => setCurrentItemId(draggableId)}
      onDragEnd={handleDragEnd}
    >
      {groupKeys.map((groupId) => {
        const items = order(groups[groupId] || [], getItemOrder, "asc")

        return (
          <Droppable droppableId={groupId}>
            {(provided) => {
              return (
                <>
                  {renderGroup({
                    groupId,
                    containerProps: {
                      ...provided.droppableProps,
                      ref: provided.innerRef,
                    },
                    content: (
                      <>
                        {items.map((item, index) => (
                          <Draggable
                            key={getItemId(item)}
                            index={index}
                            draggableId={getItemId(item)}
                          >
                            {(
                              { dragHandleProps, draggableProps, innerRef },
                              { isDragging }
                            ) => (
                              <>
                                {renderItem({
                                  item,
                                  draggableProps: {
                                    ...draggableProps,
                                    ref: innerRef,
                                  },
                                  dragHandleProps,
                                  isDraggingEnabled:
                                    currentItemId === null ||
                                    getItemId(item) !== currentItemId,
                                  isDragging,
                                })}
                              </>
                            )}
                          </Draggable>
                        ))}
                        {provided.placeholder}
                      </>
                    ),
                  })}
                </>
              )
            }}
          </Droppable>
        )
      })}
    </DragDropContext>
  )
}
```

The `DnDGroups` component requires the following props:

- `groups`: A record of items grouped by a key, which in our case would be the deadline status.
- `getGroupOrder`: A function to determine the order of groups, with the 'overdue' group being the first and the 'nextWeek' group being the last.
- `getItemOrder`: A function to determine the order of items within a group, thus sorting tasks within each deadline status group.
- `getItemId`: A function to determine the unique identifier of an item, which in our case would be the task ID.
- `onChange`: A function to handle changes in item order and group.
- `renderGroup`: A function to render a group.
- `renderItem`: A function to render an item.

First, we maintain the ID of the currently dragged item in the state. This allows us to disable dragging for other items while one item is being dragged, ensuring that other items won't display the drag handle on hover.

When the drag ends, we check if the destination or the index has changed from the source. If there is a change, we calculate the new order using the `getNewOrder` utility function and call the `onChange` function with the new order and the group ID. The `order` field is used solely to sort items within a group, and its actual value is arbitrary as long as it's unique within the group. By utilizing the `order` field, we only need to update a single task in the database when the order changes, instead of reordering all the tasks.

The logic within the `getNewOrder` function is straightforward. When it's the only item within a group, we return 0. If the destination index is 0, we return the order of the first item minus 1. If the destination index is the last index, we return the order of the last item plus 1. Otherwise, we calculate the new order based on the previous and next items' orders, "placing" the dragged item between them.

```tsx
import { getLastItem } from "../array/getLastItem"
import { isEmpty } from "../array/isEmpty"

type GetNewOrderInput = {
  orders: number[]
  sourceIndex: number | null
  destinationIndex: number
}

export const getNewOrder = ({
  orders,
  sourceIndex,
  destinationIndex,
}: GetNewOrderInput): number => {
  if (isEmpty(orders)) {
    return 0
  }

  if (destinationIndex === 0) {
    return orders[0] - 1
  }

  const movedUp = sourceIndex !== null && sourceIndex < destinationIndex
  const previousIndex = movedUp ? destinationIndex : destinationIndex - 1
  const previous = orders[previousIndex]

  const shouldBeLast =
    (destinationIndex === orders.length - 1 && sourceIndex !== null) ||
    destinationIndex > orders.length - 1

  if (shouldBeLast) {
    return getLastItem(orders) + 1
  }

  const nextIndex = movedUp ? destinationIndex + 1 : destinationIndex
  const next = orders[nextIndex]

  return previous + (next - previous) / 2
}
```

To sort the groups and items within the groups, we use the `order` utility function. This function accepts an array of items and a function to extract the order value from each item. It then sorts the items according to the specified order, which can be either ascending or descending.

```tsx
import { Order } from "../order/Order"

export const order = <T,>(
  array: T[],
  getValue: (item: T) => number,
  order: Order
) => {
  return [...array].sort((a, b) => {
    if (order === "asc") {
      return getValue(a) - getValue(b)
    } else {
      return getValue(b) - getValue(a)
    }
  })
}
```

Both the `renderGroup` and `renderItem` functions receive props and a ref, which should be propagated to the corresponding tasks list and task items, respectively. This ensures that the `Droppable` and `Draggable` components are aware of the underlying DOM elements, allowing the drag-and-drop functionality to work as expected.

### Leveraging DnDGroups for Task Management in Increaser: Integrating Drag-and-Drop into TasksToDo

Now let's examine how the `TasksToDo` component leverages the `DnDGroups` component to implement drag-and-drop functionality for tasks. We pass tasks that are already grouped into deadline buckets to the `groups` prop. To determine the order of the groups, we simply use the index of the deadline status in the `deadlineStatuses` array. The `getItemId` function accesses the task ID, while the `getItemOrder` function retrieves the order of the task. The `onChange` function is responsible for calling the API to update the task's order and deadline type.

We render each group within a vertical flexbox, which includes a title, a list of tasks (provided by the `content` prop), and a prompt to create a new task. The `renderItem` function is responsible for rendering each task item, including the drag handle and the task itself.

In the `onChange` callback, we need to convert the `groupId` into a deadline timestamp. We achieve this by using the `getDeadlineAt` utility function. This function takes the deadline type and the current timestamp as inputs and returns the timestamp for the deadline. It is straightforward, and we then call the `updateTask` mutation to update the task's order and deadline timestamp.

```ts
import { DeadlineType } from "@increaser/entities/Task"
import { convertDuration } from "@lib/utils/time/convertDuration"
import { getWeekday } from "@lib/utils/time/getWeekday"
import { endOfDay } from "date-fns"

type GetDeadlineAtInput = {
  now: number
  deadlineType: DeadlineType
}

export const getDeadlineAt = ({
  deadlineType,
  now,
}: GetDeadlineAtInput): number => {
  const todayEndsAt = endOfDay(now).getTime()
  if (deadlineType === "today") return todayEndsAt

  const tomorrowEndsAt = todayEndsAt + convertDuration(1, "d", "ms")
  if (deadlineType === "tomorrow") return tomorrowEndsAt

  const weekdayIndex = getWeekday(new Date(now))
  const thisWeekEndsAt =
    todayEndsAt +
    convertDuration(
      convertDuration(1, "w", "d") - (weekdayIndex + 1),
      "d",
      "ms"
    )
  if (deadlineType === "thisWeek") return thisWeekEndsAt

  return thisWeekEndsAt + convertDuration(1, "w", "ms")
}
```

### Optimizing Task Interaction on Mobile: Employing Context Providers and Responsive Design in Increaser

When rendering a task, we must consider whether it is on a mobile device. If it is, we keep the handle always visible by displaying it next to the task within a horizontal flexbox container. If hover is enabled, we employ the `OnHoverDragContainer` component, which changes the opacity of the drag handle based on the hover state and the `isDragging` prop. If hover is not enabled, we use the `DragContainer` component, which always displays the drag handle.

```tsx
import { DragHandle } from "@lib/ui/dnd/DragHandle"
import { HStack } from "@lib/ui/layout/Stack"
import { getColor } from "@lib/ui/theme/getters"
import styled, { css } from "styled-components"

export const DragContainer = styled(HStack)`
  width: 100%;
  gap: 4px;
  align-items: center;
  background: ${getColor("background")};
  position: relative;
`

type OnHoverDragContainerProps = {
  isDraggingEnabled?: boolean
  isDragging?: boolean
}

export const OnHoverDragContainer = styled(
  DragContainer
)<OnHoverDragContainerProps>`
  gap: 0;
  align-items: center;
  ${({ isDraggingEnabled }) =>
    !isDraggingEnabled &&
    css`
      pointer-events: none;
    `}

  @media (hover: hover) and (pointer: fine) {
    &:not(:focus-within) > ${DragHandle} {
      opacity: ${({ isDragging }) => (isDragging ? 1 : 0)};
    }
  }

  &:hover ${DragHandle} {
    opacity: 1;
  }
`
```

To make the task entity accessible to all children within the `TaskItem` component without resorting to deep prop drilling, we use the `CurrentTaskProvider` component. This component leverages the [`getValueProviderSetup`](https://github.com/search?q=repo%3Aradzionc%2Fradzionkit%20getValueProviderSetup&type=code) utility function to create lightweight context providers that serve the sole purpose of maintaining a single value.

```tsx
import { Task } from "@increaser/entities/Task"
import { getValueProviderSetup } from "@lib/ui/state/getValueProviderSetup"

export const { useValue: useCurrentTask, provider: CurrentTaskProvider } =
  getValueProviderSetup<Task>("Task")
```

Within the `TaskItem` component, we use the `useCurrentTask` hook to access the current task. We apply the same pattern to check if hover is enabled. Based on that, we either display the "manage deadline" and "delete task" buttons on hover or show the "more" button that will trigger the slide-over panel on mobile devices.

```tsx
import { useCurrentTask } from "./CurrentTaskProvider"
import { HStack } from "@lib/ui/layout/Stack"
import { ManageTaskDeadline } from "./ManageTaskDeadline"
import styled from "styled-components"
import { CheckStatus } from "@lib/ui/checklist/CheckStatus"
import { InvisibleHTMLCheckbox } from "@lib/ui/inputs/InvisibleHTMLCheckbox"
import { interactive } from "@lib/ui/css/interactive"
import { useUpdateTaskMutation } from "../api/useUpdateTaskMutation"
import { TaskItemFrame } from "./TaskItemFrame"
import { EditableTaskName } from "./EditableTaskName"
import { DeleteTask } from "./DeleteTask"
import { useMedia } from "react-use"
import { ManageTaskSlideover } from "./ManageTaskSlideover"
import { ExpandableSelector } from "@lib/ui/select/ExpandableSelector"
import { IconWrapper } from "@lib/ui/icons/IconWrapper"
import { CalendarIcon } from "@lib/ui/icons/CalendarIcon"
import { Text } from "@lib/ui/text"
import { deadlineName } from "@increaser/entities/Task"

const OnHoverActions = styled.div`
  display: grid;
  grid-template-columns: 60px 36px;
  height: 36px;
  gap: 4px;

  &:not(:focus-within) {
    opacity: 0;
  }
`

const Container = styled(HStack)`
  width: 100%;
  gap: 8px;
  align-items: center;

  &:hover ${OnHoverActions} {
    opacity: 1;
  }
`

const Check = styled(CheckStatus)`
  ${interactive};
`

export const TaskItem = () => {
  const task = useCurrentTask()
  const { completedAt } = task

  const isHoverEnabled = useMedia("(hover: hover) and (pointer: fine)")

  const { mutate: updateTask } = useUpdateTaskMutation()

  const value = !!completedAt

  return (
    <Container>
      <TaskItemFrame>
        <Check isInteractive forwardedAs="label" value={value}>
          <InvisibleHTMLCheckbox
            value={value}
            onChange={() => {
              updateTask({
                id: task.id,
                fields: {
                  completedAt: task.completedAt ? null : Date.now(),
                },
              })
            }}
          />
        </Check>
        <EditableTaskName />
      </TaskItemFrame>
      {isHoverEnabled ? (
        <OnHoverActions>
          <ManageTaskDeadline
            render={({ value, onChange, options }) => (
              <ExpandableSelector
                openerContent={
                  <IconWrapper style={{ fontSize: 18 }}>
                    <CalendarIcon />
                  </IconWrapper>
                }
                floatingOptionsWidthSameAsOpener={false}
                style={{ height: "100%", padding: 8 }}
                value={value}
                onChange={onChange}
                options={options}
                getOptionKey={(option) => option}
                renderOption={(option) => (
                  <Text key={option}>{deadlineName[option]}</Text>
                )}
              />
            )}
          />
          <DeleteTask />
        </OnHoverActions>
      ) : (
        <ManageTaskSlideover />
      )}
    </Container>
  )
}
```

We display the content within the `TaskItemFrame` component, which defines the shape of the task item. This component is also used for the "Create Task" button and the inline form for task creation. By using such "frame" components, we ensure a consistent look and feel across the application.

```tsx
import styled from "styled-components"
import { verticalPadding } from "@lib/ui/css/verticalPadding"

export const taskItemMinHeight = 40

export const TaskItemFrame = styled.div`
  display: grid;
  width: 100%;
  grid-template-columns: 24px 1fr;
  align-items: center;
  justify-items: start;
  gap: 12px;
  font-weight: 500;
  ${verticalPadding(8)};
`
```

### Enhancing Task UI with Custom Checkboxes: Implementing Interactive Elements in Increaser

To create a checkbox, we leverage a native HTML checkbox, which is hidden from the user but still functional. It will trigger the `onChange` event when the user clicks on the `Check` component, which is rendered as a `label` element.

```tsx
import React from "react"
import { InputProps } from "../props"
import { InvisibleInput } from "./InvisibleInput"

export type InvisibleHTMLCheckboxProps = InputProps<boolean> & {
  id?: string
  groupName?: string
}

export const InvisibleHTMLCheckbox: React.FC<InvisibleHTMLCheckboxProps> = ({
  id,
  value,
  onChange,
  groupName,
}) => (
  <InvisibleInput
    type="checkbox"
    checked={value}
    name={groupName}
    value={id}
    onChange={(event) => onChange(event.target.checked)}
  />
)
```

The `CheckStatus` component is a reusable component for creating "checkbox-like" elements used in checklists and form fields. It has different styles based on the `isChecked` and `isInteractive` props. By using `aspect-ratio` together with `width: 100%`, we make the component a square that covers the available space. In the case of our `TaskItemFrame`, it will cover the entire width of the `TaskItemFrame`'s first column.

```tsx
import styled, { css } from "styled-components"
import { centerContent } from "../css/centerContent"
import { getColor } from "../theme/getters"
import { transition } from "../css/transition"
import { CheckIcon } from "../icons/CheckIcon"
import { ComponentWithChildrenProps, UIComponentProps } from "../props"
import React from "react"
import { interactive } from "../css/interactive"
import { getHoverVariant } from "../theme/getHoverVariant"

type CheckStatusProps = UIComponentProps & {
  value: boolean
  as?: React.ElementType
  isInteractive?: boolean
} & Partial<ComponentWithChildrenProps>

const Container = styled.div<{ isChecked: boolean; isInteractive?: boolean }>`
  width: 100%;
  aspect-ratio: 1/1;

  ${centerContent};

  border-radius: 4px;
  border: 1px solid ${getColor("textSupporting")};
  color: ${getColor("background")};

  ${transition}

  ${({ isChecked }) =>
    isChecked &&
    css`
      background: ${getColor("primary")};
      border-color: ${getColor("primary")};
    `};

  ${({ isInteractive, isChecked }) =>
    isInteractive &&
    css`
      ${interactive};
      &:hover {
        background: ${isChecked ? getColor("primary") : getColor("mist")};
        border-color: ${isChecked
          ? getHoverVariant("primary")
          : getColor("contrast")};
      }
    `};
`

export const CheckStatus = ({
  value,
  children,
  isInteractive = false,
  ...rest
}: CheckStatusProps) => {
  return (
    <Container {...rest} isInteractive={isInteractive} isChecked={value}>
      {value && <CheckIcon />}
      {children}
    </Container>
  )
}
```

### Streamlining Task Management: Using InputDebounce for Efficient API Calls in Increaser

After the checkbox, we display the `EditableTaskName`, a component that allows the user to edit the task name. To avoid spamming our API with requests for every keystroke, we use the `InputDebounce` component.

```tsx
import { useCurrentTask } from "./CurrentTaskProvider"
import { TaskNameInput } from "./TaskNameInput"
import { useUpdateTaskMutation } from "../api/useUpdateTaskMutation"
import { InputDebounce } from "@lib/ui/inputs/InputDebounce"

export const EditableTaskName = () => {
  const task = useCurrentTask()

  const { mutate: updateTask } = useUpdateTaskMutation()

  return (
    <InputDebounce
      value={task.name}
      onChange={(name) => updateTask({ id: task.id, fields: { name } })}
      render={({ value, onChange }) => (
        <TaskNameInput
          autoComplete="off"
          value={value}
          onChange={(e) => onChange(e.target.value)}
        />
      )}
    />
  )
}
```

The `InputDebounce` component receives `value` and `onChange` props but also keeps track of the current value in the local state. When the user types, the `onChange` function is called with the new value, but the actual update is delayed by 300ms. The user won't notice the delay because the local state holds the value, and the input is updated immediately.

```tsx
import { ReactNode, useEffect, useState } from "react"
import { InputProps } from "../props"

type InputDebounceProps<T> = InputProps<T> & {
  render: (props: InputProps<T>) => ReactNode
  interval?: number
}

export function InputDebounce<T>({
  value,
  onChange,
  interval = 300,
  render,
}: InputDebounceProps<T>) {
  const [currentValue, setCurrentValue] = useState<T>(value)

  useEffect(() => {
    if (currentValue === value) return

    const timeout = setTimeout(() => {
      onChange(currentValue)
    }, interval)

    return () => clearTimeout(timeout)
  }, [currentValue, interval, onChange, value])

  return (
    <>
      {render({
        value: currentValue,
        onChange: setCurrentValue,
      })}
    </>
  )
}
```

### Adapting Task Deadline Management for Desktop and Mobile: Interactive UI Components in Increaser

The `ManageTaskDeadline` component handles the logic for providing the current deadline and calling the API, but the actual rendering is determined by the consumer. Thus, on desktop, we'll display a dropdown, and on mobile, we'll display a slide-over panel. When changing the deadline, we also need to update the task's order so that the task is placed last in the new group.

```tsx
import { useCurrentTask } from "./CurrentTaskProvider"
import { DeadlineType } from "@increaser/entities/Task"
import { getDeadlineAt } from "@increaser/entities-utils/task/getDeadlineAt"
import { getDeadlineTypes } from "@increaser/entities-utils/task/getDeadlineTypes"
import { useUpdateTaskMutation } from "../api/useUpdateTaskMutation"
import { useRhythmicRerender } from "@lib/ui/hooks/useRhythmicRerender"
import { getDeadlineStatus } from "@increaser/entities-utils/task/getDeadlineStatus"
import { useAssertUserState } from "@increaser/ui/user/UserStateContext"
import { groupItems } from "@lib/utils/array/groupItems"
import { getLastItemOrder } from "@lib/utils/order/getLastItemOrder"

type RenderParams = {
  value: DeadlineType | null
  onChange: (value: DeadlineType) => void
  options: readonly DeadlineType[]
}

type ManageTaskDeadlineProps = {
  render: (params: RenderParams) => React.ReactNode
}

export const ManageTaskDeadline = ({ render }: ManageTaskDeadlineProps) => {
  const { tasks } = useAssertUserState()
  const { id, deadlineAt } = useCurrentTask()

  const { mutate } = useUpdateTaskMutation()

  const now = useRhythmicRerender(1000)
  const deadlineStatus = getDeadlineStatus({
    now,
    deadlineAt,
  })
  const value = deadlineStatus === "overdue" ? null : deadlineStatus

  const changeDeadline = (deadlineType: DeadlineType) => {
    const deadlineAt = getDeadlineAt({
      now: Date.now(),
      deadlineType,
    })

    const groupedTasks = groupItems(
      Object.values(tasks).filter((task) => !task.completedAt),
      (task) =>
        getDeadlineStatus({
          deadlineAt: task.deadlineAt,
          now,
        })
    )

    mutate({
      id,
      fields: {
        deadlineAt,
        order: getLastItemOrder(
          (groupedTasks[deadlineType] ?? []).map((task) => task.order)
        ),
      },
    })
  }

  return (
    <>
      {render({
        value,
        onChange: changeDeadline,
        options: getDeadlineTypes(now),
      })}
    </>
  )
}
```

On mobile, we display the options for deadlines together with the "Delete Task" button within a `BottomSlideOver` component. Here, we use the same `ManageTaskDeadline` component, but instead of a dropdown, it will be a list of menu options. The `Opener` component([source code](https://github.com/search?q=repo%3Aradzionc%2Fradzionkit+Opener&type=code)) is a wrapper around the `useState` hook, which makes the code more readable and easier to maintain.

```tsx
import { Opener } from "@lib/ui/base/Opener"
import { IconButton } from "@lib/ui/buttons/IconButton"
import { MoreHorizontalIcon } from "@lib/ui/icons/MoreHorizontalIcon"
import { BottomSlideOver } from "@lib/ui/modal/BottomSlideOver"
import { useCurrentTask } from "./CurrentTaskProvider"
import { useDeleteTaskMutation } from "../api/useDeleteHabitMutation"
import { SeparatedByLine } from "@lib/ui/layout/SeparatedByLine"
import { MenuOption } from "@lib/ui/menu/MenuOption"
import { TrashBinIcon } from "@lib/ui/icons/TrashBinIcon"
import { VStack } from "@lib/ui/layout/Stack"
import { Text } from "@lib/ui/text"
import { ManageTaskDeadline } from "./ManageTaskDeadline"
import { deadlineName } from "@increaser/entities/Task"

export const ManageTaskSlideover = () => {
  const { id } = useCurrentTask()

  const { mutate: deleteTask } = useDeleteTaskMutation()

  return (
    <Opener
      renderOpener={({ onOpen }) => (
        <IconButton
          title="Manage task"
          icon={<MoreHorizontalIcon />}
          onClick={onOpen}
        />
      )}
      renderContent={({ onClose }) => (
        <BottomSlideOver title="Manage task" onClose={onClose}>
          <SeparatedByLine gap={20}>
            <VStack gap={12}>
              <Text weight="bold" size={14} color="supporting">
                Deadline
              </Text>
              <ManageTaskDeadline
                render={({ value, onChange, options }) => (
                  <>
                    {options.map((option) => (
                      <MenuOption
                        isSelected={value === option}
                        key={option}
                        text={deadlineName[option]}
                        onSelect={() => onChange(option)}
                        view="slideover"
                      />
                    ))}
                  </>
                )}
              />
            </VStack>
            <MenuOption
              kind="alert"
              view="slideover"
              text="Delete task"
              icon={<TrashBinIcon />}
              onSelect={() => deleteTask({ id })}
            />
          </SeparatedByLine>
        </BottomSlideOver>
      )}
    />
  )
}
```

### Streamlining Task Creation in Increaser: Integrating the CreateTask Component with Optimistic Updates

As we covered the tasks in the TO-DO list, let's check how the tasks are being created. As we saw before, each bucket of tasks, except for the "overdue" bucket, has the `CreateTask` component at the bottom. It receives an order and a deadline type as props so that it can create a new task in the right bucket and position it last. The `CreateTask` component will first display the `AddTaskButton`, and when clicked, it will display the `CreateTaskForm` component.

```tsx
import { DeadlineType } from "@increaser/entities/Task"
import { AddTaskButton } from "./AddTaskButton"
import { CreateTaskForm } from "./CreateTaskForm"
import { useState } from "react"

type CreateTaskProps = {
  deadlineType: DeadlineType
  order: number
}

export const CreateTask = ({ deadlineType, order }: CreateTaskProps) => {
  const [isActive, setIsActive] = useState(false)
  return isActive ? (
    <CreateTaskForm
      deadlineType={deadlineType}
      order={order}
      onFinish={() => setIsActive(false)}
    />
  ) : (
    <AddTaskButton onClick={() => setIsActive(true)} />
  )
}
```

As we mentioned before, both the `AddTaskButton` and the `CreateTaskForm` components are wrapped in the `TaskItemFrame` component, which ensures a consistent look and feel across the application. Similar to the checkbox, we use a container with aspect ratio of 1/1 to ensure that the icon takes the same size as other elements rendered within the `TaskItemFrame`. To make the hover effect appear wider then the element itself, we use the `Hoverable` component([source code](https://github.com/search?q=repo%3Aradzionc%2Fradzionkit+Hoverable&type=code)).

```tsx
import { ClickableComponentProps } from "@lib/ui/props"
import { PlusIcon } from "@lib/ui/icons/PlusIcon"
import { Text } from "@lib/ui/text"
import { Hoverable } from "@lib/ui/base/Hoverable"
import styled from "styled-components"
import { centerContent } from "@lib/ui/css/centerContent"
import { getColor } from "@lib/ui/theme/getters"
import { transition } from "@lib/ui/css/transition"
import { TaskItemFrame } from "./TaskItemFrame"

const IconContainer = styled.div`
  width: 100%;
  aspect-ratio: 1/1;
  ${centerContent};
  color: ${getColor("primary")};
`

const Container = styled(Hoverable)`
  color: ${getColor("textShy")};
  ${transition};
  &:hover {
    color: ${getColor("primary")};
  }
`

export const AddTaskButton = ({ onClick }: ClickableComponentProps) => {
  return (
    <Container verticalOffset={0} onClick={onClick}>
      <TaskItemFrame>
        <IconContainer>
          <PlusIcon />
        </IconContainer>
        <Text size={14} weight="regular">
          Add task
        </Text>
      </TaskItemFrame>
    </Container>
  )
}
```

The `CreateTaskForm` is a straightforward form with a single input field, as the bucket is already predefined by the location of the task creation prompt. Both on form submit and blur, we check if the input is empty, and if not, we create a new task and reset the input. Since there is autofocus on the input, the user can create tasks one after another without the need to click any buttons. As we are setting an ID for the task on the front-end and performing an optimistic update by updating the state before the API call is resolved, the user will see the task appear instantly.

```tsx
import { FormEvent, useState } from "react"
import { useKey } from "react-use"
import { handleWithPreventDefault } from "@increaser/app/shared/events"
import { FinishableComponentProps } from "@lib/ui/props"
import { getId } from "@increaser/entities-utils/shared/getId"
import { DeadlineType, Task } from "@increaser/entities/Task"
import { getDeadlineAt } from "@increaser/entities-utils/task/getDeadlineAt"
import { CheckStatus } from "@lib/ui/checklist/CheckStatus"
import { useCreateTaskMutation } from "../api/useCreateTaskMutation"
import { TaskItemFrame } from "./TaskItemFrame"
import { TaskNameInput } from "./TaskNameInput"

type CreateTaskFormProps = FinishableComponentProps & {
  deadlineType: DeadlineType
  order: number
}

export const CreateTaskForm = ({
  onFinish,
  deadlineType,
  order,
}: CreateTaskFormProps) => {
  const [name, setName] = useState("")

  const { mutate } = useCreateTaskMutation()

  useKey("Escape", onFinish)

  const createTask = () => {
    const startedAt = Date.now()
    const task: Task = {
      name,
      id: getId(),
      startedAt,
      deadlineAt: getDeadlineAt({ now: startedAt, deadlineType }),
      order,
    }
    mutate(task)
    setName("")
  }

  return (
    <div>
      <TaskItemFrame
        as="form"
        onBlur={() => {
          if (name) {
            createTask()
          } else {
            onFinish()
          }
        }}
        onSubmit={handleWithPreventDefault<FormEvent<HTMLFormElement>>(() => {
          if (name) {
            createTask()
          }
        })}
      >
        <CheckStatus value={false} />
        <TaskNameInput
          placeholder="Task name"
          autoFocus
          autoComplete="off"
          onChange={(event) => setName(event.target.value)}
          value={name}
        />
      </TaskItemFrame>
    </div>
  )
}
```

### Managing Completed Tasks in Increaser: Displaying and Clearing Completed Tasks Efficiently

The final piece of our feature is the view with completed tasks. Here, we filter the tasks by checking for the presence of the `completedAt` field. Based on the presence of completed tasks, we either display a message at the top of the list indicating that completed tasks will be shown here or a message that completed tasks will be cleared each week. We then display the tasks in the same way as in the TO-DO list, but without the drag-and-drop functionality.

```tsx
import { useAssertUserState } from "@increaser/ui/user/UserStateContext"
import { VStack } from "@lib/ui/layout/Stack"
import { CurrentTaskProvider } from "./CurrentTaskProvider"
import { TaskItem } from "./TaskItem"
import { ShyInfoBlock } from "@lib/ui/info/ShyInfoBlock"
import { isEmpty } from "@lib/utils/array/isEmpty"

export const TasksDone = () => {
  const { tasks } = useAssertUserState()

  const completedTasks = Object.values(tasks).filter(
    (task) => !!task.completedAt
  )
  return (
    <VStack gap={20}>
      <ShyInfoBlock>
        {isEmpty(completedTasks)
          ? `Tasks you've completed will be shown here. Keep up the good work!`
          : "Completed tasks automatically clear each week, offering a fresh start and organized view."}
      </ShyInfoBlock>
      <VStack>
        {completedTasks.map((task) => (
          <CurrentTaskProvider value={task} key={task.id}>
            <TaskItem />
          </CurrentTaskProvider>
        ))}
      </VStack>
    </VStack>
  )
}
```