Skip to content

Commit b3b00b1

Browse files
committed
feat: login ui & book create form
1 parent 7701c21 commit b3b00b1

File tree

13 files changed

+413
-65
lines changed

13 files changed

+413
-65
lines changed

app/(protected)/borrows/page.tsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { BtnReturnBook } from '@/components/borrows/BtnReturnBook'
1+
import { BtnReturnBook } from '@/components/borrows/BtnReturnBorrow'
22
import { Badge } from '@/components/ui/badge'
33
import {
44
Breadcrumb,
@@ -28,18 +28,10 @@ import {
2828
import { getListBorrows } from '@/lib/api/borrow'
2929
import { Verify } from '@/lib/firebase/firebase'
3030
import { Borrow } from '@/lib/types/borrow'
31+
import { formatDate } from '@/lib/utils'
3132
import { Book, Calendar, LibraryIcon, User } from 'lucide-react'
3233
import Link from 'next/link'
3334

34-
const formatDate = (date: string): string => {
35-
const formatter = new Intl.DateTimeFormat('en-US', {
36-
month: 'short',
37-
day: 'numeric',
38-
year: 'numeric',
39-
})
40-
return formatter.format(new Date(date))
41-
}
42-
4335
const getBorrowStatus = (borrow: Borrow) => {
4436
const now = new Date()
4537
const due = new Date(borrow.due_at)

app/books/new/page.tsx

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
import { Button } from '@/components/ui/button'
2-
import { Input } from '@/components/ui/input'
3-
import { createBook } from '@/lib/api/book'
4-
51
import {
62
Breadcrumb,
73
BreadcrumbItem,
@@ -11,29 +7,9 @@ import {
117
BreadcrumbSeparator,
128
} from '@/components/ui/breadcrumb'
139
import Link from 'next/link'
14-
import { redirect } from 'next/navigation'
10+
import { CreateBookForm } from '@/components/books/CreateBookForm'
1511

1612
export default function NewBook() {
17-
async function create(formData: FormData) {
18-
'use server'
19-
20-
const title = formData.get('title') as string
21-
const author = formData.get('author') as string
22-
const year = formData.get('year') as string
23-
const code = formData.get('code') as string
24-
const library_id = formData.get('library_id') as string
25-
26-
await createBook({
27-
title,
28-
author,
29-
year: Number(year),
30-
code,
31-
library_id,
32-
})
33-
34-
redirect('/books')
35-
}
36-
3713
return (
3814
<div className="space-y-4">
3915
<h1 className="text-2xl font-semibold">Books</h1>
@@ -56,15 +32,7 @@ export default function NewBook() {
5632
</BreadcrumbItem>
5733
</BreadcrumbList>
5834
</Breadcrumb>
59-
60-
<form action={create} className="space-y-4 md:max-w-[40%]">
61-
<Input name="title" placeholder="Title" required />
62-
<Input name="author" placeholder="Author" required />
63-
<Input name="year" type="number" placeholder="Year" required />
64-
<Input name="code" placeholder="Code" required />
65-
<Input name="library_id" placeholder="Library ID" required />
66-
<Button type="submit">Create</Button>
67-
</form>
35+
<CreateBookForm />
6836
</div>
6937
)
7038
}
Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
'use client'
2+
3+
import { zodResolver } from '@hookform/resolvers/zod'
4+
import { Check, ChevronsUpDown } from 'lucide-react'
5+
import { useForm } from 'react-hook-form'
6+
import { z } from 'zod'
7+
8+
import { cn } from '@/lib/utils'
9+
import { toast } from '@/components/hooks/use-toast'
10+
import { Button } from '@/components/ui/button'
11+
import {
12+
Command,
13+
CommandGroup,
14+
CommandInput,
15+
CommandItem,
16+
CommandList,
17+
} from '@/components/ui/command'
18+
import {
19+
Form,
20+
FormControl,
21+
FormField,
22+
FormItem,
23+
FormLabel,
24+
FormMessage,
25+
} from '@/components/ui/form'
26+
import {
27+
Popover,
28+
PopoverContent,
29+
PopoverTrigger,
30+
} from '@/components/ui/popover'
31+
import { useRouter } from 'next/navigation'
32+
import { getListLibraries } from '@/lib/api/library'
33+
import { Library } from '@/lib/types/library'
34+
import { useState, useEffect, useCallback } from 'react'
35+
import { Input } from '../ui/input'
36+
import { createBook } from '@/lib/api/book'
37+
38+
const FormSchema = z.object({
39+
title: z
40+
.string({
41+
required_error: 'Please enter book title.',
42+
})
43+
.nonempty(),
44+
author: z
45+
.string({
46+
required_error: 'Please enter author name.',
47+
})
48+
.nonempty(),
49+
year: z.coerce
50+
.number({
51+
required_error: 'Please enter year.',
52+
})
53+
.gt(1000, 'Please enter valid year.'),
54+
code: z
55+
.string({
56+
required_error: 'Please set a code for book.',
57+
})
58+
.nonempty(),
59+
library_id: z
60+
.string({
61+
required_error: 'Please select the library.',
62+
})
63+
.uuid(),
64+
})
65+
66+
export const CreateBookForm: React.FC = () => {
67+
const router = useRouter()
68+
69+
const form = useForm<z.infer<typeof FormSchema>>({
70+
resolver: zodResolver(FormSchema),
71+
defaultValues: {
72+
title: '',
73+
author: '',
74+
code: '',
75+
year: 0,
76+
library_id: '',
77+
},
78+
})
79+
80+
const [libQ, setLibQ] = useState('')
81+
const [libs, setLibs] = useState<Library[]>([])
82+
83+
useEffect(() => {
84+
getListLibraries({
85+
limit: 20,
86+
name: libQ,
87+
}).then((res) => {
88+
if ('error' in res) {
89+
toast({
90+
title: 'Error',
91+
description: res.message,
92+
})
93+
return
94+
}
95+
setLibs(res.data)
96+
})
97+
}, [libQ])
98+
99+
function onSubmit(data: z.infer<typeof FormSchema>) {
100+
createBook(data)
101+
.then(console.log)
102+
.then(() => {
103+
toast({
104+
title: 'Book Registered',
105+
})
106+
router.push('/books')
107+
})
108+
.catch((e) => {
109+
toast({
110+
title: 'Error',
111+
description: e?.error,
112+
variant: 'destructive',
113+
})
114+
})
115+
}
116+
117+
const onReset = useCallback(() => {
118+
form.reset()
119+
}, [form])
120+
121+
return (
122+
<div className="grid place-items-center">
123+
<Form {...form}>
124+
<form
125+
onSubmit={form.handleSubmit(onSubmit)}
126+
className="space-y-6 md:space-y-0 md:grid md:grid-cols-2 md:gap-2"
127+
>
128+
<FormField
129+
control={form.control}
130+
name="library_id"
131+
render={({ field }) => (
132+
<FormItem className="flex flex-col">
133+
<FormLabel>Library</FormLabel>
134+
<Popover>
135+
<PopoverTrigger asChild>
136+
<FormControl>
137+
<Button
138+
variant="outline"
139+
role="combobox"
140+
className={cn(
141+
'w-full justify-between',
142+
!field.value && 'text-muted-foreground'
143+
)}
144+
>
145+
{field.value
146+
? libs.find((lib) => lib.id === field.value)?.name
147+
: 'Select Library'}
148+
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
149+
</Button>
150+
</FormControl>
151+
</PopoverTrigger>
152+
<PopoverContent className="w-full p-0">
153+
<Command>
154+
<CommandInput
155+
onValueChange={setLibQ}
156+
value={libQ}
157+
placeholder="Search library name..."
158+
/>
159+
<CommandList>
160+
{/* <CommandEmpty>No user found.</CommandEmpty> */}
161+
<CommandGroup forceMount>
162+
{libs.map((lib) => (
163+
<CommandItem
164+
value={lib.id}
165+
key={lib.id}
166+
onSelect={() => {
167+
form.setValue('library_id', lib.id)
168+
}}
169+
>
170+
{lib.name}
171+
<Check
172+
className={cn(
173+
'ml-auto',
174+
lib.id === field.value
175+
? 'opacity-100'
176+
: 'opacity-0'
177+
)}
178+
/>
179+
</CommandItem>
180+
))}
181+
</CommandGroup>
182+
</CommandList>
183+
</Command>
184+
</PopoverContent>
185+
</Popover>
186+
{/* <FormDescription>
187+
This is the language that will be used in the dashboard.
188+
</FormDescription> */}
189+
<FormMessage />
190+
</FormItem>
191+
)}
192+
/>
193+
194+
<FormField
195+
control={form.control}
196+
name="title"
197+
render={({ field }) => (
198+
<FormItem className="flex flex-col">
199+
<FormLabel>Title</FormLabel>
200+
<Input
201+
placeholder="Title"
202+
// type="number"
203+
{...field}
204+
onChange={field.onChange}
205+
/>
206+
{/* <FormDescription>
207+
How much is the fine per day?
208+
</FormDescription> */}
209+
<FormMessage />
210+
</FormItem>
211+
)}
212+
/>
213+
214+
<FormField
215+
control={form.control}
216+
name="author"
217+
render={({ field }) => (
218+
<FormItem className="flex flex-col">
219+
<FormLabel>Author</FormLabel>
220+
<Input
221+
placeholder="Name"
222+
// type="number"
223+
{...field}
224+
onChange={field.onChange}
225+
/>
226+
{/* <FormDescription>
227+
How much is the fine per day?
228+
</FormDescription> */}
229+
<FormMessage />
230+
</FormItem>
231+
)}
232+
/>
233+
234+
<FormField
235+
control={form.control}
236+
name="year"
237+
render={({ field }) => (
238+
<FormItem className="flex flex-col">
239+
<FormLabel>Year</FormLabel>
240+
<Input
241+
placeholder="Year"
242+
// type="number"
243+
{...field}
244+
onChange={field.onChange}
245+
/>
246+
{/* <FormDescription>
247+
How much is the fine per day?
248+
</FormDescription> */}
249+
<FormMessage />
250+
</FormItem>
251+
)}
252+
/>
253+
254+
<FormField
255+
control={form.control}
256+
name="code"
257+
render={({ field }) => (
258+
<FormItem className="flex flex-col">
259+
<FormLabel>Code</FormLabel>
260+
<Input
261+
placeholder="Code"
262+
// type="number"
263+
{...field}
264+
onChange={field.onChange}
265+
/>
266+
{/* <FormDescription>
267+
How much is the fine per day?
268+
</FormDescription> */}
269+
<FormMessage />
270+
</FormItem>
271+
)}
272+
/>
273+
274+
<Button type="reset" variant="ghost" onClick={onReset}>
275+
Reset
276+
</Button>
277+
<Button disabled={form.formState.isSubmitting} type="submit">
278+
Create
279+
</Button>
280+
</form>
281+
</Form>
282+
</div>
283+
)
284+
}

0 commit comments

Comments
 (0)