Skip to content

Commit

Permalink
Andrei/agora 2296 endorsed delegates (#407)
Browse files Browse the repository at this point in the history
* Merge

* Filtering endorsed delegates

* Misc

* Removed endorsed filter toggle

* build

* Code review comments

* Hotfix

---------

Co-authored-by: Andrei Taraschuk <andrei@Andreis-MacBook-Pro.local>
  • Loading branch information
andreitr and Andrei Taraschuk authored Jul 19, 2024
1 parent 8a2eddd commit 3173be7
Show file tree
Hide file tree
Showing 13 changed files with 227 additions and 110 deletions.
9 changes: 5 additions & 4 deletions src/app/api/common/delegates/delegate.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,14 @@ export type DelegatesGetPayload = {
};

type DelegateStatement = {
signature: string;
created_at: Date;
discord: string | null;
endorsed: boolean;
payload: { delegateStatement: string };
signature: string;
twitter: string | null;
discord: string | null;
warpcast: string | null;
created_at: Date;
updated_at: Date;
warpcast: string | null;
};

export type DelegateStats = {
Expand Down
68 changes: 40 additions & 28 deletions src/app/api/common/delegates/getDelegates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,17 @@ async function getDelegates({
filters?: {
issues?: string;
stakeholders?: string;
endorsed?: boolean;
};
}): Promise<PaginatedResultEx<DelegateChunk[]>> {
const { namespace, ui, slug, contracts } = Tenant.current();

const allowList = ui.delegates?.allowed || [];

const endorsedFilterQuery = filters?.endorsed ? `AND endorsed = true` : "";

const topIssuesParam = filters?.issues || "";
const topIssuesQuery =
const topIssuesFilterQuery =
topIssuesParam && topIssuesParam !== ""
? `
AND jsonb_array_length(s.payload -> 'topIssues') > 0
Expand All @@ -62,10 +65,10 @@ async function getDelegates({
`
: "";

// TODO 1/2: There is an inconsistency between top stakeholders and top issues. Top issues are filtered by a value
// TODO 2/2 : where the top stakeholders are filtered on type. We need to make this consistent and clean up the data and UI.
// Note: There is an inconsistency between top stakeholders and top issues. Top issues are filtered by a value
// where the top stakeholders are filtered on type. We need to make this consistent and clean up the data and UI.
const topStakeholdersParam = filters?.stakeholders || "";
const topStakeholdersQuery =
const topStakeholdersFilterQuery =
topStakeholdersParam && topStakeholdersParam !== ""
? `
AND jsonb_array_length(s.payload -> 'topStakeholders') > 0
Expand Down Expand Up @@ -102,11 +105,13 @@ async function getDelegates({
discord,
created_at,
updated_at,
warpcast
warpcast,
endorsed
FROM agora.delegate_statements s
WHERE s.address = d.delegate AND s.dao_slug = $2::config.dao_slug
${topIssuesQuery}
${topStakeholdersQuery}
${endorsedFilterQuery}
${topIssuesFilterQuery}
${topStakeholdersFilterQuery}
LIMIT 1
) sub
) AS statement
Expand All @@ -118,16 +123,17 @@ async function getDelegates({
SELECT 1
FROM agora.delegate_statements s
WHERE s.address = d.delegate
${topIssuesQuery}
${topStakeholdersQuery}
${endorsedFilterQuery}
${topIssuesFilterQuery}
${topStakeholdersFilterQuery}
)
ORDER BY num_of_delegators DESC
OFFSET $4
LIMIT $5;
`,
allowList,
slug,
contracts.token.address.toLowerCase(),
contracts.token.address,
skip,
take
);
Expand All @@ -154,33 +160,35 @@ async function getDelegates({
discord,
created_at,
updated_at,
warpcast
warpcast,
endorsed
FROM agora.delegate_statements s
WHERE s.address = d.delegate AND s.dao_slug = $3::config.dao_slug
${topIssuesQuery}
${topStakeholdersQuery}
${endorsedFilterQuery}
${topIssuesFilterQuery}
${topStakeholdersFilterQuery}
LIMIT 1
) sub
) AS statement
FROM ${namespace + ".delegates"} d
WHERE voting_power > 0
AND (ARRAY_LENGTH($2::text[], 1) IS NULL OR delegate = ANY($2::text[]))
WHERE (ARRAY_LENGTH($2::text[], 1) IS NULL OR delegate = ANY($2::text[]))
AND d.contract = $4
AND EXISTS (
SELECT 1
FROM agora.delegate_statements s
WHERE s.address = d.delegate
${topIssuesQuery}
${topStakeholdersQuery}
${endorsedFilterQuery}
${topIssuesFilterQuery}
${topStakeholdersFilterQuery}
)
ORDER BY -log(random()) / voting_power
ORDER BY -log(random()) / NULLIF(voting_power, 0)
OFFSET $5
LIMIT $6;
`,
seed,
allowList,
slug,
contracts.token.address.toLowerCase(),
contracts.token.address,
skip,
take
);
Expand All @@ -206,11 +214,13 @@ async function getDelegates({
discord,
created_at,
updated_at,
warpcast
warpcast,
endorsed
FROM agora.delegate_statements s
WHERE s.address = d.delegate AND s.dao_slug = $2::config.dao_slug
${topIssuesQuery}
${topStakeholdersQuery}
${endorsedFilterQuery}
${topIssuesFilterQuery}
${topStakeholdersFilterQuery}
LIMIT 1
) sub
) AS statement
Expand All @@ -221,16 +231,17 @@ async function getDelegates({
SELECT 1
FROM agora.delegate_statements s
WHERE s.address = d.delegate
${topIssuesQuery}
${topStakeholdersQuery}
${endorsedFilterQuery}
${topIssuesFilterQuery}
${topStakeholdersFilterQuery}
)
ORDER BY voting_power DESC
OFFSET $4
LIMIT $5;
`,
allowList,
slug,
contracts.token.address.toLowerCase(),
contracts.token.address,
skip,
take
);
Expand Down Expand Up @@ -319,7 +330,8 @@ async function getDelegate(addressOrENSName: string): Promise<Delegate> {
discord,
created_at,
updated_at,
warpcast
warpcast,
endorsed
FROM agora.delegate_statements s
WHERE s.address = LOWER($1) AND s.dao_slug = $3::config.dao_slug
LIMIT 1
Expand All @@ -329,8 +341,8 @@ async function getDelegate(addressOrENSName: string): Promise<Delegate> {
address,
contracts.alligator?.address || "",
slug,
contracts.governor.address.toLowerCase(),
contracts.token.address.toLowerCase()
contracts.governor.address,
contracts.token.address
);

const [delegate, votableSupply, quorum] = await Promise.all([
Expand Down
78 changes: 78 additions & 0 deletions src/app/delegates/components/EndorsedFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"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";

const FILTER_PARAM = "endorsedFilter";
const DEFAULT_FILTER = "all";

export default function EndorsedFilter() {
const router = useRouter();
const searchParams = useSearchParams();
const addSearchParam = useAddSearchParam();
const deleteSearchParam = useDeleteSearchParam();
const filterParam = searchParams?.get(FILTER_PARAM) || "all";
const { setIsDelegatesFiltering } = useAgoraContext();

let endorsedFilterOptions: any = {
all: {
value: "All Delegates",
sort: "all",
},
true: {
value: "Endorsed Delegates",
sort: "true",
},
};

const handleChange = (value: string) => {
setIsDelegatesFiltering(true);
router.push(
value === DEFAULT_FILTER
? deleteSearchParam({ name: FILTER_PARAM })
: addSearchParam({ name: FILTER_PARAM, value }),
{ scroll: false }
);
};

return (
<Listbox
as="div"
value={filterParam || endorsedFilterOptions.all.value}
onChange={(value) => handleChange(value)}
>
{() => (
<>
<Listbox.Button className="w-full sm:w-fit bg-[#F7F7F7] text-base font-medium border-none rounded-full py-2 px-4 flex items-center">
{endorsedFilterOptions[filterParam]?.value ||
endorsedFilterOptions.all.value}
<ChevronDown className="h-4 w-4 ml-[2px] opacity-30 hover:opacity-100" />
</Listbox.Button>
<Listbox.Options className="mt-3 absolute bg-[#F7F7F7] border border-[#ebebeb] p-2 rounded-2xl flex flex-col gap-1 z-20 w-max">
{Object.keys(endorsedFilterOptions).map((key) => (
<Listbox.Option key={key} value={key} as={Fragment}>
{({ selected }) => {
return (
<li
className={`cursor-pointer text-base py-2 px-3 border rounded-xl font-medium ${
selected
? "text-black bg-white border-[#ebebeb]"
: "text-[#66676b] border-transparent"
}`}
>
{endorsedFilterOptions[key].value}
</li>
);
}}
</Listbox.Option>
))}
</Listbox.Options>
</>
)}
</Listbox>
);
}
3 changes: 3 additions & 0 deletions src/app/delegates/page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ export default async function Page({ searchParams }) {
...(searchParams.stakeholderFilter && {
stakeholders: searchParams.stakeholderFilter,
}),
...(searchParams.endorsedFilter && {
endorsed: searchParams.endorsedFilter,
}),
};

const tab = searchParams.tab;
Expand Down
1 change: 1 addition & 0 deletions src/app/staking/components/delegates/DelegateCardList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export default function DelegateCardList({
const twitter = delegate?.statement?.twitter;
const discord = delegate?.statement?.discord;
const warpcast = delegate?.statement?.warpcast;
const endorsed = delegate?.statement?.endorsed;

if (delegate?.statement?.payload) {
const delegateStatement = (
Expand Down
36 changes: 11 additions & 25 deletions src/components/Delegates/DelegateCard/DelegateCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { HStack, VStack } from "@/components/Layout/Stack";
import { bpsToString, pluralizeAddresses } from "@/lib/utils";
import { DelegateProfileImage } from "./DelegateProfileImage";
import styles from "./delegateCard.module.scss";
Expand All @@ -7,19 +6,20 @@ import { Delegate } from "@/app/api/common/delegates/delegate";

export default function DelegateCard({ delegate }: { delegate: Delegate }) {
return (
<VStack className={styles.container}>
<VStack className={styles.card}>
<VStack alignItems="items-stretch" className={styles.image}>
<div className={`flex flex-col ${styles.container}`}>
<div className={`flex flex-col ${styles.card}`}>
<div className={`flex flex-col items-stretch ${styles.image}`}>
<DelegateProfileImage
endorsed={delegate.statement?.endorsed}
address={delegate.address}
citizen={delegate.citizen}
votingPower={delegate.votingPower.total}
copyable={true}
/>
</VStack>
</div>

<div className={styles.content}>
<VStack gap={4}>
<div className="flex flex-col gap-4">
<PanelRow
title="Proposals Voted"
detail={
Expand All @@ -34,20 +34,6 @@ export default function DelegateCard({ delegate }: { delegate: Delegate }) {
title="For / Against / Abstain"
detail={`${delegate.votedFor} / ${delegate.votedAgainst} / ${delegate.votedAbstain}`}
/>
{/* <PanelRow
title="Vote Power"
detail={
<>
{bpsToString(
delegate.votingPowerRelativeToVotableSupply * 100
)}{" "}
votable supply
<br />
{bpsToString(delegate.votingPowerRelativeToQuorum * 100)}{" "}
quorum
</>
}
/> */}
<PanelRow
title="Recent activity"
detail={
Expand All @@ -65,10 +51,10 @@ export default function DelegateCard({ delegate }: { delegate: Delegate }) {
detail={pluralizeAddresses(Number(delegate.numOfDelegators))}
/>
<DelegateCardClient delegate={delegate} />
</VStack>
</div>
</div>
</VStack>
</VStack>
</div>
</div>
);
}

Expand All @@ -80,10 +66,10 @@ export const PanelRow = ({
detail: string | JSX.Element;
}) => {
return (
<HStack gap={2} className="justify-between items-center">
<div className="flex flex-row gap-2 justify-between items-center">
<span className="whitespace-nowrap">{title}</span>

<span className={styles.row}>{detail}</span>
</HStack>
</div>
);
};
Loading

0 comments on commit 3173be7

Please sign in to comment.