voting frontend

This commit is contained in:
Sendou
2020-04-04 13:18:57 +03:00
parent a84d442b70
commit 314115dc64
13 changed files with 573 additions and 427 deletions

View File

@@ -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%;
}

View File

@@ -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<SubHeaderProps> = ({ children }) => {
const { themeColorHex, themeColorHexLighter, colorMode } = useContext(
MyThemeContext
)
const style = {
"--sub-header-border":
colorMode === "dark" ? themeColorHexLighter : themeColorHex,
} as React.CSSProperties
return (
<h2 className="decorated">
<span style={style}>{children}</span>
</h2>
)
}
export default SubHeader

View File

@@ -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<VotingButtonProps> = ({
value,
handleClick,
gridArea,
active,
}) => {
return (
<Flex
flexDirection="column"
justifyContent="center"
alignItems="center"
gridArea={gridArea}
>
<Flex
onClick={() => 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}
</Flex>
</Flex>
)
}
interface PersonForVotingProps {
votes: Record<string, number>
setVotes: React.Dispatch<React.SetStateAction<Record<string, number>>>
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<PersonForVotingProps> = ({
votes,
setVotes,
user,
suggester,
description,
sameRegion = true,
}) => {
const { grayWithShade } = useContext(MyThemeContext)
const handleClick = (value: number) => {
setVotes({ ...votes, [user.discord_id]: value })
}
return (
<Grid
gridTemplateColumns="repeat(4, 1fr)"
gridTemplateRows="repeat(2, 1fr)"
my="1em"
rounded="lg"
overflow="hidden"
boxShadow="0px 0px 16px 6px rgba(0,0,0,0.1)"
p="12px"
>
<Flex
gridArea="1 / 1 / 2 / 5"
justifyContent="center"
alignItems="center"
flexDirection="column"
>
<UserAvatar twitterName={user.twitter_name} name={user.username} />
<Link to={`/u/${user.discord_id}`}>
<Box
color={grayWithShade}
fontWeight="semibold"
letterSpacing="wide"
mx="0.5em"
>
{user.username}#{user.discriminator}
</Box>
</Link>
</Flex>
{sameRegion ? (
<VotingButton
value={-2}
handleClick={handleClick}
gridArea="2 / 1 / 3 / 2"
active={votes[user.discord_id] === -2}
/>
) : (
<Box gridArea="2 / 2 / 3 / 3" />
)}
<VotingButton
value={-1}
handleClick={handleClick}
gridArea="2 / 2 / 3 / 3"
active={votes[user.discord_id] === -1}
/>
<VotingButton
value={1}
handleClick={handleClick}
gridArea="2 / 3 / 3 / 4"
active={votes[user.discord_id] === 1}
/>
{sameRegion ? (
<VotingButton
value={2}
handleClick={handleClick}
gridArea="2 / 4 / 3 / 5"
active={votes[user.discord_id] === 2}
/>
) : (
<Box gridArea="2 / 4 / 3 / 5" />
)}
</Grid>
)
}
export default PersonForVoting

View File

@@ -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<RouteComponentProps> = () => {
const { data, error, loading } = useQuery<PlusInfoData>(PLUS_INFO)
@@ -49,6 +42,7 @@ const PlusPage: React.FC<RouteComponentProps> = () => {
if (!data.plusInfo) return <Redirect to="/404" />
const maplist = maplistData.plusMaplists[0]
const plusInfo = data.plusInfo
return (
<>
@@ -56,29 +50,6 @@ const PlusPage: React.FC<RouteComponentProps> = () => {
<title>Plus Server Home | sendou.ink</title>
</Helmet>
<PageHeader title="Plus Server" />
{/*data.plusInfo.voting_ends && userData.user.plus?.membership_status ? (
<Voting
user={userData.user}
handleSuccess={handleSuccess}
handleError={handleError}
votedSoFar={data.plusInfo.voter_count}
eligibleVoters={data.plusInfo.eligible_voters}
votingEnds={
data.plusInfo.voting_ends
? parseInt(data.plusInfo.voting_ends)
: null
}
/>
) : (
<Suggestions
user={userData.user}
plusServer={userData.user?.plus?.membership_status}
showSuggestionForm={showSuggestionForm}
setShowSuggestionForm={setShowSuggestionForm}
handleSuccess={handleSuccess}
handleError={handleError}
/>
)*/}
<Flex mb="1em" flexWrap="wrap">
<Box mr="1em" mt="1em">
<Link to="/plus/history">
@@ -102,15 +73,26 @@ const PlusPage: React.FC<RouteComponentProps> = () => {
</Link>
</Box>
</Flex>
<Maplist
name={maplist.name}
sz={maplist.sz}
tc={maplist.tc}
rm={maplist.rm}
cb={maplist.cb}
voterCount={maplist.plus.voter_count}
/>
<Suggestions user={userData.user} />
{plusInfo.voting_ends ? (
<Voting
user={userData.user}
votingEnds={parseInt(plusInfo.voting_ends)}
votedSoFar={plusInfo.voter_count}
eligibleVoters={plusInfo.eligible_voters}
/>
) : (
<>
<Maplist
name={maplist.name}
sz={maplist.sz}
tc={maplist.tc}
rm={maplist.rm}
cb={maplist.cb}
voterCount={maplist.plus.voter_count}
/>
<Suggestions user={userData.user} />
</>
)}
</>
)
}

View File

@@ -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 <Error errorMessage={error.message} />
if (loading || !suggestedArrays) return <Loading />
const date = new Date()
if (votingEnds < date.getTime())
return (
<Message>
<Message.Header>Voting for the month is over</Message.Header>
Results will be posted later
</Message>
)
const hoursLeft = Math.ceil((votingEnds - date.getTime()) / (1000 * 60 * 60))
const alreadyVoted = data.usersForVoting.votes.length > 0
return (
<>
<Prompt
when={
voteCount > 0 &&
voteCount <
data.usersForVoting.users.length +
data.usersForVoting.suggested.length
}
message="Are you sure you want to leave? Vote form won't be saved."
/>
<Message
success={alreadyVoted}
icon={alreadyVoted ? "check" : null}
header={alreadyVoted ? "You have voted (editing possible)" : null}
content={`Voting ends ${new Date(
votingEnds
).toLocaleString()} (${hoursLeft}~
hours left)`}
/>
<Progress
value={votedSoFar}
total={eligibleVoters}
progress="ratio"
color="blue"
>
Voted so far
</Progress>
<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}
increaseCount={() => setVoteCount(voteCount + 1)}
/>
)
})}
</Grid>
{suggestedArrays.sameRegion.length > 0 && (
<>
<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}
increaseCount={() => setVoteCount(voteCount + 1)}
/>
)
})}
</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}
increaseCount={() => setVoteCount(voteCount + 1)}
/>
)
})}
</Grid>
{suggestedArrays.otherRegion.length > 0 && (
<>
<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}
increaseCount={() => setVoteCount(voteCount + 1)}
/>
)
})}
</Grid>
</>
)}
<Button
disabled={
voteCount <
data.usersForVoting.users.length +
data.usersForVoting.suggested.length
}
positive
style={{ marginTop: "2em" }}
onClick={handleSubmit}
>
Submit
</Button>
</>
)
}
export default Voting

