Skip to content

Commit 29546e2

Browse files
committed
feat: book manage page
1 parent 01684d1 commit 29546e2

File tree

13 files changed

+813
-89
lines changed

13 files changed

+813
-89
lines changed
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import {
2+
Breadcrumb,
3+
BreadcrumbItem,
4+
BreadcrumbLink,
5+
BreadcrumbList,
6+
BreadcrumbPage,
7+
BreadcrumbSeparator,
8+
} from '@/components/ui/breadcrumb'
9+
import {
10+
Pagination,
11+
PaginationContent,
12+
PaginationItem,
13+
PaginationNext,
14+
PaginationPrevious,
15+
} from '@/components/ui/pagination'
16+
import { getListBooks } from '@/lib/api/book'
17+
import type { Metadata } from 'next'
18+
import { SITE_NAME } from '@/lib/consts'
19+
import { Badge } from '@/components/ui/badge'
20+
import { cookies } from 'next/headers'
21+
import { Verify } from '@/lib/firebase/firebase'
22+
import { BookSelectPanel } from '@/components/books/book-select-panel'
23+
import { SearchInput } from '@/components/common/SearchInput'
24+
import { Button } from '@/components/ui/button'
25+
import Link from 'next/link'
26+
import { QrCode } from 'lucide-react'
27+
28+
export const metadata: Metadata = {
29+
title: `Books · ${SITE_NAME}`,
30+
}
31+
32+
export default async function BooksSelectPage({
33+
searchParams,
34+
}: {
35+
searchParams: Promise<{
36+
skip?: number
37+
limit?: number
38+
title?: string
39+
}>
40+
}) {
41+
const sp = await searchParams
42+
const skip = Number(sp?.skip ?? 0)
43+
const limit = Number(sp?.limit ?? 20)
44+
45+
await Verify({ from: '/admin/books/manage' })
46+
47+
const cookieStore = await cookies()
48+
const cookieName = process.env.LIBRARY_COOKIE_NAME as string
49+
const libID = cookieStore.get(cookieName)?.value
50+
51+
const res = await getListBooks({
52+
sort_by: 'created_at',
53+
sort_in: 'desc',
54+
limit: limit,
55+
skip: skip,
56+
title: sp?.title,
57+
library_id: libID,
58+
})
59+
60+
if ('error' in res) {
61+
console.log(res)
62+
return <div>{JSON.stringify(res.message)}</div>
63+
}
64+
65+
const prevSkip = skip - limit > 0 ? skip - limit : 0
66+
67+
const nextURL =
68+
`/admin/books/manage?skip=${skip + limit}&limit=${limit}` as const
69+
const prevURL = `/admin/books/manage?skip=${prevSkip}&limit=${limit}` as const
70+
71+
return (
72+
<div className="space-y-4">
73+
<nav className="backdrop-blur-sm sticky top-0 z-10">
74+
<h1 className="text-2xl font-semibold">Select Books</h1>
75+
<Breadcrumb>
76+
<BreadcrumbList>
77+
<BreadcrumbItem>
78+
<BreadcrumbLink href="/admin">Home</BreadcrumbLink>
79+
</BreadcrumbItem>
80+
<BreadcrumbSeparator />
81+
<BreadcrumbItem>
82+
<BreadcrumbLink href="/admin/books">Books</BreadcrumbLink>
83+
</BreadcrumbItem>
84+
<BreadcrumbSeparator />
85+
<BreadcrumbItem>
86+
<BreadcrumbPage>
87+
Manage Books{' '}
88+
<Badge className="ml-4" variant="outline">
89+
{res.meta.total}
90+
</Badge>
91+
</BreadcrumbPage>
92+
</BreadcrumbItem>
93+
</BreadcrumbList>
94+
</Breadcrumb>
95+
</nav>
96+
97+
<div className="flex justify-between gap-4">
98+
<SearchInput
99+
className="max-w-md"
100+
placeholder="Search by title"
101+
name="title"
102+
/>
103+
<Button variant="outline" asChild>
104+
<Link href="/admin/books/print-qr">
105+
<QrCode className="mr-2 h-4 w-4" />
106+
Print QR Codes
107+
</Link>
108+
</Button>
109+
</div>
110+
111+
<BookSelectPanel books={res.data} />
112+
113+
<Pagination>
114+
<PaginationContent>
115+
{res.meta.skip > 0 && (
116+
<PaginationItem>
117+
<PaginationPrevious href={prevURL} />
118+
</PaginationItem>
119+
)}
120+
{res.meta.limit <= res.data.length && (
121+
<PaginationItem>
122+
<PaginationNext href={nextURL} />
123+
</PaginationItem>
124+
)}
125+
</PaginationContent>
126+
</Pagination>
127+
</div>
128+
)
129+
}

