Tasks eventing wip

This commit is contained in:
Daniel Flanagan 2024-01-21 14:02:10 -06:00
parent 2ce48df6f6
commit 573692c6a7
Signed by: lytedev
GPG key ID: 5B2020A0F9921EF4
7 changed files with 231 additions and 69 deletions

6
db.ts
View file

@ -2,7 +2,7 @@ import {
createPentagon, createPentagon,
TableDefinition, TableDefinition,
} from 'https://deno.land/x/pentagon@v0.1.5/mod.ts' } from 'https://deno.land/x/pentagon@v0.1.5/mod.ts'
import { TaskModel, TodoModel, UserModel } from '@homeman/models.ts' import { DoneTaskModel, TodoModel, UserModel } from '@homeman/models.ts'
export const kv = await Deno.openKv('homeman.db') export const kv = await Deno.openKv('homeman.db')
@ -19,8 +19,8 @@ export const schema: Record<string, TableDefinition> = {
assignee: ['users', UserModel, 'assigneeUserId', 'id'], assignee: ['users', UserModel, 'assigneeUserId', 'id'],
}, },
}, },
tasks: { doneTasks: {
schema: TaskModel, schema: DoneTaskModel,
}, },
} }

View file

@ -13,6 +13,7 @@
in { in {
deno-dev = pkgs.mkShell { deno-dev = pkgs.mkShell {
buildInputs = with pkgs; [ buildInputs = with pkgs; [
sqlite
deno deno
xh xh
]; ];

View file

@ -6,6 +6,7 @@ import * as $_404 from './routes/_404.tsx'
import * as $_app from './routes/_app.tsx' import * as $_app from './routes/_app.tsx'
import * as $admin from './routes/admin.tsx' import * as $admin from './routes/admin.tsx'
import * as $api_joke from './routes/api/joke.ts' import * as $api_joke from './routes/api/joke.ts'
import * as $api_tasks from './routes/api/tasks.ts'
import * as $api_todo from './routes/api/todo.ts' import * as $api_todo from './routes/api/todo.ts'
import * as $api_todo_done from './routes/api/todo/done.ts' import * as $api_todo_done from './routes/api/todo/done.ts'
import * as $api_user from './routes/api/user.ts' import * as $api_user from './routes/api/user.ts'
@ -27,6 +28,7 @@ const manifest = {
'./routes/_app.tsx': $_app, './routes/_app.tsx': $_app,
'./routes/admin.tsx': $admin, './routes/admin.tsx': $admin,
'./routes/api/joke.ts': $api_joke, './routes/api/joke.ts': $api_joke,
'./routes/api/tasks.ts': $api_tasks,
'./routes/api/todo.ts': $api_todo, './routes/api/todo.ts': $api_todo,
'./routes/api/todo/done.ts': $api_todo_done, './routes/api/todo/done.ts': $api_todo_done,
'./routes/api/user.ts': $api_user, './routes/api/user.ts': $api_user,

View file

@ -1,9 +1,16 @@
import { DailyPhase, DailyPhaseModel, Task, toPhase } from '@homeman/models.ts' import {
DailyPhase,
DailyPhaseModel,
DoneTask,
Task,
toPhase,
} from '@homeman/models.ts'
import { useSignal } from '@preact/signals' import { useSignal } from '@preact/signals'
import { excitement } from '@homeman/common.ts' import { excitement } from '@homeman/common.ts'
export interface Props { export interface Props {
tasks: Task[] tasks: Task[]
done: DoneTask[]
} }
interface TaskWithIndex extends Task { interface TaskWithIndex extends Task {
@ -55,7 +62,7 @@ export function Routine(
<main class='flex flex-col overflow-x-scroll'> <main class='flex flex-col overflow-x-scroll'>
<ul> <ul>
{taskGroups[currentPhase.value].map(( {taskGroups[currentPhase.value].map((
{ emoji, text, doneAt, index }: TaskWithIndex, { emoji, id, doneAt, index }: TaskWithIndex,
) => ( ) => (
<li <li
role='button' role='button'
@ -67,11 +74,24 @@ export function Routine(
? null ? null
: new Date() : new Date()
tasks.value = [...tasks.value] tasks.value = [...tasks.value]
if (tasks.value[index].doneAt) excitement() if (tasks.value[index].doneAt) {
fetch('/api/tasks', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ id: id }),
})
excitement()
} else {
fetch('/api/tasks', {
method: 'DELETE',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ id: id }),
})
}
}} }}
> >
{emoji ? `${emoji.trim()} ` : ''} {emoji ? `${emoji.trim()} ` : ''}
{text.trim()} {id.trim()}
</li> </li>
))} ))}
</ul> </ul>

View file

