Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
1ad9f99
feat: add new changes to holder and delegates sections
brunod-e Jan 29, 2026
3e654a6
feat: refactor holders and delegates components for improved timestam…
brunod-e Jan 29, 2026
526229e
feat: enhance ENS avatar loading behavior and display logic
brunod-e Jan 29, 2026
4d5b60d
feat: improve loading and no data handling in TopInteractions component
brunod-e Jan 30, 2026
873e099
feat: migrate formatRelativeTime utility to shared utils and update i…
brunod-e Feb 2, 2026
96edd28
feat: implement vote composition table and voting power history features
brunod-e Feb 2, 2026
2c5da3b
Merge branch 'dev' into fix/holders-and-delegates
brunod-e Feb 3, 2026
3b24f8f
feat: enhance formatRelativeTime utility to support skipping
brunod-e Feb 3, 2026
e39eae3
Merge branch 'fix/holders-and-delegates' of https://github.com/blockf…
brunod-e Feb 3, 2026
95f2ade
Merge branch 'dev' into fix/holders-and-delegates
brunod-e Feb 3, 2026
25dc4fc
refactor: remove isNewDelegate property from DelegateTableData and re…
brunod-e Feb 3, 2026
6dc5c3c
Merge branch 'fix/holders-and-delegates' of https://github.com/blockf…
brunod-e Feb 3, 2026
63443f5
feat: add offset parameter to various query arguments and remove outd…
brunod-e Feb 3, 2026
e74c30a
Merge branch 'dev' into fix/holders-and-delegates
brunod-e Feb 3, 2026
4db47da
Merge branch 'dev' into fix/holders-and-delegates
brunod-e Feb 3, 2026
af2ce05
feat: enhance VoteComposition and VoteCompositionTable
brunod-e Feb 4, 2026
0faaa22
feat: implement DateCell component and update date handling in tables
brunod-e Feb 4, 2026
5c06ecc
Merge branch 'dev' into fix/holders-and-delegates
brunod-e Feb 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -50,53 +60,41 @@ export const HoldersAndDelegatesSection = ({ daoId }: { daoId: DaoIdEnum }) => {
delegates: <Delegates daoId={daoId} timePeriod={days || defaultDays} />,
};

const HoldersAndDelegatesLeftComponent = () => {
const tabs: Array<{ id: TabId; label: string }> = [
{
id: "tokenHolders",
label: "TOKEN HOLDERS",
},
{
id: "delegates",
label: "DELEGATES",
},
];

return (
<div className="flex w-full items-center justify-between">
<div className="flex gap-2">
{tabs.map((tab) => (
<TabButton
key={tab.id}
id={tab.id}
label={tab.label}
activeTab={activeTab as TabId}
setActiveTab={handleTabChange}
/>
))}
</div>
const TabsHeader = () => (
<div className="flex h-full w-full items-center justify-between">
<div className="flex gap-2">
{TABS.map((tab) => (
<TabButton
key={tab.id}
id={tab.id}
label={tab.label}
activeTab={activeTab as TabId}
setActiveTab={handleTabChange}
/>
))}
</div>
);
};
</div>
);

return (
<TheSectionLayout
title={PAGES_CONSTANTS.holdersAndDelegates.title}
subtitle={"Holders & Delegates"}
icon={<UserCheck className="section-layout-icon" />}
description={PAGES_CONSTANTS.holdersAndDelegates.description}
className="lg:pb-0"
>
<SubSectionsContainer>
<div className="flex w-full items-center justify-between">
<HoldersAndDelegatesLeftComponent />
<SwitcherDateMobile
defaultValue={days || defaultDays}
setTimeInterval={setDays}
/>
</div>
{tabComponentMap[activeTab as TabId]}
</SubSectionsContainer>
</TheSectionLayout>
<div>
<TheSectionLayout
title={PAGES_CONSTANTS.holdersAndDelegates.title}
subtitle={"Holders & Delegates"}
icon={<UserCheck className="section-layout-icon" />}
description={PAGES_CONSTANTS.holdersAndDelegates.description}
>
<SubSectionsContainer>
<div className="flex w-full items-center justify-between">
<TabsHeader />
<SwitcherDateMobile
defaultValue={days || defaultDays}
setTimeInterval={setDays}
/>
</div>
{tabComponentMap[activeTab as TabId]}
</SubSectionsContainer>
</TheSectionLayout>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -46,16 +46,14 @@ export const HoldersAndDelegatesDrawer = ({
),
},
{
id: "votingPower",
label: "Voting Power",
content: <VotingPower address={address} daoId={daoId} />,
id: "voteComposition",
label: "Vote Composition",
content: <VoteComposition address={address} daoId={daoId} />,
},
{
id: "delegationHistory",
label: "Delegation History",
content: (
<DelegateDelegationsHistory accountId={address} daoId={daoId} />
),
id: "votingPowerHistory",
label: "Voting Power History",
content: <VotingPowerHistory accountId={address} daoId={daoId} />,
},
],
},
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
113 changes: 79 additions & 34 deletions apps/dashboard/features/holders-and-delegates/delegate/Delegates.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -27,45 +28,30 @@ 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 {
timePeriod?: TimeInterval; // Use TimeInterval enum directly
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 = ({
Expand All @@ -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,
Expand Down Expand Up @@ -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),
Expand All @@ -166,9 +162,10 @@ export const Delegates = ({
activity,
activityPercentage,
delegators: delegate.delegationsCount,
avgVoteTiming,
};
});
}, [data, decimals]);
}, [data, decimals, votingPeriodSeconds]);

const delegateColumns: ColumnDef<DelegateTableData>[] = [
{
Expand Down Expand Up @@ -281,7 +278,7 @@ export const Delegates = ({
</Button>
),
meta: {
columnClassName: "w-72",
columnClassName: "w-40",
},
},
{
Expand Down Expand Up @@ -351,6 +348,54 @@ export const Delegates = ({
</h4>
),
},
{
accessorKey: "avgVoteTiming",
cell: ({ row }) => {
const avgVoteTiming = row.getValue("avgVoteTiming") as {
text: string;
percentage: number;
} | null;

if (loading) {
return (
<div className="flex items-center justify-start">
<SkeletonRow className="h-5 w-20" />
</div>
);
}

if (!avgVoteTiming) {
return <div className="text-secondary text-sm">-</div>;
}

return (
<div className="flex flex-col justify-center gap-1">
<div
className={cn("text-secondary text-xs font-normal", {
"text-end text-sm": avgVoteTiming.text === "-",
})}
>
{avgVoteTiming.text}
</div>
{avgVoteTiming.text !== "-" && (
<SimpleProgressBar percentage={avgVoteTiming.percentage} />
)}
</div>
);
},
header: () => (
<div className="flex items-center gap-1.5">
<Tooltip tooltipContent="Measures the average of how close to the proposal deadline a vote is cast. Delegates who vote late may be influenced by prior votes or ongoing discussion.">
<h4 className="text-table-header decoration-secondary/20 group-hover:decoration-primary hover:decoration-primary whitespace-nowrap underline decoration-dashed underline-offset-[6px] transition-colors duration-300">
Avg Vote Timing
</h4>
</Tooltip>
</div>
),
meta: {
columnClassName: "w-40",
},
},
{
accessorKey: "delegators",
cell: ({ row }) => {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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";
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
Loading