From 2c58d52e35298d0e31c9cd62abcd54a9c0e79977 Mon Sep 17 00:00:00 2001 From: Sendou Date: Thu, 2 Jan 2020 02:49:24 +0200 Subject: [PATCH] voting progress --- .../src/components/common/UserAvatar.js | 18 ++ .../src/components/freeagents/FAPostForm.js | 2 +- .../src/components/plus/PlusPage.js | 33 +++- .../src/components/plus/Suggestions.js | 23 +-- frontend-react/src/components/plus/Voting.js | 177 +++++++++++++++++- .../src/components/plus/VotingGridRow.js | 82 ++++++++ .../src/components/plus/VotingNumber.js | 25 +++ .../src/graphql/mutations/addVotes.js | 7 + .../src/graphql/queries/userLean.js | 1 + .../src/graphql/queries/usersForVoting.js | 33 ++++ graphql-schemas/plus.js | 123 +++++++++++- graphql-schemas/user.js | 3 +- index.js | 3 +- mongoose-models/state.js | 7 + utils/shuffleArray.js | 13 ++ 15 files changed, 514 insertions(+), 36 deletions(-) create mode 100644 frontend-react/src/components/common/UserAvatar.js create mode 100644 frontend-react/src/components/plus/VotingGridRow.js create mode 100644 frontend-react/src/components/plus/VotingNumber.js create mode 100644 frontend-react/src/graphql/mutations/addVotes.js create mode 100644 frontend-react/src/graphql/queries/usersForVoting.js create mode 100644 mongoose-models/state.js create mode 100644 utils/shuffleArray.js diff --git a/frontend-react/src/components/common/UserAvatar.js b/frontend-react/src/components/common/UserAvatar.js new file mode 100644 index 000000000..036dbbc75 --- /dev/null +++ b/frontend-react/src/components/common/UserAvatar.js @@ -0,0 +1,18 @@ +import React, { useState } from "react" +import { Image } from "semantic-ui-react" + +const UserAvatar = ({ twitterName }) => { + const [imageError, setImageError] = useState(false) + + if (!twitterName || imageError) return null + + return ( + setImageError(true)} + /> + ) +} + +export default UserAvatar diff --git a/frontend-react/src/components/freeagents/FAPostForm.js b/frontend-react/src/components/freeagents/FAPostForm.js index b4e2c4346..06131506e 100644 --- a/frontend-react/src/components/freeagents/FAPostForm.js +++ b/frontend-react/src/components/freeagents/FAPostForm.js @@ -74,7 +74,7 @@ const FAPostForm = ({ Object.keys(postToAdd).forEach( key => !postToAdd[key] && delete postToAdd[key] ) - if (existingFAPost) updateFAPostMutation({ variables: postToAdd }) + if (existingFAPost) await updateFAPostMutation({ variables: postToAdd }) else await addFAPostMutation({ variables: postToAdd }) } diff --git a/frontend-react/src/components/plus/PlusPage.js b/frontend-react/src/components/plus/PlusPage.js index d4d620de4..77b4e5681 100644 --- a/frontend-react/src/components/plus/PlusPage.js +++ b/frontend-react/src/components/plus/PlusPage.js @@ -5,9 +5,10 @@ import Suggestions from "./Suggestions" import Loading from "../common/Loading" import Error from "../common/Error" import { plusInfo } from "../../graphql/queries/plusInfo" -import { Input } from "semantic-ui-react" +import { Input, Message } from "semantic-ui-react" import { Redirect } from "react-router-dom" import { userLean } from "../../graphql/queries/userLean" +import Voting from "./Voting" const copyToClipboard = (e, refToUse, setCopySuccess) => { refToUse.current.select() @@ -42,6 +43,8 @@ const PlusPage = () => { const [plusOneCopySuccess, setPlusOneCopySuccess] = useState(false) const plusTwoRef = useRef(null) const [plusTwoCopySuccess, setPlusTwoCopySuccess] = useState(false) + const [successMsg, setSuccessMsg] = useState(null) + const [errorMsg, setErrorMsg] = useState(null) const { data, error, loading } = useQuery(plusInfo) const { @@ -56,6 +59,22 @@ const PlusPage = () => { if (!userData.user) return if (!data.plusInfo) return + const handleSuccess = () => { + setSuccessMsg("Suggestion successfully added.") + setShowSuggestionForm(false) + setTimeout(() => { + setSuccessMsg(null) + }, 10000) + } + + const handleError = error => { + setErrorMsg(error.message) + setShowSuggestionForm(false) + setTimeout(() => { + setErrorMsg(null) + }, 10000) + } + return ( <> {!showSuggestionForm && ( @@ -92,12 +111,22 @@ const PlusPage = () => { )} )} - {!data.plusInfo.voting_ends && ( + {successMsg && {successMsg}} + {errorMsg && {errorMsg}} + {!data.plusInfo.voting_ends ? ( + + ) : ( )} diff --git a/frontend-react/src/components/plus/Suggestions.js b/frontend-react/src/components/plus/Suggestions.js index b9f05edb1..4fe703e6d 100644 --- a/frontend-react/src/components/plus/Suggestions.js +++ b/frontend-react/src/components/plus/Suggestions.js @@ -77,10 +77,9 @@ const Suggestions = ({ showSuggestionForm, setShowSuggestionForm, plusServer, + handleSuccess, + handleError, }) => { - const [successMsg, setSuccessMsg] = useState(null) - const [errorMsg, setErrorMsg] = useState(null) - const { data, error, loading } = useQuery(suggestions) if (loading) return @@ -91,22 +90,6 @@ const Suggestions = ({ suggestion.suggester_discord_user.discord_id === user.discord_id ) - const handleSuccess = () => { - setSuccessMsg("Suggestion successfully added.") - setShowSuggestionForm(false) - setTimeout(() => { - setSuccessMsg(null) - }, 10000) - } - - const handleError = error => { - setErrorMsg(error.message) - setShowSuggestionForm(false) - setTimeout(() => { - setErrorMsg(null) - }, 10000) - } - return ( <> {!showSuggestionForm && !ownSuggestion && ( @@ -116,8 +99,6 @@ const Suggestions = ({ )} - {successMsg && {successMsg}} - {errorMsg && {errorMsg}} {showSuggestionForm && ( {} +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 }) => { + const date = new Date() + const votingEnds = date.valueOf() + 172800000 + 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], + })), + }, + }) + } + + const randomizeVotes = () => { + const obj = {} + + data.usersForVoting.suggested.forEach( + suggest => (obj[suggest.discord_user.discord_id] = 1) + ) + data.usersForVoting.users.forEach(suggest => (obj[suggest.discord_id] = 1)) + + setVotes(obj) + } + + 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, + }) + }, [loading, error, data, user]) + + if (error) return + if (loading || !suggestedArrays) return + + console.log("votes", votes) + + return ( + <> + + Voting ends {new Date(votingEnds).toLocaleString()} + +

+ {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 && ( + <> +

+ {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 && ( + <> +

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

+ + {suggestedArrays.otherRegion.map(suggestion => { + return ( + + ) + })} + + + )} + + randomizeVotes()}> + Randomize + + + ) +} export default Voting diff --git a/frontend-react/src/components/plus/VotingGridRow.js b/frontend-react/src/components/plus/VotingGridRow.js new file mode 100644 index 000000000..6792386ac --- /dev/null +++ b/frontend-react/src/components/plus/VotingGridRow.js @@ -0,0 +1,82 @@ +import React from "react" +import { Divider, Grid } from "semantic-ui-react" +import VotingNumber from "./VotingNumber" +import UserAvatar from "../common/UserAvatar" +import { Link } from "react-router-dom" + +const VotingGridRow = ({ + votes, + setVotes, + user, + suggester, + description, + sameRegion = true, +}) => { + return ( + <> + + + + + {user.username}#{user.discriminator} + + + + {sameRegion && ( + setVotes({ ...votes, [user.discord_id]: -2 })} + /> + )} + + + setVotes({ ...votes, [user.discord_id]: -1 })} + /> + + + setVotes({ ...votes, [user.discord_id]: 1 })} + /> + + + {sameRegion && ( + setVotes({ ...votes, [user.discord_id]: 2 })} + /> + )} + + + {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 new file mode 100644 index 000000000..200af891a --- /dev/null +++ b/frontend-react/src/components/plus/VotingNumber.js @@ -0,0 +1,25 @@ +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 new file mode 100644 index 000000000..b2a6ae2e0 --- /dev/null +++ b/frontend-react/src/graphql/mutations/addVotes.js @@ -0,0 +1,7 @@ +import { gql } from "apollo-boost" + +export const addVotes = gql` + mutation addVotes($votes: [VoteInput!]!) { + addVotes(votes: $votes) + } +` diff --git a/frontend-react/src/graphql/queries/userLean.js b/frontend-react/src/graphql/queries/userLean.js index 315da2710..eb115ca97 100644 --- a/frontend-react/src/graphql/queries/userLean.js +++ b/frontend-react/src/graphql/queries/userLean.js @@ -10,6 +10,7 @@ export const userLean = gql` custom_url plus { membership_status + plus_region } } } diff --git a/frontend-react/src/graphql/queries/usersForVoting.js b/frontend-react/src/graphql/queries/usersForVoting.js new file mode 100644 index 000000000..b57dae662 --- /dev/null +++ b/frontend-react/src/graphql/queries/usersForVoting.js @@ -0,0 +1,33 @@ +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 + } + } + } +` diff --git a/graphql-schemas/plus.js b/graphql-schemas/plus.js index bb4d52321..7fa8cf28f 100644 --- a/graphql-schemas/plus.js +++ b/graphql-schemas/plus.js @@ -3,10 +3,12 @@ const { AuthenticationError, gql, } = require("apollo-server-express") +const shuffle = require("../utils/shuffleArray") const User = require("../mongoose-models/user") const Suggested = require("../mongoose-models/suggested") const Summary = require("../mongoose-models/summary") const VotedPerson = require("../mongoose-models/votedperson") +const State = require("../mongoose-models/state") const typeDef = gql` extend type Query { @@ -23,6 +25,7 @@ const typeDef = gql` region: String! description: String! ): Boolean! + addVotes(votes: [VoteInput!]!): Boolean! } "+1 or +2 LFG server on Discord" @@ -67,13 +70,18 @@ const typeDef = gql` plus: PlusStatus } + input VoteInput { + discord_id: String! + score: Int! + } + type VotedPerson { discord_id: String! voter_discord_id: String! month: Int! year: Int! "Voting result -2 to +2 (-1 to +1 cross-region)" - score: Float! + score: Int! } "Voting result of a player" @@ -94,6 +102,33 @@ const typeDef = gql` } ` +const validateVotes = (votes, users, suggested, user) => { + const region = user.plus.plus_region + + votes.forEach(vote => { + const { discord_id, score } = vote + + let user = users.find(user => user.discord_id === discord_id) + + if (!user) { + user = suggested.find( + suggested => suggested.discord_user.discord_id === discord_id + ) + if (!user) + throw new UserInputError( + `Invalid user voted on with the id ${discord_id}` + ) + user = user.discord_user + } + + if (score !== -2 && score !== -1 && score !== 1 && score !== 2) + throw new Error(`Invalid score provided: ${score}`) + + if ((score === -2 || score === 2) && region !== user.plus.plus_region) + throw new Error("Score of -2 or 2 given cross region") + }) +} + const resolvers = { Query: { hasAccess: async (root, args) => { @@ -136,18 +171,21 @@ const resolvers = { ? "ONE" : "TWO" + const state = await State.findOne({}) + return { plus_one_invite_link: plus_server === "ONE" ? process.env.PLUS_ONE_LINK : null, plus_two_invite_link: process.env.PLUS_TWO_LINK, - voting_ends: null, + voting_ends: state.voting_ends, } }, usersForVoting: async (root, args, ctx) => { if (!ctx.user) throw new UserInputError("Not logged in") if (!ctx.user.plus || !ctx.user.plus.membership_status) throw new UserInputError("Not plus server member") - const plus_server = ctx.user.plus.membership_status + //const plus_server = ctx.user.plus.membership_status + const plus_server = "TWO" const users = await User.find({ $or: [ @@ -164,12 +202,18 @@ const resolvers = { }) }) - const suggested = await Suggested.find({ plus_server }).catch(e => { - throw (new Error(), - { - error: e, + const suggested = await Suggested.find({ plus_server }) + .populate("discord_user") + .populate("suggester_discord_user") + .catch(e => { + throw (new Error(), + { + error: e, + }) }) - }) + + shuffle(users) + shuffle(suggested) return { users, suggested } }, @@ -271,6 +315,69 @@ const resolvers = { }) }) + return true + }, + addVotes: async (root, args, ctx) => { + if (!ctx.user) throw new AuthenticationError("Not logged in.") + if ( + !ctx.user.plus || + (!ctx.user.plus.membership_status && !ctx.user.plus.vouch_status) + ) { + throw new AuthenticationError("Not plus member.") + } + + const state = await State.findOne({}) + + const date = new Date() + if (!state.voting_ends || state.voting_ends < date.getTime()) + throw new Error("Voting now open right now") + + const votedUsers = {} + + args.votes.forEach(vote => { + if (votedUsers[vote.discord_id]) + throw new UserInputVote( + `Duplicate vote with the id ${vote.discord_id}` + ) + votedUsers[vote.discord_id] = true + }) + + const users = await User.find({ + $or: [ + { + "plus.membership_status": plus_server, + }, + + { "plus.vouch_status": plus_server }, + ], + }) + + const suggested = await Suggested.find({ plus_server }) + .populate("discord_user") + .populate("suggester_discord_user") + + if (users.length + suggested.length !== args.votes.length) + throw new UserInputError("Invalid number of votes provided") + + validateVotes(args.votes, users, suggested, ctx.user) + + const year = date.getFullYear() + const month = date.getMonth() + await VotedPerson.deleteMany({ + voter_discord_id: ctx.user.discord_id, + month, + year, + }) + + const toInsert = args.votes.map(vote => ({ + discord_id: vote.discord_id, + voter_discord_id: ctx.user.discord_id, + month, + year, + score: vote.score, + })) + await VotedPerson.insertMany(toInsert) + return true }, }, diff --git a/graphql-schemas/user.js b/graphql-schemas/user.js index b9878a919..7f2a0b1ea 100644 --- a/graphql-schemas/user.js +++ b/graphql-schemas/user.js @@ -89,10 +89,11 @@ const resolvers = { twitter_name: "sendouc", username: "Sendou", top500: true, - //custom_url: "sendou", + custom_url: "sendou", plus: { membership_status: "ONE", vouch_status: null, + plus_region: "EU", }, } } diff --git a/index.js b/index.js index a6d1a041d..541682ac0 100644 --- a/index.js +++ b/index.js @@ -95,7 +95,7 @@ const server = new ApolloServer({ twitch_name: "sendou", twitter_name: "sendouc", username: "Sendou", - //custom_url: "sendou", + custom_url: "sendou", weapons: [ "Tenta Brella", "Range Blaster", @@ -107,6 +107,7 @@ const server = new ApolloServer({ plus: { membership_status: "ONE", vouch_status: null, + plus_region: "EU", }, }, } diff --git a/mongoose-models/state.js b/mongoose-models/state.js new file mode 100644 index 000000000..a7a502479 --- /dev/null +++ b/mongoose-models/state.js @@ -0,0 +1,7 @@ +const mongoose = require("mongoose") + +const stateSchema = new mongoose.Schema({ + voting_ends: Date, +}) + +module.exports = mongoose.model("State", stateSchema) diff --git a/utils/shuffleArray.js b/utils/shuffleArray.js new file mode 100644 index 000000000..bc1bcdfc0 --- /dev/null +++ b/utils/shuffleArray.js @@ -0,0 +1,13 @@ +//https://stackoverflow.com/a/6274381 +function shuffle(a) { + var j, x, i + for (i = a.length - 1; i > 0; i--) { + j = Math.floor(Math.random() * (i + 1)) + x = a[i] + a[i] = a[j] + a[j] = x + } + return a +} + +module.exports = shuffle