Skip to content

Commit

Permalink
Start implementing governance page
Browse files Browse the repository at this point in the history
Create shared TabHeaderFilters component
Tweak some logo SVGs
  • Loading branch information
jmrossy committed Feb 10, 2024
1 parent d065b47 commit 0ba946e
Show file tree
Hide file tree
Showing 18 changed files with 316 additions and 84 deletions.
Binary file added public/backgrounds/diamond-texture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
172 changes: 171 additions & 1 deletion src/app/governance/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,177 @@
'use client';

import Image from 'next/image';
import { useMemo, useState } from 'react';
import { SpinnerWithLabel } from 'src/components/animation/Spinner';
import { ExternalLink } from 'src/components/buttons/ExternalLink';
import { TabHeaderFilters } from 'src/components/buttons/TabHeaderButton';
import { SearchField } from 'src/components/input/SearchField';
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 { GovernanceProposal, ProposalStage } from 'src/features/governance/types';
import { useGovernanceProposals } from 'src/features/governance/useGovernanceProposals';
import BookIcon from 'src/images/icons/book.svg';
import EllipsisIcon from 'src/images/icons/ellipsis.svg';
import CeloIcon from 'src/images/logos/celo.svg';
import DiscordIcon from 'src/images/logos/discord.svg';
import { useIsMobile } from 'src/styles/mediaQueries';

enum Filter {
All = 'All',
Upvoting = 'Upvoting',
Voting = 'Voting',
Drafts = 'Drafts',
History = 'History',
}

export default function Page() {
return <Section containerClassName="space-y-8">TODO</Section>;
return (
<>
<Section className="mt-4">
<ProposalList />
</Section>
<div className="absolute bottom-12 right-5 hidden md:block">
<CtaModal />
</div>
</>
);
}

function ProposalList() {
const isMobile = useIsMobile();

const { proposals } = useGovernanceProposals();

const [searchQuery, setSearchQuery] = useState<string>('');
const [filter, setFilter] = useState<Filter>(Filter.All);

const filteredProposals = useFilteredProposals({ proposals, filter, searchQuery });

const headerCounts = useMemo<Record<Filter, number>>(() => {
const _proposals = proposals || [];
return {
[Filter.All]: _proposals?.length || 0,
[Filter.Upvoting]: _proposals.filter((p) => p.stage === ProposalStage.Queued).length,
[Filter.Voting]: _proposals.filter((p) => p.stage === ProposalStage.Referendum).length,
[Filter.Drafts]: _proposals.filter((p) => p.stage === ProposalStage.None).length,
[Filter.History]: _proposals.filter((p) => p.id === 'TODO').length,
};
}, [proposals]);

return (
<div className="space-y-6">
<div className="flex flex-col items-stretch gap-3 md:flex-row md:items-center md:justify-between">
<div className="flex items-center justify-between gap-12">
<H1>Proposals</H1>
<DropdownModal
buttonClasses="md:hidden"
button={() => <Image src={EllipsisIcon} width={24} height={24} alt="..." />}
modal={() => <CtaModal />}
/>
</div>
<SearchField
value={searchQuery}
setValue={setSearchQuery}
placeholder="Search proposals"
className="w-full text-sm md:w-64"
/>
</div>
<div></div>
{filteredProposals ? (
<div className="divide-y divide-taupe-300">
<TabHeaderFilters
activeFilter={filter}
setFilter={setFilter}
counts={headerCounts}
showCount={!isMobile}
className="pb-2 all:space-x-4 md:space-x-6"
/>
{filteredProposals.length ? (
filteredProposals.map((proposal) => <Proposal key={proposal.id} proposal={proposal} />)
) : (
<div className="flex justify-center py-10">
<p className="text-center text-taupe-600">No proposals found</p>
</div>
)}
{filteredProposals.map((proposal) => (
<Proposal key={proposal.id} proposal={proposal} />
))}
</div>
) : (
<div className="flex justify-center py-10">
<SpinnerWithLabel>Loading governance data</SpinnerWithLabel>
</div>
)}
</div>
);
}

function Proposal({ proposal }: { proposal: GovernanceProposal }) {
return (
<div className="flex justify-between">
<div>
<h3 className="font-serif text-lg">{'TODO'}</h3>
<p className="text-gray-600">{proposal.id}</p>
</div>
</div>
);
}

