Merge remote-tracking branch 'origin/master'

This commit is contained in:
Daniel Flanagan 2024-01-16 19:20:59 -06:00
commit 871472deff
Signed by: lytedev
GPG key ID: 5B2020A0F9921EF4
14 changed files with 209 additions and 85 deletions

View file

@ -1,19 +0,0 @@
// import { JSX } from 'preact'
// import { IS_BROWSER } from '$fresh/runtime.ts'
import { Bars3Outline } from 'preact-heroicons'
import { Clock } from '@homeman/islands/Clock.tsx'
export function Nav(/* props: {} */) {
return (
<nav class='bg-stone-200 dark:bg-stone-800 flex justify-items-start items-center'>
<button class='p-4 hover:bg-stone-500/20'>
<Bars3Outline class='h-6 w-6' />
</button>
<a class='p-4 hover:bg-stone-500/20' href='/'>
Flanagan Family
</a>
<Clock class='ml-auto pr-4 text-2xl' />
</nav>
)
}

View file

@ -9,19 +9,10 @@
"preview": "deno run -A main.ts", "preview": "deno run -A main.ts",
"update": "deno run -A -r https://fresh.deno.dev/update ." "update": "deno run -A -r https://fresh.deno.dev/update ."
}, },
"lint": { "lint": { "rules": { "tags": ["fresh", "recommended"] } },
"rules": { "exclude": ["**/_fresh/*"],
"tags": [
"fresh",
"recommended"
]
}
},
"exclude": [
"**/_fresh/*"
],
"imports": { "imports": {
"$fresh/": "https://deno.land/x/fresh@1.6.1/", "$fresh/": "https://deno.land/x/fresh@1.6.3/",
"preact": "https://esm.sh/preact@10.19.2", "preact": "https://esm.sh/preact@10.19.2",
"preact/": "https://esm.sh/preact@10.19.2/", "preact/": "https://esm.sh/preact@10.19.2/",
"@preact/signals": "https://esm.sh/*@preact/signals@1.2.1", "@preact/signals": "https://esm.sh/*@preact/signals@1.2.1",
@ -33,15 +24,8 @@
"$std/": "https://deno.land/std@0.208.0/", "$std/": "https://deno.land/std@0.208.0/",
"@homeman/": "./" "@homeman/": "./"
}, },
"compilerOptions": { "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "preact" },
"jsx": "react-jsx", "fmt": { "useTabs": true, "semiColons": false, "singleQuote": true },
"jsxImportSource": "preact"
},
"fmt": {
"useTabs": true,
"semiColons": false,
"singleQuote": true
},
"unstable": ["kv"], "unstable": ["kv"],
"nodeModulesDir": true "nodeModulesDir": true
} }

View file

@ -2,17 +2,17 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1701718080, "lastModified": 1705316053,
"narHash": "sha256-6ovz0pG76dE0P170pmmZex1wWcQoeiomUZGggfH9XPs=", "narHash": "sha256-J2Ey5mPFT8gdfL2XC0JTZvKaBw/b2pnyudEXFvl+dQM=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "2c7f3c0fb7c08a0814627611d9d7d45ab6d75335", "rev": "c3e128f3c0ecc1fb04aef9f72b3dcc2f6cecf370",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "2c7f3c0fb7c08a0814627611d9d7d45ab6d75335",
"type": "github" "type": "github"
} }
}, },

View file

