Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate Orama for search #6257

Merged
merged 53 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
6377ab1
feat: adds basic orama structure
micheleriva Jan 16, 2024
dfcea9f
Merge branch 'main' into feat/integrate-orama
micheleriva Jan 16, 2024
fdb40e9
feat: adds searchbox
micheleriva Jan 17, 2024
3bfc095
Merge branch 'main' into feat/integrate-orama
micheleriva Jan 17, 2024
f037bdb
Merge branch 'main' into feat/integrate-orama
micheleriva Jan 18, 2024
2dc00c1
feat: integrates searchbox
micheleriva Jan 19, 2024
5e2325c
Merge branch 'main' into feat/integrate-orama
micheleriva Jan 19, 2024
052a635
style: moves components to separate files
micheleriva Jan 19, 2024
5a8b189
feat: wip on searchbox
micheleriva Jan 19, 2024
cfacfca
feat: adds basic mobile styles
micheleriva Jan 19, 2024
9de0148
tmp: work in progress
micheleriva Jan 21, 2024
7c01328
Merge branch 'main' into feat/integrate-orama
micheleriva Jan 21, 2024
8a860b7
work in progress
micheleriva Jan 22, 2024
69bc5ca
feat: improves search page
micheleriva Jan 22, 2024
a24870d
style: addresses feedbacks on code style
micheleriva Jan 22, 2024
7ce813e
style: addresses feedbacks on code style
micheleriva Jan 22, 2024
56ce384
feat: adds texts management via i18n
micheleriva Jan 22, 2024
9c85831
fix: encodes URL components
micheleriva Jan 22, 2024
5e3cef2
style: addresses feedback
micheleriva Jan 22, 2024
a7aab20
style: addresses feedback
micheleriva Jan 22, 2024
ebc3742
docs: adds comments to Orama sync script
micheleriva Jan 22, 2024
bbc98da
style: addresses feedback
micheleriva Jan 22, 2024
5aab3cc
style: addresses feedback
micheleriva Jan 22, 2024
e1fde64
style: addresses feedback
micheleriva Jan 22, 2024
ff2086f
refactor: moves components and hooks into the correct folder structure
micheleriva Jan 22, 2024
2a7052d
refactor: moves components and hooks into the correct folder structure
micheleriva Jan 22, 2024
97f5de4
refactor: moves components and hooks into the correct folder structure
micheleriva Jan 22, 2024
5b773b8
refactor: moves components and hooks into the correct folder structure
micheleriva Jan 22, 2024
765033c
refactor: moves components and hooks into the correct folder structure
micheleriva Jan 22, 2024
f22681d
refactor: moves components and hooks into the correct folder structure
micheleriva Jan 22, 2024
cfd39e8
Merge branch 'main' into feat/integrate-orama
micheleriva Jan 22, 2024
da148e2
style: addresses feedback
micheleriva Jan 22, 2024
f876fa6
style: addresses feedback
micheleriva Jan 22, 2024
3be81f7
style: addresses feedback
micheleriva Jan 22, 2024
b1a012a
style: addresses feedback
micheleriva Jan 22, 2024
eb0915b
style: addresses feedback
micheleriva Jan 22, 2024
531cf0c
ci: adds Orama sync script to gh workflows
micheleriva Jan 23, 2024
f4e25b4
chore: removes useless log
micheleriva Jan 23, 2024
18c764e
Merge branch 'nodejs:main' into feat/integrate-orama
micheleriva Jan 24, 2024
c0c5c9f
style: addresses feedback and adds tests
micheleriva Jan 24, 2024
c86e7aa
feat: adds footer
micheleriva Jan 24, 2024
2aaf9e5
fix: fixes logo in light mode
micheleriva Jan 24, 2024
602856b
updates orama dependencies
micheleriva Jan 24, 2024
3b9d5d9
Merge branch 'main' into feat/integrate-orama
micheleriva Feb 2, 2024
ff65300
chore: updates orama dependencies to latest version
micheleriva Feb 2, 2024
38ccff5
Merge branch 'main' into feat/integrate-orama
micheleriva Feb 21, 2024
05ceb46
chore: updates Orama client
micheleriva Feb 22, 2024
e01a445
fix: fixes unexpected close of modal on click
micheleriva Feb 22, 2024
28a7bce
fix: fixes Orama logo
micheleriva Feb 22, 2024
9f57a16
chore: removes unused test attribute
micheleriva Feb 22, 2024
75230d0
fix: code-reviews
ovflowd Feb 23, 2024
f0e09ba
chore: minor copy changes
ovflowd Feb 23, 2024
0c62be5
fix: aggregate results and make them unique
ovflowd Feb 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
node_modules
npm-debug.log
.npm
.env.local
ovflowd marked this conversation as resolved.
Show resolved Hide resolved

