Skip to content

Commit

Permalink
DAO-720 feat: Implemented Time Remaining Column. (#241)
Browse files Browse the repository at this point in the history
feat: Revamped proposals to use context.
feat: Reduced api calls to backend.
feat: Separated each column into its own file.
feat: separated proposal state into its own hook.
fix: added refetch interval to voteOnProposal to avoid unnecessary calls to node
  • Loading branch information
Freshenext authored Oct 3, 2024
1 parent 8f04d77 commit 6f83201
Show file tree
Hide file tree
Showing 12 changed files with 317 additions and 136 deletions.
147 changes: 39 additions & 108 deletions src/app/proposals/LatestProposalsTable.tsx
Original file line number Diff line number Diff line change
@@ -1,130 +1,61 @@
import { useFetchLatestProposals } from '@/app/proposals/hooks/useFetchLatestProposals'
import { useGetProposalVotes } from '@/app/proposals/hooks/useGetProposalVotes'
import { getEventArguments } from '@/app/proposals/shared/utils'
import { StatusColumn } from '@/app/proposals/StatusColumn'
import { ComparativeProgressBar } from '@/components/ComparativeProgressBar/ComparativeProgressBar'
import { Popover } from '@/components/Popover'
import { Table } from '@/components/Table'
import { Header, Paragraph } from '@/components/Typography'
import { toFixed } from '@/lib/utils'
import { useRouter } from 'next/navigation'
import { useMemo } from 'react'

interface ProposalNameColumnProps {
name: string
proposalId: string
}

const ProposalNameColumn = ({ name, proposalId }: ProposalNameColumnProps) => {
const router = useRouter()
return (
<button onClick={() => router.push(`/proposals/${proposalId}`)}>
<span className="underline text-left">{name.slice(0, 20)}</span>
</button>
)
}

const VotesColumn = ({ proposalId }: Omit<ProposalNameColumnProps, 'name'>) => {
const data = useGetProposalVotes(proposalId)
const votes = data.reduce((prev, next) => Number(next) + prev, 0)
return <p>{toFixed(votes)}</p>
}

const PopoverSentiment = ({ votes }: { votes: string[] }) => {
const [againstVotes, forVotes, abstainVotes] = votes
return (
<div className="text-black">
<Paragraph variant="semibold" className="text-[12px] font-bold">
Votes for
</Paragraph>
<div className="flex flex-row">
<Paragraph variant="semibold" className="text-[12px] w-1/2 text-st-success">
For
</Paragraph>
<Paragraph variant="semibold" className="text-[12px] w-1/2">
{forVotes}
</Paragraph>
</div>
<div className="flex flex-row">
<Paragraph variant="semibold" className="text-[12px] w-1/2 text-st-error">
Against
</Paragraph>
<Paragraph variant="semibold" className="text-[12px] w-1/2">
{againstVotes}
</Paragraph>
</div>
<div className="flex flex-row">
<Paragraph variant="semibold" className="text-[12px] w-1/2 text-st-info">
Abstain
</Paragraph>
<Paragraph variant="semibold" className="text-[12px] w-1/2">
{abstainVotes}
</Paragraph>
</div>
</div>
)
}

const SentimentColumn = ({
proposalId,
index,
}: Omit<ProposalNameColumnProps, 'name'> & { index: number }) => {
const data = useGetProposalVotes(proposalId)

const sentimentValues = useMemo(() => {
const [againstVotes, forVotes, abstainVotes] = data
return [
{ value: Number(forVotes), color: 'var(--st-success)' },
{ value: Number(againstVotes), color: 'var(--st-error)' },
{ value: Number(abstainVotes), color: 'var(--st-info)' },
]
}, [data])

const position = index === 0 ? 'bottom' : 'top'
return (
<Popover
content={<PopoverSentiment votes={data} />}
trigger="hover"
background="light"
position={position}
size="small"
hasCaret={true}
>
<ComparativeProgressBar values={sentimentValues} />
</Popover>
)
}
import { Header } from '@/components/Typography'
import { SharedProposalsTableContextProvider } from '@/app/proposals/SharedProposalsTableContext'
import { ProposalsContextProvider } from '@/app/proposals/ProposalsContext'
import { SentimentColumn } from '@/app/proposals/SentimentColumn'
import { VotesColumn } from '@/app/proposals/VotesColumn'
import { ProposalNameColumn } from '@/app/proposals/ProposalNameColumn'
import { ReactNode, useMemo } from 'react'
import { TimeRemainingColumn } from '@/app/proposals/TimeRemainingColumn'

interface LatestProposalsTableProps {
latestProposals: ReturnType<typeof useFetchLatestProposals>['latestProposals']
}

const latestProposalsTransformer = (proposals: ReturnType<typeof getEventArguments>[]) =>
proposals.map((proposal, i) => ({
'Proposal Name': <ProposalNameColumn {...proposal} />,
'Current Votes': <VotesColumn {...proposal} />,
Starts: proposal.Starts.format('YYYY-MM-DD'),
Sentiment: <SentimentColumn {...proposal} index={i} key={`${proposal.proposalId}_${i}`} />,
Status: <StatusColumn {...proposal} />,
}))
proposals.map((proposal, i) => {
// Create the withContext function to wrap components in ProposalsProvider
const withContext = (component: ReactNode) => (
<ProposalsContextProvider {...proposal} index={i}>
{component}
</ProposalsContextProvider>
)
return {
'Proposal Name': withContext(<ProposalNameColumn />),
'Current Votes': withContext(<VotesColumn />),
Starts: proposal.Starts.format('YYYY-MM-DD'),
'Time Remaining': withContext(<TimeRemainingColumn />),
Sentiment: withContext(<SentimentColumn key={`${proposal.proposalId}_${i}`} />),
Status: withContext(<StatusColumn />),
}
})

export const LatestProposalsTable = ({ latestProposals }: LatestProposalsTableProps) => {
// @ts-ignore
const latestProposalsMapped = latestProposals.map(getEventArguments)
const latestProposalsMapped = useMemo(
// @ts-ignore
() => latestProposals.map(getEventArguments),
[latestProposals.length],
)

return (
<div>
<Header variant="h2" className="mb-4">
Latest Proposals
</Header>
{latestProposals.length > 0 && (
<Table
data={latestProposalsTransformer(latestProposalsMapped)}
data-testid="TableProposals"
tbodyProps={{
'data-testid': 'TableProposalsTbody',
}}
/>
<SharedProposalsTableContextProvider>
<Table
key={latestProposalsMapped.length}
data={latestProposalsTransformer(latestProposalsMapped)}
data-testid="TableProposals"
tbodyProps={{
'data-testid': 'TableProposalsTbody',
}}
/>
</SharedProposalsTableContextProvider>
)}
</div>
)
Expand Down
12 changes: 12 additions & 0 deletions src/app/proposals/ProposalNameColumn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useRouter } from 'next/navigation'
import { useProposalContext } from '@/app/proposals/ProposalsContext'

export const ProposalNameColumn = () => {
const router = useRouter()
const { name, proposalId } = useProposalContext()
return (
<button onClick={() => router.push(`/proposals/${proposalId}`)}>
<span className="underline text-left">{name.slice(0, 20)}</span>
</button>
)
}
64 changes: 64 additions & 0 deletions src/app/proposals/ProposalsContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { createContext, ReactNode, useContext, useMemo } from 'react'
import { useGetProposalVotes } from '@/app/proposals/hooks/useGetProposalVotes'
import { useProposalState } from '@/shared/hooks/useProposalState'

interface ProposalsContextProps {
proposalVotes: string[]
name: string
proposalId: string
// Index of row rendered in table
index: number
// Status transformed to human-friendly string
proposalStateHuman: string
// Block in which the proposal was created
blockNumber: string
}

const ProposalsContext = createContext<ProposalsContextProps>({
proposalVotes: [],
name: '',
proposalId: '',
index: 0,
proposalStateHuman: '',
blockNumber: '0',
})

interface ProposalsContextProviderProps {
proposalId: string
name: string
index: number
children: ReactNode
blockNumber: string // HEX blockNumber
}

export const ProposalsContextProvider = ({
proposalId,
name,
index,
children,
blockNumber,
}: ProposalsContextProviderProps) => {
const proposalVotes = useGetProposalVotes(proposalId)
const { proposalStateHuman } = useProposalState(proposalId, false)
const value: ProposalsContextProps = useMemo(
() => ({
proposalVotes: proposalVotes,
proposalId,
name,
index,
proposalStateHuman,
blockNumber,
}),
[proposalVotes, proposalId, name, index, proposalStateHuman, blockNumber],
)
return <ProposalsContext.Provider value={value}>{children}</ProposalsContext.Provider>
}

// Hook to use the ProposalsContext for an individual proposal
export const useProposalContext = () => {
const context = useContext(ProposalsContext)
if (context === undefined) {
throw new Error('useProposal must be used within a ProposalsProvider')
}
return context
}
67 changes: 67 additions & 0 deletions src/app/proposals/SentimentColumn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Paragraph } from '@/components/Typography'
import { useProposalContext } from '@/app/proposals/ProposalsContext'
import { useMemo } from 'react'
import { Popover } from '@/components/Popover'
import { ComparativeProgressBar } from '@/components/ComparativeProgressBar/ComparativeProgressBar'

