aboutsummaryrefslogtreecommitdiffstats
path: root/packages/web/app
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2024-03-01 21:01:00 +0000
committerMohamedBassem <me@mbassem.com>2024-03-01 22:11:49 +0000
commita5434730ede1272f195d6a4b13207b840a5ac2cf (patch)
tree14c8a22fbf573b36f16a434349fd3516b38ea539 /packages/web/app
parent75d315dda4232ee3b89abf054f0b6ee10105ffe3 (diff)
downloadkarakeep-a5434730ede1272f195d6a4b13207b840a5ac2cf.tar.zst
feature: Add full text search support
Diffstat (limited to 'packages/web/app')
-rw-r--r--packages/web/app/dashboard/components/Sidebar.tsx18
-rw-r--r--packages/web/app/dashboard/search/page.tsx93
2 files changed, 110 insertions, 1 deletions
diff --git a/packages/web/app/dashboard/components/Sidebar.tsx b/packages/web/app/dashboard/components/Sidebar.tsx
index 7eea6b6d..010ee103 100644
--- a/packages/web/app/dashboard/components/Sidebar.tsx
+++ b/packages/web/app/dashboard/components/Sidebar.tsx
@@ -1,4 +1,12 @@
-import { Archive, Star, Tag, Home, PackageOpen, Settings } from "lucide-react";
+import {
+ Archive,
+ Star,
+ Tag,
+ Home,
+ PackageOpen,
+ Settings,
+ Search,
+} from "lucide-react";
import { redirect } from "next/navigation";
import SidebarItem from "./SidebarItem";
import { getServerAuthSession } from "@/server/auth";
@@ -6,6 +14,7 @@ import Link from "next/link";
import SidebarProfileOptions from "./SidebarProfileOptions";
import { Separator } from "@/components/ui/separator";
import AllLists from "./AllLists";
+import serverConfig from "@hoarder/shared/config";
export default async function Sidebar() {
const session = await getServerAuthSession();
@@ -34,6 +43,13 @@ export default async function Sidebar() {
name="Favourites"
path="/dashboard/bookmarks/favourites"
/>
+ {serverConfig.meilisearch && (
+ <SidebarItem
+ logo={<Search />}
+ name="Search"
+ path="/dashboard/search"
+ />
+ )}
<SidebarItem
logo={<Archive />}
name="Archive"
diff --git a/packages/web/app/dashboard/search/page.tsx b/packages/web/app/dashboard/search/page.tsx
new file mode 100644
index 00000000..1c26608e
--- /dev/null
+++ b/packages/web/app/dashboard/search/page.tsx
@@ -0,0 +1,93 @@
+"use client";
+
+import { api } from "@/lib/trpc";
+import { usePathname, useRouter, useSearchParams } from "next/navigation";
+import BookmarksGrid from "../bookmarks/components/BookmarksGrid";
+import { Input } from "@/components/ui/input";
+import Loading from "../bookmarks/loading";
+import { keepPreviousData } from "@tanstack/react-query";
+import { Search } from "lucide-react";
+import { ActionButton } from "@/components/ui/action-button";
+import { Suspense, useRef } from "react";
+
+function SearchComp() {
+ const router = useRouter();
+ const pathname = usePathname();
+ const searchParams = useSearchParams();
+ const searchQuery = searchParams.get("q") || "";
+
+ const { data, isPending, isPlaceholderData, error } =
+ api.bookmarks.searchBookmarks.useQuery(
+ {
+ text: searchQuery,
+ },
+ {
+ placeholderData: keepPreviousData,
+ },
+ );
+
+ if (error) {
+ throw error;
+ }
+
+ const inputRef: React.MutableRefObject<HTMLInputElement | null> =
+ useRef<HTMLInputElement | null>(null);
+
+ let timeoutId: NodeJS.Timeout | undefined;
+
+ // Debounce user input
+ const doSearch = () => {
+ if (!inputRef.current) {
+ return;
+ }
+ router.replace(`${pathname}?q=${inputRef.current.value}`);
+ };
+
+ const onInputChange = () => {
+ if (timeoutId) {
+ clearTimeout(timeoutId);
+ }
+ timeoutId = setTimeout(() => {
+ doSearch();
+ }, 200);
+ };
+
+ return (
+ <div className="container flex flex-col gap-3 p-4">
+ <div className="flex gap-2">
+ <Input
+ ref={inputRef}
+ placeholder="Search"
+ defaultValue={searchQuery}
+ onChange={onInputChange}
+ />
+ <ActionButton
+ loading={isPending || isPlaceholderData}
+ onClick={doSearch}
+ >
+ <span className="flex gap-2">
+ <Search />
+ <span className="my-auto">Search</span>
+ </span>
+ </ActionButton>
+ </div>
+ <hr />
+ {data ? (
+ <BookmarksGrid
+ query={{ ids: data.bookmarks.map((b) => b.id) }}
+ bookmarks={data.bookmarks}
+ />
+ ) : (
+ <Loading />
+ )}
+ </div>
+ );
+}
+
+export default function SearchPage() {
+ return (
+ <Suspense>
+ <SearchComp />
+ </Suspense>
+ );
+}