Skip to content

Commit

Permalink
✨ Todo snoozing
Browse files Browse the repository at this point in the history
  • Loading branch information
homostellaris committed Jan 10, 2025
1 parent 2bcecbe commit f444be2
Show file tree
Hide file tree
Showing 7 changed files with 312 additions and 62 deletions.
21 changes: 20 additions & 1 deletion components/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ export interface Todo {
title: string
}

export interface TodoMeta {
order: string
snoozedUntil?: Date
}

export type WayfinderOrder = {
todoId: string
} & TodoMeta

export type EnrichedTodo = Todo & TodoMeta

export interface Note {
uri: string
}
Expand Down Expand Up @@ -42,7 +53,7 @@ export interface Setting {
}

export class DexieStarfocus extends Dexie {
wayfinderOrder!: DexieCloudTable<{ todoId: string; order: string }, 'todoId'>
wayfinderOrder!: DexieCloudTable<WayfinderOrder, 'todoId'>
lists!: Table<List>
settings!: DexieCloudTable<Setting, 'key'>
starRoles: DexieCloudTable<StarRole, 'id'>
Expand Down Expand Up @@ -80,6 +91,14 @@ export class DexieStarfocus extends Dexie {
starRolesOrder: '&starRoleId, &order',
todos: '@id, createdAt, completedAt, starRole, title',
})
this.version(5).stores({
lists: 'type',
wayfinderOrder: '&todoId, &order, snoozedUntil',
settings: '&key',
starRoles: '@id, title',
starRolesOrder: '&starRoleId, &order',
todos: '@id, createdAt, completedAt, starRole, title',
})
this.cloud.configure({
databaseUrl: process.env.NEXT_PUBLIC_DATABASE_URL!,
requireAuth: false,
Expand Down
149 changes: 103 additions & 46 deletions components/pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
import { useLiveQuery } from 'dexie-react-hooks'
import {
add,
calendarSharp,
chevronDownOutline,
chevronUpOutline,
filterSharp,
Expand All @@ -42,6 +43,7 @@ import {
import _ from 'lodash'
import {
ComponentProps,
forwardRef,
RefObject,
useCallback,
useEffect,
Expand All @@ -62,16 +64,30 @@ import { useTodoActionSheet } from '../todos/TodoActionSheet'
import useTodoContext, { TodoContextProvider } from '../todos/TodoContext'
import { useCreateTodoModal } from '../todos/create/useCreateTodoModal'
import { groupTodosByCompletedAt } from '../todos/groupTodosByCompletedAt'
import { useSnoozeTodoModal } from '../todos/snooze/useSnoozeTodoModal'
import useView, { ViewProvider } from '../view'

const Home = () => {
const searchbarRef = useRef<HTMLIonSearchbarElement>(null)
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === '/') {
event.preventDefault()
searchbarRef.current?.setFocus()
}
}
document.addEventListener('keydown', handleKeyDown)
return () => {
document.removeEventListener('keydown', handleKeyDown)
}
})
useGlobalKeyboardShortcuts()

return (
<>
<ViewProvider>
<TodoContextProvider>
<ViewMenu />
<ViewMenu searchbarRef={searchbarRef} />
<IonPage id="main-content">
<Header title="Home" />
<TodoLists />
Expand All @@ -93,7 +109,7 @@ const Home = () => {
/>
</IonButton>
</IonButtons>
<Searchbar />
<Searchbar ref={searchbarRef} />
</IonToolbar>
</IonFooter>
</IonPage>
Expand Down Expand Up @@ -150,6 +166,7 @@ export const TodoLists = ({}: {}) => {
.map((todo, index) => ({
...todo!,
order: todoOrderItems[index].order,
snoozedUntil: todoOrderItems[index].snoozedUntil,
}))
.filter(
todo => matchesQuery(query, todo) && inActiveStarRoles(todo),
Expand Down Expand Up @@ -243,6 +260,7 @@ export const TodoLists = ({}: {}) => {
}, [contentRef, fab, nextTodoPosition, openCreateTodoModal])

const [present] = useTodoActionSheet()
const [presentSnoozeTodoModal] = useSnoozeTodoModal()

const [logGroups, todayCompletedTodos] = useMemo(() => {
if (!todos?.log) return [[], []]
Expand Down Expand Up @@ -371,7 +389,14 @@ export const TodoLists = ({}: {}) => {
starRole => todo.starRole === starRole.id,
)}
todo={todo}
/>
>
<IonIcon
color="medium"
icon={calendarSharp}
slot="end"
title={`Completed on ${todo.completedAt?.toDateString()}`}
></IonIcon>
</TodoListItem>
))}
</div>
</IonItemGroup>
Expand Down Expand Up @@ -423,7 +448,14 @@ export const TodoLists = ({}: {}) => {
starRole => todo.starRole === starRole.id,
)}
todo={todo}
/>
>
<IonIcon
color="medium"
icon={calendarSharp}
slot="end"
title={`Completed on ${todo.completedAt?.toDateString()}`}
></IonIcon>
</TodoListItem>
))}
<IonReorderGroup
disabled={false}
Expand Down Expand Up @@ -521,6 +553,13 @@ export const TodoLists = ({}: {}) => {
)
},
},
{
text: 'Snooze',
data: {
action: 'snooze',
},
handler: () => presentSnoozeTodoModal(todo),
},
],
})
}}
Expand All @@ -530,7 +569,10 @@ export const TodoLists = ({}: {}) => {
)}
todo={{ ...todo }}
>
<IonReorder slot="end"></IonReorder>
<IonReorder
slot="end"
title={`Rank ${index + 1}`}
></IonReorder>
</TodoListItem>
))}
</IonReorderGroup>
Expand Down Expand Up @@ -719,14 +761,19 @@ export const MiscMenu = () => {
)
}

