Skip to content

Commit

Permalink
Add post-vote-submission page
Browse files Browse the repository at this point in the history
  • Loading branch information
csillag committed Jul 13, 2024
1 parent a9dddcd commit ce5a3fc
Show file tree
Hide file tree
Showing 7 changed files with 251 additions and 27 deletions.
65 changes: 63 additions & 2 deletions frontend/src/hooks/usePollData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,40 @@ export type PollResults = {
votes?: ListOfVotes | undefined
}

export type RemainingTime = {
pastDue: boolean
totalSeconds: number
days: number
hours: number
minutes: number
seconds: number
}

const calculateRemainingTimeFrom = (deadline: number, now: number): RemainingTime => {
const pastDue = now > deadline
const totalSeconds = Math.floor((Math.abs(deadline - now)))

return {
pastDue,
totalSeconds,
days: Math.floor(totalSeconds / (24 * 3600)),
hours: Math.floor(totalSeconds % (24 * 3600) / 3600),
minutes: Math.floor(totalSeconds % 3600 / 60),
seconds: totalSeconds % 60
}
}

const getTextDescriptionOfTime = (remaining: RemainingTime | undefined): string | undefined => {
if (!remaining) return undefined;
const hasDays = !!remaining.days
const hasHours = hasDays || !!remaining.hours
const hasMinutes = hasHours || !!remaining.minutes
if (remaining.pastDue) {
return `Voting finished ${hasDays ? remaining.days + " days, " : ""}${hasHours ? remaining.hours + " hours, " : ""}${hasMinutes ? remaining.minutes + " minutes, " : ""}${remaining.seconds} seconds ago.`;
} else {
return `Poll closes in ${hasDays ? remaining.days + " days, " : ""}${hasHours ? remaining.hours + " hours, " : ""}${hasMinutes ? remaining.minutes + " minutes, " : ""}${remaining.seconds} seconds.`;
}
}