function CtaModal() {
return (
<div className="bg-diamond-texture flex w-fit flex-col space-y-2 border border-taupe-300 bg-taupe-100 bg-right-bottom py-2.5 pl-4 pr-8 md:pr-14">
<h2 className="font-serif text-xl">Get Involved</h2>
<ExternalLink
href={links.docs}
className="flex items-center space-x-2 text-sm font-medium hover:underline"
>
<div className="w-4">
<Image src={BookIcon} alt="" />
</div>
<span>Explore the docs</span>
</ExternalLink>
<ExternalLink
href={links.discord}
className="flex items-center space-x-2 text-sm font-medium hover:underline"
>
<div className="w-4">
<Image src={DiscordIcon} alt="" />
</div>
<span>Join the chat</span>
</ExternalLink>
<ExternalLink
href={links.forum}
className="flex items-center space-x-2 text-sm font-medium hover:underline"
>
<div className="w-4 p-px">
<Image src={CeloIcon} alt="" />
</div>
<span>Join the forum</span>
</ExternalLink>
</div>
);
}

function useFilteredProposals({
proposals,
filter,
searchQuery,
}: {
proposals?: GovernanceProposal[];
filter: Filter;
searchQuery: string;
}) {
return useMemo<GovernanceProposal[] | undefined>(() => {
if (!proposals) return undefined;
const _query = searchQuery.trim().toLowerCase();
return proposals
.filter((p) => {
//TODO
return !!p && !filter;
})
.filter((p) => !p || 'TODO')
.sort((a, b) => (b.votes > a.votes ? 1 : -1));
}, [proposals, filter, searchQuery]);
}
19 changes: 9 additions & 10 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SpinnerWithLabel } from 'src/components/animation/Spinner';
import { SolidButton } from 'src/components/buttons/SolidButton';
import { Section } from 'src/components/layout/Section';
import { StatBox } from 'src/components/layout/StatBox';
import { H1 } from 'src/components/text/headers';
import { useTransactionModal } from 'src/features/transactions/TransactionModal';
import { TxModalType } from 'src/features/transactions/types';
import { ValidatorGroupTable } from 'src/features/validators/ValidatorGroupTable';
Expand All @@ -13,18 +14,16 @@ import { useValidatorGroups } from 'src/features/validators/useValidatorGroups';
import { bigIntMin } from 'src/utils/math';
import { objLength } from 'src/utils/objects';

