2024-01-11 16:23:09 -06:00
|
|
|
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'
|
2024-01-17 20:57:11 -06:00
|
|
|
import { confetti } from 'https://esm.sh/@tsparticles/confetti@3.0.3'
|
2024-01-17 15:10:28 -06:00
|
|
|
import { useEffect } from 'preact/hooks'
|
2024-01-11 16:23:09 -06:00
|
|
|
|
|
|
|
interface Props {
|
|
|
|
users: Record<string, UserWithTodos>
|
2024-01-17 15:10:28 -06:00
|
|
|
todos: Record<string, Todo>
|
2024-01-11 16:23:09 -06:00
|
|
|
unassignedTodos: Todo[]
|
2024-01-17 15:10:28 -06:00
|
|
|
lastUserIdUpdated: { value: string; versionstamp: string }
|
|
|
|
lastTodoIdUpdated: { value: string; versionstamp: string }
|
2024-01-11 16:23:09 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
const unassignedUserPlaceholder: User = {
|
|
|
|
id: '',
|
|
|
|
createdAt: new Date(),
|
|
|
|
name: 'Shared',
|
|
|
|
avatarUrl: 'http://placekitten.com/512/512',
|
|
|
|
color: '888888',
|
|
|
|
}
|
|
|
|
|
2024-01-11 18:57:29 -06:00
|
|
|
interface UserSelectButtonProps extends JSX.HTMLAttributes<HTMLInputElement> {
|
2024-01-11 16:23:09 -06:00
|
|
|
user: User
|
|
|
|
}
|
|
|
|
|
|
|
|
function UserSelectButton(
|
2024-01-11 18:57:29 -06:00
|
|
|
{ user: { id, name, avatarUrl, color }, tabindex, ...props }:
|
|
|
|
UserSelectButtonProps,
|
2024-01-11 16:23:09 -06:00
|
|
|
) {
|
|
|
|
const eid = `assigneeUserId_${id}`
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<input
|
|
|
|
aria-hidden='true'
|
|
|
|
type='radio'
|
2024-01-11 18:57:29 -06:00
|
|
|
class='peer sr-only'
|
2024-01-11 16:23:09 -06:00
|
|
|
id={eid}
|
|
|
|
name='assigneeUserId'
|
|
|
|
value={id}
|
2024-01-11 18:57:29 -06:00
|
|
|
{...props}
|
2024-01-11 16:23:09 -06:00
|
|
|
/>
|
|
|
|
<Label
|
|
|
|
for={eid}
|
2024-01-11 18:57:29 -06:00
|
|
|
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'
|
2024-01-11 16:23:09 -06:00
|
|
|
role='button'
|
|
|
|
>
|
|
|
|
<Avatar
|
|
|
|
className='mb-2'
|
|
|
|
src={avatarUrl}
|
|
|
|
/>
|
|
|
|
<span
|
|
|
|
style={`color: #${color};`}
|
|
|
|
class='font-semibold text-center'
|
|
|
|
>
|
2024-01-11 18:57:29 -06:00
|
|
|
{name}
|
2024-01-11 16:23:09 -06:00
|
|
|
</span>
|
|
|
|
</Label>
|
|
|
|
</>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2024-01-17 15:10:28 -06:00
|
|
|
export default function Dashboard(
|
|
|
|
{ todos, users, unassignedTodos, lastTodoIdUpdated, lastUserIdUpdated }:
|
|
|
|
Props,
|
|
|
|
) {
|
|
|
|
console.log('lasttodo:', lastTodoIdUpdated)
|
|
|
|
console.log('lastuser:', lastUserIdUpdated)
|
2024-01-11 16:23:09 -06:00
|
|
|
const todoAssignUserId: Signal<string | null> = useSignal(null)
|
|
|
|
const showAddTodoDialog = useSignal(false)
|
|
|
|
|
2024-01-17 15:10:28 -06:00
|
|
|
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 count = 200,
|
|
|
|
defaults = {
|
|
|
|
origin: { y: 0.7 },
|
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
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')
|
|
|
|
})
|
|
|
|
}, [])
|
|
|
|
|
2024-01-11 16:23:09 -06:00
|
|
|
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>
|
2024-01-11 18:57:29 -06:00
|
|
|
<UserSelectButton
|
|
|
|
checked={todoAssignUserId.value == null}
|
|
|
|
tabindex={0}
|
|
|
|
user={unassignedUser}
|
|
|
|
/>
|
2024-01-11 16:23:09 -06:00
|
|
|
</li>
|
|
|
|
{Object.values(users).map(
|
2024-01-11 18:57:29 -06:00
|
|
|
(user) => {
|
2024-01-11 16:23:09 -06:00
|
|
|
return (
|
|
|
|
<li>
|
2024-01-11 18:57:29 -06:00
|
|
|
<UserSelectButton
|
|
|
|
checked={todoAssignUserId.value == user.id}
|
|
|
|
user={user}
|
|
|
|
/>
|
2024-01-11 16:23:09 -06:00
|
|
|
</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>
|
|
|
|
)
|
|
|
|
}
|