@ -57,10 +57,17 @@ export function toPhase(dt?: Date | null): z.infer<typeof DailyPhase> {
} }
const Task = z.object({ const Task = z.object({
id: z.string(),
emoji: z.string().nullable(), emoji: z.string().nullable(),
text: z.string(),
doneAt: z.date().nullable(), doneAt: z.date().nullable(),
phase: DailyPhase, phase: DailyPhase,
}) })
export const TaskModel = Task export const TaskModel = Task
export type Task = z.infer<typeof Task> export type Task = z.infer<typeof Task>
const DoneTask = Task.pick({
id: true,
doneAt: true,
})
export const DoneTaskModel = DoneTask
export type DoneTask = z.infer<typeof DoneTask>

139
routes/api/tasks.ts Normal file
View file

@ -0,0 +1,139 @@
import { Handlers } from '$fresh/server.ts'
import { DoneTask, DoneTaskModel } from '@homeman/models.ts'
import { db, kv } from '@homeman/db.ts'
import { z } from 'https://deno.land/x/zod@v3.21.4/mod.ts'
const DoneTaskPayload = DoneTaskModel.partial({ doneAt: true })
type DoneTaskPayload = z.infer<typeof DoneTaskPayload>
export const handler: Handlers<DoneTask | null> = {
async POST(req, _ctx) {
// a task is marked done
let id: string | undefined
if (req.headers.get('content-type')?.includes('json')) {
id = DoneTaskPayload.parse(await req.json()).id
} else {
id = new URL(req.url).searchParams.get('id') || undefined
}
console.log('done task post:', id)
if (!id) {
return new Response(
JSON.stringify({
message: "can't complete task with id empty string",
}),
{ status: 400 },
)
}
const newDoneTask: DoneTask = {
id,
doneAt: new Date(),
}
const res = await db.doneTasks.create({
data: newDoneTask,
})
console.log('done task create result:', res)
console.log(await db.doneTasks.findMany({}))
await kv.set(['last_task_updated'], id)
return new Response(JSON.stringify({ id }))
},
async DELETE(req, _ctx) {
let id: string | undefined
if (req.headers.get('content-type')?.includes('json')) {
id = DoneTaskPayload.parse(await req.json()).id
} else {
id = new URL(req.url).searchParams.get('id') || undefined
}
console.log('done task delete:', id)
if (!id) {
return new Response(
JSON.stringify({
message: "can't uncomplete task with id empty string",
}),
{ status: 400 },
)
}
if (!id) {
return new Response(
JSON.stringify({
message: "can't complete task with id empty string",
}),
{ status: 400 },
)
}
await db.doneTasks.deleteMany({
where: {
id,
},
})
await kv.set(['last_task_updated'], id)
return new Response(JSON.stringify({ id }))
},
async GET(req, ctx) {
// TODO: json or query params
const accept = req.headers.get('accept')
if (accept === 'text/event-stream') {
console.log('Request for task event stream')
let skipFirst = true
const stream = kv.watch([['last_task_updated']]).getReader()
const body = new ReadableStream({
async start(controller) {
console.log(
`Streaming task updates to ${JSON.stringify(ctx.remoteAddr)}...`,
)
while (true) {
try {
const entries = await stream.read()
for (const entry of entries.value || []) {
if (skipFirst) {
skipFirst = false
continue
}
if (typeof entry.value !== 'string') {
continue
}
const task = await db.doneTask.findFirst({
where: { id: entry.value },
})
const chunk = `data: ${
JSON.stringify({
id: entry.value,
versionstamp: entry.versionstamp,
value: task,
})
}\n\n`
console.log('todo event chunk:', chunk)
controller.enqueue(new TextEncoder().encode(chunk))
}
if (entries.done) {
return
}
} catch (e) {
console.error(`Error refreshing todo:`, e)
}
}
},
cancel() {
stream.cancel()
console.log(
`Closed todo updates stream to ${JSON.stringify(ctx.remoteAddr)}`,
)
},
})
return new Response(body, {
headers: {
'content-type': 'text/event-stream',
},
})
}
return new Response(
JSON.stringify({ done: await db.doneTasks.findMany({}) }),
)
},
}

View file

