Skip to content

Commit da8b829

Browse files
committed
feat: review
1 parent 6499d77 commit da8b829

File tree

18 files changed

+732
-109
lines changed

18 files changed

+732
-109
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import {
2+
Breadcrumb,
3+
BreadcrumbItem,
4+
BreadcrumbLink,
5+
BreadcrumbList,
6+
BreadcrumbPage,
7+
BreadcrumbSeparator,
8+
} from '@/components/ui/breadcrumb'
9+
import { getBook } from '@/lib/api/book'
10+
import { Button } from '@/components/ui/button'
11+
import { Pen } from 'lucide-react'
12+
import Link from 'next/link'
13+
import { colorsToCssVars } from '@/lib/utils/color-utils'
14+
import { ThreeDBook } from '@/components/books/three-d-book'
15+
import { ViewTransition } from 'react'
16+
17+
export default async function BookDetailsLayout({
18+
params,
19+
children,
20+
}: {
21+
params: Promise<{ id: string }>
22+
children: React.ReactNode
23+
}) {
24+
const { id } = await params
25+
26+
const [bookRes] = await Promise.all([getBook({ id, include_stats: 'true' })])
27+
28+
if ('error' in bookRes) {
29+
console.log({ libRes: bookRes })
30+
return <div>{JSON.stringify(bookRes.message)}</div>
31+
}
32+
33+
const cssVars = colorsToCssVars(bookRes.data.colors)
34+
35+
return (
36+
<div className="space-y-4" style={cssVars}>
37+
<h1 className="text-2xl font-semibold">{bookRes.data.title}</h1>
38+
<Breadcrumb>
39+
<BreadcrumbList>
40+
<BreadcrumbItem>
41+
<BreadcrumbLink href="/admin">Home</BreadcrumbLink>
42+
</BreadcrumbItem>
43+
<BreadcrumbSeparator />
44+
<BreadcrumbItem>
45+
<BreadcrumbLink href="/admin/books">Books</BreadcrumbLink>
46+
</BreadcrumbItem>
47+
<BreadcrumbSeparator />
48+
<BreadcrumbItem>
49+
<BreadcrumbPage>{bookRes.data.title}</BreadcrumbPage>
50+
</BreadcrumbItem>
51+
</BreadcrumbList>
52+
</Breadcrumb>
53+
54+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
55+
<div className="lg:col-span-1">
56+
<div className="grid place-items-center gap-4 lg:sticky lg:top-4">
57+
<ViewTransition name={bookRes.data.id}>
58+
<div className="relative">
59+
<div className="absolute inset-0 blur-3xl opacity-80 bg-(--color-light-vibrant) dark:bg-(--color-dark-vibrant) rounded-lg" />
60+
<ThreeDBook book={bookRes.data} />
61+
</div>
62+
</ViewTransition>
63+
<Button className="w-full" asChild>
64+
<Link href={`/admin/books/${bookRes.data.id}/edit`}>
65+
<Pen />
66+
Edit
67+
</Link>
68+
</Button>
69+
</div>
70+
</div>
71+
72+
<div className="lg:col-span-2">{children}</div>
73+
</div>
74+
</div>
75+
)
76+
}
Lines changed: 149 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
1-
import {
2-
Breadcrumb,
3-
BreadcrumbItem,
4-
BreadcrumbLink,
5-
BreadcrumbList,
6-
BreadcrumbPage,
7-
BreadcrumbSeparator,
8-
} from '@/components/ui/breadcrumb'
1+
import { Calendar, Hash, Library, Star } from 'lucide-react'
2+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
3+
import { Badge } from '@/components/ui/badge'
4+
import { formatDate, getBookStatus } from '@/lib/utils'
95
import { getBook } from '@/lib/api/book'
10-
import { Button } from '@/components/ui/button'
11-
import { Pen } from 'lucide-react'
126
import Link from 'next/link'
13-
import { DetailBook } from '@/components/books/DetailBook'
14-
import { colorsToCssVars } from '@/lib/utils/color-utils'
7+
import { getListReviews } from '@/lib/api/review'
8+
import { getListBorrows } from '@/lib/api/borrow'
9+
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
10+
import { Button } from '@/components/ui/button'
11+
import { Verify } from '@/lib/firebase/firebase'
1512

