Skip to content

Commit 40c4e2b

Browse files
committed
add simple vote counting
1 parent 18fecd8 commit 40c4e2b

File tree

10 files changed

+172
-20
lines changed

10 files changed

+172
-20
lines changed

client/app/components/admin/nominations/index.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,23 @@ import NominationCard from "./card";
44
import { BASE_URL } from "@/lib/utils";
55
import { useToken } from "@/lib/user";
66

7+
interface Candidate {
8+
id: number;
9+
name: string;
10+
isMember: boolean;
11+
email: string;
12+
graduation: string;
13+
position_id: number;
14+
attend: boolean;
15+
club_benefit: string;
16+
initiative: string;
17+
join_reason: string;
18+
other_clubs: string;
19+
past_clubs: string;
20+
say_something: string;
21+
student_num: string | number;
22+
}
23+
724
const Nominations = () => {
825
const { hash } = useLocation();
926
const token = useToken();
@@ -36,7 +53,7 @@ const Nominations = () => {
3653

3754
return (
3855
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
39-
{candidates?.map((data) => (
56+
{candidates?.map((data: Candidate) => (
4057
<NominationCard key={data.name} {...data} />
4158
))}
4259
</div>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import {
2+
Card,
3+
CardContent,
4+
CardDescription,
5+
CardHeader,
6+
CardTitle,
7+
} from "@/components/ui/card";
8+
import type { Candidate } from "@/components/vote/queries";
9+
10+
interface ResultsProps {
11+
candidate: Candidate;
12+
stats: any
13+
}
14+
15+
const ResultCard = (props: ResultsProps) => {
16+
return (
17+
<Card className="relative gap-4">
18+
<CardHeader>
19+
<CardTitle>{props.candidate.name}</CardTitle>
20+
<CardDescription>{props.candidate.student_num}</CardDescription>
21+
</CardHeader>
22+
<CardContent className="flex flex-col gap-2">
23+
{JSON.stringify(props.stats)}
24+
</CardContent>
25+
</Card>
26+
);
27+
};
28+
29+
export default ResultCard;
30+
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { useLocation } from "react-router";
2+
import { useQuery } from "@tanstack/react-query";
3+
import ResultCard from "./card";
4+
import { BASE_URL } from "@/lib/utils";
5+
import { useToken } from "@/lib/user";
6+
import type { ElectedCandidate } from "@/components/vote/queries";
7+
8+
const Results = () => {
9+
const { hash } = useLocation();
10+
const token = useToken();
11+
const id = hash.split("?")[0].split("=")[1];
12+
13+
const { data: electedData, isLoading: isLoadingElected } = useQuery<ElectedCandidate[]>({
14+
queryKey: ["results", id],
15+
queryFn: async () => {
16+
const response = await fetch(`${BASE_URL}/results/${id}`, {
17+
headers: {
18+
"Content-Type": "application/json",
19+
Authorization: `Bearer ${token}`,
20+
},
21+
});
22+
const val = await response.json();
23+
return val;
24+
},
25+
});
26+
27+
const { data: countData, isLoading } = useQuery({
28+
queryKey: ["votes", id],
29+
queryFn: async () => {
30+
const response = await fetch(`${BASE_URL}/vote/winningvotes/${id}`, {
31+
headers: {
32+
"Content-Type": "application/json",
33+
Authorization: `Bearer ${token}`,
34+
},
35+
});
36+
const val = await response.json();
37+
return val;
38+
},
39+
});
40+
41+
if (isLoading)
42+
return (
43+
<div className="grid h-full place-items-center">
44+
<span className="material-symbols-sharp animate-spin text-9xl!">
45+
progress_activity
46+
</span>
47+
</div>
48+
);
49+
50+
return (
51+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
52+
{electedData && electedData.map(({ candidates: candidate }) => (
53+
<ResultCard key={candidate.name} candidate={candidate} stats={countData} />
54+
)) || (
55+
<div className="text-xl">{ isLoadingElected ? 'Loading results...' : 'No Results Found'}</div>
56+
)}
57+
</div>
58+
);
59+
};
60+
61+
export default Results;

client/app/components/admin/results/overview.tsx

Whitespace-only changes.

client/app/components/vote/queries.ts

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ type WSData = {
1111

1212
type RaceStatus = "closed" | "open" | "finished";
1313

14-
interface Race {
14+
export interface Race {
1515
race: {
1616
id: number;
1717
position_id: number;
@@ -21,7 +21,7 @@ interface Race {
2121
positions: Position;
2222
}
2323

24-
interface BaseCandidate {
24+
export interface Candidate {
2525
id: number;
2626
name: string;
2727
position_id: number;
@@ -33,27 +33,22 @@ interface BaseCandidate {
3333
past_clubs: string;
3434
say_something: string;
3535
student_num: string | number;
36+
nominations: Nomination[]
3637
}
3738

38-
interface Candidate extends BaseCandidate {
39-
nominations: {
40-
positions: {
41-
id: number;
42-
title: string;
43-
};
44-
}[];
39+
export interface Nomination {
40+
positions: {
41+
id: number;
42+
title: string;
43+
};
4544
}
4645

47-
interface ElectedCandidate {
46+
export interface ElectedCandidate {
4847
elected: {
4948
candidate_id: number;
5049
race_id: number;
5150
};
52-
candidates: {
53-
id: number;
54-
isMember: boolean;
55-
name: string;
56-
};
51+
candidates: Candidate
5752
}
5853

5954
export const useWS = () => {
@@ -151,7 +146,7 @@ export const useCandidates = (position_id?: number | string) => {
151146
const token = useToken();
152147

153148
// get by position id
154-
const { data: candidatesByPosition } = useQuery<BaseCandidate[]>({
149+
const { data: candidatesByPosition } = useQuery<Candidate[]>({
155150
enabled: !!position_id,
156151
refetchInterval: 3000,
157152
queryKey: ["nominees", position_id],

client/app/routes/admin.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import type { Position } from "@/lib/types";
2525
import Auth from "@/components/auth";
2626
import { Button } from "@/components/ui/button";
2727
import Seats from "@/components/admin/seat";
28+
import Results from "@/components/admin/results";
2829

2930
export function meta({}: Route.MetaArgs) {
3031
return [
@@ -109,7 +110,7 @@ export default function Admin() {
109110
CurrentView = Nominations;
110111
} else if (hash.includes("result")) {
111112
currentPage = `Result - ${title}`;
112-
CurrentView = () => <> </>;
113+
CurrentView = Results;
113114
}
114115
}
115116

src/models/db/vote-preference.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,20 @@ export function getVotePreference(
2222
);
2323
}
2424

25+
export function getVotePreferenceForCandidate(
26+
this: VotingObject,
27+
candidate_id: number
28+
) {
29+
return this.db
30+
.select()
31+
.from(votePreferencesTable)
32+
.where(
33+
and(
34+
eq(votePreferencesTable.candidate_id, candidate_id)
35+
)
36+
);
37+
}
38+
2539
export function getVotePreferencesForVote(this: VotingObject, vote_id: number) {
2640
return this.db
2741
.select()

src/models/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { getAllNominations, getNominationsForPosition, getNominationsForCandidat
88
import { getRace, getAllRaces, insertRace, updateRace, deleteRace, getCurrentRace, saveElectedForRace } from "./db/race";
99
import { getSeat, insertSeat, deleteSeat, getSeatByCode } from "./db/seat";
1010
import { countUsers, getAllUsers, insertUser, updateUser, getUser, getUserByEmail, deleteUser } from "./db/user";
11-
import { getAllVotePreferences, insertVotePreference, updateVotePreference, getVotePreference, deleteVotePreference, getVotePreferencesForVote } from "./db/vote-preference";
11+
import { getAllVotePreferences, insertVotePreference, updateVotePreference, getVotePreference, deleteVotePreference, getVotePreferencesForVote, getVotePreferenceForCandidate } from "./db/vote-preference";
1212
import { countVotesForRace, getAllVotesForRace, insertVote, updateVote, getAllVotesByUser, getVoteByUserAndRace, deleteVote, getVoteAggregateForRace } from "./db/vote";
1313
import { seedMasterSeat, devSeeds, seedPositions, seedRaces } from "./seed";
1414
import * as schema from "./schema";
@@ -276,6 +276,12 @@ export class VotingObject extends DurableObject {
276276
) {
277277
return getVotePreferencesForVote.call(this, ...args);
278278
}
279+
280+
getVotePreferenceForCandidate(
281+
...args: Parameters<typeof getVotePreferenceForCandidate>
282+
) {
283+
return getVotePreferenceForCandidate.call(this, ...args);
284+
}
279285

280286
insertVotePreference(...args: Parameters<typeof insertVotePreference>) {
281287
return insertVotePreference.call(this, ...args);

src/routes/candidate.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ app.get(
7070

7171
return c.json(data);
7272
} else {
73-
const data = DBData.map(({ candidates }) => ({
73+
const data = DBData.map(({ candidates, nominations }) => ({
7474
id: candidates?.id,
7575
name: candidates?.name,
7676
join_reason: candidates?.join_reason,
@@ -80,6 +80,7 @@ app.get(
8080
past_clubs: candidates?.past_clubs,
8181
attend: candidates?.attend,
8282
say_something: candidates?.say_something,
83+
nominations
8384
}));
8485
return c.json(data);
8586
}
@@ -149,6 +150,7 @@ app.post("/", requireAdmin, zValidator("json", insertSchema), async (c) => {
149150
);
150151
return c.json({ message: "Created successfully" });
151152
} catch (err) {
153+
console.error(err)
152154
throw new HTTPException(400, { message: "Failed to create candidate" });
153155
}
154156
});

src/routes/vote.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const app = factory.createApp();
88

99
app.get(
1010
"/count/:id",
11+
requireAdmin,
1112
zValidator(
1213
"param",
1314
z.object({
@@ -24,6 +25,31 @@ app.get(
2425
}
2526
);
2627

28+
app.get(
29+
"/winningvotes/:id",
30+
requireAdmin,
31+
zValidator(
32+
"param",
33+
z.object({
34+
id: z.number({ coerce: true }),
35+
})
36+
),
37+
async (c) => {
38+
const { id } = c.req.valid("param");
39+
const winners = await c.var.STUB.getElectedForRace(id)
40+
const elected_preferences = (await Promise.all(winners.map(winner => {
41+
return c.var.STUB.getVotePreferenceForCandidate(winner.elected.candidate_id)
42+
}))).map(preferences => {
43+
return preferences.reduce<{ [key: number]: number }>((acc, curr) => {
44+
if (!acc[curr.preference]) acc[curr.preference] = 0
45+
acc[curr.preference] += 1
46+
return acc
47+
}, {})
48+
})
49+
return c.json(elected_preferences);
50+
}
51+
);
52+
2753
app.post(
2854
"/:race_id",
2955
zValidator(

0 commit comments

Comments
 (0)