feat: add todo checklist tool

This commit is contained in:
MAZE 2024-08-31 15:32:04 +03:30
parent 876d575854
commit cf77d33bd8
18 changed files with 305 additions and 1 deletions

View file

@ -8,3 +8,4 @@ export { SleepTimer as SleepTimerItem } from './sleep-timer';
export { BreathingExercise as BreathingExerciseItem } from './breathing-exercise';
export { Pomodoro as PomodoroItem } from './pomodoro';
export { Notepad as NotepadItem } from './notepad';
export { Todo as TodoItem } from './todo';

View file

@ -0,0 +1,18 @@
import { MdTaskAlt } from 'react-icons/md/index';
import { Item } from '../item';
interface TodoProps {
open: () => void;
}
export function Todo({ open }: TodoProps) {
return (
<Item
icon={<MdTaskAlt />}
label="Todo Checklist"
shortcut="Shift + T"
onClick={open}
/>
);
}

View file

@ -15,6 +15,7 @@ import {
BreathingExerciseItem,
PomodoroItem,
NotepadItem,
TodoItem,
} from './items';
import { Divider } from './divider';
import { ShareLinkModal } from '@/components/modals/share-link';
@ -22,7 +23,7 @@ import { PresetsModal } from '@/components/modals/presets';
import { ShortcutsModal } from '@/components/modals/shortcuts';
import { SleepTimerModal } from '@/components/modals/sleep-timer';
import { BreathingExerciseModal } from '../modals/breathing';
import { Pomodoro, Notepad } from '../toolbox';
import { Pomodoro, Notepad, Todo } from '../toolbox';
import { fade, mix, slideY } from '@/lib/motion';
import { useSoundStore } from '@/stores/sound';
@ -44,6 +45,7 @@ export function Menu() {
shareLink: false,
shortcuts: false,
sleepTimer: false,
todo: false,
}),
[],
);
@ -113,6 +115,7 @@ export function Menu() {
<BreathingExerciseItem open={() => open('breathing')} />
<PomodoroItem open={() => open('pomodoro')} />
<NotepadItem open={() => open('notepad')} />
<TodoItem open={() => open('todo')} />
<Divider />
<ShortcutsItem open={() => open('shortcuts')} />
@ -146,6 +149,7 @@ export function Menu() {
onClose={() => close('pomodoro')}
/>
<Notepad show={modals.notepad} onClose={() => close('notepad')} />
<Todo show={modals.todo} onClose={() => close('todo')} />
<PresetsModal show={modals.presets} onClose={() => close('presets')} />
<SleepTimerModal
show={modals.sleepTimer}

View file

@ -3,6 +3,7 @@ import { useEffect } from 'react';
import { useSoundStore } from '@/stores/sound';
import { useNoteStore } from '@/stores/note';
import { usePresetStore } from '@/stores/preset';
import { useTodoStore } from '@/stores/todo';
interface StoreConsumerProps {
children: React.ReactNode;
@ -13,6 +14,7 @@ export function StoreConsumer({ children }: StoreConsumerProps) {
useSoundStore.persist.rehydrate();
useNoteStore.persist.rehydrate();
usePresetStore.persist.rehydrate();
useTodoStore.persist.rehydrate();
}, []);
return <>{children}</>;

View file

@ -1,2 +1,3 @@
export { Notepad } from './notepad';
export { Pomodoro } from './pomodoro';
export { Todo } from './todo';

View file

@ -0,0 +1,35 @@
.wrapper {
display: flex;
align-items: center;
height: 45px;
padding: 4px;
margin-top: 12px;
background-color: var(--color-neutral-50);
border: 1px solid var(--color-neutral-200);
border-radius: 8px;
& input {
flex-grow: 1;
min-width: 0;
height: 100%;
padding: 0 8px;
color: var(--color-foreground);
background-color: transparent;
border: none;
outline: none;
}
& button {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
padding: 0 16px;
font-size: var(--font-sm);
color: var(--color-foreground);
cursor: pointer;
background-color: var(--color-neutral-100);
border: 1px solid var(--color-neutral-200);
border-radius: 4px;
}
}

View file

@ -0,0 +1,33 @@
import { useState } from 'react';
import { useTodoStore } from '@/stores/todo';
import styles from './form.module.css';
export function Form() {
const [value, setValue] = useState('');
const addTodo = useTodoStore(state => state.addTodo);
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!value.trim().length) return;
addTodo(value);
setValue('');
};
return (
<form onSubmit={handleSubmit}>
<div className={styles.wrapper}>
<input
type="text"
value={value}
onChange={e => setValue(e.target.value)}
/>
<button type="submit">Add</button>
</div>
</form>
);
}

View file

