builds stuff

This commit is contained in:
Sendou 2020-01-24 23:41:10 +02:00
parent 9d422a0858
commit 2071a3beeb
15 changed files with 272 additions and 93 deletions

View File

@ -1927,6 +1927,14 @@
"@types/react": "*"
}
},
"@types/react-infinite-scroller": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@types/react-infinite-scroller/-/react-infinite-scroller-1.2.1.tgz",
"integrity": "sha512-64bpbqdSgtmy1zSZ2AQoFzguwZO7TyKjqJRTEnfNMCAQbnrX90kz+rYufZyY9CmzhwpXMwRO8xR9fMQnbYUkgQ==",
"requires": {
"@types/react": "*"
}
},
"@types/stack-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
@ -11846,6 +11854,14 @@
"camelcase": "^5.0.0"
}
},
"react-infinite-scroller": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/react-infinite-scroller/-/react-infinite-scroller-1.2.4.tgz",
"integrity": "sha512-/oOa0QhZjXPqaD6sictN2edFMsd3kkMiE19Vcz5JDgHpzEJVqYcmq+V3mkwO88087kvKGe1URNksHEOt839Ubw==",
"requires": {
"prop-types": "^15.5.8"
}
},
"react-is": {
"version": "16.12.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz",

View File

@ -16,6 +16,7 @@
"@types/reach__router": "^1.2.6",
"@types/react": "^16.9.19",
"@types/react-dom": "^16.9.5",
"@types/react-infinite-scroller": "^1.2.1",
"apollo-boost": "^0.4.7",
"emotion-theming": "^10.0.27",
"graphql": "^14.5.8",
@ -24,6 +25,7 @@
"react-helmet-async": "^1.0.4",
"react-hotkeys-hook": "^1.5.4",
"react-icons": "^3.8.0",
"react-infinite-scroller": "^1.2.4",
"react-scripts": "3.3.0",
"typescript": "^3.7.5"
},

View File

