mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-11 05:36:10 -05:00
Leaderboards (#466)
* initial leaderboards * end to end * has ranks * by mode * add head
This commit is contained in:
@@ -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 }) => {
|
||||
|
||||
@@ -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}
|
||||
|
||||
130
components/leaderboards/LeaderboardTable.tsx
Normal file
130
components/leaderboards/LeaderboardTable.tsx
Normal 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;
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
137
pages/leaderboards/[[...slug]].tsx
Normal file
137
pages/leaderboards/[[...slug]].tsx
Normal 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
107
services/leaderboards.ts
Normal 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,
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user