diff --git a/democrasite-frontend/components/Bill.tsx b/democrasite-frontend/components/Bill.tsx
index 5a7efce..b9071f2 100644
--- a/democrasite-frontend/components/Bill.tsx
+++ b/democrasite-frontend/components/Bill.tsx
@@ -1,6 +1,9 @@
+"use client";
+
import { Title, Text, Anchor, Divider, Stack, Container } from "@mantine/core";
import { type Bill } from "@/lib/models";
import { VoteButtons } from "@/components";
+import { SessionProvider } from "next-auth/react";
export function Bill({ bill }: { bill: Bill }) {
return (
@@ -28,13 +31,15 @@ export function Bill({ bill }: { bill: Bill }) {
-{bill.pullRequest.deletions}
-
+
+
+
);
}
diff --git a/democrasite-frontend/components/BillList.tsx b/democrasite-frontend/components/BillList.tsx
index 119ceed..baddeab 100644
--- a/democrasite-frontend/components/BillList.tsx
+++ b/democrasite-frontend/components/BillList.tsx
@@ -1,14 +1,6 @@
-"use client";
-
-// TODO: This has to be a client component because it uses SessionProvider
-// This could be avoided by using getServerSession in Bill.tsx but that would
-// require making Bill async, which would also force this to be a client component
-// due to Mantine. It may be worth it to ugrapde to Auth.js v5 before it leaves beta
-// to make sessions more convenient.
import { Card, Grid, GridCol } from "@mantine/core";
import { Bill } from "@/components";
import type { Bill as BillType } from "@/lib/models";
-import { SessionProvider } from "next-auth/react";
export function BillList({ bill_list }: { bill_list: BillType[] }) {
const cards = bill_list.map((bill: BillType) => (
@@ -19,9 +11,5 @@ export function BillList({ bill_list }: { bill_list: BillType[] }) {
));
- return (
-
- {cards}
-
- );
+ return {cards};
}
diff --git a/democrasite-frontend/components/VoteButtons.tsx b/democrasite-frontend/components/VoteButtons.tsx
index e4d736f..fd83fbf 100644
--- a/democrasite-frontend/components/VoteButtons.tsx
+++ b/democrasite-frontend/components/VoteButtons.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useState } from "react";
+import { Reducer, useReducer, useState } from "react";
import { ActionIcon, Group, Text, Tooltip } from "@mantine/core";
import { useSession } from "next-auth/react";
import { billsVote } from "@/lib/actions";
@@ -48,6 +48,51 @@ const ThumbIcon: React.FC = ({
);
+type VoteAction =
+ | { type: "vote"; vote: "yes" | "no" }
+ | ({ type: "update" } & VoteCounts)
+ | { type: "openTooltip"; vote: "yes" | "no"; timeout?: NodeJS.Timeout };
+
+type VoteState = {
+ vote: "yes" | "no" | null;
+ yesVotes: number;
+ noVotes: number;
+ yesTooltipOpenTimeout?: NodeJS.Timeout;
+ noTooltipOpenTimeout?: NodeJS.Timeout;
+};
+
+const voteReducer: Reducer = (state, action) => {
+ switch (action.type) {
+ case "vote": {
+ const yesVotesDiff =
+ state.vote === "yes" ? -1 : action.vote === "yes" ? 1 : 0;
+ const noVotesDiff =
+ state.vote === "no" ? -1 : action.vote === "no" ? 1 : 0;
+ const newVote = state.vote === action.vote ? null : action.vote;
+ return {
+ ...state,
+ yesVotes: state.yesVotes + yesVotesDiff,
+ noVotes: state.noVotes + noVotesDiff,
+ vote: newVote,
+ };
+ }
+ case "update":
+ return { ...state, yesVotes: action.yesVotes, noVotes: action.noVotes };
+
+ case "openTooltip":
+ // set id for state.yesTooltipOpenTimeout or state.no...
+ if (state[`${action.vote}TooltipOpenTimeout`] !== undefined) {
+ clearTimeout(state[`${action.vote}TooltipOpenTimeout`]);
+ }
+ return {
+ ...state,
+ [`${action.vote}TooltipOpenTimeout`]: action.timeout,
+ };
+ default:
+ throw Error("Invalid action type");
+ }
+};
+
export function VoteButtons({
id,
disabled,
@@ -59,39 +104,41 @@ export function VoteButtons({
disabled: boolean;
userSupports: boolean | null;
} & VoteCounts) {
- const [vote, setVote] = useState<"yes" | "no" | null>(
- userSupports ? "yes" : userSupports === false ? "no" : null,
- );
- const [yesVotesCount, setYesVotes] = useState(yesVotes);
- const [noVotesCount, setNoVotes] = useState(noVotes);
- const { status } = useSession();
+ const userVote =
+ userSupports === true ? "yes" : userSupports === false ? "no" : null;
+ const [state, dispatch] = useReducer(voteReducer, {
+ vote: userVote,
+ yesVotes,
+ noVotes,
+ });
+
+ const { status: authStatus } = useSession();
// const session = await auth();
// See BillList.tsx to understand the difficulty of accessing the session
const [isVoting, setIsVoting] = useState(false);
const handleVote = async (newVote: "yes" | "no") => {
- if (status !== "authenticated") {
- alert("You must be signed in to vote");
- return;
+ if (authStatus !== "authenticated") {
+ dispatch({
+ type: "openTooltip",
+ vote: newVote,
+ timeout: setTimeout(() => {
+ dispatch({ type: "openTooltip", vote: newVote, timeout: undefined });
+ }, 3000),
+ });
}
// Disable voting while a vote is being processed
setIsVoting(true);
- const yesVotesDiff = vote === "yes" ? -1 : newVote === "yes" ? 1 : 0;
- setYesVotes((current) => current + yesVotesDiff);
- const noVotesDiff = vote === "no" ? -1 : newVote === "no" ? 1 : 0;
- setNoVotes((current) => current + noVotesDiff);
-
- setVote((currentVote) => (currentVote === newVote ? null : newVote));
+ dispatch({ type: "vote", vote: newVote });
try {
- const { yesVotes, noVotes } = await billsVote({
+ const voteCounts = await billsVote({
id,
vote: { support: newVote === "yes" },
});
- setYesVotes(yesVotes);
- setNoVotes(noVotes);
+ dispatch({ type: "update", ...voteCounts });
} catch (error) {
console.error("Failed to cast vote", error);
} finally {
@@ -101,37 +148,26 @@ export function VoteButtons({
};
function VoteButton({ voteType }: { voteType: "yes" | "no" }) {
- const [tooltipOpen, setTooltipOpen] = useState(false);
+ const loading = !disabled && (isVoting || authStatus === "loading");
return (
{
- if (status !== "authenticated") {
- setTooltipOpen((current) => {
- if (!current) {
- setTimeout(() => {
- setTooltipOpen(false);
- }, 3000);
- }
- return !current;
- });
- return;
- }
void handleVote(voteType);
}}
>
@@ -142,10 +178,10 @@ export function VoteButtons({
- {yesVotesCount}
+ {state.yesVotes}
- {noVotesCount}
+ {state.noVotes}