From 314115dc64b656351fcb7ee950f12af0aa24761c Mon Sep 17 00:00:00 2001 From: Sendou Date: Sat, 4 Apr 2020 13:18:57 +0300 Subject: [PATCH] voting frontend --- .../src/components/common/SubHeader.css | 32 +++ .../src/components/common/SubHeader.tsx | 25 ++ .../src/components/plus/PersonForVoting.tsx | 151 +++++++++++ .../src/components/plus/PlusPage.tsx | 64 ++--- frontend-react/src/components/plus/Voting.js | 215 ---------------- frontend-react/src/components/plus/Voting.tsx | 243 ++++++++++++++++++ .../src/components/plus/VotingGridRow.js | 102 -------- .../src/components/plus/VotingNumber.js | 25 -- .../src/graphql/mutations/addVotes.js | 7 - .../src/graphql/mutations/addVotes.ts | 14 + .../src/graphql/queries/plusInfo.ts | 8 + .../src/graphql/queries/usersForVoting.js | 37 --- .../src/graphql/queries/usersForVoting.ts | 77 ++++++ 13 files changed, 573 insertions(+), 427 deletions(-) create mode 100644 frontend-react/src/components/common/SubHeader.css create mode 100644 frontend-react/src/components/common/SubHeader.tsx create mode 100644 frontend-react/src/components/plus/PersonForVoting.tsx delete mode 100644 frontend-react/src/components/plus/Voting.js create mode 100644 frontend-react/src/components/plus/Voting.tsx delete mode 100644 frontend-react/src/components/plus/VotingGridRow.js delete mode 100644 frontend-react/src/components/plus/VotingNumber.js delete mode 100644 frontend-react/src/graphql/mutations/addVotes.js create mode 100644 frontend-react/src/graphql/mutations/addVotes.ts delete mode 100644 frontend-react/src/graphql/queries/usersForVoting.js create mode 100644 frontend-react/src/graphql/queries/usersForVoting.ts diff --git a/frontend-react/src/components/common/SubHeader.css b/frontend-react/src/components/common/SubHeader.css new file mode 100644 index 000000000..0679f59a1 --- /dev/null +++ b/frontend-react/src/components/common/SubHeader.css @@ -0,0 +1,32 @@ +/*https://stackoverflow.com/a/23155413*/ + +:root { + --sub-header-border: white; +} + +.decorated { + overflow: hidden; + text-align: center; + font-weight: bold; + font-size: 1.2em; +} +.decorated > span { + position: relative; + display: inline-block; +} +.decorated > span:before, +.decorated > span:after { + content: ""; + position: absolute; + top: 50%; + border-bottom: 4px solid; + border-color: var(--sub-header-border); + width: 592px; /* half of limiter */ + margin: 0 20px; +} +.decorated > span:before { + right: 100%; +} +.decorated > span:after { + left: 100%; +} diff --git a/frontend-react/src/components/common/SubHeader.tsx b/frontend-react/src/components/common/SubHeader.tsx new file mode 100644 index 000000000..c78244d86 --- /dev/null +++ b/frontend-react/src/components/common/SubHeader.tsx @@ -0,0 +1,25 @@ +import React, { useContext } from "react" +import "./SubHeader.css" +import MyThemeContext from "../../themeContext" +import { Box, useTheme } from "@chakra-ui/core" + +interface SubHeaderProps { + children: string[] | string +} + +const SubHeader: React.FC = ({ children }) => { + const { themeColorHex, themeColorHexLighter, colorMode } = useContext( + MyThemeContext + ) + const style = { + "--sub-header-border": + colorMode === "dark" ? themeColorHexLighter : themeColorHex, + } as React.CSSProperties + return ( +

+ {children} +

+ ) +} + +export default SubHeader diff --git a/frontend-react/src/components/plus/PersonForVoting.tsx b/frontend-react/src/components/plus/PersonForVoting.tsx new file mode 100644 index 000000000..98d9bd5e4 --- /dev/null +++ b/frontend-react/src/components/plus/PersonForVoting.tsx @@ -0,0 +1,151 @@ +import React, { useContext } from "react" +import { Link } from "@reach/router" +import UserAvatar from "../common/UserAvatar" +import { Flex, Avatar, Box, Grid } from "@chakra-ui/core" +import MyThemeContext from "../../themeContext" + +interface VotingButtonProps { + value: 2 | 1 | -1 | -2 + handleClick: (oldValue: number) => void + gridArea: string + active: boolean +} + +const buttonBg = { + "-2": "red.500", + "-1": "red.500", + "1": "green.500", + "2": "green.500", +} as const + +const VotingButton: React.FC = ({ + value, + handleClick, + gridArea, + active, +}) => { + return ( + + handleClick(value)} + alignItems="center" + justifyContent="center" + borderRadius="50%" + w="50px" + h="50px" + fontWeight="bolder" + border={active ? "4px solid" : undefined} + borderColor={buttonBg[value]} + fontSize="24px" + cursor="pointer" + userSelect="none" + > + {value > 0 ? "+" : ""} + {value} + + + ) +} + +interface PersonForVotingProps { + votes: Record + setVotes: React.Dispatch>> + user: { + username: string + discriminator: string + twitter_name?: string | undefined + discord_id: string + } + suggester?: { + username: string + discriminator: string + } + description?: string + sameRegion?: boolean +} + +const PersonForVoting: React.FC = ({ + votes, + setVotes, + user, + suggester, + description, + sameRegion = true, +}) => { + const { grayWithShade } = useContext(MyThemeContext) + + const handleClick = (value: number) => { + setVotes({ ...votes, [user.discord_id]: value }) + } + + return ( + + + + + + + {user.username}#{user.discriminator} + + + + {sameRegion ? ( + + ) : ( + + )} + + + {sameRegion ? ( + + ) : ( + + )} + + ) +} + +export default PersonForVoting diff --git a/frontend-react/src/components/plus/PlusPage.tsx b/frontend-react/src/components/plus/PlusPage.tsx index c2a7aea1a..259098f61 100644 --- a/frontend-react/src/components/plus/PlusPage.tsx +++ b/frontend-react/src/components/plus/PlusPage.tsx @@ -4,7 +4,7 @@ import { useQuery } from "@apollo/react-hooks" import Suggestions from "./Suggestions" import Loading from "../common/Loading" import Error from "../common/Error" -import { PLUS_INFO } from "../../graphql/queries/plusInfo" +import { PLUS_INFO, PlusInfoData } from "../../graphql/queries/plusInfo" import { USER } from "../../graphql/queries/user" //import Voting from "./Voting" import { Redirect, RouteComponentProps, Link } from "@reach/router" @@ -18,14 +18,7 @@ import { PLUS_MAPLISTS, PlusMaplistsData, } from "../../graphql/queries/plusMaplists" - -interface PlusInfoData { - plusInfo: { - voting_ends?: String - voter_count: number - eligible_voters: number - } -} +import Voting from "./Voting" const PlusPage: React.FC = () => { const { data, error, loading } = useQuery(PLUS_INFO) @@ -49,6 +42,7 @@ const PlusPage: React.FC = () => { if (!data.plusInfo) return const maplist = maplistData.plusMaplists[0] + const plusInfo = data.plusInfo return ( <> @@ -56,29 +50,6 @@ const PlusPage: React.FC = () => { Plus Server Home | sendou.ink - {/*data.plusInfo.voting_ends && userData.user.plus?.membership_status ? ( - - ) : ( - - )*/} @@ -102,15 +73,26 @@ const PlusPage: React.FC = () => { - - + {plusInfo.voting_ends ? ( + + ) : ( + <> + + + + )} ) } diff --git a/frontend-react/src/components/plus/Voting.js b/frontend-react/src/components/plus/Voting.js deleted file mode 100644 index 9d34af5b3..000000000 --- a/frontend-react/src/components/plus/Voting.js +++ /dev/null @@ -1,215 +0,0 @@ -import React, { useState, useEffect } from "react" -import { usersForVoting } from "../../graphql/queries/usersForVoting" -import { useQuery, useMutation } from "@apollo/react-hooks" -import { Grid, Message, Button, Progress } from "semantic-ui-react" -import { Prompt } from "react-router-dom" - -import Loading from "../common/Loading" -import Error from "../common/Error" -import VotingGridRow from "./VotingGridRow" -import { addVotes } from "../../graphql/mutations/addVotes" - -const Voting = ({ - user, - handleSuccess, - handleError, - votingEnds, - votedSoFar, - eligibleVoters, -}) => { - const { data, loading, error } = useQuery(usersForVoting) - const [votes, setVotes] = useState({}) - const [voteCount, setVoteCount] = useState(0) - const [suggestedArrays, setSuggestedArrays] = useState(null) - - const [addVotesMutation] = useMutation(addVotes, { - onError: handleError, - onCompleted: () => handleSuccess("Votes successfully recorded."), - refetchQueries: [ - { - query: usersForVoting, - }, - ], - }) - - const handleSubmit = async () => { - await addVotesMutation({ - variables: { - votes: Object.keys(votes).map(key => ({ - discord_id: key, - score: votes[key], - })), - }, - }) - } - - useEffect(() => { - if (loading || error) return - - const sameRegionSuggested = [] - const otherRegionSuggested = [] - - data.usersForVoting.suggested.forEach(suggested => { - if (suggested.plus_region === user.plus.plus_region) { - sameRegionSuggested.push(suggested) - } else { - otherRegionSuggested.push(suggested) - } - }) - - setSuggestedArrays({ - sameRegion: sameRegionSuggested, - otherRegion: otherRegionSuggested, - }) - - if (data.usersForVoting.votes) { - const voteObj = {} - data.usersForVoting.votes.forEach( - vote => (voteObj[vote.discord_id] = vote.score) - ) - setVotes(voteObj) - setVoteCount(data.usersForVoting.votes.length) - } - }, [loading, error, data, user]) - - if (error) return - if (loading || !suggestedArrays) return - const date = new Date() - if (votingEnds < date.getTime()) - return ( - - Voting for the month is over - Results will be posted later - - ) - - const hoursLeft = Math.ceil((votingEnds - date.getTime()) / (1000 * 60 * 60)) - const alreadyVoted = data.usersForVoting.votes.length > 0 - - return ( - <> - 0 && - voteCount < - data.usersForVoting.users.length + - data.usersForVoting.suggested.length - } - message="Are you sure you want to leave? Vote form won't be saved." - /> - - - Voted so far - -

- {user.plus.plus_region === "EU" ? "European" : "American"} players -

- - {data.usersForVoting.users.map(userForVoting => { - if (userForVoting.plus.plus_region !== user.plus.plus_region) - return null - return ( - setVoteCount(voteCount + 1)} - /> - ) - })} - - {suggestedArrays.sameRegion.length > 0 && ( - <> -

- {user.plus.plus_region === "EU" ? "European" : "American"} players - (suggested) -

- - {suggestedArrays.sameRegion.map(suggestion => { - return ( - setVoteCount(voteCount + 1)} - /> - ) - })} - - - )} -

