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';
|
'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,19 +41,84 @@ export default function Search() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit} className="w-max-[550px] relative w-full lg:w-80 xl:w-full">
|
<>
|
||||||
<input
|
<form onSubmit={onSubmit} className="w-max-[550px] relative w-full lg:w-80 xl:w-full">
|
||||||
type="text"
|
<input
|
||||||
name="search"
|
type="text"
|
||||||
placeholder="Search for products..."
|
name="search"
|
||||||
autoComplete="off"
|
placeholder="Search for products..."
|
||||||
value={searchValue}
|
autoComplete="off"
|
||||||
onChange={(e) => setSearchValue(e.target.value)}
|
value={searchValue}
|
||||||
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"
|
onFocus={() => setShowResults(true)}
|
||||||
/>
|
onBlur={() => setShowResults(false)}
|
||||||
<div className="absolute right-0 top-0 mr-3 flex h-full items-center">
|
onChange={(e) => setSearchValue(e.target.value)}
|
||||||
<MagnifyingGlassIcon className="h-4" />
|
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>
|
/>
|
||||||
</form>
|
<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": {
|
"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
34
pnpm-lock.yaml
generated
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user