Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate FAQ page into the Typesense backend #401

Draft
wants to merge 25 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9ef8171
init faq typesense
ekamuktia Jul 25, 2021
67b7b95
Merge remote-tracking branch 'origin/main' into feature/typesense-faq
ekamuktia Jul 25, 2021
94b56ec
Merge branch 'main' into feature/typesense-faq
ekamuktia Jul 25, 2021
0f12a6a
change FaqData to Faq
ekamuktia Jul 25, 2021
a817d3d
Merge branch 'feature/typesense-faq' of https://github.com/ekamuktia/…
ekamuktia Jul 25, 2021
a81eacc
filter to modal
ekamuktia Jul 25, 2021
5894fbf
change router to window
ekamuktia Jul 25, 2021
9283a2e
filter params type
ekamuktia Jul 25, 2021
b32bd9c
remove modal
ekamuktia Jul 25, 2021
cb2a1da
Merge branch 'main' into feature/typesense-faq
ekamuktia Jul 25, 2021
81034e4
change to usetouter
ekamuktia Jul 25, 2021
c7c61b2
Merge branch 'feature/typesense-faq' of https://github.com/ekamuktia/…
ekamuktia Jul 25, 2021
fb896f7
usefiltermodal as config, console log for debug
ekamuktia Jul 25, 2021
25a7800
usefiltermodal as optional
ekamuktia Jul 25, 2021
7dda93a
usefiltermodal false
ekamuktia Jul 25, 2021
f5a0a0f
Merge remote-tracking branch 'origin/main' into feature/typesense-faq
ekamuktia Jul 26, 2021
ebf6efb
Merge branch 'main' of https://github.com/ekamuktia/wargabantuwarga.c…
ekamuktia Jul 27, 2021
aaac2f1
fix: refinement list select option value
ekamuktia Jul 27, 2021
6f35e72
fix: remove console log
ekamuktia Jul 27, 2021
de04d15
Merge branch 'main' of https://github.com/ekamuktia/wargabantuwarga.c…
ekamuktia Jul 28, 2021
96cb318
Merge branch 'main' into feature/typesense-faq
ekamuktia Jul 28, 2021
2ab64b7
Merge branch 'main' into feature/typesense-faq
ekamuktia Jul 28, 2021
69aebe7
Merge branch 'main' of https://github.com/ekamuktia/wargabantuwarga.c…
ekamuktia Aug 8, 2021
6a761e2
Merge branch 'feature/typesense-faq' of https://github.com/ekamuktia/…
ekamuktia Aug 8, 2021
c719fb6
Merge branch 'main' into feature/typesense-faq
mazipan Aug 9, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions @types/typesense-instantsearch-adapter/index.d.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
198 changes: 93 additions & 105 deletions __tests__/pages/faq.test.tsx
Original file line number Diff line number Diff line change
@@ -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";
Comment on lines +3 to +4

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commented code should not be pushed.

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";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commented code should not be pushed.


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(<FaqPage faqs={faqs} />);

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(<FaqPage />);

expect(screen.getByText(/pertanyaan yang sering ditanyakan/i))
.toMatchInlineSnapshot(`
<h1
class="text-xl font-bold leading-7 text-gray-900 sm:text-2xl sm:truncate"
>
Pertanyaan yang sering ditanyakan
</h1>
`);
});

it("renders the questions and answers correctly", () => {
render(<FaqPage faqs={faqs} />);

expect(screen.getByText(faq.pertanyaan)).toBeVisible();
expect(screen.getByText(faq.jawaban)).toBeVisible();
render(<FaqPage />);

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(<FaqPage faqs={faqs} />);

const link = screen.getByText(faq.sumber as string);

expect(screen.getByText(/sumber:/i)).toBeVisible();
expect(link).toBeVisible();
expect(link).toHaveAttribute("href", faq.link);
render(<FaqPage />);

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(<FaqPage faqs={[faqWithoutSourceLink]} />);
render(<FaqPage />);

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(<FaqPage faqs={[firstFaq, secondFaq]} />);

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(<FaqPage />);

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(<FaqPage faqs={[firstFaq, secondFaq]} />);

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(<FaqPage faqs={[firstFaq]} />);

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(<FaqPage />);

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);
});
});
98 changes: 98 additions & 0 deletions components/faq-list.tsx
Original file line number Diff line number Diff line change
@@ -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<T, U>(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<Faq | unknown, string>(data, "kategori_pertanyaan");
}, [data]);
const listFaqsKeys = Object.keys(listFaqs as Record<string, unknown>);
return (
<div className="space-y-4">
{listFaqsKeys.map((category: string) => (
<div
key={category}
className="p-4 bg-white shadow overflow-hidden rounded-md"
>
<div className="relative">
<div
aria-hidden="true"
className="absolute inset-0 flex items-center"
>
<div className="w-full border-t border-gray-300" />
</div>
<div className="relative flex flex-row items-center justify-start">
<span className="pr-3 bg-white text-lg font-medium text-gray-900">
{category}
</span>
</div>
</div>
<dl className="divide-y divide-gray-200">
{listFaqs[category].map((question: Faq) => (
<div
key={question.pertanyaan}
className="pt-6 pb-8 md:grid md:grid-cols-12 md:gap-8"
>
<dt className="text-base font-semibold text-gray-900 md:col-span-5">
{question.pertanyaan}
</dt>
<dd className="space-y-4 mt-2 md:mt-0 md:col-span-7">
<p className="text-base text-gray-500">
{htmr(question.jawaban.replace(/\n/g, "<br />"))}
</p>
<small className="block">
Sumber:{" "}
{question.link ? (
<a
className="underline text-blue-800"
href={question.link}
rel="noreferrer"
target="_blank"
>
{question.sumber}
</a>
) : (
question.sumber
)}
</small>
</dd>
</div>
))}
</dl>
</div>
))}

{listFaqsKeys.length === 0 && !isLoading && (
<div className="px-4">
<EmptyState
description="Silakan gunakan kata kunci pencarian lainnya."
icon={ExclamationCircleIcon}
title="Pertanyaan tidak ditemukan"
/>
</div>
)}

{isLoading && <FaqListSkeleton />}
</div>
);
}
18 changes: 18 additions & 0 deletions components/search/custom-hits.tsx
Original file line number Diff line number Diff line change
@@ -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 <FAQList data={results} isLoading={isSearchStalled} />;
}

export default connectStateResults(Hits);
Loading