- {user.plus.plus_region === "NA" ? "European" : "American"} players -

- - {data.usersForVoting.users.map(userForVoting => { - if (userForVoting.plus.plus_region === user.plus.plus_region) - return null - return ( - setVoteCount(voteCount + 1)} - /> - ) - })} - - {suggestedArrays.otherRegion.length > 0 && ( - <> -

- {user.plus.plus_region === "NA" ? "European" : "American"} players - (suggested) -

- - {suggestedArrays.otherRegion.map(suggestion => { - return ( - setVoteCount(voteCount + 1)} - /> - ) - })} - - - )} - - - ) -} - -export default Voting diff --git a/frontend-react/src/components/plus/Voting.tsx b/frontend-react/src/components/plus/Voting.tsx new file mode 100644 index 000000000..32533b3ef --- /dev/null +++ b/frontend-react/src/components/plus/Voting.tsx @@ -0,0 +1,243 @@ +import React, { useState, useEffect, useContext } from "react" +import { + USERS_FOR_VOTING, + UsersForVotingData, + VotingSuggested, +} from "../../graphql/queries/usersForVoting" +import { useQuery, useMutation } from "@apollo/react-hooks" + +import Loading from "../common/Loading" +import Error from "../common/Error" +import { ADD_VOTES, AddVotesVars } from "../../graphql/mutations/addVotes" +import { useToast, Progress, Box, Flex, Grid } from "@chakra-ui/core" +import { UserLean } from "../../types" +import Alert from "../elements/Alert" +import MyThemeContext from "../../themeContext" +import Button from "../elements/Button" +import PersonForVoting from "./PersonForVoting" +import SubHeader from "../common/SubHeader" + +interface VotingProps { + user: UserLean + votingEnds: number + votedSoFar: number + eligibleVoters: number +} + +interface SuggestedArrays { + sameRegion: VotingSuggested[] + otherRegion: VotingSuggested[] +} + +const Voting: React.FC = ({ + user, + votingEnds, + votedSoFar, + eligibleVoters, +}) => { + const { themeColor, grayWithShade } = useContext(MyThemeContext) + const { data, loading, error } = useQuery( + USERS_FOR_VOTING + ) + const [votes, setVotes] = useState>({}) + const [suggestedArrays, setSuggestedArrays] = useState({ + sameRegion: [], + otherRegion: [], + }) + const toast = useToast() + + const [addVotesMutation, { loading: addVotesLoading }] = useMutation< + boolean, + AddVotesVars + >(ADD_VOTES, { + onCompleted: (data) => { + window.scrollTo(0, 0) + toast({ + description: `Votes submitted`, + position: "top-right", + status: "success", + duration: 10000, + }) + }, + onError: (error) => { + toast({ + title: "An error occurred", + description: error.message, + position: "top-right", + status: "error", + duration: 10000, + }) + }, + refetchQueries: [ + { + query: USERS_FOR_VOTING, + }, + ], + }) + + const handleSubmit = async () => { + await addVotesMutation({ + variables: { + votes: Object.keys(votes).map((key) => ({ + discord_id: key, + score: (votes as any)[key], + })), + }, + }) + } + + useEffect(() => { + if (loading || error) return + + const sameRegionSuggested: VotingSuggested[] = [] + const otherRegionSuggested: VotingSuggested[] = [] + + data!.usersForVoting.suggested.forEach((suggested) => { + if (suggested.plus_region === user.plus!.plus_region) { + sameRegionSuggested.push(suggested) + } else { + otherRegionSuggested.push(suggested) + } + }) + + setSuggestedArrays({ + sameRegion: sameRegionSuggested, + otherRegion: otherRegionSuggested, + }) + + if (data!.usersForVoting.votes) { + const voteObj: Record = {} + data!.usersForVoting.votes.forEach( + (vote) => (voteObj[vote.discord_id] = vote.score) + ) + setVotes(voteObj) + } + }, [loading, error, data, user]) + + if (loading) return + if (error) return + + const date = new Date() + if (votingEnds < date.getTime()) + return ( + + Voting is over. Results will be posted a bit later. + + ) + + const hoursLeft = Math.ceil((votingEnds - date.getTime()) / (1000 * 60 * 60)) + const alreadyVoted = data!.usersForVoting.votes.length > 0 + + const missingVotes = + data!.usersForVoting.users.length + + data!.usersForVoting.suggested.length - + Object.keys(votes).length + + return ( + <> + {`${ + alreadyVoted ? "You have voted! " : "" + }Voting ends ${new Date(votingEnds).toLocaleString()} (${hoursLeft} + hours left)`} + + + {votedSoFar}/{eligibleVoters} voted so far + + + + {user.plus!.plus_region === "EU" ? "European" : "American"} players + + {data!.usersForVoting.users.map((userForVoting) => { + if (userForVoting.plus.plus_region !== user.plus!.plus_region) + return null + return ( + + ) + })} + + {suggestedArrays.sameRegion.length > 0 && ( + <> + + {user.plus!.plus_region === "EU" ? "European" : "American"} players + (suggested) + + {suggestedArrays.sameRegion.map((suggestion) => { + return ( + + ) + })} + + )} + + {user.plus!.plus_region === "NA" ? "European" : "American"} players + + {data!.usersForVoting.users.map((userForVoting) => { + if (userForVoting.plus.plus_region === user.plus!.plus_region) + return null + return ( + + ) + })} + {suggestedArrays.otherRegion.length > 0 && ( + <> + + {user.plus!.plus_region === "NA" ? "European" : "American"} players + (suggested) + + + {suggestedArrays.otherRegion.map((suggestion) => { + return ( + + ) + })} + + )} + + + + + {missingVotes > 0 && ( + <> + You need to vote on {missingVotes} more player + {missingVotes > 1 ? "s" : ""} + + )} + + + ) +} + +export default Voting diff --git a/frontend-react/src/components/plus/VotingGridRow.js b/frontend-react/src/components/plus/VotingGridRow.js deleted file mode 100644 index 77f82d0f1..000000000 --- a/frontend-react/src/components/plus/VotingGridRow.js +++ /dev/null @@ -1,102 +0,0 @@ -import React from "react" -import { Divider, Grid } from "semantic-ui-react" -import { Link } from "react-router-dom" - -import useWindowDimensions from "../../hooks/useWindowDimensions" -import VotingNumber from "./VotingNumber" -import UserAvatar from "../common/UserAvatar" - -const VotingGridRow = ({ - votes, - setVotes, - user, - suggester, - description, - increaseCount, - sameRegion = true, -}) => { - const { isMobile } = useWindowDimensions() - return ( - <> - - -
- - - {user.username}#{user.discriminator} - -
-
- {isMobile && } - - {sameRegion && ( - { - if (isNaN(votes[user.discord_id])) increaseCount() - setVotes({ ...votes, [user.discord_id]: -2 }) - }} - /> - )} - - - { - if (isNaN(votes[user.discord_id])) increaseCount() - setVotes({ ...votes, [user.discord_id]: -1 }) - }} - /> - - - { - if (isNaN(votes[user.discord_id])) increaseCount() - setVotes({ ...votes, [user.discord_id]: 1 }) - }} - /> - - - {sameRegion && ( - { - if (isNaN(votes[user.discord_id])) increaseCount() - setVotes({ ...votes, [user.discord_id]: 2 }) - }} - /> - )} - - {!isMobile && } -
- {suggester ? ( -
- - Suggested by{" "} - - {suggester.username}#{suggester.discriminator} - - -
{description}
-
- ) : ( - - )} - - ) -} - -export default VotingGridRow diff --git a/frontend-react/src/components/plus/VotingNumber.js b/frontend-react/src/components/plus/VotingNumber.js deleted file mode 100644 index c920d88c9..000000000 --- a/frontend-react/src/components/plus/VotingNumber.js +++ /dev/null @@ -1,25 +0,0 @@ -import React from "react" -import { Button } from "semantic-ui-react" - -const VotingNumber = ({ number, selected, onClick }) => { - const color = () => { - if (!selected) return "grey" - if (number === 2) return "green" - if (number === -2) return "red" - - return null - } - const style = () => { - if (!selected || number === 2 || number === -2) return {} - else if (number === -1) return { background: "#FFA07A" } - else if (number === 1) return { background: "#90EE90" } - } - return ( - - ) -} - -export default VotingNumber diff --git a/frontend-react/src/graphql/mutations/addVotes.js b/frontend-react/src/graphql/mutations/addVotes.js deleted file mode 100644 index b2a6ae2e0..000000000 --- a/frontend-react/src/graphql/mutations/addVotes.js +++ /dev/null @@ -1,7 +0,0 @@ -import { gql } from "apollo-boost" - -export const addVotes = gql` - mutation addVotes($votes: [VoteInput!]!) { - addVotes(votes: $votes) - } -` diff --git a/frontend-react/src/graphql/mutations/addVotes.ts b/frontend-react/src/graphql/mutations/addVotes.ts new file mode 100644 index 000000000..18b545ebb --- /dev/null +++ b/frontend-react/src/graphql/mutations/addVotes.ts @@ -0,0 +1,14 @@ +import { gql, DocumentNode } from "apollo-boost" + +export interface AddVotesVars { + votes: { + discord_id: string + score: -2 | -1 | 1 | 2 + }[] +} + +export const ADD_VOTES: DocumentNode = gql` + mutation addVotes($votes: [VoteInput!]!) { + addVotes(votes: $votes) + } +` diff --git a/frontend-react/src/graphql/queries/plusInfo.ts b/frontend-react/src/graphql/queries/plusInfo.ts index cc5800a00..61148a319 100644 --- a/frontend-react/src/graphql/queries/plusInfo.ts +++ b/frontend-react/src/graphql/queries/plusInfo.ts @@ -1,5 +1,13 @@ import { gql, DocumentNode } from "apollo-boost" +export interface PlusInfoData { + plusInfo?: { + voting_ends?: string + voter_count: number + eligible_voters: number + } +} + export const PLUS_INFO: DocumentNode = gql` { plusInfo { diff --git a/frontend-react/src/graphql/queries/usersForVoting.js b/frontend-react/src/graphql/queries/usersForVoting.js deleted file mode 100644 index c9c596cbf..000000000 --- a/frontend-react/src/graphql/queries/usersForVoting.js +++ /dev/null @@ -1,37 +0,0 @@ -import { gql } from "apollo-boost" - -export const usersForVoting = gql` - { - usersForVoting { - users { - username - discriminator - twitter_name - discord_id - plus { - membership_status - vouch_status - plus_region - } - } - suggested { - discord_user { - discord_id - username - discriminator - twitter_name - } - suggester_discord_user { - username - discriminator - } - plus_region - description - } - votes { - discord_id - score - } - } - } -` diff --git a/frontend-react/src/graphql/queries/usersForVoting.ts b/frontend-react/src/graphql/queries/usersForVoting.ts new file mode 100644 index 000000000..74ed3aef5 --- /dev/null +++ b/frontend-react/src/graphql/queries/usersForVoting.ts @@ -0,0 +1,77 @@ +import { gql, DocumentNode } from "apollo-boost" + +export interface VotingSuggested { + discord_user: { + discord_id: string + username: string + discriminator: string + twitter_name?: string + } + suggester_discord_user: { + username: string + discriminator: string + } + plus_region: "EU" | "NA" + description: string +} + +export interface UsersForVotingData { + usersForVoting: { + users: { + username: string + discriminator: string + twitter_name?: string + discord_id: string + plus: { + membership_status?: "ONE" | "TWO" + vouch_status?: "ONE" | "TWO" + plus_region: "EU" | "NA" + } + }[] + suggested: VotingSuggested[] + votes: { + discord_id: string + score: number + month: number + year: number + }[] + } +} + +export const USERS_FOR_VOTING: DocumentNode = gql` + { + usersForVoting { + users { + username + discriminator + twitter_name + discord_id + plus { + membership_status + vouch_status + plus_region + } + } + suggested { + discord_user { + discord_id + username + discriminator + twitter_name + } + suggester_discord_user { + username + discriminator + } + plus_region + description + } + votes { + discord_id + score + month + year + } + } + } +`