From 9ef8171a265ec1d12b910f49b107fd7ef7662d20 Mon Sep 17 00:00:00 2001 From: ekamuktia Date: Sun, 25 Jul 2021 14:27:55 +0700 Subject: [PATCH 01/12] init faq typesense --- .../index.d.ts | 31 +++ components/faq-list.tsx | 80 ++++++ components/search/custom-hits.tsx | 19 ++ components/search/custom-refinement-list.tsx | 46 ++++ components/search/custom-search-box.tsx | 46 ++++ lib/typesense.ts | 27 ++ package.json | 5 +- pages/faq.tsx | 134 ++-------- yarn.lock | 230 +++++++++++++++++- 9 files changed, 497 insertions(+), 121 deletions(-) create mode 100644 @types/typesense-instantsearch-adapter/index.d.ts create mode 100644 components/faq-list.tsx create mode 100644 components/search/custom-hits.tsx create mode 100644 components/search/custom-refinement-list.tsx create mode 100644 components/search/custom-search-box.tsx create mode 100644 lib/typesense.ts diff --git a/@types/typesense-instantsearch-adapter/index.d.ts b/@types/typesense-instantsearch-adapter/index.d.ts new file mode 100644 index 000000000..7f538dd52 --- /dev/null +++ b/@types/typesense-instantsearch-adapter/index.d.ts @@ -0,0 +1,31 @@ +declare module "typesense-instantsearch-adapter" { + type SearchClient = object; + + export interface TypesenseNode { + host: string; + port: string; + protocol: string; + } + + export interface TypesenseSearchParameters { + queryBy: string; + sortBy?: string; + highlightFullFields?: string; + } + + export interface TypesenseServer { + apiKey: string; + nodes: TypesenseNode[]; + } + + export interface TypesenseInstantsearchAdapterOptions { + server?: TypesenseServer; + additionalSearchParameters: TypesenseSearchParameters; + } + + export default class TypesenseInstantsearchAdapter { + readonly searchClient: SearchClient; + + constructor(options: TypesenseInstantsearchAdapterOptions); + } +} diff --git a/components/faq-list.tsx b/components/faq-list.tsx new file mode 100644 index 000000000..7bad2c480 --- /dev/null +++ b/components/faq-list.tsx @@ -0,0 +1,80 @@ +import { FaqData } from "~/lib/faq-databases"; + +import htmr from "htmr"; + +type FAQListProps = { + data: FaqData[]; +}; + +function groupBy(data: T[], key: U) { + return data.reduce((acc: any, currentValue: any) => { + const groupKey = currentValue[key]; + if (!acc[groupKey]) { + acc[groupKey] = []; + } + acc[groupKey].push(currentValue); + return acc; + }, {}); +} + +export function FAQList(props: FAQListProps) { + const listFaqs = groupBy(props.data, "kategori_pertanyaan"); + return ( +
+ {Object.keys(listFaqs as Record).map( + (category: string) => ( +
+
+ +
+ {listFaqs[category].map((question: FaqData) => ( +
+
+ {question.pertanyaan} +
+
+

+ {htmr(question.jawaban)} +

+ + Sumber:{" "} + {question.link ? ( + + {question.sumber} + + ) : ( + question.sumber + )} + +
+
+ ))} +
+
+ ), + )} +
+ ); +} diff --git a/components/search/custom-hits.tsx b/components/search/custom-hits.tsx new file mode 100644 index 000000000..d971f5287 --- /dev/null +++ b/components/search/custom-hits.tsx @@ -0,0 +1,19 @@ +import { FAQList } from "~/components/faq-list"; +import { FaqData } from "~/lib/faq-databases"; + +import { StateResultsProvided } from "react-instantsearch-core"; +import { connectStateResults } from "react-instantsearch-dom"; + +function Hits(stateResults: StateResultsProvided) { + const { searchResults } = stateResults; + let results: FaqData[]; + + try { + results = searchResults.hits as unknown as FaqData[]; + } catch (e) { + results = []; + } + return ; +} + +export default connectStateResults(Hits); diff --git a/components/search/custom-refinement-list.tsx b/components/search/custom-refinement-list.tsx new file mode 100644 index 000000000..598f9f230 --- /dev/null +++ b/components/search/custom-refinement-list.tsx @@ -0,0 +1,46 @@ +import { ChangeEvent } from "react"; + +import { FormLabel } from "~/components/ui/forms/form-label"; +import { InputSelect } from "~/components/ui/forms/input-select"; + +import { + connectRefinementList, + RefinementListProvided, +} from "react-instantsearch-core"; + +interface CustomRefinementListProvided extends RefinementListProvided { + readonly title: string; +} + +function RefinementList({ + title, + items, + currentRefinement, + refine, +}: CustomRefinementListProvided) { + function handleFilterChange(event: ChangeEvent) { + const filterValue = event.target.value; + if (filterValue) { + refine([filterValue]); + } else { + refine([]); + } + } + + return ( +
+ {title} + + + {items.map((item) => ( + + ))} + +
+ ); +} + +export default connectRefinementList(RefinementList); diff --git a/components/search/custom-search-box.tsx b/components/search/custom-search-box.tsx new file mode 100644 index 000000000..45366d8b5 --- /dev/null +++ b/components/search/custom-search-box.tsx @@ -0,0 +1,46 @@ +import React from "react"; + +import { FormGroup } from "~/components/ui/forms/form-group"; +import { FormLabel } from "~/components/ui/forms/form-label"; +import { InputText } from "~/components/ui/forms/input-text"; + +import { SearchBoxProvided } from "react-instantsearch-core"; +import { connectSearchBox } from "react-instantsearch-dom"; + +interface CustomSearchBoxProvided extends SearchBoxProvided { + readonly itemName: string; + readonly placeholderText?: string; +} + +function SearchBox({ + currentRefinement, + refine, + itemName, + placeholderText, +}: CustomSearchBoxProvided) { + return ( +
+
+
+
+ Cari {itemName}: + + refine(e.currentTarget.value)} + placeholder={placeholderText} + type="search" + value={currentRefinement} + /> + +
+
+
+
+ ); +} + +export default connectSearchBox(SearchBox); diff --git a/lib/typesense.ts b/lib/typesense.ts new file mode 100644 index 000000000..2ea85c931 --- /dev/null +++ b/lib/typesense.ts @@ -0,0 +1,27 @@ +import TypesenseInstantSearchAdapter from "typesense-instantsearch-adapter"; + +export function typesenseSearch({ + queryBy, + defaultSort, +}: { + queryBy: string[]; + defaultSort?: string; +}) { + const searchAdapter = new TypesenseInstantSearchAdapter({ + server: { + apiKey: "FByczfHEjsTCihgkkYlq2YbAgUKMoyVP", // Be sure to use the search-only-api-key + nodes: [ + { + host: "public-api.trustmedis.id", + port: "443", + protocol: "https", + }, + ], + }, + additionalSearchParameters: { + queryBy: queryBy.join(), + sortBy: defaultSort ? defaultSort : "", + }, + }); + return searchAdapter.searchClient; +} diff --git a/package.json b/package.json index ec68198b4..d10c942ce 100644 --- a/package.json +++ b/package.json @@ -41,8 +41,10 @@ "preact": "^10.5.14", "react": "17.0.2", "react-dom": "17.0.2", + "react-instantsearch-dom": "^6.12.0", "ts-debounce": "^3.0.0", - "typeface-inter": "^3.18.1" + "typeface-inter": "^3.18.1", + "typesense-instantsearch-adapter": "^2.0.1" }, "devDependencies": { "@babel/eslint-plugin": "7.14.5", @@ -58,6 +60,7 @@ "@types/nprogress": "^0.2.0", "@types/prettier": "^2.3.2", "@types/react": "17.0.13", + "@types/react-instantsearch-dom": "^6.12.0", "autoprefixer": "10.3.0", "chalk": "^4.1.1", "cloudinary-build-url": "^0.2.1", diff --git a/pages/faq.tsx b/pages/faq.tsx index bc0c11f8a..00dc818d9 100644 --- a/pages/faq.tsx +++ b/pages/faq.tsx @@ -1,54 +1,23 @@ -import { useMemo } from "react"; - import { BackButton } from "~/components/layout/back-button"; import { Page } from "~/components/layout/page"; import { PageContent } from "~/components/layout/page-content"; import { PageHeader } from "~/components/layout/page-header"; -import { SearchForm } from "~/components/search-form"; -import faqSheets, { FaqData } from "~/lib/faq-databases"; -import { useSearch } from "~/lib/hooks/use-search"; +import CustomHits from "~/components/search/custom-hits"; +import CustomRefinementList from "~/components/search/custom-refinement-list"; +import CustomSearchBox from "~/components/search/custom-search-box"; +import { typesenseSearch } from "~/lib/typesense"; -import htmr from "htmr"; -import { GetStaticProps } from "next"; import { NextSeo } from "next-seo"; - -type FaqsProps = { - faqSheets: FaqData[]; -}; - -function groupBy(data: T[], key: U) { - return data.reduce((acc: any, currentValue: any) => { - const groupKey = currentValue[key]; - if (!acc[groupKey]) { - acc[groupKey] = []; - } - acc[groupKey].push(currentValue); - return acc; - }, {}); -} +import { InstantSearch, RefinementItem } from "react-instantsearch-dom"; const meta = { title: "Pertanyaan yang sering ditanyakan", }; -export default function Faqs(props: FaqsProps) { - const { faqSheets: faq } = props; - const [filteredQuestions, handleSubmitKeywords, urlParams, filterItems] = - useSearch({ - items: faq, - fieldNames: ["kategori_pertanyaan", "pertanyaan", "jawaban"], - aggregationSettings: [ - { field: "kategori_pertanyaan", title: "Kategori Pertanyaan" }, - ], - }); - - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const listFaqs = useMemo(() => { - return groupBy( - filteredQuestions, - "kategori_pertanyaan", - ); - }, [filteredQuestions]); +export default function Faqs() { + const searchClient = typesenseSearch({ + queryBy: ["kategori_pertanyaan", "pertanyaan", "jawaban"], + }); return ( @@ -65,79 +34,20 @@ export default function Faqs(props: FaqsProps) { title="Pertanyaan yang sering ditanyakan" /> - - -
- {Object.keys(listFaqs as Record).map( - (category: string) => ( -
-
- -
- {listFaqs[category].map((question: FaqData) => ( -
-
- {question.pertanyaan} -
-
-

- {htmr(question.jawaban)} -

- - Sumber:{" "} - {question.link ? ( - - {question.sumber} - - ) : ( - question.sumber - )} - -
-
- ))} -
-
- ), - )} -
+ + +
+ []) => + items.sort((a, b) => a.label.localeCompare(b.label)) + } + /> +
+ +
); } - -export const getStaticProps: GetStaticProps = () => { - return { - props: { - faqSheets, - }, - }; -}; diff --git a/yarn.lock b/yarn.lock index 8cda39c57..3449446a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,110 @@ # yarn lockfile v1 +"@algolia/cache-browser-local-storage@4.10.3": + version "4.10.3" + resolved "https://registry.yarnpkg.com/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.10.3.tgz#3bf81e0f66a4a1079a75914a987eb1ef432c7c68" + integrity sha512-TD1N7zg5lb56/PLjjD4bBl2eccEvVHhC7yfgFu2r9k5tf+gvbGxEZ3NhRZVKu2MObUIcEy2VR4LVLxOQu45Hlg== + dependencies: + "@algolia/cache-common" "4.10.3" + +"@algolia/cache-common@4.10.3": + version "4.10.3" + resolved "https://registry.yarnpkg.com/@algolia/cache-common/-/cache-common-4.10.3.tgz#311b2b5ae06d55300f4230944c99bc39ad15847d" + integrity sha512-q13cPPUmtf8a2suBC4kySSr97EyulSXuxUkn7l1tZUCX/k1y5KNheMp8npBy8Kc8gPPmHpacxddRSfOncjiKFw== + +"@algolia/cache-in-memory@4.10.3": + version "4.10.3" + resolved "https://registry.yarnpkg.com/@algolia/cache-in-memory/-/cache-in-memory-4.10.3.tgz#697e4994538426272ea29ccf2b32b46ea4c48862" + integrity sha512-JhPajhOXAjUP+TZrZTh6KJpF5VKTKyWK2aR1cD8NtrcVHwfGS7fTyfXfVm5BqBqkD9U0gVvufUt/mVyI80aZww== + dependencies: + "@algolia/cache-common" "4.10.3" + +"@algolia/client-account@4.10.3": + version "4.10.3" + resolved "https://registry.yarnpkg.com/@algolia/client-account/-/client-account-4.10.3.tgz#f2cbefb1abce74c341115607d6af199df1b056ae" + integrity sha512-S/IsJB4s+e1xYctdpW3nAbwrR2y3pjSo9X21fJGoiGeIpTRdvQG7nydgsLkhnhcgAdLnmqBapYyAqMGmlcyOkg== + dependencies: + "@algolia/client-common" "4.10.3" + "@algolia/client-search" "4.10.3" + "@algolia/transporter" "4.10.3" + +"@algolia/client-analytics@4.10.3": + version "4.10.3" + resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-4.10.3.tgz#43d934ef8df0cf551c78e6b2e9f2452e7fb27d93" + integrity sha512-vlHTbBqJktRgclh3v7bPQLfZvFIqY4erNFIZA5C7nisCj9oLeTgzefoUrr+R90+I+XjfoLxnmoeigS1Z1yg1vw== + dependencies: + "@algolia/client-common" "4.10.3" + "@algolia/client-search" "4.10.3" + "@algolia/requester-common" "4.10.3" + "@algolia/transporter" "4.10.3" + +"@algolia/client-common@4.10.3": + version "4.10.3" + resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-4.10.3.tgz#c4257dd5c57c5c8ec4bd48a7b1897573e372d403" + integrity sha512-uFyP2Z14jG2hsFRbAoavna6oJf4NTXaSDAZgouZUZlHlBp5elM38sjNeA5HR9/D9J/GjwaB1SgB7iUiIWYBB4w== + dependencies: + "@algolia/requester-common" "4.10.3" + "@algolia/transporter" "4.10.3" + +"@algolia/client-personalization@4.10.3": + version "4.10.3" + resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-4.10.3.tgz#58c800f90ab8ab4aa29abdf29a97e89e6bda419e" + integrity sha512-NS7Nx8EJ/nduGXT8CFo5z7kLF0jnFehTP3eC+z+GOEESH3rrs7uR12IZHxv5QhQswZa9vl925zCOZDcDVoENCg== + dependencies: + "@algolia/client-common" "4.10.3" + "@algolia/requester-common" "4.10.3" + "@algolia/transporter" "4.10.3" + +"@algolia/client-search@4.10.3": + version "4.10.3" + resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-4.10.3.tgz#aa6b02c2d528cb264830f276739b7f68b58988ef" + integrity sha512-Zwnp2G94IrNFKWCG/k7epI5UswRkPvL9FCt7/slXe2bkjP2y/HA37gzRn+9tXoLVRwd7gBzrtOA4jFKIyjrtVw== + dependencies: + "@algolia/client-common" "4.10.3" + "@algolia/requester-common" "4.10.3" + "@algolia/transporter" "4.10.3" + +"@algolia/logger-common@4.10.3": + version "4.10.3" + resolved "https://registry.yarnpkg.com/@algolia/logger-common/-/logger-common-4.10.3.tgz#6773d2e38581bf9ac57e2dda02f0c4f1bc72ce94" + integrity sha512-M6xi+qov2bkgg1H9e1Qtvq/E/eKsGcgz8RBbXNzqPIYoDGZNkv+b3b8YMo3dxd4Wd6M24HU1iqF3kmr1LaXndg== + +"@algolia/logger-console@4.10.3": + version "4.10.3" + resolved "https://registry.yarnpkg.com/@algolia/logger-console/-/logger-console-4.10.3.tgz#bd8bdc1f9dba89db37be25d673ac1f2e68de7913" + integrity sha512-vVgRI7b4PHjgBdRkv/cRz490twvkLoGdpC4VYzIouSrKj8SIVLRhey3qgXk7oQXi3xoxVAv6NrklHfpO8Bpx0w== + dependencies: + "@algolia/logger-common" "4.10.3" + +"@algolia/requester-browser-xhr@4.10.3": + version "4.10.3" + resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.10.3.tgz#81ae8f6caf562a28f96102f03da7f4b19bba568c" + integrity sha512-4WIk1zreFbc1EF6+gsfBTQvwSNjWc20zJAAExRWql/Jq5yfVHmwOqi/CajA53/cXKFBqo80DAMRvOiwP+hOLYw== + dependencies: + "@algolia/requester-common" "4.10.3" + +"@algolia/requester-common@4.10.3": + version "4.10.3" + resolved "https://registry.yarnpkg.com/@algolia/requester-common/-/requester-common-4.10.3.tgz#c3112393cff97be79863bc28de76f9c69b2f5a95" + integrity sha512-PNfLHmg0Hujugs3rx55uz/ifv7b9HVdSFQDb2hj0O5xZaBEuQCNOXC6COrXR8+9VEfqp2swpg7zwgtqFxh+BtQ== + +"@algolia/requester-node-http@4.10.3": + version "4.10.3" + resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-4.10.3.tgz#75ea7805ac0ba25a1124989d8632ef39c31441c1" + integrity sha512-A9ZcGfEvgqf0luJApdNcIhsRh6MShn2zn2tbjwjGG1joF81w+HUY+BWuLZn56vGwAA9ZB9n00IoJJpxibbfofg== + dependencies: + "@algolia/requester-common" "4.10.3" + +"@algolia/transporter@4.10.3": + version "4.10.3" + resolved "https://registry.yarnpkg.com/@algolia/transporter/-/transporter-4.10.3.tgz#0aeee752923957cffe63e4cf1c7a22ca48d96dde" + integrity sha512-n1lRyKDbrckbMEgm7QXtj3nEWUuzA3aKLzVQ43/F/RCFib15j4IwtmYhXR6OIBRSc7+T0Hm48S0J6F+HeYCQkw== + dependencies: + "@algolia/cache-common" "4.10.3" + "@algolia/logger-common" "4.10.3" + "@algolia/requester-common" "4.10.3" + "@aws-crypto/crc32@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@aws-crypto/crc32/-/crc32-1.0.0.tgz#6a0164fd92bb365860ba6afb5dfef449701eb8ca" @@ -1110,6 +1214,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.1.2", "@babel/runtime@^7.9.2": + version "7.14.8" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.8.tgz#7119a56f421018852694290b9f9148097391b446" + integrity sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.9.6": version "7.14.6" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.6.tgz#535203bc0892efc7dec60bdc27b2ecf6e409062d" @@ -1117,13 +1228,6 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.9.2": - version "7.14.8" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.8.tgz#7119a56f421018852694290b9f9148097391b446" - integrity sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg== - dependencies: - regenerator-runtime "^0.13.4" - "@babel/template@^7.14.5", "@babel/template@^7.3.3": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.14.5.tgz#a9bc9d8b33354ff6e55a9c60d1109200a68974f4" @@ -2023,6 +2127,32 @@ resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24" integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug== +"@types/react-instantsearch-core@*": + version "6.10.4" + resolved "https://registry.yarnpkg.com/@types/react-instantsearch-core/-/react-instantsearch-core-6.10.4.tgz#daac0857600d486c2ecfd41aa5127dccfe9d8927" + integrity sha512-Qdc7h2bkXaVljbNL2wg0hRLN7FLTi7NeaD+VLvjF8A5M41olgEIIzdoDye+OfiB/COk4ynXbw0bqxhhNUA6gxw== + dependencies: + "@types/react" "*" + algoliasearch ">=4" + algoliasearch-helper ">=3" + +"@types/react-instantsearch-dom@^6.12.0": + version "6.12.0" + resolved "https://registry.yarnpkg.com/@types/react-instantsearch-dom/-/react-instantsearch-dom-6.12.0.tgz#716be1b48193bbb65271205a9bd112a65d8a2661" + integrity sha512-O08H+ye4e4kEnYHmMrov9FPNRDJwfCWthNZf4aztqahpU8LSbAiuFQGVy84SHUvg/jfNcG4333SsVnAQLtbS7A== + dependencies: + "@types/react" "*" + "@types/react-instantsearch-core" "*" + +"@types/react@*": + version "17.0.15" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.15.tgz#c7533dc38025677e312606502df7656a6ea626d0" + integrity sha512-uTKHDK9STXFHLaKv6IMnwp52fm0hwU+N89w/p9grdUqcFA6WuqDyPhaWopbNyE1k/VhgzmHl8pu1L4wITtmlLw== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/react@17.0.13": version "17.0.13" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.13.tgz#6b7c9a8f2868586ad87d941c02337c6888fb874f" @@ -2359,6 +2489,33 @@ ajv@^8.0.1: require-from-string "^2.0.2" uri-js "^4.2.2" +algoliasearch-helper@>=3, algoliasearch-helper@^3.5.3: + version "3.5.4" + resolved "https://registry.yarnpkg.com/algoliasearch-helper/-/algoliasearch-helper-3.5.4.tgz#21b20ab8a258daa9dde9aef2daa5e8994cd66077" + integrity sha512-t+FLhXYnPZiwjYe5ExyN962HQY8mi3KwRju3Lyf6OBgtRdx30d6mqvtClXf5NeBihH45Xzj6t4Y5YyvAI432XA== + dependencies: + events "^1.1.1" + +algoliasearch@>=4: + version "4.10.3" + resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-4.10.3.tgz#22df4bb02fbf13a765b18b85df8745ee9c04f00a" + integrity sha512-OLY0AWlPKGLbSaw14ivMB7BT5fPdp8VdzY4L8FtzZnqmLKsyes24cltGlf7/X96ACkYEcT390SReCDt/9SUIRg== + dependencies: + "@algolia/cache-browser-local-storage" "4.10.3" + "@algolia/cache-common" "4.10.3" + "@algolia/cache-in-memory" "4.10.3" + "@algolia/client-account" "4.10.3" + "@algolia/client-analytics" "4.10.3" + "@algolia/client-common" "4.10.3" + "@algolia/client-personalization" "4.10.3" + "@algolia/client-search" "4.10.3" + "@algolia/logger-common" "4.10.3" + "@algolia/logger-console" "4.10.3" + "@algolia/requester-browser-xhr" "4.10.3" + "@algolia/requester-common" "4.10.3" + "@algolia/requester-node-http" "4.10.3" + "@algolia/transporter" "4.10.3" + anser@1.4.9: version "1.4.9" resolved "https://registry.yarnpkg.com/anser/-/anser-1.4.9.tgz#1f85423a5dcf8da4631a341665ff675b96845760" @@ -3477,6 +3634,11 @@ classnames@2.2.6: resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== +classnames@^2.2.5: + version "2.3.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" + integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== + clean-stack@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" @@ -5173,6 +5335,11 @@ eventemitter2@^6.4.3: resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.4.tgz#aa96e8275c4dbeb017a5d0e03780c65612a1202b" integrity sha512-HLU3NDY6wARrLCEwyGKRBvuWYyvW6mHYv72SJJAH3iJN3a6eVUvkjFkcxah1bcTgGVBBrFdIopBJPhCQFMLyXw== +events@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= + events@^3.0.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" @@ -8341,6 +8508,11 @@ logalot@^2.0.0: figures "^1.3.5" squeak "^1.0.0" +loglevel@^1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197" + integrity sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw== + longest-streak@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.4.tgz#b8599957da5b5dab64dee3fe316fa774597d90e4" @@ -10187,7 +10359,7 @@ prompts@^2.0.1, prompts@^2.2.1: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@15.7.2, prop-types@^15.7.2: +prop-types@15.7.2, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -10442,6 +10614,33 @@ react-dom@17.0.2: object-assign "^4.1.1" scheduler "^0.20.2" +react-fast-compare@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" + integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== + +react-instantsearch-core@^6.12.0: + version "6.12.0" + resolved "https://registry.yarnpkg.com/react-instantsearch-core/-/react-instantsearch-core-6.12.0.tgz#f4bd044c0b5939d2129b95d3bf298e407a41a2b5" + integrity sha512-k7/sQLai5fhhiQxKFt7e3lA13SJpp843raQ6f8EtJL/bdFZPDHnOS3fPrxV/sduBkIrX7Hv+DrEOY1KkJGu2sA== + dependencies: + "@babel/runtime" "^7.1.2" + algoliasearch-helper "^3.5.3" + prop-types "^15.6.2" + react-fast-compare "^3.0.0" + +react-instantsearch-dom@^6.12.0: + version "6.12.0" + resolved "https://registry.yarnpkg.com/react-instantsearch-dom/-/react-instantsearch-dom-6.12.0.tgz#fb2a95643cfa896b4199131e70d0de308baaf932" + integrity sha512-/UMnmX0SPkvT8yOL2mAAfjz89ohGSZScb9X6hql9hydcS94cGoojysIUHJ/C+/1f8ciF2zni6I8AYddMdaqrSg== + dependencies: + "@babel/runtime" "^7.1.2" + algoliasearch-helper "^3.5.3" + classnames "^2.2.5" + prop-types "^15.6.2" + react-fast-compare "^3.0.0" + react-instantsearch-core "^6.12.0" + react-is@17.0.2, react-is@^17.0.1: version "17.0.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" @@ -12216,6 +12415,21 @@ typescript@4.3.5: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4" integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== +typesense-instantsearch-adapter@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/typesense-instantsearch-adapter/-/typesense-instantsearch-adapter-2.0.1.tgz#51c70120d36e05f698c4dd962f9347454d536c45" + integrity sha512-LsP0rb9pcZdEQr1QBH9g5kD34Us5qWIUAlyRy/vxy2KOT5wOUH28ElsByaZ6gFmtIyC9iOeGMigR9LQv3EW+Xg== + dependencies: + typesense "^0.14.0" + +typesense@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/typesense/-/typesense-0.14.0.tgz#c7a61ccadcd5186e3700a973188d6b93648b2fdd" + integrity sha512-9ZLkjwhCYXCZuYmvS6En5hJm3AMhFZMbZ7hRMVFJlyK3essNqd8MSWEt2bpAvbvTN+1FgGLBQPJnpYdsbkVsKg== + dependencies: + axios "^0.21.1" + loglevel "^1.7.1" + uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" From 0f12a6a13ce4912bdd7e84080cf3c2189c67a8c3 Mon Sep 17 00:00:00 2001 From: ekamuktia Date: Sun, 25 Jul 2021 15:01:26 +0700 Subject: [PATCH 02/12] change FaqData to Faq --- components/faq-list.tsx | 10 +++++----- components/search/custom-hits.tsx | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/components/faq-list.tsx b/components/faq-list.tsx index 7bad2c480..d58bd3cc0 100644 --- a/components/faq-list.tsx +++ b/components/faq-list.tsx @@ -1,9 +1,9 @@ -import { FaqData } from "~/lib/faq-databases"; +import { Faq } from "~/lib/faqs"; import htmr from "htmr"; -type FAQListProps = { - data: FaqData[]; +type FaqListProps = { + data: Faq[]; }; function groupBy(data: T[], key: U) { @@ -17,7 +17,7 @@ function groupBy(data: T[], key: U) { }, {}); } -export function FAQList(props: FAQListProps) { +export function FAQList(props: FaqListProps) { const listFaqs = groupBy(props.data, "kategori_pertanyaan"); return (
@@ -41,7 +41,7 @@ export function FAQList(props: FAQListProps) {
- {listFaqs[category].map((question: FaqData) => ( + {listFaqs[category].map((question: Faq) => (
Date: Sun, 25 Jul 2021 18:57:49 +0700 Subject: [PATCH 03/12] filter to modal --- components/search/custom-instant-search.tsx | 167 ++++++++++++++++++++ components/search/custom-search-box.tsx | 21 +++ components/search/refinement-modal.tsx | 118 ++++++++++++++ pages/faq.tsx | 27 ++-- 4 files changed, 315 insertions(+), 18 deletions(-) create mode 100644 components/search/custom-instant-search.tsx create mode 100644 components/search/refinement-modal.tsx diff --git a/components/search/custom-instant-search.tsx b/components/search/custom-instant-search.tsx new file mode 100644 index 000000000..6c930babd --- /dev/null +++ b/components/search/custom-instant-search.tsx @@ -0,0 +1,167 @@ +import { useCallback, useState } from "react"; + +import CustomHits from "~/components/search/custom-hits"; +import CustomSearchBox from "~/components/search/custom-search-box"; +import { RefinementModal } from "~/components/search/refinement-modal"; +import { getQueryParams } from "~/lib/string-utils"; + +import { useRouter } from "next/router"; +import { + Configure, + connectRefinementList, + SearchState, +} from "react-instantsearch-core"; +import { InstantSearch, InstantSearchProps } from "react-instantsearch-dom"; +import { debounce } from "ts-debounce"; + +type FilterSetting = { + field: string; + title: string; +}; + +interface CustomInstantSearchProps extends InstantSearchProps { + itemName: string; + filterSettings?: FilterSetting[]; +} + +const DEBOUNCE_TIME = 300; +const VirtualRefinementList = connectRefinementList(() => null); + +export function CustomInstantSearch({ + itemName, + filterSettings, + indexName, + searchClient, +}: CustomInstantSearchProps) { + const router = useRouter(); + const urlToSearchState = () => { + const searchParams: SearchState = {}; + if (typeof window !== "undefined") { + const queryParams: {} = getQueryParams(window.location.search); + if (Object.keys(queryParams).length) { + let keywordsParam: string = ""; + const filtersParam: any = {}; + const filterFields = filterSettings?.map((cur) => cur.field) ?? []; + Object.entries(queryParams).forEach(([key, value]) => { + if (key == "q") { + keywordsParam = value as string; + if (keywordsParam) { + searchParams.query = keywordsParam; + } + } else if (key == "sort") { + const sortParam: string = value as string; + if (sortParam) { + searchParams.sort = sortParam; + } + } else if (filterFields.includes(key)) { + filtersParam[key] = value ? [value] : []; + } + }); + if (Object.keys(filtersParam as {}).length) { + searchParams.refinementList = filtersParam; + } + } + } + return searchParams; + }; + + const [searchState, setSearchState] = useState(urlToSearchState()); + const [refinementList, setRefinementList] = useState<{ + [key: string]: string[]; + }>({}); + const [isFilterModalOpen, setFilterModalOpen] = useState(false); + + const createURL = (state: SearchState) => { + let isDefaultRoute: boolean = !state.query && state.page === 1; + if (filterSettings?.length) { + filterSettings.forEach((filterSetting) => { + isDefaultRoute = + isDefaultRoute && + state.refinementList?.[filterSetting.field].length === 0; + }); + } + + if (isDefaultRoute) { + return ""; + } + + const queryParameters: string[] = []; + + if (state.query) { + queryParameters.push(`q=${encodeURIComponent(state.query)}`); + } + if (filterSettings?.length) { + filterSettings.forEach((filterSetting) => { + if (state.refinementList?.[filterSetting.field]) { + queryParameters.push( + `${filterSetting.field}=${encodeURIComponent( + state.refinementList[filterSetting.field][0], + )}`, + ); + } + }); + } + + return `${router.basePath}${ + queryParameters.length ? `?${queryParameters.join("&")}` : `` + }`; + }; + + const searchStateToUrl = (searchStateParam: SearchState) => + createURL(searchStateParam); + + const debouncedUpdateUrlParams = useCallback( + debounce( + (updatedSearchState: SearchState) => + router.push(searchStateToUrl(updatedSearchState), undefined, { + shallow: true, + }), + DEBOUNCE_TIME, + ), + [], + ); + + const onSearchStateChange = (updatedSearchState: SearchState) => { + if (isFilterModalOpen && updatedSearchState.refinementList) { + setRefinementList(updatedSearchState.refinementList); + } else { + updatedSearchState.refinementList = refinementList; + } + void debouncedUpdateUrlParams(updatedSearchState); + setSearchState(updatedSearchState); + }; + + return ( + + + 0} + itemName={itemName} + onFilterButtonClick={() => setFilterModalOpen(true)} + /> + {filterSettings?.length && + filterSettings.map((filter, idx) => { + return ( + + ); + })} + + + + ); +} diff --git a/components/search/custom-search-box.tsx b/components/search/custom-search-box.tsx index 45366d8b5..fa7a655ba 100644 --- a/components/search/custom-search-box.tsx +++ b/components/search/custom-search-box.tsx @@ -4,12 +4,15 @@ import { FormGroup } from "~/components/ui/forms/form-group"; import { FormLabel } from "~/components/ui/forms/form-label"; import { InputText } from "~/components/ui/forms/input-text"; +import { FilterIcon } from "@heroicons/react/outline"; import { SearchBoxProvided } from "react-instantsearch-core"; import { connectSearchBox } from "react-instantsearch-dom"; interface CustomSearchBoxProvided extends SearchBoxProvided { readonly itemName: string; readonly placeholderText?: string; + readonly hasFilter?: boolean; + readonly onFilterButtonClick?: () => void; } function SearchBox({ @@ -17,6 +20,9 @@ function SearchBox({ refine, itemName, placeholderText, + isSearchStalled, + hasFilter, + onFilterButtonClick, }: CustomSearchBoxProvided) { return (
@@ -35,6 +41,21 @@ function SearchBox({ type="search" value={currentRefinement} /> + {!isSearchStalled && hasFilter && onFilterButtonClick && ( + + )}
diff --git a/components/search/refinement-modal.tsx b/components/search/refinement-modal.tsx new file mode 100644 index 000000000..f4d78e98a --- /dev/null +++ b/components/search/refinement-modal.tsx @@ -0,0 +1,118 @@ +import { Fragment, useRef } from "react"; + +import CustomRefinementList from "~/components/search/custom-refinement-list"; + +import { Dialog, Transition } from "@headlessui/react"; +import { XIcon } from "@heroicons/react/solid"; +import { RefinementItem } from "react-instantsearch-dom"; + +type FilterSetting = { + field: string; + title: string; +}; + +export interface RefinementModalProps { + isOpen: boolean; + onToggle: (value: boolean) => void; + filterSettings?: FilterSetting[]; + defaultRefinementList?: { [key: string]: string[] }; +} + +export function RefinementModal({ + isOpen, + onToggle, + filterSettings, + defaultRefinementList, +}: RefinementModalProps) { + const closeButtonRef = useRef(null); + const renderFilterForms = () => { + return ( + <> + {filterSettings?.length && ( +
+ {filterSettings.map((filterSetting, idx) => ( + []) => + items.sort((a, b) => a.label.localeCompare(b.label)) + } + /> + ))} +
+ )} + + ); + }; + + return ( + + +
+ + + + + {/* This element is to trick the browser into centering the modal contents. */} + + +
+
+ +
+
+
+ + Filter + +
{renderFilterForms()}
+
+
+
+
+
+
+
+ ); +} diff --git a/pages/faq.tsx b/pages/faq.tsx index 00dc818d9..e3c1ea3a3 100644 --- a/pages/faq.tsx +++ b/pages/faq.tsx @@ -2,13 +2,10 @@ import { BackButton } from "~/components/layout/back-button"; import { Page } from "~/components/layout/page"; import { PageContent } from "~/components/layout/page-content"; import { PageHeader } from "~/components/layout/page-header"; -import CustomHits from "~/components/search/custom-hits"; -import CustomRefinementList from "~/components/search/custom-refinement-list"; -import CustomSearchBox from "~/components/search/custom-search-box"; +import { CustomInstantSearch } from "~/components/search/custom-instant-search"; import { typesenseSearch } from "~/lib/typesense"; import { NextSeo } from "next-seo"; -import { InstantSearch, RefinementItem } from "react-instantsearch-dom"; const meta = { title: "Pertanyaan yang sering ditanyakan", @@ -18,7 +15,6 @@ export default function Faqs() { const searchClient = typesenseSearch({ queryBy: ["kategori_pertanyaan", "pertanyaan", "jawaban"], }); - return ( @@ -34,19 +30,14 @@ export default function Faqs() { title="Pertanyaan yang sering ditanyakan" /> - - -
- []) => - items.sort((a, b) => a.label.localeCompare(b.label)) - } - /> -
- -
+
); From 5894fbffcbee2bba56b87058361055fe57772e21 Mon Sep 17 00:00:00 2001 From: ekamuktia Date: Sun, 25 Jul 2021 19:18:51 +0700 Subject: [PATCH 04/12] change router to window --- components/search/custom-instant-search.tsx | 7 +++---- components/search/custom-search-box.tsx | 3 +-- package.json | 1 + 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/components/search/custom-instant-search.tsx b/components/search/custom-instant-search.tsx index 6c930babd..a60bcd77a 100644 --- a/components/search/custom-instant-search.tsx +++ b/components/search/custom-instant-search.tsx @@ -5,7 +5,7 @@ import CustomSearchBox from "~/components/search/custom-search-box"; import { RefinementModal } from "~/components/search/refinement-modal"; import { getQueryParams } from "~/lib/string-utils"; -import { useRouter } from "next/router"; +import Router from "next/router"; import { Configure, connectRefinementList, @@ -33,7 +33,6 @@ export function CustomInstantSearch({ indexName, searchClient, }: CustomInstantSearchProps) { - const router = useRouter(); const urlToSearchState = () => { const searchParams: SearchState = {}; if (typeof window !== "undefined") { @@ -102,7 +101,7 @@ export function CustomInstantSearch({ }); } - return `${router.basePath}${ + return `${window.location.pathname}${ queryParameters.length ? `?${queryParameters.join("&")}` : `` }`; }; @@ -113,7 +112,7 @@ export function CustomInstantSearch({ const debouncedUpdateUrlParams = useCallback( debounce( (updatedSearchState: SearchState) => - router.push(searchStateToUrl(updatedSearchState), undefined, { + Router.push(searchStateToUrl(updatedSearchState), undefined, { shallow: true, }), DEBOUNCE_TIME, diff --git a/components/search/custom-search-box.tsx b/components/search/custom-search-box.tsx index fa7a655ba..e958bac28 100644 --- a/components/search/custom-search-box.tsx +++ b/components/search/custom-search-box.tsx @@ -20,7 +20,6 @@ function SearchBox({ refine, itemName, placeholderText, - isSearchStalled, hasFilter, onFilterButtonClick, }: CustomSearchBoxProvided) { @@ -41,7 +40,7 @@ function SearchBox({ type="search" value={currentRefinement} /> - {!isSearchStalled && hasFilter && onFilterButtonClick && ( + {hasFilter && onFilterButtonClick && (