123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399 |
- import './HerdView.scss'
- import BackButton from '@/components/button/BackButton'
- import Button from '@/components/button/Button'
- import ButtonSet from '@/components/ButtonSet'
- import Chip from '@/components/Chip'
- import CreateButton from '@/components/button/CreateButton'
- import DeleteButton from '@/components/button/DeleteButton'
- import { DndContext } from '@dnd-kit/core'
- import type { DragEndEvent } from '@dnd-kit/core'
- import FormGroup from '@/components/form/FormGroup'
- import FormInput from '@/components/form/FormInput'
- import LoadingIndicator from '@/components/LoadingIndicator'
- import Main from '@/components/Main'
- import Notice from '@/components/Notice'
- import Pagination from '@/components/Pagination'
- import Placeholder from '@/components/Placeholder'
- import ResetButton from '@/components/button/ResetButton'
- import Row from '@/components/Row'
- import SaveButton from '@/components/button/SaveButton'
- import SearchForm from '@/components/SearchForm'
- import SortableRow from '@/components/SortableRow'
- import type { SubmitHandler } from 'react-hook-form'
- import api from '@/api'
- import { useForm } from 'react-hook-form'
- import { CheckCircleIcon, CloudIcon, XCircleIcon, XMarkIcon } from '@heroicons/react/20/solid'
- import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'
- import { useCallback, useEffect, useState } from 'react'
- import { useConnection, useRouteSearch, useSession } from '@/hooks'
- import { useNavigate, useParams } from 'react-router-dom'
- interface HerdUpdateFormData extends Pick<api.Herd, 'name'> {}
- interface TaskCreateFormData extends Pick<api.Task, 'description'> {}
- interface TaskUpdateFormData extends Pick<api.Task, 'description'> {}
- function useHerdUpdateForm() {
- const form = useForm<HerdUpdateFormData>({ mode: 'onBlur' })
- const inputs = {
- name: form.register('name', { validate: value => {
- if (value.length < 1) return 'Required'
- }}),
- }
- return { ...form, inputs }
- }
- function useTaskCreateForm() {
- const form = useForm<TaskCreateFormData>({ mode: 'onBlur' })
- const inputs = {
- description: form.register('description', { validate: value => {
- if (value.length < 1) return 'Required'
- }}),
- }
- return { ...form, inputs }
- }
- function useTaskUpdateForm() {
- const form = useForm<TaskUpdateFormData>({ mode: 'onBlur' })
- const inputs = {
- description: form.register('description', { validate: value => {
- if (value.length < 1) return 'Required'
- }}),
- }
- return { ...form, inputs }
- }
- export default function HerdView() {
- const { account } = useSession()
- const createTaskForm = useTaskCreateForm()
- const { id } = useParams()
- const navigate = useNavigate()
- const { options } = useConnection()
- const updateHerdForm = useHerdUpdateForm()
- const updateTaskForm = useTaskUpdateForm()
- const { limit, page, searchParams, setPage } = useRouteSearch()
- const [data, setData] = useState<api.GetHerdResponse>()
- const [taskData, setTaskData] = useState<api.SearchResponse<api.GetTaskResponse>>()
- const [busy, setBusy] = useState(false)
- const [editing, setEditing] = useState<string>()
- const [error, setError] = useState<Error>()
- const [loading, setLoading] = useState(false)
- const disableSorting = Boolean(searchParams.search)
- async function createTask(data: TaskCreateFormData) {
- if (busy) return
- try {
- setBusy(true)
- setError(undefined)
- await api.createTask(options, {
- task: {
- _herd: id || '',
- _account: account?._id || '',
- description: data.description,
- },
- })
- createTaskForm.reset({ description: '' })
- if (taskData && taskData.results.length >= limit) {
- // New task will be on a new page; change page to display it
- setPage(page + 1)
- } else {
- // Reload current page
- const taskRes = await api.searchTasks(options, id, searchParams)
- setTaskData(taskRes)
- }
- } catch (err) {
- setError(err as Error)
- } finally {
- setBusy(false)
- }
- }
- async function deleteHerd() {
- if (busy || !data) return
- try {
- setBusy(true)
- setError(undefined)
- await api.deleteHerd(options, data.herd._id)
- navigate('/')
- } catch (err) {
- setError(err as Error)
- } finally {
- setBusy(false)
- }
- }
- async function deleteTask(task: api.WithId<api.Task>) {
- if (busy) return
- try {
- setBusy(true)
- setError(undefined)
- await api.deleteTask(options, task._id)
- // Reload current page
- const taskRes = await api.searchTasks(options, id, searchParams)
- setTaskData(taskRes)
- } catch (err) {
- setError(err as Error)
- } finally {
- setBusy(false)
- }
- }
- async function moveTask({ active, over }: DragEndEvent) {
- if (busy || !taskData || !over || active.id === over.id) return
- const activeIdx = taskData.results.findIndex(({ task }) => task._id === active.id)
- const overIdx = taskData.results.findIndex(({ task }) => task._id === over.id)
- if (activeIdx < 0 || overIdx < 0) return
- const target = taskData.results[overIdx]
- try {
- setBusy(true)
- setError(undefined)
- const update = await api.moveTask(options, active.id.toString(), target.task.position)
- // Hot reorder tasks
- const results = activeIdx < overIdx
- // Task moved down
- ? [
- ...taskData.results.slice(0, overIdx + 1).filter(({ task }) => task._id !== update.task._id),
- update,
- ...taskData.results.slice(overIdx +1),
- ]
- // Task moved up
- : [
- ...taskData.results.slice(0, overIdx),
- update,
- ...taskData.results.slice(overIdx).filter(({ task }) => task._id !== update.task._id),
- ]
- setTaskData({ ...taskData, results })
- } catch (err) {
- setError(err as Error)
- } finally {
- setBusy(false)
- }
- }
- async function toggleTaskDone(task: api.WithId<api.Task>) {
- try {
- setBusy(true)
- setError(undefined)
- const update = await api.toggleTaskDone(options, task._id)
- const inPageTask = taskData?.results.find(({ task }) => task._id === update.task._id)
- if (inPageTask) inPageTask.task.done = update.task.done
- } catch (err) {
- setError(err as Error)
- } finally {
- setBusy(false)
- }
- }
- const reload = useCallback(async () => {
- if (id) {
- setError(undefined)
- setLoading(true)
- try {
- const res = await api.getHerd(options, id)
- setData(res)
- const taskRes = await api.searchTasks(options, id, searchParams)
- setTaskData(taskRes)
- } catch (err) {
- setError(err as Error)
- } finally {
- setLoading(false)
- }
- }
- }, [id, options, searchParams])
- function setTaskToEdit(task?: api.WithId<api.Task>) {
- if (task) {
- setEditing(task._id)
- updateTaskForm.reset({ description: task.description })
- } else {
- setEditing(undefined)
- }
- }
- async function updateHerd(data: HerdUpdateFormData) {
- if (busy) return
- try {
- setBusy(true)
- setError(undefined)
- const res = await api.updateHerd(options, id as string, {
- herd: data,
- })
- setData(res)
- } catch (err) {
- setError(err as Error)
- } finally {
- setBusy(false)
- }
- }
- function updateTask(task: api.WithId<api.Task>): SubmitHandler<TaskUpdateFormData> {
- return async function(data) {
- if (busy) return
- try {
- setBusy(true)
- setError(undefined)
- const update = await api.updateTask(options, task._id, { task: data })
- const inPageTask = taskData?.results.find(({ task }) => task._id === update.task._id)
- if (inPageTask) {
- inPageTask.task.description = update.task.description
- }
- setEditing(undefined)
- } catch (err) {
- setError(err as Error)
- } finally {
- setBusy(false)
- }
- }
- }
- useEffect(() => {
- reload()
- }, [reload])
- if (loading) return (
- <Main>
- <header>
- {id ? <h1>Loading Herd...</h1> : <h1>Create Herd</h1>}
- </header>
- <LoadingIndicator />
- </Main>
- )
- if (error) return (
- <Main>
- <header>
- {id ? <h1>Loading Herd...</h1> : <h1>Create Herd</h1>}
- <ButtonSet>
- <BackButton />
- </ButtonSet>
- </header>
- <Notice error={error} />
- </Main>
- )
- return data && (
- <Main>
- <header>
- <h1>{data.herd.name}</h1>
- <ButtonSet>
- <BackButton onClick={() => navigate('/')} />
- <DeleteButton onClick={deleteHerd} />
- </ButtonSet>
- </header>
- <SearchForm />
- <Notice error={error} />
- {taskData && (
- <>
- {taskData.metadata.totalCount > 0 ? (
- <DndContext onDragEnd={moveTask}>
- <SortableContext
- items={taskData.results.map(({ task }) => task._id)}
- strategy={verticalListSortingStrategy}
- >
- {taskData.results.map(({ task }, i) => (
- <SortableRow key={task._id} id={task._id} className={`task ${task.done ? 'done' : 'not-done'}`} disabled={disableSorting}>
- <div className="position">{i + 1 + ((page - 1) * limit)}</div>
- <div className="description">
- {editing === task._id ? (
- <form onSubmit={updateTaskForm.handleSubmit(updateTask(task))}>
- <Row className="edit-task">
- <FormInput>
- <input type="text" autoFocus {...updateTaskForm.inputs.description} />
- </FormInput>
- <ButtonSet>
- <SaveButton type="submit" className="mini" />
- <Button onClick={() => setTaskToEdit(undefined)} className="mini">
- <XMarkIcon />
- <span>Cancel</span>
- </Button>
- </ButtonSet>
- </Row>
- </form>
- ) : (
- <span onClick={() => setTaskToEdit(task)}>{task.description}</span>
- )}
- </div>
- <ButtonSet>
- {task.done ? (
- <Button className="positive mini fill" onClick={() => toggleTaskDone(task)}>
- <CheckCircleIcon />
- <span>Done</span>
- </Button>
- ) : (
- <Button className="negative mini" onClick={() => toggleTaskDone(task)}>
- <XCircleIcon />
- <span>Not done</span>
- </Button>
- )}
- <DeleteButton className="mini" onClick={() => deleteTask(task)} />
- </ButtonSet>
- </SortableRow>
- ))}
- </SortableContext>
- </DndContext>
- ) : (
- <Placeholder>
- <CloudIcon />
- <span>No tasks!</span>
- </Placeholder>
- )}
- <form onSubmit={createTaskForm.handleSubmit(createTask)}>
- <FormGroup name="Add a task">
- <FormInput>
- <Row>
- <input id="description" type="text" {...createTaskForm.inputs.description} />
- <ButtonSet>
- <CreateButton type="submit" className="fill" />
- </ButtonSet>
- </Row>
- <Chip className="mini" error={createTaskForm.formState.errors.description} />
- </FormInput>
- </FormGroup>
- </form>
- <Pagination totalCount={taskData.metadata.totalCount} />
- <form onSubmit={updateHerdForm.handleSubmit(updateHerd)}>
- <FormGroup name="Edit herd">
- <FormInput id="herd:name" label="Name">
- <input id="herd:name" type="text" {...updateHerdForm.inputs.name} />
- <Chip className="mini" error={updateHerdForm.formState.errors.name} />
- </FormInput>
- <ButtonSet>
- <SaveButton type="submit" className="fill" />
- <ResetButton type="reset" />
- </ButtonSet>
- </FormGroup>
- </form>
- </>
- )}
- </Main>
- )
- }
|