View File

@@ -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<VotingProps> = ({
user,
votingEnds,
votedSoFar,
eligibleVoters,
}) => {
const { themeColor, grayWithShade } = useContext(MyThemeContext)
const { data, loading, error } = useQuery<UsersForVotingData>(
USERS_FOR_VOTING
)
const [votes, setVotes] = useState<Record<string, number>>({})
const [suggestedArrays, setSuggestedArrays] = useState<SuggestedArrays>({
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<string, number> = {}
data!.usersForVoting.votes.forEach(
(vote) => (voteObj[vote.discord_id] = vote.score)
)
setVotes(voteObj)
}
}, [loading, error, data, user])
if (loading) return <Loading />
if (error) return <Error errorMessage={error.message} />
const date = new Date()
if (votingEnds < date.getTime())
return (
<Alert status="info">
Voting is over. Results will be posted a bit later.
</Alert>
)
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 (
<>
<Alert status={alreadyVoted ? "success" : "info"}>{`${
alreadyVoted ? "You have voted! " : ""
}Voting ends ${new Date(votingEnds).toLocaleString()} (${hoursLeft}
hours left)`}</Alert>
<Box mt="1em" textAlign="center" color={grayWithShade}>
<Progress
value={(votedSoFar / eligibleVoters) * 100}
color={themeColor}
/>
{votedSoFar}/{eligibleVoters} voted so far
</Box>
<Box mt="2em">
<SubHeader>
{user.plus!.plus_region === "EU" ? "European" : "American"} players
</SubHeader>
{data!.usersForVoting.users.map((userForVoting) => {
if (userForVoting.plus.plus_region !== user.plus!.plus_region)
return null
return (
<PersonForVoting
key={userForVoting.discord_id}
user={userForVoting}
votes={votes}
setVotes={setVotes}
/>
)
})}
</Box>
{suggestedArrays.sameRegion.length > 0 && (
<>
<SubHeader>
{user.plus!.plus_region === "EU" ? "European" : "American"} players
(suggested)
</SubHeader>
{suggestedArrays.sameRegion.map((suggestion) => {
return (
<PersonForVoting
key={suggestion.discord_user.discord_id}
user={suggestion.discord_user}
suggester={suggestion.suggester_discord_user}
votes={votes}
setVotes={setVotes}
description={suggestion.description}
/>
)
})}
</>
)}
<SubHeader>
{user.plus!.plus_region === "NA" ? "European" : "American"} players
</SubHeader>
{data!.usersForVoting.users.map((userForVoting) => {
if (userForVoting.plus.plus_region === user.plus!.plus_region)
return null
return (
<PersonForVoting
key={userForVoting.discord_id}
user={userForVoting}
votes={votes}
setVotes={setVotes}
sameRegion={false}
/>
)
})}
{suggestedArrays.otherRegion.length > 0 && (
<>
<SubHeader>
{user.plus!.plus_region === "NA" ? "European" : "American"} players
(suggested)
</SubHeader>
{suggestedArrays.otherRegion.map((suggestion) => {
return (
<PersonForVoting
key={suggestion.discord_user.discord_id}
user={suggestion.discord_user}
votes={votes}
setVotes={setVotes}
sameRegion={false}
/>
)
})}
</>
)}
<Flex mt="2em" alignItems="center">
<Box mr="1em">
<Button
disabled={missingVotes > 0}
onClick={handleSubmit}
loading={addVotesLoading}
>
Submit
</Button>
</Box>
{missingVotes > 0 && (
<>
You need to vote on {missingVotes} more player
{missingVotes > 1 ? "s" : ""}
</>
)}
</Flex>
</>
)
}
export default Voting

