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

Use ReactiveComponent in Search #86

Merged
merged 9 commits into from
Jul 26, 2022
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"react-youtube": "^7.14.0",
"setimmediate": "^1.0.5",
"typescript": "4.6.3",
"use-debounce": "^8.0.2",
"workbox-background-sync": "^5.1.3",
"workbox-broadcast-update": "^5.1.3",
"workbox-cacheable-response": "^5.1.3",
Expand Down
5 changes: 3 additions & 2 deletions src/components/nav/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ import { useTranslation } from "react-i18next";
import { FiMenu, FiChevronDown } from "react-icons/fi";
import { Link } from "react-router-dom";
import { useClient } from "../../modules/client";
import { Searchbox } from "./Searchbox";
import { SearchBox } from "./SearchBox";
import { LogoWithText } from "./LogoWithText";

interface MobileProps extends FlexProps {
onOpen: () => void;
}

export function NavBar({ onOpen, ...rest }: MobileProps) {
const { t } = useTranslation();
const { isLoggedIn, logout, user } = useClient();
Expand Down Expand Up @@ -57,7 +58,7 @@ export function NavBar({ onOpen, ...rest }: MobileProps) {
/>
<LogoWithText display={{ base: "none", lg: "flex" }} />

<Searchbox w={{ base: "100%", lg: "40%" }} paddingX={4} />
<SearchBox w={{ base: "100%", lg: "40%" }} paddingX={4} />

<HStack spacing={{ base: "0", lg: "6" }}>
<Flex alignItems={"center"}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,24 @@ import { RiSearch2Line } from "react-icons/ri";
import { useNavigate } from "react-router";
import { useSearchParams } from "react-router-dom";

export function Searchbox(props: BoxProps) {
export function SearchBox(props: BoxProps) {
const { t } = useTranslation();
let [isFocused, setFocused] = useState(false);
let navigate = useNavigate();
let [searchParams] = useSearchParams();
let [currentValue, setValue] = useState("");
const input = useRef<any>();
const input = useRef<HTMLInputElement>(null);

useEffect(() => {
const q = searchParams.get("q");
const prettyValue = q ? JSON.parse(q) : "";
const prettyValue: string = q ? JSON.parse(q) : "";
setValue(prettyValue);
input.current.value = prettyValue;
input.current!.value = prettyValue;
}, [searchParams]);

useHotkeys("ctrl+l, cmd+l, cmd+alt+f", (e) => {
e.preventDefault();
input.current.focus();
input.current!.focus();
});

const submitHandler = (e: FormEvent) => {
Expand Down
116 changes: 116 additions & 0 deletions src/components/search/CheckboxSearchList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import {
Checkbox,
CheckboxGroup,
IconButton,
Input,
InputGroup,
InputRightElement,
Tag,
VStack,
} from "@chakra-ui/react";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { RiCloseFill } from "react-icons/ri";

interface CheckboxSearchListProps {
dataField: string;
placeholder?: string;
showSearch?: boolean;
aggregations: {
[k: string]: { buckets: Array<{ key: string; doc_count: number }> };
};
setQuery: (query: any) => void;
value: string[] | null;
tagLabel?: string;
}

export const CheckboxSearchList = ({
dataField,
placeholder,
showSearch = false,
aggregations,
setQuery,
value,
tagLabel,
}: CheckboxSearchListProps) => {
const { t } = useTranslation();
const [filterValue, setFilterValue] = useState("");
const [checkboxValues, setCheckboxValues] = useState<Array<string | number>>(
value!,
);

const getTermsQuery = useCallback(
(values: typeof checkboxValues) => {
if (!values?.length) return {};

return { query: { terms: { [dataField]: values } } };
},
[dataField],
);

useEffect(() => {
setQuery({
value: checkboxValues,
query: getTermsQuery(checkboxValues),
});
}, [checkboxValues, getTermsQuery, setQuery]);

// Support resetting from SelectedFilters
useEffect(() => {
if (value === null) setCheckboxValues([]);
}, [value]);

if (!aggregations?.[dataField]?.buckets?.length) {
return null;
}

return (
<>
{tagLabel && (
<Tag colorScheme="brand" size="md" alignSelf="start">
{tagLabel}
</Tag>
)}
{showSearch && (
<InputGroup>
<Input
value={filterValue}
onChange={(e) => setFilterValue(e.target.value)}
placeholder={placeholder!}
/>
<InputRightElement>
{filterValue && (
<IconButton
color="red.400"
colorScheme="red"
size="sm"
variant="ghost"
aria-label={t("Clear")}
icon={<RiCloseFill />}
type="button"
title={t("Clear")}
onClick={() => setFilterValue("")}
></IconButton>
)}
</InputRightElement>
</InputGroup>
)}
<CheckboxGroup
value={checkboxValues}
onChange={(e) => setCheckboxValues(e)}
>
<VStack maxH="200px" alignItems="stretch" overflowY="scroll" p={2}>
{aggregations?.[dataField]?.buckets
?.filter(({ key }) =>
key.toLowerCase().includes(filterValue.toLowerCase()),
)
.map(({ key }) => (
<Checkbox key={key} value={key}>
{key}
</Checkbox>
))}
</VStack>
</CheckboxGroup>
</>
);
};
84 changes: 84 additions & 0 deletions src/components/search/GeneralSearchInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {
IconButton,
Input,
InputGroup,
InputRightElement,
Tag,
} from "@chakra-ui/react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { RiSearch2Line } from "react-icons/ri";
import { useDebounce } from "use-debounce";

interface GeneralInputProps {
debounceValue?: number;
placeholder?: string;
getQuery: (q: string) => object;
setQuery: (query: { value?: string; query?: any; opts?: any }) => void;
value: string | null;
tagLabel?: string;
}

export const GeneralSearchInput = ({
debounceValue = 1000,
placeholder,
getQuery,
setQuery,
value,
tagLabel,
}: GeneralInputProps) => {
const { t } = useTranslation();
const [searchText, setSearchText] = useState<string>(value!);
const [debouncedSearchText, { flush }] = useDebounce(
searchText,
debounceValue,
);

useEffect(() => {
setQuery({
value: debouncedSearchText,
query: getQuery(debouncedSearchText),
});
}, [debouncedSearchText, getQuery, setQuery]);

// Support resetting from SelectedFilters
useEffect(() => {
if (value === null) setSearchText("");
}, [value]);

return (
<>
{tagLabel && (
<Tag colorScheme="brand" size="md" alignSelf="start">
{tagLabel}
</Tag>
)}
<form
onSubmit={(e) => {
e.preventDefault();
flush();
}}
>
<InputGroup>
<Input
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
placeholder={placeholder}
></Input>
<InputRightElement>
<IconButton
color="brand.400"
colorScheme="brand"
size="sm"
variant="ghost"
aria-label={t("Search")}
icon={<RiSearch2Line />}
type="submit"
title={t("Search")}
></IconButton>
</InputRightElement>
</InputGroup>
</form>
</>
);
};
111 changes: 111 additions & 0 deletions src/components/search/RadioButtonSearchList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import {
Radio,
RadioGroup,
Input,
VStack,
Tag,
InputRightElement,
InputGroup,
IconButton,
} from "@chakra-ui/react";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { RiCloseFill } from "react-icons/ri";

interface RadioButtonSearchListProps {
dataField: string;
placeholder?: string;
showSearch?: boolean;
aggregations: {
[k: string]: { buckets: Array<{ key: string; doc_count: number }> };
};
setQuery: (query: any) => void;
value: string | null;
tagLabel?: string;
}

export const RadioButtonSearchList = ({
dataField,
placeholder,
showSearch = false,
aggregations,
setQuery,
value,
tagLabel,
}: RadioButtonSearchListProps) => {
const { t } = useTranslation();
const [filterValue, setFilterValue] = useState("");
const [radioValue, setRadioValue] = useState<string>(value!);

const getQuery = useCallback(
(value: string) => {
if (!value) return {};

return { query: { term: { [dataField]: value } } };
},
[dataField],
);

useEffect(() => {
setQuery({
value: radioValue,
query: getQuery(radioValue),
});
}, [radioValue, getQuery, setQuery]);

// Support resetting from SelectedFilters
useEffect(() => {
if (value === null) setRadioValue("");
}, [value]);

if (!aggregations?.[dataField]?.buckets?.length) {
return null;
}

return (
<>
{tagLabel && (
<Tag colorScheme="brand" size="md" alignSelf="start">
{tagLabel}
</Tag>
)}
{showSearch && (
<InputGroup>
<Input
value={filterValue}
onChange={(e) => setFilterValue(e.target.value)}
placeholder={placeholder!}
/>
<InputRightElement>
{filterValue && (
<IconButton
color="red.400"
colorScheme="red"
size="sm"
variant="ghost"
aria-label={t("Clear")}
icon={<RiCloseFill />}
type="button"
title={t("Clear")}
onClick={() => setFilterValue("")}
></IconButton>
)}
</InputRightElement>
</InputGroup>
)}
<RadioGroup value={radioValue} onChange={(value) => setRadioValue(value)}>
<VStack maxH="200px" alignItems="stretch" overflowY="scroll" p={2}>
{aggregations?.[dataField]?.buckets
?.filter(({ key }) =>
key.toLowerCase().includes(filterValue.toLowerCase()),
)
.map(({ key }) => (
<Radio key={key} value={key}>
{key}
</Radio>
))}
</VStack>
</RadioGroup>
</>
);
};
Loading