Stuff wip
This commit is contained in:
parent
5bef2090d0
commit
ece80a7359
10
README.md
10
README.md
|
@ -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
41
browser.ts
Normal 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)
|
||||||
|
}
|
59
common.ts
59
common.ts
|
@ -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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
59
db.ts
|
@ -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
52
fireworks.ts
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
49
models.ts
49
models.ts
|
@ -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
7
refactor.md
Normal 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
|
Loading…
Reference in a new issue