Skip to main content
Projects ● 3 min read

ZenToDo

Stack
  • React
  • TypeScript
  • Vite
  • Progressive Web App
  • Mantine
Source code
marekh19/zentodo
View the app
zentodo.marek.work

I use this app as a shopping list when going to supermarket - Me

What’s Special About This Todo App?

Most todo apps are just “Hello World” examples. This one has a few interesting technical features that make it worth looking at.

The Cool Parts

1. Custom Zustand Persistence Driver

Instead of the usual localStorage, this app uses IndexedDB through a custom storage driver:

export const storage: StateStorage = {
  getItem: async (id: string): Promise<string | null> => {
    const value = await getItem(id)
    return value ? JSON.stringify(value) : null
  },
  setItem: async (id: string, value: string): Promise<void> => {
    const parsedValue = JSON.parse(value)
    await setItem(id, parsedValue)
  },
  removeItem: async (id: string): Promise<void> => {
    await removeItem(id)
  },
}

The IndexedDB wrapper is clean and simple:

const dbPromise = openDB(DB_NAME, DB_VERSION, {
  upgrade(db) {
    db.createObjectStore(STORE_NAME)
  },
})

export const setItem = async (key: string, value: unknown) => {
  const db = await dbPromise
  await db.put(STORE_NAME, value, key)
}

2. Smart Todo Store with Timestamps

The Zustand store handles completion timestamps automatically:

setIsCompleted: (id, isCompleted) =>
  set((state) => ({
    todos: state.todos.map((todo) =>
      todo.id === id
        ? {
            ...todo,
            isCompleted: isCompleted,
            completedAt: isCompleted ? new Date() : null,
          }
        : todo
    ),
  })),

3. Elegant Form Handling with Mantine

The add todo form uses Mantine’s form hook with validation:

const form = useForm<FormValues>({
  mode: 'uncontrolled',
  initialValues: { title: '' },
  validate: {
    title: (value) =>
      value.trim().length < MIN_TASK_TITLE_LENGTH
        ? 'Todo must have at least 2 letters'
        : null,
  },
})

4. Mobile-First Drawer Pattern

Todo items open in a bottom drawer (perfect for mobile):

<Drawer
  opened={isOpen}
  onClose={close}
  position="bottom"
  size="xs"
  withCloseButton={false}
>
  <TodoDetail todo={todo} />
</Drawer>

5. Smart Todo Filtering and Sorting

The useTodos hook provides filtered lists with automatic sorting:

const getFilteredTodos = useMemo(() => {
  return (type: TodoListType) => {
    const filteredTodos =
      type === 'todo'
        ? todos.filter((todo) => !todo.isCompleted)
        : todos.filter((todo) => todo.isCompleted)

    return sortByCreatedAt(filteredTodos)
  }
}, [todos])

PWA Features

The app is fully installable with:

  • Service worker for offline functionality
  • App icons in multiple sizes
  • Firebase messaging setup (ready for future push notifications)
  • Custom Vite build configuration for service workers

Screenshots

ZenToDo mobile screenshot
ZenToDo PWA installation prompt

Conclusion

This isn’t just another todo app tutorial. It’s a PWA with some thoughtful technical implementations, particularly the custom IndexedDB persistence layer for offline storage.