Skip to content

Commit b5d5732

Browse files
committed
feat: collections
1 parent 682ee9c commit b5d5732

File tree

10 files changed

+644
-5
lines changed

10 files changed

+644
-5
lines changed
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import { ListBook } from '@/components/books/ListBook'
2+
import {
3+
Breadcrumb,
4+
BreadcrumbItem,
5+
BreadcrumbLink,
6+
BreadcrumbList,
7+
BreadcrumbPage,
8+
BreadcrumbSeparator,
9+
} from '@/components/ui/breadcrumb'
10+
import { Button } from '@/components/ui/button'
11+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
12+
import { getCollection, getListCollectionBooks } from '@/lib/api/collection'
13+
import {
14+
BookOpen,
15+
Calendar,
16+
Edit,
17+
Heart,
18+
Library,
19+
Plus,
20+
Settings,
21+
Trash2,
22+
Users,
23+
} from 'lucide-react'
24+
import Image from 'next/image'
25+
import Link from 'next/link'
26+
27+
export default async function CollectionDetailsPage({
28+
params,
29+
}: {
30+
params: Promise<{ id: string }>
31+
}) {
32+
// const claims = await IsLoggedIn()
33+
34+
const { id } = await params
35+
36+
const [collectionRes, bookRes] = await Promise.all([
37+
getCollection(id),
38+
getListCollectionBooks(id, {
39+
include_book: 'true',
40+
}),
41+
])
42+
43+
if ('error' in collectionRes) {
44+
console.log({ libRes: collectionRes })
45+
return <div>{JSON.stringify(collectionRes.message)}</div>
46+
}
47+
48+
if ('error' in bookRes) {
49+
console.log({ bookRes })
50+
return <div>{JSON.stringify(bookRes.message)}</div>
51+
}
52+
53+
return (
54+
<div className="space-y-4">
55+
<h1 className="text-2xl font-semibold">{collectionRes.data.title}</h1>
56+
<Breadcrumb>
57+
<BreadcrumbList>
58+
<BreadcrumbItem>
59+
<BreadcrumbLink href="/">Home</BreadcrumbLink>
60+
</BreadcrumbItem>
61+
<BreadcrumbSeparator />
62+
<BreadcrumbItem>
63+
<BreadcrumbLink href="/collections">Collections</BreadcrumbLink>
64+
</BreadcrumbItem>
65+
<BreadcrumbSeparator />
66+
<BreadcrumbItem>
67+
<BreadcrumbPage>{collectionRes.data.title}</BreadcrumbPage>
68+
</BreadcrumbItem>
69+
</BreadcrumbList>
70+
</Breadcrumb>
71+
72+
<div className="relative aspect-[2] rounded-lg overflow-hidden mb-6">
73+
<Image
74+
src={collectionRes.data.cover?.path || '/book-placeholder.svg'}
75+
alt={collectionRes.data.title}
76+
fill
77+
className="w-full h-full object-cover rounded-t"
78+
/>
79+
<div className="absolute inset-0 bg-black/20" />
80+
<div className="absolute bottom-6 left-6 text-white">
81+
<h1 className="text-4xl font-bold mb-2">
82+
{collectionRes.data.title}
83+
</h1>
84+
<div className="flex flex-col md:flex-row md:items-center gap-4 text-sm">
85+
<div className="flex items-center gap-1">
86+
<Library className="h-4 w-4" />
87+
<span>{collectionRes.data.library?.name}</span>
88+
</div>
89+
<div className="flex items-center gap-1">
90+
<Users className="h-4 w-4" />
91+
<span>{collectionRes.data.follower_count} followers</span>
92+
</div>
93+
<div className="flex items-center gap-1">
94+
<BookOpen className="h-4 w-4" />
95+
<span>{collectionRes.data.book_count} books</span>
96+
</div>
97+
</div>
98+
</div>
99+
</div>
100+
101+
{/* Action Buttons */}
102+
{/* <div className="flex gap-2 mb-8 flex-wrap">
103+
<Button variant={true ? 'secondary' : 'default'}>
104+
<Heart className={`mr-2 h-4 w-4 ${true ? 'fill-current' : ''}`} />
105+
{true ? 'Following' : 'Follow'}
106+
</Button>
107+
<Button variant="outline" asChild>
108+
<Link href={`/collections/${collectionRes.data.id}/manage-books`}>
109+
<Settings className="mr-2 h-4 w-4" />
110+
Manage Books
111+
</Link>
112+
</Button>
113+
<Button variant="outline" asChild>
114+
<Link href={`/collections/${collectionRes.data.id}/edit`}>
115+
<Edit className="mr-2 h-4 w-4" />
116+
Edit
117+
</Link>
118+
</Button>
119+
<Button
120+
variant="outline"
121+
className="text-red-600 hover:text-red-700 bg-transparent"
122+
>
123+
<Trash2 className="mr-2 h-4 w-4" />
124+
Delete
125+
</Button>
126+
</div> */}
127+
128+
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
129+
{/* Collection Info */}
130+
<div className="lg:col-span-1">
131+
<Card>
132+
<CardHeader>
133+
<CardTitle>About this Collection</CardTitle>
134+
</CardHeader>
135+
<CardContent className="space-y-4">
136+
{collectionRes.data.description && (
137+
<p className="text-sm text-muted-foreground leading-relaxed">
138+
{collectionRes.data.description}
139+
</p>
140+
)}
141+
142+
<div className="space-y-3 pt-4 border-t">
143+
<div className="flex items-center gap-2 text-sm">
144+
<Library className="h-4 w-4 text-muted-foreground" />
145+
<span>{collectionRes.data.library?.name}</span>
146+
</div>
147+
<div className="flex items-center gap-2 text-sm">
148+
<Users className="h-4 w-4 text-muted-foreground" />
149+
<span>{collectionRes.data.follower_count} followers</span>
150+
</div>
151+
<div className="flex items-center gap-2 text-sm">
152+
<BookOpen className="h-4 w-4 text-muted-foreground" />
153+
<span>{bookRes.data.length} books</span>
154+
</div>
155+
<div className="flex items-center gap-2 text-sm">
156+
<Calendar className="h-4 w-4 text-muted-foreground" />
157+
<span>
158+
Created{' '}
159+
{new Date(
160+
collectionRes.data.created_at
161+
).toLocaleDateString()}
162+
</span>
163+
</div>
164+
</div>
165+
</CardContent>
166+
</Card>
167+
</div>
168+
169+
{/* Books in Collection */}
170+
<div className="lg:col-span-3">
171+
<div className="flex justify-between items-center mb-6">
172+
<h2 className="text-2xl font-semibold">Books in Collection</h2>
173+
{/* <Button asChild>
174+
<Link href={`/collections/${collectionRes.data.id}/manage-books`}>
175+
<Plus className="mr-2 h-4 w-4" />
176+
Manage Books
177+
</Link>
178+
</Button> */}
179+
</div>
180+
181+
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
182+
{bookRes.data.map((collectionBook) => (
183+
<ListBook book={collectionBook.book!} key={collectionBook.id} />
184+
))}
185+
</div>
186+
187+
{collectionRes.data.book_count === 0 && (
188+
<Card className="p-12 text-center">
189+
<BookOpen className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
190+
<h3 className="text-lg font-medium mb-2">
191+
No books in this collection
192+
</h3>
193+
<p className="text-muted-foreground mb-4">
194+
Start building your collection by adding some books.
195+
</p>
196+
{/* <Button asChild>
197+
<Link
198+
href={`/collections/${collectionRes.data.id}/manage-books`}
199+
>
200+
<Plus className="mr-2 h-4 w-4" />
201+
Add Books
202+
</Link>
203+
</Button> */}
204+
</Card>
205+
)}
206+
</div>
207+
</div>
208+
</div>
209+
)
210+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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 Link from 'next/link'
17+
import type { Metadata } from 'next'
18+
import { SITE_NAME } from '@/lib/consts'
19+
import { BellRing, Search } from 'lucide-react'
20+
import { DebouncedInput } from '@/components/common/DebouncedInput'
21+
import { Badge } from '@/components/ui/badge'
22+
import { Verify } from '@/lib/firebase/firebase'
23+
import { Button } from '@/components/ui/button'
24+
import { getListCollections } from '@/lib/api/collection'
25+
import { ListCollection } from '@/components/collections/ListCollection'
26+
27+
export const metadata: Metadata = {
28+
title: `Collections · ${SITE_NAME}`,
29+
}
30+
31+
export default async function UserCollections({
32+
searchParams,
33+
}: {
34+
searchParams: Promise<{
35+
skip?: number
36+
limit?: number
37+
library_id?: string
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+
const library_id = sp?.library_id
45+
46+
const query = {
47+
sort_by: 'created_at',
48+
sort_in: 'desc',
49+
limit: limit,
50+
skip: skip,
51+
title: sp?.title,
52+
include_library: 'true',
53+
...(library_id ? { library_id } : {}),
54+
} as const
55+
56+
await Verify({ from: '/collections' })
57+
58+
const res = await getListCollections(query)
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 = `/collections?skip=${skip + limit}&limit=${limit}` as const
68+
const prevURL = `/collections?skip=${prevSkip}&limit=${limit}` as const
69+
70+
return (
71+
<div className="space-y-4">
72+
<nav className="backdrop-blur-sm sticky top-0 z-10">
73+
<h1 className="text-2xl font-semibold">Collections</h1>
74+
<div className="flex justify-between items-center">
75+
<Breadcrumb>
76+
<BreadcrumbList>
77+
<BreadcrumbItem>
78+
<BreadcrumbLink href="/">Home</BreadcrumbLink>
79+
</BreadcrumbItem>
80+
<BreadcrumbSeparator />
81+
82+
<BreadcrumbItem>
83+
<BreadcrumbPage>
84+
Collections{' '}
85+
<Badge className="ml-4" variant="outline">
86+
{res.meta.total}
87+
</Badge>
88+
</BreadcrumbPage>
89+
</BreadcrumbItem>
90+
</BreadcrumbList>
91+
</Breadcrumb>
92+
<Button variant="secondary" asChild>
93+
<Link href="/collections/watchlist">
94+
<>
95+
<BellRing className="mr-2 h-4 w-4" />
96+
Followings
97+
</>
98+
</Link>
99+
</Button>
100+
</div>
101+
</nav>
102+
103+
<div className="relative flex-1">
104+
<Search className="absolute left-3 top-3 size-4 text-muted-foreground" />
105+
106+
<DebouncedInput
107+
name="title"
108+
placeholder="Search by title"
109+
className="pl-8 max-w-md"
110+
/>
111+
</div>
112+
113+
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
114+
{res.data.map((col) => (
115+
<Link key={col.id} href={`/collections/${col.id}`} passHref>
116+
<ListCollection collection={col} />
117+
</Link>
118+
))}
119+
</div>
120+
121+
<Pagination>
122+
<PaginationContent>
123+
{res.meta.skip > 0 && (
124+
<PaginationItem>
125+
<PaginationPrevious href={prevURL} />
126+
</PaginationItem>
127+
)}
128+
{res.meta.limit <= res.data.length && (
129+
<PaginationItem>
130+
<PaginationNext href={nextURL} />
131+
</PaginationItem>
132+
)}
133+
</PaginationContent>
134+
</Pagination>
135+
</div>
136+
)
137+
}

app/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
BookUser,
77
Ticket,
88
Settings,
9+
BookCopy,
910
} from 'lucide-react'
1011
import { Button } from '@/components/ui/button'
1112
import { IsLoggedIn } from '@/lib/firebase/firebase'
@@ -23,6 +24,7 @@ const menuItems = [
2324
href: '/subscriptions',
2425
},
2526
{ title: 'My Borrows', icon: BookUser, href: '/borrows' },
27+
{ title: 'Collections', icon: BookCopy, href: '/collections' },
2628
] as const
2729

2830
export default async function LibraryDashboard() {

components/books/BtnWatchlist.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@ export default function BtnWatchlist({
1616
}) {
1717
const [pending, startTransition] = useTransition()
1818

19-
const handleAdd = () => startTransition(() => addToWatchlistAction(bookId))
19+
const handleAdd = () =>
20+
startTransition(async () => {
21+
await addToWatchlistAction(bookId)
22+
})
2023
const handleRemove = () =>
21-
startTransition(() => removeFromWatchlistAction(bookId))
24+
startTransition(async () => await removeFromWatchlistAction(bookId))
2225

2326
if (isWatched) {
2427
return (

0 commit comments

Comments
 (0)