From 75ae53a99a4ebaf3f276df8895ba631c8834be77 Mon Sep 17 00:00:00 2001 From: Daniel Metcalfe Date: Tue, 16 Jul 2024 09:54:49 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Edit=20todos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/pages/Home.tsx | 99 ++++++++-------- components/todos/EditTodo.tsx | 166 +++++++++++++++++++++++++++ components/todos/SelectedTodo.tsx | 33 ++++++ components/todos/TodoActionSheet.tsx | 81 +++++-------- 4 files changed, 278 insertions(+), 101 deletions(-) create mode 100644 components/todos/EditTodo.tsx create mode 100644 components/todos/SelectedTodo.tsx diff --git a/components/pages/Home.tsx b/components/pages/Home.tsx index 2068f8b..f6c0de1 100644 --- a/components/pages/Home.tsx +++ b/components/pages/Home.tsx @@ -1,8 +1,5 @@ import { menuController } from '@ionic/core/components' -import _ from 'lodash' import { - ActionSheetButton, - IonActionSheet, IonButton, IonButtons, IonCard, @@ -43,23 +40,21 @@ import { useLiveQuery, useObservable } from 'dexie-react-hooks' import { add, checkmarkDoneCircleSharp, - filterSharp, - rocketSharp, - cloudOfflineSharp, cloudDoneSharp, - logOutSharp, + cloudOfflineSharp, documentText, + filterSharp, + logOutSharp, + rocketSharp, } from 'ionicons/icons' import { RefObject, useCallback, useEffect, useRef, useState } from 'react' import { Link } from 'react-router-dom' -import { CreatedTodo, db, Todo } from '../db' +import { CreatedTodo, db } from '../db' import NoteProviders from '../notes/providers' -import useSettings from '../settings/useSettings' import useNoteProvider from '../notes/useNoteProvider' -import { - TodoActionSheetProvider, - useTodoActionSheet, -} from '../todos/TodoActionSheet' +import useSettings from '../settings/useSettings' +import { SelectedTodoProvider } from '../todos/SelectedTodo' +import { useTodoActionSheet } from '../todos/TodoActionSheet' const Home = () => { // Search stuff @@ -171,11 +166,11 @@ const Home = () => { > - + - + { } export const Log = ({ todos }: { todos: any[] }) => { - const todoActionSheet = useTodoActionSheet() + const [present] = useTodoActionSheet() return ( <> @@ -474,7 +469,7 @@ export const Log = ({ todos }: { todos: any[] }) => { button key={todo.id} onClick={_event => { - todoActionSheet.open(todo) + present(todo) }} > { order: reorderedTodoIds, }) } - const todoActionSheet = useTodoActionSheet() + const [present] = useTodoActionSheet() return ( <> @@ -549,25 +544,27 @@ export const Important = ({ todos }: { todos: any[] }) => { { - todoActionSheet.open(todo, [ - { - text: 'Move to icebox', - data: { - action: 'icebox', - }, - handler: async () => { - db.transaction('rw', db.lists, async () => { - const list = await db.lists.get('#important') - await db.lists.update('#important', { - order: removeItemFromArray( - list!.order, - list!.order.indexOf(todo.id), - ), + present(todo, { + buttons: [ + { + text: 'Move to icebox', + data: { + action: 'icebox', + }, + handler: async () => { + db.transaction('rw', db.lists, async () => { + const list = await db.lists.get('#important') + await db.lists.update('#important', { + order: removeItemFromArray( + list!.order, + list!.order.indexOf(todo.id), + ), + }) }) - }) + }, }, - }, - ]) + ], + }) }} key={todo.id} > @@ -651,28 +648,30 @@ export const IceboxItem = ({ title: string } }) => { - const todoActionSheet = useTodoActionSheet() + const [present] = useTodoActionSheet() return ( { - todoActionSheet.open(todo as CreatedTodo, [ - { - text: 'Move to ranked', - data: { - action: 'ranked', - }, - handler: async () => { - db.transaction('rw', db.lists, async () => { - const list = await db.lists.get('#important') - db.lists.update('#important', { - order: [...list!.order, todo.id], + present(todo as CreatedTodo, { + buttons: [ + { + text: 'Move to ranked', + data: { + action: 'ranked', + }, + handler: async () => { + db.transaction('rw', db.lists, async () => { + const list = await db.lists.get('#important') + db.lists.update('#important', { + order: [...list!.order, todo.id], + }) }) - }) + }, }, - }, - ]) + ], + }) }} > diff --git a/components/todos/EditTodo.tsx b/components/todos/EditTodo.tsx new file mode 100644 index 0000000..6a22a00 --- /dev/null +++ b/components/todos/EditTodo.tsx @@ -0,0 +1,166 @@ +import { + IonButton, + IonButtons, + IonContent, + IonHeader, + IonIcon, + IonInput, + IonPage, + IonTextarea, + IonTitle, + IonToolbar, + useIonModal, +} from '@ionic/react' +import { openOutline } from 'ionicons/icons' +import { useCallback, useEffect, useRef, useState } from 'react' +import useNoteProvider from '../notes/useNoteProvider' +import { CreatedTodo, db } from '../db' +import useSelectedTodo from './SelectedTodo' + +export function EditTodoModal({ + dismiss, + todo, +}: { + dismiss: (data?: any, role?: string) => void + todo: CreatedTodo +}) { + const page = useRef(null) + const input = useRef(null) + const noteInput = useRef(null) + + useEffect(() => { + input.current?.setFocus() + }, []) + + const noteProvider = useNoteProvider() + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Enter') { + event.preventDefault() + dismiss( + { + title: input.current?.value, + note: noteInput.current?.value, + }, + 'confirm', + ) + } + } + page.current?.addEventListener('keydown', handleKeyDown) + return () => { + page.current?.removeEventListener('keydown', handleKeyDown) + } + }, [dismiss]) + + return ( + + + + Edit todo + + dismiss(null, 'cancel')} + > + Cancel + + + + { + dismiss( + { + ...todo, + title: input.current?.value, + note: noteInput.current?.value, + }, + 'confirm', + ) + }} + strong={true} + > + Confirm + + + + + + + {!noteProvider && ( +

Set a note provider in settings to enable this feature.

+ )} + {todo?.note ? ( + + ) : ( + + )} +
+
+ ) +} + +export function useEditTodoModal() { + const [todo, setTodo] = useSelectedTodo() + const [present, dismiss] = useIonModal(EditTodoModal, { + dismiss: (data: string, role: string) => dismiss(data, role), + todo, + }) + const noteProvider = useNoteProvider() + const editTodo = useCallback( + async (updatedTodo: CreatedTodo) => { + let uri + if (updatedTodo.note && noteProvider) { + uri = await noteProvider.create({ content: updatedTodo.note }) + } + await db.todos.update(updatedTodo.id, { + createdAt: new Date(), + title: updatedTodo.title, + ...(uri && { note: { uri } }), + }) + }, + [noteProvider], + ) + + return [ + (todo: CreatedTodo) => { + present({ + onWillPresent: () => { + setTodo(todo) + }, + onWillDismiss: event => { + const todo = event.detail.data + if (event.detail.role === 'confirm') editTodo(todo) + setTodo(null) + }, + }) + }, + dismiss, + ] +} diff --git a/components/todos/SelectedTodo.tsx b/components/todos/SelectedTodo.tsx new file mode 100644 index 0000000..c69bc12 --- /dev/null +++ b/components/todos/SelectedTodo.tsx @@ -0,0 +1,33 @@ +import { + createContext, + Dispatch, + SetStateAction, + useContext, + useState, +} from 'react' +import { CreatedTodo } from '../db' + +export const SelecteTodoContext = createContext< + [ + todo: CreatedTodo | null, + setTodo: Dispatch>, + ] +>([null, () => null]) + +export function SelectedTodoProvider({ + children, +}: { + children: React.ReactNode +}) { + const [todo, setTodo] = useState(null) + + return ( + + {children} + + ) +} + +export default function useSelectedTodo() { + return useContext(SelecteTodoContext) +} diff --git a/components/todos/TodoActionSheet.tsx b/components/todos/TodoActionSheet.tsx index 05c7c8b..5f83b0c 100644 --- a/components/todos/TodoActionSheet.tsx +++ b/components/todos/TodoActionSheet.tsx @@ -1,41 +1,29 @@ -import React, { - createContext, - PropsWithChildren, - useContext, - useState, -} from 'react' -import { ActionSheetButton, IonActionSheet } from '@ionic/react' -import { CreatedTodo, db, Todo } from '../db' +import { ActionSheetOptions, useIonActionSheet } from '@ionic/react' +import { HookOverlayOptions } from '@ionic/react/dist/types/hooks/HookOverlayOptions' +import { CreatedTodo, db } from '../db' +import { useEditTodoModal } from './EditTodo' -export const TodoActionSheetContext = createContext<{ - open: (todo: CreatedTodo, actions?: (ActionSheetButton | string)[]) => void - close: () => void -}>({ - open: () => null, - close: () => null, -}) - -export function TodoActionSheetProvider({ children }: PropsWithChildren) { - const [todo, setTodo] = useState(null) - const [actions, setActions] = useState<(ActionSheetButton | string)[]>([]) +// TODO: Make this so that todo is never null, action sheet doesn't make sense to be open if its null +export function useTodoActionSheet() { + // Using controller action sheet rather than inline because I was re-inventing what it was doing allowing dynamic options to be passed easily + const [presentActionSheet, dismissActionSheet] = useIonActionSheet() + // Using controller modal than inline because the trigger prop doesn't work with an ID on a controller-based action sheet button + const [presentEditTodoModal] = useEditTodoModal() - return ( - { - setTodo(todo) - console.log({ actions }) - if (actions) setActions(actions) - }, - close: () => { - setTodo(null) - }, - }} - > - {children} - { + presentActionSheet({ + buttons: [ + ...(options?.buttons || []), + { + text: 'Edit', + data: { + action: 'edit', + }, + handler: () => { + presentEditTodoModal(todo) + }, + }, { text: 'Delete', role: 'destructive', @@ -54,19 +42,10 @@ export function TodoActionSheetProvider({ children }: PropsWithChildren) { }) }, }, - ]} - header={todo?.title} - isOpen={!!todo} - onDidDismiss={() => { - setTodo(null) - setActions([]) - }} - /> - - ) -} - -export function useTodoActionSheet() { - const context = useContext(TodoActionSheetContext) - return context + ], + header: todo.title, + }) + }, + dismissActionSheet, + ] }