Skip to content

Commit

Permalink
wip: feat(search): match geographic entities within search
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelgerber committed Mar 25, 2024
1 parent 3e70ab9 commit ab1d6c2
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 4 deletions.
19 changes: 19 additions & 0 deletions site/search/Search.scss
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,25 @@
}
}

.search-results__chart-hit-entities {
display: flex;
flex-wrap: wrap;
gap: 4px;
list-style: none;
font-size: 0.7em;

li {
background-color: $blue-10;
padding: 4px 8px;
border-radius: 12px;
color: $blue-90;

svg {
margin-right: 4px;
}
}
}

/*
* Tabs / Filtering
**/
Expand Down
57 changes: 53 additions & 4 deletions site/search/SearchPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import ReactDOM from "react-dom"
import React, { useCallback, useEffect, useState } from "react"
import React, { useCallback, useEffect, useMemo, useState } from "react"
import cx from "classnames"
import {
keyBy,
getWindowQueryParams,
get,
mapValues,
EntityName,
Url,
} from "@ourworldindata/utils"
import {
InstantSearch,
Expand Down Expand Up @@ -38,7 +40,11 @@ import {
} from "./searchTypes.js"
import { EXPLORERS_ROUTE_FOLDER } from "../../explorer/ExplorerConstants.js"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome/index.js"
import { faHeartBroken, faSearch } from "@fortawesome/free-solid-svg-icons"
import {
faHeartBroken,
faLocationDot,
faSearch,
} from "@fortawesome/free-solid-svg-icons"
import {
DEFAULT_SEARCH_PLACEHOLDER,
getIndexName,
Expand All @@ -51,7 +57,9 @@ import {
import {
DEFAULT_GRAPHER_HEIGHT,
DEFAULT_GRAPHER_WIDTH,
setSelectedEntityNamesParam,
} from "@ourworldindata/grapher"
import { pickEntitiesForChartHit } from "./SearchUtils.js"

function PagesHit({ hit }: { hit: IPageHit }) {
return (
Expand Down Expand Up @@ -80,13 +88,43 @@ function PagesHit({ hit }: { hit: IPageHit }) {
)
}

const getChartQueryStr = (slug: string, entities: EntityName[]) => {
if (entities.length === 0) return ""
else {
return setSelectedEntityNamesParam(
Url.fromQueryParams({
tab: "chart",
}),
entities
).queryStr
}
}

function ChartHit({ hit }: { hit: IChartHit }) {
const [imgLoaded, setImgLoaded] = useState(false)
const [imgError, setImgError] = useState(false)

const entities = useMemo(
() => pickEntitiesForChartHit(hit),
// eslint-disable-next-line react-hooks/exhaustive-deps
[hit._highlightResult?.availableEntities]
)
const queryStr = useMemo(
() => getChartQueryStr(hit.slug, entities),
[hit.slug, entities]
)
const previewUrl = queryStr
? `/grapher/thumbnail/${hit.slug}${queryStr}`
: `${BAKED_GRAPHER_URL}/exports/${hit.slug}.svg`

useEffect(() => {
setImgLoaded(false)
setImgError(false)
}, [previewUrl])

return (
<a
href={`${BAKED_GRAPHER_URL}/${hit.slug}`}
href={`${BAKED_GRAPHER_URL}/${hit.slug}${queryStr}`}
data-algolia-index={getIndexName(SearchIndexName.Charts)}
data-algolia-object-id={hit.objectID}
data-algolia-position={hit.__position}
Expand All @@ -99,11 +137,12 @@ function ChartHit({ hit }: { hit: IChartHit }) {
</div>
)}
<img
key={previewUrl}
className={cx({ loaded: imgLoaded, error: imgError })}
loading="lazy"
width={DEFAULT_GRAPHER_WIDTH}
height={DEFAULT_GRAPHER_HEIGHT}
src={`${BAKED_GRAPHER_URL}/exports/${hit.slug}.svg`}
src={previewUrl}
onLoad={() => setImgLoaded(true)}
onError={() => setImgError(true)}
/>
Expand All @@ -117,6 +156,16 @@ function ChartHit({ hit }: { hit: IChartHit }) {
<span className="search-results__chart-hit-variant">
{hit.variantName}
</span>
{entities.length > 0 && (
<ul className="search-results__chart-hit-entities">
{entities.map((entity) => (
<li key={entity}>
<FontAwesomeIcon icon={faLocationDot} />
{entity}
</li>
))}
</ul>
)}
</a>
)
}
Expand Down
38 changes: 38 additions & 0 deletions site/search/SearchUtils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { HitAttributeHighlightResult } from "instantsearch.js/es/types/results.js"
import { IChartHit } from "./searchTypes.js"
import { EntityName } from "@ourworldindata/types"

const removeHighlightTags = (text: string) =>
text.replace(/<\/?(mark|strong)>/g, "")

export function pickEntitiesForChartHit(hit: IChartHit): EntityName[] {
const availableEntitiesHighlighted = hit._highlightResult
?.availableEntities as HitAttributeHighlightResult[] | undefined

const pickedEntities = availableEntitiesHighlighted
?.filter((highlightEntry) => {
// Keep the highlight if it is fully highlighted
if (highlightEntry.fullyHighlighted) return true
if (highlightEntry.matchLevel === "none") return false

// Remove any trailing parentheses, e.g. "Africa (UN)" -> "Africa"
const withoutTrailingParens = removeHighlightTags(
highlightEntry.value
).replace(/\s?\(.*\)$/, "")

const matchedWordsLowerCase = highlightEntry.matchedWords.map(
(mw) => mw.toLowerCase()
)

// Keep the highlight if every word (except for trailing parens) is fully highlighted
// This will also highlight "Central African Republic" when searching for "african central republic",
// but that's probably okay
return withoutTrailingParens
.toLowerCase()
.split(" ")
.every((w) => matchedWordsLowerCase.includes(w))
})
.map((highlightEntry) => removeHighlightTags(highlightEntry.value))

return pickedEntities ?? []
}

0 comments on commit ab1d6c2

Please sign in to comment.