diff --git a/app/api/search/route.ts b/app/api/search/route.ts new file mode 100644 index 000000000..6493fdcb8 --- /dev/null +++ b/app/api/search/route.ts @@ -0,0 +1,19 @@ +import { search } from '@orama/orama'; +import { createOramaInstance } from 'lib/orama'; +import { NextRequest, NextResponse } from 'next/server'; + +// export const runtime = 'edge'; + +export async function GET(req: NextRequest): Promise { + const searchParams = req.nextUrl.searchParams.get('q'); + const orama = await createOramaInstance(); + + const result = await search(orama, { + term: searchParams || '', + groupBy: { + properties: ['collection.title'] + } + }); + + return NextResponse.json(result); +} diff --git a/components/layout/navbar/search.tsx b/components/layout/navbar/search.tsx index c7a410b9d..99b553f54 100644 --- a/components/layout/navbar/search.tsx +++ b/components/layout/navbar/search.tsx @@ -1,7 +1,8 @@ 'use client'; +import Link from 'next/link'; import { useRouter, useSearchParams } from 'next/navigation'; -import { useEffect, useState } from 'react'; +import { ReactNode, useEffect, useState } from 'react'; import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; import { createUrl } from 'lib/utils'; @@ -10,11 +11,19 @@ export default function Search() { const router = useRouter(); const searchParams = useSearchParams(); const [searchValue, setSearchValue] = useState(''); + const [searchResults, setSearchResults] = useState(null); + const [showResults, setShowResults] = useState(false); useEffect(() => { setSearchValue(searchParams?.get('q') || ''); }, [searchParams, setSearchValue]); + useEffect(() => { + fetch(`/api/search?q=${searchValue}`) + .then((res) => res.json()) + .then((data) => setSearchResults(data)); + }, [searchValue]); + function onSubmit(e: React.FormEvent) { e.preventDefault(); @@ -32,19 +41,84 @@ export default function Search() { } return ( -
- setSearchValue(e.target.value)} - className="w-full rounded-lg border bg-white px-4 py-2 text-sm text-black placeholder:text-neutral-500 dark:border-neutral-800 dark:bg-transparent dark:text-white dark:placeholder:text-neutral-400" - /> -
- -
-
+ <> +
+ setShowResults(true)} + onBlur={() => setShowResults(false)} + onChange={(e) => setSearchValue(e.target.value)} + className="w-full rounded-lg border bg-white px-4 py-2 text-sm text-black placeholder:text-neutral-500 dark:border-neutral-800 dark:bg-transparent dark:text-white dark:placeholder:text-neutral-400" + /> +
+ +
+ + + ); } + +function SearchWindowContainer({ children }: { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} + +function SearchResults({ searchResults, query, show }) { + function breakSentence(sentence: string) { + if (sentence.length > 50) { + return sentence.slice(0, 50) + '...'; + } + + return sentence; + } + + const shouldShow = show && searchResults?.count; + + if (!shouldShow) return null; + + if (searchResults?.count) + return ( + + {searchResults?.count && + searchResults.groups.map((group) => ( +
+
+ {' '} + {group.values?.[0]}{' '} +
+ {group.result.map(({ document }) => ( +
+ + {document.title} +
+ + {' '} + {breakSentence(document.description)}{' '} + + +
+ ))} +
+ ))} +
+ ); + + if (!searchResults?.count) { + return ( + +
No results found for "{query}"
+
+ ); + } +} diff --git a/lib/orama/index.ts b/lib/orama/index.ts new file mode 100644 index 000000000..1c507cca3 --- /dev/null +++ b/lib/orama/index.ts @@ -0,0 +1,32 @@ +import { create, insertMultiple } from '@orama/orama'; +import { getCollectionProducts, getCollections } from 'lib/shopify'; + +export async function createOramaInstance() { + const collections = await getCollections(); + + const products = await Promise.all( + collections.map(({ handle, title }) => { + return getCollectionProducts({ collection: handle }).then((products) => + products.map((product) => ({ ...product, collection: { handle, title } })) + ); + }) + ); + + const allProducts = products + .flat() + .filter((product, index, self) => self.findIndex(({ id }) => id === product.id) === index); + + const db = await create({ + schema: { + title: 'string', + collection: { + handle: 'string', + title: 'string' + } + } + }); + + await insertMultiple(db, allProducts); + + return db; +} diff --git a/package.json b/package.json index ced3d26a4..56a5ad2bb 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.0", + "@orama/plugin-data-persistence": "^1.2.0", "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..e5c271fb9 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.0 + version: 1.2.0 + '@orama/plugin-data-persistence': + specifier: ^1.2.0 + version: 1.2.0 clsx: specifier: ^2.0.0 version: 2.0.0 @@ -224,6 +234,11 @@ packages: '@jridgewell/sourcemap-codec': 1.4.14 dev: true + /@msgpack/msgpack@2.8.0: + resolution: {integrity: sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==} + engines: {node: '>= 10'} + dev: false + /@next/env@13.4.13-canary.15: resolution: {integrity: sha512-AljMmO5a2uB0ZTDcBVhcfkE7WtdQDfnPg2zz/e6jKjVMRFPSvxaoRoSGUwONIhk9CAPbX9px7bZYom2wbhrTkw==} dev: false @@ -336,6 +351,19 @@ packages: fastq: 1.15.0 dev: true + /@orama/orama@1.2.0: + resolution: {integrity: sha512-qs61y0S35sikDKnnyuap4hfyeP2Bs+c+6dN5LSwSRSSqr+qmsLa6hQ5x34hdcjY9ZwZjPhARmJApi7as7NKyZA==} + engines: {node: '>= 16.0.0'} + dev: false + + /@orama/plugin-data-persistence@1.2.0: + resolution: {integrity: sha512-Z89NDFg0NiTSPx4QlKWOYSXpHg4d5Wp2gpRN6Ng2bm1g/anKwN/REKyvosUudJbHGsLT7Rn+St1cdhmJvocmRg==} + dependencies: + '@msgpack/msgpack': 2.8.0 + '@orama/orama': 1.2.0 + dpack: 0.6.22 + dev: false + /@pkgr/utils@2.4.2: resolution: {integrity: sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -1008,6 +1036,10 @@ packages: esutils: 2.0.3 dev: true + /dpack@0.6.22: + resolution: {integrity: sha512-WGPNlW2OAE7Bj0eODMpAHUcEqxrlg01e9OFZDxQodminIgC194/cRHT7K04Z1j7AUEWTeeplYGrIv/xRdwU9Hg==} + dev: false + /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} dev: true