diff --git a/.gitignore b/.gitignore
index 23e6d2504..1eee3377f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -51,4 +51,5 @@ next-env.d.ts
dist
+# version manager
.tool-versions
diff --git a/src/app/api/common/citizens/getCitizens.ts b/src/app/api/common/citizens/getCitizens.ts
index e3646f136..e821120ed 100644
--- a/src/app/api/common/citizens/getCitizens.ts
+++ b/src/app/api/common/citizens/getCitizens.ts
@@ -15,7 +15,7 @@ async function getCitizens({
sort = "shuffle",
seed,
}: {
- pagination: PaginationParams;
+ pagination?: PaginationParams;
sort: string;
seed?: number;
}): Promise> {
@@ -26,7 +26,7 @@ async function getCitizens({
if (sort === "shuffle") {
return prisma.$queryRawUnsafe(
`
- SELECT
+ SELECT
citizens.address AS delegate,
delegate.voting_power,
advanced_vp,
@@ -54,7 +54,7 @@ async function getCitizens({
WHERE citizens.retro_funding_round = (SELECT MAX(retro_funding_round) FROM agora.citizens)
ORDER BY md5(CAST(citizens.address AS TEXT) || CAST($2 AS TEXT))
OFFSET $3
- LIMIT $4;
+ LIMIT $4;
`,
slug,
seed,
@@ -64,7 +64,7 @@ async function getCitizens({
} else {
return prisma.$queryRawUnsafe(
`
- SELECT
+ SELECT
citizens.address AS delegate,
delegate.voting_power,
direct_vp,
@@ -92,7 +92,7 @@ async function getCitizens({
AND citizens.dao_slug = $1::config.dao_slug
WHERE citizens.retro_funding_round = (SELECT MAX(retro_funding_round) FROM agora.citizens)
ORDER BY COALESCE(delegate.voting_power, 0) DESC,
- citizens.address ASC
+ citizens.address ASC
OFFSET $2
LIMIT $3;
`,
@@ -114,6 +114,7 @@ async function getCitizens({
direct: citizen.direct_vp?.toFixed(0) || "0",
advanced: citizen.advanced_vp?.toFixed(0) || "0",
},
+ votingParticipation: 0,
citizen: citizen.citizen,
statement: citizen.statement,
})),
diff --git a/src/app/api/common/delegates/getDelegates.ts b/src/app/api/common/delegates/getDelegates.ts
index c1aa75b7d..0343685ae 100644
--- a/src/app/api/common/delegates/getDelegates.ts
+++ b/src/app/api/common/delegates/getDelegates.ts
@@ -18,6 +18,7 @@ import { fetchCurrentQuorum } from "@/app/api/common/quorum/getQuorum";
import { fetchVotableSupply } from "@/app/api/common/votableSupply/getVotableSupply";
import { doInSpan } from "@/app/lib/logging";
import { TENANT_NAMESPACES } from "@/lib/constants";
+import { getProxyAddress } from "@/lib/alligatorUtils";
/*
* Fetches a list of delegates
@@ -35,10 +36,11 @@ async function getDelegates({
seed,
filters,
}: {
- pagination: PaginationParams;
+ pagination?: PaginationParams;
sort: string;
seed?: number;
filters?: {
+ delegator?: `0x${string}`;
issues?: string;
stakeholders?: string;
endorsed?: boolean;
@@ -102,22 +104,57 @@ async function getDelegates({
: "";
let delegateUniverseCTE: string;
-
const tokenAddress = contracts.token.address;
+ const proxyAddress = filters?.delegator
+ ? await getProxyAddress(filters?.delegator?.toLowerCase())
+ : null;
- delegateUniverseCTE = `with del_statements as (select address from agora.delegate_statements where dao_slug='${slug}'),
- del_with_del as (select * from ${namespace + ".delegates"} d where contract = '${tokenAddress}'),
- del_card_universe as (select COALESCE(d.delegate, ds.address) as delegate,
- coalesce(d.num_of_delegators, 0) as num_of_delegators,
- coalesce(d.direct_vp, 0) as direct_vp,
- coalesce(d.advanced_vp, 0) as advanced_vp,
- coalesce(d.voting_power, 0) as voting_power
- from del_with_del d full join del_statements ds on d.delegate = ds.address)`;
+ delegateUniverseCTE = `
+ with del_statements as (
+ select address
+ from agora.delegate_statements
+ where dao_slug='${slug}'
+ ),
+ filtered_delegates as (
+ select d.*
+ from ${namespace}.delegates d
+ where d.contract = '${tokenAddress}'
+ ${
+ filters?.delegator
+ ? `
+ AND d.delegate IN (
+ SELECT delegatee
+ FROM (
+ SELECT delegatee, block_number
+ FROM ${namespace}.delegatees
+ WHERE delegator = '${filters.delegator.toLowerCase()}'
+ ${proxyAddress ? `AND delegatee <> '${proxyAddress.toLowerCase()}'` : ""}
+ AND contract = '${tokenAddress}'
+ UNION ALL
+ SELECT "to" as delegatee, block_number
+ FROM ${namespace}.advanced_delegatees
+ WHERE "from" = '${filters.delegator.toLowerCase()}'
+ AND delegated_amount > 0
+ AND contract = '${contracts.alligator?.address || tokenAddress}'
+ ) combined_delegations
+ ORDER BY block_number DESC
+ )
+ `
+ : ""
+ }
+ ),
+ del_card_universe as (
+ select
+ d.delegate as delegate,
+ d.num_of_delegators as num_of_delegators,
+ d.direct_vp as direct_vp,
+ d.advanced_vp as advanced_vp,
+ d.voting_power as voting_power
+ from filtered_delegates d
+ )`;
// Applies allow-list filtering to the delegate list
const paginatedAllowlistQuery = async (skip: number, take: number) => {
- console.log(sort);
-
const allowListString = allowList.map((value) => `'${value}'`).join(", ");
switch (sort) {
@@ -154,13 +191,12 @@ async function getDelegates({
) AS statement
FROM del_card_universe d
WHERE num_of_delegators IS NOT NULL
- AND (ARRAY_LENGTH(ARRAY[${allowListString}]::text[], 1) IS NULL OR delegate = ANY(ARRAY[${allowListString}]::text[]))
+ AND (ARRAY_LENGTH(ARRAY[${allowListString}]::text[], 1) IS NULL OR d.delegate = ANY(ARRAY[${allowListString}]::text[]))
${delegateStatementFiler}
ORDER BY num_of_delegators DESC
OFFSET $1
LIMIT $2;
- `;
- // console.log(QRY1);
+ `;
return prisma.$queryRawUnsafe(QRY1, skip, take);
case "weighted_random":
@@ -202,7 +238,6 @@ async function getDelegates({
OFFSET $1
LIMIT $2;
`;
- // console.log(QRY2);
return prisma.$queryRawUnsafe(QRY2, skip, take);
default:
@@ -243,7 +278,6 @@ async function getDelegates({
OFFSET $1
LIMIT $2;
`;
- // console.log(QRY3);
return prisma.$queryRawUnsafe(QRY3, skip, take);
}
};
@@ -436,5 +470,30 @@ async function getDelegate(addressOrENSName: string): Promise {
};
}
+async function getVoterStats(addressOrENSName: string): Promise {
+ const { namespace, contracts } = Tenant.current();
+ const address = isAddress(addressOrENSName)
+ ? addressOrENSName.toLowerCase()
+ : await resolveENSName(addressOrENSName);
+
+ const statsQuery = await prisma.$queryRawUnsafe<
+ Pick[]
+ >(
+ `
+ SELECT
+ voter,
+ participation_rate,
+ last_10_props
+ FROM ${namespace + ".voter_stats"}
+ WHERE voter = $1 AND contract = $2
+ `,
+ address,
+ contracts.governor.address
+ );
+
+ return statsQuery?.[0] || undefined;
+}
+
export const fetchDelegates = cache(getDelegates);
export const fetchDelegate = cache(getDelegate);
+export const fetchVoterStats = cache(getVoterStats);
diff --git a/src/app/api/common/delegations/getDelegations.ts b/src/app/api/common/delegations/getDelegations.ts
index eb845bd91..ec6b76cea 100644
--- a/src/app/api/common/delegations/getDelegations.ts
+++ b/src/app/api/common/delegations/getDelegations.ts
@@ -151,51 +151,51 @@ async function getCurrentDelegatorsForAddress({
// Replace with the Agora Governor flag
if (contracts.alligator || namespace === TENANT_NAMESPACES.SCROLL) {
- advancedDelegatorsSubQry = `SELECT
+ advancedDelegatorsSubQry = `SELECT
"from",
"to",
delegated_amount as allowance,
- 'ADVANCED' AS type,
+ 'ADVANCED' AS type,
block_number,
CASE WHEN delegated_share >= 1 THEN 'FULL' ELSE 'PARTIAL' END as amount,
transaction_hash
- FROM
+ FROM
${namespace}.advanced_delegatees ad
- WHERE
+ WHERE
ad."to" = $1
- AND delegated_amount > 0
+ AND delegated_amount > 0
AND contract = $2`;
} else {
- advancedDelegatorsSubQry = `WITH ghost as (SELECT
+ advancedDelegatorsSubQry = `WITH ghost as (SELECT
null::text as "from",
null::text as "to",
null::numeric as allowance,
- 'ADVANCED' AS type,
+ 'ADVANCED' AS type,
null::numeric as block_number,
'FULL' as amount,
null::text as transaction_hash)
select * from ghost
- WHERE
+ WHERE
ghost."to" = $1
AND ghost."from" = $2`;
}
if (namespace == TENANT_NAMESPACES.SCROLL) {
- directDelegatorsSubQry = `WITH ghost as (SELECT
+ directDelegatorsSubQry = `WITH ghost as (SELECT
null::text as "from",
null::text as "to",
null::numeric as allowance,
- 'DIRECT' AS type,
+ 'DIRECT' AS type,
null::numeric as block_number,
'FULL' as amount,
null::text as transaction_hash)
select * from ghost
- WHERE
+ WHERE
ghost."to" = $3
AND ghost."from" = $3`;
} else {
directDelegatorsSubQry = `
- SELECT
+ SELECT
"from",
"to",
null::numeric as allowance,
@@ -251,7 +251,7 @@ async function getCurrentDelegatorsForAddress({
>(
`
WITH advanced_delegatees AS ( ${advancedDelegatorsSubQry} )
-
+
, direct_delegatees AS ( ${directDelegatorsSubQry} )
SELECT * FROM advanced_delegatees
UNION ALL
@@ -357,10 +357,13 @@ const getDirectDelegateeForAddress = async ({
}: {
address: string;
}) => {
- const { namespace } = Tenant.current();
+ const { namespace, contracts } = Tenant.current();
const delegatee = await prisma[`${namespace}Delegatees`].findFirst({
- where: { delegator: address.toLowerCase() },
+ where: {
+ delegator: address.toLowerCase(),
+ contract: contracts.token.address.toLowerCase(),
+ },
});
if (namespace === TENANT_NAMESPACES.OPTIMISM) {
diff --git a/src/app/api/common/proposals/proposal.d.ts b/src/app/api/common/proposals/proposal.d.ts
index 3622bb35e..c742ce32d 100644
--- a/src/app/api/common/proposals/proposal.d.ts
+++ b/src/app/api/common/proposals/proposal.d.ts
@@ -20,8 +20,8 @@ export type Proposal = {
queuedTime: Date | null;
markdowntitle: string;
description: string | null;
- quorum: BigNumberish | null;
- approvalThreshold: BigNumberish | null;
+ quorum: bigint | null;
+ approvalThreshold: bigint | null;
proposalData: ParsedProposalData[ProposalType]["kind"];
unformattedProposalData: `0x${string}` | null | any;
proposalResults: ParsedProposalResults[ProposalType]["kind"];
diff --git a/src/app/api/common/votes/getVotes.ts b/src/app/api/common/votes/getVotes.ts
index 446453ebc..fd27275f1 100644
--- a/src/app/api/common/votes/getVotes.ts
+++ b/src/app/api/common/votes/getVotes.ts
@@ -193,6 +193,64 @@ async function getSnapshotVotesForDelegateForAddress({
}
}
+async function getVotersWhoHaveNotVotedForProposal({
+ proposalId,
+ pagination = { offset: 0, limit: 20 },
+}: {
+ proposalId: string;
+ pagination?: PaginationParams;
+}) {
+ const { namespace, contracts, slug } = Tenant.current();
+
+ const queryFunction = (skip: number, take: number) => {
+ const notVotedQuery = `
+ SELECT
+ del.*,
+ ds.twitter,
+ ds.discord,
+ ds.warpcast
+ FROM ${namespace + ".delegates"} del
+ LEFT JOIN agora.delegate_statements ds
+ ON del.delegate = ds.address
+ AND ds.dao_slug = '${slug}'
+ WHERE del.delegate NOT IN (
+ SELECT voter FROM ${namespace + ".vote_cast_events"} WHERE proposal_id = $1
+ )
+ AND del.contract = $2
+ ORDER BY del.voting_power DESC
+ `;
+
+ return prisma.$queryRawUnsafe(
+ `${notVotedQuery}
+ OFFSET $3
+ LIMIT $4;`,
+ proposalId,
+ contracts.token.address.toLowerCase(),
+ skip,
+ take
+ );
+ };
+
+ const [{ meta, data: nonVoters }, latestBlock] = await Promise.all([
+ doInSpan({ name: "getVotersWhoHaveNotVotedForProposal" }, async () =>
+ paginateResult(queryFunction, pagination)
+ ),
+ contracts.token.provider.getBlock("latest"),
+ ]);
+
+ if (!nonVoters || nonVoters.length === 0) {
+ return {
+ meta,
+ data: [],
+ };
+ }
+
+ return {
+ meta,
+ data: nonVoters,
+ };
+}
+
async function getVotesForProposal({
proposalId,
pagination = { offset: 0, limit: 20 },
@@ -255,8 +313,8 @@ async function getVotesForProposal({
) q
ORDER BY ${sort} DESC
OFFSET $3
- LIMIT $4;
- `;
+ LIMIT $4;`;
+
return prisma.$queryRawUnsafe(
query,
proposalId,
@@ -372,3 +430,6 @@ export const fetchUserVotesForProposal = cache(getUserVotesForProposal);
export const fetchVotesForProposalAndDelegate = cache(
getVotesForProposalAndDelegate
);
+export const fetchVotersWhoHaveNotVotedForProposal = cache(
+ getVotersWhoHaveNotVotedForProposal
+);
diff --git a/src/app/delegates/[addressOrENSName]/page.tsx b/src/app/delegates/[addressOrENSName]/page.tsx
index 5a2174d68..f33e37456 100644
--- a/src/app/delegates/[addressOrENSName]/page.tsx
+++ b/src/app/delegates/[addressOrENSName]/page.tsx
@@ -1,21 +1,7 @@
-/*
- * Show page for a single delegate
- * Takes in the delegate address as a parameter
- */
import { Metadata, ResolvingMetadata } from "next";
import DelegateCard from "@/components/Delegates/DelegateCard/DelegateCard";
-import DelegateVotes from "@/components/Delegates/DelegateVotes/DelegateVotes";
-import DelegationsContainer from "@/components/Delegates/Delegations/DelegationsContainer";
import ResourceNotFound from "@/components/shared/ResourceNotFound/ResourceNotFound";
-import DelegateStatementContainer from "@/components/Delegates/DelegateStatement/DelegateStatementContainer";
-import TopIssues from "@/components/Delegates/DelegateStatement/TopIssues";
-import {
- fetchCurrentDelegatees,
- fetchCurrentDelegators,
- fetchDelegate,
- fetchVotesForDelegate,
-} from "@/app/delegates/actions";
-import { fetchSnapshotVotesForDelegate } from "@/app/api/common/votes/getVotes";
+import { fetchDelegate } from "@/app/delegates/actions";
import { formatNumber } from "@/lib/tokenUtils";
import {
processAddressOrEnsName,
@@ -23,10 +9,16 @@ import {
resolveENSProfileImage,
} from "@/app/lib/ENSUtils";
import Tenant from "@/lib/tenant/tenant";
-import TopStakeholders from "@/components/Delegates/DelegateStatement/TopStakeholders";
-import SnapshotVotes from "@/components/Delegates/DelegateVotes/SnapshotVotes";
-import VotesContainer from "@/components/Delegates/DelegateVotes/VotesContainer";
-import { PaginationParams } from "@/app/lib/pagination";
+import { Suspense } from "react";
+import DelegateStatementWrapper, {
+ DelegateStatementSkeleton,
+} from "@/components/Delegates/DelegateStatement/DelegateStatementWrapper";
+import DelegationsContainerWrapper, {
+ DelegationsContainerSkeleton,
+} from "@/components/Delegates/Delegations/DelegationsContainerWrapper";
+import VotesContainerWrapper, {
+ VotesContainerSkeleton,
+} from "@/components/Delegates/DelegateVotes/VotesContainerWrapper";
export async function generateMetadata(
{ params }: { params: { addressOrENSName: string } },
@@ -90,16 +82,7 @@ export default async function Page({
params: { addressOrENSName: string };
}) {
const address = (await resolveENSName(addressOrENSName)) || addressOrENSName;
- const [delegate, delegateVotes, delegates, delegators, snapshotVotes] =
- await Promise.all([
- fetchDelegate(address),
- fetchVotesForDelegate(address),
- fetchCurrentDelegatees(address),
- fetchCurrentDelegators(address),
- fetchSnapshotVotesForDelegate({ addressOrENSName: address }),
- ]);
-
- const statement = delegate.statement;
+ const delegate = await fetchDelegate(address);
if (!delegate) {
return (
@@ -112,75 +95,16 @@ export default async function Page({
-
-
-
- {statement && (
- <>
-
-
- >
- )}
-
-
{
- "use server";
-
- return fetchCurrentDelegators(addressOrENSName, pagination);
- }}
- />
-
- {delegateVotes && delegateVotes.data.length > 0 ? (
-
- {
- "use server";
- return fetchVotesForDelegate(
- addressOrENSName,
- pagination
- );
- }}
- />
-
- ) : (
-
- No past votes available.
-
- )}
- >
- }
- snapshotVotes={
- <>
- {snapshotVotes && snapshotVotes.data.length > 0 ? (
- {
- "use server";
- return await fetchSnapshotVotesForDelegate({
- addressOrENSName: addressOrENSName,
- pagination,
- });
- }}
- />
- ) : (
-
- No past votes available.
-
- )}
- >
- }
- />
+ }>
+
+
+ }>
+
+
+ }>
+
+
);
diff --git a/src/app/delegates/actions.ts b/src/app/delegates/actions.ts
index 36a841e05..e818a236a 100644
--- a/src/app/delegates/actions.ts
+++ b/src/app/delegates/actions.ts
@@ -9,7 +9,10 @@ import {
fetchVotingPowerAvailableForDirectDelegation,
fetchVotingPowerAvailableForSubdelegation,
} from "@/app/api/common/voting-power/getVotingPower";
-import { fetchDelegate as apiFetchDelegate } from "@/app/api/common/delegates/getDelegates";
+import {
+ fetchDelegate as apiFetchDelegate,
+ fetchVoterStats as apiFetchVoterStats,
+} from "@/app/api/common/delegates/getDelegates";
import { fetchDelegateStatement as apiFetchDelegateStatement } from "@/app/api/common/delegateStatement/getDelegateStatement";
import {
fetchAllDelegatorsInChains,
@@ -25,6 +28,10 @@ export async function fetchDelegate(address: string) {
return apiFetchDelegate(address);
}
+export async function fetchVoterStats(address: string) {
+ return apiFetchVoterStats(address);
+}
+
export async function fetchDelegateStatement(address: string) {
return apiFetchDelegateStatement(address);
}
diff --git a/src/app/delegates/components/DelegatorFilter.tsx b/src/app/delegates/components/DelegatorFilter.tsx
new file mode 100644
index 000000000..e7cef4077
--- /dev/null
+++ b/src/app/delegates/components/DelegatorFilter.tsx
@@ -0,0 +1,88 @@
+"use client";
+
+import { useRouter, useSearchParams } from "next/navigation";
+import { Listbox } from "@headlessui/react";
+import { Fragment } from "react";
+import { ChevronDown } from "lucide-react";
+import { useAddSearchParam, useDeleteSearchParam } from "@/hooks";
+import { useAgoraContext } from "@/contexts/AgoraContext";
+import { useAccount } from "wagmi";
+
+const FILTER_PARAM = "delegatorFilter";
+const DEFAULT_FILTER = "all_delegates";
+
+export default function DelegatorFilter() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const addSearchParam = useAddSearchParam();
+ const deleteSearchParam = useDeleteSearchParam();
+ const { setIsDelegatesFiltering } = useAgoraContext();
+ const { address } = useAccount();
+
+ const filterParam = searchParams?.get(FILTER_PARAM) || DEFAULT_FILTER;
+ const delegateeFilterOptions: { value: string; sort: string }[] = [
+ {
+ value: "All delegates",
+ sort: "all_delegates",
+ },
+ {
+ value: "My delegates",
+ sort: "my_delegates",
+ },
+ ];
+
+ const handleChange = (value: string) => {
+ setIsDelegatesFiltering(true);
+ router.push(
+ value === DEFAULT_FILTER
+ ? deleteSearchParam({ name: FILTER_PARAM })
+ : addSearchParam({ name: FILTER_PARAM, value: address || "" }),
+ { scroll: false }
+ );
+ };
+
+ if (!address) return null;
+
+ return (
+ handleChange(value)}
+ >
+ {() => (
+ <>
+
+
+ {filterParam === DEFAULT_FILTER
+ ? "All delegates"
+ : "My delegates"}
+
+
+
+
+ {delegateeFilterOptions.map((key) => (
+
+ {(selected) => {
+ return (
+
+ {key.value}
+
+ );
+ }}
+
+ ))}
+
+ >
+ )}
+
+ );
+}
diff --git a/src/app/delegates/page.jsx b/src/app/delegates/page.jsx
index ce5f395f1..96dd1a204 100644
--- a/src/app/delegates/page.jsx
+++ b/src/app/delegates/page.jsx
@@ -1,32 +1,9 @@
-import { fetchCitizens as apiFetchCitizens } from "@/app/api/common/citizens/getCitizens";
-import { fetchDelegates as apiFetchDelegates } from "@/app/api/common/delegates/getDelegates";
-import { fetchCurrentDelegators as apiFetchCurrentDelegators } from "@/app/api/common/delegations/getDelegations";
-import DelegateCardList from "@/components/Delegates/DelegateCardList/DelegateCardList";
-import CitizenCardList from "@/components/Delegates/DelegateCardList/CitzenCardList";
-import DelegateTabs from "@/components/Delegates/DelegatesTabs/DelegatesTabs";
-import Hero from "@/components/Hero/Hero";
-import { TabsContent } from "@/components/ui/tabs";
-import { citizensFilterOptions, delegatesFilterOptions } from "@/lib/constants";
import Tenant from "@/lib/tenant/tenant";
-import React from "react";
-
-async function fetchCitizens(sort, seed, pagination) {
- "use server";
-
- return apiFetchCitizens({ pagination, seed, sort });
-}
-
-async function fetchDelegates(sort, seed, filters, pagination) {
- "use server";
-
- return apiFetchDelegates({ pagination, seed, sort, filters });
-}
-
-async function fetchDelegators(address) {
- "use server";
-
- return apiFetchCurrentDelegators(address);
-}
+import React, { Suspense } from "react";
+import DelegateCardWrapper, {
+ DelegateCardLoadingState,
+} from "@/components/Delegates/DelegateCardList/DelegateCardWrapper";
+import Hero from "@/components/Hero/Hero";
export async function generateMetadata({}, parent) {
const { ui } = Tenant.current();
@@ -58,66 +35,12 @@ export async function generateMetadata({}, parent) {
}
export default async function Page({ searchParams }) {
- const { ui } = Tenant.current();
-
- const sort =
- delegatesFilterOptions[searchParams.orderBy]?.sort ||
- delegatesFilterOptions.weightedRandom.sort;
- const citizensSort =
- citizensFilterOptions[searchParams.citizensOrderBy]?.value ||
- citizensFilterOptions.shuffle.sort;
-
- const filters = {
- ...(searchParams.issueFilter && { issues: searchParams.issueFilter }),
- ...(searchParams.stakeholderFilter && {
- stakeholders: searchParams.stakeholderFilter,
- }),
- };
-
- const endorsedToggle = ui.toggle("delegates/endorsed-filter");
- if (endorsedToggle?.enabled) {
- const defaultFilter = endorsedToggle.config.defaultFilter;
- filters.endorsed =
- searchParams?.endorsedFilter === undefined
- ? defaultFilter
- : searchParams.endorsedFilter === "true";
- }
-
- const tab = searchParams.tab;
- const seed = Math.random();
- const delegates =
- tab === "citizens"
- ? await fetchCitizens(citizensSort, seed)
- : await fetchDelegates(sort, seed, filters);
-
return (
-
-
- {
- "use server";
- return apiFetchDelegates({ pagination, seed, sort, filters });
- }}
- fetchDelegators={fetchDelegators}
- />
-
-
- {
- "use server";
-
- return apiFetchCitizens({ pagination, seed, sort: citizensSort });
- }}
- fetchDelegators={fetchDelegators}
- />{" "}
-
-
+ }>
+
+
);
}
diff --git a/src/app/lib/hooks/useIsAdvancedUser.ts b/src/app/lib/hooks/useIsAdvancedUser.ts
index 2cf5a2e6c..fa09e9669 100644
--- a/src/app/lib/hooks/useIsAdvancedUser.ts
+++ b/src/app/lib/hooks/useIsAdvancedUser.ts
@@ -66,6 +66,7 @@ const useIsAdvancedUser = () => {
"0x30b6e0b4f29FA72E8C7D014B6309668024ceB881", // QA 10
"0x9b3d738C07Cd0E45eE98a792bA48ba67Bb5dAbca", // QA 11
"0x416a0343470ac6694D39e2fCd6C494eeEF39BeEB", // SAFE QA 3
+ "0x648BFC4dB7e43e799a84d0f607aF0b4298F932DB", // Michael test
] as `0x${string}`[];
const { data: balance, isFetched: isBalanceFetched } = useReadContract({
diff --git a/src/app/proposals/actions.tsx b/src/app/proposals/actions.tsx
index 3a5eb2917..184c6d333 100644
--- a/src/app/proposals/actions.tsx
+++ b/src/app/proposals/actions.tsx
@@ -4,16 +4,25 @@ import { fetchAllForVoting as apiFetchAllForVoting } from "@/app/api/votes/getVo
import {
fetchUserVotesForProposal as apiFetchUserVotesForProposal,
fetchVotesForProposal as apiFetchVotesForProposal,
+ fetchVotersWhoHaveNotVotedForProposal as apiFetchVotersWhoHaveNotVotedForProposal,
} from "@/app/api/common/votes/getVotes";
import { PaginationParams } from "../lib/pagination";
+import { VotesSort } from "../api/common/votes/vote";
-export const fetchProposalVotes = (
+export const fetchVotersWhoHaveNotVotedForProposal = (
proposalId: string,
pagination?: PaginationParams
+) => apiFetchVotersWhoHaveNotVotedForProposal({ proposalId, pagination });
+
+export const fetchProposalVotes = (
+ proposalId: string,
+ pagination?: PaginationParams,
+ sort?: VotesSort
) =>
apiFetchVotesForProposal({
proposalId,
pagination,
+ sort,
});
export const fetchUserVotesForProposal = (
diff --git a/src/app/proposals/draft/components/stages/DraftForm/DraftFormClient.tsx b/src/app/proposals/draft/components/stages/DraftForm/DraftFormClient.tsx
index 8b75ca530..ea9f6cef7 100644
--- a/src/app/proposals/draft/components/stages/DraftForm/DraftFormClient.tsx
+++ b/src/app/proposals/draft/components/stages/DraftForm/DraftFormClient.tsx
@@ -89,13 +89,13 @@ const DraftFormClient = ({
const { watch, handleSubmit, control } = methods;
- const proposalType = watch("type");
+ const votingModuleType = watch("type");
const stageIndex = getStageIndexForTenant("DRAFTING") as number;
useEffect(() => {
const newValidProposalTypes = getValidProposalTypesForVotingType(
proposalTypes,
- proposalType
+ votingModuleType
);
setValidProposalTypes(newValidProposalTypes);
@@ -106,7 +106,7 @@ const DraftFormClient = ({
newValidProposalTypes[0].proposal_type_id
);
}
- }, [proposalType, proposalTypes, methods]);
+ }, [votingModuleType, proposalTypes, methods]);
const onSubmit = async (data: z.output) => {
setIsPending(true);
@@ -153,7 +153,7 @@ const DraftFormClient = ({
/>
- {ProposalTypeMetadata[proposalType].description}
+ {ProposalTypeMetadata[votingModuleType].description}
@@ -197,7 +197,7 @@ const DraftFormClient = ({
{(() => {
- switch (proposalType) {
+ switch (votingModuleType) {
case ProposalType.BASIC:
return ;
case ProposalType.SOCIAL:
@@ -207,7 +207,7 @@ const DraftFormClient = ({
case ProposalType.OPTIMISTIC:
return ;
default:
- const exhaustiveCheck: never = proposalType;
+ const exhaustiveCheck: never = votingModuleType;
return exhaustiveCheck;
}
})()}
diff --git a/src/app/staking/components/PanelClaimRewards.tsx b/src/app/staking/components/PanelClaimRewards.tsx
index 9d24a303e..6473a144e 100644
--- a/src/app/staking/components/PanelClaimRewards.tsx
+++ b/src/app/staking/components/PanelClaimRewards.tsx
@@ -26,7 +26,7 @@ export const PanelClaimRewards = () => {
Available to collect
- {formatNumber(0, 18)} WETH
+ {formatNumber(0n, 18)} WETH
diff --git a/src/app/staking/components/StakingStats.tsx b/src/app/staking/components/StakingStats.tsx
index 4bde8df96..f8386c2fd 100644
--- a/src/app/staking/components/StakingStats.tsx
+++ b/src/app/staking/components/StakingStats.tsx
@@ -1,12 +1,11 @@
import React from "react";
-import { BigNumberish } from "ethers";
import TokenAmountDisplay from "@/components/shared/TokenAmountDisplay";
interface StakingStatsProps {
rewardDuration: string;
- rewardPerToken: BigNumberish;
- totalStaked: BigNumberish;
- totalSupply: BigNumberish;
+ rewardPerToken: bigint;
+ totalStaked: bigint;
+ totalSupply: bigint;
}
export const StakingStats = ({
diff --git a/src/components/Admin/ProposalType.tsx b/src/components/Admin/ProposalType.tsx
index 7ca6ea328..c61c53d4b 100644
--- a/src/components/Admin/ProposalType.tsx
+++ b/src/components/Admin/ProposalType.tsx
@@ -196,7 +196,7 @@ export default function ProposalType({
{formatNumber(
Math.floor(
(formattedVotableSupply * formValues.quorum) / 100
- ),
+ ).toString(),
0,
1
)}{" "}
diff --git a/src/components/Button.tsx b/src/components/Button.tsx
index 5187e5d93..2f35a60ea 100644
--- a/src/components/Button.tsx
+++ b/src/components/Button.tsx
@@ -51,7 +51,7 @@ export function UpdatedButton({
className={cn(
className,
type === "primary" &&
- "bg-agora-stone-900 hover:bg-agora-stone-900/90 text-white transition-colors",
+ "bg-primary hover:bg-primary/90 text-white transition-colors",
type === "secondary" && "",
type === "link" && "",
variant === "rounded" && "rounded-full",
@@ -67,10 +67,11 @@ export function UpdatedButton({
{
switch (namespace) {
case TENANT_NAMESPACES.SCROLL:
@@ -53,16 +63,14 @@ export function DelegateActions({
);
} else {
return (
-
+
);
}
// The following tenants only support full token-based delegation:
// ENS,Cyber,Ether.fi, Uniswap
default:
- return (
-
- );
+ return ;
}
};
@@ -95,7 +103,7 @@ export function DelegateActions({
show?.();
}}
>
- Delegate
+ {isConnectedAccountDelegate ? "Undelegate" : "Delegate"}
)}
diff --git a/src/components/Delegates/DelegateCard/DelegateButton.tsx b/src/components/Delegates/DelegateCard/DelegateButton.tsx
index 4081bbf99..3658e1a8d 100644
--- a/src/components/Delegates/DelegateCard/DelegateButton.tsx
+++ b/src/components/Delegates/DelegateCard/DelegateButton.tsx
@@ -1,4 +1,4 @@
-import { Button } from "@/components/Button";
+import { UpdatedButton } from "@/components/Button";
import { useOpenDialog } from "@/components/Dialogs/DialogProvider/DialogProvider";
import { DelegateChunk } from "@/app/api/common/delegates/delegate";
import {
@@ -16,7 +16,8 @@ export function DelegateButton({
const openDialog = useOpenDialog();
return (
- {
e.preventDefault();
openDialog({
@@ -31,6 +32,6 @@ export function DelegateButton({
className={full ? "w-full" : undefined}
>
Delegate
-
+
);
}
diff --git a/src/components/Delegates/DelegateCard/DelegateCard.tsx b/src/components/Delegates/DelegateCard/DelegateCard.tsx
index 91bbb320c..6494d4377 100644
--- a/src/components/Delegates/DelegateCard/DelegateCard.tsx
+++ b/src/components/Delegates/DelegateCard/DelegateCard.tsx
@@ -1,13 +1,60 @@
-import { bpsToString, pluralizeAddresses } from "@/lib/utils";
import { DelegateProfileImage } from "./DelegateProfileImage";
import DelegateCardClient from "./DelegateCardClient";
+import { formatNumber } from "@/lib/tokenUtils";
import { Delegate } from "@/app/api/common/delegates/delegate";
+const CardHeader = ({
+ title,
+ cornerTitle,
+ subtitle,
+}: {
+ title: string;
+ cornerTitle: string;
+ subtitle: string;
+}) => {
+ return (
+
+
+
+
{title}
+ {cornerTitle}
+
+
{subtitle}
+
+
+ );
+};
+
+const ActiveHeader = ({ outOfTen }: { outOfTen: string }) => {
+ return (
+
+ );
+};
+
+const InactiveHeader = ({ outOfTen }: { outOfTen: string }) => {
+ return (
+
+ );
+};
+
export default function DelegateCard({ delegate }: { delegate: Delegate }) {
return (
+ {parseInt(delegate.lastTenProps) > 5 ? (
+
+ ) : (
+
+ )}
-
+
-
+
+ {/*
-
-
*/}
+ {/*
*/}
+
+
+ {delegate.votedFor}
+
+
+ {delegate.votedAgainst}
+
+
+ {delegate.votedAbstain}
+
+
+ }
/>
@@ -66,8 +130,16 @@ export const PanelRow = ({
}) => {
return (
- {title}
- {detail}
+ {title}
+
+ {detail}
+
);
};
+
+export const DelegateCardSkeleton = () => {
+ return (
+
+ );
+};
diff --git a/src/components/Delegates/DelegateCard/DelegateProfileImage.tsx b/src/components/Delegates/DelegateCard/DelegateProfileImage.tsx
index 394f12e95..d028517b4 100644
--- a/src/components/Delegates/DelegateCard/DelegateProfileImage.tsx
+++ b/src/components/Delegates/DelegateCard/DelegateProfileImage.tsx
@@ -18,6 +18,7 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { UIEndorsedConfig } from "@/lib/tenant/tenantUI";
+import { truncateAddress } from "@/app/lib/utils/text";
interface Props {
address: string;
@@ -36,7 +37,6 @@ export function DelegateProfileImage({
}: Props) {
const { ui } = Tenant.current();
const { refetchDelegate, setRefetchDelegate } = useConnectButtonContext();
- const { token } = Tenant.current();
const formattedNumber = useMemo(() => {
return formatNumber(votingPower);
}, [votingPower]);
@@ -80,7 +80,7 @@ export function DelegateProfileImage({
}, [address, formattedNumber, refetchDelegate, setRefetchDelegate]);
return (
-
+
{citizen && (
)}
-
- {formattedNumber} {token.symbol}
-
);
diff --git a/src/components/Delegates/DelegateCard/UndelegateButton.tsx b/src/components/Delegates/DelegateCard/UndelegateButton.tsx
new file mode 100644
index 000000000..8a00b8a1b
--- /dev/null
+++ b/src/components/Delegates/DelegateCard/UndelegateButton.tsx
@@ -0,0 +1,37 @@
+import { UpdatedButton } from "@/components/Button";
+import { useOpenDialog } from "@/components/Dialogs/DialogProvider/DialogProvider";
+import { DelegateChunk } from "@/app/api/common/delegates/delegate";
+import {
+ fetchDirectDelegatee,
+ fetchBalanceForDirectDelegation,
+} from "@/app/delegates/actions";
+
+export function UndelegateButton({
+ full,
+ delegate,
+}: {
+ full: boolean;
+ delegate: DelegateChunk;
+}) {
+ const openDialog = useOpenDialog();
+
+ return (
+
{
+ e.preventDefault();
+ openDialog({
+ type: "UNDELEGATE",
+ params: {
+ delegate,
+ fetchBalanceForDirectDelegation,
+ fetchDirectDelegatee,
+ },
+ });
+ }}
+ className={full ? "w-full" : undefined}
+ >
+ Undelegate
+
+ );
+}
diff --git a/src/components/Delegates/DelegateCardList/CitzenCardList.tsx b/src/components/Delegates/DelegateCardList/CitzenCardList.tsx
index 5615fd13e..511b886a7 100644
--- a/src/components/Delegates/DelegateCardList/CitzenCardList.tsx
+++ b/src/components/Delegates/DelegateCardList/CitzenCardList.tsx
@@ -12,15 +12,15 @@ import useConnectedDelegate from "@/hooks/useConnectedDelegate";
import { cn } from "@/lib/utils";
import { useAgoraContext } from "@/contexts/AgoraContext";
import { PaginatedResult, PaginationParams } from "@/app/lib/pagination";
-import { Delegate } from "@/app/api/common/delegates/delegate";
+import { DelegateChunk } from "@/app/api/common/delegates/delegate";
interface Props {
isDelegatesCitizensFetching: boolean;
- initialDelegates: PaginatedResult
;
+ initialDelegates: PaginatedResult;
fetchDelegates: (
pagination: PaginationParams,
seed: number
- ) => Promise>;
+ ) => Promise>;
fetchDelegators: (addressOrENSName: string) => Promise;
}
diff --git a/src/components/Delegates/DelegateCardList/DelegateCard.tsx b/src/components/Delegates/DelegateCardList/DelegateCard.tsx
new file mode 100644
index 000000000..7967f7461
--- /dev/null
+++ b/src/components/Delegates/DelegateCardList/DelegateCard.tsx
@@ -0,0 +1,80 @@
+import Link from "next/link";
+import { DelegateChunk } from "@/app/api/common/delegates/delegate";
+import { cn } from "@/lib/utils";
+import { formatNumber } from "@/lib/tokenUtils";
+import { DelegateProfileImage } from "../DelegateCard/DelegateProfileImage";
+import { DelegateActions } from "../DelegateCard/DelegateActions";
+import Tenant from "@/lib/tenant/tenant";
+import useConnectedDelegate from "@/hooks/useConnectedDelegate";
+import { useVotingStats } from "@/hooks/useVotingStats";
+
+const DelegateCard = ({
+ delegate,
+ isDelegatesCitizensFetching,
+ isDelegatesFiltering,
+ isAdvancedUser,
+ truncatedStatement,
+}: {
+ delegate: DelegateChunk;
+ isDelegatesCitizensFetching: boolean;
+ isDelegatesFiltering: boolean;
+ isAdvancedUser: boolean;
+ truncatedStatement: string;
+}) => {
+ const { token } = Tenant.current();
+ const { advancedDelegators } = useConnectedDelegate();
+ const { data: votingStats, isLoading: isVotingStatsLoading } = useVotingStats(
+ {
+ address: delegate.address as `0x${string}`,
+ }
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+ {formatNumber(delegate.votingPower.total)} {token.symbol}
+
+ {votingStats && (
+
+ {(votingStats?.last_10_props || 0) * 10}% Participation
+
+ )}
+
+
+ {truncatedStatement}
+
+
+
+
+
+
+
+
+ );
+};
+
+export default DelegateCard;
diff --git a/src/components/Delegates/DelegateCardList/DelegateCardList.tsx b/src/components/Delegates/DelegateCardList/DelegateCardList.tsx
index b337a8a91..a8c74f4b4 100644
--- a/src/components/Delegates/DelegateCardList/DelegateCardList.tsx
+++ b/src/components/Delegates/DelegateCardList/DelegateCardList.tsx
@@ -2,17 +2,15 @@
import { useEffect, useRef, useState } from "react";
import InfiniteScroll from "react-infinite-scroller";
-import { DelegateActions } from "../DelegateCard/DelegateActions";
-import { DelegateProfileImage } from "../DelegateCard/DelegateProfileImage";
import { DialogProvider } from "@/components/Dialogs/DialogProvider/DialogProvider";
import { DelegateChunk } from "@/app/api/common/delegates/delegate";
import useIsAdvancedUser from "@/app/lib/hooks/useIsAdvancedUser";
-import Link from "next/link";
import { Delegation } from "@/app/api/common/delegations/delegation";
import useConnectedDelegate from "@/hooks/useConnectedDelegate";
-import { cn } from "@/lib/utils";
import { useAgoraContext } from "@/contexts/AgoraContext";
import { PaginatedResult, PaginationParams } from "@/app/lib/pagination";
+import Tenant from "@/lib/tenant/tenant";
+import DelegateCard from "./DelegateCard";
interface Props {
isDelegatesCitizensFetching: boolean;
@@ -29,6 +27,7 @@ export default function DelegateCardList({
fetchDelegates,
isDelegatesCitizensFetching,
}: Props) {
+ const { token } = Tenant.current();
const fetching = useRef(false);
const [meta, setMeta] = useState(initialDelegates.meta);
const [delegates, setDelegates] = useState(initialDelegates.data);
@@ -74,7 +73,7 @@ export default function DelegateCardList({
}
element="div"
>
- {delegates?.map((delegate) => {
+ {delegates?.map((delegate, idx) => {
let truncatedStatement = "";
if (delegate?.statement?.payload) {
@@ -85,38 +84,14 @@ export default function DelegateCardList({
}
return (
-
-
-
-
-
-
- {truncatedStatement}
-
-
-
-
-
-
-
-
+
);
})}
diff --git a/src/components/Delegates/DelegateCardList/DelegateCardWrapper.tsx b/src/components/Delegates/DelegateCardList/DelegateCardWrapper.tsx
new file mode 100644
index 000000000..1611cc10b
--- /dev/null
+++ b/src/components/Delegates/DelegateCardList/DelegateCardWrapper.tsx
@@ -0,0 +1,137 @@
+import { fetchCitizens as apiFetchCitizens } from "@/app/api/common/citizens/getCitizens";
+import { fetchDelegates as apiFetchDelegates } from "@/app/api/common/delegates/getDelegates";
+import { fetchCurrentDelegators as apiFetchCurrentDelegators } from "@/app/api/common/delegations/getDelegations";
+import DelegateCardList from "@/components/Delegates/DelegateCardList/DelegateCardList";
+import CitizenCardList from "@/components/Delegates/DelegateCardList/CitzenCardList";
+import DelegateTabs from "@/components/Delegates/DelegatesTabs/DelegatesTabs";
+import { TabsContent } from "@/components/ui/tabs";
+import { citizensFilterOptions, delegatesFilterOptions } from "@/lib/constants";
+import Tenant from "@/lib/tenant/tenant";
+import React from "react";
+import { PaginationParams } from "@/app/lib/pagination";
+import { UIEndorsedConfig } from "@/lib/tenant/tenantUI";
+
+async function fetchCitizens(
+ sort: string,
+ seed: number,
+ pagination?: PaginationParams
+) {
+ "use server";
+
+ return apiFetchCitizens({ pagination, seed, sort });
+}
+
+async function fetchDelegates(
+ sort: string,
+ seed: number,
+ filters: any,
+ pagination?: PaginationParams
+) {
+ "use server";
+
+ return apiFetchDelegates({ pagination, seed, sort, filters });
+}
+
+async function fetchDelegators(address: string) {
+ "use server";
+
+ return apiFetchCurrentDelegators(address);
+}
+
+const DelegateCardWrapper = async ({ searchParams }: { searchParams: any }) => {
+ const { ui } = Tenant.current();
+
+ const sort =
+ delegatesFilterOptions[
+ searchParams.orderBy as keyof typeof delegatesFilterOptions
+ ]?.sort || delegatesFilterOptions.weightedRandom.sort;
+ const citizensSort =
+ citizensFilterOptions[
+ searchParams.citizensOrderBy as keyof typeof citizensFilterOptions
+ ]?.value || citizensFilterOptions.shuffle.sort;
+
+ const filters = {
+ ...(searchParams.delegatorFilter && {
+ delegator: searchParams.delegatorFilter,
+ }),
+ ...(searchParams.issueFilter && { issues: searchParams.issueFilter }),
+ ...(searchParams.stakeholderFilter && {
+ stakeholders: searchParams.stakeholderFilter,
+ }),
+ };
+
+ const endorsedToggle = ui.toggle("delegates/endorsed-filter");
+ if (endorsedToggle?.enabled) {
+ const defaultFilter = (endorsedToggle.config as UIEndorsedConfig)
+ .defaultFilter;
+ filters.endorsed =
+ searchParams?.endorsedFilter === undefined
+ ? defaultFilter
+ : searchParams.endorsedFilter === "true";
+ }
+
+ const tab = searchParams.tab;
+ const seed = Math.random();
+ const delegates =
+ tab === "citizens"
+ ? await fetchCitizens(citizensSort, seed)
+ : await fetchDelegates(sort, seed, filters);
+
+ return (
+
+
+ {
+ "use server";
+ return apiFetchDelegates({ pagination, seed, sort, filters });
+ }}
+ // @ts-ignore
+ fetchDelegators={fetchDelegators}
+ />
+
+
+ {
+ "use server";
+
+ return apiFetchCitizens({ pagination, seed, sort: citizensSort });
+ }}
+ // @ts-ignore
+ fetchDelegators={fetchDelegators}
+ />{" "}
+
+
+ );
+};
+
+export const DelegateCardLoadingState = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default DelegateCardWrapper;
diff --git a/src/components/Delegates/DelegateStatement/DelegateStatementContainer.tsx b/src/components/Delegates/DelegateStatement/DelegateStatementContainer.tsx
index 4d921e2eb..d28423151 100644
--- a/src/components/Delegates/DelegateStatement/DelegateStatementContainer.tsx
+++ b/src/components/Delegates/DelegateStatement/DelegateStatementContainer.tsx
@@ -5,6 +5,7 @@ import { useAccount } from "wagmi";
import { useSearchParams } from "next/navigation";
import DelegateStatement from "./DelegateStatement";
import { DelegateStatement as DelegateStatementType } from "@/app/api/common/delegateStatement/delegateStatement";
+import { fetchDelegate } from "@/app/delegates/actions";
export default function DelegateStatementContainer({
addressOrENSName,
diff --git a/src/components/Delegates/DelegateStatement/DelegateStatementWrapper.tsx b/src/components/Delegates/DelegateStatement/DelegateStatementWrapper.tsx
new file mode 100644
index 000000000..010ad86a0
--- /dev/null
+++ b/src/components/Delegates/DelegateStatement/DelegateStatementWrapper.tsx
@@ -0,0 +1,40 @@
+import { fetchDelegate } from "@/app/delegates/actions";
+import { resolveENSName } from "@/app/lib/ENSUtils";
+import DelegateStatementContainer from "./DelegateStatementContainer";
+import TopStakeholders from "./TopStakeholders";
+import TopIssues from "./TopIssues";
+
+const DelegateStatementWrapper = async ({
+ addressOrENSName,
+}: {
+ addressOrENSName: string;
+}) => {
+ const address = await resolveENSName(addressOrENSName);
+ const delegate = await fetchDelegate(address);
+
+ return (
+ <>
+
+ {delegate.statement && (
+ <>
+
+
+ >
+ )}
+ >
+ );
+};
+
+export const DelegateStatementSkeleton = () => {
+ return (
+
+ );
+};
+
+export default DelegateStatementWrapper;
diff --git a/src/components/Delegates/DelegateVotes/VotesContainerWrapper.tsx b/src/components/Delegates/DelegateVotes/VotesContainerWrapper.tsx
new file mode 100644
index 000000000..8a737290d
--- /dev/null
+++ b/src/components/Delegates/DelegateVotes/VotesContainerWrapper.tsx
@@ -0,0 +1,74 @@
+import { resolveENSName } from "@/app/lib/ENSUtils";
+import VotesContainer from "./VotesContainer";
+import { fetchVotesForDelegate } from "@/app/delegates/actions";
+import { fetchSnapshotVotesForDelegate } from "@/app/api/common/votes/getVotes";
+import { PaginationParams } from "@/app/lib/pagination";
+import DelegateVotes from "./DelegateVotes";
+import SnapshotVotes from "./SnapshotVotes";
+
+const VotesContainerWrapper = async ({
+ addressOrENSName,
+}: {
+ addressOrENSName: string;
+}) => {
+ const address = (await resolveENSName(addressOrENSName)) || addressOrENSName;
+ const [delegateVotes, snapshotVotes] = await Promise.all([
+ fetchVotesForDelegate(address),
+ fetchSnapshotVotesForDelegate({ addressOrENSName: address }),
+ ]);
+
+ return (
+
+ {delegateVotes && delegateVotes.data.length > 0 ? (
+
+ {
+ "use server";
+ return fetchVotesForDelegate(addressOrENSName, pagination);
+ }}
+ />
+
+ ) : (
+
+ No past votes available.
+
+ )}
+ >
+ }
+ snapshotVotes={
+ <>
+ {snapshotVotes && snapshotVotes.data.length > 0 ? (
+ {
+ "use server";
+ return await fetchSnapshotVotesForDelegate({
+ addressOrENSName: addressOrENSName,
+ pagination,
+ });
+ }}
+ />
+ ) : (
+
+ No past votes available.
+
+ )}
+ >
+ }
+ />
+ );
+};
+
+export const VotesContainerSkeleton = () => {
+ return (
+
+ );
+};
+
+export default VotesContainerWrapper;
diff --git a/src/components/Delegates/DelegatesTabs/DelegatesTabs.tsx b/src/components/Delegates/DelegatesTabs/DelegatesTabs.tsx
index 4d7965999..6edc6fff5 100644
--- a/src/components/Delegates/DelegatesTabs/DelegatesTabs.tsx
+++ b/src/components/Delegates/DelegatesTabs/DelegatesTabs.tsx
@@ -11,6 +11,7 @@ import { useAddSearchParam, useDeleteSearchParam } from "@/hooks";
import StakeholdersFilter from "@/app/delegates/components/StakeholdersFilter";
import IssuesFilter from "@/app/delegates/components/IssuesFilter";
import EndorsedFilter from "@/app/delegates/components/EndorsedFilter";
+import DelegateeFilter from "@/app/delegates/components/DelegatorFilter";
export default function DelegateTabs({ children }: { children: ReactNode }) {
const { ui } = Tenant.current();
@@ -25,6 +26,10 @@ export default function DelegateTabs({ children }: { children: ReactNode }) {
ui.toggle("delegates/endorsed-filter")?.enabled
);
+ const hasMyDelegatesFilter = Boolean(
+ ui.toggle("delegates/my-delegates-filter")?.enabled
+ );
+
const router = useRouter();
const searchParams = useSearchParams();
const addSearchParam = useAddSearchParam();
@@ -61,6 +66,7 @@ export default function DelegateTabs({ children }: { children: ReactNode }) {
+ {hasMyDelegatesFilter &&
}
{hasStakeholdersFilter && tabParam !== "citizens" && (
)}
diff --git a/src/components/Delegates/Delegations/DelegationsContainerWrapper.tsx b/src/components/Delegates/Delegations/DelegationsContainerWrapper.tsx
new file mode 100644
index 000000000..947bfabed
--- /dev/null
+++ b/src/components/Delegates/Delegations/DelegationsContainerWrapper.tsx
@@ -0,0 +1,40 @@
+import { PaginationParams } from "@/app/lib/pagination";
+import DelegationsContainer from "./DelegationsContainer";
+import { resolveENSName } from "@/app/lib/ENSUtils";
+import {
+ fetchCurrentDelegatees,
+ fetchCurrentDelegators,
+} from "@/app/delegates/actions";
+
+const DelegationsContainerWrapper = async ({
+ addressOrENSName,
+}: {
+ addressOrENSName: string;
+}) => {
+ const address = (await resolveENSName(addressOrENSName)) || addressOrENSName;
+ const [delegatees, delegators] = await Promise.all([
+ fetchCurrentDelegatees(address),
+ fetchCurrentDelegators(address),
+ ]);
+ return (
+
{
+ "use server";
+ return fetchCurrentDelegators(addressOrENSName, pagination);
+ }}
+ />
+ );
+};
+
+export const DelegationsContainerSkeleton = () => {
+ return (
+
+ );
+};
+
+export default DelegationsContainerWrapper;
diff --git a/src/components/Dialogs/DialogProvider/dialogs.tsx b/src/components/Dialogs/DialogProvider/dialogs.tsx
index de6b946d8..d6a361768 100644
--- a/src/components/Dialogs/DialogProvider/dialogs.tsx
+++ b/src/components/Dialogs/DialogProvider/dialogs.tsx
@@ -1,5 +1,6 @@
import { DialogDefinitions } from "./types";
import { DelegateDialog } from "../DelegateDialog/DelegateDialog";
+import { UndelegateDialog } from "../UndelegateDialog/UndelegateDialog";
import { SwitchNetwork } from "../SwitchNetworkDialog/SwitchNetworkDialog";
import { CastProposalDialog } from "@/components/Proposals/ProposalCreation/CastProposalDialog";
import {
@@ -25,14 +26,12 @@ import SponsorOnchainProposalDialog from "@/app/proposals/draft/components/dialo
import SponsorSnapshotProposalDialog from "@/app/proposals/draft/components/dialogs/SponsorSnapshotProposalDialog";
import AddGithubPRDialog from "@/app/proposals/draft/components/dialogs/AddGithubPRDialog";
import { StakedDeposit } from "@/lib/types";
-import {
- fetchAllForAdvancedDelegation,
- fetchCurrentDelegatees,
-} from "@/app/delegates/actions";
+import { fetchAllForAdvancedDelegation } from "@/app/delegates/actions";
import { PartialDelegationDialog } from "@/components/Dialogs/PartialDelegateDialog/PartialDelegationDialog";
export type DialogType =
| DelegateDialogType
+ | UndelegateDialogType
| CastProposalDialogType
| CastVoteDialogType
| AdvancedDelegateDialogType
@@ -61,6 +60,19 @@ export type DelegateDialogType = {
};
};
+export type UndelegateDialogType = {
+ type: "UNDELEGATE";
+ params: {
+ delegate: DelegateChunk;
+ fetchBalanceForDirectDelegation: (
+ addressOrENSName: string
+ ) => Promise;
+ fetchDirectDelegatee: (
+ addressOrENSName: string
+ ) => Promise;
+ };
+};
+
export type AdvancedDelegateDialogType = {
type: "ADVANCED_DELEGATE";
params: {
@@ -197,6 +209,18 @@ export const dialogs: DialogDefinitions = {
/>
);
},
+ UNDELEGATE: (
+ { delegate, fetchBalanceForDirectDelegation, fetchDirectDelegatee },
+ closeDialog
+ ) => {
+ return (
+
+ );
+ },
PARTIAL_DELEGATE: ({ delegate, fetchCurrentDelegatees }, closeDialog) => {
return (
{
+ return formatNumber(amount);
+ }, [amount]);
+
+ return (
+
+
+ {formattedNumber} {token.symbol}
+
+ );
+}
diff --git a/src/components/Dialogs/UndelegateDialog/UndelegateDialog.tsx b/src/components/Dialogs/UndelegateDialog/UndelegateDialog.tsx
new file mode 100644
index 000000000..ed034b46e
--- /dev/null
+++ b/src/components/Dialogs/UndelegateDialog/UndelegateDialog.tsx
@@ -0,0 +1,232 @@
+import {
+ useAccount,
+ useWriteContract,
+ useEnsName,
+ useWaitForTransactionReceipt,
+} from "wagmi";
+import { ArrowDownIcon } from "@heroicons/react/20/solid";
+import { Button } from "@/components/Button";
+import { Button as ShadcnButton } from "@/components/ui/button";
+import { DelegateChunk } from "@/app/api/common/delegates/delegate";
+import { useCallback, useEffect, useState } from "react";
+import {
+ AgoraLoaderSmall,
+ LogoLoader,
+} from "@/components/shared/AgoraLoader/AgoraLoader";
+import ENSAvatar from "@/components/shared/ENSAvatar";
+import ENSName from "@/components/shared/ENSName";
+import BlockScanUrls from "@/components/shared/BlockScanUrl";
+import { useConnectButtonContext } from "@/contexts/ConnectButtonContext";
+import { DelegateePayload } from "@/app/api/common/delegations/delegation";
+import Tenant from "@/lib/tenant/tenant";
+import { revalidateData } from "./revalidateAction";
+import { zeroAddress } from "viem";
+
+export function UndelegateDialog({
+ delegate,
+ fetchBalanceForDirectDelegation,
+ fetchDirectDelegatee,
+}: {
+ delegate: DelegateChunk;
+ fetchBalanceForDirectDelegation: (
+ addressOrENSName: string
+ ) => Promise;
+ fetchDirectDelegatee: (
+ addressOrENSName: string
+ ) => Promise;
+}) {
+ const { ui, contracts, token } = Tenant.current();
+ const shouldHideAgoraBranding = ui.hideAgoraBranding;
+ const { address: accountAddress } = useAccount();
+ const [votingPower, setVotingPower] = useState("");
+ const [delegatee, setDelegatee] = useState(null);
+ const [isReady, setIsReady] = useState(false);
+ const { setRefetchDelegate } = useConnectButtonContext();
+ const sameDelegatee =
+ delegate.address.toLowerCase() === accountAddress?.toLowerCase();
+
+ const isDisabledInTenant = ui.toggle("delegates/delegate")?.enabled === false;
+
+ const { data: accountEnsName } = useEnsName({
+ chainId: 1,
+ address: accountAddress as `0x${string}`,
+ });
+
+ const { data: delegateeEnsName } = useEnsName({
+ chainId: 1,
+ address: delegatee?.delegatee as `0x${string}`,
+ });
+
+ const { isError, writeContract: write, data } = useWriteContract();
+
+ const {
+ isLoading: isProcessingDelegation,
+ isSuccess: didProcessDelegation,
+ isError: didFailDelegation,
+ } = useWaitForTransactionReceipt({
+ hash: data,
+ });
+
+ const fetchData = useCallback(async () => {
+ setIsReady(false);
+ if (!accountAddress) return;
+
+ try {
+ const vp = await fetchBalanceForDirectDelegation(accountAddress);
+ setVotingPower(vp.toString());
+
+ const direct = await fetchDirectDelegatee(accountAddress);
+ setDelegatee(direct);
+ } finally {
+ setIsReady(true);
+ }
+ }, [fetchBalanceForDirectDelegation, accountAddress, fetchDirectDelegatee]);
+
+ const renderActionButtons = () => {
+ if (isDisabledInTenant) {
+ return (
+
+ {token.symbol} delegation is disabled at this time
+
+ );
+ }
+
+ if (sameDelegatee) {
+ return (
+
+ You are already delegated to yourself
+
+ );
+ }
+
+ if (isError || didFailDelegation) {
+ return (
+
+ write({
+ address: contracts.token.address as any,
+ abi: contracts.token.abi,
+ functionName: "delegate",
+ args: [zeroAddress],
+ })
+ }
+ >
+ Undelegation failed - try again
+
+ );
+ }
+
+ if (isProcessingDelegation) {
+ return (
+ Submitting your undelegation request...
+ );
+ }
+
+ if (didProcessDelegation) {
+ return (
+
+
+ Undelegation completed!
+
+
+
+ );
+ }
+
+ return (
+
+ write({
+ address: contracts.token.address as any,
+ abi: contracts.token.abi,
+ functionName: "delegate",
+ args: [zeroAddress],
+ })
+ }
+ >
+ Undelegate
+
+ );
+ };
+
+ useEffect(() => {
+ if (!isReady) {
+ fetchData();
+ }
+
+ if (didProcessDelegation) {
+ // Refresh delegation
+ if (Number(votingPower) > 0) {
+ setRefetchDelegate({
+ address: delegate.address,
+ prevVotingPowerDelegatee: delegate.votingPower.total,
+ });
+ }
+ revalidateData();
+ }
+ }, [isReady, fetchData, didProcessDelegation, delegate, votingPower]);
+
+ if (!isReady) {
+ return (
+
+ {shouldHideAgoraBranding ?
:
}
+
+ );
+ }
+
+ return (
+
+
+ {delegatee ? (
+
+
+
+ Remove as your
+ delegate
+
+
+ This delegate will no longer be able to vote on your behalf.
+ Your votes will be returned to you.
+
+
+
+
+
+
+
+ Currently delegated to
+
+
+
+
+
+
+
+
+
+
+ Remove your delegate votes
+
+
+
+
+
+
+
+
+ ) : (
+
+
+ You are not currently delegating any votes.
+
+
+ )}
+
+ {renderActionButtons()}
+
+
+ );
+}
diff --git a/src/components/Dialogs/UndelegateDialog/revalidateAction.tsx b/src/components/Dialogs/UndelegateDialog/revalidateAction.tsx
new file mode 100644
index 000000000..24958a37e
--- /dev/null
+++ b/src/components/Dialogs/UndelegateDialog/revalidateAction.tsx
@@ -0,0 +1,7 @@
+"use server";
+
+import { revalidatePath } from "next/cache";
+
+export async function revalidateData() {
+ revalidatePath("/delegates");
+}
diff --git a/src/components/Proposals/ProposalPage/OPProposalApprovalPage/ApprovalVotesPanel/ApprovalVotesPanel.tsx b/src/components/Proposals/ProposalPage/OPProposalApprovalPage/ApprovalVotesPanel/ApprovalVotesPanel.tsx
index 35143cc8d..468c1fe20 100644
--- a/src/components/Proposals/ProposalPage/OPProposalApprovalPage/ApprovalVotesPanel/ApprovalVotesPanel.tsx
+++ b/src/components/Proposals/ProposalPage/OPProposalApprovalPage/ApprovalVotesPanel/ApprovalVotesPanel.tsx
@@ -12,10 +12,13 @@ import { Vote } from "@/app/api/common/votes/vote";
import { VotingPowerData } from "@/app/api/common/voting-power/votingPower";
import { Delegate } from "@/app/api/common/delegates/delegate";
import { PaginatedResult, PaginationParams } from "@/app/lib/pagination";
+import ProposalVotesFilter from "@/components/Proposals/ProposalPage/OPProposalPage/ProposalVotesCard/ProposalVotesFilter";
+import ProposalNonVoterList from "@/components/Votes/ProposalVotesList/ProposalNonVoterList";
type Props = {
proposal: Proposal;
initialProposalVotes: PaginatedResult;
+ nonVoters: any;
fetchVotesForProposal: (
proposalId: string,
pagination?: PaginationParams
@@ -39,10 +42,12 @@ type Props = {
export default function ApprovalVotesPanel({
proposal,
initialProposalVotes,
+ nonVoters,
fetchVotesForProposal,
fetchAllForVoting,
fetchUserVotesForProposal,
}: Props) {
+ const [showVoters, setShowVoters] = useState(true);
const [activeTab, setActiveTab] = useState(1);
const [isPending, startTransition] = useTransition();
function handleTabsChange(index: number) {
@@ -79,12 +84,29 @@ export default function ApprovalVotesPanel({
{activeTab === 1 ? (
) : (
-
+ <>
+
+
{
+ setShowVoters(value === "Voters");
+ }}
+ />
+
+ {showVoters ? (
+
+ ) : (
+
+ )}
+ >
)}
diff --git a/src/components/Proposals/ProposalPage/OPProposalApprovalPage/OPProposalApprovalPage.tsx b/src/components/Proposals/ProposalPage/OPProposalApprovalPage/OPProposalApprovalPage.tsx
index 260d5a511..5aadaf6a2 100644
--- a/src/components/Proposals/ProposalPage/OPProposalApprovalPage/OPProposalApprovalPage.tsx
+++ b/src/components/Proposals/ProposalPage/OPProposalApprovalPage/OPProposalApprovalPage.tsx
@@ -9,6 +9,7 @@ import {
fetchVotesForProposal,
} from "@/app/api/common/votes/getVotes";
import { PaginationParams } from "@/app/lib/pagination";
+import { fetchVotersWhoHaveNotVotedForProposal } from "@/app/proposals/actions";
async function fetchProposalVotes(
proposalId: string,
@@ -50,6 +51,7 @@ export default async function OPProposalApprovalPage({
proposal: Proposal;
}) {
const proposalVotes = await fetchProposalVotes(proposal.id);
+ const nonVoters = await fetchVotersWhoHaveNotVotedForProposal(proposal.id);
return (
// 2 Colum Layout: Description on left w/ transactions and Votes / voting on the right
@@ -72,6 +74,7 @@ export default async function OPProposalApprovalPage({
{
const [isClicked, setIsClicked] = useState(false);
+ const [showVoters, setShowVoters] = useState(true);
const handleClick = () => {
setIsClicked(!isClicked);
// var div = document.getElementsByClassName("mobile-web-scroll-div")[0];
@@ -93,11 +98,26 @@ const OptimisticProposalVotesCard = ({
/>
+
+
{
+ setShowVoters(value === "Voters");
+ }}
+ />
+
{/* Show the scrolling list of votes for the proposal */}
-
+ {showVoters ? (
+
+ ) : (
+
+ )}
{/* Show the input for the user to vote on a proposal if allowed */}
diff --git a/src/components/Proposals/ProposalPage/OPProposalPage/ProposalVotesCard/ProposalVotesCard.tsx b/src/components/Proposals/ProposalPage/OPProposalPage/ProposalVotesCard/ProposalVotesCard.tsx
index f5821056b..71ca8816e 100644
--- a/src/components/Proposals/ProposalPage/OPProposalPage/ProposalVotesCard/ProposalVotesCard.tsx
+++ b/src/components/Proposals/ProposalPage/OPProposalPage/ProposalVotesCard/ProposalVotesCard.tsx
@@ -8,15 +8,20 @@ import { icons } from "@/assets/icons/icons";
import { Proposal } from "@/app/api/common/proposals/proposal";
import { PaginatedResult } from "@/app/lib/pagination";
import { Vote } from "@/app/api/common/votes/vote";
+import ProposalVotesFilter from "./ProposalVotesFilter";
+import ProposalNonVoterList from "@/components/Votes/ProposalVotesList/ProposalNonVoterList";
const ProposalVotesCard = ({
proposal,
proposalVotes,
+ nonVoters,
}: {
proposal: Proposal;
proposalVotes: PaginatedResult;
+ nonVoters: PaginatedResult; // TODO: add better types
}) => {
const [isClicked, setIsClicked] = useState(false);
+ const [showVoters, setShowVoters] = useState(true);
const handleClick = () => {
setIsClicked(!isClicked);
@@ -38,19 +43,33 @@ const ProposalVotesCard = ({
-
-
Proposal votes
-
+
+
Proposal votes
+
+
{
+ setShowVoters(value === "Voters");
+ }}
+ />
+
-
+ {showVoters ? (
+
+ ) : (
+
+ )}
{/* Show the input for the user to vote on a proposal if allowed */}
diff --git a/src/components/Proposals/ProposalPage/OPProposalPage/ProposalVotesCard/ProposalVotesFilter.tsx b/src/components/Proposals/ProposalPage/OPProposalPage/ProposalVotesCard/ProposalVotesFilter.tsx
new file mode 100644
index 000000000..48deb7a61
--- /dev/null
+++ b/src/components/Proposals/ProposalPage/OPProposalPage/ProposalVotesCard/ProposalVotesFilter.tsx
@@ -0,0 +1,36 @@
+"use client";
+
+import { useState } from "react";
+import { Switch } from "@/components/shared/Switch";
+
+const voteFilterOptions = {
+ ["Voters"]: {
+ sortKey: "voters",
+ },
+ ["Hasn't voted"]: {
+ sortKey: "nonVoters",
+ },
+};
+
+const ProposalVotesFilter = ({
+ initialSelection,
+ onSelectionChange,
+}: {
+ initialSelection: string;
+ onSelectionChange: (value: string) => void;
+}) => {
+ const [value, setValue] = useState(initialSelection);
+
+ return (
+
key)}
+ selection={value}
+ onSelectionChanged={(value: any) => {
+ setValue(value);
+ onSelectionChange(value);
+ }}
+ />
+ );
+};
+
+export default ProposalVotesFilter;
diff --git a/src/components/Proposals/ProposalPage/OPProposalPage/StandardProposalPage.tsx b/src/components/Proposals/ProposalPage/OPProposalPage/StandardProposalPage.tsx
index 64d7c0c79..06fbef5ae 100644
--- a/src/components/Proposals/ProposalPage/OPProposalPage/StandardProposalPage.tsx
+++ b/src/components/Proposals/ProposalPage/OPProposalPage/StandardProposalPage.tsx
@@ -1,7 +1,10 @@
import ProposalDescription from "../ProposalDescription/ProposalDescription";
import { Proposal } from "@/app/api/common/proposals/proposal";
import StandardProposalDelete from "./StandardProposalDelete";
-import { fetchProposalVotes } from "@/app/proposals/actions";
+import {
+ fetchProposalVotes,
+ fetchVotersWhoHaveNotVotedForProposal,
+} from "@/app/proposals/actions";
import ProposalVotesCard from "./ProposalVotesCard/ProposalVotesCard";
import Tenant from "@/lib/tenant/tenant";
import { ProposalStateAdmin } from "@/app/proposals/components/ProposalStateAdmin";
@@ -11,11 +14,12 @@ export default async function StandardProposalPage({
}: {
proposal: Proposal;
}) {
- const { contracts, ui } = Tenant.current();
+ const { contracts } = Tenant.current();
// TODO: Replace with governor-level check
const isAlligator = Boolean(contracts?.alligator);
const proposalVotes = await fetchProposalVotes(proposal.id);
+ const nonVoters = await fetchVotersWhoHaveNotVotedForProposal(proposal.id);
return (
@@ -30,6 +34,7 @@ export default async function StandardProposalPage({
diff --git a/src/components/Proposals/ProposalStatus/ProposalStatusDetail.tsx b/src/components/Proposals/ProposalStatus/ProposalStatusDetail.tsx
index a350ca03e..7b14fc965 100644
--- a/src/components/Proposals/ProposalStatus/ProposalStatusDetail.tsx
+++ b/src/components/Proposals/ProposalStatus/ProposalStatusDetail.tsx
@@ -1,4 +1,3 @@
-import { HStack } from "@/components/Layout/Stack";
import ProposalTimeStatus from "@/components/Proposals/Proposal/ProposalTimeStatus";
import { type ProposalStatus } from "@/lib/proposalUtils";
import { ArrowTopRightOnSquareIcon } from "@heroicons/react/20/solid";
@@ -18,11 +17,7 @@ export default function ProposalStatusDetail({
cancelledTransactionHash: string | null;
}) {
return (
-
+
{proposalStatus === "ACTIVE" && (
@@ -75,6 +70,6 @@ export default function ProposalStatusDetail({
proposalCancelledTime={proposalCancelledTime}
/>
-
+
);
}
diff --git a/src/components/Votes/ApprovalProposalVotesList/ApprovalProposalSingleVote.tsx b/src/components/Votes/ApprovalProposalVotesList/ApprovalProposalSingleVote.tsx
index 2065dd32e..9f1803156 100644
--- a/src/components/Votes/ApprovalProposalVotesList/ApprovalProposalSingleVote.tsx
+++ b/src/components/Votes/ApprovalProposalVotesList/ApprovalProposalSingleVote.tsx
@@ -54,7 +54,7 @@ export default function ApprovalProposalSingleVote({ vote }: { vote: Vote }) {
className="mb-2 text-xs leading-4"
>
-
+
{address?.toLowerCase() === voterAddress && (
(you)
diff --git a/src/components/Votes/ProposalVotesList/ProposalNonVoterList.tsx b/src/components/Votes/ProposalVotesList/ProposalNonVoterList.tsx
new file mode 100644
index 000000000..0fa265f26
--- /dev/null
+++ b/src/components/Votes/ProposalVotesList/ProposalNonVoterList.tsx
@@ -0,0 +1,61 @@
+"use client";
+
+import { useRef, useState, useCallback } from "react";
+import { PaginatedResult } from "@/app/lib/pagination";
+import { fetchVotersWhoHaveNotVotedForProposal } from "@/app/proposals/actions";
+import InfiniteScroll from "react-infinite-scroller";
+import { ProposalSingleNonVoter } from "./ProposalSingleNonVoter";
+
+const ProposalNonVoterList = ({
+ proposalId,
+ initialNonVoters,
+}: {
+ proposalId: string;
+ initialNonVoters: PaginatedResult
; // TODO: add better types
+}) => {
+ const fetching = useRef(false);
+ const [pages, setPages] = useState([initialNonVoters]);
+ const [meta, setMeta] = useState(initialNonVoters.meta);
+
+ const loadMore = useCallback(async () => {
+ if (!fetching.current && meta.has_next) {
+ fetching.current = true;
+ const data = await fetchVotersWhoHaveNotVotedForProposal(proposalId, {
+ limit: 20,
+ offset: meta.next_offset,
+ });
+ setPages((prev) => [...prev, { ...data, votes: data.data }]);
+ setMeta(data.meta);
+ fetching.current = false;
+ }
+ }, [proposalId, meta]);
+
+ const voters = pages.flatMap((page) => page.data);
+
+ return (
+
+
+ Loading more voters...
+
+ }
+ element="main"
+ >
+
+ {voters.map((voter) => (
+
+
+
+ ))}
+
+
+
+ );
+};
+
+export default ProposalNonVoterList;
diff --git a/src/components/Votes/ProposalVotesList/ProposalSingleNonVoter.tsx b/src/components/Votes/ProposalVotesList/ProposalSingleNonVoter.tsx
new file mode 100644
index 000000000..f3003826e
--- /dev/null
+++ b/src/components/Votes/ProposalVotesList/ProposalSingleNonVoter.tsx
@@ -0,0 +1,104 @@
+import { VStack, HStack } from "@/components/Layout/Stack";
+import HumanAddress from "@/components/shared/HumanAddress";
+import TokenAmountDisplay from "@/components/shared/TokenAmountDisplay";
+import ENSAvatar from "@/components/shared/ENSAvatar";
+import { useEnsName, useAccount } from "wagmi";
+import discordIcon from "@/icons/discord.svg";
+import xIcon from "@/icons/x.svg";
+import warpcastIcon from "@/icons/warpcast.svg";
+import Image from "next/image";
+import { toast } from "react-hot-toast";
+
+export function ProposalSingleNonVoter({
+ voter,
+}: {
+ voter: {
+ delegate: string;
+ direct_vp: string;
+ twitter: string | null;
+ discord: string | null;
+ warpcast: string | null;
+ };
+}) {
+ const { address: connectedAddress } = useAccount();
+ const { data } = useEnsName({
+ chainId: 1,
+ address: voter.delegate as `0x${string}`,
+ });
+
+ console.log(voter);
+
+ return (
+
+
+
+
+
+ {voter.delegate === connectedAddress?.toLowerCase() && (you)
}
+ {voter.twitter && (
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ window &&
+ window.open(`https://twitter.com/${voter.twitter}`, "_blank");
+ }}
+ >
+
+
+ )}
+ {voter.discord && (
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ toast("copied discord handle to clipboard");
+ navigator.clipboard.writeText(voter.discord ?? "");
+ }}
+ >
+
+
+ )}
+ {voter.warpcast && (
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ window &&
+ window.open(
+ `https://warpcast.com/${voter.warpcast?.replace(/@/g, "")}`,
+ "_blank"
+ );
+ }}
+ >
+
+
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/shared/TokenAmountDisplay.tsx b/src/components/shared/TokenAmountDisplay.tsx
index f341d607c..908cb25ba 100644
--- a/src/components/shared/TokenAmountDisplay.tsx
+++ b/src/components/shared/TokenAmountDisplay.tsx
@@ -1,5 +1,4 @@
import { formatNumber } from "@/lib/utils";
-import { BigNumberish } from "ethers";
import React, { useMemo } from "react";
import Tenant from "@/lib/tenant/tenant";
const { token } = Tenant.current();
@@ -10,7 +9,7 @@ export default function TokenAmountDisplay({
currency = token.symbol,
maximumSignificantDigits = 2,
}: {
- amount: BigNumberish;
+ amount: string | bigint;
decimals?: number;
currency?: string;
maximumSignificantDigits?: number;
diff --git a/src/hooks/useGetDelegatee.ts b/src/hooks/useGetDelegatee.ts
new file mode 100644
index 000000000..2b4b3d92d
--- /dev/null
+++ b/src/hooks/useGetDelegatee.ts
@@ -0,0 +1,21 @@
+import { useQuery } from "@tanstack/react-query";
+import { fetchDirectDelegatee } from "@/app/delegates/actions";
+
+export const DELEGATEE_QK = "delegatee";
+
+export const useGetDelegatee = ({
+ address,
+}: {
+ address: `0x${string}` | undefined;
+}) => {
+ const { data, isFetching, isFetched } = useQuery({
+ enabled: !!address,
+ queryKey: [DELEGATEE_QK, address],
+ queryFn: async () => {
+ const delegatee = await fetchDirectDelegatee(address as `0x${string}`);
+ return delegatee;
+ },
+ });
+
+ return { data, isFetching, isFetched };
+};
diff --git a/src/hooks/useVotingStats.tsx b/src/hooks/useVotingStats.tsx
new file mode 100644
index 000000000..5e624a00d
--- /dev/null
+++ b/src/hooks/useVotingStats.tsx
@@ -0,0 +1,9 @@
+import { useQuery } from "@tanstack/react-query";
+import { fetchVoterStats } from "@/app/delegates/actions";
+
+export const useVotingStats = ({ address }: { address: `0x${string}` }) => {
+ return useQuery({
+ queryKey: ["voting-stats", address],
+ queryFn: () => fetchVoterStats(address),
+ });
+};
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index 837a5c748..4c62c44e9 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -58,6 +58,7 @@ export const delegatesFilterOptions = {
value: "Most delegators",
},
};
+
export const citizensFilterOptions = {
mostVotingPower: {
value: "Most voting power",
diff --git a/src/lib/tenant/configs/ui/uniswap.ts b/src/lib/tenant/configs/ui/uniswap.ts
index b08a55af1..23f3d7ccf 100644
--- a/src/lib/tenant/configs/ui/uniswap.ts
+++ b/src/lib/tenant/configs/ui/uniswap.ts
@@ -9,8 +9,6 @@ import infoPageCard02 from "@/assets/tenant/uniswap_info_2.png";
import infoPageCard03 from "@/assets/tenant/uniswap_info_3.png";
import infoPageCard04 from "@/assets/tenant/uniswap_info_4.png";
import infoPageHero from "@/assets/tenant/uniswap_info_hero.png";
-import { ProposalStage as PrismaProposalStage } from "@prisma/client";
-import { ProposalGatingType, ProposalType } from "@/app/proposals/draft/types";
export const uniswapTenantUIConfig = new TenantUI({
title: "Uniswap Agora",
@@ -200,6 +198,10 @@ export const uniswapTenantUIConfig = new TenantUI({
name: "delegates/edit",
enabled: true,
},
+ {
+ name: "delegates/my-delegates-filter",
+ enabled: true,
+ },
{
name: "proposals",
enabled: true,
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 0e182ea31..a100033b8 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -1,5 +1,4 @@
import { type ClassValue, clsx } from "clsx";
-import { BigNumberish, formatUnits } from "ethers";
import { twMerge } from "tailwind-merge";
import { useMemo } from "react";
import Tenant from "./tenant/tenant";
@@ -159,11 +158,31 @@ export function numberToToken(number: number) {
}
export function formatNumber(
- amount: string | BigNumberish,
+ amount: string | bigint,
decimals: number,
maximumSignificantDigits = 4
) {
- const standardUnitAmount = Number(formatUnits(amount, decimals));
+ let bigIntAmount: bigint;
+
+ if (typeof amount === "string") {
+ // Handle potential scientific notation
+ if (amount.includes("e")) {
+ bigIntAmount = scientificNotationToPrecision(amount);
+ } else {
+ bigIntAmount = BigInt(amount);
+ }
+ } else {
+ bigIntAmount = amount;
+ }
+
+ // Convert to standard unit
+ const divisor = BigInt(10) ** BigInt(decimals);
+ const wholePart = bigIntAmount / divisor;
+ const fractionalPart = bigIntAmount % divisor;
+
+ // Convert to number for formatting
+ const standardUnitAmount =
+ Number(wholePart) + Number(fractionalPart) / Number(divisor);
const numberFormat = new Intl.NumberFormat("en", {
notation: "compact",
@@ -179,7 +198,7 @@ export function TokenAmountDisplay({
currency = token.symbol,
maximumSignificantDigits = 2,
}: {
- amount: string | BigNumberish;
+ amount: string | bigint;
decimals?: number;
currency?: string;
maximumSignificantDigits?: number;