From 84d73c82199a147e9cdc4acc840e9393bbdbd830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Jos=C3=A9=20Borba=20Fernandes?= Date: Mon, 14 Oct 2024 00:48:53 -0300 Subject: [PATCH] feat(search): implements Orama searchbox --- .../Common/Search/States/WithAllResults.tsx | 40 --- .../Common/Search/States/WithError.tsx | 14 - .../Common/Search/States/WithNoResults.tsx | 16 -- .../Common/Search/States/WithPoweredBy.tsx | 41 --- .../Common/Search/States/WithSearchBox.tsx | 256 ----------------- .../Common/Search/States/WithSearchResult.tsx | 54 ---- .../Common/Search/States/index.module.css | 192 ------------- .../components/Common/Search/index.module.css | 38 --- apps/site/components/Common/Search/index.tsx | 154 +++++++---- apps/site/components/Common/Search/utils.ts | 50 +++- .../components/Containers/NavBar/index.tsx | 6 +- .../MDX/SearchPage/index.module.css | 82 ------ apps/site/components/MDX/SearchPage/index.tsx | 146 ---------- .../useBottomScrollListener.test.mjs | 37 --- .../__tests__/useClickOutside.test.mjs | 45 --- .../__tests__/useKeyboardCommands.test.mjs | 51 ---- apps/site/hooks/react-client/index.ts | 3 - .../react-client/useBottomScrollListener.ts | 40 --- .../hooks/react-client/useClickOutside.ts | 20 -- .../hooks/react-client/useKeyboardCommands.ts | 38 --- apps/site/next.constants.mjs | 12 +- apps/site/next.mdx.use.mjs | 3 - apps/site/next.orama.mjs | 34 --- apps/site/package.json | 4 +- apps/site/pages/en/search.mdx | 6 - apps/site/pages/fa/search.mdx | 6 - apps/site/pages/fr/search.mdx | 6 - apps/site/pages/id/search.mdx | 6 - apps/site/pages/ja/search.mdx | 6 - apps/site/pages/ko/search.mdx | 6 - apps/site/pages/pt/search.mdx | 6 - apps/site/pages/tr/search.mdx | 6 - apps/site/pages/uk/search.mdx | 6 - apps/site/pages/zh-cn/search.mdx | 6 - apps/site/pages/zh-tw/search.mdx | 6 - .../scripts/orama-search/get-documents.mjs | 10 +- apps/site/types/index.ts | 1 - apps/site/types/search.ts | 8 - apps/site/util/searchUtils.ts | 10 - package-lock.json | 257 +++++++++++++++++- 40 files changed, 430 insertions(+), 1298 deletions(-) delete mode 100644 apps/site/components/Common/Search/States/WithAllResults.tsx delete mode 100644 apps/site/components/Common/Search/States/WithError.tsx delete mode 100644 apps/site/components/Common/Search/States/WithNoResults.tsx delete mode 100644 apps/site/components/Common/Search/States/WithPoweredBy.tsx delete mode 100644 apps/site/components/Common/Search/States/WithSearchBox.tsx delete mode 100644 apps/site/components/Common/Search/States/WithSearchResult.tsx delete mode 100644 apps/site/components/Common/Search/States/index.module.css delete mode 100644 apps/site/components/Common/Search/index.module.css delete mode 100644 apps/site/components/MDX/SearchPage/index.module.css delete mode 100644 apps/site/components/MDX/SearchPage/index.tsx delete mode 100644 apps/site/hooks/react-client/__tests__/useBottomScrollListener.test.mjs delete mode 100644 apps/site/hooks/react-client/__tests__/useClickOutside.test.mjs delete mode 100644 apps/site/hooks/react-client/__tests__/useKeyboardCommands.test.mjs delete mode 100644 apps/site/hooks/react-client/useBottomScrollListener.ts delete mode 100644 apps/site/hooks/react-client/useClickOutside.ts delete mode 100644 apps/site/hooks/react-client/useKeyboardCommands.ts delete mode 100644 apps/site/next.orama.mjs delete mode 100644 apps/site/pages/en/search.mdx delete mode 100644 apps/site/pages/fa/search.mdx delete mode 100644 apps/site/pages/fr/search.mdx delete mode 100644 apps/site/pages/id/search.mdx delete mode 100644 apps/site/pages/ja/search.mdx delete mode 100644 apps/site/pages/ko/search.mdx delete mode 100644 apps/site/pages/pt/search.mdx delete mode 100644 apps/site/pages/tr/search.mdx delete mode 100644 apps/site/pages/uk/search.mdx delete mode 100644 apps/site/pages/zh-cn/search.mdx delete mode 100644 apps/site/pages/zh-tw/search.mdx delete mode 100644 apps/site/types/search.ts delete mode 100644 apps/site/util/searchUtils.ts diff --git a/apps/site/components/Common/Search/States/WithAllResults.tsx b/apps/site/components/Common/Search/States/WithAllResults.tsx deleted file mode 100644 index 481edb8cc3966..0000000000000 --- a/apps/site/components/Common/Search/States/WithAllResults.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import type { Results } from '@orama/orama'; -import NextLink from 'next/link'; -import { useParams } from 'next/navigation'; -import { useTranslations } from 'next-intl'; -import type { FC } from 'react'; - -import type { SearchDoc } from '@/types'; - -import styles from './index.module.css'; - -type SearchResults = Results; - -type SeeAllProps = { - searchResults: SearchResults; - searchTerm: string; - selectedFacetName: string; - onSeeAllClick: () => void; -}; - -export const WithAllResults: FC = props => { - const t = useTranslations(); - const params = useParams(); - - const locale = params?.locale ?? 'en'; - const resultsCount = props.searchResults?.count?.toLocaleString('en') ?? 0; - const searchParams = new URLSearchParams(); - - searchParams.set('q', props.searchTerm); - searchParams.set('section', props.selectedFacetName); - - const allResultsURL = `/${locale}/search?${searchParams.toString()}`; - - return ( -
- - {t('components.search.seeAll.text', { count: resultsCount })} - -
- ); -}; diff --git a/apps/site/components/Common/Search/States/WithError.tsx b/apps/site/components/Common/Search/States/WithError.tsx deleted file mode 100644 index 33eecbabd147d..0000000000000 --- a/apps/site/components/Common/Search/States/WithError.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { useTranslations } from 'next-intl'; -import type { FC } from 'react'; - -import styles from './index.module.css'; - -export const WithError: FC = () => { - const t = useTranslations(); - - return ( -
- {t('components.search.searchError.text')} -
- ); -}; diff --git a/apps/site/components/Common/Search/States/WithNoResults.tsx b/apps/site/components/Common/Search/States/WithNoResults.tsx deleted file mode 100644 index 5b55c60469c4b..0000000000000 --- a/apps/site/components/Common/Search/States/WithNoResults.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { useTranslations } from 'next-intl'; -import type { FC } from 'react'; - -import styles from './index.module.css'; - -type NoResultsProps = { searchTerm: string }; - -export const WithNoResults: FC = props => { - const t = useTranslations(); - - return ( -
- {t('components.search.noResults.text', { query: props.searchTerm })} -
- ); -}; diff --git a/apps/site/components/Common/Search/States/WithPoweredBy.tsx b/apps/site/components/Common/Search/States/WithPoweredBy.tsx deleted file mode 100644 index 3986280d5d7a6..0000000000000 --- a/apps/site/components/Common/Search/States/WithPoweredBy.tsx +++ /dev/null @@ -1,41 +0,0 @@ -'use client'; - -import Image from 'next/image'; -import { useTranslations } from 'next-intl'; -import { useTheme } from 'next-themes'; -import { useEffect, useState } from 'react'; - -import styles from './index.module.css'; - -const getLogoURL = (theme: string = 'dark') => - `https://website-assets.oramasearch.com/orama-when-${theme}.svg`; - -export const WithPoweredBy = () => { - const t = useTranslations(); - const { resolvedTheme } = useTheme(); - const [logoURL, setLogoURL] = useState(); - - useEffect(() => setLogoURL(getLogoURL(resolvedTheme)), [resolvedTheme]); - - return ( -
- {t('components.search.poweredBy.text')} - - - {logoURL && ( - Powered by OramaSearch - )} - -
- ); -}; diff --git a/apps/site/components/Common/Search/States/WithSearchBox.tsx b/apps/site/components/Common/Search/States/WithSearchBox.tsx deleted file mode 100644 index d3a2cff45087c..0000000000000 --- a/apps/site/components/Common/Search/States/WithSearchBox.tsx +++ /dev/null @@ -1,256 +0,0 @@ -'use client'; - -import { - MagnifyingGlassIcon, - ChevronLeftIcon, -} from '@heroicons/react/24/outline'; -import type { Results, Nullable } from '@orama/orama'; -import { useState, useRef, useEffect } from 'react'; -import type { FC } from 'react'; - -import styles from '@/components/Common/Search/States/index.module.css'; -import { WithAllResults } from '@/components/Common/Search/States/WithAllResults'; -import { WithError } from '@/components/Common/Search/States/WithError'; -import { WithNoResults } from '@/components/Common/Search/States/WithNoResults'; -import { WithPoweredBy } from '@/components/Common/Search/States/WithPoweredBy'; -import { WithSearchResult } from '@/components/Common/Search/States/WithSearchResult'; -import Tabs from '@/components/Common/Tabs'; -import { useClickOutside, useKeyboardCommands } from '@/hooks/react-client'; -import { useRouter } from '@/navigation.mjs'; -import { DEFAULT_ORAMA_QUERY_PARAMS } from '@/next.constants.mjs'; -import { search as oramaSearch, getInitialFacets } from '@/next.orama.mjs'; -import type { SearchDoc } from '@/types'; -import { searchHitToLinkPath } from '@/util/searchUtils'; - -type Facets = { [key: string]: number }; - -type SearchResults = Nullable>; - -type SearchBoxProps = { onClose: () => void }; - -export const WithSearchBox: FC = ({ onClose }) => { - const [searchTerm, setSearchTerm] = useState(''); - const [searchResults, setSearchResults] = useState(null); - const [selectedResult, setSelectedResult] = useState(); - const [selectedFacet, setSelectedFacet] = useState(0); - const [searchError, setSearchError] = useState>(null); - - const router = useRouter(); - const searchInputRef = useRef(null); - const searchBoxRef = useRef(null); - - const search = (term: string) => { - oramaSearch({ - term, - ...DEFAULT_ORAMA_QUERY_PARAMS, - mode: 'fulltext', - returning: [ - 'path', - 'pageSectionTitle', - 'pageTitle', - 'path', - 'siteSection', - ], - ...filterBySection(), - }) - .then(setSearchResults) - .catch(setSearchError); - }; - - const reset = () => { - setSearchTerm(''); - setSearchResults(null); - setSelectedResult(undefined); - setSelectedFacet(0); - }; - - const handleClose = () => { - reset(); - onClose(); - }; - - useClickOutside(searchBoxRef, handleClose); - - useEffect(() => { - searchInputRef.current?.focus(); - - getInitialFacets().then(setSearchResults).catch(setSearchError); - - return reset; - }, []); - - useEffect( - () => { - search(searchTerm); - }, - // we don't need to care about memoization of search function - // eslint-disable-next-line react-hooks/exhaustive-deps - [searchTerm, selectedFacet] - ); - - useKeyboardCommands(cmd => { - if (searchError || !searchResults || searchResults.count <= 0) { - return; - } - - switch (true) { - case cmd === 'down' && selectedResult === undefined: - setSelectedResult(0); - break; - case cmd === 'down' && - selectedResult != undefined && - selectedResult < searchResults.count && - selectedResult < DEFAULT_ORAMA_QUERY_PARAMS.limit - 1: - setSelectedResult(selectedResult + 1); - break; - case cmd === 'up' && selectedResult != undefined && selectedResult != 0: - setSelectedResult(selectedResult - 1); - break; - case cmd === 'enter': - handleEnter(); - break; - default: - } - }); - - const handleEnter = () => { - if (!searchResults || !selectedResult) { - return; - } - - const selectedHit = searchResults.hits[selectedResult]; - - if (!selectedHit) { - return; - } - - handleClose(); - router.push(searchHitToLinkPath(selectedHit)); - }; - - const onSubmit = (e: React.FormEvent) => { - e.preventDefault(); - - handleClose(); - router.push(`/search?q=${searchTerm}§ion=${selectedFacetName}`); - }; - - const changeFacet = (idx: string) => setSelectedFacet(Number(idx)); - - const filterBySection = () => { - if (selectedFacet === 0) { - return {}; - } - - return { where: { siteSection: { eq: selectedFacetName } } }; - }; - - const facets: Facets = { - all: searchResults?.facets - ? Object.values(searchResults?.facets.siteSection.values).reduce( - (a, b) => a + b, - 0 - ) - : 0, - ...(searchResults?.facets?.siteSection?.values ?? {}), - }; - - const selectedFacetName = Object.keys(facets)[selectedFacet]; - - return ( -
-
-
-
- - - - -
- setSearchTerm(event.target.value)} - value={searchTerm} - /> -
-
- -
- ({ - key: facetName, - label: facetName, - secondaryLabel: `(${facets[facetName].toLocaleString('en')})`, - value: idx.toString(), - }))} - onValueChange={changeFacet} - /> -
- -
- {searchError && } - - {!searchError && ( - <> - {searchResults && - searchResults.count > 0 && - searchResults.hits.map((hit, idx) => ( - - ))} - - {searchResults && searchResults.count === 0 && ( - - )} - - {searchResults && searchResults.count > 8 && ( - - )} - - )} -
- -
- -
-
-
-
- ); -}; diff --git a/apps/site/components/Common/Search/States/WithSearchResult.tsx b/apps/site/components/Common/Search/States/WithSearchResult.tsx deleted file mode 100644 index 05e0dd47ef051..0000000000000 --- a/apps/site/components/Common/Search/States/WithSearchResult.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import type { Result } from '@orama/orama'; -import { useEffect, type FC, useRef } from 'react'; - -import { pathToBreadcrumbs } from '@/components/Common/Search/utils'; -import Link from '@/components/Link'; -import { highlighter } from '@/next.orama.mjs'; -import type { SearchDoc } from '@/types'; -import { searchHitToLinkPath } from '@/util/searchUtils'; - -import styles from './index.module.css'; - -type SearchResultProps = { - hit: Result; - searchTerm: string; - selected: boolean; - idx: number; -}; - -export const WithSearchResult: FC = props => { - const divRef = useRef(null); - const path = searchHitToLinkPath(props.hit); - - useEffect(() => { - if (props.selected && divRef.current) { - divRef.current.scrollIntoView({ block: 'center' }); - } - }, [props.selected]); - - return ( - -
-
- {pathToBreadcrumbs(props.hit.document.path).join(' > ')} - {' > '} - {props.hit.document.pageTitle} -
- - ); -}; diff --git a/apps/site/components/Common/Search/States/index.module.css b/apps/site/components/Common/Search/States/index.module.css deleted file mode 100644 index a4a3f7041c97e..0000000000000 --- a/apps/site/components/Common/Search/States/index.module.css +++ /dev/null @@ -1,192 +0,0 @@ -.searchBoxModalContainer { - @apply fixed - inset-0 - z-50 - flex - items-center - justify-center - bg-neutral-900 - bg-opacity-90 - dark:bg-neutral-900 - dark:bg-opacity-90; -} - -.searchBoxModalPanel { - @apply fixed - h-screen - w-full - bg-neutral-100 - md:h-[450px] - md:max-w-3xl - md:rounded-xl - md:shadow-lg - dark:bg-neutral-950; -} - -.searchBoxInnerPanel { - @apply pt-12 - text-neutral-800 - md:pt-2 - dark:text-neutral-400; -} - -.searchBoxMagnifyingGlassIcon { - @apply absolute - top-[10px] - hidden - size-6 - md:block; -} - -.searchBoxBackIconContainer { - @apply block - md:hidden; -} - -.searchBoxBackIcon { - @apply absolute - top-[7px] - block - size-6 - md:hidden; -} - -.searchBoxInputContainer { - @apply relative - px-2 - md:px-4; -} - -.searchBoxInput { - @apply w-full - rounded-b-none - border-b - border-neutral-300 - bg-transparent - py-2 - pl-8 - pr-4 - focus:outline-none - dark:border-neutral-900 - dark:text-neutral-300 - dark:placeholder-neutral-300; -} - -.fulltextResultsContainer { - @apply h-80 - overflow-auto - md:px-4; -} - -.fulltextSearchResult { - @apply flex - flex-col - rounded-md - p-2 - text-left - text-sm; - - &[aria-selected='true'], - &:hover { - @apply bg-neutral-300 - dark:bg-neutral-900; - } -} - -.fulltextSearchResultTitle { - @apply text-neutral-800 - dark:text-neutral-300; -} - -.fulltextSearchResultBreadcrumb { - @apply mt-1 - text-xs - capitalize - text-neutral-800 - dark:text-neutral-600; -} - -.fulltextSearchSections { - @apply mb-1 - mt-2 - p-2 - md:px-4; -} - -.seeAllFulltextSearchResults { - @apply m-auto - mb-2 - mt-4 - w-full - text-center - text-sm - text-neutral-700 - hover:underline - dark:text-neutral-600; -} - -.poweredBy { - @apply flex - text-xs - text-neutral-950 - dark:text-neutral-200; -} - -.poweredByLogo { - @apply ml-2 - w-16; -} - -.emptyStateContainer { - @apply flex - h-[80%] - w-full - flex-col - items-center - justify-center - text-center - text-sm - text-neutral-600 - dark:text-neutral-500; -} - -.noResultsContainer { - @apply flex - h-[80%] - w-full - items-center - justify-center - text-center - text-sm - text-neutral-600 - dark:text-neutral-500; -} - -.noResultsTerm { - @apply font-semibold; -} - -.searchErrorContainer { - @apply flex - h-[80%] - w-full - items-center - justify-center - text-center - text-sm - text-neutral-600 - dark:text-neutral-500; -} - -.fulltextSearchFooter { - @apply flex - w-full - justify-end - rounded-b-xl - border-t - border-neutral-300 - bg-neutral-100 - p-4 - dark:border-neutral-900 - dark:bg-neutral-950; -} diff --git a/apps/site/components/Common/Search/index.module.css b/apps/site/components/Common/Search/index.module.css deleted file mode 100644 index 734019bf8cad0..0000000000000 --- a/apps/site/components/Common/Search/index.module.css +++ /dev/null @@ -1,38 +0,0 @@ -.searchButton { - @apply flex - grow - basis-80 - items-center - gap-2 - rounded-md - bg-neutral-200 - p-2 - text-sm - text-neutral-800 - hover:bg-neutral-300 - hover:text-neutral-900 - sm:mr-auto - dark:bg-neutral-900 - dark:text-neutral-600 - dark:hover:bg-neutral-800 - dark:hover:text-neutral-500; -} - -.magnifyingGlassIcon { - @apply size-5; -} - -.shortcutIndicator { - @apply font-ibm-plex-mono - invisible - flex - flex-1 - items-center - justify-end - self-center - px-1 - text-xs - motion-safe:transition-opacity - motion-safe:duration-100 - md:visible; -} diff --git a/apps/site/components/Common/Search/index.tsx b/apps/site/components/Common/Search/index.tsx index 652daa66f1d74..4161164230643 100644 --- a/apps/site/components/Common/Search/index.tsx +++ b/apps/site/components/Common/Search/index.tsx @@ -1,61 +1,123 @@ 'use client'; -import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; -import classNames from 'classnames'; -import { useTranslations } from 'next-intl'; -import { useState, type FC } from 'react'; +import { OramaSearchBox, OramaSearchButton } from '@orama/react-components'; +import { useTranslations, useLocale } from 'next-intl'; +import { useTheme } from 'next-themes'; +import { type FC } from 'react'; -import { WithSearchBox } from '@/components/Common/Search/States/WithSearchBox'; -import { useDetectOS } from '@/hooks'; -import { useKeyboardCommands } from '@/hooks/react-client'; +import { + ORAMA_CLOUD_ENDPOINT, + ORAMA_CLOUD_API_KEY, + DEFAULT_ORAMA_QUERY_PARAMS, + DEFAULT_ORAMA_SUGGESTIONS, + BASE_URL, +} from '@/next.constants.mjs'; -import styles from './index.module.css'; +type ResultMapDescription = { + path: string; + pageSectionTitle: string; +}; + +type ResultMapPath = { path: string; siteSection: string }; + +import { themeConfig } from './utils'; + +const uppercaseFirst = (word: string) => + word.charAt(0).toUpperCase() + word.slice(1); -export const SearchButton: FC = () => { - const [isOpen, setIsOpen] = useState(false); +const getFormattedPath = (path: string, title: string) => + `${path + .replace(/#.+$/, '') + .split('/') + .map(element => element.replaceAll('-', ' ')) + .map(element => uppercaseFirst(element)) + .filter(Boolean) + .join(' > ')} — ${title}`; + +const SearchButton: FC = () => { + const { resolvedTheme } = useTheme(); const t = useTranslations(); - const openSearchBox = () => setIsOpen(true); - const closeSearchBox = () => setIsOpen(false); - - useKeyboardCommands(cmd => { - switch (cmd) { - case 'cmd-k': - openSearchBox(); - break; - case 'escape': - closeSearchBox(); - break; - default: - } - }); - - const { os } = useDetectOS(); - - const osCommandKey = os === 'MAC' ? '⌘' : 'Ctrl'; - const isOSLoading = os === 'LOADING'; + const locale = useLocale(); + const colorScheme = resolvedTheme as 'light' | 'dark'; + + const sourceMap = { + title: 'pageSectionTitle', + description: 'formattedPath', + path: 'path', + }; + + const resultMap = { + ...sourceMap, + description: ({ path, pageSectionTitle }: ResultMapDescription) => + getFormattedPath(path, pageSectionTitle), + path: ({ path, siteSection }: ResultMapPath) => + siteSection.toLowerCase() === 'docs' ? `/${path}` : `/${locale}/${path}`, + section: 'siteSection', + }; return ( <> - - - {isOpen ? : null} + + + { + if (!href) return href; + + const baseUrl = BASE_URL.toLowerCase(); + const docsBaseUrl = `${baseUrl}/docs`; + const lowerCaseHref = href.toLowerCase(); + + // Keep original link if URl should not have locale + if ( + !lowerCaseHref.startsWith(baseUrl) || + lowerCaseHref.startsWith(docsBaseUrl) + ) { + return href; + } + + const before = lowerCaseHref.slice(0, baseUrl.length); + const after = lowerCaseHref.slice(baseUrl.length); + return `${before}/${locale}${after}`; + }} + onAnswerSourceClick={event => { + event.preventDefault(); + + // We're aware of this issue with the types. It will be fixed in the next release. + const { path } = event.detail as unknown as { path: string }; + + console.log(path); + + if (!path.startsWith('docs/')) { + window.open(`${BASE_URL}/${locale}/${path}`, '_blank'); + } else { + window.open(`${BASE_URL}/${path}`, '_blank'); + } + }} + /> ); }; + +export default SearchButton; diff --git a/apps/site/components/Common/Search/utils.ts b/apps/site/components/Common/Search/utils.ts index ca204dda9b64f..429f47b3a7d58 100644 --- a/apps/site/components/Common/Search/utils.ts +++ b/apps/site/components/Common/Search/utils.ts @@ -1,7 +1,43 @@ -export const pathToBreadcrumbs = (path: string) => - path - .replace(/#.+$/, '') - .split('/') - .slice(0, -1) - .map(element => element.replaceAll('-', ' ')) - .filter(Boolean); +import tailwindConfig from '@/tailwind.config'; + +const colors = tailwindConfig.theme.colors; +export const themeConfig = { + colors: { + light: { + '--text-color-primary': colors.neutral[900], + '--text-color-accent': colors.green[600], + '--background-color-secondary': colors.neutral[100], + '--background-color-tertiary': colors.neutral[300], + '--border-color-accent': colors.green[600], + '--border-color-primary': colors.neutral[200], + '--border-color-tertiary': colors.green[700], + '--button-background-color-primary': colors.green[600], + '--button-background-color-secondary': colors.white, + '--button-background-color-secondary-hover': colors.neutral[100], + '--button-border-color-secondary': colors.neutral[300], + '--button-text-color-secondary': colors.neutral[900], + '--chat-button-border-color-gradientThree': colors.green[400], + '--chat-button-border-color-gradientFour': colors.green[700], + '--chat-button-background-color-gradientOne': colors.green[600], + '--chat-button-background-color-gradientTwo': colors.green[300], + }, + dark: { + '--text-color-primary': colors.neutral[100], + '--text-color-accent': colors.green[400], + '--background-color-secondary': colors.neutral[950], + '--background-color-tertiary': colors.neutral[900], + '--border-color-accent': colors.green[400], + '--border-color-primary': colors.neutral[900], + '--border-color-tertiary': colors.green[300], + '--button-background-color-primary': colors.green[400], + '--button-background-color-secondary': colors.neutral[950], + '--button-background-color-secondary-hover': colors.neutral[900], + '--button-border-color-secondary': colors.neutral[900], + '--button-text-color-secondary': colors.neutral[200], + '--chat-button-border-color-gradientThree': colors.green[400], + '--chat-button-border-color-gradientFour': colors.green[700], + '--chat-button-background-color-gradientOne': colors.green[400], + '--chat-button-background-color-gradientTwo': colors.green[800], + }, + }, +}; diff --git a/apps/site/components/Containers/NavBar/index.tsx b/apps/site/components/Containers/NavBar/index.tsx index 1ce06a1a5d0f6..5af65deddd09a 100644 --- a/apps/site/components/Containers/NavBar/index.tsx +++ b/apps/site/components/Containers/NavBar/index.tsx @@ -3,11 +3,11 @@ import Hamburger from '@heroicons/react/24/solid/Bars3Icon'; import XMark from '@heroicons/react/24/solid/XMarkIcon'; import * as Label from '@radix-ui/react-label'; +import dynamic from 'next/dynamic'; import { useState } from 'react'; import type { FC, ComponentProps, HTMLAttributeAnchorTarget } from 'react'; import LanguageDropdown from '@/components/Common/LanguageDropDown'; -import { SearchButton } from '@/components/Common/Search'; import ThemeToggle from '@/components/Common/ThemeToggle'; import NavItem from '@/components/Containers/NavBar/NavItem'; import GitHub from '@/components/Icons/Social/GitHub'; @@ -17,6 +17,10 @@ import type { FormattedMessage } from '@/types'; import style from './index.module.css'; +const SearchButton = dynamic(() => import('@/components/Common/Search'), { + ssr: false, +}); + const navInteractionIcons = { show: , close: , diff --git a/apps/site/components/MDX/SearchPage/index.module.css b/apps/site/components/MDX/SearchPage/index.module.css deleted file mode 100644 index 777e5af913050..0000000000000 --- a/apps/site/components/MDX/SearchPage/index.module.css +++ /dev/null @@ -1,82 +0,0 @@ -.searchPageContainer { - @apply mx-auto - w-full - px-4 - py-14 - md:max-w-screen-xl; -} - -.searchTermContainer { - @apply relative - flex - w-full - flex-col - justify-start - gap-1 - px-6 - text-left - md:px-0; -} - -.searchResultsColumns { - @apply relative - mt-12 - grid - gap-4 - md:grid-cols-[15%_1fr]; -} - -.facetsColumn { - @apply sticky - top-0 - flex - gap-4 - overflow-x-auto - px-6 - capitalize - md:flex-col - md:px-0; -} - -.facetCount { - @apply ml-2 - text-sm - text-neutral-500 - dark:text-neutral-800; -} - -.resultsColumn { - @apply flex - flex-col - gap-4 - px-2; -} - -.searchResult { - @apply flex - w-full - flex-col - rounded-lg - px-4 - py-2 - hover:bg-neutral-100 - dark:hover:bg-neutral-900; -} - -.searchResultTitle { - @apply text-lg; -} - -.searchResultPageTitle { - @apply text-sm - capitalize - text-neutral-500 - dark:text-neutral-600; -} - -.searchResultSnippet { - @apply my-2 - text-sm - text-neutral-500 - dark:text-neutral-400; -} diff --git a/apps/site/components/MDX/SearchPage/index.tsx b/apps/site/components/MDX/SearchPage/index.tsx deleted file mode 100644 index 1702eb288dc7c..0000000000000 --- a/apps/site/components/MDX/SearchPage/index.tsx +++ /dev/null @@ -1,146 +0,0 @@ -'use client'; - -import type { Nullable, Results, Result } from '@orama/orama'; -import { useSearchParams } from 'next/navigation'; -import { useTranslations } from 'next-intl'; -import { useEffect, useState, type FC } from 'react'; - -import { WithPoweredBy } from '@/components/Common/Search/States/WithPoweredBy'; -import { WithSearchBox } from '@/components/Common/Search/States/WithSearchBox'; -import { pathToBreadcrumbs } from '@/components/Common/Search/utils'; -import Link from '@/components/Link'; -import { useBottomScrollListener } from '@/hooks/react-client'; -import { BASE_URL, DEFAULT_ORAMA_QUERY_PARAMS } from '@/next.constants.mjs'; -import { search as oramaSearch, highlighter } from '@/next.orama.mjs'; -import type { SearchDoc } from '@/types'; - -import styles from './index.module.css'; - -type SearchResults = Nullable>; -type Hit = Result; - -const SearchPage: FC = () => { - const t = useTranslations(); - const searchParams = useSearchParams(); - const [searchResults, setSearchResults] = useState(null); - const [hits, setHits] = useState>([]); - const [offset, setOffset] = useState(0); - - const searchTerm = searchParams?.get('q'); - const searchSection = searchParams?.get('section'); - const [shownSearchBox, setShownSearchbox] = useState(!searchTerm); - - useBottomScrollListener(() => setOffset(offset => offset + 10)); - - // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => search(offset), [offset]); - - useEffect(() => { - setHits([]); - search(0); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [searchSection, searchTerm]); - - const uniqueHits = (newHits: Array) => - newHits.filter( - (obj, index) => newHits.findIndex(item => item.id === obj.id) === index - ); - - const search = (resultsOffset = 0) => { - oramaSearch({ - ...DEFAULT_ORAMA_QUERY_PARAMS, - mode: 'fulltext', - term: searchTerm || '', - limit: 10, - offset: resultsOffset, - ...filterBySection(), - }) - .then(results => { - setSearchResults(results); - setHits(hits => uniqueHits([...hits, ...(results?.hits ?? [])])); - }) - .catch(); - }; - - const facets = { - all: searchResults?.count ?? 0, - ...(searchResults?.facets?.siteSection?.values ?? {}), - }; - - const filterBySection = () => - searchSection && searchSection !== 'all' - ? { where: { siteSection: { eq: searchSection } } } - : {}; - - const getDocumentURL = (siteSection: string, path: string) => { - const isAPIResult = siteSection.toLowerCase() === 'docs'; - const basePath = isAPIResult ? BASE_URL : ''; - return `${basePath}/${path}`; - }; - - return ( -
- {shownSearchBox ? ( - setShownSearchbox(false)} /> - ) : null} -
-

- {t('components.search.searchPage.title', { query: searchTerm })} -

- - -
- -
-
- {Object.keys(facets).map(facetName => ( - - {facetName} - - ({facets[facetName as keyof typeof facets]}) - - - ))} -
- -
- {hits?.map(hit => ( - -
-

- {hit.document.pageSectionTitle} -

- -

- -

- Home {'>'} {pathToBreadcrumbs(hit.document.path).join(' > ')} -
-
- - ))} -
-
-
- ); -}; - -export default SearchPage; diff --git a/apps/site/hooks/react-client/__tests__/useBottomScrollListener.test.mjs b/apps/site/hooks/react-client/__tests__/useBottomScrollListener.test.mjs deleted file mode 100644 index db3dee7456d6f..0000000000000 --- a/apps/site/hooks/react-client/__tests__/useBottomScrollListener.test.mjs +++ /dev/null @@ -1,37 +0,0 @@ -import { fireEvent, renderHook, act } from '@testing-library/react'; - -import useBottomScrollListener from '@/hooks/react-client/useBottomScrollListener'; - -describe('useBottomScrollListener', () => { - it('should call the callback when the scroll reaches the bottom', () => { - const callback = jest.fn(); - renderHook(() => useBottomScrollListener(callback)); - - act(() => { - fireEvent.scroll(window, { - target: { scrollY: 100, innerHeight: 200, scrollHeight: 200 }, - }); - }); - - // timout is needed because the callback is called in the next tick - setTimeout(() => { - expect(callback).toHaveBeenCalled(); - }, 1); - }); - - it('should not call the callback when the scroll does not reach the bottom', () => { - const callback = jest.fn(); - renderHook(() => useBottomScrollListener(callback)); - - act(() => { - fireEvent.scroll(window, { - target: { scrollY: 100, innerHeight: 200, scrollHeight: 300 }, - }); - }); - - // timout is needed because the callback is called in the next tick - setTimeout(() => { - expect(callback).not.toHaveBeenCalled(); - }, 1); - }); -}); diff --git a/apps/site/hooks/react-client/__tests__/useClickOutside.test.mjs b/apps/site/hooks/react-client/__tests__/useClickOutside.test.mjs deleted file mode 100644 index 9bcb82fd092de..0000000000000 --- a/apps/site/hooks/react-client/__tests__/useClickOutside.test.mjs +++ /dev/null @@ -1,45 +0,0 @@ -import { renderHook, act } from '@testing-library/react'; - -import useClickOutside from '@/hooks/react-client/useClickOutside'; - -describe('useClickOutside', () => { - it('should call the callback function when clicked outside the element', () => { - const fn = jest.fn(); - const { rerender } = renderHook(() => - useClickOutside({ current: null }, fn) - ); - - const mockEvent = new MouseEvent('click', { bubbles: true }); - const mockElement = document.createElement('div'); - - rerender({ current: mockElement }, fn); - - act(() => { - document.dispatchEvent(mockEvent); - }); - - setTimeout(() => { - expect(fn).toHaveBeenCalledTimes(1); - }, 1); - }); - - it('should not call the callback function when clicked inside the element', () => { - const fn = jest.fn(); - const { rerender } = renderHook(() => - useClickOutside({ current: null }, fn) - ); - - const mockEvent = new MouseEvent('click', { bubbles: true }); - const mockElement = document.createElement('div'); - const mockChildElement = document.createElement('button'); - mockElement.appendChild(mockChildElement); - - rerender({ current: mockElement }, fn); - - act(() => { - mockChildElement.dispatchEvent(mockEvent); - }); - - expect(fn).not.toHaveBeenCalled(); - }); -}); diff --git a/apps/site/hooks/react-client/__tests__/useKeyboardCommands.test.mjs b/apps/site/hooks/react-client/__tests__/useKeyboardCommands.test.mjs deleted file mode 100644 index dc52ed2acf454..0000000000000 --- a/apps/site/hooks/react-client/__tests__/useKeyboardCommands.test.mjs +++ /dev/null @@ -1,51 +0,0 @@ -import { renderHook, act, fireEvent } from '@testing-library/react'; - -import useKeyboardCommands from '@/hooks/react-client/useKeyboardCommands'; - -describe('useKeyboardCommands', () => { - it('should call the callback function with the correct command', () => { - const fn = jest.fn(); - renderHook(props => useKeyboardCommands(props), { initialProps: fn }); - - act(() => { - fireEvent.keyDown(document, { key: 'k', metaKey: true }); - }); - expect(fn).toHaveBeenCalledWith('cmd-k'); - fn.mockClear(); - - act(() => { - fireEvent.keyDown(document, { key: 'Escape' }); - }); - expect(fn).toHaveBeenCalledWith('escape'); - fn.mockClear(); - - act(() => { - fireEvent.keyDown(document, { key: 'Enter' }); - }); - expect(fn).toHaveBeenCalledWith('enter'); - fn.mockClear(); - - act(() => { - fireEvent.keyDown(document, { key: 'ArrowDown' }); - }); - expect(fn).toHaveBeenCalledWith('down'); - fn.mockClear(); - - act(() => { - fireEvent.keyDown(document, { key: 'ArrowUp' }); - }); - expect(fn).toHaveBeenCalledWith('up'); - fn.mockClear(); - }); - - it('should not call the callback function for unsupported keys', () => { - const fn = jest.fn(); - renderHook(props => useKeyboardCommands(props), { initialProps: fn }); - - act(() => { - fireEvent.keyDown(document, { key: 'a' }); - }); - - expect(fn).not.toHaveBeenCalled(); - }); -}); diff --git a/apps/site/hooks/react-client/index.ts b/apps/site/hooks/react-client/index.ts index c78f8d3cfbc38..33a9392db1a58 100644 --- a/apps/site/hooks/react-client/index.ts +++ b/apps/site/hooks/react-client/index.ts @@ -3,7 +3,4 @@ export { default as useDetectOS } from './useDetectOS'; export { default as useMediaQuery } from './useMediaQuery'; export { default as useNotification } from './useNotification'; export { default as useClientContext } from './useClientContext'; -export { default as useKeyboardCommands } from './useKeyboardCommands'; -export { default as useClickOutside } from './useClickOutside'; -export { default as useBottomScrollListener } from './useBottomScrollListener'; export { default as useNavigationState } from './useNavigationState'; diff --git a/apps/site/hooks/react-client/useBottomScrollListener.ts b/apps/site/hooks/react-client/useBottomScrollListener.ts deleted file mode 100644 index 7c65471159701..0000000000000 --- a/apps/site/hooks/react-client/useBottomScrollListener.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { useState, useEffect } from 'react'; - -import { debounce } from '@/util/debounce'; - -type CallbackFunction = () => void; - -const useBottomScrollListener = ( - callback: CallbackFunction, - debounceTime = 300 -) => { - const [bottomReached, setBottomReached] = useState(false); - - const debouncedCallback = debounce(callback, debounceTime); - - const handleScroll = () => { - const scrollTop = document.documentElement.scrollTop; - const windowHeight = window.innerHeight; - const height = document.documentElement.scrollHeight; - - const bottomOfWindow = Math.ceil(scrollTop + windowHeight) >= height; - - if (bottomOfWindow) { - setBottomReached(true); - debouncedCallback(); - } else { - setBottomReached(false); - } - }; - - useEffect(() => { - window.addEventListener('scroll', handleScroll, { passive: true }); - - return () => window.removeEventListener('scroll', handleScroll); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return bottomReached; -}; - -export default useBottomScrollListener; diff --git a/apps/site/hooks/react-client/useClickOutside.ts b/apps/site/hooks/react-client/useClickOutside.ts deleted file mode 100644 index 0d64cf8c2e5ce..0000000000000 --- a/apps/site/hooks/react-client/useClickOutside.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { RefObject } from 'react'; -import { useEffect } from 'react'; - -const useClickOutside = ( - ref: RefObject, - fn: () => void -) => { - useEffect(() => { - const element = ref?.current; - const handleClickOutside = (event: Event) => { - if (element && !element.contains(event.target as Node)) { - fn(); - } - }; - document.addEventListener('click', handleClickOutside); - return () => document.removeEventListener('click', handleClickOutside); - }, [ref, fn]); -}; - -export default useClickOutside; diff --git a/apps/site/hooks/react-client/useKeyboardCommands.ts b/apps/site/hooks/react-client/useKeyboardCommands.ts deleted file mode 100644 index 89b4513edbd85..0000000000000 --- a/apps/site/hooks/react-client/useKeyboardCommands.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { useEffect } from 'react'; - -type KeyboardCommand = 'cmd-k' | 'escape' | 'down' | 'up' | 'enter'; - -type KeyboardCommandCallback = (key: KeyboardCommand) => void; - -const useKeyboardCommands = (fn: KeyboardCommandCallback) => { - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - // Detect ⌘ + k on Mac, Ctrl + k on Windows - if ((event.metaKey || event.ctrlKey) && event.key === 'k') { - event.preventDefault(); - fn('cmd-k'); - } - - switch (event.key) { - case 'Escape': - fn('escape'); - break; - case 'Enter': - fn('enter'); - break; - case 'ArrowDown': - fn('down'); - break; - case 'ArrowUp': - fn('up'); - break; - } - }; - - document.addEventListener('keydown', handleKeyDown); - - return () => document.removeEventListener('keydown', handleKeyDown); - }, [fn]); -}; - -export default useKeyboardCommands; diff --git a/apps/site/next.constants.mjs b/apps/site/next.constants.mjs index 9aa7059d1fb4b..bc9ee3a50fc16 100644 --- a/apps/site/next.constants.mjs +++ b/apps/site/next.constants.mjs @@ -131,8 +131,7 @@ export const EXTERNAL_LINKS_SITEMAP = [ * @see https://docs.oramasearch.com/open-source/usage/search/introduction */ export const DEFAULT_ORAMA_QUERY_PARAMS = { - mode: 'fulltext', - limit: 8, + limit: 25, threshold: 0, boost: { pageSectionTitle: 4, @@ -144,6 +143,15 @@ export const DEFAULT_ORAMA_QUERY_PARAMS = { }, }; +/** + * The initial Orama Cloud chat suggestions visible in the empty state of the search box. + */ +export const DEFAULT_ORAMA_SUGGESTIONS = [ + 'How to install Node.js?', + 'How to create an HTTP server?', + 'Upgrading Node.js version', +]; + /** * The default batch size to use when syncing Orama Cloud */ diff --git a/apps/site/next.mdx.use.mjs b/apps/site/next.mdx.use.mjs index 54191ff55b952..67c8adaf9082f 100644 --- a/apps/site/next.mdx.use.mjs +++ b/apps/site/next.mdx.use.mjs @@ -17,7 +17,6 @@ import SourceButton from './components/Downloads/Release/SourceButton'; import VerifyingBinariesLink from './components/Downloads/Release/VerifyingBinariesLink'; import VersionDropdown from './components/Downloads/Release/VersionDropdown'; import UpcomingMeetings from './components/MDX/Calendar/UpcomingMeetings'; -import SearchPage from './components/MDX/SearchPage'; import WithBadge from './components/withBadge'; import WithBanner from './components/withBanner'; import WithNodeRelease from './components/withNodeRelease'; @@ -39,8 +38,6 @@ export const mdxComponents = { DownloadButton: DownloadButton, // Renders a Download Link DownloadLink: DownloadLink, - // Renders a Search Page - SearchPage: SearchPage, // Renders an container for Upcoming Node.js Meetings UpcomingMeetings: UpcomingMeetings, // Group of components that enable you to select versions for Node.js diff --git a/apps/site/next.orama.mjs b/apps/site/next.orama.mjs deleted file mode 100644 index 5a7c150d2b777..0000000000000 --- a/apps/site/next.orama.mjs +++ /dev/null @@ -1,34 +0,0 @@ -import { Highlight } from '@orama/highlight'; -import { OramaClient } from '@oramacloud/client'; - -import { - DEFAULT_ORAMA_QUERY_PARAMS, - ORAMA_CLOUD_ENDPOINT, - ORAMA_CLOUD_API_KEY, -} from './next.constants.mjs'; - -// Provides a safe-wrapper that initialises the OramaClient -// based on the presence of environmental variables -const { search, getInitialFacets } = (() => { - if (ORAMA_CLOUD_ENDPOINT && ORAMA_CLOUD_API_KEY) { - const orama = new OramaClient({ - endpoint: ORAMA_CLOUD_ENDPOINT, - api_key: ORAMA_CLOUD_API_KEY, - }); - - return { - search: orama.search.bind(orama), - getInitialFacets: async () => - orama.search({ term: '', ...DEFAULT_ORAMA_QUERY_PARAMS }).catch(), - }; - } - - return { search: async () => null, getInitialFacets: async () => null }; -})(); - -export { search, getInitialFacets }; - -export const highlighter = new Highlight({ - CSSClass: 'font-bold', - HTMLTag: 'span', -}); diff --git a/apps/site/package.json b/apps/site/package.json index 26f1e56022ad7..0dbc8d6d9a159 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -27,8 +27,8 @@ "@node-core/website-i18n": "*", "@nodevu/core": "0.3.0", "@opentelemetry/api": "1.9.0", - "@orama/highlight": "0.1.6", - "@oramacloud/client": "1.3.16", + "@orama/react-components": "^0.1.19", + "@oramacloud/client": "^1.3.16", "@radix-ui/react-accessible-icon": "^1.1.0", "@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.2", diff --git a/apps/site/pages/en/search.mdx b/apps/site/pages/en/search.mdx deleted file mode 100644 index ac57c0e414803..0000000000000 --- a/apps/site/pages/en/search.mdx +++ /dev/null @@ -1,6 +0,0 @@ ---- -layout: search -title: Search Results ---- - - diff --git a/apps/site/pages/fa/search.mdx b/apps/site/pages/fa/search.mdx deleted file mode 100644 index 789c12514cdfa..0000000000000 --- a/apps/site/pages/fa/search.mdx +++ /dev/null @@ -1,6 +0,0 @@ ---- -layout: search -title: نتایج جستجو ---- - - diff --git a/apps/site/pages/fr/search.mdx b/apps/site/pages/fr/search.mdx deleted file mode 100644 index e1e31021bab81..0000000000000 --- a/apps/site/pages/fr/search.mdx +++ /dev/null @@ -1,6 +0,0 @@ ---- -layout: search -title: Résultats de la recherche ---- - - diff --git a/apps/site/pages/id/search.mdx b/apps/site/pages/id/search.mdx deleted file mode 100644 index 7b4a2ae44d4c4..0000000000000 --- a/apps/site/pages/id/search.mdx +++ /dev/null @@ -1,6 +0,0 @@ ---- -layout: search -title: Hasil Pencarian ---- - - diff --git a/apps/site/pages/ja/search.mdx b/apps/site/pages/ja/search.mdx deleted file mode 100644 index ad7e918069f96..0000000000000 --- a/apps/site/pages/ja/search.mdx +++ /dev/null @@ -1,6 +0,0 @@ ---- -layout: search -title: 検索結果 ---- - - diff --git a/apps/site/pages/ko/search.mdx b/apps/site/pages/ko/search.mdx deleted file mode 100644 index e9eaeb4f3fb9e..0000000000000 --- a/apps/site/pages/ko/search.mdx +++ /dev/null @@ -1,6 +0,0 @@ ---- -layout: search -title: 검색결과 ---- - - diff --git a/apps/site/pages/pt/search.mdx b/apps/site/pages/pt/search.mdx deleted file mode 100644 index 8760cc8d396e3..0000000000000 --- a/apps/site/pages/pt/search.mdx +++ /dev/null @@ -1,6 +0,0 @@ ---- -layout: search -title: Resultados da Pesquisa ---- - - diff --git a/apps/site/pages/tr/search.mdx b/apps/site/pages/tr/search.mdx deleted file mode 100644 index 5db97e62c1b65..0000000000000 --- a/apps/site/pages/tr/search.mdx +++ /dev/null @@ -1,6 +0,0 @@ ---- -layout: search -title: Arama Sonuçları ---- - - diff --git a/apps/site/pages/uk/search.mdx b/apps/site/pages/uk/search.mdx deleted file mode 100644 index df80d434d9e4c..0000000000000 --- a/apps/site/pages/uk/search.mdx +++ /dev/null @@ -1,6 +0,0 @@ ---- -layout: search -title: Результати пошуку ---- - - diff --git a/apps/site/pages/zh-cn/search.mdx b/apps/site/pages/zh-cn/search.mdx deleted file mode 100644 index c33e34ff57dcd..0000000000000 --- a/apps/site/pages/zh-cn/search.mdx +++ /dev/null @@ -1,6 +0,0 @@ ---- -layout: search -title: 搜索结果 ---- - - diff --git a/apps/site/pages/zh-tw/search.mdx b/apps/site/pages/zh-tw/search.mdx deleted file mode 100644 index d484b08ae27f5..0000000000000 --- a/apps/site/pages/zh-tw/search.mdx +++ /dev/null @@ -1,6 +0,0 @@ ---- -layout: search -title: 搜尋結果 ---- - - diff --git a/apps/site/scripts/orama-search/get-documents.mjs b/apps/site/scripts/orama-search/get-documents.mjs index 617ea11301a91..73f7dfd71a010 100644 --- a/apps/site/scripts/orama-search/get-documents.mjs +++ b/apps/site/scripts/orama-search/get-documents.mjs @@ -46,6 +46,9 @@ const splitIntoSections = markdownContent => { })); }; +const uppercaseFirst = string => + string.charAt(0).toUpperCase() + string.slice(1); + const getPageTitle = data => data.title || data.pathname @@ -63,11 +66,12 @@ export const siteContent = [...pageData, ...apiData] const siteSection = pathname.split('/').shift(); const subSections = splitIntoSections(markdownContent); - return subSections.map(section => { + const path = `${pathname}#${slug(section.pageSectionTitle)}`; + return { - path: pathname + '#' + slug(section.pageSectionTitle), - siteSection, + path, + siteSection: uppercaseFirst(siteSection), pageTitle: title, ...section, }; diff --git a/apps/site/types/index.ts b/apps/site/types/index.ts index 28cee2cbc8130..0c7289d93d4ce 100644 --- a/apps/site/types/index.ts +++ b/apps/site/types/index.ts @@ -10,5 +10,4 @@ export * from './redirects'; export * from './server'; export * from './github'; export * from './calendar'; -export * from './search'; export * from './author'; diff --git a/apps/site/types/search.ts b/apps/site/types/search.ts deleted file mode 100644 index 03ac4e67a4a18..0000000000000 --- a/apps/site/types/search.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface SearchDoc { - id: string; - path: string; - pageTitle: string; - siteSection: string; - pageSectionTitle: string; - pageSectionContent: string; -} diff --git a/apps/site/util/searchUtils.ts b/apps/site/util/searchUtils.ts deleted file mode 100644 index 6c5e1cb243b2c..0000000000000 --- a/apps/site/util/searchUtils.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Result } from '@orama/orama'; - -import { BASE_URL } from '@/next.constants.mjs'; -import type { SearchDoc } from '@/types'; - -export const searchHitToLinkPath = (hit: Result) => { - const isAPIResult = hit.document.siteSection.toLowerCase() === 'docs'; - const basePath = isAPIResult ? BASE_URL : ''; - return `${basePath}/${hit.document.path}`; -}; diff --git a/package-lock.json b/package-lock.json index de03f18214804..89ef4cbeedaba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,8 +39,8 @@ "@node-core/website-i18n": "*", "@nodevu/core": "0.3.0", "@opentelemetry/api": "1.9.0", - "@orama/highlight": "0.1.6", - "@oramacloud/client": "1.3.16", + "@orama/react-components": "^0.1.19", + "@oramacloud/client": "^1.3.16", "@radix-ui/react-accessible-icon": "^1.1.0", "@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.2", @@ -2672,6 +2672,21 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.1.tgz", + "integrity": "sha512-wx4aBmgeGvFmOKucFKY+8VFJSYZxs9poN3SDNQFF6lT6NrQUnHiPB2PWz2sc4ieEcAaYYzN+1uWahEeTq2aRIQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@lit/reactive-element": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.4.tgz", + "integrity": "sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.2.0" + } + }, "node_modules/@mdx-js/mdx": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.0.tgz", @@ -3431,6 +3446,15 @@ "node": ">=14" } }, + "node_modules/@orama/cuid2": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@orama/cuid2/-/cuid2-2.2.3.tgz", + "integrity": "sha512-Lcak3chblMejdlSHgYU2lS2cdOhDpU6vkfIJH4m+YKvqQyLqs1bB8+w6NT1MG5bO12NUK2GFc34Mn2xshMIQ1g==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@orama/highlight": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/@orama/highlight/-/highlight-0.1.6.tgz", @@ -3449,6 +3473,71 @@ "node": ">= 16.0.0" } }, + "node_modules/@orama/react-components": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@orama/react-components/-/react-components-0.1.19.tgz", + "integrity": "sha512-zW+y3T5vv4Q4agSBTWG3a7BtpDSD3LDcm4FwFKkTP9zbAEjoL8VmiUQeats8cmAd05QAkmsOwbS9ZuYtGgfKmg==", + "license": "Apache-2.0", + "dependencies": { + "@orama/wc-components": "0.1.18" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.3.1", + "react-dom": "^17.0.0 || ^18.3.1" + } + }, + "node_modules/@orama/wc-components": { + "version": "0.1.18", + "resolved": "https://registry.npmjs.org/@orama/wc-components/-/wc-components-0.1.18.tgz", + "integrity": "sha512-470EnmIZdCgePBLuGmcnDJjLwmI3uNZkfTbiTl2AuZzteTHnMwQokhyFknIH4aXyGmO7dC9QDncv/T7j2kA9Qw==", + "license": "Apache-2.0", + "dependencies": { + "@orama/highlight": "^0.1.6", + "@orama/orama": "^3.0.0", + "@orama/switch": "^3.0.0", + "@oramacloud/client": "^2.1.0", + "@phosphor-icons/webcomponents": "^2.1.5", + "@stencil/core": "^4.19.0", + "@stencil/store": "^2.0.16", + "dompurify": "^3.1.6", + "highlight.js": "^11.10.0", + "markdown-it": "^14.1.0", + "marked": "^13.0.2", + "marked-highlight": "^2.1.3", + "shiki": "^1.10.3", + "sse.js": "^2.5.0" + } + }, + "node_modules/@orama/wc-components/node_modules/@orama/orama": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@orama/orama/-/orama-3.0.2.tgz", + "integrity": "sha512-1dfxup89K2DB2bbfx9rXyr/IAvhCKbH79lZCXVh5HWvdJ9g0VAvPIs3+UzjiyOdycEHYTbYundCTN6+Ygj3z4w==", + "license": "Apache-2.0", + "engines": { + "node": ">= 16.0.0" + } + }, + "node_modules/@orama/wc-components/node_modules/@orama/switch": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@orama/switch/-/switch-3.0.2.tgz", + "integrity": "sha512-ui59rKC67rqF5rg6ncQhHCl11LDw4WZJfhsqJ2UHxxsh3S5F7LsT3a8/5MZDGKkFtj3IVqIRKF+pi0flMJxuTQ==", + "license": "Apache-2.0", + "peerDependencies": { + "@orama/orama": "3.0.2", + "@oramacloud/client": "^2.1.1" + } + }, + "node_modules/@orama/wc-components/node_modules/@oramacloud/client": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@oramacloud/client/-/client-2.1.4.tgz", + "integrity": "sha512-uNPFs4wq/iOPbggCwTkVNbIr64Vfd7ZS/h+cricXVnzXWocjDTfJ3wLL4lr0qiSu41g8z+eCAGBqJ30RO2O4AA==", + "license": "ISC", + "dependencies": { + "@orama/cuid2": "^2.2.3", + "@orama/orama": "^3.0.0", + "lodash": "^4.17.21" + } + }, "node_modules/@oramacloud/client": { "version": "1.3.16", "resolved": "https://registry.npmjs.org/@oramacloud/client/-/client-1.3.16.tgz", @@ -3472,6 +3561,15 @@ "@noble/hashes": "^1.1.5" } }, + "node_modules/@phosphor-icons/webcomponents": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@phosphor-icons/webcomponents/-/webcomponents-2.1.5.tgz", + "integrity": "sha512-JcvQkZxvcX2jK+QCclm8+e8HXqtdFW9xV4/kk2aL9Y3dJA2oQVt+pzbv1orkumz3rfx4K9mn9fDoMr1He1yr7Q==", + "license": "MIT", + "dependencies": { + "lit": "^3" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -4446,6 +4544,32 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@stencil/core": { + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.22.3.tgz", + "integrity": "sha512-dYaletX938WgEA2oMROLdh8wpUn1MgBx2zg5kYqwuUR8aua4Gy9EqGQ3zBu1AvL5MzLlZC+lMlxe/g5Dg1ldpw==", + "license": "MIT", + "bin": { + "stencil": "bin/stencil" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.10.0" + } + }, + "node_modules/@stencil/store": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/@stencil/store/-/store-2.0.16.tgz", + "integrity": "sha512-ET3EByKlmNyTA8O+tcp5YWePOiVnPIiuoiIaxTrf3zFFVo7JWVsVoak9IE0UTn3MkIM0ubR9lgxvi70uN588/A==", + "license": "MIT", + "engines": { + "node": ">=12.0.0", + "npm": ">=6.0.0" + }, + "peerDependencies": { + "@stencil/core": ">=2.0.0 || >=3.0.0 || >= 4.0.0-beta.0 || >= 4.0.0" + } + }, "node_modules/@storybook/addon-controls": { "version": "8.4.5", "resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-8.4.5.tgz", @@ -5707,6 +5831,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -8808,6 +8938,15 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.1.tgz", + "integrity": "sha512-NBHEsc0/kzRYQd+AY6HR6B/IgsqzBABrqJbpCDQII/OK6h7B7LXzweZTDsqSW2LkTRpoxf18YUP+YjGySk6B3w==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", @@ -11678,6 +11817,15 @@ "he": "bin/he" } }, + "node_modules/highlight.js": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.10.0.tgz", + "integrity": "sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/homedir-polyfill": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", @@ -14152,6 +14300,15 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/lint-staged": { "version": "15.2.10", "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.10.tgz", @@ -14418,6 +14575,37 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/lit": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.2.1.tgz", + "integrity": "sha512-1BBa1E/z0O9ye5fZprPtdqnc0BFzxIxTTOO/tQFmyC/hj1O3jL4TfmLBw0WEwjAokdLwpclkvGgDJwTIh0/22w==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^2.0.4", + "lit-element": "^4.1.0", + "lit-html": "^3.2.0" + } + }, + "node_modules/lit-element": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.1.1.tgz", + "integrity": "sha512-HO9Tkkh34QkTeUmEdNYhMT8hzLid7YlMlATSi1q4q17HE5d9mrrEHJ/o8O2D0cMi182zK1F3v7x0PWFjrhXFew==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.2.0", + "@lit/reactive-element": "^2.0.4", + "lit-html": "^3.2.0" + } + }, + "node_modules/lit-html": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.2.1.tgz", + "integrity": "sha512-qI/3lziaPMSKsrwlxH/xMgikhQ0EGOX2ICU73Bi/YHFvz2j/yMCIrw4+puF2IpQ4+upd3EWbvnHM9+PnJn48YA==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, "node_modules/load-plugin": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/load-plugin/-/load-plugin-6.0.3.tgz", @@ -14859,6 +15047,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, "node_modules/markdown-table": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", @@ -14869,6 +15074,27 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/marked": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-13.0.3.tgz", + "integrity": "sha512-rqRix3/TWzE9rIoFGIn8JmsVfhiuC8VIQ8IdX5TfzmeBucdY05/0UlzKaw0eVtpcN/OdVFpBk7CjKGo9iHJ/zA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/marked-highlight": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/marked-highlight/-/marked-highlight-2.2.1.tgz", + "integrity": "sha512-SiCIeEiQbs9TxGwle9/OwbOejHCZsohQRaNTY2u8euEXYt2rYUFoiImUirThU3Gd/o6Q1gHGtH9qloHlbJpNIA==", + "license": "MIT", + "peerDependencies": { + "marked": ">=4 <16" + } + }, "node_modules/mathml-tag-names": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", @@ -15309,6 +15535,12 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, "node_modules/memfs": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", @@ -17963,6 +18195,15 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -20398,6 +20639,12 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "license": "BSD-3-Clause" }, + "node_modules/sse.js": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/sse.js/-/sse.js-2.5.0.tgz", + "integrity": "sha512-I7zYndqOOkNpz9KIdFZ8c8A7zs1YazNewBr8Nsi/tqThfJkVPuP1q7UE2h4B0RwoWZxbBYpd06uoW3NI3SaZXg==", + "license": "Apache-2.0" + }, "node_modules/stable-hash": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz", @@ -22100,6 +22347,12 @@ } } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, "node_modules/uglify-js": { "version": "3.19.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",