Skip to content

Commit

Permalink
feat: add persistence to search tables (#86)
Browse files Browse the repository at this point in the history
Closes #84

Filters are now persisted locally.
  • Loading branch information
PupoSDC authored Jan 20, 2024
1 parent c1a773c commit cf1b006
Show file tree
Hide file tree
Showing 15 changed files with 386 additions and 209 deletions.
14 changes: 0 additions & 14 deletions libs/react/components/src/cta-search/cta-search.stories.tsx

This file was deleted.

2 changes: 0 additions & 2 deletions libs/react/components/src/cta-search/index.ts

This file was deleted.

15 changes: 8 additions & 7 deletions libs/react/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,30 @@ import "@chair-flight/base/types";
export * from "./app-buttons";
export * from "./app-head";
export * from "./app-logo";
export * from "./background-faded-image";
export * from "./background-sliding-images";
export * from "./blog-post-chip";
export * from "./container-wrapper";
export * from "./background-sliding-images";
export * from "./background-faded-image";
export * from "./count-up";
export * from "./cta-search";
export * from "./flashcard";
export * from "./flashcard-tinder";
export * from "./hook-form";
export * from "./hooks/use-disclose";
export * from "./hooks/use-media-query";
export * from "./hooks/use-window-resize";
export * from "./hooks/use-disclose";
export * from "./module-selection-button";
export * from "./image-viewer/image-viewer";
export * from "./input-slider";
export * from "./markdown-client";
export * from "./module-selection-button";
export * from "./nested-checkbox-select";
export * from "./question-list";
export * from "./question-multiple-choice";
export * from "./question-navigation";
export * from "./question-variant-preview";
export * from "./question-list";
export * from "./search-filters";
export * from "./search-query";
export * from "./sidebar";
export * from "./test-preview";
export * from "./question-navigation";
export * from "./test-question-result";
export * from "./theme";
export * from "./toaster";
Expand Down
1 change: 1 addition & 0 deletions libs/react/components/src/search-filters/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { SearchFilters } from "./search-filters";
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Select, Option } from "@mui/joy";
import { SearchFilters } from "./search-filters";
import type { SearchFiltersProps } from "./search-filters";
import type { Meta, StoryObj } from "@storybook/react";

type Story = StoryObj<typeof SearchFilters>;

export const Playground: Story = {
args: {
filters: "2 selects" as unknown as SearchFiltersProps["filters"],
fallback: "2 selects" as unknown as SearchFiltersProps["fallback"],
},
};

const meta: Meta<typeof SearchFilters> = {
title: "Components/SearchFilters",
component: SearchFilters,
tags: ["autodocs"],
argTypes: {
filters: {
control: { type: "radio" },
options: ["2 selects"],
mapping: {
"2 selects": (
<>
<Select>
<Option value="Potatos">Potatos</Option>
<Option value="Kiwis">Kiwis</Option>
<Option value="Banas">Banas</Option>
</Select>
<Select>
<Option value="Batman">Batman</Option>
<Option value="Superman">Superman</Option>
<Option value="Kiwiman">Kiwiman</Option>
</Select>
</>
),
},
},
fallback: {
control: { type: "radio" },
options: ["2 selects"],
mapping: {
"2 selects": (
<>
<Select />
<Select />
</>
),
},
},
},
};

export default meta;
77 changes: 77 additions & 0 deletions libs/react/components/src/search-filters/search-filters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { useEffect, useState } from "react";
import { NoSsr } from "@mui/base";
import { default as FilterIcon } from "@mui/icons-material/FilterAltOutlined";
import {
Badge,
Button,
Divider,
IconButton,
Modal,
ModalClose,
ModalDialog,
Stack,
Typography,
} from "@mui/joy";
import { useDisclose } from "../hooks/use-disclose";
import type { FC, ReactNode } from "react";

export type SearchFiltersProps = {
filters: ReactNode;
fallback: ReactNode;
activeFilters: number;
mobileBreakpoint?: "sm" | "md" | "lg";
};

/**
* Opinionated component to display search filters. Includes a `NoSsr`boundary
* to make it safe to render when filter information is persisted client side.
*
* Provided `fallback`should closely mirror `filters`.
*/
export const SearchFilters: FC<SearchFiltersProps> = ({
filters: filters,
fallback,
activeFilters,
mobileBreakpoint = "md",
}) => {
const filterModal = useDisclose();
const [deferredActiveFilters, setDeferredActiveFilters] = useState(0);

useEffect(() => setDeferredActiveFilters(activeFilters), [activeFilters]);

return (
<>
<Stack
direction={"row"}
gap={1}
sx={{ display: { xs: "none", [mobileBreakpoint]: "flex" } }}
>
<NoSsr fallback={fallback}>{filters}</NoSsr>
</Stack>
<IconButton
size="sm"
variant="outlined"
color="neutral"
onClick={filterModal.open}
sx={{ display: { [mobileBreakpoint]: "none" } }}
>
<Badge badgeContent={deferredActiveFilters} size="sm">
<FilterIcon />
</Badge>
</IconButton>
<Modal open={filterModal.isOpen} onClose={filterModal.close}>
<ModalDialog aria-labelledby="filter-modal">
<ModalClose />
<Typography id="filter-modal" level="h2">
Filters
</Typography>
<Divider sx={{ my: 2 }} />
<Stack gap={2}>{filters}</Stack>
<Button color="primary" onClick={filterModal.close}>
Submit
</Button>
</ModalDialog>
</Modal>
</>
);
};
2 changes: 2 additions & 0 deletions libs/react/components/src/search-query/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { SearchQuery } from "./search-query";
export type { SearchQueryProps } from "./search-query";
14 changes: 14 additions & 0 deletions libs/react/components/src/search-query/search-query.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { SearchQuery } from "./search-query";
import type { Meta, StoryObj } from "@storybook/react";

