team page stuff

This commit is contained in:
Sendou
2020-02-10 17:51:41 +02:00
parent 97b4af26f8
commit 57cb5c3330
15 changed files with 1355 additions and 11 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -16,6 +16,7 @@
"@types/node": "^13.7.0",
"@types/reach__router": "^1.2.6",
"@types/react": "^16.9.19",
"@types/react-datepicker": "^2.11.0",
"@types/react-dom": "^16.9.5",
"@types/react-infinite-scroller": "^1.2.1",
"@types/react-select": "^3.0.10",
@@ -23,7 +24,9 @@
"emotion-theming": "^10.0.27",
"graphql": "^14.6.0",
"jstz": "^2.1.1",
"node-sass": "^4.13.1",
"react": "^16.12.0",
"react-datepicker": "^2.12.0",
"react-dom": "^16.12.0",
"react-draggable": "^4.2.0",
"react-helmet-async": "^1.0.4",
@@ -32,6 +35,7 @@
"react-infinite-scroller": "^1.2.4",
"react-scripts": "^3.3.1",
"react-select": "^3.0.8",
"react-tweet-embed": "^1.2.2",
"typescript": "^3.7.5"
},
"scripts": {

View File

@@ -0,0 +1,30 @@
import React from "react"
import HackerOneDatePicker from "react-datepicker"
import Button from "./Button"
import "react-datepicker/src/stylesheets/datepicker.scss"
interface DatePickerProps {
date: Date
setDate: (date: Date | null) => void
}
interface CustomInputProps {
value?: string
onClick?: () => void
}
const CustomInput: React.FC<CustomInputProps> = ({ value, onClick }) => (
<Button onClick={onClick as () => void}>{value as string}</Button>
)
const DatePicker: React.FC<DatePickerProps> = ({ date, setDate }) => {
return (
<HackerOneDatePicker
selected={date}
onChange={date => setDate(date)}
customInput={<CustomInput />}
/>
)
}
export default DatePicker

View File

@@ -8,6 +8,7 @@ const BuildsPage = lazy(() => import("../builds/BuildsPage"))
const CalendarPage = lazy(() => import("../calendar/CalendarPage"))
const MapPlannerPage = lazy(() => import("../plans/MapPlannerPage"))
const FreeAgentsPage = lazy(() => import("../freeagents/FreeAgentsPage"))
const TeamPage = lazy(() => import("../team/TeamPage"))
const Routes: React.FC = () => {
return (
@@ -15,6 +16,7 @@ const Routes: React.FC = () => {
<Router>
<HomePage path="/" />
<UserPage path="/u/:id" />
<TeamPage path="/t/:name" />
<BuildsPage path="/builds" />
<MapPlannerPage path="/plans" />
<CalendarPage path="/calendar" />

View File

@@ -0,0 +1,90 @@
import React, { useState } from "react"
import Modal from "../elements/Modal"
import Box from "../elements/Box"
import { TournamentResult } from "../../types"
import Input from "../elements/Input"
import { useMutation } from "@apollo/react-hooks"
import { useToast } from "@chakra-ui/core"
import { ADD_RESULT } from "../../graphql/mutations/addResult"
import TweetEmbed from "react-tweet-embed"
import { useContext } from "react"
import MyThemeContext from "../../themeContext"
import DatePicker from "../elements/DatePicker"
import Label from "../elements/Label"
interface AddResultModalProps {
closeModal: () => void
}
const AddResultModal: React.FC<AddResultModalProps> = ({ closeModal }) => {
const { colorMode } = useContext(MyThemeContext)
const [result, setResult] = useState<Partial<TournamentResult>>({})
const toast = useToast()
const [addResult] = useMutation<boolean, TournamentResult>(ADD_RESULT, {
variables: result as TournamentResult,
onCompleted: () => {
closeModal()
toast({
description: "Result added",
position: "top-right",
status: "success",
duration: 10000,
})
},
onError: error => {
toast({
title: "An error occurred",
description: error.message,
position: "top-right",
status: "success",
duration: 10000,
})
},
refetchQueries: ["searchForUser"],
})
const handleChange = (newValueObject: Partial<TournamentResult>) => {
console.log("newV", newValueObject)
setResult({ ...result, ...newValueObject })
}
return (
<Modal title="Adding a new tournament result" closeModal={closeModal}>
<Box display="flex" flexDirection="column">
<Box>
<Input
label="Tournament name"
value={result.tournament_name}
setValue={value => handleChange({ tournament_name: value })}
/>
</Box>
<Box mt="1em">
<Label>Date</Label>
<DatePicker
date={result.date ? new Date(result.date) : new Date()}
setDate={value => handleChange({ date: value?.toString() })}
/>
</Box>
<Box mt="1em">
<Input
label="Tweet id"
value={result.tweet_id}
setValue={value => handleChange({ tweet_id: value })}
textLeft="https://twitter.com/.../status/"
/>
</Box>
{result.tweet_id && (
<Box mt="1em">
<TweetEmbed
id={result.tweet_id}
options={{ theme: "dark", dnt: "true", conversation: "none" }}
/>
</Box>
)}
</Box>
</Modal>
)
}
export default AddResultModal

View File

@@ -0,0 +1,32 @@
import React from "react"
import UserAvatar from "../common/UserAvatar"
import Box from "../elements/Box"
import { useContext } from "react"
import MyThemeContext from "../../themeContext"
interface LogoHeaderProps {
name: string
twitter_name?: string
}
const LogoHeader: React.FC<LogoHeaderProps> = ({ name, twitter_name }) => {
const { themeColorWithShade } = useContext(MyThemeContext)
return (
<Box display="flex" flexDirection="column" alignItems="center">
<UserAvatar name={name} twitterName={twitter_name} size="2xl" />
<Box
fontFamily="'Pacifico', cursive"
fontWeight="light"
fontSize="48px"
borderBottomColor={themeColorWithShade}
borderBottomWidth="5px"
w="100%"
textAlign="center"
>
{name}
</Box>
</Box>
)
}
export default LogoHeader

View File

@@ -0,0 +1,72 @@
import React from "react"
import { Weapon, CountryCode } from "../../types"
import Box from "../elements/Box"
import { useContext } from "react"
import MyThemeContext from "../../themeContext"
import { Link } from "@reach/router"
import UserAvatar from "../common/UserAvatar"
import WeaponImage from "../common/WeaponImage"
import Flag from "../common/Flag"
import { countries } from "../../utils/lists"
interface MemberCardProps {
member: {
discord_id: string
username: string
discriminator: string
twitch_name?: string
twitter_name?: string
country?: CountryCode
weapons: Weapon[]
custom_url?: string
}
}
const MemberCard: React.FC<MemberCardProps> = ({ member }) => {
const { borderStyle, grayWithShade } = useContext(MyThemeContext)
return (
<Box
as="fieldset"
display="block"
borderWidth="1px"
border={borderStyle}
//w="300px"
rounded="lg"
overflow="hidden"
//pb={showUser && build.discord_user ? "20px" : "15px"}
p="15px"
w="240px"
h="180px"
>
<Box
as="legend"
color={grayWithShade}
fontWeight="semibold"
letterSpacing="wide"
fontSize="s"
>
<Link to={`/u/${member.custom_url ?? member.discord_id}`}>
{member.username}#{member.discriminator}
</Link>
</Box>
<Box display="flex" flexDirection="column" alignItems="center">
<UserAvatar name={member.username} twitterName={member.twitter_name} />
{member.country && (
<Box mt="0.5em" display="flex" alignItems="center">
<Flag code={member.country} />
{countries.find(obj => obj.code === member.country)?.name}
</Box>
)}
<Box display="flex" mt="0.5em">
{member.weapons.map(wpn => (
<Box mx="0.3em" key={wpn}>
<WeaponImage englishName={wpn} size="SMALL" />
</Box>
))}
</Box>
</Box>
</Box>
)
}
export default MemberCard

View File

@@ -0,0 +1,24 @@
import React from "react"
import Button from "../elements/Button"
import { useState } from "react"
import AddResultModal from "./AddResultModal"
interface ResultsProps {
canAddResults: boolean
}
const Results: React.FC<ResultsProps> = ({ canAddResults }) => {
const [showModal, setShowModal] = useState(false)
return (
<>
{showModal && <AddResultModal closeModal={() => setShowModal(false)} />}
{canAddResults && (
<Button onClick={() => setShowModal(true)}>
Add tournament result
</Button>
)}
</>
)
}
export default Results

View File

@@ -0,0 +1,74 @@
import React from "react"
import { RouteComponentProps, Redirect } from "@reach/router"
import { useQuery } from "@apollo/react-hooks"
import { SEARCH_FOR_TEAM } from "../../graphql/queries/searchForTeam"
import { Team, UserData } from "../../types"
import Loading from "../common/Loading"
import Error from "../common/Error"
import LogoHeader from "./LogoHeader"
import MemberCard from "./MemberCard"
import Box from "../elements/Box"
import { Helmet } from "react-helmet-async"
import Results from "./Results"
import { USER } from "../../graphql/queries/user"
interface SearchForTeamData {
searchForTeam: Team
}
interface SearchForTeamVars {
name: string
}
interface TeamPageProps {
name?: string
}
const TeamPage: React.FC<RouteComponentProps & TeamPageProps> = ({ name }) => {
const { data, error, loading } = useQuery<
SearchForTeamData,
SearchForTeamVars
>(SEARCH_FOR_TEAM, {
variables: { name: name as string },
skip: !name,
})
const { data: userData, error: userError, loading: userLoading } = useQuery<
UserData
>(USER)
if (!name) return <Redirect to="/404" />
if (loading || userLoading) return <Loading />
if (error) return <Error errorMessage={error.message} />
if (userError) return <Error errorMessage={userError.message} />
if (!data || !data.searchForTeam || !userData) return <Redirect to="/404" />
const team = data.searchForTeam
const user = userData.user
console.log("team", team)
return (
<>
<Helmet>
<title>{team.name} | sendou.ink</title>
</Helmet>
<LogoHeader name={team.name} twitter_name={team.twitter_name} />
<Box display="flex" flexWrap="wrap" justifyContent="center">
{team.member_users.map(member => (
<Box key={member.discord_id} p="0.5em">
<MemberCard member={member} />
</Box>
))}
</Box>
<Box mt="1em">
<Results
canAddResults={
team.captain_discord_id === user?.discord_id &&
team.tournament_results.length < 100
}
/>
</Box>
</>
)
}
export default TeamPage

View File

@@ -26,6 +26,7 @@ import BuildTab from "./BuildTab"
import MyThemeContext from "../../themeContext"
import { PLAYER_INFO } from "../../graphql/queries/playerInfo"
import XRankTab from "./XRankTab"
import { weapons } from "../../utils/lists"
interface Tab {
id: number
@@ -95,7 +96,9 @@ const UserPage: React.FC<RouteComponentProps & UserPageProps> = ({ id }) => {
content: (
<TabPanel key={1}>
<BuildTab
builds={builds}
builds={builds.sort(
(a, b) => weapons.indexOf(a.weapon) - weapons.indexOf(b.weapon)
)}
canModifyBuilds={userLean?.discord_id === user.discord_id}
/>
</TabPanel>

View File

@@ -0,0 +1,17 @@
import { gql, DocumentNode } from "apollo-boost"
export const ADD_RESULT: DocumentNode = gql`
mutation addResult(
$date: String!
$tweet_id: String
$tournament_name: String!
$placement: Int!
) {
updateUser(
date: $date
tweet_id: $tweet_id
tournament_name: $tournament_name
placement: $placement
)
}
`

View File

@@ -0,0 +1,31 @@
import { gql, DocumentNode } from "apollo-boost"
export const SEARCH_FOR_TEAM: DocumentNode = gql`
query searchForTeam($name: String!) {
searchForTeam(name: $name) {
name
twitter_name
captain_discord_id
member_discord_ids
member_users {
discord_id
username
discriminator
twitch_name
twitter_name
country
weapons
custom_url
}
countries
tag
lf_post
tournament_results {
date
tweet_id
tournament_name
placement
}
}
}
`

View File

@@ -150,6 +150,34 @@ export interface FreeAgentPost {
}
}
export interface TournamentResult {
date: string
tweet_id?: string
tournament_name: string
placement: number
}
export interface Team {
name: string
twitter_name?: string
captain_discord_id: string
member_discord_ids: string[]
member_users: {
discord_id: string
username: string
discriminator: string
twitch_name?: string
twitter_name?: string
country?: CountryCode
weapons: Weapon[]
custom_url?: string
}[]
countries: string[]
tag?: String
lf_post?: String
tournament_results: TournamentResult[]
}
//==============================================================================
// Apollo
//==============================================================================

View File

@@ -4,11 +4,18 @@ const User = require("../mongoose-models/user")
const typeDef = gql`
extend type Query {
searchForTeam(name: String!): Team
teams: [Team!]!
}
extend type Mutation {
createTeam(name: String!): Team!
addTeam(name: String!): Team!
addResult(
date: String!
tweet_id: String
tournament_name: String!
placement: Int!
): Boolean!
}
extend type User {
@@ -16,8 +23,8 @@ const typeDef = gql`
}
type Result {
date: String
tweet_url: String
date: String!
tweet_id: String
tournament_name: String!
placement: Int!
}
@@ -30,7 +37,6 @@ const typeDef = gql`
member_users: [User!]!
countries: [String!]!
tag: String
invite_code: String
lf_post: String
tournament_results: [Result!]!
}
@@ -38,9 +44,15 @@ const typeDef = gql`
const resolvers = {
Query: {
teams: (root, args) => Team.find({}),
searchForTeam: (root, { name }) => {
const name_regex = `^${name.replace("_", " ")}$`
return Team.findOne({
name: { $regex: new RegExp(name_regex, "i") },
}).populate("member_users")
},
},
Mutation: {
createTeam: async (root, args, { user }) => {
addTeam: async (root, args, { user }) => {
if (!user) throw new UserInputError("Must be logged in to create a team")
if (user.team)
throw new UserInputError(
@@ -49,7 +61,7 @@ const resolvers = {
const name = args.name.replace(/\s\s+/g, " ").trim()
if (name.length < 2 || name.length > 32 || !/^[a-z0-9]+$/i.test(name)) {
if (name.length < 2 || name.length > 32 || !/^[a-z0-9 ]+$/i.test(name)) {
throw new UserInputError("Invalid team name provided", {
invalidArgs: args,
})
@@ -62,10 +74,59 @@ const resolvers = {
if (existing_team)
throw new UserInputError("Team with this name already exists")
const team = new Team({ name, captain_discord_id: user.discord_id })
const team = new Team({
name,
captain_discord_id: user.discord_id,
member_discord_ids: [user.discord_id],
})
await User.findByIdAndUpdate(user._id, { $set: { team: team._id } })
return team.save()
},
addResult: async (root, args, { user }) => {
if (!user) {
throw new UserInputError("Must be logged in")
}
if (user.team.captain_discord_id !== user.discord_id) {
//??
throw new UserInputError("Must be a captain to add a result")
}
if (user.team.tournament_results.length >= 100) {
throw new UserInputError("Can't have more than 100 tournament results")
}
if (Date.parse(args.date) === NaN) {
throw new UserInputError("Invalid date")
}
if (args.tweet_id && !isNaN(args.tweet_id)) {
throw new UserInputError("Tweet ID can only contain numbers")
}
if (
args.tournament_name.length < 2 ||
args.tournament_name.length > 100
) {
throw new UserInputError(
"Tournament name has to be between 2 and 100 characters long"
)
}
if (args.placement < 1 || args.placement > 500) {
throw new UserInputError("Placement has to be between 1 and 500")
}
const team = await Team.findById(user.team)
team.push({
date: args.date,
tournament_name: args.tournament_name,
placement: args.placement,
tweet_id: args.tweet_id ? args.tweet_id : undefined,
})
await team.save()
return true
},
},
}

View File

@@ -23,7 +23,6 @@ teamSchema.virtual("member_users", {
ref: "User",
localField: "member_discord_ids",
foreignField: "discord_id",
justOne: true,
})
module.exports = mongoose.model("Team", teamSchema)