diff --git a/site/DataCatalog.scss b/site/DataCatalog.scss index 4ca66028a5e..46557c68c52 100644 --- a/site/DataCatalog.scss +++ b/site/DataCatalog.scss @@ -35,48 +35,60 @@ } .data-catalog-facets-list { - display: flex; - flex-direction: row; + margin-bottom: 16px; list-style: none; - flex-wrap: wrap; } .data-catalog-facets-list-item { - margin-right: 8px; - border: 1px solid $blue-20; - border-radius: 50px; - margin-bottom: 4px; + @include body-3-medium; + display: inline; + font-weight: bold; + cursor: pointer; + color: $blue-90; &:hover { - border-color: $blue-40; - } - - &.ais-RefinementList-item--selected { - background-color: $blue-20; + text-decoration: underline; } } -.data-catalog-facets-list-item__checkbox { - display: none; +.data-catalog-facets-list-item__hit-count { + color: $blue-60; + margin-left: 4px; } -.data-catalog-facets-list-item__label { - cursor: pointer; - padding: 2px 4px; +.data-catalog-facets-list-separator { display: inline-block; + pointer-events: none; + margin: 0 8px; + color: $blue-30; + &::after { + content: "•"; + } } -.data-catalog-facets-list-item__label-text { - @include body-3-medium; - margin-left: 8px; +.data-catalog-applied-filters-list { + list-style: none; + margin-bottom: 8px; } - -.data-catalog-facets-list-item__count { - margin-left: 8px; - background-color: $blue-10; - font-size: 0.75rem; +.data-catalog-applied-filters-item { + display: inline-block; + margin-right: 8px; + margin-bottom: 8px; +} +.data-catalog-applied-filters-button { + background-color: $blue-20; color: $blue-90; + padding: 5.5px 16px; + border: none; border-radius: 50px; - padding: 4px 8px; + cursor: pointer; + svg { + height: 16px; + margin-bottom: -2px; + margin-left: 4px; + } + &:hover { + color: $blue-100; + } } .data-catalog-ribbon { @@ -91,16 +103,30 @@ margin-bottom: 24px; h2 { margin: 0; + font-weight: bold; } a { line-height: 38.4px; margin-right: 16px; } } -.data-catalog-ribbon-hits { -} -.data-catalog-ribbon-hit { + +.data-catalog-ribbon__hit-count { + color: $blue-65; + &:hover { + text-decoration: underline; + } + svg { + height: 10px; + width: 10px; + background-color: $blue-65; + border-radius: 50px; + padding: 2px; + color: $blue-10; + margin-left: 8px; + } } + .data-catalog-search-list, .data-catalog-ribbon-list { list-style: none; diff --git a/site/DataCatalog.tsx b/site/DataCatalog.tsx index 1b583c5d045..dedec031ea0 100644 --- a/site/DataCatalog.tsx +++ b/site/DataCatalog.tsx @@ -1,22 +1,13 @@ import React, { useEffect, useState } from "react" import ReactDOM from "react-dom" -import { - excludeNullish, - identity, - isArray, - TagGraphNode, - TagGraphRoot, -} from "@ourworldindata/utils" +import { get, isArray, TagGraphNode, TagGraphRoot } from "@ourworldindata/utils" import { Configure, Hits, Index, InstantSearch, - RefinementList, SearchBox, - useConfigure, useInstantSearch, - useRefinementList, } from "react-instantsearch" import algoliasearch from "algoliasearch" import { @@ -26,7 +17,13 @@ import { } from "../settings/clientSettings.js" import { SearchIndexName } from "./search/searchTypes.js" import { getIndexName } from "./search/searchClient.js" -import { UiState } from "instantsearch.js" +import { ScopedResult, UiState } from "instantsearch.js" +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" +import { + faArrowRight, + faChevronRight, + faClose, +} from "@fortawesome/free-solid-svg-icons" const ChartHit = ({ hit }: { hit: any }) => { return ( @@ -47,11 +44,12 @@ const ChartHit = ({ hit }: { hit: any }) => { const DataCatalogRibbon = ({ tagName, - setGlobalFacetFilters, + addGlobalFacetFilter, }: { tagName: string - setGlobalFacetFilters: (facetFilters: string[]) => void + addGlobalFacetFilter: (x: string) => void }) => { + const { scopedResults } = useInstantSearch() return ( @@ -62,10 +60,13 @@ const DataCatalogRibbon = ({ href={`/charts?topics=${tagName}`} onClick={(e) => { e.preventDefault() - setGlobalFacetFilters([`tags:${tagName}`]) + addGlobalFacetFilter(tagName) }} > - See all charts {">"} + + {getNbHitsForTag(tagName, scopedResults)} indicators + + child.name === tag) + if (tagNode) areas.push(...tagNode.children) + } else { + areas.push(...tagGraph.children) + } + return areas +} + const DataCatalogRibbonView = ({ tagGraph, tagToShow, - setGlobalFacetFilters, + addGlobalFacetFilter, }: { tagGraph: TagGraphRoot tagToShow: string | undefined - setGlobalFacetFilters: (facetFilters: string[]) => void + addGlobalFacetFilter: (x: string) => void }) => { - const areas: TagGraphNode[] = [] - if (tagToShow) { - const tagNode = tagGraph.children.find( - (child) => child.name === tagToShow - ) - if (tagNode) areas.push(...tagNode.children) - } else { - areas.push(...tagGraph.children) - } + const areas = getAreaChildrenFromTag(tagGraph, tagToShow) return (
@@ -106,14 +113,14 @@ const DataCatalogRibbonView = ({ ))}
) } -// "Energy and Environment, Air Pollution" => ["tags:Energy and Environment", "tags:Air Pollution"] +// "Energy and Environment, Air Pollution" => ["Energy and Environment", "Air Pollution"] // Currently unclear why this seems to work even though I thought it should be string[][] function transformRouteTopicsToFacetFilters( topics: string | undefined @@ -151,45 +158,44 @@ function checkIfNoFacetsOrOneAreaFacetApplied( return areas.includes(tag) } +function checkShouldShowRibbonView( + query: string | undefined, + facetFilters: string[], + areas: string[] +) { + return !query && checkIfNoFacetsOrOneAreaFacetApplied(facetFilters, areas) +} + const DataCatalogResults = ({ tagGraph, - setGlobalFacetFilters, + addGlobalFacetFilter, }: { tagGraph: TagGraphRoot - setGlobalFacetFilters: (facetFilters: string[]) => void + addGlobalFacetFilter: (tag: string) => void }) => { const { uiState } = useInstantSearch() const genericState = uiState[""] const query = genericState.query const facetFilters = parseFacetFilters(genericState.configure?.facetFilters) const areaNames = tagGraph.children.map((child) => child.name) - const shouldShowRibbons = - !query && checkIfNoFacetsOrOneAreaFacetApplied(facetFilters, areaNames) + + const shouldShowRibbons = checkShouldShowRibbonView( + query, + facetFilters, + areaNames + ) if (shouldShowRibbons) return ( ) return ( - {/* */} - {/* */} { - const configure = useConfigure({}) +function getNbHitsForTag(tag: string, results: ScopedResult[]) { + const result = results.find((r) => + // for some reason I can only find facetFilters in the internal _state object + parseFacetFilters(get(r, "results._state.facetFilters")).includes(tag) + ) + return result ? result.results.nbHits : undefined +} + +const TopicsRefinementList = ({ + tagGraph, + addGlobalFacetFilter, + removeGlobalFacetFilter, +}: { + tagGraph: TagGraphRoot + addGlobalFacetFilter: (tag: string) => void + removeGlobalFacetFilter: (tag: string) => void +}) => { + const { uiState, scopedResults } = useInstantSearch() + const genericState = uiState[""] + const areaNames = tagGraph.children.map((child) => child.name) + const facetFilters = parseFacetFilters(genericState.configure?.facetFilters) + const isShowingRibbons = checkShouldShowRibbonView( + genericState.query, + facetFilters, + areaNames + ) + + const appliedFiltersSection = ( +
    + {facetFilters.map((facetFilter) => ( +
  • + +
  • + ))} +
+ ) + if (isShowingRibbons) { + const areas = getAreaChildrenFromTag(tagGraph, facetFilters[0]) + return ( + + {appliedFiltersSection} +
    + {areas.map((area, i) => { + const isLast = i === areas.length - 1 + return ( + +
  • { + addGlobalFacetFilter(area.name) + }} + > + {area.name} + + ( + {getNbHitsForTag( + area.name, + scopedResults + )} + ) + +
  • + {!isLast ? ( +
  • + {" "} +
  • + ) : null} +
    + ) + })} +
+
+ ) + } return ( -
- - -
+
{appliedFiltersSection}
) } export const DataCatalog = (props: { tagGraph: TagGraphRoot }) => { const searchClient = algoliasearch(ALGOLIA_ID, ALGOLIA_SEARCH_KEY) + // globalFacetFilters apply to all indexes, unless they're overridden by a nested Configure component. + // They're only relevant when we're not showing the ribbon view (because each ribbon has its own Configure.) + // They're stored as ["Energy", "Air Pollution"] which is easier to work with in other components, + // then are formatted into [["tags:Energy"], ["tags:Air Pollution"]] to be used in this component's Configure. const [globalFacetFilters, setGlobalFacetFilters] = useState< string[] | undefined >() + function addGlobalFacetFilter(tag: string) { + setGlobalFacetFilters((prev) => { + if (!prev) return [tag] + if (prev.includes(tag)) return prev + return prev.concat(tag) + }) + } + function removeGlobalFacetFilter(tag: string) { + setGlobalFacetFilters((prev) => { + if (!prev) return [] + return prev.filter((t) => t !== tag) + }) + } + const formattedGlobalFacetFilters = globalFacetFilters?.map((f) => [ + `tags:${f}`, + ]) useEffect(() => { const handlePopState = () => { const urlParams = new URLSearchParams(window.location.search) const topics = urlParams.get("topics") || "" - setGlobalFacetFilters(transformRouteTopicsToFacetFilters(topics)) + setGlobalFacetFilters(topics ? topics.split(",") : undefined) } window.addEventListener("popstate", handlePopState) handlePopState() @@ -287,7 +382,7 @@ export const DataCatalog = (props: { tagGraph: TagGraphRoot }) => { }, }} > - +

Data Catalog

@@ -306,9 +401,14 @@ export const DataCatalog = (props: { tagGraph: TagGraphRoot }) => { className="span-cols-12 col-start-2" />
+ )