free agent page milestone

This commit is contained in:
Sendou 2020-02-01 19:28:50 +02:00
parent 1626a824ac
commit 77af38de54
16 changed files with 384 additions and 191 deletions

View File

@ -1,7 +1,6 @@
import React, { useState, useEffect, useRef } from "react"
import jstz from "jstz"
import {
Button,
Popover,
PopoverTrigger,
PopoverContent,
@ -17,6 +16,7 @@ import { useContext } from "react"
import MyThemeContext from "../themeContext"
import PageHeader from "../components/common/PageHeader"
import { Helmet } from "react-helmet-async"
import Button from "../components/elements/Button"
const CalendarPage: React.FC<RouteComponentProps> = () => {
const {
@ -46,14 +46,11 @@ const CalendarPage: React.FC<RouteComponentProps> = () => {
<PageHeader title="Competitive Calendar" />
<Popover placement="top-start">
<PopoverTrigger>
<Button
variantColor={themeColor}
variant="outline"
onClick={() => setShowCode(!showCode)}
mt="1em"
>
Show emoji code
</Button>
<Box mt="1em">
<Button outlined onClick={() => setShowCode(!showCode)}>
Show emoji code
</Button>
</Box>
</PopoverTrigger>
<PopoverContent zIndex={4} background={darkerBgColor} width="280px">
<PopoverArrow />

View File

@ -1,11 +1,12 @@
import React, { useState } from "react"
import FieldsetWithLegend from "../common/FieldsetWithLegend"
import { Flex, Box, Button } from "@chakra-ui/core"
import { Flex, Box } from "@chakra-ui/core"
import { abilitiesGameOrder } from "../../utils/lists"
import AbilityIcon from "./AbilityIcon"
import { Ability } from "../../types"
import { useContext } from "react"
import MyThemeContext from "../../themeContext"
import Button from "../elements/Button"
interface AbilitySelectorProps {
abilities: Ability[]
@ -21,7 +22,6 @@ const AbilitySelector: React.FC<AbilitySelectorProps> = ({
return show ? (
<>
<Button
variantColor={themeColor}
onClick={() => {
setShow(!show)
setAbilities([])
@ -86,9 +86,7 @@ const AbilitySelector: React.FC<AbilitySelectorProps> = ({
)}
</>
) : (
<Button variantColor={themeColor} onClick={() => setShow(!show)}>
Filter by ability
</Button>
<Button onClick={() => setShow(!show)}>Filter by ability</Button>
)
}

View File

@ -11,16 +11,7 @@ import {
Build,
} from "../../types"
import useBreakPoints from "../../hooks/useBreakPoints"
import {
Box,
Flex,
Heading,
FormLabel,
Switch,
Button,
Alert,
AlertIcon,
} from "@chakra-ui/core"
import { Box, Flex, Heading, FormLabel, Switch, Button } from "@chakra-ui/core"
import { useContext } from "react"
import MyThemeContext from "../../themeContext"
import useLocalStorage from "@rehooks/local-storage"
@ -32,6 +23,7 @@ import BuildCard from "./BuildCard"
import InfiniteScroll from "react-infinite-scroller"
import PageHeader from "../common/PageHeader"
import AbilitySelector from "./AbilitySelector"
import Alert from "../elements/Alert"
const BuildsPage: React.FC<RouteComponentProps> = () => {
const { themeColor } = useContext(MyThemeContext)
@ -126,10 +118,7 @@ const BuildsPage: React.FC<RouteComponentProps> = () => {
</>
)}
{weapon && buildsFilterByAbilities.length === 0 && (
<Alert status="info" borderRadius="5px" mt="2em">
<AlertIcon />
No builds found with the current selection. Please adjust it above.
</Alert>
<Alert status="info">No builds found with the current filter</Alert>
)}
</>
)

View File

@ -1,17 +1,12 @@
import React from "react"
import { Alert, AlertIcon } from "@chakra-ui/core"
import Alert from "../elements/Alert"
interface ErrorProps {
errorMessage: string
}
const Error: React.FC<ErrorProps> = ({ errorMessage }) => {
return (
<Alert status="error">
<AlertIcon color="red.500" />
{errorMessage}
</Alert>
)
return <Alert status="error">{errorMessage}</Alert>
}
export default Error

View File

@ -7,7 +7,9 @@ import Select from "../elements/Select"
interface WeaponSelectorProps {
setValue: (value: string) => void
label?: string
autoFocus?: boolean
clearable?: boolean
}
const singleOption = (props: any) => (
@ -23,19 +25,29 @@ const singleOption = (props: any) => (
const WeaponSelector: React.FC<WeaponSelectorProps> = ({
setValue,
autoFocus = false,
label,
clearable,
autoFocus,
}) => {
return (
<Select
options={weaponSelectOptions}
setValue={setValue}
placeholder="Select a weapon"
components={{
IndicatorSeparator: () => null,
Option: singleOption,
}}
autoFocus={autoFocus}
/>
<>
{label && (
<Box mb="0.2em">
<b>{label}</b>
</Box>
)}
<Select
options={weaponSelectOptions}
setValue={setValue}
placeholder="Select weapon"
clearable={clearable}
components={{
IndicatorSeparator: () => null,
Option: singleOption,
}}
autoFocus={autoFocus}
/>
</>
)
}

View File

@ -0,0 +1,18 @@
import React from "react"
import { Alert as ChakraAlert, AlertIcon } from "@chakra-ui/core"
interface AlertProps {
children: string | string[]
status: "error" | "success" | "warning" | "info"
}
const Alert: React.FC<AlertProps> = ({ children, status }) => {
return (
<ChakraAlert status={status} borderRadius="5px" mt="2em">
<AlertIcon />
{children}
</ChakraAlert>
)
}
export default Alert

View File

@ -0,0 +1,12 @@
import React from "react"
import { Box as ChakraBox, BoxProps } from "@chakra-ui/core"
interface MyBoxProps {
children: JSX.Element | JSX.Element[]
}
const Box: React.FC<BoxProps & MyBoxProps> = ({ children, ...props }) => {
return <ChakraBox {...props}>{children}</ChakraBox>
}
export default Box

View File

@ -0,0 +1,39 @@
import React from "react"
import { Button as ChakraButton } from "@chakra-ui/core"
import { useContext } from "react"
import MyThemeContext from "../../themeContext"
import { IconType } from "react-icons/lib/cjs"
interface ButtonProps {
children: string | string[]
onClick: () => void
size?: "xs" | "sm" | "lg" | "md"
icon?: IconType
outlined?: boolean
disabled?: boolean
}
const Button: React.FC<ButtonProps> = ({
children,
onClick,
icon,
size,
disabled,
outlined = false,
}) => {
const { themeColor } = useContext(MyThemeContext)
return (
<ChakraButton
variant={outlined ? "outline" : "solid"}
variantColor={themeColor}
leftIcon={icon}
onClick={onClick}
size={size}
isDisabled={disabled}
>
{children}
</ChakraButton>
)
}
export default Button

View File

@ -0,0 +1,48 @@
import React, { useContext } from "react"
import { RadioGroup as ChakraRadioGroup, Radio } from "@chakra-ui/core"
import MyThemeContext from "../../themeContext"
import Box from "./Box"
interface RadioGroupProps {
options: string[]
value: string
label?: string
setValue: (value: any) => void
}
const RadioGroup: React.FC<RadioGroupProps> = ({
value,
setValue,
options,
label,
}) => {
const { themeColor } = useContext(MyThemeContext)
return (
<>
{label && (
<Box mb="0.2em">
<b>{label}</b>
</Box>
)}
<ChakraRadioGroup
spacing={5}
isInline
onChange={e => setValue(e.target.value)}
value={value}
>
{options.map(option => (
<Radio
key={option}
variantColor={themeColor}
value={option}
isChecked={value === option}
>
{option}
</Radio>
))}
</ChakraRadioGroup>
</>
)
}
export default RadioGroup

View File

@ -29,6 +29,7 @@ interface SelectProps {
value: string
}>
>
clearable?: boolean
}
const Select: React.FC<SelectProps> = ({
@ -36,7 +37,8 @@ const Select: React.FC<SelectProps> = ({
components,
placeholder,
setValue,
autoFocus = false,
clearable,
autoFocus,
}) => {
const {
colorMode,
@ -46,7 +48,7 @@ const Select: React.FC<SelectProps> = ({
} = useContext(MyThemeContext)
const handleChange = (selectedOption: any) => {
setValue(selectedOption.value)
setValue(selectedOption?.value)
}
return (
@ -56,6 +58,7 @@ const Select: React.FC<SelectProps> = ({
onChange={handleChange}
placeholder={placeholder}
isSearchable
isClearable={clearable}
options={options}
components={{
IndicatorSeparator: () => null,

View File

@ -1,7 +1,12 @@
import React, { useState } from "react"
import { useQuery } from "@apollo/react-hooks"
import { USER } from "../../graphql/queries/user"
import { UserData, FreeAgentPostsData, Weapon } from "../../types"
import {
UserData,
FreeAgentPostsData,
Weapon,
FreeAgentPost,
} from "../../types"
import { FREE_AGENT_POSTS } from "../../graphql/queries/freeAgentPosts"
import Loading from "../common/Loading"
import Error from "../common/Error"
@ -10,9 +15,27 @@ import PostsAccordion from "./PostsAccordion"
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"
/*can_vc: "YES" | "USUALLY" | "SOMETIMES" | "NO"
playstyles: ("FRONTLINE" | "MIDLINE" | "BACKLINE")[]*/
const playstyleToEnum = {
"Frontline/Slayer": "FRONTLINE",
"Midline/Support": "MIDLINE",
"Backline/Anchor": "BACKLINE",
} as const
const FreeAgentsPage: React.FC<RouteComponentProps> = () => {
const [weapon, setWeapon] = useState<Weapon | null>(null)
const [playstyle, setPlaystyle] = useState<
"Any" | "Frontline/Slayer" | "Midline/Support" | "Backline/Anchor"
>("Any")
const [region, setRegion] = useState<
"Any" | "Europe" | "The Americas" | "Oceania" | "Other"
>("Any")
const { data, error, loading } = useQuery<FreeAgentPostsData>(
FREE_AGENT_POSTS
)
@ -28,17 +51,83 @@ const FreeAgentsPage: React.FC<RouteComponentProps> = () => {
const faPosts = data.freeAgentPosts
const postsFilter = (post: FreeAgentPost) => {
if (post.hidden) return false
if (weapon && post.discord_user.weapons.indexOf(weapon) === -1) {
return false
}
if (playstyle !== "Any") {
if (post.playstyles.indexOf(playstyleToEnum[playstyle]) === -1)
return false
}
if (region !== "Any") {
if (!post.discord_user.country) {
if (region === "Other") return true
return false
}
const continentCode = continents[post.discord_user.country]
if (region === "Europe" && continentCode !== "EU") return false
else if (
region === "The Americas" &&
continentCode !== "NA" &&
continentCode !== "SA"
)
return false
else if (region === "Oceania" && continentCode !== "OC") return false
else if (
region === "Other" &&
continentCode !== "AF" &&
continentCode !== "AN" &&
continentCode !== "AS" &&
continentCode !== "OC"
)
return false
}
return true
}
return (
<>
<Helmet>
<title>Free Agents | sendou.ink</title>
</Helmet>
<PageHeader title="Free Agents" />
<WeaponSelector
setValue={(weapon: string) => setWeapon(weapon as Weapon)}
autoFocus
/>
<PostsAccordion posts={faPosts} />
<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"
setValue={(weapon: string) => setWeapon(weapon as Weapon)}
clearable
/>
</Box>
<PostsAccordion posts={faPosts.filter(postsFilter)} />
</>
)
}

View File

@ -21,6 +21,7 @@ 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[]
@ -37,125 +38,126 @@ const hasExtraInfo = (post: FreeAgentPost) => {
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
.filter(post => !post.hidden)
.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>
{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" />
)}
</AccordionItem>
)
})}
<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>
)
}

View File

@ -6,6 +6,7 @@ import { useContext } from "react"
import MyThemeContext from "../../themeContext"
import "./HomePage.css"
import { Helmet } from "react-helmet-async"
import Alert from "../elements/Alert"
const HomePage: React.FC<RouteComponentProps> = () => {
const { colorMode, grayWithShade } = useContext(MyThemeContext)

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef, useContext } from "react"
import React, { useState, useEffect, useRef } from "react"
import { SketchField, Tools } from "@sendou/react-sketch"
import { CirclePicker } from "react-color"
import weaponDict from "../../utils/english_internal.json"
@ -9,20 +9,19 @@ import { Helmet } from "react-helmet-async"
import { Weapon } from "../../types"
import Error from "../common/Error"
import {
Button,
Flex,
Box,
InputGroup,
Input,
InputRightElement,
} from "@chakra-ui/core"
import MyThemeContext from "../../themeContext"
import { FaFileDownload, FaFileUpload, FaFileImage } from "react-icons/fa"
import reef from "../../assets/plannerMaps/reef-sz.png"
import MapSelect from "./MapSelect"
import { RouteComponentProps } from "@reach/router"
import PageHeader from "../common/PageHeader"
import DraggableWeaponSelector from "./DraggableWeaponSelector"
import Button from "../elements/Button"
const MapPlannerPage: React.FC<RouteComponentProps> = () => {
let sketch: any = null
@ -53,7 +52,6 @@ const MapPlannerPage: React.FC<RouteComponentProps> = () => {
enableCopyPaste: false,
}
const fileInput = useRef<HTMLInputElement | null>(null)
const { themeColor } = useContext(MyThemeContext)
const [tool, setTool] = useState(Tools.Pencil)
const [color, setColor] = useState("#f44336")
const [canUndo, setCanUndo] = useState(false)
@ -202,9 +200,8 @@ const MapPlannerPage: React.FC<RouteComponentProps> = () => {
<Flex justifyContent="space-between" mt="1em" flexWrap="wrap">
<Button
onClick={() => download(sketch.toDataURL(), "png")}
leftIcon={FaFileImage}
variantColor={themeColor}
variant="outline"
icon={FaFileImage}
outlined
>
Download as .png
</Button>
@ -216,18 +213,12 @@ const MapPlannerPage: React.FC<RouteComponentProps> = () => {
"json"
)
}
leftIcon={FaFileDownload}
variantColor={themeColor}
variant="outline"
icon={FaFileDownload}
outlined
>
Download as .json
</Button>
<Button
onClick={() => handleUpload()}
leftIcon={FaFileUpload}
variantColor={themeColor}
variant="outline"
>
<Button onClick={() => handleUpload()} icon={FaFileUpload} outlined>
Load from .json
</Button>
<input type="file" accept=".json" ref={fileInput} />
@ -244,10 +235,9 @@ const MapPlannerPage: React.FC<RouteComponentProps> = () => {
/>
<InputRightElement width="7rem">
<Button
h="1.75rem"
size="sm"
onClick={() => addTextToSketch()}
isDisabled={text === ""}
disabled={text === ""}
>
Add to picture
</Button>

View File

@ -72,7 +72,7 @@ export interface User {
twitch_name?: string
twitter_name?: string
country?: CountryCode
weapons?: Weapon[]
weapons: Weapon[]
top500: boolean
custom_url?: string
sens?: {
@ -143,7 +143,7 @@ export interface FreeAgentPost {
discord_id: string
twitter_name?: string
country?: CountryCode
weapons?: Weapon[]
weapons: Weapon[]
top500: boolean
}
}

View File

@ -42,7 +42,7 @@ const typeDef = gql`
twitter_name: String
country: String
sens: Sens
weapons: [String]!
weapons: [String!]!
custom_url: String
top500: Boolean!
}