export const ViewMenu = () => {
export const ViewMenu = ({
searchbarRef,
}: {
searchbarRef: RefObject<HTMLIonSearchbarElement>
}) => {
const starRoles = useLiveQuery(() => db.starRoles.toArray())
const isLoading = starRoles === undefined
const {
activateStarRole,
activeStarRoles,
deactivateStarRole,
setActiveStarRoles,
setQuery,
} = useView()

return (
Expand All @@ -742,6 +789,18 @@ export const ViewMenu = () => {
</IonToolbar>
</IonHeader>
<IonContent className="space-y-4 ion-padding">
<IonButton
onClick={() => {
console.log({ searchbarRef })
if (searchbarRef.current) {
searchbarRef.current.value = 'is:snoozed'
// Setting the value doesn't trigger ionic searchbar events so need to also set query ourselves
setQuery('is:snoozed')
}
}}
>
View snoozed
</IonButton>
<IonButton routerLink="/constellation">Edit roles</IonButton>
{isLoading ? (
<IonSpinner
Expand Down Expand Up @@ -942,45 +1001,33 @@ export const Icebox = ({ todos }: { todos: Todo[] }) => {
)
}

export const Searchbar = () => {
const searchbarRef = useRef<HTMLIonSearchbarElement>(null)

useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === '/') {
event.preventDefault()
searchbarRef.current?.setFocus()
}
}
document.addEventListener('keydown', handleKeyDown)
return () => {
document.removeEventListener('keydown', handleKeyDown)
}
})
const { setQuery } = useView()
export const Searchbar = forwardRef<HTMLIonSearchbarElement>(
function Searchbar(_props, ref) {
const { setQuery } = useView()

return (
<IonSearchbar
ref={searchbarRef}
debounce={100}
/* Binding to the capture phase allows the searchbar to complete its native behaviour of clearing the input.
* Without this the input would blur but the input would still have a value and the todos would still be filtered. */
onKeyDownCapture={event => {
if (event.key === 'Escape') {
// TS complains unless we narrow the type
if (document.activeElement instanceof HTMLElement)
document.activeElement.blur()
}
}}
onIonInput={event => {
const target = event.target as HTMLIonSearchbarElement
let query = ''
if (target?.value) query = target.value.toLowerCase()
setQuery(query)
}}
></IonSearchbar>
)
}
return (
<IonSearchbar
ref={ref}
debounce={100}
/* Binding to the capture phase allows the searchbar to complete its native behaviour of clearing the input.
* Without this the input would blur but the input would still have a value and the todos would still be filtered. */
onKeyDownCapture={event => {
if (event.key === 'Escape') {
// TS complains unless we narrow the type
if (document.activeElement instanceof HTMLElement)
document.activeElement.blur()
}
}}
onIonInput={event => {
const target = event.target as HTMLIonSearchbarElement
let query = ''
if (target?.value) query = target.value.toLowerCase()
setQuery(query)
}}
></IonSearchbar>
)
},
)

function JourneyLabel({ children }: ComponentProps<typeof IonItemDivider>) {
return (
Expand All @@ -1001,8 +1048,18 @@ function TimeInfo({ datetime, label }: { datetime: string; label: string }) {
)
}

function matchesQuery(query: string, todo: Todo) {
if (!query) return true
function matchesQuery(query: string, todo: Todo & { snoozedUntil?: Date }) {
console.log({ query, todo })
if (!query && todo.snoozedUntil && todo.snoozedUntil > new Date()) {
return false
}
if (!query) {
return true
}
if (todo.snoozedUntil && query === 'is:snoozed') {
console.log('HALLELUJAH')
return true
}
return todo?.title.toLowerCase().includes(query)
}

Expand Down
2 changes: 1 addition & 1 deletion components/todos/TodoActionSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useEditTodoModal } from './edit/useEditTodoModal'
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
// Using controller modal rather than inline because the trigger prop doesn't work with an ID on a controller-based action sheet button
const [presentEditTodoModal] = useEditTodoModal()

return [
Expand Down
3 changes: 1 addition & 2 deletions components/todos/TodoModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,17 @@ import {
IonTitle,
IonToolbar,
} from '@ionic/react'
import { useLiveQuery } from 'dexie-react-hooks'
import { openOutline } from 'ionicons/icons'
import {
ComponentProps,
MutableRefObject,
ReactNode,
useCallback,
useEffect,
useRef,
} from 'react'
import { db, Todo } from '../db'
import useNoteProvider from '../notes/useNoteProvider'
import { useLiveQuery } from 'dexie-react-hooks'

export default function TodoModal({
dismiss,
Expand Down
Loading

0 comments on commit f444be2

Please sign in to comment.