type LoadedData =
[
Expand Down Expand Up @@ -75,6 +109,7 @@ export const usePollData = (eth: EthereumContext, pollId: string) => {
const [hasVoted, setHasVoted] = useState(false);
const [isClosing, setIsClosing] = useState(false);
const [isClosed, setIsClosed] = useState(false);
const [pollLoaded, setPollLoaded] = useState(true)
const [poll, setPoll] = useState<LoadedPoll>();
const [winningChoice, setWinningChoice] = useState<bigint | undefined>(undefined);
const [selectedChoice, setSelectedChoice] = useState<bigint | undefined>();
Expand All @@ -98,6 +133,8 @@ export const usePollData = (eth: EthereumContext, pollId: string) => {
const [canVote, setCanVote] = useState(false)

const [canSelect, setCanSelect] = useState(false)
const [remainingTime, setRemainingTime] = useState<RemainingTime>()
const [remainingTimeString, setRemainingTimeString] = useState<string | undefined>()

useEffect(
() => setCanVote(!!eth.state.address &&
Expand Down Expand Up @@ -292,13 +329,34 @@ export const usePollData = (eth: EthereumContext, pollId: string) => {
}
}, [eth.state.signer])

const [pollLoaded, setPollLoaded] = useState(true)

useEffect(
( ) => setPollLoaded(false),
[pollId]
);

const updateRemainingTime = useCallback(
() => {
const deadline = poll?.ipfsParams.options.closeTimestamp
const now = new Date().getTime()/1000
const remaining = deadline ? calculateRemainingTimeFrom(deadline, now) : undefined;
setRemainingTime(remaining)
setRemainingTimeString(getTextDescriptionOfTime(remaining))
if (deadline) {
// console.log("Scheduling next update")
setTimeout(() => {
// console.log("Should update remainingTime")
updateRemainingTime()
}, 1000)
}
},
[poll, setRemainingTime]
)

useEffect(() => {
updateRemainingTime()
}, [poll]);


const loadProposal = useCallback(async () => {
if (!dao || !daoAddress || !pollACL || !gaslessVoting || !userAddress) {
// console.log("not loading, because dependencies are not yet available")
Expand Down Expand Up @@ -492,6 +550,9 @@ export const usePollData = (eth: EthereumContext, pollId: string) => {
canSelect,
setSelectedChoice,

remainingTime,
remainingTimeString,

canVote,
vote,
isVoting,
Expand Down
20 changes: 11 additions & 9 deletions frontend/src/pages/PollPage/ActivePoll.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,25 @@ import { FC, useCallback } from 'react';
import { Poll } from '../../types';
import classes from "./index.module.css"
import { Button } from '../../components/Button';
import { RemainingTime } from '../../hooks/usePollData';

export const ActivePoll: FC<{
poll: Poll
remainingTime: RemainingTime | undefined
remainingTimeString: string | undefined
selectedChoice: bigint | undefined,
canSelect: boolean,
setSelectedChoice: (choice: bigint | undefined) => void,
canVote: boolean,
vote: () => Promise<void>;
isVoting: boolean,
hasVoted: boolean,
existingVote: bigint | undefined,

}> =
({
poll: {name, description, choices},
remainingTime, remainingTimeString,
selectedChoice, canSelect, setSelectedChoice,
canVote, vote, isVoting,
hasVoted, existingVote,

}) => {

const handleSelect = useCallback((index: number) => {
Expand All @@ -29,17 +30,16 @@ export const ActivePoll: FC<{
} else {
setSelectedChoice(BigInt(index))
}
// } else {
// console.log("Ignoring clicking, since we can't select.")
}
}, [canSelect, selectedChoice, setSelectedChoice])

const handleSubmit = () => {
console.log("Submitting")
vote()
}

console.log("selected:", selectedChoice, "can select?", canSelect, "can Vote?", canVote, "voting?", isVoting, "hasVoted?", hasVoted, "existing vote", existingVote)
const pastDue = !!remainingTime?.pastDue

// console.log("selected:", selectedChoice, "can select?", canSelect, "can Vote?", canVote, "voting?", isVoting)
return (
<div className={`${classes.card} ${classes.darkCard}`}>
<h2>{name}</h2>
Expand All @@ -56,7 +56,9 @@ export const ActivePoll: FC<{
</div>
))}
</>
<Button disabled={!canVote || isVoting} onClick={handleSubmit}>{isVoting ? "Submitting ..." : "Submit vote"}</Button>
{ !pastDue && <Button disabled={!canVote || isVoting} onClick={handleSubmit}>{isVoting ? "Submitting ..." : "Submit vote"}</Button> }
{ remainingTimeString && <h4>{remainingTimeString}</h4>}
{ pastDue && <h4>Results will be available when the owner formally closes the vote.</h4>}
</div>
)
}
Expand Down
33 changes: 33 additions & 0 deletions frontend/src/pages/PollPage/BigCountdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { FC } from 'react';
import { RemainingTime } from '../../hooks/usePollData';
import classes from "./index.module.css"

type Cell = {
number: number,
unit: string,
}

export const BigCountdown: FC<{
remainingTime: RemainingTime
}> = ({remainingTime}) => {

const wantedCells: Cell[] = []
const hasDays = !!remainingTime.days
const hasHours = hasDays || !!remainingTime.hours
const hasMinutes = hasHours || !!remainingTime.minutes

if (hasDays) wantedCells.push({number: remainingTime.days, unit: "days"})
if (hasHours) wantedCells.push({number: remainingTime.hours, unit: "hours"})
if (hasMinutes) wantedCells.push({number: remainingTime.minutes, unit: "minutes"})
wantedCells.push({number: remainingTime.seconds, unit: "seconds"})
return (
<div className={classes.countdownContainer}>
{wantedCells.map(cell => (
<div key={`cell-${cell.unit}`} className={classes.countdownCell}>
<div className={classes.countdownNumber}>{cell.number}</div>
<div className={classes.countdownUnit}>{cell.unit}</div>
</div>
))}
</div>
)
}
18 changes: 9 additions & 9 deletions frontend/src/pages/PollPage/CompletedPoll.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,24 @@ export const CompletedPoll: FC<{poll: Poll, results: PollResults}> =
<h3>{name}</h3>
<h4>{description}</h4>
<>
{ Object.entries(choices)
.map(([index, entry])=> (
<div className={`${classes.choice} ${entry.winner ? classes.winner : ''}`} key={`choice-${index}`}>
<div className={classes.sizeBar} style={{width: `${entry.rate}%`}}/>
<div className={classes.above}>{entry.choice}</div>
<div className={`${classes.percentage} ${classes.above}`}>{entry.rate}%</div>
</div>
{Object.entries(choices)
.map(([index, entry]) => (
<div className={`${classes.choice} ${entry.winner ? classes.winner : ''}`} key={`choice-${index}`}>
<div className={classes.sizeBar} style={{ width: `${entry.rate}%` }} />
<div className={classes.above}>{entry.choice}</div>
<div className={`${classes.percentage} ${classes.above}`}>{entry.rate}%</div>
</div>
))}
</>
{ !!votes?.out_count && (
{!!votes?.out_count && (
<div>
<h4>Individual votes:</h4>
<>
{votes.out_voters.map((voter, index) => {
const [weight, choice] = votes.out_choices[index]
return (
<div key={`voter-${index}`}>
{voter} ({weight.toString()}): { choices[choice.toString()].choice }
{voter} ({weight.toString()}): {choices[choice.toString()].choice}
</div>
)
})}
Expand Down
68 changes: 68 additions & 0 deletions frontend/src/pages/PollPage/ThanksForVoting.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { FC } from 'react';
import classes from "./index.module.css"
import { Poll } from '../../types'
import { RemainingTime } from '../../hooks/usePollData';
import { BigCountdown } from './BigCountdown';

const VoteIcon: FC = () => {
return (
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M10.49 0.5C4.97 0.5 0.5 4.98 0.5 10.5C0.5 16.02 4.97 20.5 10.49 20.5C16.02 20.5 20.5 16.02 20.5 10.5C20.5 4.98 16.02 0.5 10.49 0.5ZM10.5 18.5C6.08 18.5 2.5 14.92 2.5 10.5C2.5 6.08 6.08 2.5 10.5 2.5C14.92 2.5 18.5 6.08 18.5 10.5C18.5 14.92 14.92 18.5 10.5 18.5ZM14 9.5C14.83 9.5 15.5 8.83 15.5 8C15.5 7.17 14.83 6.5 14 6.5C13.17 6.5 12.5 7.17 12.5 8C12.5 8.83 13.17 9.5 14 9.5ZM7 9.5C7.83 9.5 8.5 8.83 8.5 8C8.5 7.17 7.83 6.5 7 6.5C6.17 6.5 5.5 7.17 5.5 8C5.5 8.83 6.17 9.5 7 9.5ZM10.5 16C12.83 16 14.81 14.54 15.61 12.5H5.39C6.19 14.54 8.17 16 10.5 16Z"
fill="#010038" />
</svg>

)
}

const StatusInfo: FC<{
remainingTime: RemainingTime | undefined,
remainingTimeString: string | undefined
}> = ({remainingTime, remainingTimeString}) => {
if (remainingTime) {
if (remainingTime.pastDue) {
return (
<>
<h4>{remainingTimeString}</h4>
<h4>Results will be available when the owner formally closes the vote.</h4>
</>
)
} else {
return (
<>
<h4>Poll closes in:</h4>
<BigCountdown remainingTime={remainingTime} />
</>
);
}
} else {
return (
<h4>Results will be available when the owner closes the vote.</h4>
)
}
}

export const ThanksForVote: FC<{
poll: Poll,
myVote: bigint
remainingTime: RemainingTime | undefined
remainingTimeString: string | undefined
}> = ({ poll, myVote, remainingTime, remainingTimeString}) => {
const {
name,
description,
choices
} = poll
return (
<div className={classes.card}>
<h2>Thanks for voting!</h2>
<h3>{name}</h3>
<h4>{description}</h4>
<div className={`${classes.choice} ${classes.submitted}`}>
<VoteIcon />
{choices[Number(myVote)]}
</div>
<StatusInfo remainingTime={remainingTime} remainingTimeString={remainingTimeString} />
</div>
)
}
47 changes: 46 additions & 1 deletion frontend/src/pages/PollPage/index.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@
border: 2px solid #130FFF;
}

.submitted {
border: 1px solid #010038;
justify-content: center;
gap: 10px;
}

.percentage {
font-family: 'Inter';
font-style: normal;
Expand Down Expand Up @@ -110,4 +116,43 @@
background: #D2D1FF;
box-shadow: 0 7px 7px 0 rgba(0, 0, 0, 0.17);

}
}

.countdownContainer {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 32px;
color: #01001F;
}

.countdownCell {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}

.countdownNumber {
width: 64px;
height: 45px;
text-align: center;

font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-size: 32px;
line-height: 140%;

}

.countdownUnit {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-size: 11px;
line-height: 16px;
text-align: center;

}
Loading

0 comments on commit ce5a3fc

Please sign in to comment.