Skip to content

Commit 7ed4174

Browse files
committed
feat(example-search): add paginated search
1 parent 0dadba7 commit 7ed4174

File tree

9 files changed

+270
-35
lines changed

9 files changed

+270
-35
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { JsonApiParams } from "next-drupal"
2+
import { useRouter } from "next/router"
3+
import { useInfiniteQuery } from "@tanstack/react-query"
4+
5+
interface SearchParams extends JsonApiParams {
6+
page?: number
7+
}
8+
9+
const NUMBER_OF_RESULTS_PER_PAGE = 1
10+
11+
async function fetchSearch(params: SearchParams) {
12+
const response = await fetch("/api/search/paginated", {
13+
method: "POST",
14+
headers: {
15+
"Content-Type": "application/json",
16+
},
17+
body: JSON.stringify({
18+
page: params.page,
19+
params: {
20+
fields: {
21+
"node--article": "id,title,created,field_image",
22+
},
23+
include: "field_image",
24+
filter: {
25+
fulltext: params.keywords,
26+
},
27+
page: {
28+
limit: NUMBER_OF_RESULTS_PER_PAGE,
29+
offset: params.page * NUMBER_OF_RESULTS_PER_PAGE,
30+
},
31+
},
32+
}),
33+
})
34+
35+
return response.json()
36+
}
37+
38+
export function usePaginatedSearch(params: SearchParams = { page: 0 }) {
39+
const router = useRouter()
40+
41+
const query = router.query
42+
43+
const results = useInfiniteQuery(
44+
["search", router],
45+
({ pageParam = params.page }) => {
46+
return fetchSearch({ ...router.query, page: pageParam })
47+
},
48+
{
49+
refetchOnWindowFocus: false,
50+
refetchInterval: 0,
51+
initialData: {
52+
pageParams: [params.page],
53+
pages: [],
54+
},
55+
enabled: Object.keys(query)?.length !== 0,
56+
getNextPageParam: (lastPage) => {
57+
return lastPage?.nextPage ?? undefined
58+
},
59+
}
60+
)
61+
62+
return { ...results, query }
63+
}

