diff --git a/package.json b/package.json
index 219b7443..1e474970 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/components/nav/NavBar.tsx b/src/components/nav/NavBar.tsx
index a015af92..c214283e 100644
--- a/src/components/nav/NavBar.tsx
+++ b/src/components/nav/NavBar.tsx
@@ -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();
@@ -57,7 +58,7 @@ export function NavBar({ onOpen, ...rest }: MobileProps) {
/>
-
+
diff --git a/src/components/nav/Searchbox.tsx b/src/components/nav/SearchBox.tsx
similarity index 93%
rename from src/components/nav/Searchbox.tsx
rename to src/components/nav/SearchBox.tsx
index ce90c13a..7b4e784a 100644
--- a/src/components/nav/Searchbox.tsx
+++ b/src/components/nav/SearchBox.tsx
@@ -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();
+ const input = useRef(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) => {
diff --git a/src/components/search/CheckboxSearchList.tsx b/src/components/search/CheckboxSearchList.tsx
new file mode 100644
index 00000000..eae6a5e6
--- /dev/null
+++ b/src/components/search/CheckboxSearchList.tsx
@@ -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>(
+ 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 && (
+
+ {tagLabel}
+
+ )}
+ {showSearch && (
+
+ setFilterValue(e.target.value)}
+ placeholder={placeholder!}
+ />
+
+ {filterValue && (
+ }
+ type="button"
+ title={t("Clear")}
+ onClick={() => setFilterValue("")}
+ >
+ )}
+
+
+ )}
+ setCheckboxValues(e)}
+ >
+
+ {aggregations?.[dataField]?.buckets
+ ?.filter(({ key }) =>
+ key.toLowerCase().includes(filterValue.toLowerCase()),
+ )
+ .map(({ key }) => (
+
+ {key}
+
+ ))}
+
+
+ >
+ );
+};
diff --git a/src/components/search/GeneralSearchInput.tsx b/src/components/search/GeneralSearchInput.tsx
new file mode 100644
index 00000000..d9fe15e8
--- /dev/null
+++ b/src/components/search/GeneralSearchInput.tsx
@@ -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(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 && (
+
+ {tagLabel}
+
+ )}
+
+ >
+ );
+};
diff --git a/src/components/search/RadioButtonSearchList.tsx b/src/components/search/RadioButtonSearchList.tsx
new file mode 100644
index 00000000..9b68ad29
--- /dev/null
+++ b/src/components/search/RadioButtonSearchList.tsx
@@ -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(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 && (
+
+ {tagLabel}
+
+ )}
+ {showSearch && (
+
+ setFilterValue(e.target.value)}
+ placeholder={placeholder!}
+ />
+
+ {filterValue && (
+ }
+ type="button"
+ title={t("Clear")}
+ onClick={() => setFilterValue("")}
+ >
+ )}
+
+
+ )}
+ setRadioValue(value)}>
+
+ {aggregations?.[dataField]?.buckets
+ ?.filter(({ key }) =>
+ key.toLowerCase().includes(filterValue.toLowerCase()),
+ )
+ .map(({ key }) => (
+
+ {key}
+
+ ))}
+
+
+ >
+ );
+};
diff --git a/src/components/search/ToggleButtonSearchInput.tsx b/src/components/search/ToggleButtonSearchInput.tsx
new file mode 100644
index 00000000..5f7c0634
--- /dev/null
+++ b/src/components/search/ToggleButtonSearchInput.tsx
@@ -0,0 +1,70 @@
+import { Button, HStack, Tag } from "@chakra-ui/react";
+import { ReactElement, useCallback, useEffect, useState } from "react";
+
+interface ToggleButtonSearchInputProps {
+ dataField: string;
+ buttons: Array<{ label: string; value: string; icon?: ReactElement }>;
+ setQuery: (query: { value?: string; query?: any; opts?: any }) => void;
+ value: string | null;
+ tagLabel?: string;
+}
+
+export const ToggleButtonSearchInput = ({
+ dataField,
+ setQuery,
+ value,
+ buttons,
+ tagLabel,
+}: ToggleButtonSearchInputProps) => {
+ const [buttonValue, setButtonValue] = useState(value!);
+
+ const getQuery = useCallback(
+ (value: string) => {
+ if (!value) return {};
+
+ return { query: { term: { [dataField]: value } } };
+ },
+ [dataField],
+ );
+
+ useEffect(() => {
+ setQuery({
+ value: buttonValue,
+ query: getQuery(buttonValue),
+ });
+ }, [buttonValue, getQuery, setQuery]);
+
+ // Support resetting from SelectedFilters
+ useEffect(() => {
+ if (value === null) setButtonValue("");
+ }, [value]);
+
+ return (
+ <>
+ {tagLabel && (
+
+ {tagLabel}
+
+ )}
+
+ {buttons.map(({ label, value, icon }) => {
+ return (
+
+ );
+ })}
+
+ >
+ );
+};
diff --git a/src/pages/Search.css b/src/pages/Search.css
index 77fadd8e..178faa61 100644
--- a/src/pages/Search.css
+++ b/src/pages/Search.css
@@ -1,21 +1,5 @@
-.m-search .input-fix input {
- height: unset !important;
-}
-
-.m-search label > span > span:nth-child(2) {
- color: #555 !important;
-}
-
-.m-search .m-filters li {
- padding: 0 4px;
- border-radius: 3px;
-}
-
-.m-search .m-filters li[aria-checked="true"] {
- background-color: var(--chakra-colors-gray-800);
-}
-
.m-search .sort-select {
+ color: inherit;
outline: transparent solid 2px;
outline-offset: 2px;
appearance: auto;
@@ -44,3 +28,33 @@
border-color: rgb(99, 179, 237);
box-shadow: rgb(99, 179, 237) 0 0 0 1px;
}
+
+.m-search .custom-chakra-button > a {
+ border-radius: var(--chakra-radii-md);
+ font-weight: var(--chakra-fontWeights-semibold);
+ transition-property: var(--chakra-transition-property-common);
+ transition-duration: var(--chakra-transition-duration-normal);
+ height: var(--chakra-sizes-10);
+ min-width: var(--chakra-sizes-10);
+ font-size: var(--chakra-fontSizes-md);
+ -webkit-padding-start: var(--chakra-space-4);
+ padding-inline-start: var(--chakra-space-4);
+ -webkit-padding-end: var(--chakra-space-4);
+ padding-inline-end: var(--chakra-space-4);
+ background: var(--chakra-colors-whiteAlpha-200);
+}
+
+.m-search .custom-chakra-button > a:hover {
+ background: var(--chakra-colors-n2-300);
+}
+
+.m-search .custom-chakra-button > a:focus {
+ outline: none;
+ border-color: transparent;
+ background: var(--chakra-colors-whiteAlpha-200);
+}
+
+.m-search .custom-chakra-button > a.active {
+ background: var(--chakra-colors-n2-100);
+ color: var(--chakra-colors-gray-800);
+}
diff --git a/src/pages/Search.tsx b/src/pages/Search.tsx
index e2751835..1c8cbcd3 100644
--- a/src/pages/Search.tsx
+++ b/src/pages/Search.tsx
@@ -1,12 +1,8 @@
import {
- DataSearch,
- MultiList,
ReactiveBase,
ReactiveComponent,
ReactiveList,
SelectedFilters,
- SingleList,
- ToggleButton,
} from "@appbaseio/reactivesearch";
import {
Accordion,
@@ -14,19 +10,24 @@ import {
AccordionIcon,
AccordionItem,
AccordionPanel,
- Box,
Flex,
Heading,
- Input,
Progress,
- Tag,
useBreakpointValue,
VStack,
} from "@chakra-ui/react";
-import { useEffect, useState } from "react";
+import { useCallback, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { BiMovie, BiMoviePlay } from "react-icons/bi";
import { useNavigate, useSearchParams } from "react-router-dom";
import { SongTable, SongTableCol } from "../components/data/SongTable";
import "./Search.css";
+import { GeneralSearchInput } from "../components/search/GeneralSearchInput";
+import { CheckboxSearchList } from "../components/search/CheckboxSearchList";
+import { RadioButtonSearchList } from "../components/search/RadioButtonSearchList";
+import { ToggleButtonSearchInput } from "../components/search/ToggleButtonSearchInput";
+
+const debounceValue = 1000;
const SearchResultSongTable = ({
data,
@@ -50,6 +51,7 @@ const SearchResultSongTable = ({
size="xs"
isIndeterminate
visibility={loading ? "visible" : "hidden"}
+ mt={1}
/>
{data && (
{
- if (searchParams.has("ch")) setChannelSelected(true);
- }, [searchParams]); // searchParams --> isExact
+ const getGeneralQuery = useCallback((value: string) => {
+ if (!value) return {};
+
+ return {
+ query: {
+ multi_match: {
+ query: value,
+ fields: [
+ "general",
+ "general.romaji",
+ "original_artist",
+ "original_artist.romaji",
+ ],
+ type: "phrase",
+ },
+ },
+ };
+ }, []);
+
+ const getSongQuery = useCallback((value: string) => {
+ if (!value) return {};
+
+ return {
+ query: {
+ multi_match: {
+ query: value,
+ fields: ["name.ngram", "name"],
+ type: "phrase",
+ },
+ },
+ };
+ }, []);
+
+ const getArtistQuery = useCallback((value: string) => {
+ if (!value) return {};
+
+ return {
+ query: {
+ multi_match: {
+ query: value,
+ fields: [
+ "original_artist.ngram^2",
+ "original_artist^2",
+ "original_artist.romaji^0.5",
+ ],
+ type: "phrase",
+ },
+ },
+ };
+ }, []);
return (
- {
- if (!q) return {};
-
- return {
- query: {
- multi_match: {
- query: q,
- fields: [
- "general",
- "general.romaji",
- "original_artist",
- "original_artist.romaji",
- ],
- type: "phrase",
- },
- },
- };
- }}
- dataField=""
- queryFormat="and"
- placeholder="Search for Music / Artist"
- autosuggest={false}
- enableDefaultSuggestions={false}
- iconPosition="right"
- onError={(e) => console.log(e)}
filterLabel="Search"
+ customQuery={getGeneralQuery}
+ render={(props) => (
+
+ )}
+ onError={(e) => console.error(e)}
/>
- Advanced Filters
+ {t("Advanced Filters")}
- (
+ },
+ {
+ label: t("Stream"),
+ value: "false",
+ icon: ,
+ },
+ ]}
+ {...props}
+ />
+ )}
+ onError={(e) => console.error(e)}
/>
-
- Song
-
- {
- if (!q) return {};
-
- return {
- query: {
- multi_match: {
- query: q,
- fields: ["name.ngram", "name"],
- type: "phrase",
- },
- },
- };
- }}
- queryFormat="and"
- placeholder="Song Name"
- autosuggest={false}
- enableDefaultSuggestions={false}
- iconPosition="right"
- onError={(e) => console.log(e)}
- filterLabel="Song Name"
+ filterLabel={t("Song Name")}
+ customQuery={getSongQuery}
+ render={(props) => (
+
+ )}
+ onError={(e) => console.error(e)}
/>
-
- Artist
-
-
- {
- if (!q) return {};
-
- return {
- query: {
- multi_match: {
- query: q,
- fields: [
- "original_artist.ngram^2",
- "original_artist^2",
- "original_artist.romaji^0.5",
- ],
- type: "phrase",
- },
- },
- };
- }}
- queryFormat="and"
- placeholder="Original Artist Name"
- autosuggest={false}
- enableDefaultSuggestions={false}
- iconPosition="right"
- onError={(e) => console.log(e)}
- filterLabel="Original Artist"
+ filterLabel={t("Original Artist")}
+ customQuery={getArtistQuery}
+ render={(props) => (
+
+ )}
+ onError={(e) => console.error(e)}
/>
- {
- setChannelSelected(e && e.length > 0);
+ defaultQuery={() => ({
+ aggs: {
+ "channel.name": {
+ terms: {
+ field: "channel.name",
+ size: 12,
+ order: { _count: "desc" },
+ },
+ },
+ },
+ })}
+ render={(props) => {
+ setChannelSelected(props.value?.length > 0);
+ return (
+
+ );
}}
- URLParams
- size={12}
- showCheckbox
+ onError={(e) => console.error(e)}
/>
{!channelSelected && (
- {
- setSuborgVisible(!!e);
+ defaultQuery={() => ({
+ aggs: {
+ org: {
+ terms: {
+ field: "org",
+ order: { _count: "desc" },
+ },
+ },
+ },
+ })}
+ render={(props) => {
+ setSuborgVisible(!!props.value);
+ return (
+
+ );
}}
- URLParams
+ onError={(e) => console.error(e)}
/>
)}
{suborgVisible && !channelSelected && (
- ({
+ aggs: {
+ suborg: {
+ terms: {
+ field: "suborg",
+ order: { _count: "desc" },
+ },
+ },
+ },
+ })}
+ render={(props) => (
+
+ )}
+ onError={(e) => console.error(e)}
/>
)}
@@ -284,7 +335,10 @@ export default function Search() {
flexGrow={2}
flexShrink={1}
>
-
+
@@ -319,99 +373,3 @@ export default function Search() {
);
}
-
-// Garbage stuff:
-/* {
- console.log(args);
- return {
- query: {
- bool: {
- must: [
- {
- multi_match: {
- query: qFuzzy,
- fields: [
- "general",
- "general.romaji",
- "original_artist",
- "original_artist.romaji",
- ],
- },
- },
- ],
- },
- },
- value: qFuzzy,
- }
- }}
- render={({ setQuery, value }) => {
- return (
- {
- if (qFuzzy) {
- setQuery({
- query: {
- bool: {
- must: [
- {
- multi_match: {
- query: qFuzzy,
- fields: [
- "general",
- "general.romaji",
- "original_artist",
- "original_artist.romaji",
- ],
- },
- },
- ],
- },
- },
- value: qFuzzy,
- });
- } else if (qExact) {
- setQuery({
- query: {
- bool: {
- must: [
- {
- multi_match: {
- query: qExact,
- fields: [
- "general.ngram",
- "original_artist.ngram",
- ],
- },
- },
- ,
- ],
- should: [
- {
- multi_match: {
- query: qExact,
- fields: [
- "general",
- "general.romaji",
- "original_artist",
- "original_artist.romaji",
- ],
- },
- },
- ],
- },
- },
- value: qExact,
- });
- }
- }}
- />
- );
- }}
- > */
diff --git a/yarn.lock b/yarn.lock
index 8ae3c8aa..ae51b00b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10763,6 +10763,11 @@ use-callback-ref@^1.2.3, use-callback-ref@^1.2.5:
resolved "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.2.5.tgz"
integrity sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg==
+use-debounce@^8.0.2:
+ version "8.0.2"
+ resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-8.0.2.tgz#bd1b522c7b5b5d9dc249824fd2e4c3e4137b1ea9"
+ integrity sha512-4yCQ4FmlmYNpcHXJk1E19chO1X58fH4+QrwKpa5nkx3d7szHR3MjheRgECLvHivp3ClUqEom+SHOGB9zBz+qlw==
+
use-debouncy@^4.2.0:
version "4.2.1"
resolved "https://registry.npmjs.org/use-debouncy/-/use-debouncy-4.2.1.tgz"