feat: books page

This commit is contained in:
Yehoshua Sandler 2025-04-18 10:43:25 -05:00
parent 81229144cc
commit 2c1ae9962c
12 changed files with 204 additions and 61 deletions

View File

@ -1,8 +1,15 @@
# blank
blank
## Attributes ## Attributes
- **Database**: mongodb - **Database**: postgres
- **Storage Adapter**: localDisk - **Storage Adapter**: localDisk
## external resources
https://openlibrary.org/dev/docs/api
## maybe use
https://www.reddit.com/r/nextjs/comments/1ej1y32/share_cool_shadcnstyle_components_libraries_you/
https://motion-primitives.com/docs

27
package-lock.json generated
View File

@ -25,6 +25,7 @@
"framer-motion": "^12.7.4", "framer-motion": "^12.7.4",
"graphql": "^16.8.1", "graphql": "^16.8.1",
"lucide-react": "^0.488.0", "lucide-react": "^0.488.0",
"motion": "^12.7.4",
"next": "15.2.3", "next": "15.2.3",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"payload": "3.31.0", "payload": "3.31.0",
@ -10787,6 +10788,32 @@
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/motion": {
"version": "12.7.4",
"resolved": "https://registry.npmjs.org/motion/-/motion-12.7.4.tgz",
"integrity": "sha512-MBGrMbYageHw4iZJn+pGTr7abq5n53jCxYkhFC1It3vYukQPRWg5zij46MnwYGpLR8KG465MLHSASXot9edYOw==",
"license": "MIT",
"dependencies": {
"framer-motion": "^12.7.4",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/motion-dom": { "node_modules/motion-dom": {
"version": "12.7.4", "version": "12.7.4",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.7.4.tgz", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.7.4.tgz",

View File

@ -32,6 +32,7 @@
"framer-motion": "^12.7.4", "framer-motion": "^12.7.4",
"graphql": "^16.8.1", "graphql": "^16.8.1",
"lucide-react": "^0.488.0", "lucide-react": "^0.488.0",
"motion": "^12.7.4",
"next": "15.2.3", "next": "15.2.3",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"payload": "3.31.0", "payload": "3.31.0",

View File

View File

@ -0,0 +1,23 @@
'use client'
import BookList from '@/components/BookList'
import { Book } from '@/payload-types'
import { PaginatedDocs } from 'payload'
import { useMemo } from 'react'
type Props = {
initialBooks: PaginatedDocs<Book>
}
const BooksPageClient = (props: Props) => {
const initialBooks = useMemo(() => {
return props.initialBooks
}, [props.initialBooks])
return (
<div>
<BookList books={initialBooks} />
</div>
)
}
export default BooksPageClient

View File

@ -0,0 +1,44 @@
import { Book } from '@/payload-types'
import { getPayload, PaginatedDocs } from 'payload'
import configPromise from '@payload-config'
import BooksPageClient from './page.client'
type Params = Promise<{ slug: string }>
type SearchParams = Promise<{ [key: string]: string | undefined }>
type Props = {
params: Params
searchParams: SearchParams
}
const BooksPage = async (props: Props) => {
const searchParams = await props.searchParams
const defaultLimit = 25
const defaultPage = 1
const pageFromUrl = parseInt(searchParams?.page || '', 10)
const limitFromUrl = parseInt(searchParams?.limit || '', 10)
const payload = await getPayload({ config: configPromise })
const initialBooks = (await payload.find({
collection: 'books',
depth: 2,
page: !Number.isNaN(pageFromUrl) ? pageFromUrl : defaultPage,
limit: !Number.isNaN(limitFromUrl) ? limitFromUrl : defaultLimit,
overrideAccess: false,
select: {
title: true,
authors: true,
publication: true,
lcc: true,
genre: true,
isbn: true,
copies: true,
},
})) as PaginatedDocs<Book>
return <BooksPageClient initialBooks={initialBooks} />
}
export default BooksPage

View File

@ -1,5 +1,4 @@
import { headers as getHeaders } from 'next/headers.js' import { headers as getHeaders } from 'next/headers.js'
import Image from 'next/image'
import { getPayload, PaginatedDocs } from 'payload' import { getPayload, PaginatedDocs } from 'payload'
import React from 'react' import React from 'react'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
@ -8,6 +7,7 @@ import config from '@/payload.config'
import BookList from '@/components/BookList' import BookList from '@/components/BookList'
import { Book } from '@/payload-types' import { Book } from '@/payload-types'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { TextShimmer } from '@/components/ui/text-shimmer'
export default async function HomePage() { export default async function HomePage() {
const headers = await getHeaders() const headers = await getHeaders()
@ -33,19 +33,21 @@ export default async function HomePage() {
}, },
})) as PaginatedDocs<Book> })) as PaginatedDocs<Book>
//let safariaQuote = ''
//const randomSefariaQuoteRequest = await fetch('https://www.sefaria.org/api/texts/random?categories=english')
//if (randomSefariaQuoteRequest.body) safariaQuote = await randomSefariaQuoteRequest.json()
//console.log(safariaQuote)
return ( return (
<div className="home"> <div className="home">
<div className="py-24 sm:py-32"> <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-7xl px-6 lg:px-8">
<div className="mx-auto max-w-2xl lg:mx-0"> <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-indigo-600">Get the help you need</p>
<h2 className="mt-2 text-5xl font-semibold tracking-tight text-foreground sm:text-7xl"> <h2 className="mt-2 text-5xl font-semibold tracking-tight text-foreground sm:text-7xl">
Welcome {user && <small>{`user.firstName`}</small>} <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)]"
>
Welcome
</TextShimmer>
{user && <small>{`user.firstName`}</small>}
</h2> </h2>
<p className="mt-8 text-lg font-medium text-pretty text-muted-foreground sm:text-xl/8"> <p className="mt-8 text-lg font-medium text-pretty text-muted-foreground sm:text-xl/8">
Anim aute id magna aliqua ad ad non deserunt sunt. Qui irure qui lorem cupidatat Anim aute id magna aliqua ad ad non deserunt sunt. Qui irure qui lorem cupidatat

