Tasks eventing wip
This commit is contained in:
parent
2ce48df6f6
commit
573692c6a7
6
db.ts
6
db.ts
|
@ -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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
];
|
];
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
139
routes/api/tasks.ts
Normal 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({}) }),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
|
@ -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
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
Loading…
Reference in a new issue