midrashim/src/components/Manage/LoanedBooks.tsx

219 lines
7.9 KiB
TypeScript

'use client'
import { Author, Book, Checkout, Copy, Repository } from '@/payload-types'
import { useGlobal } from '@/providers/GlobalProvider'
import { getUserLoans } from '@/serverActions/GetUserLoans'
import { ownerReturnCheckout } from '@/serverActions/ReturnCheckout'
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
import { EllipsisVerticalIcon } from '@heroicons/react/20/solid'
import clsx from 'clsx'
import { Loader2Icon } from 'lucide-react'
import { PaginatedDocs, User } from 'payload'
import { useCallback, useMemo, useState } from 'react'
import { toast } from 'sonner'
const statuses = {
'Passed Due': 'text-gray-200 bg-red-500 ring-red-700/10',
'Due Soon': 'text-amber-800 bg-amber-50 ring-amber-600/20',
'': '',
}
type Row = {
id: number
title: string
authors: string[]
owners: string[]
repositoryAbbreviation: string
status: 'Passed Due' | 'Due Soon' | ''
dueDate: Date
href: string
}
type ListProps = {
rows: Row[]
onUpdate: () => Promise<void>
}
const LoanedBooksList = (props: ListProps) => {
const { rows, onUpdate } = props
const [returningBookId, setReturningBookId] = useState(0)
const isReturningBook = useCallback((id: number) => id === returningBookId, [returningBookId])
const handleReturnClick = useCallback(
async (id: number) => {
if (returningBookId) return
setReturningBookId(id)
const updatedCheckout = await ownerReturnCheckout({ checkoutId: id })
if (!updatedCheckout) {
setReturningBookId(0)
toast('There was an issue returning your book')
return
}
await onUpdate()
setReturningBookId(0)
toast('Book was returned into your collection')
},
[returningBookId, setReturningBookId, onUpdate],
)
return (
<ul role="list" className="divide-y divide-gray-100">
{rows.map((r) => (
<li
key={r.id}
className={clsx(
'flex relative items-center justify-between gap-x-6 py-5',
isReturningBook(r.id) ? 'pointer-events-none opacity-30' : '',
)}
>
<div className="min-w-0">
<div className="flex items-start gap-x-3">
<p className="text-sm/6 text-foreground">
<span className="font-semibold">{`[${r.repositoryAbbreviation}] `}</span>
<span>{r.title}</span>
</p>
{!!r.status && (
<p
className={clsx(
statuses[r.status as keyof typeof statuses],
'mt-0.5 rounded-md px-1.5 py-0.5 text-xs font-medium whitespace-nowrap ring-1 ring-inset',
)}
>
{r.status}
</p>
)}
</div>
<div className="mt-1 flex items-center gap-x-2 text-xs/5 text-gray-500">
<p className="whitespace-nowrap">
<span>
Due on{' '}
<time dateTime={r.dueDate.toString()}>{r.dueDate.toLocaleDateString()}</time>
</span>
</p>
<svg viewBox="0 0 2 2" className="size-0.5 fill-current">
<circle r={1} cx={1} cy={1} />
</svg>
<p className="truncate">Borrowed from {r.owners.join(', ')}</p>
</div>
</div>
<div className="flex flex-none items-center gap-x-4">
<button
type="button"
disabled={!!returningBookId}
className="hidden disabled:opacity-20 cursor-pointer disabled:cursor-auto rounded-md bg-background px-2.5 py-1.5 text-sm font-semibold text-muted-foreground shadow-xs ring-1 ring-gray-300 ring-inset hover:bg-gray-50 sm:flex items-center gap-1"
onClick={() => handleReturnClick(r.id)}
>
{isReturningBook(r.id) && <Loader2Icon className="animate-spin" />}
<span>Return</span>
</button>
<Menu as="div" className="relative flex-none">
<MenuButton className="-m-2.5 block p-2.5 text-gray-500 hover:text-gray-900">
<span className="sr-only">Open options</span>
<EllipsisVerticalIcon aria-hidden="true" className="size-5" />
</MenuButton>
<MenuItems
transition
className="absolute right-0 z-10 mt-2 w-32 origin-top-right rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 transition focus:outline-hidden data-closed:scale-95 data-closed:transform data-closed:opacity-0 data-enter:duration-100 data-enter:ease-out data-leave:duration-75 data-leave:ease-in"
>
<MenuItem>
<button
className="block w-full text-left cursor-pointer px-3 py-1 text-sm/6 text-gray-900 data-focus:bg-gray-50 data-focus:outline-hidden"
type="button"
onClick={() => handleReturnClick(r.id)}
>
Return<span className="sr-only">, {r.title}</span>
</button>
</MenuItem>
{/*<MenuItem>
<a
href={`/repositories/${r.repositoryAbbreviation}`}
className="block px-3 py-1 text-sm/6 text-gray-900 data-focus:bg-gray-50 data-focus:outline-hidden"
>
Repository<span className="sr-only">, {r.repositoryAbbreviation}</span>
</a>
</MenuItem>*/}
<MenuItem>
<a
href={r.href}
className="block px-3 py-1 text-sm/6 text-gray-900 data-focus:bg-gray-50 data-focus:outline-hidden"
>
View<span className="sr-only"> {r.title}</span>
</a>
</MenuItem>
</MenuItems>
</Menu>
</div>
</li>
))}
</ul>
)
}
type Props = { initialLoans: PaginatedDocs<Checkout> | null }
const LoanedBooks = (props: Props) => {
const { initialLoans } = props
const { user } = useGlobal()
const [loans, setLoans] = useState<PaginatedDocs<Checkout> | null>(initialLoans)
const onUpdate = useCallback(async () => {
if (!user) return
const updatedUserLoans = await getUserLoans({ userId: user.id })
setLoans(updatedUserLoans)
}, [setLoans, user])
const rows: Row[] = useMemo(
() =>
loans?.docs?.map((c) => {
const copy = c.copy as Copy
const book = copy.book as Book
const authors = book.authors as Author[]
const repository = copy.repository as Repository
const owners = repository.owner as (number | User)[]
const parsedDateDue = c.dateDue ? new Date(c.dateDue) : new Date()
const milisecondsUntilDue = parsedDateDue.getTime() - new Date().getTime()
const secondsUntilDue = Math.floor(milisecondsUntilDue / 1000)
const minutesUntilDue = Math.floor(secondsUntilDue / 60)
const hoursUntilDue = Math.floor(minutesUntilDue / 60)
const daysUntilDue = Math.floor(hoursUntilDue / 24)
let status = ''
if (daysUntilDue <= 0) status = 'Passed Due'
else if (daysUntilDue <= 5) status = 'Due Soon'
return {
id: c.id || 0,
title: book.title || '',
authors: authors.map((a) => a.lf) || ([] as string[]),
owners:
owners.map((o) => `${(o as User).firstName} ${(o as User).lastName}`) ||
([] as string[]),
repositoryAbbreviation: repository.abbreviation || '',
status,
dueDate: parsedDateDue,
href: `/books/${book.id}`,
} as Row
}) || [],
[loans],
)
if (!rows.length) return null
return (
<section className="py-6">
<div className="mb-4 flex justify-between items-end">
<h3 className="px-4 text-lg font-semibold">Loaned Out Books</h3>
</div>
<LoanedBooksList onUpdate={onUpdate} rows={rows} />
</section>
)
}
export default LoanedBooks