Skip to content

Commit a494ee3

Browse files
committed
fix(Proposal): support voter counting of all snapshot voting types; display results in tab if needed
1 parent 432f089 commit a494ee3

File tree

6 files changed

+94
-75
lines changed

6 files changed

+94
-75
lines changed

components/Proposal/ProposalOptions.tsx

+22-22
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,22 @@ import { Tooltip } from "flowbite-react";
22
import { useProposalVotes } from "@/utils/hooks/snapshot/Proposals";
33
import { SnapshotProposal } from "@/models/SnapshotTypes";
44
import { formatNumber } from "@/utils/functions/NumberFormatter";
5+
import { processChoicesCount } from "@/utils/functions/snapshotUtil";
56

6-
// BasicVoting: For Against Abstain
7-
const SUPPORTED_VOTING_TYPES_FOR_GROUP = ["basic", "single-choice", "approval"];
7+
function merge(arrayOfObjects: { [choice: string]: number }[]) {
8+
return arrayOfObjects.reduce((result, currentObj) => {
9+
for (let key in currentObj) {
10+
if (result.hasOwnProperty(key)) {
11+
// If key exists, add values
12+
result[key] += currentObj[key];
13+
} else {
14+
// If key doesn't exist, create new key-value pair
15+
result[key] = currentObj[key];
16+
}
17+
}
18+
return result;
19+
}, {});
20+
}
821

