diff --git a/components/builds/BuildFilters.tsx b/components/builds/BuildFilters.tsx index 8ecf50cd7..5be9ce657 100644 --- a/components/builds/BuildFilters.tsx +++ b/components/builds/BuildFilters.tsx @@ -2,125 +2,109 @@ import { Box, Button, Flex, Select } from "@chakra-ui/core"; import { t, Trans } from "@lingui/macro"; import { Ability } from "@prisma/client"; import AbilityIcon from "components/common/AbilityIcon"; +import { + BuildFilterType, + UseBuildsByWeaponDispatch, + UseBuildsByWeaponState, +} from "hooks/builds"; import { abilities, isMainAbility } from "lib/lists/abilities"; -import { Dispatch, SetStateAction } from "react"; - -const abilityPointOptions = [ - 0, - 3, - 6, - 9, - 10, - 12, - 13, - 15, - 16, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26, - 27, - 28, - 29, - 30, - 31, - 32, - 33, - 34, - 35, - 36, - 37, - 38, - 39, - 40, - 41, - 42, - 43, - 44, - 45, - 46, - 47, - 48, - 49, - 50, - 51, - 52, - 53, - 54, - 55, - 56, - 57, -]; +import { abilityPoints } from "lib/lists/abilityPoints"; interface Props { - filters: BuildFilter[]; - setFilters: Dispatch>; + filters: UseBuildsByWeaponState["filters"]; + dispatch: UseBuildsByWeaponDispatch; } -interface BuildFilter { - type: "AT_LEAST" | "AT_MOST" | "HAS" | "DOES_NOT_HAVE"; - abilityPoints: number; - ability: Ability; -} - -const BuildFilters: React.FC = ({ filters, setFilters }) => { +const BuildFilters: React.FC = ({ filters, dispatch }) => { return ( <> - {filters.map((filter, i) => ( - + {filters.map((filter, index) => ( + {isMainAbility(filter.ability) ? ( - + dispatch({ + type: "SET_FILTER_TYPE", + index, + filterType: e.target.value as BuildFilterType, + }) + } + variant="flushed" + size="sm" + width={32} + m={2} + > ) : ( <> - + dispatch({ + type: "SET_FILTER_TYPE", + index, + filterType: e.target.value as BuildFilterType, + }) + } + variant="flushed" + size="sm" + width={32} + m={2} + > - + dispatch({ + type: "SET_FILTER_ABILITY_POINTS", + index, + abilityPoints: Number(e.target.value), + }) + } + variant="flushed" + size="sm" + width={20} + m={2} + > + {abilityPoints.map((apOption) => ( + ))} )} ))} - diff --git a/hooks/builds.ts b/hooks/builds.ts new file mode 100644 index 000000000..4454f6a5c --- /dev/null +++ b/hooks/builds.ts @@ -0,0 +1,163 @@ +import { Ability } from "@prisma/client"; +import { weaponToCode } from "lib/lists/weaponCodes"; +import { GetBuildsByWeaponData } from "prisma/queries/getBuildsByWeapon"; +import { Dispatch, useReducer } from "react"; +import useSWR from "swr"; + +export type BuildFilterType = "AT_LEAST" | "AT_MOST" | "HAS" | "DOES_NOT_HAVE"; + +interface BuildFilter { + type: BuildFilterType; + abilityPoints: number; + ability: Ability; + key: number; +} + +export interface UseBuildsByWeaponState { + weapon: string; + filters: BuildFilter[]; + expandedUsers: Set; +} + +type Action = + | { + type: "SET_WEAPON"; + weapon: string; + } + | { + type: "EXPAND_USER"; + id: number; + } + | { + type: "ADD_FILTER"; + } + | { + type: "REMOVE_FILTER"; + index: number; + } + | { + type: "SET_FILTER_TYPE"; + index: number; + filterType: BuildFilter["type"]; + } + | { + type: "SET_FILTER_ABILITY_POINTS"; + index: number; + abilityPoints: number; + } + | { + type: "SET_FILTER_ABILITY"; + index: number; + ability: Ability; + }; + +export type UseBuildsByWeaponDispatch = Dispatch; + +export function useBuildsByWeapon() { + const [state, dispatch] = useReducer( + (oldState: UseBuildsByWeaponState, action: Action) => { + switch (action.type) { + case "SET_WEAPON": + return { ...oldState, weapon: action.weapon }; + case "EXPAND_USER": + return { + ...oldState, + expandedUsers: new Set([...oldState.expandedUsers, action.id]), + }; + case "ADD_FILTER": + return { + ...oldState, + filters: [ + ...oldState.filters, + { + ability: "ISM", + type: "AT_LEAST", + abilityPoints: 12, + key: new Date().getTime(), + }, + ] as BuildFilter[], + }; + case "REMOVE_FILTER": + return { + ...oldState, + filters: oldState.filters.filter((_, i) => i !== action.index), + }; + case "SET_FILTER_TYPE": + const filtersTypeCopy = [...oldState.filters]; + filtersTypeCopy[action.index] = { + ...filtersTypeCopy[action.index], + type: action.filterType, + }; + + return { + ...oldState, + filters: filtersTypeCopy, + }; + case "SET_FILTER_ABILITY": + const filtersAbilityCopy = [...oldState.filters]; + filtersAbilityCopy[action.index] = { + ...filtersAbilityCopy[action.index], + ability: action.ability, + }; + + return { + ...oldState, + filters: filtersAbilityCopy, + }; + case "SET_FILTER_ABILITY_POINTS": + const filtersAbilityPointsCopy = [...oldState.filters]; + filtersAbilityPointsCopy[action.index] = { + ...filtersAbilityPointsCopy[action.index], + abilityPoints: action.abilityPoints, + }; + + return { + ...oldState, + filters: filtersAbilityPointsCopy, + }; + default: + return oldState; + } + }, + { weapon: "", filters: [], expandedUsers: new Set() as Set } + ); + + const { data = [] } = useSWR(() => { + if (!state.weapon) return null; + + const key = state.weapon as keyof typeof weaponToCode; + return `/api/builds/${weaponToCode[key]}`; + }); + + return { + data: state.filters.length + ? data.reduce((acc: GetBuildsByWeaponData, buildArray) => { + const filteredArray = buildArray.filter((build) => { + return state.filters.every((filter) => { + // @ts-ignore + const apCount = build.abilityPoints[filter.ability] ?? 0; + + switch (filter.type) { + case "HAS": + return !!apCount; + case "DOES_NOT_HAVE": + return !apCount; + case "AT_MOST": + return apCount <= filter.abilityPoints; + case "AT_LEAST": + return apCount >= filter.abilityPoints; + default: + throw Error("Invalid filter type"); + } + }); + }); + + if (!filteredArray.length) return acc; + + return [...acc, filteredArray]; + }, []) + : data, + state, + dispatch, + }; +} diff --git a/lib/lists/abilities.ts b/lib/lists/abilities.ts index e5c0b7411..0d5838a0c 100644 --- a/lib/lists/abilities.ts +++ b/lib/lists/abilities.ts @@ -23,7 +23,7 @@ export const abilities = [ { code: "SSU", name: t`Swim Speed Up`, type: "STACKABLE" }, { code: "SCU", name: t`Special Charge Up`, type: "STACKABLE" }, { code: "SS", name: t`Special Saver`, type: "STACKABLE" }, - { code: "RSU", name: t`Special Power Up`, type: "STACKABLE" }, + { code: "SPU", name: t`Special Power Up`, type: "STACKABLE" }, { code: "QR", name: t`Quick Respawn`, type: "STACKABLE" }, { code: "QSJ", name: t`Quick Super Jump`, type: "STACKABLE" }, { code: "BRU", name: t`Sub Power Up`, type: "STACKABLE" }, diff --git a/lib/lists/abilityPoints.ts b/lib/lists/abilityPoints.ts new file mode 100644 index 000000000..b903d1a8e --- /dev/null +++ b/lib/lists/abilityPoints.ts @@ -0,0 +1,51 @@ +export const abilityPoints = [ + 0, + 3, + 6, + 9, + 10, + 12, + 13, + 15, + 16, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, +]; diff --git a/lib/types.ts b/lib/types.ts index c9746dd55..247640478 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,9 +1 @@ -export type Unwrap = T extends Promise - ? U - : T extends (...args: any) => Promise - ? U - : T extends (...args: any) => infer U - ? U - : T; - export type Unpacked = T extends (infer U)[] ? U : T; diff --git a/pages/builds/[[...slug]].tsx b/pages/builds/[[...slug]].tsx index 22b2d90df..021a9be01 100644 --- a/pages/builds/[[...slug]].tsx +++ b/pages/builds/[[...slug]].tsx @@ -1,44 +1,30 @@ import { Box, Wrap, WrapItem } from "@chakra-ui/core"; import { t } from "@lingui/macro"; -import { Ability } from "@prisma/client"; import BuildCard from "components/builds/BuildCard"; import BuildFilters from "components/builds/BuildFilters"; import Breadcrumbs from "components/common/Breadcrumbs"; import WeaponSelector from "components/common/WeaponSelector"; -import { weaponToCode } from "lib/lists/weaponCodes"; -import { GetBuildsByWeaponData } from "prisma/queries/getBuildsByWeapon"; -import { useState } from "react"; -import useSWR from "swr"; - -interface BuildFilter { - type: "AT_LEAST" | "AT_MOST" | "HAS" | "DOES_NOT_HAVE"; - abilityPoints: number; - ability: Ability; -} +import { useBuildsByWeapon } from "hooks/builds"; const BuildsPage = () => { - const [weapon, setWeapon] = useState(""); - const [expanded, setExpanded] = useState>(new Set()); - const [filters, setFilters] = useState([]); - - const { data = [] } = useSWR(() => { - if (!weapon) return null; - - const key = weapon as keyof typeof weaponToCode; - return `/api/builds/${weaponToCode[key]}`; - }); + const { data, state, dispatch } = useBuildsByWeapon(); return ( <> - + dispatch({ type: "SET_WEAPON", weapon })} + excludeAlt + isHeader + /> - + {data.flatMap((buildArray) => { const firstBuild = buildArray[0]; - if (expanded.has(firstBuild.userId)) { + if (state.expandedUsers.has(firstBuild.userId)) { return buildArray.map((build) => ( @@ -52,7 +38,7 @@ const BuildsPage = () => { build={firstBuild} otherBuildCount={buildArray.length - 1} onShowAllByUser={() => - setExpanded(new Set([...expanded, firstBuild.userId])) + dispatch({ type: "EXPAND_USER", id: firstBuild.userId }) } /> diff --git a/prisma/queries/getBuildsByWeapon.ts b/prisma/queries/getBuildsByWeapon.ts index 3f471bfe1..1437585ea 100644 --- a/prisma/queries/getBuildsByWeapon.ts +++ b/prisma/queries/getBuildsByWeapon.ts @@ -1,11 +1,8 @@ -import { PrismaClient } from "@prisma/client"; -import { Unwrap } from "lib/types"; +import { PrismaClient, PromiseReturnType } from "@prisma/client"; -export type GetBuildsByWeaponData = Unwrap< - ReturnType ->; +export type GetBuildsByWeaponData = PromiseReturnType; -type BuildsByWeapon = Unwrap>; +type BuildsByWeapon = PromiseReturnType; const getBuildsByWeaponQuery = async ({ prisma,