Skip to content

Commit

Permalink
break, then fix, the search bar
Browse files Browse the repository at this point in the history
  • Loading branch information
sphinxrave committed Oct 28, 2024
1 parent 80e15b9 commit c1969a9
Show file tree
Hide file tree
Showing 11 changed files with 304 additions and 317 deletions.
381 changes: 150 additions & 231 deletions packages/react/package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"lucide-react": "^0.294.0",
"nanoid": "^5.0.7",
"oauth-open": "^1.0.4",
"picoquery": "^2.4.0",
"protoframe": "^1.1.1",
"react": "^18.3.1",
"react-day-picker": "^8.9.1",
Expand Down Expand Up @@ -108,7 +109,7 @@
"@typescript-eslint/eslint-plugin": "^7.14.1",
"@typescript-eslint/parser": "^7.14.1",
"@unocss/eslint-config": "^0.60.3",
"@vitejs/plugin-react": "^4.3.1",
"@vitejs/plugin-react": "^4.3.3",
"@vitejs/plugin-react-swc": "^3.7.0",
"autoprefixer": "^10.4.19",
"browserslist": "^4.23.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import * as React from "react";
import { Badge } from "@/shadcn/ui/badge";
import { QueryItem } from "../types";
import { PrimitiveAtom, useAtomValue, useSetAtom } from "jotai";
import { splitQueryAtom } from "../hooks/useAutocomplete";
import { PrimitiveAtom, useAtomValue } from "jotai";
import { useTranslation } from "react-i18next";

