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 { BreathingExercise as BreathingExerciseItem } from './breathing-exercise';
|
||||||
export { Pomodoro as PomodoroItem } from './pomodoro';
|
export { Pomodoro as PomodoroItem } from './pomodoro';
|
||||||
export { Notepad as NotepadItem } from './notepad';
|
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,
|
BreathingExerciseItem,
|
||||||
PomodoroItem,
|
PomodoroItem,
|
||||||
NotepadItem,
|
NotepadItem,
|
||||||
|
TodoItem,
|
||||||
} from './items';
|
} from './items';
|
||||||
import { Divider } from './divider';
|
import { Divider } from './divider';
|
||||||
import { ShareLinkModal } from '@/components/modals/share-link';
|
import { ShareLinkModal } from '@/components/modals/share-link';
|
||||||
|
|
@ -22,7 +23,7 @@ import { PresetsModal } from '@/components/modals/presets';
|
||||||
import { ShortcutsModal } from '@/components/modals/shortcuts';
|
import { ShortcutsModal } from '@/components/modals/shortcuts';
|
||||||
import { SleepTimerModal } from '@/components/modals/sleep-timer';
|
import { SleepTimerModal } from '@/components/modals/sleep-timer';
|
||||||
import { BreathingExerciseModal } from '../modals/breathing';
|
import { BreathingExerciseModal } from '../modals/breathing';
|
||||||
import { Pomodoro, Notepad } from '../toolbox';
|
import { Pomodoro, Notepad, Todo } from '../toolbox';
|
||||||
import { fade, mix, slideY } from '@/lib/motion';
|
import { fade, mix, slideY } from '@/lib/motion';
|
||||||
import { useSoundStore } from '@/stores/sound';
|
import { useSoundStore } from '@/stores/sound';
|
||||||
|
|
||||||
|
|
@ -44,6 +45,7 @@ export function Menu() {
|
||||||
shareLink: false,
|
shareLink: false,
|
||||||
shortcuts: false,
|
shortcuts: false,
|
||||||
sleepTimer: false,
|
sleepTimer: false,
|
||||||
|
todo: false,
|
||||||
}),
|
}),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
@ -113,6 +115,7 @@ export function Menu() {
|
||||||
<BreathingExerciseItem open={() => open('breathing')} />
|
<BreathingExerciseItem open={() => open('breathing')} />
|
||||||
<PomodoroItem open={() => open('pomodoro')} />
|
<PomodoroItem open={() => open('pomodoro')} />
|
||||||
<NotepadItem open={() => open('notepad')} />
|
<NotepadItem open={() => open('notepad')} />
|
||||||
|
<TodoItem open={() => open('todo')} />
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
<ShortcutsItem open={() => open('shortcuts')} />
|
<ShortcutsItem open={() => open('shortcuts')} />
|
||||||
|
|
@ -146,6 +149,7 @@ export function Menu() {
|
||||||
onClose={() => close('pomodoro')}
|
onClose={() => close('pomodoro')}
|
||||||
/>
|
/>
|
||||||
<Notepad show={modals.notepad} onClose={() => close('notepad')} />
|
<Notepad show={modals.notepad} onClose={() => close('notepad')} />
|
||||||
|
<Todo show={modals.todo} onClose={() => close('todo')} />
|
||||||
<PresetsModal show={modals.presets} onClose={() => close('presets')} />
|
<PresetsModal show={modals.presets} onClose={() => close('presets')} />
|
||||||
<SleepTimerModal
|
<SleepTimerModal
|
||||||
show={modals.sleepTimer}
|
show={modals.sleepTimer}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { useEffect } from 'react';
|
||||||
import { useSoundStore } from '@/stores/sound';
|
import { useSoundStore } from '@/stores/sound';
|
||||||
import { useNoteStore } from '@/stores/note';
|
import { useNoteStore } from '@/stores/note';
|
||||||
import { usePresetStore } from '@/stores/preset';
|
import { usePresetStore } from '@/stores/preset';
|
||||||
|
import { useTodoStore } from '@/stores/todo';
|
||||||
|
|
||||||
interface StoreConsumerProps {
|
interface StoreConsumerProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
|
@ -13,6 +14,7 @@ export function StoreConsumer({ children }: StoreConsumerProps) {
|
||||||
useSoundStore.persist.rehydrate();
|
useSoundStore.persist.rehydrate();
|
||||||
useNoteStore.persist.rehydrate();
|
useNoteStore.persist.rehydrate();
|
||||||
usePresetStore.persist.rehydrate();
|
usePresetStore.persist.rehydrate();
|
||||||
|
useTodoStore.persist.rehydrate();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
export { Notepad } from './notepad';
|
export { Notepad } from './notepad';
|
||||||
export { Pomodoro } from './pomodoro';
|
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