-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Define delegate metadata schema Update nav icons
- Loading branch information
Showing
16 changed files
with
540 additions
and
15 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
'use client'; | ||
|
||
import clsx from 'clsx'; | ||
import { useMemo, useState } from 'react'; | ||
import { FullWidthSpinner } from 'src/components/animation/Spinner'; | ||
import { BackLink } from 'src/components/buttons/BackLink'; | ||
import { ExternalLink } from 'src/components/buttons/ExternalLink'; | ||
import { ChevronIcon } from 'src/components/icons/Chevron'; | ||
import { Section } from 'src/components/layout/Section'; | ||
import { links } from 'src/config/links'; | ||
import { ProposalBadgeRow } from 'src/features/governance/components/ProposalCard'; | ||
import { ProposalUpvotersTable } from 'src/features/governance/components/ProposalUpvotersTable'; | ||
import { | ||
ProposalUpvoteButton, | ||
ProposalVoteButtons, | ||
} from 'src/features/governance/components/ProposalVoteButtons'; | ||
import { | ||
ProposalQuorumChart, | ||
ProposalVoteChart, | ||
} from 'src/features/governance/components/ProposalVoteChart'; | ||
import { ProposalVotersTable } from 'src/features/governance/components/ProposalVotersTable'; | ||
import { | ||
MergedProposalData, | ||
useGovernanceProposals, | ||
} from 'src/features/governance/hooks/useGovernanceProposals'; | ||
import { useProposalContent } from 'src/features/governance/hooks/useProposalContent'; | ||
import { ProposalStage } from 'src/features/governance/types'; | ||
import { useWindowSize } from 'src/styles/mediaQueries'; | ||
import { usePageInvariant } from 'src/utils/navigation'; | ||
import { trimToLength } from 'src/utils/strings'; | ||
import { getHumanReadableDuration } from 'src/utils/time'; | ||
import styles from './styles.module.css'; | ||
|
||
const ID_PARAM_REGEX = /^(cgp-)?(\d+)$/; | ||
|
||
export const dynamicParams = true; | ||
|
||
export default function Page({ params: { id } }: { params: { id: string } }) { | ||
const { proposals } = useGovernanceProposals(); | ||
|
||
const propData = useMemo(() => { | ||
if (!proposals || !id) return undefined; | ||
const matches = ID_PARAM_REGEX.exec(id); | ||
if (matches?.[1] === 'cgp-') { | ||
const cgpId = parseInt(matches[2]); | ||
return proposals.find((p) => p.metadata?.cgp === cgpId); | ||
} else if (matches?.[2]) { | ||
const propId = parseInt(matches[2]); | ||
return proposals.find((p) => p.proposal?.id === propId); | ||
} else { | ||
return undefined; | ||
} | ||
}, [proposals, id]); | ||
|
||
usePageInvariant(!proposals || propData, '/governance', 'Proposal not found'); | ||
|
||
const [isMenuCollapsed, setIsMenuCollapsed] = useState(false); | ||
|
||
if (!propData) { | ||
return <FullWidthSpinner>Loading proposals</FullWidthSpinner>; | ||
} | ||
|
||
return ( | ||
<Section containerClassName="mt-4 lg:flex lg:flex-row lg:gap-6"> | ||
<ProposalContent propData={propData} /> | ||
{propData.stage !== ProposalStage.None && ( | ||
<div className="fixed bottom-0 left-0 right-0 z-10 bg-white lg:static lg:bg-transparent"> | ||
<button | ||
onClick={() => setIsMenuCollapsed(!isMenuCollapsed)} | ||
className="flex-center w-full space-x-4 border-y border-taupe-300 py-2 lg:hidden" | ||
> | ||
<span>{isMenuCollapsed ? 'More' : 'Less'}</span> | ||
<ChevronIcon direction={isMenuCollapsed ? 'n' : 's'} width={15} height={10} /> | ||
</button> | ||
<div | ||
className={clsx( | ||
'transition-all duration-300', | ||
isMenuCollapsed ? 'max-h-0 lg:max-h-none' : 'max-h-screen lg:max-h-none', | ||
)} | ||
> | ||
<ProposalChainData propData={propData} /> | ||
</div> | ||
</div> | ||
)} | ||
</Section> | ||
); | ||
} | ||
|
||
function ProposalContent({ propData }: { propData: MergedProposalData }) { | ||
const { proposal, metadata } = propData; | ||
const title = trimToLength(metadata?.title || `Proposal #${proposal?.id}`, 80); | ||
|
||
const { content, isLoading } = useProposalContent(metadata); | ||
|
||
return ( | ||
<div className="space-y-3"> | ||
<BackLink href="/governance">Browse proposals</BackLink> | ||
<h1 className="font-serif text-2xl md:text-2xl">{title}</h1> | ||
<ProposalBadgeRow data={propData} showProposer /> | ||
{isLoading && !content && <FullWidthSpinner>Loading proposal content</FullWidthSpinner>} | ||
{!isLoading && !content && ( | ||
<div className="flex flex-col items-start justify-center space-y-3 py-8"> | ||
<p className="text-taupe-600"> | ||
No valid CGP data found for this proposal. It may be missing or malformed. | ||
</p> | ||
<p className="text-taupe-600">You can still upvote and/or vote for this proposal.</p> | ||
<p className="text-taupe-600"> | ||
See the{' '} | ||
<ExternalLink href={links.governance} className="underline"> | ||
Celo Governance repository | ||
</ExternalLink>{' '} | ||
for more information. | ||
</p> | ||
</div> | ||
)} | ||
{content && ( | ||
<div | ||
dangerouslySetInnerHTML={{ __html: content }} | ||
className={`space-y-4 pb-4 ${styles.proposal}`} | ||
></div> | ||
)} | ||
</div> | ||
); | ||
} | ||
|
||
function ProposalChainData({ propData }: { propData: MergedProposalData }) { | ||
const { id, stage, proposal } = propData; | ||
const expiryTimestamp = proposal?.expiryTimestamp; | ||
|
||
const windowSize = useWindowSize(); | ||
const showTables = windowSize?.width && windowSize.width > 1024; | ||
|
||
if (stage === ProposalStage.None) return null; | ||
|
||
return ( | ||
<div className="space-y-4 lg:min-w-[20rem]"> | ||
<div className="space-y-4 border-taupe-300 p-3 lg:border"> | ||
{stage === ProposalStage.Queued && <ProposalUpvoteButton proposalId={id} />} | ||
{stage === ProposalStage.Referendum && <ProposalVoteButtons proposalId={id} />} | ||
{expiryTimestamp && ( | ||
<div>{`Voting ends in ${getHumanReadableDuration(expiryTimestamp)}`}</div> | ||
)} | ||
{stage >= ProposalStage.Referendum && <ProposalVoteChart propData={propData} />} | ||
{stage === ProposalStage.Referendum && <ProposalQuorumChart propData={propData} />} | ||
</div> | ||
{showTables && stage >= ProposalStage.Queued && stage < ProposalStage.Referendum && ( | ||
<div className="border-taupe-300 p-3 lg:border"> | ||
<ProposalUpvotersTable propData={propData} /> | ||
</div> | ||
)} | ||
{showTables && stage >= ProposalStage.Referendum && ( | ||
<div className="overflow-auto border-taupe-300 p-3 lg:border"> | ||
<ProposalVotersTable propData={propData} /> | ||
</div> | ||
)} | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
.proposal { | ||
max-width: min(96vw, 768px); | ||
overflow: auto; | ||
} | ||
|
||
.proposal h1, | ||
.proposal h2, | ||
.proposal h3, | ||
.proposal h4, | ||
.proposal a, | ||
.proposal p { | ||
overflow-wrap: break-word; | ||
word-wrap: break-word; | ||
} | ||
|
||
.proposal h1, | ||
.proposal h2, | ||
.proposal h3, | ||
.proposal h4 { | ||
font-family: 'Garamond', serif; | ||
} | ||
|
||
.proposal h1, | ||
.proposal h2 { | ||
font-size: 24px; | ||
} | ||
|
||
.proposal h3 { | ||
font-size: 20px; | ||
} | ||
|
||
.proposal h4 { | ||
font-size: 18px; | ||
} | ||
|
||
.proposal a { | ||
text-decoration: underline; | ||
cursor: pointer; | ||
} | ||
|
||
.proposal a:hover { | ||
opacity: 0.75; | ||
} | ||
|
||
.proposal a:active { | ||
opacity: 0.65; | ||
} | ||
|
||
.proposal ol, | ||
.proposal ul { | ||
padding-left: 1rem; | ||
} | ||
|
||
.proposal ul { | ||
list-style: disc; | ||
} | ||
|
||
.proposal ol { | ||
list-style: decimal; | ||
} | ||
|
||
.proposal li { | ||
padding-left: 1rem; | ||
margin-top: 0.6rem; | ||
} | ||
|
||
.proposal pre { | ||
padding: 0.5rem; | ||
background-color: rgba(0, 0, 0, 0.04); | ||
overflow-x: hidden; | ||
} | ||
|
||
.proposal img { | ||
display: none; | ||
} | ||
|
||
.proposal table th, | ||
.proposal table td { | ||
padding: 0.2rem 0.5rem; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
'use client'; | ||
|
||
import { useMemo, useState } from 'react'; | ||
import { Fade } from 'src/components/animation/Fade'; | ||
import { FullWidthSpinner } from 'src/components/animation/Spinner'; | ||
import { ExternalLink } from 'src/components/buttons/ExternalLink'; | ||
import { TabHeaderButton } from 'src/components/buttons/TabHeaderButton'; | ||
import { SearchField } from 'src/components/input/SearchField'; | ||
import { CtaCard } from 'src/components/layout/CtaCard'; | ||
import { Section } from 'src/components/layout/Section'; | ||
import { H1 } from 'src/components/text/headers'; | ||
import { links } from 'src/config/links'; | ||
import { Delegatee } from 'src/features/delegation/types'; | ||
import { useDelegatees } from 'src/features/delegation/useDelegatees'; | ||
|
||
export default function Page() { | ||
return ( | ||
<> | ||
<Section className="mt-4" containerClassName="space-y-5"> | ||
<H1>Delegate voting power</H1> | ||
<RegisterCtaCard /> | ||
<DelegateeList /> | ||
</Section> | ||
</> | ||
); | ||
} | ||
|
||
function DelegateeList() { | ||
const [searchQuery, setSearchQuery] = useState<string>(''); | ||
|
||
const { delegatees } = useDelegatees(); | ||
const filteredDelegatees = useFilteredDelegatees({ delegatees, searchQuery }); | ||
|
||
return ( | ||
<div className=""> | ||
<div className="flex flex-col items-stretch gap-3 md:flex-row md:items-center md:justify-between"></div> | ||
{filteredDelegatees ? ( | ||
<Fade show> | ||
<div className="flex justify-between"> | ||
<TabHeaderButton isActive={true} count={filteredDelegatees.length}> | ||
Delegates | ||
</TabHeaderButton> | ||
<SearchField | ||
value={searchQuery} | ||
setValue={setSearchQuery} | ||
placeholder="Search delegates" | ||
className="w-full text-sm md:w-64" | ||
/> | ||
</div> | ||
<div className="mt-6 divide-y divide-taupe-300"> | ||
{filteredDelegatees.length ? ( | ||
filteredDelegatees.map((data, i) => ( | ||
<div key={i} className="py-6 first:pt-0"> | ||
{JSON.stringify(data)} | ||
</div> | ||
)) | ||
) : ( | ||
<div className="flex justify-center py-10"> | ||
<p className="text-center text-taupe-600">No delegates found</p> | ||
</div> | ||
)} | ||
</div> | ||
</Fade> | ||
) : ( | ||
<FullWidthSpinner>Loading delegate data</FullWidthSpinner> | ||
)} | ||
</div> | ||
); | ||
} | ||
|
||
function useFilteredDelegatees({ | ||
delegatees, | ||
searchQuery, | ||
}: { | ||
delegatees?: Delegatee[]; | ||
searchQuery: string; | ||
}) { | ||
return useMemo<Delegatee[] | undefined>(() => { | ||
if (!delegatees) return undefined; | ||
const query = searchQuery.trim().toLowerCase(); | ||
return delegatees.filter( | ||
(d) => | ||
!query || | ||
d.name.toLowerCase().includes(query) || | ||
d.address.toLowerCase().includes(query) || | ||
d.description.toLowerCase().includes(query), | ||
); | ||
}, [delegatees, searchQuery]); | ||
} | ||
|
||
export function RegisterCtaCard() { | ||
return ( | ||
<CtaCard> | ||
<div className="space-y-2"> | ||
<h3 className="font-serif text-xl sm:text-2xl">Passionate about Celo governance?</h3> | ||
<p className="text-sm sm:text-base">Add your information on Github to be listed here.</p> | ||
</div> | ||
<ExternalLink href={links.delegate} className="btn btn-primary rounded-full border-taupe-300"> | ||
Register as a delegate | ||
</ExternalLink> | ||
</CtaCard> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import clsx from 'clsx'; | ||
import { PropsWithChildren } from 'react'; | ||
|
||
// Call to action card | ||
export function CtaCard({ children, className }: PropsWithChildren<{ className?: string }>) { | ||
return ( | ||
<div | ||
className={clsx( | ||
'flex items-center justify-between space-x-6 border border-taupe-300 bg-white bg-diamond-texture bg-right-bottom bg-no-repeat px-3 py-4 md:px-5 md:py-6', | ||
className, | ||
)} | ||
> | ||
{children} | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.