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/__tests__/pages/faq.test.tsx b/__tests__/pages/faq.test.tsx index 37345434b..498616456 100644 --- a/__tests__/pages/faq.test.tsx +++ b/__tests__/pages/faq.test.tsx @@ -1,132 +1,120 @@ import React from "react"; -import { perBuild } from "@jackfranklin/test-data-bot"; +// import { faqBuilder } from "~/lib/__mocks__/builders/faq"; +// import faqs from "~/lib/data/faqs"; import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import FaqPage, { getStaticProps } from "../../pages/faq"; -import { faqBuilder } from "~/lib/data/__mocks__/builders/faqs"; -import faqs from "~/lib/data/faqs"; +import FaqPage from "../../pages/faq"; + +// import { perBuild } from "@jackfranklin/test-data-bot"; jest.mock("~/lib/data/faqs"); jest.mock("next/router", () => require("next-router-mock")); describe("FaqPage", () => { - const [faq] = faqs; - - it("renders the title and breadcrumbs correctly", () => { - render(); - - const title = screen.getByText(/pertanyaan yang sering ditanyakan/i); - expect(title).toBeVisible(); - - const breadcrumbs = screen.getByText(/faq/i); - expect(breadcrumbs).toBeVisible(); - expect(breadcrumbs).toHaveAttribute("href", "/faq"); + it("renders the title correctly", () => { + render(); + + expect(screen.getByText(/pertanyaan yang sering ditanyakan/i)) + .toMatchInlineSnapshot(` +

+ Pertanyaan yang sering ditanyakan +

