Stuff wip

This commit is contained in:
Daniel Flanagan 2024-02-18 09:23:37 -06:00
parent 5bef2090d0
commit ece80a7359
Signed by: lytedev
GPG key ID: 5B2020A0F9921EF4
9 changed files with 178 additions and 153 deletions

View file

@ -34,3 +34,13 @@ This will watch the project directory and restart as necessary.
- https://github.com/briosheje/Fresh-Deno-Mongo-Docker-Todoapp/tree/main - https://github.com/briosheje/Fresh-Deno-Mongo-Docker-Todoapp/tree/main
- https://hearthdisplay.com/ - https://hearthdisplay.com/
- https://github.com/denoland/showcase_todo - https://github.com/denoland/showcase_todo
# To Do
- Ditch pentagon, just use zod more heavily?
- These need a generic implementation (`crud.ts`?) I think:
- routes/api/user.ts
- routes/api/todo.ts
- routes/api/tasks.ts
- routes/api/todo/done.ts
- Avatar images are not perfectly round

41
browser.ts Normal file
View file

@ -0,0 +1,41 @@
export async function confirmBeforeDo(text: string, cb: () => Promise<void>) {
if (confirm(text)) await cb()
}
export async function GET(path: string) {
const req: RequestInit = {
method: 'GET',
headers: new Headers({
'accept': 'application/json',
}),
}
await fetch(path, req)
}
// deno-lint-ignore no-explicit-any
export async function reqWithBody(method: string, path: string, body: any) {
const req: RequestInit = {
method,
headers: new Headers({
'content-type': 'application/json',
'accept': 'application/json',
}),
body: JSON.stringify(body),
}
await fetch(path, req)
}
// deno-lint-ignore no-explicit-any
export async function POST(path: string, body: any) {
await reqWithBody('POST', path, body)
}
// deno-lint-ignore no-explicit-any
export async function PUT(path: string, body: any) {
await reqWithBody('POST', path, body)
}
// deno-lint-ignore no-explicit-any
export async function DELETE(path: string, body: any) {
await reqWithBody('DELETE', path, body)
}

View file

@ -1,59 +0,0 @@
import { confetti } from 'https://esm.sh/@tsparticles/confetti@3.0.3'
import { IS_BROWSER } from '$fresh/runtime.ts'
let fireworks: HTMLAudioElement | null
if (IS_BROWSER) {
fireworks = new Audio('/fireworks.mp3')
}
export function excitement() {
if (IS_BROWSER) {
const count = 200,
defaults = {
origin: { y: 0.7 },
}
if (fireworks) fireworks.play()
// deno-lint-ignore no-inner-declarations
function fire(particleRatio: number, opts: {
spread?: number
startVelocity?: number
decay?: number
scalar?: number
}) {
confetti(
Object.assign({}, defaults, opts, {
particleCount: Math.floor(count * particleRatio),
}),
)
}
fire(0.25, {
spread: 26,
startVelocity: 55,
})
fire(0.2, {
spread: 60,
})
fire(0.35, {
spread: 100,
decay: 0.91,
scalar: 0.8,
})
fire(0.1, {
spread: 120,
startVelocity: 25,
decay: 0.92,
scalar: 1.2,
})
fire(0.1, {
spread: 120,
startVelocity: 45,
})
}
}

View file

@ -1,26 +0,0 @@
import { JSX } from 'preact'
export function Select(
{ children, className, ...props }: JSX.HTMLAttributes<HTMLSelectElement>,
) {
if (!props.id && props.name) {
props.id = props.name
} else if (!props.name && props.id) {
props.name = props.id
}
let defaultClassName =
'border-2 bg-stone-500/20 border-stone-500/20 px-2 py-1 rounded'
if (props.type == 'submit') {
defaultClassName =
'px-2 py-1 bg-emerald-500/20 rounded hover:bg-gray-500/25 cursor-pointer transition-colors'
}
return (
<select
{...props}
class={`${defaultClassName} ${className}`}
>
{children}
</select>
)
}

59
db.ts
View file

