Skip to content

Commit 67602f7

Browse files
committed
feat: lost feature
1 parent 93187ba commit 67602f7

File tree

17 files changed

+397
-72
lines changed

17 files changed

+397
-72
lines changed

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,11 @@ export default async function BorrowDetailsLayout({
5050

5151
<Badge
5252
variant={
53-
getBorrowStatus(borrowRes.data) === 'overdue'
54-
? 'destructive'
55-
: getBorrowStatus(borrowRes.data) === 'returned'
56-
? 'secondary'
57-
: 'default'
53+
getBorrowStatus(borrowRes.data) === 'active'
54+
? 'default'
55+
: getBorrowStatus(borrowRes.data) === 'overdue'
56+
? 'destructive'
57+
: 'secondary'
5858
}
5959
className="uppercase h-8 min-w-24 justify-center"
6060
>

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

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { Button } from '@/components/ui/button'
88
import { BtnUndoReturn } from '@/components/borrows/BtnUndoReturn'
99
import Link from 'next/link'
1010
import { Pen } from 'lucide-react'
11+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
12+
import { FormLostBorrow } from '@/components/borrows/FormLostBorrow'
1113

1214
export default async function BorrowDetailsPage({
1315
params,
@@ -53,30 +55,42 @@ export default async function BorrowDetailsPage({
5355

5456
return (
5557
<DetailBorrow borrow={borrowRes.data} prevBorrows={prevBorrows}>
56-
<div className="bottom-0 sticky py-2 grid md:grid-cols-2 gap-2">
57-
{borrowRes.data.returning ? (
58-
<BtnUndoReturn
59-
variant="outline"
60-
className="w-full backdrop-blur-md"
61-
borrow={borrowRes.data}
62-
/>
63-
) : (
64-
<BtnReturnBook
65-
variant="outline"
66-
className="w-full"
67-
borrow={borrowRes.data}
68-
/>
58+
<>
59+
{borrowRes.data.returning || borrowRes.data.lost ? null : (
60+
<Card className="bg-destructive/10 border-destructive/20">
61+
<CardHeader>
62+
<CardTitle>Mark as Lost</CardTitle>
63+
</CardHeader>
64+
<CardContent>
65+
<FormLostBorrow id={borrowRes.data.id} />
66+
</CardContent>
67+
</Card>
6968
)}
70-
<Button asChild>
71-
<Link
72-
href={`/admin/borrows/${borrowRes.data.id}/edit`}
73-
className="w-full"
74-
>
75-
<Pen />
76-
Edit
77-
</Link>
78-
</Button>
79-
</div>
69+
<div className="bottom-0 sticky py-2 grid md:grid-cols-2 gap-2">
70+
{borrowRes.data.returning ? (
71+
<BtnUndoReturn
72+
variant="outline"
73+
className="w-full backdrop-blur-md"
74+
borrow={borrowRes.data}
75+
/>
76+
) : (
77+
<BtnReturnBook
78+
variant="outline"
79+
className="w-full"
80+
borrow={borrowRes.data}
81+
/>
82+
)}
83+
<Button asChild>
84+
<Link
85+
href={`/admin/borrows/${borrowRes.data.id}/edit`}
86+
className="w-full"
87+
>
88+
<Pen />
89+
Edit
90+
</Link>
91+
</Button>
92+
</div>
93+
</>
8094
</DetailBorrow>
8195
)
8296
}

components.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"$schema": "https://ui.shadcn.com/schema.json",
3-
"style": "new-york-v4",
3+
"style": "new-york",
44
"rsc": true,
55
"tsx": true,
66
"tailwind": {

components/books/DetailBook.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@ import { Badge } from '@/components/ui/badge'
55
import Link from 'next/link'
66
import { BookDetail } from '@/lib/types/book'
77
import { unstable_ViewTransition as ViewTransition } from 'react'
8+
import { getBookStatus } from '@/lib/utils'
89

910
export const DetailBook: React.FC<
1011
React.PropsWithChildren<{ book: BookDetail }>
1112
> = ({ book, children }) => {
13+
const status = getBookStatus(book.stats)
14+
const isAvailable = status === 'available'
15+
1216
return (
1317
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
1418
{/* Book Cover */}
@@ -25,8 +29,11 @@ export const DetailBook: React.FC<
2529
<h1 className="text-3xl font-bold mb-2">{book.title}</h1>
2630
<p className="text-xl text-muted-foreground mb-4">{book.author}</p>
2731
<div className="flex flex-wrap gap-2 mb-4">
28-
<Badge variant={book.stats?.is_available ? 'default' : 'secondary'}>
29-
{book.stats?.is_available ? 'Available' : 'Borrowed'}
32+
<Badge
33+
className="uppercase"
34+
variant={isAvailable ? 'default' : 'secondary'}
35+
>
36+
{status}
3037
</Badge>
3138
<Badge variant="outline">book.genre</Badge>
3239
</div>

components/books/ListBook.tsx

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ import {
1010
} from '@/components/ui/card'
1111
import clsx from 'clsx'
1212
import { unstable_ViewTransition as ViewTransition } from 'react'
13+
import { getBookStatus } from '@/lib/utils'
1314

1415
export const ListBook: React.FC<{ book: Book }> = ({ book }) => {
16+
const status = getBookStatus(book.stats)
17+
1518
return (
1619
<Card
1720
className={clsx(
18-
'group cursor-pointer transition-all duration-200 hover:shadow-lg hover:-translate-y-1',
19-
book.stats?.is_available && ''
21+
'group cursor-pointer transition-all duration-200 hover:shadow-lg hover:-translate-y-1'
2022
)}
2123
>
2224
<CardHeader className="pb-3">
@@ -35,7 +37,8 @@ export const ListBook: React.FC<{ book: Book }> = ({ book }) => {
3537
className={clsx(
3638
'shadow-xl rounded-r-md w-32 h-48 object-cover',
3739
'[transform:perspective(800px)_rotateY(14deg)]',
38-
!book.stats?.is_available && 'grayscale'
40+
status === 'borrowed' && 'grayscale-75',
41+
status === 'lost' && 'grayscale opacity-50'
3942
)}
4043
priority
4144
/>
@@ -63,18 +66,20 @@ export const ListBook: React.FC<{ book: Book }> = ({ book }) => {
6366
</Badge>
6467
)} */}
6568

66-
<div className="flex items-center gap-2 text-sm text-muted-foreground">
67-
{book.stats?.is_available ? (
68-
<>
69-
<BadgeCheck className="h-3 w-3 text-primary" />
70-
<span>Available</span>
71-
</>
69+
<div
70+
className={clsx(
71+
'flex capitalize items-center gap-2 text-sm',
72+
status === 'available' && 'text-primary',
73+
status === 'borrowed' && 'text-muted-foreground',
74+
status === 'lost' && 'text-destructive'
75+
)}
76+
>
77+
{status === 'available' ? (
78+
<BadgeCheck className="h-3 w-3" />
7279
) : (
73-
<>
74-
<BadgeMinus className="h-3 w-3" />
75-
<span>Borrowed</span>
76-
</>
80+
<BadgeMinus className="h-3 w-3" />
7781
)}
82+
<span>{status}</span>
7883
</div>
7984
</div>
8085
</CardContent>

components/borrows/BtnReturnBorrow.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ export const BtnReturnBook: React.FC<
4444
})
4545
}
4646

47+
if (borrow.lost) {
48+
return (
49+
<Button {...props} variant="destructive" disabled>
50+
Lost on {formatDate(borrow.lost.reported_at)}
51+
</Button>
52+
)
53+
}
54+
4755
if (clientBorrow.returning)
4856
return (
4957
<Button {...props} variant="secondary" disabled>

components/borrows/DetailBorrow.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
CreditCard,
1919
Gavel,
2020
Library,
21+
Pen,
2122
Tally5,
2223
User,
2324
UserCog,
@@ -29,6 +30,7 @@ import { DateTime } from '@/components/common/DateTime'
2930
import { ThreeDBook } from '@/components/books/three-d-book'
3031
import { Route } from 'next'
3132
import { unstable_ViewTransition as ViewTransition } from 'react'
33+
import { Alert, AlertDescription, AlertTitle } from '../ui/alert'
3234

3335
export const DetailBorrow: React.FC<
3436
React.PropsWithChildren<{
@@ -173,6 +175,33 @@ export const DetailBorrow: React.FC<
173175
</CardContent>
174176
</Card>
175177
</div>
178+
179+
{borrow.lost ? (
180+
<Alert variant="destructive" className="bg-destructive/10">
181+
<AlertTitle className="font-semibold mb-4">
182+
Borrow Marked as Lost
183+
</AlertTitle>
184+
<AlertDescription>
185+
<div className="grid gap-2 grid-cols-[max-content_1fr] items-center">
186+
<Pen className="size-4 text-muted-foreground" />
187+
<p>
188+
<span className="font-medium">Note:&nbsp;</span>
189+
{borrow.lost.note}
190+
</p>
191+
<Calendar className="size-4 text-muted-foreground" />
192+
<p>
193+
<span className="font-medium">Reported:&nbsp;</span>
194+
<DateTime dateTime={borrow.lost.reported_at} />
195+
</p>
196+
<Gavel className="size-4 text-muted-foreground" />
197+
<p>
198+
<span className="font-medium">Fine:&nbsp;</span>3 pts
199+
</p>
200+
</div>
201+
</AlertDescription>
202+
</Alert>
203+
) : null}
204+
176205
<Card>
177206
<CardHeader>
178207
<div className="flex justify-between items-center">
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
'use client'
2+
3+
import { zodResolver } from '@hookform/resolvers/zod'
4+
import { useForm } from 'react-hook-form'
5+
import { z } from 'zod'
6+
import {
7+
Form,
8+
FormControl,
9+
FormField,
10+
FormItem,
11+
FormLabel,
12+
FormMessage,
13+
} from '../ui/form'
14+
import { toast } from 'sonner'
15+
import { Button } from '../ui/button'
16+
import { Input } from '../ui/input'
17+
import { useCallback, useTransition } from 'react'
18+
import { lostBorrowAction } from '@/lib/actions/lost-borrow'
19+
import { Textarea } from '../ui/textarea'
20+
import { Spinner } from '../ui/spinner'
21+
22+
const FormSchema = z.object({
23+
id: z.uuid(),
24+
reported_at: z.date(),
25+
note: z.string().nonempty(),
26+
fine: z.number(),
27+
})
28+
29+
export const FormLostBorrow: React.FC<{
30+
id: string
31+
}> = ({ id }) => {
32+
const form = useForm<z.infer<typeof FormSchema>>({
33+
resolver: zodResolver(FormSchema),
34+
defaultValues: {
35+
id,
36+
reported_at: new Date(),
37+
note: '',
38+
fine: 0,
39+
},
40+
})
41+
42+
const [isPending, startTransition] = useTransition()
43+
44+
function onSubmit(data: z.infer<typeof FormSchema>) {
45+
startTransition(async () => {
46+
const msg = await lostBorrowAction({
47+
id: data.id,
48+
reported_at: new Date().toJSON(),
49+
note: data.note,
50+
fine: data.fine,
51+
})
52+
if ('error' in msg) {
53+
toast.error(msg.error, { richColors: true })
54+
} else {
55+
toast.success(msg.message)
56+
}
57+
})
58+
}
59+
60+
const onReset = useCallback(() => {
61+
form.reset()
62+
}, [form])
63+
64+
return (
65+
<Form {...form}>
66+
<form onSubmit={form.handleSubmit(onSubmit)} className="grid gap-2">
67+
<FormField
68+
control={form.control}
69+
name="note"
70+
render={({ field }) => (
71+
<FormItem className="flex flex-col">
72+
<FormLabel>Note</FormLabel>
73+
<FormControl>
74+
<Textarea
75+
placeholder="Remarks"
76+
{...field}
77+
onChange={field.onChange}
78+
rows={3}
79+
/>
80+
</FormControl>
81+
<FormMessage />
82+
</FormItem>
83+
)}
84+
/>
85+
<FormField
86+
control={form.control}
87+
name="fine"
88+
render={({ field }) => (
89+
<FormItem className="flex flex-col">
90+
<FormLabel>Fine</FormLabel>
91+
<FormControl>
92+
<Input
93+
placeholder="Pts"
94+
type="number"
95+
{...field}
96+
onChange={(e) => field.onChange(e.target.valueAsNumber)}
97+
/>
98+
</FormControl>
99+
<FormMessage />
100+
</FormItem>
101+
)}
102+
/>
103+
<Button type="reset" variant="ghost" onClick={onReset}>
104+
Reset
105+
</Button>
106+
<Button type="submit" disabled={!form.formState.isDirty || isPending}>
107+
{isPending && <Spinner />}
108+
Submit
109+
</Button>
110+
</form>
111+
</Form>
112+
)
113+
}

components/borrows/ListCardBorrow.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ export const ListCardBorrow: React.FC<
5555
variant={
5656
getBorrowStatus(borrow) === 'overdue'
5757
? 'destructive'
58-
: getBorrowStatus(borrow) === 'returned'
59-
? 'secondary'
60-
: 'default'
58+
: getBorrowStatus(borrow) === 'active'
59+
? 'default'
60+
: 'secondary'
6161
}
6262
className="capitalize"
6363
>

0 commit comments

Comments
 (0)