Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Voting Pages UI #66

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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;
Expand Down Expand Up @@ -74,6 +76,8 @@ function App() {
<Route path="/events/:id" element={<EventDetailsPage />} />
<Route path="/user/" element={<UserPreference />} />
<Route path="/record" element={<AttendanceRecordPage />} />
<Route path="/voting" element={<VotingPage />} />
<Route path="/voting/past" element={<VotingHistory />} />
</Route>

<Route path="*" element={<Error404 />} />
Expand Down
16 changes: 16 additions & 0 deletions src/client/VotingClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import axios from "axios";
import { VoteSelection } from "../util/Types";

export const createVote = async (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we turn this into a react query hook instead?

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;
};
19 changes: 19 additions & 0 deletions src/components/ErrorComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import TriangleError from "../assets/TriangleError.svg";

export const ErrorComponent = () => {
return (
<div className="flex flex-col items-center justify-center h-[65vh] lg:h-[100vh] w-full">
<img src={TriangleError} alt="" />
<div className="flex flex-col items-center font-sans mt-1 mb-12 gap-2">
<p className="text-xl text-center max-w-sm">
Oops! Something went wrong.
</p>
<p className="text-xl text-center max-w-sm">
This page failed to load. Please try again another time.
</p>
</div>
</div>
);
};

export default ErrorComponent;
15 changes: 12 additions & 3 deletions src/components/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,17 +105,26 @@ const Menu = (): ReactElement => {
Record
</button>
<button
className={`text-slate-400 ${
className={`App ${
useLocation().pathname === "/voting"
? "underline"
: "no-underline"
}`}
disabled
onClick={() => Click("/voting")}
>
Voting
</button>
<button
className={`App ${
useLocation().pathname === "/voting/past"
? "underline"
: "no-underline"
}`}
onClick={() => Click("/voting/past")}
>
Past Votes
</button>
</div>

<div className="flex items-start font-sans font-bold text-white text-xl h-fit w-full border-t-4 border-gray-300 border-opacity-70 p-8 pt-4 hover:text-slate-200">
<button onClick={logout}>Logout</button>
</div>
Expand Down
101 changes: 101 additions & 0 deletions src/components/Question.tsx
Original file line number Diff line number Diff line change
@@ -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<VoteSelection>();
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh nvm, i see what's happening here. ignore above comment

},
onSuccess: () => {
setSubmitted(true);
// TODO: this success should update the voting history query however
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you mean by this? could we just invalidate the voting history query?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whenever a vote is successfully created it should update the content on the vote record page ( your vote and submssion). The useMutation can force an endpoint to refresh using invalidate query, so I was trying get the endpoint that fetches the votingHistory to refresh, but adding that in was essentially skipping the confirmation message shown at the end of the vote, which isn't ideal.

// 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 (
<div className="font-montserrat">
<div className="flex flex-col">
<span className="font-bold text-xl">{question.question}</span>
<span className="text-center p-3">{question.description}</span>
</div>
{submitted ? (
<div className="text-lg pt-10 text-center">
<span className="font-bold">Thank you for voting. </span>
You voted: {getLabelFromSelection(selectedVote)}.
</div>
) : (
<div className="flex flex-col">
<div className="flex flex-col gap-6 pb-10">
{error && <div>Please input a value before casting vote</div>}
{availableOptions.map((option, index) => {
return (
<button
className={` border ${
selectedVote === option.value
? "border-red-700 text-red-700"
: "border-black"
} rounded-md p-1 pl-4 font-bold text-lg font-montserrat text-left`}
value={option.value}
key={index}
onClick={() => {
setError(false);
setSelectedVote(option.value);
}}
>
{option.label}
</button>
);
})}
</div>
<button
className={`${
selectedVote
? "bg-red-500 text-white"
: "bg-atn-disabled atn-disabled-text border-atn-disabled"
} button-base-red px-4 my-2 w-32 self-end`}
onClick={() => {
if (!selectedVote) {
setError(true);
return;
} else {
castVote(selectedVote);
}
}}
>
{" "}
Confirm
</button>
</div>
)}
</div>
);
};
33 changes: 22 additions & 11 deletions src/components/Settings.tsx
Original file line number Diff line number Diff line change
@@ -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<React.SetStateAction<boolean>>;
}): 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);
Expand Down Expand Up @@ -52,16 +46,33 @@ const Settings = (props: {
>
My Record
</button>
<button className="text-slate-400" disabled>
<button
className={`App ${
useLocation().pathname === "/voting"
? "underline"
: "no-underline"
}`}
onClick={() => Click("/voting")}
>
Voting
</button>
<button
className={`App ${
useLocation().pathname === "/voting"
? "underline"
: "no-underline"
}`}
onClick={() => Click("/voting/past")}
>
Past Votes
</button>
</div>

{/* <div> */}
<hr className="h-px bg-white border-0 dark:bg-white-700 w-full"></hr>

<div className="flex p-8">
<button onClick={() => logout()}>Logout</button>
<button onClick={logout}>Logout</button>
</div>
{/* </div> */}
</div>
Expand Down
14 changes: 14 additions & 0 deletions src/components/VoteHistoryRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { VoteHistory } from "../util/Types";

interface VoteHistoryProps {
voteRow: VoteHistory;
}

export const VoteHistoryRow = ({ voteRow }: VoteHistoryProps) => {
return (
<div className="flex justify-between pb-2">
<div className="w-10/12 pr-4">{voteRow.question}</div>
<div className="w-10 font-bold pl-4">{voteRow.vote_selection}</div>
</div>
);
};
16 changes: 16 additions & 0 deletions src/hooks/useVote.ts
Original file line number Diff line number Diff line change
@@ -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<VoteQuestion[]> => {
const response = await axios.get<VoteQuestion[]>(
`/api/voting/getIfMemberVoted?id=${id}`
);

return response.data;
},
});
}
16 changes: 16 additions & 0 deletions src/hooks/useVotingRecord.ts
Original file line number Diff line number Diff line change
@@ -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<VoteHistory[]> => {
const response = await axios.get<VoteHistory[]>(
`/api/voting/getVotingRecord?id=${id}`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we change this a bit to make the endpoint clearer, i.e. getVotingRecordByMemberId

);

return response.data;
},
});
}
2 changes: 1 addition & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does this need to be exported?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its needed for the (eventual?) invalidateQuery within the useMutation, i.e queryClient.invalidateQuery(queryKey)


const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
Expand Down
43 changes: 43 additions & 0 deletions src/pages/VotingHistory.tsx
Original file line number Diff line number Diff line change
@@ -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 <Loading />;
} else if (status === "error") {
return <ErrorComponent />;
} else if (votehistory) {
return (
<div className="flex justify-center py-10 w-3/4 mx-20 lg:mx-60">
<div className="flex flex-col space-y-4 min-w-full">
<div className="flex justify-between">
<div className="font-bold">Question</div>
<div className="font-bold text-right">Vote</div>
</div>

{votehistory.length === 0 ? (
<span> No Voting Records to Display </span>
) : (
votehistory.map((voteRow) => <VoteHistoryRow voteRow={voteRow} />)
)}
</div>
</div>
);
} else {
return <></>;
}
};

export default VotingHistory;
40 changes: 40 additions & 0 deletions src/pages/VotingPage.tsx
Original file line number Diff line number Diff line change
@@ -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 <Loading />;
} else if (status === "error") {
return <ErrorComponent />;
} else if (questions) {
return (
<div className="flex justify-center w-full p-10">
{" "}
{questions.length !== 0 ? (
<Question question={questions[0]} member_id={member?.id} />
) : (
<span className="font-bold text-xl pt-10 font-montserrat">
{" "}
There is currently no live vote.
</span>
)}
</div>
);
} else {
return <></>;
}
};

export default VotingPage;
Loading
Loading