const PopoverSentiment = ({ votes }: { votes: string[] }) => {
const [againstVotes, forVotes, abstainVotes] = votes
return (
<div className="text-black">
<Paragraph variant="semibold" className="text-[12px] font-bold">
Votes for
</Paragraph>
<div className="flex flex-row">
<Paragraph variant="semibold" className="text-[12px] w-1/2 text-st-success">
For
</Paragraph>
<Paragraph variant="semibold" className="text-[12px] w-1/2">
{forVotes}
</Paragraph>
</div>
<div className="flex flex-row">
<Paragraph variant="semibold" className="text-[12px] w-1/2 text-st-error">
Against
</Paragraph>
<Paragraph variant="semibold" className="text-[12px] w-1/2">
{againstVotes}
</Paragraph>
</div>
<div className="flex flex-row">
<Paragraph variant="semibold" className="text-[12px] w-1/2 text-st-info">
Abstain
</Paragraph>
<Paragraph variant="semibold" className="text-[12px] w-1/2">
{abstainVotes}
</Paragraph>
</div>
</div>
)
}

export const SentimentColumn = () => {
const { proposalVotes: data, index } = useProposalContext()

const sentimentValues = useMemo(() => {
const [againstVotes, forVotes, abstainVotes] = data
return [
{ value: Number(forVotes), color: 'var(--st-success)' },
{ value: Number(againstVotes), color: 'var(--st-error)' },
{ value: Number(abstainVotes), color: 'var(--st-info)' },
]
}, [data])

const position = index === 0 ? 'bottom' : 'top'
return (
<Popover
content={<PopoverSentiment votes={data} />}
trigger="hover"
background="light"
position={position}
size="small"
hasCaret={true}
>
<ComparativeProgressBar values={sentimentValues} />
</Popover>
)
}
31 changes: 31 additions & 0 deletions src/app/proposals/SharedProposalsTableContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { createContext, ReactNode, useContext, useMemo } from 'react'
import { useBlockNumber } from 'wagmi'

