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,
TableDefinition,
} 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')
@ -19,8 +19,8 @@ export const schema: Record<string, TableDefinition> = {
assignee: ['users', UserModel, 'assigneeUserId', 'id'],
},
},
tasks: {
schema: TaskModel,
doneTasks: {
schema: DoneTaskModel,
},
}

View file

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

View file

@ -6,6 +6,7 @@ import * as $_404 from './routes/_404.tsx'
import * as $_app from './routes/_app.tsx'
import * as $admin from './routes/admin.tsx'
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_done from './routes/api/todo/done.ts'
import * as $api_user from './routes/api/user.ts'
@ -27,6 +28,7 @@ const manifest = {
'./routes/_app.tsx': $_app,
'./routes/admin.tsx': $admin,
'./routes/api/joke.ts': $api_joke,
'./routes/api/tasks.ts': $api_tasks,
'./routes/api/todo.ts': $api_todo,
'./routes/api/todo/done.ts': $api_todo_done,
'./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 { excitement } from '@homeman/common.ts'
export interface Props {
tasks: Task[]
done: DoneTask[]
}
interface TaskWithIndex extends Task {
@ -55,7 +62,7 @@ export function Routine(
<main class='flex flex-col overflow-x-scroll'>
<ul>
{taskGroups[currentPhase.value].map((
{ emoji, text, doneAt, index }: TaskWithIndex,
{ emoji, id, doneAt, index }: TaskWithIndex,
) => (
<li
role='button'
@ -67,11 +74,24 @@ export function Routine(
? null
: new Date()
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()} ` : ''}
{text.trim()}
{id.trim()}
</li>
))}
</ul>

View file

@ -57,10 +57,17 @@ export function toPhase(dt?: Date | null): z.infer<typeof DailyPhase> {
}
const Task = z.object({
id: z.string(),
emoji: z.string().nullable(),
text: z.string(),
doneAt: z.date().nullable(),
phase: DailyPhase,
})
export const TaskModel = 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,18 +1,21 @@
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 { db } from '@homeman/db.ts'
// import { db, kv, Todo, UserWithTodos } from '@homeman/models.ts'
interface Data {
tasks: Task[]
done: DoneTask[]
}
export const handler: Handlers = {
GET(_req, ctx) {
async GET(_req, ctx) {
const tasks: Task[] = Array.from([])
console.log(tasks)
Array.prototype.forEach.apply([
const hardcoded: Record<DailyPhase, [string, string][]> = {
'Morning': [
['🥣', 'Breakfast'],
['🪥', 'Brush teeth'],
['👕', 'Get dressed'],
@ -23,57 +26,47 @@ export const handler: Handlers = {
['🏗️', ' Build time: 🧱legos 🚂train tracks 🏎magna tiles'],
['👯', 'Friend time: playdate or neighbor time'],
['🚗', 'Outing: 📚library 🌳park 🥑groceries ☕ coffee shop'],
], [([emoji, text]) => {
tasks.push({
emoji,
text,
doneAt: null,
phase: 'Morning',
],
'Midday': [
['🥓', '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: 😭 😡 😂 😟 😣 😀 ☹️ 😰 😁'],
],
'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>) {
console.log(props)
return <Routine tasks={props.data.tasks} />
return <Routine tasks={props.data.tasks} done={props.data.done} />
}
/*
🌄 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
*/