@ -1,4 +1,4 @@
import React, { useState, useContext } from "react"
import React, { useState, useContext, useEffect } from "react"
import { Build } from "../../types"
import {
Box,
@ -16,20 +16,31 @@ import { FaInfo, FaPlus, FaMinus } from "react-icons/fa"
import ViewGear from "./ViewGear"
import ViewAP from "./ViewAP"
import MyThemeContext from "../../themeContext"
import { Link } from "@reach/router"
interface BuildCardProps {
build: Build
defaultToAPView: Boolean
defaultToAPView: boolean
showUser?: boolean
}
const BuildCard: React.FC<BuildCardProps> = ({ build, defaultToAPView }) => {
const BuildCard: React.FC<BuildCardProps> = ({
build,
defaultToAPView,
showUser = false,
}) => {
const [apView, setApView] = useState(defaultToAPView)
const { borderStyle, themeColor, darkerBgColor, grayWithShade } = useContext(
MyThemeContext
)
useEffect(() => {
setApView(defaultToAPView)
}, [defaultToAPView])
return (
<Box
as="fieldset"
width="325px"
borderWidth="1px"
border={borderStyle}
@ -39,6 +50,19 @@ const BuildCard: React.FC<BuildCardProps> = ({ build, defaultToAPView }) => {
pb="6"
px="6"
>
{showUser && build.discord_user && (
<Box
as="legend"
color={grayWithShade}
fontWeight="semibold"
letterSpacing="wide"
fontSize="s"
>
<Link to={`/u/${build.discord_user.discord_id}`}>
{build.discord_user.username}#{build.discord_user.discriminator}
</Link>
</Box>
)}
<Flex justifyContent="space-between">
<Box width="24" height="auto">
<WeaponImage englishName={build.weapon} size="MEDIUM" />
@ -58,11 +82,12 @@ const BuildCard: React.FC<BuildCardProps> = ({ build, defaultToAPView }) => {
fontWeight="semibold"
letterSpacing="wide"
fontSize="xs"
ml="8px"
>
{new Date(parseInt(build.updatedAt)).toLocaleString()}
</Box>
{build.title && (
<Box mt="1" fontWeight="semibold" as="h4" lineHeight="tight">
<Box ml="8px" fontWeight="semibold" as="h4" lineHeight="tight">
{build.title}
</Box>
)}

View File

@ -3,50 +3,88 @@ import { Helmet } from "react-helmet-async"
import { RouteComponentProps } from "@reach/router"
import WeaponSelector from "../common/WeaponSelector"
import { useState } from "react"
import { Weapon, Ability } from "../../types"
import {
Weapon,
Ability,
SearchForBuildsData,
SearchForBuildsVars,
Build,
} from "../../types"
import useBreakPoints from "../../hooks/useBreakPoints"
import { abilitiesGameOrder } from "../../utils/lists"
import { Box, Flex, Heading, FormLabel, Switch } from "@chakra-ui/core"
import { Box, Flex, Heading, FormLabel, Switch, Button } from "@chakra-ui/core"
import AbilityIcon from "./AbilityIcon"
import { useContext } from "react"
import MyThemeContext from "../../themeContext"
import useLocalStorage from "@rehooks/local-storage"
import { useQuery } from "@apollo/react-hooks"
import { SEARCH_FOR_BUILDS } from "../../graphql/queries/searchForBuilds"
import Loading from "../common/Loading"
import Error from "../common/Error"
import BuildCard from "./BuildCard"
import InfiniteScroll from "react-infinite-scroller"
const BuildsPage: React.FC<RouteComponentProps> = () => {
const { themeColor } = useContext(MyThemeContext)
const [weapon, setWeapon] = useState<Weapon | null>(null)
const [abilities, setAbilities] = useState<Ability[]>(["T"])
const [buildsToShow, setBuildsToShow] = useState(4)
const [abilities, setAbilities] = useState<Ability[]>([])
const [prefersAPView, setAPPreference] = useLocalStorage<boolean>(
"prefersAPView"
)
console.log("prefersAPView", prefersAPView)
const isSmall = useBreakPoints(870)
const { data, error, loading } = useQuery<
SearchForBuildsData,
SearchForBuildsVars
>(SEARCH_FOR_BUILDS, {
variables: { weapon: weapon as any },
skip: !weapon,
})
if (error) return <Error errorMessage={error.message} />
const buildsFilterByAbilities: Build[] = !data
? []
: data.searchForBuilds.filter(build => {
if (abilities.length === 0) return true
const abilitiesInBuild = new Set([
...build.headgear,
...build.clothing,
...build.shoes,
])
return abilities.every(ability => abilitiesInBuild.has(ability))
})
return (
<>
<Helmet>
<title>Builds | sendou.ink</title>
<title>{weapon ? `${weapon} ` : ""}Builds | sendou.ink</title>
</Helmet>
<FormLabel htmlFor="apview">Default to Ability Point view</FormLabel>
<Switch
id="apview"
color={themeColor}
isChecked={prefersAPView === null ? false : prefersAPView}
onChange={() => setAPPreference(!prefersAPView)}
/>
<Box mb="1em">
<FormLabel htmlFor="apview">Default to Ability Point view</FormLabel>
<Switch
id="apview"
color={themeColor}
isChecked={prefersAPView === null ? false : prefersAPView}
onChange={() => setAPPreference(!prefersAPView)}
/>
</Box>
<WeaponSelector
weapon={weapon}
setWeapon={(weapon: Weapon | null) => setWeapon(weapon)}
dropdownMode={isSmall}
/>
{!weapon && (
<Flex flexWrap="wrap" justifyContent="center" pt="1em">
{weapon && (
<Flex flexWrap="wrap" justifyContent="center" mt="1em">
{abilitiesGameOrder.map(ability => (
<Box
key={ability}
p="5px"
cursor="pointer"
onClick={() => setAbilities(abilities.concat(ability))}
cursor={abilities.indexOf(ability) === -1 ? "pointer" : undefined}
onClick={() => {
if (abilities.indexOf(ability) !== -1) return
setAbilities(abilities.concat(ability))
}}
>
<AbilityIcon
ability={abilities.indexOf(ability) === -1 ? ability : "EMPTY"}
@ -81,6 +119,43 @@ const BuildsPage: React.FC<RouteComponentProps> = () => {
</Flex>
</>
)}
{loading && <Loading />}
{buildsFilterByAbilities.length > 0 && data && (
<>
<InfiniteScroll
pageStart={1}
loadMore={page => setBuildsToShow(page * 4)}
hasMore={buildsToShow < data.searchForBuilds.length}
>
<Flex flexWrap="wrap" pt="2em">
{buildsFilterByAbilities
.filter((build, index) => index < buildsToShow)
.map(build => (
<Box key={build.id} p="0.2em">
<BuildCard
build={build}
defaultToAPView={
prefersAPView === null ? false : prefersAPView
}
showUser
/>
</Box>
))}
</Flex>
</InfiniteScroll>
<Box w="50%" textAlign="center" mx="auto">
<Heading size="sm">No more builds to show</Heading>
<Button
variantColor={themeColor}
variant="outline"
mt="1em"
onClick={() => window.scrollTo(0, 0)}
>
Return to the top
</Button>
</Box>
</>
)}
</>
)
}

View File

@ -17,7 +17,7 @@ const ViewGear: React.FC<ViewGearProps> = ({ build }) => {
gridRowGap="10px"
justifyItems="center"
alignItems="center"
mt={noItems ? "2" : "0"}
mt="1em"
>
<GearImage
englishName={build.headgearItem}

View File

@ -3,7 +3,7 @@ import { Box } from "@chakra-ui/core"
import MyThemeContext from "../../themeContext"
interface DividingBoxProps {
children: JSX.Element | JSX.Element[] | string
children: React.ReactNode
location: "top" | "left" | "bottom" | "right"
margin?: string
width?: string

View File

@ -0,0 +1,41 @@
import React from "react"
import { Box, BoxProps } from "@chakra-ui/core"
import { useContext } from "react"
import MyThemeContext from "../../themeContext"
interface FieldsetWithLegendProps {
children: React.ReactNode
title: string
titleFontSize: string
}
const FieldsetWithLegend: React.FC<FieldsetWithLegendProps> = ({
children,
title,
titleFontSize,
}) => {
const { borderStyle, grayWithShade } = useContext(MyThemeContext)
return (
<Box
as="fieldset"
maxW="sm"
border={borderStyle}
rounded="lg"
display="inline-block"
p="1em"
>
<Box
as="legend"
color={grayWithShade}
fontWeight="semibold"
letterSpacing="wide"
fontSize={titleFontSize}
>
{title}
</Box>
{children}
</Box>
)
}
export default FieldsetWithLegend

View File

@ -7,7 +7,7 @@ interface LoadingProps {}
const Loading: React.FC<LoadingProps> = () => {
const { themeColorWithShade } = useContext(MyThemeContext)
return (
<Box textAlign="center">
<Box textAlign="center" pt="2em">
<Spinner
color={themeColorWithShade}
size="xl"

View File

@ -1,6 +1,6 @@
import React from "react"
import { Weapon } from "../../types"
import { Input, Flex, PseudoBox, Select } from "@chakra-ui/core"
import { Input, Flex, PseudoBox, Select, Box } from "@chakra-ui/core"
import { useState } from "react"
import { weapons } from "../../utils/lists"
import WeaponImage from "./WeaponImage"
@ -18,7 +18,9 @@ const WeaponSelector: React.FC<WeaponSelectorProps> = ({
setWeapon,
dropdownMode = false,
}) => {
const { darkerBgColor } = useContext(MyThemeContext)
const { darkerBgColor, borderStyle, grayWithShade } = useContext(
MyThemeContext
)
const [input, setInput] = useState("")
const filterWeaponArray = (weapon: Weapon) =>
@ -30,7 +32,7 @@ const WeaponSelector: React.FC<WeaponSelectorProps> = ({
if (dropdownMode)
return (
<Select
placeholder="Select a weapon"
placeholder="Filter weapons"
value={weapon ?? ""}
onChange={(event: React.ChangeEvent<HTMLSelectElement>) =>
setWeapon(event.target.value as Weapon)
@ -50,40 +52,62 @@ const WeaponSelector: React.FC<WeaponSelectorProps> = ({
return (
<>
<Input
placeholder="Click below or search for a weapon"
value={input}
onChange={handleInputChange}
w="50%"
ml="auto"
mr="auto"
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key !== "Enter") return
const oneWeaponArray = weapons.filter(filterWeaponArray)
if (oneWeaponArray.length !== 1) return
setWeapon(oneWeaponArray[0])
}}
autoFocus
/>
<Flex flexWrap="wrap" justifyContent="center" pt="1em">
{weapons.filter(filterWeaponArray).map(weapon => (
<PseudoBox
key={weapon}
px="3px"
py="2px"
cursor="pointer"
onClick={() => setWeapon(weapon)}
userSelect="none"
_hover={{
bg: "rgba(128, 128, 128, 0.3)",
borderRadius: "50%",
transition: "background-color .5s",
}}
>
<WeaponImage englishName={weapon} size="SMALL" />
</PseudoBox>
))}
</Flex>
<Box pb="1em">
<Input
placeholder="Filter weapons"
value={input}
onChange={handleInputChange}
w="50%"
ml="auto"
mr="auto"
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key !== "Enter") return
const oneWeaponArray = weapons.filter(filterWeaponArray)
if (oneWeaponArray.length !== 1) return
setWeapon(oneWeaponArray[0])
}}
autoFocus
/>
</Box>
<Box
as="fieldset"
border={borderStyle}
borderWidth="1px"
overflow="hidden"
borderX="none"
borderBottom="none"
>
<Box
as="legend"
color={grayWithShade}
fontWeight="semibold"
letterSpacing="wide"
fontSize="md"
textAlign="center"
px="5px"
>
Click a weapon to select it
</Box>
<Flex flexWrap="wrap" justifyContent="center" p="0.5em">
{weapons.filter(filterWeaponArray).map(weapon => (
<PseudoBox
key={weapon}
px="3px"
py="2px"
cursor="pointer"
onClick={() => setWeapon(weapon)}
userSelect="none"
_hover={{
bg: "rgba(128, 128, 128, 0.3)",
borderRadius: "50%",
transition: "background-color .5s",
}}
>
<WeaponImage englishName={weapon} size="SMALL" />
</PseudoBox>
))}
</Flex>
</Box>
</>
)
}

View File

@ -130,7 +130,9 @@ const UserPage: React.FC<RouteComponentProps & UserPageProps> = ({ id }) => {
return (
<>
<Helmet>
<title>{user.username} | sendou.ink</title>
<title>
{user.username}#{user.discriminator} | sendou.ink
</title>
</Helmet>
<AvatarWithInfo user={user} />
{user.weapons && user.weapons.length > 0 && (

View File

@ -1,8 +1,8 @@
import React, { useContext } from "react"
import React from "react"
import { Weapon } from "../../types"
import { Box, Flex } from "@chakra-ui/core"
import WeaponImage from "../common/WeaponImage"
import MyThemeContext from "../../themeContext"
import FieldsetWithLegend from "../common/FieldsetWithLegend"
interface WeaponPoolProps {
weapons: Weapon[]
@ -14,29 +14,8 @@ const styles = {
} as const
const WeaponPool: React.FC<WeaponPoolProps> = ({ weapons }) => {
const { colorMode, grayWithShade } = useContext(MyThemeContext)
const borderStyle: string = styles[colorMode]
return (
<Box
as="fieldset"
maxW="sm"
border={borderStyle}
rounded="lg"
overflow="hidden"
display="inline-block"
p="1em"
>
<Box
as="legend"
color={grayWithShade}
fontWeight="semibold"
letterSpacing="wide"
fontSize="xs"
textTransform="uppercase"
>
Weapon pool
</Box>
<FieldsetWithLegend title="WEAPON POOL" titleFontSize="xs">
<Flex>
{weapons.map(wpn => (
<Box mx="0.3em" key={wpn}>
@ -44,7 +23,7 @@ const WeaponPool: React.FC<WeaponPoolProps> = ({ weapons }) => {
</Box>
))}
</Flex>
</Box>
</FieldsetWithLegend>
)
}

View File

@ -1,8 +1,8 @@
import { gql, DocumentNode } from "apollo-boost"
export const SEARCH_FOR_BUILDS: DocumentNode = gql`
query searchForBuilds($discord_id: String!) {
searchForBuilds(discord_id: $discord_id) {
query searchForBuilds($discord_id: String, $weapon: String) {
searchForBuilds(discord_id: $discord_id, weapon: $weapon) {
id
weapon
title
@ -15,6 +15,11 @@ export const SEARCH_FOR_BUILDS: DocumentNode = gql`
shoesItem
updatedAt
top
discord_user {
username
discriminator
discord_id
}
}
}
`

View File

@ -110,6 +110,11 @@ export interface Build {
shoesItem?: ShoesGear
updatedAt: string
top: boolean
discord_user?: {
username: string
discriminator: string
discord_id: string
}
}
export interface Placement {
@ -145,7 +150,8 @@ export interface SearchForBuildsData {
}
export interface SearchForBuildsVars {
discord_id: string
discord_id?: string
weapon?: Weapon
}
export interface PlayerInfoData {

View File

@ -11,7 +11,7 @@ const gear = require("../utils/gear")
const typeDef = gql`
extend type Query {
searchForBuilds(discord_id: String!): [Build!]!
searchForBuilds(discord_id: String, weapon: String): [Build!]!
"Returns builds by given weapon. If weapon is omitted returns latest builds instead."
searchForBuildsByWeapon(weapon: String, page: Int): BuildCollection!
}
@ -93,8 +93,13 @@ const typeDef = gql`
const resolvers = {
Query: {
searchForBuilds: (root, args) => {
return Build.find({ discord_id: args.discord_id })
.sort({ weapon: "asc" })
if (!args.discord_id && !args.weapon)
throw new UserInputError(
"Discord ID or weapon has to be in the arguments"
)
return Build.find({ ...args })
.sort({ top: "desc", updatedAt: "desc" })
.populate("discord_user")
.catch(e => {
throw new UserInputError(e.message, {
invalidArgs: args,

View File

@ -37,7 +37,6 @@ const typeDef = gql`
username: String!
"Discord discriminator. For example with Sendou#0043 0043 is the discriminator."
discriminator: String!
"String that allows finding users avatar on Discord."
discord_id: String!
twitch_name: String
twitter_name: String