From 1262b8929799159cff50ca78a65a647c7193c359 Mon Sep 17 00:00:00 2001 From: Mike Decker Date: Fri, 26 Apr 2024 16:12:07 -0700 Subject: [PATCH] Implement search page logic with facets and parameters --- app/search/algolia-search.tsx | 263 +++++++++++++++++++----- next.config.js | 3 +- src/components/elements/select-list.tsx | 2 +- 3 files changed, 211 insertions(+), 57 deletions(-) diff --git a/app/search/algolia-search.tsx b/app/search/algolia-search.tsx index 9a78d1c6..cab8980f 100644 --- a/app/search/algolia-search.tsx +++ b/app/search/algolia-search.tsx @@ -1,16 +1,20 @@ "use client"; import algoliasearch from "algoliasearch/lite"; -import {useHits, useSearchBox} from "react-instantsearch"; +import {useHits, useSearchBox, useCurrentRefinements, useRefinementList, Snippet, useRange, useClearRefinements} from "react-instantsearch"; import {InstantSearchNext} from "react-instantsearch-nextjs"; import Link from "@components/elements/link"; import {H2} from "@components/elements/headers"; import Image from "next/image"; -import {useRef} from "react"; +import {useEffect, useId, useRef, useState} from "react"; import Button from "@components/elements/button"; import {UseSearchBoxProps} from "react-instantsearch"; import {useRouter, useSearchParams} from "next/navigation"; -import {UseHitsProps} from "react-instantsearch-core/dist/es/connectors/useHits"; +import {Hit as HitType} from "instantsearch.js"; +import SelectList from "@components/elements/select-list"; +import {SelectOptionDefinition} from "@mui/base/useSelect"; +import {RangeBoundaries} from "instantsearch.js/es/connectors/range/connectRange"; +import {IndexUiState} from "instantsearch.js/es/types/ui-state"; type Props = { appId: string @@ -22,27 +26,188 @@ const AlgoliaSearch = ({appId, searchIndex, searchApiKey}: Props) => { const searchClient = algoliasearch(appId, searchApiKey); const searchParams = useSearchParams(); + const initialUiState: IndexUiState = {} + if (searchParams.get("q")) initialUiState.query = searchParams.get("q") as string + if (searchParams.get("subjects")) { + initialUiState.refinementList = {book_subject: searchParams.get("subjects")?.split(",") as string[]} + } + if (searchParams.get("books")) { + initialUiState.refinementList = {book_type: ["book"]} + } + if (searchParams.get("published-min") || searchParams.get("published-max")) { + initialUiState.range = {book_published: (searchParams.get("published-min") || "0" as string) + ":" + (searchParams.get("published-max") || "3000" as string)} + } + return (
- +
+
+ +
+
+ +
+
) } -const HitList = (props: UseHitsProps) => { - const {hits} = useHits(props); +const FacetFilters = () => { + const router = useRouter() + const searchParams = useSearchParams(); + + const { + items: bookSubjectRefinementList, + refine: refineBookSubjects, + canToggleShowMore: canShowMoreRefinements, + toggleShowMore: showMoreRefinements + } = useRefinementList({attribute: "book_subject"}); + + const {refine: refineBookType} = useRefinementList({attribute: "book_type"}); + + const {items: currentRefinements, canRefine: canRefineCurrent, refine: removeRefinement} = useCurrentRefinements({}); + + const {range, canRefine: canRefineRange, refine: refineRange} = useRange({attribute: "book_published"}); + const {min: minYear, max: maxYear} = range; + const [rangeChoices, setRangeChoices] = useState([parseInt(searchParams.get("published-min") || "1000"), parseInt(searchParams.get("published-max") || "3000")]); + const [subjectChoices, setSubjectChoices] = useState(searchParams.get("subjects")?.split(",") || []) + + const {refine: clearRefinements} = useClearRefinements({}); + + const yearOptions: SelectOptionDefinition[] = []; + for (let i = (maxYear || new Date().getFullYear()); i >= (minYear || 1990); i--) { + yearOptions.push({value: `${i}`, label: `${i}`}); + } + + const id = useId(); + + useEffect(() => { + const rangeFrom = rangeChoices[0] && minYear && rangeChoices[0] > minYear ? rangeChoices[0] : minYear + const rangeTo = rangeChoices[1] && maxYear && rangeChoices[1] < maxYear ? rangeChoices[1] : maxYear + refineRange([rangeFrom, rangeTo]); + + const params = new URLSearchParams(searchParams.toString()); + if (rangeFrom && minYear && rangeFrom > minYear) { + params.set("published-min", `${rangeFrom}`) + } else { + params.delete("published-min") + } + + if (rangeTo && maxYear && rangeTo < maxYear) { + params.set("published-max", `${rangeTo}`) + } else { + params.delete("published-max") + } + + if (subjectChoices.length > 0) { + params.set("subjects", subjectChoices.join(",")) + } else { + params.delete("subjects") + } + + router.replace(`?${params.toString()}`, {scroll: false}) + }, [rangeChoices, router, searchParams, maxYear, minYear, refineRange, subjectChoices]); + + return ( +
+

Filter by

+ +
    + {currentRefinements.filter(refinement => refinement.attribute === "book_subject").map(refinement => { + return refinement.refinements.map((item, i) => +
  • + {item.value} + +
  • + ) + })} + + {canShowMoreRefinements && + + } +
+ +
+ +
+ +
+ Subject + {bookSubjectRefinementList.map(refinementOption => + + )} +
+ +
+ Published Date + +
+
+
Minimum&nbps;Year
+ parseInt(option.value) <= (rangeChoices[1] || new Date().getFullYear()))} + value={(!rangeChoices[0] || !minYear || rangeChoices[0] < minYear) ? undefined : `${rangeChoices[0]}`} + ariaLabelledby={`${id}-min-year`} + disabled={!canRefineRange} + onChange={(_e, value) => setRangeChoices((prevState) => [parseInt(value as string) || minYear, prevState[1]])} + /> +
+ - +
+
Minimum&nbps;Year
+ parseInt(option.value) >= (rangeChoices[0] || 1900))} + value={(!rangeChoices[1] || !maxYear || rangeChoices[1] > maxYear) ? undefined : `${rangeChoices[1]}`} + ariaLabelledby={`${id}-max-year`} + disabled={!canRefineRange} + onChange={(_e, value) => setRangeChoices((prevState) => [prevState[0], parseInt(value as string) || maxYear])} + /> +
+
+
+ + +
+ ) +} + +const HitList = () => { + const {hits,results} = useHits>({}); if (hits.length === 0) { return (

No results for your search. Please try another search.

@@ -50,13 +215,19 @@ const HitList = (props: UseHitsProps) => { } return ( -
    - {hits.map(hit => -
  • - -
  • - )} -
+
+ {results?.nbHits && +
{results.nbHits} {results.nbHits > 1 ? "Results" : "Result"}
+ } +
    + {hits.map(hit => +
  • + +
  • + )} +
+ +
) } @@ -66,9 +237,12 @@ type AlgoliaHit = { summary?: string photo?: string updated?: number + html?: string + book_published?: number + book_authors?: string } -const Hit = ({hit}: { hit: AlgoliaHit }) => { +const Hit = ({hit}: { hit: HitType }) => { const hitUrl = new URL(hit.url); return ( @@ -79,26 +253,30 @@ const Hit = ({hit}: { hit: AlgoliaHit }) => { {hit.title} -

{hit.summary}

- - {hit.updated && -
- Last Updated: {new Date(hit.updated * 1000).toLocaleDateString("en-us", { - month: "long", - day: "numeric", - year: "numeric" - })} -
+ + {hit.summary && +

{hit.summary}

} + {(hit.html && !hit.summary) && + + } + +
+ {hit.book_authors} +
+
+ {hit.book_published} +
{hit.photo &&
} @@ -108,36 +286,20 @@ const Hit = ({hit}: { hit: AlgoliaHit }) => { const SearchBox = (props?: UseSearchBoxProps) => { - const router = useRouter(); - const {query, refine} = useSearchBox(props); const inputRef = useRef(null); - - if (query) { - router.replace(`?q=${query}`, {scroll: false}) - } + const {query, refine} = useSearchBox(props); return (
{ e.preventDefault(); - e.stopPropagation(); + inputRef.current?.blur(); refine(inputRef.current?.value || ""); }} - onReset={(event) => { - event.preventDefault(); - event.stopPropagation(); - refine(""); - - if (inputRef.current) { - inputRef.current.value = ""; - inputRef.current.focus(); - } - }} >
-
Showing results for {query}
diff --git a/next.config.js b/next.config.js index 63206ebc..af4276fb 100644 --- a/next.config.js +++ b/next.config.js @@ -9,8 +9,7 @@ const nextConfig = { images: { remotePatterns: [ { - // Allow any stanford domain for images, but require https. - protocol: 'https', + // Allow any stanford domain for images. hostname: '**.stanford.edu', }, { diff --git a/src/components/elements/select-list.tsx b/src/components/elements/select-list.tsx index 689469e6..4ea065dc 100644 --- a/src/components/elements/select-list.tsx +++ b/src/components/elements/select-list.tsx @@ -160,7 +160,7 @@ const SelectList = ({ } - +