@ -1,5 +1,5 @@
{ {
inputs.nixpkgs.url = "github:NixOS/nixpkgs?rev=2c7f3c0fb7c08a0814627611d9d7d45ab6d75335"; inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs = { outputs = {
self, self,
nixpkgs, nixpkgs,

View file

@ -15,6 +15,7 @@ import * as $Admin from './islands/Admin.tsx'
import * as $Clock from './islands/Clock.tsx' import * as $Clock from './islands/Clock.tsx'
import * as $Counter from './islands/Counter.tsx' import * as $Counter from './islands/Counter.tsx'
import * as $Dashboard from './islands/Dashboard.tsx' import * as $Dashboard from './islands/Dashboard.tsx'
import * as $Nav from './islands/Nav.tsx'
import * as $TodoList from './islands/TodoList.tsx' import * as $TodoList from './islands/TodoList.tsx'
import { type Manifest } from '$fresh/server.ts' import { type Manifest } from '$fresh/server.ts'
@ -35,6 +36,7 @@ const manifest = {
'./islands/Clock.tsx': $Clock, './islands/Clock.tsx': $Clock,
'./islands/Counter.tsx': $Counter, './islands/Counter.tsx': $Counter,
'./islands/Dashboard.tsx': $Dashboard, './islands/Dashboard.tsx': $Dashboard,
'./islands/Nav.tsx': $Nav,
'./islands/TodoList.tsx': $TodoList, './islands/TodoList.tsx': $TodoList,
}, },
baseUrl: import.meta.url, baseUrl: import.meta.url,

View file

@ -21,12 +21,23 @@ export interface Props {
} }
async function promptDeleteUser(id: string, name: string) { async function promptDeleteUser(id: string, name: string) {
if (confirm(`Are you sure you want to delete ${name} (${id})?`)) { if (confirm(`Are you sure you want to delete '${name}' (${id})?`)) {
await fetch(`/api/user?id=${id}`, { method: 'DELETE' }) await fetch(`/api/user?id=${id}`, { method: 'DELETE' })
location.reload() 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
@ -128,7 +139,7 @@ export function Admin({ users, todos }: Props) {
<td class=''> <td class=''>
<Button <Button
title='Delete' title='Delete'
className='py-2 mr-2' className='py-2 mr-2 hover:bg-rose-500'
onClick={() => promptDeleteUser(id, name)} onClick={() => promptDeleteUser(id, name)}
> >
<TrashOutline class='w-6 h-6' /> <TrashOutline class='w-6 h-6' />
@ -152,19 +163,35 @@ export function Admin({ users, todos }: Props) {
<tr> <tr>
<th>Description</th> <th>Description</th>
<th>Assignee</th> <th>Assignee</th>
<th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{Object.entries(todos).map(( {Object.entries(todos).map((
[_id, { description, assigneeUserId }], [_index, { id, description, assigneeUserId }],
) => ( ) => (
<tr> <tr>
<td>{description}</td> <td>{description}</td>
<td> <td
style={`color: #${
assigneeUserId == null
? 'ffffff'
: users[assigneeUserId]?.color
}`}
>
{assigneeUserId == null {assigneeUserId == null
? 'Unassigned' ? 'Unassigned'
: users[assigneeUserId]?.name} : users[assigneeUserId]?.name}
</td> </td>
<td>
<Button
title='Delete'
className='py-2 mr-2 hover:bg-rose-500'
onClick={() => promptDeleteTodo(id, description)}
>
<TrashOutline class='w-6 h-6' />
</Button>
</td>
</tr> </tr>
))} ))}
</tbody> </tbody>

37
islands/Nav.tsx Normal file
View file

@ -0,0 +1,37 @@
// import { JSX } from 'preact'
// import { IS_BROWSER } from '$fresh/runtime.ts'
import { Bars3Outline } from 'preact-heroicons'
import { Clock } from '@homeman/islands/Clock.tsx'
import { Dialog } from '@homeman/components/Dialog.tsx'
import { useSignal } from '@preact/signals'
export function Nav(/* props: {} */) {
const showMenu = useSignal(false)
return (
<>
<Dialog
headerTitle='Main Menu'
show={showMenu.value}
onClose={() => {
showMenu.value = false
}}
>
</Dialog>
<nav class='bg-stone-200 dark:bg-stone-800 flex justify-items-start items-center'>
<button
class='p-4 hover:bg-stone-500/20'
onClick={() => {
showMenu.value = true
}}
>
<Bars3Outline class='h-6 w-6' />
</button>
<a class='p-4 hover:bg-stone-500/20' href='/'>
Flanagan Family
</a>
<Clock class='ml-auto pr-4 text-2xl' />
</nav>
</>
)
}

View file

@ -2,46 +2,84 @@ import { Todo, UserWithTodos } from '@homeman/models.ts'
import { JSX } from 'preact' import { JSX } from 'preact'
import { Button } from '@homeman/components/Button.tsx' import { Button } from '@homeman/components/Button.tsx'
import { Avatar } from '@homeman/components/Avatar.tsx' import { Avatar } from '@homeman/components/Avatar.tsx'
import { TrashSolid } from 'preact-heroicons'
export interface Props { export interface Props {
user: UserWithTodos user: UserWithTodos
onNewButtonClicked: JSX.MouseEventHandler<HTMLButtonElement> onNewButtonClicked: JSX.MouseEventHandler<HTMLButtonElement>
onTodoDone: (id: string) => Promise<void>
} }
export function TodoList( export function TodoList(
{ onNewButtonClicked, user: { avatarUrl, assignedTodos, name, color } }: { onTodoDone, onNewButtonClicked, user: { avatarUrl, name, color, ...user } }:
Props, Props,
) { ) {
const doneTodos = user.assignedTodos.filter((t) => t.doneAt != null)
const inProgressTodos = user.assignedTodos.filter((t) => t.doneAt == null)
const todoItem = ( const todoItem = (
{ id, doneAt, className, description, hideDone }: { id, doneAt, className, description, virtual }:
& Pick<Todo, 'description' | 'id' | 'doneAt'> & Pick<Todo, 'description' | 'id' | 'doneAt'>
& { & {
className?: string className?: string
hideDone?: boolean virtual?: boolean
}, },
) => ( ) => (
<li <li
style={`border-color: #${color}`} style={`border-color: #${color}`}
title={doneAt != null ? `Completed at ${doneAt}` : ''}
class={`${className || ''} ${ class={`${className || ''} ${
hideDone ? '' : 'border-l-4' virtual ? '' : 'border-l-4'
} p-4 rounded drop-shadow-lg bg-white dark:bg-stone-900 flex flex-col gap-2`} } p-4 text-black cursor-auto dark:text-white rounded drop-shadow-lg bg-white dark:bg-stone-900 flex flex-col gap-2`}
> >
{JSON.stringify(doneAt)}
<span class='text-xl'>{description}</span> <span class='text-xl'>{description}</span>
{(hideDone || doneAt != null) ? '' : ( <footer class='flex gap-2'>
<Button {(virtual || doneAt != null) ? '' : (
onClick={async () => { <Button
await fetch('/api/todo/done', { className='grow'
method: 'POST', onClick={async () => {
body: JSON.stringify({ id }), await fetch('/api/todo/done', {
}) method: 'PUT',
// TODO: remove the todoitem? body: JSON.stringify({ id }),
// TODO: confetti })
}} await onTodoDone(id)
> }}
Done >
</Button> Done
)} </Button>
)}
{(!virtual && doneAt != null)
? (
<Button
className='grow'
onClick={async () => {
await fetch('/api/todo/done', {
method: 'DELETE',
body: JSON.stringify({ id }),
})
await onTodoDone(id)
}}
>
Not Done
</Button>
)
: ''}
{virtual ? '' : (
<Button
className='hover:bg-rose-500'
onClick={async () => {
if (confirm(`Are you sure you want to delete '${description}'`)) {
await fetch('/api/todo', {
method: 'DELETE',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ id }),
})
}
}}
>
<TrashSolid className='w-6 h-6' />
</Button>
)}
</footer>
</li> </li>
) )
return ( return (
@ -61,14 +99,28 @@ export function TodoList(
+ New + New
</button> </button>
<ul class='flex flex-col gap-y-4'> <ul class='flex flex-col gap-y-4'>
{assignedTodos.length < 1 {inProgressTodos.length < 1
? todoItem({ ? todoItem({
id: '',
doneAt: null,
description: 'All clear! 🎉', description: 'All clear! 🎉',
className: 'text-center', className: 'text-center',
hideDone: true, virtual: true,
}) })
: assignedTodos.map(todoItem)} : inProgressTodos.map(todoItem)}
</ul> </ul>
{doneTodos.length > 0
? (
<summary class='text-gray-500 mt-4'>
+{doneTodos.length} completed todos
<details class='cursor-pointer'>
<ul class='flex flex-col gap-y-4 mt-4'>
{doneTodos.map(todoItem)}
</ul>
</details>
</summary>
)
: ''}
</div> </div>
) )
} }

View file

@ -1,8 +1,11 @@
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'
import { createPentagon } from 'https://deno.land/x/pentagon@v0.1.5/mod.ts' import {
createPentagon,
TableDefinition,
} from 'https://deno.land/x/pentagon@v0.1.5/mod.ts'
// import { ulid } from 'https://deno.land/x/ulid@v0.3.0/mod.ts' // import { ulid } from 'https://deno.land/x/ulid@v0.3.0/mod.ts'
const kv = await Deno.openKv('homeman.db') export const kv = await Deno.openKv('homeman.db')
// const todos = kv.list({ prefix: ['todos'] }) // const todos = kv.list({ prefix: ['todos'] })
// const deleteme = [] // const deleteme = []
@ -41,7 +44,7 @@ const Todo = z.object({
export const TodoModel = Todo export const TodoModel = Todo
export type Todo = z.infer<typeof Todo> export type Todo = z.infer<typeof Todo>
export const db = createPentagon(kv, { export const schema: Record<string, TableDefinition> = {
users: { users: {
schema: User, schema: User,
relations: { relations: {
@ -54,7 +57,9 @@ export const db = createPentagon(kv, {
assignee: ['users', User, 'assigneeUserId', 'id'], assignee: ['users', User, 'assigneeUserId', 'id'],
}, },
}, },
}) }
export const db = createPentagon(kv, schema)
// const daddy: User = { // const daddy: User = {
// id: ulid(), // id: ulid(),

View file

@ -1,5 +1,5 @@
import { type PageProps } from '$fresh/server.ts' import { type PageProps } from '$fresh/server.ts'
import { Nav } from '@homeman/components/Nav.tsx' import { Nav } from '@homeman/islands/Nav.tsx'
export default function App({ Component }: PageProps) { export default function App({ Component }: PageProps) {
return ( return (

View file

@ -4,7 +4,9 @@ import { Admin, Props } from '@homeman/islands/Admin.tsx'
export const handler: Handlers = { export const handler: Handlers = {
async GET(_req, ctx) { async GET(_req, ctx) {
const users = await db.users.findMany({}) const users = Object.fromEntries((await db.users.findMany({})).map(
(u) => [u.id, u],
))
const todos = await db.todos.findMany({}) const todos = await db.todos.findMany({})
return ctx.render({ users, todos }) return ctx.render({ users, todos })
}, },

View file

@ -60,6 +60,7 @@ export const handler: Handlers<Todo | null> = {
} else { } else {
data = { id: new URL(req.url).searchParams.get('id') } data = { id: new URL(req.url).searchParams.get('id') }
} }
console.log('delete todo data:', data)
const todoData = TodoModel.pick({ id: true }).parse(data) const todoData = TodoModel.pick({ id: true }).parse(data)
const result = await db.todos.delete({ where: todoData }) const result = await db.todos.delete({ where: todoData })
return new Response(JSON.stringify(result)) return new Response(JSON.stringify(result))

View file

@ -3,12 +3,23 @@ import { db, TodoModel } from '@homeman/models.ts'
const Model = TodoModel.pick({ id: true }) const Model = TodoModel.pick({ id: true })
async function markDone(id: string) {
const todo = await db.todos.findFirst({ where: { id } })
todo.doneAt = new Date()
return await db.todos.update({ where: { id }, data: todo })
}
async function markNotDone(id: string) {
return await db.todos.update({ where: { id }, data: { doneAt: null } })
}
export const handler: Handlers = { export const handler: Handlers = {
async POST(req, _ctx) { async PUT(req, _ctx) {
const { id } = Model.parse(await req.json()) const { id } = Model.parse(await req.json())
const todo = await db.todos.findFirst({ where: { id } }) return new Response(JSON.stringify(await markDone(id)))
todo.doneAt = new Date() },
const newTodo = await db.todos.update({ where: { id }, data: todo }) async DELETE(req, _ctx) {
return new Response(JSON.stringify(newTodo)) const { id } = Model.parse(await req.json())
return new Response(JSON.stringify(await markNotDone(id)))
}, },
} }

View file

@ -1,12 +1,23 @@
import { Handlers, PageProps } from '$fresh/server.ts' import { Handlers, PageProps } from '$fresh/server.ts'
import { db, Todo, UserWithTodos } from '@homeman/models.ts' import { db, kv, schema, Todo, UserWithTodos } from '@homeman/models.ts'
import Dashboard from '@homeman/islands/Dashboard.tsx' import Dashboard from '@homeman/islands/Dashboard.tsx'
import { useSignal } from '@preact/signals'
import { useEffect } from 'preact/hooks'
interface Data { interface Data {
users: Record<string, UserWithTodos> users: Record<string, UserWithTodos>
unassignedTodos: Todo[] unassignedTodos: Todo[]
} }
const allTableNames = Object.keys(schema).map((s) => [s])
async function watcher() {
console.log('watching:', allTableNames)
for await (const entry of kv.watch(allTableNames)) {
console.log('entry:', entry)
}
}
watcher()
export const handler: Handlers = { export const handler: Handlers = {
async GET(_req, ctx) { async GET(_req, ctx) {
const users = Object.fromEntries( const users = Object.fromEntries(
@ -22,5 +33,16 @@ export const handler: Handlers = {
} }
export default function Home({ data }: PageProps<Data>) { export default function Home({ data }: PageProps<Data>) {
return <Dashboard {...data} /> const rdata = useSignal(data)
useEffect(() => {
async function watcher() {
console.log('watcher watching...')
for await (const entry of kv.watch(allTableNames)) {
console.log('entry:', entry)
}
}
watcher().catch(console.error)
}, [rdata])
console.log('Home rendered')
return <Dashboard {...rdata.value} />
} }