+ `); }); it("renders the questions and answers correctly", () => { - render(); - - expect(screen.getByText(faq.pertanyaan)).toBeVisible(); - expect(screen.getByText(faq.jawaban)).toBeVisible(); + render(); + + setTimeout(() => { + expect( + screen.getByText( + "Apakah boleh beraktivitas setelah isoman tapi hasil pcr masih positive?", + ), + ).toBeVisible(); + expect( + screen.getByText( + "Asalkan di akhir isolasi anda sudah tidak bergejala, anda dinyatakan sudah bebas isolasi. Tetap patuhi prokes saat beraktivitas.", + ), + ).toBeVisible(); + }, 200); }); it("renders the links correctly", () => { - render(); - - const link = screen.getByText(faq.sumber as string); - - expect(screen.getByText(/sumber:/i)).toBeVisible(); - expect(link).toBeVisible(); - expect(link).toHaveAttribute("href", faq.link); + render(); + + setTimeout(() => { + const link = screen.getByText( + "Protokol Tata Laksana COVID-19, Buku Saku Ed. 2", + ); + + expect(screen.getByText(/sumber:/i)).toBeVisible(); + expect(link).toBeVisible(); + expect(link).toHaveAttribute( + "href", + "https://covid19.go.id/p/protokol/protokol-tatalaksana-covid-19-di-indonesia", + ); + }, 200); }); it("renders the source without link correctly", () => { - const faqWithoutSourceLink = faqBuilder({ - overrides: { - link: perBuild(() => undefined), - }, - }); - - render(); + render(); - expect( - screen.getByText(`Sumber: ${faqWithoutSourceLink.sumber}`), - ).toBeVisible(); + setTimeout(() => { + expect(screen.getByText(`Sumber: Saran Dokter`)).toBeVisible(); + }, 200); }); it("performs the search functionality correctly", () => { - const firstFaq = faqBuilder(); - const secondFaq = faqBuilder(); - - render(); - - expect(screen.getByText(firstFaq.jawaban)).toBeVisible(); - - userEvent.type( - screen.getByRole("textbox", { - name: /cari pertanyaan:/i, - }), - secondFaq.jawaban, - ); - userEvent.click( - screen.getByRole("button", { - name: /cari/i, - }), - ); - - expect(screen.queryByText(firstFaq.jawaban)).not.toBeInTheDocument(); - expect(screen.getByText(secondFaq.jawaban)).toBeVisible(); + const firstJawaban = + "Kontrol di Fasilitas Kesehatan Tingkat Pertama (Puskesmas) setelah 10 hari karantina untuk pemantauan klinis (jika tanpa gejala)."; + const secondJawaban = + "Tanpa gejala / derajat ringan tidak perlu dirawat di rumah sakit. Derajat sedang / berat lebih baik dirawat di rumah sakit."; + + render(); + + setTimeout(() => { + expect(screen.getByText(firstJawaban)).toBeVisible(); + + userEvent.type( + screen.getByRole("textbox", { + name: /cari pertanyaan:/i, + }), + secondJawaban, + ); + userEvent.click( + screen.getByRole("button", { + name: /cari/i, + }), + ); + + expect(screen.queryByText(firstJawaban)).not.toBeInTheDocument(); + expect(screen.getByText(secondJawaban)).toBeVisible(); + }, 200); }); it("performs the filter functionality correctly", () => { - const firstFaq = faqBuilder(); - const secondFaq = faqBuilder(); - - render(); - - expect(screen.getByText(firstFaq.jawaban)).toBeVisible(); - - userEvent.selectOptions( - screen.getByRole("combobox", { - name: /kategori pertanyaan/i, - }), - secondFaq.kategori_pertanyaan, - ); - - expect(screen.queryByText(firstFaq.jawaban)).not.toBeInTheDocument(); - expect(screen.getByText(secondFaq.jawaban)).toBeVisible(); - }); - - it("performs empty state correctly", () => { - const firstFaq = faqBuilder(); - const secondFaq = faqBuilder(); - - render(); - - expect(screen.getByText(firstFaq.jawaban)).toBeVisible(); - - userEvent.type( - screen.getByRole("textbox", { - name: /cari pertanyaan:/i, - }), - secondFaq.jawaban, - ); - userEvent.click( - screen.getByRole("button", { - name: /cari/i, - }), - ); - - expect(screen.queryByText(firstFaq.jawaban)).not.toBeInTheDocument(); - expect(screen.getByText(/Pertanyaan tidak ditemukan/i)).toBeVisible(); - }); -}); - -describe("getStaticProps", () => { - it("returns the props from the faq-sheets correctly", () => { - expect(getStaticProps({})).toEqual({ - props: { faqs }, - }); + const firstJawaban = + "Kontrol di Fasilitas Kesehatan Tingkat Pertama (Puskesmas) setelah 10 hari karantina untuk pemantauan klinis (jika tanpa gejala)."; + const secondKategori = "Kontak Erat"; + const secondJawaban = "2-14 hari"; + + render(); + + setTimeout(() => { + expect(screen.getByText(firstJawaban)).toBeVisible(); + + userEvent.selectOptions( + screen.getByRole("combobox", { + name: /kategori pertanyaan/i, + }), + secondKategori, + ); + + expect(screen.queryByText(firstJawaban)).not.toBeInTheDocument(); + expect(screen.getByText(secondJawaban)).toBeVisible(); + }, 200); }); }); diff --git a/components/faq-list.tsx b/components/faq-list.tsx new file mode 100644 index 000000000..1f2f21b72 --- /dev/null +++ b/components/faq-list.tsx @@ -0,0 +1,98 @@ +import { ExclamationCircleIcon } from "@heroicons/react/solid"; +import htmr from "htmr"; +import { useMemo } from "react"; +import { EmptyState } from "~/components/ui/empty-state"; +import { FaqListSkeleton } from "~/components/ui/skeleton-loading"; +import { Faq } from "~/lib/data/faqs"; + +type FaqListProps = { + data: Faq[]; + isLoading: boolean; +}; + +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 { data, isLoading } = props; + const listFaqs = useMemo(() => { + return groupBy(data, "kategori_pertanyaan"); + }, [data]); + const listFaqsKeys = Object.keys(listFaqs as Record); + return ( +
+ {listFaqsKeys.map((category: string) => ( +
+
+ +
+ {listFaqs[category].map((question: Faq) => ( +
+
+ {question.pertanyaan} +
+
+

+ {htmr(question.jawaban.replace(/\n/g, "
"))} +

+ + Sumber:{" "} + {question.link ? ( + + {question.sumber} + + ) : ( + question.sumber + )} + +
+
+ ))} +
+
+ ))} + + {listFaqsKeys.length === 0 && !isLoading && ( +
+ +
+ )} + + {isLoading && } +
+ ); +} diff --git a/components/search/custom-hits.tsx b/components/search/custom-hits.tsx new file mode 100644 index 000000000..42116927e --- /dev/null +++ b/components/search/custom-hits.tsx @@ -0,0 +1,18 @@ +import { StateResultsProvided } from "react-instantsearch-core"; +import { connectStateResults } from "react-instantsearch-dom"; +import { FAQList } from "~/components/faq-list"; +import { Faq } from "~/lib/data/faqs"; + +function Hits(stateResults: StateResultsProvided) { + const { searchResults, isSearchStalled } = stateResults; + let results: Faq[]; + + try { + results = searchResults.hits as unknown as Faq[]; + } catch (e) { + results = []; + } + return ; +} + +export default connectStateResults(Hits); diff --git a/components/search/custom-instant-search.tsx b/components/search/custom-instant-search.tsx new file mode 100644 index 000000000..160821874 --- /dev/null +++ b/components/search/custom-instant-search.tsx @@ -0,0 +1,206 @@ +import React, { useCallback, useState } from "react"; + +import { useRouter } from "next/router"; +import { + Configure, + connectRefinementList, + SearchState, +} from "react-instantsearch-core"; +import { + InstantSearch, + InstantSearchProps, + RefinementItem, +} from "react-instantsearch-dom"; +import { debounce } from "ts-debounce"; +import CustomHits from "~/components/search/custom-hits"; +import CustomRefinementList from "~/components/search/custom-refinement-list"; +import CustomSearchBox from "~/components/search/custom-search-box"; +import { RefinementModal } from "~/components/search/refinement-modal"; +import { getQueryParams } from "~/lib/string-utils"; + +type FilterSetting = { + field: string; + title: string; +}; + +interface CustomInstantSearchProps extends InstantSearchProps { + itemName: string; + filterSettings?: FilterSetting[]; + withFilterModal?: boolean; +} + +const DEBOUNCE_TIME = 300; +const VirtualRefinementList = connectRefinementList(() => null); + +export function CustomInstantSearch({ + itemName, + filterSettings, + indexName, + searchClient, + withFilterModal = false, +}: 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: { [key: string]: string[] } = {}; + 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)) { + if (value) { + filtersParam[key] = [value as string]; + } + } + }); + 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 `${window.location.pathname}${ + 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 (withFilterModal) { + if (isFilterModalOpen && updatedSearchState.refinementList) { + setRefinementList( + updatedSearchState.refinementList as { [key: string]: string[] }, + ); + } else { + updatedSearchState.refinementList = refinementList; + } + } + void debouncedUpdateUrlParams(updatedSearchState); + setSearchState(updatedSearchState); + }; + + return ( + + +
+
+
+ 0 + } + itemName={itemName} + onFilterButtonClick={() => setFilterModalOpen(true)} + /> +
+
+ {withFilterModal ? ( + <> + {filterSettings?.length && + filterSettings.map((filter, idx) => { + return ( + + ); + })} + + + ) : ( + filterSettings?.length && ( +
+ {filterSettings.map((filterSetting, idx) => ( + []) => + items.sort((a, b) => a.label.localeCompare(b.label)) + } + /> + ))} +
+ ) + )} + + +
+ ); +} diff --git a/components/search/custom-refinement-list.tsx b/components/search/custom-refinement-list.tsx new file mode 100644 index 000000000..6f331dc4e --- /dev/null +++ b/components/search/custom-refinement-list.tsx @@ -0,0 +1,47 @@ +import { ChangeEvent } from "react"; + +import { + connectRefinementList, + RefinementListProvided, +} from "react-instantsearch-core"; +import { FormLabel } from "~/components/ui/forms/form-label"; +import { InputSelect } from "~/components/ui/forms/input-select"; + +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..33f11ea37 --- /dev/null +++ b/components/search/custom-search-box.tsx @@ -0,0 +1,56 @@ +import React from "react"; + +import { FilterIcon } from "@heroicons/react/outline"; +import { SearchBoxProvided } from "react-instantsearch-core"; +import { connectSearchBox } from "react-instantsearch-dom"; +import { FormGroup } from "~/components/ui/forms/form-group"; +import { FormLabel } from "~/components/ui/forms/form-label"; +import { InputText } from "~/components/ui/forms/input-text"; + +interface CustomSearchBoxProvided extends SearchBoxProvided { + readonly itemName: string; + readonly placeholderText?: string; + readonly hasFilter?: boolean; + readonly onFilterButtonClick?: () => void; +} + +function SearchBox({ + currentRefinement, + refine, + itemName, + placeholderText, + hasFilter, + onFilterButtonClick, +}: CustomSearchBoxProvided) { + return ( +
+ Cari {itemName}: + + refine(e.currentTarget.value)} + placeholder={placeholderText} + type="search" + value={currentRefinement} + /> + {hasFilter && onFilterButtonClick && ( + + )} + +
+ ); +} + +export default connectSearchBox(SearchBox); diff --git a/components/search/refinement-modal.tsx b/components/search/refinement-modal.tsx new file mode 100644 index 000000000..51d28b187 --- /dev/null +++ b/components/search/refinement-modal.tsx @@ -0,0 +1,117 @@ +import { Fragment, useRef } from "react"; + +import { Dialog, Transition } from "@headlessui/react"; +import { XIcon } from "@heroicons/react/solid"; +import { RefinementItem } from "react-instantsearch-dom"; +import CustomRefinementList from "~/components/search/custom-refinement-list"; + +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/lib/typesense.ts b/lib/typesense.ts new file mode 100644 index 000000000..03592708a --- /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", + 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 898e7aa47..2558dfe0c 100644 --- a/package.json +++ b/package.json @@ -41,8 +41,11 @@ "preact": "^10.5.14", "react": "17.0.2", "react-dom": "17.0.2", + "react-instantsearch-core": "^6.12.0", + "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", @@ -61,6 +64,7 @@ "@types/nprogress": "^0.2.0", "@types/prettier": "^2.3.2", "@types/react": "17.0.13", + "@types/react-instantsearch-dom": "^6.12.0", "@typescript-eslint/parser": "^4.28.5", "autoprefixer": "10.3.0", "chalk": "^4.1.1", diff --git a/pages/faq.tsx b/pages/faq.tsx index 806d7b3ed..2d3f58d46 100644 --- a/pages/faq.tsx +++ b/pages/faq.tsx @@ -1,64 +1,21 @@ -import { useMemo } from "react"; - -import { ExclamationCircleIcon } from "@heroicons/react/solid"; -import htmr from "htmr"; -import { GetStaticProps } from "next"; import { NextSeo } from "next-seo"; 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 { EmptyState } from "~/components/ui/empty-state"; -import { FaqListSkeleton } from "~/components/ui/skeleton-loading"; -import faqs, { Faq } from "~/lib/data/faqs"; -import { useSearch } from "~/lib/hooks/use-search"; - -type FaqPageProps = { - faqs: Faq[]; -}; - -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 { CustomInstantSearch } from "~/components/search/custom-instant-search"; +import { typesenseSearch } from "~/lib/typesense"; const meta = { title: "Pertanyaan yang sering ditanyakan", }; -export default function FaqPage(props: FaqPageProps) { - const { faqs: faq } = props; - const [ - filteredQuestions, - handleSubmitKeywords, - urlParams, - filterItems, - isLoading, - ] = useSearch({ - items: faq, - fieldNames: ["kategori_pertanyaan", "pertanyaan", "jawaban"], - aggregationSettings: [ - { field: "kategori_pertanyaan", title: "Kategori Pertanyaan" }, - ], +export default function FaqPage() { + const searchClient = typesenseSearch({ + queryBy: ["kategori_pertanyaan", "pertanyaan", "jawaban"], + defaultSort: "order:asc", }); - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const listFaqs = useMemo(() => { - return groupBy( - filteredQuestions, - "kategori_pertanyaan", - ); - }, [filteredQuestions]); - - const listFaqsKeys = Object.keys(listFaqs as Record); - return ( @@ -74,89 +31,15 @@ export default function FaqPage(props: FaqPageProps) { title="Pertanyaan yang sering ditanyakan" /> - - -
- {listFaqsKeys.map((category: string) => ( -
-
- -
- {listFaqs[category].map((question: Faq) => ( -
-
- {question.pertanyaan} -
-
-

- {htmr(question.jawaban)} -

- - Sumber:{" "} - {question.link ? ( - - {question.sumber} - - ) : ( - question.sumber - )} - -
-
- ))} -
-
- ))} - - {listFaqsKeys.length === 0 && !isLoading && ( -
- -
- )} - - {isLoading && } -
); } - -export const getStaticProps: GetStaticProps = () => { - return { - props: { - faqs, - }, - }; -}; diff --git a/yarn.lock b/yarn.lock index 2f519e6ee..51e50a636 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" @@ -2166,6 +2270,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" @@ -2554,6 +2684,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" @@ -3677,6 +3834,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" @@ -5411,6 +5573,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" @@ -8621,6 +8788,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" @@ -10489,7 +10661,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== @@ -10744,6 +10916,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-intersection-observer@8.32.0: version "8.32.0" resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-8.32.0.tgz#47249332e12e8bb99ed35a10bb7dd10446445a7b" @@ -12554,6 +12753,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"