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
- **Database**: mongodb
- **Database**: postgres
- **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",
"graphql": "^16.8.1",
"lucide-react": "^0.488.0",
"motion": "^12.7.4",
"next": "15.2.3",
"next-themes": "^0.4.6",
"payload": "3.31.0",
@ -10787,6 +10788,32 @@
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"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": {
"version": "12.7.4",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.7.4.tgz",

View File

@ -32,6 +32,7 @@
"framer-motion": "^12.7.4",
"graphql": "^16.8.1",
"lucide-react": "^0.488.0",
"motion": "^12.7.4",
"next": "15.2.3",
"next-themes": "^0.4.6",
"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 Image from 'next/image'
import { getPayload, PaginatedDocs } from 'payload'
import React from 'react'
import { fileURLToPath } from 'url'
@ -8,6 +7,7 @@ import config from '@/payload.config'
import BookList from '@/components/BookList'
import { Book } from '@/payload-types'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { TextShimmer } from '@/components/ui/text-shimmer'
export default async function HomePage() {
const headers = await getHeaders()
@ -33,19 +33,21 @@ export default async function HomePage() {
},
})) 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 (
<div className="home">
<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>
<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>
<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

View File

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

View File

@ -47,17 +47,17 @@ export default function BookList(props: Props) {
const { docs, hasNextPage, hasPrevPage, limit, totalPages, page, prevPage, nextPage, totalDocs } =
props.books
console.log(props.books)
const currentPage = page || 0
const books = docs
console.log(props)
return (
<section id="user-repositories">
<ul role="list" className="divide-y divide-gray-800">
{books?.map((b) => (
<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">
<Avatar
square
@ -101,7 +101,7 @@ export default function BookList(props: Props) {
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious isActive={!hasPrevPage} href="#" />
<PaginationPrevious href={prevPage ? `/books?limit=${limit}&page=${prevPage}` : ''} />
</PaginationItem>
{hasPrevPage && currentPage > totalPages / 2 && (
@ -112,7 +112,9 @@ export default function BookList(props: Props) {
{prevPage && (
<PaginationItem>
<PaginationLink href="#">{prevPage}</PaginationLink>
<PaginationLink href={`/books?limit=${limit}&page=${prevPage}`}>
{prevPage}
</PaginationLink>
</PaginationItem>
)}
@ -124,7 +126,9 @@ export default function BookList(props: Props) {
{nextPage && (
<PaginationItem>
<PaginationLink href="#">{nextPage}</PaginationLink>
<PaginationLink href={`/books?limit=${limit}&page=${nextPage}`}>
{nextPage}
</PaginationLink>
</PaginationItem>
)}
@ -135,7 +139,7 @@ export default function BookList(props: Props) {
)}
<PaginationItem>
<PaginationNext isActive={hasNextPage} href="#" />
<PaginationNext href={nextPage ? `/books?limit=${limit}&page=${nextPage}` : ''} />
</PaginationItem>
</PaginationContent>
</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">
<span>viewing</span>
<span className="text-foreground">
{currentPage * limit}-{currentPage * limit + (limit - 1)}
{currentPage * limit - limit + 1}-{currentPage * limit}
</span>
<span>of</span>
<span className="text-foreground">{totalDocs}</span>

View File

@ -1,79 +1,64 @@
import * as React from "react"
import {
ChevronLeftIcon,
ChevronRightIcon,
MoreHorizontalIcon,
} from "lucide-react"
import * as jeact from 'react'
import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from 'lucide-react'
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
import { cn } from '@/lib/utils'
import { Button, buttonVariants } from '@/components/ui/button'
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
return (
<nav
role="navigation"
aria-label="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}
/>
)
}
function PaginationContent({
className,
...props
}: React.ComponentProps<"ul">) {
function PaginationContent({ className, ...props }: React.ComponentProps<'ul'>) {
return (
<ul
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}
/>
)
}
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
function PaginationItem({ ...props }: React.ComponentProps<'li'>) {
return <li data-slot="pagination-item" {...props} />
}
type PaginationLinkProps = {
isActive?: boolean
} & Pick<React.ComponentProps<typeof Button>, "size"> &
React.ComponentProps<"a">
} & Pick<React.ComponentProps<typeof Button>, 'size'> &
React.ComponentProps<'a'>
function PaginationLink({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) {
function PaginationLink({ className, isActive, size = 'icon', ...props }: PaginationLinkProps) {
return (
<a
aria-current={isActive ? "page" : undefined}
aria-current={isActive ? 'page' : undefined}
data-slot="pagination-link"
data-active={isActive}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
variant: isActive ? 'outline' : 'ghost',
size,
}),
className
className,
)}
{...props}
/>
)
}
function PaginationPrevious({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
function PaginationPrevious({ className, ...props }: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to previous page"
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}
>
<ChevronLeftIcon />
@ -82,15 +67,12 @@ function PaginationPrevious({
)
}
function PaginationNext({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
function PaginationNext({ className, ...props }: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to next page"
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}
>
<span className="hidden sm:block">Next</span>
@ -99,15 +81,12 @@ function PaginationNext({
)
}
function PaginationEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
function PaginationEllipsis({ className, ...props }: React.ComponentProps<'span'>) {
return (
<span
aria-hidden
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}
>
<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);