xrank in app

This commit is contained in:
Kalle (Sendou) 2021-03-09 11:57:42 +02:00
parent 52d7e31fd8
commit cd16d92f0e
9 changed files with 379 additions and 360 deletions

View File

@ -0,0 +1,90 @@
import {
Box,
Flex,
Popover,
PopoverArrow,
PopoverContent,
PopoverTrigger,
} from "@chakra-ui/react";
import { useLingui } from "@lingui/react";
import WeaponLineChart from "app/xrank/components/WeaponLineChart";
import SubText from "components/common/SubText";
import WeaponImage from "components/common/WeaponImage";
import { useMyTheme } from "hooks/common";
const TrendTier = ({
tier,
weapons,
getDataForChart,
mode,
}: {
tier: { label: string; criteria: number; color: string };
weapons: {
name: string;
count: number;
xPowerAverage: number;
}[];
mode: string;
getDataForChart: (weapon: string) => { count: number }[];
}) => {
const { i18n } = useLingui();
const { gray, secondaryBgColor } = useMyTheme();
if (!weapons.length) return null;
return (
<Flex key={tier.criteria} my={4}>
<Flex
flexDir="column"
w="80px"
minH="100px"
px="10px"
borderRight="5px solid"
borderColor={tier.color}
marginRight="1em"
justifyContent="center"
>
<Box fontSize="2em" fontWeight="bolder">
{tier.label}
</Box>
<Box color={gray}>
{tier.criteria === 0.002 ? ">0%" : `${tier.criteria}%`}
</Box>
</Flex>
<Flex flexDir="row" flex={1} flexWrap="wrap" alignItems="center" py="1em">
{weapons.map((weapon) => (
<Popover key={weapon.name} placement="top-start" isLazy>
<PopoverTrigger>
<Flex m={4} cursor="pointer" flexDir="column" align="center">
<WeaponImage name={weapon.name} size={64} />
<SubText mt={2}>
{weapon.count} / {weapon.xPowerAverage.toFixed(1)}
</SubText>
</Flex>
</PopoverTrigger>
<PopoverContent zIndex={4} p="0.5em" bg={secondaryBgColor}>
<PopoverArrow bg={secondaryBgColor} />
<Flex flexDir="column" alignItems="center">
<Box
as="span"
fontWeight="bolder"
fontSize="1.2em"
mb="0.5em"
textAlign="center"
>
{i18n._(weapon.name)}
<SubText>{i18n._(mode)}</SubText>
</Box>
<WeaponLineChart
getDataForChart={() => getDataForChart(weapon.name)}
/>
</Flex>
</PopoverContent>
</Popover>
))}
</Flex>
</Flex>
);
};
export default TrendTier;

View File

@ -0,0 +1,85 @@
import { Radio, RadioGroup, Select, Stack } from "@chakra-ui/react";
import { t } from "@lingui/macro";
import { RankedMode } from "@prisma/client";
import Top500Table from "app/xrank/components/Top500Table";
import HeaderBanner from "components/layout/HeaderBanner";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { Top500PlacementsByMonth } from "../service";
export interface XSearchPageProps {
placements: Top500PlacementsByMonth;
monthOptions: { label: string; value: string }[];
}
const XSearchPage = ({ placements, monthOptions }: XSearchPageProps) => {
const [variables, setVariables] = useState<{
month: number;
year: number;
mode: RankedMode;
}>({
month: Number(monthOptions[0].value.split(",")[0]),
year: Number(monthOptions[0].value.split(",")[1]),
mode: "SZ" as RankedMode,
});
const router = useRouter();
useEffect(() => {
router.replace(
`/xsearch/${variables.year}/${variables.month}/${variables.mode}`
);
}, [variables]);
//TODO: layout can be persistent between route changes
return (
<>
<Select
value={`${variables.month},${variables.year}`}
onChange={(e) => {
const [month, year] = e.target.value.split(",");
setVariables({
...variables,
month: Number(month),
year: Number(year),
});
}}
mb={4}
maxW={64}
>
{monthOptions.map((monthYear) => (
<option key={monthYear.value} value={monthYear.value}>
{monthYear.label}
</option>
))}
</Select>
<RadioGroup
value={variables.mode}
onChange={(value) =>
setVariables({ ...variables, mode: value as RankedMode })
}
mt={4}
mb={8}
>
<Stack direction="row">
<Radio value="SZ">{t`SZ`}</Radio>
<Radio value="TC">{t`TC`}</Radio>
<Radio value="RM">{t`RM`}</Radio>
<Radio value="CB">{t`CB`}</Radio>
</Stack>
</RadioGroup>
<Top500Table placements={placements} />
</>
);
};
XSearchPage.header = (
<HeaderBanner
icon="xsearch"
title="Top 500 Browser"
subtitle="History of X Rank"
/>
);
export default XSearchPage;

