From 81b353aeb618ca943490945222c0328ae57366f1 Mon Sep 17 00:00:00 2001 From: Raff Viglianti Date: Tue, 29 Oct 2024 17:07:17 -0400 Subject: [PATCH] added first implementation of facets --- src/components/Button.tsx | 7 +- src/components/FacetAccordion.tsx | 34 +++++--- src/pages/search.tsx | 137 +++++++++++++++++++++++------- 3 files changed, 133 insertions(+), 45 deletions(-) diff --git a/src/components/Button.tsx b/src/components/Button.tsx index cf71e6e0c..3e4683ba8 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -4,11 +4,14 @@ import type { PropsWithChildren } from "react" interface ButtonProps { href?: string style?: React.CSSProperties + color?: "default" | "transparent" onClick?: (event: React.MouseEvent) => void } -const Button: React.FC = ({href, children, style, onClick}) => ( - = ({href, color, children, style, onClick}) => ( + {children} diff --git a/src/components/FacetAccordion.tsx b/src/components/FacetAccordion.tsx index 2be5cc2f2..5c47eaad7 100644 --- a/src/components/FacetAccordion.tsx +++ b/src/components/FacetAccordion.tsx @@ -4,16 +4,18 @@ import type { PropsWithChildren } from "react" interface FacetAccordionProps { label: string items: Item[] + fieldName: string + activeFacets: {cat: string, val: string}[] + add: (cat: string, val: string) => void + remove: (cat: string, val: string) => void } interface Item { label: string count: number - selected?: boolean - action: () => void } -const FacetAccordion: React.FC = ({label, items}) => { +const FacetAccordion: React.FC = ({label, items, fieldName, activeFacets, add, remove}) => { const [expanded, setExpanded] = React.useState(false) const [expanding, setExpanding] = React.useState(false) const [maxHeight, setMaxHeight] = React.useState(undefined); @@ -43,17 +45,23 @@ const FacetAccordion: React.FC = ({lab const handleItemClick = (e: React.MouseEvent, item: Item) => { e.preventDefault() - item.action() + add(fieldName, item.label) + } + + const handleRemoveFacet = (e: React.MouseEvent, item: Item) => { + e.preventDefault() + remove(fieldName, item.label) } return ( -
- @@ -66,14 +74,18 @@ const FacetAccordion: React.FC = ({lab > {expanded && }
diff --git a/src/pages/search.tsx b/src/pages/search.tsx index 1a1759d66..e719c783b 100644 --- a/src/pages/search.tsx +++ b/src/pages/search.tsx @@ -31,10 +31,12 @@ const Results = ({results, start}: ResultProps) => ( {d._xxxcollectionDescriptionxtxt} } {d.scd_publish_status !== "collection-owner-title-description-only" && <> - - Content type{ctypes.length > 1 ? 's': ''}: - {ctypes.join("; ")} - + {ctypes.length > 0 && + + Content type{ctypes.length > 1 ? 's': ''}: + {ctypes.join("; ")} + + } {d._xcollectionFormatsxtxtxxxcollectionFormatsxtxt && Format: {d._xcollectionFormatsxtxtxxxcollectionFormatsxtxt} @@ -60,6 +62,7 @@ const Results = ({results, start}: ResultProps) => ( ) const SearchPage: React.FC = ({data}) => { + const [showFacets, setShowFacets] = React.useState(false) const results = (data as Queries.qSearchPageQuery).allAirtableScdItems.nodes const [currentPage, setCurrentPage] = React.useState(1) const [resultsPerPage, setResultsPerPage] = React.useState(20) @@ -68,16 +71,30 @@ const SearchPage: React.FC = ({data}) => { const totalPages = Math.ceil(results.length / resultsPerPage) const [sortOrder, setSortOrder] = React.useState("asc"); + const [facets, setFacets] = React.useState<{cat: string, val: string}[]>([]); + + // apply facets + const facetedResults = facets.length > 0 ? results.filter(r => { + for (const f of facets) { + const cat = f.cat as keyof Queries.qSearchPageQuery["allAirtableScdItems"]["nodes"][0]["data"] + if (Object.keys(r.data!).includes(cat)) { + if ((r.data![cat] as string[])?.includes(f.val)) { + return true + } + } + } + return false + }) : results; // sort then paginate - (results as DeepWritable).sort((a, b) => { + (facetedResults as DeepWritable).sort((a, b) => { if (sortOrder === "asc") { return a.data!._xxxcollectionTitlextxt!.localeCompare(b.data!._xxxcollectionTitlextxt!) } else { return b.data!._xxxcollectionTitlextxt!.localeCompare(a.data!._xxxcollectionTitlextxt!) } }) - const paginatedResults = results.slice(startIndex, endIndex) + const paginatedResults = facetedResults.slice(startIndex, endIndex) // Update component with existing query parameters on load React.useEffect(() => { @@ -127,6 +144,16 @@ const SearchPage: React.FC = ({data}) => { quietlyUpdateUrlSearch("sort", val.toString()) } + const handleAddFacet = (cat: string, val: string) => { + if (facets.filter(f => f.cat === cat && f.val === val)[0] === undefined) { + setFacets([...facets, {cat, val}]) + } + } + + const handleRemoveFacet = (cat: string, val: string) => { + setFacets(facets.filter(f => !(f.cat === cat && f.val === val))) + } + const SmallPagination = () => { const prev = currentPage > 1 ? <> handleChange(e, "prev")} className="hover:underline">« Previous | : ""; const next = currentPage < totalPages ? <> | handleChange(e, "next")} className="hover:underline">Next » : ""; @@ -146,40 +173,86 @@ const SearchPage: React.FC = ({data}) => { history.pushState(null, '', '?' + urlParams.toString()); } } - return
{prev}{startIndex+1} – {endIndex} of {results.length}{next}
+ return
{prev}{startIndex+1} – {endIndex} of {facetedResults.length}{next}
} - // Get content types - const contentTypes = results.reduce((acc: { label: string; count: number, action: () => null }[], item) => { - // If contentTypes is not present, skip - if (!item.data?._xxxcollectionContentTypesxtxtxxxcollectionContentTypesxtxt) return acc; - - // Iterate through each contentType in the current item - item.data?._xxxcollectionContentTypesxtxtxxxcollectionContentTypesxtxt.forEach(type => { - // Find if the contentType is already in the accumulator - const existing = acc.find(entry => entry.label === type); - if (existing) { - // If found, increment the count - existing.count += 1; - } else { - // If not found, add a new entry with count 1 - acc.push({ label: type || "", count: 1, action: () => null }); - } - }); - - return acc; - }, []) - .sort((a, b) => b.count - a.count); + // Function to extract facets + const extractFacet = (field: string) => { + const f = field as keyof Queries.qSearchPageQuery["allAirtableScdItems"]["nodes"][0]["data"] + return results.reduce((acc: { label: string; count: number, action: () => null }[], item) => { + // If facet value is not present, skip + if (!item.data![f]) return acc; + const values = Array.isArray(item.data![f]) ? item.data![f] : [item.data![f] as string] + // Iterate through each facet value in the current item + values.forEach(type => { + // Find if the facet value is already in the accumulator + const existing = acc.find(entry => entry.label === type); + if (existing) { + // If found, increment the count + existing.count += 1; + } else { + // If not found, add a new entry with count 1 + acc.push({ label: type || "", count: 1, action: () => null }); + } + }); + + return acc; + }, []) + .sort((a, b) => b.count - a.count); + } return (
-
-
- - +
+
+
+ + + +
+
+ f.cat === "_xxxcollectionContentTypesxtxtxxxcollectionContentTypesxtxt")} + add={handleAddFacet} + remove={handleRemoveFacet}/> + f.cat === "_xcollectionFormatsxtxtxxxcollectionFormatsxtxt")} + add={handleAddFacet} + remove={handleRemoveFacet}/> + f.cat === "_xxxcollectionGenresxtxtxxxcollectionGenresxtxt")} + add={handleAddFacet} + remove={handleRemoveFacet}/> + f.cat === "_xxxcollectionOwnerNamextxt")} + add={handleAddFacet} + remove={handleRemoveFacet}/> + f.cat === "_xxxcollectionOwnerLocationCountryxtxt")} + add={handleAddFacet} + remove={handleRemoveFacet}/> + f.cat === "_xxxcollectionOwnerLocationStatextxt")} + add={handleAddFacet} + remove={handleRemoveFacet}/> +