Skip to content

Commit

Permalink
added result leaderboard
Browse files Browse the repository at this point in the history
  • Loading branch information
yashgo0018 committed Apr 28, 2024
1 parent 6fc123c commit dfdae05
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 53 deletions.
1 change: 1 addition & 0 deletions packages/nextjs/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@
NEXT_PUBLIC_ALCHEMY_API_KEY=
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=
NEXT_PUBLIC_PINATA_JWT=
NEXT_PUBLIC_PINATA_GATEWAY=
10 changes: 9 additions & 1 deletion packages/nextjs/app/admin/page.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -52,7 +53,7 @@ export default function AdminPage() {
</tr>
</thead>
<tbody>
{polls.map((poll: any) => (
{polls.map(poll => (
<tr key={poll.id} className="pt-10 text-center">
<td>{poll.name}</td>
<td>{new Date(Number(poll.startTime) * 1000).toLocaleString()}</td>
Expand All @@ -65,6 +66,13 @@ export default function AdminPage() {
(Required Actions)
</button>
</>
) : poll.status == PollStatus.RESULT_COMPUTED ? (
<>
{poll.status}{" "}
<Link href={`/polls/${poll.id}`} className="text-accent underline">
(View Results)
</Link>
</>
) : (
poll.status
)}
Expand Down
96 changes: 80 additions & 16 deletions packages/nextjs/app/polls/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -28,6 +30,8 @@ export default function PollDetail() {
const [isVotesInvalid, setIsVotesInvalid] = useState<Record<number, boolean>>({});

const isAnyInvalid = Object.values(isVotesInvalid).some(v => v);
const [result, setResult] = useState<{ candidate: string; votes: number }[] | null>(null);
const [status, setStatus] = useState<PollStatus>();

useEffect(() => {
if (!poll || !poll.metadata) {
Expand All @@ -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({
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -188,6 +223,7 @@ export default function PollDetail() {
{poll?.options.map((candidate, index) => (
<div className="pb-5 flex" key={index}>
<VoteCard
pollOpen={status === PollStatus.OPEN}
index={index}
candidate={candidate}
clicked={false}
Expand All @@ -198,15 +234,43 @@ export default function PollDetail() {
/>
</div>
))}
<div className={`mt-2 shadow-2xl`}>
<button
onClick={castVote}
disabled={!true}
className="hover:border-black border-2 border-accent w-full text-lg text-center bg-accent py-3 rounded-xl font-bold"
>
{true ? "Vote Now" : "Voting Closed"}{" "}
</button>
</div>
{status === PollStatus.OPEN && (
<div className={`mt-2 shadow-2xl`}>
<button
onClick={castVote}
disabled={!true}
className="hover:border-black border-2 border-accent w-full text-lg text-center bg-accent py-3 rounded-xl font-bold"
>
{true ? "Vote Now" : "Voting Closed"}{" "}
</button>
</div>
)}

{result && (
<div className="mt-5">
<div className="text-2xl font-bold">Results</div>
<div className="mt-3">
<table className="border-separate w-full mt-7 mb-4">
<thead>
<tr className="text-lg font-extralight">
<th className="border border-slate-600 bg-primary">Rank</th>
<th className="border border-slate-600 bg-primary">Candidate</th>
<th className="border border-slate-600 bg-primary">Votes</th>
</tr>
</thead>
<tbody>
{result.map((r, i) => (
<tr key={i} className="text-center">
<td>{i + 1}</td>
<td>{r.candidate}</td>
<td>{r.votes}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
</div>
);
Expand Down
75 changes: 39 additions & 36 deletions packages/nextjs/components/card/VoteCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,55 +9,58 @@ 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<HTMLInputElement>(null);

return (
<>
<div className="bg-primary flex w-full px-2 py-2 rounded-lg">
<input
type={pollType === PollType.SINGLE_VOTE ? "radio" : "checkbox"}
className="mr-2"
value={index}
onChange={e => {
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 && (
<input
type={pollType === PollType.SINGLE_VOTE ? "radio" : "checkbox"}
className="mr-2"
value={index}
onChange={e => {
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}`}
/>
)}

<div>{candidate}</div>
<div className={!pollOpen ? "ml-2" : ""}>{candidate}</div>
</div>

{pollType === PollType.WEIGHTED_MULTIPLE_VOTE && (
{pollOpen && pollType === PollType.WEIGHTED_MULTIPLE_VOTE && (
<input
ref={votesFieldRef}
type="number"
Expand Down
6 changes: 6 additions & 0 deletions packages/nextjs/utils/pinata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,9 @@ export async function uploadToPinata(jsonData: any) {

return data.IpfsHash;
}

export async function getDataFromPinata(hash: string) {
const url = `${process.env.NEXT_PUBLIC_PINATA_GATEWAY || "https://gateway.pinata.cloud"}/ipfs/${hash}`;
const { data } = await axios.get(url);
return data;
}

0 comments on commit dfdae05

Please sign in to comment.