export function QueryBadge({ item }: { item: PrimitiveAtom<QueryItem> }) {
export function QueryBadge({
item,
onRemoveItem,
}: {
item: PrimitiveAtom<QueryItem>;
onRemoveItem: () => void;
}) {
const queryItem = useAtomValue(item);
const querySplitItemAction = useSetAtom(splitQueryAtom);
const { t } = useTranslation();
const categoryName = React.useCallback(
(query: QueryItem) => {
Expand Down Expand Up @@ -45,14 +49,14 @@ export function QueryBadge({ item }: { item: PrimitiveAtom<QueryItem> }) {
className="ml-1 rounded-full outline-none ring-offset-base-2 focus:ring-2 focus:ring-primary-9 focus:ring-offset-2"
onKeyDown={(e) => {
if (e.key === "Enter") {
querySplitItemAction({ type: "remove", atom: item });
onRemoveItem();
}
}}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onClick={() => querySplitItemAction({ type: "remove", atom: item })}
onClick={onRemoveItem}
>
<div className="i-lucide:x h-3 w-3 text-sm text-base-8 hover:text-base-11"></div>
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,13 @@ import { useAtom } from "jotai";
import { JSON_SCHEMA, QueryItem } from "../types";
import { QueryBadge } from "./QueryBadge";
import { useTranslation } from "react-i18next";
import {
HTMLAttributes,
useRef,
useState,
useCallback,
useEffect,
} from "react";
import { HTMLAttributes, useRef, useState, useCallback } from "react";
import { AutocompleteDropdownItem } from "./AutocompleteDropdownItem";
import { Popover, PopoverTrigger } from "@/shadcn/ui/popover";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { getQueryModelFromQuery } from "../helper";
import { useNavigate } from "react-router-dom";
import { stringify } from "picoquery";

export function SearchBar({
className,
Expand All @@ -31,10 +28,7 @@ export function SearchBar({
const [query, setQuery] = useAtom(queryAtom);
const [queryPieces, setQueryPieces] = useAtom(splitQueryAtom);
const { search, updateSearch, autocomplete } = useSearchboxAutocomplete();

useEffect(() => {
console.log(query);
}, [query]);
const navigate = useNavigate();

const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLDivElement>) => {
Expand Down Expand Up @@ -86,6 +80,16 @@ export function SearchBar({
[query, updateSearch, t, setQuery, setQueryPieces],
);

const doSearch = useCallback(() => {
if (query.length > 0) {
const qm = getQueryModelFromQuery(query);
navigate({
pathname: "/search",
search: "?" + stringify(qm),
});
}
}, [navigate, query]);

return (
<Command
onKeyDown={handleKeyDown}
Expand All @@ -102,7 +106,15 @@ export function SearchBar({
<div className="group rounded-md bg-base-2 p-2 text-sm ring-offset-base-2 focus-within:ring-2 focus-within:ring-primary focus-within:ring-offset-2 hover:bg-base-3">
<div className="flex flex-wrap gap-1">
{queryPieces.map((queryItem, i) => {
return <QueryBadge item={queryItem} key={"badge" + i} />;
return (
<QueryBadge
item={queryItem}
key={"badge" + i}
onRemoveItem={() => {
setQueryPieces({ type: "remove", atom: queryItem });
}}
/>
);
})}
{/* Avoid having the "Search" Icon */}
<CommandPrimitive.Input
Expand All @@ -115,6 +127,11 @@ export function SearchBar({
placeholder={t("component.search.searchLabel")}
className="ml-2 flex-1 bg-transparent outline-none placeholder:text-base-8"
/>
<button
className="i-carbon-search text-base-11 opacity-0 group-focus-within:opacity-100"
tabIndex={3}
onClick={() => doSearch()}
/>
</div>
</div>
</PopoverTrigger>
Expand Down
46 changes: 26 additions & 20 deletions packages/react/src/components/header/searchbar/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@ function split(input: Readonly<string>, separator: RegExp, limit: number = -1) {
return output;
}

/**
* Split a search term into a SearchableCategory and the search value.
* If the q_class is invalid, returns undefined for the category.
*
* Used for splitting out search terms intelligently from the searchbar.
* @param {string} term - The search term, e.g. "org:hololive"
* @param {Record<string, keyof typeof JSON_SCHEMA>} langCategoryReversemapClass
* @return {[SearchableCategory | undefined, string]} - [category, value]
*/
export function splitSearchClassTerms(
term: string,
langCategoryReversemapClass: Record<string, keyof typeof JSON_SCHEMA>,
Expand Down Expand Up @@ -242,28 +251,25 @@ export async function getQueryFromQueryModel(
return await gen2array(generator());
}

// type WithOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type WithOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
// export function useSearch(
// queryModel: MaybeRef<VideoQueryModel>,
// queryContainer: MaybeRef<WithOptional<VideoQueryContainer, "query">>,
// // options: Ref<Partial<UseQueryOptions>>
// queryModel: VideoQueryModel,
// queryContainer: WithOptional<VideoQueryContainer, "query">,
// ) {
// const query = computed(() => {
// const qC = get(queryContainer);
// qC.query = sanitizeQueryModel(get(queryModel));
// // Create the query container with sanitized model
// const finalQueryContainer = {
// ...queryContainer,
// query: sanitizeQueryModel(queryModel),
// };

// return qC;
// });

// return useQuery(
// ["search", query] as const,
// async (key) => {
// return (await backendApi.searchV3(key.queryKey[1] as any)).data;
// },
// {
// enabled: true,
// staleTime: 10 * 60 * 1000, // 10min.
// cacheTime: 12 * 60 * 1000, // 12min.
// return useQuery({
// queryKey: ["search", finalQueryContainer] as const,
// queryFn: async ({ queryKey }) => {
// const [_, queryParams] = queryKey;
// return (await backendApi.searchV3(queryParams)).data;
// },
// );
// staleTime: 10 * 60 * 1000, // 10 minutes
// cacheTime: 12 * 60 * 1000, // 12 minutes,
// enabled: true,
// });
// }
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ function useClientSuggestions(
searchString: string,
t: TFunction<"translation", undefined>,
): QueryItem[] {
console.log(
"client suggestions",
"searchCategory",
searchCategory,
"searchString",
searchString,
);
const { data: orgs } = useOrgs({ enabled: !!searchString });

return useMemo(() => {
Expand Down Expand Up @@ -134,8 +141,14 @@ function useClientSuggestions(
}

// Handle category-specific static suggestions that only show up when a category is specified
if (searchCategory && STATIC_SUGGESTIONS[searchCategory]) {
else if (searchCategory && STATIC_SUGGESTIONS[searchCategory]) {
suggestions.push(...STATIC_SUGGESTIONS[searchCategory]);
} else if (searchCategory) {
suggestions.push({
type: searchCategory,
value: searchString,
text: searchString,
});
}

// Handle general search when no category is specified
Expand All @@ -147,12 +160,13 @@ function useClientSuggestions(
text: searchString,
});
}

// Add category suggestions
const categoryAutofill = FIRST_SEARCH.filter(
(x) =>
!searchString ||
t(`search.class.${x.type}`, x.type).startsWith(searchString),
t(`search.class.${x.type}`, { defaultValue: x.type }).startsWith(
searchString,
),
);
suggestions.push(...categoryAutofill);
}
Expand Down Expand Up @@ -189,7 +203,7 @@ export function useSearchboxAutocomplete() {
);
const clientSuggestions = useClientSuggestions(
searchCategory,
searchString,
searchString ?? "",
t,
);

Expand Down Expand Up @@ -217,6 +231,7 @@ export function useSearchboxAutocomplete() {
.filter((x): x is QueryItem => x !== null);
}, [serverSuggestions, clientSuggestions, query]);

console.log("autocomplete", autocomplete);
return {
search,
updateSearch,
Expand Down
53 changes: 29 additions & 24 deletions packages/react/src/components/layout/Frame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,30 +108,35 @@ export function Frame() {
const [reportedVideo, setReportedVideo] = useAtom(videoReportAtom);

return (
<div className={mainClasses} id="layout">
<LocationAwareReactivity />
<Sidebar />
<Header id="header" />
<main className="">
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense fallback={<Loading size="xl" />}>
<Outlet />
</Suspense>
</ErrorBoundary>
</main>
<SelectionFooter />
{reportedVideo && (
<LazyVideoReportDialog
open={!!reportedVideo}
onOpenChange={() => setReportedVideo(null)}
video={reportedVideo}
/>
)}
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => window.location.reload()}
>
<div className={mainClasses} id="layout">
<LocationAwareReactivity />
<Sidebar />
<Header id="header" />
<main className="">
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense fallback={<Loading size="xl" />}>
<Outlet />
</Suspense>
</ErrorBoundary>
</main>
<SelectionFooter />
{reportedVideo && (
<LazyVideoReportDialog
open={!!reportedVideo}
onOpenChange={() => setReportedVideo(null)}
video={reportedVideo}
/>
)}

{isMobile && <Footer />}
{miniPlayer && <MiniPlayer />}
<Toaster />
<GlobalReactivity />
</div>
{isMobile && <Footer />}
{miniPlayer && <MiniPlayer />}
<Toaster />
<GlobalReactivity />
</div>
</ErrorBoundary>
);
}
3 changes: 0 additions & 3 deletions packages/react/src/locales/en/ui.yml
Original file line number Diff line number Diff line change
Expand Up @@ -262,12 +262,9 @@ views:
official: Archive
subber: Clips
noLiveStreams:
- '*Tumbleweed rolls across the screen*'
- Nobody is live right now
- Seems like all the VTubers are busy touching grass
- Time to watch the backlog, because no one's streaming
- Vtubers are sleeping, time to watch clips!
- Vtubers doko?
- No one is live right now, go listen to some bangers on Musicdex!
library:
savedVideosTitle: Saved Videos
Expand Down
14 changes: 6 additions & 8 deletions packages/react/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React, { Suspense } from "react";
import ReactDOM from "react-dom/client";
import { GoogleOAuthProvider } from "@react-oauth/google";
import { HelmetProvider } from "react-helmet-async";
import { ErrorBoundary } from "react-error-boundary";
import "./index.css";
import "uno.css";
import { QueryClientProvider } from "@tanstack/react-query";
Expand All @@ -13,7 +12,6 @@ import relativeTime from "dayjs/plugin/relativeTime";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import localizedFormat from "dayjs/plugin/localizedFormat";
import { ErrorFallback } from "./components/common/ErrorFallback";
import { App } from "./App";
import { TooltipProvider } from "./shadcn/ui/tooltip";
import { globalQueryClient } from "./lib/query";
Expand Down Expand Up @@ -59,14 +57,14 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
</Suspense>
)}
<GoogleOAuthProvider clientId={GOOGLE_CLIENT_ID}>
<ErrorBoundary
{/* <ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => window.location.reload()}
>
<TooltipProvider>
<App />
</TooltipProvider>
</ErrorBoundary>
> */}
<TooltipProvider>
<App />
</TooltipProvider>
{/* </ErrorBoundary> */}
</GoogleOAuthProvider>
</QueryClientProvider>
</HelmetProvider>
Expand Down
24 changes: 24 additions & 0 deletions packages/react/src/routes/search.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Helmet } from "react-helmet-async";

export default function Search() {
const { t } = useTranslation();
const { id } = useParams();

if (status === "pending") {
return <div>Loading...</div>;
}

if (status === "error") {
return <div>{t("component.apiError.title")}</div>;
}

return (
<>
<Helmet>
<title>{t("component.search.searchLabel")} - Holodex</title>
</Helmet>
</>
);
}
Loading

0 comments on commit c1969a9

Please sign in to comment.