mirror of
https://github.com/vercel/commerce.git
synced 2025-05-15 14:06:59 +00:00
chore: adds orama
This commit is contained in:
parent
faa7491a55
commit
ca1a8cbce4
19
app/api/search/route.ts
Normal file
19
app/api/search/route.ts
Normal 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);
|
||||
}
|
@ -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<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
|
||||
@ -32,19 +41,84 @@ export default function Search() {
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit} className="w-max-[550px] relative w-full lg:w-80 xl:w-full">
|
||||
<input
|
||||
type="text"
|
||||
name="search"
|
||||
placeholder="Search for products..."
|
||||
autoComplete="off"
|
||||
value={searchValue}
|
||||
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"
|
||||
/>
|
||||
<div className="absolute right-0 top-0 mr-3 flex h-full items-center">
|
||||
<MagnifyingGlassIcon className="h-4" />
|
||||
</div>
|
||||
</form>
|
||||
<>
|
||||
<form onSubmit={onSubmit} className="w-max-[550px] relative w-full lg:w-80 xl:w-full">
|
||||
<input
|
||||
type="text"
|
||||
name="search"
|
||||
placeholder="Search for products..."
|
||||
autoComplete="off"
|
||||
value={searchValue}
|
||||
onFocus={() => 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"
|
||||
/>
|
||||
<div className="absolute right-0 top-0 mr-3 flex h-full items-center">
|
||||
<MagnifyingGlassIcon className="h-4" />
|
||||
</div>
|
||||
<SearchResults show={showResults} searchResults={searchResults} query={searchValue} />
|
||||
</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
32
lib/orama/index.ts
Normal 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;
|
||||
}
|
@ -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",
|
||||
|
34
pnpm-lock.yaml
generated
34
pnpm-lock.yaml
generated
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user