examples/example-search-api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"dependencies": {
1717
"@tailwindcss/forms": "^0.4.0",
1818
"@tailwindcss/typography": "^0.5.1",
19+
"@tanstack/react-query": "^4.20.4",
1920
"next": "^12.2.3",
2021
"next-drupal": "^1.6.0",
2122
"react": "^17.0.2",
Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
1+
import * as React from "react"
2+
import {
3+
Hydrate,
4+
QueryClient,
5+
QueryClientProvider,
6+
} from "@tanstack/react-query"
7+
18
import "tailwindcss/tailwind.css"
29

310
export default function App({ Component, pageProps }) {
4-
return <Component {...pageProps} />
11+
const [queryClient] = React.useState(() => new QueryClient())
12+
13+
return (
14+
<QueryClientProvider client={queryClient}>
15+
<Hydrate state={pageProps.dehydratedState}>
16+
<Component {...pageProps} />
17+
</Hydrate>
18+
</QueryClientProvider>
19+
)
520
}

examples/example-search-api/pages/api/search/[index].ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export default async function handler(
1212

1313
const results = await getSearchIndex<DrupalNode>(index as string, body)
1414

15+
console.log({ results })
16+
1517
response.json(results)
1618
} catch (error) {
1719
return response.status(400).json(error.message)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { NextApiRequest, NextApiResponse } from "next"
2+
import { deserialize, DrupalNode, getSearchIndex } from "next-drupal"
3+
4+
export default async function handler(
5+
request: NextApiRequest,
6+
response: NextApiResponse
7+
) {
8+
try {
9+
const results = await getSearchIndex<DrupalNode>("article", {
10+
params: request.body.params,
11+
// We are using deserialize: false here because we need the links for pagination.
12+
deserialize: false,
13+
})
14+
15+
response.json({
16+
total: results.meta.count,
17+
items: deserialize(results),
18+
nextPage: results.links?.next ? parseInt(request.body.page) + 1 : null,
19+
})
20+
} catch (error) {
21+
return response.status(400).json(error.message)
22+
}
23+
}

examples/example-search-api/pages/index.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,19 @@ export default function IndexPage() {
3535
<a>See Example</a>
3636
</Link>
3737
</p>
38+
<h2>Paginated</h2>
39+
<p>
40+
A full text search implemented using Search API and JSON:API Search
41+
API.
42+
</p>
43+
<p>
44+
Pagination is implemented using <code>@tanstack/react-query</code>.
45+
</p>
46+
<p>
47+
<Link href="/paginated" passHref>
48+
<a>See Example</a>
49+
</Link>
50+
</p>
3851
<h2>Documentation</h2>
3952
See{" "}
4053
<a href="https://next-drupal.org/docs/search-api">
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import Head from "next/head"
2+
import Link from "next/link"
3+
import Image from "next/image"
4+
import { useRouter } from "next/router"
5+
6+
import { usePaginatedSearch } from "../hooks/use-paginated-search"
7+
8+
function formatDate(input: string): string {
9+
const date = new Date(input)
10+
return date.toLocaleDateString("en-US", {
11+
month: "long",
12+
day: "numeric",
13+
year: "numeric",
14+
})
15+
}
16+
17+
export default function PaginatedPage() {
18+
const router = useRouter()
19+
const { data, hasNextPage, isFetching, fetchNextPage, isError } =
20+
usePaginatedSearch()
21+
22+
function onSubmit(event) {
23+
event.preventDefault()
24+
25+
router.push({
26+
pathname: "/paginated",
27+
query: `keywords=${event.target.keywords.value}`,
28+
})
29+
}
30+
31+
return (
32+
<>
33+
<Head>
34+
<title>Next.js for Drupal | Search API Example</title>
35+
</Head>
36+
<div className="container max-w-2xl px-6 py-10 mx-auto">
37+
<article className="prose lg:prose-xl">
38+
<h1>Next.js for Drupal</h1>
39+
<h2>Search API Example - Paginated</h2>
40+
<p>
41+
A full text search implemented using Search API and JSON:API Search
42+
API.
43+
</p>
44+
<p>
45+
Pagination is implemented using <code>@tanstack/react-query</code>.
46+
</p>
47+
<p>Use the form below to search for article nodes.</p>
48+
<form onSubmit={onSubmit} className="mb-4">
49+
<div className="items-center gap-4 sm:grid sm:grid-cols-7">
50+
<input
51+
type="search"
52+
placeholder="Search articles..."
53+
name="keywords"
54+
required
55+
className="relative block w-full col-span-5 px-3 py-2 text-gray-900 placeholder-gray-500 border border-gray-300 rounded-md appearance-none focus:outline-none focus:ring-black focus:border-black focus:z-10 sm:text-sm"
56+
/>
57+
<button
58+
type="submit"
59+
data-cy="btn-submit"
60+
className="flex justify-center w-full px-4 py-2 mt-4 text-sm font-medium text-white bg-black border border-transparent rounded-md shadow-sm sm:col-span-2 sm:mt-0 hover:bg-black"
61+
>
62+
{isFetching ? "Please wait..." : "Search"}
63+
</button>
64+
</div>
65+
</form>
66+
{isError ? (
67+
<div className="px-4 py-2 text-sm text-red-600 bg-red-100 border-red-200 rounded-md">
68+
An error occured. Please try again.
69+
</div>
70+
) : null}
71+
{!data?.pages?.length ? (
72+
<p className="text-sm" data-cy="search-no-results">
73+
No results found. Try searching for <strong>static</strong> or{" "}
74+
<strong>preview</strong>.
75+
</p>
76+
) : (
77+
<div className="pt-4">
78+
<h3 className="mt-0" data-cy="search-results">
79+
Found {data?.pages[0]?.total} result(s).
80+
</h3>
81+
{data?.pages.map((page, index) => (
82+
<div key={index}>
83+
{page.items?.map((node) => (
84+
<div key={node.id} className="pb-4 mb-4 border-b">
85+
<article
86+
className="grid-cols-3 gap-4 sm:grid"
87+
data-cy="search-result"
88+
>
89+
{node.field_image?.uri && (
90+
<div className="col-span-1 mb-4 sm:mb-0">
91+
<Image
92+
src={`${process.env.NEXT_PUBLIC_DRUPAL_BASE_URL}${node.field_image.uri.url}`}
93+
width={200}
94+
height={110}
95+
layout="responsive"
96+
objectFit="cover"
97+
/>
98+
</div>
99+
)}
100+
<div className="col-span-2 not-prose">
101+
<h4 className="font-semibold text-black leading-normal">
102+
{node.title}
103+
</h4>
104+
<p className="mb-0">
105+
<small>{formatDate(node.created)}</small>
106+
</p>
107+
</div>
108+
</article>
109+
</div>
110+
))}
111+
</div>
112+
))}
113+
{hasNextPage && (
114+
<button
115+
onClick={() => fetchNextPage()}
116+
disabled={isFetching}
117+
className="flex justify-center px-4 py-2 mt-4 text-sm font-medium text-black bg-slate-200 border border-slate-200 rounded-md shadow-sm sm:col-span-2 sm:mt-0"
118+
>
119+
{isFetching ? "Loading..." : "Show more"}
120+
</button>
121+
)}
122+
</div>
123+
)}
124+
<p>
125+
<Link href="/" passHref>
126+
<a>Go back</a>
127+
</Link>
128+
</p>
129+
</article>
130+
</div>
131+
</>
132+
)
133+
}

examples/example-search-api/pages/simple.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,10 @@ export default function SimplePage() {
112112
/>
113113
</div>
114114
)}
115-
<div className="col-span-2">
116-
<h4 className="mt-0">{node.title}</h4>
115+
<div className="col-span-2 not-prose">
116+
<h4 className="font-semibold text-black leading-normal">
117+
{node.title}
118+
</h4>
117119
<p className="mb-0">
118120
<small>{formatDate(node.created)}</small>
119121
</p>

yarn.lock

Lines changed: 15 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3035,6 +3035,11 @@
30353035
lodash.merge "^4.6.2"
30363036
postcss-selector-parser "6.0.10"
30373037

3038+
"@tanstack/[email protected]":
3039+
version "4.20.4"
3040+
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.20.4.tgz#1f7975a2db26a8bc2f382bad8a44cd422c846b17"
3041+
integrity sha512-lhLtGVNJDsJ/DyZXrLzekDEywQqRVykgBqTmkv0La32a/RleILXy6JMLBb7UmS3QCatg/F/0N9/5b0i5j6IKcA==
3042+
30383043
"@tanstack/query-core@^4.0.0-beta.1":
30393044
version "4.0.10"
30403045
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.0.10.tgz#cae6f818006616dc72c95c863592f5f68b47548a"
@@ -3049,6 +3054,14 @@
30493054
"@types/use-sync-external-store" "^0.0.3"
30503055
use-sync-external-store "^1.2.0"
30513056

3057+
"@tanstack/react-query@^4.20.4":
3058+
version "4.20.4"
3059+
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.20.4.tgz#562b34fb919adea884eccaba2b5be50e8ba7fb16"
3060+
integrity sha512-SJRxx13k/csb9lXAJfycgVA1N/yU/h3bvRNWP0+aHMfMjmbyX82FdoAcckDBbOdEyAupvb0byelNHNeypCFSyA==
3061+
dependencies:
3062+
"@tanstack/query-core" "4.20.4"
3063+
use-sync-external-store "^1.2.0"
3064+
30523065
"@testing-library/cypress@^8.0.2":
30533066
version "8.0.3"
30543067
resolved "https://registry.yarnpkg.com/@testing-library/cypress/-/cypress-8.0.3.tgz#24ab34df34d7896866603ade705afbdd186e273c"
@@ -3280,58 +3293,28 @@
32803293
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
32813294
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
32823295

3283-
3296+
"@types/[email protected]", "@types/react-dom@^18.0.9":
32843297
version "17.0.2"
32853298
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.2.tgz#35654cf6c49ae162d5bc90843d5437dc38008d43"
32863299
integrity sha512-Icd9KEgdnFfJs39KyRyr0jQ7EKhq8U6CcHRMGAS45fp5qgUvxL3ujUCfWFttUK2UErqZNj97t9gsVPNAqcwoCg==
32873300
dependencies:
32883301
"@types/react" "*"
32893302

3290-
"@types/react-dom@^18.0.9":
3291-
version "18.0.9"
3292-
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.9.tgz#ffee5e4bfc2a2f8774b15496474f8e7fe8d0b504"
3293-
integrity sha512-qnVvHxASt/H7i+XG1U1xMiY5t+IHcPGUK7TDMDzom08xa7e86eCeKOiLZezwCKVxJn6NEiiy2ekgX8aQssjIKg==
3294-
dependencies:
3295-
"@types/react" "*"
3296-
3297-
"@types/react@*", "@types/[email protected]", "@types/react@^17.0.0":
3303+
"@types/react@*", "@types/[email protected]", "@types/react@^17.0.0", "@types/react@^17.0.43", "@types/react@^18.0.26":
32983304
version "17.0.2"
32993305
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.2.tgz#3de24c4efef902dd9795a49c75f760cbe4f7a5a8"
33003306
integrity sha512-Xt40xQsrkdvjn1EyWe1Bc0dJLcil/9x2vAuW7ya+PuQip4UYUaXyhzWmAbwRsdMgwOFHpfp7/FFZebDU6Y8VHA==
33013307
dependencies:
33023308
"@types/prop-types" "*"
33033309
csstype "^3.0.2"
33043310

3305-
"@types/react@^17.0.43":
3306-
version "17.0.52"
3307-
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.52.tgz#10d8b907b5c563ac014a541f289ae8eaa9bf2e9b"
3308-
integrity sha512-vwk8QqVODi0VaZZpDXQCmEmiOuyjEFPY7Ttaw5vjM112LOq37yz1CDJGrRJwA1fYEq4Iitd5rnjd1yWAc/bT+A==
3309-
dependencies:
3310-
"@types/prop-types" "*"
3311-
"@types/scheduler" "*"
3312-
csstype "^3.0.2"
3313-
3314-
"@types/react@^18.0.26":
3315-
version "18.0.26"
3316-
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.26.tgz#8ad59fc01fef8eaf5c74f4ea392621749f0b7917"
3317-
integrity sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==
3318-
dependencies:
3319-
"@types/prop-types" "*"
3320-
"@types/scheduler" "*"
3321-
csstype "^3.0.2"
3322-
33233311
33243312
version "1.17.1"
33253313
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6"
33263314
integrity sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==
33273315
dependencies:
33283316
"@types/node" "*"
33293317

3330-
"@types/scheduler@*":
3331-
version "0.16.2"
3332-
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
3333-
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
3334-
33353318
33363319
version "8.1.1"
33373320
resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz#b49c2c70150141a15e0fa7e79cf1f92a72934ce3"

0 commit comments

Comments
 (0)