2024-01-09 21:52:47 -06:00
|
|
|
import { JSX } from 'preact'
|
2024-01-07 16:21:14 -06:00
|
|
|
import { type Signal, useSignal } from '@preact/signals'
|
2024-01-07 02:24:36 -06:00
|
|
|
import { Todo, User } from '@homeman/models.ts'
|
|
|
|
import { Button } from '@homeman/components/Button.tsx'
|
2024-01-07 10:55:18 -06:00
|
|
|
import { Input } from '@homeman/components/Input.tsx'
|
|
|
|
import { Label } from '@homeman/components/Label.tsx'
|
2024-01-09 21:52:47 -06:00
|
|
|
import { Dialog } from '@homeman/components/Dialog.tsx'
|
2024-01-09 23:06:44 -06:00
|
|
|
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'
|
2024-01-07 02:24:36 -06:00
|
|
|
|
|
|
|
export interface Props {
|
2024-01-09 21:52:47 -06:00
|
|
|
users: Record<string, User>
|
|
|
|
todos: Record<string, Todo>
|
2024-01-07 02:24:36 -06:00
|
|
|
}
|
|
|
|
|
2024-01-07 11:38:25 -06:00
|
|
|
async function promptDeleteUser(id: string, name: string) {
|
2024-01-16 16:45:51 -06:00
|
|
|
if (confirm(`Are you sure you want to delete '${name}' (${id})?`)) {
|
2024-01-07 11:38:25 -06:00
|
|
|
await fetch(`/api/user?id=${id}`, { method: 'DELETE' })
|
|
|
|
location.reload()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-16 16:45:51 -06:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-09 21:52:47 -06:00
|
|
|
interface UserFormProps extends JSX.HTMLAttributes<HTMLFormElement> {
|
|
|
|
onCancelButtonClicked: JSX.MouseEventHandler<HTMLButtonElement>
|
|
|
|
userData: User | null
|
|
|
|
}
|
|
|
|
|
|
|
|
function UserForm(
|
|
|
|
{ onCancelButtonClicked, userData, ...props }: UserFormProps,
|
|
|
|
) {
|
|
|
|
return (
|
|
|
|
<form
|
|
|
|
{...props}
|
|
|
|
class='p-4 gap-4 flex flex-col'
|
|
|
|
action='/api/user'
|
|
|
|
method='post'
|
|
|
|
encType='multipart/form-data'
|
|
|
|
>
|
|
|
|
{userData ? <Input type='hidden' name='id' value={userData.id} /> : <></>}
|
|
|
|
<Label for='name'>
|
|
|
|
Name
|
|
|
|
<Input autofocus name='name' value={userData?.name} />
|
|
|
|
</Label>
|
|
|
|
<Label for='avatar'>
|
|
|
|
Avatar
|
|
|
|
<Input type='file' name='avatar' />
|
|
|
|
</Label>
|
|
|
|
<Label for='color'>
|
|
|
|
Color
|
|
|
|
<Input type='color' name='color' value={`#${userData?.color}`} />
|
|
|
|
</Label>
|
|
|
|
<footer class='flex justify-end gap-2'>
|
|
|
|
<Button type='button' onClick={onCancelButtonClicked}>
|
|
|
|
Cancel
|
|
|
|
</Button>
|
|
|
|
<Input type='submit' value='Save' />
|
|
|
|
</footer>
|
|
|
|
</form>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2024-01-07 02:24:36 -06:00
|
|
|
export function Admin({ users, todos }: Props) {
|
2024-01-09 21:52:47 -06:00
|
|
|
const showAddUserDialog = useSignal(false)
|
2024-01-07 16:21:14 -06:00
|
|
|
const editUser: Signal<User | null> = useSignal(null)
|
2024-01-09 21:52:47 -06:00
|
|
|
|
2024-01-07 02:24:36 -06:00
|
|
|
return (
|
|
|
|
<main class='flex flex-col'>
|
2024-01-12 10:04:50 -06:00
|
|
|
<header class='flex items-center border-b-2 border-stone-500/20 '>
|
|
|
|
<h1 class='p-5 text-2xl'>
|
|
|
|
Administration Page
|
|
|
|
</h1>
|
|
|
|
</header>
|
2024-01-09 21:52:47 -06:00
|
|
|
<Dialog
|
|
|
|
headerTitle='Add user'
|
|
|
|
show={showAddUserDialog.value}
|
|
|
|
onClose={() => showAddUserDialog.value = false}
|
2024-01-07 10:55:18 -06:00
|
|
|
>
|
2024-01-09 21:52:47 -06:00
|
|
|
<UserForm
|
|
|
|
onCancelButtonClicked={() => showAddUserDialog.value = false}
|
|
|
|
userData={null}
|
|
|
|
/>
|
|
|
|
</Dialog>
|
|
|
|
<Dialog
|
|
|
|
headerTitle={`Edit '${editUser.value?.name}'`}
|
|
|
|
show={!!editUser.value}
|
|
|
|
onClose={() => editUser.value = null}
|
2024-01-07 16:21:14 -06:00
|
|
|
>
|
2024-01-09 21:52:47 -06:00
|
|
|
<UserForm
|
|
|
|
onCancelButtonClicked={() => editUser.value = null}
|
2024-01-09 23:06:44 -06:00
|
|
|
userData={editUser.value}
|
2024-01-09 21:52:47 -06:00
|
|
|
/>
|
|
|
|
</Dialog>
|
2024-01-17 21:14:33 -06:00
|
|
|
<SectionHead text={`Users (${Object.keys(users).length})`}>
|
2024-01-09 21:52:47 -06:00
|
|
|
<Button onClick={() => showAddUserDialog.value = true}>
|
2024-01-09 23:06:44 -06:00
|
|
|
<PlusOutline class='w-6 h-6' />
|
2024-01-07 02:24:36 -06:00
|
|
|
</Button>
|
2024-01-09 23:06:44 -06:00
|
|
|
</SectionHead>
|
|
|
|
<Table>
|
2024-01-07 02:24:36 -06:00
|
|
|
<thead>
|
|
|
|
<tr>
|
|
|
|
<th>Name</th>
|
|
|
|
<th>Avatar</th>
|
|
|
|
<th>Color</th>
|
2024-01-07 11:38:25 -06:00
|
|
|
<th>Actions</th>
|
2024-01-07 02:24:36 -06:00
|
|
|
</tr>
|
|
|
|
</thead>
|
|
|
|
<tbody>
|
2024-01-09 21:52:47 -06:00
|
|
|
{Object.entries(users).map(([id, user]) => {
|
|
|
|
const { name, avatarUrl, color } = user
|
2024-01-07 16:21:14 -06:00
|
|
|
return (
|
|
|
|
<tr>
|
|
|
|
<td>{name}</td>
|
|
|
|
<td>
|
|
|
|
{avatarUrl == null
|
|
|
|
? 'None'
|
2024-01-17 21:14:33 -06:00
|
|
|
: <Avatar className='h-[64px] w-[64px]' src={avatarUrl} />}
|
2024-01-07 16:21:14 -06:00
|
|
|
</td>
|
|
|
|
<td style={`color: #${color}`}>
|
|
|
|
#{color}
|
|
|
|
</td>
|
|
|
|
<td class=''>
|
|
|
|
<Button
|
|
|
|
title='Delete'
|
2024-01-16 16:45:51 -06:00
|
|
|
className='py-2 mr-2 hover:bg-rose-500'
|
2024-01-07 16:21:14 -06:00
|
|
|
onClick={() => promptDeleteUser(id, name)}
|
2024-01-07 11:38:25 -06:00
|
|
|
>
|
2024-01-09 21:52:47 -06:00
|
|
|
<TrashOutline class='w-6 h-6' />
|
2024-01-07 16:21:14 -06:00
|
|
|
</Button>
|
|
|
|
<Button
|
|
|
|
title='Edit'
|
|
|
|
className='py-2'
|
2024-01-09 21:52:47 -06:00
|
|
|
onClick={() => editUser.value = user}
|
2024-01-07 16:21:14 -06:00
|
|
|
>
|
2024-01-09 21:52:47 -06:00
|
|
|
<PencilSquareOutline class='w-6 h-6' />
|
2024-01-07 16:21:14 -06:00
|
|
|
</Button>
|
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
)
|
|
|
|
})}
|
2024-01-07 02:24:36 -06:00
|
|
|
</tbody>
|
2024-01-09 23:06:44 -06:00
|
|
|
</Table>
|
|
|
|
<SectionHead text={`Todos (${todos.length})`} />
|
|
|
|
<Table>
|
2024-01-07 02:24:36 -06:00
|
|
|
<thead>
|
|
|
|
<tr>
|
|
|
|
<th>Description</th>
|
|
|
|
<th>Assignee</th>
|
2024-01-17 21:14:33 -06:00
|
|
|
<th>Done At</th>
|
|
|
|
<th>Emoji</th>
|
2024-01-16 16:45:51 -06:00
|
|
|
<th>Actions</th>
|
2024-01-07 02:24:36 -06:00
|
|
|
</tr>
|
|
|
|
</thead>
|
|
|
|
<tbody>
|
2024-01-09 21:52:47 -06:00
|
|
|
{Object.entries(todos).map((
|
2024-01-17 21:14:33 -06:00
|
|
|
[_index, { id, emoji, doneAt, description, assigneeUserId }],
|
2024-01-09 21:52:47 -06:00
|
|
|
) => (
|
2024-01-07 02:24:36 -06:00
|
|
|
<tr>
|
|
|
|
<td>{description}</td>
|
2024-01-16 16:45:51 -06:00
|
|
|
<td
|
|
|
|
style={`color: #${
|
|
|
|
assigneeUserId == null
|
|
|
|
? 'ffffff'
|
|
|
|
: users[assigneeUserId]?.color
|
|
|
|
}`}
|
|
|
|
>
|
2024-01-07 02:24:36 -06:00
|
|
|
{assigneeUserId == null
|
|
|
|
? 'Unassigned'
|
2024-01-09 21:52:47 -06:00
|
|
|
: users[assigneeUserId]?.name}
|
2024-01-07 02:24:36 -06:00
|
|
|
</td>
|
2024-01-17 21:14:33 -06:00
|
|
|
<td>{doneAt == null ? 'Not Done' : doneAt.toLocaleString()}</td>
|
|
|
|
<td>{emoji || 'No Emoji'}</td>
|
2024-01-16 16:45:51 -06:00
|
|
|
<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>
|
2024-01-07 02:24:36 -06:00
|
|
|
</tr>
|
|
|
|
))}
|
|
|
|
</tbody>
|
2024-01-09 23:06:44 -06:00
|
|
|
</Table>
|
2024-01-07 02:24:36 -06:00
|
|
|
</main>
|
|
|
|
)
|
|
|
|
}
|