237 lines
7.2 KiB
TypeScript
237 lines
7.2 KiB
TypeScript
import type { Book, Checkout, HoldRequest, Repository, User } from '@/payload-types'
|
|
import { getPayload, PaginatedDocs } from 'payload'
|
|
import config from '@/payload.config'
|
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '../ui/card'
|
|
import Image from 'next/image'
|
|
import clsx from 'clsx'
|
|
import Link from 'next/link'
|
|
import { LoginForm } from '../login-form'
|
|
|
|
type Props = {
|
|
user?: User
|
|
repos: PaginatedDocs<Repository> | null
|
|
}
|
|
const UserFeed = async (props: Props) => {
|
|
const { user, repos } = props
|
|
const isLoggedIn = !!user
|
|
|
|
if (!isLoggedIn)
|
|
return (
|
|
<div className="flex w-full max-w-sm flex-col gap-6 mx-auto my-6">
|
|
<LoginForm />
|
|
</div>
|
|
)
|
|
|
|
const payloadConfig = await config
|
|
const payload = await getPayload({ config: payloadConfig })
|
|
|
|
const borrowRequests = (await payload.find({
|
|
collection: 'holdRequests',
|
|
limit: 10,
|
|
depth: 3,
|
|
select: {
|
|
copy: true,
|
|
dateRequested: true,
|
|
repository: true,
|
|
book: true,
|
|
},
|
|
where: {
|
|
userRequested: {
|
|
equals: user?.id,
|
|
},
|
|
isCheckedOut: {
|
|
not_equals: true,
|
|
},
|
|
isRejected: {
|
|
not_equals: true,
|
|
},
|
|
},
|
|
})) as PaginatedDocs<HoldRequest>
|
|
|
|
const currentlyHeldBooks = (await payload.find({
|
|
collection: 'holdRequests',
|
|
limit: 10,
|
|
depth: 3,
|
|
select: {
|
|
copy: true,
|
|
dateRequested: true,
|
|
repository: true,
|
|
book: true,
|
|
},
|
|
where: {
|
|
'repository.owner': {
|
|
equals: user?.id,
|
|
},
|
|
isHolding: {
|
|
equals: true,
|
|
},
|
|
},
|
|
})) as PaginatedDocs<HoldRequest>
|
|
|
|
let loanedOutBooks: PaginatedDocs<Checkout> | null = null
|
|
if (user.id)
|
|
loanedOutBooks = (await payload.find({
|
|
collection: 'checkouts',
|
|
limit: 10,
|
|
depth: 2,
|
|
select: {
|
|
book: true,
|
|
dateDue: true,
|
|
loanerReturnedDate: true,
|
|
isReturned: true,
|
|
ownerVerifiedReturnedDate: true,
|
|
},
|
|
where: {
|
|
and: [
|
|
{
|
|
'copy.repository.owner': {
|
|
equals: user.id,
|
|
},
|
|
},
|
|
{
|
|
isReturned: {
|
|
not_equals: true,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
})) as PaginatedDocs<Checkout>
|
|
|
|
const totalHoldNotifications =
|
|
repos?.docs
|
|
.flatMap((r) => r.holdRequests?.docs)
|
|
.filter((r) => {
|
|
const repo = r as HoldRequest
|
|
return !repo.isRejected && !repo.isCheckedOut
|
|
}).length || 0
|
|
const outBoundLoanCount = loanedOutBooks?.totalDocs || 0
|
|
const currentlyHoldingCount = currentlyHeldBooks?.totalDocs || 0
|
|
|
|
const stats = [
|
|
{
|
|
name: 'Hold Request',
|
|
iconSrc: '/images/mail.svg',
|
|
value: totalHoldNotifications,
|
|
href: '/manage',
|
|
ctaText: 'Answer Requests',
|
|
shouldAnimate: totalHoldNotifications > 0,
|
|
},
|
|
{
|
|
name: 'Loaned Out',
|
|
iconSrc: '/images/book-loan.svg',
|
|
value: outBoundLoanCount,
|
|
href: '/manage',
|
|
ctaText: 'Handle Returns',
|
|
shouldAnimate: false,
|
|
},
|
|
{
|
|
name: 'Currently Holding',
|
|
iconSrc: '/images/book-shelf.svg',
|
|
value: currentlyHoldingCount,
|
|
href: '/manage',
|
|
ctaText: 'Loan Out',
|
|
shouldAnimate: false,
|
|
},
|
|
]
|
|
|
|
return (
|
|
<section>
|
|
<div className="my-6">
|
|
<h3 className="text-lg font-semibold text-foreground">Loan Activity</h3>
|
|
<dl className="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3">
|
|
{stats.map((s) => {
|
|
return (
|
|
<div
|
|
key={s.name}
|
|
className={clsx(
|
|
'relative overflow-hidden rounded-lg shadow-sm border border-accent px-4 pt-5 pb-12 sm:px-6 sm:pt-6',
|
|
s.shouldAnimate && 'animate-pulse',
|
|
)}
|
|
>
|
|
<dt>
|
|
<div className="absolute rounded-md bg-emerald-500 p-3">
|
|
<Image
|
|
src={s.iconSrc}
|
|
alt={s.name}
|
|
width={30}
|
|
height={30}
|
|
className="dark:invert"
|
|
/>
|
|
</div>
|
|
<p className="ml-16 truncate text-sm font-medium text-muted-foreground">
|
|
{s.name}
|
|
</p>
|
|
</dt>
|
|
<dd className="ml-16 flex items-baseline pb-6 sm:pb-7">
|
|
<p className="text-2xl font-semibold text-foreground">{s.value}</p>
|
|
<div className="absolute inset-x-0 bottom-0 px-4 py-4 sm:px-6">
|
|
<div className="text-sm">
|
|
<a
|
|
href={s.href}
|
|
className="font-medium text-emerald-800 hover:text-emerald-300"
|
|
>
|
|
<span>{s.ctaText}</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</dd>
|
|
</div>
|
|
)
|
|
})}
|
|
</dl>
|
|
</div>
|
|
|
|
<div className="my-6">
|
|
<h2 className="text-lg font-semibold text-foreground">Borrow Activity</h2>
|
|
<div className="my-3">
|
|
<h3 className="text-base font-semibold text-muted-foreground mb-4">Your Holds</h3>
|
|
<ul className="grid grid-cols-1 gap-y-6 sm:grid-cols-3 md:grid-cols-4 last-child-adjustment">
|
|
{borrowRequests.docs?.map((h) => {
|
|
const book = h.book as Book
|
|
const repository = h.repository as Repository
|
|
|
|
const formatedDateRequested = h.dateRequested
|
|
? new Date(h.dateRequested).toDateString()
|
|
: ''
|
|
|
|
return (
|
|
<li className="col-span-1 auto-rows-fr inline-block" key={book.isbn}>
|
|
<Link className="block hover:scale-105 transition-all" href={`/books/${book.id}`}>
|
|
<Card className="w-48 sm:w-42 mx-auto pt-4 pb-2">
|
|
<CardHeader className="-mb-4">
|
|
<CardTitle className="text-overflow-ellipsis line-clamp-2">
|
|
{book.title}
|
|
</CardTitle>
|
|
<CardDescription>{repository.abbreviation}</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Image
|
|
alt="book cover"
|
|
className="mx-auto h-[170px]"
|
|
width={120}
|
|
height={180}
|
|
src={
|
|
book.isbn
|
|
? `https://covers.openlibrary.org/b/isbn/${book.isbn}-M.jpg`
|
|
: '/images/book-48.svg'
|
|
}
|
|
/>
|
|
</CardContent>
|
|
<CardFooter className="text-muted-foreground text-sm block -mt-5">
|
|
<span className="block w-full text-xs">Requested On</span>
|
|
<span className="block w-full">{formatedDateRequested}</span>
|
|
</CardFooter>
|
|
</Card>
|
|
</Link>
|
|
</li>
|
|
)
|
|
})}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
export default UserFeed
|