Skip to content

Commit 9a1a777

Browse files
committed
feat: server rendered collection books & book select
1 parent ec062f6 commit 9a1a777

File tree

4 files changed

+267
-338
lines changed

4 files changed

+267
-338
lines changed

app/(protected)/admin/collections/[id]/manage-books/page.tsx

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { FormManageCollectionBooks } from '@/components/collections/FormManageCollectionBooks'
2+
import { SearchInput } from '@/components/common/SearchInput'
23
import {
34
Breadcrumb,
45
BreadcrumbItem,
@@ -8,19 +9,31 @@ import {
89
} from '@/components/ui/breadcrumb'
910
import { Card } from '@/components/ui/card'
1011
import { updateCollectionBooksAction } from '@/lib/actions/collection'
12+
import { getListBooks } from '@/lib/api/book'
1113
import { getCollection, getListCollectionBooks } from '@/lib/api/collection'
1214
import { Book } from 'lucide-react'
15+
import {
16+
Pagination,
17+
PaginationContent,
18+
PaginationItem,
19+
PaginationNext,
20+
PaginationPrevious,
21+
} from '@/components/ui/pagination'
22+
import { Route } from 'next'
1323

1424
export default async function CollectionManageBooksPage({
1525
params,
26+
searchParams,
1627
}: {
1728
params: Promise<{ id: string }>
29+
searchParams: Promise<{ skip?: number; limit?: number; title?: string }>
1830
}) {
19-
// const claims = await IsLoggedIn()
20-
2131
const { id } = await params
32+
const sp = await searchParams
33+
const skip = Number(sp?.skip ?? 0)
34+
const limit = Number(sp?.limit ?? 20)
2235

23-
const [collectionRes, bookRes] = await Promise.all([
36+
const [collectionRes, colBookRes] = await Promise.all([
2437
getCollection(id, { include_book_ids: 'true' }),
2538
getListCollectionBooks(id, {
2639
include_book: 'true',
@@ -32,11 +45,32 @@ export default async function CollectionManageBooksPage({
3245
return <div>{JSON.stringify(collectionRes.message)}</div>
3346
}
3447

48+
if ('error' in colBookRes) {
49+
console.log({ bookRes: colBookRes })
50+
return <div>{JSON.stringify(colBookRes.message)}</div>
51+
}
52+
53+
const bookRes = await getListBooks({
54+
sort_by: 'created_at',
55+
sort_in: 'desc',
56+
limit: limit,
57+
skip: skip,
58+
title: sp?.title,
59+
library_id: collectionRes.data.library_id,
60+
})
61+
3562
if ('error' in bookRes) {
36-
console.log({ bookRes })
63+
console.log(bookRes)
3764
return <div>{JSON.stringify(bookRes.message)}</div>
3865
}
3966

67+
const prevSkip = skip - limit > 0 ? skip - limit : 0
68+
69+
const nextURL =
70+
`/admin/collections/${id}/manage-books?skip=${skip + limit}&limit=${limit}` as Route
71+
const prevURL =
72+
`/admin/collections/${id}/manage-books?skip=${prevSkip}&limit=${limit}` as Route
73+
4074
return (
4175
<div className="space-y-4">
4276
<h1 className="text-2xl font-semibold">{collectionRes.data.title}</h1>
@@ -61,13 +95,19 @@ export default async function CollectionManageBooksPage({
6195
</BreadcrumbList>
6296
</Breadcrumb>
6397

98+
<SearchInput
99+
className="max-w-md sticky top-4"
100+
placeholder="Search by title"
101+
name="title"
102+
/>
103+
64104
{/* Books in Collection */}
65105
<div className="lg:col-span-3">
66106
<FormManageCollectionBooks
67-
initialBooks={bookRes.data.map((b) => b.book!)}
68-
libraryID={collectionRes.data.library_id}
107+
initialBooks={colBookRes.data.map((b) => b.book!)}
69108
initialBookIDs={collectionRes.data.book_ids}
70109
onSubmitAction={updateCollectionBooksAction.bind(null, id)}
110+
books={bookRes.data}
71111
/>
72112
</div>
73113

@@ -82,6 +122,20 @@ export default async function CollectionManageBooksPage({
82122
</p>
83123
</Card>
84124
)}
125+
<Pagination>
126+
<PaginationContent>
127+
{bookRes.meta.skip > 0 && (
128+
<PaginationItem>
129+
<PaginationPrevious href={prevURL} />
130+
</PaginationItem>
131+
)}
132+
{bookRes.meta.limit <= bookRes.data.length && (
133+
<PaginationItem>
134+
<PaginationNext href={nextURL} />
135+
</PaginationItem>
136+
)}
137+
</PaginationContent>
138+
</Pagination>
85139
</div>
86140
)
87141
}
Lines changed: 81 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,97 @@
1+
'use client'
2+
13
import { Book } from '@/lib/types/book'
4+
import { memo, ViewTransition } from 'react'
25
import {
36
Card,
47
CardContent,
58
CardDescription,
69
CardHeader,
710
CardTitle,
8-
} from '@/components/ui/card'
11+
} from '../ui/card'
912
import Image from 'next/image'
10-
import { Check } from 'lucide-react'
11-
import { Fragment } from 'react/jsx-runtime'
13+
import clsx from 'clsx'
1214

13-
interface BookSelectCardProps {
15+
export const SelectedBook = memo<{
1416
book: Book
15-
isSelected: boolean
16-
onToggle: (book: Book) => void
17-
ImageWrapper?: React.FC<React.PropsWithChildren>
18-
}
19-
20-
export const BookSelectCard: React.FC<BookSelectCardProps> = ({
21-
book,
22-
isSelected,
23-
onToggle,
24-
ImageWrapper = Fragment,
25-
}) => {
17+
onDeselect?: (bookID: string) => void
18+
disabled?: boolean
19+
}>(({ book, onDeselect, disabled }) => {
2620
return (
27-
<Card
28-
className={`cursor-pointer transition-all duration-200 hover:shadow-md ${
29-
isSelected ? 'ring-1 ring-primary bg-primary/5' : ''
30-
}`}
31-
onClick={() => onToggle(book)}
21+
<div
22+
onClick={() => (disabled ? null : onDeselect?.(book.id))}
23+
className={clsx(
24+
'shrink-0 relative left-0 transition-all not-first-of-type:-ml-12',
25+
'hover:transition-all hover:-translate-y-4 hover:transform-none',
26+
'peer peer-hover:left-12 peer-hover:transition-all',
27+
'[transform:perspective(800px)_rotateY(20deg)]',
28+
!disabled && 'hover:cursor-pointer',
29+
'group'
30+
)}
3231
>
33-
<CardHeader className="pb-3">
34-
<div className="relative mx-auto mb-4 flex justify-center">
35-
<ImageWrapper
36-
children={
37-
<Image
38-
src={book.cover ?? '/book-placeholder.svg'}
39-
alt={`${book.title} cover`}
40-
width={100}
41-
height={150}
42-
/>
43-
}
32+
<ViewTransition name={book.id}>
33+
<div className="relative">
34+
<Image
35+
src={book?.cover ?? '/book-placeholder.svg'}
36+
alt={book.title + "'s cover"}
37+
width={128}
38+
height={192}
39+
className="shadow-md rounded-lg w-32 h-48 place-self-center object-cover"
4440
/>
45-
{isSelected && (
46-
<div className="absolute -top-2 -right-2 w-6 h-6 bg-primary rounded-full flex items-center justify-center">
47-
<Check className="h-3 w-3 text-white" />
48-
</div>
49-
)}
50-
</div>
51-
<CardTitle className="text-base line-clamp-2">{book.title}</CardTitle>
52-
<CardDescription className="line-clamp-1">
53-
{book.author}
54-
</CardDescription>
55-
</CardHeader>
56-
<CardContent className="pt-0">
57-
<div className="space-y-2">
58-
<div className="flex items-center gap-2 text-sm text-muted-foreground">
59-
<span>{book.year}</span>
60-
<span></span>
61-
<span>{book.code}</span>
41+
<div className="absolute inset-0 bg-black/70 opacity-0 group-hover:opacity-100 transition-opacity rounded-lg flex flex-col justify-end p-2">
42+
<p className="text-white text-xs font-semibold line-clamp-2 mb-1">
43+
{book.title}
44+
</p>
45+
<p className="text-white/90 text-xs font-mono">{book.code}</p>
6246
</div>
6347
</div>
64-
</CardContent>
65-
</Card>
48+
</ViewTransition>
49+
</div>
50+
)
51+
})
52+
53+
SelectedBook.displayName = 'SelectedBook'
54+
55+
// Memoize the card component to prevent unnecessary re-renders
56+
export const UnselectedBook = memo<{
57+
book: Book
58+
onSelect?: (bookID: string) => void
59+
disabled?: boolean
60+
}>(({ book, onSelect, disabled }) => {
61+
return (
62+
<ViewTransition name={'card-' + book.id}>
63+
<Card
64+
className={clsx(
65+
'cursor-pointer transition-all duration-200 hover:shadow-md',
66+
disabled && 'opacity-50 cursor-not-allowed'
67+
)}
68+
onClick={disabled ? undefined : () => onSelect?.(book.id)}
69+
>
70+
<CardHeader className="pb-3 text-center">
71+
<ViewTransition name={book.id}>
72+
<Image
73+
src={book.cover ?? '/book-placeholder.svg'}
74+
alt={`${book.title} cover`}
75+
width={96}
76+
height={144}
77+
className="mx-auto w-24 h-[9rem] rounded-lg"
78+
/>
79+
</ViewTransition>
80+
<CardTitle className="text-base line-clamp-2">{book.title}</CardTitle>
81+
<CardDescription className="line-clamp-1">
82+
{book.author}
83+
</CardDescription>
84+
</CardHeader>
85+
<CardContent className="flex items-center justify-center gap-2 text-sm text-muted-foreground">
86+
<span>{book.year}</span>
87+
<span></span>
88+
<span>{book.code}</span>
89+
</CardContent>
90+
</Card>
91+
</ViewTransition>
6692
)
67-
}
93+
})
94+
95+
UnselectedBook.displayName = 'UnselectedBook'
96+
97+
export const BookSelectCard = UnselectedBook

0 commit comments

Comments
 (0)