Skip to content

Commit a82f05e

Browse files
committed
feat: book detail recent borrows
1 parent f570d5f commit a82f05e

File tree

7 files changed

+180
-4
lines changed

7 files changed

+180
-4
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { getListBorrows } from '@/lib/api/borrow'
99
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
1010
import { Button } from '@/components/ui/button'
1111
import { Verify } from '@/lib/firebase/firebase'
12+
import { DataRecentBorrows } from '@/components/books/DataRecentBorrows'
1213

1314
export default async function BookDetailsPage({
1415
params,
@@ -98,6 +99,8 @@ export default async function BookDetailsPage({
9899
</CardContent>
99100
</Card>
100101

102+
<DataRecentBorrows book_id={id} />
103+
101104
<Card>
102105
<CardHeader>
103106
<div className="flex items-center justify-between">

app/(protected)/books/[id]/layout.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { ThreeDBook } from '@/components/books/three-d-book'
1111
import { ViewTransition } from 'react'
1212
import BtnWatchlist from '@/components/books/BtnWatchlist'
1313
import { Route } from 'next'
14+
import { IsLoggedIn } from '@/lib/firebase/firebase'
1415

1516
export default async function BookDetailsLayout({
1617
params,
@@ -21,7 +22,11 @@ export default async function BookDetailsLayout({
2122
}) {
2223
const { id } = await params
2324

24-
const [bookRes] = await Promise.all([getBook({ id, include_stats: 'true' })])
25+
const claim = await IsLoggedIn()
26+
27+
const [bookRes] = await Promise.all([
28+
getBook({ id, include_stats: 'true', user_id: claim?.librarease.id }),
29+
])
2530

2631
if ('error' in bookRes) {
2732
console.log({ libRes: bookRes })

app/(protected)/books/[id]/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Avatar, AvatarFallback } from '@/components/ui/avatar'
99
import { Button } from '@/components/ui/button'
1010
import { IsLoggedIn, Verify } from '@/lib/firebase/firebase'
1111
import { Review } from '@/components/reviews/Review'
12+
import { DataRecentBorrows } from '@/components/books/DataRecentBorrows'
1213

1314
export default async function BookDetailsPage({
1415
params,
@@ -103,6 +104,8 @@ export default async function BookDetailsPage({
103104
</CardContent>
104105
</Card>
105106

107+
<DataRecentBorrows book_id={id} user_id={claim?.librarease.id} />
108+
106109
{myReviewsRes.data.map((review) => (
107110
<Review key={review.id} review={review} />
108111
))}

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export default async function UserBooks({
5151
...(library_id ? { library_id } : {}),
5252
} as const
5353

54-
const headers = await Verify({ from: '/books' })
54+
const headers = await Verify({ from: '/books/watchlist' })
5555

5656
const res = await getListWatchlist(query, { headers })
5757

@@ -62,8 +62,9 @@ export default async function UserBooks({
6262

6363
const prevSkip = skip - limit > 0 ? skip - limit : 0
6464

65-
const nextURL = `/books?skip=${skip + limit}&limit=${limit}` as const
66-
const prevURL = `/books?skip=${prevSkip}&limit=${limit}` as const
65+
const nextURL =
66+
`/books/watchlist?skip=${skip + limit}&limit=${limit}` as const
67+
const prevURL = `/books/watchlist?skip=${prevSkip}&limit=${limit}` as const
6768

6869
return (
6970
<div className="space-y-4">
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
2+
import { getListBorrows } from '@/lib/api/borrow'
3+
import { Verify } from '@/lib/firebase/firebase'
4+
import {
5+
AlertCircle,
6+
Book,
7+
ChevronRight,
8+
Clock,
9+
MessageSquare,
10+
Star,
11+
Calendar,
12+
CalendarCheck,
13+
} from 'lucide-react'
14+
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
15+
import Link from 'next/link'
16+
import { Route } from 'next'
17+
import { Skeleton } from '../ui/skeleton'
18+
import { Suspense } from 'react'
19+
import { Avatar, AvatarFallback } from '../ui/avatar'
20+
import { formatDate, getBorrowStatus } from '@/lib/utils'
21+
import { Badge } from '../ui/badge'
22+
import { Button } from '../ui/button'
23+
24+
export const DataRecentBorrows: React.FC<
25+
React.ComponentProps<typeof RecentBorrows>
26+
> = async ({ book_id, user_id }) => {
27+
return (
28+
<Card>
29+
<CardHeader>
30+
<div className="flex items-center justify-between">
31+
<CardTitle className="flex items-center gap-2">
32+
<Clock className="h-5 w-5" />
33+
{user_id ? 'Your Borrowing History' : 'Recent Borrows'}
34+
</CardTitle>
35+
</div>
36+
</CardHeader>
37+
<CardContent>
38+
<Suspense fallback={<Loading />}>
39+
<RecentBorrows user_id={user_id} book_id={book_id} />
40+
</Suspense>
41+
</CardContent>
42+
</Card>
43+
)
44+
}
45+
46+
const RecentBorrows: React.FC<{
47+
book_id: string
48+
user_id?: string // if provided, fetch borrows for this user only
49+
}> = async ({ user_id, book_id }) => {
50+
const headers = await Verify({ from: '' })
51+
52+
const [res] = await Promise.all([
53+
getListBorrows(
54+
{
55+
book_id,
56+
sort_in: 'desc',
57+
sort_by: 'created_at',
58+
limit: 5,
59+
user_id,
60+
include_review: 'true',
61+
},
62+
{
63+
headers,
64+
}
65+
),
66+
])
67+
68+
if ('error' in res) {
69+
return (
70+
<div className="space-y-4">
71+
<Alert variant="destructive">
72+
<AlertCircle className="h-4 w-4" />
73+
<AlertTitle>Error</AlertTitle>
74+
<AlertDescription>{JSON.stringify(res.error)}</AlertDescription>
75+
</Alert>
76+
</div>
77+
)
78+
}
79+
const isAdmin = !user_id
80+
81+
if (res.meta.total === 0) {
82+
return (
83+
<div className="text-center py-8 text-muted-foreground">
84+
<Book className="h-12 w-12 mx-auto mb-3 opacity-50" />
85+
<p>
86+
{user_id
87+
? "You haven't borrowed this book yet"
88+
: 'No borrowing records yet'}
89+
</p>
90+
</div>
91+
)
92+
}
93+
94+
return (
95+
<div className="space-y-4">
96+
{res.data.map((borrow) => (
97+
<Link
98+
href={((isAdmin ? '/admin' : '') + `/borrows/${borrow.id}`) as Route}
99+
key={borrow.id}
100+
className="flex items-center justify-between p-4 rounded-lg border bg-muted/30 hover:bg-muted/50 transition-colors"
101+
>
102+
<div className="flex items-center gap-4">
103+
{isAdmin && (
104+
<Avatar className="h-10 w-10">
105+
<AvatarFallback className="text-xs bg-primary/10">
106+
{borrow.subscription.user.name.slice(0, 2)}
107+
</AvatarFallback>
108+
</Avatar>
109+
)}
110+
<div>
111+
<span>{borrow.subscription.user.name}</span>
112+
<div className="flex items-center gap-3 text-xs text-muted-foreground mt-1">
113+
<span className="flex items-center gap-1">
114+
<Calendar className="h-3 w-3" />
115+
{formatDate(borrow.borrowed_at)}
116+
</span>
117+
{borrow.returning && (
118+
<span className="flex items-center gap-1">
119+
<CalendarCheck className="h-3 w-3" />
120+
{formatDate(borrow.returning.returned_at)}
121+
</span>
122+
)}
123+
</div>
124+
</div>
125+
</div>
126+
<div className="flex items-center gap-3">
127+
{borrow.review ? (
128+
<div className="flex items-center gap-1 text-sm">
129+
<Star className="h-4 w-4 fill-(--color-vibrant,var(--color-yellow-400)) text-(--color-vibrant,var(--color-yellow-400))" />
130+
<span>{borrow.review.rating}</span>
131+
</div>
132+
) : borrow.returning ? (
133+
<Badge variant="outline" className="text-xs">
134+
<MessageSquare className="h-3 w-3 mr-1" />
135+
No review
136+
</Badge>
137+
) : null}
138+
{getBorrowStatus(borrow)}
139+
<Button variant="ghost" size="icon" className="h-8 w-8">
140+
<ChevronRight className="h-4 w-4" />
141+
</Button>
142+
</div>
143+
</Link>
144+
))}
145+
<div className="">
146+
<Link
147+
href={
148+
((isAdmin ? '/admin' : '') + `/borrows?book_id=${book_id}`) as Route
149+
}
150+
>
151+
<Button variant="outline" className="w-full bg-transparent">
152+
View All Borrows ({res.meta.total})
153+
</Button>
154+
</Link>
155+
</div>
156+
</div>
157+
)
158+
}
159+
160+
const Loading: React.FC = () => {
161+
return <Skeleton className="p-6 w-full h-60" />
162+
}

lib/api/borrow.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type GetListBorrowsQuery = QueryParams<
1313
user_id?: string
1414
returned_at?: string
1515
lost_at?: string
16+
include_review?: 'true'
1617
}
1718
>
1819
type GetListBorrowsResponse = Promise<ResList<Borrow>>

lib/types/borrow.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export type Borrow = WithCommon<{
1818
'id' | 'user_id' | 'membership_id' | 'user' | 'membership'
1919
>
2020
staff: Pick<Staff, 'id' | 'name'>
21+
review?: Pick<Review, 'id' | 'rating' | 'comment' | 'user' | 'created_at'> // available with include_review=true
2122
}>
2223

2324
export type BorrowDetail = Omit<Borrow, 'book' | 'subscription' | 'staff'> & {

0 commit comments

Comments
 (0)