# Next.js Build Output
.next
Expand Down
3 changes: 3 additions & 0 deletions components/Containers/NavBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import NodejsDark from '@/components/Icons/Logos/NodejsDark';
import NodejsLight from '@/components/Icons/Logos/NodejsLight';
import GitHub from '@/components/Icons/Social/GitHub';
import Link from '@/components/Link';
import { SearchButton } from '@/components/SearchBox';
import type { FormattedMessage } from '@/types';

import style from './index.module.css';
Expand Down Expand Up @@ -64,6 +65,8 @@ const NavBar: FC<NavbarProps> = ({
</div>

<div className={style.actionsWrapper}>
<SearchButton />

<ThemeToggle onClick={onThemeTogglerClick} />

<LanguageDropdown
Expand Down
14 changes: 14 additions & 0 deletions components/SearchBox/components/EmptyState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useTranslations } from 'next-intl';
import type { FC } from 'react';

import styles from './index.module.css';

export const EmptyState: FC = () => {
const t = useTranslations();

return (
<div className={styles.emptyStateContainer}>
{t('components.search.emptyState.text')}
</div>
);
};
16 changes: 16 additions & 0 deletions components/SearchBox/components/NoResults.tsx
micheleriva marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useTranslations } from 'next-intl';
import type { FC } from 'react';

import styles from './index.module.css';

type NoResultsProps = { searchTerm: string };

export const NoResults: FC<NoResultsProps> = props => {
const t = useTranslations();

return (
micheleriva marked this conversation as resolved.
Show resolved Hide resolved
<div className={styles.noResultsContainer}>
{t('components.search.noResults.text', { query: props.searchTerm })}
</div>
);
};
27 changes: 27 additions & 0 deletions components/SearchBox/components/PoweredBy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Image from 'next/image';
import { useTranslations } from 'next-intl';

import styles from './index.module.css';

export const PoweredBy = () => {
const t = useTranslations();

return (
micheleriva marked this conversation as resolved.
Show resolved Hide resolved
<div className={styles.poweredBy}>
{t('components.search.poweredBy.text')}
<a
href="https://oramasearch.com?utm_source=nodejs.org"
target="_blank"
rel="noreferer"
>
<Image
src="https://website-assets.oramasearch.com/light-orama-logo.svg"
alt="Powered by OramaSearch"
className={styles.poweredByLogo}
width={80}
height={20}
/>
</a>
</div>
);
};
196 changes: 196 additions & 0 deletions components/SearchBox/components/SearchBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import {
MagnifyingGlassIcon,
ChevronLeftIcon,
} from '@heroicons/react/24/outline';
import type { Results, Nullable } from '@orama/orama';
import classNames from 'classnames';
import { useRouter } from 'next/navigation';
import { useState, useRef, type FC, useEffect } from 'react';

