feat: requestHold feature

This commit is contained in:
Yehoshua Sandler 2025-04-21 12:39:56 -05:00
parent 54d9794d41
commit 06573d9044
9 changed files with 336 additions and 11 deletions

View File

@ -4,18 +4,31 @@ import { Book, Genre, Repository } from '@/payload-types'
import { Combobox, ComboboxLabel, ComboboxOption, ComboboxDescription } from '@/components/combobox'
import { Field, Label } from '@/components/fieldset'
import { PaginatedDocs } from 'payload'
import type { PaginatedDocs } from 'payload'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Loader2 } from 'lucide-react'
import { RichText } from '@/components/RichText'
import { useMemo, useState } from 'react'
import requestHold from '../../serverCalls/requestHold'
type DropDownProps = {
currentRepository: Repository
repositories: Repository[]
isDisabled: boolean
isRequesting: boolean
onClickRequest: () => void
onChange: (repo: Repository | null) => void
}
function RepoDropdown({ currentRepository, repositories }: DropDownProps) {
function RepoDropdown({
currentRepository,
repositories,
isRequesting,
isDisabled,
onClickRequest,
onChange,
}: DropDownProps) {
return (
<Field className="">
<Label htmlFor="repositories">From Repository</Label>
@ -25,7 +38,8 @@ function RepoDropdown({ currentRepository, repositories }: DropDownProps) {
options={repositories}
displayValue={(repo) => repo?.name}
defaultValue={currentRepository}
className="w"
className=""
onChange={(repo) => onChange(repo)}
>
{(repo) => (
<ComboboxOption value={repo}>
@ -34,8 +48,12 @@ function RepoDropdown({ currentRepository, repositories }: DropDownProps) {
</ComboboxOption>
)}
</Combobox>
<Button className="hover:scale-105 bg-emerald-500 text-foreground hover:text-black">
<Loader2 className="animate-spin" />
<Button
disabled={isDisabled}
onClick={onClickRequest}
className="hover:scale-105 bg-emerald-500 text-foreground hover:text-black cursor-pointer"
>
{isRequesting && <Loader2 className="animate-spin" />}
<span>Request Copy</span>
</Button>
</div>
@ -53,6 +71,29 @@ export default function BookByIdPageClient(props: Props) {
const repos = repositories.docs
const [isRequestingCopy, setIsRequestingCopy] = useState(false)
const [selectedRepository, setSelectedRepository] = useState<Repository | null>(
repos.length ? repos[0] : null,
)
const isRequestDisabled = useMemo(() => {
return isRequestingCopy
}, [isRequestingCopy])
const onClickRequest = async () => {
if (isRequestingCopy || !selectedRepository || !book) return
setIsRequestingCopy(true)
const response = await requestHold({
repositoryId: selectedRepository.id,
bookId: book.id,
})
console.log(response)
setIsRequestingCopy(false)
}
return (
<div className="mx-auto px-4 py-16">
{/* Book */}
@ -94,7 +135,14 @@ export default function BookByIdPageClient(props: Props) {
<p className="my-6 text-accent-foreground">{book.summary}</p>
<RepoDropdown currentRepository={repos[0]} repositories={repos} />
<RepoDropdown
currentRepository={repos[0]}
repositories={repos}
isRequesting={isRequestingCopy}
isDisabled={isRequestDisabled}
onClickRequest={onClickRequest}
onChange={(repo) => setSelectedRepository(repo)}
/>
<div className="mt-10 border-t border-gray-200 pt-10 ">
<h3 className="text-sm font-medium text-foreground">Genres</h3>

View File

@ -38,12 +38,14 @@ export default async function HomePage() {
<div className="py-24 sm:py-32">
<div className="mx-auto max-w-7xl px-6 lg:px-8">
<div className="mx-auto max-w-2xl lg:mx-0">
<p className="text-base/7 font-semibold text-indigo-600">Get the help you need</p>
<p className="text-base/7 font-semibold text-foreground">
Engage In Our Community Resources
</p>
<h2 className="mt-2 text-5xl font-semibold tracking-tight text-foreground sm:text-7xl">
<TextShimmer
duration={1.2}
className="[--base-color:var(--color-indigo-600)] [--base-gradient-color:var(--color-blue-200)] dark:[--base-color:var(--color-blue-700)] dark:[--base-gradient-color:var(--color-blue-400)]"
duration={2.2}
className="[--base-color:var(--color-emerald-700)] [--base-gradient-color:var(--color-white)] dark:[--base-color:var(--color-emerald-600)] dark:[--base-gradient-color:var(--color-white)]"
>
Welcome
</TextShimmer>

View File

@ -0,0 +1,24 @@
'use server'
import { getPayload } from "payload"
import configPromise from '@payload-config'
type Props = {
bookId: number,
repositoryId: number,
}
const requestHold = async (props: Props) => {
const payload = await getPayload({ config: configPromise })
const requestHoldResponse = await payload.create({
collection: 'holdRequests',
data: {
repository: props.repositoryId,
book: props.bookId,
},
})
return requestHoldResponse
}
export default requestHold

View File

@ -0,0 +1,34 @@
import { CollectionConfig } from "payload";
const Checkouts: CollectionConfig = {
slug: 'checkouts',
fields: [
{
name: 'fromHold',
type: 'relationship',
relationTo: 'holdRequests',
hasMany: false,
},
{
name: 'user',
type: 'relationship',
relationTo: 'users',
hasMany: false,
},
{
name: 'copy',
type: 'relationship',
relationTo: 'copies',
hasMany: false,
},
{
name: 'book',
type: 'join',
collection: 'copies',
on: 'book',
hasMany: false,
}
]
}
export default Checkouts

View File

@ -0,0 +1,79 @@
import { CollectionConfig } from "payload";
const HoldRequests: CollectionConfig = {
slug: 'holdRequests',
fields: [
{
name: 'copy',
type: 'relationship',
relationTo: 'copies',
filterOptions: ({ data }) => {
return {
book: {
equals: data.book
},
}
}
},
{
name: 'book',
type: 'relationship',
relationTo: 'books',
required: true,
},
{
name: 'repository',
type: 'relationship',
relationTo: 'repositories',
},
{
name: 'userRequested',
type: 'relationship',
relationTo: 'users',
required: true,
},
{
name: 'dateRequested',
type: 'date',
},
{
name: 'isHolding',
type: 'checkbox',
hooks: {
beforeValidate: [({ data, originalDoc }) => {
if (data?.isHolding && !data.copy) return originalDoc
}]
}
},
{
name: 'holdingUntilDate',
type: 'date',
},
{
name: 'isCheckedOut',
type: 'checkbox',
hooks:
{
beforeValidate: [({ data, originalDoc }) => {
if (data?.isCheckedOut && !data.copy) return originalDoc
if (originalDoc.isCheckedOut && !data?.isCheckedOut) return originalDoc
}],
afterChange: [({ value, data, req }) => {
if (value) {
req.payload.create({
collection: 'checkouts',
data: {
user: data?.userRequested,
copy: data?.copy,
}
})
}
}]
}
}
],
}
export default HoldRequests

View File

@ -73,6 +73,26 @@ export const Copies: CollectionConfig = {
{
name: 'notes',
type: 'richText'
},
{
name: 'holdRequests',
type: 'join',
collection: 'holdRequests',
on: 'copy',
where: {
or: [
{
isCheckedOut: {
equals: false
}
},
{
isCheckedOut: {
equals: null
}
}
]
}
}
],
hooks: {

View File

@ -30,6 +30,27 @@ export const Repositories: CollectionConfig = {
{
name: 'dateOpenToPublic',
type: 'date'
}
},
{
name: 'holdRequests',
type: 'join',
collection: 'holdRequests',
on: 'repository',
defaultSort: 'dateRequested',
where: {
or: [
{
isCheckedOut: {
equals: false
}
},
{
isCheckedOut: {
equals: null
}
}
]
}
},
]
}

View File

@ -73,6 +73,8 @@ export interface Config {
authors: Author;
repositories: Repository;
copies: Copy;
holdRequests: HoldRequest;
checkouts: Checkout;
genre: Genre;
pages: Page;
'payload-locked-documents': PayloadLockedDocument;
@ -86,6 +88,15 @@ export interface Config {
authors: {
books: 'books';
};
repositories: {
holdRequests: 'holdRequests';
};
copies: {
holdRequests: 'holdRequests';
};
checkouts: {
book: 'copies';
};
};
collectionsSelect: {
users: UsersSelect<false> | UsersSelect<true>;
@ -94,6 +105,8 @@ export interface Config {
authors: AuthorsSelect<false> | AuthorsSelect<true>;
repositories: RepositoriesSelect<false> | RepositoriesSelect<true>;
copies: CopiesSelect<false> | CopiesSelect<true>;
holdRequests: HoldRequestsSelect<false> | HoldRequestsSelect<true>;
checkouts: CheckoutsSelect<false> | CheckoutsSelect<true>;
genre: GenreSelect<false> | GenreSelect<true>;
pages: PagesSelect<false> | PagesSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
@ -265,6 +278,11 @@ export interface Copy {
};
[k: string]: unknown;
} | null;
holdRequests?: {
docs?: (number | HoldRequest)[];
hasNextPage?: boolean;
totalDocs?: number;
};
updatedAt: string;
createdAt: string;
}
@ -281,6 +299,45 @@ export interface Repository {
abbreviation: string;
owner: (number | User)[];
dateOpenToPublic?: string | null;
holdRequests?: {
docs?: (number | HoldRequest)[];
hasNextPage?: boolean;
totalDocs?: number;
};
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "holdRequests".
*/
export interface HoldRequest {
id: number;
copy?: (number | null) | Copy;
book: number | Book;
repository?: (number | null) | Repository;
userRequested: number | User;
dateRequested?: string | null;
isHolding?: boolean | null;
holdingUntilDate?: string | null;
isCheckedOut?: boolean | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "checkouts".
*/
export interface Checkout {
id: number;
fromHold?: (number | null) | HoldRequest;
user?: (number | null) | User;
copy?: (number | null) | Copy;
book?: {
docs?: (number | Copy)[];
hasNextPage?: boolean;
totalDocs?: number;
};
updatedAt: string;
createdAt: string;
}
@ -337,6 +394,14 @@ export interface PayloadLockedDocument {
relationTo: 'copies';
value: number | Copy;
} | null)
| ({
relationTo: 'holdRequests';
value: number | HoldRequest;
} | null)
| ({
relationTo: 'checkouts';
value: number | Checkout;
} | null)
| ({
relationTo: 'genre';
value: number | Genre;
@ -463,6 +528,7 @@ export interface RepositoriesSelect<T extends boolean = true> {
abbreviation?: T;
owner?: T;
dateOpenToPublic?: T;
holdRequests?: T;
updatedAt?: T;
createdAt?: T;
}
@ -476,6 +542,35 @@ export interface CopiesSelect<T extends boolean = true> {
book?: T;
repository?: T;
notes?: T;
holdRequests?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "holdRequests_select".
*/
export interface HoldRequestsSelect<T extends boolean = true> {
copy?: T;
book?: T;
repository?: T;
userRequested?: T;
dateRequested?: T;
isHolding?: T;
holdingUntilDate?: T;
isCheckedOut?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "checkouts_select".
*/
export interface CheckoutsSelect<T extends boolean = true> {
fromHold?: T;
user?: T;
copy?: T;
book?: T;
updatedAt?: T;
createdAt?: T;
}

View File

@ -15,6 +15,8 @@ import { Copies } from './collections/Copies/Copies'
import { Genre } from './collections/Books/Genre'
import { Header } from './globals/header/config'
import { Pages } from './collections/Pages/Pages'
import HoldRequests from './collections/Checkouts/HoldRequests'
import Checkouts from './collections/Checkouts/Checkouts'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
@ -35,7 +37,7 @@ export default buildConfig({
},
},
globals: [Header],
collections: [Users, Media, Books, Authors, Repositories, Copies, Genre, Pages],
collections: [Users, Media, Books, Authors, Repositories, Copies, HoldRequests, Checkouts, Genre, Pages],
editor: lexicalEditor(),
secret: process.env.PAYLOAD_SECRET || '',
typescript: {