diff --git a/src/app/governance/page.tsx b/src/app/governance/page.tsx index 04ea064..4ff4b79 100644 --- a/src/app/governance/page.tsx +++ b/src/app/governance/page.tsx @@ -10,6 +10,7 @@ import { Section } from 'src/components/layout/Section'; import { DropdownModal } from 'src/components/menus/Dropdown'; import { H1 } from 'src/components/text/headers'; import { links } from 'src/config/links'; +import { ProposalCard } from 'src/features/governance/ProposalCard'; import { ProposalStage } from 'src/features/governance/contractTypes'; import { MergedProposalData, @@ -83,22 +84,28 @@ function ProposalList() {
{filteredProposals ? ( -
+ <> - {filteredProposals.length ? ( - filteredProposals.map((data, i) => ) - ) : ( -
-

No proposals found

-
- )} -
+
+ {filteredProposals.length ? ( + filteredProposals.map((data, i) => ( +
+ +
+ )) + ) : ( +
+

No proposals found

+
+ )} +
+ ) : (
Loading governance data @@ -108,22 +115,6 @@ function ProposalList() { ); } -function ProposalCard({ data }: { data: MergedProposalData }) { - const { stage, proposal, metadata } = data; - - const title = - metadata?.title || metadata?.cgp ? `Proposal CGP-${metadata.cgp}` : `Proposal #${proposal?.id}`; - - return ( -
-
-

{title}

-
{stage}
-
-
- ); -} - function CtaModal() { return (
@@ -182,11 +173,11 @@ function useFilteredProposals({ .filter( (p) => !query || - p.proposal?.proposer.includes(query) || - p.proposal?.url.includes(query) || - p.metadata?.title.includes(query) || - p.metadata?.author.includes(query) || - p.metadata?.url?.includes(query), + p.proposal?.proposer.toLowerCase().includes(query) || + p.proposal?.url.toLowerCase().includes(query) || + p.metadata?.title.toLowerCase().includes(query) || + p.metadata?.author.toLowerCase().includes(query) || + p.metadata?.url?.toLowerCase().includes(query), ); }, [proposals, filter, searchQuery]); } diff --git a/src/config/consts.ts b/src/config/consts.ts index 20c7b1b..ee70f9b 100644 --- a/src/config/consts.ts +++ b/src/config/consts.ts @@ -1,13 +1,20 @@ export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; export const DEFAULT_DISPLAY_DECIMALS = 2; export const DEFAULT_TOKEN_DECIMALS = 18; -export const MIN_REMAINING_BALANCE = 10000000000000000n; // 0.01 CELO export const AVG_BLOCK_TIMES_MS = 5_000; // 5 seconds export const EPOCH_DURATION_MS = 86_400_000; // 1 day export const BALANCE_REFRESH_INTERVAL = 5_000; // 5 seconds + +// Locking +export const MIN_REMAINING_BALANCE = 10000000000000000n; // 0.01 CELO + +// Staking export const MIN_GROUP_SCORE_FOR_RANDOM = 90; export const MIN_INCREMENTAL_VOTE_AMOUNT = 10000000000000000n; // 0.01 CELO - -// From the Election contract electableValidators config export const MAX_NUM_ELECTABLE_VALIDATORS = 110; export const MAX_NUM_GROUPS_VOTED_FOR = 10; + +// Governance +export const QUEUED_STAGE_EXPIRY_TIME = 2_419_200_000; // 4 weeks +export const APPROVAL_STAGE_EXPIRY_TIME = 86_400_000; // 1 day +export const EXECUTION_STAGE_EXPIRY_TIME = 259_200_000; // 3 days diff --git a/src/features/governance/ProposalCard.tsx b/src/features/governance/ProposalCard.tsx new file mode 100644 index 0000000..19d06ee --- /dev/null +++ b/src/features/governance/ProposalCard.tsx @@ -0,0 +1,49 @@ +import Image from 'next/image'; +import { StageBadge } from 'src/features/governance/StageBadge'; +import { MergedProposalData } from 'src/features/governance/useGovernanceProposals'; +import ClockIcon from 'src/images/icons/clock.svg'; +import { trimToLength } from 'src/utils/strings'; +import { getHumanReadableTimeString } from 'src/utils/time'; + +export function ProposalCard({ data }: { data: MergedProposalData }) { + const { stage, proposal, metadata } = data; + + const { id, timestamp, expiryTimestamp, votes } = proposal || {}; + const { title, timestamp: cgpTimestamp, timestampExecuted, cgp } = metadata || {}; + + const idValue = id ? `# ${id}` : cgp ? `CGP ${cgp}` : undefined; + const titleValue = title ? trimToLength(title, 50) : undefined; + const proposedTimestamp = timestamp || cgpTimestamp; + const proposedTimeValue = proposedTimestamp + ? new Date(proposedTimestamp).toLocaleDateString() + : undefined; + const endTimestamp = timestampExecuted || expiryTimestamp; + const endTimeValue = endTimestamp ? getHumanReadableTimeString(endTimestamp) : undefined; + const endTimeLabel = timestampExecuted ? 'Executed' : 'Expires'; + + return ( +
+
+ {idValue && ( +
+ {idValue} +
+ )} + + {proposedTimeValue && ( +
{`Proposed ${proposedTimeValue}`}
+ )} +
+ {titleValue &&

{titleValue}

} + {votes && ( +
{`Votes: ${votes.yes} Yes, ${votes.no} No, ${votes.abstain} Abstain`}
+ )} + {endTimeValue && ( +
+ +
{`${endTimeLabel} ${endTimeValue}`}
+
+ )} +
+ ); +} diff --git a/src/features/governance/StageBadge.tsx b/src/features/governance/StageBadge.tsx new file mode 100644 index 0000000..8770960 --- /dev/null +++ b/src/features/governance/StageBadge.tsx @@ -0,0 +1,14 @@ +import clsx from 'clsx'; +import { ProposalStage, ProposalStageToStyle } from 'src/features/governance/contractTypes'; + +export function StageBadge({ stage, className }: { stage: ProposalStage; className?: string }) { + const { color, label } = ProposalStageToStyle[stage]; + return ( +
+ {label} +
+ ); +} diff --git a/src/features/governance/contractTypes.ts b/src/features/governance/contractTypes.ts index ae37387..72800b2 100644 --- a/src/features/governance/contractTypes.ts +++ b/src/features/governance/contractTypes.ts @@ -1,3 +1,5 @@ +import { Color } from 'src/styles/Color'; + export enum VoteValue { None = 'none', Abstain = 'abstain', @@ -22,6 +24,18 @@ export enum ProposalStage { Rejected = 8, } +export const ProposalStageToStyle: Record = { + [ProposalStage.None]: { color: Color.Sky, label: 'Draft' }, + [ProposalStage.Queued]: { color: Color.Lavender, label: 'Upvoting' }, + [ProposalStage.Approval]: { color: Color.Lavender, label: 'Approval' }, + [ProposalStage.Referendum]: { color: Color.Jade, label: 'Voting' }, + [ProposalStage.Execution]: { color: Color.Jade, label: 'Passed' }, + [ProposalStage.Expiration]: { color: Color.Red, label: 'Expired' }, + [ProposalStage.Executed]: { color: Color.Jade, label: 'Executed' }, + [ProposalStage.Withdrawn]: { color: Color.Red, label: 'Withdrawn' }, + [ProposalStage.Rejected]: { color: Color.Red, label: 'Rejected' }, +}; + export const FAILED_PROPOSAL_STAGES = [ ProposalStage.Expiration, ProposalStage.Rejected, @@ -31,6 +45,7 @@ export const FAILED_PROPOSAL_STAGES = [ export interface Proposal { id: number; timestamp: number; + expiryTimestamp?: number; url: string; proposer: Address; deposit: bigint; diff --git a/src/features/governance/useGovernanceProposals.ts b/src/features/governance/useGovernanceProposals.ts index a839e58..11e8976 100644 --- a/src/features/governance/useGovernanceProposals.ts +++ b/src/features/governance/useGovernanceProposals.ts @@ -142,6 +142,11 @@ async function fetchGovernanceProposals(publicClient: PublicClient): Promise + + + diff --git a/src/styles/Color.ts b/src/styles/Color.ts index c97ab09..cb9e2dc 100644 --- a/src/styles/Color.ts +++ b/src/styles/Color.ts @@ -7,6 +7,7 @@ export enum Color { // Accents Yellow = '#FCFF52', + Red = '#F68098', Gypsum = '#FCF6F1', Sand = '#E7E3D4', Wood = '#655947',