Leaderboards (#466)

* initial leaderboards

* end to end

* has ranks

* by mode

* add head
This commit is contained in:
Kalle
2021-04-30 18:57:33 +03:00
committed by GitHub
parent caeb7fcd57
commit d5766cba03
7 changed files with 381 additions and 6 deletions

View File

@@ -34,7 +34,7 @@ export default function NewTable({
if (isSmall) {
return (
<>
{data.map((row) => {
{data.map((row, i) => {
return (
<Grid
key={row.id}
@@ -45,6 +45,7 @@ export default function NewTable({
py={2}
mb={4}
templateColumns="1fr 2fr"
autoRows="1fr"
gridRowGap={1}
alignItems="center"
>
@@ -85,7 +86,7 @@ export default function NewTable({
</Tr>
</Thead>
<Tbody>
{data.map((row) => {
{data.map((row, i) => {
return (
<Tr key={row.id}>
{headers.map(({ dataKey }) => {

View File

@@ -14,7 +14,7 @@ const NavButtons = ({ onButtonClick }: { onButtonClick?: () => void }) => {
return (
<MyLink key={code} href={"/" + code} isColored={false} noUnderline>
<Flex
width="9rem"
width="9.5rem"
rounded="lg"
p={1}
m={2}

View File

@@ -0,0 +1,130 @@
import { Flex } from "@chakra-ui/layout";
import ModeImage from "components/common/ModeImage";
import MyLink from "components/common/MyLink";
import NewTable from "components/common/NewTable";
import UserAvatar from "components/common/UserAvatar";
import WeaponImage from "components/common/WeaponImage";
import { LeaderboardsPageProps } from "pages/leaderboards/[[...slug]]";
import React from "react";
const LeaderboardTable = (props: LeaderboardsPageProps) => {
switch (props.type) {
case "LEAGUE":
const addToHeaders =
props.placements[0].members.length === 4
? [
{ name: "member 3", dataKey: "member3" },
{ name: "member 4", dataKey: "member4" },
]
: [];
return (
<NewTable
smallAtPx="800"
headers={[
{ name: "power", dataKey: "leaguePower" },
{ name: "time", dataKey: "time" },
{ name: "member 1", dataKey: "member1" },
{ name: "member 2", dataKey: "member2" },
...addToHeaders,
{ name: "weapons", dataKey: "weapons" },
]}
data={props.placements.map((placement) => {
const members = placement.members.reduce(
(acc: Record<string, React.ReactNode>, member, i) => {
const name =
member.player.user?.username ?? member.player.name ?? "???";
acc[`member${i + 1}`] = (
<Flex align="center" wordBreak="break-all" maxW={36}>
{member.player.user ? (
<MyLink href={`/u/${member.player.user.discordId}`}>
<UserAvatar
user={member.player.user}
size="xs"
mr={1}
/>
</MyLink>
) : null}
<MyLink
href={`/player/${member.switchAccountId}`}
isColored={false}
>
{name}
</MyLink>
</Flex>
);
return acc;
},
{}
);
return {
id: placement.id,
leaguePower: placement.leaguePower,
time: new Date(placement.startTime).toLocaleString("en", {
month: "numeric",
year: "2-digit",
hour: "numeric",
day: "numeric",
}),
weapons: (
<Flex align="center" flexWrap="wrap">
{placement.members.map((member) => (
<WeaponImage
key={member.switchAccountId}
name={member.weapon}
size={32}
/>
))}
</Flex>
),
...members,
};
})}
/>
);
case "XPOWER_PEAK":
return (
<NewTable
headers={[
{ name: "name", dataKey: "name" },
{ name: "x power", dataKey: "xPower" },
{ name: "weapon", dataKey: "weapon" },
{ name: "mode", dataKey: "mode" },
{ name: "month", dataKey: "month" },
]}
data={props.placements.map((placement) => {
return {
id: placement.id,
name: (
<Flex align="center">
{placement.player.user ? (
<MyLink href={`/u/${placement.player.user.discordId}`}>
<UserAvatar
user={placement.player.user}
size="xs"
mr={1}
/>
</MyLink>
) : null}
<MyLink
href={`/player/${placement.switchAccountId}`}
isColored={false}
>
{placement.playerName}
</MyLink>
</Flex>
),
xPower: placement.xPower,
weapon: <WeaponImage name={placement.weapon} size={32} />,
mode: <ModeImage mode={placement.mode} size={32} />,
month: `${placement.month}/${placement.year}`,
};
})}
/>
);
default:
throw Error("invalid leaderboard type");
}
};
export default LeaderboardTable;

View File

@@ -2,13 +2,12 @@ import { Box, Flex, Heading, useColorMode } from "@chakra-ui/react";
import { Trans } from "@lingui/macro";
import MyLink from "components/common/MyLink";
import NavButtons from "components/layout/NavButtons";
import { useMyTheme, useUser } from "hooks/common";
import { useMyTheme } from "hooks/common";
import Image from "next/image";
const HomePage = () => {
const { bgColor, secondaryBgColor, gray } = useMyTheme();
const { gray } = useMyTheme();
const { colorMode } = useColorMode();
const [user] = useUser();
return (
<>

View File

@@ -0,0 +1,137 @@
import { LeagueType, RankedMode, Region } from ".prisma/client";
import { Select } from "@chakra-ui/select";
import MyHead from "components/common/MyHead";
import LeaderboardTable from "components/leaderboards/LeaderboardTable";
import { GetStaticPaths, GetStaticProps } from "next";
import { useRouter } from "next/router";
import leaderboardsService, {
Peak,
PeakByWeapon,
PeakLeague,
} from "services/leaderboards";
import { weapons } from "utils/lists/weapons";
export type LeaderboardsPageProps =
| {
placements: Peak | PeakByWeapon;
type: "XPOWER_PEAK";
}
| {
placements: PeakLeague;
type: "LEAGUE";
};
export const LeaderboardsPage = (props: LeaderboardsPageProps) => {
const router = useRouter();
const dropdownValue =
router.asPath.replace("/leaderboards", "").length === 0
? "ALL"
: decodeURI(router.asPath.split("/")[2]);
return (
<>
<MyHead title="Leaderboards" />
<Select
maxW={64}
mx="auto"
mb={6}
value={dropdownValue}
onChange={(e) => {
router.push(leaderboardTypeToHref(e.target.value));
}}
>
<option value="LEAGUE-TWIN-EU">Twin league (EU)</option>
<option value="LEAGUE-TWIN-NA">Twin league (NA)</option>
<option value="LEAGUE-TWIN-JP">Twin league (JP)</option>
<option value="LEAGUE-QUAD-EU">Quad league (EU)</option>
<option value="LEAGUE-QUAD-NA">Quad league (NA)</option>
<option value="LEAGUE-QUAD-JP">Quad league (JP)</option>
<option value="ALL">X Power - All</option>
<option value="SZ">X Power - SZ</option>
<option value="TC">X Power - TC</option>
<option value="RM">X Power - RM</option>
<option value="CB">X Power - CB</option>
{weapons.map((wpn) => {
return (
<option key={wpn} value={wpn}>
X Power - {wpn}
</option>
);
})}
</Select>
<LeaderboardTable {...props} />
</>
);
};
export const getStaticPaths: GetStaticPaths = async () => {
return { paths: [], fallback: "blocking" };
};
export const getStaticProps: GetStaticProps<LeaderboardsPageProps> = async ({
params,
}) => {
const slug = params?.slug;
if (
typeof slug === "string" ||
(typeof slug === "object" && slug.length !== 1)
)
return { notFound: true };
if (!slug) {
return {
props: {
type: "XPOWER_PEAK",
placements: await leaderboardsService.peak(),
},
};
}
if (/^(SZ|TC|RM|CB)$/.test(slug[0])) {
return {
props: {
type: "XPOWER_PEAK",
placements: await leaderboardsService.peak(slug[0] as RankedMode),
},
};
}
if (/^(LEAGUE)-(TWIN|QUAD)-(EU|NA|JP)$/.test(slug[0])) {
const [, type, region] = slug[0].split("-");
return {
props: {
type: "LEAGUE",
placements: JSON.parse(
JSON.stringify(
await leaderboardsService.peakLeague({
type: type as LeagueType,
region: region as Region,
})
)
),
},
};
}
if (!weapons.includes(slug[0] as any)) return { notFound: true };
return {
props: {
type: "XPOWER_PEAK",
placements: await leaderboardsService.peakByWeapon(slug[0]),
},
};
};
function leaderboardTypeToHref(value: string) {
switch (value) {
case "ALL":
return "/leaderboards";
default:
return `/leaderboards/${value}`;
}
}
export default LeaderboardsPage;

107
services/leaderboards.ts Normal file
View File

@@ -0,0 +1,107 @@
import { LeagueType, Prisma, RankedMode, Region } from ".prisma/client";
import prisma from "prisma/client";
import { userBasicSelection } from "utils/prisma";
export type PeakByWeapon = Prisma.PromiseReturnType<typeof peakByWeapon>;
const peakByWeapon = (weapon: string) => {
return prisma.xRankPlacement.findMany({
distinct: ["switchAccountId"],
orderBy: {
xPower: "desc",
},
where: { weapon },
select: {
id: true,
mode: true,
month: true,
playerName: true,
switchAccountId: true,
xPower: true,
year: true,
weapon: true,
player: {
select: {
user: {
select: userBasicSelection,
},
},
},
},
take: 100,
});
};
export type Peak = Prisma.PromiseReturnType<typeof peak>;
const peak = (mode?: RankedMode) => {
return prisma.xRankPlacement.findMany({
distinct: ["switchAccountId"],
orderBy: [
{
xPower: "desc",
},
{ year: "asc" },
{ month: "asc" },
],
select: {
id: true,
mode: true,
month: true,
playerName: true,
switchAccountId: true,
xPower: true,
year: true,
weapon: true,
player: {
select: {
user: {
select: userBasicSelection,
},
},
},
},
where: { mode },
take: 100,
});
};
export type PeakLeague = Prisma.PromiseReturnType<typeof peakLeague>;
const peakLeague = ({ region, type }: { region: Region; type: LeagueType }) => {
return prisma.leagueSquad.findMany({
orderBy: [
{
leaguePower: "desc",
},
{ startTime: "asc" },
],
select: {
id: true,
leaguePower: true,
startTime: true,
members: {
select: {
switchAccountId: true,
weapon: true,
player: {
select: {
name: true,
user: {
select: userBasicSelection,
},
},
},
},
},
},
where: { region, type },
take: 100,
});
};
export default {
peak,
peakByWeapon,
peakLeague,
};

View File

@@ -29,6 +29,7 @@ export const navItems: {
}[] = [
{ code: "xsearch", name: "Browser" },
{ code: "xtrends", name: "Tier List" },
{ code: "leaderboards", name: "Leaderboards" },
{
code: "sr",
name: "Salmon Run",