Skip to content

Commit

Permalink
✨ Role icons & editing
Browse files Browse the repository at this point in the history
  • Loading branch information
homostellaris committed Aug 8, 2024
1 parent 87eca0a commit 20e5b83
Show file tree
Hide file tree
Showing 9 changed files with 224 additions and 28 deletions.
8 changes: 7 additions & 1 deletion components/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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'
Expand Down
40 changes: 27 additions & 13 deletions components/pages/Constellation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -34,6 +36,8 @@ export default function Constellation() {
})
}, [fab, presentCreateStarRoleModal])

const [present] = useStarRoleActionSheet()

useGlobalKeyboardShortcuts(fab, openCreateStarRoleModal)

return (
Expand Down Expand Up @@ -64,20 +68,30 @@ export default function Constellation() {
</div>
) : (
<IonList inset>
<IonReorderGroup
{/* <IonReorderGroup
disabled={false}
// onIonItemReorder={handleReorder}
>
{starRoles.map(role => (
<IonItem
button
key={role.id}
>
<IonLabel>{role?.title}</IonLabel>
<IonReorder slot="end"></IonReorder>
</IonItem>
))}
</IonReorderGroup>
onIonItemReorder={handleReorder}
> */}
{starRoles.map(starRole => (
<IonItem
button
key={starRole.id}
onClick={() => {
console.log({ starRole })
present(starRole)
}}
>
<IonLabel>{starRole?.title}</IonLabel>
{starRole?.icon && (
<IonIcon
icon={getIonIcon(starRole.icon.name)}
slot="end"
/>
)}
<IonReorder slot="end"></IonReorder>
</IonItem>
))}
{/* </IonReorderGroup> */}
</IonList>
)}
<IonFab
Expand Down
12 changes: 11 additions & 1 deletion components/pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ import {
cloudUploadSharp,
documentText,
filterSharp,
logOutSharp,
rocketSharp,
thunderstormSharp,
} from 'ionicons/icons'
Expand All @@ -66,6 +65,7 @@ import { SelectedTodoProvider } from '../todos/SelectedTodo'
import { useTodoActionSheet } from '../todos/TodoActionSheet'
import { useCreateTodoModal } from '../todos/create/useCreateTodoModal'
import useView, { ViewProvider } from '../view'
import { getIonIcon } from '../starRoles/icons'

