diff --git a/.env.example b/.env.example index 9ff0463db..219da6a4c 100644 --- a/.env.example +++ b/.env.example @@ -5,3 +5,5 @@ SITE_NAME="Next.js Commerce" SHOPIFY_REVALIDATION_SECRET="" SHOPIFY_STOREFRONT_ACCESS_TOKEN="" SHOPIFY_STORE_DOMAIN="[your-shopify-store-subdomain].myshopify.com" +NEXT_PUBLIC_ORAMA_API_KEY="" +NEXT_PUBLIC_ORAMA_ENDPOINT="" diff --git a/components/layout/navbar/search.tsx b/components/layout/navbar/search.tsx index c7a410b9d..962794256 100644 --- a/components/layout/navbar/search.tsx +++ b/components/layout/navbar/search.tsx @@ -1,20 +1,38 @@ 'use client'; +import Link from 'next/link'; import { useRouter, useSearchParams } from 'next/navigation'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; +import { Results } from '@orama/orama'; +import { orama, trimDescription } from 'lib/orama'; import { createUrl } from 'lib/utils'; +import { useOutsideClick } from './useOutsideClick'; export default function Search() { const router = useRouter(); const searchParams = useSearchParams(); const [searchValue, setSearchValue] = useState(''); + const [searchResults, setSearchResults] = useState(); useEffect(() => { setSearchValue(searchParams?.get('q') || ''); }, [searchParams, setSearchValue]); + useEffect(() => { + orama.search({ + term: searchValue, + limit: 5, + boost: { + title: 2, + } + }) + .then(setSearchResults) + .catch(console.log); + + }, [searchValue]); + function onSubmit(e: React.FormEvent) { e.preventDefault(); @@ -31,6 +49,14 @@ export default function Search() { router.push(createUrl('/search', newParams)); } + const searchResultsRef = useRef(null); + + useOutsideClick(searchResultsRef.current, () => { + setSearchValue(''); + }); + + const showSearchResults = searchValue.length > 0 && !!searchResults + return (
+ { + showSearchResults && ( +
    + {searchResults?.hits?.map((product) => ( +
  • + +
    + {product.document.title as string} +
    +
    + {trimDescription((product.document.description || product.document.title) as string)} +
    + +
  • + ))} +
+ ) + } ); } diff --git a/components/layout/navbar/useOutsideClick.ts b/components/layout/navbar/useOutsideClick.ts new file mode 100644 index 000000000..394238126 --- /dev/null +++ b/components/layout/navbar/useOutsideClick.ts @@ -0,0 +1,9 @@ +import { useEffect } from 'react'; + +export function useOutsideClick(ref: any, onClickOut: () => void, deps = []){ + useEffect(() => { + const onClick = ({target}: any) => !ref?.contains(target) && onClickOut?.() + document.addEventListener("click", onClick); + return () => document.removeEventListener("click", onClick); + }, deps); +} \ No newline at end of file diff --git a/lib/orama/index.ts b/lib/orama/index.ts new file mode 100644 index 000000000..6356660d4 --- /dev/null +++ b/lib/orama/index.ts @@ -0,0 +1,16 @@ +import { OramaClient } from '@oramacloud/client' + +const ORAMA_API_KEY = process.env.NEXT_PUBLIC_ORAMA_API_KEY! +const ORAMA_ENDPOINT = process.env.NEXT_PUBLIC_ORAMA_ENDPOINT! + +export const orama = new OramaClient({ + endpoint: ORAMA_ENDPOINT, + api_key: ORAMA_API_KEY +}) + +export function trimDescription(description: string, maxSize = 80) { + if (description.length > maxSize) { + return `${description.substring(0, maxSize)}...` + } + return description +} \ No newline at end of file diff --git a/package.json b/package.json index ced3d26a4..eeab16fce 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,8 @@ "dependencies": { "@headlessui/react": "^1.7.15", "@heroicons/react": "^2.0.18", + "@orama/orama": "^1.2.1", + "@oramacloud/client": "1.0.0-beta.19", "clsx": "^2.0.0", "next": "13.4.13-canary.15", "react": "18.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 67dc0e88d..137bbea74 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,8 @@ -lockfileVersion: '6.0' +lockfileVersion: '6.1' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false dependencies: '@headlessui/react': @@ -7,6 +11,12 @@ dependencies: '@heroicons/react': specifier: ^2.0.18 version: 2.0.18(react@18.2.0) + '@orama/orama': + specifier: ^1.2.1 + version: 1.2.1 + '@oramacloud/client': + specifier: 1.0.0-beta.19 + version: 1.0.0-beta.19 clsx: specifier: ^2.0.0 version: 2.0.0 @@ -315,6 +325,11 @@ packages: dev: false optional: true + /@noble/hashes@1.3.1: + resolution: {integrity: sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==} + engines: {node: '>= 16'} + dev: false + /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -336,6 +351,25 @@ packages: fastq: 1.15.0 dev: true + /@orama/orama@1.2.1: + resolution: {integrity: sha512-vq3ar9REofq7Q87riamQB5y1aJc068Qwbnd7uAboeSfRTDj2+KdNiwjTEk01SVjr8eAvCx8iRZl4rkEYo0ETYw==} + engines: {node: '>= 16.0.0'} + dev: false + + /@oramacloud/client@1.0.0-beta.19: + resolution: {integrity: sha512-iig7JV7xzI+PYczf4XbQTv8ai6xaA5BlJNDlQJSEcFjOZQ3q2Wf8w7soTu1tQvWcXto60mbivttV5pYAoo/h9A==} + dependencies: + '@orama/orama': 1.2.1 + '@paralleldrive/cuid2': 2.2.2 + react: 18.2.0 + dev: false + + /@paralleldrive/cuid2@2.2.2: + resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==} + dependencies: + '@noble/hashes': 1.3.1 + dev: false + /@pkgr/utils@2.4.2: resolution: {integrity: sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}