@ -1,24 +1,45 @@
import { import { Identifiable, Task, Todo, User } from '@homeman/models.ts'
createPentagon, import { z } from 'https://deno.land/x/zod@v3.21.4/mod.ts'
TableDefinition, import { Created } from '@homeman/models.ts'
} from 'https://deno.land/x/pentagon@v0.1.5/mod.ts'
import { TodoModel, UserModel } from '@homeman/models.ts'
export const kv = await Deno.openKv('homeman.db') export const kv = await Deno.openKv('homeman.db')
export const schema: Record<string, TableDefinition> = { export const schema = {
users: { // key is table name, value is zod schema
schema: UserModel, user: User,
relations: { todo: Todo,
assignedTodos: ['todos', [TodoModel], 'id', 'assigneeUserId'], task: Task,
},
},
todos: {
schema: TodoModel,
relations: {
assignee: ['users', UserModel, 'assigneeUserId', 'id'],
},
},
} }
export const db = createPentagon(kv, schema) export type Schema = typeof schema
export type Table = keyof Schema
export type Model<T extends Table> = z.infer<Schema[T]>
export async function get<T extends Table>(
a: T,
id: z.infer<typeof Identifiable>['id'],
): Promise<Deno.KvEntryMaybe<Model<T>>> {
return await kv.get<Model<T>>([a, id])
}
export function all<T extends Table>(
a: T,
): Deno.KvListIterator<Model<T>> {
return kv.list<Model<T>>({ prefix: [a] })
}
type Create<T> = Omit<T, 'id' | 'createdAt'> | Partial<T>
export function create<T extends Table, K extends Identifiable>(
a: T,
payload: Create<K>,
): Deno.KvListIterator<Model<T>> {
const data: Create<Model<T>> | undefined = schema[a].parse(payload)
if (Identifiable.safeParse(data).success) {
delete data.id
}
if (Created.safeParse(data).success) {
delete data.createdBy
}
return kv.set([a, )
}

52
fireworks.ts Normal file
View file

@ -0,0 +1,52 @@
import { confetti } from 'https://esm.sh/@tsparticles/confetti@3.0.3'
const fireworks = new Audio('/fireworks.mp3')
function fire(particleRatio: number, opts: {
spread?: number
startVelocity?: number
decay?: number
scalar?: number
}) {
const count = 200,
defaults = {
origin: { y: 0.7 },
}
confetti(
Object.assign({}, defaults, opts, {
particleCount: Math.floor(count * particleRatio),
}),
)
}
export function excitement() {
fireworks.play()
fire(0.25, {
spread: 26,
startVelocity: 55,
})
fire(0.2, {
spread: 60,
})
fire(0.35, {
spread: 100,
decay: 0.91,
scalar: 0.8,
})
fire(0.1, {
spread: 120,
startVelocity: 25,
decay: 0.92,
scalar: 1.2,
})
fire(0.1, {
spread: 120,
startVelocity: 45,
})
}

View file

@ -8,36 +8,18 @@ import { Dialog } from '@homeman/components/Dialog.tsx'
import { import {
PencilSquareOutline, PencilSquareOutline,
PlusOutline, PlusOutline,
PlusSolid,
TrashOutline, TrashOutline,
} from 'preact-heroicons' } from 'preact-heroicons'
import { Table } from '@homeman/components/Table.tsx' import { Table } from '@homeman/components/Table.tsx'
import { SectionHead } from '@homeman/components/SectionHead.tsx' import { SectionHead } from '@homeman/components/SectionHead.tsx'
import { Avatar } from '@homeman/components/Avatar.tsx' import { Avatar } from '@homeman/components/Avatar.tsx'
import { confirmBeforeDo, DELETE } from '@homeman/browser.ts'
export interface Props { export interface Props {
users: Record<string, User> users: Record<string, User>
todos: Record<string, Todo> todos: Record<string, Todo>
} }
async function promptDeleteUser(id: string, name: string) {
if (confirm(`Are you sure you want to delete '${name}' (${id})?`)) {
await fetch(`/api/user?id=${id}`, { method: 'DELETE' })
location.reload()
}
}
async function promptDeleteTodo(id: string, description: string) {
if (confirm(`Are you sure you want to delete '${description}' (${id})?`)) {
await fetch(`/api/todo`, {
method: 'DELETE',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ id }),
})
location.reload()
}
}
interface UserFormProps extends JSX.HTMLAttributes<HTMLFormElement> { interface UserFormProps extends JSX.HTMLAttributes<HTMLFormElement> {
onCancelButtonClicked: JSX.MouseEventHandler<HTMLButtonElement> onCancelButtonClicked: JSX.MouseEventHandler<HTMLButtonElement>
userData: User | null userData: User | null
@ -140,7 +122,11 @@ export function Admin({ users, todos }: Props) {
<Button <Button
title='Delete' title='Delete'
className='py-2 mr-2 hover:bg-rose-500' className='py-2 mr-2 hover:bg-rose-500'
onClick={() => promptDeleteUser(id, name)} onClick={async () =>
await confirmBeforeDo(
`Are you sure you want to delete '${name}' (${id})?`,
async () => await DELETE('/api/user', { id }),
)}
> >
<TrashOutline class='w-6 h-6' /> <TrashOutline class='w-6 h-6' />
</Button> </Button>
@ -191,7 +177,7 @@ export function Admin({ users, todos }: Props) {
<Button <Button
title='Delete' title='Delete'
className='py-2 mr-2 hover:bg-rose-500' className='py-2 mr-2 hover:bg-rose-500'
onClick={() => promptDeleteTodo(id, description)} onClick={async () => await DELETE(`/api/todo`, { id })}
> >
<TrashOutline class='w-6 h-6' /> <TrashOutline class='w-6 h-6' />
</Button> </Button>

View file

@ -1,43 +1,41 @@
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 User = z.object({ export const Identifiable = z.object({
id: z.string().ulid().describe('primary'), id: z.string().ulid().describe('primary').default(ulid),
createdAt: z.date(),
name: z.string(),
avatarUrl: z.string(),
color: z.string(),
}) })
export const UserModel = User
const KVO = Identifiable
export const Created = z.object({
createdAt: z.date().default(() => new Date()),
})
export const User = KVO.merge(z.object({
name: z.string(),
avatarUrl: z.string().default('https://placekitten.com/512/512'),
color: z.string().default('#00aaff'),
}))
export type User = z.infer<typeof User> export type User = z.infer<typeof User>
export type UserWithTodos = User & {
assignedTodos: Todo[]
}
const Todo = z.object({
id: z.string().ulid().describe('primary'),
createdAt: z.date(),
export const Todo = KVO.merge(z.object({
description: z.string(), description: z.string(),
emoji: z.string().nullable(), emoji: z.string().nullable(),
doneAt: z.date().nullable(), doneAt: z.date().nullable(),
assigneeUserId: z.string().ulid().nullable(), assigneeUserId: z.string().ulid().nullable(),
}) }))
export const TodoModel = Todo
export type Todo = z.infer<typeof Todo> export type Todo = z.infer<typeof Todo>
const DailyPhase = z.enum([ export const DailyPhase = z.enum([
'Morning', 'Morning',
'Midday', 'Midday',
'Evening', 'Evening',
'Bedtime', 'Bedtime',
'Night', 'Night',
]) ])
export const DailyPhaseModel = DailyPhase
export type DailyPhase = z.infer<typeof DailyPhase> export type DailyPhase = z.infer<typeof DailyPhase>
export function toPhase(dt?: Date | null): z.infer<typeof DailyPhase> { export function toPhase(dt?: Date | null): DailyPhase {
const d = dt || new Date() const d = dt || new Date()
const h = d.getHours() const h = d.getHours()
@ -56,12 +54,7 @@ export function toPhase(dt?: Date | null): z.infer<typeof DailyPhase> {
// } else // } else
} }
const Task = z.object({ export const Task = Todo.omit({ assigneeUserId: true }).merge(z.object({
id: z.string(),
description: z.string(),
emoji: z.string().nullable(),
doneAt: z.date().nullable(),
phase: DailyPhase, phase: DailyPhase,
}) }))
export const TaskModel = Task
export type Task = z.infer<typeof Task> export type Task = z.infer<typeof Task>

7
refactor.md Normal file
View file

@ -0,0 +1,7 @@
<!-- Files that IMO need cleanup -->
- These need a generic implementation I think:
- routes/api/user.ts
- routes/api/todo.ts
- routes/api/tasks.ts
- routes/api/todo/done.ts