mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-26 13:22:50 -05:00
build page stats
This commit is contained in:
100
components/builds/APStats.tsx
Normal file
100
components/builds/APStats.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { Flex } from "@chakra-ui/react";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import AbilityIcon from "components/common/AbilityIcon";
|
||||
import SubText from "components/common/SubText";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "components/common/Table";
|
||||
import { mainOnlyAbilities } from "lib/lists/abilities";
|
||||
|
||||
interface Props {
|
||||
stats: {
|
||||
code: string;
|
||||
average: number;
|
||||
counts: number[][];
|
||||
}[];
|
||||
}
|
||||
|
||||
const APStats: React.FC<Props> = ({ stats }) => {
|
||||
if (Number.isNaN(stats[0].average)) return null;
|
||||
return (
|
||||
<Flex justify="space-evenly" flexWrap="wrap" mt={6}>
|
||||
<Table maxW={64}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeader>
|
||||
<Trans>Ability</Trans>
|
||||
</TableHeader>
|
||||
<TableHeader>
|
||||
<Trans>Average AP</Trans>
|
||||
</TableHeader>
|
||||
<TableHeader>
|
||||
<Trans>Popular values</Trans>
|
||||
</TableHeader>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{stats
|
||||
.filter((stat) => !mainOnlyAbilities.includes(stat.code as any))
|
||||
.map((stat) => {
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<AbilityIcon ability={stat.code} size="TINY" />
|
||||
</TableCell>
|
||||
<TableCell fontWeight="bold">
|
||||
{stat.average.toFixed(2)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{stat.counts.map((count) => {
|
||||
return (
|
||||
<Flex align="center">
|
||||
{count[0]} <SubText ml={1}>{count[1]}</SubText>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<Table maxW={64}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeader>
|
||||
<Trans>Ability</Trans>
|
||||
</TableHeader>
|
||||
<TableHeader>
|
||||
<Trans>Appearance %</Trans>
|
||||
</TableHeader>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{stats
|
||||
.filter((stat) => mainOnlyAbilities.includes(stat.code as any))
|
||||
.map((stat) => {
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<AbilityIcon ability={stat.code} size="TINY" />
|
||||
</TableCell>
|
||||
<TableCell fontWeight="bold">
|
||||
{(stat.average * 10).toFixed(2)}%
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default APStats;
|
||||
@@ -205,11 +205,55 @@ export function useBuildsByWeapon() {
|
||||
[]
|
||||
);
|
||||
|
||||
const stats = Object.entries(
|
||||
buildsToShow.reduce(
|
||||
(acc: Record<Ability, Record<number, number>>, buildArr) => {
|
||||
buildArr.forEach(({ abilityPoints }) => {
|
||||
abilities.forEach(({ code }) => {
|
||||
// @ts-ignore
|
||||
const buildAPCount = (abilityPoints[code] as number) ?? 0;
|
||||
let existingApRecord = acc[code][buildAPCount] ?? 0;
|
||||
acc[code][buildAPCount] = ++existingApRecord;
|
||||
});
|
||||
});
|
||||
|
||||
return acc;
|
||||
},
|
||||
getStatsInitialValue()
|
||||
)
|
||||
)
|
||||
.map(([abilityCode, apCounts]) => {
|
||||
return {
|
||||
code: abilityCode,
|
||||
average:
|
||||
Object.entries(apCounts).reduce(
|
||||
(acc, cur) => Number(cur[0]) * cur[1] + acc,
|
||||
0
|
||||
) / Object.values(apCounts).reduce((acc, cur) => acc + cur, 0),
|
||||
counts: Object.entries(apCounts)
|
||||
.map(([ap, count]) => [Number(ap), count])
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 3),
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.average - a.average);
|
||||
|
||||
return {
|
||||
data: buildsToShow,
|
||||
isLoading: state.weapon && !data,
|
||||
stats,
|
||||
state,
|
||||
dispatch,
|
||||
hiddenBuildCount: buildArrays.length - buildsToShow.length,
|
||||
};
|
||||
}
|
||||
|
||||
function getStatsInitialValue() {
|
||||
const result: Partial<Record<Ability, Record<number, number>>> = {};
|
||||
|
||||
abilities.forEach(({ code }) => {
|
||||
result[code] = {};
|
||||
});
|
||||
|
||||
return result as Record<Ability, Record<number, number>>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Box, Button, Flex } from "@chakra-ui/react";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import APStats from "components/builds/APStats";
|
||||
import BuildCard from "components/builds/BuildCard";
|
||||
import BuildFilters from "components/builds/BuildFilters";
|
||||
import BuildsSkeleton from "components/builds/BuildsSkeleton";
|
||||
@@ -10,11 +11,13 @@ import WeaponImage from "components/common/WeaponImage";
|
||||
import WeaponSelector from "components/common/WeaponSelector";
|
||||
import { useBuildsByWeapon } from "hooks/builds";
|
||||
import { useMyTheme, useUser } from "hooks/common";
|
||||
import { RiTShirtLine } from "react-icons/ri";
|
||||
import { useState } from "react";
|
||||
import { RiBarChart2Fill, RiTShirtLine } from "react-icons/ri";
|
||||
|
||||
const BuildsPage = () => {
|
||||
const {
|
||||
data,
|
||||
stats,
|
||||
isLoading,
|
||||
state,
|
||||
dispatch,
|
||||
@@ -22,6 +25,8 @@ const BuildsPage = () => {
|
||||
} = useBuildsByWeapon();
|
||||
const [user] = useUser();
|
||||
const { secondaryBgColor, themeColorShade } = useMyTheme();
|
||||
const [showStats, setShowStats] = useState(true);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Breadcrumbs pages={[{ name: t`Builds` }]} />
|
||||
@@ -36,20 +41,39 @@ const BuildsPage = () => {
|
||||
</Box>
|
||||
{state.weapon && (
|
||||
<>
|
||||
{user && (
|
||||
<MyLink href={`/u/${user.discordId}?build=${state.weapon}`}>
|
||||
<Button size="sm" variant="outline" leftIcon={<RiTShirtLine />}>
|
||||
<Trans>Add build</Trans>
|
||||
</Button>
|
||||
</MyLink>
|
||||
)}
|
||||
<Flex justify="space-between" mt={6}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
leftIcon={<RiBarChart2Fill />}
|
||||
onClick={() => setShowStats(!showStats)}
|
||||
>
|
||||
{showStats ? (
|
||||
<Trans>Hide Stats</Trans>
|
||||
) : (
|
||||
<Trans>Show Stats</Trans>
|
||||
)}
|
||||
</Button>
|
||||
{user && (
|
||||
<MyLink href={`/u/${user.discordId}?build=${state.weapon}`}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
leftIcon={<RiTShirtLine />}
|
||||
onClick={() => setShowStats(false)}
|
||||
>
|
||||
<Trans>Add build</Trans>
|
||||
</Button>
|
||||
</MyLink>
|
||||
)}
|
||||
</Flex>
|
||||
<Box mt={4} pr={3} mb="-5rem">
|
||||
<WeaponImage name={state.weapon} size={128} />
|
||||
</Box>
|
||||
<Flex
|
||||
justifyContent="flex-end"
|
||||
p={2}
|
||||
mb={8}
|
||||
mb={2}
|
||||
w="100%"
|
||||
bg={secondaryBgColor}
|
||||
rounded="lg"
|
||||
@@ -89,6 +113,8 @@ const BuildsPage = () => {
|
||||
<BuildFilters filters={state.filters} dispatch={dispatch} />
|
||||
)}
|
||||
|
||||
{showStats && <APStats stats={stats} />}
|
||||
|
||||
{isLoading && <BuildsSkeleton />}
|
||||
|
||||
<MyInfiniteScroller>
|
||||
|
||||
Reference in New Issue
Block a user