From dd7be7cc0a1ecb963f0ca02054805e615fe501ea Mon Sep 17 00:00:00 2001 From: justagoodfriend Date: Thu, 14 Nov 2024 01:47:35 -0500 Subject: [PATCH] first go with the voting pages --- src/App.tsx | 6 +- src/client/VotingClient.ts | 16 +++++ src/components/ErrorComponent.tsx | 19 ++++++ src/components/Menu.tsx | 15 ++++- src/components/Question.tsx | 101 ++++++++++++++++++++++++++++++ src/components/Settings.tsx | 33 ++++++---- src/components/VoteHistoryRow.tsx | 14 +++++ src/hooks/useVote.ts | 16 +++++ src/hooks/useVotingRecord.ts | 16 +++++ src/index.tsx | 2 +- src/pages/VotingHistory.tsx | 43 +++++++++++++ src/pages/VotingPage.tsx | 40 ++++++++++++ src/util/Types.tsx | 17 +++++ 13 files changed, 322 insertions(+), 16 deletions(-) create mode 100644 src/client/VotingClient.ts create mode 100644 src/components/ErrorComponent.tsx create mode 100644 src/components/Question.tsx create mode 100644 src/components/VoteHistoryRow.tsx create mode 100644 src/hooks/useVote.ts create mode 100644 src/hooks/useVotingRecord.ts create mode 100644 src/pages/VotingHistory.tsx create mode 100644 src/pages/VotingPage.tsx diff --git a/src/App.tsx b/src/App.tsx index 399d8b4..2170ce3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ import { jwtDecode } from "jwt-decode"; import React, { createContext, useContext, useEffect } from "react"; import { useCookies } from "react-cookie"; -import { Route, BrowserRouter as Router, Routes } from "react-router-dom"; +import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; import "./App.css"; import MemberClient from "./client/MemberClient"; import Footer from "./components/Footer"; @@ -14,6 +14,8 @@ import EventDetailsPage from "./pages/EventDetailsPage"; import Homepage from "./pages/Homepage"; import LoginPage from "./pages/LoginPage"; import UserPreference from "./pages/UserPreference"; +import VotingHistory from "./pages/VotingHistory"; +import VotingPage from "./pages/VotingPage"; import { JWTAuthToken } from "./util/Types"; export type UserID = string | null; @@ -74,6 +76,8 @@ function App() { } /> } /> } /> + } /> + } /> } /> diff --git a/src/client/VotingClient.ts b/src/client/VotingClient.ts new file mode 100644 index 0000000..df3e72a --- /dev/null +++ b/src/client/VotingClient.ts @@ -0,0 +1,16 @@ +import axios from "axios"; +import { VoteSelection } from "../util/Types"; + +export const createVote = async ( + vote_id: string, + member_id: string | undefined, + vote_selection: VoteSelection +) => { + const response = await axios.post("api/voting/createVote", { + member_id, + vote_id, + vote_selection, + }); + + return response.data; +}; diff --git a/src/components/ErrorComponent.tsx b/src/components/ErrorComponent.tsx new file mode 100644 index 0000000..5680c24 --- /dev/null +++ b/src/components/ErrorComponent.tsx @@ -0,0 +1,19 @@ +import TriangleError from "../assets/TriangleError.svg"; + +export const ErrorComponent = () => { + return ( +
+ +
+

+ Oops! Something went wrong. +

+

+ This page failed to load. Please try again another time. +

+
+
+ ); +}; + +export default ErrorComponent; diff --git a/src/components/Menu.tsx b/src/components/Menu.tsx index 407c45f..61a0a6b 100644 --- a/src/components/Menu.tsx +++ b/src/components/Menu.tsx @@ -105,17 +105,26 @@ const Menu = (): ReactElement => { Record + -
diff --git a/src/components/Question.tsx b/src/components/Question.tsx new file mode 100644 index 0000000..6fe4f9d --- /dev/null +++ b/src/components/Question.tsx @@ -0,0 +1,101 @@ +import { useMutation } from "@tanstack/react-query"; +import { useState } from "react"; +import { createVote } from "../client/VotingClient"; +import { VoteQuestion, VoteSelection } from "../util/Types"; + +interface QuestionProps { + question: VoteQuestion; + member_id: string | undefined; +} + +export const Question = ({ question, member_id }: QuestionProps) => { + const [selectedVote, setSelectedVote] = useState(); + const [error, setError] = useState(false); + const [submitted, setSubmitted] = useState(false); + + const availableOptions = [ + { value: VoteSelection.YES, label: "Yes" }, + { value: VoteSelection.NO, label: "No" }, + { value: VoteSelection.ABSTAIN, label: "Abstain" }, + ]; + + const getLabelFromSelection = (value: VoteSelection | undefined) => { + return availableOptions.find((option) => option.value === value)?.label; + }; + + const createVoteMutation = useMutation({ + mutationFn: async (selectedVote: VoteSelection) => { + return createVote(question.uuid, member_id, selectedVote); + }, + onSuccess: () => { + setSubmitted(true); + // TODO: this success should update the voting history query however + // this useMutation is setting the data too quickly to the point where + // the submission message doesn't show + }, + }); + + const castVote = async (selectedVote: VoteSelection) => { + createVoteMutation.mutateAsync(selectedVote).then((response) => { + return response; + }); + }; + + return ( +
+
+ {question.question} + {question.description} +
+ {submitted ? ( +
+ Thank you for voting. + You voted: {getLabelFromSelection(selectedVote)}. +
+ ) : ( +
+
+ {error &&
Please input a value before casting vote
} + {availableOptions.map((option, index) => { + return ( + + ); + })} +
+ +
+ )} +
+ ); +}; diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index 5d9f692..4289e02 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -1,18 +1,12 @@ -import { ReactElement, useContext } from "react"; +import { ReactElement } from "react"; import { useLocation, useNavigate } from "react-router-dom"; -import { LoginContext } from "../App"; +import { useAuth } from "../hooks/useAuth"; const Settings = (props: { useState: React.Dispatch>; }): ReactElement => { - const { setUserID } = useContext(LoginContext); let navigate = useNavigate(); - - function logout() { - localStorage.removeItem("user"); - setUserID(null); - navigate("/"); - } + const { logout } = useAuth(); function Click(route: string) { navigate(route); @@ -52,16 +46,33 @@ const Settings = (props: { > My Record - + {/*
*/}
- +
{/*
*/} diff --git a/src/components/VoteHistoryRow.tsx b/src/components/VoteHistoryRow.tsx new file mode 100644 index 0000000..6b02a67 --- /dev/null +++ b/src/components/VoteHistoryRow.tsx @@ -0,0 +1,14 @@ +import { VoteHistory } from "../util/Types"; + +interface VoteHistoryProps { + voteRow: VoteHistory; +} + +export const VoteHistoryRow = ({ voteRow }: VoteHistoryProps) => { + return ( +
+
{voteRow.question}
+
{voteRow.vote_selection}
+
+ ); +}; diff --git a/src/hooks/useVote.ts b/src/hooks/useVote.ts new file mode 100644 index 0000000..a44b005 --- /dev/null +++ b/src/hooks/useVote.ts @@ -0,0 +1,16 @@ +import { useQuery } from "@tanstack/react-query"; +import axios from "axios"; +import { VoteQuestion } from "../util/Types"; + +export default function useVoteQuestion(id: string | undefined) { + return useQuery({ + queryKey: ["voting", id], + queryFn: async (): Promise => { + const response = await axios.get( + `/api/voting/getIfMemberVoted?id=${id}` + ); + + return response.data; + }, + }); +} diff --git a/src/hooks/useVotingRecord.ts b/src/hooks/useVotingRecord.ts new file mode 100644 index 0000000..3461519 --- /dev/null +++ b/src/hooks/useVotingRecord.ts @@ -0,0 +1,16 @@ +import { useQuery } from "@tanstack/react-query"; +import axios from "axios"; +import { VoteHistory } from "../util/Types"; + +export default function useVotingRecord(id: string | undefined) { + return useQuery({ + queryKey: ["voting", "history", id], + queryFn: async (): Promise => { + const response = await axios.get( + `/api/voting/getVotingRecord?id=${id}` + ); + + return response.data; + }, + }); +} diff --git a/src/index.tsx b/src/index.tsx index 22fd3cd..7d050a4 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -9,7 +9,7 @@ import reportWebVitals from "./reportWebVitals"; import { Member } from "./util/Types"; // Create a client -const queryClient = new QueryClient(); +export const queryClient = new QueryClient(); const root = ReactDOM.createRoot( document.getElementById("root") as HTMLElement diff --git a/src/pages/VotingHistory.tsx b/src/pages/VotingHistory.tsx new file mode 100644 index 0000000..4a158ab --- /dev/null +++ b/src/pages/VotingHistory.tsx @@ -0,0 +1,43 @@ +import { ReactElement } from "react"; +import ErrorComponent from "../components/ErrorComponent"; +import Loading from "../components/Loading"; +import { VoteHistoryRow } from "../components/VoteHistoryRow"; +import { useAuth } from "../hooks/useAuth"; +import useVotingRecord from "../hooks/useVotingRecord"; + +export const VotingHistory = (): ReactElement => { + const { member } = useAuth(); + const { + status, + data: votehistory, + error, + isFetching, + } = useVotingRecord(member?.id); + + if (isFetching) { + return ; + } else if (status === "error") { + return ; + } else if (votehistory) { + return ( +
+
+
+
Question
+
Vote
+
+ + {votehistory.length === 0 ? ( + No Voting Records to Display + ) : ( + votehistory.map((voteRow) => ) + )} +
+
+ ); + } else { + return <>; + } +}; + +export default VotingHistory; diff --git a/src/pages/VotingPage.tsx b/src/pages/VotingPage.tsx new file mode 100644 index 0000000..2291f54 --- /dev/null +++ b/src/pages/VotingPage.tsx @@ -0,0 +1,40 @@ +import { ReactElement } from "react"; +import ErrorComponent from "../components/ErrorComponent"; +import Loading from "../components/Loading"; +import { Question } from "../components/Question"; +import { useAuth } from "../hooks/useAuth"; +import useVoteQuestion from "../hooks/useVote"; + +export const VotingPage = (): ReactElement => { + const { member } = useAuth(); + const { + status, + data: questions, + error, + isFetching, + } = useVoteQuestion(member?.id); + + if (isFetching) { + return ; + } else if (status === "error") { + return ; + } else if (questions) { + return ( +
+ {" "} + {questions.length !== 0 ? ( + + ) : ( + + {" "} + There is currently no live vote. + + )} +
+ ); + } else { + return <>; + } +}; + +export default VotingPage; diff --git a/src/util/Types.tsx b/src/util/Types.tsx index 0317d88..dc32723 100644 --- a/src/util/Types.tsx +++ b/src/util/Types.tsx @@ -102,3 +102,20 @@ export enum LoginError { NONE, UNKNOWN, } + +export enum VoteSelection { + YES = "Y", + ABSTAIN = "A", + NO = "N", +} + +export type VoteQuestion = { + uuid: string; + question: string; + description: string; +}; + +export type VoteHistory = { + question: string; + vote_selection: VoteSelection; +};