Merge branch 'typescript-frontend-rewrite'

This commit is contained in:
Sendou
2020-03-09 16:38:56 +02:00
27 changed files with 793 additions and 408 deletions

View File

@@ -2100,9 +2100,9 @@
}
},
"@types/jest": {
"version": "25.1.3",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-25.1.3.tgz",
"integrity": "sha512-jqargqzyJWgWAJCXX96LBGR/Ei7wQcZBvRv0PLEu9ZByMfcs23keUJrKv9FMR6YZf9YCbfqDqgmY+JUBsnqhrg==",
"version": "25.1.4",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-25.1.4.tgz",
"integrity": "sha512-QDDY2uNAhCV7TMCITrxz+MRk1EizcsevzfeS6LykIlq2V1E5oO4wXG8V2ZEd9w7Snxeeagk46YbMgZ8ESHx3sw==",
"requires": {
"jest-diff": "^25.1.0",
"pretty-format": "^25.1.0"
@@ -2120,9 +2120,9 @@
}
},
"@types/yargs": {
"version": "15.0.3",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.3.tgz",
"integrity": "sha512-XCMQRK6kfpNBixHLyHUsGmXrpEmFFxzMrcnSXFMziHd8CoNJo8l16FkHyQq4x+xbM7E2XL83/O78OD8u+iZTdQ==",
"version": "15.0.4",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.4.tgz",
"integrity": "sha512-9T1auFmbPZoxHz0enUFlUuKRy3it01R+hlggyVUMtnCTQRunsQYifnSGb8hET4Xo8yiC0o0r1paW3ud5+rbURg==",
"requires": {
"@types/yargs-parser": "*"
}
@@ -2216,9 +2216,9 @@
"integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA=="
},
"@types/node": {
"version": "13.7.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.7.7.tgz",
"integrity": "sha512-Uo4chgKbnPNlxQwoFmYIwctkQVkMMmsAoGGU4JKwLuvBefF0pCq4FybNSnfkfRCpC7ZW7kttcC/TrRtAJsvGtg=="
"version": "13.9.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.9.0.tgz",
"integrity": "sha512-0ARSQootUG1RljH2HncpsY2TJBfGQIKOOi7kxzUY6z54ePu/ZD+wJA8zI2Q6v8rol2qpG/rvqsReco8zNMPvhQ=="
},
"@types/parse-json": {
"version": "4.0.0",
@@ -2287,6 +2287,14 @@
"@types/react": "*"
}
},
"@types/react-paginate": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@types/react-paginate/-/react-paginate-6.2.1.tgz",
"integrity": "sha512-+q8k1N0WzbMyOCsIEH/p5D6/KQD8dXYLzfvSvriYn//94icd2sqhAL2rWXkgwGvqHGCSTU9AoHtsWCJxPfquUQ==",
"requires": {
"@types/react": "*"
}
},
"@types/react-select": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@types/react-select/-/react-select-3.0.10.tgz",
@@ -11933,9 +11941,9 @@
}
},
"react-datepicker": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-2.13.0.tgz",
"integrity": "sha512-vUp3tGGT8SwJX5c6hKnhSJFOFjjBgwKQUv9nl7dkhe/viraxrihLikDr/KVDRGYyj/Q1MBvTpOog4fsSly9o9A==",
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-2.14.0.tgz",
"integrity": "sha512-9TUDNj0zoeQT3ey6i7Dv4NLcqONyYqXNEOLA3++HwQKR5NK4eRoG4QaohM/5XmWw2tDpJWpl3ByCWP4kWQtqgQ==",
"requires": {
"classnames": "^2.2.6",
"date-fns": "^2.0.1",
@@ -12168,6 +12176,14 @@
"resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.9.0.tgz",
"integrity": "sha512-8ltIY3bC7oGhj2nPAvWOGi+xGFybPNhJM0V1H8hY/whNcXgmDeaeoCMPPd8VatrpTsUWjb/vGzrmu6SrXVty3A=="
},
"react-paginate": {
"version": "6.3.2",
"resolved": "https://registry.npmjs.org/react-paginate/-/react-paginate-6.3.2.tgz",
"integrity": "sha512-Ch++Njfv8UHpLtIMiQouAPeJQA5Ki86kIYfCer6c1B96Rvn3UF27se+goCilCP8oHNXNsA2R2kxvzanY1YIkyg==",
"requires": {
"prop-types": "^15.6.1"
}
},
"react-popper": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.7.tgz",

View File

@@ -13,13 +13,14 @@
"@testing-library/jest-dom": "^5.1.1",
"@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^10.0.0",
"@types/jest": "^25.1.3",
"@types/jest": "^25.1.4",
"@types/reach__router": "^1.3.0",
"@types/react": "^16.9.23",
"@types/react-color": "^3.0.1",
"@types/react-datepicker": "^2.11.0",
"@types/react-dom": "^16.9.5",
"@types/react-infinite-scroller": "^1.2.1",
"@types/react-paginate": "^6.2.1",
"@types/react-select": "^3.0.10",
"@types/webpack-env": "^1.15.1",
"apollo-boost": "^0.4.7",
@@ -29,13 +30,14 @@
"node-sass": "^4.13.1",
"react": "^16.13.0",
"react-color": "^2.18.0",
"react-datepicker": "^2.13.0",
"react-datepicker": "^2.14.0",
"react-dom": "^16.13.0",
"react-draggable": "^4.2.0",
"react-helmet-async": "^1.0.4",
"react-hotkeys-hook": "^1.5.4",
"react-icons": "^3.9.0",
"react-infinite-scroller": "^1.2.4",
"react-paginate": "^6.3.2",
"react-scripts": "^3.4.0",
"react-select": "^3.0.8",
"react-super-responsive-table": "^5.1.1",

View File