922
export default function ProposalOptions({
1023
proposal,
@@ -19,7 +32,7 @@ export default function ProposalOptions({
1932
"created",
2033
"",
2134
isOverview,
22-
proposal.votes,
35+
proposal.votes
2336
);
2437

2538
let scores = proposal?.scores
@@ -30,22 +43,12 @@ export default function ProposalOptions({
3043
// sort by score desc
3144
.sort((a, b) => b.score - a.score);
3245

33-
const displayVotesByGroup = SUPPORTED_VOTING_TYPES_FOR_GROUP.includes(
34-
proposal.type,
35-
);
46+
const displayVotesByGroup = true;
3647
let votesGroupByChoice: { [choice: string]: number } = {};
3748
if (!isOverview && displayVotesByGroup) {
3849
// iterate votesData and group by choice
39-
votesGroupByChoice = data?.votesData.reduce(
40-
(acc: { [choice: string]: number }, vote) => {
41-
const choice = vote.choice;
42-
if (!acc[choice]) {
43-
acc[choice] = 0;
44-
}
45-
acc[choice]++;
46-
return acc;
47-
},
48-
{},
50+
votesGroupByChoice = merge(
51+
data?.votesData.map((d) => processChoicesCount(proposal.type, d.choice))
4952
);
5053
}
5154

@@ -57,17 +60,14 @@ export default function ProposalOptions({
5760
scores.map(({ score, index }) => (
5861
<div
5962
key={index}
60-
className="overflow-hidden rounded-lg bg-white px-4 py-5 shadow sm:p-6"
63+
className="overflow-hidden rounded-lg bg-white p-3 shadow"
6164
>
6265
<Tooltip content={proposal?.choices[index]} trigger="hover">
6366
<dt className="truncate text-sm font-medium text-gray-500">
6467
{proposal?.choices[index]}
6568
</dt>
6669
</Tooltip>
67-
<Tooltip
68-
content={`${((score * 100) / proposal.scores_total).toFixed(2)}%`}
69-
trigger="hover"
70-
>
70+
<div>
7171
{/* <dd className="mt-1 text-3xl tracking-tight font-semibold text-gray-900">{(proposal.voteByChoice[choice]*100/proposal.scores_total).toFixed(2)}%</dd> */}
7272
<dd className="mt-1 text-3xl font-semibold tracking-tight text-gray-900">
7373
{formatNumber(score)}
@@ -82,7 +82,7 @@ export default function ProposalOptions({
8282
{((score * 100) / proposal.scores_total).toFixed()}%
8383
</span>
8484
)}
85-
</Tooltip>
85+
</div>
8686
</div>
8787
))}
8888
</dl>

components/Proposal/ProposalTabs.tsx

+39-28
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,43 @@
1-
import {
2-
useContext,
3-
useEffect,
4-
useState,
5-
useMemo,
6-
} from "react";
1+
import { useContext, useEffect, useState, useMemo } from "react";
72
import { ProposalContext } from "./context/ProposalContext";
83
import { useRouter } from "next/router";
94
import { classNames } from "@/utils/functions/tailwind";
105
import ProposalContent from "./ProposalContent";
116
import ProposalActivityFeeds from "./sub/ProposalActivityFeeds";
127
import ProposalMetadata from "./sub/ProposalMetadata";
8+
import { SnapshotVotingType } from "@/models/SnapshotTypes";
9+
import ProposalOptions from "./ProposalOptions";
1310

14-
15-
const TABS = ["Content", "Activity", "Actions"] as const;
11+
const TABS = ["Content", "Activity", "Actions", "Results"] as const;
1612
const LG_MIN_WIDTH = 1024;
1713

1814
export default function ProposalTabs() {
1915
const router = useRouter();
20-
const [query, setQuery] = useState<typeof TABS[number]>("Content");
21-
const { commonProps } = useContext(ProposalContext);
16+
const [query, setQuery] = useState<(typeof TABS)[number]>("Content");
17+
const { commonProps, proposalInfo } = useContext(ProposalContext);
2218

2319
// Memoize filtered tabs
2420
const filteredTabs = useMemo(() => {
25-
const tabs = TABS.filter(t => t !== "Content");
21+
const tabs = TABS.filter((t) => t !== "Content");
2622
if (commonProps.actions.length === 0) {
27-
return tabs.filter(t => t !== "Actions");
23+
return tabs.filter((t) => t !== "Actions");
24+
}
25+
if (
26+
proposalInfo === undefined ||
27+
proposalInfo?.type === SnapshotVotingType.BASIC
28+
) {
29+
return tabs.filter((t) => t !== "Results");
2830
}
2931
return tabs;
3032
}, [commonProps.actions]);
3133

3234
useEffect(() => {
3335
const correctContentTab = () => {
34-
if (window.innerWidth >= LG_MIN_WIDTH && query === "Content" && router.isReady) {
36+
if (
37+
window.innerWidth >= LG_MIN_WIDTH &&
38+
query === "Content" &&
39+
router.isReady
40+
) {
3541
handleTabChange("Activity");
3642
}
3743
};
@@ -51,21 +57,18 @@ export default function ProposalTabs() {
5157
};
5258
}, [query, router.isReady]);
5359

54-
const handleTabChange = (tab: typeof TABS[number]) => {
60+
const handleTabChange = (tab: (typeof TABS)[number]) => {
5561
setQuery(tab);
56-
router.replace({
57-
pathname: router.pathname,
58-
query: { ...router.query, tab },
59-
});
6062
};
6163

62-
const getTabClasses = useMemo(() => (isActive: boolean) =>
63-
classNames(
64-
isActive
65-
? "border-indigo-500 text-indigo-600"
66-
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
67-
"whitespace-nowrap border-b-2 p-1 text-sm font-medium cursor-pointer"
68-
),
64+
const getTabClasses = useMemo(
65+
() => (isActive: boolean) =>
66+
classNames(
67+
isActive
68+
? "border-indigo-500 text-indigo-600"
69+
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
70+
"whitespace-nowrap border-b-2 p-1 text-sm font-medium cursor-pointer"
71+
),
6972
[]
7073
);
7174

@@ -88,7 +91,7 @@ export default function ProposalTabs() {
8891

8992
{/* Small screen nav */}
9093
<nav aria-label="Tabs" className="-mb-px flex lg:hidden space-x-8">
91-
{TABS.map((tab) => (
94+
{[TABS[0], ...filteredTabs].map((tab) => (
9295
<button
9396
key={tab}
9497
onClick={() => handleTabChange(tab)}
@@ -102,13 +105,15 @@ export default function ProposalTabs() {
102105
</div>
103106

104107
<div>
105-
{(query === "Content" || (!query && window.innerWidth < LG_MIN_WIDTH)) && (
108+
{(query === "Content" ||
109+
(!query && window.innerWidth < LG_MIN_WIDTH)) && (
106110
<div className="hidden mt-4 w-[90vw] max-lg:block">
107111
<ProposalContent />
108112
</div>
109113
)}
110114

111-
{(query === "Activity" || (!query && window.innerWidth >= LG_MIN_WIDTH)) && (
115+
{(query === "Activity" ||
116+
(!query && window.innerWidth >= LG_MIN_WIDTH)) && (
112117
<div className="mt-4 max-lg:w-[90vw] block">
113118
<ProposalActivityFeeds />
114119
</div>
@@ -119,6 +124,12 @@ export default function ProposalTabs() {
119124
<ProposalMetadata />
120125
</div>
121126
)}
127+
128+
{query === "Results" && proposalInfo !== undefined && (
129+
<div className="mt-4 max-lg:w-[90vw] overflow-x-auto block">
130+
<ProposalOptions proposal={proposalInfo} />
131+
</div>
132+
)}
122133
</div>
123134
</div>
124135
);

components/Proposal/ProposalVoteOverview.tsx

+4-9
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from "next-query-params";
1010
import ColorBar from "@/components/common/ColorBar";
1111
import { formatNumber } from "@/utils/functions/NumberFormatter";
12+
import { SnapshotVotingType } from "@/models/SnapshotTypes";
1213

1314
export default function ProposalVoteOverview({
1415
temperatureCheckVotes,
@@ -25,13 +26,7 @@ export default function ProposalVoteOverview({
2526

2627
const proposalType = proposalInfo?.type ?? "";
2728
const threshold = commonProps?.minTokenPassingAmount ?? 0;
28-
29-
const isSimpleVoting = ![
30-
"approval",
31-
"ranked-choice",
32-
"quadratic",
33-
"weighted",
34-
].includes(proposalType);
29+
const isBasicVoting = proposalType === SnapshotVotingType.BASIC;
3530

3631
if (!proposalInfo) {
3732
return (
@@ -85,7 +80,7 @@ export default function ProposalVoteOverview({
8580

8681
return (
8782
<div className="mb-4">
88-
{isSimpleVoting && (
83+
{isBasicVoting && (
8984
<>
9085
<div className="flex justify-between">
9186
<p
@@ -140,7 +135,7 @@ export default function ProposalVoteOverview({
140135
</>
141136
)}
142137

143-
{!isSimpleVoting && (
138+
{!isBasicVoting && (
144139
<>
145140
<div className="flex justify-between">
146141
<p className="text-sm text-green-500">

models/SnapshotTypes.ts

+9
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,15 @@ export interface FollowedSpacesData {
9999
activeProposals: number;
100100
}
101101

102+
export enum SnapshotVotingType {
103+
BASIC = "basic", // For Against Abstain
104+
SINGLE_CHOICE = "single-choice",
105+
APPROVAL = "approval", // Approve/Disapprove on multiple options
106+
RANKED_CHOICE = "ranked-choice",
107+
QUADRATIC = "quadratic",
108+
WEIGHTED = "weighted", // give explicit weight
109+
}
110+
102111
// Model for a single proposal
103112
export interface SnapshotProposal {
104113
id: string;

pages/s/[space]/[proposal]/index.tsx

-16
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import { useProposal } from "@/utils/hooks/NanceHooks";
1414
import { useParams } from "next/navigation";
1515
import ProposalTabs from "@/components/Proposal/ProposalTabs";
1616
import ProposalHeader from "@/components/Proposal/ProposalHeader";
17-
import ProposalOptions from "@/components/Proposal/ProposalOptions";
1817
import ProposalVoteOverview from "@/components/Proposal/ProposalVoteOverview";
1918

2019
export default function NanceProposalPage() {
@@ -142,21 +141,6 @@ export default function NanceProposalPage() {
142141
<ProposalHeader />
143142
<ProposalContent />
144143
</div>
145-
146-
{/* Display Options if not basic (For Against) */}
147-
<div>
148-
{snapshotProposal &&
149-
[
150-
"approval",
151-
"ranked-choice",
152-
"quadratic",
153-
"weighted",
154-
].includes(snapshotProposal.type) && (
155-
<div className="mt-6 flow-root">
156-
<ProposalOptions proposal={snapshotProposal} />
157-
</div>
158-
)}
159-
</div>
160144
</div>
161145

162146
<section

utils/functions/snapshotUtil.ts

+20
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,26 @@ export function getColorOfChoice(choice: string | string[]) {
3838
}
3939
}
4040

41+
export function processChoicesCount(
42+
type: string | undefined,
43+
choice: any
44+
): { [k: string]: number } {
45+
if (!choice || !type) return {};
46+
if (choice === "🔐") return {}; // undefined entry appears with shutter voting
47+
if (type == "approval" || type == "ranked-choice") {
48+
const choices = choice as string[];
49+
const ret: { [k: string]: number } = {};
50+
choices.forEach((c) => (ret[c] = 1));
51+
return ret;
52+
} else if (type == "quadratic" || type == "weighted") {
53+
const obj = choice as { [key: string]: number };
54+
return obj;
55+
} else {
56+
const c = choice as string;
57+
return { [c]: 1 };
58+
}
59+
}
60+
4161
export function processChoices(
4262
type: string | undefined,
4363
choice: any

0 commit comments

Comments
 (0)