diff options
Diffstat (limited to 'apps')
| -rw-r--r-- | apps/web/components/dashboard/search/QueryExplainerTooltip.tsx | 98 | ||||
| -rw-r--r-- | apps/web/components/dashboard/search/SearchInput.tsx | 27 | ||||
| -rw-r--r-- | apps/web/lib/hooks/bookmark-search.ts | 20 |
3 files changed, 128 insertions, 17 deletions
diff --git a/apps/web/components/dashboard/search/QueryExplainerTooltip.tsx b/apps/web/components/dashboard/search/QueryExplainerTooltip.tsx new file mode 100644 index 00000000..191c9ff3 --- /dev/null +++ b/apps/web/components/dashboard/search/QueryExplainerTooltip.tsx @@ -0,0 +1,98 @@ +import InfoTooltip from "@/components/ui/info-tooltip"; +import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table"; + +import { TextAndMatcher } from "@hoarder/shared/searchQueryParser"; +import { Matcher } from "@hoarder/shared/types/search"; + +export default function QueryExplainerTooltip({ + parsedSearchQuery, + className, +}: { + parsedSearchQuery: TextAndMatcher & { result: string }; + className?: string; +}) { + if (parsedSearchQuery.result == "invalid") { + return null; + } + + const MatcherComp = ({ matcher }: { matcher: Matcher }) => { + switch (matcher.type) { + case "tagName": + return ( + <TableRow> + <TableCell>Tag Name</TableCell> + <TableCell>{matcher.tagName}</TableCell> + </TableRow> + ); + case "listName": + return ( + <TableRow> + <TableCell>List Name</TableCell> + <TableCell>{matcher.listName}</TableCell> + </TableRow> + ); + case "dateAfter": + return ( + <TableRow> + <TableCell>Created After</TableCell> + <TableCell>{matcher.dateAfter.toDateString()}</TableCell> + </TableRow> + ); + case "dateBefore": + return ( + <TableRow> + <TableCell>Created Before</TableCell> + <TableCell>{matcher.dateBefore.toDateString()}</TableCell> + </TableRow> + ); + case "favourited": + return ( + <TableRow> + <TableCell>Favourited</TableCell> + <TableCell>{matcher.favourited.toString()}</TableCell> + </TableRow> + ); + case "archived": + return ( + <TableRow> + <TableCell>Archived</TableCell> + <TableCell>{matcher.archived.toString()}</TableCell> + </TableRow> + ); + case "and": + case "or": + return ( + <TableRow> + <TableCell className="capitalize">{matcher.type}</TableCell> + <TableCell> + <Table> + <TableBody> + {matcher.matchers.map((m, i) => ( + <MatcherComp key={i} matcher={m} /> + ))} + </TableBody> + </Table> + </TableCell> + </TableRow> + ); + } + }; + + return ( + <InfoTooltip className={className}> + <Table> + <TableBody> + {parsedSearchQuery.text && ( + <TableRow> + <TableCell>Text</TableCell> + <TableCell>{parsedSearchQuery.text}</TableCell> + </TableRow> + )} + {parsedSearchQuery.matcher && ( + <MatcherComp matcher={parsedSearchQuery.matcher} /> + )} + </TableBody> + </Table> + </InfoTooltip> + ); +} diff --git a/apps/web/components/dashboard/search/SearchInput.tsx b/apps/web/components/dashboard/search/SearchInput.tsx index 55f304e3..8ed2ea3c 100644 --- a/apps/web/components/dashboard/search/SearchInput.tsx +++ b/apps/web/components/dashboard/search/SearchInput.tsx @@ -4,6 +4,9 @@ import React, { useEffect, useImperativeHandle, useRef } from "react"; import { Input } from "@/components/ui/input"; import { useDoBookmarkSearch } from "@/lib/hooks/bookmark-search"; import { useTranslation } from "@/lib/i18n/client"; +import { cn } from "@/lib/utils"; + +import QueryExplainerTooltip from "./QueryExplainerTooltip"; function useFocusSearchOnKeyPress( inputRef: React.RefObject<HTMLInputElement>, @@ -47,7 +50,8 @@ const SearchInput = React.forwardRef< React.HTMLAttributes<HTMLInputElement> & { loading?: boolean } >(({ className, ...props }, ref) => { const { t } = useTranslation(); - const { debounceSearch, searchQuery, isInSearchPage } = useDoBookmarkSearch(); + const { debounceSearch, searchQuery, parsedSearchQuery, isInSearchPage } = + useDoBookmarkSearch(); const [value, setValue] = React.useState(searchQuery); @@ -67,14 +71,19 @@ const SearchInput = React.forwardRef< }, [isInSearchPage]); return ( - <Input - ref={inputRef} - value={value} - onChange={onChange} - placeholder={t("common.search")} - className={className} - {...props} - /> + <div className={cn("relative flex-1", className)}> + <QueryExplainerTooltip + className="-translate-1/2 absolute right-1.5 top-2 p-0.5" + parsedSearchQuery={parsedSearchQuery} + /> + <Input + ref={inputRef} + value={value} + onChange={onChange} + placeholder={t("common.search")} + {...props} + /> + </div> ); }); SearchInput.displayName = "SearchInput"; diff --git a/apps/web/lib/hooks/bookmark-search.ts b/apps/web/lib/hooks/bookmark-search.ts index 9890ac6f..4662ffb6 100644 --- a/apps/web/lib/hooks/bookmark-search.ts +++ b/apps/web/lib/hooks/bookmark-search.ts @@ -1,17 +1,20 @@ -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { api } from "@/lib/trpc"; import { keepPreviousData } from "@tanstack/react-query"; +import { parseSearchQuery } from "@hoarder/shared/searchQueryParser"; + function useSearchQuery() { const searchParams = useSearchParams(); - const searchQuery = searchParams.get("q") ?? ""; - return { searchQuery }; + const searchQuery = decodeURIComponent(searchParams.get("q") ?? ""); + const parsed = useMemo(() => parseSearchQuery(searchQuery), [searchQuery]); + return { searchQuery, parsedSearchQuery: parsed }; } export function useDoBookmarkSearch() { const router = useRouter(); - const { searchQuery } = useSearchQuery(); + const { searchQuery, parsedSearchQuery } = useSearchQuery(); const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout | undefined>(); const pathname = usePathname(); @@ -26,7 +29,7 @@ export function useDoBookmarkSearch() { const doSearch = (val: string) => { setTimeoutId(undefined); - router.replace(`/dashboard/search?q=${val}`); + router.replace(`/dashboard/search?q=${encodeURIComponent(val)}`); }; const debounceSearch = (val: string) => { @@ -43,12 +46,13 @@ export function useDoBookmarkSearch() { doSearch, debounceSearch, searchQuery, + parsedSearchQuery, isInSearchPage: pathname.startsWith("/dashboard/search"), }; } export function useBookmarkSearch() { - const { searchQuery } = useSearchQuery(); + const { parsedSearchQuery } = useSearchQuery(); const { data, @@ -60,7 +64,8 @@ export function useBookmarkSearch() { isFetchingNextPage, } = api.bookmarks.searchBookmarks.useInfiniteQuery( { - text: searchQuery, + text: parsedSearchQuery.text, + matcher: parsedSearchQuery.matcher, }, { placeholderData: keepPreviousData, @@ -75,7 +80,6 @@ export function useBookmarkSearch() { } return { - searchQuery, error, data, isPending, |