View File

@ -121,7 +121,6 @@
} }
} }
html[data-theme='dark'], html[data-theme='dark'],
html[data-theme='light'] { html[data-theme='light'] {
opacity: initial; opacity: initial;

View File

@ -47,17 +47,17 @@ export default function BookList(props: Props) {
const { docs, hasNextPage, hasPrevPage, limit, totalPages, page, prevPage, nextPage, totalDocs } = const { docs, hasNextPage, hasPrevPage, limit, totalPages, page, prevPage, nextPage, totalDocs } =
props.books props.books
console.log(props.books)
const currentPage = page || 0 const currentPage = page || 0
const books = docs const books = docs
console.log(props)
return ( return (
<section id="user-repositories"> <section id="user-repositories">
<ul role="list" className="divide-y divide-gray-800"> <ul role="list" className="divide-y divide-gray-800">
{books?.map((b) => ( {books?.map((b) => (
<li key={b.lcc + (b.title || '')}> <li key={b.lcc + (b.title || '')}>
<Link href={`/book/${b.id}`} className="flex justify-between gap-x-6 py-5"> <Link href={`/books/${b.id}`} className="flex justify-between gap-x-6 py-5">
<div className="flex max-w-9/12 min-w-0 gap-x-4"> <div className="flex max-w-9/12 min-w-0 gap-x-4">
<Avatar <Avatar
square square
@ -101,7 +101,7 @@ export default function BookList(props: Props) {
<Pagination> <Pagination>
<PaginationContent> <PaginationContent>
<PaginationItem> <PaginationItem>
<PaginationPrevious isActive={!hasPrevPage} href="#" /> <PaginationPrevious href={prevPage ? `/books?limit=${limit}&page=${prevPage}` : ''} />
</PaginationItem> </PaginationItem>
{hasPrevPage && currentPage > totalPages / 2 && ( {hasPrevPage && currentPage > totalPages / 2 && (
@ -112,7 +112,9 @@ export default function BookList(props: Props) {
{prevPage && ( {prevPage && (
<PaginationItem> <PaginationItem>
<PaginationLink href="#">{prevPage}</PaginationLink> <PaginationLink href={`/books?limit=${limit}&page=${prevPage}`}>
{prevPage}
</PaginationLink>
</PaginationItem> </PaginationItem>
)} )}
@ -124,7 +126,9 @@ export default function BookList(props: Props) {
{nextPage && ( {nextPage && (
<PaginationItem> <PaginationItem>
<PaginationLink href="#">{nextPage}</PaginationLink> <PaginationLink href={`/books?limit=${limit}&page=${nextPage}`}>
{nextPage}
</PaginationLink>
</PaginationItem> </PaginationItem>
)} )}
@ -135,7 +139,7 @@ export default function BookList(props: Props) {
)} )}
<PaginationItem> <PaginationItem>
<PaginationNext isActive={hasNextPage} href="#" /> <PaginationNext href={nextPage ? `/books?limit=${limit}&page=${nextPage}` : ''} />
</PaginationItem> </PaginationItem>
</PaginationContent> </PaginationContent>
</Pagination> </Pagination>
@ -143,7 +147,7 @@ export default function BookList(props: Props) {
<p className="flex justify-center mt-2 gap-2 mx-auto text-muted-foreground"> <p className="flex justify-center mt-2 gap-2 mx-auto text-muted-foreground">
<span>viewing</span> <span>viewing</span>
<span className="text-foreground"> <span className="text-foreground">
{currentPage * limit}-{currentPage * limit + (limit - 1)} {currentPage * limit - limit + 1}-{currentPage * limit}
</span> </span>
<span>of</span> <span>of</span>
<span className="text-foreground">{totalDocs}</span> <span className="text-foreground">{totalDocs}</span>

View File

@ -1,79 +1,64 @@
import * as React from "react" import * as jeact from 'react'
import { import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from 'lucide-react'
ChevronLeftIcon,
ChevronRightIcon,
MoreHorizontalIcon,
} from "lucide-react"
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils'
import { Button, buttonVariants } from "@/components/ui/button" import { Button, buttonVariants } from '@/components/ui/button'
function Pagination({ className, ...props }: React.ComponentProps<"nav">) { function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
return ( return (
<nav <nav
role="navigation" role="navigation"
aria-label="pagination" aria-label="pagination"
data-slot="pagination" data-slot="pagination"
className={cn("mx-auto flex w-full justify-center", className)} className={cn('mx-auto flex w-full justify-center', className)}
{...props} {...props}
/> />
) )
} }
function PaginationContent({ function PaginationContent({ className, ...props }: React.ComponentProps<'ul'>) {
className,
...props
}: React.ComponentProps<"ul">) {
return ( return (
<ul <ul
data-slot="pagination-content" data-slot="pagination-content"
className={cn("flex flex-row items-center gap-1", className)} className={cn('flex flex-row items-center gap-1', className)}
{...props} {...props}
/> />
) )
} }
function PaginationItem({ ...props }: React.ComponentProps<"li">) { function PaginationItem({ ...props }: React.ComponentProps<'li'>) {
return <li data-slot="pagination-item" {...props} /> return <li data-slot="pagination-item" {...props} />
} }
type PaginationLinkProps = { type PaginationLinkProps = {
isActive?: boolean isActive?: boolean
} & Pick<React.ComponentProps<typeof Button>, "size"> & } & Pick<React.ComponentProps<typeof Button>, 'size'> &
React.ComponentProps<"a"> React.ComponentProps<'a'>
function PaginationLink({ function PaginationLink({ className, isActive, size = 'icon', ...props }: PaginationLinkProps) {
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) {
return ( return (
<a <a
aria-current={isActive ? "page" : undefined} aria-current={isActive ? 'page' : undefined}
data-slot="pagination-link" data-slot="pagination-link"
data-active={isActive} data-active={isActive}
className={cn( className={cn(
buttonVariants({ buttonVariants({
variant: isActive ? "outline" : "ghost", variant: isActive ? 'outline' : 'ghost',
size, size,
}), }),
className className,
)} )}
{...props} {...props}
/> />
) )
} }
function PaginationPrevious({ function PaginationPrevious({ className, ...props }: React.ComponentProps<typeof PaginationLink>) {
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return ( return (
<PaginationLink <PaginationLink
aria-label="Go to previous page" aria-label="Go to previous page"
size="default" size="default"
className={cn("gap-1 px-2.5 sm:pl-2.5", className)} className={cn('gap-1 px-2.5 sm:pl-2.5', className)}
{...props} {...props}
> >
<ChevronLeftIcon /> <ChevronLeftIcon />
@ -82,15 +67,12 @@ function PaginationPrevious({
) )
} }
function PaginationNext({ function PaginationNext({ className, ...props }: React.ComponentProps<typeof PaginationLink>) {
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return ( return (
<PaginationLink <PaginationLink
aria-label="Go to next page" aria-label="Go to next page"
size="default" size="default"
className={cn("gap-1 px-2.5 sm:pr-2.5", className)} className={cn('gap-1 px-2.5 sm:pr-2.5', className)}
{...props} {...props}
> >
<span className="hidden sm:block">Next</span> <span className="hidden sm:block">Next</span>
@ -99,15 +81,12 @@ function PaginationNext({
) )
} }
function PaginationEllipsis({ function PaginationEllipsis({ className, ...props }: React.ComponentProps<'span'>) {
className,
...props
}: React.ComponentProps<"span">) {
return ( return (
<span <span
aria-hidden aria-hidden
data-slot="pagination-ellipsis" data-slot="pagination-ellipsis"
className={cn("flex size-9 items-center justify-center", className)} className={cn('flex size-9 items-center justify-center', className)}
{...props} {...props}
> >
<MoreHorizontalIcon className="size-4" /> <MoreHorizontalIcon className="size-4" />

View File

@ -0,0 +1,57 @@
'use client';
import React, { useMemo, type JSX } from 'react';
import { motion } from 'motion/react';
import { cn } from '@/lib/utils';
export type TextShimmerProps = {
children: string;
as?: React.ElementType;
className?: string;
duration?: number;
spread?: number;
};
function TextShimmerComponent({
children,
as: Component = 'p',
className,
duration = 2,
spread = 2,
}: TextShimmerProps) {
const MotionComponent = motion.create(
Component as keyof JSX.IntrinsicElements
);
const dynamicSpread = useMemo(() => {
return children.length * spread;
}, [children, spread]);
return (
<MotionComponent
className={cn(
'relative inline-block bg-[length:250%_100%,auto] bg-clip-text',
'text-transparent [--base-color:#a1a1aa] [--base-gradient-color:#000]',
'[background-repeat:no-repeat,padding-box] [--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--base-gradient-color),#0000_calc(50%+var(--spread)))]',
'dark:[--base-color:#71717a] dark:[--base-gradient-color:#ffffff] dark:[--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--base-gradient-color),#0000_calc(50%+var(--spread)))]',
className
)}
initial={{ backgroundPosition: '100% center' }}
animate={{ backgroundPosition: '0% center' }}
transition={{
repeat: Infinity,
duration,
ease: 'linear',
}}
style={
{
'--spread': `${dynamicSpread}px`,
backgroundImage: `var(--bg), linear-gradient(var(--base-color), var(--base-color))`,
} as React.CSSProperties
}
>
{children}
</MotionComponent>
);
}
export const TextShimmer = React.memo(TextShimmerComponent);