feat: conditional UX around if logged in and hold request exists on book page
This commit is contained in:
parent
7deb02391a
commit
9db5736487
@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { Book, Genre, Repository } from '@/payload-types'
|
import { Book, Genre, HoldRequest, Repository } from '@/payload-types'
|
||||||
|
|
||||||
import { Combobox, ComboboxLabel, ComboboxOption, ComboboxDescription } from '@/components/combobox'
|
import { Combobox, ComboboxLabel, ComboboxOption, ComboboxDescription } from '@/components/combobox'
|
||||||
import { Field, Label } from '@/components/fieldset'
|
import { Field, Label } from '@/components/fieldset'
|
||||||
@ -12,11 +12,13 @@ import { Loader2 } from 'lucide-react'
|
|||||||
import { RichText } from '@/components/RichText'
|
import { RichText } from '@/components/RichText'
|
||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import requestHold from '../../serverCalls/requestHold'
|
import requestHold from '../../serverCalls/requestHold'
|
||||||
|
import { useGlobal } from '@/providers/GlobalProvider'
|
||||||
|
|
||||||
type DropDownProps = {
|
type DropDownProps = {
|
||||||
currentRepository: Repository
|
currentRepository: Repository
|
||||||
repositories: Repository[]
|
repositories: Repository[]
|
||||||
isDisabled: boolean
|
isDisabled: boolean
|
||||||
|
doesHoldExist: boolean
|
||||||
isRequesting: boolean
|
isRequesting: boolean
|
||||||
onClickRequest: () => void
|
onClickRequest: () => void
|
||||||
onChange: (repo: Repository | null) => void
|
onChange: (repo: Repository | null) => void
|
||||||
@ -25,17 +27,25 @@ function RepoDropdown({
|
|||||||
currentRepository,
|
currentRepository,
|
||||||
repositories,
|
repositories,
|
||||||
isRequesting,
|
isRequesting,
|
||||||
|
doesHoldExist,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
onClickRequest,
|
onClickRequest,
|
||||||
onChange,
|
onChange,
|
||||||
}: DropDownProps) {
|
}: DropDownProps) {
|
||||||
return (
|
return (
|
||||||
<Field className="">
|
<Field className="">
|
||||||
|
{doesHoldExist ? (
|
||||||
|
<span className="font-medium text-emerald-800 dark:text-emerald-300 ">
|
||||||
|
You have requested a hold for this book
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<Label htmlFor="repositories">From Repository</Label>
|
<Label htmlFor="repositories">From Repository</Label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Combobox
|
<Combobox
|
||||||
name="repositories"
|
name="repositories"
|
||||||
options={repositories}
|
options={repositories}
|
||||||
|
disabled={isDisabled || doesHoldExist}
|
||||||
displayValue={(repo) => repo?.name}
|
displayValue={(repo) => repo?.name}
|
||||||
defaultValue={currentRepository}
|
defaultValue={currentRepository}
|
||||||
className=""
|
className=""
|
||||||
@ -51,12 +61,14 @@ function RepoDropdown({
|
|||||||
<Button
|
<Button
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
onClick={onClickRequest}
|
onClick={onClickRequest}
|
||||||
className="hover:scale-105 bg-emerald-500 text-foreground hover:text-background cursor-pointer"
|
className="hover:scale-105 bg-emerald-500 text-foreground hover:text-background cursor-pointer disabled:bg-muted-foreground/80"
|
||||||
>
|
>
|
||||||
{isRequesting && <Loader2 className="animate-spin" />}
|
{isRequesting && <Loader2 className="animate-spin" />}
|
||||||
<span>Request Copy</span>
|
<span>Request Hold</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -64,39 +76,41 @@ function RepoDropdown({
|
|||||||
type Props = {
|
type Props = {
|
||||||
book: Book
|
book: Book
|
||||||
repositories: PaginatedDocs<Repository>
|
repositories: PaginatedDocs<Repository>
|
||||||
|
existingHolds: PaginatedDocs<HoldRequest> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BookByIdPageClient(props: Props) {
|
export default function BookByIdPageClient(props: Props) {
|
||||||
const { book, repositories } = props
|
const { book, repositories, existingHolds } = props
|
||||||
|
|
||||||
const repos = repositories.docs
|
const repos = repositories.docs
|
||||||
|
|
||||||
|
const { user } = useGlobal()
|
||||||
|
|
||||||
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const [doesHoldExist, setDoesHoldExist] = useState(!!existingHolds?.totalDocs)
|
||||||
|
|
||||||
const isRequestDisabled = useMemo(() => {
|
const isRequestDisabled = useMemo(() => {
|
||||||
return isRequestingCopy
|
return isRequestingCopy
|
||||||
}, [isRequestingCopy])
|
}, [isRequestingCopy])
|
||||||
|
|
||||||
const onClickRequest = async () => {
|
const onClickRequest = async () => {
|
||||||
if (isRequestingCopy || !selectedRepository || !book) return
|
if (isRequestingCopy || !selectedRepository || !book || doesHoldExist) return
|
||||||
|
|
||||||
setIsRequestingCopy(true)
|
setIsRequestingCopy(true)
|
||||||
const response = await requestHold({
|
const newHoldResponse = await requestHold({
|
||||||
repositoryId: selectedRepository.id,
|
repositoryId: selectedRepository.id,
|
||||||
bookId: book.id,
|
bookId: book.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(response)
|
if (newHoldResponse?.id) setDoesHoldExist(true)
|
||||||
|
|
||||||
setIsRequestingCopy(false)
|
setIsRequestingCopy(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto px-4 py-16">
|
<div className="mx-auto px-4 py-16">
|
||||||
{/* Book */}
|
|
||||||
<div className="md:grid md:grid-cols-7 md:gap-8">
|
<div className="md:grid md:grid-cols-7 md:gap-8">
|
||||||
{/* Book Cover */}
|
{/* Book Cover */}
|
||||||
<div className="md:col-span-3">
|
<div className="md:col-span-3">
|
||||||
@ -113,7 +127,6 @@ export default function BookByIdPageClient(props: Props) {
|
|||||||
|
|
||||||
{/* Book details */}
|
{/* Book details */}
|
||||||
<div className="mt-14 sm:mt-16 md:col-span-4 md:mt-0">
|
<div className="mt-14 sm:mt-16 md:col-span-4 md:mt-0">
|
||||||
<div className="">
|
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<h1 className="text-2xl font-bold tracking-tight text-foreground sm:text-3xl">
|
<h1 className="text-2xl font-bold tracking-tight text-foreground sm:text-3xl">
|
||||||
{book.title}
|
{book.title}
|
||||||
@ -131,7 +144,6 @@ export default function BookByIdPageClient(props: Props) {
|
|||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="my-6 text-accent-foreground">{book.summary}</p>
|
<p className="my-6 text-accent-foreground">{book.summary}</p>
|
||||||
|
|
||||||
@ -139,7 +151,8 @@ export default function BookByIdPageClient(props: Props) {
|
|||||||
currentRepository={repos[0]}
|
currentRepository={repos[0]}
|
||||||
repositories={repos}
|
repositories={repos}
|
||||||
isRequesting={isRequestingCopy}
|
isRequesting={isRequestingCopy}
|
||||||
isDisabled={isRequestDisabled}
|
doesHoldExist={doesHoldExist}
|
||||||
|
isDisabled={isRequestDisabled || !user}
|
||||||
onClickRequest={onClickRequest}
|
onClickRequest={onClickRequest}
|
||||||
onChange={(repo) => setSelectedRepository(repo)}
|
onChange={(repo) => setSelectedRepository(repo)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { Book, Copy, Repository } from '@/payload-types'
|
import { Book, Copy, HoldRequest, Repository } from '@/payload-types'
|
||||||
import { getPayload, PaginatedDocs } from 'payload'
|
import { getPayload, PaginatedDocs } from 'payload'
|
||||||
import configPromise from '@payload-config'
|
import configPromise from '@payload-config'
|
||||||
|
import { headers as getHeaders } from 'next/headers.js'
|
||||||
import BookByIdPageClient from './page.client'
|
import BookByIdPageClient from './page.client'
|
||||||
import { PageBreadCrumb, Route } from '@/components/PageBreadCrumb'
|
import { PageBreadCrumb, Route } from '@/components/PageBreadCrumb'
|
||||||
|
|
||||||
@ -31,7 +32,8 @@ const BookByIdPage = async (props: Props) => {
|
|||||||
|
|
||||||
const payload = await getPayload({ config: configPromise })
|
const payload = await getPayload({ config: configPromise })
|
||||||
|
|
||||||
const defaultRepositoryLimit = 10
|
const headers = await getHeaders()
|
||||||
|
const { user } = await payload.auth({ headers })
|
||||||
|
|
||||||
const foundBook = (await payload.findByID({
|
const foundBook = (await payload.findByID({
|
||||||
collection: 'books',
|
collection: 'books',
|
||||||
@ -51,16 +53,45 @@ const BookByIdPage = async (props: Props) => {
|
|||||||
},
|
},
|
||||||
})) as Book
|
})) as Book
|
||||||
|
|
||||||
const repositoryIds = foundBook.copies?.docs?.map((c) => (c as Copy).repository)
|
let existingHolds: PaginatedDocs<HoldRequest> | null = null
|
||||||
|
if (user?.id)
|
||||||
const orQueries = repositoryIds?.map((c) => {
|
existingHolds = (await payload.find({
|
||||||
return {
|
collection: 'holdRequests',
|
||||||
id: {
|
depth: 1,
|
||||||
equals: c,
|
limit: 1,
|
||||||
|
select: {
|
||||||
|
dateRequested: true,
|
||||||
|
holdingUntilDate: true,
|
||||||
},
|
},
|
||||||
}
|
where: {
|
||||||
})
|
and: [
|
||||||
|
{
|
||||||
|
'book.id': {
|
||||||
|
equals: bookId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userRequested: {
|
||||||
|
equals: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isCheckedOut: {
|
||||||
|
not_equals: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isRejected: {
|
||||||
|
not_equals: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})) as PaginatedDocs<HoldRequest>
|
||||||
|
|
||||||
|
const defaultRepositoryLimit = 10
|
||||||
|
const repositoryIds = foundBook.copies?.docs?.map((c) => (c as Copy).repository)
|
||||||
|
const repositoryOrQueries = repositoryIds?.map((c) => ({ id: { equals: c } }))
|
||||||
const repositories = (await payload.find({
|
const repositories = (await payload.find({
|
||||||
collection: 'repositories',
|
collection: 'repositories',
|
||||||
depth: 3,
|
depth: 3,
|
||||||
@ -72,28 +103,38 @@ const BookByIdPage = async (props: Props) => {
|
|||||||
name: true,
|
name: true,
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
or: orQueries?.length ? orQueries : [],
|
or: repositoryOrQueries?.length ? repositoryOrQueries : [],
|
||||||
},
|
},
|
||||||
})) as PaginatedDocs<Repository>
|
})) as PaginatedDocs<Repository>
|
||||||
|
|
||||||
const createBreadcrumbRoutes =() => {
|
const createBreadcrumbRoutes = () => {
|
||||||
|
const maxTitleLength = 18
|
||||||
|
|
||||||
let title
|
let title
|
||||||
if (!foundBook?.title) title = 'Book Not Found'
|
if (!foundBook?.title) title = 'Book Not Found'
|
||||||
else title = foundBook.title.length > 25 ? `${foundBook.title.slice(0, 25)}...` : foundBook.title
|
else
|
||||||
|
title =
|
||||||
|
foundBook.title.length > maxTitleLength
|
||||||
|
? `${foundBook.title.slice(0, maxTitleLength)}...`
|
||||||
|
: foundBook.title
|
||||||
|
|
||||||
|
return [
|
||||||
return [...staticBreadcrumRoutes, {
|
...staticBreadcrumRoutes,
|
||||||
|
{
|
||||||
label: title,
|
label: title,
|
||||||
href: `/books/${bookId}`
|
href: `/books/${bookId}`,
|
||||||
}]
|
},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(createBreadcrumbRoutes())
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageBreadCrumb routes={createBreadcrumbRoutes()} />
|
<PageBreadCrumb routes={createBreadcrumbRoutes()} />
|
||||||
<BookByIdPageClient book={foundBook} repositories={repositories} />
|
<BookByIdPageClient
|
||||||
|
book={foundBook}
|
||||||
|
repositories={repositories}
|
||||||
|
existingHolds={existingHolds}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ const LoginPage = async () => {
|
|||||||
if (Boolean(userResult.user)) redirect('/profile')
|
if (Boolean(userResult.user)) redirect('/profile')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-svh flex-col items-center justify-center gap-6 bg-muted p-6 md:p-10">
|
<div className="flex min-h-svh flex-col items-center justify-center gap-6 bg-muted p-6 md:p-10 rounded-md">
|
||||||
<div className="flex w-full max-w-sm flex-col gap-6">
|
<div className="flex w-full max-w-sm flex-col gap-6">
|
||||||
<a
|
<a
|
||||||
href="https://beitzah.net?ref=midrashim"
|
href="https://beitzah.net?ref=midrashim"
|
||||||
@ -29,7 +29,7 @@ const LoginPage = async () => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="block leading-3.5">Developed with 💜</span>
|
<span className="block leading-3.5">Developed with 💜</span>
|
||||||
<span className="block leading-3.5">Beitzah.tech</span>
|
<span className="block leading-3.5">by Beitzah.tech</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<LoginForm />
|
<LoginForm />
|
||||||
|
|||||||
@ -49,6 +49,10 @@ const HoldRequests: CollectionConfig = {
|
|||||||
name: 'holdingUntilDate',
|
name: 'holdingUntilDate',
|
||||||
type: 'date',
|
type: 'date',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'isRejected',
|
||||||
|
type: 'checkbox',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'isCheckedOut',
|
name: 'isCheckedOut',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
|
|||||||
@ -105,28 +105,31 @@ export default function SiteNavigation(props: { children: React.ReactNode }) {
|
|||||||
<Avatar src={profilePicture?.url || ''} initials={initials} square={false} />
|
<Avatar src={profilePicture?.url || ''} initials={initials} square={false} />
|
||||||
</DropdownButton>
|
</DropdownButton>
|
||||||
<DropdownMenu className="min-w-64" anchor="bottom end">
|
<DropdownMenu className="min-w-64" anchor="bottom end">
|
||||||
|
{!user ? (
|
||||||
|
<DropdownItem href="/login">
|
||||||
|
<UserIcon />
|
||||||
|
<DropdownLabel>Login</DropdownLabel>
|
||||||
|
</DropdownItem>
|
||||||
|
) : (
|
||||||
<DropdownItem href="/profile">
|
<DropdownItem href="/profile">
|
||||||
<UserIcon />
|
<UserIcon />
|
||||||
<DropdownLabel>My profile</DropdownLabel>
|
<DropdownLabel>My profile</DropdownLabel>
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
{/*<DropdownItem href="/settings">
|
)}
|
||||||
<Cog8ToothIcon />
|
|
||||||
<DropdownLabel>Settings</DropdownLabel>
|
|
||||||
</DropdownItem>
|
|
||||||
<DropdownDivider />
|
|
||||||
<DropdownItem href="/privacy-policy">
|
|
||||||
<ShieldCheckIcon />
|
|
||||||
<DropdownLabel>Privacy policy</DropdownLabel>
|
|
||||||
</DropdownItem>*/}
|
|
||||||
<DropdownItem href="/feedback">
|
<DropdownItem href="/feedback">
|
||||||
<LightBulbIcon />
|
<LightBulbIcon />
|
||||||
<DropdownLabel>Share feedback</DropdownLabel>
|
<DropdownLabel>Share feedback</DropdownLabel>
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
|
||||||
|
{!!user && (
|
||||||
|
<>
|
||||||
<DropdownDivider />
|
<DropdownDivider />
|
||||||
<DropdownItem href="/logout">
|
<DropdownItem href="/logout">
|
||||||
<ArrowRightStartOnRectangleIcon />
|
<ArrowRightStartOnRectangleIcon />
|
||||||
<DropdownLabel>Sign out</DropdownLabel>
|
<DropdownLabel>Sign out</DropdownLabel>
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</NavbarSection>
|
</NavbarSection>
|
||||||
|
|||||||
@ -234,6 +234,7 @@ export interface HoldRequest {
|
|||||||
dateRequested?: string | null;
|
dateRequested?: string | null;
|
||||||
isHolding?: boolean | null;
|
isHolding?: boolean | null;
|
||||||
holdingUntilDate?: string | null;
|
holdingUntilDate?: string | null;
|
||||||
|
isRejected?: boolean | null;
|
||||||
isCheckedOut?: boolean | null;
|
isCheckedOut?: boolean | null;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@ -583,6 +584,7 @@ export interface HoldRequestsSelect<T extends boolean = true> {
|
|||||||
dateRequested?: T;
|
dateRequested?: T;
|
||||||
isHolding?: T;
|
isHolding?: T;
|
||||||
holdingUntilDate?: T;
|
holdingUntilDate?: T;
|
||||||
|
isRejected?: T;
|
||||||
isCheckedOut?: T;
|
isCheckedOut?: T;
|
||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user