voting progress

This commit is contained in:
Sendou
2020-01-02 02:49:24 +02:00
parent 7b68be59bb
commit 2c58d52e35
15 changed files with 514 additions and 36 deletions

View File

@@ -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 (
<Image
src={`https://avatars.io/twitter/${twitterName}`}
avatar
onError={error => setImageError(true)}
/>
)
}
export default UserAvatar

View File

@@ -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 })
}

View File

@@ -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 <Redirect to="/access" />
if (!data.plusInfo) return <Redirect to="/404" />
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 && <Message success>{successMsg}</Message>}
{errorMsg && <Message error>{errorMsg}</Message>}
{!data.plusInfo.voting_ends ? (
<Voting
user={userData.user}
handleSuccess={handleSuccess}
handleError={handleError}
/>
) : (
<Suggestions
user={userData.user}
plusServer={userData.user?.plus?.membership_status}
showSuggestionForm={showSuggestionForm}
setShowSuggestionForm={setShowSuggestionForm}
handleSuccess={handleSuccess}
handleError={handleError}
/>
)}
</>

View File

@@ -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 <Loading minHeight="250px" />
@@ -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 = ({
</Button>
</div>
)}
{successMsg && <Message success>{successMsg}</Message>}
{errorMsg && <Message error>{errorMsg}</Message>}
{showSuggestionForm && (
<SuggestionForm
plusServer={plusServer}

View File

@@ -1,5 +1,178 @@
import React from "react"
import React, { useState, useEffect } from "react"
import { usersForVoting } from "../../graphql/queries/usersForVoting"
import { useQuery, useMutation } from "@apollo/react-hooks"
import { Grid, Message, Button } from "semantic-ui-react"
const Voting = () => {}
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 <Error errorMessage={error.message} />
if (loading || !suggestedArrays) return <Loading />
console.log("votes", votes)
return (
<>
<Message>
Voting ends <b>{new Date(votingEnds).toLocaleString()}</b>
</Message>
<h2 style={{ marginTop: "1em" }}>
{user.plus.plus_region === "EU" ? "European" : "American"} players
</h2>
<Grid>
{data.usersForVoting.users.map(userForVoting => {
if (userForVoting.plus.plus_region !== user.plus.plus_region)
return null
return (
<VotingGridRow
key={userForVoting.discord_id}
user={userForVoting}
votes={votes}
setVotes={setVotes}
/>
)
})}
</Grid>
{suggestedArrays.sameRegion && (
<>
<h2 style={{ marginTop: "2em" }}>
{user.plus.plus_region === "EU" ? "European" : "American"} players
(suggested)
</h2>
<Grid>
{suggestedArrays.sameRegion.map(suggestion => {
return (
<VotingGridRow
key={suggestion.discord_user.discord_id}
user={suggestion.discord_user}
suggester={suggestion.suggester_discord_user}
votes={votes}
setVotes={setVotes}
description={suggestion.description}
/>
)
})}
</Grid>
</>
)}
<h2 style={{ marginTop: "2em" }}>
{user.plus.plus_region === "NA" ? "European" : "American"} players
</h2>
<Grid>
{data.usersForVoting.users.map(userForVoting => {
if (userForVoting.plus.plus_region === user.plus.plus_region)
return null
return (
<VotingGridRow
key={userForVoting.discord_id}
user={userForVoting}
votes={votes}
setVotes={setVotes}
sameRegion={false}
/>
)
})}
</Grid>
{suggestedArrays.otherRegion && (
<>
<h2 style={{ marginTop: "2em" }}>
{user.plus.plus_region === "NA" ? "European" : "American"} players
(suggested)
</h2>
<Grid>
{suggestedArrays.otherRegion.map(suggestion => {
return (
<VotingGridRow
key={suggestion.discord_user.discord_id}
user={suggestion.discord_user}
votes={votes}
setVotes={setVotes}
sameRegion={false}
/>
)
})}
</Grid>
</>
)}
<Button
disabled={
Object.keys(votes).length !==
data.usersForVoting.users.length +
data.usersForVoting.suggested.length
}
positive
style={{ marginTop: "2em" }}
onClick={handleSubmit}
>
Submit
</Button>
<span style={{ marginLeft: "2em" }} onClick={() => randomizeVotes()}>
Randomize
</span>
</>
)
}
export default Voting

View File

@@ -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 (
<>
<Grid.Row columns={5}>
<Grid.Column style={{ wordWrap: "normal" }}>
<Link
to={`/u/${user.discord_id}`}
target="_blank"
rel="noopener noreferrer"
>
<UserAvatar twitterName={user.twitter_name} />
{user.username}#{user.discriminator}
</Link>
</Grid.Column>
<Grid.Column>
{sameRegion && (
<VotingNumber
number={-2}
selected={votes[user.discord_id] === -2}
onClick={() => setVotes({ ...votes, [user.discord_id]: -2 })}
/>
)}
</Grid.Column>
<Grid.Column>
<VotingNumber
number={-1}
selected={votes[user.discord_id] === -1}
onClick={() => setVotes({ ...votes, [user.discord_id]: -1 })}
/>
</Grid.Column>
<Grid.Column>
<VotingNumber
number={1}
selected={votes[user.discord_id] === 1}
onClick={() => setVotes({ ...votes, [user.discord_id]: 1 })}
/>
</Grid.Column>
<Grid.Column>
{sameRegion && (
<VotingNumber
number={2}
selected={votes[user.discord_id] === 2}
onClick={() => setVotes({ ...votes, [user.discord_id]: 2 })}
/>
)}
</Grid.Column>
</Grid.Row>
{suggester ? (
<div style={{ margin: "0.5em 0 0.5em 0" }}>
<b>
Suggested by{" "}
<Link
to={`/u/${suggester.discord_id}`}
target="_blank"
rel="noopener noreferrer"
>
{suggester.username}#{suggester.discriminator}
</Link>
</b>
<div>{description}</div>
</div>
) : (
<Divider />
)}
</>
)
}
export default VotingGridRow

View File

@@ -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 (
<Button color={color()} style={style()} circular onClick={onClick}>
{number > 0 && "+"}
{number}
</Button>
)
}
export default VotingNumber

View File

@@ -0,0 +1,7 @@
import { gql } from "apollo-boost"
export const addVotes = gql`
mutation addVotes($votes: [VoteInput!]!) {
addVotes(votes: $votes)
}
`

View File

@@ -10,6 +10,7 @@ export const userLean = gql`
custom_url
plus {
membership_status
plus_region
}
}
}

View File

@@ -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
}
}
}
`

View File

@@ -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
},
},

View File

@@ -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",
},
}
}

View File

@@ -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",
},
},
}

7
mongoose-models/state.js Normal file
View File

@@ -0,0 +1,7 @@
const mongoose = require("mongoose")
const stateSchema = new mongoose.Schema({
voting_ends: Date,
})
module.exports = mongoose.model("State", stateSchema)

13
utils/shuffleArray.js Normal file
View File

@@ -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