import styles from '@/components/SearchBox/components/index.module.css';
import { PoweredBy } from '@/components/SearchBox/components/PoweredBy';
import { SearchResult } from '@/components/SearchBox/components/SearchResult';
import { SeeAll } from '@/components/SearchBox/components/SeeAll';
import { orama, getInitialFacets } from '@/components/SearchBox/lib/orama';
import { useClickOutside } from '@/components/SearchBox/lib/useClickOutside';

import { EmptyState } from './EmptyState';
import { NoResults } from './NoResults';

export type SearchDoc = {
id: string;
path: string;
pageTitle: string;
siteSection: string;
pageSectionTitle: string;
pageSectionContent: string;
};

type SearchResults = Nullable<Results<SearchDoc>>;

type SearchBoxProps = { onClose: () => void };

export const SearchBox: FC<SearchBoxProps> = ({ onClose }) => {
const [searchTerm, setSearchTerm] = useState('');
const [searchResults, setSearchResults] = useState<SearchResults>(null);
const [selectedFacet, setSelectedFacet] = useState<number>(0);
const [searchError, setSearchError] = useState<Nullable<Error>>(null);

const router = useRouter();
const searchInputRef = useRef<HTMLInputElement>(null);
const searchBoxRef = useRef<HTMLDivElement>(null);

useClickOutside(searchBoxRef, () => {
reset();
onClose();
});

useEffect(() => {
searchInputRef.current?.focus();
getInitialFacets().then(setSearchResults).catch(setSearchError);

return () => reset();
}, []);

useEffect(() => {
search(searchTerm);
}, [searchTerm, selectedFacet]);

const search = (term: string) => {
orama
.search({
term,
limit: 8,
micheleriva marked this conversation as resolved.
Show resolved Hide resolved
threshold: 0,
boost: {
pageSectionTitle: 4,
pageSectionContent: 2.5,
pageTitle: 1.5,
},
facets: {
siteSection: {},
},
...filterBySection(),
})
.then(setSearchResults)
.catch(setSearchError);
};

const reset = () => {
setSearchTerm('');
setSearchResults(null);
setSelectedFacet(0);
};

const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
router.push(`/en/search?q=${searchTerm}&section=${selectedFacetName}`);
onClose();
};

const changeFacet = (idx: number) => {
setSelectedFacet(idx);
};

const filterBySection = () => {
if (selectedFacet === 0) {
return {};
}

return {
where: {
siteSection: {
eq: selectedFacetName,
},
},
};
};

const facets = {
all: searchResults?.count ?? 0,
...(searchResults?.facets?.siteSection?.values ?? {}),
};

const selectedFacetName = Object.keys(facets)[selectedFacet];

return (
<div className={styles.searchBoxModalContainer}>
<div className={styles.searchBoxModalPanel} ref={searchBoxRef}>
<div className={styles.searchBoxInnerPanel}>
<div className={styles.searchBoxInputContainer}>
<button
onClick={onClose}
className={styles.searchBoxBackIconContainer}
>
<ChevronLeftIcon className={styles.searchBoxBackIcon} />
</button>
<MagnifyingGlassIcon
className={styles.searchBoxMagnifyingGlassIcon}
/>
<form onSubmit={onSubmit}>
<input
ref={searchInputRef}
type="search"
className={styles.searchBoxInput}
onChange={event => setSearchTerm(event.target.value)}
value={searchTerm}
/>
</form>
</div>

<div className={styles.fulltextSearchSections}>
{Object.keys(facets).map((facetName, idx) => (
<button
key={facetName}
className={classNames(styles.fulltextSearchSection, {
[styles.fulltextSearchSectionSelected]: selectedFacet === idx,
})}
onClick={() => changeFacet(idx)}
>
{facetName}
<span className={styles.fulltextSearchSectionCount}>
(
{facets[facetName as keyof typeof facets].toLocaleString(
'en'
)}
)
</span>
</button>
))}
</div>

<div className={styles.fulltextResultsContainer}>
{searchError ? <></> : null}

{(searchTerm ? (
searchResults?.count ? (
searchResults?.hits.map(hit => (
<SearchResult
key={hit.id}
hit={hit}
searchTerm={searchTerm}
/>
))
) : (
<NoResults searchTerm={searchTerm} />
)
) : (
<EmptyState />
)) ?? null}

{searchResults?.count
? searchResults?.count > 8 && (
<SeeAll
searchResults={searchResults}
searchTerm={searchTerm}
selectedFacetName={selectedFacetName}
/>
)
: null}
</div>
<PoweredBy />
</div>
</div>
</div>
);
};
14 changes: 14 additions & 0 deletions components/SearchBox/components/SearchError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useTranslations } from 'next-intl';
import type { FC } from 'react';

