diff --git a/apps/dashboard/features/holders-and-delegates/HoldersAndDelegatesSection.tsx b/apps/dashboard/features/holders-and-delegates/HoldersAndDelegatesSection.tsx index 1b0563d37..a16232594 100644 --- a/apps/dashboard/features/holders-and-delegates/HoldersAndDelegatesSection.tsx +++ b/apps/dashboard/features/holders-and-delegates/HoldersAndDelegatesSection.tsx @@ -15,6 +15,16 @@ import { parseAsString, parseAsStringEnum, useQueryState } from "nuqs"; type TabId = "tokenHolders" | "delegates"; +interface TabConfig { + id: TabId; + label: string; +} + +const TABS: TabConfig[] = [ + { id: "tokenHolders", label: "TOKEN HOLDERS" }, + { id: "delegates", label: "DELEGATES" }, +] as const; + export const HoldersAndDelegatesSection = ({ daoId }: { daoId: DaoIdEnum }) => { const defaultDays = TimeInterval.NINETY_DAYS; const [days, setDays] = useQueryState( @@ -50,53 +60,41 @@ export const HoldersAndDelegatesSection = ({ daoId }: { daoId: DaoIdEnum }) => { delegates: , }; - const HoldersAndDelegatesLeftComponent = () => { - const tabs: Array<{ id: TabId; label: string }> = [ - { - id: "tokenHolders", - label: "TOKEN HOLDERS", - }, - { - id: "delegates", - label: "DELEGATES", - }, - ]; - - return ( -
-
- {tabs.map((tab) => ( - - ))} -
+ const TabsHeader = () => ( +
+
+ {TABS.map((tab) => ( + + ))}
- ); - }; +
+ ); return ( - } - description={PAGES_CONSTANTS.holdersAndDelegates.description} - className="lg:pb-0" - > - -
- - -
- {tabComponentMap[activeTab as TabId]} -
-
+
+ } + description={PAGES_CONSTANTS.holdersAndDelegates.description} + > + +
+ + +
+ {tabComponentMap[activeTab as TabId]} +
+
+
); }; diff --git a/apps/dashboard/features/holders-and-delegates/components/HoldersAndDelegatesDrawer.tsx b/apps/dashboard/features/holders-and-delegates/components/HoldersAndDelegatesDrawer.tsx index b4579cf5e..dd8e00801 100644 --- a/apps/dashboard/features/holders-and-delegates/components/HoldersAndDelegatesDrawer.tsx +++ b/apps/dashboard/features/holders-and-delegates/components/HoldersAndDelegatesDrawer.tsx @@ -7,9 +7,9 @@ import { cn } from "@/shared/utils"; import { Tabs, TabsList, TabsTrigger } from "@radix-ui/react-tabs"; import { useScreenSize } from "@/shared/hooks"; import { CopyAndPasteButton } from "@/shared/components/buttons/CopyAndPasteButton"; -import { DelegateDelegationsHistory } from "@/features/holders-and-delegates/delegate/drawer/delegation-history/DelegateDelegationsHistory"; +import { VotingPowerHistory } from "@/features/holders-and-delegates/delegate/drawer/voting-power-history/VotingPowerHistory"; import { DaoIdEnum } from "@/shared/types/daos"; -import { VotingPower } from "@/features/holders-and-delegates/delegate/drawer/voting-power/VotingPower"; +import { VoteComposition } from "@/features/holders-and-delegates/delegate/drawer/vote-composition/VoteComposition"; import { BalanceHistory } from "@/features/holders-and-delegates/token-holder/drawer/balance-history/BalanceHistory"; import { DelegationHistory } from "@/features/holders-and-delegates/token-holder/drawer/delegation-history/DelegationHistory"; import { DelegateProposalsActivity } from "@/features/holders-and-delegates/delegate/drawer/votes/DelegateProposalsActivity"; @@ -46,16 +46,14 @@ export const HoldersAndDelegatesDrawer = ({ ), }, { - id: "votingPower", - label: "Voting Power", - content: , + id: "voteComposition", + label: "Vote Composition", + content: , }, { - id: "delegationHistory", - label: "Delegation History", - content: ( - - ), + id: "votingPowerHistory", + label: "Voting Power History", + content: , }, ], }, diff --git a/apps/dashboard/features/holders-and-delegates/components/index.ts b/apps/dashboard/features/holders-and-delegates/components/index.ts index 220384cdc..05bff91ae 100644 --- a/apps/dashboard/features/holders-and-delegates/components/index.ts +++ b/apps/dashboard/features/holders-and-delegates/components/index.ts @@ -1,6 +1,6 @@ export * from "@/features/holders-and-delegates/delegate/Delegates"; export * from "@/features/holders-and-delegates/token-holder/drawer/balance-history/BalanceHistory"; -export * from "@/features/holders-and-delegates/delegate/drawer/delegation-history/DelegateDelegationHistoryTable"; +export * from "@/features/holders-and-delegates/delegate/drawer/voting-power-history/VotingPowerHistoryTable"; export * from "@/features/holders-and-delegates/components/HoldersAndDelegatesDrawer"; export * from "@/features/holders-and-delegates/components/ProgressCircle"; export * from "@/features/holders-and-delegates/components/TabButton"; diff --git a/apps/dashboard/features/holders-and-delegates/delegate/Delegates.tsx b/apps/dashboard/features/holders-and-delegates/delegate/Delegates.tsx index 54f93ecb8..20722349a 100644 --- a/apps/dashboard/features/holders-and-delegates/delegate/Delegates.tsx +++ b/apps/dashboard/features/holders-and-delegates/delegate/Delegates.tsx @@ -7,15 +7,16 @@ import { useDelegates, HoldersAndDelegatesDrawer, } from "@/features/holders-and-delegates"; +import { getAvgVoteTimingData } from "@/features/holders-and-delegates/utils"; import { TimeInterval } from "@/shared/types/enums"; -import { SkeletonRow, Button } from "@/shared/components"; +import { SkeletonRow, Button, SimpleProgressBar } from "@/shared/components"; import { EnsAvatar } from "@/shared/components/design-system/avatars/ens-avatar/EnsAvatar"; import { ArrowUpDown, ArrowState } from "@/shared/components/icons"; -import { formatNumberUserReadable } from "@/shared/utils"; +import { cn, formatNumberUserReadable } from "@/shared/utils"; import { Plus } from "lucide-react"; import { ProgressCircle } from "@/features/holders-and-delegates/components/ProgressCircle"; import { DaoIdEnum } from "@/shared/types/daos"; -import { useScreenSize } from "@/shared/hooks"; +import { useScreenSize, useDaoData } from "@/shared/hooks"; import { Address, formatUnits } from "viem"; import { Table } from "@/shared/components/design-system/table/Table"; import { Percentage } from "@/shared/components/design-system/table/Percentage"; @@ -27,14 +28,20 @@ import { QueryInput_VotingPowers_OrderBy, QueryInput_VotingPowers_OrderDirection, } from "@anticapture/graphql-client"; +import { Tooltip } from "@/shared/components/design-system/tooltips/Tooltip"; +import { DAYS_IN_SECONDS } from "@/shared/constants/time-related"; import { DEFAULT_ITEMS_PER_PAGE } from "@/features/holders-and-delegates/utils"; interface DelegateTableData { address: string; votingPower: string; - variation?: { percentageChange: number; absoluteChange: number }; + variation?: { + percentageChange: number; + absoluteChange: number; + }; activity?: string | null; activityPercentage?: number | null; delegators: number; + avgVoteTiming?: { text: string; percentage: number } | null; } interface DelegatesProps { @@ -42,30 +49,9 @@ interface DelegatesProps { daoId: DaoIdEnum; } -// Helper function to convert time period to timestamp and block number -export const getTimeDataFromPeriod = (period: TimeInterval) => { - const now = Date.now(); - const msPerDay = 24 * 60 * 60 * 1000; - - let daysBack: number; - switch (period) { - case TimeInterval.SEVEN_DAYS: - daysBack = 7; - break; - case TimeInterval.THIRTY_DAYS: - daysBack = 30; - break; - case TimeInterval.NINETY_DAYS: - daysBack = 90; - break; - case TimeInterval.ONE_YEAR: - daysBack = 365; - break; - default: - daysBack = 30; - } - - return Math.floor((now - daysBack * msPerDay) / 1000); +// Converts a TimeInterval to a timestamp (in seconds) representing the start date. +const getFromTimestamp = (period: TimeInterval): number => { + return Math.floor(Date.now() / 1000) - DAYS_IN_SECONDS[period]; }; export const Delegates = ({ @@ -88,16 +74,20 @@ export const Delegates = ({ ), ); const { decimals } = daoConfig[daoId]; + const { data: daoData } = useDaoData(daoId); + + const votingPeriodSeconds = useMemo(() => { + if (!daoData?.votingPeriod) return 0; + const blockTime = daoConfig[daoId].daoOverview.chain.blockTime; + return (Number(daoData.votingPeriod) * blockTime) / 1000; + }, [daoData?.votingPeriod, daoId]); const handleAddressFilterApply = (address: string | undefined) => { setCurrentAddressFilter(address || ""); }; // Calculate time-based parameters - const fromDate = useMemo( - () => getTimeDataFromPeriod(timePeriod), - [timePeriod], - ); + const fromDate = useMemo(() => getFromTimestamp(timePeriod), [timePeriod]); const { data, @@ -151,6 +141,12 @@ export const Delegates = ({ 100 : null; + const avgVoteTiming = getAvgVoteTimingData( + delegate.proposalsActivity?.avgTimeBeforeEnd, + votingPeriodSeconds, + delegate.proposalsActivity?.votedProposals, + ); + return { address: delegate.accountId, votingPower: formatNumberUserReadable(votingPowerFormatted), @@ -166,9 +162,10 @@ export const Delegates = ({ activity, activityPercentage, delegators: delegate.delegationsCount, + avgVoteTiming, }; }); - }, [data, decimals]); + }, [data, decimals, votingPeriodSeconds]); const delegateColumns: ColumnDef[] = [ { @@ -281,7 +278,7 @@ export const Delegates = ({ ), meta: { - columnClassName: "w-72", + columnClassName: "w-40", }, }, { @@ -351,6 +348,54 @@ export const Delegates = ({ ), }, + { + accessorKey: "avgVoteTiming", + cell: ({ row }) => { + const avgVoteTiming = row.getValue("avgVoteTiming") as { + text: string; + percentage: number; + } | null; + + if (loading) { + return ( +
+ +
+ ); + } + + if (!avgVoteTiming) { + return
-
; + } + + return ( +
+
+ {avgVoteTiming.text} +
+ {avgVoteTiming.text !== "-" && ( + + )} +
+ ); + }, + header: () => ( +
+ +

+ Avg Vote Timing +

+
+
+ ), + meta: { + columnClassName: "w-40", + }, + }, { accessorKey: "delegators", cell: ({ row }) => { diff --git a/apps/dashboard/features/holders-and-delegates/delegate/drawer/delegation-history/DelegateDelegationsHistory.tsx b/apps/dashboard/features/holders-and-delegates/delegate/drawer/delegation-history/DelegateDelegationsHistory.tsx deleted file mode 100644 index 8a8b79351..000000000 --- a/apps/dashboard/features/holders-and-delegates/delegate/drawer/delegation-history/DelegateDelegationsHistory.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { DaoIdEnum } from "@/shared/types/daos"; -import { DelegateDelegationHistoryTable } from "@/features/holders-and-delegates/delegate/drawer/delegation-history/DelegateDelegationHistoryTable"; -import { VotingPowerVariationGraph } from "@/features/holders-and-delegates/delegate/drawer/delegation-history/VotingPowerVariationGraph"; - -interface DelegateDelegationsHistoryProps { - accountId: string; - daoId: DaoIdEnum; -} - -export const DelegateDelegationsHistory = ({ - accountId, - daoId, -}: DelegateDelegationsHistoryProps) => { - return ( -
- {/* Graph Section */} -
- -
- - {/* Table Section */} -
- -
-
- ); -}; diff --git a/apps/dashboard/features/holders-and-delegates/delegate/drawer/index.ts b/apps/dashboard/features/holders-and-delegates/delegate/drawer/index.ts index b0a615973..126953520 100644 --- a/apps/dashboard/features/holders-and-delegates/delegate/drawer/index.ts +++ b/apps/dashboard/features/holders-and-delegates/delegate/drawer/index.ts @@ -1,3 +1,5 @@ -export * from "@/features/holders-and-delegates/delegate/drawer/voting-power/VotingPower"; -export * from "@/features/holders-and-delegates/delegate/drawer/voting-power/ThePieChart"; -export * from "@/features/holders-and-delegates/delegate/drawer/voting-power/VotingPowerTable"; +export * from "@/features/holders-and-delegates/delegate/drawer/vote-composition/VoteComposition"; +export * from "@/features/holders-and-delegates/delegate/drawer/vote-composition/ThePieChart"; +export * from "@/features/holders-and-delegates/delegate/drawer/vote-composition/VoteCompositionTable"; +export * from "@/features/holders-and-delegates/delegate/drawer/voting-power-history/VotingPowerHistory"; +export * from "@/features/holders-and-delegates/delegate/drawer/voting-power-history/VotingPowerHistoryTable"; diff --git a/apps/dashboard/features/holders-and-delegates/delegate/drawer/voting-power/ThePieChart.tsx b/apps/dashboard/features/holders-and-delegates/delegate/drawer/vote-composition/ThePieChart.tsx similarity index 97% rename from apps/dashboard/features/holders-and-delegates/delegate/drawer/voting-power/ThePieChart.tsx rename to apps/dashboard/features/holders-and-delegates/delegate/drawer/vote-composition/ThePieChart.tsx index 85e89e716..38810a52c 100644 --- a/apps/dashboard/features/holders-and-delegates/delegate/drawer/voting-power/ThePieChart.tsx +++ b/apps/dashboard/features/holders-and-delegates/delegate/drawer/vote-composition/ThePieChart.tsx @@ -9,7 +9,7 @@ import { TooltipProps, } from "recharts"; import { formatNumberUserReadable } from "@/shared/utils"; -import { renderCustomizedLabel } from "@/features/holders-and-delegates/delegate/drawer/voting-power/utils/renderCustomizedLabel"; +import { renderCustomizedLabel } from "@/features/holders-and-delegates/delegate/drawer/vote-composition/utils/renderCustomizedLabel"; import { SkeletonRow } from "@/shared/components/skeletons/SkeletonRow"; import { AnticaptureWatermark } from "@/shared/components/icons/AnticaptureWatermark"; diff --git a/apps/dashboard/features/holders-and-delegates/delegate/drawer/voting-power/VotingPower.tsx b/apps/dashboard/features/holders-and-delegates/delegate/drawer/vote-composition/VoteComposition.tsx similarity index 64% rename from apps/dashboard/features/holders-and-delegates/delegate/drawer/voting-power/VotingPower.tsx rename to apps/dashboard/features/holders-and-delegates/delegate/drawer/vote-composition/VoteComposition.tsx index 6165a367b..a8ca88e16 100644 --- a/apps/dashboard/features/holders-and-delegates/delegate/drawer/voting-power/VotingPower.tsx +++ b/apps/dashboard/features/holders-and-delegates/delegate/drawer/vote-composition/VoteComposition.tsx @@ -1,11 +1,11 @@ "use client"; -import { ThePieChart } from "@/features/holders-and-delegates/delegate/drawer/voting-power/ThePieChart"; -import { VotingPowerTable } from "@/features/holders-and-delegates/delegate/drawer/voting-power/VotingPowerTable"; +import { ThePieChart } from "@/features/holders-and-delegates/delegate/drawer/vote-composition/ThePieChart"; +import { VoteCompositionTable } from "@/features/holders-and-delegates/delegate/drawer/vote-composition/VoteCompositionTable"; import { DaoIdEnum } from "@/shared/types/daos"; import { formatNumberUserReadable } from "@/shared/utils"; import { SkeletonRow } from "@/shared/components/skeletons/SkeletonRow"; -import { useVotingPowerData } from "@/features/holders-and-delegates/delegate/drawer/voting-power/hooks/useVotingPowerData"; +import { useVoteCompositionData } from "@/features/holders-and-delegates/delegate/drawer/vote-composition/hooks/useVoteCompositionData"; import { BlankSlate } from "@/shared/components/design-system/blank-slate/BlankSlate"; import { Inbox } from "lucide-react"; @@ -37,32 +37,36 @@ const ChartLegend = ({ return (
- {items.map((item) => { - return ( -
- - - {item.label} + {items.length === 0 ? ( +
No delegators found
+ ) : ( + items.map((item) => { + return ( +
- {item.percentage}% + className="rounded-xs size-2" + style={{ backgroundColor: item.color }} + /> + + {item.label} + + {item.percentage}% + - -
- ); - })} +
+ ); + }) + )}
); }; -export const VotingPower = ({ +export const VoteComposition = ({ address, daoId, }: { @@ -76,20 +80,7 @@ export const VotingPower = ({ pieData, chartConfig, loading: loadingVotingPowerData, - } = useVotingPowerData(daoId, address); - - if ( - !topFiveDelegators || - (topFiveDelegators.length === 0 && !loadingVotingPowerData) - ) { - return ( - - ); - } + } = useVoteCompositionData(daoId, address); return (
@@ -129,19 +120,10 @@ export const VotingPower = ({

- {!legendItems || !topFiveDelegators ? ( - - ) : !topFiveDelegators ? ( -
- Loading delegators... -
- ) : topFiveDelegators && topFiveDelegators.length > 0 ? ( - - ) : ( -
- No delegators found -
- )} +
@@ -149,7 +131,7 @@ export const VotingPower = ({
- +
); diff --git a/apps/dashboard/features/holders-and-delegates/delegate/drawer/voting-power/VotingPowerTable.tsx b/apps/dashboard/features/holders-and-delegates/delegate/drawer/vote-composition/VoteCompositionTable.tsx similarity index 89% rename from apps/dashboard/features/holders-and-delegates/delegate/drawer/voting-power/VotingPowerTable.tsx rename to apps/dashboard/features/holders-and-delegates/delegate/drawer/vote-composition/VoteCompositionTable.tsx index 5ca873d7b..af25945c2 100644 --- a/apps/dashboard/features/holders-and-delegates/delegate/drawer/voting-power/VotingPowerTable.tsx +++ b/apps/dashboard/features/holders-and-delegates/delegate/drawer/vote-composition/VoteCompositionTable.tsx @@ -16,19 +16,20 @@ import { CopyAndPasteButton } from "@/shared/components/buttons/CopyAndPasteButt import { parseAsStringEnum, useQueryState } from "nuqs"; import { QueryInput_AccountBalances_OrderDirection } from "@anticapture/graphql-client"; import { DEFAULT_ITEMS_PER_PAGE } from "@/features/holders-and-delegates/utils"; +import { DateCell } from "@/shared/components/design-system/table/cells/DateCell"; -export const VotingPowerTable = ({ +export const VoteCompositionTable = ({ address, daoId, }: { address: string; daoId: string; }) => { - const limit: number = 20; + const limit: number = DEFAULT_ITEMS_PER_PAGE; const [isMounted, setIsMounted] = useState(false); const [sortBy, setSortBy] = useQueryState( "orderBy", - parseAsStringEnum(["balance"]).withDefault("balance"), + parseAsStringEnum(["balance", "timestamp"]).withDefault("balance"), ); const [sortOrder, setSortOrder] = useQueryState( "orderDirection", @@ -42,7 +43,7 @@ export const VotingPowerTable = ({ useVotingPower({ daoId: daoId as DaoIdEnum, address: address, - orderBy: sortBy, + orderBy: sortBy as "balance" | "timestamp", orderDirection: sortOrder as QueryInput_AccountBalances_OrderDirection, limit, }); @@ -55,14 +56,14 @@ export const VotingPowerTable = ({ return { address: account.address, amount: Number(account.balance) || 0, - date: account.timestamp, + timestamp: account.timestamp, }; }); const columns: ColumnDef<{ address: string; amount: number; - date: string; + timestamp: string; }>[] = [ { accessorKey: "address", @@ -171,16 +172,16 @@ export const VotingPowerTable = ({ }, }, { - accessorKey: "date", + accessorKey: "timestamp", header: () => { return ( -
+
Date
); }, cell: ({ row }) => { - const date: string = row.getValue("date"); + const timestamp: string = row.getValue("timestamp"); if (!isMounted || loading) { return ( @@ -194,14 +195,8 @@ export const VotingPowerTable = ({ } return ( -
- {date - ? new Date(Number(date) * 1000).toLocaleDateString("en-US", { - year: "numeric", - month: "long", - day: "numeric", - }) - : "N/A"} +
+ {timestamp ? : "N/A"}
); }, @@ -212,7 +207,7 @@ export const VotingPowerTable = ({ ]; return ( -
+
{ +): VoteCompositionData => { const { decimals, daoOverview: { token }, @@ -56,7 +56,7 @@ export const useVotingPowerData = ( const { data: ensData } = useMultipleEnsData(delegatorAddresses); // default Value when there is no data - const defaultData: VotingPowerData = { + const defaultData: VoteCompositionData = { topFiveDelegators: [], currentVotingPower: 0, loading, @@ -82,9 +82,9 @@ export const useVotingPowerData = ( const delegateCurrentVotingPower = accountPowerVotingPower ? BigInt(accountPowerVotingPower) : topFiveDelegators.reduce( - (acc, item) => acc + BigInt(item.rawBalance), - BigInt(0), - ); + (acc, item) => acc + BigInt(item.rawBalance), + BigInt(0), + ); const currentVotingPowerNumber = Number( token === "ERC20" @@ -119,7 +119,7 @@ export const useVotingPowerData = ( const percentage = Number( (Number(BigInt(delegator.rawBalance)) / Number(delegateCurrentVotingPower)) * - 100, + 100, ); const ensName = ensData?.[delegator.address as Address]?.ens; diff --git a/apps/dashboard/features/holders-and-delegates/delegate/drawer/voting-power/utils/renderCustomizedLabel.tsx b/apps/dashboard/features/holders-and-delegates/delegate/drawer/vote-composition/utils/renderCustomizedLabel.tsx similarity index 100% rename from apps/dashboard/features/holders-and-delegates/delegate/drawer/voting-power/utils/renderCustomizedLabel.tsx rename to apps/dashboard/features/holders-and-delegates/delegate/drawer/vote-composition/utils/renderCustomizedLabel.tsx diff --git a/apps/dashboard/features/holders-and-delegates/delegate/drawer/votes/DelegateProposalsActivity.tsx b/apps/dashboard/features/holders-and-delegates/delegate/drawer/votes/DelegateProposalsActivity.tsx index d29df9045..786dc7bb3 100644 --- a/apps/dashboard/features/holders-and-delegates/delegate/drawer/votes/DelegateProposalsActivity.tsx +++ b/apps/dashboard/features/holders-and-delegates/delegate/drawer/votes/DelegateProposalsActivity.tsx @@ -20,6 +20,22 @@ interface DelegateProposalsActivityProps { daoId: DaoIdEnum; } +export const formatAvgTime = ( + avgTimeBeforeEndSeconds: number, + votedProposals: number, +): string => { + const avgTimeBeforeEndDays = avgTimeBeforeEndSeconds / SECONDS_PER_DAY; + + if (!votedProposals) { + return "-"; + } + + if (avgTimeBeforeEndDays < 1) { + return "< 1d before the end"; + } + return `${Math.round(avgTimeBeforeEndDays)}d before the end`; +}; + export const DelegateProposalsActivity = ({ address, daoId, @@ -65,23 +81,6 @@ export const DelegateProposalsActivity = ({ limit, }); - // Helper function to format average time (convert seconds to days) - const formatAvgTime = ( - avgTimeBeforeEndSeconds: number, - votedProposals: number, - ): string => { - const avgTimeBeforeEndDays = avgTimeBeforeEndSeconds / SECONDS_PER_DAY; - - if (!votedProposals) { - return "-"; - } - - if (avgTimeBeforeEndDays < 1) { - return "< 1d before the end"; - } - return `${Math.round(avgTimeBeforeEndDays)}d before the end`; - }; - // Prepare values - undefined when loading/error, actual values when data is available const votedProposalsValue = loading || error || !data diff --git a/apps/dashboard/features/holders-and-delegates/delegate/drawer/votes/ProposalsTable.tsx b/apps/dashboard/features/holders-and-delegates/delegate/drawer/votes/ProposalsTable.tsx index bcb2085b0..b6ad98f83 100644 --- a/apps/dashboard/features/holders-and-delegates/delegate/drawer/votes/ProposalsTable.tsx +++ b/apps/dashboard/features/holders-and-delegates/delegate/drawer/votes/ProposalsTable.tsx @@ -29,6 +29,7 @@ import { } from "@/features/holders-and-delegates/utils/proposalsTableUtils"; import { Table } from "@/shared/components/design-system/table/Table"; import daoConfig from "@/shared/dao-config"; +import { Tooltip } from "@/shared/components/design-system/tooltips/Tooltip"; import { DEFAULT_ITEMS_PER_PAGE } from "@/features/holders-and-delegates/utils"; interface ProposalTableData { proposalId: string; @@ -227,7 +228,7 @@ export const ProposalsTable = ({ ); }, header: () => ( -
+
User Vote {userVoteFilterOptions && onUserVoteFilterChange && ( ( - +
+ + + +
), }, { diff --git a/apps/dashboard/features/holders-and-delegates/delegate/drawer/voting-power-history/VotingPowerHistory.tsx b/apps/dashboard/features/holders-and-delegates/delegate/drawer/voting-power-history/VotingPowerHistory.tsx new file mode 100644 index 000000000..06495fb31 --- /dev/null +++ b/apps/dashboard/features/holders-and-delegates/delegate/drawer/voting-power-history/VotingPowerHistory.tsx @@ -0,0 +1,46 @@ +import { DaoIdEnum } from "@/shared/types/daos"; +import { VotingPowerHistoryTable } from "@/features/holders-and-delegates/delegate/drawer/voting-power-history/VotingPowerHistoryTable"; +import { VotingPowerVariationGraph } from "@/features/holders-and-delegates/delegate/drawer/voting-power-history/VotingPowerVariationGraph"; +import { parseAsStringEnum, useQueryState } from "nuqs"; +import { useMemo } from "react"; +import { getTimestampRangeFromPeriod } from "@/features/holders-and-delegates/utils"; +import { TimePeriod } from "@/features/holders-and-delegates/components/TimePeriodSwitcher"; + +interface VotingPowerHistoryProps { + accountId: string; + daoId: DaoIdEnum; +} + +export const VotingPowerHistory = ({ + accountId, + daoId, +}: VotingPowerHistoryProps) => { + const [selectedPeriod] = useQueryState( + "selectedPeriod", + parseAsStringEnum(["30d", "90d", "all"]).withDefault("all"), + ); + + const { fromTimestamp, toTimestamp } = useMemo( + () => getTimestampRangeFromPeriod(selectedPeriod), + [selectedPeriod], + ); + + return ( +
+ {/* Graph Section */} +
+ +
+ + {/* Table Section */} +
+ +
+
+ ); +}; diff --git a/apps/dashboard/features/holders-and-delegates/delegate/drawer/delegation-history/DelegateDelegationHistoryTable.tsx b/apps/dashboard/features/holders-and-delegates/delegate/drawer/voting-power-history/VotingPowerHistoryTable.tsx similarity index 88% rename from apps/dashboard/features/holders-and-delegates/delegate/drawer/delegation-history/DelegateDelegationHistoryTable.tsx rename to apps/dashboard/features/holders-and-delegates/delegate/drawer/voting-power-history/VotingPowerHistoryTable.tsx index d510ab785..bf3629f0e 100644 --- a/apps/dashboard/features/holders-and-delegates/delegate/drawer/delegation-history/DelegateDelegationHistoryTable.tsx +++ b/apps/dashboard/features/holders-and-delegates/delegate/drawer/voting-power-history/VotingPowerHistoryTable.tsx @@ -18,10 +18,7 @@ import { import daoConfigByDaoId from "@/shared/dao-config"; import { Table } from "@/shared/components/design-system/table/Table"; import { AmountFilter } from "@/shared/components/design-system/table/filters/amount-filter/AmountFilter"; -import { - AmountFilterState, - useAmountFilterStore, -} from "@/shared/components/design-system/table/filters/amount-filter/store/amount-filter-store"; +import { useAmountFilterStore } from "@/shared/components/design-system/table/filters/amount-filter/store/amount-filter-store"; import daoConfig from "@/shared/dao-config"; import { CopyAndPasteButton } from "@/shared/components/buttons/CopyAndPasteButton"; import { @@ -32,16 +29,21 @@ import { useQueryStates, } from "nuqs"; import { DEFAULT_ITEMS_PER_PAGE } from "@/features/holders-and-delegates/utils"; +import { DateCell } from "@/shared/components/design-system/table/cells/DateCell"; -interface DelegateDelegationHistoryTableProps { +interface VotingPowerHistoryTableProps { accountId: string; daoId: DaoIdEnum; + fromTimestamp?: number; + toTimestamp?: number; } -export const DelegateDelegationHistoryTable = ({ +export const VotingPowerHistoryTable = ({ accountId, daoId, -}: DelegateDelegationHistoryTableProps) => { + fromTimestamp, + toTimestamp, +}: VotingPowerHistoryTableProps) => { const limit: number = 20; const { decimals } = daoConfig[daoId]; @@ -61,8 +63,7 @@ export const DelegateDelegationHistoryTable = ({ "active", parseAsBoolean.withDefault(false), ); - // const [fromFilter, setFromFilter] = useQueryState("from", parseAsAddress); - // const [toFilter, setToFilter] = useQueryState("to", parseAsAddress); + const sortOptions: SortOption[] = [ { value: "largest-first", label: "Largest first" }, { value: "smallest-first", label: "Smallest first" }, @@ -75,40 +76,14 @@ export const DelegateDelegationHistoryTable = ({ orderBy: sortBy, orderDirection: sortDirection, filterVariables, + fromTimestamp, + toTimestamp, limit, }); const isInitialLoading = loading && (!delegationHistory || delegationHistory.length === 0); - // Format timestamp to relative time - const formatRelativeTime = (timestamp: string) => { - const date = new Date(parseInt(timestamp) * 1000); - const now = new Date(); - const diffInMs = now.getTime() - date.getTime(); - const diffInSeconds = Math.floor(diffInMs / 1000); - const diffInMinutes = Math.floor(diffInSeconds / 60); - const diffInHours = Math.floor(diffInMinutes / 60); - const diffInDays = Math.floor(diffInHours / 24); - const diffInWeeks = Math.floor(diffInDays / 7); - const diffInMonths = Math.floor(diffInDays / 30); - - if (diffInMonths > 0) { - return `${diffInMonths} month${diffInMonths > 1 ? "s" : ""} ago`; - } else if (diffInWeeks > 0) { - return `${diffInWeeks} week${diffInWeeks > 1 ? "s" : ""} ago`; - } else if (diffInDays > 0) { - return `${diffInDays} day${diffInDays > 1 ? "s" : ""} ago`; - } else if (diffInHours > 0) { - return `${diffInHours} hour${diffInHours > 1 ? "s" : ""} ago`; - } else if (diffInMinutes > 0) { - return `${diffInMinutes} minute${diffInMinutes > 1 ? "s" : ""} ago`; - } else { - return "Just now"; - } - }; - - // Determine delegation type and color based on gain/loss const getDelegationType = (item: DelegationHistoryItem) => { let statusText = ""; @@ -188,9 +163,7 @@ export const DelegateDelegationHistoryTable = ({ return (
- - {formatRelativeTime(timestamp)} - +
); }, @@ -205,7 +178,7 @@ export const DelegateDelegationHistoryTable = ({

Amount ({daoId})

{ + onApply={(filterState) => { if (filterState.sortOrder) { setSortDirection( filterState.sortOrder === "largest-first" ? "desc" : "asc", @@ -236,7 +209,6 @@ export const DelegateDelegationHistoryTable = ({ onReset={() => { setIsFilterActive(false); setSortBy("timestamp"); - setSortDirection("desc"); setFilterVariables(() => ({ fromValue: "", toValue: "", @@ -502,7 +474,7 @@ export const DelegateDelegationHistoryTable = ({ ]; return ( -
+
{ const [selectedPeriod, setSelectedPeriod] = useQueryState( "selectedPeriod", - parseAsStringEnum(["30d", "90d", "all"]).withDefault("all"), + parseAsStringEnum(["30d", "90d", "all"]).withDefault("all"), ); - // Calculate timestamp range based on time period - const { fromTimestamp, toTimestamp } = useMemo(() => { - // For "all", treat as all time by not setting limits - if (selectedPeriod === "all") { - return { fromTimestamp: undefined, toTimestamp: undefined }; - } - - const nowInSeconds = Date.now() / 1000; - let daysInSeconds: number; - switch (selectedPeriod) { - case "90d": - daysInSeconds = 90 * SECONDS_PER_DAY; - break; - default: - daysInSeconds = 30 * SECONDS_PER_DAY; - break; - } - - return { - fromTimestamp: Math.floor(nowInSeconds - daysInSeconds), - toTimestamp: Math.floor(nowInSeconds), - }; - }, [selectedPeriod]); + const { fromTimestamp, toTimestamp } = useMemo( + () => getTimestampRangeFromPeriod(selectedPeriod), + [selectedPeriod], + ); const { delegationHistory, loading, error } = useDelegateDelegationHistoryGraph( diff --git a/apps/dashboard/features/holders-and-delegates/delegate/drawer/voting-power/hooks/index.ts b/apps/dashboard/features/holders-and-delegates/delegate/drawer/voting-power/hooks/index.ts deleted file mode 100644 index 44588c5a0..000000000 --- a/apps/dashboard/features/holders-and-delegates/delegate/drawer/voting-power/hooks/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { useVotingPowerData } from "./useVotingPowerData"; -export type { VotingPowerData } from "./useVotingPowerData"; diff --git a/apps/dashboard/features/holders-and-delegates/hooks/index.ts b/apps/dashboard/features/holders-and-delegates/hooks/index.ts index 71651e15c..32319f3bc 100644 --- a/apps/dashboard/features/holders-and-delegates/hooks/index.ts +++ b/apps/dashboard/features/holders-and-delegates/hooks/index.ts @@ -1,12 +1,21 @@ -export * from "./useDelegates"; -export * from "./useTokenHolders"; -export * from "./useBalanceHistory"; +export type { + PaginationInfo, + SimplePaginationInfo, + AmountFilterVariables, + LoadingState, +} from "./types"; +export { useDelegates } from "./useDelegates"; +export { useTokenHolders } from "./useTokenHolders"; +export type { TokenHolder } from "./useTokenHolders"; +export { useBalanceHistory } from "./useBalanceHistory"; export { useDelegateDelegationHistory, type DelegationHistoryItem, type UseDelegateDelegationHistoryResult, } from "./useDelegateDelegationHistory"; -export * from "./useDelegateDelegationHistoryGraph"; +export { useDelegateDelegationHistoryGraph } from "./useDelegateDelegationHistoryGraph"; +export type { DelegationHistoryGraphItem } from "./useDelegateDelegationHistoryGraph"; export { useDelegationHistory } from "./useDelegationHistory"; -export { useTokenHolders } from "./useTokenHolders"; -export { useDelegates } from "./useDelegates"; +export { useBalanceHistoryGraph } from "./useBalanceHistoryGraph"; +export type { BalanceHistoryGraphItem } from "./useBalanceHistoryGraph"; +export { useProposalsActivity } from "./useProposalsActivity"; diff --git a/apps/dashboard/features/holders-and-delegates/hooks/types.ts b/apps/dashboard/features/holders-and-delegates/hooks/types.ts new file mode 100644 index 000000000..c99293e7a --- /dev/null +++ b/apps/dashboard/features/holders-and-delegates/hooks/types.ts @@ -0,0 +1,24 @@ +export interface PaginationInfo { + hasNextPage: boolean; + hasPreviousPage: boolean; + currentPage: number; + totalPages: number; + totalCount: number; + currentItemsCount: number; +} + +export interface SimplePaginationInfo { + hasNextPage: boolean; + hasPreviousPage: boolean; + currentPage: number; +} + +export interface AmountFilterVariables { + fromValue?: string | null; + toValue?: string | null; +} + +export interface LoadingState { + loading: boolean; + fetchingMore: boolean; +} diff --git a/apps/dashboard/features/holders-and-delegates/hooks/useBalanceHistory.ts b/apps/dashboard/features/holders-and-delegates/hooks/useBalanceHistory.ts index 07897bdc6..d6e4750fb 100644 --- a/apps/dashboard/features/holders-and-delegates/hooks/useBalanceHistory.ts +++ b/apps/dashboard/features/holders-and-delegates/hooks/useBalanceHistory.ts @@ -1,3 +1,5 @@ +"use client"; + import { formatUnits } from "viem"; import { useMemo, useState, useEffect, useCallback } from "react"; @@ -9,7 +11,7 @@ import { } from "@anticapture/graphql-client/hooks"; import { DaoIdEnum } from "@/shared/types/daos"; -import { AmountFilterVariables } from "@/features/holders-and-delegates/hooks/useDelegateDelegationHistory"; +import { AmountFilterVariables } from "./types"; import { QueryInput_Transfers_SortBy, QueryInput_Transfers_SortOrder, @@ -26,6 +28,8 @@ export function useBalanceHistory({ filterVariables, limit = 10, decimals, + fromTimestamp, + toTimestamp, }: { accountId: string; daoId: DaoIdEnum; @@ -36,6 +40,9 @@ export function useBalanceHistory({ orderDirection?: "asc" | "desc"; transactionType?: "all" | "buy" | "sell"; filterVariables?: AmountFilterVariables; + itemsPerPage?: number; + fromTimestamp?: number; + toTimestamp?: number; limit?: number; }) { const [currentPage, setCurrentPage] = useState(1); @@ -51,6 +58,8 @@ export function useBalanceHistory({ customFromFilter, customToFilter, filterVariables, + fromTimestamp, + toTimestamp, ]); const variables = useMemo(() => { @@ -63,6 +72,8 @@ export function useBalanceHistory({ from: customFromFilter, to: customToFilter, offset: 0, + fromDate: fromTimestamp, + toDate: toTimestamp, limit, }; @@ -85,6 +96,8 @@ export function useBalanceHistory({ filterVariables, orderBy, orderDirection, + fromTimestamp, + toTimestamp, limit, ]); diff --git a/apps/dashboard/features/holders-and-delegates/hooks/useBalanceHistoryGraph.ts b/apps/dashboard/features/holders-and-delegates/hooks/useBalanceHistoryGraph.ts index 9f65ce99b..70290f400 100644 --- a/apps/dashboard/features/holders-and-delegates/hooks/useBalanceHistoryGraph.ts +++ b/apps/dashboard/features/holders-and-delegates/hooks/useBalanceHistoryGraph.ts @@ -1,3 +1,5 @@ +"use client"; + import { useMemo } from "react"; import { formatUnits } from "viem"; diff --git a/apps/dashboard/features/holders-and-delegates/hooks/useDelegateDelegationHistory.ts b/apps/dashboard/features/holders-and-delegates/hooks/useDelegateDelegationHistory.ts index f34cab626..02329fd58 100644 --- a/apps/dashboard/features/holders-and-delegates/hooks/useDelegateDelegationHistory.ts +++ b/apps/dashboard/features/holders-and-delegates/hooks/useDelegateDelegationHistory.ts @@ -12,6 +12,7 @@ import { HistoricalVotingPowerByAccountQueryVariables, QueryInput_HistoricalVotingPowerByAccountId_OrderDirection, } from "@anticapture/graphql-client"; +import { AmountFilterVariables } from "./types"; // Interface for a single delegation history item export interface DelegationHistoryItem { @@ -52,10 +53,19 @@ export interface UseDelegateDelegationHistoryResult { hasPreviousPage: boolean; } -export type AmountFilterVariables = Pick< - HistoricalVotingPowerByAccountQueryVariables, - "fromValue" | "toValue" ->; +interface UseDelegateDelegationHistoryParams { + accountId: string; + daoId: DaoIdEnum; + orderBy?: string; + orderDirection?: "asc" | "desc"; + customFromFilter?: string; + customToFilter?: string; + filterVariables?: AmountFilterVariables; + itemsPerPage?: number; + fromTimestamp?: number; + toTimestamp?: number; + limit?: number; +} export function useDelegateDelegationHistory({ accountId, @@ -65,18 +75,10 @@ export function useDelegateDelegationHistory({ filterVariables, customFromFilter, customToFilter, + fromTimestamp, + toTimestamp, limit = 10, -}: { - accountId: string; - daoId: DaoIdEnum; - orderBy?: string; - orderDirection?: "asc" | "desc"; - transactionType?: "all" | "buy" | "sell"; - customFromFilter?: string; - customToFilter?: string; - filterVariables?: AmountFilterVariables; - limit?: number; -}): UseDelegateDelegationHistoryResult { +}: UseDelegateDelegationHistoryParams): UseDelegateDelegationHistoryResult { const [currentPage, setCurrentPage] = useState(1); const [isPaginationLoading, setIsPaginationLoading] = useState(false); @@ -123,6 +125,8 @@ export function useDelegateDelegationHistory({ }), ...(fromFilter && { delegator: fromFilter }), ...(toFilter && { delegate: toFilter }), + ...(fromTimestamp && { fromDate: fromTimestamp.toString() }), + ...(toTimestamp && { toDate: toTimestamp.toString() }), }), [ accountId, @@ -132,6 +136,8 @@ export function useDelegateDelegationHistory({ filterVariables, fromFilter, toFilter, + fromTimestamp, + toTimestamp, ], ); diff --git a/apps/dashboard/features/holders-and-delegates/hooks/useDelegateDelegationHistoryGraph.ts b/apps/dashboard/features/holders-and-delegates/hooks/useDelegateDelegationHistoryGraph.ts index 35eead289..a5ac00437 100644 --- a/apps/dashboard/features/holders-and-delegates/hooks/useDelegateDelegationHistoryGraph.ts +++ b/apps/dashboard/features/holders-and-delegates/hooks/useDelegateDelegationHistoryGraph.ts @@ -1,3 +1,5 @@ +"use client"; + import { useMemo } from "react"; import { QueryInput_HistoricalVotingPowerByAccountId_OrderDirection, diff --git a/apps/dashboard/features/holders-and-delegates/hooks/useDelegates.ts b/apps/dashboard/features/holders-and-delegates/hooks/useDelegates.ts index bd144a036..60c0a9fd2 100644 --- a/apps/dashboard/features/holders-and-delegates/hooks/useDelegates.ts +++ b/apps/dashboard/features/holders-and-delegates/hooks/useDelegates.ts @@ -19,6 +19,7 @@ interface ProposalsActivity { totalProposals: number; votedProposals: number; neverVoted: boolean; + avgTimeBeforeEnd?: number; } interface Delegate { @@ -242,6 +243,7 @@ export const useDelegates = ({ }; const proposalsActivity = delegateActivities.get(delegate.accountId); + return { ...delegate, ...votingPowerVariation, diff --git a/apps/dashboard/features/holders-and-delegates/hooks/useDelegationHistory.ts b/apps/dashboard/features/holders-and-delegates/hooks/useDelegationHistory.ts index f6ae9c80b..612ab6267 100644 --- a/apps/dashboard/features/holders-and-delegates/hooks/useDelegationHistory.ts +++ b/apps/dashboard/features/holders-and-delegates/hooks/useDelegationHistory.ts @@ -8,19 +8,10 @@ import { import { useMemo, useCallback, useState, useEffect } from "react"; import { NetworkStatus } from "@apollo/client"; import { DaoIdEnum } from "@/shared/types/daos"; -import { AmountFilterVariables } from "@/features/holders-and-delegates/hooks/useDelegateDelegationHistory"; - -interface PaginationInfo { - hasNextPage: boolean; - hasPreviousPage: boolean; - endCursor?: string | null; - startCursor?: string | null; - totalCount: number; - currentPage: number; - totalPages: number; - limit: number; - currentItemsCount: number; -} +import { + AmountFilterVariables, + PaginationInfo, +} from "@/features/holders-and-delegates/hooks/types"; interface UseDelegationHistoryResult { data: diff --git a/apps/dashboard/features/holders-and-delegates/hooks/useProposalsActivity.ts b/apps/dashboard/features/holders-and-delegates/hooks/useProposalsActivity.ts index fd951dbbb..6ac7acdca 100644 --- a/apps/dashboard/features/holders-and-delegates/hooks/useProposalsActivity.ts +++ b/apps/dashboard/features/holders-and-delegates/hooks/useProposalsActivity.ts @@ -1,3 +1,5 @@ +"use client"; + import { useCallback, useEffect, useMemo, useState } from "react"; import { useGetProposalsActivityQuery } from "@anticapture/graphql-client/hooks"; diff --git a/apps/dashboard/features/holders-and-delegates/token-holder/drawer/balance-history/BalanceHistory.tsx b/apps/dashboard/features/holders-and-delegates/token-holder/drawer/balance-history/BalanceHistory.tsx index c24424591..a9e0a4dc2 100644 --- a/apps/dashboard/features/holders-and-delegates/token-holder/drawer/balance-history/BalanceHistory.tsx +++ b/apps/dashboard/features/holders-and-delegates/token-holder/drawer/balance-history/BalanceHistory.tsx @@ -3,6 +3,10 @@ import { DaoIdEnum } from "@/shared/types/daos"; import { BalanceHistoryVariationGraph } from "@/features/holders-and-delegates/token-holder/drawer/balance-history/BalanceHistoryVariationGraph"; import { BalanceHistoryTable } from "@/features/holders-and-delegates/token-holder/drawer/balance-history/BalanceHistoryTable"; +import { parseAsStringEnum, useQueryState } from "nuqs"; +import { useMemo } from "react"; +import { getTimestampRangeFromPeriod } from "@/features/holders-and-delegates/utils"; +import { TimePeriod } from "@/features/holders-and-delegates/components/TimePeriodSwitcher"; interface BalanceHistoryProps { accountId: string; @@ -10,6 +14,16 @@ interface BalanceHistoryProps { } export const BalanceHistory = ({ accountId, daoId }: BalanceHistoryProps) => { + const [selectedPeriod] = useQueryState( + "selectedPeriod", + parseAsStringEnum(["30d", "90d", "all"]).withDefault("all"), + ); + + const { fromTimestamp, toTimestamp } = useMemo( + () => getTimestampRangeFromPeriod(selectedPeriod), + [selectedPeriod], + ); + return (
{/* Graph Section */} @@ -19,7 +33,12 @@ export const BalanceHistory = ({ accountId, daoId }: BalanceHistoryProps) => { {/* Table Section */}
- +
); diff --git a/apps/dashboard/features/holders-and-delegates/token-holder/drawer/balance-history/BalanceHistoryTable.tsx b/apps/dashboard/features/holders-and-delegates/token-holder/drawer/balance-history/BalanceHistoryTable.tsx index 0feb2388b..2967016fc 100644 --- a/apps/dashboard/features/holders-and-delegates/token-holder/drawer/balance-history/BalanceHistoryTable.tsx +++ b/apps/dashboard/features/holders-and-delegates/token-holder/drawer/balance-history/BalanceHistoryTable.tsx @@ -33,10 +33,11 @@ import { } from "nuqs"; import { DEFAULT_ITEMS_PER_PAGE } from "@/features/holders-and-delegates/utils"; import { useAmountFilterStore } from "@/shared/components/design-system/table/filters/amount-filter/store/amount-filter-store"; +import { DateCell } from "@/shared/components/design-system/table/cells/DateCell"; interface BalanceHistoryData { id: string; - date: string; + timestamp: string; amount: string; type: "Buy" | "Sell"; fromAddress: string; @@ -48,9 +49,13 @@ interface BalanceHistoryData { export const BalanceHistoryTable = ({ accountId, daoId, + fromTimestamp, + toTimestamp, }: { accountId: string; daoId: DaoIdEnum; + fromTimestamp?: number; + toTimestamp?: number; }) => { const limit: number = 20; const { decimals } = daoConfig[daoId]; @@ -105,6 +110,8 @@ export const BalanceHistoryTable = ({ customFromFilter, customToFilter, filterVariables, + fromTimestamp, + toTimestamp, limit, }); @@ -113,37 +120,9 @@ export const BalanceHistoryTable = ({ // Transform transfers to table data format const transformedData = useMemo(() => { return transfers.map((transfer) => { - const transferDate = new Date(parseInt(transfer.timestamp) * 1000); - const now = new Date(); - const diffInMs = now.getTime() - transferDate.getTime(); - const diffInSeconds = Math.floor(diffInMs / 1000); - const diffInMinutes = Math.floor(diffInSeconds / 60); - const diffInHours = Math.floor(diffInMinutes / 60); - const diffInDays = Math.floor(diffInHours / 24); - const diffInWeeks = Math.floor(diffInDays / 7); - const diffInMonths = Math.floor(diffInDays / 30); - const diffInYears = Math.floor(diffInDays / 365); - - let relativeTime; - if (diffInYears > 0) { - relativeTime = `${diffInYears} year${diffInYears > 1 ? "s" : ""} ago`; - } else if (diffInMonths > 0) { - relativeTime = `${diffInMonths} month${diffInMonths > 1 ? "s" : ""} ago`; - } else if (diffInWeeks > 0) { - relativeTime = `${diffInWeeks} week${diffInWeeks > 1 ? "s" : ""} ago`; - } else if (diffInDays > 0) { - relativeTime = `${diffInDays} day${diffInDays > 1 ? "s" : ""} ago`; - } else if (diffInHours > 0) { - relativeTime = `${diffInHours} hour${diffInHours > 1 ? "s" : ""} ago`; - } else if (diffInMinutes > 0) { - relativeTime = `${diffInMinutes} minute${diffInMinutes > 1 ? "s" : ""} ago`; - } else { - relativeTime = "Just now"; - } - return { id: transfer.transactionHash, - date: relativeTime, + timestamp: transfer.timestamp, amount: formatNumberUserReadable(transfer.amount), type: transfer.direction === "in" ? "Buy" : ("Sell" as "Buy" | "Sell"), fromAddress: transfer.fromAccountId, @@ -154,12 +133,12 @@ export const BalanceHistoryTable = ({ const balanceHistoryColumns: ColumnDef[] = [ { - accessorKey: "date", + accessorKey: "timestamp", meta: { columnClassName: "w-32", }, cell: ({ row }) => { - const date = row.getValue("date") as string; + const timestamp = row.getValue("timestamp") as string; if (isInitialLoading) { return ( @@ -174,7 +153,7 @@ export const BalanceHistoryTable = ({ return (
- {date} +
); }, diff --git a/apps/dashboard/features/holders-and-delegates/token-holder/drawer/balance-history/BalanceHistoryVariationGraph.tsx b/apps/dashboard/features/holders-and-delegates/token-holder/drawer/balance-history/BalanceHistoryVariationGraph.tsx index 3e2368890..fb00df872 100644 --- a/apps/dashboard/features/holders-and-delegates/token-holder/drawer/balance-history/BalanceHistoryVariationGraph.tsx +++ b/apps/dashboard/features/holders-and-delegates/token-holder/drawer/balance-history/BalanceHistoryVariationGraph.tsx @@ -17,13 +17,16 @@ import { BalanceHistoryGraphItem, useBalanceHistoryGraph, } from "@/features/holders-and-delegates/hooks/useBalanceHistoryGraph"; -import { TimePeriodSwitcher } from "@/features/holders-and-delegates/components/TimePeriodSwitcher"; +import { + TimePeriod, + TimePeriodSwitcher, +} from "@/features/holders-and-delegates/components/TimePeriodSwitcher"; import { ChartExceptionState } from "@/shared/components"; import { EnsAvatar } from "@/shared/components/design-system/avatars/ens-avatar/EnsAvatar"; import { AnticaptureWatermark } from "@/shared/components/icons/AnticaptureWatermark"; import { parseAsStringEnum, useQueryState } from "nuqs"; import { useMemo } from "react"; -import { SECONDS_PER_DAY } from "@/shared/constants/time-related"; +import { getTimestampRangeFromPeriod } from "@/features/holders-and-delegates/utils"; interface BalanceHistoryVariationGraphProps { accountId: string; @@ -85,30 +88,13 @@ export const BalanceHistoryVariationGraph = ({ }: BalanceHistoryVariationGraphProps) => { const [selectedPeriod, setSelectedPeriod] = useQueryState( "selectedPeriod", - parseAsStringEnum(["30d", "90d", "all"]).withDefault("all"), + parseAsStringEnum(["30d", "90d", "all"]).withDefault("all"), ); - const fromDate = useMemo(() => { - // For "all", treat as all time by not setting limits - if (selectedPeriod === "all") return undefined; - - // Use start of today for a stable reference that won't change on each render - const today = new Date(); - today.setHours(0, 0, 0, 0); - const todayInSeconds = today.getTime() / 1000; - - let daysInSeconds: number; - switch (selectedPeriod) { - case "90d": - daysInSeconds = 90 * SECONDS_PER_DAY; - break; - default: - daysInSeconds = 30 * SECONDS_PER_DAY; - break; - } - - return Math.floor(todayInSeconds - daysInSeconds); - }, [selectedPeriod]); + const { fromTimestamp: fromDate } = useMemo( + () => getTimestampRangeFromPeriod(selectedPeriod), + [selectedPeriod], + ); const { balanceHistory, loading, error } = useBalanceHistoryGraph( accountId, diff --git a/apps/dashboard/features/holders-and-delegates/token-holder/drawer/delegation-history/DelegationHistoryTable.tsx b/apps/dashboard/features/holders-and-delegates/token-holder/drawer/delegation-history/DelegationHistoryTable.tsx index ecab8dd5f..64e31728b 100644 --- a/apps/dashboard/features/holders-and-delegates/token-holder/drawer/delegation-history/DelegationHistoryTable.tsx +++ b/apps/dashboard/features/holders-and-delegates/token-holder/drawer/delegation-history/DelegationHistoryTable.tsx @@ -7,10 +7,7 @@ import { useEffect, useState } from "react"; import { Address, parseUnits } from "viem"; import { useDelegationHistory } from "@/features/holders-and-delegates/hooks/useDelegationHistory"; import { formatUnits } from "viem"; -import { - formatNumberUserReadable, - formatDateUserReadable, -} from "@/shared/utils/"; +import { formatNumberUserReadable } from "@/shared/utils/"; import { ExternalLink } from "lucide-react"; import { ArrowState, ArrowUpDown } from "@/shared/components/icons/ArrowUpDown"; import daoConfigByDaoId from "@/shared/dao-config"; @@ -33,12 +30,12 @@ import { } from "nuqs"; import { DEFAULT_ITEMS_PER_PAGE } from "@/features/holders-and-delegates/utils"; import { useAmountFilterStore } from "@/shared/components/design-system/table/filters/amount-filter/store/amount-filter-store"; +import { DateCell } from "@/shared/components/design-system/table/cells/DateCell"; interface DelegationData { address: string; amount: string; - date: string; - timestamp: number; + timestamp: string; } interface DelegationHistoryTableProps { @@ -107,22 +104,17 @@ export const DelegationHistoryTable = ({ .map((delegation) => { const delegateAddress = delegation.delegateAddress || ""; const delegatedValue = delegation.amount || "0"; - const timestamp = delegation.timestamp || 0; + const timestamp = delegation.timestamp || "0"; const formattedAmount = Number( formatUnits(BigInt(delegatedValue), decimals), ).toFixed(2); - const date = timestamp - ? formatDateUserReadable(new Date(Number(timestamp) * 1000)) - : "Unknown"; - return { address: delegateAddress, amount: formattedAmount, transactionHash: delegation.transactionHash, - date, - timestamp: Number(timestamp), + timestamp: String(timestamp), }; }) || []; @@ -264,7 +256,7 @@ export const DelegationHistoryTable = ({ }, }, { - accessorKey: "date", + accessorKey: "timestamp", header: ({ column }) => { const handleSortToggle = () => { const newSortOrder = sortOrder === "desc" ? "asc" : "desc"; @@ -308,11 +300,11 @@ export const DelegationHistoryTable = ({ ); } - const date: string = row.getValue("date"); + const timestamp: string = row.getValue("timestamp"); return (
- {date} +
); }, diff --git a/apps/dashboard/features/holders-and-delegates/token-holder/drawer/top-interactions/TopInteractions.tsx b/apps/dashboard/features/holders-and-delegates/token-holder/drawer/top-interactions/TopInteractions.tsx index d548e4b6e..5f8d634bd 100644 --- a/apps/dashboard/features/holders-and-delegates/token-holder/drawer/top-interactions/TopInteractions.tsx +++ b/apps/dashboard/features/holders-and-delegates/token-holder/drawer/top-interactions/TopInteractions.tsx @@ -19,7 +19,7 @@ const ChartLegend = ({ if (loading) { return (
- {Array.from({ length: 10 }, (_, i) => ( + {Array.from({ length: 6 }, (_, i) => (
))} @@ -35,6 +35,10 @@ const ChartLegend = ({ ); } + if (items.length === 0) { + return
No interactions found
; + } + return (
{items.map((item) => { @@ -72,108 +76,104 @@ export const TopInteractions = ({ const { topFive, totalCount, - netBalanceChange, legendItems, pieData, chartConfig, + netBalanceChange, loading: loadingVotingPowerData, } = useAccountInteractionsData({ daoId, address }); - if (!topFive || (topFive.length === 0 && !loadingVotingPowerData)) { - return ( -
- -
- ); - } - const variant = netBalanceChange >= 0 ? "positive" : "negative"; return (
-
-
-
-
- -
+ {!topFive || (topFive.length === 0 && !loadingVotingPowerData) ? ( + + ) : ( +
+
+
+
+ {loadingVotingPowerData ? ( + + ) : ( + + )} +
-
-
-

- Net Tokens In/Out (90D) -

-
- {!netBalanceChange ? ( - - ) : ( - // this is inverted because is relative to the drawer address - // thus a positive value on the row means the drawer address is sending tokens -

- {netBalanceChange < 0 ? ( - - ) : ( - - )} - {formatNumberUserReadable(Math.abs(netBalanceChange))} -

- )} +
+
+

+ Net Tokens In/Out +

+
+ {!netBalanceChange || loadingVotingPowerData ? ( + + ) : ( + // this is inverted because is relative to the drawer address + // thus a positive value on the row means the drawer address is sending tokens +

+ {netBalanceChange < 0 ? ( + + ) : ( + + )} + {formatNumberUserReadable(Math.abs(netBalanceChange))} +

+ )} +
-
-
+
-
-

- Top Interaction (by aggregated value) -

+
+

+ Top Interaction (by aggregated value) +

-
- {!legendItems || !topFive ? ( - - ) : !topFive ? ( -
- Loading Interactions... -
- ) : topFive && topFive.length > 0 ? ( - - ) : ( -
- No interactions found -
- )} +
+ +
-
+ )}
diff --git a/apps/dashboard/features/holders-and-delegates/token-holder/drawer/top-interactions/TopInteractionsChart.tsx b/apps/dashboard/features/holders-and-delegates/token-holder/drawer/top-interactions/TopInteractionsChart.tsx index 32583f313..15f320922 100644 --- a/apps/dashboard/features/holders-and-delegates/token-holder/drawer/top-interactions/TopInteractionsChart.tsx +++ b/apps/dashboard/features/holders-and-delegates/token-holder/drawer/top-interactions/TopInteractionsChart.tsx @@ -9,7 +9,7 @@ import { TooltipProps, } from "recharts"; import { formatNumberUserReadable } from "@/shared/utils"; -import { renderCustomizedLabel } from "@/features/holders-and-delegates/delegate/drawer/voting-power/utils/renderCustomizedLabel"; +import { renderCustomizedLabel } from "@/features/holders-and-delegates/delegate/drawer/vote-composition/utils/renderCustomizedLabel"; import { SkeletonRow } from "@/shared/components/skeletons/SkeletonRow"; import { AnticaptureWatermark } from "@/shared/components/icons/AnticaptureWatermark"; diff --git a/apps/dashboard/features/holders-and-delegates/token-holder/drawer/top-interactions/TopInteractionsTable.tsx b/apps/dashboard/features/holders-and-delegates/token-holder/drawer/top-interactions/TopInteractionsTable.tsx index 51e2b9a0b..08c95ac60 100644 --- a/apps/dashboard/features/holders-and-delegates/token-holder/drawer/top-interactions/TopInteractionsTable.tsx +++ b/apps/dashboard/features/holders-and-delegates/token-holder/drawer/top-interactions/TopInteractionsTable.tsx @@ -45,9 +45,7 @@ export const TopInteractionsTable = ({ const [sortBy, setSortBy] = useQueryState( "orderBy", - parseAsStringEnum(["transferCount", "totalVolume"]).withDefault( - "transferCount", - ), + parseAsStringEnum(["count", "volume"]).withDefault("count"), ); const [sortDirection, setSortDirection] = useQueryState( "orderDirection", @@ -178,9 +176,9 @@ export const TopInteractionsTable = ({ setSortDirection( filterState.sortOrder === "largest-first" ? "desc" : "asc", ); - setSortBy("totalVolume"); + setSortBy("volume"); } else { - setSortBy("transferCount"); + setSortBy("count"); setSortDirection("desc"); } @@ -203,7 +201,7 @@ export const TopInteractionsTable = ({ }} onReset={() => { setIsFilterActive(false); - setSortBy("transferCount"); + setSortBy("count"); setFilterVariables(() => ({ minAmount: null, maxAmount: null, diff --git a/apps/dashboard/features/holders-and-delegates/token-holder/drawer/top-interactions/hooks/useAccountInteractionsData.ts b/apps/dashboard/features/holders-and-delegates/token-holder/drawer/top-interactions/hooks/useAccountInteractionsData.ts index 72174c4fd..2b340c74a 100644 --- a/apps/dashboard/features/holders-and-delegates/token-holder/drawer/top-interactions/hooks/useAccountInteractionsData.ts +++ b/apps/dashboard/features/holders-and-delegates/token-holder/drawer/top-interactions/hooks/useAccountInteractionsData.ts @@ -9,6 +9,7 @@ import { useGetAccountInteractionsQuery } from "@anticapture/graphql-client/hook import daoConfig from "@/shared/dao-config"; import { Query_AccountInteractions_Items_Items, + QueryInput_AccountInteractions_OrderBy, QueryInput_AccountInteractions_OrderDirection, } from "@anticapture/graphql-client"; import { DAYS_IN_SECONDS } from "@/shared/constants/time-related"; @@ -53,6 +54,7 @@ export const useAccountInteractionsData = ({ daoId, address, filterAddress, + sortBy, sortDirection, filterVariables, limit = 100, @@ -60,7 +62,7 @@ export const useAccountInteractionsData = ({ daoId: DaoIdEnum; address: string; filterAddress?: string; - sortBy?: "transferCount" | "totalVolume"; + sortBy?: "count" | "volume"; sortDirection?: "asc" | "desc"; filterVariables?: { minAmount: string | null; @@ -79,7 +81,7 @@ export const useAccountInteractionsData = ({ const { data, loading, error } = useGetAccountInteractionsQuery({ variables: { address, - fromDate, + orderBy: sortBy as QueryInput_AccountInteractions_OrderBy, orderDirection: sortDirection as QueryInput_AccountInteractions_OrderDirection, minAmount: filterVariables?.minAmount, diff --git a/apps/dashboard/features/holders-and-delegates/utils/index.ts b/apps/dashboard/features/holders-and-delegates/utils/index.ts index b04bfcf75..2ccdb628c 100644 --- a/apps/dashboard/features/holders-and-delegates/utils/index.ts +++ b/apps/dashboard/features/holders-and-delegates/utils/index.ts @@ -1 +1,3 @@ export * from "./constants"; +export { getAvgVoteTimingData } from "./proposalsTableUtils"; +export { getTimestampRangeFromPeriod } from "./timestampUtils"; diff --git a/apps/dashboard/features/holders-and-delegates/utils/proposalsTableUtils.tsx b/apps/dashboard/features/holders-and-delegates/utils/proposalsTableUtils.tsx index 101d4adca..b5cc4a9e4 100644 --- a/apps/dashboard/features/holders-and-delegates/utils/proposalsTableUtils.tsx +++ b/apps/dashboard/features/holders-and-delegates/utils/proposalsTableUtils.tsx @@ -133,6 +133,27 @@ export const isProposalFinished = (finalResultStatus: string): boolean => { return status !== "ongoing" && status !== "pending"; }; +const formatVoteTiming = ( + timeBeforeEnd: number, + votingPeriod: number, + suffix: "left" | "avg", +): { text: string; percentage: number } => { + const timeElapsed = votingPeriod - timeBeforeEnd; + const percentage = Math.max( + 0, + Math.min(100, (timeElapsed / votingPeriod) * 100), + ); + + const daysLeft = Math.floor(timeBeforeEnd / (24 * 60 * 60)); + + if (daysLeft >= 4) { + return { text: `Early (${daysLeft}d ${suffix})`, percentage }; + } else if (daysLeft < 1) { + return { text: `Late (<1d ${suffix})`, percentage }; + } + return { text: `Late (${daysLeft}d ${suffix})`, percentage }; +}; + // Helper function to format vote timing and calculate percentage export const getVoteTimingData = ( userVote: Query_ProposalsActivity_Proposals_Items_UserVote | null | undefined, @@ -159,22 +180,18 @@ export const getVoteTimingData = ( return { text: "Expired", percentage: 100 }; } - // Calculate how much time has passed as a percentage - const timeElapsed = voteTime - startTime; - const percentage = Math.max( - 0, - Math.min(100, (timeElapsed / daoVotingPeriod) * 100), - ); - - const timeDiff = endTime - voteTime; - const daysLeft = Math.floor(timeDiff / (24 * 60 * 60)); + const timeBeforeEnd = endTime - voteTime; + return formatVoteTiming(timeBeforeEnd, daoVotingPeriod, "left"); +}; - if (daysLeft >= 4) { - return { text: `Early (${daysLeft}d left)`, percentage }; - } else { - if (daysLeft == 0) { - return { text: `Late (<1d left)`, percentage }; - } - return { text: `Late (${daysLeft}d left)`, percentage }; +export const getAvgVoteTimingData = ( + avgTimeBeforeEnd: number | undefined | null, + votingPeriodSeconds: number, + votedProposals: number = 0, +): { text: string; percentage: number } => { + if (!avgTimeBeforeEnd || votedProposals === 0) { + return { text: "-", percentage: 0 }; } + + return formatVoteTiming(avgTimeBeforeEnd, votingPeriodSeconds, "avg"); }; diff --git a/apps/dashboard/features/holders-and-delegates/utils/timestampUtils.ts b/apps/dashboard/features/holders-and-delegates/utils/timestampUtils.ts new file mode 100644 index 000000000..defc25718 --- /dev/null +++ b/apps/dashboard/features/holders-and-delegates/utils/timestampUtils.ts @@ -0,0 +1,24 @@ +import { TimePeriod } from "@/features/holders-and-delegates/components/TimePeriodSwitcher"; +import { SECONDS_PER_DAY } from "@/shared/constants/time-related"; + +interface TimestampRange { + fromTimestamp: number | undefined; + toTimestamp: number | undefined; +} + +export function getTimestampRangeFromPeriod( + selectedPeriod: TimePeriod, +): TimestampRange { + if (selectedPeriod === "all") { + return { fromTimestamp: undefined, toTimestamp: undefined }; + } + + const nowInSeconds = Date.now() / 1000; + const daysInSeconds = + selectedPeriod === "90d" ? 90 * SECONDS_PER_DAY : 30 * SECONDS_PER_DAY; + + return { + fromTimestamp: Math.floor(nowInSeconds - daysInSeconds), + toTimestamp: Math.floor(nowInSeconds), + }; +} diff --git a/apps/dashboard/features/transactions/TransactionsTable.tsx b/apps/dashboard/features/transactions/TransactionsTable.tsx index 5194d55d8..7412aa05e 100644 --- a/apps/dashboard/features/transactions/TransactionsTable.tsx +++ b/apps/dashboard/features/transactions/TransactionsTable.tsx @@ -90,7 +90,7 @@ export const TransactionsTable = ({ id: "loading-row", affectedSupply: ["CEX", "DEX"] as SupplyType[], amount: "1000000", - date: "2 hours ago", + timestamp: "0", from: "0x1234567890abcdef", to: "0xabcdef1234567890", txHash: diff --git a/apps/dashboard/features/transactions/hooks/useTransactionsTableData.ts b/apps/dashboard/features/transactions/hooks/useTransactionsTableData.ts index 960ac987c..f0a144715 100644 --- a/apps/dashboard/features/transactions/hooks/useTransactionsTableData.ts +++ b/apps/dashboard/features/transactions/hooks/useTransactionsTableData.ts @@ -33,7 +33,7 @@ export interface TransactionsFilters extends TransactionsParamsType { export interface TransactionData { id: string; amount: string; - date: string; + timestamp: string; from: string; to: string; affectedSupply?: SupplyType[]; diff --git a/apps/dashboard/features/transactions/utils/getTransactionsColumns.tsx b/apps/dashboard/features/transactions/utils/getTransactionsColumns.tsx index a16a433e7..e7281ea38 100644 --- a/apps/dashboard/features/transactions/utils/getTransactionsColumns.tsx +++ b/apps/dashboard/features/transactions/utils/getTransactionsColumns.tsx @@ -17,6 +17,7 @@ import { Address, zeroAddress } from "viem"; import { EnsAvatar } from "@/shared/components/design-system/avatars/ens-avatar/EnsAvatar"; import { cn } from "@/shared/utils"; import { TransactionsParamsType } from "@/features/transactions/hooks/useTransactionParams"; +import { DateCell } from "@/shared/components/design-system/table/cells/DateCell"; export const getTransactionsColumns = ({ loading, @@ -136,7 +137,7 @@ export const getTransactionsColumns = ({ size: 162, }, { - accessorKey: "date", + accessorKey: "timestamp", header: () => (