From ed6a3bfac52437c0b86767d7a17dc1ae48d8ccb2 Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Sun, 23 Nov 2025 10:13:28 +0000 Subject: feat: Add search bar to highlights page (#2155) * feat: Add search bar to All highlights page This commit adds a search bar to the "All highlights page" that allows users to search their highlights by text content or notes. Changes: - Added search method to Highlight model with SQL LIKE query on text and note fields - Added search endpoint to highlights router with pagination support - Updated AllHighlights component to include search input with debouncing - Search input includes clear button and search icon - Maintains existing infinite scroll pagination for search results Technical details: - Uses SQL ilike for case-insensitive search - 300ms debounce to reduce API calls - Conditionally uses search or getAll endpoint based on search query * fix db query * small fixes --------- Co-authored-by: Claude --- .../dashboard/highlights/AllHighlights.tsx | 127 ++++++++++++++------- 1 file changed, 87 insertions(+), 40 deletions(-) (limited to 'apps') diff --git a/apps/web/components/dashboard/highlights/AllHighlights.tsx b/apps/web/components/dashboard/highlights/AllHighlights.tsx index 23fa51d2..928f4e05 100644 --- a/apps/web/components/dashboard/highlights/AllHighlights.tsx +++ b/apps/web/components/dashboard/highlights/AllHighlights.tsx @@ -1,17 +1,19 @@ "use client"; -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; import Link from "next/link"; import { ActionButton } from "@/components/ui/action-button"; +import { Input } from "@/components/ui/input"; import useRelativeTime from "@/lib/hooks/relative-time"; import { api } from "@/lib/trpc"; import { Separator } from "@radix-ui/react-dropdown-menu"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; -import { Dot, LinkIcon } from "lucide-react"; +import { Dot, LinkIcon, Search, X } from "lucide-react"; import { useTranslation } from "react-i18next"; import { useInView } from "react-intersection-observer"; +import { useDebounce } from "@karakeep/shared-react/hooks/use-debounce"; import { ZGetAllHighlightsResponse, ZHighlight, @@ -48,18 +50,38 @@ export default function AllHighlights({ highlights: ZGetAllHighlightsResponse; }) { const { t } = useTranslation(); + const [searchInput, setSearchInput] = useState(""); + const debouncedSearch = useDebounce(searchInput, 300); + + // Use search endpoint if searchQuery is provided, otherwise use getAll + const useSearchQuery = debouncedSearch.trim().length > 0; + + const getAllQuery = api.highlights.getAll.useInfiniteQuery( + {}, + { + enabled: !useSearchQuery, + initialData: !useSearchQuery + ? () => ({ + pages: [initialHighlights], + pageParams: [null], + }) + : undefined, + initialCursor: null, + getNextPageParam: (lastPage) => lastPage.nextCursor, + }, + ); + + const searchQueryResult = api.highlights.search.useInfiniteQuery( + { text: debouncedSearch }, + { + enabled: useSearchQuery, + initialCursor: null, + getNextPageParam: (lastPage) => lastPage.nextCursor, + }, + ); + const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = - api.highlights.getAll.useInfiniteQuery( - {}, - { - initialData: () => ({ - pages: [initialHighlights], - pageParams: [null], - }), - initialCursor: null, - getNextPageParam: (lastPage) => lastPage.nextCursor, - }, - ); + useSearchQuery ? searchQueryResult : getAllQuery; const { ref: loadMoreRef, inView: loadMoreButtonInView } = useInView(); useEffect(() => { @@ -71,33 +93,58 @@ export default function AllHighlights({ const allHighlights = data?.pages.flatMap((p) => p.highlights); return ( -
- {allHighlights && - allHighlights.length > 0 && - allHighlights.map((h) => ( - - - - - ))} - {allHighlights && allHighlights.length == 0 && ( -

- {t("highlights.no_highlights")} -

- )} - {hasNextPage && ( -
- fetchNextPage()} - variant="ghost" - > - Load More - -
- )} +
+ {/* Search Input */} +
+ setSearchInput(e.target.value)} + startIcon={} + endIcon={ + searchInput && ( + + ) + } + className="w-full" + /> +
+ + {/* Results */} +
+ {allHighlights && + allHighlights.length > 0 && + allHighlights.map((h) => ( + + + + + ))} + {allHighlights && allHighlights.length == 0 && ( +

+ {t("highlights.no_highlights")} +

+ )} + {hasNextPage && ( +
+ fetchNextPage()} + variant="ghost" + > + Load More + +
+ )} +
); } -- cgit v1.2.3-70-g09d2