This commit is contained in:
Daniel Flanagan 2024-01-21 14:23:45 -06:00
parent 573692c6a7
commit beb488b3d6
Signed by: lytedev
GPG key ID: 5B2020A0F9921EF4
6 changed files with 145 additions and 135 deletions

2
.gitignore vendored
View file

@ -17,3 +17,5 @@ node_modules/
*.db-shm *.db-shm
/static/uploads /static/uploads
/data

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 { DoneTaskModel, TodoModel, UserModel } from '@homeman/models.ts' import { TaskModel, 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'],
}, },
}, },
doneTasks: { tasks: {
schema: DoneTaskModel, schema: TaskModel,
}, },
} }

View file

@ -58,16 +58,10 @@ export function toPhase(dt?: Date | null): z.infer<typeof DailyPhase> {
const Task = z.object({ const Task = z.object({
id: z.string(), id: z.string(),
description: z.string(),
emoji: z.string().nullable(), emoji: z.string().nullable(),
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>

60
playground.ts Normal file
View file

@ -0,0 +1,60 @@
import { DailyPhase, TaskModel } from '@homeman/models.ts'
import { db } from '@homeman/db.ts'
import { ulid } from 'https://deno.land/x/ulid@v0.3.0/mod.ts'
const hardcoded: Record<DailyPhase, [string, string][]> = {
'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(async ([tphase, phaseTasks]) => {
const phase = tphase as DailyPhase
for (const [emoji, description] of phaseTasks) {
const task = TaskModel.parse({
phase,
emoji,
description,
id: ulid(),
doneAt: null,
})
console.log(task)
const result = await db.tasks.create({
data: task,
})
console.log('after create:', result)
}
})

View file

@ -1,81 +1,81 @@
import { Handlers } from '$fresh/server.ts' import { Handlers } from '$fresh/server.ts'
import { DoneTask, DoneTaskModel } from '@homeman/models.ts' import { Task, TaskModel } from '@homeman/models.ts'
import { db, kv } from '@homeman/db.ts' import { db, kv } from '@homeman/db.ts'
import { ulid } from 'https://deno.land/x/ulid@v0.3.0/mod.ts'
import { z } from 'https://deno.land/x/zod@v3.21.4/mod.ts' import { z } from 'https://deno.land/x/zod@v3.21.4/mod.ts'
const DoneTaskPayload = DoneTaskModel.partial({ doneAt: true }) const TaskPayload = TaskModel.partial({ id: true })
type DoneTaskPayload = z.infer<typeof DoneTaskPayload> type TaskPayload = z.infer<typeof TaskPayload>
export const handler: Handlers<DoneTask | null> = { async function createOrUpdate(task: TaskPayload) {
if (!task.id || task.id === '') {
const newTask: Task = {
...task,
id: ulid(),
}
const result = await db.tasks.create({ data: newTask })
await kv.set(['last_task_updated'], newTask.id)
return result
} else {
const result = await db.tasks.update({ where: { id: task.id }, data: task })
await kv.set(['last_task_updated'], task.id)
return result
}
}
export const handler: Handlers<Task | null> = {
async POST(req, _ctx) { async POST(req, _ctx) {
// a task is marked done
let id: string | undefined
if (req.headers.get('content-type')?.includes('json')) { if (req.headers.get('content-type')?.includes('json')) {
id = DoneTaskPayload.parse(await req.json()).id const result = await createOrUpdate(
} else { TaskPayload.parse(await req.json()),
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 },
) )
return new Response(JSON.stringify(result))
} else {
const form = await req.formData()
const id = form.get('id')?.toString() || undefined
const doneAt = form.get('doneAt')
console.log('task POST doneAt:', doneAt)
const task = TaskPayload.parse({
id: id,
emoji: form.get('emoji')?.toString() || null,
doneAt: form.get('doneAt')?.toString() || null,
description: form.get('description')?.toString(),
assigneeUserId: form.get('assigneeUserId')?.toString() || null,
})
if (!id) {
delete task.id
}
if (!task.assigneeUserId) {
delete task.id
}
await createOrUpdate(task)
const url = new URL(req.url)
url.pathname = '/'
return Response.redirect(url, 303)
} }
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) { async DELETE(req, _ctx) {
let id: string | undefined // task: form or query params or json
let data
if (req.headers.get('content-type')?.includes('json')) { if (req.headers.get('content-type')?.includes('json')) {
id = DoneTaskPayload.parse(await req.json()).id data = await req.json()
} else { } else {
id = new URL(req.url).searchParams.get('id') || undefined data = { id: new URL(req.url).searchParams.get('id') }
} }
console.log('done task delete:', id) console.log('delete task data:', data)
if (!id) { const taskData = TaskModel.pick({ id: true }).parse(data)
return new Response( const result = await db.tasks.delete({ where: taskData })
JSON.stringify({ await kv.set(['last_task_updated'], taskData.id)
message: "can't uncomplete task with id empty string", return new Response(JSON.stringify(result))
}),
{ 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) { async GET(req, ctx) {
// TODO: json or query params // task: json or query params
const accept = req.headers.get('accept') const accept = req.headers.get('accept')
if (accept === 'text/event-stream') { if (accept === 'text/event-stream') {
console.log('Request for task event stream') console.log('Request for task event stream')
@ -98,7 +98,7 @@ export const handler: Handlers<DoneTask | null> = {
continue continue
} }
const task = await db.doneTask.findFirst({ const task = await db.tasks.findFirst({
where: { id: entry.value }, where: { id: entry.value },
}) })
const chunk = `data: ${ const chunk = `data: ${
@ -108,21 +108,21 @@ export const handler: Handlers<DoneTask | null> = {
value: task, value: task,
}) })
}\n\n` }\n\n`
console.log('todo event chunk:', chunk) console.log('task event chunk:', chunk)
controller.enqueue(new TextEncoder().encode(chunk)) controller.enqueue(new TextEncoder().encode(chunk))
} }
if (entries.done) { if (entries.done) {
return return
} }
} catch (e) { } catch (e) {
console.error(`Error refreshing todo:`, e) console.error(`Error refreshing task:`, e)
} }
} }
}, },
cancel() { cancel() {
stream.cancel() stream.cancel()
console.log( console.log(
`Closed todo updates stream to ${JSON.stringify(ctx.remoteAddr)}`, `Closed task updates stream to ${JSON.stringify(ctx.remoteAddr)}`,
) )
}, },
}) })
@ -132,8 +132,14 @@ export const handler: Handlers<DoneTask | null> = {
}, },
}) })
} }
return new Response( const data = await req.json().catch(() => {})
JSON.stringify({ done: await db.doneTasks.findMany({}) }), const taskData = TaskModel.pick({ id: true }).safeParse(data)
) if (taskData.success) {
return new Response(
JSON.stringify(await db.tasks.findFirst({ where: taskData.data })),
)
} else {
return new Response(JSON.stringify(await db.tasks.findMany({})))
}
}, },
} }

View file

@ -1,72 +1,20 @@
import { Handlers, PageProps } from '$fresh/server.ts' import { Handlers, PageProps } from '$fresh/server.ts'
import { DailyPhase, DoneTask, Task } from '@homeman/models.ts' import { 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 } 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 = {
async GET(_req, ctx) { async GET(_req, ctx) {
const tasks: Task[] = Array.from([]) const tasks = await db.tasks.findMany({})
console.log(tasks) return ctx.render({ tasks })
const hardcoded: Record<DailyPhase, [string, string][]> = {
'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 })
}
})
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>) {
return <Routine tasks={props.data.tasks} done={props.data.done} /> return <Routine tasks={props.data.tasks} />
} }