feat: ui for book item
This commit is contained in:
parent
2c1ae9962c
commit
54d9794d41
122
src/app/(frontend)/books/[bookId]/page.client.tsx
Normal file
122
src/app/(frontend)/books/[bookId]/page.client.tsx
Normal file
@ -0,0 +1,122 @@
|
||||
'use client'
|
||||
|
||||
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 { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { RichText } from '@/components/RichText'
|
||||
|
||||
type DropDownProps = {
|
||||
currentRepository: Repository
|
||||
repositories: Repository[]
|
||||
}
|
||||
function RepoDropdown({ currentRepository, repositories }: DropDownProps) {
|
||||
return (
|
||||
<Field className="">
|
||||
<Label htmlFor="repositories">From Repository</Label>
|
||||
<div className="flex gap-2">
|
||||
<Combobox
|
||||
name="repositories"
|
||||
options={repositories}
|
||||
displayValue={(repo) => repo?.name}
|
||||
defaultValue={currentRepository}
|
||||
className="w"
|
||||
>
|
||||
{(repo) => (
|
||||
<ComboboxOption value={repo}>
|
||||
<ComboboxLabel>{repo.abbreviation}</ComboboxLabel>
|
||||
<ComboboxDescription>{repo.name}</ComboboxDescription>
|
||||
</ComboboxOption>
|
||||
)}
|
||||
</Combobox>
|
||||
<Button className="hover:scale-105 bg-emerald-500 text-foreground hover:text-black">
|
||||
<Loader2 className="animate-spin" />
|
||||
<span>Request Copy</span>
|
||||
</Button>
|
||||
</div>
|
||||
</Field>
|
||||
)
|
||||
}
|
||||
|
||||
type Props = {
|
||||
book: Book
|
||||
repositories: PaginatedDocs<Repository>
|
||||
}
|
||||
|
||||
export default function BookByIdPageClient(props: Props) {
|
||||
const { book, repositories } = props
|
||||
|
||||
const repos = repositories.docs
|
||||
|
||||
return (
|
||||
<div className="mx-auto px-4 py-16">
|
||||
{/* Book */}
|
||||
<div className="md:grid md:grid-cols-7 md:gap-8">
|
||||
{/* Book Cover */}
|
||||
<div className="md:col-span-3">
|
||||
<img
|
||||
alt="book cover art"
|
||||
src={
|
||||
book.isbn
|
||||
? `https://covers.openlibrary.org/b/isbn/${book.isbn}-L.jpg`
|
||||
: '/images/book-48.svg'
|
||||
}
|
||||
className="w-full rounded-lg bg-gray-100 object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Book details */}
|
||||
<div className="mt-14 sm:mt-16 md:col-span-4 md:mt-0">
|
||||
<div className="">
|
||||
<div className="mt-4">
|
||||
<h1 className="text-2xl font-bold tracking-tight text-foreground sm:text-3xl">
|
||||
{book.title}
|
||||
</h1>
|
||||
|
||||
<h2 id="information-heading" className="sr-only">
|
||||
Book information
|
||||
</h2>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
{book.publication && (
|
||||
<span>
|
||||
Published:
|
||||
<time dateTime={book.publication}>{book.publication}</time>
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="my-6 text-accent-foreground">{book.summary}</p>
|
||||
|
||||
<RepoDropdown currentRepository={repos[0]} repositories={repos} />
|
||||
|
||||
<div className="mt-10 border-t border-gray-200 pt-10 ">
|
||||
<h3 className="text-sm font-medium text-foreground">Genres</h3>
|
||||
<div className="mt-4">
|
||||
<ul role="list" className="text-foreground flex gap-2 flex-wrap">
|
||||
{(book.genre as Genre[])?.map((g) => (
|
||||
<Badge key={g.id} className="inline-block cursor-pointer">
|
||||
{g.name}
|
||||
</Badge>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{book.description && (
|
||||
<div className="mt-10 border-t border-gray-200 pt-10">
|
||||
<h3 className="text-sm font-medium text-muted-foreground">Description</h3>
|
||||
<RichText data={book.description} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
70
src/app/(frontend)/books/[bookId]/page.tsx
Normal file
70
src/app/(frontend)/books/[bookId]/page.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import { Book, Copy, Repository } from '@/payload-types'
|
||||
import { getPayload, PaginatedDocs } from 'payload'
|
||||
import configPromise from '@payload-config'
|
||||
import BookByIdPageClient from './page.client'
|
||||
|
||||
type Params = Promise<{ bookId: string }>
|
||||
type SearchParams = Promise<{ [key: string]: string | undefined }>
|
||||
|
||||
type Props = {
|
||||
params: Params
|
||||
searchParams: SearchParams
|
||||
}
|
||||
const BookByIdPage = async (props: Props) => {
|
||||
const params = await props.params
|
||||
|
||||
const bookIdFromUrl = parseInt(params?.bookId || '', 10)
|
||||
if (Number.isNaN(bookIdFromUrl)) return null
|
||||
const bookId = bookIdFromUrl as number
|
||||
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
|
||||
const defaultRepositoryLimit = 10
|
||||
|
||||
const foundBook = (await payload.findByID({
|
||||
collection: 'books',
|
||||
depth: 3,
|
||||
overrideAccess: false,
|
||||
id: bookId,
|
||||
select: {
|
||||
title: true,
|
||||
authors: true,
|
||||
publication: true,
|
||||
lcc: true,
|
||||
genre: true,
|
||||
isbn: true,
|
||||
copies: true,
|
||||
summary: true,
|
||||
description: true,
|
||||
},
|
||||
})) as Book
|
||||
|
||||
const repositoryIds = foundBook.copies?.docs?.map((c) => (c as Copy).repository)
|
||||
|
||||
const orQueries = repositoryIds?.map((c) => {
|
||||
return {
|
||||
id: {
|
||||
equals: c,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const repositories = (await payload.find({
|
||||
collection: 'repositories',
|
||||
depth: 3,
|
||||
limit: defaultRepositoryLimit,
|
||||
select: {
|
||||
id: true,
|
||||
owners: true,
|
||||
abbreviation: true,
|
||||
name: true,
|
||||
},
|
||||
where: {
|
||||
or: orQueries?.length ? orQueries : [],
|
||||
},
|
||||
})) as PaginatedDocs<Repository>
|
||||
|
||||
return <BookByIdPageClient book={foundBook} repositories={repositories} />
|
||||
}
|
||||
|
||||
export default BookByIdPage
|
@ -50,8 +50,6 @@ export default function BookList(props: Props) {
|
||||
const currentPage = page || 0
|
||||
const books = docs
|
||||
|
||||
console.log(props)
|
||||
|
||||
return (
|
||||
<section id="user-repositories">
|
||||
<ul role="list" className="divide-y divide-gray-800">
|
||||
|
12
src/components/RichText/index.tsx
Normal file
12
src/components/RichText/index.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { DefaultTypedEditorState } from '@payloadcms/richtext-lexical'
|
||||
import { RichText as RichTextConverter } from '@payloadcms/richtext-lexical/react'
|
||||
|
||||
type Props = {
|
||||
data: DefaultTypedEditorState
|
||||
} & React.HTMLAttributes<HTMLDivElement>
|
||||
|
||||
export const RichText = (props: Props) => {
|
||||
const { className, ...rest } = props
|
||||
|
||||
return <RichTextConverter {...rest} className={className} />
|
||||
}
|
@ -20,7 +20,11 @@ function CloseMenuIcon() {
|
||||
)
|
||||
}
|
||||
|
||||
function MobileSidebar({ open, close, children }: React.PropsWithChildren<{ open: boolean; close: () => void }>) {
|
||||
function MobileSidebar({
|
||||
open,
|
||||
close,
|
||||
children,
|
||||
}: React.PropsWithChildren<{ open: boolean; close: () => void }>) {
|
||||
return (
|
||||
<Headless.Dialog open={open} onClose={close} className="lg:hidden">
|
||||
<Headless.DialogBackdrop
|
||||
|
Loading…
x
Reference in New Issue
Block a user