From 6377ab1c1c9ccc129a409d7b278b29240df94acf Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Tue, 16 Jan 2024 22:08:27 +0100 Subject: [PATCH 01/44] feat: adds basic orama structure --- package-lock.json | 185 ++++++++++++++++++++++++++++- package.json | 3 + scripts/orama/create.mjs | 14 +++ scripts/orama/get-documents.mjs | 77 ++++++++++++ scripts/orama/sync-orama-cloud.mjs | 0 5 files changed, 275 insertions(+), 4 deletions(-) create mode 100644 scripts/orama/create.mjs create mode 100644 scripts/orama/get-documents.mjs create mode 100644 scripts/orama/sync-orama-cloud.mjs diff --git a/package-lock.json b/package-lock.json index b48a9dff4b69c..1becba30e33f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,9 @@ "@heroicons/react": "~2.1.1", "@mdx-js/mdx": "^3.0.0", "@nodevu/core": "~0.1.0", + "@orama/highlight": "^0.1.2", + "@orama/orama": "^2.0.0", + "@oramacloud/client": "^1.0.2", "@radix-ui/react-accessible-icon": "^1.0.3", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", @@ -731,7 +734,6 @@ "version": "7.23.6", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", - "dev": true, "bin": { "parser": "bin/babel-parser.js" }, @@ -4515,6 +4517,17 @@ "node": ">= 10" } }, + "node_modules/@noble/hashes": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -4630,6 +4643,41 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/@orama/highlight": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@orama/highlight/-/highlight-0.1.2.tgz", + "integrity": "sha512-B48PnxFwRRHBeEIkmKI38tZmpQDWdt6o4bch5dZaChdZh0pwPHtostMv++eVlNv3/qLtfcdLoSYHWvoN9Mp0Lw==", + "dependencies": { + "@orama/orama": "^2.0.0-beta.1" + } + }, + "node_modules/@orama/orama": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@orama/orama/-/orama-2.0.0.tgz", + "integrity": "sha512-Mg3cuIDSMmcQzu7ucLZRhLCzVwZN3+xGmeCNTyuzPUAXlrpF/g3MGUdFOc9ZX++S1Huu/hlxIpk4k8QY7rTr2g==", + "engines": { + "node": ">= 16.0.0" + } + }, + "node_modules/@oramacloud/client": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@oramacloud/client/-/client-1.0.2.tgz", + "integrity": "sha512-iM7HsXwCDtW9bWaPWGtHRQ5lhTKUF01XerFglXfI/lWhl2u9ezMkIOsxKWaCJr0pY/fxcFGmKqhjTKECzR3EpA==", + "dependencies": { + "@orama/orama": "^2.0.0-beta.9", + "@paralleldrive/cuid2": "^2.2.1", + "react": "^18.2.0", + "vue": "^3.3.4" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -8360,6 +8408,117 @@ "integrity": "sha512-Q7O0bnV11KUqNKAKA984YWIGxCjqgOuJdwH1cdItqlkUVTLbIm8BhObro6hJobGMSUHm+z7xjQ0YbC8ps1ekDg==", "hasInstallScript": true }, + "node_modules/@vue/compiler-core": { + "version": "3.4.14", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.14.tgz", + "integrity": "sha512-ro4Zzl/MPdWs7XwxT7omHRxAjMbDFRZEEjD+2m3NBf8YzAe3HuoSEZosXQo+m1GQ1G3LQ1LdmNh1RKTYe+ssEg==", + "dependencies": { + "@babel/parser": "^7.23.6", + "@vue/shared": "3.4.14", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-core/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/@vue/compiler-dom": { + "version": "3.4.14", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.14.tgz", + "integrity": "sha512-nOZTY+veWNa0DKAceNWxorAbWm0INHdQq7cejFaWM1WYnoNSJbSEKYtE7Ir6lR/+mo9fttZpPVI9ZFGJ1juUEQ==", + "dependencies": { + "@vue/compiler-core": "3.4.14", + "@vue/shared": "3.4.14" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.4.14", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.14.tgz", + "integrity": "sha512-1vHc9Kv1jV+YBZC/RJxQJ9JCxildTI+qrhtDh6tPkR1O8S+olBUekimY0km0ZNn8nG1wjtFAe9XHij+YLR8cRQ==", + "dependencies": { + "@babel/parser": "^7.23.6", + "@vue/compiler-core": "3.4.14", + "@vue/compiler-dom": "3.4.14", + "@vue/compiler-ssr": "3.4.14", + "@vue/shared": "3.4.14", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.5", + "postcss": "^8.4.33", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/@vue/compiler-sfc/node_modules/magic-string": { + "version": "0.30.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", + "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.4.14", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.14.tgz", + "integrity": "sha512-bXT6+oAGlFjTYVOTtFJ4l4Jab1wjsC0cfSfOe2B4Z0N2vD2zOBSQ9w694RsCfhjk+bC2DY5Gubb1rHZVii107Q==", + "dependencies": { + "@vue/compiler-dom": "3.4.14", + "@vue/shared": "3.4.14" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.4.14", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.14.tgz", + "integrity": "sha512-xRYwze5Q4tK7tT2J4uy4XLhK/AIXdU5EBUu9PLnIHcOKXO0uyXpNNMzlQKuq7B+zwtq6K2wuUL39pHA6ZQzObw==", + "dependencies": { + "@vue/shared": "3.4.14" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.4.14", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.14.tgz", + "integrity": "sha512-qu+NMkfujCoZL6cfqK5NOfxgXJROSlP2ZPs4CTcVR+mLrwl4TtycF5Tgo0QupkdBL+2kigc6EsJlTcuuZC1NaQ==", + "dependencies": { + "@vue/reactivity": "3.4.14", + "@vue/shared": "3.4.14" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.4.14", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.14.tgz", + "integrity": "sha512-B85XmcR4E7XsirEHVqhmy4HPbRT9WLFWV9Uhie3OapV9m1MEN9+Er6hmUIE6d8/l2sUygpK9RstFM2bmHEUigA==", + "dependencies": { + "@vue/runtime-core": "3.4.14", + "@vue/shared": "3.4.14", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.4.14", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.14.tgz", + "integrity": "sha512-pwSKXQfYdJBTpvWHGEYI+akDE18TXAiLcGn+Q/2Fj8wQSHWztoo7PSvfMNqu6NDhp309QXXbPFEGCU5p85HqkA==", + "dependencies": { + "@vue/compiler-ssr": "3.4.14", + "@vue/shared": "3.4.14" + }, + "peerDependencies": { + "vue": "3.4.14" + } + }, + "node_modules/@vue/shared": { + "version": "3.4.14", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.14.tgz", + "integrity": "sha512-nmi3BtLpvqXAWoRZ6HQ+pFJOHBU4UnH3vD3opgmwXac7vhaHKA9nj1VeGjMggdB9eLtW83eHyPCmOU1qzdsC7Q==" + }, "node_modules/@webassemblyjs/ast": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", @@ -11128,8 +11287,7 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -11849,7 +12007,6 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, "engines": { "node": ">=0.12" }, @@ -34258,6 +34415,26 @@ "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", "dev": true }, + "node_modules/vue": { + "version": "3.4.14", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.14.tgz", + "integrity": "sha512-Rop5Al/ZcBbBz+KjPZaZDgHDX0kUP4duEzDbm+1o91uxYUNmJrZSBuegsNIJvUGy+epLevNRNhLjm08VKTgGyw==", + "dependencies": { + "@vue/compiler-dom": "3.4.14", + "@vue/compiler-sfc": "3.4.14", + "@vue/runtime-dom": "3.4.14", + "@vue/server-renderer": "3.4.14", + "@vue/shared": "3.4.14" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", diff --git a/package.json b/package.json index 40e575c9dc9ce..1a103cd1ab59b 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,9 @@ "@heroicons/react": "~2.1.1", "@mdx-js/mdx": "^3.0.0", "@nodevu/core": "~0.1.0", + "@orama/highlight": "^0.1.2", + "@orama/orama": "^2.0.0", + "@oramacloud/client": "^1.0.2", "@radix-ui/react-accessible-icon": "^1.0.3", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", diff --git a/scripts/orama/create.mjs b/scripts/orama/create.mjs new file mode 100644 index 0000000000000..53814ee0283d9 --- /dev/null +++ b/scripts/orama/create.mjs @@ -0,0 +1,14 @@ +import { create, insertMultiple } from '@orama/orama'; + +import { siteContent } from './get-documents.mjs'; + +export const orama = await create({ + schema: { + siteSection: 'enum', + pageTitle: 'string', + pageSectionTitle: 'string', + pageSectionContent: 'string', + }, +}); + +await insertMultiple(orama, siteContent); diff --git a/scripts/orama/get-documents.mjs b/scripts/orama/get-documents.mjs new file mode 100644 index 0000000000000..b2c637c32df0a --- /dev/null +++ b/scripts/orama/get-documents.mjs @@ -0,0 +1,77 @@ +import crypto from 'node:crypto'; +import zlib from 'node:zlib'; + +import { slug } from 'github-slugger'; + +import { NEXT_DATA_URL } from '../../next.constants.mjs'; + +const nextPageData = await fetch(`${NEXT_DATA_URL}/page-data`); +const pageData = await nextPageData.json(); + +function inflate(data) { + return zlib.inflateSync(Buffer.from(data, 'base64')).toString('utf-8'); +} + +function splitIntoSections(markdownContent) { + const lines = markdownContent.split(/\n/gm); + const sections = []; + + let section = null; + + for (const line of lines) { + if (line.match(/^#{1,6}\s/)) { + section = { + pageSectionTitle: stripMarkdownTags(line.replace(/^#{1,6}\s*/, '')), + pageSectionContent: [], + }; + sections.push(section); + } else if (section) { + section.pageSectionContent.push(line); + } + } + + return sections.map(section => ({ + ...section, + pageSectionContent: stripMarkdownTags( + section.pageSectionContent.join('\n') + ), + })); +} + +function stripMarkdownTags(markdownContent) { + return ( + markdownContent + // Remove links, but keep the text + .replace(/\[([^\]]+)\]\([^)]+\)/gm, '$1') + // Remove self-closing links + .replace(/\[([^\]]+)\]\[\]/g, '$1') + // Remove lists + .replace(/^-\s/gm, '') + // Remove bold elements + .replace(/\*\*(.*?)\*\*/g, '$1') + ); +} + +export const siteContent = pageData + .map(data => { + const { pathname, title, content } = data; + const markdownContent = inflate(content); + const siteSection = pathname.split('/').shift(); + const subSections = splitIntoSections(markdownContent); + + return subSections.map(section => { + const id = crypto + .createHash('sha256') + .update(`${pathname}:${title}:${section.pageSectionTitle}`) + .digest('hex') + .substring(0, 24); + return { + id, + path: pathname + '#' + slug(section.pageSectionTitle), + siteSection, + pageTitle: title, + ...section, + }; + }); + }) + .flat(); diff --git a/scripts/orama/sync-orama-cloud.mjs b/scripts/orama/sync-orama-cloud.mjs new file mode 100644 index 0000000000000..e69de29bb2d1d From fdb40e950a71f0f43da2298c1b9edeea2de39dca Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Wed, 17 Jan 2024 15:13:28 +0100 Subject: [PATCH 02/44] feat: adds searchbox --- .gitignore | 1 + components/Containers/NavBar/index.tsx | 3 + components/SearchBox/index.module.css | 156 ++++++++++++++++++ components/SearchBox/index.tsx | 220 +++++++++++++++++++++++++ components/SearchBox/lib/orama.ts | 14 ++ scripts/orama/create.mjs | 1 + scripts/orama/get-documents.mjs | 26 +-- scripts/orama/sync-orama-cloud.mjs | 70 ++++++++ util/stringUtils.ts | 6 +- 9 files changed, 471 insertions(+), 26 deletions(-) create mode 100644 components/SearchBox/index.module.css create mode 100644 components/SearchBox/index.tsx create mode 100644 components/SearchBox/lib/orama.ts diff --git a/.gitignore b/.gitignore index e137b9768de66..7f47879b973a9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ node_modules npm-debug.log .npm +.env.local # Next.js Build Output .next diff --git a/components/Containers/NavBar/index.tsx b/components/Containers/NavBar/index.tsx index f465b8765804d..8480b623efc36 100644 --- a/components/Containers/NavBar/index.tsx +++ b/components/Containers/NavBar/index.tsx @@ -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'; @@ -64,6 +65,8 @@ const NavBar: FC = ({
+ + >; + +export const SearchBox: FC = () => { + const [searchTerm, setSearchTerm] = useState(''); + const [searchResults, setSearchResults] = useState(null); + const [selectedFacet, setSelectedFacet] = useState(0); + + const searchInputRef = useRef(null); + + useEffect(() => { + searchInputRef.current?.focus(); + + return () => { + setSearchTerm(''); + setSearchResults(null); + setSelectedFacet(0); + }; + }, []); + + useEffect(() => { + search(searchTerm); + }, [searchTerm]); + + function search(term: string) { + const filters = filterBySection(); + + orama + .search({ + term, + limit: 8, + boost: { + pageSectionTitle: 3, + pageSectionContent: 2, + pageTitle: 1, + }, + facets: { + siteSection: {}, + }, + ...filters, + }) + .then(setSearchResults); + } + + function changeFacet(idx: number) { + setSelectedFacet(idx); + search(searchTerm); + } + + function 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 ( +
+
+
+
+ + setSearchTerm(event.target.value)} + value={searchTerm} + /> +
+ +
+ {Object.keys(facets).map((facetName, idx) => ( + + ))} +
+ +
+ {searchResults?.hits.map(hit => ( + + +
+ powered by + + Orama + +
+
+
+
+ ); +}; + +function pathToBreadcrumbs(path: string) { + return path + .replace(/#.+$/, '') + .split('/') + .slice(0, -1) + .map(element => element.replaceAll('-', ' ')) + .filter(Boolean); +} + +export const SearchButton: FC = () => { + const [isOpen, setIsOpen] = useState(false); + + useEffect(() => { + document.addEventListener('keydown', event => { + if ((event.metaKey || event.ctrlKey) && event.key === 'k') { + event.preventDefault(); + openSearchBox(); + } + + if (event.key === 'Escape') { + setIsOpen(false); + } + }); + }, []); + + function openSearchBox() { + setIsOpen(true); + } + + return ( + <> + + {isOpen && } + + ); +}; diff --git a/components/SearchBox/lib/orama.ts b/components/SearchBox/lib/orama.ts new file mode 100644 index 0000000000000..bdb3f7665ff25 --- /dev/null +++ b/components/SearchBox/lib/orama.ts @@ -0,0 +1,14 @@ +import { Highlight } from '@orama/highlight'; +import { OramaClient } from '@oramacloud/client'; + +export const orama = new OramaClient({ + endpoint: process.env.NEXT_PUBLIC_ORAMA_ENDPOINT!, + api_key: process.env.NEXT_PUBLIC_ORAMA_API_KEY!, +}); + +orama.startHeartBeat({ frequency: 3500 }); + +export const highlighter = new Highlight({ + CSSClass: 'font-bold dark:text-neutral-800', + HTMLTag: 'span', +}); diff --git a/scripts/orama/create.mjs b/scripts/orama/create.mjs index 53814ee0283d9..8866f09ce5f35 100644 --- a/scripts/orama/create.mjs +++ b/scripts/orama/create.mjs @@ -4,6 +4,7 @@ import { siteContent } from './get-documents.mjs'; export const orama = await create({ schema: { + id: 'string', siteSection: 'enum', pageTitle: 'string', pageSectionTitle: 'string', diff --git a/scripts/orama/get-documents.mjs b/scripts/orama/get-documents.mjs index b2c637c32df0a..551a794f2f258 100644 --- a/scripts/orama/get-documents.mjs +++ b/scripts/orama/get-documents.mjs @@ -21,7 +21,7 @@ function splitIntoSections(markdownContent) { for (const line of lines) { if (line.match(/^#{1,6}\s/)) { section = { - pageSectionTitle: stripMarkdownTags(line.replace(/^#{1,6}\s*/, '')), + pageSectionTitle: line.replace(/^#{1,6}\s*/, ''), pageSectionContent: [], }; sections.push(section); @@ -32,26 +32,10 @@ function splitIntoSections(markdownContent) { return sections.map(section => ({ ...section, - pageSectionContent: stripMarkdownTags( - section.pageSectionContent.join('\n') - ), + pageSectionContent: section.pageSectionContent.join('\n'), })); } -function stripMarkdownTags(markdownContent) { - return ( - markdownContent - // Remove links, but keep the text - .replace(/\[([^\]]+)\]\([^)]+\)/gm, '$1') - // Remove self-closing links - .replace(/\[([^\]]+)\]\[\]/g, '$1') - // Remove lists - .replace(/^-\s/gm, '') - // Remove bold elements - .replace(/\*\*(.*?)\*\*/g, '$1') - ); -} - export const siteContent = pageData .map(data => { const { pathname, title, content } = data; @@ -60,11 +44,7 @@ export const siteContent = pageData const subSections = splitIntoSections(markdownContent); return subSections.map(section => { - const id = crypto - .createHash('sha256') - .update(`${pathname}:${title}:${section.pageSectionTitle}`) - .digest('hex') - .substring(0, 24); + const id = crypto.randomUUID(); return { id, path: pathname + '#' + slug(section.pageSectionTitle), diff --git a/scripts/orama/sync-orama-cloud.mjs b/scripts/orama/sync-orama-cloud.mjs index e69de29bb2d1d..608b6af08c424 100644 --- a/scripts/orama/sync-orama-cloud.mjs +++ b/scripts/orama/sync-orama-cloud.mjs @@ -0,0 +1,70 @@ +import { siteContent } from './get-documents.mjs'; + +const INDEX_ID = process.env.ORAMA_INDEX_ID; +const API_KEY = process.env.ORAMA_SECRET_KEY; + +async function runUpdate() { + const batchSize = 50; + const batches = []; + + for (let i = 0; i < siteContent.length; i += batchSize) { + batches.push(siteContent.slice(i, i + batchSize)); + } + + console.log( + `Inserting ${batches.length} batches of ${batchSize} documents each.` + ); + await Promise.all(batches.map(insertBatch)); + console.log('Done inserting batches.'); +} + +async function insertBatch(batch) { + await fetch( + `https://api.oramasearch.com/api/v1/webhooks/${INDEX_ID}/notify`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${API_KEY}`, + }, + body: JSON.stringify({ + upsert: batch, + }), + } + ); +} + +async function triggerDeployment() { + console.log('Triggering deployment'); + await fetch( + `https://api.oramasearch.com/api/v1/webhooks/${INDEX_ID}/deploy`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${API_KEY}`, + }, + } + ); + console.log('Done triggering deployment'); +} + +async function emptyOramaIndex() { + console.log('Emptying index'); + await fetch( + `https://api.oramasearch.com/api/v1/webhooks/${INDEX_ID}/snapshot`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${API_KEY}`, + }, + body: JSON.stringify([]), + } + ); + console.log('Done emptying index'); +} + +await emptyOramaIndex(); +await runUpdate(); +await triggerDeployment(); diff --git a/util/stringUtils.ts b/util/stringUtils.ts index c31cc08a8e787..6950bbae8773c 100644 --- a/util/stringUtils.ts +++ b/util/stringUtils.ts @@ -10,8 +10,6 @@ export const parseRichTextIntoPlainText = (richText: string) => .replace(/\[([^\]]+)\]\([^)]+\)/gm, '$1') // replaces Markdown lists with their content .replace(/^[*-] (.*)$/gm, '$1') - // replaces Markdown headings with their content - .replace(/^#+ (.*)$/gm, '$1') // replaces Markdown underscore, bold and italic with their content .replace(/[_*]{1,2}(.*)[_*]{1,2}/gm, '$1') // replaces Markdown multiline codeblocks with their content @@ -19,4 +17,6 @@ export const parseRichTextIntoPlainText = (richText: string) => // replaces emppty lines or lines just with spaces with an empty string .replace(/^\s*\n/gm, '') // replaces leading and trailing spaces from each line with an empty string - .replace(/^[ ]+|[ ]+$/gm, ''); + .replace(/^[ ]+|[ ]+$/gm, '') + // replaces leading numbers and dots from each line with an empty string + .replace(/^\d+\.\s/gm, ''); From 2dc00c15c0a6b4a40b435353cf5d7b095a4e0557 Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Fri, 19 Jan 2024 09:59:55 +0100 Subject: [PATCH 03/44] feat: integrates searchbox --- components/SearchBox/index.module.css | 30 +++++--- components/SearchBox/index.tsx | 82 ++++++++++++++------- components/SearchBox/lib/useClickOutside.ts | 20 +++++ 3 files changed, 98 insertions(+), 34 deletions(-) create mode 100644 components/SearchBox/lib/useClickOutside.ts diff --git a/components/SearchBox/index.module.css b/components/SearchBox/index.module.css index 7e2c47fc52382..666772f524be3 100644 --- a/components/SearchBox/index.module.css +++ b/components/SearchBox/index.module.css @@ -54,7 +54,9 @@ } .searchBoxInnerPanel { - @apply rounded-lg bg-neutral-200 p-2 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-400; + @apply p-2 + text-neutral-800 + dark:text-neutral-400; } .searchBoxMagnifyingGlassIcon { @@ -68,13 +70,13 @@ .searchBoxInput { @apply w-full border-b - border-neutral-200 + border-neutral-300 bg-transparent py-2 pl-8 pr-4 focus:outline-none - dark:border-neutral-700 + dark:border-neutral-900 dark:text-neutral-300 dark:placeholder-neutral-300; } @@ -85,13 +87,13 @@ .fulltextSearchResult { @apply flex - flex-col + flex-col rounded-md p-2 text-left text-sm hover:bg-neutral-300 - dark:hover:bg-neutral-800; + dark:hover:bg-neutral-900; } .fulltextSearchResultTitle { @@ -105,7 +107,8 @@ } .fulltextSearchSections { - @apply mt-2 + @apply mb-1 + mt-2 flex gap-2 p-2 @@ -116,15 +119,24 @@ } .fulltextSearchSection { - @apply border-b border-neutral-200 p-2 capitalize dark:border-neutral-900; + @apply rounded-lg + border-b + border-transparent + px-2 + py-1 + capitalize + hover:bg-neutral-200 + dark:border-neutral-900 + dark:border-b-transparent + dark:hover:bg-neutral-900; } .fulltextSearchSectionSelected { - @apply border-neutral-700 text-neutral-900 dark:border-neutral-700 dark:text-neutral-300; + @apply rounded-b-none border-neutral-700 text-neutral-900 dark:border-neutral-700 dark:text-neutral-300; } .fulltextSearchSectionCount { - @apply ml-1 dark:text-neutral-800; + @apply ml-1 text-neutral-500 dark:text-neutral-800; } .seeAllFulltextSearchResults { diff --git a/components/SearchBox/index.tsx b/components/SearchBox/index.tsx index 5c0ad7566a218..be4906808126f 100644 --- a/components/SearchBox/index.tsx +++ b/components/SearchBox/index.tsx @@ -1,10 +1,12 @@ import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; import type { Results, Nullable } from '@orama/orama'; import clx from 'classnames'; +import { useRouter } from 'next/navigation'; import { useState, useRef, type FC, useEffect } from 'react'; import styles from './index.module.css'; import { orama, highlighter } from './lib/orama'; +import { useClickOutside } from './lib/useClickOutside'; type SearchDoc = { id: string; @@ -17,20 +19,29 @@ type SearchDoc = { type SearchResults = Nullable>; -export const SearchBox: FC = () => { +type SearchBoxProps = { + onClose: () => void; +}; + +export const SearchBox: FC = props => { const [searchTerm, setSearchTerm] = useState(''); const [searchResults, setSearchResults] = useState(null); const [selectedFacet, setSelectedFacet] = useState(0); + const router = useRouter(); const searchInputRef = useRef(null); + const searchBoxRef = useRef(null); + + useClickOutside(searchBoxRef, () => { + reset(); + props.onClose(); + }); useEffect(() => { searchInputRef.current?.focus(); - + search(''); return () => { - setSearchTerm(''); - setSearchResults(null); - setSelectedFacet(0); + reset(); }; }, []); @@ -45,9 +56,10 @@ export const SearchBox: FC = () => { .search({ term, limit: 8, + threshold: 0, boost: { - pageSectionTitle: 3, - pageSectionContent: 2, + pageSectionTitle: 4, + pageSectionContent: 2.5, pageTitle: 1, }, facets: { @@ -58,6 +70,17 @@ export const SearchBox: FC = () => { .then(setSearchResults); } + function reset() { + setSearchTerm(''); + setSearchResults(null); + setSelectedFacet(0); + } + + function onSubmit(e: React.FormEvent) { + e.preventDefault(); + router.push(`/en/search?q=${searchTerm}§ion=${selectedFacetName}`); + } + function changeFacet(idx: number) { setSelectedFacet(idx); search(searchTerm); @@ -86,19 +109,21 @@ export const SearchBox: FC = () => { return (
-
+
- setSearchTerm(event.target.value)} - value={searchTerm} - /> +
+ setSearchTerm(event.target.value)} + value={searchTerm} + /> +
@@ -145,15 +170,18 @@ export const SearchBox: FC = () => { ))} - {searchResults?.count && searchResults?.count > 8 && ( - - )} + {searchResults?.count + ? searchResults?.count > 8 && ( + + ) + : null}
powered by @@ -204,6 +232,10 @@ export const SearchButton: FC = () => { setIsOpen(true); } + function closeSearchBox() { + setIsOpen(false); + } + return ( <> - {isOpen && } + {isOpen && } ); }; diff --git a/components/SearchBox/lib/useClickOutside.ts b/components/SearchBox/lib/useClickOutside.ts new file mode 100644 index 0000000000000..23278e6331c8d --- /dev/null +++ b/components/SearchBox/lib/useClickOutside.ts @@ -0,0 +1,20 @@ +import type { RefObject } from 'react'; +import { useEffect } from 'react'; + +export const useClickOutside = ( + ref: RefObject, + fn: () => void +) => { + useEffect(() => { + const element = ref?.current; + function handleClickOutside(event: Event) { + if (element && !element.contains(event.target as Node | null)) { + fn(); + } + } + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [ref, fn]); +}; From 052a63544084023dd6acc81cd7001bf6240a8737 Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Fri, 19 Jan 2024 11:18:54 +0100 Subject: [PATCH 04/44] style: moves components to separate files --- app/[locale]/search.tsx | 7 + .../SearchBox/components/EmptyState.tsx | 7 + components/SearchBox/components/NoResults.tsx | 16 ++ components/SearchBox/components/PoweredBy.tsx | 20 ++ components/SearchBox/components/SearchBox.tsx | 188 +++++++++++++++ .../SearchBox/components/SearchError.tsx | 11 + .../SearchBox/components/SearchResult.tsx | 37 +++ components/SearchBox/components/SeeAll.tsx | 26 +++ .../SearchBox/components/index.module.css | 207 +++++++++++++++++ components/SearchBox/index.module.css | 139 ----------- components/SearchBox/index.tsx | 218 +----------------- components/SearchBox/lib/utils.ts | 8 + 12 files changed, 535 insertions(+), 349 deletions(-) create mode 100644 app/[locale]/search.tsx create mode 100644 components/SearchBox/components/EmptyState.tsx create mode 100644 components/SearchBox/components/NoResults.tsx create mode 100644 components/SearchBox/components/PoweredBy.tsx create mode 100644 components/SearchBox/components/SearchBox.tsx create mode 100644 components/SearchBox/components/SearchError.tsx create mode 100644 components/SearchBox/components/SearchResult.tsx create mode 100644 components/SearchBox/components/SeeAll.tsx create mode 100644 components/SearchBox/components/index.module.css create mode 100644 components/SearchBox/lib/utils.ts diff --git a/app/[locale]/search.tsx b/app/[locale]/search.tsx new file mode 100644 index 0000000000000..8e6eab5fe0563 --- /dev/null +++ b/app/[locale]/search.tsx @@ -0,0 +1,7 @@ +import type { FC } from 'react'; + +const SearchPage: FC = () => { + return
Search page
; +}; + +export default SearchPage; diff --git a/components/SearchBox/components/EmptyState.tsx b/components/SearchBox/components/EmptyState.tsx new file mode 100644 index 0000000000000..9c13de7a76e8e --- /dev/null +++ b/components/SearchBox/components/EmptyState.tsx @@ -0,0 +1,7 @@ +import type { FC } from 'react'; + +import styles from './index.module.css'; + +export const EmptyState: FC = () => { + return
Search something...
; +}; diff --git a/components/SearchBox/components/NoResults.tsx b/components/SearchBox/components/NoResults.tsx new file mode 100644 index 0000000000000..cab112c3ec73e --- /dev/null +++ b/components/SearchBox/components/NoResults.tsx @@ -0,0 +1,16 @@ +import type { FC } from 'react'; + +import styles from './index.module.css'; + +type NoResultsProps = { + searchTerm: string; +}; + +export const NoResults: FC = props => { + return ( +
+ No results for  + {`"${props.searchTerm}"`}. +
+ ); +}; diff --git a/components/SearchBox/components/PoweredBy.tsx b/components/SearchBox/components/PoweredBy.tsx new file mode 100644 index 0000000000000..994ef5cce4363 --- /dev/null +++ b/components/SearchBox/components/PoweredBy.tsx @@ -0,0 +1,20 @@ +import styles from './index.module.css'; + +export const PoweredBy = () => { + return ( +
+ powered by + + Orama + +
+ ); +}; diff --git a/components/SearchBox/components/SearchBox.tsx b/components/SearchBox/components/SearchBox.tsx new file mode 100644 index 0000000000000..a2e16fc692c58 --- /dev/null +++ b/components/SearchBox/components/SearchBox.tsx @@ -0,0 +1,188 @@ +import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; +import type { Results, Nullable } from '@orama/orama'; +import clx 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 } 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>; + +type SearchBoxProps = { + onClose: () => void; +}; + +export const SearchBox: FC = props => { + const [searchTerm, setSearchTerm] = useState(''); + const [searchResults, setSearchResults] = useState(null); + const [selectedFacet, setSelectedFacet] = useState(0); + const [searchError, setSearchError] = useState>(null); + + const router = useRouter(); + const searchInputRef = useRef(null); + const searchBoxRef = useRef(null); + + useClickOutside(searchBoxRef, () => { + reset(); + props.onClose(); + }); + + useEffect(() => { + searchInputRef.current?.focus(); + return () => { + reset(); + }; + }, []); + + useEffect(() => { + search(searchTerm); + }, [searchTerm, selectedFacet]); + + function search(term: string) { + orama + .search({ + term, + limit: 8, + threshold: 0, + boost: { + pageSectionTitle: 4, + pageSectionContent: 2.5, + pageTitle: 1, + }, + facets: { + siteSection: {}, + }, + ...filterBySection(), + }) + .then(setSearchResults) + .catch(setSearchError); + } + + function reset() { + setSearchTerm(''); + setSearchResults(null); + setSelectedFacet(0); + } + + function onSubmit(e: React.FormEvent) { + e.preventDefault(); + router.push(`/en/search?q=${searchTerm}§ion=${selectedFacetName}`); + } + + function changeFacet(idx: number) { + setSelectedFacet(idx); + } + + function 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 ( +
+
+
+
+ +
+ setSearchTerm(event.target.value)} + value={searchTerm} + /> +
+
+ +
+ {Object.keys(facets).map((facetName, idx) => ( + + ))} +
+ +
+ {searchError ? <> : null} + + {(searchTerm ? ( + searchResults?.count ? ( + searchResults?.hits.map(hit => ( + + )) + ) : ( + + ) + ) : ( + + )) ?? null} + + {searchResults?.count + ? searchResults?.count > 8 && ( + + ) + : null} +
+ +
+
+
+ ); +}; diff --git a/components/SearchBox/components/SearchError.tsx b/components/SearchBox/components/SearchError.tsx new file mode 100644 index 0000000000000..e11e8adf15d0c --- /dev/null +++ b/components/SearchBox/components/SearchError.tsx @@ -0,0 +1,11 @@ +import type { FC } from 'react'; + +import styles from './index.module.css'; + +export const SearchError: FC = () => { + return ( +
+ An error occurred while searching. Please try again later. +
+ ); +}; diff --git a/components/SearchBox/components/SearchResult.tsx b/components/SearchBox/components/SearchResult.tsx new file mode 100644 index 0000000000000..d043aa17a51ae --- /dev/null +++ b/components/SearchBox/components/SearchResult.tsx @@ -0,0 +1,37 @@ +import type { Result } from '@orama/orama'; +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; + searchTerm: string; +}; + +export const SearchResult: FC = props => { + return ( + +
+
+ {pathToBreadcrumbs(props.hit.document.path).join(' > ')} + {' > '} + {props.hit.document.pageTitle} +
+
+ ); +}; diff --git a/components/SearchBox/components/SeeAll.tsx b/components/SearchBox/components/SeeAll.tsx new file mode 100644 index 0000000000000..0bdfda5ca8100 --- /dev/null +++ b/components/SearchBox/components/SeeAll.tsx @@ -0,0 +1,26 @@ +import type { Results } from '@orama/orama'; +import type { FC } from 'react'; + +import type { SearchDoc } from '@/components/SearchBox/components/SearchBox'; + +import styles from './index.module.css'; + +type SearchResults = Results; + +type SeeAllProps = { + searchResults: SearchResults; + searchTerm: string; + selectedFacetName: string; +}; + +export const SeeAll: FC = props => { + return ( + + ); +}; diff --git a/components/SearchBox/components/index.module.css b/components/SearchBox/components/index.module.css new file mode 100644 index 0000000000000..75dee47c67487 --- /dev/null +++ b/components/SearchBox/components/index.module.css @@ -0,0 +1,207 @@ +.searchButton { + @apply relative + w-52 + rounded-md + bg-neutral-100 + py-2 + pl-9 + pr-4 + text-left + text-sm + text-neutral-700 + transition-colors + duration-200 + ease-in-out + hover:bg-neutral-200 + hover:text-neutral-800 + dark:bg-neutral-900 + dark:text-neutral-600 + dark:hover:bg-neutral-800 + dark:hover:text-neutral-500; +} + +.magnifyingGlassIcon { + @apply absolute + left-2 + top-[8px] + h-5 + w-5; +} + +.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 + top-60 + w-full + max-w-3xl + rounded-xl + bg-neutral-100 + p-2 + shadow-lg + dark:bg-neutral-950; +} + +.searchBoxInnerPanel { + @apply p-2 + text-neutral-800 + dark:text-neutral-400; +} + +.searchBoxMagnifyingGlassIcon { + @apply absolute top-[10px] h-6 w-6; +} + +.searchBoxInputContainer { + @apply relative px-2; +} + +.searchBoxInput { + @apply w-full + 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-scroll; +} + +.fulltextSearchResult { + @apply flex + flex-col + rounded-md + p-2 + text-left + text-sm + hover:bg-neutral-300 + dark:hover:bg-neutral-900; +} + +.fulltextSearchResultTitle { + @apply text-neutral-700 + dark:text-neutral-300; +} + +.fulltextSearchResultBreadcrumb { + @apply mt-1 text-xs capitalize text-neutral-800 + dark:text-neutral-600; +} + +.fulltextSearchSections { + @apply mb-1 + mt-2 + flex + gap-2 + p-2 + text-xs + font-semibold + text-neutral-700 + dark:text-neutral-600; +} + +.fulltextSearchSection { + @apply rounded-lg + border-b + border-transparent + px-2 + py-1 + capitalize + hover:bg-neutral-200 + dark:border-neutral-900 + dark:border-b-transparent + dark:hover:bg-neutral-900; +} + +.fulltextSearchSectionSelected { + @apply rounded-b-none border-neutral-700 text-neutral-900 dark:border-neutral-700 dark:text-neutral-300; +} + +.fulltextSearchSectionCount { + @apply ml-1 text-neutral-500 dark:text-neutral-800; +} + +.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 absolute + -bottom-8 + left-0 + flex + w-full + items-center + justify-center + text-xs + 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; +} diff --git a/components/SearchBox/index.module.css b/components/SearchBox/index.module.css index 666772f524be3..17063319277c3 100644 --- a/components/SearchBox/index.module.css +++ b/components/SearchBox/index.module.css @@ -27,142 +27,3 @@ h-5 w-5; } - -.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 - top-60 - w-full - max-w-3xl - rounded-xl - bg-neutral-100 - p-2 - shadow-lg - dark:bg-neutral-950; -} - -.searchBoxInnerPanel { - @apply p-2 - text-neutral-800 - dark:text-neutral-400; -} - -.searchBoxMagnifyingGlassIcon { - @apply absolute top-[10px] h-6 w-6; -} - -.searchBoxInputContainer { - @apply relative px-2; -} - -.searchBoxInput { - @apply w-full - 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-scroll; -} - -.fulltextSearchResult { - @apply flex - flex-col - rounded-md - p-2 - text-left - text-sm - hover:bg-neutral-300 - dark:hover:bg-neutral-900; -} - -.fulltextSearchResultTitle { - @apply text-neutral-700 - dark:text-neutral-300; -} - -.fulltextSearchResultBreadcrumb { - @apply mt-1 text-xs capitalize text-neutral-800 - dark:text-neutral-600; -} - -.fulltextSearchSections { - @apply mb-1 - mt-2 - flex - gap-2 - p-2 - text-xs - font-semibold - text-neutral-700 - dark:text-neutral-600; -} - -.fulltextSearchSection { - @apply rounded-lg - border-b - border-transparent - px-2 - py-1 - capitalize - hover:bg-neutral-200 - dark:border-neutral-900 - dark:border-b-transparent - dark:hover:bg-neutral-900; -} - -.fulltextSearchSectionSelected { - @apply rounded-b-none border-neutral-700 text-neutral-900 dark:border-neutral-700 dark:text-neutral-300; -} - -.fulltextSearchSectionCount { - @apply ml-1 text-neutral-500 dark:text-neutral-800; -} - -.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 absolute - -bottom-8 - left-0 - flex - w-full - items-center - justify-center - text-xs - text-neutral-200; -} - -.poweredByLogo { - @apply ml-2 w-16; -} diff --git a/components/SearchBox/index.tsx b/components/SearchBox/index.tsx index be4906808126f..6fe3887e50d26 100644 --- a/components/SearchBox/index.tsx +++ b/components/SearchBox/index.tsx @@ -1,222 +1,16 @@ import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; -import type { Results, Nullable } from '@orama/orama'; -import clx from 'classnames'; -import { useRouter } from 'next/navigation'; -import { useState, useRef, type FC, useEffect } from 'react'; +import { useState, type FC, useEffect } from 'react'; -import styles from './index.module.css'; -import { orama, highlighter } from './lib/orama'; -import { useClickOutside } from './lib/useClickOutside'; - -type SearchDoc = { - id: string; - path: string; - pageTitle: string; - siteSection: string; - pageSectionTitle: string; - pageSectionContent: string; -}; - -type SearchResults = Nullable>; - -type SearchBoxProps = { - onClose: () => void; -}; - -export const SearchBox: FC = props => { - const [searchTerm, setSearchTerm] = useState(''); - const [searchResults, setSearchResults] = useState(null); - const [selectedFacet, setSelectedFacet] = useState(0); - - const router = useRouter(); - const searchInputRef = useRef(null); - const searchBoxRef = useRef(null); - - useClickOutside(searchBoxRef, () => { - reset(); - props.onClose(); - }); - - useEffect(() => { - searchInputRef.current?.focus(); - search(''); - return () => { - reset(); - }; - }, []); - - useEffect(() => { - search(searchTerm); - }, [searchTerm]); - - function search(term: string) { - const filters = filterBySection(); - - orama - .search({ - term, - limit: 8, - threshold: 0, - boost: { - pageSectionTitle: 4, - pageSectionContent: 2.5, - pageTitle: 1, - }, - facets: { - siteSection: {}, - }, - ...filters, - }) - .then(setSearchResults); - } - - function reset() { - setSearchTerm(''); - setSearchResults(null); - setSelectedFacet(0); - } +import { SearchBox } from '@/components/SearchBox/components/SearchBox'; - function onSubmit(e: React.FormEvent) { - e.preventDefault(); - router.push(`/en/search?q=${searchTerm}§ion=${selectedFacetName}`); - } - - function changeFacet(idx: number) { - setSelectedFacet(idx); - search(searchTerm); - } - - function 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 ( -
-
-
-
- -
- setSearchTerm(event.target.value)} - value={searchTerm} - /> -
-
- -
- {Object.keys(facets).map((facetName, idx) => ( - - ))} -
- -
- {searchResults?.hits.map(hit => ( - - -
- powered by - - Orama - -
-
-
-
- ); -}; - -function pathToBreadcrumbs(path: string) { - return path - .replace(/#.+$/, '') - .split('/') - .slice(0, -1) - .map(element => element.replaceAll('-', ' ')) - .filter(Boolean); -} +import styles from './index.module.css'; export const SearchButton: FC = () => { const [isOpen, setIsOpen] = useState(false); useEffect(() => { document.addEventListener('keydown', event => { + // Detect ⌘ + k on Mac, Ctrl + k on Windows if ((event.metaKey || event.ctrlKey) && event.key === 'k') { event.preventDefault(); openSearchBox(); @@ -226,6 +20,10 @@ export const SearchButton: FC = () => { setIsOpen(false); } }); + + return () => { + document.removeEventListener('keydown', () => {}); + }; }, []); function openSearchBox() { diff --git a/components/SearchBox/lib/utils.ts b/components/SearchBox/lib/utils.ts new file mode 100644 index 0000000000000..84ec34a576c1f --- /dev/null +++ b/components/SearchBox/lib/utils.ts @@ -0,0 +1,8 @@ +export function pathToBreadcrumbs(path: string) { + return path + .replace(/#.+$/, '') + .split('/') + .slice(0, -1) + .map(element => element.replaceAll('-', ' ')) + .filter(Boolean); +} From 5a8b189bcb969fea6a47886782771dcaf3f29059 Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Fri, 19 Jan 2024 14:09:54 +0100 Subject: [PATCH 05/44] feat: wip on searchbox --- app/[locale]/search.tsx | 7 ------- components/SearchBox/components/SearchBox.tsx | 4 +++- components/SearchBox/components/SeeAll.tsx | 4 ++++ components/SearchBox/lib/orama.ts | 9 +++++++++ pages/en/search.mdx | 4 ++++ scripts/orama/create.mjs | 1 - 6 files changed, 20 insertions(+), 9 deletions(-) delete mode 100644 app/[locale]/search.tsx create mode 100644 pages/en/search.mdx diff --git a/app/[locale]/search.tsx b/app/[locale]/search.tsx deleted file mode 100644 index 8e6eab5fe0563..0000000000000 --- a/app/[locale]/search.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import type { FC } from 'react'; - -const SearchPage: FC = () => { - return
Search page
; -}; - -export default SearchPage; diff --git a/components/SearchBox/components/SearchBox.tsx b/components/SearchBox/components/SearchBox.tsx index a2e16fc692c58..c693e70add466 100644 --- a/components/SearchBox/components/SearchBox.tsx +++ b/components/SearchBox/components/SearchBox.tsx @@ -8,7 +8,7 @@ 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 } from '@/components/SearchBox/lib/orama'; +import { orama, getInitialFacets } from '@/components/SearchBox/lib/orama'; import { useClickOutside } from '@/components/SearchBox/lib/useClickOutside'; import { EmptyState } from './EmptyState'; @@ -46,6 +46,8 @@ export const SearchBox: FC = props => { useEffect(() => { searchInputRef.current?.focus(); + getInitialFacets().then(setSearchResults).catch(setSearchError); + return () => { reset(); }; diff --git a/components/SearchBox/components/SeeAll.tsx b/components/SearchBox/components/SeeAll.tsx index 0bdfda5ca8100..223d82670afc1 100644 --- a/components/SearchBox/components/SeeAll.tsx +++ b/components/SearchBox/components/SeeAll.tsx @@ -14,6 +14,10 @@ type SeeAllProps = { }; export const SeeAll: FC = props => { + if (!props.searchTerm) { + return null; + } + return (
Date: Fri, 19 Jan 2024 14:34:03 +0100 Subject: [PATCH 06/44] feat: adds basic mobile styles --- components/SearchBox/components/SearchBox.tsx | 11 ++- .../SearchBox/components/index.module.css | 73 ++++++++----------- 2 files changed, 39 insertions(+), 45 deletions(-) diff --git a/components/SearchBox/components/SearchBox.tsx b/components/SearchBox/components/SearchBox.tsx index c693e70add466..fccb2c00d1f1d 100644 --- a/components/SearchBox/components/SearchBox.tsx +++ b/components/SearchBox/components/SearchBox.tsx @@ -1,4 +1,7 @@ -import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; +import { + MagnifyingGlassIcon, + ChevronLeftIcon, +} from '@heroicons/react/24/outline'; import type { Results, Nullable } from '@orama/orama'; import clx from 'classnames'; import { useRouter } from 'next/navigation'; @@ -118,6 +121,12 @@ export const SearchBox: FC = props => {
+ diff --git a/components/SearchBox/components/index.module.css b/components/SearchBox/components/index.module.css index 75dee47c67487..0efc2a6e8cb1c 100644 --- a/components/SearchBox/components/index.module.css +++ b/components/SearchBox/components/index.module.css @@ -1,33 +1,3 @@ -.searchButton { - @apply relative - w-52 - rounded-md - bg-neutral-100 - py-2 - pl-9 - pr-4 - text-left - text-sm - text-neutral-700 - transition-colors - duration-200 - ease-in-out - hover:bg-neutral-200 - hover:text-neutral-800 - dark:bg-neutral-900 - dark:text-neutral-600 - dark:hover:bg-neutral-800 - dark:hover:text-neutral-500; -} - -.magnifyingGlassIcon { - @apply absolute - left-2 - top-[8px] - h-5 - w-5; -} - .searchBoxModalContainer { @apply fixed inset-0 @@ -43,24 +13,35 @@ .searchBoxModalPanel { @apply fixed - top-60 + h-screen w-full - max-w-3xl - rounded-xl bg-neutral-100 p-2 - shadow-lg - dark:bg-neutral-950; + dark:bg-neutral-950 + md:top-60 + md:h-[450px] + md:max-w-3xl + md:rounded-xl + md:shadow-lg; } .searchBoxInnerPanel { - @apply p-2 - text-neutral-800 - dark:text-neutral-400; + @apply pt-12 + text-neutral-800 + dark:text-neutral-400 + md:p-2; } .searchBoxMagnifyingGlassIcon { - @apply absolute top-[10px] h-6 w-6; + @apply absolute top-[10px] hidden h-6 w-6 md:block; +} + +.searchBoxBackIconContainer { + @apply block md:hidden; +} + +.searchBoxBackIcon { + @apply absolute top-[7px] block h-6 w-6 md:hidden; } .searchBoxInputContainer { @@ -69,6 +50,7 @@ .searchBoxInput { @apply w-full + rounded-b-none border-b border-neutral-300 bg-transparent @@ -111,6 +93,7 @@ mt-2 flex gap-2 + overflow-x-scroll p-2 text-xs font-semibold @@ -169,7 +152,7 @@ .emptyStateContainer { @apply flex - h-[80%] + h-[80%] w-full flex-col items-center @@ -181,8 +164,9 @@ } .noResultsContainer { - @apply flex h-[80%] - w-full + @apply flex + h-[80%] + w-full items-center justify-center text-center @@ -196,8 +180,9 @@ } .searchErrorContainer { - @apply flex h-[80%] - w-full + @apply flex + h-[80%] + w-full items-center justify-center text-center From 9de01482f4e0ad6323fd0e5ccebe8dd8f1960f87 Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Sun, 21 Jan 2024 20:40:29 +0100 Subject: [PATCH 07/44] tmp: work in progress --- .../SearchBox/components/EmptyState.tsx | 6 +- components/SearchBox/components/NoResults.tsx | 14 ++- components/SearchBox/components/PoweredBy.tsx | 38 ++++---- components/SearchBox/index.tsx | 2 + components/SearchBox/lib/useClickOutside.ts | 4 +- components/SearchBox/lib/utils.ts | 5 +- components/SearchPage/index.module.css | 19 ++++ components/SearchPage/index.tsx | 89 +++++++++++++++++++ components/withLayout.tsx | 2 + layouts/New/Search.tsx | 18 ++++ next.mdx.use.mjs | 3 + pages/en/search.mdx | 4 +- scripts/orama/get-documents.mjs | 7 ++ types/layouts.ts | 3 +- 14 files changed, 177 insertions(+), 37 deletions(-) create mode 100644 components/SearchPage/index.module.css create mode 100644 components/SearchPage/index.tsx create mode 100644 layouts/New/Search.tsx diff --git a/components/SearchBox/components/EmptyState.tsx b/components/SearchBox/components/EmptyState.tsx index 9c13de7a76e8e..63e29ebc5e749 100644 --- a/components/SearchBox/components/EmptyState.tsx +++ b/components/SearchBox/components/EmptyState.tsx @@ -2,6 +2,6 @@ import type { FC } from 'react'; import styles from './index.module.css'; -export const EmptyState: FC = () => { - return
Search something...
; -}; +export const EmptyState: FC = () => ( +
Search something...
+); diff --git a/components/SearchBox/components/NoResults.tsx b/components/SearchBox/components/NoResults.tsx index cab112c3ec73e..57f5084c0a594 100644 --- a/components/SearchBox/components/NoResults.tsx +++ b/components/SearchBox/components/NoResults.tsx @@ -6,11 +6,9 @@ type NoResultsProps = { searchTerm: string; }; -export const NoResults: FC = props => { - return ( -
- No results for  - {`"${props.searchTerm}"`}. -
- ); -}; +export const NoResults: FC = props => ( +
+ No results for  + {`"${props.searchTerm}"`}. +
+); diff --git a/components/SearchBox/components/PoweredBy.tsx b/components/SearchBox/components/PoweredBy.tsx index 994ef5cce4363..24424498e8802 100644 --- a/components/SearchBox/components/PoweredBy.tsx +++ b/components/SearchBox/components/PoweredBy.tsx @@ -1,20 +1,22 @@ +import Image from 'next/image'; + import styles from './index.module.css'; -export const PoweredBy = () => { - return ( -
- ); -}; +export const PoweredBy = () => ( +
+ powered by + + Powered by OramaSearch + +
+); diff --git a/components/SearchBox/index.tsx b/components/SearchBox/index.tsx index 6fe3887e50d26..4d911bc423ed7 100644 --- a/components/SearchBox/index.tsx +++ b/components/SearchBox/index.tsx @@ -1,3 +1,5 @@ +'use client'; + import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; import { useState, type FC, useEffect } from 'react'; diff --git a/components/SearchBox/lib/useClickOutside.ts b/components/SearchBox/lib/useClickOutside.ts index 23278e6331c8d..72b063248fca3 100644 --- a/components/SearchBox/lib/useClickOutside.ts +++ b/components/SearchBox/lib/useClickOutside.ts @@ -13,8 +13,6 @@ export const useClickOutside = ( } } document.addEventListener('mousedown', handleClickOutside); - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; + return () => document.removeEventListener('mousedown', handleClickOutside); }, [ref, fn]); }; diff --git a/components/SearchBox/lib/utils.ts b/components/SearchBox/lib/utils.ts index 84ec34a576c1f..ca204dda9b64f 100644 --- a/components/SearchBox/lib/utils.ts +++ b/components/SearchBox/lib/utils.ts @@ -1,8 +1,7 @@ -export function pathToBreadcrumbs(path: string) { - return path +export const pathToBreadcrumbs = (path: string) => + path .replace(/#.+$/, '') .split('/') .slice(0, -1) .map(element => element.replaceAll('-', ' ')) .filter(Boolean); -} diff --git a/components/SearchPage/index.module.css b/components/SearchPage/index.module.css new file mode 100644 index 0000000000000..85cb8d42aaa40 --- /dev/null +++ b/components/SearchPage/index.module.css @@ -0,0 +1,19 @@ +.searchPageContainer { + @apply mx-auto w-full px-4 py-14 md:max-w-screen-xl; +} + +.searchTermContainer { + @apply w-full text-left capitalize; +} + +.searchResultsColumns { + @apply mt-12 grid gap-4 md:grid-cols-[15%_1fr_40%]; +} + +.facetsColumn { + @apply flex flex-col gap-4 capitalize; +} + +.facetCount { + @apply ml-2 text-sm text-neutral-500 dark:text-neutral-800; +} diff --git a/components/SearchPage/index.tsx b/components/SearchPage/index.tsx new file mode 100644 index 0000000000000..82ba335009653 --- /dev/null +++ b/components/SearchPage/index.tsx @@ -0,0 +1,89 @@ +'use client'; + +import Link from 'next/link'; +import type { Nullable, Results } from '@orama/orama'; +import { useSearchParams } from 'next/navigation'; +import { useEffect, useState, type FC } from 'react'; + +import type { SearchDoc } from '@/components/SearchBox/components/SearchBox'; +import { orama } from '@/components/SearchBox/lib/orama'; + +import styles from './index.module.css'; + +type SearchResults = Nullable>; + +const SearchPage: FC = () => { + const searchParams = useSearchParams(); + const [searchResults, setSearchResults] = useState(null); + const [selectedFacet, setSelectedFacet] = useState(0); + + const searchTerm = searchParams?.get('q'); + const searchSection = searchParams?.get('section'); + + useEffect(() => { + orama + .search({ + term: searchTerm || '', + mode: 'hybrid', + facets: { + siteSection: {}, + }, + ...filterBySection(), + }) + .then(setSearchResults) + .catch(console.log); + }, []); + + const facets = { + all: searchResults?.count ?? 0, + ...(searchResults?.facets?.siteSection?.values ?? {}), + }; + + const selectedFacetName = Object.keys(facets)[selectedFacet]; + + function filterBySection() { + if (!searchSection || searchSection === 'all') { + return {}; + } + + return { + where: { + siteSection: { + eq: searchSection, + }, + }, + }; + } + + function changeFacet(idx: number) { + setSelectedFacet(idx); + } + + return ( +
+
+

{searchTerm}

+
+ +
+
+ {Object.keys(facets).map(facetName => ( + + {facetName} + + ({facets[facetName as keyof typeof facets].toLocaleString('en')} + ) + + + ))} +
+
+
+ ); +}; + +export default SearchPage; diff --git a/components/withLayout.tsx b/components/withLayout.tsx index 771a57a7d702f..6f082c2c38713 100644 --- a/components/withLayout.tsx +++ b/components/withLayout.tsx @@ -15,6 +15,7 @@ import DocsLayout from '@/layouts/New/Docs'; import HomeLayout from '@/layouts/New/Home'; import LearnLayout from '@/layouts/New/Learn'; import PostLayout from '@/layouts/New/Post'; +import SearchLayout from '@/layouts/New/Search'; import { ENABLE_WEBSITE_REDESIGN } from '@/next.constants.mjs'; import type { Layouts, LegacyLayouts } from '@/types'; @@ -39,6 +40,7 @@ const redesignLayouts = { 'page.hbs': DefaultLayout, 'blog-post.hbs': PostLayout, 'blog-category.hbs': BlogLayout, + 'search.hbs': SearchLayout, } satisfies Record; type WithLayout = PropsWithChildren<{ layout: L }>; diff --git a/layouts/New/Search.tsx b/layouts/New/Search.tsx new file mode 100644 index 0000000000000..789a2df239b06 --- /dev/null +++ b/layouts/New/Search.tsx @@ -0,0 +1,18 @@ +'use client'; + +import type { FC, PropsWithChildren } from 'react'; + +import WithFooter from '@/components/withFooter'; +import WithNavBar from '@/components/withNavBar'; + +// import styles from './layouts.module.css'; + +const SearchLayout: FC = ({ children }) => ( + <> + +
{children}
+ + +); + +export default SearchLayout; diff --git a/next.mdx.use.mjs b/next.mdx.use.mjs index 2b7f3d51978e8..879596ec427b2 100644 --- a/next.mdx.use.mjs +++ b/next.mdx.use.mjs @@ -9,6 +9,7 @@ import HomeDownloadButton from './components/Home/HomeDownloadButton'; import Link from './components/Link'; import MDXCodeBox from './components/MDX/CodeBox'; import MDXCodeTabs from './components/MDX/CodeTabs'; +import SearchPage from './components/SearchPage'; import WithBadge from './components/withBadge'; import WithBanner from './components/withBanner'; import WithNodeRelease from './components/withNodeRelease'; @@ -38,6 +39,8 @@ export const mdxComponents = { DownloadLink: DownloadLink, // Renders a Button Component for `button` tags Button: Button, + // Renders a Search Page + SearchPage: SearchPage, }; /** diff --git a/pages/en/search.mdx b/pages/en/search.mdx index bde3111ad9f52..211fe28e6084e 100644 --- a/pages/en/search.mdx +++ b/pages/en/search.mdx @@ -1,4 +1,6 @@ --- -layout: default.hbs +layout: search.hbs title: Search Results --- + + diff --git a/scripts/orama/get-documents.mjs b/scripts/orama/get-documents.mjs index 551a794f2f258..e945cf9531dfe 100644 --- a/scripts/orama/get-documents.mjs +++ b/scripts/orama/get-documents.mjs @@ -6,7 +6,14 @@ import { slug } from 'github-slugger'; import { NEXT_DATA_URL } from '../../next.constants.mjs'; const nextPageData = await fetch(`${NEXT_DATA_URL}/page-data`); +const nextAPIPageData = await fetch(`${NEXT_DATA_URL}/en/next-data/api-data`); + const pageData = await nextPageData.json(); +const apiData = await nextAPIPageData.json(); + +apiData.forEach(data => { + console.log(inflate(data.content)); +}); function inflate(data) { return zlib.inflateSync(Buffer.from(data, 'base64')).toString('utf-8'); diff --git a/types/layouts.ts b/types/layouts.ts index ec7c518c6bc92..41177c34d9858 100644 --- a/types/layouts.ts +++ b/types/layouts.ts @@ -6,7 +6,8 @@ export type Layouts = | 'learn.hbs' | 'page.hbs' | 'blog-category.hbs' - | 'blog-post.hbs'; + | 'blog-post.hbs' + | 'search.hbs'; // @TODO: These are legacy layouts that are going to be replaced with the `nodejs/nodejs.dev` Layouts in the future export type LegacyLayouts = From 8a860b7c672a6b4f6a31e53d0ad6b539f9362900 Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Mon, 22 Jan 2024 15:52:02 +0100 Subject: [PATCH 08/44] work in progress --- components/SearchBox/components/NoResults.tsx | 4 +- components/SearchBox/index.tsx | 29 ++++------ components/SearchBox/lib/orama.ts | 2 +- components/SearchBox/lib/useClickOutside.ts | 4 +- .../SearchBox/lib/useKeyboardCommands.ts | 33 +++++++++++ scripts/orama/get-documents.mjs | 27 +++++---- scripts/orama/sync-orama-cloud.mjs | 58 ++++++++----------- 7 files changed, 89 insertions(+), 68 deletions(-) create mode 100644 components/SearchBox/lib/useKeyboardCommands.ts diff --git a/components/SearchBox/components/NoResults.tsx b/components/SearchBox/components/NoResults.tsx index 57f5084c0a594..dc13775f97329 100644 --- a/components/SearchBox/components/NoResults.tsx +++ b/components/SearchBox/components/NoResults.tsx @@ -2,9 +2,7 @@ import type { FC } from 'react'; import styles from './index.module.css'; -type NoResultsProps = { - searchTerm: string; -}; +type NoResultsProps = { searchTerm: string }; export const NoResults: FC = props => (
diff --git a/components/SearchBox/index.tsx b/components/SearchBox/index.tsx index 4d911bc423ed7..1bd29a5c698d5 100644 --- a/components/SearchBox/index.tsx +++ b/components/SearchBox/index.tsx @@ -1,32 +1,27 @@ 'use client'; import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; -import { useState, type FC, useEffect } from 'react'; +import { useState, type FC } from 'react'; import { SearchBox } from '@/components/SearchBox/components/SearchBox'; +import { useKeyboardCommands } from '@/components/SearchBox/lib/useKeyboardCommands'; import styles from './index.module.css'; export const SearchButton: FC = () => { const [isOpen, setIsOpen] = useState(false); - useEffect(() => { - document.addEventListener('keydown', event => { - // Detect ⌘ + k on Mac, Ctrl + k on Windows - if ((event.metaKey || event.ctrlKey) && event.key === 'k') { - event.preventDefault(); + useKeyboardCommands(cmd => { + switch (cmd) { + case 'cmd-k': openSearchBox(); - } - - if (event.key === 'Escape') { - setIsOpen(false); - } - }); - - return () => { - document.removeEventListener('keydown', () => {}); - }; - }, []); + break; + case 'escape': + closeSearchBox(); + break; + default: + } + }); function openSearchBox() { setIsOpen(true); diff --git a/components/SearchBox/lib/orama.ts b/components/SearchBox/lib/orama.ts index dc8b663863a7d..3f15bf6fb6202 100644 --- a/components/SearchBox/lib/orama.ts +++ b/components/SearchBox/lib/orama.ts @@ -15,7 +15,7 @@ export const highlighter = new Highlight({ export async function getInitialFacets() { return await orama.search({ - term: 'a e i o u', + term: '', facets: { siteSection: {}, }, diff --git a/components/SearchBox/lib/useClickOutside.ts b/components/SearchBox/lib/useClickOutside.ts index 72b063248fca3..7b15e30b7d983 100644 --- a/components/SearchBox/lib/useClickOutside.ts +++ b/components/SearchBox/lib/useClickOutside.ts @@ -7,11 +7,11 @@ export const useClickOutside = ( ) => { useEffect(() => { const element = ref?.current; - function handleClickOutside(event: Event) { + const handleClickOutside = (event: Event) => { if (element && !element.contains(event.target as Node | null)) { fn(); } - } + }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, [ref, fn]); diff --git a/components/SearchBox/lib/useKeyboardCommands.ts b/components/SearchBox/lib/useKeyboardCommands.ts new file mode 100644 index 0000000000000..699edc1711734 --- /dev/null +++ b/components/SearchBox/lib/useKeyboardCommands.ts @@ -0,0 +1,33 @@ +import { useEffect } from 'react'; + +type KeyboardCommand = 'cmd-k' | 'escape' | 'down' | 'up'; + +type KeyboardCommandCallback = (key: KeyboardCommand) => void; + +export const useKeyboardCommands = (fn: KeyboardCommandCallback) => { + useEffect(() => { + document.addEventListener('keydown', event => { + // Detect ⌘ + k on Mac, Ctrl + k on Windows + if ((event.metaKey || event.ctrlKey) && event.key === 'k') { + event.preventDefault(); + fn('cmd-k'); + } + + if (event.key === 'Escape') { + fn('escape'); + } + + if (event.key === 'ArrowDown') { + fn('down'); + } + + if (event.key === 'ArrowUp') { + fn('up'); + } + }); + + return () => { + document.removeEventListener('keydown', () => {}); + }; + }, []); +}; diff --git a/scripts/orama/get-documents.mjs b/scripts/orama/get-documents.mjs index e945cf9531dfe..50920502c0250 100644 --- a/scripts/orama/get-documents.mjs +++ b/scripts/orama/get-documents.mjs @@ -1,4 +1,3 @@ -import crypto from 'node:crypto'; import zlib from 'node:zlib'; import { slug } from 'github-slugger'; @@ -6,15 +5,11 @@ import { slug } from 'github-slugger'; import { NEXT_DATA_URL } from '../../next.constants.mjs'; const nextPageData = await fetch(`${NEXT_DATA_URL}/page-data`); -const nextAPIPageData = await fetch(`${NEXT_DATA_URL}/en/next-data/api-data`); +const nextAPIPageData = await fetch(`${NEXT_DATA_URL}/api-data`); const pageData = await nextPageData.json(); const apiData = await nextAPIPageData.json(); -apiData.forEach(data => { - console.log(inflate(data.content)); -}); - function inflate(data) { return zlib.inflateSync(Buffer.from(data, 'base64')).toString('utf-8'); } @@ -43,17 +38,29 @@ function splitIntoSections(markdownContent) { })); } -export const siteContent = pageData +function getPageTitle(data) { + const { title } = data; + + if (title) { + return title; + } + + const { pathname } = data; + const parts = pathname.split('/'); + const lastPart = parts[parts.length - 1].replace(/\.html$/, ''); + + return lastPart.replace(/-/g, ' '); +} + +export const siteContent = [...pageData, ...apiData] .map(data => { - const { pathname, title, content } = data; + const { pathname, title = getPageTitle(data), content } = data; const markdownContent = inflate(content); const siteSection = pathname.split('/').shift(); const subSections = splitIntoSections(markdownContent); return subSections.map(section => { - const id = crypto.randomUUID(); return { - id, path: pathname + '#' + slug(section.pageSectionTitle), siteSection, pageTitle: title, diff --git a/scripts/orama/sync-orama-cloud.mjs b/scripts/orama/sync-orama-cloud.mjs index 608b6af08c424..e604d57f1d57d 100644 --- a/scripts/orama/sync-orama-cloud.mjs +++ b/scripts/orama/sync-orama-cloud.mjs @@ -2,6 +2,12 @@ import { siteContent } from './get-documents.mjs'; const INDEX_ID = process.env.ORAMA_INDEX_ID; const API_KEY = process.env.ORAMA_SECRET_KEY; +const oramaAPIBaseURL = `https://api.oramasearch.com/api/v1/webhooks/${INDEX_ID}`; + +const oramaHeaders = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${API_KEY}`, +}; async function runUpdate() { const batchSize = 50; @@ -15,53 +21,35 @@ async function runUpdate() { `Inserting ${batches.length} batches of ${batchSize} documents each.` ); await Promise.all(batches.map(insertBatch)); - console.log('Done inserting batches.'); + console.log(`Done inserting batches. ${siteContent.length} documents total.`); } async function insertBatch(batch) { - await fetch( - `https://api.oramasearch.com/api/v1/webhooks/${INDEX_ID}/notify`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${API_KEY}`, - }, - body: JSON.stringify({ - upsert: batch, - }), - } - ); + await fetch(`${oramaAPIBaseURL}/notify`, { + method: 'POST', + headers: oramaHeaders, + body: JSON.stringify({ + upsert: batch, + }), + }); } async function triggerDeployment() { console.log('Triggering deployment'); - await fetch( - `https://api.oramasearch.com/api/v1/webhooks/${INDEX_ID}/deploy`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${API_KEY}`, - }, - } - ); + await fetch(`${oramaAPIBaseURL}/deploy`, { + method: 'POST', + headers: oramaHeaders, + }); console.log('Done triggering deployment'); } async function emptyOramaIndex() { console.log('Emptying index'); - await fetch( - `https://api.oramasearch.com/api/v1/webhooks/${INDEX_ID}/snapshot`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${API_KEY}`, - }, - body: JSON.stringify([]), - } - ); + await fetch(`${oramaAPIBaseURL}/snapshot`, { + method: 'POST', + headers: oramaHeaders, + body: JSON.stringify([]), + }); console.log('Done emptying index'); } From 69bc5ca0f972faa4693d1a660372815eb1409e36 Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Mon, 22 Jan 2024 17:18:50 +0100 Subject: [PATCH 09/44] feat: improves search page --- components/SearchBox/components/SearchBox.tsx | 3 +- .../SearchBox/components/SearchResult.tsx | 13 +++- components/SearchBox/components/SeeAll.tsx | 5 +- components/SearchBox/index.tsx | 2 +- components/SearchBox/lib/orama.ts | 2 +- .../SearchBox/lib/useKeyboardCommands.ts | 6 +- components/SearchPage/index.module.css | 33 ++++++++- components/SearchPage/index.tsx | 67 +++++++++++++++---- .../utils/useBottomScrollListener.ts | 29 ++++++++ package-lock.json | 6 +- 10 files changed, 139 insertions(+), 27 deletions(-) create mode 100644 components/SearchPage/utils/useBottomScrollListener.ts diff --git a/components/SearchBox/components/SearchBox.tsx b/components/SearchBox/components/SearchBox.tsx index fccb2c00d1f1d..262b2ddc5ea55 100644 --- a/components/SearchBox/components/SearchBox.tsx +++ b/components/SearchBox/components/SearchBox.tsx @@ -69,7 +69,7 @@ export const SearchBox: FC = props => { boost: { pageSectionTitle: 4, pageSectionContent: 2.5, - pageTitle: 1, + pageTitle: 1.5, }, facets: { siteSection: {}, @@ -89,6 +89,7 @@ export const SearchBox: FC = props => { function onSubmit(e: React.FormEvent) { e.preventDefault(); router.push(`/en/search?q=${searchTerm}§ion=${selectedFacetName}`); + props.onClose(); } function changeFacet(idx: number) { diff --git a/components/SearchBox/components/SearchResult.tsx b/components/SearchBox/components/SearchResult.tsx index d043aa17a51ae..779e6e35a42b8 100644 --- a/components/SearchBox/components/SearchResult.tsx +++ b/components/SearchBox/components/SearchResult.tsx @@ -1,4 +1,5 @@ import type { Result } from '@orama/orama'; +import NextLink from 'next/link'; import type { FC } from 'react'; import type { SearchDoc } from '@/components/SearchBox/components/SearchBox'; @@ -13,11 +14,17 @@ type SearchResultProps = { }; export const SearchResult: FC = 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 ( -
= props => { {' > '} {props.hit.document.pageTitle}
-
+ ); }; diff --git a/components/SearchBox/components/SeeAll.tsx b/components/SearchBox/components/SeeAll.tsx index 223d82670afc1..961fd9e7b49c3 100644 --- a/components/SearchBox/components/SeeAll.tsx +++ b/components/SearchBox/components/SeeAll.tsx @@ -1,4 +1,5 @@ import type { Results } from '@orama/orama'; +import NextLink from 'next/link'; import type { FC } from 'react'; import type { SearchDoc } from '@/components/SearchBox/components/SearchBox'; @@ -20,11 +21,11 @@ export const SeeAll: FC = props => { return ( ); }; diff --git a/components/SearchBox/index.tsx b/components/SearchBox/index.tsx index 1bd29a5c698d5..28d261ea8f158 100644 --- a/components/SearchBox/index.tsx +++ b/components/SearchBox/index.tsx @@ -41,7 +41,7 @@ export const SearchButton: FC = () => { Start typing... - {isOpen && } + {isOpen ? : null} ); }; diff --git a/components/SearchBox/lib/orama.ts b/components/SearchBox/lib/orama.ts index 3f15bf6fb6202..0997cc9f94fea 100644 --- a/components/SearchBox/lib/orama.ts +++ b/components/SearchBox/lib/orama.ts @@ -9,7 +9,7 @@ export const orama = new OramaClient({ orama.startHeartBeat({ frequency: 3500 }); export const highlighter = new Highlight({ - CSSClass: 'font-bold dark:text-neutral-800', + CSSClass: 'font-bold', HTMLTag: 'span', }); diff --git a/components/SearchBox/lib/useKeyboardCommands.ts b/components/SearchBox/lib/useKeyboardCommands.ts index 699edc1711734..13502f5596c8b 100644 --- a/components/SearchBox/lib/useKeyboardCommands.ts +++ b/components/SearchBox/lib/useKeyboardCommands.ts @@ -1,6 +1,6 @@ import { useEffect } from 'react'; -type KeyboardCommand = 'cmd-k' | 'escape' | 'down' | 'up'; +type KeyboardCommand = 'cmd-k' | 'escape' | 'down' | 'up' | 'enter'; type KeyboardCommandCallback = (key: KeyboardCommand) => void; @@ -17,6 +17,10 @@ export const useKeyboardCommands = (fn: KeyboardCommandCallback) => { fn('escape'); } + if (event.key === 'Enter') { + fn('enter'); + } + if (event.key === 'ArrowDown') { fn('down'); } diff --git a/components/SearchPage/index.module.css b/components/SearchPage/index.module.css index 85cb8d42aaa40..cee2da571030b 100644 --- a/components/SearchPage/index.module.css +++ b/components/SearchPage/index.module.css @@ -3,17 +3,44 @@ } .searchTermContainer { - @apply w-full text-left capitalize; + @apply relative w-full px-6 text-left capitalize md:px-0; } .searchResultsColumns { - @apply mt-12 grid gap-4 md:grid-cols-[15%_1fr_40%]; + @apply relative mt-12 grid gap-4 md:grid-cols-[15%_1fr]; } .facetsColumn { - @apply flex flex-col gap-4 capitalize; + @apply sticky top-0 flex gap-4 overflow-x-scroll 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 text-neutral-500 dark:text-neutral-600; +} + +.searchResultSnippet { + @apply my-2 text-sm text-neutral-500 dark:text-neutral-400; +} diff --git a/components/SearchPage/index.tsx b/components/SearchPage/index.tsx index 82ba335009653..ea313b4e3374a 100644 --- a/components/SearchPage/index.tsx +++ b/components/SearchPage/index.tsx @@ -1,46 +1,66 @@ 'use client'; +import type { Nullable, Results, Result } from '@orama/orama'; import Link from 'next/link'; -import type { Nullable, Results } from '@orama/orama'; import { useSearchParams } from 'next/navigation'; import { useEffect, useState, type FC } from 'react'; import type { SearchDoc } from '@/components/SearchBox/components/SearchBox'; -import { orama } from '@/components/SearchBox/lib/orama'; +import { orama, highlighter } from '@/components/SearchBox/lib/orama'; +import { pathToBreadcrumbs } from '@/components/SearchBox/lib/utils'; +import { useBottomScrollListener } from '@/components/SearchPage/utils/useBottomScrollListener'; import styles from './index.module.css'; type SearchResults = Nullable>; +type Hit = Result; const SearchPage: FC = () => { const searchParams = useSearchParams(); const [searchResults, setSearchResults] = useState(null); - const [selectedFacet, setSelectedFacet] = useState(0); + const [hits, setHits] = useState>([]); + const [offset, setOffset] = useState(0); const searchTerm = searchParams?.get('q'); const searchSection = searchParams?.get('section'); + useBottomScrollListener(() => { + setOffset(offset => offset + 10); + }); + + useEffect(() => { + search(offset); + }, [offset]); + useEffect(() => { + setHits([]); + search(0); + }, [searchSection, searchTerm]); + + function search(resultsOffset = 0) { + console.log({ resultsOffset }); orama .search({ term: searchTerm || '', - mode: 'hybrid', + limit: 10, + offset: resultsOffset, facets: { siteSection: {}, }, ...filterBySection(), }) - .then(setSearchResults) + .then(results => { + setSearchResults(results); + setHits(hits => [...hits, ...results.hits]); + }) .catch(console.log); - }, []); + } const facets = { all: searchResults?.count ?? 0, ...(searchResults?.facets?.siteSection?.values ?? {}), }; - const selectedFacetName = Object.keys(facets)[selectedFacet]; - function filterBySection() { if (!searchSection || searchSection === 'all') { return {}; @@ -55,10 +75,6 @@ const SearchPage: FC = () => { }; } - function changeFacet(idx: number) { - setSelectedFacet(idx); - } - return (
@@ -81,6 +97,33 @@ const SearchPage: FC = () => { ))}
+ +
+ {hits?.map(hit => ( + +
+

+ {hit.document.pageSectionTitle} +

+

+

+ Home {'>'} {pathToBreadcrumbs(hit.document.path).join(' > ')} +
+
+ + ))} +
); diff --git a/components/SearchPage/utils/useBottomScrollListener.ts b/components/SearchPage/utils/useBottomScrollListener.ts new file mode 100644 index 0000000000000..d5e2eb2c0d10b --- /dev/null +++ b/components/SearchPage/utils/useBottomScrollListener.ts @@ -0,0 +1,29 @@ +import { useState, useEffect } from 'react'; + +type CallbackFunction = () => void; + +export const useBottomScrollListener = (callback: CallbackFunction) => { + const [bottomReached, setBottomReached] = useState(false); + + 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); + callback(); + } else { + setBottomReached(false); + } + }; + + useEffect(() => { + window.addEventListener('scroll', handleScroll); + return () => window.removeEventListener('scroll', handleScroll); + }, []); + + return bottomReached; +}; diff --git a/package-lock.json b/package-lock.json index 1becba30e33f8..493568305256e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4644,9 +4644,9 @@ } }, "node_modules/@orama/highlight": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@orama/highlight/-/highlight-0.1.2.tgz", - "integrity": "sha512-B48PnxFwRRHBeEIkmKI38tZmpQDWdt6o4bch5dZaChdZh0pwPHtostMv++eVlNv3/qLtfcdLoSYHWvoN9Mp0Lw==", + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@orama/highlight/-/highlight-0.1.3.tgz", + "integrity": "sha512-KmqMkSaGZxKnS2UiK1/nacu7+D+wadT+irgBdIBoda5BkDVPPsIXwIta0ISKmZRaM3GnUs2oKx3KteYojBkIVA==", "dependencies": { "@orama/orama": "^2.0.0-beta.1" } From a24870d658e8ee61d336d4241a1f7c33d855b738 Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Mon, 22 Jan 2024 17:23:01 +0100 Subject: [PATCH 10/44] style: addresses feedbacks on code style --- components/SearchBox/components/SearchBox.tsx | 40 +++++++++---------- components/SearchPage/index.tsx | 8 ++-- 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/components/SearchBox/components/SearchBox.tsx b/components/SearchBox/components/SearchBox.tsx index 262b2ddc5ea55..7e62fef2019e1 100644 --- a/components/SearchBox/components/SearchBox.tsx +++ b/components/SearchBox/components/SearchBox.tsx @@ -3,7 +3,7 @@ import { ChevronLeftIcon, } from '@heroicons/react/24/outline'; import type { Results, Nullable } from '@orama/orama'; -import clx from 'classnames'; +import classNames from 'classnames'; import { useRouter } from 'next/navigation'; import { useState, useRef, type FC, useEffect } from 'react'; @@ -28,11 +28,9 @@ export type SearchDoc = { type SearchResults = Nullable>; -type SearchBoxProps = { - onClose: () => void; -}; +type SearchBoxProps = { onClose: () => void }; -export const SearchBox: FC = props => { +export const SearchBox: FC = ({ onClose }) => { const [searchTerm, setSearchTerm] = useState(''); const [searchResults, setSearchResults] = useState(null); const [selectedFacet, setSelectedFacet] = useState(0); @@ -44,23 +42,21 @@ export const SearchBox: FC = props => { useClickOutside(searchBoxRef, () => { reset(); - props.onClose(); + onClose(); }); useEffect(() => { searchInputRef.current?.focus(); getInitialFacets().then(setSearchResults).catch(setSearchError); - return () => { - reset(); - }; + return () => reset(); }, []); useEffect(() => { search(searchTerm); }, [searchTerm, selectedFacet]); - function search(term: string) { + const search = (term: string) => { orama .search({ term, @@ -78,25 +74,25 @@ export const SearchBox: FC = props => { }) .then(setSearchResults) .catch(setSearchError); - } + }; - function reset() { + const reset = () => { setSearchTerm(''); setSearchResults(null); setSelectedFacet(0); - } + }; - function onSubmit(e: React.FormEvent) { + const onSubmit = (e: React.FormEvent) => { e.preventDefault(); router.push(`/en/search?q=${searchTerm}§ion=${selectedFacetName}`); - props.onClose(); - } + onClose(); + }; - function changeFacet(idx: number) { + const changeFacet = (idx: number) => { setSelectedFacet(idx); - } + }; - function filterBySection() { + const filterBySection = () => { if (selectedFacet === 0) { return {}; } @@ -108,7 +104,7 @@ export const SearchBox: FC = props => { }, }, }; - } + }; const facets = { all: searchResults?.count ?? 0, @@ -123,7 +119,7 @@ export const SearchBox: FC = props => {
{isOpen ? : null} diff --git a/components/SearchPage/index.tsx b/components/SearchPage/index.tsx index 110dfd6072410..2e44c61b3ad5c 100644 --- a/components/SearchPage/index.tsx +++ b/components/SearchPage/index.tsx @@ -38,7 +38,6 @@ const SearchPage: FC = () => { }, [searchSection, searchTerm]); const search = (resultsOffset = 0) => { - console.log({ resultsOffset }); orama .search({ term: searchTerm || '', diff --git a/i18n/locales/en.json b/i18n/locales/en.json index 4a98b48a82487..5b4d32b121a3c 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -175,6 +175,26 @@ "changelogModal": { "startContributing": "Start Contributing" } + }, + "search": { + "searchBox": { + "placeholder": "Start typing..." + }, + "seeAll": { + "text": "See all {count} results" + }, + "searchError": { + "text": "An error occurred while searching. Please try again later." + }, + "poweredBy": { + "text": "Powered by" + }, + "noResults": { + "text": "No results found for \"{query}\"." + }, + "emptyState": { + "text": "Search something..." + } } }, "layouts": { diff --git a/turbo.json b/turbo.json index f74c69540a862..5b139d394cb12 100644 --- a/turbo.json +++ b/turbo.json @@ -12,7 +12,9 @@ "NEXT_PUBLIC_VERCEL_URL", "NEXT_PUBLIC_DIST_URL", "NEXT_PUBLIC_DOCS_URL", - "NEXT_PUBLIC_BASE_PATH" + "NEXT_PUBLIC_BASE_PATH", + "NEXT_PUBLIC_ORAMA_API_KEY", + "NEXT_PUBLIC_ORAMA_ENDPOINT" ] }, "build": { @@ -31,7 +33,9 @@ "NEXT_PUBLIC_VERCEL_URL", "NEXT_PUBLIC_DIST_URL", "NEXT_PUBLIC_DOCS_URL", - "NEXT_PUBLIC_BASE_PATH" + "NEXT_PUBLIC_BASE_PATH", + "NEXT_PUBLIC_ORAMA_API_KEY", + "NEXT_PUBLIC_ORAMA_ENDPOINT" ] }, "start": { @@ -44,7 +48,9 @@ "NEXT_PUBLIC_VERCEL_URL", "NEXT_PUBLIC_DIST_URL", "NEXT_PUBLIC_DOCS_URL", - "NEXT_PUBLIC_BASE_PATH" + "NEXT_PUBLIC_BASE_PATH", + "NEXT_PUBLIC_ORAMA_API_KEY", + "NEXT_PUBLIC_ORAMA_ENDPOINT" ] }, "deploy": { @@ -63,7 +69,9 @@ "NEXT_PUBLIC_VERCEL_URL", "NEXT_PUBLIC_DIST_URL", "NEXT_PUBLIC_DOCS_URL", - "NEXT_PUBLIC_BASE_PATH" + "NEXT_PUBLIC_BASE_PATH", + "NEXT_PUBLIC_ORAMA_API_KEY", + "NEXT_PUBLIC_ORAMA_ENDPOINT" ] }, "lint:js": { From 9c85831809410cbc05276b3827d1c8f549fe9ccb Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Mon, 22 Jan 2024 18:51:44 +0100 Subject: [PATCH 13/44] fix: encodes URL components --- components/SearchBox/components/SeeAll.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/components/SearchBox/components/SeeAll.tsx b/components/SearchBox/components/SeeAll.tsx index 92d1bac9d6997..ec904be04d7b3 100644 --- a/components/SearchBox/components/SeeAll.tsx +++ b/components/SearchBox/components/SeeAll.tsx @@ -23,11 +23,13 @@ export const SeeAll: FC = props => { return null; } + const sanitizedSearchTerm = encodeURIComponent(props.searchTerm); + const sanitizedFacetName = encodeURIComponent(props.selectedFacetName); + const allResultsURL = `/en/search?q=${sanitizedSearchTerm}§ion=${sanitizedFacetName}`; + return (
- + {t('components.search.seeAll.text', { count: resultsCount })}
From 5e3cef2aafe949cc487567ab42b06f0cc5f4682b Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Mon, 22 Jan 2024 19:01:14 +0100 Subject: [PATCH 14/44] style: addresses feedback --- package-lock.json | 8 ++++---- package.json | 2 +- scripts/orama/get-documents.mjs | 16 +++++++--------- scripts/orama/sync-orama-cloud.mjs | 24 ++++++++++++------------ 4 files changed, 24 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index 493568305256e..2b6cfef59cc1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "@mdx-js/mdx": "^3.0.0", "@nodevu/core": "~0.1.0", "@orama/highlight": "^0.1.2", - "@orama/orama": "^2.0.0", "@oramacloud/client": "^1.0.2", "@radix-ui/react-accessible-icon": "^1.0.3", "@radix-ui/react-avatar": "^1.0.4", @@ -61,6 +60,7 @@ "vfile-matter": "~5.0.0" }, "devDependencies": { + "@orama/orama": "^2.0.1", "@storybook/addon-controls": "~7.6.8", "@storybook/addon-interactions": "~7.6.8", "@storybook/addon-themes": "~7.6.8", @@ -4652,9 +4652,9 @@ } }, "node_modules/@orama/orama": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@orama/orama/-/orama-2.0.0.tgz", - "integrity": "sha512-Mg3cuIDSMmcQzu7ucLZRhLCzVwZN3+xGmeCNTyuzPUAXlrpF/g3MGUdFOc9ZX++S1Huu/hlxIpk4k8QY7rTr2g==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@orama/orama/-/orama-2.0.1.tgz", + "integrity": "sha512-9HUHToE93yvDGcmnEELNynG4kmzSrhSElnfLN9UFPO9ZwfHVPp1NvALli7rm1F/Bqdgi6YM6dEXKmPJUQnwiHg==", "engines": { "node": ">= 16.0.0" } diff --git a/package.json b/package.json index 1a103cd1ab59b..4552dca157267 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,6 @@ "@mdx-js/mdx": "^3.0.0", "@nodevu/core": "~0.1.0", "@orama/highlight": "^0.1.2", - "@orama/orama": "^2.0.0", "@oramacloud/client": "^1.0.2", "@radix-ui/react-accessible-icon": "^1.0.3", "@radix-ui/react-avatar": "^1.0.4", @@ -92,6 +91,7 @@ "vfile-matter": "~5.0.0" }, "devDependencies": { + "@orama/orama": "^2.0.1", "@storybook/addon-controls": "~7.6.8", "@storybook/addon-interactions": "~7.6.8", "@storybook/addon-themes": "~7.6.8", diff --git a/scripts/orama/get-documents.mjs b/scripts/orama/get-documents.mjs index 50920502c0250..4774fd99161b0 100644 --- a/scripts/orama/get-documents.mjs +++ b/scripts/orama/get-documents.mjs @@ -10,11 +10,7 @@ const nextAPIPageData = await fetch(`${NEXT_DATA_URL}/api-data`); const pageData = await nextPageData.json(); const apiData = await nextAPIPageData.json(); -function inflate(data) { - return zlib.inflateSync(Buffer.from(data, 'base64')).toString('utf-8'); -} - -function splitIntoSections(markdownContent) { +const splitIntoSections = markdownContent => { const lines = markdownContent.split(/\n/gm); const sections = []; @@ -36,9 +32,9 @@ function splitIntoSections(markdownContent) { ...section, pageSectionContent: section.pageSectionContent.join('\n'), })); -} +}; -function getPageTitle(data) { +const getPageTitle = data => { const { title } = data; if (title) { @@ -50,12 +46,14 @@ function getPageTitle(data) { const lastPart = parts[parts.length - 1].replace(/\.html$/, ''); return lastPart.replace(/-/g, ' '); -} +}; export const siteContent = [...pageData, ...apiData] .map(data => { const { pathname, title = getPageTitle(data), content } = data; - const markdownContent = inflate(content); + const markdownContent = zlib + .inflateSync(Buffer.from(content, 'base64')) + .toString('utf-8'); const siteSection = pathname.split('/').shift(); const subSections = splitIntoSections(markdownContent); diff --git a/scripts/orama/sync-orama-cloud.mjs b/scripts/orama/sync-orama-cloud.mjs index e604d57f1d57d..7b382198049c6 100644 --- a/scripts/orama/sync-orama-cloud.mjs +++ b/scripts/orama/sync-orama-cloud.mjs @@ -2,14 +2,14 @@ import { siteContent } from './get-documents.mjs'; const INDEX_ID = process.env.ORAMA_INDEX_ID; const API_KEY = process.env.ORAMA_SECRET_KEY; -const oramaAPIBaseURL = `https://api.oramasearch.com/api/v1/webhooks/${INDEX_ID}`; +const ORAMA_API_BASE_URL = `https://api.oramasearch.com/api/v1/webhooks/${INDEX_ID}`; const oramaHeaders = { 'Content-Type': 'application/json', Authorization: `Bearer ${API_KEY}`, }; -async function runUpdate() { +const runUpdate = async () => { const batchSize = 50; const batches = []; @@ -22,36 +22,36 @@ async function runUpdate() { ); await Promise.all(batches.map(insertBatch)); console.log(`Done inserting batches. ${siteContent.length} documents total.`); -} +}; -async function insertBatch(batch) { - await fetch(`${oramaAPIBaseURL}/notify`, { +const insertBatch = async batch => { + await fetch(`${ORAMA_API_BASE_URL}/notify`, { method: 'POST', headers: oramaHeaders, body: JSON.stringify({ upsert: batch, }), }); -} +}; -async function triggerDeployment() { +const triggerDeployment = async () => { console.log('Triggering deployment'); - await fetch(`${oramaAPIBaseURL}/deploy`, { + await fetch(`${ORAMA_API_BASE_URL}/deploy`, { method: 'POST', headers: oramaHeaders, }); console.log('Done triggering deployment'); -} +}; -async function emptyOramaIndex() { +const emptyOramaIndex = async () => { console.log('Emptying index'); - await fetch(`${oramaAPIBaseURL}/snapshot`, { + await fetch(`${ORAMA_API_BASE_URL}/snapshot`, { method: 'POST', headers: oramaHeaders, body: JSON.stringify([]), }); console.log('Done emptying index'); -} +}; await emptyOramaIndex(); await runUpdate(); From a7aab20824f90dbf37f7f2d089faa275a9325740 Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Mon, 22 Jan 2024 21:39:02 +0100 Subject: [PATCH 15/44] style: addresses feedback --- components/SearchPage/index.module.css | 3 ++- components/SearchPage/index.tsx | 18 +++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/components/SearchPage/index.module.css b/components/SearchPage/index.module.css index 6707bd244d51e..dd7ae3c0bd0cf 100644 --- a/components/SearchPage/index.module.css +++ b/components/SearchPage/index.module.css @@ -71,7 +71,8 @@ } .searchResultSnippet { - @apply my-2 text-sm + @apply my-2 + text-sm text-neutral-500 dark:text-neutral-400; } diff --git a/components/SearchPage/index.tsx b/components/SearchPage/index.tsx index 2e44c61b3ad5c..f42cf128d1d4b 100644 --- a/components/SearchPage/index.tsx +++ b/components/SearchPage/index.tsx @@ -61,17 +61,17 @@ const SearchPage: FC = () => { }; const filterBySection = () => { - if (!searchSection || searchSection === 'all') { - return {}; + if (searchSection && searchSection !== 'all') { + return { + where: { + siteSection: { + eq: searchSection, + }, + }, + }; } - return { - where: { - siteSection: { - eq: searchSection, - }, - }, - }; + return {}; }; return ( From ebc374247ad2a0a357c874ea3a657bd83188f780 Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Mon, 22 Jan 2024 21:48:17 +0100 Subject: [PATCH 16/44] docs: adds comments to Orama sync script --- scripts/orama/sync-orama-cloud.mjs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/scripts/orama/sync-orama-cloud.mjs b/scripts/orama/sync-orama-cloud.mjs index 7b382198049c6..230f07ca99627 100644 --- a/scripts/orama/sync-orama-cloud.mjs +++ b/scripts/orama/sync-orama-cloud.mjs @@ -1,5 +1,7 @@ import { siteContent } from './get-documents.mjs'; +// The following follows the instructions at https://docs.oramasearch.com/cloud/data-sources/custom-integrations/webhooks + const INDEX_ID = process.env.ORAMA_INDEX_ID; const API_KEY = process.env.ORAMA_SECRET_KEY; const ORAMA_API_BASE_URL = `https://api.oramasearch.com/api/v1/webhooks/${INDEX_ID}`; @@ -9,6 +11,8 @@ const oramaHeaders = { Authorization: `Bearer ${API_KEY}`, }; +// Orama allows to send several documents at once, so we batch them in groups of 50. +// This is not strictly necessary, but it makes the process faster. const runUpdate = async () => { const batchSize = 50; const batches = []; @@ -24,6 +28,9 @@ const runUpdate = async () => { console.log(`Done inserting batches. ${siteContent.length} documents total.`); }; +// We call the "notify" API to upsert the documents in the index. +// Orama will keep a queue of all the documents we send, and will process them once we call the "deploy" API. +// Full docs on the "notify" API: https://docs.oramasearch.com/cloud/data-sources/custom-integrations/webhooks#updating-removing-inserting-elements-in-a-live-index const insertBatch = async batch => { await fetch(`${ORAMA_API_BASE_URL}/notify`, { method: 'POST', @@ -34,6 +41,8 @@ const insertBatch = async batch => { }); }; +// We call the "deploy" API to trigger a deployment of the index, which will process all the documents in the queue. +// Full docs on the "deploy" API: https://docs.oramasearch.com/cloud/data-sources/custom-integrations/webhooks#deploying-the-index const triggerDeployment = async () => { console.log('Triggering deployment'); await fetch(`${ORAMA_API_BASE_URL}/deploy`, { @@ -43,6 +52,10 @@ const triggerDeployment = async () => { console.log('Done triggering deployment'); }; +// We call the "snapshot" API to empty the index before inserting the new documents. +// The "snapshot" API is tipically used to replace the entire index with a fresh set of documents, but we use it here to empty the index. +// This operation gets queued, so the live index will still be available until we call the "deploy" API and redeploy the index. +// Full docs on the "snapshot" API: https://docs.oramasearch.com/cloud/data-sources/custom-integrations/webhooks#inserting-a-snapshot const emptyOramaIndex = async () => { console.log('Emptying index'); await fetch(`${ORAMA_API_BASE_URL}/snapshot`, { @@ -53,6 +66,12 @@ const emptyOramaIndex = async () => { console.log('Done emptying index'); }; +// Now we proceed to call the APIs in order: +// 1. Empty the index +// 2. Insert the documents +// 3. Trigger a deployment +// Once all these steps are done, the new documents will be available in the live index. +// Allow Orama up to 1 minute to distribute the documents to all the 300+ nodes worldwide. await emptyOramaIndex(); await runUpdate(); await triggerDeployment(); From bbc98da16db3d6e99f2d50be8e84cb22b7bb2888 Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Mon, 22 Jan 2024 21:56:58 +0100 Subject: [PATCH 17/44] style: addresses feedback --- .../utils/useBottomScrollListener.ts | 23 +++++++++++++++---- layouts/New/Search.tsx | 2 -- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/components/SearchPage/utils/useBottomScrollListener.ts b/components/SearchPage/utils/useBottomScrollListener.ts index d5e2eb2c0d10b..7809e59357f6c 100644 --- a/components/SearchPage/utils/useBottomScrollListener.ts +++ b/components/SearchPage/utils/useBottomScrollListener.ts @@ -2,8 +2,12 @@ import { useState, useEffect } from 'react'; type CallbackFunction = () => void; -export const useBottomScrollListener = (callback: CallbackFunction) => { +export const useBottomScrollListener = ( + callback: CallbackFunction, + debounceTime = 300 +) => { const [bottomReached, setBottomReached] = useState(false); + let timeoutId: NodeJS.Timeout | null = null; const handleScroll = () => { const scrollTop = document.documentElement.scrollTop; @@ -14,15 +18,26 @@ export const useBottomScrollListener = (callback: CallbackFunction) => { if (bottomOfWindow) { setBottomReached(true); - callback(); + if (timeoutId) { + clearTimeout(timeoutId); + } + timeoutId = setTimeout(() => { + callback(); + }, debounceTime); } else { setBottomReached(false); } }; useEffect(() => { - window.addEventListener('scroll', handleScroll); - return () => window.removeEventListener('scroll', handleScroll); + // Add the event listener with the passive option set to true + window.addEventListener('scroll', handleScroll, { passive: true }); + return () => { + window.removeEventListener('scroll', handleScroll); + if (timeoutId) { + clearTimeout(timeoutId); + } + }; }, []); return bottomReached; diff --git a/layouts/New/Search.tsx b/layouts/New/Search.tsx index 789a2df239b06..ee2324d76c1da 100644 --- a/layouts/New/Search.tsx +++ b/layouts/New/Search.tsx @@ -5,8 +5,6 @@ import type { FC, PropsWithChildren } from 'react'; import WithFooter from '@/components/withFooter'; import WithNavBar from '@/components/withNavBar'; -// import styles from './layouts.module.css'; - const SearchLayout: FC = ({ children }) => ( <> From 5aab3ccb4450bbfd4d9349af080f5cbf3679f58d Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Mon, 22 Jan 2024 22:07:23 +0100 Subject: [PATCH 18/44] style: addresses feedback --- components/SearchBox/components/SearchBox.tsx | 12 +---- .../SearchBox/components/index.module.css | 49 ++++++++++++++----- components/SearchPage/index.tsx | 5 +- next.constants.mjs | 19 ++++++- 4 files changed, 58 insertions(+), 27 deletions(-) diff --git a/components/SearchBox/components/SearchBox.tsx b/components/SearchBox/components/SearchBox.tsx index 7e62fef2019e1..57eef0a6255ac 100644 --- a/components/SearchBox/components/SearchBox.tsx +++ b/components/SearchBox/components/SearchBox.tsx @@ -13,6 +13,7 @@ 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 { DEFAULT_ORAMA_QUERY_PARAMS } from '@/next.constants.mjs'; import { EmptyState } from './EmptyState'; import { NoResults } from './NoResults'; @@ -60,16 +61,7 @@ export const SearchBox: FC = ({ onClose }) => { orama .search({ term, - limit: 8, - threshold: 0, - boost: { - pageSectionTitle: 4, - pageSectionContent: 2.5, - pageTitle: 1.5, - }, - facets: { - siteSection: {}, - }, + ...DEFAULT_ORAMA_QUERY_PARAMS, ...filterBySection(), }) .then(setSearchResults) diff --git a/components/SearchBox/components/index.module.css b/components/SearchBox/components/index.module.css index 0efc2a6e8cb1c..b344c0fc6b384 100644 --- a/components/SearchBox/components/index.module.css +++ b/components/SearchBox/components/index.module.css @@ -27,25 +27,37 @@ .searchBoxInnerPanel { @apply pt-12 - text-neutral-800 + text-neutral-800 dark:text-neutral-400 - md:p-2; + md:p-2; } .searchBoxMagnifyingGlassIcon { - @apply absolute top-[10px] hidden h-6 w-6 md:block; + @apply absolute + top-[10px] + hidden + h-6 + w-6 + md:block; } .searchBoxBackIconContainer { - @apply block md:hidden; + @apply block + md:hidden; } .searchBoxBackIcon { - @apply absolute top-[7px] block h-6 w-6 md:hidden; + @apply absolute + top-[7px] + block + h-6 + w-6 + md:hidden; } .searchBoxInputContainer { - @apply relative px-2; + @apply relative + px-2; } .searchBoxInput { @@ -64,7 +76,8 @@ } .fulltextResultsContainer { - @apply h-80 overflow-scroll; + @apply h-80 + overflow-scroll; } .fulltextSearchResult { @@ -84,7 +97,10 @@ } .fulltextSearchResultBreadcrumb { - @apply mt-1 text-xs capitalize text-neutral-800 + @apply mt-1 + text-xs + capitalize + text-neutral-800 dark:text-neutral-600; } @@ -115,11 +131,17 @@ } .fulltextSearchSectionSelected { - @apply rounded-b-none border-neutral-700 text-neutral-900 dark:border-neutral-700 dark:text-neutral-300; + @apply rounded-b-none + border-neutral-700 + text-neutral-900 + dark:border-neutral-700 + dark:text-neutral-300; } .fulltextSearchSectionCount { - @apply ml-1 text-neutral-500 dark:text-neutral-800; + @apply ml-1 + text-neutral-500 + dark:text-neutral-800; } .seeAllFulltextSearchResults { @@ -129,9 +151,9 @@ w-full text-center text-sm - text-neutral-700 + text-neutral-700 hover:underline - dark:text-neutral-600; + dark:text-neutral-600; } .poweredBy { @@ -147,7 +169,8 @@ } .poweredByLogo { - @apply ml-2 w-16; + @apply ml-2 + w-16; } .emptyStateContainer { diff --git a/components/SearchPage/index.tsx b/components/SearchPage/index.tsx index f42cf128d1d4b..01825247fd977 100644 --- a/components/SearchPage/index.tsx +++ b/components/SearchPage/index.tsx @@ -9,6 +9,7 @@ import type { SearchDoc } from '@/components/SearchBox/components/SearchBox'; import { orama, highlighter } from '@/components/SearchBox/lib/orama'; import { pathToBreadcrumbs } from '@/components/SearchBox/lib/utils'; import { useBottomScrollListener } from '@/components/SearchPage/utils/useBottomScrollListener'; +import { DEFAULT_ORAMA_QUERY_PARAMS } from '@/next.constants.mjs'; import styles from './index.module.css'; @@ -40,12 +41,10 @@ const SearchPage: FC = () => { const search = (resultsOffset = 0) => { orama .search({ + ...DEFAULT_ORAMA_QUERY_PARAMS, term: searchTerm || '', limit: 10, offset: resultsOffset, - facets: { - siteSection: {}, - }, ...filterBySection(), }) .then(results => { diff --git a/next.constants.mjs b/next.constants.mjs index 1d75f8261c4ee..89ea600cbe95a 100644 --- a/next.constants.mjs +++ b/next.constants.mjs @@ -122,7 +122,7 @@ export const BLOG_POSTS_PER_PAGE = ENABLE_WEBSITE_REDESIGN ? 6 : 20; */ export const THEME_STORAGE_KEY = 'theme'; -/*** +/** * This is a list of all external links that are used on website sitemap. * @see https://github.com/nodejs/nodejs.org/issues/5813 for more context */ @@ -135,3 +135,20 @@ export const EXTERNAL_LINKS_SITEMAP = [ 'https://trademark-list.openjsf.org/', 'https://www.linuxfoundation.org/cookies', ]; + +/** + * These are the default Orama Query Parameters that are used by the Website + * @see https://docs.oramasearch.com/open-source/usage/search/introduction + */ +export const DEFAULT_ORAMA_QUERY_PARAMS = { + limit: 8, + threshold: 0, + boost: { + pageSectionTitle: 4, + pageSectionContent: 2.5, + pageTitle: 1.5, + }, + facets: { + siteSection: {}, + }, +}; From e1fde645bc31ae294d03286d3af69f391d184deb Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Mon, 22 Jan 2024 22:12:45 +0100 Subject: [PATCH 19/44] style: addresses feedback --- components/SearchBox/components/SearchBox.tsx | 7 +++---- components/SearchBox/components/SearchResult.tsx | 2 +- components/SearchBox/components/SeeAll.tsx | 4 ---- components/SearchBox/{lib => }/utils.ts | 0 components/SearchPage/index.tsx | 2 +- 5 files changed, 5 insertions(+), 10 deletions(-) rename components/SearchBox/{lib => }/utils.ts (100%) diff --git a/components/SearchBox/components/SearchBox.tsx b/components/SearchBox/components/SearchBox.tsx index 57eef0a6255ac..ed0fd178d8a76 100644 --- a/components/SearchBox/components/SearchBox.tsx +++ b/components/SearchBox/components/SearchBox.tsx @@ -171,11 +171,10 @@ export const SearchBox: FC = ({ onClose }) => { )) ?? null} {searchResults?.count - ? searchResults?.count > 8 && ( + ? searchResults?.count > 8 && + searchTerm && ( ) : null} diff --git a/components/SearchBox/components/SearchResult.tsx b/components/SearchBox/components/SearchResult.tsx index 779e6e35a42b8..c04fdf16a4800 100644 --- a/components/SearchBox/components/SearchResult.tsx +++ b/components/SearchBox/components/SearchResult.tsx @@ -4,7 +4,7 @@ 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 { pathToBreadcrumbs } from '@/components/SearchBox/utils'; import styles from './index.module.css'; diff --git a/components/SearchBox/components/SeeAll.tsx b/components/SearchBox/components/SeeAll.tsx index ec904be04d7b3..baabfd4a19102 100644 --- a/components/SearchBox/components/SeeAll.tsx +++ b/components/SearchBox/components/SeeAll.tsx @@ -19,10 +19,6 @@ export const SeeAll: FC = props => { 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}§ion=${sanitizedFacetName}`; diff --git a/components/SearchBox/lib/utils.ts b/components/SearchBox/utils.ts similarity index 100% rename from components/SearchBox/lib/utils.ts rename to components/SearchBox/utils.ts diff --git a/components/SearchPage/index.tsx b/components/SearchPage/index.tsx index 01825247fd977..83fc64ae830d4 100644 --- a/components/SearchPage/index.tsx +++ b/components/SearchPage/index.tsx @@ -7,7 +7,7 @@ import { useEffect, useState, type FC } from 'react'; import type { SearchDoc } from '@/components/SearchBox/components/SearchBox'; import { orama, highlighter } from '@/components/SearchBox/lib/orama'; -import { pathToBreadcrumbs } from '@/components/SearchBox/lib/utils'; +import { pathToBreadcrumbs } from '@/components/SearchBox/utils'; import { useBottomScrollListener } from '@/components/SearchPage/utils/useBottomScrollListener'; import { DEFAULT_ORAMA_QUERY_PARAMS } from '@/next.constants.mjs'; From ff2086f5905bfffd0bb81d01e69acdb52569774e Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Mon, 22 Jan 2024 22:21:36 +0100 Subject: [PATCH 20/44] refactor: moves components and hooks into the correct folder structure --- components/SearchBox/components/SearchBox.tsx | 4 ++-- components/SearchBox/components/SearchResult.tsx | 2 +- components/SearchBox/index.tsx | 2 +- components/SearchPage/index.tsx | 4 ++-- .../react-client}/useBottomScrollListener.ts | 0 .../lib => hooks/react-client}/useClickOutside.ts | 0 .../lib => hooks/react-client}/useKeyboardCommands.ts | 0 components/SearchBox/lib/orama.ts => next.orama.mjs | 10 +++++----- 8 files changed, 11 insertions(+), 11 deletions(-) rename {components/SearchPage/utils => hooks/react-client}/useBottomScrollListener.ts (100%) rename {components/SearchBox/lib => hooks/react-client}/useClickOutside.ts (100%) rename {components/SearchBox/lib => hooks/react-client}/useKeyboardCommands.ts (100%) rename components/SearchBox/lib/orama.ts => next.orama.mjs (64%) diff --git a/components/SearchBox/components/SearchBox.tsx b/components/SearchBox/components/SearchBox.tsx index ed0fd178d8a76..67cf54c7bf7df 100644 --- a/components/SearchBox/components/SearchBox.tsx +++ b/components/SearchBox/components/SearchBox.tsx @@ -11,9 +11,9 @@ 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 { useClickOutside } from '@/hooks/react-client/useClickOutside'; import { DEFAULT_ORAMA_QUERY_PARAMS } from '@/next.constants.mjs'; +import { orama, getInitialFacets } from '@/next.orama.mjs'; import { EmptyState } from './EmptyState'; import { NoResults } from './NoResults'; diff --git a/components/SearchBox/components/SearchResult.tsx b/components/SearchBox/components/SearchResult.tsx index c04fdf16a4800..4ce1b325657d6 100644 --- a/components/SearchBox/components/SearchResult.tsx +++ b/components/SearchBox/components/SearchResult.tsx @@ -3,8 +3,8 @@ 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/utils'; +import { highlighter } from '@/next.orama.mjs'; import styles from './index.module.css'; diff --git a/components/SearchBox/index.tsx b/components/SearchBox/index.tsx index 4076907a577b0..f1b84de948ca7 100644 --- a/components/SearchBox/index.tsx +++ b/components/SearchBox/index.tsx @@ -5,7 +5,7 @@ import { useTranslations } from 'next-intl'; import { useState, type FC } from 'react'; import { SearchBox } from '@/components/SearchBox/components/SearchBox'; -import { useKeyboardCommands } from '@/components/SearchBox/lib/useKeyboardCommands'; +import { useKeyboardCommands } from '@/hooks/react-client/useKeyboardCommands'; import styles from './index.module.css'; diff --git a/components/SearchPage/index.tsx b/components/SearchPage/index.tsx index 83fc64ae830d4..0d65a19a3a45b 100644 --- a/components/SearchPage/index.tsx +++ b/components/SearchPage/index.tsx @@ -6,10 +6,10 @@ import { useSearchParams } from 'next/navigation'; import { useEffect, useState, type FC } from 'react'; import type { SearchDoc } from '@/components/SearchBox/components/SearchBox'; -import { orama, highlighter } from '@/components/SearchBox/lib/orama'; import { pathToBreadcrumbs } from '@/components/SearchBox/utils'; -import { useBottomScrollListener } from '@/components/SearchPage/utils/useBottomScrollListener'; +import { useBottomScrollListener } from '@/hooks/react-client/useBottomScrollListener'; import { DEFAULT_ORAMA_QUERY_PARAMS } from '@/next.constants.mjs'; +import { orama, highlighter } from '@/next.orama.mjs'; import styles from './index.module.css'; diff --git a/components/SearchPage/utils/useBottomScrollListener.ts b/hooks/react-client/useBottomScrollListener.ts similarity index 100% rename from components/SearchPage/utils/useBottomScrollListener.ts rename to hooks/react-client/useBottomScrollListener.ts diff --git a/components/SearchBox/lib/useClickOutside.ts b/hooks/react-client/useClickOutside.ts similarity index 100% rename from components/SearchBox/lib/useClickOutside.ts rename to hooks/react-client/useClickOutside.ts diff --git a/components/SearchBox/lib/useKeyboardCommands.ts b/hooks/react-client/useKeyboardCommands.ts similarity index 100% rename from components/SearchBox/lib/useKeyboardCommands.ts rename to hooks/react-client/useKeyboardCommands.ts diff --git a/components/SearchBox/lib/orama.ts b/next.orama.mjs similarity index 64% rename from components/SearchBox/lib/orama.ts rename to next.orama.mjs index 0997cc9f94fea..bab4726fe5ad0 100644 --- a/components/SearchBox/lib/orama.ts +++ b/next.orama.mjs @@ -1,9 +1,11 @@ import { Highlight } from '@orama/highlight'; import { OramaClient } from '@oramacloud/client'; +import { DEFAULT_ORAMA_QUERY_PARAMS } from './next.constants.mjs'; + export const orama = new OramaClient({ - endpoint: process.env.NEXT_PUBLIC_ORAMA_ENDPOINT!, - api_key: process.env.NEXT_PUBLIC_ORAMA_API_KEY!, + endpoint: process.env.NEXT_PUBLIC_ORAMA_ENDPOINT, + api_key: process.env.NEXT_PUBLIC_ORAMA_API_KEY, }); orama.startHeartBeat({ frequency: 3500 }); @@ -16,8 +18,6 @@ export const highlighter = new Highlight({ export async function getInitialFacets() { return await orama.search({ term: '', - facets: { - siteSection: {}, - }, + ...DEFAULT_ORAMA_QUERY_PARAMS, }); } From 2a7052d3cc76e69e49cbcff7451b21492919c8db Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Mon, 22 Jan 2024 22:24:53 +0100 Subject: [PATCH 21/44] refactor: moves components and hooks into the correct folder structure --- components/SearchBox/components/SearchBox.tsx | 2 +- components/SearchBox/index.tsx | 2 +- components/SearchPage/index.tsx | 2 +- hooks/react-client/index.ts | 3 +++ hooks/react-client/useBottomScrollListener.ts | 4 +++- hooks/react-client/useClickOutside.ts | 4 +++- hooks/react-client/useKeyboardCommands.ts | 4 +++- 7 files changed, 15 insertions(+), 6 deletions(-) diff --git a/components/SearchBox/components/SearchBox.tsx b/components/SearchBox/components/SearchBox.tsx index 67cf54c7bf7df..ff84bbea6e95c 100644 --- a/components/SearchBox/components/SearchBox.tsx +++ b/components/SearchBox/components/SearchBox.tsx @@ -11,7 +11,7 @@ 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 { useClickOutside } from '@/hooks/react-client/useClickOutside'; +import { useClickOutside } from '@/hooks/react-client'; import { DEFAULT_ORAMA_QUERY_PARAMS } from '@/next.constants.mjs'; import { orama, getInitialFacets } from '@/next.orama.mjs'; diff --git a/components/SearchBox/index.tsx b/components/SearchBox/index.tsx index f1b84de948ca7..6302a983f4fe5 100644 --- a/components/SearchBox/index.tsx +++ b/components/SearchBox/index.tsx @@ -5,7 +5,7 @@ import { useTranslations } from 'next-intl'; import { useState, type FC } from 'react'; import { SearchBox } from '@/components/SearchBox/components/SearchBox'; -import { useKeyboardCommands } from '@/hooks/react-client/useKeyboardCommands'; +import { useKeyboardCommands } from '@/hooks/react-client'; import styles from './index.module.css'; diff --git a/components/SearchPage/index.tsx b/components/SearchPage/index.tsx index 0d65a19a3a45b..152f6f8179eca 100644 --- a/components/SearchPage/index.tsx +++ b/components/SearchPage/index.tsx @@ -7,7 +7,7 @@ import { useEffect, useState, type FC } from 'react'; import type { SearchDoc } from '@/components/SearchBox/components/SearchBox'; import { pathToBreadcrumbs } from '@/components/SearchBox/utils'; -import { useBottomScrollListener } from '@/hooks/react-client/useBottomScrollListener'; +import { useBottomScrollListener } from '@/hooks/react-client'; import { DEFAULT_ORAMA_QUERY_PARAMS } from '@/next.constants.mjs'; import { orama, highlighter } from '@/next.orama.mjs'; diff --git a/hooks/react-client/index.ts b/hooks/react-client/index.ts index a6cf9825ba2c2..a4423177fef31 100644 --- a/hooks/react-client/index.ts +++ b/hooks/react-client/index.ts @@ -3,3 +3,6 @@ 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'; diff --git a/hooks/react-client/useBottomScrollListener.ts b/hooks/react-client/useBottomScrollListener.ts index 7809e59357f6c..4b1bab73e4ce0 100644 --- a/hooks/react-client/useBottomScrollListener.ts +++ b/hooks/react-client/useBottomScrollListener.ts @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; type CallbackFunction = () => void; -export const useBottomScrollListener = ( +const useBottomScrollListener = ( callback: CallbackFunction, debounceTime = 300 ) => { @@ -42,3 +42,5 @@ export const useBottomScrollListener = ( return bottomReached; }; + +export default useBottomScrollListener; diff --git a/hooks/react-client/useClickOutside.ts b/hooks/react-client/useClickOutside.ts index 7b15e30b7d983..98cda0c6e3c87 100644 --- a/hooks/react-client/useClickOutside.ts +++ b/hooks/react-client/useClickOutside.ts @@ -1,7 +1,7 @@ import type { RefObject } from 'react'; import { useEffect } from 'react'; -export const useClickOutside = ( +const useClickOutside = ( ref: RefObject, fn: () => void ) => { @@ -16,3 +16,5 @@ export const useClickOutside = ( return () => document.removeEventListener('mousedown', handleClickOutside); }, [ref, fn]); }; + +export default useClickOutside; diff --git a/hooks/react-client/useKeyboardCommands.ts b/hooks/react-client/useKeyboardCommands.ts index 13502f5596c8b..6ec7e3fa5c46e 100644 --- a/hooks/react-client/useKeyboardCommands.ts +++ b/hooks/react-client/useKeyboardCommands.ts @@ -4,7 +4,7 @@ type KeyboardCommand = 'cmd-k' | 'escape' | 'down' | 'up' | 'enter'; type KeyboardCommandCallback = (key: KeyboardCommand) => void; -export const useKeyboardCommands = (fn: KeyboardCommandCallback) => { +const useKeyboardCommands = (fn: KeyboardCommandCallback) => { useEffect(() => { document.addEventListener('keydown', event => { // Detect ⌘ + k on Mac, Ctrl + k on Windows @@ -35,3 +35,5 @@ export const useKeyboardCommands = (fn: KeyboardCommandCallback) => { }; }, []); }; + +export default useKeyboardCommands; From 97f5de4fdde51bf2d79c9688ac7bd142d901395b Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Mon, 22 Jan 2024 22:26:09 +0100 Subject: [PATCH 22/44] refactor: moves components and hooks into the correct folder structure --- scripts/{orama => orama-search}/get-documents.mjs | 0 .../{orama => orama-search}/sync-orama-cloud.mjs | 0 scripts/orama/create.mjs | 14 -------------- 3 files changed, 14 deletions(-) rename scripts/{orama => orama-search}/get-documents.mjs (100%) rename scripts/{orama => orama-search}/sync-orama-cloud.mjs (100%) delete mode 100644 scripts/orama/create.mjs diff --git a/scripts/orama/get-documents.mjs b/scripts/orama-search/get-documents.mjs similarity index 100% rename from scripts/orama/get-documents.mjs rename to scripts/orama-search/get-documents.mjs diff --git a/scripts/orama/sync-orama-cloud.mjs b/scripts/orama-search/sync-orama-cloud.mjs similarity index 100% rename from scripts/orama/sync-orama-cloud.mjs rename to scripts/orama-search/sync-orama-cloud.mjs diff --git a/scripts/orama/create.mjs b/scripts/orama/create.mjs deleted file mode 100644 index 53814ee0283d9..0000000000000 --- a/scripts/orama/create.mjs +++ /dev/null @@ -1,14 +0,0 @@ -import { create, insertMultiple } from '@orama/orama'; - -import { siteContent } from './get-documents.mjs'; - -export const orama = await create({ - schema: { - siteSection: 'enum', - pageTitle: 'string', - pageSectionTitle: 'string', - pageSectionContent: 'string', - }, -}); - -await insertMultiple(orama, siteContent); From 5b773b856c51d3dffc4feae7ad42177a3ac0bfec Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Mon, 22 Jan 2024 22:34:32 +0100 Subject: [PATCH 23/44] refactor: moves components and hooks into the correct folder structure --- .../Search/States/WithAllResults.tsx} | 6 ++-- .../Search/States/WithEmptyState.tsx} | 2 +- .../Search/States/WithError.tsx} | 2 +- .../Search/States/WithNoResults.tsx} | 2 +- .../Search/States/WithPoweredBy.tsx} | 2 +- .../Search/States/WithSearchBox.tsx} | 28 +++++++++---------- .../Search/States/WithSearchResult.tsx} | 6 ++-- .../Search/States}/index.module.css | 0 .../Search}/index.module.css | 0 .../{SearchBox => Common/Search}/index.tsx | 4 +-- .../{SearchBox => Common/Search}/utils.ts | 0 components/Containers/NavBar/index.tsx | 2 +- components/SearchPage/index.tsx | 4 +-- 13 files changed, 29 insertions(+), 29 deletions(-) rename components/{SearchBox/components/SeeAll.tsx => Common/Search/States/WithAllResults.tsx} (88%) rename components/{SearchBox/components/EmptyState.tsx => Common/Search/States/WithEmptyState.tsx} (86%) rename components/{SearchBox/components/SearchError.tsx => Common/Search/States/WithError.tsx} (87%) rename components/{SearchBox/components/NoResults.tsx => Common/Search/States/WithNoResults.tsx} (85%) rename components/{SearchBox/components/PoweredBy.tsx => Common/Search/States/WithPoweredBy.tsx} (94%) rename components/{SearchBox/components/SearchBox.tsx => Common/Search/States/WithSearchBox.tsx} (84%) rename components/{SearchBox/components/SearchResult.tsx => Common/Search/States/WithSearchResult.tsx} (84%) rename components/{SearchBox/components => Common/Search/States}/index.module.css (100%) rename components/{SearchBox => Common/Search}/index.module.css (100%) rename components/{SearchBox => Common/Search}/index.tsx (87%) rename components/{SearchBox => Common/Search}/utils.ts (100%) diff --git a/components/SearchBox/components/SeeAll.tsx b/components/Common/Search/States/WithAllResults.tsx similarity index 88% rename from components/SearchBox/components/SeeAll.tsx rename to components/Common/Search/States/WithAllResults.tsx index baabfd4a19102..a90f769fd7573 100644 --- a/components/SearchBox/components/SeeAll.tsx +++ b/components/Common/Search/States/WithAllResults.tsx @@ -3,10 +3,10 @@ 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'; +import type { SearchDoc } from '@/components/Common/Search/States/SearchBox'; + type SearchResults = Results; type SeeAllProps = { @@ -15,7 +15,7 @@ type SeeAllProps = { selectedFacetName: string; }; -export const SeeAll: FC = props => { +export const WithAllResults: FC = props => { const t = useTranslations(); const resultsCount = props.searchResults?.count?.toLocaleString('en') ?? 0; diff --git a/components/SearchBox/components/EmptyState.tsx b/components/Common/Search/States/WithEmptyState.tsx similarity index 86% rename from components/SearchBox/components/EmptyState.tsx rename to components/Common/Search/States/WithEmptyState.tsx index 29ba8956cc07a..f554a0bec206b 100644 --- a/components/SearchBox/components/EmptyState.tsx +++ b/components/Common/Search/States/WithEmptyState.tsx @@ -3,7 +3,7 @@ import type { FC } from 'react'; import styles from './index.module.css'; -export const EmptyState: FC = () => { +export const WithEmptyState: FC = () => { const t = useTranslations(); return ( diff --git a/components/SearchBox/components/SearchError.tsx b/components/Common/Search/States/WithError.tsx similarity index 87% rename from components/SearchBox/components/SearchError.tsx rename to components/Common/Search/States/WithError.tsx index ec3165a22ef71..33eecbabd147d 100644 --- a/components/SearchBox/components/SearchError.tsx +++ b/components/Common/Search/States/WithError.tsx @@ -3,7 +3,7 @@ import type { FC } from 'react'; import styles from './index.module.css'; -export const SearchError: FC = () => { +export const WithError: FC = () => { const t = useTranslations(); return ( diff --git a/components/SearchBox/components/NoResults.tsx b/components/Common/Search/States/WithNoResults.tsx similarity index 85% rename from components/SearchBox/components/NoResults.tsx rename to components/Common/Search/States/WithNoResults.tsx index 15a4e42a5d0fd..5b55c60469c4b 100644 --- a/components/SearchBox/components/NoResults.tsx +++ b/components/Common/Search/States/WithNoResults.tsx @@ -5,7 +5,7 @@ import styles from './index.module.css'; type NoResultsProps = { searchTerm: string }; -export const NoResults: FC = props => { +export const WithNoResults: FC = props => { const t = useTranslations(); return ( diff --git a/components/SearchBox/components/PoweredBy.tsx b/components/Common/Search/States/WithPoweredBy.tsx similarity index 94% rename from components/SearchBox/components/PoweredBy.tsx rename to components/Common/Search/States/WithPoweredBy.tsx index 8c1da7e26d330..7977ee99759da 100644 --- a/components/SearchBox/components/PoweredBy.tsx +++ b/components/Common/Search/States/WithPoweredBy.tsx @@ -3,7 +3,7 @@ import { useTranslations } from 'next-intl'; import styles from './index.module.css'; -export const PoweredBy = () => { +export const WithPoweredBy = () => { const t = useTranslations(); return ( diff --git a/components/SearchBox/components/SearchBox.tsx b/components/Common/Search/States/WithSearchBox.tsx similarity index 84% rename from components/SearchBox/components/SearchBox.tsx rename to components/Common/Search/States/WithSearchBox.tsx index ff84bbea6e95c..917d4639635e2 100644 --- a/components/SearchBox/components/SearchBox.tsx +++ b/components/Common/Search/States/WithSearchBox.tsx @@ -7,17 +7,17 @@ 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 styles from '@/components/Common/Search/States/index.module.css'; +import { WithAllResults } from '@/components/Common/Search/States/WithAllResults'; +import { WithEmptyState } from '@/components/Common/Search/States/WithEmptyState'; +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 { useClickOutside } from '@/hooks/react-client'; import { DEFAULT_ORAMA_QUERY_PARAMS } from '@/next.constants.mjs'; import { orama, getInitialFacets } from '@/next.orama.mjs'; -import { EmptyState } from './EmptyState'; -import { NoResults } from './NoResults'; - export type SearchDoc = { id: string; path: string; @@ -31,7 +31,7 @@ type SearchResults = Nullable>; type SearchBoxProps = { onClose: () => void }; -export const SearchBox: FC = ({ onClose }) => { +export const WithSearchBox: FC = ({ onClose }) => { const [searchTerm, setSearchTerm] = useState(''); const [searchResults, setSearchResults] = useState(null); const [selectedFacet, setSelectedFacet] = useState(0); @@ -152,34 +152,34 @@ export const SearchBox: FC = ({ onClose }) => {
- {searchError ? <> : null} + {searchError ? : null} {(searchTerm ? ( searchResults?.count ? ( searchResults?.hits.map(hit => ( - )) ) : ( - + ) ) : ( - + )) ?? null} {searchResults?.count ? searchResults?.count > 8 && searchTerm && ( - ) : null}
- +
diff --git a/components/SearchBox/components/SearchResult.tsx b/components/Common/Search/States/WithSearchResult.tsx similarity index 84% rename from components/SearchBox/components/SearchResult.tsx rename to components/Common/Search/States/WithSearchResult.tsx index 4ce1b325657d6..57cd8e77bbb8c 100644 --- a/components/SearchBox/components/SearchResult.tsx +++ b/components/Common/Search/States/WithSearchResult.tsx @@ -2,8 +2,8 @@ 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 { pathToBreadcrumbs } from '@/components/SearchBox/utils'; +import type { SearchDoc } from '@/components/Common/Search/States/WithSearchBox'; +import { pathToBreadcrumbs } from '@/components/Common/Search/utils'; import { highlighter } from '@/next.orama.mjs'; import styles from './index.module.css'; @@ -13,7 +13,7 @@ type SearchResultProps = { searchTerm: string; }; -export const SearchResult: FC = props => { +export const WithSearchResult: FC = 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}`; diff --git a/components/SearchBox/components/index.module.css b/components/Common/Search/States/index.module.css similarity index 100% rename from components/SearchBox/components/index.module.css rename to components/Common/Search/States/index.module.css diff --git a/components/SearchBox/index.module.css b/components/Common/Search/index.module.css similarity index 100% rename from components/SearchBox/index.module.css rename to components/Common/Search/index.module.css diff --git a/components/SearchBox/index.tsx b/components/Common/Search/index.tsx similarity index 87% rename from components/SearchBox/index.tsx rename to components/Common/Search/index.tsx index 6302a983f4fe5..bf7bc52171f9a 100644 --- a/components/SearchBox/index.tsx +++ b/components/Common/Search/index.tsx @@ -4,7 +4,7 @@ import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; import { useTranslations } from 'next-intl'; import { useState, type FC } from 'react'; -import { SearchBox } from '@/components/SearchBox/components/SearchBox'; +import { WithSearchBox } from '@/components/Common/Search/States/WithSearchBox'; import { useKeyboardCommands } from '@/hooks/react-client'; import styles from './index.module.css'; @@ -43,7 +43,7 @@ export const SearchButton: FC = () => { {t('components.search.searchBox.placeholder')} - {isOpen ? : null} + {isOpen ? : null} ); }; diff --git a/components/SearchBox/utils.ts b/components/Common/Search/utils.ts similarity index 100% rename from components/SearchBox/utils.ts rename to components/Common/Search/utils.ts diff --git a/components/Containers/NavBar/index.tsx b/components/Containers/NavBar/index.tsx index 8480b623efc36..64ec8e21b62e5 100644 --- a/components/Containers/NavBar/index.tsx +++ b/components/Containers/NavBar/index.tsx @@ -7,13 +7,13 @@ import { useState } from 'react'; import type { FC, ComponentProps } 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 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'; diff --git a/components/SearchPage/index.tsx b/components/SearchPage/index.tsx index 152f6f8179eca..6f24d1952b1fc 100644 --- a/components/SearchPage/index.tsx +++ b/components/SearchPage/index.tsx @@ -5,8 +5,8 @@ import Link from 'next/link'; import { useSearchParams } from 'next/navigation'; import { useEffect, useState, type FC } from 'react'; -import type { SearchDoc } from '@/components/SearchBox/components/SearchBox'; -import { pathToBreadcrumbs } from '@/components/SearchBox/utils'; +import type { SearchDoc } from '@/components/Common/Search/States/WithSearchBox'; +import { pathToBreadcrumbs } from '@/components/Common/Search/utils'; import { useBottomScrollListener } from '@/hooks/react-client'; import { DEFAULT_ORAMA_QUERY_PARAMS } from '@/next.constants.mjs'; import { orama, highlighter } from '@/next.orama.mjs'; From 765033cc76c9e26f25b1f5c8912e30d892cc4bc7 Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Mon, 22 Jan 2024 22:44:05 +0100 Subject: [PATCH 24/44] refactor: moves components and hooks into the correct folder structure --- .../{ => MDX}/SearchPage/index.module.css | 0 components/{ => MDX}/SearchPage/index.tsx | 0 hooks/react-client/useClickOutside.ts | 4 +-- hooks/react-client/useKeyboardCommands.ts | 27 +++++++++---------- next.mdx.use.mjs | 2 +- 5 files changed, 16 insertions(+), 17 deletions(-) rename components/{ => MDX}/SearchPage/index.module.css (100%) rename components/{ => MDX}/SearchPage/index.tsx (100%) diff --git a/components/SearchPage/index.module.css b/components/MDX/SearchPage/index.module.css similarity index 100% rename from components/SearchPage/index.module.css rename to components/MDX/SearchPage/index.module.css diff --git a/components/SearchPage/index.tsx b/components/MDX/SearchPage/index.tsx similarity index 100% rename from components/SearchPage/index.tsx rename to components/MDX/SearchPage/index.tsx diff --git a/hooks/react-client/useClickOutside.ts b/hooks/react-client/useClickOutside.ts index 98cda0c6e3c87..7211eba5aaecd 100644 --- a/hooks/react-client/useClickOutside.ts +++ b/hooks/react-client/useClickOutside.ts @@ -12,8 +12,8 @@ const useClickOutside = ( fn(); } }; - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); + document.addEventListener('click', handleClickOutside); + return () => document.removeEventListener('click', handleClickOutside); }, [ref, fn]); }; diff --git a/hooks/react-client/useKeyboardCommands.ts b/hooks/react-client/useKeyboardCommands.ts index 6ec7e3fa5c46e..b2e3fabb7524d 100644 --- a/hooks/react-client/useKeyboardCommands.ts +++ b/hooks/react-client/useKeyboardCommands.ts @@ -13,20 +13,19 @@ const useKeyboardCommands = (fn: KeyboardCommandCallback) => { fn('cmd-k'); } - if (event.key === 'Escape') { - fn('escape'); - } - - if (event.key === 'Enter') { - fn('enter'); - } - - if (event.key === 'ArrowDown') { - fn('down'); - } - - if (event.key === 'ArrowUp') { - fn('up'); + switch (event.key) { + case 'Escape': + fn('escape'); + break; + case 'Enter': + fn('enter'); + break; + case 'ArrowDown': + fn('down'); + break; + case 'ArrowUp': + fn('up'); + break; } }); diff --git a/next.mdx.use.mjs b/next.mdx.use.mjs index 879596ec427b2..c9ebb4f0a48ac 100644 --- a/next.mdx.use.mjs +++ b/next.mdx.use.mjs @@ -9,7 +9,7 @@ import HomeDownloadButton from './components/Home/HomeDownloadButton'; import Link from './components/Link'; import MDXCodeBox from './components/MDX/CodeBox'; import MDXCodeTabs from './components/MDX/CodeTabs'; -import SearchPage from './components/SearchPage'; +import SearchPage from './components/MDX/SearchPage'; import WithBadge from './components/withBadge'; import WithBanner from './components/withBanner'; import WithNodeRelease from './components/withNodeRelease'; From f22681db58c0d5d6bccebbafeb6401a946004f63 Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Mon, 22 Jan 2024 22:46:44 +0100 Subject: [PATCH 25/44] refactor: moves components and hooks into the correct folder structure --- components/Common/Search/States/WithSearchBox.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/Common/Search/States/WithSearchBox.tsx b/components/Common/Search/States/WithSearchBox.tsx index 917d4639635e2..58f6b76645192 100644 --- a/components/Common/Search/States/WithSearchBox.tsx +++ b/components/Common/Search/States/WithSearchBox.tsx @@ -1,3 +1,5 @@ +'use client'; + import { MagnifyingGlassIcon, ChevronLeftIcon, From da148e2814ee18e2dc0ca403a84918389a6df1c1 Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Mon, 22 Jan 2024 22:54:32 +0100 Subject: [PATCH 26/44] style: addresses feedback --- components/Common/Search/States/WithAllResults.tsx | 4 ++-- components/Common/Search/index.tsx | 10 ++-------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/components/Common/Search/States/WithAllResults.tsx b/components/Common/Search/States/WithAllResults.tsx index a90f769fd7573..4562e8d9a729a 100644 --- a/components/Common/Search/States/WithAllResults.tsx +++ b/components/Common/Search/States/WithAllResults.tsx @@ -3,9 +3,9 @@ import NextLink from 'next/link'; import { useTranslations } from 'next-intl'; import type { FC } from 'react'; -import styles from './index.module.css'; +import type { SearchDoc } from '@/components/Common/Search/States/WithSearchBox'; -import type { SearchDoc } from '@/components/Common/Search/States/SearchBox'; +import styles from './index.module.css'; type SearchResults = Results; diff --git a/components/Common/Search/index.tsx b/components/Common/Search/index.tsx index bf7bc52171f9a..b82628e3ac629 100644 --- a/components/Common/Search/index.tsx +++ b/components/Common/Search/index.tsx @@ -12,6 +12,8 @@ import styles from './index.module.css'; export const SearchButton: FC = () => { const [isOpen, setIsOpen] = useState(false); const t = useTranslations(); + const openSearchBox = () => setIsOpen(true); + const closeSearchBox = () => setIsOpen(false); useKeyboardCommands(cmd => { switch (cmd) { @@ -25,14 +27,6 @@ export const SearchButton: FC = () => { } }); - const openSearchBox = () => { - setIsOpen(true); - }; - - const closeSearchBox = () => { - setIsOpen(false); - }; - return ( <> ))} @@ -176,7 +174,9 @@ export const WithSearchBox: FC = ({ onClose }) => { ? searchResults?.count > 8 && searchTerm && ( ) : null} diff --git a/components/Common/Search/States/WithSearchResult.tsx b/components/Common/Search/States/WithSearchResult.tsx index 57cd8e77bbb8c..e2769bd4f4b16 100644 --- a/components/Common/Search/States/WithSearchResult.tsx +++ b/components/Common/Search/States/WithSearchResult.tsx @@ -1,9 +1,9 @@ import type { Result } from '@orama/orama'; -import NextLink from 'next/link'; import type { FC } from 'react'; import type { SearchDoc } from '@/components/Common/Search/States/WithSearchBox'; import { pathToBreadcrumbs } from '@/components/Common/Search/utils'; +import Link from '@/components/Link'; import { highlighter } from '@/next.orama.mjs'; import styles from './index.module.css'; @@ -15,16 +15,14 @@ type SearchResultProps = { export const WithSearchResult: FC = props => { const isAPIResult = props.hit.document.siteSection.toLowerCase() === 'api'; - const basePath = isAPIResult ? 'https://nodejs.org/docs/latest' : '/en'; + const basePath = isAPIResult ? 'https://nodejs.org' : ''; const path = `${basePath}/${props.hit.document.path}`; return ( -
= props => { {' > '} {props.hit.document.pageTitle}
-
+ ); }; diff --git a/components/Common/Search/index.module.css b/components/Common/Search/index.module.css index 9717e606051e7..02be508deaae5 100644 --- a/components/Common/Search/index.module.css +++ b/components/Common/Search/index.module.css @@ -1,23 +1,23 @@ .searchButton { @apply relative - w-52 - rounded-md - bg-neutral-100 - py-2 - pl-9 - pr-4 - text-left - text-sm - text-neutral-700 - transition-colors - duration-200 - ease-in-out - hover:bg-neutral-200 - hover:text-neutral-800 - dark:bg-neutral-900 - dark:text-neutral-600 - dark:hover:bg-neutral-800 - dark:hover:text-neutral-500; + w-52 + rounded-md + bg-neutral-100 + py-2 + pl-9 + pr-4 + text-left + text-sm + text-neutral-700 + transition-colors + duration-200 + ease-in-out + hover:bg-neutral-200 + hover:text-neutral-800 + dark:bg-neutral-900 + dark:text-neutral-600 + dark:hover:bg-neutral-800 + dark:hover:text-neutral-500; } .magnifyingGlassIcon { diff --git a/package-lock.json b/package-lock.json index 2b6cfef59cc1e..72a7779d5fb54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,7 +60,7 @@ "vfile-matter": "~5.0.0" }, "devDependencies": { - "@orama/orama": "^2.0.1", + "@orama/orama": "~2.0.1", "@storybook/addon-controls": "~7.6.8", "@storybook/addon-interactions": "~7.6.8", "@storybook/addon-themes": "~7.6.8", diff --git a/package.json b/package.json index 4552dca157267..079f591dffe96 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "vfile-matter": "~5.0.0" }, "devDependencies": { - "@orama/orama": "^2.0.1", + "@orama/orama": "~2.0.1", "@storybook/addon-controls": "~7.6.8", "@storybook/addon-interactions": "~7.6.8", "@storybook/addon-themes": "~7.6.8", diff --git a/scripts/orama-search/get-documents.mjs b/scripts/orama-search/get-documents.mjs index 4774fd99161b0..7b94778fcb5fb 100644 --- a/scripts/orama-search/get-documents.mjs +++ b/scripts/orama-search/get-documents.mjs @@ -34,19 +34,13 @@ const splitIntoSections = markdownContent => { })); }; -const getPageTitle = data => { - const { title } = data; - - if (title) { - return title; - } - - const { pathname } = data; - const parts = pathname.split('/'); - const lastPart = parts[parts.length - 1].replace(/\.html$/, ''); - - return lastPart.replace(/-/g, ' '); -}; +const getPageTitle = data => + data.title || + data.pathname + .split('/') + .pop() + .replace(/\.html$/, '') + .replace(/-/g, ' '); export const siteContent = [...pageData, ...apiData] .map(data => { diff --git a/scripts/orama-search/sync-orama-cloud.mjs b/scripts/orama-search/sync-orama-cloud.mjs index 230f07ca99627..fb13a6741c20e 100644 --- a/scripts/orama-search/sync-orama-cloud.mjs +++ b/scripts/orama-search/sync-orama-cloud.mjs @@ -21,50 +21,37 @@ const runUpdate = async () => { batches.push(siteContent.slice(i, i + batchSize)); } - console.log( - `Inserting ${batches.length} batches of ${batchSize} documents each.` - ); await Promise.all(batches.map(insertBatch)); - console.log(`Done inserting batches. ${siteContent.length} documents total.`); }; // We call the "notify" API to upsert the documents in the index. // Orama will keep a queue of all the documents we send, and will process them once we call the "deploy" API. // Full docs on the "notify" API: https://docs.oramasearch.com/cloud/data-sources/custom-integrations/webhooks#updating-removing-inserting-elements-in-a-live-index -const insertBatch = async batch => { +const insertBatch = async batch => await fetch(`${ORAMA_API_BASE_URL}/notify`, { method: 'POST', headers: oramaHeaders, - body: JSON.stringify({ - upsert: batch, - }), + body: JSON.stringify({ upsert: batch }), }); -}; // We call the "deploy" API to trigger a deployment of the index, which will process all the documents in the queue. // Full docs on the "deploy" API: https://docs.oramasearch.com/cloud/data-sources/custom-integrations/webhooks#deploying-the-index -const triggerDeployment = async () => { - console.log('Triggering deployment'); +const triggerDeployment = async () => await fetch(`${ORAMA_API_BASE_URL}/deploy`, { method: 'POST', headers: oramaHeaders, }); - console.log('Done triggering deployment'); -}; // We call the "snapshot" API to empty the index before inserting the new documents. // The "snapshot" API is tipically used to replace the entire index with a fresh set of documents, but we use it here to empty the index. // This operation gets queued, so the live index will still be available until we call the "deploy" API and redeploy the index. // Full docs on the "snapshot" API: https://docs.oramasearch.com/cloud/data-sources/custom-integrations/webhooks#inserting-a-snapshot -const emptyOramaIndex = async () => { - console.log('Emptying index'); +const emptyOramaIndex = async () => await fetch(`${ORAMA_API_BASE_URL}/snapshot`, { method: 'POST', headers: oramaHeaders, body: JSON.stringify([]), }); - console.log('Done emptying index'); -}; // Now we proceed to call the APIs in order: // 1. Empty the index From b1a012a89ed8841fca2bd4b84290b1079c906e5e Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Tue, 23 Jan 2024 00:35:29 +0100 Subject: [PATCH 29/44] style: addresses feedback --- .../Common/Search/States/WithSearchBox.tsx | 21 ++++++++----------- hooks/react-client/useBottomScrollListener.ts | 20 ++++++------------ util/debounce.ts | 16 ++++++++++++++ 3 files changed, 31 insertions(+), 26 deletions(-) create mode 100644 util/debounce.ts diff --git a/components/Common/Search/States/WithSearchBox.tsx b/components/Common/Search/States/WithSearchBox.tsx index 7d9defa88bf07..a0154873bfcee 100644 --- a/components/Common/Search/States/WithSearchBox.tsx +++ b/components/Common/Search/States/WithSearchBox.tsx @@ -154,7 +154,7 @@ export const WithSearchBox: FC = ({ onClose }) => {
{searchError ? : null} - {(searchTerm ? ( + {searchTerm ? ( searchResults?.count ? ( searchResults?.hits.map(hit => ( = ({ onClose }) => { ) ) : ( - )) ?? null} + )} - {searchResults?.count - ? searchResults?.count > 8 && - searchTerm && ( - - ) - : null} + {searchResults?.count && searchResults?.count > 8 && searchTerm ? ( + + ) : null}
diff --git a/hooks/react-client/useBottomScrollListener.ts b/hooks/react-client/useBottomScrollListener.ts index 4b1bab73e4ce0..a380a0e495303 100644 --- a/hooks/react-client/useBottomScrollListener.ts +++ b/hooks/react-client/useBottomScrollListener.ts @@ -1,5 +1,7 @@ import { useState, useEffect } from 'react'; +import { debounce } from '@/util/debounce'; + type CallbackFunction = () => void; const useBottomScrollListener = ( @@ -7,7 +9,8 @@ const useBottomScrollListener = ( debounceTime = 300 ) => { const [bottomReached, setBottomReached] = useState(false); - let timeoutId: NodeJS.Timeout | null = null; + + const debouncedCallback = debounce(callback, debounceTime); const handleScroll = () => { const scrollTop = document.documentElement.scrollTop; @@ -18,26 +21,15 @@ const useBottomScrollListener = ( if (bottomOfWindow) { setBottomReached(true); - if (timeoutId) { - clearTimeout(timeoutId); - } - timeoutId = setTimeout(() => { - callback(); - }, debounceTime); + debouncedCallback(); } else { setBottomReached(false); } }; useEffect(() => { - // Add the event listener with the passive option set to true window.addEventListener('scroll', handleScroll, { passive: true }); - return () => { - window.removeEventListener('scroll', handleScroll); - if (timeoutId) { - clearTimeout(timeoutId); - } - }; + return () => window.removeEventListener('scroll', handleScroll); }, []); return bottomReached; diff --git a/util/debounce.ts b/util/debounce.ts new file mode 100644 index 0000000000000..73472b35e07e4 --- /dev/null +++ b/util/debounce.ts @@ -0,0 +1,16 @@ +type DebounceFunction = (...args: Array) => void; + +export const debounce = ( + func: T, + delay: number +): ((...args: Parameters) => void) => { + let timeoutId: NodeJS.Timeout; + + return (...args: Parameters) => { + clearTimeout(timeoutId); + + timeoutId = setTimeout(() => { + func(...args); + }, delay); + }; +}; From eb0915bf043f05d9c6140f4b923aa8e6fc24bdb7 Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Tue, 23 Jan 2024 00:37:29 +0100 Subject: [PATCH 30/44] style: addresses feedback --- layouts/New/Search.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/layouts/New/Search.tsx b/layouts/New/Search.tsx index ee2324d76c1da..e2b3bb982ef6e 100644 --- a/layouts/New/Search.tsx +++ b/layouts/New/Search.tsx @@ -1,5 +1,3 @@ -'use client'; - import type { FC, PropsWithChildren } from 'react'; import WithFooter from '@/components/withFooter'; From 531cf0cc547fc2baa7737befdb0d34f7fa643c30 Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Tue, 23 Jan 2024 14:51:51 +0100 Subject: [PATCH 31/44] ci: adds Orama sync script to gh workflows --- .github/workflows/build.yml | 5 +++++ package.json | 1 + scripts/orama-search/get-documents.mjs | 16 +++++++++++----- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0627758f0d8f4..4175285b19452 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -123,3 +123,8 @@ jobs: # this should be a last resort in case by any chances the build memory gets too high # but in general this should never happen NODE_OPTIONS: '--max_old_space_size=4096' + + - name: Sync Orama Cloud + if: github.ref == 'refs/heads/main' + run: | + npm run sync-orama diff --git a/package.json b/package.json index 079f591dffe96..5309c383fd9db 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "prettier": "prettier \"**/*.{js,mjs,ts,tsx,md,mdx,json,yml,css}\" --check --cache --cache-strategy=content --cache-location=.prettiercache", "prettier:fix": "npm run prettier -- --write", "format": "npm run lint:fix && npm run prettier:fix", + "sync-orama": "node ./scripts/orama-search/sync-orama-cloud.mjs", "storybook": "cross-env NODE_NO_WARNINGS=1 storybook dev -p 6006 --quiet --no-open", "storybook:build": "cross-env NODE_NO_WARNINGS=1 storybook build --quiet --webpack-stats-json", "test:unit": "cross-env NODE_NO_WARNINGS=1 jest", diff --git a/scripts/orama-search/get-documents.mjs b/scripts/orama-search/get-documents.mjs index 7b94778fcb5fb..c26a5444f83eb 100644 --- a/scripts/orama-search/get-documents.mjs +++ b/scripts/orama-search/get-documents.mjs @@ -1,14 +1,20 @@ +import { readFileSync } from 'node:fs'; import zlib from 'node:zlib'; import { slug } from 'github-slugger'; -import { NEXT_DATA_URL } from '../../next.constants.mjs'; +const dataBasePath = new URL( + '../../.next/server/app/en/next-data', + import.meta.url +).pathname; -const nextPageData = await fetch(`${NEXT_DATA_URL}/page-data`); -const nextAPIPageData = await fetch(`${NEXT_DATA_URL}/api-data`); +const nextPageData = readFileSync(`${dataBasePath}/page-data.body`, 'utf-8'); +const nextAPIPageData = readFileSync(`${dataBasePath}/api-data.body`, 'utf-8'); -const pageData = await nextPageData.json(); -const apiData = await nextAPIPageData.json(); +const pageData = JSON.parse(nextPageData); +const apiData = JSON.parse(nextAPIPageData); + +console.log('apiData', apiData); const splitIntoSections = markdownContent => { const lines = markdownContent.split(/\n/gm); From f4e25b405d33f69dbcdea5e04e88685c4222a6fb Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Tue, 23 Jan 2024 14:53:37 +0100 Subject: [PATCH 32/44] chore: removes useless log --- scripts/orama-search/get-documents.mjs | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/orama-search/get-documents.mjs b/scripts/orama-search/get-documents.mjs index c26a5444f83eb..06376afb8c555 100644 --- a/scripts/orama-search/get-documents.mjs +++ b/scripts/orama-search/get-documents.mjs @@ -14,8 +14,6 @@ const nextAPIPageData = readFileSync(`${dataBasePath}/api-data.body`, 'utf-8'); const pageData = JSON.parse(nextPageData); const apiData = JSON.parse(nextAPIPageData); -console.log('apiData', apiData); - const splitIntoSections = markdownContent => { const lines = markdownContent.split(/\n/gm); const sections = []; From c0c5c9fdc21c4a9cf457c965c0a7e10241617cda Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Wed, 24 Jan 2024 18:20:12 +0100 Subject: [PATCH 33/44] style: addresses feedback and adds tests --- .../Common/Search/States/WithAllResults.tsx | 12 ++- .../Common/Search/States/index.module.css | 46 ++++++----- components/Common/Search/index.module.css | 3 +- components/MDX/SearchPage/index.module.css | 78 +++++++++---------- next.constants.mjs | 22 ++++++ next.orama.mjs | 13 +++- scripts/orama-search/sync-orama-cloud.mjs | 5 +- util/__tests__/stringUtils.test.mjs | 51 +++++++++++- util/stringUtils.ts | 4 +- 9 files changed, 157 insertions(+), 77 deletions(-) diff --git a/components/Common/Search/States/WithAllResults.tsx b/components/Common/Search/States/WithAllResults.tsx index 4562e8d9a729a..959ce8dadfe78 100644 --- a/components/Common/Search/States/WithAllResults.tsx +++ b/components/Common/Search/States/WithAllResults.tsx @@ -1,5 +1,6 @@ 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'; @@ -17,11 +18,16 @@ type SeeAllProps = { 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 sanitizedSearchTerm = encodeURIComponent(props.searchTerm); - const sanitizedFacetName = encodeURIComponent(props.selectedFacetName); - const allResultsURL = `/en/search?q=${sanitizedSearchTerm}§ion=${sanitizedFacetName}`; + const allResultsURL = `/${locale}/search?${searchParams.toString()}`; return (
diff --git a/components/Common/Search/States/index.module.css b/components/Common/Search/States/index.module.css index b344c0fc6b384..497fa5289f014 100644 --- a/components/Common/Search/States/index.module.css +++ b/components/Common/Search/States/index.module.css @@ -27,32 +27,30 @@ .searchBoxInnerPanel { @apply pt-12 - text-neutral-800 - dark:text-neutral-400 + text-neutral-800 + dark:text-neutral-400 md:p-2; } .searchBoxMagnifyingGlassIcon { @apply absolute - top-[10px] - hidden - h-6 - w-6 - md:block; + top-[10px] + hidden + size-6 + md:block; } .searchBoxBackIconContainer { @apply block - md:hidden; + md:hidden; } .searchBoxBackIcon { @apply absolute - top-[7px] - block - h-6 - w-6 - md:hidden; + top-[7px] + block + size-6 + md:hidden; } .searchBoxInputContainer { @@ -70,14 +68,14 @@ pl-8 pr-4 focus:outline-none - dark:border-neutral-900 - dark:text-neutral-300 - dark:placeholder-neutral-300; + dark:border-neutral-900 + dark:text-neutral-300 + dark:placeholder-neutral-300; } .fulltextResultsContainer { @apply h-80 - overflow-scroll; + overflow-scroll; } .fulltextSearchResult { @@ -132,16 +130,16 @@ .fulltextSearchSectionSelected { @apply rounded-b-none - border-neutral-700 - text-neutral-900 - dark:border-neutral-700 - dark:text-neutral-300; + border-neutral-700 + text-neutral-900 + dark:border-neutral-700 + dark:text-neutral-300; } .fulltextSearchSectionCount { @apply ml-1 - text-neutral-500 - dark:text-neutral-800; + text-neutral-500 + dark:text-neutral-800; } .seeAllFulltextSearchResults { @@ -170,7 +168,7 @@ .poweredByLogo { @apply ml-2 - w-16; + w-16; } .emptyStateContainer { diff --git a/components/Common/Search/index.module.css b/components/Common/Search/index.module.css index 02be508deaae5..cd9dbcac7e9b9 100644 --- a/components/Common/Search/index.module.css +++ b/components/Common/Search/index.module.css @@ -24,6 +24,5 @@ @apply absolute left-2 top-[8px] - h-5 - w-5; + size-5; } diff --git a/components/MDX/SearchPage/index.module.css b/components/MDX/SearchPage/index.module.css index dd7ae3c0bd0cf..674957681c580 100644 --- a/components/MDX/SearchPage/index.module.css +++ b/components/MDX/SearchPage/index.module.css @@ -1,63 +1,63 @@ .searchPageContainer { @apply mx-auto - w-full - px-4 - py-14 - md:max-w-screen-xl; + w-full + px-4 + py-14 + md:max-w-screen-xl; } .searchTermContainer { @apply relative - w-full - px-6 - text-left - capitalize - md:px-0; + w-full + px-6 + text-left + capitalize + md:px-0; } .searchResultsColumns { @apply relative - mt-12 - grid - gap-4 - md:grid-cols-[15%_1fr]; + mt-12 + grid + gap-4 + md:grid-cols-[15%_1fr]; } .facetsColumn { @apply sticky - top-0 - flex - gap-4 - overflow-x-scroll - px-6 - capitalize - md:flex-col - md:px-0; + top-0 + flex + gap-4 + overflow-x-scroll + px-6 + capitalize + md:flex-col + md:px-0; } .facetCount { @apply ml-2 - text-sm - text-neutral-500 - dark:text-neutral-800; + text-sm + text-neutral-500 + dark:text-neutral-800; } .resultsColumn { @apply flex - flex-col - gap-4 - px-2; + 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; + w-full + flex-col + rounded-lg + px-4 + py-2 + hover:bg-neutral-100 + dark:hover:bg-neutral-900; } .searchResultTitle { @@ -66,13 +66,13 @@ .searchResultPageTitle { @apply text-sm - text-neutral-500 - dark:text-neutral-600; + text-neutral-500 + dark:text-neutral-600; } .searchResultSnippet { @apply my-2 - text-sm - text-neutral-500 - dark:text-neutral-400; + text-sm + text-neutral-500 + dark:text-neutral-400; } diff --git a/next.constants.mjs b/next.constants.mjs index 6f5d906c2c74d..ed2e2f3e6098e 100644 --- a/next.constants.mjs +++ b/next.constants.mjs @@ -152,3 +152,25 @@ export const DEFAULT_ORAMA_QUERY_PARAMS = { siteSection: {}, }, }; + +/** + * The default batch size to use when syncing Orama Cloud + */ +export const ORAMA_SYNC_BATCH_SIZE = 50; + +/** + * The default heartbeat interval to use when communicating with Orama Cloud. + * Default should be 3500ms (3.5 seconds). + */ +export const ORAMA_CLOUD_HEARTBEAT_INTERVAL = 3500; + +/** + * The default Orama Cloud endpoint to use when searching with Orama Cloud. + */ +export const ORAMA_CLOUD_ENDPOINT = process.env.NEXT_PUBLIC_ORAMA_ENDPOINT; + +/** + * The default Orama Cloud API Key to use when searching with Orama Cloud. + * This is a public API key and can be shared publicly on the frontend. + */ +export const ORAMA_CLOUD_API_KEY = process.env.NEXT_PUBLIC_ORAMA_API_KEY; diff --git a/next.orama.mjs b/next.orama.mjs index bab4726fe5ad0..c92a37e940de9 100644 --- a/next.orama.mjs +++ b/next.orama.mjs @@ -1,14 +1,19 @@ import { Highlight } from '@orama/highlight'; import { OramaClient } from '@oramacloud/client'; -import { DEFAULT_ORAMA_QUERY_PARAMS } from './next.constants.mjs'; +import { + DEFAULT_ORAMA_QUERY_PARAMS, + ORAMA_CLOUD_HEARTBEAT_INTERVAL, + ORAMA_CLOUD_ENDPOINT, + ORAMA_CLOUD_API_KEY, +} from './next.constants.mjs'; export const orama = new OramaClient({ - endpoint: process.env.NEXT_PUBLIC_ORAMA_ENDPOINT, - api_key: process.env.NEXT_PUBLIC_ORAMA_API_KEY, + endpoint: ORAMA_CLOUD_ENDPOINT, + api_key: ORAMA_CLOUD_API_KEY, }); -orama.startHeartBeat({ frequency: 3500 }); +orama.startHeartBeat({ frequency: ORAMA_CLOUD_HEARTBEAT_INTERVAL }); export const highlighter = new Highlight({ CSSClass: 'font-bold', diff --git a/scripts/orama-search/sync-orama-cloud.mjs b/scripts/orama-search/sync-orama-cloud.mjs index fb13a6741c20e..91a734cc13877 100644 --- a/scripts/orama-search/sync-orama-cloud.mjs +++ b/scripts/orama-search/sync-orama-cloud.mjs @@ -1,4 +1,5 @@ import { siteContent } from './get-documents.mjs'; +import { ORAMA_SYNC_BATCH_SIZE } from '../../next.constants.mjs'; // The following follows the instructions at https://docs.oramasearch.com/cloud/data-sources/custom-integrations/webhooks @@ -14,7 +15,7 @@ const oramaHeaders = { // Orama allows to send several documents at once, so we batch them in groups of 50. // This is not strictly necessary, but it makes the process faster. const runUpdate = async () => { - const batchSize = 50; + const batchSize = ORAMA_SYNC_BATCH_SIZE; const batches = []; for (let i = 0; i < siteContent.length; i += batchSize) { @@ -43,7 +44,7 @@ const triggerDeployment = async () => }); // We call the "snapshot" API to empty the index before inserting the new documents. -// The "snapshot" API is tipically used to replace the entire index with a fresh set of documents, but we use it here to empty the index. +// The "snapshot" API is typically used to replace the entire index with a fresh set of documents, but we use it here to empty the index. // This operation gets queued, so the live index will still be available until we call the "deploy" API and redeploy the index. // Full docs on the "snapshot" API: https://docs.oramasearch.com/cloud/data-sources/custom-integrations/webhooks#inserting-a-snapshot const emptyOramaIndex = async () => diff --git a/util/__tests__/stringUtils.test.mjs b/util/__tests__/stringUtils.test.mjs index 5a04cd095c606..05362d4938f7c 100644 --- a/util/__tests__/stringUtils.test.mjs +++ b/util/__tests__/stringUtils.test.mjs @@ -1,4 +1,7 @@ -import { getAcronymFromString } from '@/util/stringUtils'; +import { + getAcronymFromString, + parseRichTextIntoPlainText, +} from '@/util/stringUtils'; describe('String utils', () => { it('getAcronymFromString returns the correct acronym', () => { @@ -12,4 +15,50 @@ describe('String utils', () => { it('getAcronymFromString if the string is empty, it returns NA', () => { expect(getAcronymFromString('')).toBe(''); }); + + it('parseRichTextIntoPlainText returns the correct plain text from an HTML tag', () => { + expect(parseRichTextIntoPlainText('

John Doe

')).toBe('John Doe'); + }); + + it('parseRichTextIntoPlainText returns only the text of a link tag', () => { + expect( + parseRichTextIntoPlainText('[this is a link](https://www.google.com)') + ).toBe('this is a link'); + }); + + it('parseRichTextIntoPlainText replaces markdown lists with their content', () => { + expect( + parseRichTextIntoPlainText('- this is a list item\n- this is another') + ).toBe('this is a list item\nthis is another'); + }); + + it('parseRichTextIntoPlainText removes underscore, bold and italic with their content', () => { + expect( + parseRichTextIntoPlainText( + '**bold content**, *italic content*, _underscore content_' + ) + ).toBe('bold content, italic content, underscore content'); + }); + + it('parseRichTextIntoPlainText removes code blocks with their content', () => { + expect( + parseRichTextIntoPlainText('this is a\n```code block```\nwith content') + ).toBe('this is a\nwith content'); + }); + + it('parseRichTextIntoPlainText replaces empty lines or lines just with spaces with an empty string', () => { + expect(parseRichTextIntoPlainText('\n \n')).toBe(''); + }); + + it('parseRichTextIntoPlainText replaces leading and trailing spaces from each line with an empty string', () => { + expect(parseRichTextIntoPlainText(' this is a line ')).toBe( + 'this is a line' + ); + }); + + it('parseRichTextIntoPlainText replaces leading numbers and dots from each line with an empty string', () => { + expect( + parseRichTextIntoPlainText('1. this is a line\n2. this is a second line') + ).toBe('this is a line\nthis is a second line'); + }); }); diff --git a/util/stringUtils.ts b/util/stringUtils.ts index c229969a41127..4dc48a046bf19 100644 --- a/util/stringUtils.ts +++ b/util/stringUtils.ts @@ -12,10 +12,10 @@ export const parseRichTextIntoPlainText = (richText: string) => // replaces Markdown lists with their content .replace(/^[*-] (.*)$/gm, '$1') // replaces Markdown underscore, bold and italic with their content - .replace(/[_*]{1,2}(.*)[_*]{1,2}/gm, '$1') + .replace(/(\*\*|\*|__|_)(.*?)\1/gm, '$2') // replaces Markdown multiline codeblocks with their content .replace(/```.+?```/gms, '') - // replaces emppty lines or lines just with spaces with an empty string + // replaces empty lines or lines just with spaces with an empty string .replace(/^\s*\n/gm, '') // replaces leading and trailing spaces from each line with an empty string .replace(/^[ ]+|[ ]+$/gm, '') From c86e7aa830199a89d7d9cce8afcee4f7eef186ec Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Wed, 24 Jan 2024 20:37:30 +0100 Subject: [PATCH 34/44] feat: adds footer --- .../Common/Search/States/WithSearchBox.tsx | 4 ++- .../Common/Search/States/index.module.css | 31 ++++++++++++------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/components/Common/Search/States/WithSearchBox.tsx b/components/Common/Search/States/WithSearchBox.tsx index a0154873bfcee..7befeece1b830 100644 --- a/components/Common/Search/States/WithSearchBox.tsx +++ b/components/Common/Search/States/WithSearchBox.tsx @@ -178,7 +178,9 @@ export const WithSearchBox: FC = ({ onClose }) => { /> ) : null}
- +
+ +
diff --git a/components/Common/Search/States/index.module.css b/components/Common/Search/States/index.module.css index 497fa5289f014..b8aee858ac45c 100644 --- a/components/Common/Search/States/index.module.css +++ b/components/Common/Search/States/index.module.css @@ -16,7 +16,6 @@ h-screen w-full bg-neutral-100 - p-2 dark:bg-neutral-950 md:top-60 md:h-[450px] @@ -29,7 +28,7 @@ @apply pt-12 text-neutral-800 dark:text-neutral-400 - md:p-2; + md:pt-2; } .searchBoxMagnifyingGlassIcon { @@ -55,7 +54,8 @@ .searchBoxInputContainer { @apply relative - px-2; + px-2 + md:px-4; } .searchBoxInput { @@ -75,13 +75,14 @@ .fulltextResultsContainer { @apply h-80 - overflow-scroll; + overflow-scroll + md:px-4; } .fulltextSearchResult { @apply flex flex-col - rounded-md + rounded-t-md p-2 text-left text-sm @@ -112,7 +113,8 @@ text-xs font-semibold text-neutral-700 - dark:text-neutral-600; + dark:text-neutral-600 + md:px-4; } .fulltextSearchSection { @@ -155,13 +157,10 @@ } .poweredBy { - @apply absolute - -bottom-8 - left-0 - flex + @apply flex w-full items-center - justify-center + justify-end text-xs text-neutral-200; } @@ -211,3 +210,13 @@ text-neutral-600 dark:text-neutral-500; } + +.fulltextSearchFooter { + @apply relative + w-full + rounded-b-xl + border-t + border-neutral-900 + bg-neutral-950 + p-4; +} From 2aaf9e5e29e8f167061fe6925ecd0373072ea2f9 Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Wed, 24 Jan 2024 20:45:34 +0100 Subject: [PATCH 35/44] fix: fixes logo in light mode --- components/Common/Search/States/WithPoweredBy.tsx | 5 ++++- components/Common/Search/States/index.module.css | 13 ++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/components/Common/Search/States/WithPoweredBy.tsx b/components/Common/Search/States/WithPoweredBy.tsx index 7977ee99759da..d5cac2bbd7ae5 100644 --- a/components/Common/Search/States/WithPoweredBy.tsx +++ b/components/Common/Search/States/WithPoweredBy.tsx @@ -1,10 +1,13 @@ import Image from 'next/image'; import { useTranslations } from 'next-intl'; +import { useTheme } from 'next-themes'; import styles from './index.module.css'; export const WithPoweredBy = () => { const t = useTranslations(); + const { theme } = useTheme(); + const logoURL = `https://website-assets.oramasearch.com/orama-when-${theme}.svg`; return (
@@ -15,7 +18,7 @@ export const WithPoweredBy = () => { rel="noreferer" > Powered by OramaSearch Date: Wed, 24 Jan 2024 20:56:22 +0100 Subject: [PATCH 36/44] updates orama dependencies --- components/Common/Search/States/WithSearchBox.tsx | 1 + components/MDX/SearchPage/index.tsx | 1 + next.constants.mjs | 1 + package-lock.json | 14 +++++++------- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/components/Common/Search/States/WithSearchBox.tsx b/components/Common/Search/States/WithSearchBox.tsx index 7befeece1b830..b2c5f9c4550f8 100644 --- a/components/Common/Search/States/WithSearchBox.tsx +++ b/components/Common/Search/States/WithSearchBox.tsx @@ -66,6 +66,7 @@ export const WithSearchBox: FC = ({ onClose }) => { .search({ term, ...DEFAULT_ORAMA_QUERY_PARAMS, + mode: 'fulltext', ...filterBySection(), }) .then(setSearchResults) diff --git a/components/MDX/SearchPage/index.tsx b/components/MDX/SearchPage/index.tsx index 6f24d1952b1fc..ed8d3efd03192 100644 --- a/components/MDX/SearchPage/index.tsx +++ b/components/MDX/SearchPage/index.tsx @@ -42,6 +42,7 @@ const SearchPage: FC = () => { orama .search({ ...DEFAULT_ORAMA_QUERY_PARAMS, + mode: 'fulltext', term: searchTerm || '', limit: 10, offset: resultsOffset, diff --git a/next.constants.mjs b/next.constants.mjs index ed2e2f3e6098e..476313c4a6473 100644 --- a/next.constants.mjs +++ b/next.constants.mjs @@ -141,6 +141,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, threshold: 0, boost: { diff --git a/package-lock.json b/package-lock.json index fc97d16203065..72436601571b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4652,19 +4652,19 @@ } }, "node_modules/@orama/orama": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@orama/orama/-/orama-2.0.1.tgz", - "integrity": "sha512-9HUHToE93yvDGcmnEELNynG4kmzSrhSElnfLN9UFPO9ZwfHVPp1NvALli7rm1F/Bqdgi6YM6dEXKmPJUQnwiHg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@orama/orama/-/orama-2.0.2.tgz", + "integrity": "sha512-pET5UNj9+MDQCWj5QJHotkYhUDPQqBu97ufO8nQunKZ+8s6pAqUOdYJwjF7+F4DbFYoSURnRtR22HQMp7ifOjA==", "engines": { "node": ">= 16.0.0" } }, "node_modules/@oramacloud/client": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@oramacloud/client/-/client-1.0.2.tgz", - "integrity": "sha512-iM7HsXwCDtW9bWaPWGtHRQ5lhTKUF01XerFglXfI/lWhl2u9ezMkIOsxKWaCJr0pY/fxcFGmKqhjTKECzR3EpA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@oramacloud/client/-/client-1.0.4.tgz", + "integrity": "sha512-AsIJBwbUzZWfEtuR78WOOXD4ckjXpgw97scxkqmzp6/Gghp+smWb3pVNnqc16vavNnNJuwMDcL44S874JrWqkA==", "dependencies": { - "@orama/orama": "^2.0.0-beta.9", + "@orama/orama": "^2.0.1", "@paralleldrive/cuid2": "^2.2.1", "react": "^18.2.0", "vue": "^3.3.4" From ff65300afd3801c9ac6b457c2ee1662b7f99a3fb Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Fri, 2 Feb 2024 16:49:28 +0100 Subject: [PATCH 37/44] chore: updates orama dependencies to latest version --- package-lock.json | 16 ++++++++-------- package.json | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 72436601571b1..348ec07ef85a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "@mdx-js/mdx": "^3.0.0", "@nodevu/core": "~0.1.0", "@orama/highlight": "^0.1.2", - "@oramacloud/client": "^1.0.2", + "@oramacloud/client": "^1.0.5", "@radix-ui/react-accessible-icon": "^1.0.3", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", @@ -60,7 +60,7 @@ "vfile-matter": "~5.0.0" }, "devDependencies": { - "@orama/orama": "~2.0.1", + "@orama/orama": "~2.0.3", "@storybook/addon-controls": "~7.6.8", "@storybook/addon-interactions": "~7.6.8", "@storybook/addon-themes": "~7.6.8", @@ -4652,17 +4652,17 @@ } }, "node_modules/@orama/orama": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@orama/orama/-/orama-2.0.2.tgz", - "integrity": "sha512-pET5UNj9+MDQCWj5QJHotkYhUDPQqBu97ufO8nQunKZ+8s6pAqUOdYJwjF7+F4DbFYoSURnRtR22HQMp7ifOjA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@orama/orama/-/orama-2.0.3.tgz", + "integrity": "sha512-8BXTrXqP+kcyIExipZyf6voB3pzGPREh1BUrIqEP7V4PJwN/SnEcLJsafyPiPFM23fPSyH9krwLrXzvisLL19A==", "engines": { "node": ">= 16.0.0" } }, "node_modules/@oramacloud/client": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@oramacloud/client/-/client-1.0.4.tgz", - "integrity": "sha512-AsIJBwbUzZWfEtuR78WOOXD4ckjXpgw97scxkqmzp6/Gghp+smWb3pVNnqc16vavNnNJuwMDcL44S874JrWqkA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@oramacloud/client/-/client-1.0.5.tgz", + "integrity": "sha512-hwnq949/3lhxtN/0ZK2kfV4QfCxm6wEn5QY536tNrTjxerbsp89eF6N7M8lK2tThfL64QiEaA/Q8VS3GZlLCZQ==", "dependencies": { "@orama/orama": "^2.0.1", "@paralleldrive/cuid2": "^2.2.1", diff --git a/package.json b/package.json index 7f356d5801160..b60ffea98e36a 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "@mdx-js/mdx": "^3.0.0", "@nodevu/core": "~0.1.0", "@orama/highlight": "^0.1.2", - "@oramacloud/client": "^1.0.2", + "@oramacloud/client": "^1.0.5", "@radix-ui/react-accessible-icon": "^1.0.3", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", @@ -92,7 +92,7 @@ "vfile-matter": "~5.0.0" }, "devDependencies": { - "@orama/orama": "~2.0.1", + "@orama/orama": "~2.0.3", "@storybook/addon-controls": "~7.6.8", "@storybook/addon-interactions": "~7.6.8", "@storybook/addon-themes": "~7.6.8", From 05ceb46781403792dc5451088063ff783ca1158c Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Wed, 21 Feb 2024 17:29:39 -0800 Subject: [PATCH 38/44] chore: updates Orama client --- .../Common/Search/States/WithSearchBox.tsx | 7 ++++++ components/MDX/SearchPage/index.tsx | 2 +- package-lock.json | 24 ++++++++++++------- package.json | 4 ++-- 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/components/Common/Search/States/WithSearchBox.tsx b/components/Common/Search/States/WithSearchBox.tsx index b2c5f9c4550f8..de2fc4a968b69 100644 --- a/components/Common/Search/States/WithSearchBox.tsx +++ b/components/Common/Search/States/WithSearchBox.tsx @@ -67,6 +67,13 @@ export const WithSearchBox: FC = ({ onClose }) => { term, ...DEFAULT_ORAMA_QUERY_PARAMS, mode: 'fulltext', + returning: [ + 'path', + 'pageSectionTitle', + 'pageTitle', + 'path', + 'siteSection', + ], ...filterBySection(), }) .then(setSearchResults) diff --git a/components/MDX/SearchPage/index.tsx b/components/MDX/SearchPage/index.tsx index ed8d3efd03192..5109bf3c3933f 100644 --- a/components/MDX/SearchPage/index.tsx +++ b/components/MDX/SearchPage/index.tsx @@ -50,7 +50,7 @@ const SearchPage: FC = () => { }) .then(results => { setSearchResults(results); - setHits(hits => [...hits, ...results.hits]); + setHits(hits => [...hits, ...(results?.hits ?? [])]); }) .catch(console.log); }; diff --git a/package-lock.json b/package-lock.json index 823d51b4a5729..b53bab752ad1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,8 @@ "@heroicons/react": "~2.1.1", "@mdx-js/mdx": "^3.0.0", "@nodevu/core": "~0.1.0", - "@orama/highlight": "^0.1.2", - "@oramacloud/client": "^1.0.5", + "@orama/highlight": "^0.1.3", + "@oramacloud/client": "^1.0.9", "@radix-ui/react-accessible-icon": "^1.0.3", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", @@ -4176,12 +4176,15 @@ } }, "node_modules/@oramacloud/client": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@oramacloud/client/-/client-1.0.5.tgz", - "integrity": "sha512-hwnq949/3lhxtN/0ZK2kfV4QfCxm6wEn5QY536tNrTjxerbsp89eF6N7M8lK2tThfL64QiEaA/Q8VS3GZlLCZQ==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@oramacloud/client/-/client-1.0.9.tgz", + "integrity": "sha512-qBYzppjtFfINYHoBRito8hLKJO5KbYswzZYvldBrLZoxSLrPluqt+vW4Ex8E0VhyvqPaezu8koYc79aqBLLEHA==", "dependencies": { "@orama/orama": "^2.0.1", "@paralleldrive/cuid2": "^2.2.1", + "lodash": "^4.17.21", + "lodash.debounce": "^4.0.8", + "lodash.throttle": "^4.1.1", "react": "^18.2.0", "vue": "^3.3.4" } @@ -18322,14 +18325,12 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -18337,6 +18338,11 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==" + }, "node_modules/lodash.truncate": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", diff --git a/package.json b/package.json index cc1e956da207d..217613f56160a 100644 --- a/package.json +++ b/package.json @@ -41,8 +41,8 @@ "@heroicons/react": "~2.1.1", "@mdx-js/mdx": "^3.0.0", "@nodevu/core": "~0.1.0", - "@orama/highlight": "^0.1.2", - "@oramacloud/client": "^1.0.5", + "@orama/highlight": "^0.1.3", + "@oramacloud/client": "^1.0.9", "@radix-ui/react-accessible-icon": "^1.0.3", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", From e01a445e61192e9d6f6675462aa9b1bffda7588e Mon Sep 17 00:00:00 2001 From: Michele Riva Date: Wed, 21 Feb 2024 17:36:13 -0800 Subject: [PATCH 39/44] fix: fixes unexpected close of modal on click --- components/Common/Search/States/WithSearchBox.tsx | 6 +++++- hooks/react-client/useClickOutside.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/components/Common/Search/States/WithSearchBox.tsx b/components/Common/Search/States/WithSearchBox.tsx index de2fc4a968b69..eabede1fd3963 100644 --- a/components/Common/Search/States/WithSearchBox.tsx +++ b/components/Common/Search/States/WithSearchBox.tsx @@ -119,7 +119,11 @@ export const WithSearchBox: FC = ({ onClose }) => { return (
-
+
+ +
= ({ onClose }) => {
- {searchError ? : null} - - {searchTerm ? ( - searchResults?.count ? ( - searchResults?.hits.map(hit => ( - } + + {!searchError && !searchTerm && } + + {!searchError && searchTerm && ( + <> + {searchResults && + searchResults.count > 0 && + searchResults.hits.map(hit => ( + + ))} + + {searchResults && searchResults.count === 0 && ( + + )} + + {searchResults && searchResults.count > 8 && ( + - )) - ) : ( - - ) - ) : ( - + )} + )} - - {searchResults?.count && searchResults?.count > 8 && searchTerm ? ( - - ) : null}
+
diff --git a/components/Common/Search/States/WithSearchResult.tsx b/components/Common/Search/States/WithSearchResult.tsx index e2769bd4f4b16..76757dafa4d1b 100644 --- a/components/Common/Search/States/WithSearchResult.tsx +++ b/components/Common/Search/States/WithSearchResult.tsx @@ -1,10 +1,10 @@ import type { Result } from '@orama/orama'; import type { FC } from 'react'; -import type { SearchDoc } from '@/components/Common/Search/States/WithSearchBox'; import { pathToBreadcrumbs } from '@/components/Common/Search/utils'; import Link from '@/components/Link'; import { highlighter } from '@/next.orama.mjs'; +import type { SearchDoc } from '@/types'; import styles from './index.module.css'; diff --git a/components/Common/Search/States/index.module.css b/components/Common/Search/States/index.module.css index bf0954d73a6bd..c711eac2f7d63 100644 --- a/components/Common/Search/States/index.module.css +++ b/components/Common/Search/States/index.module.css @@ -75,14 +75,14 @@ .fulltextResultsContainer { @apply h-80 - overflow-scroll + overflow-auto md:px-4; } .fulltextSearchResult { @apply flex flex-col - rounded-t-md + rounded-md p-2 text-left text-sm @@ -108,7 +108,7 @@ mt-2 flex gap-2 - overflow-x-scroll + overflow-x-auto p-2 text-xs font-semibold diff --git a/components/MDX/SearchPage/index.module.css b/components/MDX/SearchPage/index.module.css index 53e660dd5cfc2..a0ac2526aeaf7 100644 --- a/components/MDX/SearchPage/index.module.css +++ b/components/MDX/SearchPage/index.module.css @@ -32,7 +32,7 @@ top-0 flex gap-4 - overflow-x-scroll + overflow-x-auto px-6 capitalize md:flex-col @@ -70,6 +70,7 @@ .searchResultPageTitle { @apply text-sm + capitalize text-neutral-500 dark:text-neutral-600; } diff --git a/components/MDX/SearchPage/index.tsx b/components/MDX/SearchPage/index.tsx index 55bc324361d86..5aa52548fd197 100644 --- a/components/MDX/SearchPage/index.tsx +++ b/components/MDX/SearchPage/index.tsx @@ -1,17 +1,17 @@ 'use client'; import type { Nullable, Results, Result } from '@orama/orama'; -import Link from 'next/link'; 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 type { SearchDoc } 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 { DEFAULT_ORAMA_QUERY_PARAMS } from '@/next.constants.mjs'; -import { orama, highlighter } from '@/next.orama.mjs'; +import { search as oramaSearch, highlighter } from '@/next.orama.mjs'; +import type { SearchDoc } from '@/types'; import styles from './index.module.css'; @@ -28,34 +28,31 @@ const SearchPage: FC = () => { const searchTerm = searchParams?.get('q'); const searchSection = searchParams?.get('section'); - useBottomScrollListener(() => { - setOffset(offset => offset + 10); - }); + useBottomScrollListener(() => setOffset(offset => offset + 10)); - useEffect(() => { - search(offset); - }, [offset]); + // 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 search = (resultsOffset = 0) => { - orama - .search({ - ...DEFAULT_ORAMA_QUERY_PARAMS, - mode: 'fulltext', - term: searchTerm || '', - limit: 10, - offset: resultsOffset, - ...filterBySection(), - }) + oramaSearch({ + ...DEFAULT_ORAMA_QUERY_PARAMS, + mode: 'fulltext', + term: searchTerm || '', + limit: 10, + offset: resultsOffset, + ...filterBySection(), + }) .then(results => { setSearchResults(results); - setHits(hits => [...hits, ...(results?.hits ?? [])]); + setHits(() => results?.hits ?? []); }) - .catch(console.log); + .catch(); }; const facets = { @@ -63,19 +60,13 @@ const SearchPage: FC = () => { ...(searchResults?.facets?.siteSection?.values ?? {}), }; - const filterBySection = () => { - if (searchSection && searchSection !== 'all') { - return { - where: { - siteSection: { - eq: searchSection, - }, - }, - }; - } - - return {}; - }; + const filterBySection = () => + searchSection && searchSection !== 'all' + ? { where: { siteSection: { eq: searchSection } } } + : {}; + + const getDocumentURL = (path: string) => + path.startsWith('api/') ? `https://nodejs.org/${path}` : path; return (
@@ -83,6 +74,7 @@ const SearchPage: FC = () => {

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

+
@@ -92,12 +84,11 @@ const SearchPage: FC = () => { {facetName} - ({facets[facetName as keyof typeof facets].toLocaleString('en')} - ) + ({facets[facetName as keyof typeof facets]}) ))} @@ -107,13 +98,14 @@ const SearchPage: FC = () => { {hits?.map(hit => (

{hit.document.pageSectionTitle}

+

{ .trim(180), }} /> +

Home {'>'} {pathToBreadcrumbs(hit.document.path).join(' > ')}
diff --git a/next.constants.mjs b/next.constants.mjs index 476313c4a6473..04beebf490015 100644 --- a/next.constants.mjs +++ b/next.constants.mjs @@ -168,10 +168,12 @@ export const ORAMA_CLOUD_HEARTBEAT_INTERVAL = 3500; /** * The default Orama Cloud endpoint to use when searching with Orama Cloud. */ -export const ORAMA_CLOUD_ENDPOINT = process.env.NEXT_PUBLIC_ORAMA_ENDPOINT; +export const ORAMA_CLOUD_ENDPOINT = + process.env.NEXT_PUBLIC_ORAMA_ENDPOINT || + 'https://cloud.orama.run/v1/indexes/nodejs-org-dev-hhqrzv'; /** * The default Orama Cloud API Key to use when searching with Orama Cloud. * This is a public API key and can be shared publicly on the frontend. */ -export const ORAMA_CLOUD_API_KEY = process.env.NEXT_PUBLIC_ORAMA_API_KEY; +export const ORAMA_CLOUD_API_KEY = process.env.NEXT_PUBLIC_ORAMA_API_KEY || ''; diff --git a/next.orama.mjs b/next.orama.mjs index c92a37e940de9..b01319925ba14 100644 --- a/next.orama.mjs +++ b/next.orama.mjs @@ -8,21 +8,30 @@ import { ORAMA_CLOUD_API_KEY, } from './next.constants.mjs'; -export const orama = new OramaClient({ - endpoint: ORAMA_CLOUD_ENDPOINT, - api_key: ORAMA_CLOUD_API_KEY, -}); +// 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, + }); + + orama.startHeartBeat({ frequency: ORAMA_CLOUD_HEARTBEAT_INTERVAL }); + + return { + search: orama.search.bind(orama), + getInitialFacets: async () => + orama.search({ term: '', ...DEFAULT_ORAMA_QUERY_PARAMS }).catch(), + }; + } -orama.startHeartBeat({ frequency: ORAMA_CLOUD_HEARTBEAT_INTERVAL }); + return { search: async () => null, getInitialFacets: async () => null }; +})(); + +export { search, getInitialFacets }; export const highlighter = new Highlight({ CSSClass: 'font-bold', HTMLTag: 'span', }); - -export async function getInitialFacets() { - return await orama.search({ - term: '', - ...DEFAULT_ORAMA_QUERY_PARAMS, - }); -} diff --git a/scripts/orama-search/get-documents.mjs b/scripts/orama-search/get-documents.mjs index 06376afb8c555..617ea11301a91 100644 --- a/scripts/orama-search/get-documents.mjs +++ b/scripts/orama-search/get-documents.mjs @@ -1,12 +1,19 @@ -import { readFileSync } from 'node:fs'; +import { existsSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; import zlib from 'node:zlib'; import { slug } from 'github-slugger'; -const dataBasePath = new URL( - '../../.next/server/app/en/next-data', - import.meta.url -).pathname; +import { getRelativePath } from '../../next.helpers.mjs'; + +const currentRoot = getRelativePath(import.meta.url); +const dataBasePath = join(currentRoot, '../../.next/server/app/en/next-data'); + +if (!existsSync(dataBasePath)) { + throw new Error( + 'The data directory does not exist. Please run `npm run build` first.' + ); +} const nextPageData = readFileSync(`${dataBasePath}/page-data.body`, 'utf-8'); const nextAPIPageData = readFileSync(`${dataBasePath}/api-data.body`, 'utf-8'); @@ -26,6 +33,7 @@ const splitIntoSections = markdownContent => { pageSectionTitle: line.replace(/^#{1,6}\s*/, ''), pageSectionContent: [], }; + sections.push(section); } else if (section) { section.pageSectionContent.push(line); @@ -52,6 +60,7 @@ export const siteContent = [...pageData, ...apiData] const markdownContent = zlib .inflateSync(Buffer.from(content, 'base64')) .toString('utf-8'); + const siteSection = pathname.split('/').shift(); const subSections = splitIntoSections(markdownContent); diff --git a/types/index.ts b/types/index.ts index c26a8e205a2b9..567d44d0e088a 100644 --- a/types/index.ts +++ b/types/index.ts @@ -10,3 +10,4 @@ export * from './redirects'; export * from './server'; export * from './github'; export * from './calendar'; +export * from './search'; diff --git a/types/search.ts b/types/search.ts new file mode 100644 index 0000000000000..03ac4e67a4a18 --- /dev/null +++ b/types/search.ts @@ -0,0 +1,8 @@ +export interface SearchDoc { + id: string; + path: string; + pageTitle: string; + siteSection: string; + pageSectionTitle: string; + pageSectionContent: string; +} From f0e09bad9cf862543d2051e0c736182645101f3d Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Fri, 23 Feb 2024 22:05:12 +0100 Subject: [PATCH 43/44] chore: minor copy changes --- components/MDX/SearchPage/index.module.css | 1 - i18n/locales/en.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/components/MDX/SearchPage/index.module.css b/components/MDX/SearchPage/index.module.css index a0ac2526aeaf7..777e5af913050 100644 --- a/components/MDX/SearchPage/index.module.css +++ b/components/MDX/SearchPage/index.module.css @@ -15,7 +15,6 @@ gap-1 px-6 text-left - capitalize md:px-0; } diff --git a/i18n/locales/en.json b/i18n/locales/en.json index 0767f23871de9..c6854003a9a1e 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -221,7 +221,7 @@ "text": "Search something..." }, "searchPage": { - "title": "Search results for: {query}" + "title": "You're searching: {query}" } } }, From 0c62be5665ed15594d7651fa48b84e28a0abffb2 Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Fri, 23 Feb 2024 22:08:48 +0100 Subject: [PATCH 44/44] fix: aggregate results and make them unique --- components/MDX/SearchPage/index.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/components/MDX/SearchPage/index.tsx b/components/MDX/SearchPage/index.tsx index 5aa52548fd197..29b6c5065402d 100644 --- a/components/MDX/SearchPage/index.tsx +++ b/components/MDX/SearchPage/index.tsx @@ -39,6 +39,11 @@ const SearchPage: FC = () => { // 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, @@ -50,7 +55,7 @@ const SearchPage: FC = () => { }) .then(results => { setSearchResults(results); - setHits(() => results?.hits ?? []); + setHits(hits => uniqueHits([...hits, ...(results?.hits ?? [])])); }) .catch(); };