From 4cd62aadbcda546c50cc2522b5335af906421b73 Mon Sep 17 00:00:00 2001 From: Cecilia Sanare Date: Wed, 31 Jan 2024 00:39:06 -0600 Subject: [PATCH] fix: properly count the number of items --- src/components/List.tsx | 56 +++++++++++++++ src/lib/__tests__/items.spec.ts | 121 +++++++++++++++++++++++++++++++- src/lib/items.ts | 61 ++++++++++++++++ src/routes/TodoListsPage.tsx | 45 ++++-------- 4 files changed, 250 insertions(+), 33 deletions(-) create mode 100644 src/components/List.tsx diff --git a/src/components/List.tsx b/src/components/List.tsx new file mode 100644 index 0000000..d52ec74 --- /dev/null +++ b/src/components/List.tsx @@ -0,0 +1,56 @@ +import { ConfirmDialog } from '@/components/ConfirmDialog'; +import { Button } from '@/components/ui/button'; +import { ListStatus, getStatus, getStatusCount } from '@/lib/items'; +import { cn } from '@/lib/utils'; +import { Storage, StorageKeys, Todo } from '@/storage'; +import { CheckCircle, CircleDot, Flame, ListTodo, XCircle } from 'lucide-react'; +import { useMemo } from 'react'; +import { Link } from 'react-router-dom'; + +export type ListProps = { + list: Todo.List; + onDelete: () => void; +}; + +const LIST_ICONS: Record = { + [ListStatus.NOT_STARTED]: () => , + [ListStatus.IN_PROGRESS]: () => , + [ListStatus.DONE]: () => , +}; + +export function List({ list, onDelete }: ListProps) { + const { done, total } = useMemo(() => getStatusCount(list.items), [list.items]); + const status = getStatus({ done, total }); + const Icon = LIST_ICONS[status]; + + return ( +
+ +
+
+ {done} / {total} +
+ +
+ + + +
+ ); +} + +export namespace TodoListsPage { + export async function loader() { + return await Storage.get(StorageKeys.LISTS); + } +} diff --git a/src/lib/__tests__/items.spec.ts b/src/lib/__tests__/items.spec.ts index ff3d986..b9f5d3e 100644 --- a/src/lib/__tests__/items.spec.ts +++ b/src/lib/__tests__/items.spec.ts @@ -1,6 +1,6 @@ import { Todo } from '@/storage'; import { describe, expect, it } from 'vitest'; -import { isDone, setDone } from '../items'; +import { getStatusCount, isDone, setDone } from '../items'; describe('Item Utils', () => { describe('fn(isDone)', () => { @@ -114,4 +114,123 @@ describe('Item Utils', () => { ]); }); }); + + describe('fn(getStatusCount)', () => { + it('should support a simple item being done', () => { + expect( + getStatusCount({ + id: '', + label: '', + done: true, + subItems: [], + }) + ).toEqual({ + done: 1, + total: 1, + }); + }); + + it('should support a simple item not being done', () => { + expect( + getStatusCount({ + id: '', + label: '', + done: false, + subItems: [], + }) + ).toEqual({ + done: 0, + total: 1, + }); + }); + + it('should support nested items', () => { + expect( + getStatusCount({ + id: '', + label: '', + done: true, + subItems: [ + { + id: '', + label: '', + done: true, + subItems: [], + }, + { + id: '', + label: '', + done: true, + subItems: [], + }, + ], + }) + ).toEqual({ + done: 2, + total: 2, + }); + }); + + it('should support partially completed nested items', () => { + expect( + getStatusCount({ + id: '', + label: '', + done: false, + subItems: [ + { + id: '', + label: '', + done: false, + subItems: [], + }, + { + id: '', + label: '', + done: true, + subItems: [], + }, + ], + }) + ).toEqual({ + done: 1, + total: 2, + }); + }); + + it('should support complex lists', () => { + expect( + getStatusCount([ + { + id: '', + label: '', + done: true, + subItems: [ + { + id: '', + label: '', + done: true, + subItems: [], + }, + { + id: '', + label: '', + done: true, + subItems: [], + }, + ], + }, + { + id: '', + label: '', + done: true, + subItems: [], + }, + ]) + ).toEqual({ + done: 3, + total: 3, + }); + }); + }); }); diff --git a/src/lib/items.ts b/src/lib/items.ts index c15cac2..4e13247 100644 --- a/src/lib/items.ts +++ b/src/lib/items.ts @@ -35,3 +35,64 @@ export function hasChanged(updatedItem: Todo.Item, originalItem?: Todo.Item): bo export function hasNotChanged(updatedItem: Todo.Item, originalItem?: Todo.Item): boolean { return !hasChanged(updatedItem, originalItem); } + +export type StatusCountResult = { + done: number; + total: number; +}; + +// TODO(Refactor): Likely a way cleaner way to do this that my brain just isn't coming up with at the moment +export function getStatusCount(items: Todo.Item | Todo.Item[]): StatusCountResult { + if (items === undefined) + return { + done: 0, + total: 0, + }; + + if (Array.isArray(items)) { + return items.reduce( + (output, item) => { + const count = getStatusCount(item); + + return { + done: output.done + count.done, + total: output.total + count.total, + }; + }, + { + done: 0, + total: 0, + } + ); + } + + if (items.subItems.length > 0) { + return getStatusCount(items.subItems); + } + + if (!items.done) { + return { + done: 0, + total: 1, + }; + } + + return { + done: 1, + total: 1, + }; +} + +export enum ListStatus { + NOT_STARTED, + IN_PROGRESS, + DONE, +} + +export function getStatus({ done, total }: StatusCountResult): ListStatus { + if (done === 0) return ListStatus.NOT_STARTED; + + if (done === total) return ListStatus.DONE; + + return ListStatus.IN_PROGRESS; +} diff --git a/src/routes/TodoListsPage.tsx b/src/routes/TodoListsPage.tsx index 6459815..46dc3ae 100644 --- a/src/routes/TodoListsPage.tsx +++ b/src/routes/TodoListsPage.tsx @@ -1,23 +1,18 @@ -import { ConfirmDialog } from '@/components/ConfirmDialog'; +import { List } from '@/components/List'; import { PageContent } from '@/components/PageContent'; import { PageHeader } from '@/components/PageHeader'; import { Button } from '@/components/ui/button'; import { Storage, StorageKeys, Todo } from '@/storage'; import { createId } from '@paralleldrive/cuid2'; import { useCachedState } from '@rain-cafe/react-utils'; -import { BadgePlus, Flame, ListTodo } from 'lucide-react'; -import { useMemo, useState } from 'react'; -import { Link, useLoaderData } from 'react-router-dom'; +import { BadgePlus } from 'lucide-react'; +import { useLoaderData } from 'react-router-dom'; export function TodoListsPage() { const externalList = useLoaderData() as Todo.List[]; - const [search, setSearch] = useState(''); const [lists, setLists] = useCachedState(() => externalList, [externalList]); - const filteredLists = useMemo(() => { - return search ? lists?.filter((list) => list.label.includes(search)) : lists; - }, [lists, search]); - if (!lists || !filteredLists) return null; + if (!lists) return null; return ( <> @@ -49,29 +44,15 @@ export function TodoListsPage() { ) : ( <> - {filteredLists.map(({ id, label, items }) => ( -
- -
- {items.length} Items -
- { - setLists(lists.filter((list) => list.id !== id)); - await Storage.delete(StorageKeys.LISTS, id); - }} - > - - -
+ {lists.map((list) => ( + { + setLists(lists.filter(({ id }) => id !== list.id)); + await Storage.delete(StorageKeys.LISTS, list.id); + }} + /> ))} )}