|
1 | 1 | import { useState, useEffect, useCallback } from 'react'; |
2 | | -import { Search, Unplug, Server, Trash2, Download } from 'lucide-react'; |
| 2 | +import { Search, Unplug, Server, Trash2, Download, CornerDownLeft } from 'lucide-react'; |
3 | 3 |
|
4 | 4 | import { buildIdeConfigForPkg, buildIdeConfigForRemote } from '~/lib/ide-config'; |
5 | 5 | import { Card } from '~/components/ui/card'; |
@@ -288,33 +288,35 @@ export default function App() { |
288 | 288 | [registryUrl, resultsPerPage] |
289 | 289 | ); |
290 | 290 |
|
291 | | - // Fetch servers when search, apiUrl, filterDate, or resultsPerPage changes |
| 291 | + // Fetch servers when apiUrl, filterDate, or resultsPerPage changes (not on every keystroke). |
| 292 | + // Use `performSearch` to trigger searches from the UI (Enter or button). |
292 | 293 | useEffect(() => { |
| 294 | + // Reset pagination and fetch for the currently confirmed `search` value. |
293 | 295 | setCurrentCursor(null); |
294 | 296 | setPreviousCursors([]); |
295 | 297 | setNextCursor(null); |
296 | 298 | setCurrentPage(1); |
297 | | - // Reset page cursor map for the new search / filters / settings. |
| 299 | + // Reset page cursor map for the new filters / settings. |
298 | 300 | setPageCursors({ 1: null }); |
299 | 301 | fetchServers(search, null, filterDate); |
300 | | - }, [search, registryUrl, filterDate, resultsPerPage, fetchServers]); |
301 | | - |
302 | | - // // Initialize Orama (client-side) on mount |
303 | | - // useEffect(() => { |
304 | | - // let mounted = true; |
305 | | - // (async () => { |
306 | | - // try { |
307 | | - // if (typeof window === 'undefined') return; |
308 | | - // await initOrama(); |
309 | | - // if (!mounted) return; |
310 | | - // } catch (err) { |
311 | | - // // ignore |
312 | | - // } |
313 | | - // })(); |
314 | | - // return () => { |
315 | | - // mounted = false; |
316 | | - // }; |
317 | | - // }, []); |
| 302 | + // NOTE: we intentionally omit `search` from the dependency list so typing into |
| 303 | + // the input doesn't trigger a fetch on every keystroke. Searches are triggered |
| 304 | + // explicitly via `performSearch` which updates `search` and performs the fetch. |
| 305 | + // eslint-disable-next-line react-hooks/exhaustive-deps |
| 306 | + }, [registryUrl, filterDate, resultsPerPage, fetchServers]); |
| 307 | + |
| 308 | + // Helper to perform a confirmed search (called on Enter or when clicking the search button) |
| 309 | + const performSearch = (newSearch: string) => { |
| 310 | + // Update the confirmed `search` state which also keeps URL in sync via the existing effect. |
| 311 | + setSearch(newSearch); |
| 312 | + // If the query differs from the current confirmed search, reset pagination and fetch |
| 313 | + setCurrentCursor(null); |
| 314 | + setPreviousCursors([]); |
| 315 | + setNextCursor(null); |
| 316 | + setCurrentPage(1); |
| 317 | + setPageCursors({ 1: null }); |
| 318 | + fetchServers(newSearch, null, filterDate); |
| 319 | + }; |
318 | 320 |
|
319 | 321 | const handleNext = () => { |
320 | 322 | if (nextCursor) { |
@@ -551,21 +553,38 @@ export default function App() { |
551 | 553 | <div className="relative flex gap-2 items-center"> |
552 | 554 | <div className="relative flex-1"> |
553 | 555 | <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> |
554 | | - <input |
555 | | - type="text" |
556 | | - placeholder="Search MCP servers by name" |
557 | | - value={search} |
558 | | - onChange={(e) => setSearch(e.target.value)} |
559 | | - className="w-full rounded-lg border border-input bg-background px-10 py-3 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2" |
560 | | - /> |
| 556 | + <div className="flex"> |
| 557 | + <input |
| 558 | + type="text" |
| 559 | + placeholder="Search MCP servers by name" |
| 560 | + value={search} |
| 561 | + onChange={(e) => setSearch(e.target.value)} |
| 562 | + onKeyDown={(e) => { |
| 563 | + if (e.key === 'Enter') { |
| 564 | + e.preventDefault(); |
| 565 | + performSearch(search); |
| 566 | + } |
| 567 | + }} |
| 568 | + className="w-full rounded-lg border border-input bg-background px-10 py-3 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2" |
| 569 | + /> |
| 570 | + <Button |
| 571 | + variant="outline" |
| 572 | + size="sm" |
| 573 | + onClick={() => performSearch(search)} |
| 574 | + className="-ml-10 mr-2 self-center h-5 w-8 px-2 text-muted-foreground/80" |
| 575 | + aria-label="Search" |
| 576 | + > |
| 577 | + <CornerDownLeft className="h-3.5 w-3.5" /> |
| 578 | + </Button> |
| 579 | + </div> |
561 | 580 | </div> |
562 | 581 | {/* Filter Date Button */} |
563 | 582 | <DatePicker |
564 | 583 | date={filterDate} |
565 | 584 | onDateChange={setFilterDate} |
566 | 585 | placeholder="Filter by date" |
567 | 586 | variant={filterDate ? 'default' : 'outline'} |
568 | | - className="h-auto py-3 px-4" |
| 587 | + className="h-auto py-3 px-4 text-muted-foreground" |
569 | 588 | /> |
570 | 589 | {/* Results Per Page Selector */} |
571 | 590 | <DropdownMenu> |
|
0 commit comments