const Home = () => {
const [logLimit, setLogLimit] = useState(7)
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -727,6 +728,15 @@ export const Important = () => {
}}
/>
<IonLabel>{todo?.title}</IonLabel>
{todo.starRole && (
<IonIcon
icon={getIonIcon(
starRoles?.find(starRole => starRole.id === todo.starRole)
?.icon?.name,
)}
slot="end"
/>
)}
{todo.note && (
<a
href={todo.note.uri}
Expand Down
51 changes: 51 additions & 0 deletions components/starRoles/StarRoleActionSheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { ActionSheetOptions, useIonActionSheet } from '@ionic/react'
import { HookOverlayOptions } from '@ionic/react/dist/types/hooks/HookOverlayOptions'
import { StarRole, db } from '../db'
import { useEditStarRoleModal } from './edit/useEditStarRoleModal'

// TODO: Make this so that todo is never null, action sheet doesn't make sense to be open if its null
export function useStarRoleActionSheet() {
// 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 [presentEditStarRoleModal] = useEditStarRoleModal()

return [
(starRole: StarRole, options?: ActionSheetOptions & HookOverlayOptions) => {
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,
]
}
67 changes: 64 additions & 3 deletions components/starRoles/StarRoleModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import {
IonContent,
IonFooter,
IonHeader,
IonIcon,
IonInput,
IonPage,
IonTitle,
IonToolbar,
} from '@ionic/react'
import { ComponentProps, MutableRefObject, ReactNode } from 'react'
import { ComponentProps, MutableRefObject, ReactNode, useState } from 'react'
import { StarRole } from '../db'
import Icons from './icons'

export default function StarRoleModal({
dismiss,
Expand All @@ -26,6 +28,16 @@ export default function StarRoleModal({
titleInput: MutableRefObject<HTMLIonInputElement | null>
toolbarSlot?: ReactNode
} & ComponentProps<typeof IonPage>) {
const [starRoleTitle, setStarRoleTitle] = useState<string>(
starRole?.title || '',
) // TODO: Figure out why this becomes necessary due to other states resetting the title input value when its uncontrolled.
const [iconQuery, setIconQuery] = useState<string>('')
const [showIcons, setShowIcons] = useState(false)
const [selectedIcon, setSelectedIcon] = useState<{
name: string
value: string
} | null>(null)

return (
<IonPage
{...props}
Expand All @@ -35,6 +47,14 @@ export default function StarRoleModal({
dismiss(
{
...starRole,
...(selectedIcon
? {
icon: {
type: 'ionicon',
name: selectedIcon?.name,
},
}
: {}),
title: titleInput.current?.value,
},
'confirm',
Expand All @@ -55,8 +75,41 @@ export default function StarRoleModal({
type="text"
label="Title"
labelPlacement="floating"
value={starRole?.title}
onIonInput={event => {
setStarRoleTitle(event.detail.value || '')
}}
value={starRoleTitle}
/>
<IonInput
debounce={200}
fill="outline"
helperText="Type a search query and select an icon from the list that appears below"
label="Icon"
labelPlacement="floating"
onIonInput={event => {
setIconQuery(event.detail.value || '')
}}
onIonFocus={() => setShowIcons(true)}
placeholder="Rocket"
type="text"
value={iconQuery}
>
{selectedIcon && (
<IonIcon
icon={selectedIcon.value}
slot="end"
/>
)}
</IonInput>
{/* TODO: Refactor to radio group or select to avoid too many event listeners */}
{showIcons && (
<Icons
query={iconQuery}
onClick={icon => {
setSelectedIcon(icon)
}}
/>
)}
</IonContent>
<IonFooter>
<IonToolbar>
Expand All @@ -74,7 +127,15 @@ export default function StarRoleModal({
dismiss(
{
...starRole,
title: titleInput.current?.value,
...(selectedIcon
? {
icon: {
type: 'ionicon',
name: selectedIcon?.name,
},
}
: {}),
title: starRoleTitle,
},
'confirm',
)
Expand Down
6 changes: 3 additions & 3 deletions components/starRoles/create/useCreateStarRoleModal.ts
Original file line number Diff line number Diff line change
@@ -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(): [
Expand All @@ -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 ({ icon, title }: StarRole) => {
db.starRoles.add({ icon, title })
}, [])

return [
Expand Down
6 changes: 4 additions & 2 deletions components/starRoles/edit/useEditStarRoleModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ export function useEditStarRoleModal(): [
setStarRole(starRole)
},
onWillDismiss: event => {
const starRole = event.detail.data
if (event.detail.role === 'confirm') editStarRole(starRole)
if (event.detail.role === 'confirm') {
const { starRole } = event.detail.data
editStarRole(starRole)
}
setStarRole(null)
},
})
Expand Down
54 changes: 54 additions & 0 deletions components/starRoles/icons.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<IonGrid className="relative p-2 border rounded">
<IonRow>
<a
className="absolute p-1 space-x-1 text-xs bg-white right-2 -top-3"
href="https://ionic.io/ionicons"
target="_blank"
>
<span>View all icons</span>
<IonIcon icon={icons.openSharp} />
</a>
{matchingIcons.length ? (
matchingIcons.map(([name, value]) => (
<IonCol
key={name}
size="1"
>
<IonIcon
className="cursor-pointer"
onClick={() => onClick({ name, value })}
icon={value}
></IonIcon>
</IonCol>
))
) : (
<p className="w-full m-2 text-center text-gray-300">
No matching icons
</p>
)}
</IonRow>
</IonGrid>
)
}

export function getIonIcon(name) {
return icons[name]
}
8 changes: 3 additions & 5 deletions components/todos/create/modal.tsx
Original file line number Diff line number Diff line change
@@ -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({
Expand Down

0 comments on commit 20e5b83

Please sign in to comment.