mirror of
https://github.com/remvze/moodist.git
synced 2025-12-19 09:54:17 +00:00
feat: add todo checklist tool
This commit is contained in:
parent
876d575854
commit
cf77d33bd8
18 changed files with 305 additions and 1 deletions
|
|
@ -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';
|
||||
|
|
|
|||
18
src/components/menu/items/todo.tsx
Normal file
18
src/components/menu/items/todo.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}</>;
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
export { Notepad } from './notepad';
|
||||
export { Pomodoro } from './pomodoro';
|
||||
export { Todo } from './todo';
|
||||
|
|
|
|||
35
src/components/toolbox/todo/form/form.module.css
Normal file
35
src/components/toolbox/todo/form/form.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
33
src/components/toolbox/todo/form/form.tsx
Normal file
33
src/components/toolbox/todo/form/form.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
1
src/components/toolbox/todo/form/index.ts
Normal file
1
src/components/toolbox/todo/form/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { Form } from './form';
|
||||
1
src/components/toolbox/todo/index.ts
Normal file
1
src/components/toolbox/todo/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { Todo } from './todo';
|
||||
1
src/components/toolbox/todo/todo.module.css
Normal file
1
src/components/toolbox/todo/todo.module.css
Normal file
|
|
@ -0,0 +1 @@
|
|||
/* WIP */
|
||||
20
src/components/toolbox/todo/todo.tsx
Normal file
20
src/components/toolbox/todo/todo.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
1
src/components/toolbox/todo/todos/index.ts
Normal file
1
src/components/toolbox/todo/todos/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { Todos } from './todos';
|
||||
1
src/components/toolbox/todo/todos/todo/index.ts
Normal file
1
src/components/toolbox/todo/todos/todo/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { Todo } from './todo';
|
||||
45
src/components/toolbox/todo/todos/todo/todo.module.css
Normal file
45
src/components/toolbox/todo/todos/todo/todo.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
41
src/components/toolbox/todo/todos/todo/todo.tsx
Normal file
41
src/components/toolbox/todo/todos/todo/todo.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
1
src/components/toolbox/todo/todos/todos.module.css
Normal file
1
src/components/toolbox/todo/todos/todos.module.css
Normal file
|
|
@ -0,0 +1 @@
|
|||
/* WIP */
|
||||
17
src/components/toolbox/todo/todos/todos.tsx
Normal file
17
src/components/toolbox/todo/todos/todos.tsx
Normal 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
81
src/stores/todo.ts
Normal 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,
|
||||
},
|
||||
),
|
||||
);
|
||||
Loading…
Add table
Reference in a new issue