interface SharedProposalsTableContextProps {
latestBlockNumber?: bigint
}

const SharedProposalsTableContext = createContext<SharedProposalsTableContextProps>({
latestBlockNumber: undefined,
})

interface SharedProposalsTableContextProviderProps {
children: ReactNode
}

export const SharedProposalsTableContextProvider = ({
children,
}: SharedProposalsTableContextProviderProps) => {
const { data: latestBlockNumber } = useBlockNumber()

const value = useMemo(
() => ({
latestBlockNumber,
}),
[latestBlockNumber],
)

return <SharedProposalsTableContext.Provider value={value}>{children}</SharedProposalsTableContext.Provider>
}

export const useSharedProposalsTableContext = () => useContext(SharedProposalsTableContext)
6 changes: 3 additions & 3 deletions src/app/proposals/StatusColumn.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useVoteOnProposal } from '@/shared/hooks/useVoteOnProposal'
import { Status } from '@/components/Status'
import { StatusSeverity } from '@/components/Status/types'
import { useProposalContext } from '@/app/proposals/ProposalsContext'

const StatusByProposalState = {
Pending: 'in-progress',
Expand All @@ -14,8 +14,8 @@ const StatusByProposalState = {
undefined: null,
}

export const StatusColumn = ({ proposalId }: { proposalId: string }) => {
const { proposalStateHuman } = useVoteOnProposal(proposalId)
export const StatusColumn = () => {
const { proposalStateHuman } = useProposalContext()
const statusMap = StatusByProposalState[
proposalStateHuman as keyof typeof StatusByProposalState
] as StatusSeverity
Expand Down
Loading

0 comments on commit 6f83201

Please sign in to comment.