diff --git a/db.ts b/db.ts index 58b2f3b..2b02ada 100644 --- a/db.ts +++ b/db.ts @@ -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 = { assignee: ['users', UserModel, 'assigneeUserId', 'id'], }, }, - tasks: { - schema: TaskModel, + doneTasks: { + schema: DoneTaskModel, }, } diff --git a/flake.nix b/flake.nix index e0e76b7..e144448 100644 --- a/flake.nix +++ b/flake.nix @@ -13,6 +13,7 @@ in { deno-dev = pkgs.mkShell { buildInputs = with pkgs; [ + sqlite deno xh ]; diff --git a/fresh.gen.ts b/fresh.gen.ts index 2105b02..d715e1f 100644 --- a/fresh.gen.ts +++ b/fresh.gen.ts @@ -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, diff --git a/islands/Routine.tsx b/islands/Routine.tsx index f98c8d5..1f465f2 100644 --- a/islands/Routine.tsx +++ b/islands/Routine.tsx @@ -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(
diff --git a/models.ts b/models.ts index 30d95d9..2a87408 100644 --- a/models.ts +++ b/models.ts @@ -57,10 +57,17 @@ export function toPhase(dt?: Date | null): z.infer { } 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 + +const DoneTask = Task.pick({ + id: true, + doneAt: true, +}) +export const DoneTaskModel = DoneTask +export type DoneTask = z.infer diff --git a/routes/api/tasks.ts b/routes/api/tasks.ts new file mode 100644 index 0000000..a21e1fe --- /dev/null +++ b/routes/api/tasks.ts @@ -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 + +export const handler: Handlers = { + 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({}) }), + ) + }, +} diff --git a/routes/routine.tsx b/routes/routine.tsx index 1458145..49ac3ef 100644 --- a/routes/routine.tsx +++ b/routes/routine.tsx @@ -1,79 +1,72 @@ 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([ - ['🥣', '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'], - ], [([emoji, text]) => { - tasks.push({ - emoji, - text, - doneAt: null, - phase: 'Morning', - }) - }]) + const hardcoded: Record = { + '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'], + ], + '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) { - console.log(props) - return + return } - -/* -🌄 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 - -*/