diff --git a/govtool/frontend/src/components/organisms/DashboardGovernanceActions.tsx b/govtool/frontend/src/components/organisms/DashboardGovernanceActions.tsx index f9b22bc1e..1ce96990d 100644 --- a/govtool/frontend/src/components/organisms/DashboardGovernanceActions.tsx +++ b/govtool/frontend/src/components/organisms/DashboardGovernanceActions.tsx @@ -1,10 +1,11 @@ -import { useState, useCallback, useEffect } from "react"; +import { useState, useEffect } from "react"; import { Box, CircularProgress, Tab, Tabs, styled } from "@mui/material"; import { useLocation } from "react-router-dom"; import { GOVERNANCE_ACTIONS_FILTERS } from "@consts"; import { useCardano } from "@context"; import { + useDataActionsBar, useGetProposalsQuery, useGetVoterInfo, useScreenDimension, @@ -64,11 +65,8 @@ const StyledTab = styled((props: StyledTabProps) => ( })); export const DashboardGovernanceActions = () => { - const [searchText, setSearchText] = useState(""); - const [filtersOpen, setFiltersOpen] = useState(false); - const [chosenFilters, setChosenFilters] = useState([]); - const [sortOpen, setSortOpen] = useState(false); - const [chosenSorting, setChosenSorting] = useState(""); + const { debouncedSearchText, ...dataActionsBarProps } = useDataActionsBar(); + const { chosenFilters, chosenSorting } = dataActionsBarProps; const { voter } = useGetVoterInfo(); const { isMobile } = useScreenDimension(); const { t } = useTranslation(); @@ -80,7 +78,7 @@ export const DashboardGovernanceActions = () => { const { proposals, isProposalsLoading } = useGetProposalsQuery({ filters: queryFilters, sorting: chosenSorting, - searchPhrase: searchText, + searchPhrase: debouncedSearchText, }); const { state } = useLocation(); @@ -92,14 +90,6 @@ export const DashboardGovernanceActions = () => { setContent(newValue); }; - const closeFilters = useCallback(() => { - setFiltersOpen(false); - }, [setFiltersOpen]); - - const closeSorts = useCallback(() => { - setSortOpen(false); - }, [setSortOpen]); - useEffect(() => { window.history.replaceState({}, document.title); }, []); @@ -113,22 +103,7 @@ export const DashboardGovernanceActions = () => { flexDirection="column" > <> - + {!proposals || !voter || isEnableLoading || isProposalsLoading ? ( { @@ -185,7 +160,7 @@ export const DashboardGovernanceActions = () => { diff --git a/govtool/frontend/src/hooks/index.ts b/govtool/frontend/src/hooks/index.ts index 126c2d0c9..da6b8743b 100644 --- a/govtool/frontend/src/hooks/index.ts +++ b/govtool/frontend/src/hooks/index.ts @@ -1,4 +1,7 @@ export { useTranslation } from "react-i18next"; + +export * from "./useDataActionsBar"; +export * from "./useDebounce"; export * from "./useFetchNextPageDetector"; export * from "./useOutsideClick"; export * from "./useSaveScrollPosition"; diff --git a/govtool/frontend/src/hooks/queries/useGetProposalsInfiniteQuery.ts b/govtool/frontend/src/hooks/queries/useGetProposalsInfiniteQuery.ts index 7e141842e..fee1e9482 100644 --- a/govtool/frontend/src/hooks/queries/useGetProposalsInfiniteQuery.ts +++ b/govtool/frontend/src/hooks/queries/useGetProposalsInfiniteQuery.ts @@ -2,13 +2,14 @@ import { useInfiniteQuery } from "react-query"; import { QUERY_KEYS } from "@consts"; import { useCardano } from "@context"; -import { getProposals, getProposalsArguments } from "@services"; +import { getProposals, GetProposalsArguments } from "@services"; export const useGetProposalsInfiniteQuery = ({ filters = [], pageSize = 10, + searchPhrase, sorting = "", -}: getProposalsArguments) => { +}: GetProposalsArguments) => { const { dRepID, isEnabled, pendingTransaction } = useCardano(); const fetchProposals = ({ pageParam = 0 }) => @@ -17,6 +18,7 @@ export const useGetProposalsInfiniteQuery = ({ filters, page: pageParam, pageSize, + searchPhrase, sorting, }); @@ -34,6 +36,7 @@ export const useGetProposalsInfiniteQuery = ({ filters, isEnabled, pendingTransaction.vote?.transactionHash, + searchPhrase, sorting, ], fetchProposals, diff --git a/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts b/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts index 242add713..981cd0b55 100644 --- a/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts +++ b/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts @@ -2,20 +2,19 @@ import { useQuery } from "react-query"; import { QUERY_KEYS } from "@consts"; import { useCardano } from "@context"; -import { getProposals, getProposalsArguments } from "@services"; -import { getFullGovActionId } from "@utils"; +import { getProposals, GetProposalsArguments } from "@services"; export const useGetProposalsQuery = ({ filters = [], - sorting, searchPhrase, -}: getProposalsArguments) => { + sorting, +}: GetProposalsArguments) => { const { dRepID, pendingTransaction } = useCardano(); const fetchProposals = async (): Promise => { const allProposals = await Promise.all( filters.map((filter) => - getProposals({ dRepID, filters: [filter], sorting }), + getProposals({ dRepID, filters: [filter], searchPhrase, sorting }), ), ); @@ -26,6 +25,7 @@ export const useGetProposalsQuery = ({ [ QUERY_KEYS.useGetProposalsKey, filters, + searchPhrase, sorting, dRepID, pendingTransaction.vote?.transactionHash, @@ -33,38 +33,27 @@ export const useGetProposalsQuery = ({ fetchProposals, ); - const mappedData = Object.values( - (groupedByType( - data?.filter((i) => - getFullGovActionId(i.txHash, i.index) - .toLowerCase() - .includes(searchPhrase.toLowerCase())) - ) ?? []) as ToVoteDataType + const proposals = Object.values( + (groupByType(data) ?? []) ); return { isProposalsLoading: isLoading, - proposals: mappedData, + proposals, }; }; -const groupedByType = (data?: ActionType[]) => data?.reduce((groups, item) => { - const itemType = item.type; +const groupByType = (data?: ActionType[]) => + data?.reduce>>((groups, item) => { + const itemType = item.type; - // TODO: Provide better typing for groups - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - if (!groups[itemType]) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - groups[itemType] = { - title: itemType, - actions: [], - }; - } - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - groups[itemType].actions.push(item); + if (!groups[itemType]) { + groups[itemType] = { + title: itemType, + actions: [], + }; + } + groups[itemType].actions.push(item); - return groups; -}, {}); + return groups; + }, {}); diff --git a/govtool/frontend/src/hooks/useDataActionsBar.tsx b/govtool/frontend/src/hooks/useDataActionsBar.tsx new file mode 100644 index 000000000..61723d0ea --- /dev/null +++ b/govtool/frontend/src/hooks/useDataActionsBar.tsx @@ -0,0 +1,58 @@ +import { useState, useCallback, Dispatch, SetStateAction } from "react"; + +import { + useDebounce, +} from "@hooks"; + +type UseDataActionsBarReturnType = { + chosenFilters: string[]; + chosenFiltersLength: number; + chosenSorting: string; + closeFilters: () => void; + closeSorts: () => void; + debouncedSearchText: string; + filtersOpen: boolean; + searchText: string; + setChosenFilters: Dispatch>; + setChosenSorting: Dispatch>; + setFiltersOpen: Dispatch>; + setSearchText: Dispatch>; + setSortOpen: Dispatch>; + sortingActive: boolean; + sortOpen: boolean; +}; + +export const useDataActionsBar = (): UseDataActionsBarReturnType => { + const [searchText, setSearchText] = useState(""); + const debouncedSearchText = useDebounce(searchText, 300); + const [filtersOpen, setFiltersOpen] = useState(false); + const [chosenFilters, setChosenFilters] = useState([]); + const [sortOpen, setSortOpen] = useState(false); + const [chosenSorting, setChosenSorting] = useState(""); + + const closeFilters = useCallback(() => { + setFiltersOpen(false); + }, [setFiltersOpen]); + + const closeSorts = useCallback(() => { + setSortOpen(false); + }, [setSortOpen]); + + return { + chosenFilters, + chosenFiltersLength: chosenFilters.length, + chosenSorting, + closeFilters, + closeSorts, + debouncedSearchText, + filtersOpen, + searchText, + setChosenFilters, + setChosenSorting, + setFiltersOpen, + setSearchText, + setSortOpen, + sortingActive: Boolean(chosenSorting), + sortOpen, + }; +}; diff --git a/govtool/frontend/src/hooks/useDebounce.ts b/govtool/frontend/src/hooks/useDebounce.ts new file mode 100644 index 000000000..335b5cf26 --- /dev/null +++ b/govtool/frontend/src/hooks/useDebounce.ts @@ -0,0 +1,17 @@ +import { useEffect, useState } from 'react'; + +export function useDebounce(value: T, delay: number) { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const timerID = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(timerID); + }; + }, [value, delay]); + + return debouncedValue; +} diff --git a/govtool/frontend/src/pages/DashboardGovernanceActionsCategory.tsx b/govtool/frontend/src/pages/DashboardGovernanceActionsCategory.tsx index 47384da36..8ae9f49e7 100644 --- a/govtool/frontend/src/pages/DashboardGovernanceActionsCategory.tsx +++ b/govtool/frontend/src/pages/DashboardGovernanceActionsCategory.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useRef, useState } from "react"; +import { useMemo, useRef } from "react"; import { generatePath, useNavigate, useParams } from "react-router-dom"; import { Box, CircularProgress, Link } from "@mui/material"; @@ -7,6 +7,7 @@ import { ICONS, PATHS } from "@consts"; import { useCardano } from "@context"; import { DataActionsBar, GovernanceActionCard } from "@molecules"; import { + useDataActionsBar, useFetchNextPageDetector, useGetProposalsInfiniteQuery, useGetVoterInfo, @@ -23,9 +24,8 @@ import { export const DashboardGovernanceActionsCategory = () => { const { category } = useParams(); - const [searchText, setSearchText] = useState(""); - const [sortOpen, setSortOpen] = useState(false); - const [chosenSorting, setChosenSorting] = useState(""); + const { debouncedSearchText, ...dataActionsBarProps } = useDataActionsBar(); + const { chosenSorting } = dataActionsBarProps; const { isMobile, screenWidth } = useScreenDimension(); const navigate = useNavigate(); const { pendingTransaction, isEnableLoading } = useCardano(); @@ -42,7 +42,7 @@ export const DashboardGovernanceActionsCategory = () => { } = useGetProposalsInfiniteQuery({ filters: [category?.replace(/ /g, "") ?? ""], sorting: chosenSorting, - searchPhrase: searchText, + searchPhrase: debouncedSearchText, }); const loadNextPageRef = useRef(null); @@ -57,25 +57,12 @@ export const DashboardGovernanceActionsCategory = () => { isProposalsFetching, ); - const mappedData = useMemo(() => { - const uniqueProposals = removeDuplicatedProposals(proposals); - - return uniqueProposals?.filter((i) => - getFullGovActionId(i.txHash, i.index) - .toLowerCase() - .includes(searchText.toLowerCase()), - ); - }, [ + const mappedData = useMemo(() => removeDuplicatedProposals(proposals), [ proposals, voter?.isRegisteredAsDRep, - searchText, isProposalsFetchingNextPage, ]); - const closeSorts = useCallback(() => { - setSortOpen(false); - }, [setSortOpen]); - return ( { { - const [searchText, setSearchText] = useState(""); - const [filtersOpen, setFiltersOpen] = useState(false); - const [chosenFilters, setChosenFilters] = useState([]); - const [sortOpen, setSortOpen] = useState(false); - const [chosenSorting, setChosenSorting] = useState(""); + const { debouncedSearchText, ...dataActionsBarProps } = useDataActionsBar(); + const { chosenFilters, chosenSorting } = dataActionsBarProps; const { isMobile, pagePadding } = useScreenDimension(); const { isEnabled } = useCardano(); const navigate = useNavigate(); @@ -35,7 +33,7 @@ export const GovernanceActions = () => { const { proposals, isProposalsLoading } = useGetProposalsQuery({ filters: queryFilters, sorting: chosenSorting, - searchPhrase: searchText, + searchPhrase: debouncedSearchText, }); useEffect(() => { @@ -44,14 +42,6 @@ export const GovernanceActions = () => { } }, [isEnabled]); - const closeFilters = useCallback(() => { - setFiltersOpen(false); - }, [setFiltersOpen]); - - const closeSorts = useCallback(() => { - setSortOpen(false); - }, [setSortOpen]); - return ( @@ -79,22 +69,7 @@ export const GovernanceActions = () => { /> )} - + {!proposals || isProposalsLoading ? ( { diff --git a/govtool/frontend/src/pages/GovernanceActionsCategory.tsx b/govtool/frontend/src/pages/GovernanceActionsCategory.tsx index 82b67ce46..bafaa8ea5 100644 --- a/govtool/frontend/src/pages/GovernanceActionsCategory.tsx +++ b/govtool/frontend/src/pages/GovernanceActionsCategory.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useEffect, useMemo, useRef } from "react"; +import { useEffect, useMemo, useRef } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { Box, CircularProgress, Link } from "@mui/material"; @@ -14,6 +14,7 @@ import { useScreenDimension, useTranslation, useGetVoterInfo, + useDataActionsBar, } from "@hooks"; import { WALLET_LS_KEY, @@ -25,9 +26,8 @@ import { export const GovernanceActionsCategory = () => { const { category } = useParams(); - const [searchText, setSearchText] = useState(""); - const [sortOpen, setSortOpen] = useState(false); - const [chosenSorting, setChosenSorting] = useState(""); + const { debouncedSearchText, ...dataActionsBarProps } = useDataActionsBar(); + const { chosenSorting } = dataActionsBarProps; const { isMobile, pagePadding, screenWidth } = useScreenDimension(); const { isEnabled } = useCardano(); const navigate = useNavigate(); @@ -44,7 +44,7 @@ export const GovernanceActionsCategory = () => { } = useGetProposalsInfiniteQuery({ filters: [category?.replace(/ /g, "") ?? ""], sorting: chosenSorting, - searchPhrase: searchText, + searchPhrase: debouncedSearchText, }); const loadNextPageRef = useRef(null); @@ -59,19 +59,10 @@ export const GovernanceActionsCategory = () => { isProposalsFetching, ); - const mappedData = useMemo(() => { - const uniqueProposals = removeDuplicatedProposals(proposals); - - return uniqueProposals?.filter((i) => - getFullGovActionId(i.txHash, i.index) - .toLowerCase() - .includes(searchText.toLowerCase()), - ); - }, [ + const mappedData = useMemo(() => removeDuplicatedProposals(proposals), [ voter?.isRegisteredAsDRep, isProposalsFetchingNextPage, proposals, - searchText, ]); useEffect(() => { @@ -81,10 +72,6 @@ export const GovernanceActionsCategory = () => { } }, [isEnabled]); - const closeSorts = useCallback(() => { - setSortOpen(false); - }, [setSortOpen]); - return ( { { {category}   - {searchText && ( + {debouncedSearchText && ( <> {t("govActions.withCategoryNotExist.optional")}   - {searchText} + {debouncedSearchText} )} diff --git a/govtool/frontend/src/services/requests/getProposals.ts b/govtool/frontend/src/services/requests/getProposals.ts index e4d1c4bf6..96c52a99c 100644 --- a/govtool/frontend/src/services/requests/getProposals.ts +++ b/govtool/frontend/src/services/requests/getProposals.ts @@ -1,12 +1,12 @@ import { API } from "../API"; -export type getProposalsArguments = { +export type GetProposalsArguments = { dRepID?: string; filters?: string[]; page?: number; pageSize?: number; sorting?: string; - searchPhrase: string; + searchPhrase?: string; }; export const getProposals = async ({ @@ -15,23 +15,18 @@ export const getProposals = async ({ page = 0, // It allows fetch proposals and if we have 7 items, display 6 cards and "view all" button pageSize = 7, + searchPhrase = "", sorting = "", -}: Omit) => { - const urlBase = "/proposal/list"; - let urlParameters = `?page=${page}&pageSize=${pageSize}`; - - if (filters.length > 0) { - filters.forEach((item) => { - urlParameters += `&type=${item}`; - }); - } - if (sorting.length) { - urlParameters += `&sort=${sorting}`; - } - if (dRepID) { - urlParameters += `&drepId=${dRepID}`; - } - - const response = await API.get(`${urlBase}${urlParameters}`); +}: GetProposalsArguments) => { + const response = await API.get("/proposal/list", { + params: { + page, + pageSize, + ...(searchPhrase && { search: searchPhrase }), + ...(filters.length && { type: filters }), + ...(sorting && { sort: sorting }), + ...(dRepID && { drepId: dRepID }), + }, + }); return response.data; }; diff --git a/govtool/frontend/src/types/global.d.ts b/govtool/frontend/src/types/global.d.ts index 49890ef5b..816cb26ec 100644 --- a/govtool/frontend/src/types/global.d.ts +++ b/govtool/frontend/src/types/global.d.ts @@ -56,4 +56,7 @@ declare global { | null | { [property: string]: JSONValue } | JSONValue[]; + + type ArrayElement = + ArrayType extends readonly (infer ElementType)[] ? ElementType : never; }