From eaf54d10de5701982b4baac5231f171af9d66cba Mon Sep 17 00:00:00 2001 From: Tom Slanda Date: Wed, 14 Aug 2024 11:43:10 -0400 Subject: [PATCH] Voting State, Part 1 (#81) * added voting to replies * created hidden state * made voting buttons a component * added new voting component to pages * reverted useState * halfway there * tweaks * thank god for chatGPT * added patch routes * moved voting hook call inside vote container * added votes for comments * hive voting content working * removed useState * content content container voting working * fixed conditional styling * commented out voting container in replies to deploy --------- Co-authored-by: Tom Slanda --- src/components/CommentContainer.tsx | 48 +------- src/components/ContentContentContainer.tsx | 104 ++++------------ src/components/HiveContentContainer.tsx | 103 ++++------------ src/components/HomeContentContainer.tsx | 25 ++-- src/components/LoginForm.tsx | 7 +- src/components/ReplyContainer.tsx | 12 +- src/components/UserDropdown.tsx | 1 + src/components/VoteContainer.tsx | 135 +++++++++++++++++++++ src/hooks/useGetVotes.tsx | 36 +++--- src/hooks/useShowVotes.tsx | 17 --- src/hooks/useUpvote.tsx | 42 +++++++ src/pages/HIve.tsx | 30 ++--- test-results/.last-run.json | 8 ++ 13 files changed, 283 insertions(+), 285 deletions(-) create mode 100644 src/components/VoteContainer.tsx delete mode 100644 src/hooks/useShowVotes.tsx create mode 100644 src/hooks/useUpvote.tsx create mode 100644 test-results/.last-run.json diff --git a/src/components/CommentContainer.tsx b/src/components/CommentContainer.tsx index 6f622e0..4fe7a96 100755 --- a/src/components/CommentContainer.tsx +++ b/src/components/CommentContainer.tsx @@ -2,24 +2,25 @@ import { useState } from "react"; import { useParams } from "react-router-dom"; import { useForm } from "@tanstack/react-form"; import getIso from "../utils/tokenTools/getIso"; -import useShowVotes from "../hooks/useShowVotes"; +import useGetVotes from "../hooks/useGetVotes"; +import VoteContainer from "./VoteContainer"; import { TComment } from "../types"; import ReplyContainer from "./ReplyContainer"; import CommentIcon from "../assets/CommentIcon"; -import UpvoteIcon from "../assets/UpvoteIcon"; -import DownvoteIcon from "../assets/DownvoteIcon"; const CommentContainer = (props: any) => { // URL Variables const baseURL = import.meta.env.VITE_BASEURL; const params = useParams(); - const votingState = useShowVotes(props.Uuid); + const voteURL = baseURL + "/comment/uuid/" + props.Uuid; // State const [replyTextareaShow, setReplyTextareaShow] = useState(false); const [replyButtonShow, setReplyButtonShow] = useState(false); const [replyButtonText, setReplyButtonText] = useState("Add Reply"); + useGetVotes(baseURL + "/content/votes"); + // ----- Reply Start ----- // const form = useForm({ defaultValues: { @@ -97,44 +98,7 @@ const CommentContainer = (props: any) => { {/* Horizontal Vote Container */}
- {votingState.upvote === true ? ( - <> - -

{props.Upvote}

- - ) : votingState.downvote === false && - votingState.upvote === false ? ( - <> - -

{props.Upvote}

- - ) : null} - {votingState.downvote === true ? ( - <> - -

{props.Downvote}

- - ) : votingState.downvote === false && - votingState.upvote === false ? ( - <> - -

{props.Downvote}

- - ) : null} + {/* Comment Container */} -

{props.Upvote}

- - ) : votingState.downvote === false && votingState.upvote === false ? ( - <> - -

{props.Upvote}

- - ) : null} - {votingState.downvote === true ? ( - <> - -

{props.Downvote}

- - ) : votingState.downvote === false && votingState.upvote === false ? ( - <> - -

{props.Downvote}

- - ) : null} + {isLoading || isFetching ? ( + "..." + ) : isError ? ( + "Error" + ) : ( + + )}
{/* User & Time Container */} @@ -76,44 +49,13 @@ const ContentContentContainer = (props: TContent) => { {/* Horizontal Vote Container */}
- {votingState.upvote === true ? ( - <> - -

{props.Upvote}

- - ) : votingState.downvote === false && - votingState.upvote === false ? ( - <> - -

{props.Upvote}

- - ) : null} - {votingState.downvote === true ? ( - <> - -

{props.Downvote}

- - ) : votingState.downvote === false && - votingState.upvote === false ? ( - <> - -

{props.Downvote}

- - ) : null} + {isLoading || isFetching ? ( + "..." + ) : isError ? ( + "Error" + ) : ( + + )}
{/* Comment Container */}
diff --git a/src/components/HiveContentContainer.tsx b/src/components/HiveContentContainer.tsx index 619f794..532e759 100755 --- a/src/components/HiveContentContainer.tsx +++ b/src/components/HiveContentContainer.tsx @@ -1,14 +1,16 @@ import { Link } from "react-router-dom"; import getIso from "../utils/tokenTools/getIso"; -import useShowVotes from "../hooks/useShowVotes"; +import useGetVotes from "../hooks/useGetVotes"; import { TContent } from "../types"; -import UpvoteIcon from "../assets/UpvoteIcon"; -import DownvoteIcon from "../assets/DownvoteIcon"; import CommentIcon from "../assets/CommentIcon"; +import VoteContainer from "./VoteContainer"; const HiveContentContainer = ({ item }: { item: TContent }) => { - const votingState = useShowVotes(item.Uuid); - + const baseURL = import.meta.env.VITE_BASEURL; + const voteURL = baseURL + "/content/uuid/" + item.Uuid; + const { data, isLoading, isFetching, isError } = useGetVotes( + baseURL + "/content/votes" + ); return (
{
{/* Vertical Vote Container */}
- {votingState.upvote === true ? ( - <> - -

{item.Upvote}

- - ) : votingState.downvote === false && votingState.upvote === false ? ( - <> - -

{item.Upvote}

- - ) : null} - {votingState.downvote === true ? ( - <> - -

{item.Downvote}

- - ) : votingState.downvote === false && votingState.upvote === false ? ( - <> - -

{item.Downvote}

- - ) : null} + {isLoading || isFetching ? ( + "..." + ) : isError ? ( + "Error" + ) : ( + + )}
{/* User & Time Container */} @@ -77,44 +47,13 @@ const HiveContentContainer = ({ item }: { item: TContent }) => { {/* Horizontal Vote Container */}
- {votingState.upvote === true ? ( - <> - -

{item.Upvote}

- - ) : votingState.downvote === false && - votingState.upvote === false ? ( - <> - -

{item.Upvote}

- - ) : null} - {votingState.downvote === true ? ( - <> - -

{item.Downvote}

- - ) : votingState.downvote === false && - votingState.upvote === false ? ( - <> - -

{item.Downvote}

- - ) : null} + {isLoading || isFetching ? ( + "..." + ) : isError ? ( + "Error" + ) : ( + + )}
{/* Comment Container */}
diff --git a/src/components/HomeContentContainer.tsx b/src/components/HomeContentContainer.tsx index 675a980..f7ed407 100755 --- a/src/components/HomeContentContainer.tsx +++ b/src/components/HomeContentContainer.tsx @@ -1,21 +1,20 @@ import { Link } from "react-router-dom"; - const HomeContentContainer = ({ item }: { item: any }) => { return ( <> -
- -
-

{item.Name}

-
- -
- - ) +
+ +
+

{item.Name}

+
+ +
+ + ); }; export default HomeContentContainer; diff --git a/src/components/LoginForm.tsx b/src/components/LoginForm.tsx index f16e1fd..30034ce 100755 --- a/src/components/LoginForm.tsx +++ b/src/components/LoginForm.tsx @@ -1,18 +1,17 @@ import { useForm } from "@tanstack/react-form"; -import { useState} from "react"; +import { useState } from "react"; import { Link } from "react-router-dom"; import { useNavigate } from "react-router-dom"; import validateEmail from "../utils/formValidation/validateEmail"; import setStorage from "../utils/setStorage"; - type LoginCredentials = { email: string; password: string; }; const LoginForm: React.FC = () => { - const [buttonText, setButtonText] = useState("Submit") + const [buttonText, setButtonText] = useState("Submit"); const baseURL = import.meta.env.VITE_BASEURL; const navigate = useNavigate(); @@ -46,8 +45,8 @@ const LoginForm: React.FC = () => { } const results = await response.json(); setStorage(results.Token, results.RefreshToken); - navigate("/"); + location.reload(); } catch (error) { console.error("Login Failed", error); } diff --git a/src/components/ReplyContainer.tsx b/src/components/ReplyContainer.tsx index c5f3970..0809826 100755 --- a/src/components/ReplyContainer.tsx +++ b/src/components/ReplyContainer.tsx @@ -1,7 +1,6 @@ import getIso from "../utils/tokenTools/getIso"; +// import VoteContainer from "./VoteContainer"; import { TComment } from "../types"; -//import UpvoteIcon from "../assets/UpvoteIcon"; -// import DownvoteIcon from "../assets/DownvoteIcon"; const ReplyContainer = (props: TComment) => { return ( @@ -25,14 +24,7 @@ const ReplyContainer = (props: TComment) => { {/* Horizontal Vote Container */}
{/*
- -

- -

+
*/}
diff --git a/src/components/UserDropdown.tsx b/src/components/UserDropdown.tsx index c6f31ab..88ab95b 100755 --- a/src/components/UserDropdown.tsx +++ b/src/components/UserDropdown.tsx @@ -19,6 +19,7 @@ const UserDropdown = () => { accountUUID: "", })); navigate("/login"); + location.reload(); } return ( diff --git a/src/components/VoteContainer.tsx b/src/components/VoteContainer.tsx new file mode 100644 index 0000000..0937983 --- /dev/null +++ b/src/components/VoteContainer.tsx @@ -0,0 +1,135 @@ +import { useState, useEffect } from "react"; +import { TContent } from "../types"; +import useUpvote from "../hooks/useUpvote"; +import UpvoteIcon from "../assets/UpvoteIcon"; +import DownvoteIcon from "../assets/DownvoteIcon"; + +const VoteContainer = ({ + Upvote, + Downvote, + Uuid, + voteData, + voteURL, +}: TContent & { voteURL: string; voteData: any }) => { + const [votingState, setVotingState] = useState({ + upvoteState: false, + downvoteState: false, + upvoteCount: Upvote, + downvoteCount: Downvote, + upvoteIconDisplay: "", + downvoteIconDisplay: "", + upvoteIconFill: "rgba(0, 0, 0, 1)", + downvoteIconFill: "rgba(0, 0, 0, 1)", + upvoteIconStroke: "none", + downvoteIconStroke: "none", + }); + + const markVotes = () => { + if (voteData?.Upvotes && voteData.Upvotes.includes(Uuid)) { + setVotingState((prev) => ({ + ...prev, + upvoteState: true, + upvoteIconFill: "rgba(251,191,36,1)", + upvoteIconStroke: "rgba(0, 0, 0, 1)", + downvoteIconDisplay: "hidden ", + })); + } + if (voteData?.Downvotes && voteData.Downvotes.includes(Uuid)) { + setVotingState((prev) => ({ + ...prev, + downvoteState: true, + downvoteIconFill: "rgba(251,191,36,1)", + downvoteIconStroke: "rgba(0, 0, 0, 1)", + upvoteIconDisplay: "hidden ", + })); + } + }; + + useEffect(() => { + markVotes(); + }, [voteData]); + + const upvoteClickHandler = (voteURL: string) => { + if (votingState.upvoteState === false) { + useUpvote(voteURL + "/add-upvote"); + } + if (votingState.upvoteState === true) { + useUpvote(voteURL + "/remove-upvote"); + } + setVotingState((prevState) => { + const newUpvoteState = !prevState.upvoteState; + const newUpvoteCount = newUpvoteState + ? prevState.upvoteCount + 1 + : prevState.upvoteCount - 1; + + return { + ...prevState, + upvoteState: newUpvoteState, + upvoteCount: newUpvoteCount, + upvoteIconFill: newUpvoteState + ? "rgba(251,191,36,1)" + : "rgba(0, 0, 0, 1)", + upvoteIconStroke: newUpvoteState ? "rgba(0, 0, 0, 1)" : "none", + downvoteIconDisplay: newUpvoteState ? "hidden " : "", + }; + }); + }; + const downvoteClickHandler = (voteURL: string) => { + if (votingState.downvoteState === false) { + useUpvote(voteURL + "/add-downvote"); + } + if (votingState.downvoteState === true) { + useUpvote(voteURL + "/remove-downvote"); + } + setVotingState((prevState) => { + const newDownvoteState = !prevState.downvoteState; + const newDownvoteCount = newDownvoteState + ? prevState.downvoteCount + 1 + : prevState.downvoteCount - 1; + + return { + ...prevState, + downvoteState: newDownvoteState, + downvoteCount: newDownvoteCount, + downvoteIconFill: newDownvoteState + ? "rgba(251,191,36,1)" + : "rgba(0, 0, 0, 1)", + downvoteIconStroke: newDownvoteState ? "rgba(0, 0, 0, 1)" : "none", + upvoteIconDisplay: newDownvoteState ? "hidden " : "", + }; + }); + }; + + return ( + <> + +

+ {votingState.upvoteCount} +

+ +

+ {votingState.downvoteCount} +

+ + ); +}; + +export default VoteContainer; diff --git a/src/hooks/useGetVotes.tsx b/src/hooks/useGetVotes.tsx index c50d9c2..2974ca1 100755 --- a/src/hooks/useGetVotes.tsx +++ b/src/hooks/useGetVotes.tsx @@ -11,32 +11,32 @@ const useGetVotes = (url: string) => { Upon refresh, the user will be routed to the login page. */ const getVotes = async () => { - const token = await validateToken() + const token = await validateToken(); if (token?.refreshTokenExpired === true) { - localStorage.clear() - return + localStorage.clear(); + return; } if (token?.accessTokenExpired === true) { await getNewAccessToken(); } const data = await getData(url); return data; + }; + const getData = async (url: string) => { + const response = await fetch(url, { + method: "GET", + headers: { + Authorization: `Bearer ${localStorage.accessToken}`, + }, + }); + if (!response.ok) { + throw new Error(`${response.status}: Failed to fetch`); } - const getData = async (url: string) => { - const response = await fetch(url, { - method: 'GET', - headers: { - Authorization: `Bearer ${localStorage.accessToken}`, - }, - }); - if (!response.ok) { - throw new Error(`${response.status}: Failed to fetch`); - } - const data = await response.json(); - localStorage.setItem("Upvotes", data.Upvotes); - localStorage.setItem("Downvotes", data.Downvotes); - return data; - }; + const data = await response.json(); + localStorage.setItem("Upvotes", data.Upvotes); + localStorage.setItem("Downvotes", data.Downvotes); + return data; + }; const { data, error, refetch, isLoading, isError, isFetching } = useQuery({ queryKey: ["votes", url], diff --git a/src/hooks/useShowVotes.tsx b/src/hooks/useShowVotes.tsx deleted file mode 100644 index 9e17e29..0000000 --- a/src/hooks/useShowVotes.tsx +++ /dev/null @@ -1,17 +0,0 @@ -const useShowVotes = (Uuid: string) => { - const votingState = { - upvote: false, - downvote: false, - }; - const upvotes = localStorage.getItem("Upvotes"); - const downvotes = localStorage.getItem("Downvotes"); - if (upvotes?.includes(Uuid)) { - votingState.upvote = true; - } - if (downvotes?.includes(Uuid)) { - votingState.downvote = true; - } - return votingState; - } - - export default useShowVotes; \ No newline at end of file diff --git a/src/hooks/useUpvote.tsx b/src/hooks/useUpvote.tsx new file mode 100644 index 0000000..ff5fd15 --- /dev/null +++ b/src/hooks/useUpvote.tsx @@ -0,0 +1,42 @@ +import getNewAccessToken from "../utils/tokenTools/getNewAccessToken"; +import validateToken from "../utils/tokenTools/validateToken"; + +const useUpvote = (url: string) => { + /* + This function calls the validateToken helper function. The validateToken helper function returns an object with two properties - accessTokenExpired and refreshTokenExpired. Both properties are booleans. + If the access token is expired, a new token will be fetched from the server before the getData call. + If the access token is fresh, the getData calls fires normally. + If the refresh token is expired, local storage is deleted and the function returns early. This is create an error on the page, instructing the user to refresh. + Upon refresh, the user will be routed to the login page. + */ + const getData = async () => { + const token = await validateToken(); + if (token?.refreshTokenExpired === true) { + localStorage.clear(); + return; + } + if (token?.accessTokenExpired === true) { + await getNewAccessToken(); + } + const data = await fetchData(url); + return data; + }; + /* +Async GET request +*/ + const fetchData = async (url: string) => { + const response = await fetch(url, { + method: "PATCH", + headers: { + Authorization: `Bearer ${localStorage.accessToken}`, + }, + }); + if (!response.ok) { + throw new Error(`${response.status}: Failed to fetch`); + } + const data = await response.json(); + return data; + }; + getData(); +}; +export default useUpvote; diff --git a/src/pages/HIve.tsx b/src/pages/HIve.tsx index dbfe5dc..205c732 100755 --- a/src/pages/HIve.tsx +++ b/src/pages/HIve.tsx @@ -1,5 +1,4 @@ import useGET from "../hooks/useGET"; -import useGetVotes from "../hooks/useGetVotes"; import HiveContentContainer from "../components/HiveContentContainer"; import { useParams } from "react-router-dom"; import { TContent } from "../types"; @@ -7,28 +6,23 @@ import { TContent } from "../types"; const Hive = () => { const params = useParams(); const baseURL = import.meta.env.VITE_BASEURL; - - useGetVotes(baseURL + "/content/votes") + const { data, error, isLoading, isFetching, isError } = useGET( - baseURL + "/hive/uuid/" + params.hiveUuid + "/content", + baseURL + "/hive/uuid/" + params.hiveUuid + "/content" ); - return ( <>
{isLoading || isFetching ? ( - - Loading... - - ) : isError ? ( - - Error: {error?.message} - - ) : ( - data && data.length > 0 ? data[0].Hive : "There's nothing here..." - ) - } + Loading... + ) : isError ? ( + Error: {error?.message} + ) : data && data.length > 0 ? ( + data[0].Hive + ) : ( + "There's nothing here..." + )}
{isLoading || isFetching ? ( @@ -40,8 +34,8 @@ const Hive = () => { ) : ( data && - data.map((item: TContent) => ( - + data.map((data: TContent) => ( + )) )} diff --git a/test-results/.last-run.json b/test-results/.last-run.json new file mode 100644 index 0000000..3a395b7 --- /dev/null +++ b/test-results/.last-run.json @@ -0,0 +1,8 @@ +{ + "status": "failed", + "failedTests": [ + "41d3fd2474a19feb00a1-d349047fdb6e8e743729", + "41d3fd2474a19feb00a1-c4296665263f262d5090", + "41d3fd2474a19feb00a1-730ba0894fbf7069d702" + ] +} \ No newline at end of file