View File

@ -0,0 +1,135 @@
import { Box, Select } from "@chakra-ui/react";
import { Trans } from "@lingui/macro";
import ModeSelector from "components/common/ModeSelector";
import HeaderBanner from "components/layout/HeaderBanner";
import { useMyTheme } from "hooks/common";
import { useXTrends } from "hooks/xtrends";
import { XTrends } from "../service";
import TrendTier from "./TrendTier";
const tiers = [
{
label: "X",
criteria: 6,
color: "purple.700",
},
{
label: "S+",
criteria: 5,
color: "red.700",
},
{
label: "S",
criteria: 4,
color: "red.700",
},
{
label: "A+",
criteria: 3,
color: "orange.700",
},
{
label: "A",
criteria: 2,
color: "orange.700",
},
{
label: "B+",
criteria: 1.5,
color: "yellow.700",
},
{
label: "B",
criteria: 1,
color: "yellow.700",
},
{
label: "C+",
criteria: 0.4,
color: "green.700",
},
{
label: "C",
criteria: 0.002, //1 in 500
color: "green.700",
},
] as const;
export interface XTrendsPageProps {
trends: XTrends;
}
const XTrendsPage = ({ trends }: XTrendsPageProps) => {
const { gray } = useMyTheme();
const {
state,
dispatch,
weaponData,
getDataForChart,
monthOptions,
} = useXTrends(trends);
return (
<>
<Box color={gray} fontSize="sm" mb={8}>
<Trans>
Here you can find X Rank Top 500 usage tier lists. For example for a
weapon to be in the X tier it needs at least 30 placements in that
mode that month. Below the weapon count and X Power average are shown.
</Trans>
</Box>
<Select
value={`${state.month},${state.year}`}
onChange={(e) => {
const [month, year] = e.target.value.split(",");
dispatch({
type: "SET_MONTH_YEAR",
month: Number(month),
year: Number(year),
});
}}
mt={8}
mb={4}
maxW={64}
>
{monthOptions.map((monthYear) => (
<option key={monthYear.value} value={monthYear.value}>
{monthYear.label}
</option>
))}
</Select>
<ModeSelector
mode={state.mode}
setMode={(mode) => dispatch({ type: "SET_MODE", mode })}
/>
{tiers.map((tier, i) => (
<TrendTier
key={tier.label}
tier={tier}
weapons={weaponData.filter((weapon) => {
const targetCount = 500 * (tier.criteria / 100);
const previousTargetCount =
i === 0 ? Infinity : 500 * (tiers[i - 1].criteria / 100);
return (
weapon.count >= targetCount && weapon.count < previousTargetCount
);
})}
getDataForChart={getDataForChart}
mode={state.mode}
/>
))}
</>
);
};
XTrendsPage.header = (
<HeaderBanner
icon="xsearch"
title="Top 500 Trends"
subtitle="What's popular in X Rank now and before"
/>
);
export default XTrendsPage;

View File