export default function Index() {
export default function Page() {
const { groups, totalVotes } = useValidatorGroups();

return (
<>
<Section className="mt-4">
<div className="space-y-6">
<HeroSection totalVotes={totalVotes} groups={groups} />
<TableSection totalVotes={totalVotes} groups={groups} />
</div>
</Section>
</>
<Section className="mt-4">
<div className="space-y-6">
<HeroSection totalVotes={totalVotes} groups={groups} />
<TableSection totalVotes={totalVotes} groups={groups} />
</div>
</Section>
);
}

Expand All @@ -50,7 +49,7 @@ function HeroSection({ totalVotes, groups }: { totalVotes?: bigint; groups?: Val
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h1 className="font-serif text-3xl sm:text-4xl">Discover Validators</h1>
<H1>Discover Validators</H1>
<SolidButton onClick={() => showStakeModal()} className="px-8">
Stake
</SolidButton>
Expand Down
8 changes: 3 additions & 5 deletions src/app/staking/[address]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,9 @@ export default function Page({ params: { address } }: { params: { address: Addre

return (
<Section containerClassName="space-y-8 mt-4">
<>
<HeaderSection group={group} />
<StatSection group={group} />
<DetailsSection group={group} />
</>
<HeaderSection group={group} />
<StatSection group={group} />
<DetailsSection group={group} />
</Section>
);
}
Expand Down
32 changes: 31 additions & 1 deletion src/components/buttons/TabHeaderButton.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import clsx from 'clsx';
import { PropsWithChildren, useState } from 'react';
import { objKeys } from 'src/utils/objects';
import { isNullish } from 'src/utils/typeof';

export function TabHeaderButton({
Expand All @@ -20,7 +21,7 @@ export function TabHeaderButton({
{!isNullish(count) && (
<div
className={clsx(
'ml-2 w-10 rounded-full border border-purple-500 text-xs font-light',
'ml-2 min-w-[2rem] rounded-full border border-purple-500 text-xs font-light',
(hover || isActive) && 'bg-purple-500 text-white',
)}
>
Expand All @@ -33,3 +34,32 @@ export function TabHeaderButton({
</button>
);
}

export function TabHeaderFilters<Filter extends string>({
activeFilter,
setFilter,
counts,
showCount = true,
className,
}: {
activeFilter: Filter;
setFilter: (f: Filter) => void;
counts: Record<Filter, number>;
showCount?: boolean;
className?: string;
}) {
return (
<div className={`flex justify-between space-x-7 ${className}`}>
{objKeys<Filter>(counts).map((f) => (
<TabHeaderButton
key={f}
isActive={activeFilter === f}
count={showCount ? counts[f] : undefined}
onClick={() => setFilter(f)}
>
{f}
</TabHeaderButton>
))}
</div>
);
}
2 changes: 1 addition & 1 deletion src/components/menus/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export function DropdownModal({ button, buttonClasses, modal, modalClasses }: Mo
leaveTo="transform opacity-0 scale-95"
>
<Popover.Panel
className={`absolute right-0 z-40 mt-2 bg-white ring-1 ring-black/5 drop-shadow-md focus:outline-none ${modalClasses}`}
className={`absolute right-0 z-40 mt-2 w-max bg-white ring-1 ring-black/5 drop-shadow-md focus:outline-none ${modalClasses}`}
>
{({ close }) => modal({ close })}
</Popover.Panel>
Expand Down
4 changes: 2 additions & 2 deletions src/components/nav/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ export function Footer() {

function FooterIconLink({ to, imgSrc, alt }: { to: string; imgSrc: any; alt: string }) {
return (
<a className="relative h-5 w-5" href={to} target="_blank" rel="noopener noreferrer">
<Image src={imgSrc} alt={alt} width={25} height={25} />
<a className="relative h-4 w-4" href={to} target="_blank" rel="noopener noreferrer">
<Image src={imgSrc} alt={alt} width={22} height={22} />
</a>
);
}
Expand Down
5 changes: 5 additions & 0 deletions src/components/text/headers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { PropsWithChildren } from 'react';

export function H1({ children }: PropsWithChildren<unknown>) {
return <h1 className="font-serif text-3xl sm:text-4xl">{children}</h1>;
}
2 changes: 2 additions & 0 deletions src/config/links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export const links = {
discord: 'https://discord.gg/celo',
github: 'https://github.com/jmrossy/celo-station',
twitter: 'https://twitter.com/CeloOrg',
docs: 'https://docs.celo.org',
forum: 'https://forum.celo.org',
// RPCs
forno: 'https://forno.celo.org',
infura: 'https://celo-mainnet.infura.io/v3',
Expand Down
35 changes: 35 additions & 0 deletions src/features/governance/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export enum VoteValue {
None = 'none',
Abstain = 'abstain',
No = 'no',
Yes = 'yes',
}

export const OrderedVoteValue = [VoteValue.None, VoteValue.Abstain, VoteValue.No, VoteValue.Yes];

// Using ints to align with solidity enum
export enum ProposalStage {
None = 0,
Queued = 1,
Approval = 2,
Referendum = 3,
Execution = 4,
Expiration = 5,
}

export interface GovernanceProposal {
id: string;
timestamp: number;
url: string;
stage: ProposalStage;
votes: {
[VoteValue.Yes]: string;
[VoteValue.No]: string;
[VoteValue.Abstain]: string;
};
}

export interface GovernanceProposalWithMetadata extends GovernanceProposal {
description: string;
//TODO
}
34 changes: 34 additions & 0 deletions src/features/governance/useGovernanceProposals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useQuery } from '@tanstack/react-query';
import { useToastError } from 'src/components/notifications/useToastError';
import { GovernanceProposal } from 'src/features/governance/types';
import { logger } from 'src/utils/logger';
import { PublicClient } from 'viem';
import { usePublicClient } from 'wagmi';

export function useGovernanceProposals() {
const publicClient = usePublicClient();

const { isLoading, isError, error, data } = useQuery({
queryKey: ['useGovernanceProposals', publicClient],
queryFn: () => {
logger.debug('Fetching governance proposals');
return fetchGovernanceProposals(publicClient);
},
gcTime: Infinity,
staleTime: 60 * 60 * 1000, // 1 hour
});

useToastError(error, 'Error fetching governance proposals');

return {
isLoading,
isError,
proposals: data,
};
}

async function fetchGovernanceProposals(
_publicClient: PublicClient,
): Promise<GovernanceProposal[]> {
return [];
}
Loading

0 comments on commit 0ba946e

Please sign in to comment.