app/(protected)/admin/books/page.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { getListBooks } from '@/lib/api/book'
1818
import Link from 'next/link'
1919
import type { Metadata } from 'next'
2020
import { SITE_NAME } from '@/lib/consts'
21-
import { Plus, Search, Upload } from 'lucide-react'
21+
import { Plus, Search, Settings, Upload } from 'lucide-react'
2222
import { DebouncedInput } from '@/components/common/DebouncedInput'
2323
import { Badge } from '@/components/ui/badge'
2424
import { ListBook } from '@/components/books/ListBook'
@@ -97,6 +97,12 @@ export default async function Books({
9797
Import Books
9898
</Link>
9999
</Button>
100+
<Button variant="outline" asChild>
101+
<Link href="/admin/books/manage">
102+
<Settings className="mr-2 h-4 w-4" />
103+
Manage Books
104+
</Link>
105+
</Button>
100106
<Button asChild>
101107
<Link href="/admin/books/new">
102108
<Plus className="mr-2 h-4 w-4" />

components/books/ListBook.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ export const ListBook: React.FC<{ book: Book }> = ({ book }) => {
2222
)}
2323
>
2424
<CardHeader className="pb-3">
25-
<ViewTransition name={book.id}>
26-
<div className="grid place-items-center">
27-
{/* 3D Book Effect */}
28-
<div className="flex my-12">
29-
<div className="bg-accent [transform:perspective(400px)_rotateY(314deg)] -mr-1 w-4">
30-
<span className="inline-block text-nowrap text-[0.5rem] font-bold text-accent-foreground/50 [transform:rotate(90deg)_translateY(-16px)] origin-top-left"></span>
31-
</div>
25+
<div className="grid place-items-center">
26+
{/* 3D Book Effect */}
27+
<div className="flex my-12">
28+
<div className="bg-accent [transform:perspective(400px)_rotateY(314deg)] -mr-1 w-4">
29+
<span className="inline-block text-nowrap text-[0.5rem] font-bold text-accent-foreground/50 [transform:rotate(90deg)_translateY(-16px)] origin-top-left"></span>
30+
</div>
31+
<ViewTransition name={book.id}>
3232
<Image
3333
src={book?.cover ?? '/book-placeholder.svg'}
3434
alt={book.title + "'s cover"}
@@ -42,9 +42,9 @@ export const ListBook: React.FC<{ book: Book }> = ({ book }) => {
4242
)}
4343
priority
4444
/>
45-
</div>
45+
</ViewTransition>
4646
</div>
47-
</ViewTransition>
47+
</div>
4848
<CardTitle className="text-lg line-clamp-1">{book.title}</CardTitle>
4949
<CardDescription className="line-clamp-1">
5050
{book.author}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { Book } from '@/lib/types/book'
2+
import {
3+
Card,
4+
CardContent,
5+
CardDescription,
6+
CardHeader,
7+
CardTitle,
8+
} from '@/components/ui/card'
9+
import Image from 'next/image'
10+
import { Check } from 'lucide-react'
11+
import { Fragment } from 'react/jsx-runtime'
12+
13+
interface BookSelectCardProps {
14+
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+
}) => {
26+
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)}
32+
>
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+
}
44+
/>
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>
62+
</div>
63+
</div>
64+
</CardContent>
65+
</Card>
66+
)
67+
}

0 commit comments

Comments
 (0)