Skip to content

Commit

Permalink
Use headless ui select (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
ertrzyiks authored Aug 4, 2023
1 parent aa51123 commit bb119e5
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 28 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
"build-storybook": "storybook build"
},
"dependencies": {
"@headlessui/react": "^1.7.16",
"@prisma/client": "4.12.0",
"@tanstack/react-query": "^4.32.0",
"@types/marked": "^5.0.1",
"@types/node": "18.15.11",
"@types/react": "18.0.34",
Expand Down
21 changes: 21 additions & 0 deletions src/app/api/search/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { search } from '@/lib/search'

export async function GET(request: Request) {
const { searchParams } = new URL(request.url)

const query = searchParams.get('query')

if (!query || query.length < 3) {
return new Response(JSON.stringify({ data: [] }))
}

const recipes = await search(query)

const data = recipes.map(recipe => ({
id: recipe.id,
label: recipe.title,
value: `/${recipe.category.slug}/${recipe.slug}`
}))

return new Response(JSON.stringify({ data }))
}
27 changes: 13 additions & 14 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { PrismaClient } from '@prisma/client'
import Link from 'next/link'
import { Search } from '@/components/search/search'

const prisma = new PrismaClient()

Expand All @@ -12,21 +13,19 @@ export default async function Home() {

return (
<main className="flex flex-col items-center">
<form action="/search" className="flex justify-center align-center">
<div className="border">
<input name="query" />
<button type="submit">Search</button>
</div>
</form>
<div>
<Search />
</div>


{recipes.map((recipe) => (
<div key={recipe.id} className="flex place-items-center">
<Link href={`/${recipe.category.slug}/${recipe.slug}`}>
{recipe.title}
</Link>
</div>
))}
<div>
{recipes.map((recipe) => (
<div key={recipe.id} className="flex place-items-center">
<Link href={`/${recipe.category.slug}/${recipe.slug}`}>
{recipe.title}
</Link>
</div>
))}
</div>
</main >
)
}
15 changes: 2 additions & 13 deletions src/app/search/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { PrismaClient } from '@prisma/client'
import Link from 'next/link'
import { SearchForm } from '@/components/search-form/search-form'

const prisma = new PrismaClient()
import { search } from '@/lib/search'

interface Props {
searchParams: { query: string }
Expand All @@ -11,16 +9,7 @@ interface Props {
export default async function Home({ searchParams }: Props) {
const { query } = searchParams

const recipes = await prisma.recipe.findMany({
where: {
OR: [
{ title: { contains: query } }
]
},
include: {
category: true,
}
})
const recipes = await search(query)

return (
<main className="flex flex-col items-center">
Expand Down
130 changes: 130 additions & 0 deletions src/components/search/search.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
'use client'
import { Fragment, useState } from 'react'
import { Combobox, Transition } from '@headlessui/react'
import { useRouter } from 'next/navigation'
import {
useQuery,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
// import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid'

const queryClient = new QueryClient()

function useSearch({ query }: { query: string }) {
return useQuery({
queryKey: ['search', query],
enabled: query !== '',
queryFn: async () => {
const res = await fetch('/api/search?query=' + encodeURIComponent(query));
return res.json();
},
})
}

interface Props {
query: string
onChange: (value: string) => void
onSelected: (value: string) => void
loading?: boolean
error?: string
results?: { id: string, label: string, url: string }[]
}


function SearchForm({ query, onChange, onSelected, loading, error, results = [] }: Props) {
return (
<div className="top-16 w-72">
<Combobox onChange={(recipe: { value: string }) => onSelected(recipe.value)}>
<div className="relative mt-1">
<div className="relative w-full cursor-default overflow-hidden rounded-lg bg-white text-left shadow-md focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-teal-300 sm:text-sm">
<Combobox.Input
className="w-full border-none py-2 pl-3 pr-10 text-sm leading-5 text-gray-900 focus:ring-0"
displayValue={(person: { name: string } | null) => person ? person.name : ''}
onChange={(event) => onChange(event.target.value)}
/>
<Combobox.Button className="absolute inset-y-0 right-0 flex items-center pr-2">
{/* <ChevronUpDownIcon
className="h-5 w-5 text-gray-400"
aria-hidden="true"
/> */}
V
</Combobox.Button>
</div>
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
afterLeave={() => onChange('')}
>
<Combobox.Options className="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
{results?.length === 0 && query !== '' ? (
<div className="relative cursor-default select-none py-2 px-4 text-gray-700">
Nothing found.
</div>
) : (
(results ?? []).map((recipe) => (
<Combobox.Option
key={recipe.id}
className={({ active }) =>
`relative cursor-default select-none py-2 pl-10 pr-4 ${active ? 'bg-teal-600 text-white' : 'text-gray-900'
}`
}
value={recipe}
>
{({ selected, active }) => (
<>
<span
className={`block truncate ${selected ? 'font-medium' : 'font-normal'
}`}
>
{recipe.label}
</span>
{selected ? (
<span
className={`absolute inset-y-0 left-0 flex items-center pl-3 ${active ? 'text-white' : 'text-teal-600'
}`}
>
{/* <CheckIcon className="h-5 w-5" aria-hidden="true" /> */}
V
</span>
) : null}
</>
)}
</Combobox.Option>
))
)}
</Combobox.Options>
</Transition>
</div>
</Combobox>
</div>
)
}

function SearchWidget({ onSelected }: { onSelected: (value: string) => void }) {
const [query, setQuery] = useState('')

const { status, data, error, isFetching } = useSearch({ query })

return (
<SearchForm
query={query}
onChange={(value) => setQuery(value)}
onSelected={onSelected}
loading={isFetching}
results={data?.data ?? []}
/>
)
}

export function Search() {
const router = useRouter()

return (
<QueryClientProvider client={queryClient}>
<SearchWidget onSelected={url => router.push(url)} />
</QueryClientProvider>
)
}
21 changes: 21 additions & 0 deletions src/lib/search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()

export async function search(query: string) {
if (!query || query.length < 3) {
return []
}

const recipes = await prisma.recipe.findMany({
where: {
OR: [
{ title: { contains: query } }
]
},
include: {
category: true,
}
})

return recipes
}
28 changes: 27 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1315,6 +1315,13 @@
resolved "https://registry.yarnpkg.com/@fal-works/esbuild-plugin-global-externals/-/esbuild-plugin-global-externals-2.1.2.tgz#c05ed35ad82df8e6ac616c68b92c2282bd083ba4"
integrity sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==

"@headlessui/react@^1.7.16":
version "1.7.16"
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.16.tgz#9c458c9c4dbb708258c9e8da3fe5363f915f7b11"
integrity sha512-2MphIAZdSUacZBT6EXk8AJkj+EuvaaJbtCyHTJrPsz8inhzCl7qeNPI1uk1AUvCgWylVtdN8cVVmnhUDPxPy3g==
dependencies:
client-only "^0.0.1"

"@humanwhocodes/config-array@^0.11.8":
version "0.11.8"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9"
Expand Down Expand Up @@ -2528,6 +2535,19 @@
dependencies:
tslib "^2.4.0"

"@tanstack/[email protected]":
version "4.32.0"
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.32.0.tgz#e0f4a830283612430450c13badd353766423f523"
integrity sha512-ei4IYwL2kmlKSlCw9WgvV7PpXi0MiswVwfQRxawhJA690zWO3dU49igaQ/UMTl+Jy9jj9dK5IKAYvbX7kUvviQ==

"@tanstack/react-query@^4.32.0":
version "4.32.0"
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.32.0.tgz#701b45b149cfd4b54a68705f9100973db3ba5d5d"
integrity sha512-B8WUMcByYAH9500ENejDCATOmEZhqjtS9wsfiQ3BNa+s+yAynY8SESI8WWHhSqUmjd0pmCSFRP6BOUGSda3QXA==
dependencies:
"@tanstack/query-core" "4.32.0"
use-sync-external-store "^1.2.0"

"@testing-library/dom@^9.0.0":
version "9.3.1"
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-9.3.1.tgz#8094f560e9389fb973fe957af41bf766937a9ee9"
Expand Down Expand Up @@ -3965,7 +3985,7 @@ cli-table3@^0.6.1:
optionalDependencies:
"@colors/colors" "1.5.0"

[email protected]:
[email protected], client-only@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
Expand Down Expand Up @@ -8502,6 +8522,7 @@ streamsearch@^1.1.0:
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==

"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0:
name string-width-cjs
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
Expand Down Expand Up @@ -9155,6 +9176,11 @@ use-resize-observer@^9.1.0:
dependencies:
"@juggle/resize-observer" "^3.3.1"

use-sync-external-store@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==

util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
Expand Down

0 comments on commit bb119e5

Please sign in to comment.