diff --git a/packages/nextjs/.env.example b/packages/nextjs/.env.example index d816775..300c253 100644 --- a/packages/nextjs/.env.example +++ b/packages/nextjs/.env.example @@ -12,3 +12,4 @@ NEXT_PUBLIC_ALCHEMY_API_KEY= NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID= NEXT_PUBLIC_PINATA_JWT= +NEXT_PUBLIC_PINATA_GATEWAY= diff --git a/packages/nextjs/app/admin/page.tsx b/packages/nextjs/app/admin/page.tsx index a7f0c7e..6e9c18c 100644 --- a/packages/nextjs/app/admin/page.tsx +++ b/packages/nextjs/app/admin/page.tsx @@ -1,6 +1,7 @@ "use client"; import { useEffect, useState } from "react"; +import Link from "next/link"; import { redirect } from "next/navigation"; import CreatePollModal from "./_components/CreatePollModal"; import PollStatusModal from "./_components/PollStatusModal"; @@ -52,7 +53,7 @@ export default function AdminPage() { - {polls.map((poll: any) => ( + {polls.map(poll => ( {poll.name} {new Date(Number(poll.startTime) * 1000).toLocaleString()} @@ -65,6 +66,13 @@ export default function AdminPage() { (Required Actions) + ) : poll.status == PollStatus.RESULT_COMPUTED ? ( + <> + {poll.status}{" "} + + (View Results) + + ) : ( poll.status )} diff --git a/packages/nextjs/app/polls/[id]/page.tsx b/packages/nextjs/app/polls/[id]/page.tsx index 3f09b82..b98123e 100644 --- a/packages/nextjs/app/polls/[id]/page.tsx +++ b/packages/nextjs/app/polls/[id]/page.tsx @@ -10,7 +10,9 @@ import VoteCard from "~~/components/card/VoteCard"; import { useAuthContext } from "~~/contexts/AuthContext"; import { useAuthUserOnly } from "~~/hooks/useAuthUserOnly"; import { useFetchPoll } from "~~/hooks/useFetchPoll"; -import { PollType } from "~~/types/poll"; +import { getPollStatus } from "~~/hooks/useFetchPolls"; +import { PollStatus, PollType } from "~~/types/poll"; +import { getDataFromPinata } from "~~/utils/pinata"; import { notification } from "~~/utils/scaffold-eth"; export default function PollDetail() { @@ -28,6 +30,8 @@ export default function PollDetail() { const [isVotesInvalid, setIsVotesInvalid] = useState>({}); const isAnyInvalid = Object.values(isVotesInvalid).some(v => v); + const [result, setResult] = useState<{ candidate: string; votes: number }[] | null>(null); + const [status, setStatus] = useState(); useEffect(() => { if (!poll || !poll.metadata) { @@ -40,6 +44,39 @@ export default function PollDetail() { } catch (err) { console.log("err", err); } + + if (poll.tallyJsonCID) { + (async () => { + try { + const { + results: { tally }, + } = await getDataFromPinata(poll.tallyJsonCID); + if (poll.options.length > tally.length) { + throw new Error("Invalid tally data"); + } + const tallyCounts: number[] = tally.map((v: string) => Number(v)).slice(0, poll.options.length); + const result = []; + for (let i = 0; i < poll.options.length; i++) { + const candidate = poll.options[i]; + const votes = tallyCounts[i]; + result.push({ candidate, votes }); + } + result.sort((a, b) => b.votes - a.votes); + setResult(result); + console.log("data", result); + } catch (err) { + console.log("err", err); + } + })(); + } + + const statusUpdateInterval = setInterval(async () => { + setStatus(getPollStatus(poll)); + }, 1000); + + return () => { + clearInterval(statusUpdateInterval); + }; }, [poll]); const { data: coordinatorPubKeyResult } = useContractRead({ @@ -91,12 +128,10 @@ export default function PollDetail() { } // check if the poll is closed - // if (Number(poll.endTime) * 1000 < new Date().getTime()) { - // notification.error("Voting is closed for this poll"); - // return; - // } - - // TODO: check if the poll is not started + if (status !== PollStatus.OPEN) { + notification.error("Voting is closed for this poll"); + return; + } const votesToMessage = votes.map((v, i) => getMessageAndEncKeyPair( @@ -188,6 +223,7 @@ export default function PollDetail() { {poll?.options.map((candidate, index) => (
))} -
- -
+ {status === PollStatus.OPEN && ( +
+ +
+ )} + + {result && ( +
+
Results
+
+ + + + + + + + + + {result.map((r, i) => ( + + + + + + ))} + +
RankCandidateVotes
{i + 1}{r.candidate}{r.votes}
+
+
+ )} ); diff --git a/packages/nextjs/components/card/VoteCard.tsx b/packages/nextjs/components/card/VoteCard.tsx index 97a78a6..fe937dd 100644 --- a/packages/nextjs/components/card/VoteCard.tsx +++ b/packages/nextjs/components/card/VoteCard.tsx @@ -9,9 +9,10 @@ type VoteCardProps = { onChange: (checked: boolean, votes: number) => void; setIsInvalid: (value: boolean) => void; isInvalid: boolean; + pollOpen: boolean; }; -const VoteCard = ({ index, candidate, onChange, pollType, isInvalid, setIsInvalid }: VoteCardProps) => { +const VoteCard = ({ index, candidate, onChange, pollType, isInvalid, setIsInvalid, pollOpen }: VoteCardProps) => { const [selected, setSelected] = useState(false); const [votes, setVotes] = useState(0); const votesFieldRef = useRef(null); @@ -19,45 +20,47 @@ const VoteCard = ({ index, candidate, onChange, pollType, isInvalid, setIsInvali return ( <>
- { - console.log(e.target.checked); - setSelected(e.target.checked); - if (e.target.checked) { - switch (pollType) { - case PollType.SINGLE_VOTE: - onChange(true, 1); - break; - case PollType.MULTIPLE_VOTE: - onChange(true, 1); - break; - case PollType.WEIGHTED_MULTIPLE_VOTE: - if (votes) { - onChange(true, votes); - } else { - setIsInvalid(true); - } - break; - } - } else { - onChange(false, 0); - setIsInvalid(false); - setVotes(0); - if (votesFieldRef.current) { - votesFieldRef.current.value = ""; + {pollOpen && ( + { + console.log(e.target.checked); + setSelected(e.target.checked); + if (e.target.checked) { + switch (pollType) { + case PollType.SINGLE_VOTE: + onChange(true, 1); + break; + case PollType.MULTIPLE_VOTE: + onChange(true, 1); + break; + case PollType.WEIGHTED_MULTIPLE_VOTE: + if (votes) { + onChange(true, votes); + } else { + setIsInvalid(true); + } + break; + } + } else { + onChange(false, 0); + setIsInvalid(false); + setVotes(0); + if (votesFieldRef.current) { + votesFieldRef.current.value = ""; + } } - } - }} - name={pollType === PollType.SINGLE_VOTE ? "candidate-votes" : `candidate-votes-${index}`} - /> + }} + name={pollType === PollType.SINGLE_VOTE ? "candidate-votes" : `candidate-votes-${index}`} + /> + )} -
{candidate}
+
{candidate}
- {pollType === PollType.WEIGHTED_MULTIPLE_VOTE && ( + {pollOpen && pollType === PollType.WEIGHTED_MULTIPLE_VOTE && (