Skip to content

Commit ca88889

Browse files
committed
feat: book import template download
1 parent 85bb77b commit ca88889

File tree

4 files changed

+496
-4
lines changed

4 files changed

+496
-4
lines changed

app/(protected)/admin/books/import/layout.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010
import { Button } from '@/components/ui/button'
1111
import { Verify } from '@/lib/firebase/firebase'
1212
import { Download, FileText } from 'lucide-react'
13+
import { DownloadTemplateButton } from '@/components/books/DownloadTemplateButton'
14+
import { cookies } from 'next/headers'
1315

1416
export default async function BorrowDetailsLayout({
1517
children,
@@ -20,6 +22,10 @@ export default async function BorrowDetailsLayout({
2022
}>) {
2123
const headers = await Verify({ from: `/admin/books/import` })
2224

25+
const cookieStore = await cookies()
26+
const cookieName = process.env.LIBRARY_COOKIE_NAME as string
27+
const libraryId = cookieStore.get(cookieName)?.value
28+
2329
return (
2430
<div className="space-y-4">
2531
<nav className="backdrop-blur-sm sticky top-0 z-10">
@@ -63,10 +69,7 @@ export default async function BorrowDetailsLayout({
6369
<strong>year</strong> - Year of publication (optional)
6470
</li>
6571
</ul>
66-
<Button variant="link" className="p-0 h-auto">
67-
<Download className="h-3 w-3 mr-1" />
68-
Download template
69-
</Button>
72+
<DownloadTemplateButton libraryId={libraryId} />
7073
</div>
7174
</AlertDescription>
7275
</Alert>
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
'use client'
2+
3+
import { useState, useEffect, useCallback } from 'react'
4+
import { Check, ChevronsUpDown, X } from 'lucide-react'
5+
import { cn } from '@/lib/utils'
6+
import { Button } from '@/components/ui/button'
7+
import {
8+
Command,
9+
CommandEmpty,
10+
CommandGroup,
11+
CommandInput,
12+
CommandItem,
13+
CommandList,
14+
} from '@/components/ui/command'
15+
import {
16+
Popover,
17+
PopoverContent,
18+
PopoverTrigger,
19+
} from '@/components/ui/popover'
20+
import { Badge } from '@/components/ui/badge'
21+
import { getListBooks } from '@/lib/api/book'
22+
import { Book } from '@/lib/types/book'
23+
24+
interface BookMultiSelectProps {
25+
libraryId?: string
26+
selectedIds: string[]
27+
onSelectionChange: (ids: string[]) => void
28+
}
29+
30+
export const BookMultiSelect: React.FC<BookMultiSelectProps> = ({
31+
libraryId,
32+
selectedIds,
33+
onSelectionChange,
34+
}) => {
35+
const [open, setOpen] = useState(false)
36+
const [searchQuery, setSearchQuery] = useState('')
37+
const [books, setBooks] = useState<Book[]>([])
38+
const [selectedBooks, setSelectedBooks] = useState<Book[]>([])
39+
const [loading, setLoading] = useState(false)
40+
41+
// Fetch books based on search query
42+
const fetchBooks = useCallback(
43+
async (query: string) => {
44+
if (!libraryId) return
45+
46+
setLoading(true)
47+
try {
48+
const res = await getListBooks({
49+
library_id: libraryId,
50+
title: query,
51+
limit: 50,
52+
})
53+
54+
if ('data' in res) {
55+
setBooks(res.data)
56+
}
57+
} catch (error) {
58+
console.error('Error fetching books:', error)
59+
} finally {
60+
setLoading(false)
61+
}
62+
},
63+
[libraryId]
64+
)
65+
66+
// Debounced search
67+
useEffect(() => {
68+
const timer = setTimeout(() => {
69+
fetchBooks(searchQuery)
70+
}, 300)
71+
72+
return () => clearTimeout(timer)
73+
}, [searchQuery, fetchBooks])
74+
75+
// Fetch selected books details
76+
useEffect(() => {
77+
const fetchSelectedBooks = async () => {
78+
if (selectedIds.length === 0 || !libraryId) {
79+
setSelectedBooks([])
80+
return
81+
}
82+
83+
try {
84+
const res = await getListBooks({
85+
library_id: libraryId,
86+
id: selectedIds.join(','),
87+
limit: selectedIds.length,
88+
})
89+
90+
if ('data' in res) {
91+
setSelectedBooks(res.data)
92+
}
93+
} catch (error) {
94+
console.error('Error fetching selected books:', error)
95+
}
96+
}
97+
98+
fetchSelectedBooks()
99+
}, [selectedIds, libraryId])
100+
101+
const toggleBook = (bookId: string) => {
102+
const newSelectedIds = selectedIds.includes(bookId)
103+
? selectedIds.filter((id) => id !== bookId)
104+
: [...selectedIds, bookId]
105+
106+
onSelectionChange(newSelectedIds)
107+
}
108+
109+
const removeBook = (bookId: string) => {
110+
onSelectionChange(selectedIds.filter((id) => id !== bookId))
111+
}
112+
113+
return (
114+
<div className="space-y-2">
115+
<Popover open={open} onOpenChange={setOpen}>
116+
<PopoverTrigger asChild>
117+
<Button
118+
variant="outline"
119+
role="combobox"
120+
aria-expanded={open}
121+
className="w-full justify-between"
122+
>
123+
<span className="truncate">
124+
{selectedIds.length > 0
125+
? `${selectedIds.length} selected`
126+
: 'Search and select books...'}
127+
</span>
128+
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
129+
</Button>
130+
</PopoverTrigger>
131+
<PopoverContent className="w-full p-0" align="start">
132+
<Command shouldFilter={false}>
133+
<CommandInput
134+
placeholder="Search books by title..."
135+
value={searchQuery}
136+
onValueChange={setSearchQuery}
137+
/>
138+
<CommandList>
139+
<CommandEmpty>
140+
{loading ? 'Searching...' : 'No books found'}
141+
</CommandEmpty>
142+
<CommandGroup>
143+
{books.map((book) => (
144+
<CommandItem
145+
key={book.id}
146+
value={book.id}
147+
onSelect={() => toggleBook(book.id)}
148+
>
149+
<Check
150+
className={cn(
151+
'mr-2 h-4 w-4',
152+
selectedIds.includes(book.id)
153+
? 'opacity-100'
154+
: 'opacity-0'
155+
)}
156+
/>
157+
<div className="flex-1 truncate">
158+
<div className="font-medium truncate">{book.title}</div>
159+
<div className="text-xs text-muted-foreground">
160+
{book.author}{book.code}
161+
</div>
162+
</div>
163+
</CommandItem>
164+
))}
165+
</CommandGroup>
166+
</CommandList>
167+
</Command>
168+
</PopoverContent>
169+
</Popover>
170+
171+
{/* Selected books badges */}
172+
{selectedBooks.length > 0 && (
173+
<div className="flex flex-wrap gap-1">
174+
{selectedBooks.map((book) => (
175+
<Badge
176+
key={book.id}
177+
variant="secondary"
178+
className="text-xs gap-1 pr-1"
179+
>
180+
<span className="truncate max-w-[200px]">{book.title}</span>
181+
<button
182+
type="button"
183+
onClick={() => removeBook(book.id)}
184+
className="ml-1 rounded-full hover:bg-muted p-0.5"
185+
>
186+
<X className="h-3 w-3" />
187+
</button>
188+
</Badge>
189+
))}
190+
</div>
191+
)}
192+
</div>
193+
)
194+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'use client'
2+
3+
import { ModalDownloadTemplate } from '@/components/books/ModalDownloadTemplate'
4+
5+
interface DownloadTemplateButtonProps {
6+
libraryId?: string
7+
}
8+
9+
export const DownloadTemplateButton: React.FC<DownloadTemplateButtonProps> = ({
10+
libraryId,
11+
}) => {
12+
return <ModalDownloadTemplate libraryId={libraryId} />
13+
}

0 commit comments

Comments
 (0)