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://hearthdisplay.com/
|
||||
- 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 {
|
||||
createPentagon,
|
||||
TableDefinition,
|
||||
} from 'https://deno.land/x/pentagon@v0.1.5/mod.ts'
|
||||
import { TodoModel, UserModel } from '@homeman/models.ts'
|
||||
import { Identifiable, Task, Todo, User } from '@homeman/models.ts'
|
||||
import { z } from 'https://deno.land/x/zod@v3.21.4/mod.ts'
|
||||
import { Created } from '@homeman/models.ts'
|
||||
|
||||
export const kv = await Deno.openKv('homeman.db')
|
||||
|
||||
export const schema: Record<string, TableDefinition> = {
|
||||
users: {
|
||||
schema: UserModel,
|
||||
relations: {
|
||||
assignedTodos: ['todos', [TodoModel], 'id', 'assigneeUserId'],
|
||||
},
|
||||
},
|
||||
todos: {
|
||||
schema: TodoModel,
|
||||
relations: {
|
||||
assignee: ['users', UserModel, 'assigneeUserId', 'id'],
|
||||
},
|
||||
},
|
||||
export const schema = {
|
||||
// key is table name, value is zod schema
|
||||
user: User,
|
||||
todo: Todo,
|
||||
task: Task,
|
||||
}
|
||||
|
||||
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 {
|
||||
PencilSquareOutline,
|
||||
PlusOutline,
|
||||
PlusSolid,
|
||||
TrashOutline,
|
||||
} from 'preact-heroicons'
|
||||
import { Table } from '@homeman/components/Table.tsx'
|
||||
import { SectionHead } from '@homeman/components/SectionHead.tsx'
|
||||
import { Avatar } from '@homeman/components/Avatar.tsx'
|
||||
import { confirmBeforeDo, DELETE } from '@homeman/browser.ts'
|
||||
|
||||
export interface Props {
|
||||
users: Record<string, User>
|
||||
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> {
|
||||
onCancelButtonClicked: JSX.MouseEventHandler<HTMLButtonElement>
|
||||
userData: User | null
|
||||
|
@ -140,7 +122,11 @@ export function Admin({ users, todos }: Props) {
|
|||
<Button
|
||||
title='Delete'
|
||||
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' />
|
||||
</Button>
|
||||
|
@ -191,7 +177,7 @@ export function Admin({ users, todos }: Props) {
|
|||
<Button
|
||||
title='Delete'
|
||||
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' />
|
||||
</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'
|
||||
|
||||
const User = z.object({
|
||||
id: z.string().ulid().describe('primary'),
|
||||
createdAt: z.date(),
|
||||
|
||||
name: z.string(),
|
||||
avatarUrl: z.string(),
|
||||
color: z.string(),
|
||||
export const Identifiable = z.object({
|
||||
id: z.string().ulid().describe('primary').default(ulid),
|
||||
})
|
||||
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 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(),
|
||||
emoji: z.string().nullable(),
|
||||
|
||||
doneAt: z.date().nullable(),
|
||||
assigneeUserId: z.string().ulid().nullable(),
|
||||
})
|
||||
export const TodoModel = Todo
|
||||
}))
|
||||
export type Todo = z.infer<typeof Todo>
|
||||
|
||||
const DailyPhase = z.enum([
|
||||
export const DailyPhase = z.enum([
|
||||
'Morning',
|
||||
'Midday',
|
||||
'Evening',
|
||||
'Bedtime',
|
||||
'Night',
|
||||
])
|
||||
export const DailyPhaseModel = 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 h = d.getHours()
|
||||
|
||||
|
@ -56,12 +54,7 @@ export function toPhase(dt?: Date | null): z.infer<typeof DailyPhase> {
|
|||
// } else
|
||||
}
|
||||
|
||||
const Task = z.object({
|
||||
id: z.string(),
|
||||
description: z.string(),
|
||||
emoji: z.string().nullable(),
|
||||
doneAt: z.date().nullable(),
|
||||
export const Task = Todo.omit({ assigneeUserId: true }).merge(z.object({
|
||||
phase: DailyPhase,
|
||||
})
|
||||
export const TaskModel = 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