@ -1,9 +1,9 @@
import { Prisma } from "@prisma/client";
import { Prisma, RankedMode } from "@prisma/client";
import prisma from "prisma/client";
export type GetXTrendsData = Prisma.PromiseReturnType<typeof getXTrends>;
export type XTrends = Prisma.PromiseReturnType<typeof getXTrends>;
export const getXTrends = async () => {
const getXTrends = async () => {
const placements = await prisma.xRankPlacement.findMany({
select: { mode: true, month: true, year: true, weapon: true, xPower: true },
});
@ -49,3 +49,54 @@ export const getXTrends = async () => {
return result;
};
export type Top500PlacementsByMonth = Prisma.PromiseReturnType<
typeof getTop500PlacementsByMonth
>;
const getTop500PlacementsByMonth = async ({
month,
year,
mode,
}: {
month: number;
year: number;
mode: RankedMode;
}) => {
return prisma.xRankPlacement.findMany({
where: { month, year, mode },
orderBy: { ranking: "asc" },
select: {
playerName: true,
xPower: true,
ranking: true,
switchAccountId: true,
weapon: true,
player: {
include: {
user: {
select: {
discordId: true,
discordAvatar: true,
discriminator: true,
username: true,
profile: { select: { customUrlPath: true } },
},
},
},
},
},
});
};
export type MostRecentResult = Prisma.PromiseReturnType<
typeof getMostRecentResult
>;
const getMostRecentResult = () => {
return prisma.xRankPlacement.findFirst({
orderBy: [{ year: "desc" }, { month: "desc" }],
});
};
export default { getXTrends, getTop500PlacementsByMonth, getMostRecentResult };

View File

@ -1,89 +1,13 @@
import { Radio, RadioGroup, Select, Stack } from "@chakra-ui/react";
import { t } from "@lingui/macro";
import { RankedMode } from "@prisma/client";
import HeaderBanner from "components/layout/HeaderBanner";
import Top500Table from "components/top500/Top500Table";
import XSearchPage, {
XSearchPageProps,
} from "app/xrank/components/XSearchPage";
import xRankService from "app/xrank/service";
import { GetStaticPaths, GetStaticProps } from "next";
import { useRouter } from "next/router";
import prisma from "prisma/client";
import {
getTop500PlacementsByMonth,
GetTop500PlacementsByMonthData,
} from "prisma/queries/getTop500PlacementsByMonth";
import { useEffect, useState } from "react";
import { getLocalizedMonthYearString } from "utils/strings";
interface Props {
placements: GetTop500PlacementsByMonthData;
monthOptions: { label: string; value: string }[];
}
const XSearchPage = ({ placements, monthOptions }: Props) => {
const [variables, setVariables] = useState<{
month: number;
year: number;
mode: RankedMode;
}>({
month: Number(monthOptions[0].value.split(",")[0]),
year: Number(monthOptions[0].value.split(",")[1]),
mode: "SZ" as RankedMode,
});
const router = useRouter();
useEffect(() => {
router.replace(
`/xsearch/${variables.year}/${variables.month}/${variables.mode}`
);
}, [variables]);
//TODO: layout can be persistent between route changes
return (
<>
<Select
value={`${variables.month},${variables.year}`}
onChange={(e) => {
const [month, year] = e.target.value.split(",");
setVariables({
...variables,
month: Number(month),
year: Number(year),
});
}}
mb={4}
maxW={64}
>
{monthOptions.map((monthYear) => (
<option key={monthYear.value} value={monthYear.value}>
{monthYear.label}
</option>
))}
</Select>
<RadioGroup
value={variables.mode}
onChange={(value) =>
setVariables({ ...variables, mode: value as RankedMode })
}
mt={4}
mb={8}
>
<Stack direction="row">
<Radio value="SZ">{t`SZ`}</Radio>
<Radio value="TC">{t`TC`}</Radio>
<Radio value="RM">{t`RM`}</Radio>
<Radio value="CB">{t`CB`}</Radio>
</Stack>
</RadioGroup>
<Top500Table placements={placements} />
</>
);
};
export const getStaticPaths: GetStaticPaths = async () => {
const mostRecentResult = await prisma.xRankPlacement.findFirst({
orderBy: [{ year: "desc" }, { month: "desc" }],
});
const mostRecentResult = await xRankService.getMostRecentResult();
if (!mostRecentResult) return { paths: [], fallback: false };
@ -120,10 +44,10 @@ export const getStaticPaths: GetStaticPaths = async () => {
};
};
export const getStaticProps: GetStaticProps<Props> = async ({ params }) => {
const mostRecentResult = await prisma.xRankPlacement.findFirst({
orderBy: [{ year: "desc" }, { month: "desc" }],
});
export const getStaticProps: GetStaticProps<XSearchPageProps> = async ({
params,
}) => {
const mostRecentResult = await xRankService.getMostRecentResult();
if (!mostRecentResult) throw Error("No X Rank Placements");
@ -138,7 +62,7 @@ export const getStaticProps: GetStaticProps<Props> = async ({ params }) => {
if (isNaN(month) || isNaN(year)) return { notFound: true };
const placements = await getTop500PlacementsByMonth({
const placements = await xRankService.getTop500PlacementsByMonth({
month,
year,
mode: slug[2] as RankedMode,
@ -185,12 +109,4 @@ export function getMonthOptions(latestMonth: number, latestYear: number) {
return monthChoices;
}
XSearchPage.header = (
<HeaderBanner
icon="xsearch"
title="Top 500 Browser"
subtitle="History of X Rank"
/>
);
export default XSearchPage;

View File

@ -1,228 +1,11 @@
import {
Box,
Flex,
Popover,
PopoverArrow,
PopoverContent,
PopoverTrigger,
Select,
} from "@chakra-ui/react";
import { Trans } from "@lingui/macro";
import { useLingui } from "@lingui/react";
import ModeSelector from "components/common/ModeSelector";
import SubText from "components/common/SubText";
import WeaponImage from "components/common/WeaponImage";
import HeaderBanner from "components/layout/HeaderBanner";
import WeaponLineChart from "components/top500/WeaponLineChart";
import { useMyTheme } from "hooks/common";
import { useXTrends } from "hooks/xtrends";
import { XTrendsPageProps } from "app/xrank/components/XTrendsPage";
import xRankService from "app/xrank/service";
import { GetStaticProps } from "next";
import { getXTrends, GetXTrendsData } from "prisma/queries/getXTrends";
const tiers = [
{
label: "X",
criteria: 6,
color: "purple.700",
},
{
label: "S+",
criteria: 5,
color: "red.700",
},
{
label: "S",
criteria: 4,
color: "red.700",
},
{
label: "A+",
criteria: 3,
color: "orange.700",
},
{
label: "A",
criteria: 2,
color: "orange.700",
},
{
label: "B+",
criteria: 1.5,
color: "yellow.700",
},
{
label: "B",
criteria: 1,
color: "yellow.700",
},
{
label: "C+",
criteria: 0.4,
color: "green.700",
},
{
label: "C",
criteria: 0.002, //1 in 500
color: "green.700",
},
] as const;
export { default as default } from "app/xrank/components/XTrendsPage";
interface Props {
trends: GetXTrendsData;
}
const XTrendsPage = ({ trends }: { trends: GetXTrendsData }) => {
const { gray } = useMyTheme();
const {
state,
dispatch,
weaponData,
getDataForChart,
monthOptions,
} = useXTrends(trends);
return (
<>
<Box color={gray} fontSize="sm" mb={8}>
<Trans>
Here you can find X Rank Top 500 usage tier lists. For example for a
weapon to be in the X tier it needs at least 30 placements in that
mode that month. Below the weapon count and X Power average are shown.
</Trans>
</Box>
<Select
value={`${state.month},${state.year}`}
onChange={(e) => {
const [month, year] = e.target.value.split(",");
dispatch({
type: "SET_MONTH_YEAR",
month: Number(month),
year: Number(year),
});
}}
mt={8}
mb={4}
maxW={64}
>
{monthOptions.map((monthYear) => (
<option key={monthYear.value} value={monthYear.value}>
{monthYear.label}
</option>
))}
</Select>
<ModeSelector
mode={state.mode}
setMode={(mode) => dispatch({ type: "SET_MODE", mode })}
/>
{tiers.map((tier, i) => (
<Tier
key={tier.label}
tier={tier}
weapons={weaponData.filter((weapon) => {
const targetCount = 500 * (tier.criteria / 100);
const previousTargetCount =
i === 0 ? Infinity : 500 * (tiers[i - 1].criteria / 100);
return (
weapon.count >= targetCount && weapon.count < previousTargetCount
);
})}
getDataForChart={getDataForChart}
mode={state.mode}
/>
))}
</>
);
};
function Tier({
tier,
weapons,
getDataForChart,
mode,
}: {
tier: { label: string; criteria: number; color: string };
weapons: {
name: string;
count: number;
xPowerAverage: number;
}[];
mode: string;
getDataForChart: (weapon: string) => { count: number }[];
}) {
const { i18n } = useLingui();
const { gray, secondaryBgColor } = useMyTheme();
if (!weapons.length) return null;
return (
<Flex key={tier.criteria} my={4}>
<Flex
flexDir="column"
w="80px"
minH="100px"
px="10px"
borderRight="5px solid"
borderColor={tier.color}
marginRight="1em"
justifyContent="center"
>
<Box fontSize="2em" fontWeight="bolder">
{tier.label}
</Box>
<Box color={gray}>
{tier.criteria === 0.002 ? ">0%" : `${tier.criteria}%`}
</Box>
</Flex>
<Flex flexDir="row" flex={1} flexWrap="wrap" alignItems="center" py="1em">
{weapons.map((weapon) => (
<Popover key={weapon.name} placement="top-start" isLazy>
<PopoverTrigger>
<Flex m={4} cursor="pointer" flexDir="column" align="center">
<WeaponImage name={weapon.name} size={64} />
<SubText mt={2}>
{weapon.count} / {weapon.xPowerAverage.toFixed(1)}
</SubText>
</Flex>
</PopoverTrigger>
<PopoverContent zIndex={4} p="0.5em" bg={secondaryBgColor}>
<PopoverArrow bg={secondaryBgColor} />
<Flex flexDir="column" alignItems="center">
<Box
as="span"
fontWeight="bolder"
fontSize="1.2em"
mb="0.5em"
textAlign="center"
>
{i18n._(weapon.name)}
<SubText>{i18n._(mode)}</SubText>
</Box>
<WeaponLineChart
getDataForChart={() => getDataForChart(weapon.name)}
/>
</Flex>
</PopoverContent>
</Popover>
))}
</Flex>
</Flex>
);
}
export const getStaticProps: GetStaticProps<Props> = async () => {
const trends = await getXTrends();
export const getStaticProps: GetStaticProps<XTrendsPageProps> = async () => {
const trends = await xRankService.getXTrends();
return { props: { trends } };
};
XTrendsPage.header = (
<HeaderBanner
icon="xsearch"
title="Top 500 Trends"
subtitle="What's popular in X Rank now and before"
/>
);
export default XTrendsPage;

View File

@ -1,41 +0,0 @@
import { Prisma, RankedMode } from "@prisma/client";
import prisma from "prisma/client";
export type GetTop500PlacementsByMonthData = Prisma.PromiseReturnType<
typeof getTop500PlacementsByMonth
>;
export const getTop500PlacementsByMonth = async ({
month,
year,
mode,
}: {
month: number;
year: number;
mode: RankedMode;
}) => {
return prisma.xRankPlacement.findMany({
where: { month, year, mode },
orderBy: { ranking: "asc" },
select: {
playerName: true,
xPower: true,
ranking: true,
switchAccountId: true,
weapon: true,
player: {
include: {
user: {
select: {
discordId: true,
discordAvatar: true,
discriminator: true,
username: true,
profile: { select: { customUrlPath: true } },
},
},
},
},
},
});
};