mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-07-19 00:57:55 -05:00
build filters useReducer
This commit is contained in:
parent
3cc97ebb76
commit
7e074d8e7a
|
|
@ -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<SetStateAction<BuildFilter[]>>;
|
||||
filters: UseBuildsByWeaponState["filters"];
|
||||
dispatch: UseBuildsByWeaponDispatch;
|
||||
}
|
||||
|
||||
interface BuildFilter {
|
||||
type: "AT_LEAST" | "AT_MOST" | "HAS" | "DOES_NOT_HAVE";
|
||||
abilityPoints: number;
|
||||
ability: Ability;
|
||||
}
|
||||
|
||||
const BuildFilters: React.FC<Props> = ({ filters, setFilters }) => {
|
||||
const BuildFilters: React.FC<Props> = ({ filters, dispatch }) => {
|
||||
return (
|
||||
<>
|
||||
{filters.map((filter, i) => (
|
||||
<Flex alignItems="center" justifyContent="center">
|
||||
{filters.map((filter, index) => (
|
||||
<Flex key={filter.key} alignItems="center" justifyContent="center">
|
||||
<Select
|
||||
value={filter.ability}
|
||||
onChange={(e) => {
|
||||
const newFilters = [...filters];
|
||||
newFilters[i] = { ...filter, ability: e.target.value as Ability };
|
||||
|
||||
setFilters(newFilters);
|
||||
}}
|
||||
onChange={(e) =>
|
||||
dispatch({
|
||||
type: "SET_FILTER_ABILITY",
|
||||
index,
|
||||
ability: e.target.value as Ability,
|
||||
})
|
||||
}
|
||||
variant="flushed"
|
||||
size="sm"
|
||||
width={48}
|
||||
m={2}
|
||||
>
|
||||
{abilities.map((ability) => (
|
||||
<option value={ability.code}>{ability.name}</option>
|
||||
<option key={ability.code} value={ability.code}>
|
||||
{ability.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<Box mx={4}>
|
||||
<AbilityIcon ability={filter.ability} size="TINY" />
|
||||
</Box>
|
||||
{isMainAbility(filter.ability) ? (
|
||||
<Select variant="flushed" size="sm" width={32} m={2}>
|
||||
<Select
|
||||
value={filter.type}
|
||||
onChange={(e) =>
|
||||
dispatch({
|
||||
type: "SET_FILTER_TYPE",
|
||||
index,
|
||||
filterType: e.target.value as BuildFilterType,
|
||||
})
|
||||
}
|
||||
variant="flushed"
|
||||
size="sm"
|
||||
width={32}
|
||||
m={2}
|
||||
>
|
||||
<option value="HAS">{t`Has`}</option>
|
||||
<option value="DOES_NOT_HAVE">{t`Doesn't have`}</option>
|
||||
</Select>
|
||||
) : (
|
||||
<>
|
||||
<Select variant="flushed" size="sm" width={32} m={2}>
|
||||
<Select
|
||||
value={filter.type}
|
||||
onChange={(e) =>
|
||||
dispatch({
|
||||
type: "SET_FILTER_TYPE",
|
||||
index,
|
||||
filterType: e.target.value as BuildFilterType,
|
||||
})
|
||||
}
|
||||
variant="flushed"
|
||||
size="sm"
|
||||
width={32}
|
||||
m={2}
|
||||
>
|
||||
<option value="AT_LEAST">{t`At least`}</option>
|
||||
<option value="AT_MOST">{t`At most`}</option>
|
||||
</Select>
|
||||
<Select variant="flushed" size="sm" width={20} m={2}>
|
||||
{abilityPointOptions.map((apOption) => (
|
||||
<option value={apOption}>{apOption} AP</option>
|
||||
<Select
|
||||
value={filter.abilityPoints}
|
||||
onChange={(e) =>
|
||||
dispatch({
|
||||
type: "SET_FILTER_ABILITY_POINTS",
|
||||
index,
|
||||
abilityPoints: Number(e.target.value),
|
||||
})
|
||||
}
|
||||
variant="flushed"
|
||||
size="sm"
|
||||
width={20}
|
||||
m={2}
|
||||
>
|
||||
{abilityPoints.map((apOption) => (
|
||||
<option key={apOption} value={apOption}>
|
||||
{apOption} AP
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
<Button
|
||||
onClick={() =>
|
||||
setFilters([
|
||||
...filters,
|
||||
{ ability: "ISM", type: "AT_LEAST", abilityPoints: 12 },
|
||||
])
|
||||
}
|
||||
>
|
||||
<Button onClick={() => dispatch({ type: "ADD_FILTER" })}>
|
||||
<Trans>Add filter</Trans>
|
||||
</Button>
|
||||
</>
|
||||
|
|
|
|||
163
hooks/builds.ts
Normal file
163
hooks/builds.ts
Normal file
|
|
@ -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<number>;
|
||||
}
|
||||
|
||||
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<Action>;
|
||||
|
||||
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<number> }
|
||||
);
|
||||
|
||||
const { data = [] } = useSWR<GetBuildsByWeaponData>(() => {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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" },
|
||||
|
|
|
|||
51
lib/lists/abilityPoints.ts
Normal file
51
lib/lists/abilityPoints.ts
Normal file
|
|
@ -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,
|
||||
];
|
||||
|
|
@ -1,9 +1 @@
|
|||
export type Unwrap<T> = T extends Promise<infer U>
|
||||
? U
|
||||
: T extends (...args: any) => Promise<infer U>
|
||||
? U
|
||||
: T extends (...args: any) => infer U
|
||||
? U
|
||||
: T;
|
||||
|
||||
export type Unpacked<T> = T extends (infer U)[] ? U : T;
|
||||
|
|
|
|||
|
|
@ -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<Set<number>>(new Set());
|
||||
const [filters, setFilters] = useState<BuildFilter[]>([]);
|
||||
|
||||
const { data = [] } = useSWR<GetBuildsByWeaponData>(() => {
|
||||
if (!weapon) return null;
|
||||
|
||||
const key = weapon as keyof typeof weaponToCode;
|
||||
return `/api/builds/${weaponToCode[key]}`;
|
||||
});
|
||||
const { data, state, dispatch } = useBuildsByWeapon();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Breadcrumbs pages={[{ name: t`Builds` }]} />
|
||||
<WeaponSelector value={weapon} onChange={setWeapon} excludeAlt isHeader />
|
||||
<WeaponSelector
|
||||
value={state.weapon}
|
||||
onChange={(weapon) => dispatch({ type: "SET_WEAPON", weapon })}
|
||||
excludeAlt
|
||||
isHeader
|
||||
/>
|
||||
<Box mt={10}>
|
||||
<BuildFilters filters={filters} setFilters={setFilters} />
|
||||
<BuildFilters filters={state.filters} dispatch={dispatch} />
|
||||
</Box>
|
||||
<Wrap pt={16} justifyContent="center" spacing={8}>
|
||||
{data.flatMap((buildArray) => {
|
||||
const firstBuild = buildArray[0];
|
||||
if (expanded.has(firstBuild.userId)) {
|
||||
if (state.expandedUsers.has(firstBuild.userId)) {
|
||||
return buildArray.map((build) => (
|
||||
<WrapItem key={build.id}>
|
||||
<BuildCard build={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 })
|
||||
}
|
||||
/>
|
||||
</WrapItem>
|
||||
|
|
|
|||
|
|
@ -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<typeof getBuildsByWeapon>
|
||||
>;
|
||||
export type GetBuildsByWeaponData = PromiseReturnType<typeof getBuildsByWeapon>;
|
||||
|
||||
type BuildsByWeapon = Unwrap<ReturnType<typeof getBuildsByWeaponQuery>>;
|
||||
type BuildsByWeapon = PromiseReturnType<typeof getBuildsByWeaponQuery>;
|
||||
|
||||
const getBuildsByWeaponQuery = async ({
|
||||
prisma,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user