@ -1,79 +1,72 @@
import { Handlers, PageProps } from '$fresh/server.ts' import { Handlers, PageProps } from '$fresh/server.ts'
import { Task } from '@homeman/models.ts' import { DailyPhase, DoneTask, Task } from '@homeman/models.ts'
import { Routine } from '@homeman/islands/Routine.tsx' import { Routine } from '@homeman/islands/Routine.tsx'
import { db } from '@homeman/db.ts'
// import { db, kv, Todo, UserWithTodos } from '@homeman/models.ts' // import { db, kv, Todo, UserWithTodos } from '@homeman/models.ts'
interface Data { interface Data {
tasks: Task[] tasks: Task[]
done: DoneTask[]
} }
export const handler: Handlers = { export const handler: Handlers = {
GET(_req, ctx) { async GET(_req, ctx) {
const tasks: Task[] = Array.from([]) const tasks: Task[] = Array.from([])
console.log(tasks) console.log(tasks)
Array.prototype.forEach.apply([ const hardcoded: Record<DailyPhase, [string, string][]> = {
['🥣', 'Breakfast'], 'Morning': [
['🪥', 'Brush teeth'], ['🥣', 'Breakfast'],
['👕', 'Get dressed'], ['🪥', 'Brush teeth'],
['🙏', 'Ask and thank Jesus'], ['👕', 'Get dressed'],
['✝️', 'Bible story or devotional'], ['🙏', 'Ask and thank Jesus'],
['📚', 'School: with Mrs. Emily or Mama'], ['✝️', 'Bible story or devotional'],
['🎨', 'Create time: 🧶craft ✏️ draw 🧑🏼‍🎨 paint'], ['📚', 'School: with Mrs. Emily or Mama'],
['🏗️', ' Build time: 🧱legos 🚂train tracks 🏎magna tiles'], ['🎨', 'Create time: 🧶craft ✏️ draw 🧑🏼‍🎨 paint'],
['👯', 'Friend time: playdate or neighbor time'], ['🏗️', ' Build time: 🧱legos 🚂train tracks 🏎magna tiles'],
['🚗', 'Outing: 📚library 🌳park 🥑groceries ☕ coffee shop'], ['👯', 'Friend time: playdate or neighbor time'],
], [([emoji, text]) => { ['🚗', 'Outing: 📚library 🌳park 🥑groceries ☕ coffee shop'],
tasks.push({ ],
emoji, 'Midday': [
text, ['🥓', 'Lunch'],
doneAt: null, ['🧹', 'Tidy time'],
phase: 'Morning', ['🤫', 'Quiet time'],
}) ['🏃', 'BIG energy time: 🚲bike 🥁 drums 🤸 silly play'],
}]) ['👯', 'Friend time: playdate or neighbor time'],
['🚗', 'Outing: 📚library 🌳park 🥑groceries'],
],
'Evening': [
['🍽️', 'Dinner'],
['🚗', 'Outing'],
['👪', 'Family time: 🃏games 🕺dance party 🤸silly play 🏋workout'],
],
'Bedtime': [
['🪥', 'Brush teeth'],
['🛁', 'Take bath'],
['👕', 'Put on pajamas'],
['🙏', 'Ask and thank Jesus'],
['✝️', 'Bible story or devotional'],
['📕', 'Read a story'],
['❤', 'Emotions check in: 😭 😡 😂 😟 😣 😀 ☹️ 😰 😁'],
],
'Night': [
['👨‍⚖️', 'Sleep!'],
],
}
Object.entries(hardcoded).forEach(([tphase, phaseTasks]) => {
const phase = tphase as DailyPhase
for (const [emoji, id] of phaseTasks) {
tasks.push({ phase, emoji, id, doneAt: null })
}
})
return ctx.render({ tasks }) const done = await db.doneTasks.findMany({})
console.log({ done })
return ctx.render({ tasks, done })
}, },
} }
export default function Page(props: PageProps<Data>) { export default function Page(props: PageProps<Data>) {
console.log(props) return <Routine tasks={props.data.tasks} done={props.data.done} />
return <Routine tasks={props.data.tasks} />
} }
/*
🌄 MORNING
🥣 Breakfast
🪥Brush teeth
👕Get dressed
🙏Ask and thank Jesus
Bible story or devotional
📚School: with Mrs. Emily or Mama
🎨Create time: 🧶craft draw 🧑🏼🎨 paint
🏗 Build time: 🧱legos 🚂train tracks 🏎magna tiles
👯Friend time: playdate or neighbor time
🚗 Outing: 📚library 🌳park 🥑groceries coffee shop
🌤 AFTERNOON
🥓Lunch
🧹Tidy time
🤫Quiet time
🏃 BIG energy time: 🚲bike 🥁 drums 🤸 silly play
👯Friend time: playdate or neighbor time
🚗Outing: 📚library 🌳park 🥑groceries
🌇 EVENING
🍽Dinner
🚗Outing
👪Family time: 🃏games 🕺dance party 🤸silly play 🏋workout
🛏 BEDTIME
🪥Brush teeth
🛁Take bath
👕Put on pajamas
🙏Ask and thank Jesus
Bible story or devotional
📕Read a story
😭 😡 😂 😟 😣 😀 😰 😁Emotions check in
*/