@@ -1,7 +1,6 @@
import React from "react"
const customIcons = {
//sz.svg
sz: {
path: (
<path
@@ -11,7 +10,6 @@ const customIcons = {
),
viewBox: "0 0 128 128",
},
//tc.svg
tc: {
path: (
<path
@@ -21,7 +19,6 @@ const customIcons = {
),
viewBox: "0 0 128 128",
},
//rm.svg
rm: {
path: (
<path
@@ -31,7 +28,6 @@ const customIcons = {
),
viewBox: "0 0 128 128",
},
//cb.svg
cb: {
path: (
<path
@@ -41,6 +37,15 @@ const customIcons = {
),
viewBox: "0 0 128 128",
},
tw: {
path: (
<path
fill="currentColor"
d="M8 45l9 30 4-4 5-4 3 2c2 3 2 3 1 5l-1 6c1 4 1 4-7 13l-8 8 13 13 13 14 12-12 11-12 12 12 13 11 14-13 13-13-9-8c-8-9-8-9-8-13v-6c-2-2-2-3 1-5l2-3 5 5 5 4 1-4 8-30 7-27a676 676 0 0 0-61 16l4 5 5 5-6 5-5 5-6-5-5-6 4-4 4-5-4-1-30-8-26-7 7 30zm22-18l22 6-3 3-3 4 17 17c11 11 17 17 16 18l-15 8c-3 0-6-2-15-8l-10-5c-2 0-5-2-8-5l-5-5-4 3c-3 2-3 2-3 0A1874 1874 0 0 1 8 21l22 6zm90-4l-6 21-6 20c0 1-2 0-3-2-3-3-3-2-10 3-2 3-6 5-7 5-3 1-4 1-12-7l-9-9 8-7 7-7-3-4-3-3a1474 1474 0 0 1 44-10zM43 79c-2 5 6 12 11 9l4-3c1-1 1-1 1 1 2 4-4 9-10 9s-12-6-12-11c0-4 2-8 5-8 2 0 2 0 1 3zm46-1c2 3 1 9-1 12-1 3-7 5-11 5s-9-4-9-8c0-2 0-3 4 0 3 2 3 2 6 1 5-2 8-5 6-9-1-3-1-3 2-3l3 2zm-36 2c2 2 2 2 1 3-2 2-6 1-7-1-1-4 2-5 6-2zm27 1c0 3-3 4-6 3-1-2-1-2 1-4 3-3 5-2 5 1zM30 99l-5 6-2-2c-2-2-2-2 3-8l6-6 2 2c2 3 2 3-4 8zm36-8l2 5c1 2 3 3 5 3l11 9 7 8-2 2-2 3-13-14C62 95 61 94 61 91c1-2 4-3 5 0zm39 12l-2 2-6-5-5-6 1-2 2-3 6 6 6 5-2 3zm-61-4c2 0 1 1-5 7l-6 7-3-2-2-2 6-6c3-4 6-6 7-5l3 1zm49 4l6 6-2 2-3 2-6-7-6-6 2-2 3-1 6 6zm-34-3l-9 10c-10 10-10 10-12 9l-2-2 8-9 10-8h5z"
/>
),
viewBox: "0 0 128 128",
},
}
export default customIcons

View File

@@ -1,29 +1,21 @@
.pagination {
display: inline-block;
border-color: red;
}
.pagination span {
color: black;
float: left;
padding: 8px 16px;
text-decoration: none;
font-weight: bold;
font-size: 18px;
user-select: none;
}
.pagination span:not(.dots) {
cursor: pointer;
.page {
display: inline;
margin: 0.2em;
}
.pagination span.active {
.active-page {
border-bottom: 3px solid;
}
.pagination span:hover:not(.active):not(.dots) {
background-color: rgba(165, 165, 165, 0.15);
border-radius: 5px;
}
.dots {
user-select: none;
.disabled-page {
display: none;
}

View File

@@ -1,7 +1,6 @@
import React, { useContext } from "react"
import React from "react"
import "./Pagination.css"
import { Box } from "@chakra-ui/core"
import MyThemeContext from "../../themeContext"
import ReactPaginate from "react-paginate"
interface PaginationProps {
pageCount: number
@@ -9,55 +8,26 @@ interface PaginationProps {
onChange: (page: number) => void
}
const Pagination: React.FC<PaginationProps> = ({
pageCount,
currentPage,
onChange,
}) => {
const { colorMode, themeColorHex } = useContext(MyThemeContext)
const Pagination: React.FC<PaginationProps> = ({ pageCount, onChange }) => {
return (
<Box className="pagination">
<Box
as="span"
style={{
color: colorMode === "light" ? "black" : "white",
borderBottomColor: themeColorHex,
}}
onClick={() => onChange(currentPage === 1 ? 1 : currentPage - 1)}
>
&laquo;
</Box>
{[...Array(6)].map((page, index) => {
const pageNumber = index + currentPage
return (
<Box
as="span"
className={pageNumber === currentPage ? "active" : undefined}
style={{
color: colorMode === "light" ? "black" : "white",
borderBottomColor: themeColorHex,
}}
key={pageNumber}
onClick={() => onChange(pageNumber)}
>
{pageNumber}
</Box>
)
})}
<Box
as="span"
style={{
color: colorMode === "light" ? "black" : "white",
borderBottomColor: themeColorHex,
}}
onClick={() =>
onChange(currentPage === pageCount ? pageCount : currentPage + 1)
}
>
&raquo;
</Box>
</Box>
<ReactPaginate
previousLabel={<>&laquo;</>}
nextLabel={<>&raquo;</>}
breakLabel={"..."}
breakClassName={"page"}
pageCount={pageCount}
marginPagesDisplayed={2}
pageRangeDisplayed={3}
onPageChange={({ selected }) => onChange(selected)}
containerClassName="pagination"
pageClassName="page"
previousClassName="page"
nextClassName="page"
pageLinkClassName="page"
activeLinkClassName="active-page"
disabledClassName="disabled-page"
activeClassName={"active"}
/>
)
}

View File

@@ -4,7 +4,7 @@ import { Avatar, BoxProps } from "@chakra-ui/core"
interface UserAvatarProps {
name: string
twitterName?: string
size?: undefined | "2xl" | "xs" | "sm"
size?: undefined | "2xl" | "lg" | "xs" | "sm"
}
const UserAvatar: React.FC<UserAvatarProps & BoxProps> = ({

View File

@@ -35,7 +35,7 @@ const Input: React.FC<InputProps> = ({
setValue(event.target.value)
return (
<>
<Box>
{label && <Label required={required}>{label}</Label>}
<InputGroup>
{textLeft && <InputLeftAddon>{textLeft}</InputLeftAddon>}
@@ -57,7 +57,7 @@ const Input: React.FC<InputProps> = ({
{(value ?? "").length}/{limit}
</Box>
)}
</>
</Box>
)
}

View File

@@ -90,7 +90,7 @@ const Select: React.FC<SelectProps> = ({
}
return (
<Box w={width}>
<Box>
{label && <Label required={required}>{label}</Label>}
<ReactSelect
className="basic-single"

View File

@@ -0,0 +1,170 @@
import React, { useContext, useState } from "react"
import { Flex, Box, Image, IconButton, Heading } from "@chakra-ui/core"
import { FreeAgentPost } from "../../types"
import UserAvatar from "../common/UserAvatar"
import { Link } from "@reach/router"
import MyThemeContext from "../../themeContext"
import { FaTwitter, FaPlus, FaMinus } from "react-icons/fa"
import { top500 } from "../../assets/imageImports"
import { countries } from "../../utils/lists"
import Flag from "../common/Flag"
import RoleIcons from "./RoleIcons"
import WeaponImage from "../common/WeaponImage"
import VCIcon from "./VCIcon"
interface FreeAgentCardProps {
post: FreeAgentPost
}
const hasExtraInfo = (post: FreeAgentPost) => {
const { activity, description, looking_for, past_experience } = post
if (!activity && !description && !looking_for && !past_experience) {
return false
}
return true
}
const FreeAgentCard: React.FC<FreeAgentCardProps> = ({ post }) => {
const [expanded, setExpanded] = useState(false)
const { grayWithShade, themeColorWithShade } = useContext(MyThemeContext)
const { discord_user } = post
const canBeExpanded = hasExtraInfo(post)
return (
<Flex
rounded="lg"
overflow="hidden"
boxShadow="0px 0px 16px 6px rgba(0,0,0,0.1)"
p="25px"
flexDirection="column"
justifyContent="space-between"
maxW="500px"
>
<Flex justifyContent="space-between" flexWrap="wrap" alignItems="center">
<Box color="#999999" width="50px" m="1em">
{new Date(parseInt(post.createdAt)).toLocaleDateString()}
</Box>
<Box width="50px" m="1em">
{discord_user.top500 && (
<Image
src={top500}
alt="x rank top 500 logo"
height="40px"
width="auto"
title="Free agent has reached Top 500 in X Rank"
/>
)}
</Box>
</Flex>
<Flex flexDirection="column" alignItems="center">
<UserAvatar
twitterName={discord_user.twitter_name}
name={discord_user.username}
mr="5px"
size="lg"
/>
<Box
as="span"
fontWeight="semibold"
letterSpacing="wide"
fontSize="md"
mt="0.5em"
>
<Flex alignItems="center">
<Link to={`/u/${discord_user.discord_id}`}>
{discord_user.username}#{discord_user.discriminator}
</Link>
{discord_user.twitter_name && (
<a href={`https://twitter.com/${discord_user.twitter_name}`}>
<Box color="#1DA1F2" as={FaTwitter} ml="0.5em" />
</a>
)}
</Flex>
</Box>
<Box color={grayWithShade}>
{discord_user.country && (
<>
<Flag code={discord_user.country} />
{countries.find(obj => obj.code === discord_user.country)?.name}
</>
)}
</Box>
</Flex>
<Flex flexDirection="column" alignItems="center" mt="1em">
<Flex justifyContent="center" w="250px">
{discord_user?.weapons &&
discord_user.weapons.map(wpn => (
<Box mx="0.3em" key={wpn}>
<WeaponImage englishName={wpn} size="SMALL" />
</Box>
))}
</Flex>
</Flex>
<Flex justifyContent="center" mt="2em">
<Box mr="0.5em">
<RoleIcons playstyles={post.playstyles} />
</Box>
<Box borderLeft="1px solid" borderColor="#999999" ml="0.5em" px="1em">
<VCIcon canVC={post.can_vc} />
</Box>
</Flex>
{canBeExpanded && (
<Flex mt="2em" justifyContent="center">
<IconButton
variant="ghost"
aria-label="More information"
isRound
fontSize="20px"
icon={expanded ? FaMinus : FaPlus}
onClick={() => setExpanded(!expanded)}
/>
</Flex>
)}
{expanded && (
<Box whiteSpace="pre-wrap">
{post.activity && (
<Box>
<Heading size="md" color={themeColorWithShade}>
Activity
</Heading>
{post.activity}
</Box>
)}
{post.looking_for && (
<Box mt={post.activity ? "1em" : undefined}>
<Heading size="md" color={themeColorWithShade}>
Looking for
</Heading>
{post.looking_for}
</Box>
)}
{post.past_experience && (
<Box mt={post.activity || post.looking_for ? "1em" : undefined}>
<Heading size="md" color={themeColorWithShade}>
Past experience
</Heading>
{post.past_experience}
</Box>
)}
{post.description && (
<Box
mt={
post.activity || post.looking_for || post.past_experience
? "1em"
: undefined
}
>
<Heading size="md" color={themeColorWithShade}>
Description
</Heading>
{post.description}
</Box>
)}
</Box>
)}
</Flex>
)
}
export default FreeAgentCard

View File

@@ -11,13 +11,15 @@ import { FREE_AGENT_POSTS } from "../../graphql/queries/freeAgentPosts"
import Loading from "../common/Loading"
import Error from "../common/Error"
import { RouteComponentProps } from "@reach/router"
import PostsAccordion from "./PostsAccordion"
import PostsAccordion from "./Posts"
import PageHeader from "../common/PageHeader"
import { Helmet } from "react-helmet-async"
import WeaponSelector from "../common/WeaponSelector"
import RadioGroup from "../elements/RadioGroup"
import Box from "../elements/Box"
import { continents } from "../../utils/lists"
import { Collapse } from "@chakra-ui/core"
import Button from "../elements/Button"
const playstyleToEnum = {
"Frontline/Slayer": "FRONTLINE",
@@ -27,6 +29,7 @@ const playstyleToEnum = {
const FreeAgentsPage: React.FC<RouteComponentProps> = () => {
const [weapon, setWeapon] = useState<Weapon | null>(null)
const [show, setShow] = useState(false)
const [playstyle, setPlaystyle] = useState<
"Any" | "Frontline/Slayer" | "Midline/Support" | "Backline/Anchor"
>("Any")
@@ -96,35 +99,40 @@ const FreeAgentsPage: React.FC<RouteComponentProps> = () => {
<title>Free Agents | sendou.ink</title>
</Helmet>
<PageHeader title="Free Agents" />
<Box maxW="600px" my="1em">
<RadioGroup
value={playstyle}
setValue={setPlaystyle}
label="Filter by playstyle"
options={[
"Any",
"Frontline/Slayer",
"Midline/Support",
"Backline/Anchor",
]}
/>
</Box>
<Box maxW="600px" my="1em">
<RadioGroup
value={region}
setValue={setRegion}
label="Filter by region"
options={["Any", "Europe", "The Americas", "Oceania", "Other"]}
/>
</Box>
<Box maxW="600px" my="1em">
<WeaponSelector
label="Filter by weapon"
value={weapon}
setValue={(weapon: string) => setWeapon(weapon as Weapon)}
clearable
/>
</Box>
<Button onClick={() => setShow(!show)}>
{show ? "Hide filters" : "Show filters"}
</Button>
<Collapse mt={4} isOpen={show}>
<Box maxW="600px" my="1em">
<RadioGroup
value={playstyle}
setValue={setPlaystyle}
label="Filter by playstyle"
options={[
"Any",
"Frontline/Slayer",
"Midline/Support",
"Backline/Anchor",
]}
/>
</Box>
<Box maxW="600px" my="1em">
<RadioGroup
value={region}
setValue={setRegion}
label="Filter by region"
options={["Any", "Europe", "The Americas", "Oceania", "Other"]}
/>
</Box>
<Box maxW="600px" my="1em">
<WeaponSelector
label="Filter by weapon"
value={weapon}
setValue={(weapon: string) => setWeapon(weapon as Weapon)}
clearable
/>
</Box>
</Collapse>
<PostsAccordion posts={faPosts.filter(postsFilter)} />
</>
)

View File

@@ -0,0 +1,50 @@
import React, { useState } from "react"
import { FreeAgentPost } from "../../types"
import { Grid, Box, Heading } from "@chakra-ui/core"
import Alert from "../elements/Alert"
import FreeAgentCard from "./FreeAgentCard"
import InfiniteScroll from "react-infinite-scroller"
import Button from "../elements/Button"
interface PostsAccordionProps {
posts: FreeAgentPost[]
}
const Posts: React.FC<PostsAccordionProps> = ({ posts }) => {
const [agentsToShow, setAgentsToShow] = useState(5)
if (posts.length === 0) {
return (
<Alert status="info">No free agents found with the current filter</Alert>
)
}
return (
<>
<Grid gridTemplateColumns="repeat(auto-fit, minmax(260px, 1fr))" mt="1em">
<InfiniteScroll
pageStart={1}
loadMore={page => setAgentsToShow(page * 10)}
hasMore={agentsToShow < posts.length}
>
{posts
.filter((post, index) => index < agentsToShow)
.map(post => (
<Box my="1em">
<FreeAgentCard key={post.id} post={post} />
</Box>
))}
</InfiniteScroll>
</Grid>
<Box w="50%" textAlign="center" mx="auto" mt="1em">
<Heading size="sm">No more free agents to show</Heading>
<Box mt="1em">
<Button outlined onClick={() => window.scrollTo(0, 0)}>
Return to the top
</Button>
</Box>
</Box>
</>
)
}
export default Posts

View File

@@ -1,165 +0,0 @@
import React from "react"
import { FreeAgentPost } from "../../types"
import {
Accordion,
AccordionItem,
AccordionHeader,
AccordionIcon,
AccordionPanel,
Box,
Grid,
Flex,
Heading,
} from "@chakra-ui/core"
import { useContext } from "react"
import MyThemeContext from "../../themeContext"
import UserAvatar from "../common/UserAvatar"
import { Link } from "@reach/router"
import Flag from "../common/Flag"
import { countries } from "../../utils/lists"
import { FaTwitter } from "react-icons/fa"
import WeaponImage from "../common/WeaponImage"
import RoleIcons from "./RoleIcons"
import VCIcon from "./VCIcon"
import Alert from "../elements/Alert"
interface PostsAccordionProps {
posts: FreeAgentPost[]
}
const hasExtraInfo = (post: FreeAgentPost) => {
const { activity, description, looking_for, past_experience } = post
if (!activity && !description && !looking_for && !past_experience) {
return false
}
return true
}
const PostsAccordion: React.FC<PostsAccordionProps> = ({ posts }) => {
const { darkerBgColor } = useContext(MyThemeContext)
if (posts.length === 0) {
return (
<Alert status="info">No free agents found with the current filter</Alert>
)
}
return (
<Accordion allowMultiple>
{posts.map(post => {
const { discord_user } = post
const canBeExpanded = hasExtraInfo(post)
return (
<AccordionItem key={post.id}>
<AccordionHeader cursor={canBeExpanded ? undefined : "default"}>
{canBeExpanded ? (
<AccordionIcon size="2em" mr="1em" />
) : (
<Box w="2em" h="2em" mr="1em" />
)}
<Grid
gridTemplateColumns="repeat(auto-fill, 200px)"
width="100%"
rowGap="0.7em"
justifyItems="center"
justifyContent="center"
alignItems="center"
>
<Link to={`/u/${discord_user.discord_id}`}>
<Flex alignItems="center">
<UserAvatar
twitterName={discord_user.twitter_name}
name={discord_user.username}
mr="5px"
/>
<span>
{discord_user.username}#{discord_user.discriminator}
</span>
</Flex>
</Link>
<Box>
{discord_user.country && (
<>
<Flag code={discord_user.country} />
{
countries.find(obj => obj.code === discord_user.country)
?.name
}
</>
)}
</Box>
<Flex alignItems="center">
{discord_user.twitter_name && (
<>
<Box as={FaTwitter} />
<Box as="span" ml="5px">
{discord_user.twitter_name}
</Box>
</>
)}
</Flex>
<Box color="#999999">
{new Date(parseInt(post.createdAt)).toLocaleDateString()}
</Box>
<Flex alignItems="center">
{discord_user?.weapons &&
discord_user.weapons.map(wpn => (
<Box mx="0.3em" key={wpn}>
<WeaponImage englishName={wpn} size="SMALL" />
</Box>
))}
</Flex>
<RoleIcons playstyles={post.playstyles} />
<VCIcon canVC={post.can_vc} />
</Grid>
</AccordionHeader>
{canBeExpanded && (
<AccordionPanel
mt="3px"
py={4}
background={darkerBgColor}
whiteSpace="pre-wrap"
textAlign="center"
>
{post.activity && (
<Box>
<Heading size="md">Activity</Heading>
{post.activity}
</Box>
)}
{post.looking_for && (
<Box mt={post.activity ? "1em" : undefined}>
<Heading size="md">Looking for</Heading>
{post.looking_for}
</Box>
)}
{post.past_experience && (
<Box
mt={post.activity || post.looking_for ? "1em" : undefined}
>
<Heading size="md">Past experience</Heading>
{post.past_experience}
</Box>
)}
{post.description && (
<Box
mt={
post.activity || post.looking_for || post.past_experience
? "1em"
: undefined
}
>
<Heading size="md">Description</Heading>
{post.description}
</Box>
)}
</AccordionPanel>
)}
</AccordionItem>
)
})}
</Accordion>
)
}
export default PostsAccordion

View File

@@ -15,12 +15,14 @@ const HomePage: React.FC<RouteComponentProps> = () => {
<Helmet>
<title>sendou.ink | Competitive Splatoon Hub</title>
</Helmet>
<Flex
flexDirection="column"
alignItems="center"
title="Picture by borzoic (@borzoic_ on Twitter)"
>
<Image className="rgb" src={posterGirl[colorMode]} w="400px" h="auto" />
<Flex flexDirection="column" alignItems="center">
<Image
className="rgb"
src={posterGirl[colorMode]}
w="400px"
h="auto"
title="Picture by borzoic (@borzoic_ on Twitter)"
/>
<Heading
size="2xl"
letterSpacing="1px"
@@ -29,6 +31,7 @@ const HomePage: React.FC<RouteComponentProps> = () => {
>
Sendou.ink
</Heading>
<Heading
size="md"
letterSpacing="1px"

View File

@@ -123,7 +123,7 @@ const VotingHistoryPage: React.FC<RouteComponentProps> = () => {
</Link>
<Flex flexWrap="wrap" justifyContent="center" mt="1em">
{userData.user.plus.membership_status === "ONE" && (
<Box m="0.5em">
<Box m="0.5em" minW="250px">
<Select
label=""
options={[
@@ -135,7 +135,7 @@ const VotingHistoryPage: React.FC<RouteComponentProps> = () => {
/>
</Box>
)}
<Box m="0.5em">
<Box m="0.5em" minW="250px">
<Select
label=""
options={monthChoices.map(my => ({ label: my, value: my }))}

View File

@@ -9,7 +9,7 @@ import {
import Error from "../common/Error"
import Loading from "../common/Loading"
import TournamentCard from "./TournamentCard"
import { Box, Flex, Image, Avatar, Icon, Grid } from "@chakra-ui/core"
import { Box, Flex, Avatar, Icon, Grid } from "@chakra-ui/core"
import Button from "../elements/Button"
import { Helmet } from "react-helmet-async"
import { FaLongArrowAltLeft } from "react-icons/fa"
@@ -17,17 +17,52 @@ import MyThemeContext from "../../themeContext"
import { mapIcons } from "../../assets/imageImports"
import WeaponImage from "../common/WeaponImage"
import AbilityIcon from "../builds/AbilityIcon"
import { Ability } from "../../types"
import { Ability, Weapon } from "../../types"
import {
useQueryParams,
StringParam,
ArrayParam,
encodeQueryParams,
} from "use-query-params"
import { stringify } from "querystring"
import { removeFalsy } from "../../utils/helperFunctions"
interface TournamentDetailsPageProps {
id?: string
}
interface Round {
stage: string
mode: "SZ" | "TC" | "RM" | "CB" | "TW"
round_name: string
round_number: number
game_number: number
winning_team_name: string
winning_team_players: string[]
winning_team_weapons: Weapon[]
winning_team_main_abilities: Ability[][]
losing_team_name: string
losing_team_players: string[]
losing_team_weapons: Weapon[]
losing_team_main_abilities: Ability[][]
}
const filterMap = {
team_name: StringParam,
player_name: StringParam,
comp: ArrayParam,
mode: StringParam,
stage: StringParam,
}
const TournamentDetailsPage: React.FC<RouteComponentProps &
TournamentDetailsPageProps> = ({ id }) => {
const { themeColorWithShade, grayWithShade, textColor } = useContext(
MyThemeContext
)
const {
themeColorWithShade,
grayWithShade,
textColor,
darkerBgColor,
} = useContext(MyThemeContext)
const { data, error, loading } = useQuery<
SearchForTournamentByIdData,
SearchForTournamentByIdVars
@@ -35,6 +70,7 @@ const TournamentDetailsPage: React.FC<RouteComponentProps &
variables: { id: id! },
skip: !id,
})
const [filter] = useQueryParams(filterMap)
if (!id) return <Redirect to="/404" />
if (error) return <Error errorMessage={error.message} />
@@ -52,12 +88,69 @@ const TournamentDetailsPage: React.FC<RouteComponentProps &
)
}
const matchesFilter = (round: Round) => {
const { team_name, player_name, comp, mode, stage } = filter
if (team_name) {
const teamNameUpper = team_name.toUpperCase()
if (
round.winning_team_name.toUpperCase() === teamNameUpper ||
round.losing_team_name.toUpperCase() === teamNameUpper
) {
return true
}
}
if (player_name) {
const playerNameUpper = player_name.toUpperCase()
if (
round.winning_team_players.some(
player => player.toUpperCase() === playerNameUpper
) ||
round.losing_team_players.some(
player => player.toUpperCase() === playerNameUpper
)
) {
return true
}
}
if (comp) {
if (
comp.every(
weapon => round.winning_team_weapons.indexOf(weapon as any) !== -1
) ||
comp.every(
weapon => round.losing_team_weapons.indexOf(weapon as any) !== -1
)
) {
return true
}
}
if (mode && stage) {
if (mode === round.mode && stage === round.stage) {
return true
}
}
return false
}
const encoded = encodeQueryParams(filterMap, removeFalsy(filter))
const linkSuffix = `?${stringify(encoded)}`
console.log("filter", filter)
return (
<>
<Helmet>
<title>{tournament.name} | sendou.ink</title>
</Helmet>
<Link to="/tournaments">
<Link
to={
linkSuffix.length === 1 ? "/tournaments" : `/tournaments${linkSuffix}`
}
>
<Button outlined icon={FaLongArrowAltLeft}>
All tournaments
</Button>
@@ -84,6 +177,7 @@ const TournamentDetailsPage: React.FC<RouteComponentProps &
</Box>
)}
<Box
bg={matchesFilter(round) ? darkerBgColor : undefined}
display="flex"
rounded="lg"
overflow="hidden"
@@ -104,7 +198,7 @@ const TournamentDetailsPage: React.FC<RouteComponentProps &
<Avatar src={mapIcons[round.stage]} size="lg" my="5px" />
{round.stage}
<Icon
name={"sz" as any}
name={round.mode.toLowerCase() as any}
color={themeColorWithShade}
size="2em"
/>

View File

@@ -1,20 +1,121 @@
import React, { useState } from "react"
import React, { useState, useContext } from "react"
import Button from "../elements/Button"
import { Collapse } from "@chakra-ui/core"
import { Collapse, Grid, Flex, Box, RadioGroup, Radio } from "@chakra-ui/core"
import Input from "../elements/Input"
import WeaponSelector from "../common/WeaponSelector"
import Select from "../elements/Select"
import Label from "../elements/Label"
import MyThemeContext from "../../themeContext"
import { Weapon } from "../../types"
import { maps } from "../../utils/lists"
interface TournamentFiltersProps {}
interface TournamentFiltersProps {
forms: {
tournament_name?: string
region?: string
player_name?: string
team_name?: string
comp?: string[]
mode?: string
stage?: string
}
handleChange: (value: Object) => void
handleClear: () => void
onSubmit: () => void
}
const TournamentFilters: React.FC<TournamentFiltersProps> = ({}) => {
const [show, setShow] = useState(true)
const TournamentFilters: React.FC<TournamentFiltersProps> = ({
forms,
handleChange,
handleClear,
onSubmit,
}) => {
const { themeColor } = useContext(MyThemeContext)
const [show, setShow] = useState(false)
return (
<>
<Button onClick={() => setShow(!show)}>
{show ? "Hide filters" : "Show filters"}
</Button>
<Collapse mt={4} isOpen={show}>
Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus
terry richardson ad squid. Nihil anim keffiyeh helvetica, craft beer
labore wes anderson cred nesciunt sapiente ea proident.
<Grid maxW="500px" gridRowGap="1em" gridTemplateColumns="1fr">
<Input
label="Tournament"
value={forms.tournament_name ?? ""}
setValue={(value: string) =>
handleChange({ tournament_name: value })
}
/>
<Input
label="Team"
value={forms.team_name ?? ""}
setValue={(value: string) => handleChange({ team_name: value })}
/>
<Input
label="Player"
value={forms.player_name ?? ""}
setValue={(value: string) => handleChange({ player_name: value })}
/>
<WeaponSelector
label="Comp"
value={(forms.comp as Weapon[]) ?? []}
setValue={(value: Weapon[]) => handleChange({ comp: value })}
isMulti
/>
<Select
label="Map & mode"
isSearchable
value={
forms.stage && forms.mode ? `${forms.stage} (${forms.mode})` : ""
}
setValue={(value: string) => {
const partsArray = value.split(" (")
handleChange({
stage: partsArray[0],
mode: partsArray[1].substring(0, partsArray[1].length - 1),
})
}}
options={maps.reduce(
(acc: { label: string; value: string }[], cur: string) => [
...acc,
{ label: `${cur} (TW)`, value: `${cur} (TW)` },
{ label: `${cur} (SZ)`, value: `${cur} (SZ)` },
{ label: `${cur} (TC)`, value: `${cur} (TC)` },
{ label: `${cur} (RM)`, value: `${cur} (RM)` },
{ label: `${cur} (CB)`, value: `${cur} (CB)` },
],
[]
)}
/>
<Box mt="0.5em">
<Label>Mode</Label>
<RadioGroup
value={forms.region ?? "all"}
defaultValue="0"
spacing={5}
isInline
onChange={(e, value: any) => handleChange({ region: value })}
>
<Radio variantColor={themeColor} value="all">
All
</Radio>
<Radio variantColor={themeColor} value="western">
Western only
</Radio>
<Radio variantColor={themeColor} value="jpn">
Japanese only
</Radio>
</RadioGroup>
</Box>
<Flex mt="1em">
<Button onClick={onSubmit}>Apply</Button>
<Box mx="1em">
<Button outlined onClick={handleClear}>
Clear filters
</Button>
</Box>
</Flex>
</Grid>
</Collapse>
</>
)

View File

@@ -7,14 +7,17 @@ import {
NumberParam,
StringParam,
ArrayParam,
encodeQueryParams,
} from "use-query-params"
import { useQuery } from "@apollo/react-hooks"
import { SEARCH_FOR_TOURNAMENTS } from "../../graphql/queries/searchForTournaments"
import Loading from "../common/Loading"
import Error from "../common/Error"
import TournamentCard from "./TournamentCard"
import { Box, Grid } from "@chakra-ui/core"
import { Box, Grid, Alert, AlertIcon } from "@chakra-ui/core"
import TournamentFilters from "./TournamentFilters"
import Pagination from "../common/Pagination"
import { stringify } from "querystring"
interface SearchForTournamentsData {
searchForTournaments: {
@@ -37,21 +40,26 @@ interface SearchForTournamentsVars {
tournament_name?: string
region?: string
player_name?: string
unique_id?: string
team_name?: string
comp?: string[]
page?: number
mode?: string
stage?: string
}
const TournamentsPage: React.FC<RouteComponentProps> = ({}) => {
const [query, setQuery] = useQueryParams({
page: NumberParam,
tournament_name: StringParam,
region: StringParam,
team_name: StringParam,
player_name: StringParam,
comp: ArrayParam,
})
const queryMap = {
page: NumberParam,
tournament_name: StringParam,
region: StringParam,
team_name: StringParam,
player_name: StringParam,
comp: ArrayParam,
mode: StringParam,
stage: StringParam,
}
const TournamentsPage: React.FC<RouteComponentProps> = () => {
const [query, setQuery] = useQueryParams(queryMap)
const [forms, setForms] = useState<SearchForTournamentsVars>({
page: query.page,
tournament_name: query.tournament_name,
@@ -59,6 +67,8 @@ const TournamentsPage: React.FC<RouteComponentProps> = ({}) => {
team_name: query.team_name,
player_name: query.player_name,
comp: query.comp,
mode: query.mode,
stage: query.stage,
})
const { data, error, loading } = useQuery<
@@ -69,29 +79,89 @@ const TournamentsPage: React.FC<RouteComponentProps> = ({}) => {
})
if (error) return <Error errorMessage={error.message} />
if (loading || !data) return <Loading />
const { tournaments } = data.searchForTournaments
const handleFormChange = (value: Object) => {
setForms({ ...forms, ...value })
}
const handleClear = () => {
setForms({})
setQuery({}, "replace")
}
const encodedQuery = encodeQueryParams(queryMap, query)
const linkSuffix = `?${stringify(encodedQuery)}`
console.log(
"data?.searchForTournaments.pageCount",
data?.searchForTournaments.pageCount
)
return (
<>
<Helmet>
<title>Tournaments | sendou.ink</title>
</Helmet>
<PageHeader title="Tournaments" />
<TournamentFilters />
<Grid
gridGap="1em"
gridTemplateColumns="repeat(auto-fit, minmax(260px, 1fr))"
mt="1em"
>
{tournaments.map(tournament => (
<Box key={tournament.id}>
<Link to={`/tournaments/${tournament.id}`}>
<TournamentCard tournament={tournament} styledOnHover />
</Link>
<TournamentFilters
forms={forms}
handleChange={handleFormChange}
handleClear={handleClear}
onSubmit={() => {
const newObject = { ...forms, page: 1 }
setForms(newObject)
setQuery(newObject)
}}
/>
{data && data.searchForTournaments.tournaments.length > 0 ? (
<>
<Box mt="1em">
<Pagination
currentPage={forms.page ?? 1}
pageCount={data?.searchForTournaments.pageCount ?? 999}
onChange={page => {
setForms({ ...forms, page })
setQuery({ ...query, page })
}}
/>
</Box>
))}
</Grid>
{data && data.searchForTournaments ? (
<>
<Grid
gridGap="1em"
gridTemplateColumns="repeat(auto-fit, minmax(260px, 1fr))"
mt="1em"
>
{data.searchForTournaments.tournaments.map(tournament => (
<Box key={tournament.id}>
<Link to={`/tournaments/${tournament.id}${linkSuffix}`}>
<TournamentCard tournament={tournament} styledOnHover />
</Link>
</Box>
))}
</Grid>
<Box mt="1em">
<Pagination
currentPage={forms.page ?? 1}
pageCount={data.searchForTournaments.pageCount}
onChange={page => {
setForms({ ...forms, page })
setQuery({ ...query, page })
}}
/>
</Box>
</>
) : (
<Loading />
)}
</>
) : (
!loading && (
<Alert status="info" mt="2em">
<AlertIcon />
Your doesn't match any tournaments. Try another filter!
</Alert>
)
)}
</>
)
}

View File

@@ -267,26 +267,29 @@ const BuildFormModal: React.FC<BuildFormModalProps> = ({
)}
<Box asFlex mt="1em" justifyContent="space-between" flexWrap="wrap">
<Box asFlex flexDirection="column" alignItems="center">
<Select
label="Headgear"
options={headSelectOptions}
clearable
value={build.headgearItem}
setValue={(headgearItem: HeadGear) => {
if (
(!build.headgear ||
build.headgear.every(ability => ability === "UNKNOWN")) &&
existingGear.hasOwnProperty(headgearItem)
) {
handleChange({
headgearItem,
headgear: existingGear[headgearItem],
})
} else {
handleChange({ headgearItem })
}
}}
/>
<Box minW="275px">
<Select
label="Headgear"
options={headSelectOptions}
clearable
isSearchable
value={build.headgearItem}
setValue={(headgearItem: HeadGear) => {
if (
(!build.headgear ||
build.headgear.every(ability => ability === "UNKNOWN")) &&
existingGear.hasOwnProperty(headgearItem)
) {
handleChange({
headgearItem,
headgear: existingGear[headgearItem],
})
} else {
handleChange({ headgearItem })
}
}}
/>
</Box>
{build.headgearItem && (
<Box mt="0.5em">
<GearImage englishName={build.headgearItem} />
@@ -294,26 +297,29 @@ const BuildFormModal: React.FC<BuildFormModalProps> = ({
)}
</Box>
<Box asFlex flexDirection="column" alignItems="center">
<Select
label="Clothing"
options={clothingSelectOptions}
clearable
value={build.clothingItem}
setValue={(clothingItem: ClothingGear) => {
if (
(!build.clothing ||
build.clothing.every(ability => ability === "UNKNOWN")) &&
existingGear.hasOwnProperty(clothingItem)
) {
handleChange({
clothingItem,
clothing: existingGear[clothingItem],
})
} else {
handleChange({ clothingItem })
}
}}
/>
<Box minW="275px">
<Select
label="Clothing"
options={clothingSelectOptions}
clearable
isSearchable
value={build.clothingItem}
setValue={(clothingItem: ClothingGear) => {
if (
(!build.clothing ||
build.clothing.every(ability => ability === "UNKNOWN")) &&
existingGear.hasOwnProperty(clothingItem)
) {
handleChange({
clothingItem,
clothing: existingGear[clothingItem],
})
} else {
handleChange({ clothingItem })
}
}}
/>
</Box>
{build.clothingItem && (
<Box mt="0.5em">
<GearImage englishName={build.clothingItem} />
@@ -321,26 +327,29 @@ const BuildFormModal: React.FC<BuildFormModalProps> = ({
)}
</Box>
<Box asFlex flexDirection="column" alignItems="center">
<Select
label="Shoes"
options={shoesSelectOptions}
clearable
value={build.shoesItem}
setValue={(shoesItem: ShoesGear) => {
if (
(!build.shoes ||
build.shoes.every(ability => ability === "UNKNOWN")) &&
existingGear.hasOwnProperty(shoesItem)
) {
handleChange({
shoesItem,
shoes: existingGear[shoesItem],
})
} else {
handleChange({ shoesItem })
}
}}
/>
<Box minW="275px">
<Select
label="Shoes"
options={shoesSelectOptions}
clearable
isSearchable
value={build.shoesItem}
setValue={(shoesItem: ShoesGear) => {
if (
(!build.shoes ||
build.shoes.every(ability => ability === "UNKNOWN")) &&
existingGear.hasOwnProperty(shoesItem)
) {
handleChange({
shoesItem,
shoes: existingGear[shoesItem],
})
} else {
handleChange({ shoesItem })
}
}}
/>
</Box>
{build.shoesItem && (
<Box mt="0.5em">
<GearImage englishName={build.shoesItem} />

View File

@@ -39,7 +39,11 @@ const ProfileModal: React.FC<ProfileModalProps> = ({
const toast = useToast()
useEffect(() => {
if (profile.motion_sens !== null && !profile.stick_sens) {
if (
profile.motion_sens !== null &&
!profile.stick_sens &&
profile.stick_sens !== 0
) {
setError("Motion sensitivity entered without stick sensitivity")
} else if (profile.custom_url && profile.custom_url.length < 2) {
setError("Custom URL has to be over 2 characters long")
@@ -110,6 +114,7 @@ const ProfileModal: React.FC<ProfileModalProps> = ({
<Box mt="1em">
<Select
label="Country"
isSearchable
value={
profile.country
? {

View File

@@ -84,11 +84,7 @@ const Top500Forms: React.FC<Top500FormsProps> = ({ forms, handleChange }) => {
isInline
onChange={(e, value: any) => handleChange({ mode: parseInt(value) })}
>
<Radio
variantColor={themeColor}
value="0"
isChecked={forms.mode === 0 || !forms.mode}
>
<Radio variantColor={themeColor} value="0">
All modes
</Radio>
<Radio variantColor={themeColor} value="1">

View File

@@ -8,6 +8,8 @@ export const SEARCH_FOR_TOURNAMENTS: DocumentNode = gql`
$team_name: String
$player_name: String
$comp: [String]
$mode: Mode
$stage: String
) {
searchForTournaments(
page: $page
@@ -16,6 +18,8 @@ export const SEARCH_FOR_TOURNAMENTS: DocumentNode = gql`
team_name: $team_name
player_name: $player_name
comp: $comp
mode: $mode
stage: $stage
) {
tournaments {
id

View File

@@ -25,3 +25,14 @@ export function importAll(r: __WebpackModuleApi.RequireContext) {
})
return images
}
// https://coderwall.com/p/urxpsa/remove-falsy-values-or-empty-strings-from-javascript-objects
export const removeFalsy = (obj: { [key: string]: any }) => {
const newObj: { [key: string]: any } = {}
Object.keys(obj).forEach((prop: any) => {
if (obj[prop]) {
newObj[prop] = obj[prop]
}
})
return newObj
}

View File

@@ -789,6 +789,32 @@ export const continents = {
zw: "AF",
} as const
export const maps = [
"The Reef",
"Musselforge Fitness",
"Starfish Mainstage",
"Humpback Pump Track",
"Inkblot Art Academy",
"Sturgeon Shipyard",
"Moray Towers",
"Port Mackerel",
"Manta Maria",
"Kelp Dome",
"Snapper Canal",
"Blackbelly Skatepark",
"MakoMart",
"Walleye Warehouse",
"Shellendorf Institute",
"Arowana Mall",
"Goby Arena",
"Piranha Pit",
"Camp Triggerfish",
"Wahoo World",
"New Albacore Hotel",
"Ancho-V Games",
"Skipper Pavilion",
]
export const modes = [
"",
"Splat Zones",

View File

@@ -141,14 +141,14 @@ const resolvers = {
Mutation: {
addBuild: async (root, args, ctx) => {
if (!ctx.user) throw new AuthenticationError("Not logged in.")
if (args.title)
if (args.title) {
if (args.title.length > 100)
throw new UserInputError("Title too long.", {
invalidArgs: args,
})
else {
args.title = undefined
}
} else {
args.title = undefined
}
if (args.description) {
if (args.description.length > 1000)

View File

@@ -12,6 +12,8 @@ const typeDef = gql`
unique_id: String
team_name: String
comp: [String]
stage: String
mode: Mode
page: Int
): TournamentCollection!
}
@@ -166,6 +168,14 @@ const resolvers = {
})
}
if (args.mode) {
roundSearchCriteria.mode = args.mode
}
if (args.stage) {
roundSearchCriteria.stage = args.stage
}
// if criteria were presented that we have to search
// from the Round collection
let tournament_ids = null
@@ -176,7 +186,15 @@ const resolvers = {
if (args.region && args.region === "jpn")
tournamentSearchCriteria.jpn = true
if (roundSearchCriteria.$or.length !== 0) {
if (roundSearchCriteria.$or.length === 0) {
delete roundSearchCriteria.$or
}
if (
roundSearchCriteria.$or ||
roundSearchCriteria.stage ||
roundSearchCriteria.mode
) {
tournament_ids = await Round.find(roundSearchCriteria).distinct(
"tournament_id"
)

6
package-lock.json generated
View File

@@ -828,9 +828,9 @@
}
},
"cross-env": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.1.tgz",
"integrity": "sha512-1+DmLosu38kC4s1H4HzNkcolwdANifu9+5bE6uKQCV4L6jvVdV9qdRAk8vV3GoWRe0x4z+K2fFhgoDMqwNsPqQ==",
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.2.tgz",
"integrity": "sha512-KZP/bMEOJEDCkDQAyRhu3RL2ZO/SUVrxQVI0G3YEQ+OLbRA3c6zgixe8Mq8a/z7+HKlNEjo8oiLUs8iRijY2Rw==",
"dev": true,
"requires": {
"cross-spawn": "^7.0.1"

View File

@@ -33,7 +33,7 @@
"webhook-discord": "^3.5.1"
},
"devDependencies": {
"cross-env": "^7.0.1",
"cross-env": "^7.0.2",
"nodemon": "^2.0.2"
}
}