import styles from './index.module.css';

export const SearchError: FC = () => {
const t = useTranslations();

return (
micheleriva marked this conversation as resolved.
Show resolved Hide resolved
<div className={styles.searchErrorContainer}>
{t('components.search.searchError.text')}
</div>
);
};
44 changes: 44 additions & 0 deletions components/SearchBox/components/SearchResult.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { Result } from '@orama/orama';
import NextLink from 'next/link';
import type { FC } from 'react';

import type { SearchDoc } from '@/components/SearchBox/components/SearchBox';
import { highlighter } from '@/components/SearchBox/lib/orama';
import { pathToBreadcrumbs } from '@/components/SearchBox/lib/utils';

import styles from './index.module.css';

type SearchResultProps = {
hit: Result<SearchDoc>;
searchTerm: string;
};

export const SearchResult: FC<SearchResultProps> = props => {
const isAPIResult = props.hit.document.siteSection.toLowerCase() === 'api';
const basePath = isAPIResult ? 'https://nodejs.org/docs/latest' : '/en';
const path = `${basePath}/${props.hit.document.path}`;

return (
ovflowd marked this conversation as resolved.
Show resolved Hide resolved
<NextLink
key={props.hit.id}
href={path}
className={styles.fulltextSearchResult}
target={isAPIResult ? '_blank' : undefined}
rel={isAPIResult ? 'noopener noreferrer' : undefined}
>
<div
className={styles.fulltextSearchResultTitle}
dangerouslySetInnerHTML={{
micheleriva marked this conversation as resolved.
Show resolved Hide resolved
__html: highlighter
.highlight(props.hit.document.pageSectionTitle, props.searchTerm)
.trim(125),
}}
/>
<div className={styles.fulltextSearchResultBreadcrumb}>
{pathToBreadcrumbs(props.hit.document.path).join(' > ')}
{' > '}
{props.hit.document.pageTitle}
</div>
</NextLink>
);
};
37 changes: 37 additions & 0 deletions components/SearchBox/components/SeeAll.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { Results } from '@orama/orama';
import NextLink from 'next/link';
import { useTranslations } from 'next-intl';
import type { FC } from 'react';

import type { SearchDoc } from '@/components/SearchBox/components/SearchBox';

import styles from './index.module.css';

type SearchResults = Results<SearchDoc>;

type SeeAllProps = {
searchResults: SearchResults;
searchTerm: string;
selectedFacetName: string;
};

export const SeeAll: FC<SeeAllProps> = props => {
micheleriva marked this conversation as resolved.
Show resolved Hide resolved
const t = useTranslations();
const resultsCount = props.searchResults?.count?.toLocaleString('en') ?? 0;

if (!props.searchTerm) {
return null;
}

const sanitizedSearchTerm = encodeURIComponent(props.searchTerm);
const sanitizedFacetName = encodeURIComponent(props.selectedFacetName);
const allResultsURL = `/en/search?q=${sanitizedSearchTerm}&section=${sanitizedFacetName}`;

return (
<div className={styles.seeAllFulltextSearchResults}>
<NextLink href={allResultsURL}>
{t('components.search.seeAll.text', { count: resultsCount })}
</NextLink>
</div>
);
};
Loading