View File

@@ -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 (
<>
<Grid.Row columns={6}>
<Grid.Column>
<div>
<Link
to={`/u/${user.discord_id}`}
target="_blank"
rel="noopener noreferrer"
>
<UserAvatar twitterName={user.twitter_name} />
{user.username}#{user.discriminator}
</Link>
</div>
</Grid.Column>
{isMobile && <Grid.Column />}
<Grid.Column>
{sameRegion && (
<VotingNumber
number={-2}
selected={votes[user.discord_id] === -2}
onClick={() => {
if (isNaN(votes[user.discord_id])) increaseCount()
setVotes({ ...votes, [user.discord_id]: -2 })
}}
/>
)}
</Grid.Column>
<Grid.Column>
<VotingNumber
number={-1}
selected={votes[user.discord_id] === -1}
onClick={() => {
if (isNaN(votes[user.discord_id])) increaseCount()
setVotes({ ...votes, [user.discord_id]: -1 })
}}
/>
</Grid.Column>
<Grid.Column>
<VotingNumber
number={1}
selected={votes[user.discord_id] === 1}
onClick={() => {
if (isNaN(votes[user.discord_id])) increaseCount()
setVotes({ ...votes, [user.discord_id]: 1 })
}}
/>
</Grid.Column>
<Grid.Column>
{sameRegion && (
<VotingNumber
number={2}
selected={votes[user.discord_id] === 2}
onClick={() => {
if (isNaN(votes[user.discord_id])) increaseCount()
setVotes({ ...votes, [user.discord_id]: 2 })
}}
/>
)}
</Grid.Column>
{!isMobile && <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

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

View File

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

View File

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

View File

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

View File

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

View File

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