homeman-deno/islands/Admin.tsx

170 lines
4.2 KiB
TypeScript

import { JSX } from 'preact'
import { type Signal, useSignal } from '@preact/signals'
import { Todo, User } from '@homeman/models.ts'
import { Button } from '@homeman/components/Button.tsx'
import { Input } from '@homeman/components/Input.tsx'
import { Label } from '@homeman/components/Label.tsx'
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'
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()
}
}
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>
)
}
export function Admin({ users, todos }: Props) {
const showAddUserDialog = useSignal(false)
const editUser: Signal<User | null> = useSignal(null)
return (
<main class='flex flex-col'>
<Dialog
headerTitle='Add user'
show={showAddUserDialog.value}
onClose={() => showAddUserDialog.value = false}
>
<UserForm
onCancelButtonClicked={() => showAddUserDialog.value = false}
userData={null}
/>
</Dialog>
<Dialog
headerTitle={`Edit '${editUser.value?.name}'`}
show={!!editUser.value}
onClose={() => editUser.value = null}
>
<UserForm
onCancelButtonClicked={() => editUser.value = null}
userData={editUser.value}
/>
</Dialog>
<SectionHead text={`Users (${users.length})`}>
<Button onClick={() => showAddUserDialog.value = true}>
<PlusOutline class='w-6 h-6' />
</Button>
</SectionHead>
<Table>
<thead>
<tr>
<th>Name</th>
<th>Avatar</th>
<th>Color</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{Object.entries(users).map(([id, user]) => {
const { name, avatarUrl, color } = user
return (
<tr>
<td>{name}</td>
<td>
{avatarUrl == null
? 'None'
: <Avatar className='h-16 w-16' src={avatarUrl} />}
</td>
<td style={`color: #${color}`}>
#{color}
</td>
<td class=''>
<Button
title='Delete'
className='py-2 mr-2'
onClick={() => promptDeleteUser(id, name)}
>
<TrashOutline class='w-6 h-6' />
</Button>
<Button
title='Edit'
className='py-2'
onClick={() => editUser.value = user}
>
<PencilSquareOutline class='w-6 h-6' />
</Button>
</td>
</tr>
)
})}
</tbody>
</Table>
<SectionHead text={`Todos (${todos.length})`} />
<Table>
<thead>
<tr>
<th>Description</th>
<th>Assignee</th>
</tr>
</thead>
<tbody>
{Object.entries(todos).map((
[_id, { description, assigneeUserId }],
) => (
<tr>
<td>{description}</td>
<td>
{assigneeUserId == null
? 'Unassigned'
: users[assigneeUserId]?.name}
</td>
</tr>
))}
</tbody>
</Table>
</main>
)
}