chore: adds orama

This commit is contained in:
Michele Riva 2023-08-07 14:40:49 +02:00
parent faa7491a55
commit ca1a8cbce4
5 changed files with 175 additions and 16 deletions

19
app/api/search/route.ts Normal file
View File

@ -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<NextResponse> {
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);
}

View File

@ -1,7 +1,8 @@
'use client'; 'use client';
import Link from 'next/link';
import { useRouter, useSearchParams } from 'next/navigation'; 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 { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import { createUrl } from 'lib/utils'; import { createUrl } from 'lib/utils';
@ -10,11 +11,19 @@ export default function Search() {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
const [searchResults, setSearchResults] = useState(null);
const [showResults, setShowResults] = useState(false);
useEffect(() => { useEffect(() => {
setSearchValue(searchParams?.get('q') || ''); setSearchValue(searchParams?.get('q') || '');
}, [searchParams, setSearchValue]); }, [searchParams, setSearchValue]);
useEffect(() => {
fetch(`/api/search?q=${searchValue}`)
.then((res) => res.json())
.then((data) => setSearchResults(data));
}, [searchValue]);
function onSubmit(e: React.FormEvent<HTMLFormElement>) { function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault(); e.preventDefault();
@ -32,6 +41,7 @@ export default function Search() {
} }
return ( return (
<>
<form onSubmit={onSubmit} className="w-max-[550px] relative w-full lg:w-80 xl:w-full"> <form onSubmit={onSubmit} className="w-max-[550px] relative w-full lg:w-80 xl:w-full">
<input <input
type="text" type="text"
@ -39,12 +49,76 @@ export default function Search() {
placeholder="Search for products..." placeholder="Search for products..."
autoComplete="off" autoComplete="off"
value={searchValue} value={searchValue}
onFocus={() => setShowResults(true)}
onBlur={() => setShowResults(false)}
onChange={(e) => setSearchValue(e.target.value)} 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" 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"
/> />
<div className="absolute right-0 top-0 mr-3 flex h-full items-center"> <div className="absolute right-0 top-0 mr-3 flex h-full items-center">
<MagnifyingGlassIcon className="h-4" /> <MagnifyingGlassIcon className="h-4" />
</div> </div>
<SearchResults show={showResults} searchResults={searchResults} query={searchValue} />
</form> </form>
</>
); );
} }
function SearchWindowContainer({ children }: { children: ReactNode }) {
return (
<div className="absolute z-50 mt-2.5 w-full rounded-lg border bg-white px-4 py-2 text-sm text-black backdrop-blur-3xl placeholder:text-neutral-500 dark:border-neutral-800 dark:bg-transparent dark:text-white dark:placeholder:text-neutral-400">
{children}
</div>
);
}
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 (
<SearchWindowContainer>
{searchResults?.count &&
searchResults.groups.map((group) => (
<div className="py-2">
<div className="mb-2 border-b border-b-neutral-600 pb-1 text-xs font-semibold uppercase text-neutral-400">
{' '}
{group.values?.[0]}{' '}
</div>
{group.result.map(({ document }) => (
<div className="grid grid-cols-1 gap-2">
<Link
href={`/product/${document.handle}`}
className="rounded-lg px-1.5 py-2 hover:bg-blue-600 hover:bg-opacity-20"
>
<span className="font-bold text-neutral-200"> {document.title} </span>
<br />
<span className="break-words text-neutral-400">
{' '}
{breakSentence(document.description)}{' '}
</span>
</Link>
</div>
))}
</div>
))}
</SearchWindowContainer>
);
if (!searchResults?.count) {
return (
<SearchWindowContainer>
<div className="p-4 text-center">No results found for "{query}"</div>
</SearchWindowContainer>
);
}
}

32
lib/orama/index.ts Normal file
View File

@ -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;
}

View File

@ -24,6 +24,8 @@
"dependencies": { "dependencies": {
"@headlessui/react": "^1.7.15", "@headlessui/react": "^1.7.15",
"@heroicons/react": "^2.0.18", "@heroicons/react": "^2.0.18",
"@orama/orama": "^1.2.0",
"@orama/plugin-data-persistence": "^1.2.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"next": "13.4.13-canary.15", "next": "13.4.13-canary.15",
"react": "18.2.0", "react": "18.2.0",

34
pnpm-lock.yaml generated
View File

@ -1,4 +1,8 @@
lockfileVersion: '6.0' lockfileVersion: '6.1'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies: dependencies:
'@headlessui/react': '@headlessui/react':
@ -7,6 +11,12 @@ dependencies:
'@heroicons/react': '@heroicons/react':
specifier: ^2.0.18 specifier: ^2.0.18
version: 2.0.18(react@18.2.0) 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: clsx:
specifier: ^2.0.0 specifier: ^2.0.0
version: 2.0.0 version: 2.0.0
@ -224,6 +234,11 @@ packages:
'@jridgewell/sourcemap-codec': 1.4.14 '@jridgewell/sourcemap-codec': 1.4.14
dev: true 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: /@next/env@13.4.13-canary.15:
resolution: {integrity: sha512-AljMmO5a2uB0ZTDcBVhcfkE7WtdQDfnPg2zz/e6jKjVMRFPSvxaoRoSGUwONIhk9CAPbX9px7bZYom2wbhrTkw==} resolution: {integrity: sha512-AljMmO5a2uB0ZTDcBVhcfkE7WtdQDfnPg2zz/e6jKjVMRFPSvxaoRoSGUwONIhk9CAPbX9px7bZYom2wbhrTkw==}
dev: false dev: false
@ -336,6 +351,19 @@ packages:
fastq: 1.15.0 fastq: 1.15.0
dev: true 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: /@pkgr/utils@2.4.2:
resolution: {integrity: sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==} resolution: {integrity: sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
@ -1008,6 +1036,10 @@ packages:
esutils: 2.0.3 esutils: 2.0.3
dev: true dev: true
/dpack@0.6.22:
resolution: {integrity: sha512-WGPNlW2OAE7Bj0eODMpAHUcEqxrlg01e9OFZDxQodminIgC194/cRHT7K04Z1j7AUEWTeeplYGrIv/xRdwU9Hg==}
dev: false
/eastasianwidth@0.2.0: /eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
dev: true dev: true