diff --git a/src/components/DefaultLayout.tsx b/src/components/DefaultLayout.tsx index f398f0117db4f47dd1a6b337c3bfd94f241df593..a86d92727292e3df20cdbf6d29886e94d3ddf73e 100644 --- a/src/components/DefaultLayout.tsx +++ b/src/components/DefaultLayout.tsx @@ -282,7 +282,13 @@ function UserField({ isUnavailable }: { isUnavailable: boolean }) { ); } -export function Search({ className }: { className?: string }) { +export function Search({ + className, + queryCallback, +}: { + className?: string; + queryCallback?: (query: string) => void; +}) { const router = useRouter(); const [query, setQuery] = useState(""); const { language } = useLanguage(); @@ -296,6 +302,7 @@ export function Search({ className }: { className?: string }) { }; const onChange = (e: React.ChangeEvent<HTMLInputElement>) => { setQuery(e.target.value); + if (queryCallback) queryCallback(e.target.value); }; return ( diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 045aa9fd11973658898a5801e5e65908d00729ed..15c71c2e7cadfcfa04b9dde42ff979b5e50f9228 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -11,7 +11,7 @@ import { } from "@/components/OMConfigComponent"; import { ReloadBoundary, useReloadBoundary } from "@/components/ReloadBoundary"; import { useUserContext } from "@/components/UserDataProvider"; -import { ResourceType, fetchDataWrapper } from "@/misc/PromiseHelpers"; +import { ResourceType, fetchDataWrapper, useDebounceWithArgument } from "@/misc/PromiseHelpers"; import React from "react"; import { ErrorPage, showError, showWarningToast } from "@/misc/ErrorHandlers"; import { Suspense, useEffect, useState } from "react"; @@ -23,6 +23,7 @@ import { useRouter } from "next/router"; import { VideoCard } from "@/components/VideoCard"; import { LectureLiveLabel } from "@/components/LiveLabel"; import { Search } from "@/components/DefaultLayout"; +import { SearchResults } from "./search"; function FeatureCard({ data, all_featured }: { data: featured; all_featured: featured[] }) { const { editMode } = useEditMode(); @@ -257,6 +258,9 @@ export default function Home() { const userContext = useUserContext(); const [homepageData, setHomepageData] = useState<ResourceType<GetHomepageResponse>>(); + const [query, setQuery] = useState(""); + const [updateQueryDeferred, _] = useDebounceWithArgument(setQuery, 500); + const hasUserInfo = userContext.hasUserInfo(); const { editMode } = useEditMode(); const { language } = useLanguage(); @@ -289,8 +293,6 @@ export default function Home() { useEffect(reloadData, [api, hasUserInfo]); - if (homepageData === undefined) return <></>; - return ( <ReloadBoundary reloadFunc={reloadData}> <FallbackErrorBoundary @@ -308,38 +310,46 @@ export default function Home() { <h1 className="ms-3">VideoAG</h1> </div> - <Search className="mt-3" /> + <Search className="mt-3" queryCallback={updateQueryDeferred} /> </div> </div> <hr /> - <div className="row"> - {editMode && <ModButtons />} - <Suspense fallback={<div className="spinner-border" />}> - <div className="col-md-6"> - <FeaturedContent homepageData={homepageData} /> - </div> - <div className="col-md-6"> - <div className="card mb-3"> - <div className="card-body"> - <h5 className="card-title"> - {language.get("ui.index.next_recordings")} - </h5> - - <UpcomingUploads homepageData={homepageData} /> - </div> + {query.length > 0 && ( + <> + <SearchResults query={query} /> + <hr /> + </> + )} + {homepageData && ( + <div className="row"> + {editMode && <ModButtons />} + <Suspense fallback={<div className="spinner-border" />}> + <div className="col-md-6"> + <FeaturedContent homepageData={homepageData} /> </div> - <div className="card mb-3"> - <div className="card-body"> - <h5 className="card-title"> - {language.get("ui.index.recent_videos")} - </h5> - - <RecentUploads homepageData={homepageData} /> + <div className="col-md-6"> + <div className="card mb-3"> + <div className="card-body"> + <h5 className="card-title"> + {language.get("ui.index.next_recordings")} + </h5> + + <UpcomingUploads homepageData={homepageData} /> + </div> + </div> + <div className="card mb-3"> + <div className="card-body"> + <h5 className="card-title"> + {language.get("ui.index.recent_videos")} + </h5> + + <RecentUploads homepageData={homepageData} /> + </div> </div> </div> - </div> - </Suspense> - </div> + </Suspense> + </div> + )} </FallbackErrorBoundary> </ReloadBoundary> ); diff --git a/src/pages/search.tsx b/src/pages/search.tsx index 5b7b0e99429aaaced4a760547b7ee8fa39ce5511..034fc465c67c1567f7aff610c6327d9d4a448243 100644 --- a/src/pages/search.tsx +++ b/src/pages/search.tsx @@ -48,42 +48,23 @@ function LectureResults({ ); } -export default function Search() { +export function SearchResults({ query }: { query: string }) { const api = useBackendContext(); - const params = useSearchParams(); - const [query, setQuery] = useState(params.get("q")); + const { language } = useLanguage(); const [searchResults, setSearchResults] = useState<SearchResponse>(); - const workingQuery = useRef(query); - const [_, doForceReRender] = useState(0); const [error, setError] = useState<any>(); - const router = useRouter(); - const { language } = useLanguage(); - - // TODO: this code needs more cleanup, variable names are not very descriptive - - useEffect(() => { - if (params.has("q")) setQuery(params.get("q")); - else if (query !== undefined && query !== null && query.length > 0) setQuery(""); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [params]); + const fetchedQuery = useRef(query); const updateSearchResults = () => { setSearchResults(undefined); - if (query === undefined || query === null || query === "") { - if (params.has("q")) router.push({ search: new URLSearchParams().toString() }); + if (query === "") { return; } - if (query !== params.get("q")) { - const newParams = new URLSearchParams(params.toString()); - newParams.set("q", query); - router.push({ search: newParams.toString() }); - } - - workingQuery.current = query; + fetchedQuery.current = query; api.search(query) .then((results) => { - if (query !== workingQuery.current) return; // if the query has changed, don't update the results + if (query !== fetchedQuery.current) return; // if the query has changed, don't update the results setSearchResults(results); setError(undefined); }) @@ -91,35 +72,21 @@ export default function Search() { setError(e); }); }; - // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(updateSearchResults, [query, api]); - const [updateQueryDeferred, updateQueryNow] = useDebounce(() => { - setQuery(workingQuery.current); - }, 500); + if (error) return <ErrorPage error={error} objectName="Suchergebnisse" objectPlural={true} />; - let results = ( - <div className="spinner-border" role="status"> - <span className="visually-hidden">Loading...</span> - </div> - ); - if (error) { - results = <ErrorPage error={error} objectName="Suchergebnisse" objectPlural={true} />; - } else if ( - query !== workingQuery.current && - query !== undefined && - query !== null && - query !== "" - ) { - results = ( + if (query !== fetchedQuery.current && query !== "") + return ( <div className="spinner-border text-secondary" role="status"> <span className="visually-hidden">Loading...</span> </div> ); - } else if (searchResults) { - results = ( - <> + + if (searchResults) + return ( + <ReloadBoundary reloadFunc={updateSearchResults}> {searchResults.courses.length > 0 && ( <CourseResults courses={searchResults.courses.map( @@ -136,11 +103,56 @@ export default function Search() { {searchResults.courses.length === 0 && searchResults.lectures.length === 0 && ( <h4 className="text-muted">{language.get("ui.search.no_results")}</h4> )} - </> + </ReloadBoundary> ); - } else if (query === undefined || query === null || query === "") { - results = <h4 className="text-muted">{language.get("ui.search.no_query")}</h4>; - } + + if (query === "") return <h4 className="text-muted">{language.get("ui.search.no_query")}</h4>; + + return ( + <div className="spinner-border" role="status"> + <span className="visually-hidden">Loading...</span> + </div> + ); +} + +export default function Search() { + const params = useSearchParams(); + const [query, setActualQuery] = useState(params.get("q") ?? ""); + const router = useRouter(); + const [_, doForceReRender] = useState(0); + const workingQuery = useRef(query); + const { language } = useLanguage(); + + // This code is somewhat complicated because we need to debounce the query update AND update url parameters + const [updateQueryDeferred, updateQueryNow] = useDebounce(() => { + const newQ = workingQuery.current; + setActualQuery(newQ); + if (newQ === undefined || newQ === null || newQ === "") { + if (params.has("q")) router.push({ search: new URLSearchParams().toString() }); + return; + } + if (newQ !== params.get("q")) { + const newParams = new URLSearchParams(params.toString()); + newParams.set("q", newQ); + + router.push({ search: newParams.toString() }); + } + }, 500); + + useEffect(() => { + if (params.has("q")) { + if (query !== params.get("q") && workingQuery.current !== params.get("q")) { + workingQuery.current = params.get("q") ?? ""; + updateQueryNow(); + doForceReRender((r) => r + 1); + } + } else if (query.length > 0) { + workingQuery.current = ""; + updateQueryNow(); + doForceReRender((r) => r + 1); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [params]); const onChange = (e: React.ChangeEvent<HTMLInputElement>) => { workingQuery.current = e.target.value; @@ -149,29 +161,27 @@ export default function Search() { }; return ( - <ReloadBoundary reloadFunc={updateSearchResults}> - <div> - <h1 className="d-flex align-items-center w-100"> - <span className="me-2">{language.get("ui.generic.search")}:</span> - <form - className="d-inline flex-fill" - onSubmit={(e) => { - e.preventDefault(); - updateQueryNow(); - }} - > - <input - className="sneak-100-input fst-italic text-muted w-100 border-bottom" - type="text" - value={workingQuery.current || ""} - onChange={onChange} - autoFocus - tabIndex={0} - /> - </form> - </h1> - {results} - </div> - </ReloadBoundary> + <div> + <h1 className="d-flex align-items-center w-100"> + <span className="me-2">{language.get("ui.generic.search")}:</span> + <form + className="d-inline flex-fill" + onSubmit={(e) => { + e.preventDefault(); + updateQueryNow(); + }} + > + <input + className="sneak-100-input fst-italic text-muted w-100 border-bottom" + type="text" + value={workingQuery.current || ""} + onChange={onChange} + autoFocus + tabIndex={0} + /> + </form> + </h1> + <SearchResults query={query} /> + </div> ); }