From 6e775230c510136818c524acae775b7744cd0d50 Mon Sep 17 00:00:00 2001 From: Daniel Metcalfe Date: Thu, 8 Aug 2024 16:00:14 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Role=20icons=20&=20editing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/launch.json | 2 +- components/db.ts | 8 +- components/pages/Constellation.tsx | 39 +++++--- components/pages/Home.tsx | 12 ++- components/starRoles/StarRoleActionSheet.tsx | 51 ++++++++++ components/starRoles/StarRoleModal.tsx | 92 +++++++++++++++---- components/starRoles/create/modal.tsx | 19 +--- .../create/useCreateStarRoleModal.ts | 9 +- components/starRoles/edit/modal.tsx | 19 +--- .../starRoles/edit/useEditStarRoleModal.ts | 16 ++-- components/starRoles/icons.tsx | 54 +++++++++++ components/todos/create/modal.tsx | 8 +- 12 files changed, 244 insertions(+), 85 deletions(-) create mode 100644 components/starRoles/StarRoleActionSheet.tsx create mode 100644 components/starRoles/icons.tsx diff --git a/.vscode/launch.json b/.vscode/launch.json index 0e16d72..242391c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,7 +8,7 @@ "type": "chrome", "request": "launch", "name": "Launch Chrome against localhost", - "url": "http://localhost:3000", + "url": "http://localhost:6603", "webRoot": "${workspaceFolder}" } ] diff --git a/components/db.ts b/components/db.ts index 28c5713..a73e444 100644 --- a/components/db.ts +++ b/components/db.ts @@ -6,7 +6,7 @@ export interface Todo { completedAt?: Date id: string note?: Note - starRole?: StarRole['id'] + starRole?: StarRole['id'] // TODO: How do we delete this when the star role is deleted? Need to add associative table and compute ID of association from todo ID and starRoleID, then delete entries when star roles gets deleted. starPoints?: string title: string } @@ -17,9 +17,15 @@ export interface Note { export interface StarRole { id: string + icon?: Icon title: string } +export interface Icon { + type: 'ionicon' + name: string +} + export interface List { order: string[] // Todo IDs type: '#important' diff --git a/components/pages/Constellation.tsx b/components/pages/Constellation.tsx index 947a336..6989300 100644 --- a/components/pages/Constellation.tsx +++ b/components/pages/Constellation.tsx @@ -19,6 +19,8 @@ import { add, starOutline } from 'ionicons/icons' import { RefObject, useCallback, useEffect, useRef } from 'react' import { db } from '../db' import { useCreateStarRoleModal } from '../starRoles/create/useCreateStarRoleModal' +import { getIonIcon } from '../starRoles/icons' +import { useStarRoleActionSheet } from '../starRoles/StarRoleActionSheet' export default function Constellation() { const starRoles = useLiveQuery(() => db.starRoles.toArray()) @@ -34,6 +36,8 @@ export default function Constellation() { }) }, [fab, presentCreateStarRoleModal]) + const [present] = useStarRoleActionSheet() + useGlobalKeyboardShortcuts(fab, openCreateStarRoleModal) return ( @@ -64,20 +68,29 @@ export default function Constellation() { ) : ( - - {starRoles.map(role => ( - - {role?.title} - - - ))} - + onIonItemReorder={handleReorder} + > */} + {starRoles.map(starRole => ( + { + present(starRole) + }} + > + {starRole?.title} + {starRole?.icon && ( + + )} + + + ))} + {/* */} )} { const [logLimit, setLogLimit] = useState(7) @@ -630,6 +630,7 @@ export const Important = () => { todo => matchesQuery(query, todo!) && inActiveStarRoles(todo!), ) as Todo[] }, [importantList, inActiveStarRoles, query]) + const starRoles = useLiveQuery(() => db.starRoles.toArray()) const [present] = useTodoActionSheet() @@ -727,6 +728,15 @@ export const Important = () => { }} /> {todo?.title} + {todo.starRole && ( + starRole.id === todo.starRole) + ?.icon?.name, + )} + slot="end" + /> + )} {todo.note && ( { + presentActionSheet({ + buttons: [ + ...(options?.buttons || []), + { + text: 'Edit', + data: { + action: 'edit', + }, + handler: () => { + presentEditStarRoleModal(starRole) + }, + }, + { + text: 'Delete', + role: 'destructive', + data: { + action: 'delete', + }, + handler: async () => { + db.transaction('rw', db.lists, db.starRoles, async () => { + await db.starRoles.delete(starRole.id) + const starRoles = await db.lists.get('#starRoles') + if (starRoles!.order.includes(starRole.id)) { + await db.lists.update('#starRoles', list => { + list.order = list.order.filter(id => id !== starRole.id) + }) + } + }) + }, + }, + ], + header: starRole.title, + }) + }, + dismissActionSheet, + ] +} diff --git a/components/starRoles/StarRoleModal.tsx b/components/starRoles/StarRoleModal.tsx index daba0c0..08bc78d 100644 --- a/components/starRoles/StarRoleModal.tsx +++ b/components/starRoles/StarRoleModal.tsx @@ -4,41 +4,75 @@ import { IonContent, IonFooter, IonHeader, + IonIcon, IonInput, IonPage, IonTitle, IonToolbar, } from '@ionic/react' -import { ComponentProps, MutableRefObject, ReactNode } from 'react' +import { + ComponentProps, + MutableRefObject, + ReactNode, + useCallback, + useState, +} from 'react' import { StarRole } from '../db' +import Icons, { getIonIcon } from './icons' export default function StarRoleModal({ dismiss, title, titleInput, - starRole = {}, + starRole, toolbarSlot, ...props }: { dismiss: (data?: any, role?: string) => void - title: string starRole?: Partial + title: string titleInput: MutableRefObject toolbarSlot?: ReactNode } & ComponentProps) { + const [starRoleTitleInput, setStarRoleTitleInput] = useState( + starRole?.title || '', + ) // TODO: Figure out why this becomes necessary due to other states resetting the title input value when its uncontrolled. + const [iconInput, setIconInput] = useState<{ + name: string + value: string + } | null>() + const [iconQuery, setIconQuery] = useState('') + + const initialIcon = starRole?.icon + ? { name: starRole.icon.name, value: getIonIcon(starRole?.icon.name) } + : null + const icon = iconInput || initialIcon + const starRoleTitle = starRoleTitleInput || starRole?.title + + const emitStarRole = useCallback(() => { + dismiss( + { + ...(icon + ? { + icon: { + type: 'ionicon', + name: icon.name, + }, + } + : {}), + title: starRoleTitle, + }, + 'confirm', + ) + }, [dismiss, icon, starRoleTitle]) + return ( { if (event.key === 'Enter') { event.preventDefault() - dismiss( - { - ...starRole, - title: titleInput.current?.value, - }, - 'confirm', - ) + emitStarRole() } props.onKeyDown?.(event) }} @@ -55,7 +89,35 @@ export default function StarRoleModal({ type="text" label="Title" labelPlacement="floating" - value={starRole?.title} + onIonInput={event => { + setStarRoleTitleInput(event.detail.value || '') + }} + value={starRoleTitleInput || starRole?.title} + /> + { + setIconQuery(event.detail.value || '') + }} + placeholder="Rocket" + type="text" + value={iconQuery} + > + + + {/* TODO: Refactor to radio group or select to avoid too many event listeners */} + { + setIconInput(icon) + }} /> @@ -71,13 +133,7 @@ export default function StarRoleModal({ { - dismiss( - { - ...starRole, - title: titleInput.current?.value, - }, - 'confirm', - ) + emitStarRole() }} strong={true} > diff --git a/components/starRoles/create/modal.tsx b/components/starRoles/create/modal.tsx index 53400a8..3e4b662 100644 --- a/components/starRoles/create/modal.tsx +++ b/components/starRoles/create/modal.tsx @@ -2,22 +2,7 @@ import { ComponentProps } from 'react' import StarRoleModal from '../StarRoleModal' export function CreateStarRoleModal({ - dismiss, ...props -}: { - dismiss: (data?: any, role?: string) => void -} & ComponentProps) { - return ( - { - dismiss( - { - starRole: data, - }, - role, - ) - }} - {...props} - /> - ) +}: ComponentProps) { + return } diff --git a/components/starRoles/create/useCreateStarRoleModal.ts b/components/starRoles/create/useCreateStarRoleModal.ts index 1233642..2f4867e 100644 --- a/components/starRoles/create/useCreateStarRoleModal.ts +++ b/components/starRoles/create/useCreateStarRoleModal.ts @@ -1,7 +1,7 @@ import { useIonModal } from '@ionic/react' import { HookOverlayOptions } from '@ionic/react/dist/types/hooks/HookOverlayOptions' import { useCallback, useRef } from 'react' -import { db } from '../../db' +import { db, StarRole } from '../../db' import { CreateStarRoleModal } from './modal' export function useCreateStarRoleModal(): [ @@ -18,8 +18,8 @@ export function useCreateStarRoleModal(): [ title: 'Create star role', titleInput, }) - const createStarRole = useCallback(async ({ title }: { title: any }) => { - db.starRoles.add({ title }) + const createStarRole = useCallback(async (properties: StarRole) => { + db.starRoles.add(properties) }, []) return [ @@ -30,8 +30,7 @@ export function useCreateStarRoleModal(): [ }, onWillDismiss: event => { if (event.detail.role === 'confirm') { - const { starRole } = event.detail.data - createStarRole(starRole) + createStarRole(event.detail.data) } onWillDismiss?.(event) }, diff --git a/components/starRoles/edit/modal.tsx b/components/starRoles/edit/modal.tsx index 512385e..9746c37 100644 --- a/components/starRoles/edit/modal.tsx +++ b/components/starRoles/edit/modal.tsx @@ -2,22 +2,7 @@ import { ComponentProps } from 'react' import StarRoleModal from '../StarRoleModal' export function EditStarRoleModal({ - dismiss, ...props -}: { - dismiss: (data?: any, role?: string) => void -} & ComponentProps) { - return ( - { - dismiss( - { - starRole: data, - }, - role, - ) - }} - {...props} - /> - ) +}: ComponentProps) { + return } diff --git a/components/starRoles/edit/useEditStarRoleModal.ts b/components/starRoles/edit/useEditStarRoleModal.ts index 040b7f0..b5ed6fd 100644 --- a/components/starRoles/edit/useEditStarRoleModal.ts +++ b/components/starRoles/edit/useEditStarRoleModal.ts @@ -16,11 +16,12 @@ export function useEditStarRoleModal(): [ titleInput, }) - const editStarRole = useCallback(async (updatedStarRole: StarRole) => { - await db.starRoles.update(updatedStarRole.id, { - title: updatedStarRole.title, - }) - }, []) + const editStarRole = useCallback( + async (starRoleId: string, updatedProperties: StarRole) => { + await db.starRoles.update(starRoleId, updatedProperties) + }, + [], + ) return [ (starRole: StarRole) => { @@ -32,8 +33,9 @@ export function useEditStarRoleModal(): [ setStarRole(starRole) }, onWillDismiss: event => { - const starRole = event.detail.data - if (event.detail.role === 'confirm') editStarRole(starRole) + if (event.detail.role === 'confirm') { + editStarRole(starRole.id, event.detail.data) + } setStarRole(null) }, }) diff --git a/components/starRoles/icons.tsx b/components/starRoles/icons.tsx new file mode 100644 index 0000000..9d6440d --- /dev/null +++ b/components/starRoles/icons.tsx @@ -0,0 +1,54 @@ +import { IonCol, IonGrid, IonIcon, IonRow } from '@ionic/react' +import * as icons from 'ionicons/icons' +import { useMemo } from 'react' + +export default function Icons({ + query, + onClick, +}: { + query: string + onClick: (icon: { name: string; value: string }) => void +}) { + const matchingIcons = useMemo(() => { + return Object.entries(icons).filter(([name]) => + !query ? true : name.includes('Sharp') && name.includes(query), + ) + }, [query]) + + return ( + + + + View all icons + + + {matchingIcons.length ? ( + matchingIcons.map(([name, value]) => ( + + onClick({ name, value })} + icon={value} + > + + )) + ) : ( +

+ No matching icons +

+ )} + + + ) +} + +export function getIonIcon(name) { + return icons[name] +} diff --git a/components/todos/create/modal.tsx b/components/todos/create/modal.tsx index 39f6086..4791662 100644 --- a/components/todos/create/modal.tsx +++ b/components/todos/create/modal.tsx @@ -1,8 +1,6 @@ -import { IonSelect, IonSelectOption, useIonModal } from '@ionic/react' -import { HookOverlayOptions } from '@ionic/react/dist/types/hooks/HookOverlayOptions' -import { ComponentProps, useCallback, useRef } from 'react' -import { db, ListType } from '../../db' -import useNoteProvider from '../../notes/useNoteProvider' +import { IonSelect, IonSelectOption } from '@ionic/react' +import { ComponentProps, useRef } from 'react' +import { ListType } from '../../db' import TodoModal from '../TodoModal' export function CreateTodoModal({