1613
export default async function BookDetailsPage({
1714
params,
@@ -20,42 +17,155 @@ export default async function BookDetailsPage({
2017
}) {
2118
const { id } = await params
2219

23-
const [bookRes] = await Promise.all([getBook({ id, include_stats: 'true' })])
20+
const headers = await Verify({
21+
from: `/admin/books/${id}`,
22+
})
23+
24+
const [bookRes, reviewsRes, borrowsRes] = await Promise.all([
25+
getBook({ id, include_stats: 'true' }),
26+
getListReviews({ book_id: id, limit: 3 }, { headers }),
27+
getListBorrows({ book_id: id, limit: 3 }, { headers }),
28+
])
2429

2530
if ('error' in bookRes) {
2631
console.log({ libRes: bookRes })
2732
return <div>{JSON.stringify(bookRes.message)}</div>
2833
}
34+
if ('error' in reviewsRes) {
35+
console.log({ reviewsRes })
36+
return <div>{JSON.stringify(reviewsRes.message)}</div>
37+
}
38+
if ('error' in borrowsRes) {
39+
console.log({ borrowsRes })
40+
return <div>{JSON.stringify(borrowsRes.message)}</div>
41+
}
2942

30-
const cssVars = colorsToCssVars(bookRes.data.colors)
43+
const status = getBookStatus(bookRes.data.stats)
44+
const isAvailable = status === 'available'
3145

3246
return (
3347
<div className="space-y-4">
34-
<h1 className="text-2xl font-semibold">{bookRes.data.title}</h1>
35-
<Breadcrumb>
36-
<BreadcrumbList>
37-
<BreadcrumbItem>
38-
<BreadcrumbLink href="/admin">Home</BreadcrumbLink>
39-
</BreadcrumbItem>
40-
<BreadcrumbSeparator />
41-
<BreadcrumbItem>
42-
<BreadcrumbLink href="/admin/books">Books</BreadcrumbLink>
43-
</BreadcrumbItem>
44-
<BreadcrumbSeparator />
45-
<BreadcrumbItem>
46-
<BreadcrumbPage>{bookRes.data.title}</BreadcrumbPage>
47-
</BreadcrumbItem>
48-
</BreadcrumbList>
49-
</Breadcrumb>
50-
51-
<DetailBook book={bookRes.data} style={cssVars}>
52-
<Button className="w-full" asChild>
53-
<Link href={`/admin/books/${bookRes.data.id}/edit`}>
54-
<Pen />
55-
Edit
56-
</Link>
57-
</Button>
58-
</DetailBook>
48+
<div>
49+
<h1 className="text-3xl font-bold mb-2">{bookRes.data.title}</h1>
50+
<p className="text-xl text-muted-foreground mb-4">
51+
{bookRes.data.author}
52+
</p>
53+
<div className="flex flex-wrap gap-2 mb-4">
54+
<Badge
55+
className="uppercase"
56+
variant={isAvailable ? 'default' : 'secondary'}
57+
>
58+
{status}
59+
</Badge>
60+
{/* <Badge variant="outline">bookRes.data.genre</Badge> */}
61+
</div>
62+
</div>
63+
64+
<Card>
65+
<CardHeader>
66+
<CardTitle>Book Information</CardTitle>
67+
</CardHeader>
68+
<CardContent className="grid gap-2 grid-cols-[max-content_1fr] md:grid-cols-[max-content_1fr_max-content_1fr] items-center">
69+
<Hash className="size-4" />
70+
<p>
71+
<span className="font-medium">Code:&nbsp;</span>
72+
{bookRes.data.code}
73+
</p>
74+
<Calendar className="size-4" />
75+
<p>
76+
<span className="font-medium">Year:&nbsp;</span>
77+
{bookRes.data.year}
78+
</p>
79+
<Library className="size-4" />
80+
<p>
81+
<span className="font-medium">Library:&nbsp;</span>
82+
<Link
83+
href={`/libraries/${bookRes.data.library.id}`}
84+
className="link"
85+
>
86+
{bookRes.data.library.name}
87+
</Link>
88+
</p>
89+
</CardContent>
90+
</Card>
91+
92+
<Card>
93+
<CardHeader>
94+
<CardTitle>Description</CardTitle>
95+
</CardHeader>
96+
<CardContent>
97+
<p className="text-sm leading-relaxed">{bookRes.data.description}</p>
98+
</CardContent>
99+
</Card>
100+
101+
<Card>
102+
<CardHeader>
103+
<div className="flex items-center justify-between">
104+
<CardTitle>Reviews</CardTitle>
105+
<div className="flex items-center gap-2">
106+
<div className="flex items-center gap-1">
107+
<Star className="h-5 w-5 fill-(--color-vibrant,var(--color-yellow-400)) text-(--color-vibrant,var(--color-yellow-400))" />
108+
<span className="font-semibold">
109+
{bookRes.data.stats?.rating?.toFixed(1)}
110+
</span>
111+
</div>
112+
<span className="text-sm text-muted-foreground">
113+
({reviewsRes.meta.total}{' '}
114+
{reviewsRes.meta.total === 1 ? 'review' : 'reviews'})
115+
</span>
116+
</div>
117+
</div>
118+
</CardHeader>
119+
<CardContent>
120+
<div className="space-y-6">
121+
{reviewsRes.data.map((review) => (
122+
<div
123+
key={review.id}
124+
className="border-b last:border-b-0 pb-6 last:pb-0"
125+
>
126+
<div className="flex items-start justify-between mb-2">
127+
<div className="flex items-center gap-2">
128+
<Avatar className="h-8 w-8">
129+
<AvatarFallback className="text-xs bg-primary/10">
130+
{review.user?.name?.slice(0, 2)}
131+
</AvatarFallback>
132+
</Avatar>
133+
<div>
134+
{review.user?.name}
135+
<div className="flex items-center gap-1 text-xs text-muted-foreground">
136+
<Calendar className="h-3 w-3" />
137+
{formatDate(review.created_at)}
138+
</div>
139+
</div>
140+
</div>
141+
<div className="flex items-center gap-1">
142+
{[1, 2, 3, 4, 5].map((star) => (
143+
<Star
144+
key={star}
145+
className={`h-4 w-4 ${
146+
star <= review.rating
147+
? 'fill-(--color-vibrant,var(--color-yellow-400)) text-(--color-vibrant,var(--color-yellow-400))'
148+
: 'text-gray-300'
149+
}`}
150+
/>
151+
))}
152+
</div>
153+
</div>
154+
<p className="text-sm leading-relaxed text-foreground">
155+
{review.comment}
156+
</p>
157+
</div>
158+
))}
159+
</div>
160+
<div className="mt-6 pt-6 border-t">
161+
{/* <Link href="/reviews"> */}
162+
<Button variant="outline" className="w-full bg-transparent">
163+
View All Reviews
164+
</Button>
165+
{/* </Link> */}
166+
</div>
167+
</CardContent>
168+
</Card>
59169
</div>
60170
)
61171
}

app/(protected)/admin/borrows/[id]/layout.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export default async function BorrowDetailsLayout({
3434
return <div>{JSON.stringify(borrowRes.message)}</div>
3535
}
3636
return (
37-
<div className="space-y-4">
37+
<>
3838
<nav className="backdrop-blur-sm sticky top-0 z-10">
3939
<h1 className="text-2xl font-semibold">{borrowRes.data.book.title}</h1>
4040
<div className="flex justify-between items-center">
@@ -70,6 +70,6 @@ export default async function BorrowDetailsLayout({
7070
{children}
7171
{lost}
7272
{returnNode}
73-
</div>
73+
</>
7474
)
7575
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import {
2+
Breadcrumb,
3+
BreadcrumbItem,
4+
BreadcrumbLink,
5+
BreadcrumbList,
6+
BreadcrumbPage,
7+
BreadcrumbSeparator,
8+
} from '@/components/ui/breadcrumb'
9+
import { getBook } from '@/lib/api/book'
10+
import { colorsToCssVars } from '@/lib/utils/color-utils'
11+
import { ThreeDBook } from '@/components/books/three-d-book'
12+
import { ViewTransition } from 'react'
13+
import BtnWatchlist from '@/components/books/BtnWatchlist'
14+
15+
export default async function BookDetailsLayout({
16+
params,
17+
children,
18+
}: {
19+
params: Promise<{ id: string }>
20+
children: React.ReactNode
21+
}) {
22+
const { id } = await params
23+
24+
const [bookRes] = await Promise.all([getBook({ id, include_stats: 'true' })])
25+
26+
if ('error' in bookRes) {
27+
console.log({ libRes: bookRes })
28+
return <div>{JSON.stringify(bookRes.message)}</div>
29+
}
30+
31+
const cssVars = colorsToCssVars(bookRes.data.colors)
32+
33+
return (
34+
<div className="space-y-4" style={cssVars}>
35+
<h1 className="text-2xl font-semibold">{bookRes.data.title}</h1>
36+
<Breadcrumb>
37+
<BreadcrumbList>
38+
<BreadcrumbItem>
39+
<BreadcrumbLink href="/admin">Home</BreadcrumbLink>
40+
</BreadcrumbItem>
41+
<BreadcrumbSeparator />
42+
<BreadcrumbItem>
43+
<BreadcrumbLink href="/admin/books">Books</BreadcrumbLink>
44+
</BreadcrumbItem>
45+
<BreadcrumbSeparator />
46+
<BreadcrumbItem>
47+
<BreadcrumbPage>{bookRes.data.title}</BreadcrumbPage>
48+
</BreadcrumbItem>
49+
</BreadcrumbList>
50+
</Breadcrumb>
51+
52+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
53+
<div className="lg:col-span-1">
54+
<div className="grid place-items-center gap-4 lg:sticky lg:top-4">
55+
<ViewTransition name={bookRes.data.id}>
56+
<div className="relative">
57+
<div className="absolute inset-0 blur-3xl opacity-80 bg-(--color-light-vibrant) dark:bg-(--color-dark-vibrant) rounded-lg" />
58+
<ThreeDBook book={bookRes.data} />
59+
</div>
60+
</ViewTransition>
61+
<BtnWatchlist
62+
bookId={bookRes.data.id}
63+
isWatched={!!bookRes.data.watchlists?.[0]}
64+
/>
65+
</div>
66+
</div>
67+
68+
<div className="lg:col-span-2">{children}</div>
69+
</div>
70+
</div>
71+
)
72+
}

0 commit comments

Comments
 (0)