@ -0,0 +1 @@
export { Form } from './form';

View file

@ -0,0 +1 @@
export { Todo } from './todo';

View file

@ -0,0 +1 @@
/* WIP */

View file

@ -0,0 +1,20 @@
import { Modal } from '@/components/modal';
import { Form } from './form';
import { Todos } from './todos';
import styles from './todo.module.css';
interface TodoProps {
onClose: () => void;
show: boolean;
}
export function Todo({ onClose, show }: TodoProps) {
return (
<Modal show={show} onClose={onClose}>
<h2 className={styles.title}>Todos</h2>
<Form />
<Todos />
</Modal>
);
}

View file

@ -0,0 +1 @@
export { Todos } from './todos';

View file

@ -0,0 +1 @@
export { Todo } from './todo';

View file

@ -0,0 +1,45 @@
.wrapper {
display: flex;
column-gap: 4px;
align-items: center;
height: 45px;
padding: 4px;
margin-top: 12px;
background-color: var(--color-neutral-50);
border: 1px solid var(--color-neutral-200);
border-radius: 8px;
& .checkbox {
display: block;
margin: 0 8px 0 4px;
}
& .textbox {
flex-grow: 1;
min-width: 0;
height: 100%;
font-size: var(--font-sm);
color: var(--color-foreground);
background-color: transparent;
border: none;
outline: none;
&.done {
color: var(--color-foreground-subtle);
text-decoration: line-through;
}
}
& button {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
aspect-ratio: 1 / 1;
color: #f43f5e;
cursor: pointer;
background-color: rgb(244 63 94 / 15%);
border: none;
border-radius: 4px;
}
}

View file

@ -0,0 +1,41 @@
import { FaRegTrashAlt } from 'react-icons/fa/index';
import { useTodoStore } from '@/stores/todo';
import { cn } from '@/helpers/styles';
import styles from './todo.module.css';
interface TodoProps {
done: boolean;
id: string;
todo: string;
}
export function Todo({ done, id, todo }: TodoProps) {
const deleteTodo = useTodoStore(state => state.deleteTodo);
const toggleTodo = useTodoStore(state => state.toggleTodo);
const editTodo = useTodoStore(state => state.editTodo);
const handleCheck = () => toggleTodo(id);
const handleDelete = () => deleteTodo(id);
return (
<div className={styles.wrapper}>
<input
checked={done}
className={styles.checkbox}
type="checkbox"
onChange={handleCheck}
/>
<input
className={cn(styles.textbox, done && styles.done)}
type="text"
value={todo}
onChange={e => editTodo(id, e.target.value)}
/>
<button onClick={handleDelete}>
<FaRegTrashAlt />
</button>
</div>
);
}

View file

@ -0,0 +1 @@
/* WIP */

View file

@ -0,0 +1,17 @@
import { Todo } from './todo';
import { useTodoStore } from '@/stores/todo';
import styles from './todos.module.css';
export function Todos() {
const todos = useTodoStore(state => state.todos);
return (
<div className={styles.todos}>
{todos.map(todo => (
<Todo done={todo.done} id={todo.id} key={todo.id} todo={todo.todo} />
))}
</div>
);
}

81
src/stores/todo.ts Normal file
View file

@ -0,0 +1,81 @@
import { create } from 'zustand';
import { createJSONStorage, persist } from 'zustand/middleware';
import merge from 'deepmerge';
import { v4 as uuid } from 'uuid';
interface TodoStore {
addTodo: (todo: string) => void;
deleteTodo: (id: string) => void;
editTodo: (id: string, newTodo: string) => void;
todos: Array<{
createdAt: number;
done: boolean;
id: string;
todo: string;
}>;
toggleTodo: (id: string) => void;
}
export const useTodoStore = create<TodoStore>()(
persist(
(set, get) => ({
addTodo(todo) {
set({
todos: [
{
createdAt: Date.now(),
done: false,
id: uuid(),
todo,
},
...get().todos,
],
});
},
deleteTodo(id) {
set({
todos: get().todos.filter(todo => todo.id !== id),
});
},
editTodo(id, newTodo) {
set({
todos: get().todos.map(todo => {
if (todo.id !== id) return todo;
return {
...todo,
todo: newTodo,
};
}),
});
},
todos: [],
toggleTodo(id) {
set({
todos: get().todos.map(todo => {
if (todo.id !== id) return todo;
return {
...todo,
done: !todo.done,
};
}),
});
},
}),
{
merge: (persisted, current) =>
merge(current, persisted as Partial<TodoStore>),
name: 'moodist-todos',
partialize: state => ({ todos: state.todos }),
skipHydration: true,
storage: createJSONStorage(() => localStorage),
version: 0,
},
),
);