type Story = StoryObj<typeof SearchQuery>;

export const Playground: Story = {};

const meta: Meta<typeof SearchQuery> = {
title: "Components/SearchQuery",
component: SearchQuery,
tags: ["autodocs"],
};

export default meta;
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { render, screen, waitFor } from "@testing-library/react";
import { default as userEvent } from "@testing-library/user-event";
import { CtaSearch } from "./cta-search";
import { SearchQuery } from "./search-query";

describe("CtaSearch", () => {
describe("SearchQuery", () => {
it("debounces fast typing", async () => {
const onChange = vi.fn();
render(<CtaSearch onChange={onChange} value={""} />);
render(<SearchQuery onChange={onChange} value={""} />);
const input = screen.getByRole("search");
await userEvent.type(input, "123", { delay: 50 });

Expand All @@ -16,7 +16,7 @@ describe("CtaSearch", () => {

it("does not debounce slow typing", async () => {
const onChange = vi.fn();
render(<CtaSearch onChange={onChange} value={""} />);
render(<SearchQuery onChange={onChange} value={""} />);
const input = screen.getByRole("search");
await userEvent.type(input, "123", { delay: 300 });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { default as SearchIcon } from "@mui/icons-material/Search";
import { CircularProgress, Input } from "@mui/joy";
import type { InputProps } from "@mui/joy";

export type CtaSearchProps = {
export type SearchQueryProps = {
value: string;
onChange: (value: string) => void;
loading?: boolean;
Expand All @@ -16,7 +16,7 @@ export type CtaSearchProps = {
* It uses a debounce to avoid sending too many requests to the API, and displays
* a fake loading spinner as soon as the user starts typing.
*/
export const CtaSearch = forwardRef<HTMLInputElement, CtaSearchProps>(
export const SearchQuery = forwardRef<HTMLInputElement, SearchQueryProps>(
(
{
value,
Expand Down Expand Up @@ -79,4 +79,4 @@ export const CtaSearch = forwardRef<HTMLInputElement, CtaSearchProps>(
},
);

CtaSearch.displayName = "CtaSearch";
SearchQuery.displayName = "SearchQuery";
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createUsePersistenceHook } from "../../hooks/use-persistence";
import type { QuestionBankName } from "@chair-flight/base/types";
import type { UseFormReturn } from "react-hook-form";

const searchConfigSchema = z.object({
searchField: z.enum(["text", "id", "all"]),
subject: z.string(),
course: z.enum([
"ATPL_A",
"CPL_A",
"ATPL_H_IR",
"ATPL_H_VFR",
"CPL_H",
"IR",
"CBIR_A",
"all",
]),
});

type SearchConfig = z.infer<typeof searchConfigSchema>;

const defaultSearchConfig: z.infer<typeof searchConfigSchema> = {
searchField: "all",
subject: "all",
course: "all",
};

const searchPersistence = {
"cf-learning-objective-search-atpl": createUsePersistenceHook<SearchConfig>(
"cf-learning-objective-search-atpl",
),
"cf-learning-objective-search-type": createUsePersistenceHook<SearchConfig>(
"cf-learning-objective-search-type",
),
"cf-learning-objective-search-prep": createUsePersistenceHook<SearchConfig>(
"cf-learning-objective-search-prep",
),
};

const resolver = zodResolver(searchConfigSchema);

export const useSearchConfig = (
questionBank: QuestionBankName,
): [SearchConfig, UseFormReturn<SearchConfig>] => {
const key = `cf-learning-objective-search-${questionBank}` as const;
const useSearchPersistence = searchPersistence[key];
const { persistedData, setPersistedData } = useSearchPersistence();
const defaultValues = persistedData ?? defaultSearchConfig;
const form = useForm({ defaultValues, resolver });
const searchField = form.watch("searchField");
const course = form.watch("course");
const subject = form.watch("subject");

useEffect(() => {
setPersistedData({ searchField, course, subject });
}, [searchField, course, subject, setPersistedData]);

return [{ searchField, course, subject }, form];
};
Loading

1 comment on commit cf1b006

@vercel
Copy link

@vercel vercel bot commented on cf1b006 Jan 20, 2024

Choose a reason for hiding this comment

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

Please sign in to comment.