From 272c23e675f5ca7ad8efd8ce9f1f69e0046def38 Mon Sep 17 00:00:00 2001 From: huanfeng Date: Tue, 26 Nov 2024 20:58:03 -0500 Subject: [PATCH 01/17] add hierarchicalMenu(fake data) --- components/search/HierarchicalMenuToggler.tsx | 40 ++++++++++++ components/search/SearchContainer.tsx | 65 +++++++++++++++++++ .../search/SearchableHierarchicalMenu.tsx | 21 ++++++ components/search/bills/BillSearch.tsx | 1 + components/search/useRefinements.tsx | 17 ++++- 5 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 components/search/HierarchicalMenuToggler.tsx create mode 100644 components/search/SearchableHierarchicalMenu.tsx diff --git a/components/search/HierarchicalMenuToggler.tsx b/components/search/HierarchicalMenuToggler.tsx new file mode 100644 index 000000000..2066af120 --- /dev/null +++ b/components/search/HierarchicalMenuToggler.tsx @@ -0,0 +1,40 @@ +import { useEffect } from "react" + +const HierarchicalMenuToggler = () => { + useEffect(() => { + // Select all parent items + const parentItems = document.querySelectorAll( + ".ais-HierarchicalMenu-item--parent" + ) + + // Add click listeners to toggle child lists + parentItems.forEach(parent => { + const childList = parent.querySelector( + ".ais-HierarchicalMenu-list--child" + ) + + const toggleChildList = () => { + if (childList) { + if (childList.style.display === "none" || !childList.style.display) { + // Expand: Show the child list + childList.style.display = "block" + } else { + // Collapse: Hide the child list + childList.style.display = "none" + } + } + } + + parent.addEventListener("click", toggleChildList) + + // Cleanup: Remove event listener on unmount + return () => { + parent.removeEventListener("click", toggleChildList) + } + }) + }, []) + + return null +} + +export default HierarchicalMenuToggler diff --git a/components/search/SearchContainer.tsx b/components/search/SearchContainer.tsx index b128bf9e8..49b67f462 100644 --- a/components/search/SearchContainer.tsx +++ b/components/search/SearchContainer.tsx @@ -146,4 +146,69 @@ export const SearchContainer = styled.div` .ais-RefinementList-label { border-bottom: dashed 1px; } + + .ais-HierarchicalMenu-list { + background-color: white; + padding: 1rem; + border-radius: 4px; + margin-top: 0.5rem; + margin-bottom: 1.5rem; + max-height: 250px; + overflow-y: auto; + } + + .ais-HierarchicalMenu-item { + font-size: 1rem; + border-bottom: dashed 1px; + } + + .ais-HierarchicalMenu-label { + white-space: normal; + display: inline-block; + width: 75%; + } + + .ais-HierarchicalMenu-count { + background: var(--bs-blue); + color: white; + font-size: 0.75rem; + line-height: 1rem; + padding-right: 10px; + padding-left: 10px; + border: none; + } + + .ais-HierarchicalMenu-list--child { + margin-left: -1rem; + padding-left: 1rem; + border-left: none; + white-space: normal; + /* word-wrap: break-word; */ + overflow-x: hidden; + max-width: 100%; + width: 100%; + } + + .ais-HierarchicalMenu-list--child .ais-HierarchicalMenu-label { + display: block; + word-wrap: break-word; + display: inline-block; + max-width: 100%; + } + + .ais-HierarchicalMenu-list--child .ais-HierarchicalMenu-list { + display: block; + display: inline-block; + overflow: visible; + max-width: 200%; + } + .ais-HierarchicalMenu-link::before { + content: "+"; + background-image: none; + font-size: 35px; + color: var(--bs-blue); + vertical-align: middle; + line-height: -3; + margin-bottom: 1rem; + } ` diff --git a/components/search/SearchableHierarchicalMenu.tsx b/components/search/SearchableHierarchicalMenu.tsx new file mode 100644 index 000000000..be2f7e139 --- /dev/null +++ b/components/search/SearchableHierarchicalMenu.tsx @@ -0,0 +1,21 @@ +import React from "react" +import { HierarchicalMenu } from "react-instantsearch" +import HierarchicalMenuToggler from "./HierarchicalMenuToggler" + +const SearchableHierarchicalMenu = ({ + attributes +}: { + attributes: string[] +}) => { + return ( +
+ {/* Toggle behavior */} + + + {/* Algolia HierarchicalMenu */} + +
+ ) +} + +export default SearchableHierarchicalMenu diff --git a/components/search/bills/BillSearch.tsx b/components/search/bills/BillSearch.tsx index b224f467f..7ebe24f92 100644 --- a/components/search/bills/BillSearch.tsx +++ b/components/search/bills/BillSearch.tsx @@ -1,5 +1,6 @@ import { CurrentRefinements, + HierarchicalMenu, Hits, InstantSearch, Pagination, diff --git a/components/search/useRefinements.tsx b/components/search/useRefinements.tsx index 762f4c70b..c55ca4118 100644 --- a/components/search/useRefinements.tsx +++ b/components/search/useRefinements.tsx @@ -1,4 +1,8 @@ -import { RefinementList, useInstantSearch } from "react-instantsearch" +import { + HierarchicalMenu, + RefinementList, + useInstantSearch +} from "react-instantsearch" import { faFilter } from "@fortawesome/free-solid-svg-icons" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { useCallback, useEffect, useState } from "react" @@ -6,6 +10,7 @@ import styled from "styled-components" import { useMediaQuery } from "usehooks-ts" import { Button, Col, Offcanvas } from "../bootstrap" import { SearchContainer } from "./SearchContainer" +import SearchableHierarchicalMenu from "./SearchableHierarchicalMenu" export const FilterButton = styled(Button)` font-size: 1rem; @@ -37,8 +42,14 @@ export const useRefinements = ({ const refinements = ( <> - {refinementProps.map((p, i) => ( - + + {refinementProps.slice(2).map((p, i) => ( + ))} ) From 13bc0fc90c3f21f4f3ea78c609ac3451b6cb6984 Mon Sep 17 00:00:00 2001 From: huanfeng Date: Tue, 3 Dec 2024 19:56:54 -0500 Subject: [PATCH 02/17] Update HierarchialMenu --- components/search/HierarchicalMenuToggler.tsx | 40 ------------- components/search/SearchContainer.tsx | 41 ++++++------- .../search/SearchableHierarchicalMenu.tsx | 60 +++++++++++++++---- components/search/bills/BillSearch.tsx | 1 - components/search/useRefinements.tsx | 6 +- 5 files changed, 72 insertions(+), 76 deletions(-) delete mode 100644 components/search/HierarchicalMenuToggler.tsx diff --git a/components/search/HierarchicalMenuToggler.tsx b/components/search/HierarchicalMenuToggler.tsx deleted file mode 100644 index 2066af120..000000000 --- a/components/search/HierarchicalMenuToggler.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { useEffect } from "react" - -const HierarchicalMenuToggler = () => { - useEffect(() => { - // Select all parent items - const parentItems = document.querySelectorAll( - ".ais-HierarchicalMenu-item--parent" - ) - - // Add click listeners to toggle child lists - parentItems.forEach(parent => { - const childList = parent.querySelector( - ".ais-HierarchicalMenu-list--child" - ) - - const toggleChildList = () => { - if (childList) { - if (childList.style.display === "none" || !childList.style.display) { - // Expand: Show the child list - childList.style.display = "block" - } else { - // Collapse: Hide the child list - childList.style.display = "none" - } - } - } - - parent.addEventListener("click", toggleChildList) - - // Cleanup: Remove event listener on unmount - return () => { - parent.removeEventListener("click", toggleChildList) - } - }) - }, []) - - return null -} - -export default HierarchicalMenuToggler diff --git a/components/search/SearchContainer.tsx b/components/search/SearchContainer.tsx index 49b67f462..cc6952147 100644 --- a/components/search/SearchContainer.tsx +++ b/components/search/SearchContainer.tsx @@ -165,7 +165,7 @@ export const SearchContainer = styled.div` .ais-HierarchicalMenu-label { white-space: normal; display: inline-block; - width: 75%; + width: 100%; } .ais-HierarchicalMenu-count { @@ -178,29 +178,13 @@ export const SearchContainer = styled.div` border: none; } - .ais-HierarchicalMenu-list--child { - margin-left: -1rem; - padding-left: 1rem; - border-left: none; - white-space: normal; - /* word-wrap: break-word; */ - overflow-x: hidden; - max-width: 100%; - width: 100%; - } - - .ais-HierarchicalMenu-list--child .ais-HierarchicalMenu-label { - display: block; - word-wrap: break-word; - display: inline-block; - max-width: 100%; - } - - .ais-HierarchicalMenu-list--child .ais-HierarchicalMenu-list { + .ais-HierarchicalMenu-list .ais-HierarchicalMenu-list--child { display: block; display: inline-block; - overflow: visible; - max-width: 200%; + overflow-y: visible; + margin: 0; + padding: 0 0 0 0rem; + width: 100%; } .ais-HierarchicalMenu-link::before { content: "+"; @@ -211,4 +195,17 @@ export const SearchContainer = styled.div` line-height: -3; margin-bottom: 1rem; } + .ais-HierarchicalMenu-link--selected::before { + content: "-"; + background-image: none; + font-size: 50px; + color: var(--bs-blue); + vertical-align: middle; + line-height: -3; + margin-bottom: 1rem; + } + + .ais-HierarchicalMenu-list--child .ais-HierarchicalMenu-item:last-child { + border-top: none; /* Remove top border for the last child */ + } ` diff --git a/components/search/SearchableHierarchicalMenu.tsx b/components/search/SearchableHierarchicalMenu.tsx index be2f7e139..46e896ca3 100644 --- a/components/search/SearchableHierarchicalMenu.tsx +++ b/components/search/SearchableHierarchicalMenu.tsx @@ -1,19 +1,57 @@ -import React from "react" -import { HierarchicalMenu } from "react-instantsearch" -import HierarchicalMenuToggler from "./HierarchicalMenuToggler" +import React, { useState } from "react" +import { HierarchicalMenu, HierarchicalMenuProps } from "react-instantsearch" -const SearchableHierarchicalMenu = ({ - attributes -}: { +type Props = { attributes: string[] -}) => { +} + +const SearchableHierarchicalMenu: React.FC = ({ attributes }) => { + const [searchQuery, setSearchQuery] = useState("") + + const handleInputChange = (event: React.ChangeEvent) => { + setSearchQuery(event.target.value) + } + + const filterAndTransformItems = (items: any[]) => + items + .filter(item => + item.label.toLowerCase().includes(searchQuery.toLowerCase()) + ) + .map(item => ({ + ...item, + label: item.label.toUpperCase() + })) + const transformItems: HierarchicalMenuProps["transformItems"] = items => { + if (!items) { + console.error("transformItems received null or undefined items") + return [] + } + return items.map(item => ({ + ...item, + label: item.label + })) + } return (
- {/* Toggle behavior */} - + {/* Custom Search Input */} + {/* */} - {/* Algolia HierarchicalMenu */} - + {/* HierarchicalMenu with filter and transform logic */} +
) } diff --git a/components/search/bills/BillSearch.tsx b/components/search/bills/BillSearch.tsx index 7ebe24f92..b224f467f 100644 --- a/components/search/bills/BillSearch.tsx +++ b/components/search/bills/BillSearch.tsx @@ -1,6 +1,5 @@ import { CurrentRefinements, - HierarchicalMenu, Hits, InstantSearch, Pagination, diff --git a/components/search/useRefinements.tsx b/components/search/useRefinements.tsx index c55ca4118..5d9eaebd2 100644 --- a/components/search/useRefinements.tsx +++ b/components/search/useRefinements.tsx @@ -1,5 +1,6 @@ import { HierarchicalMenu, + HierarchicalMenuProps, RefinementList, useInstantSearch } from "react-instantsearch" @@ -44,9 +45,10 @@ export const useRefinements = ({ <> {refinementProps.slice(2).map((p, i) => ( From f8505987f83ced88489725ba9fa629a33261d507 Mon Sep 17 00:00:00 2001 From: huanfeng Date: Tue, 3 Dec 2024 21:33:18 -0500 Subject: [PATCH 03/17] Update menu with llm data --- components/search/HierarchicalMenuWidget.tsx | 156 ++++++++++++++++++ .../search/SearchableHierarchicalMenu.tsx | 59 ------- .../search/bills/useBillRefinements.tsx | 8 + components/search/useRefinements.tsx | 15 +- 4 files changed, 170 insertions(+), 68 deletions(-) create mode 100644 components/search/HierarchicalMenuWidget.tsx delete mode 100644 components/search/SearchableHierarchicalMenu.tsx diff --git a/components/search/HierarchicalMenuWidget.tsx b/components/search/HierarchicalMenuWidget.tsx new file mode 100644 index 000000000..3fa0f2cd2 --- /dev/null +++ b/components/search/HierarchicalMenuWidget.tsx @@ -0,0 +1,156 @@ +import React, { useState } from "react" +import styled from "styled-components" + +const StyledMenu = styled.div` + font-family: "Nunito"; + + .category { + cursor: pointer; + font-size: 1rem; + font-weight: bold; + margin: 8px 0; + padding: 8px; + background-color: white; + border-radius: 4px; + border-bottom: dashed 1px; + display: flex; + align-items: center; + } + + .category:hover { + background-color: #f5f5f5; + } + + .category--expanded::before { + content: "-"; + margin-right: 10px; + font-size: 20px; + color: var(--bs-blue); + } + + .category--collapsed::before { + content: "+"; + margin-right: 10px; + font-size: 20px; + color: var(--bs-blue); + } + + .child-list { + padding-left: 1rem; + margin-top: 8px; + list-style-type: none; + background-color: white; + border-radius: 4px; + padding: 1rem; + border: 1px solid #ddd; + } + + .child-item { + margin: 4px 0; + display: flex; + align-items: center; + font-size: 1rem; + cursor: pointer; + } + + .child-item input { + margin-right: 8px; + } + + .child-item:hover { + background-color: #f9f9f9; + border-radius: 4px; + padding: 4px; + } + + .child-item--selected { + font-weight: bold; + color: var(--bs-blue); + } +` + +export interface HierarchicalItem { + id: string + label: string + items: { value: string; label: string; isSelected?: boolean }[] +} + +interface HierarchicalMenuWidgetProps { + categories: HierarchicalItem[] + onSelectionChange?: ( + categoryId: string, + selectedItems: { value: string; label: string }[] + ) => void +} + +const HierarchicalMenuWidget: React.FC = ({ + categories, + onSelectionChange +}) => { + const [expandedCategories, setExpandedCategories] = useState([]) + + const toggleCategory = (id: string) => { + setExpandedCategories(prev => + prev.includes(id) ? prev.filter(catId => catId !== id) : [...prev, id] + ) + } + + const handleChildSelection = ( + categoryId: string, + selectedItem: { value: string; label: string }, + isSelected: boolean + ) => { + if (onSelectionChange) { + const updatedItem = { ...selectedItem, isSelected } + onSelectionChange(categoryId, [updatedItem]) + } + } + + return ( + + {categories.map(category => ( +
+ {/* Category */} +
toggleCategory(category.id)} + > + {category.label} +
+ + {/* Child Items */} + {expandedCategories.includes(category.id) && ( +
    + {category.items.map(item => ( +
  • + handleChildSelection(category.id, item, !item.isSelected) + } + > + + e.stopPropagation() + } /* Prevent click propagation */ + /> + {item.label} +
  • + ))} +
+ )} +
+ ))} +
+ ) +} + +export default HierarchicalMenuWidget diff --git a/components/search/SearchableHierarchicalMenu.tsx b/components/search/SearchableHierarchicalMenu.tsx deleted file mode 100644 index 46e896ca3..000000000 --- a/components/search/SearchableHierarchicalMenu.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React, { useState } from "react" -import { HierarchicalMenu, HierarchicalMenuProps } from "react-instantsearch" - -type Props = { - attributes: string[] -} - -const SearchableHierarchicalMenu: React.FC = ({ attributes }) => { - const [searchQuery, setSearchQuery] = useState("") - - const handleInputChange = (event: React.ChangeEvent) => { - setSearchQuery(event.target.value) - } - - const filterAndTransformItems = (items: any[]) => - items - .filter(item => - item.label.toLowerCase().includes(searchQuery.toLowerCase()) - ) - .map(item => ({ - ...item, - label: item.label.toUpperCase() - })) - const transformItems: HierarchicalMenuProps["transformItems"] = items => { - if (!items) { - console.error("transformItems received null or undefined items") - return [] - } - return items.map(item => ({ - ...item, - label: item.label - })) - } - return ( -
- {/* Custom Search Input */} - {/* */} - - {/* HierarchicalMenu with filter and transform logic */} - -
- ) -} - -export default SearchableHierarchicalMenu diff --git a/components/search/bills/useBillRefinements.tsx b/components/search/bills/useBillRefinements.tsx index 4e613ae61..c2ecd0512 100644 --- a/components/search/bills/useBillRefinements.tsx +++ b/components/search/bills/useBillRefinements.tsx @@ -6,6 +6,14 @@ import { useRefinements } from "../useRefinements" export const useBillRefinements = () => { const baseProps = { limit: 500, searchable: true } const propsList = [ + { + attribute: "topics.lvl0", + ...baseProps + }, + { + attribute: "topics.lvl1", + ...baseProps + }, { transformItems: useCallback( (i: RefinementListItem[]) => diff --git a/components/search/useRefinements.tsx b/components/search/useRefinements.tsx index 5d9eaebd2..850f2d563 100644 --- a/components/search/useRefinements.tsx +++ b/components/search/useRefinements.tsx @@ -11,7 +11,9 @@ import styled from "styled-components" import { useMediaQuery } from "usehooks-ts" import { Button, Col, Offcanvas } from "../bootstrap" import { SearchContainer } from "./SearchContainer" -import SearchableHierarchicalMenu from "./SearchableHierarchicalMenu" +import HierarchicalMenuWidget, { + HierarchicalItem +} from "./HierarchicalMenuWidget" export const FilterButton = styled(Button)` font-size: 1rem; @@ -20,7 +22,6 @@ export const FilterButton = styled(Button)` padding: 0.25rem 0.5rem 0.25rem 0.5rem; align-self: flex-start; ` - const useHasRefinements = () => { const { results } = useInstantSearch() const refinements = results.getRefinements() @@ -37,21 +38,17 @@ export const useRefinements = ({ const handleClose = useCallback(() => setShow(false), []) const handleOpen = useCallback(() => setShow(true), []) - useEffect(() => { - if (inline) setShow(false) - }, [inline]) - const refinements = ( <> {refinementProps.slice(2).map((p, i) => ( - + ))} ) From 462947f3aed64f3cc76bb5dcd91e3a8c5dd4d38c Mon Sep 17 00:00:00 2001 From: huanfeng Date: Tue, 10 Dec 2024 21:08:57 -0500 Subject: [PATCH 04/17] Create useHierarchicalMenu and fix add padding to the left of child --- components/search/SearchContainer.tsx | 10 ++- components/search/bills/BillSearch.tsx | 8 ++- .../search/bills/useBillHierarchicalMenu.tsx | 17 +++++ .../search/bills/useBillRefinements.tsx | 8 --- .../search/testimony/TestimonySearch.tsx | 4 +- components/search/useHierarchicalMenu.tsx | 70 +++++++++++++++++++ components/search/useRefinements.tsx | 16 +---- 7 files changed, 107 insertions(+), 26 deletions(-) create mode 100644 components/search/bills/useBillHierarchicalMenu.tsx create mode 100644 components/search/useHierarchicalMenu.tsx diff --git a/components/search/SearchContainer.tsx b/components/search/SearchContainer.tsx index cc6952147..9f5730a2e 100644 --- a/components/search/SearchContainer.tsx +++ b/components/search/SearchContainer.tsx @@ -183,7 +183,7 @@ export const SearchContainer = styled.div` display: inline-block; overflow-y: visible; margin: 0; - padding: 0 0 0 0rem; + padding: 0 0 0 20px; width: 100%; } .ais-HierarchicalMenu-link::before { @@ -206,6 +206,12 @@ export const SearchContainer = styled.div` } .ais-HierarchicalMenu-list--child .ais-HierarchicalMenu-item:last-child { - border-top: none; /* Remove top border for the last child */ + border-top: none; + } + + .bill-search-filter { + display: flex; + flex-wrap: wrap; + align-items: stretch; } ` diff --git a/components/search/bills/BillSearch.tsx b/components/search/bills/BillSearch.tsx index b224f467f..7f1c14ad2 100644 --- a/components/search/bills/BillSearch.tsx +++ b/components/search/bills/BillSearch.tsx @@ -17,6 +17,7 @@ import { SearchErrorBoundary } from "../SearchErrorBoundary" import { useRouting } from "../useRouting" import { BillHit } from "./BillHit" import { useBillRefinements } from "./useBillRefinements" +import { useBillHierarchicalMenu } from "./useBillHierarchicalMenu" import { SortBy, SortByWithConfigurationItem } from "../SortBy" import { getServerConfig } from "../common" import { useBillSort } from "./useBillSort" @@ -75,6 +76,7 @@ const Layout: FC< React.PropsWithChildren<{ items: SortByWithConfigurationItem[] }> > = ({ items }) => { const refinements = useBillRefinements() + const hierarchicalMenu = useBillHierarchicalMenu() const status = useSearchStatus() return ( @@ -83,11 +85,15 @@ const Layout: FC< - {refinements.options} + + {hierarchicalMenu.options} + {refinements.options} + + {hierarchicalMenu.show} {refinements.show} { + const baseProps = { limit: 500, searchable: true } + const propsList = [ + { + attribute: "topics.lvl0", + ...baseProps + }, + { + attribute: "topics.lvl1", + ...baseProps + } + ] + + return useHierarchicalMenu({ hierarchicalMenuProps: propsList }) +} diff --git a/components/search/bills/useBillRefinements.tsx b/components/search/bills/useBillRefinements.tsx index c2ecd0512..4e613ae61 100644 --- a/components/search/bills/useBillRefinements.tsx +++ b/components/search/bills/useBillRefinements.tsx @@ -6,14 +6,6 @@ import { useRefinements } from "../useRefinements" export const useBillRefinements = () => { const baseProps = { limit: 500, searchable: true } const propsList = [ - { - attribute: "topics.lvl0", - ...baseProps - }, - { - attribute: "topics.lvl1", - ...baseProps - }, { transformItems: useCallback( (i: RefinementListItem[]) => diff --git a/components/search/testimony/TestimonySearch.tsx b/components/search/testimony/TestimonySearch.tsx index 03397e022..601a5ba80 100644 --- a/components/search/testimony/TestimonySearch.tsx +++ b/components/search/testimony/TestimonySearch.tsx @@ -142,7 +142,9 @@ const Layout = () => { - {refinements.options} + + {refinements.options} + diff --git a/components/search/useHierarchicalMenu.tsx b/components/search/useHierarchicalMenu.tsx new file mode 100644 index 000000000..b1b942d3d --- /dev/null +++ b/components/search/useHierarchicalMenu.tsx @@ -0,0 +1,70 @@ +import { HierarchicalMenu, useInstantSearch } from "react-instantsearch" +import { faFilter } from "@fortawesome/free-solid-svg-icons" +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" +import { useCallback, useEffect, useState } from "react" +import styled from "styled-components" +import { useMediaQuery } from "usehooks-ts" +import { Button, Col, Offcanvas } from "../bootstrap" +import { SearchContainer } from "./SearchContainer" + +export const FilterButton = styled(Button)` + font-size: 1rem; + line-height: 1rem; + min-height: 2rem; + padding: 0.25rem 0.5rem 0.25rem 0.5rem; + align-self: flex-start; +` +const useHasRefinements = () => { + const { results } = useInstantSearch() + const refinements = results.getRefinements() + return refinements.length !== 0 +} + +export const useHierarchicalMenu = ({ + hierarchicalMenuProps +}: { + hierarchicalMenuProps: any[] +}) => { + const inline = useMediaQuery("(min-width: 768px)") + const [show, setShow] = useState(false) + const handleClose = useCallback(() => setShow(false), []) + const handleOpen = useCallback(() => setShow(true), []) + + const hierarchicalMenu = ( + <> + + + ) + const hasRefinements = useHasRefinements() + + return { + options: inline ? ( +
{hierarchicalMenu}
+ ) : ( + + + Filter + + + {hierarchicalMenu} + + + ), + show: inline ? null : ( + + Filter + + ) + } +} diff --git a/components/search/useRefinements.tsx b/components/search/useRefinements.tsx index 850f2d563..2cfb62a28 100644 --- a/components/search/useRefinements.tsx +++ b/components/search/useRefinements.tsx @@ -11,9 +11,6 @@ import styled from "styled-components" import { useMediaQuery } from "usehooks-ts" import { Button, Col, Offcanvas } from "../bootstrap" import { SearchContainer } from "./SearchContainer" -import HierarchicalMenuWidget, { - HierarchicalItem -} from "./HierarchicalMenuWidget" export const FilterButton = styled(Button)` font-size: 1rem; @@ -40,14 +37,7 @@ export const useRefinements = ({ const refinements = ( <> - - {refinementProps.slice(2).map((p, i) => ( + {refinementProps.map((p, i) => ( ))} @@ -56,9 +46,7 @@ export const useRefinements = ({ return { options: inline ? ( - - {refinements} - +
{refinements}
) : ( From b579b49d5e27aec12621cc8f4634f3db0623cb69 Mon Sep 17 00:00:00 2001 From: huanfeng Date: Sun, 15 Dec 2024 01:48:04 -0500 Subject: [PATCH 05/17] Craete a customize HierarchicalMenuWidget --- components/search/HierarchicalMenuWidget.tsx | 529 +++++++++++++----- components/search/SearchContainer.tsx | 91 ++- .../search/bills/useBillRefinements.tsx | 10 + components/search/useHierarchicalMenu.tsx | 5 +- components/search/useRefinements.tsx | 2 +- package.json | 4 +- tsconfig.json | 3 +- yarn.lock | 193 +++++++ 8 files changed, 701 insertions(+), 136 deletions(-) diff --git a/components/search/HierarchicalMenuWidget.tsx b/components/search/HierarchicalMenuWidget.tsx index 3fa0f2cd2..b7dc5ff4d 100644 --- a/components/search/HierarchicalMenuWidget.tsx +++ b/components/search/HierarchicalMenuWidget.tsx @@ -1,156 +1,427 @@ -import React, { useState } from "react" -import styled from "styled-components" - -const StyledMenu = styled.div` - font-family: "Nunito"; - - .category { - cursor: pointer; - font-size: 1rem; - font-weight: bold; - margin: 8px 0; - padding: 8px; - background-color: white; - border-radius: 4px; - border-bottom: dashed 1px; - display: flex; - align-items: center; - } +import { useConnector } from "react-instantsearch" +import type { SearchResults } from "algoliasearch-helper" +import type { Connector } from "instantsearch.js" +import type { AdditionalWidgetProperties } from "react-instantsearch" +import { useCallback, useMemo, useState } from "react" - .category:hover { - background-color: #f5f5f5; - } +const cx = (...classNames: string[]): string => + classNames.filter(Boolean).join(" ") - .category--expanded::before { - content: "-"; - margin-right: 10px; - font-size: 20px; - color: var(--bs-blue); - } +// Types - .category--collapsed::before { - content: "+"; - margin-right: 10px; - font-size: 20px; - color: var(--bs-blue); - } +type MultiselectHierarchicalMenuItem = SearchResults.FacetValue & { + label: string +} - .child-list { - padding-left: 1rem; - margin-top: 8px; - list-style-type: none; - background-color: white; - border-radius: 4px; - padding: 1rem; - border: 1px solid #ddd; - } +type MultiselectHierarchicalMenuLevel = { + attribute: string + items: MultiselectHierarchicalMenuItem[] + refine: (value: string) => void +} - .child-item { - margin: 4px 0; - display: flex; - align-items: center; - font-size: 1rem; - cursor: pointer; - } +type MultiselectHierarchicalMenuRender = { + levels: MultiselectHierarchicalMenuLevel[] +} - .child-item input { - margin-right: 8px; - } +type MultiselectHierarchicalMenuState = { + levels: MultiselectHierarchicalMenuLevel[] + refinements: string[] +} - .child-item:hover { - background-color: #f9f9f9; - border-radius: 4px; - padding: 4px; +type MultiselectHierarchicalMenuWidget = { + $$type: string + renderState: MultiselectHierarchicalMenuRender + indexRenderState: { + multiselectHierarchicalMenu: MultiselectHierarchicalMenuRender } - - .child-item--selected { - font-weight: bold; - color: var(--bs-blue); + indexUiState: { + multiselectHierarchicalMenu: MultiselectHierarchicalMenuRender } -` +} -export interface HierarchicalItem { - id: string - label: string - items: { value: string; label: string; isSelected?: boolean }[] +export type MultiselectHierarchicalMenuParams = { + attributes: string[] + separator?: string } -interface HierarchicalMenuWidgetProps { - categories: HierarchicalItem[] - onSelectionChange?: ( - categoryId: string, - selectedItems: { value: string; label: string }[] - ) => void +// Connector + +export type MultiselectHierarchicalMenuConnector = Connector< + MultiselectHierarchicalMenuWidget, + MultiselectHierarchicalMenuParams +> + +export const connectMultiselectHierarchicalMenu: MultiselectHierarchicalMenuConnector = + (renderFn, unmountFn = () => {}) => { + return widgetParams => { + const { attributes, separator } = widgetParams + + // Store information that needs to be shared across multiple method calls. + const connectorState: MultiselectHierarchicalMenuState = { + levels: [], + refinements: [] + } + + return { + $$type: "ais.multiselectHierarchicalMenu", + getWidgetRenderState({ results, helper }) { + // When there are no results, return the API with default values. + if (!results) return { levels: [], widgetParams } + + // Get the last refinement. + const lastRefinement = results.getRefinements().pop()?.attributeName + + // Merge the results items with the initial ones. + const getItems = ( + attribute: string + ): MultiselectHierarchicalMenuItem[] => { + // Safely attempt to retrieve facet values, default to an empty array if unavailable + const facetValues = + (results?.getFacetValues(attribute, { + sortBy: ["name:asc"] + }) as SearchResults.FacetValue[]) || [] + + // Mapping over facetValues with an additional safety check + const resultsItems = + facetValues.length > 0 + ? facetValues.map(facetValue => ({ + ...facetValue, + label: facetValue.name + .split(separator || " > ") + .pop() as string, + count: facetValue.count + })) + : [] + + if (lastRefinement && !attributes.includes(lastRefinement)) + return resultsItems + + const level = connectorState.levels.find( + level => level.attribute === attribute + ) + const levelItems = level?.items || [] + + if (!levelItems.length && resultsItems.length) return resultsItems + if (!resultsItems.length) return levelItems + + // Merge and sort items from results and existing state + const mergedItems = levelItems.map(levelItem => { + const resultsItem = resultsItems.find( + resultItem => resultItem.name === levelItem.name + ) + return resultsItem ? { ...levelItem, ...resultsItem } : levelItem + }) + + return mergedItems.sort((a, b) => a.name.localeCompare(b.name)) + } + + // Register refinements and items for each attribute. + for (const [i, attribute] of attributes.entries()) { + if (!connectorState.levels[i]) { + const refine = (value: string) => { + for (const attr of attributes) { + const isLastAttribute = + attribute === attributes[attributes.length - 1] && + attribute === attr + if ( + !isLastAttribute && + helper.getRefinements(attr).length > 0 + ) + helper.clearRefinements(attr) + } + const refinement = helper + .getRefinements(attribute) + .find(ref => ref.value === value) + refinement + ? helper.removeDisjunctiveFacetRefinement(attribute, value) + : helper.addDisjunctiveFacetRefinement(attribute, value) + helper.search() + } + connectorState.levels[i] = { attribute, refine, items: [] } + } + + // Register the initial items. + if (results && !connectorState.levels[i].items.length) { + connectorState.levels[i].items = getItems(attribute) + } + } + + // Call the getItems to get the updated items state. + const levels = connectorState.levels.map(level => ({ + ...level, + items: getItems(level.attribute) + })) + + return { levels, widgetParams } + }, + getRenderState(renderState, renderOptions) { + return { + ...renderState, + multiselectHierarchicalMenu: { + ...renderState.multiselectHierarchicalMenu, + ...this.getWidgetRenderState(renderOptions) + } + } + }, + init(initOptions) { + const { instantSearchInstance } = initOptions + const renderState = this.getWidgetRenderState(initOptions) + console.log("RenderState at init:", renderState) + + renderFn( + { + ...renderState, + instantSearchInstance + }, + false + ) + }, + render(renderOptions) { + const { instantSearchInstance } = renderOptions + const renderState = this.getWidgetRenderState(renderOptions) + console.log("RenderState at render:", renderState) + + renderFn( + { + ...renderState, + instantSearchInstance + }, + false + ) + }, + dispose() { + unmountFn() + }, + getWidgetUiState(uiState, { searchParameters }) { + const state = attributes.reduce( + (levelState, attribute) => ({ + ...levelState, + [attribute]: + searchParameters.getDisjunctiveRefinements(attribute) || [] + }), + {} + ) + + return { + ...uiState, + multiselectHierarchicalMenu: { + ...uiState.multiselectHierarchicalMenu, + levels: uiState.multiselectHierarchicalMenu?.levels || [] + } + } + }, + getWidgetSearchParameters(searchParameters, { uiState }) { + // Apply the refinements from the URL parameters. + for (const attribute of attributes) { + const values = + (uiState.multiselectHierarchicalMenu?.[ + attribute as keyof MultiselectHierarchicalMenuRender + ] as unknown as string[]) || [] + + if (Array.isArray(values)) { + const refinements = + searchParameters.disjunctiveFacetsRefinements[attribute] || [] + + searchParameters.disjunctiveFacetsRefinements = { + ...searchParameters.disjunctiveFacetsRefinements, + [attribute]: [...refinements, ...values] + } + } + } + + return searchParameters + } + } + } + } + +// Hook + +export const useMultiselectHierarchicalMenu = ( + props: MultiselectHierarchicalMenuParams, + additionalWidgetProperties?: AdditionalWidgetProperties +): MultiselectHierarchicalMenuState => { + return useConnector( + connectMultiselectHierarchicalMenu, + props, + additionalWidgetProperties + ) as MultiselectHierarchicalMenuState } -const HierarchicalMenuWidget: React.FC = ({ - categories, - onSelectionChange -}) => { - const [expandedCategories, setExpandedCategories] = useState([]) +// Component - const toggleCategory = (id: string) => { - setExpandedCategories(prev => - prev.includes(id) ? prev.filter(catId => catId !== id) : [...prev, id] - ) +type MultiselectHierarchicalMenuElementProps = { + levels: MultiselectHierarchicalMenuLevel[] + index?: number + item?: SearchResults.FacetValue & { label: string } +} + +const MultiselectHierarchicalMenuItem = ({ + levels, + index = 0, + item = { + name: "", + label: "", + escapedValue: "", + count: 0, + isRefined: false, + isExcluded: false } +}: MultiselectHierarchicalMenuElementProps): JSX.Element => { + const subLevelItems = useMemo(() => { + const subLevel = levels[index + 1] + if (!subLevel) return [] + return subLevel.items.filter(subItem => subItem.name.startsWith(item.name)) + }, [levels, index, item]) + + const hasSubLevel: boolean = useMemo( + () => subLevelItems.length > 0, + [subLevelItems.length] + ) + + const isSubLevelRefined: boolean = useMemo( + () => subLevelItems.some(subItem => subItem.isRefined), + [subLevelItems] + ) + + const [isOpen, setIsOpen] = useState( + item.isRefined || isSubLevelRefined + ) - const handleChildSelection = ( - categoryId: string, - selectedItem: { value: string; label: string }, - isSelected: boolean - ) => { - if (onSelectionChange) { - const updatedItem = { ...selectedItem, isSelected } - onSelectionChange(categoryId, [updatedItem]) + const { refine }: MultiselectHierarchicalMenuLevel = useMemo( + () => levels[index], + [levels, index] + ) + + const onButtonClick = useCallback(() => { + if (isOpen) { + // Clear all refinements + levels.forEach(level => { + level.items.forEach(subItem => { + if (subItem.isRefined) { + level.refine(subItem.name) + } + }) + }) } - } + setIsOpen(!isOpen) + }, [isOpen, levels]) + + const onLabelClick = useCallback(() => { + if (hasSubLevel) { + onButtonClick + return + } + if (item.isRefined && isOpen && !isSubLevelRefined) { + setIsOpen(false) + refine(item.name) + return + } + setIsOpen(hasSubLevel || !isOpen) + refine(item.name) + }, [ + hasSubLevel, + item.isRefined, + item.name, + isOpen, + isSubLevelRefined, + refine, + onButtonClick + ]) return ( - - {categories.map(category => ( -
- {/* Category */} -
+
- - {/* Child Items */} - {expandedCategories.includes(category.id) && ( -
    - {category.items.map(item => ( -
  • - handleChildSelection(category.id, item, !item.isSelected) - } - > - - e.stopPropagation() - } /* Prevent click propagation */ - /> - {item.label} -
  • - ))} -
+ {isOpen ? "-" : "+"} + + ) : ( + + )} + + > + {item.label} + + + {item.count} + + + {hasSubLevel && isOpen && ( + + )} + + ) +} + +const MultiselectHierarchicalMenuList = ({ + levels, + index = 0, + item +}: MultiselectHierarchicalMenuElementProps): JSX.Element => { + const levelItems = useMemo( + () => + levels[index].items.filter( + levelItem => !item || levelItem.name.startsWith(item.name) + ), + [levels, index, item] + ) + + return ( +
    + {levelItems.map(levelItem => ( + ))} - +
) } -export default HierarchicalMenuWidget +export const MultiselectHierarchicalMenu = ({ + attributes, + separator = " + " +}: MultiselectHierarchicalMenuParams): JSX.Element => { + const { levels } = useMultiselectHierarchicalMenu({ attributes, separator }) + + return ( +
+ {levels.length > 0 && } +
+ ) +} diff --git a/components/search/SearchContainer.tsx b/components/search/SearchContainer.tsx index 9f5730a2e..ae88e59de 100644 --- a/components/search/SearchContainer.tsx +++ b/components/search/SearchContainer.tsx @@ -159,7 +159,7 @@ export const SearchContainer = styled.div` .ais-HierarchicalMenu-item { font-size: 1rem; - border-bottom: dashed 1px; + /* border-bottom: dashed 1px; */ } .ais-HierarchicalMenu-label { @@ -214,4 +214,93 @@ export const SearchContainer = styled.div` flex-wrap: wrap; align-items: stretch; } + .ais-HierarchicalMenu-list--child .ais-HierarchicalMenu-link::before { + content: ""; + width: 22px; + height: 16px; + border: 2px solid navy; + background-color: white; + background-image: none; + font-size: 35px; + vertical-align: middle; + margin-bottom: 0rem; + box-shadow: 0 0 0 1.5px black; + } + .ais-HierarchicalMenu-list--child + .ais-HierarchicalMenu-link--selected::before { + content: "✔"; + display: inline-block; + font-size: 16px; + text-align: center; + line-height: 14px; + width: 22px; + height: 16px; + } + + .ais-MultiselectHierarchicalMenu-list { + background-color: white; + padding: 1rem; + border-radius: 4px; + margin-top: 0.5rem; + margin-bottom: 1.5rem; + max-height: 250px; + overflow-y: auto; + } + + .ais-MultiselectHierarchicalMenu-item { + font-size: 1rem; + /* border-bottom: dashed 1px; */ + } + + .ais-MultiselectHierarchicalMenu-label { + white-space: normal; + display: flex; + width: 100%; + align-items: center; + gap: 10px; +} + } + + .ais-MultiselectHierarchicalMenu-count { + background: var(--bs-blue); + color: white; + font-size: 0.75rem; + line-height: 1rem; + padding-right: 10px; + padding-left: 10px; + border-radius: 10px; + border: none; + } + + .ais-MultiselectHierarchicalMenu-list + .ais-MultiselectHierarchicalMenu-list--child { + display: block; + display: inline-block; + overflow-y: visible; + margin: 0; + padding: 0 0 0 20px; + width: 100%; + } + .ais-MultiselectHierarchicalMenu-toggle { + font-size: 25px; + color: var(--bs-blue); + vertical-align: middle; + margin-bottom: 1rem; + background-color: transparent; + border: none; + cursor: pointer; + } + .ais-MultiselectHierarchicalMenu-link--selected::before { + content: "-"; + background-image: none; + font-size: 50px; + color: var(--bs-blue); + vertical-align: middle; + line-height: -3; + margin-bottom: 1rem; + } + + .ais-HierarchicalMenu-list--child .ais-HierarchicalMenu-item:last-child { + border-top: none; + } ` diff --git a/components/search/bills/useBillRefinements.tsx b/components/search/bills/useBillRefinements.tsx index 4e613ae61..7f3faba3a 100644 --- a/components/search/bills/useBillRefinements.tsx +++ b/components/search/bills/useBillRefinements.tsx @@ -40,6 +40,16 @@ export const useBillRefinements = () => { attribute: "cosponsors", ...baseProps, searchablePlaceholder: "Cosponsor" + }, + { + attribute: "topics.lvl0", + ...baseProps, + searchablePlaceholder: "topics.lvl0" + }, + { + attribute: "topics.lvl1", + ...baseProps, + searchablePlaceholder: "topics.lvl1" } ] diff --git a/components/search/useHierarchicalMenu.tsx b/components/search/useHierarchicalMenu.tsx index b1b942d3d..816ac64c8 100644 --- a/components/search/useHierarchicalMenu.tsx +++ b/components/search/useHierarchicalMenu.tsx @@ -6,7 +6,7 @@ import styled from "styled-components" import { useMediaQuery } from "usehooks-ts" import { Button, Col, Offcanvas } from "../bootstrap" import { SearchContainer } from "./SearchContainer" - +import { MultiselectHierarchicalMenu } from "./HierarchicalMenuWidget" export const FilterButton = styled(Button)` font-size: 1rem; line-height: 1rem; @@ -32,12 +32,11 @@ export const useHierarchicalMenu = ({ const hierarchicalMenu = ( <> - ) diff --git a/components/search/useRefinements.tsx b/components/search/useRefinements.tsx index 2cfb62a28..453e989ea 100644 --- a/components/search/useRefinements.tsx +++ b/components/search/useRefinements.tsx @@ -38,7 +38,7 @@ export const useRefinements = ({ const refinements = ( <> {refinementProps.map((p, i) => ( - + ))} ) diff --git a/package.json b/package.json index 335b224de..cebe004e4 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "@react-aria/utils": "^3.13.1", "@reduxjs/toolkit": "^1.8.3", "@testing-library/dom": "^9.3.3", + "algoliasearch": "^5.17.0", "autolinker": "^3.16.0", "awesome-debounce-promise": "^2.1.0", "bootstrap": "^5.3.2", @@ -117,6 +118,7 @@ "react-i18next": "^13.2.2", "react-inlinesvg": "^3.0.1", "react-instantsearch": "^7.12.4", + "react-instantsearch-hooks-web": "^6.47.3", "react-is": "^18.2.0", "react-markdown": "^8.0.4", "react-overlays": "^5.1.1", @@ -172,9 +174,9 @@ "eslint": "^8.7.0", "eslint-config-next": "^14.0.4", "eslint-config-prettier": "^8.3.0", - "firebase-admin": "^10", "eslint-plugin-i18next": "^6.0.3", "eslint-plugin-jsx-a11y": "^6.9.0", + "firebase-admin": "^10", "firebase-tools": "^11.16.0", "ini": "^1.3.5", "inquirer": "^6.5.1", diff --git a/tsconfig.json b/tsconfig.json index e047c1dff..f38cae5ca 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,8 @@ "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", - "incremental": true + "incremental": true, + "downlevelIteration": true }, "include": [ "next-env.d.ts", diff --git a/yarn.lock b/yarn.lock index e8d641512..2d36a96cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,11 +12,135 @@ resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.2.tgz#a6abc715fb6884851fca9dad37fc34739a04fd11" integrity sha512-DA5a1C0gD/pLOvhv33YMrbf2FK3oUzwNl9oOJqE4XVjuEtt6XIakRcsd7eLiOSPkp1kTRQGICTA8cKra/vFbjw== +"@algolia/client-abtesting@5.17.0": + version "5.17.0" + resolved "https://registry.yarnpkg.com/@algolia/client-abtesting/-/client-abtesting-5.17.0.tgz#9f1e3edb9ccd256ebad75490390b8c16bbb4cddd" + integrity sha512-6+7hPdOEPfJqjWNYPRaVcttLLAtVqQyp1U7xBA1e1uSya1ivIr9FtS/GBr31mfvwk2N2yxV4W7itxuBtST8SWg== + dependencies: + "@algolia/client-common" "5.17.0" + "@algolia/requester-browser-xhr" "5.17.0" + "@algolia/requester-fetch" "5.17.0" + "@algolia/requester-node-http" "5.17.0" + +"@algolia/client-analytics@5.17.0": + version "5.17.0" + resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-5.17.0.tgz#8d3fdd97ac48940f47cb392ae9be27b15c707197" + integrity sha512-nhJ+elL8h0Fts3xD9261zE2NvTs7nPMe9/SfAgMnWnbvxmuhJn7ZymnBsfm2VkTDb4Dy810ZAdBfzYEk7PjlAw== + dependencies: + "@algolia/client-common" "5.17.0" + "@algolia/requester-browser-xhr" "5.17.0" + "@algolia/requester-fetch" "5.17.0" + "@algolia/requester-node-http" "5.17.0" + +"@algolia/client-common@5.17.0": + version "5.17.0" + resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-5.17.0.tgz#000a8cfadbda56e90563d2cf0872de98940ff153" + integrity sha512-9eC8i41/4xcQ/wI6fVM4LwC/ZGcDl3mToqjM0wTZzePWhXgRrdzOzqy/XgP+L1yYCDfkMFBZZsruNL5U8aEOag== + +"@algolia/client-insights@5.17.0": + version "5.17.0" + resolved "https://registry.yarnpkg.com/@algolia/client-insights/-/client-insights-5.17.0.tgz#b2d869e24914ea1b31fa8b79ead57c92ec83c809" + integrity sha512-JL/vWNPUIuScsJubyC4aPHkpMftlK2qGqMiR2gy0rGvrh8v0w+ec6Ebq+efoFgE8wO55HJPTxiKeerE1DaQgvA== + dependencies: + "@algolia/client-common" "5.17.0" + "@algolia/requester-browser-xhr" "5.17.0" + "@algolia/requester-fetch" "5.17.0" + "@algolia/requester-node-http" "5.17.0" + +"@algolia/client-personalization@5.17.0": + version "5.17.0" + resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-5.17.0.tgz#99884afab8c30c6079cc737b0eb4cc206e492462" + integrity sha512-PkMUfww8QiRpyLkW4kzmc7IJDcW90sfUpnTgUOVlug5zEE2iv1ruHrJxdcNRTXkA0fgVpHu3oxXmCQL/ie2p7A== + dependencies: + "@algolia/client-common" "5.17.0" + "@algolia/requester-browser-xhr" "5.17.0" + "@algolia/requester-fetch" "5.17.0" + "@algolia/requester-node-http" "5.17.0" + +"@algolia/client-query-suggestions@5.17.0": + version "5.17.0" + resolved "https://registry.yarnpkg.com/@algolia/client-query-suggestions/-/client-query-suggestions-5.17.0.tgz#748fcd97192b7c208e42c855faf2528ff26e1eca" + integrity sha512-bokfgPN2whetLuiX9NB6C6d7Eke+dvHuASOPiB+jdI8Z6hacLHkcJjYeZY4Mppj0/oJ1KlyNivj+8WNpZeGhYA== + dependencies: + "@algolia/client-common" "5.17.0" + "@algolia/requester-browser-xhr" "5.17.0" + "@algolia/requester-fetch" "5.17.0" + "@algolia/requester-node-http" "5.17.0" + +"@algolia/client-search@5.17.0": + version "5.17.0" + resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-5.17.0.tgz#ee666d847d9753e7962c06f0a00036779c735767" + integrity sha512-alY3U79fiEvlR/0optgt1LZp9MfthXFnuEA4GYS81svozDOF61gdvxgBjt6SYtmskmTQQZDWVgakvUvvHrDzMw== + dependencies: + "@algolia/client-common" "5.17.0" + "@algolia/requester-browser-xhr" "5.17.0" + "@algolia/requester-fetch" "5.17.0" + "@algolia/requester-node-http" "5.17.0" + "@algolia/events@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@algolia/events/-/events-4.0.1.tgz#fd39e7477e7bc703d7f893b556f676c032af3950" integrity sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ== +"@algolia/ingestion@1.17.0": + version "1.17.0" + resolved "https://registry.yarnpkg.com/@algolia/ingestion/-/ingestion-1.17.0.tgz#a6d2ebb9feb7ef7e5ef206c05575743cbc350587" + integrity sha512-9+mO+FbIpWz6izh1lXzON9BcenBKx4K3qVjSWiFFmL8nv+7b7zpGq++LXWr/Lxv/bZ9+D71Go6QVL6AZQhFOmg== + dependencies: + "@algolia/client-common" "5.17.0" + "@algolia/requester-browser-xhr" "5.17.0" + "@algolia/requester-fetch" "5.17.0" + "@algolia/requester-node-http" "5.17.0" + +"@algolia/monitoring@1.17.0": + version "1.17.0" + resolved "https://registry.yarnpkg.com/@algolia/monitoring/-/monitoring-1.17.0.tgz#c89c2277a91d5bf5e36385dd9cc363e90206e60c" + integrity sha512-Db7Qh51zVchmHa8d9nQFzTz2Ta6H2D4dpCnPj1giC/LE6UG/6e3iOnRxUzV+9ZR7etHKIrri2hbnkyNrvbqA9A== + dependencies: + "@algolia/client-common" "5.17.0" + "@algolia/requester-browser-xhr" "5.17.0" + "@algolia/requester-fetch" "5.17.0" + "@algolia/requester-node-http" "5.17.0" + +"@algolia/recommend@5.17.0": + version "5.17.0" + resolved "https://registry.yarnpkg.com/@algolia/recommend/-/recommend-5.17.0.tgz#dd770f09eee811b2bdb9f9535fa7dba425d77e68" + integrity sha512-7vM4+mfuLYbslj8+RNsP/ISwY7izu5HcQqQhA0l+q3EZRHF+PBeRaJXc3S1N0fTRxj8ystvwXWZPmjssB/xMLw== + dependencies: + "@algolia/client-common" "5.17.0" + "@algolia/requester-browser-xhr" "5.17.0" + "@algolia/requester-fetch" "5.17.0" + "@algolia/requester-node-http" "5.17.0" + +"@algolia/requester-browser-xhr@5.17.0": + version "5.17.0" + resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.17.0.tgz#9bdf1e4b0b734a0f56fc102f08bc2052b37b49b7" + integrity sha512-bXSiPL2R08s4e9qvNZsJA0bXZeyWH2A5D4shS8kRT22b8GgjtnGTuoZmi6MxtKOEaN0lpHPbjvjXAO7UIOhDog== + dependencies: + "@algolia/client-common" "5.17.0" + +"@algolia/requester-fetch@5.17.0": + version "5.17.0" + resolved "https://registry.yarnpkg.com/@algolia/requester-fetch/-/requester-fetch-5.17.0.tgz#0b6e08047d20c719925083d29613855cbbc42034" + integrity sha512-mjJ6Xv7TlDDoZ6RLKrEzH1ved3g2GAq3YJjb94bA639INfxK1HM8A/wCAFSZ8ye+QM/jppwauDXe1PENkuareQ== + dependencies: + "@algolia/client-common" "5.17.0" + +"@algolia/requester-node-http@5.17.0": + version "5.17.0" + resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-5.17.0.tgz#9693ed6aa683c68f9ffab29d644cb2093f4885d3" + integrity sha512-Z2BXTR7BctlGPNig21k2wf/5nlH+96lU2UElzXTKiptyn2iM8lDU8zdO+dRll0AxQUxUGWEnkBysst9xL3S2cg== + dependencies: + "@algolia/client-common" "5.17.0" + +"@algolia/ui-components-highlight-vdom@^1.2.1": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@algolia/ui-components-highlight-vdom/-/ui-components-highlight-vdom-1.2.3.tgz#d8554266705193a03ecf700e1b1da12e89577772" + integrity sha512-gNlPkCwX2M7LnNhCSAjvQkOhdeYDu+FP0ISc0IY297Kw9EmGeUxpce8lvKGByYglz7ifW3qTWRxrtCV3is5+Ag== + dependencies: + "@algolia/ui-components-shared" "1.2.3" + "@babel/runtime" "^7.0.0" + "@algolia/ui-components-highlight-vdom@^1.2.2": version "1.2.2" resolved "https://registry.yarnpkg.com/@algolia/ui-components-highlight-vdom/-/ui-components-highlight-vdom-1.2.2.tgz#913ac447e41afc79dcbd95ca37531bbfbdb81cfe" @@ -30,6 +154,11 @@ resolved "https://registry.yarnpkg.com/@algolia/ui-components-shared/-/ui-components-shared-1.2.2.tgz#ec49246e2de7d0461cdeadf2e7742d2f2c7c0bd9" integrity sha512-FYwEG5sbr8p4V8mqP0iUaKgmWfcrMXRXwp7e6iBuB65P/7QyL8pT4I6/iGb85Q5mNH+UtYYSmLZhKjEblllKEQ== +"@algolia/ui-components-shared@1.2.3", "@algolia/ui-components-shared@^1.2.1": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@algolia/ui-components-shared/-/ui-components-shared-1.2.3.tgz#c27d448627583edc71cfc1864dd39d2a689d9ca1" + integrity sha512-Nk4stv4FW9qIpvmdkJMf6oS49xA8Ns/IAAYNngpaFFQTErZupegXvyr8W+6NVvBSzHzeE4H8YO7spg3xWR8e8A== + "@ampproject/remapping@^2.2.0": version "2.2.1" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" @@ -5238,6 +5367,13 @@ ajv@^8.0.0, ajv@^8.3.0, ajv@^8.9.0: require-from-string "^2.0.2" uri-js "^4.2.2" +algoliasearch-helper@3.14.0: + version "3.14.0" + resolved "https://registry.yarnpkg.com/algoliasearch-helper/-/algoliasearch-helper-3.14.0.tgz#2409c2591952719ab6fba1de77b3bbe5094ab85e" + integrity sha512-gXDXzsSS0YANn5dHr71CUXOo84cN4azhHKUbg71vAWnH+1JBiR4jf7to3t3JHXknXkbV0F7f055vUSBKrltHLQ== + dependencies: + "@algolia/events" "^4.0.1" + algoliasearch-helper@3.16.0: version "3.16.0" resolved "https://registry.yarnpkg.com/algoliasearch-helper/-/algoliasearch-helper-3.16.0.tgz#42c7c8cecf5fa91fb9dd467011fa68c9050be7dc" @@ -5252,6 +5388,25 @@ algoliasearch-helper@3.22.5: dependencies: "@algolia/events" "^4.0.1" +algoliasearch@^5.17.0: + version "5.17.0" + resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-5.17.0.tgz#794b25bc386ba688a6f790e414db44940398f5df" + integrity sha512-BpuFprDFc3Pe9a1ZXLzLeqZ+l8Ur37AfzBswkOB4LwikqnRPbIGdluT/nFc/Xk+u/QMxMzUlTN+izqQJVb5vYA== + dependencies: + "@algolia/client-abtesting" "5.17.0" + "@algolia/client-analytics" "5.17.0" + "@algolia/client-common" "5.17.0" + "@algolia/client-insights" "5.17.0" + "@algolia/client-personalization" "5.17.0" + "@algolia/client-query-suggestions" "5.17.0" + "@algolia/client-search" "5.17.0" + "@algolia/ingestion" "1.17.0" + "@algolia/monitoring" "1.17.0" + "@algolia/recommend" "5.17.0" + "@algolia/requester-browser-xhr" "5.17.0" + "@algolia/requester-fetch" "5.17.0" + "@algolia/requester-node-http" "5.17.0" + all-contributors-cli@^6.20.5: version "6.26.1" resolved "https://registry.yarnpkg.com/all-contributors-cli/-/all-contributors-cli-6.26.1.tgz#9f3358c9b9d0a7e66c8f84ffebf5a6432a859cae" @@ -10321,6 +10476,25 @@ instantsearch.css@^7.4.5: resolved "https://registry.yarnpkg.com/instantsearch.css/-/instantsearch.css-7.4.5.tgz#2a521aa634329bf1680f79adf87c79d67669ec8d" integrity sha512-iIGBYjCokU93DDB8kbeztKtlu4qVEyTg1xvS6iSO1YvqRwkIZgf0tmsl/GytsLdZhuw8j4wEaeYsCzNbeJ/zEQ== +instantsearch.js@4.56.8: + version "4.56.8" + resolved "https://registry.yarnpkg.com/instantsearch.js/-/instantsearch.js-4.56.8.tgz#c417c68a792a4b7d6cc866cdb059d828799e84da" + integrity sha512-40DJ5l70ZzVzWPK3qrHTKlJLaHGq1PRZpzfL6281P2mz8G19WOHQHKAP4Zh6a4lOZaRtJQUiPjQwqCHSurXZ5g== + dependencies: + "@algolia/events" "^4.0.1" + "@algolia/ui-components-highlight-vdom" "^1.2.1" + "@algolia/ui-components-shared" "^1.2.1" + "@types/dom-speech-recognition" "^0.0.1" + "@types/google.maps" "^3.45.3" + "@types/hogan.js" "^3.0.0" + "@types/qs" "^6.5.3" + algoliasearch-helper "3.14.0" + hogan.js "^3.0.2" + htm "^3.0.0" + preact "^10.10.0" + qs "^6.5.1 < 6.10" + search-insights "^2.6.0" + instantsearch.js@4.74.2: version "4.74.2" resolved "https://registry.yarnpkg.com/instantsearch.js/-/instantsearch.js-4.74.2.tgz#7ebaaa3d9983691a9346134b889b7ff5ff3ed7f5" @@ -14651,6 +14825,25 @@ react-instantsearch-core@7.13.2: instantsearch.js "4.74.2" use-sync-external-store "^1.0.0" +react-instantsearch-hooks-web@^6.47.3: + version "6.47.3" + resolved "https://registry.yarnpkg.com/react-instantsearch-hooks-web/-/react-instantsearch-hooks-web-6.47.3.tgz#aad0381c72ee85dc0465a59894d1f0c16bf4e3b5" + integrity sha512-JTkPm11xwCX9eO4FgeeJ4v4O98wz1L7cAa2LkspgzDD1MPjMLtmiRVzvGxuYnOayQTtfC5+0GOBwuJEN8TDI8A== + dependencies: + "@babel/runtime" "^7.1.2" + instantsearch.js "4.56.8" + react-instantsearch-hooks "6.47.3" + +react-instantsearch-hooks@6.47.3: + version "6.47.3" + resolved "https://registry.yarnpkg.com/react-instantsearch-hooks/-/react-instantsearch-hooks-6.47.3.tgz#66876c9a5fdf0bb0e777fcf14901b63269a17953" + integrity sha512-QuGSwZ664MHrzvndXGnsyPhpKHywGqyDgqOVorYpEE24Y063OPv5XtmJaZqn27MIvvByUormTb6dbPgbjqkd8w== + dependencies: + "@babel/runtime" "^7.1.2" + algoliasearch-helper "3.14.0" + instantsearch.js "4.56.8" + use-sync-external-store "^1.0.0" + react-instantsearch@^7.12.4: version "7.13.2" resolved "https://registry.yarnpkg.com/react-instantsearch/-/react-instantsearch-7.13.2.tgz#db84d04bd399596fb0078625bc75a6abc65e4bc6" From 163e9ffbbd35ca90a1ca884bbe36a5e49b04c8be Mon Sep 17 00:00:00 2001 From: huanfeng Date: Sun, 15 Dec 2024 05:00:36 -0500 Subject: [PATCH 06/17] update css for llm tag menu --- components/search/HierarchicalMenuWidget.tsx | 76 +++++---- components/search/SearchContainer.tsx | 151 ++++++------------ .../search/bills/useBillRefinements.tsx | 20 +-- components/search/useHierarchicalMenu.tsx | 17 +- 4 files changed, 119 insertions(+), 145 deletions(-) diff --git a/components/search/HierarchicalMenuWidget.tsx b/components/search/HierarchicalMenuWidget.tsx index b7dc5ff4d..45b084ce5 100644 --- a/components/search/HierarchicalMenuWidget.tsx +++ b/components/search/HierarchicalMenuWidget.tsx @@ -71,27 +71,36 @@ export const connectMultiselectHierarchicalMenu: MultiselectHierarchicalMenuConn // Get the last refinement. const lastRefinement = results.getRefinements().pop()?.attributeName + // Cache for static counts + const cachedCounts = new Map() // Merge the results items with the initial ones. const getItems = ( attribute: string ): MultiselectHierarchicalMenuItem[] => { // Safely attempt to retrieve facet values, default to an empty array if unavailable const facetValues = - (results?.getFacetValues(attribute, { - sortBy: ["name:asc"] - }) as SearchResults.FacetValue[]) || [] + (results?.getFacetValues( + attribute, + {} + ) as SearchResults.FacetValue[]) || [] // Mapping over facetValues with an additional safety check - const resultsItems = - facetValues.length > 0 - ? facetValues.map(facetValue => ({ - ...facetValue, - label: facetValue.name - .split(separator || " > ") - .pop() as string, - count: facetValue.count - })) - : [] + const resultsItems = facetValues.map(facetValue => { + const count = facetValue.count + + // Cache static counts on the first render + if (!cachedCounts.has(facetValue.name)) { + cachedCounts.set(facetValue.name, count) + } + + return { + ...facetValue, + label: facetValue.name + .split(separator || " > ") + .pop() as string, + count: cachedCounts.get(facetValue.name) || count // Use static count + } + }) if (lastRefinement && !attributes.includes(lastRefinement)) return resultsItems @@ -112,7 +121,7 @@ export const connectMultiselectHierarchicalMenu: MultiselectHierarchicalMenuConn return resultsItem ? { ...levelItem, ...resultsItem } : levelItem }) - return mergedItems.sort((a, b) => a.name.localeCompare(b.name)) + return mergedItems.sort((a, b) => b.count - a.count) } // Register refinements and items for each attribute. @@ -331,9 +340,14 @@ const MultiselectHierarchicalMenuItem = ({ return (