feat: reject inbound hold requests

This commit is contained in:
Yehoshua Sandler 2025-05-03 16:41:47 -05:00
parent 7a52261b1e
commit bce3a7c23e
7 changed files with 231 additions and 106 deletions

View File

@ -86,8 +86,6 @@ export default function BookByIdPageClient(props: Props) {
const { user } = useGlobal() const { user } = useGlobal()
console.log(user)
const [isRequestingCopy, setIsRequestingCopy] = useState(false) const [isRequestingCopy, setIsRequestingCopy] = useState(false)
const [selectedRepository, setSelectedRepository] = useState<Repository | null>( const [selectedRepository, setSelectedRepository] = useState<Repository | null>(
repos.length ? repos[0] : null, repos.length ? repos[0] : null,

View File

@ -90,7 +90,7 @@ export default async function HomePage() {
return ( return (
<div className="home"> <div className="home">
<HomeHero user={user} /> <HomeHero user={user} />
<div id="homeContent" className='flex flex-col justify-around min-h-[90vh]'> <div id="homeContent" className="flex flex-col justify-around min-h-[90vh]">
{user ? ( {user ? (
<Tabs id="tabs" defaultValue="feed" className="p-4"> <Tabs id="tabs" defaultValue="feed" className="p-4">
<TabsList className="grid w-full grid-cols-3"> <TabsList className="grid w-full grid-cols-3">
@ -108,7 +108,7 @@ export default async function HomePage() {
</TabsContent> </TabsContent>
<TabsContent value="manage"> <TabsContent value="manage">
<Manage repos={userRepos} checkouts={userCheckouts} /> <Manage repos={userRepos} user={user} checkouts={userCheckouts} />
</TabsContent> </TabsContent>
</Tabs> </Tabs>
) : ( ) : (

View File

@ -42,6 +42,9 @@ const UserFeed = async (props: Props) => {
isCheckedOut: { isCheckedOut: {
not_equals: true, not_equals: true,
}, },
isRejected: {
not_equals: true,
},
}, },
})) as PaginatedDocs<HoldRequest> })) as PaginatedDocs<HoldRequest>

View File

@ -3,23 +3,50 @@ import { Author, Book, HoldRequest, Repository, User } from '@/payload-types'
import { Button } from '../ui/button' import { Button } from '../ui/button'
import Image from 'next/image' import Image from 'next/image'
import ApproveHoldRequestModal from './ApproveHoldRequestModal' import ApproveHoldRequestModal from './ApproveHoldRequestModal'
import { useState } from 'react' import { useCallback, useState } from 'react'
import CheckoutFromHoldModal from './CheckoutFromHoldModal' import CheckoutFromHoldModal from './CheckoutFromHoldModal'
import rejectHoldRequest from '@/serverActions/RejectHoldRequests'
import { getUserRepos } from '@/serverActions/GetUserRepos'
import { Loader2 } from 'lucide-react'
import { toast } from 'sonner'
type Props = { type Props = {
repos: PaginatedDocs<Repository> | null repos: PaginatedDocs<Repository> | null
user: User
} }
const HoldRequestNotifications = (props: Props) => { const HoldRequestNotifications = (props: Props) => {
const { repos } = props const [repos, setRepos] = useState<PaginatedDocs<Repository> | null>(props.repos)
const [openedModalId, setOpenedModalId] = useState<number | null>(null) const [openedModalId, setOpenedModalId] = useState<number | null>(null)
const totalHoldNotifications = repos?.docs.flatMap((r) => r.holdRequests?.docs).length || 0 const totalHoldNotifications = repos?.docs.flatMap((r) => r.holdRequests?.docs).length || 0
const [isRejectingId, setIsRejectingId] = useState<number | null>()
const handleRejectHoldClick = useCallback(
async (holdRequestId: number) => {
if (isRejectingId) return
setIsRejectingId(holdRequestId)
const rejectRequest = await rejectHoldRequest({ holdRequestId })
if (rejectRequest?.isRejected) {
const updatedRepos = await getUserRepos({ userId: props.user.id })
setRepos(updatedRepos)
toast('Request was rejected')
} else {
toast('Error rejecting request')
}
setIsRejectingId(null)
},
[isRejectingId, setIsRejectingId, props.user.id],
)
const holdRequestsByRepoElements = repos?.docs.map((r) => { const holdRequestsByRepoElements = repos?.docs.map((r) => {
return ( return (
<ul key={r.id} role="list" className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3"> <ul key={r.id} role="list" className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{r.holdRequests?.docs?.map((h) => { {r.holdRequests?.docs
?.map((h) => {
const hold = h as HoldRequest const hold = h as HoldRequest
const book = hold.book as Book const book = hold.book as Book
const authors = book.authors as Author[] const authors = book.authors as Author[]
@ -30,6 +57,8 @@ const HoldRequestNotifications = (props: Props) => {
const userRequested = hold.userRequested as User const userRequested = hold.userRequested as User
const userName = `${userRequested.firstName} ${userRequested.lastName}` const userName = `${userRequested.firstName} ${userRequested.lastName}`
if (hold.isRejected) return null
return ( return (
<li key={hold.id} className="col-span-1 rounded-lg shadow-sm border border-accent"> <li key={hold.id} className="col-span-1 rounded-lg shadow-sm border border-accent">
<div className="flex w-full items-center justify-between space-x-6 p-6"> <div className="flex w-full items-center justify-between space-x-6 p-6">
@ -68,12 +97,32 @@ const HoldRequestNotifications = (props: Props) => {
</div> </div>
<div> <div>
<div className="flex gap-2 justify-around"> <div className="flex gap-2 justify-around">
<Button className="inline-flex flex-1 items-center justify-center gap-3 rounded-bl-lg border border-transparent py-4 text-sm font-semibold text-foreground bg-foreground/5 hover:bg-red-400/10 cursor-pointer hover:scale-105"> <Button
<Image width={24} height={24} src="/images/reject.svg" alt="approve hold" /> className="inline-flex flex-1 items-center justify-center gap-3 rounded-bl-lg border border-transparent py-4 text-sm font-semibold text-foreground bg-foreground/5 hover:bg-red-400/10 cursor-pointer [&:not(:disabled)]:hover:scale-105"
<span>Decline</span> disabled={!!isRejectingId}
onClick={() => handleRejectHoldClick(hold.id)}
>
{isRejectingId === hold.id ? (
<>
<Loader2 className="animate-spin size-[24px] text-red-200 dark:text-red-700" />
<span className="text-red-200 dark:text-red-700">Rejecting</span>
</>
) : (
<>
<Image
width={24}
height={24}
src="/images/reject.svg"
alt="approve hold"
/>
<span>Reject</span>
</>
)}
</Button> </Button>
<Button <Button
className="inline-flex flex-1 items-center justify-center gap-3 rounded-br-lg border border-transparent py-4 text-sm font-semibold text-foreground bg-emerald-400/30 hover:bg-emerald-300/60 cursor-pointer hover:scale-105" className="inline-flex flex-1 items-center justify-center gap-3 rounded-br-lg border border-transparent py-4 text-sm font-semibold text-foreground bg-emerald-400/30 hover:bg-emerald-300/60 cursor-pointer hover:scale-105"
disabled={!!isRejectingId || !!openedModalId}
onClick={() => setOpenedModalId(hold.id)} onClick={() => setOpenedModalId(hold.id)}
> >
<Image width={24} height={24} src="/images/approve.svg" alt="approve hold" /> <Image width={24} height={24} src="/images/approve.svg" alt="approve hold" />
@ -97,7 +146,8 @@ const HoldRequestNotifications = (props: Props) => {
</div> </div>
</li> </li>
) )
})} })
.filter((element) => !!element)}
</ul> </ul>
) )
}) })

View File

@ -1,6 +1,6 @@
'use client' 'use client'
import { Checkout, Repository } from '@/payload-types' import { Checkout, Repository, User } from '@/payload-types'
import { PaginatedDocs } from 'payload' import { PaginatedDocs } from 'payload'
import RepoList from './RepoList' import RepoList from './RepoList'
import HoldRequestNotifications from './HoldRequests' import HoldRequestNotifications from './HoldRequests'
@ -9,9 +9,10 @@ import CheckedOutBooks from './CheckedOutBooks'
type Props = { type Props = {
repos: PaginatedDocs<Repository> | null repos: PaginatedDocs<Repository> | null
checkouts: PaginatedDocs<Checkout> | null checkouts: PaginatedDocs<Checkout> | null
user: User | null
} }
const Manage = (props: Props) => { const Manage = (props: Props) => {
const { repos, checkouts } = props const { repos, checkouts, user } = props
return ( return (
<section> <section>
@ -19,9 +20,7 @@ const Manage = (props: Props) => {
<RepoList repos={repos} /> <RepoList repos={repos} />
</div> </div>
<div> <div>{user && <HoldRequestNotifications repos={repos} user={user} />}</div>
<HoldRequestNotifications repos={repos} />
</div>
<div> <div>
<CheckedOutBooks initialCheckoutPage={checkouts} /> <CheckedOutBooks initialCheckoutPage={checkouts} />

View File

@ -0,0 +1,44 @@
'use server'
import { getPayload } from 'payload'
import config from '@/payload.config'
import { PaginatedDocs } from "payload"
import { Repository } from '@/payload-types'
type Props = {
userId: number
}
export const getUserRepos = async (props: Props) => {
const { userId } = props
const payloadConfig = await config
const payload = await getPayload({ config: payloadConfig })
let userRepos: PaginatedDocs<Repository> | null = null
if (userId)
userRepos = (await payload.find({
collection: 'repositories',
depth: 3,
limit: 10,
select: {
name: true,
abbreviation: true,
image: true,
description: true,
dateOpenToPublic: true,
holdRequests: true,
},
where: {
'owner.id': {
equals: userId,
},
},
joins: {
holdRequests: {
limit: 100,
},
},
})) as PaginatedDocs<Repository>
return userRepos
}

View File

@ -0,0 +1,31 @@
'use server'
import { getPayload } from 'payload'
import config from '@/payload.config'
import { HoldRequest } from '@/payload-types'
type Props = {
holdRequestId: number,
}
export const rejectHoldRequest = async (props: Props): Promise<HoldRequest | null> => {
const { holdRequestId } = props
const payloadConfig = await config
const payload = await getPayload({ config: payloadConfig })
try {
const updatedHold = await payload.update({
collection: 'holdRequests',
id: holdRequestId,
data: {
isRejected: true,
isHolding: false,
}
})
return updatedHold
} catch (_) {
return null
}
}
export default rejectHoldRequest