mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-30 15:17:43 -05:00
Merge branch 'typescript-frontend-rewrite'
This commit is contained in:
40
frontend-react/package-lock.json
generated
40
frontend-react/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
>
|
||||
«
|
||||
</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)
|
||||
}
|
||||
>
|
||||
»
|
||||
</Box>
|
||||
</Box>
|
||||
<ReactPaginate
|
||||
previousLabel={<>«</>}
|
||||
nextLabel={<>»</>}
|
||||
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"}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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> = ({
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ const Select: React.FC<SelectProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Box w={width}>
|
||||
<Box>
|
||||
{label && <Label required={required}>{label}</Label>}
|
||||
<ReactSelect
|
||||
className="basic-single"
|
||||
|
||||
170
frontend-react/src/components/freeagents/FreeAgentCard.tsx
Normal file
170
frontend-react/src/components/freeagents/FreeAgentCard.tsx
Normal 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
|
||||
@@ -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)} />
|
||||
</>
|
||||
)
|
||||
|
||||
50
frontend-react/src/components/freeagents/Posts.tsx
Normal file
50
frontend-react/src/components/freeagents/Posts.tsx
Normal 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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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 }))}
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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
|
||||
? {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
6
package-lock.json
generated
@@ -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"
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"webhook-discord": "^3.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.1",
|
||||
"cross-env": "^7.0.2",
|
||||
"nodemon": "^2.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user