Skip to content

Commit d1d7d59

Browse files
committed
feat: points & rank based voting
1 parent 842f129 commit d1d7d59

File tree

6 files changed

+389
-129
lines changed

6 files changed

+389
-129
lines changed

platforms/eVoting/src/app/(app)/[id]/page.tsx

Lines changed: 239 additions & 104 deletions
Large diffs are not rendered by default.

platforms/eVoting/src/app/(app)/create/page.tsx

Lines changed: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,7 @@ export default function CreatePoll() {
6464
},
6565
});
6666

67-
handleSubmit((data) => {
68-
console.log("Form submitted:", data);
69-
console.log(data);
70-
});
67+
7168

7269
const watchedMode = watch("mode");
7370
const watchedVisibility = watch("visibility");
@@ -166,14 +163,14 @@ export default function CreatePoll() {
166163
}
167164
className="mt-2"
168165
>
169-
<div className="flex items-center space-x-4">
166+
<div className="grid grid-cols-3 gap-4">
170167
<Label className="flex items-center cursor-pointer">
171168
<RadioGroupItem
172169
value="normal"
173170
className="sr-only"
174171
/>
175172
<div
176-
className={`border-2 rounded-lg p-4 flex-1 transition-all ${
173+
className={`border-2 rounded-lg p-4 w-full h-24 transition-all ${
177174
watchedMode === "normal"
178175
? "border-(--crimson) bg-(--crimson-50)"
179176
: "border-gray-300 hover:border-(--crimson)"
@@ -183,10 +180,10 @@ export default function CreatePoll() {
183180
<CircleUser className="text-(--crimson) w-6 h-6 mr-3" />
184181
<div>
185182
<div className="font-semibold text-gray-900">
186-
1P 1V
183+
Simple
187184
</div>
188185
<div className="text-sm text-gray-600">
189-
One person, one vote
186+
Select one option to vote for
190187
</div>
191188
</div>
192189
</div>
@@ -199,7 +196,7 @@ export default function CreatePoll() {
199196
className="sr-only"
200197
/>
201198
<div
202-
className={`border-2 rounded-lg p-4 flex-1 transition-all ${
199+
className={`border-2 rounded-lg p-4 w-full h-24 transition-all ${
203200
watchedMode === "point"
204201
? "border-(--crimson) bg-(--crimson-50)"
205202
: "border-gray-300 hover:border-(--crimson)"
@@ -225,7 +222,7 @@ export default function CreatePoll() {
225222
className="sr-only"
226223
/>
227224
<div
228-
className={`border-2 rounded-lg p-4 flex-1 transition-all ${
225+
className={`border-2 rounded-lg p-4 w-full h-24 transition-all ${
229226
watchedMode === "rank"
230227
? "border-(--crimson) bg-(--crimson-50)"
231228
: "border-gray-300 hover:border-(--crimson)"
@@ -249,6 +246,64 @@ export default function CreatePoll() {
249246
</RadioGroup>
250247
</div>
251248

249+
<div>
250+
<Label className="text-sm font-semibold text-gray-700">
251+
Voting Weight
252+
</Label>
253+
<RadioGroup
254+
value=""
255+
disabled
256+
className="mt-2"
257+
>
258+
<div className="grid grid-cols-2 gap-4">
259+
<Label className="flex items-center cursor-not-allowed opacity-50">
260+
<RadioGroupItem
261+
value="1p1v"
262+
className="sr-only"
263+
disabled
264+
/>
265+
<div className="border-2 border-gray-300 rounded-lg p-4 w-full h-24 bg-gray-50">
266+
<div className="flex items-center">
267+
<CircleUser className="text-gray-400 w-6 h-6 mr-3" />
268+
<div>
269+
<div className="font-semibold text-gray-500">
270+
1P 1V
271+
</div>
272+
<div className="text-sm text-gray-400">
273+
One person, one vote
274+
</div>
275+
</div>
276+
</div>
277+
</div>
278+
</Label>
279+
280+
<Label className="flex items-center cursor-not-allowed opacity-50">
281+
<RadioGroupItem
282+
value="reputation"
283+
className="sr-only"
284+
disabled
285+
/>
286+
<div className="border-2 border-gray-300 rounded-lg p-4 w-full h-24 bg-gray-50">
287+
<div className="flex items-center">
288+
<ChartLine className="text-gray-400 w-6 h-6 mr-3" />
289+
<div>
290+
<div className="font-semibold text-gray-500">
291+
eReputation Weighted
292+
</div>
293+
<div className="text-sm text-gray-400">
294+
Votes weighted by eReputation
295+
</div>
296+
</div>
297+
</div>
298+
</div>
299+
</Label>
300+
</div>
301+
</RadioGroup>
302+
<p className="mt-2 text-sm text-gray-500 italic">
303+
Coming soon - currently disabled
304+
</p>
305+
</div>
306+
252307
<div>
253308
<Label className="text-sm font-semibold text-gray-700">
254309
Vote Visibility
@@ -263,14 +318,14 @@ export default function CreatePoll() {
263318
}
264319
className="mt-2"
265320
>
266-
<div className="flex items-center space-x-4">
321+
<div className="grid grid-cols-2 gap-4">
267322
<Label className="flex items-center cursor-pointer">
268323
<RadioGroupItem
269324
value="public"
270325
className="sr-only"
271326
/>
272327
<div
273-
className={`border-2 rounded-lg p-4 flex-1 transition-all ${
328+
className={`border-2 rounded-lg p-4 w-full h-24 transition-all ${
274329
watchedVisibility === "public"
275330
? "border-(--crimson) bg-(--crimson-50)"
276331
: "border-gray-300 hover:border-(--crimson)"
@@ -296,7 +351,7 @@ export default function CreatePoll() {
296351
className="sr-only"
297352
/>
298353
<div
299-
className={`border-2 rounded-lg p-4 flex-1 transition-all ${
354+
className={`border-2 rounded-lg p-4 w-full h-24 transition-all ${
300355
watchedVisibility === "private"
301356
? "border-(--crimson) bg-(--crimson-50)"
302357
: "border-gray-300 hover:border-(--crimson)"

platforms/eVoting/src/app/(app)/page.tsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,14 @@ export default function Home() {
117117
{poll.title}
118118
</h3>
119119
<div className="flex items-center space-x-2">
120+
<Badge
121+
variant="secondary"
122+
className="text-xs"
123+
>
124+
{poll.mode === "normal" ? "Single Choice" :
125+
poll.mode === "rank" ? "Ranked" :
126+
poll.mode === "point" ? "Points" : "Unknown"}
127+
</Badge>
120128
<Badge
121129
variant={
122130
poll.visibility ===
@@ -151,7 +159,9 @@ export default function Home() {
151159
</Badge>
152160
</div>
153161
<div className="text-sm text-gray-500">
154-
{poll.votes?.length || 0} votes
162+
{poll.mode === "rank"
163+
? `${poll.votes?.length || 0} points`
164+
: `${poll.votes?.length || 0} votes`}
155165
</div>
156166
<Button
157167
asChild
@@ -203,6 +213,14 @@ export default function Home() {
203213
{poll.title}
204214
</h3>
205215
<div className="flex items-center space-x-2">
216+
<Badge
217+
variant="secondary"
218+
className="text-xs"
219+
>
220+
{poll.mode === "normal" ? "Single Choice" :
221+
poll.mode === "rank" ? "Ranked" :
222+
poll.mode === "point" ? "Points" : "Unknown"}
223+
</Badge>
206224
<Badge
207225
variant={
208226
poll.visibility ===
@@ -292,6 +310,14 @@ export default function Home() {
292310
{poll.title}
293311
</h3>
294312
<div className="flex items-center space-x-2">
313+
<Badge
314+
variant="secondary"
315+
className="text-xs"
316+
>
317+
{poll.mode === "normal" ? "Single Choice" :
318+
poll.mode === "rank" ? "Ranked" :
319+
poll.mode === "point" ? "Points" : "Unknown"}
320+
</Badge>
295321
<Badge
296322
variant={
297323
poll.visibility ===

platforms/eVoting/src/lib/pollApi.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,10 @@ export const pollApi = {
8686
},
8787

8888
// Submit a vote
89-
submitVote: async (pollId: string, optionId: number): Promise<Vote> => {
89+
submitVote: async (pollId: string, voteData: any): Promise<Vote> => {
9090
const response = await apiClient.post("/api/votes", {
9191
pollId,
92-
optionId
92+
...voteData
9393
});
9494
return response.data;
9595
},

platforms/evoting-api/src/controllers/VoteController.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ export class VoteController {
1010

1111
createVote = async (req: Request, res: Response) => {
1212
try {
13-
const { pollId, optionId } = req.body;
13+
const { pollId, optionId, points, ranks } = req.body;
1414
const userId = (req as any).user.id;
1515

1616
const vote = await this.voteService.createVote({
1717
pollId,
1818
userId,
19-
optionId
19+
optionId,
20+
points,
21+
ranks
2022
});
2123

2224
res.status(201).json(vote);

platforms/evoting-api/src/services/VoteService.ts

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ export class VoteService {
1818
async createVote(voteData: {
1919
pollId: string;
2020
userId: string;
21-
optionId: number;
21+
optionId?: number;
22+
points?: { [key: number]: number };
23+
ranks?: { [key: number]: number };
2224
}): Promise<Vote> {
2325
const poll = await this.pollRepository.findOne({
2426
where: { id: voteData.pollId }
@@ -48,16 +50,42 @@ export class VoteService {
4850
throw new Error("User has already voted on this poll");
4951
}
5052

53+
let voteDataToStore;
54+
if (voteData.optionId !== undefined) {
55+
// Normal voting mode
56+
voteDataToStore = {
57+
mode: "normal" as const,
58+
data: [voteData.optionId.toString()]
59+
};
60+
} else if (voteData.ranks) {
61+
// Ranked choice voting mode - convert to points (50, 35, 15)
62+
const rankData = Object.entries(voteData.ranks).map(([rank, optionIndex]) => {
63+
const rankNum = parseInt(rank);
64+
let points = 0;
65+
if (rankNum === 1) points = 50;
66+
else if (rankNum === 2) points = 35;
67+
else if (rankNum === 3) points = 15;
68+
69+
return {
70+
option: poll.options[optionIndex],
71+
points: points
72+
};
73+
});
74+
voteDataToStore = {
75+
mode: "rank" as const,
76+
data: rankData
77+
};
78+
} else {
79+
throw new Error("Invalid vote data");
80+
}
81+
5182
const vote = this.voteRepository.create({
5283
poll,
5384
user,
5485
pollId: voteData.pollId,
5586
userId: voteData.userId,
5687
voterId: voteData.userId,
57-
data: {
58-
mode: "normal" as const,
59-
data: [voteData.optionId.toString()]
60-
}
88+
data: voteDataToStore
6189
});
6290

6391
return await this.voteRepository.save(vote);
@@ -98,22 +126,36 @@ export class VoteService {
98126

99127
votes.forEach(vote => {
100128
if (vote.data.mode === "normal" && Array.isArray(vote.data.data)) {
129+
// Normal voting: count each option
101130
vote.data.data.forEach(optionIdStr => {
102131
const optionId = parseInt(optionIdStr);
103132
if (!isNaN(optionId) && optionId >= 0 && optionId < poll.options.length) {
104133
voteCounts[optionId]++;
105134
}
106135
});
136+
} else if (vote.data.mode === "rank" && Array.isArray(vote.data.data)) {
137+
// Ranked voting: sum points for each option (50, 35, 15)
138+
vote.data.data.forEach((rankData: any) => {
139+
const optionIndex = poll.options.indexOf(rankData.option);
140+
if (optionIndex >= 0) {
141+
voteCounts[optionIndex] += rankData.points || 0;
142+
}
143+
});
107144
}
108145
});
109146

147+
// Calculate total for percentage calculation
148+
const total = poll.mode === "rank"
149+
? Object.values(voteCounts).reduce((sum, count) => sum + count, 0)
150+
: votes.length;
151+
110152
return {
111153
poll,
112-
totalVotes: votes.length,
154+
totalVotes: total,
113155
results: poll.options.map((option, index) => ({
114156
option,
115157
votes: voteCounts[index] || 0,
116-
percentage: votes.length > 0 ? ((voteCounts[index] || 0) / votes.length) * 100 : 0
158+
percentage: total > 0 ? ((voteCounts[index] || 0) / total) * 100 : 0
117159
}))
118160
};
119161
}

0 commit comments

Comments
 (0)