diff --git a/src/app/api/getFinalityProviders.ts b/src/app/api/getFinalityProviders.ts index 6d444d4e..2dd30538 100644 --- a/src/app/api/getFinalityProviders.ts +++ b/src/app/api/getFinalityProviders.ts @@ -19,6 +19,7 @@ interface FinalityProvidersAPIResponse { interface FinalityProviderAPI { description: DescriptionAPI; + state: "active" | "standby"; commission: string; btc_pk: string; active_tvl: number; @@ -35,14 +36,28 @@ interface DescriptionAPI { details: string; } -export const getFinalityProviders = async ( - key: string, -): Promise => { +export const getFinalityProviders = async ({ + key, + pk, + sortBy, + order, + name, +}: { + key: string; + name?: string; + sortBy?: string; + order?: "asc" | "desc"; + pk?: string; +}): Promise => { // const limit = 100; // const reverse = false; const params = { pagination_key: encode(key), + finality_provider_pk: pk, + sort_by: sortBy, + order, + name, // "pagination_reverse": reverse, // "pagination_limit": limit, }; @@ -70,6 +85,7 @@ export const getFinalityProviders = async ( securityContact: fp.description.security_contact, details: fp.description.details, }, + state: fp.state, commission: fp.commission, btcPk: fp.btc_pk, activeTVLSat: fp.active_tvl, diff --git a/src/app/components/Staking/FinalityProviders/FinalityProvider.tsx b/src/app/components/Staking/FinalityProviders/FinalityProvider.tsx index 74fb92ed..425c1295 100644 --- a/src/app/components/Staking/FinalityProviders/FinalityProvider.tsx +++ b/src/app/components/Staking/FinalityProviders/FinalityProvider.tsx @@ -13,6 +13,7 @@ import { maxDecimals } from "@/utils/maxDecimals"; interface FinalityProviderProps { moniker: string; pkHex: string; + state: "active" | "standby"; stakeSat: number; commission: string; onClick: () => void; @@ -22,6 +23,7 @@ interface FinalityProviderProps { export const FinalityProvider: React.FC = ({ moniker, + state = "active", pkHex, stakeSat, commission, @@ -127,6 +129,9 @@ export const FinalityProvider: React.FC = ({ className="tooltip-wrap" /> +
+ Status: {state} +
); diff --git a/src/app/components/Staking/FinalityProviders/FinalityProviderSearch.tsx b/src/app/components/Staking/FinalityProviders/FinalityProviderSearch.tsx index 65f3f9b2..941e07d2 100644 --- a/src/app/components/Staking/FinalityProviders/FinalityProviderSearch.tsx +++ b/src/app/components/Staking/FinalityProviders/FinalityProviderSearch.tsx @@ -1,23 +1,17 @@ -import { useDebounce } from "@uidotdev/usehooks"; -import React, { useEffect, useState } from "react"; +import React from "react"; import { FiSearch } from "react-icons/fi"; interface FinalityProviderSearchProps { + searchValue: string; onSearch: (searchTerm: string) => void; } export const FinalityProviderSearch: React.FC = ({ + searchValue, onSearch, }) => { - const [searchTerm, setSearchTerm] = useState(""); - const debouncedSearchTerm = useDebounce(searchTerm, 300); - - useEffect(() => { - onSearch(debouncedSearchTerm); - }, [debouncedSearchTerm, onSearch]); - const handleSearch = (e: React.ChangeEvent) => { - setSearchTerm(e.target.value); + onSearch(e.target.value); }; return ( @@ -29,7 +23,7 @@ export const FinalityProviderSearch: React.FC = ({ diff --git a/src/app/components/Staking/FinalityProviders/FinalityProviders.tsx b/src/app/components/Staking/FinalityProviders/FinalityProviders.tsx index 58d4339b..bbfcb0cc 100644 --- a/src/app/components/Staking/FinalityProviders/FinalityProviders.tsx +++ b/src/app/components/Staking/FinalityProviders/FinalityProviders.tsx @@ -1,102 +1,44 @@ -import { useInfiniteQuery } from "@tanstack/react-query"; import { useEffect } from "react"; import InfiniteScroll from "react-infinite-scroll-component"; -import { - PaginatedFinalityProviders, - getFinalityProviders, -} from "@/app/api/getFinalityProviders"; import { LoadingTableList, LoadingView, } from "@/app/components/Loading/Loading"; -import { useError } from "@/app/context/Error/ErrorContext"; -import { useFinalityProvidersData } from "@/app/hooks/finalityProviders/useFinalityProvidersData"; -import { ErrorState } from "@/app/types/errors"; -import { FinalityProvidersProps } from "@/app/types/finalityProviders"; +import { useFinalityProviderService } from "@/app/hooks/services/useFinalityProviderService"; +import type { FinalityProvider as FinalityProviderInterface } from "@/app/types/finalityProviders"; import { FinalityProvider } from "./FinalityProvider"; import { FinalityProviderSearch } from "./FinalityProviderSearch"; +export interface FinalityProvidersProps { + onFinalityProvidersLoad: (data: FinalityProviderInterface[]) => void; + selectedFinalityProvider: FinalityProviderInterface | undefined; + onFinalityProviderChange: (btcPkHex: string) => void; +} + export const FinalityProviders: React.FC = ({ selectedFinalityProvider, onFinalityProviderChange, onFinalityProvidersLoad, }) => { - const { isErrorOpen, showError, handleError } = useError(); const { - data: fps, - fetchNextPage: finalityProvidersFetchNext, - hasNextPage: finalityProvidersHasNext, - isFetchingNextPage: finalityProvidersIsFetchingMore, - error: finalityProvidersError, - isError: hasFinalityProvidersError, - refetch: refetchFinalityProvidersData, - isRefetchError: isRefetchFinalityProvidersError, - } = useInfiniteQuery({ - queryKey: ["finality providers"], - queryFn: ({ pageParam = "" }) => getFinalityProviders(pageParam), - getNextPageParam: (lastPage) => - lastPage?.pagination?.next_key !== "" - ? lastPage?.pagination?.next_key - : null, - initialPageParam: "", - refetchInterval: 60000, // 1 minute - select: (data) => { - const flattenedData = data.pages.reduce( - (acc, page) => { - acc.finalityProviders.push(...page.finalityProviders); - acc.pagination = page.pagination; - return acc; - }, - { finalityProviders: [], pagination: { next_key: "" } }, - ); - return flattenedData; - }, - retry: (failureCount) => { - return !isErrorOpen && failureCount <= 3; - }, - }); - - useEffect(() => { - fps?.finalityProviders && onFinalityProvidersLoad(fps.finalityProviders); - }, [fps, onFinalityProvidersLoad]); + isLoading, + finalityProviders, + searchValue, + hasNextPage, + fetchNextPage, + handleSearch, + handleSort, + } = useFinalityProviderService(); useEffect(() => { - if ( - finalityProvidersHasNext && - finalityProvidersFetchNext && - !finalityProvidersIsFetchingMore - ) { - finalityProvidersFetchNext(); + if (finalityProviders) { + onFinalityProvidersLoad(finalityProviders); } - }, [ - finalityProvidersHasNext, - finalityProvidersFetchNext, - finalityProvidersIsFetchingMore, - ]); - - useEffect(() => { - handleError({ - error: finalityProvidersError, - hasError: hasFinalityProvidersError, - errorState: ErrorState.SERVER_ERROR, - refetchFunction: refetchFinalityProvidersData, - }); - }, [ - hasFinalityProvidersError, - isRefetchFinalityProvidersError, - finalityProvidersError, - refetchFinalityProvidersData, - showError, - handleError, - ]); - - const { handleSearch, filteredProviders } = useFinalityProvidersData( - fps?.finalityProviders, - ); + }, [finalityProviders, onFinalityProvidersLoad]); - if (!fps?.finalityProviders?.length) { + if (!finalityProviders?.length) { return ; } @@ -106,13 +48,23 @@ export const FinalityProviders: React.FC = ({ Step-1: Select a finality provider

- +
-

Finality Provider

-

BTC PK

-

Total Delegation

-

Commission

+

handleSort("name")}> + Finality Provider +

+

BTC PK

+

handleSort("active_tvl")}> + Total Delegation +

+

handleSort("commission")}> + Commission +

+

Status

= ({ > : null} + dataLength={finalityProviders?.length || 0} + next={fetchNextPage} + hasMore={hasNextPage} + loader={isLoading ? : null} scrollableTarget="finality-providers" > - {filteredProviders?.map((fp) => ( + {finalityProviders?.map((fp) => ( + getFinalityProviders({ key: pageParam, pk, sortBy, order, name }), + getNextPageParam: (lastPage) => + lastPage?.pagination?.next_key !== "" + ? lastPage?.pagination?.next_key + : null, + initialPageParam: "", + refetchInterval: ONE_MINUTE, + placeholderData: (prev) => prev, + select: (data) => { + const flattenedData = data.pages.reduce( + (acc, page) => { + acc.finalityProviders.push(...page.finalityProviders); + acc.pagination = page.pagination; + return acc; + }, + { finalityProviders: [], pagination: { next_key: "" } }, + ); + return flattenedData; + }, + retry: (failureCount) => { + return !isErrorOpen && failureCount <= 3; + }, + }); + + useEffect(() => { + handleError({ + error: query.error, + hasError: query.isError, + errorState: ErrorState.SERVER_ERROR, + refetchFunction: query.refetch, + }); + }, [query.isError, query.error, query.refetch, handleError]); + + return query; +} diff --git a/src/app/hooks/services/useFinalityProviderService.ts b/src/app/hooks/services/useFinalityProviderService.ts new file mode 100644 index 00000000..16b7ff6b --- /dev/null +++ b/src/app/hooks/services/useFinalityProviderService.ts @@ -0,0 +1,56 @@ +import { useDebounce } from "@uidotdev/usehooks"; +import { useCallback, useState } from "react"; + +import { useFinalityProviders } from "@/app/hooks/api/useFinalityProviders"; + +interface SortState { + field?: string; + direction?: "asc" | "desc"; +} + +const SORT_DIRECTIONS = { + undefined: "desc", + desc: "asc", + asc: undefined, +} as const; + +export function useFinalityProviderService() { + const [searchValue, setSearchValue] = useState(""); + const [sortState, setSortState] = useState({}); + const name = useDebounce(searchValue, 300); + + const { data, hasNextPage, isFetchingNextPage, fetchNextPage } = + useFinalityProviders({ + sortBy: sortState.field, + order: sortState.direction, + name, + }); + + const handleSearch = useCallback((searchTerm: string) => { + setSearchValue(searchTerm); + }, []); + + const handleSort = useCallback((sortField: string) => { + setSortState(({ field, direction }) => + field === sortField + ? { + field: SORT_DIRECTIONS[`${direction}`] ? field : undefined, + direction: SORT_DIRECTIONS[`${direction}`], + } + : { + field: sortField, + direction: "desc", + }, + ); + }, []); + + return { + searchValue, + finalityProviders: data?.finalityProviders, + hasNextPage, + isLoading: isFetchingNextPage, + fetchNextPage, + handleSearch, + handleSort, + }; +} diff --git a/src/app/types/finalityProviders.ts b/src/app/types/finalityProviders.ts index c1bb1b3a..244e3d0c 100644 --- a/src/app/types/finalityProviders.ts +++ b/src/app/types/finalityProviders.ts @@ -1,7 +1,6 @@ -import { Dispatch, SetStateAction } from "react"; - export interface FinalityProvider { description: Description; + state: "active" | "standby"; commission: string; btcPk: string; activeTVLSat: number; @@ -17,11 +16,3 @@ export interface Description { securityContact: string; details: string; } - -export interface FinalityProvidersProps { - onFinalityProvidersLoad: Dispatch< - SetStateAction - >; - selectedFinalityProvider: FinalityProvider | undefined; - onFinalityProviderChange: (btcPkHex: string) => void; -} diff --git a/tailwind.config.ts b/tailwind.config.ts index 6db2fdb4..b6d5373d 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -23,7 +23,7 @@ const config: Config = { }, gridTemplateColumns: { stakingFinalityProvidersMobile: "2fr 1fr", - stakingFinalityProvidersDesktop: "2fr 1.5fr 2fr 0.75fr", + stakingFinalityProvidersDesktop: "2fr 1.5fr 2fr 0.75fr 0.75fr", }, }, },