homeman-deno/islands/Dashboard.tsx
2024-01-17 21:30:41 -06:00

277 lines
6.2 KiB
TypeScript

import { Todo, User, UserWithTodos } from '@homeman/models.ts'
import { Signal, useSignal } from '@preact/signals'
import { TodoList } from '@homeman/islands/TodoList.tsx'
import { Dialog } from '@homeman/components/Dialog.tsx'
import { Label } from '@homeman/components/Label.tsx'
import { Input } from '@homeman/components/Input.tsx'
import { Avatar } from '@homeman/components/Avatar.tsx'
import { Button } from '@homeman/components/Button.tsx'
import { JSX } from 'preact'
import { confetti } from 'https://esm.sh/@tsparticles/confetti@3.0.3'
import { useEffect } from 'preact/hooks'
interface Props {
users: Record<string, UserWithTodos>
todos: Record<string, Todo>
unassignedTodos: Todo[]
lastUserIdUpdated: { value: string; versionstamp: string }
lastTodoIdUpdated: { value: string; versionstamp: string }
}
const unassignedUserPlaceholder: User = {
id: '',
createdAt: new Date(),
name: 'Shared',
avatarUrl: 'http://placekitten.com/512/512',
color: '888888',
}
interface UserSelectButtonProps extends JSX.HTMLAttributes<HTMLInputElement> {
user: User
}
function UserSelectButton(
{ user: { id, name, avatarUrl, color }, tabindex, ...props }:
UserSelectButtonProps,
) {
const eid = `assigneeUserId_${id}`
return (
<>
<input
aria-hidden='true'
type='radio'
class='peer sr-only'
id={eid}
name='assigneeUserId'
value={id}
{...props}
/>
<Label
for={eid}
style={`border-color: #${color};`}
tabindex={tabindex}
className='cursor-pointer peer-checked:border-t-2 peer-checked:bg-gray-500/20 hover:bg-gray-500/25 rounded p-2'
role='button'
>
<Avatar
className='mb-2'
src={avatarUrl}
/>
<span
style={`color: #${color};`}
class='font-semibold text-center'
>
{name}
</span>
</Label>
</>
)
}
export default function Dashboard(
{ todos, users, unassignedTodos, lastTodoIdUpdated, lastUserIdUpdated }:
Props,
) {
console.log('lasttodo:', lastTodoIdUpdated)
console.log('lastuser:', lastUserIdUpdated)
const todoAssignUserId: Signal<string | null> = useSignal(null)
const showAddTodoDialog = useSignal(false)
useEffect(() => {
let es = new EventSource('/api/user')
es.addEventListener('message', (e) => {
console.log('user event from server:', e)
})
es.addEventListener('error', async () => {
es.close()
const backoff = 10000 + Math.random() * 5000
await new Promise((resolve) => setTimeout(resolve, backoff))
es = new EventSource('/api/user')
})
}, [])
useEffect(() => {
const fireworks = new Audio('/fireworks.mp3')
const count = 200,
defaults = {
origin: { y: 0.7 },
}
function fire(particleRatio: number, opts: {
spread?: number
startVelocity?: number
decay?: number
scalar?: number
}) {
fireworks.play()
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,
})
})
useEffect(() => {
let es = new EventSource('/api/todo')
console.log('Streaming todo events...')
es.addEventListener('message', (e) => {
console.log('todo event from server:', e)
const payload = JSON.parse(e.data)
const { id: id } = payload
if (
payload.versionstamp === lastTodoIdUpdated.versionstamp &&
payload.id === lastTodoIdUpdated.value
) {
console.log('skipping...')
// skip, we hydrated with it
return
}
if (!payload.value) {
// deleted, so reload
location.reload()
} else {
const { description, doneAt, assigneeUserId, emoji } = payload.value
const t = todos[id]
if (
!t ||
id != t.id ||
description != t.description ||
doneAt != t.doneAt ||
assigneeUserId != t.assigneeUserId ||
emoji != t.emoji
) {
console.log('Should reload!')
location.reload()
}
}
})
es.addEventListener('error', async (e) => {
console.log('Streaming todo events error:', e)
es.close()
const backoff = 10000 + Math.random() * 5000
await new Promise((resolve) => setTimeout(resolve, backoff))
es = new EventSource('/api/todo')
})
}, [])
const unassignedUser: UserWithTodos = {
...unassignedUserPlaceholder,
assignedTodos: unassignedTodos,
}
return (
<main class='flex flex-col'>
<Dialog
headerTitle='Add todo'
show={showAddTodoDialog.value}
onClose={() => showAddTodoDialog.value = false}
>
<form
class='p-4 gap-4 flex flex-col'
action='/api/todo'
method='post'
encType='multipart/form-data'
>
<Label for='description'>
Description
<Input autofocus name='description' />
</Label>
<span>
Assignee
<ul class='flex gap-2'>
<li>
<UserSelectButton
checked={todoAssignUserId.value == null}
tabindex={0}
user={unassignedUser}
/>
</li>
{Object.values(users).map(
(user) => {
return (
<li>
<UserSelectButton
checked={todoAssignUserId.value == user.id}
user={user}
/>
</li>
)
},
)}
</ul>
</span>
<footer class='flex justify-end gap-2'>
<Button
type='button'
onClick={() => showAddTodoDialog.value = false}
>
Cancel
</Button>
<Input type='submit' value='Save' />
</footer>
</form>
</Dialog>
<h1 class='p-5 border-b-2 border-stone-500/20 text-2xl'>
Todos
</h1>
<ul class='p-4 relative flex overflow-x-scroll max-w-screen'>
<li>
<TodoList
onNewButtonClicked={() => {
console.log('shared new')
showAddTodoDialog.value = true
todoAssignUserId.value = null
}}
user={unassignedUser}
/>
</li>
{Object.values(users).map((u) => (
<li>
<TodoList
onNewButtonClicked={() => {
showAddTodoDialog.value = true
todoAssignUserId.value = u.id
}}
user={u}
/>
</li>
))}
</ul>
</main>
)
}