feat: books page
This commit is contained in:
parent
81229144cc
commit
2c1ae9962c
13
README.md
13
README.md
@ -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
27
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
0
src/app/(frontend)/[slug]/page.client.tsx
Normal file
0
src/app/(frontend)/[slug]/page.client.tsx
Normal file
0
src/app/(frontend)/[slug]/page.tsx
Normal file
0
src/app/(frontend)/[slug]/page.tsx
Normal file
23
src/app/(frontend)/books/page.client.tsx
Normal file
23
src/app/(frontend)/books/page.client.tsx
Normal 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
|
44
src/app/(frontend)/books/page.tsx
Normal file
44
src/app/(frontend)/books/page.tsx
Normal 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
|
@ -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
|
||||
|
@ -121,7 +121,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
html[data-theme='dark'],
|
||||
html[data-theme='light'] {
|
||||
opacity: initial;
|
||||
|
@ -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>
|
||||
|
@ -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" />
|
||||
|
57
src/components/ui/text-shimmer.tsx
Normal file
57
src/components/ui/text-shimmer.tsx
Normal 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);
|
Loading…
x
Reference in New Issue
Block a user