Skip to content

Commit 1deda79

Browse files
committed
feat: Enhance Library component with search, selection, and batch deletion capabilities
1 parent 41b258e commit 1deda79

File tree

2 files changed

+330
-18
lines changed

2 files changed

+330
-18
lines changed

src/app/library/page.tsx

Lines changed: 212 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
'use client';
22

33
import DeleteChat from '@/components/DeleteChat';
4+
import BatchDeleteChats from '@/components/BatchDeleteChats';
45
import { cn, formatTimeDifference } from '@/lib/utils';
5-
import { BookOpenText, ClockIcon, Delete, ScanEye } from 'lucide-react';
6+
import { BookOpenText, Check, ClockIcon, Delete, ScanEye, Search, X } from 'lucide-react';
67
import Link from 'next/link';
78
import { useEffect, useState } from 'react';
9+
import { toast } from 'sonner';
810

911
export interface Chat {
1012
id: string;
@@ -15,7 +17,13 @@ export interface Chat {
1517

1618
const Page = () => {
1719
const [chats, setChats] = useState<Chat[]>([]);
20+
const [filteredChats, setFilteredChats] = useState<Chat[]>([]);
1821
const [loading, setLoading] = useState(true);
22+
const [searchQuery, setSearchQuery] = useState('');
23+
const [selectionMode, setSelectionMode] = useState(false);
24+
const [selectedChats, setSelectedChats] = useState<string[]>([]);
25+
const [hoveredChatId, setHoveredChatId] = useState<string | null>(null);
26+
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
1927

2028
useEffect(() => {
2129
const fetchChats = async () => {
@@ -31,12 +39,71 @@ const Page = () => {
3139
const data = await res.json();
3240

3341
setChats(data.chats);
42+
setFilteredChats(data.chats);
3443
setLoading(false);
3544
};
3645

3746
fetchChats();
3847
}, []);
3948

49+
useEffect(() => {
50+
if (searchQuery.trim() === '') {
51+
setFilteredChats(chats);
52+
} else {
53+
const filtered = chats.filter((chat) =>
54+
chat.title.toLowerCase().includes(searchQuery.toLowerCase())
55+
);
56+
setFilteredChats(filtered);
57+
}
58+
}, [searchQuery, chats]);
59+
60+
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
61+
setSearchQuery(e.target.value);
62+
};
63+
64+
const clearSearch = () => {
65+
setSearchQuery('');
66+
};
67+
68+
const toggleSelectionMode = () => {
69+
setSelectionMode(!selectionMode);
70+
setSelectedChats([]);
71+
};
72+
73+
const toggleChatSelection = (chatId: string) => {
74+
if (selectedChats.includes(chatId)) {
75+
setSelectedChats(selectedChats.filter(id => id !== chatId));
76+
} else {
77+
setSelectedChats([...selectedChats, chatId]);
78+
}
79+
};
80+
81+
const selectAllChats = () => {
82+
if (selectedChats.length === filteredChats.length) {
83+
setSelectedChats([]);
84+
} else {
85+
setSelectedChats(filteredChats.map(chat => chat.id));
86+
}
87+
};
88+
89+
const deleteSelectedChats = () => {
90+
if (selectedChats.length === 0) return;
91+
setIsDeleteDialogOpen(true);
92+
};
93+
94+
const handleBatchDeleteComplete = () => {
95+
setSelectedChats([]);
96+
setSelectionMode(false);
97+
};
98+
99+
const updateChatsAfterDelete = (newChats: Chat[]) => {
100+
setChats(newChats);
101+
setFilteredChats(newChats.filter(chat =>
102+
searchQuery.trim() === '' ||
103+
chat.title.toLowerCase().includes(searchQuery.toLowerCase())
104+
));
105+
};
106+
40107
return loading ? (
41108
<div className="flex flex-row items-center justify-center min-h-screen">
42109
<svg
@@ -64,49 +131,176 @@ const Page = () => {
64131
<h1 className="text-3xl font-medium p-2">Library</h1>
65132
</div>
66133
<hr className="border-t border-[#2B2C2C] my-4 w-full" />
134+
135+
{/* Search Box */}
136+
<div className="relative mt-6 mb-6">
137+
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
138+
<Search className="w-5 h-5 text-black/50 dark:text-white/50" />
139+
</div>
140+
<input
141+
type="text"
142+
className="block w-full p-2 pl-10 pr-10 bg-light-secondary dark:bg-dark-secondary border border-light-200 dark:border-dark-200 rounded-md text-black dark:text-white focus:outline-none focus:ring-1 focus:ring-blue-500"
143+
placeholder="Search your threads..."
144+
value={searchQuery}
145+
onChange={handleSearchChange}
146+
/>
147+
{searchQuery && (
148+
<button
149+
onClick={clearSearch}
150+
className="absolute inset-y-0 right-0 flex items-center pr-3"
151+
>
152+
<X className="w-5 h-5 text-black/50 dark:text-white/50 hover:text-black dark:hover:text-white" />
153+
</button>
154+
)}
155+
</div>
156+
157+
{/* Thread Count and Selection Controls */}
158+
<div className="mb-4">
159+
{!selectionMode ? (
160+
<div className="flex items-center justify-between">
161+
<div className="text-black/70 dark:text-white/70">
162+
You have {chats.length} threads in Perplexica
163+
</div>
164+
<button
165+
onClick={toggleSelectionMode}
166+
className="text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white text-sm transition duration-200"
167+
>
168+
Select
169+
</button>
170+
</div>
171+
) : (
172+
<div className="flex items-center justify-between">
173+
<div className="text-black/70 dark:text-white/70">
174+
{selectedChats.length} selected thread{selectedChats.length !== 1 ? 's' : ''}
175+
</div>
176+
<div className="flex space-x-4">
177+
<button
178+
onClick={selectAllChats}
179+
className="text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white text-sm transition duration-200"
180+
>
181+
{selectedChats.length === filteredChats.length ? 'Deselect all' : 'Select all'}
182+
</button>
183+
184+
<button
185+
onClick={toggleSelectionMode}
186+
className="text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white text-sm transition duration-200"
187+
>
188+
Cancel
189+
</button>
190+
191+
<button
192+
onClick={deleteSelectedChats}
193+
disabled={selectedChats.length === 0}
194+
className={cn(
195+
"text-sm transition duration-200",
196+
selectedChats.length === 0
197+
? "text-red-400/50 hover:text-red-500/50 cursor-not-allowed"
198+
: "text-red-400 hover:text-red-500 cursor-pointer"
199+
)}
200+
>
201+
Delete Selected
202+
</button>
203+
</div>
204+
</div>
205+
)}
206+
</div>
67207
</div>
68-
{chats.length === 0 && (
69-
<div className="flex flex-row items-center justify-center min-h-screen">
208+
209+
{filteredChats.length === 0 && (
210+
<div className="flex flex-row items-center justify-center min-h-[50vh]">
70211
<p className="text-black/70 dark:text-white/70 text-sm">
71-
No chats found.
212+
{searchQuery ? 'No threads found matching your search.' : 'No threads found.'}
72213
</p>
73214
</div>
74215
)}
75-
{chats.length > 0 && (
216+
217+
{filteredChats.length > 0 && (
76218
<div className="flex flex-col pb-20 lg:pb-2">
77-
{chats.map((chat, i) => (
219+
{filteredChats.map((chat, i) => (
78220
<div
79221
className={cn(
80222
'flex flex-col space-y-4 py-6',
81-
i !== chats.length - 1
223+
i !== filteredChats.length - 1
82224
? 'border-b border-white-200 dark:border-dark-200'
83225
: '',
84226
)}
85227
key={i}
228+
onMouseEnter={() => setHoveredChatId(chat.id)}
229+
onMouseLeave={() => setHoveredChatId(null)}
86230
>
87-
<Link
88-
href={`/c/${chat.id}`}
89-
className="text-black dark:text-white lg:text-xl font-medium truncate transition duration-200 hover:text-[#24A0ED] dark:hover:text-[#24A0ED] cursor-pointer"
90-
>
91-
{chat.title}
92-
</Link>
231+
<div className="flex items-center">
232+
{/* Checkbox - visible when in selection mode or when hovering */}
233+
{(selectionMode || hoveredChatId === chat.id) && (
234+
<div
235+
className="mr-3 cursor-pointer"
236+
onClick={(e) => {
237+
e.preventDefault();
238+
if (!selectionMode) setSelectionMode(true);
239+
toggleChatSelection(chat.id);
240+
}}
241+
>
242+
<div className={cn(
243+
"w-5 h-5 border rounded flex items-center justify-center transition-colors",
244+
selectedChats.includes(chat.id)
245+
? "bg-blue-500 border-blue-500"
246+
: "border-gray-400 dark:border-gray-600"
247+
)}>
248+
{selectedChats.includes(chat.id) && (
249+
<Check className="w-4 h-4 text-white" />
250+
)}
251+
</div>
252+
</div>
253+
)}
254+
255+
{/* Chat Title */}
256+
<Link
257+
href={`/c/${chat.id}`}
258+
className={cn(
259+
"text-black dark:text-white lg:text-xl font-medium truncate transition duration-200 hover:text-[#24A0ED] dark:hover:text-[#24A0ED] cursor-pointer",
260+
selectionMode && "pointer-events-none text-black dark:text-white hover:text-black dark:hover:text-white"
261+
)}
262+
onClick={(e) => {
263+
if (selectionMode) {
264+
e.preventDefault();
265+
toggleChatSelection(chat.id);
266+
}
267+
}}
268+
>
269+
{chat.title}
270+
</Link>
271+
</div>
272+
93273
<div className="flex flex-row items-center justify-between w-full">
94274
<div className="flex flex-row items-center space-x-1 lg:space-x-1.5 text-black/70 dark:text-white/70">
95275
<ClockIcon size={15} />
96276
<p className="text-xs">
97277
{formatTimeDifference(new Date(), chat.createdAt)} Ago
98278
</p>
99279
</div>
100-
<DeleteChat
101-
chatId={chat.id}
102-
chats={chats}
103-
setChats={setChats}
104-
/>
280+
281+
{/* Delete button - only visible when not in selection mode */}
282+
{!selectionMode && (
283+
<DeleteChat
284+
chatId={chat.id}
285+
chats={chats}
286+
setChats={updateChatsAfterDelete}
287+
/>
288+
)}
105289
</div>
106290
</div>
107291
))}
108292
</div>
109293
)}
294+
295+
{/* Batch Delete Confirmation Dialog */}
296+
<BatchDeleteChats
297+
chatIds={selectedChats}
298+
chats={chats}
299+
setChats={updateChatsAfterDelete}
300+
onComplete={handleBatchDeleteComplete}
301+
isOpen={isDeleteDialogOpen}
302+
setIsOpen={setIsDeleteDialogOpen}
303+
/>
110304
</div>
111305
);
112306
};

0 commit comments

Comments
 (0)