mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-04-24 23:19:39 -05:00
Ladder initial with FAQ (#443)
* queries for play service * seed ladder day * seed ladder registered teams * ladder day get for future * seed add ladder matches * extract RegisterTab component * MyError / MySpinner * tweaks * FAQ tab * add to faq * stats tab * tabs to search params * tabs for mobile
This commit is contained in:
parent
c78f971b91
commit
89afd0e41d
12
components/common/MyError.tsx
Normal file
12
components/common/MyError.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { Alert, AlertIcon } from "@chakra-ui/alert";
|
||||
|
||||
const MyError = ({ message }: { message: string }) => {
|
||||
return (
|
||||
<Alert status="error" rounded="lg">
|
||||
<AlertIcon />
|
||||
{message}
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyError;
|
||||
9
components/common/MySpinner.tsx
Normal file
9
components/common/MySpinner.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { Spinner } from "@chakra-ui/spinner";
|
||||
import { useMyTheme } from "hooks/common";
|
||||
|
||||
const MySpinner = () => {
|
||||
const { themeColorShade } = useMyTheme();
|
||||
return <Spinner color={themeColorShade} thickness="3px" />;
|
||||
};
|
||||
|
||||
export default MySpinner;
|
||||
80
components/play/ActiveMatchesTab.tsx
Normal file
80
components/play/ActiveMatchesTab.tsx
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { Box, Center, Flex, Grid, Heading } from "@chakra-ui/layout";
|
||||
import ModeImage from "components/common/ModeImage";
|
||||
import MyError from "components/common/MyError";
|
||||
import MySpinner from "components/common/MySpinner";
|
||||
import SubText from "components/common/SubText";
|
||||
import { useMyTheme } from "hooks/common";
|
||||
import { Fragment } from "react";
|
||||
import { hoursToMilliseconds } from "utils/numbers";
|
||||
import { trpc } from "utils/trpc";
|
||||
import MatchUp from "./MatchUp";
|
||||
|
||||
const ActiveMatchesTab = () => {
|
||||
const { gray } = useMyTheme();
|
||||
const previousLadderDayQuery = trpc.useQuery(["play.previousLadderDay"]);
|
||||
|
||||
if (previousLadderDayQuery.isLoading) return <MySpinner />;
|
||||
if (previousLadderDayQuery.error)
|
||||
return <MyError message={previousLadderDayQuery.error.message} />;
|
||||
|
||||
const previousLadderDay = previousLadderDayQuery.data!;
|
||||
|
||||
return (
|
||||
<>
|
||||
{previousLadderDay && previousLadderDay.matches.length > 0 && (
|
||||
<>
|
||||
{[1].map((round) => (
|
||||
<Fragment key={round}>
|
||||
<Heading size="md">Round {round}</Heading>
|
||||
<Box fontSize="sm" fontWeight="bold">
|
||||
{new Date(
|
||||
new Date(previousLadderDay.date).getTime() +
|
||||
hoursToMilliseconds(round - 1)
|
||||
).toLocaleString()}{" "}
|
||||
({Intl.DateTimeFormat().resolvedOptions().timeZone})
|
||||
</Box>
|
||||
<SubText mt={4}>Maplist</SubText>
|
||||
{(previousLadderDay.matches.find(
|
||||
(match) => match.order === round
|
||||
)!.maplist as any[]).map(({ stage, mode }, i) => {
|
||||
return (
|
||||
<Flex
|
||||
key={stage + mode}
|
||||
align="center"
|
||||
color={gray}
|
||||
fontSize="sm"
|
||||
my={2}
|
||||
>
|
||||
{i + 1}){" "}
|
||||
<Center mx={1}>
|
||||
<ModeImage mode={mode} size={24} />
|
||||
</Center>{" "}
|
||||
{stage}
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
<Grid
|
||||
templateColumns="3fr 1fr 3fr"
|
||||
placeItems="center"
|
||||
rowGap={4}
|
||||
mt={8}
|
||||
mb={8}
|
||||
>
|
||||
{previousLadderDay.matches
|
||||
.filter((match) => match.order === round)
|
||||
.map((match) => (
|
||||
<MatchUp
|
||||
key={match.players[0].user.discordId}
|
||||
matchUp={match}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActiveMatchesTab;
|
||||
197
components/play/FAQTab.tsx
Normal file
197
components/play/FAQTab.tsx
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
import {
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
} from "@chakra-ui/accordion";
|
||||
import { Box } from "@chakra-ui/layout";
|
||||
import MyLink from "components/common/MyLink";
|
||||
|
||||
/*
|
||||
// //"Moray Towers",
|
||||
// //"Port Mackerel",
|
||||
// //"Walleye Warehouse",
|
||||
// //"Arowana Mall",
|
||||
// //"Kelp Dome"
|
||||
*/
|
||||
|
||||
const questionsAndAnswers = [
|
||||
{
|
||||
q: "What is the schedule?",
|
||||
a: (
|
||||
<>
|
||||
During the beta testing phase follow Sendou's Twitter for updates. You
|
||||
can choose when to play - there is no commitment. <br />
|
||||
<br />
|
||||
The plan after the beta testing phase is to have 5 event dates a week
|
||||
when you can play: Monday and Wednesday Americas friendly times (8PM
|
||||
ET). Tuesday and Thursday Europe friendly times (8PM CET). Sunday for a
|
||||
time that is aimed for both regions (9PM CET).
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
q: "How many matches do we play when we register for an event?",
|
||||
a: (
|
||||
<>
|
||||
If there is an even number of teams everyone plays 2 matches. If there
|
||||
is an uneven number of teams then two teams play 1 match each and the
|
||||
rest play 2 matches.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
q:
|
||||
"Can I switch who I play with between the event dates? Can I sign up without a team?",
|
||||
a: (
|
||||
<>
|
||||
You can play with a different set of people each time you play the
|
||||
ladder if you want. However you need to find people to play with before
|
||||
signing up (so it doesn't work like solo queue in that sense).
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
q: "What this the format of the matches?",
|
||||
a: (
|
||||
<>
|
||||
Best of 9. Even numbered weeks SZ/TC/RM/CB but Splat Zones is the every
|
||||
other mode. Uneven numbered weeks SZ only.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
q: "What is the maplist used?",
|
||||
a: <>Every map except Moray, Port, Walleye, Mall & Kelp Dome is in play.</>,
|
||||
},
|
||||
{
|
||||
q: "What ranking system is used?",
|
||||
a: (
|
||||
<>
|
||||
You can read more about the ranking system used{" "}
|
||||
<MyLink
|
||||
isExternal
|
||||
href="https://www.microsoft.com/en-us/research/project/trueskill-ranking-system"
|
||||
>
|
||||
here
|
||||
</MyLink>
|
||||
.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
q: "How are the teams matched?",
|
||||
a: (
|
||||
<>
|
||||
The algorithm goes through every possible way to pair the teams that are
|
||||
signed up. It chooses the match-ups based on which two ways without
|
||||
repeat matches produce the best quality of matches. Quality of match is
|
||||
determined by the theoretical likelihood of a draw as provided by the
|
||||
ranking system we use.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
q: "Why are players ranked and not teams? What does this mean in practice?",
|
||||
a: (
|
||||
<>
|
||||
In Splatoon teams are pretty volatile. It's common for people to play
|
||||
with a lot of different people. Even in one team there is typically a
|
||||
big difference in level depending on who from the roster is playing.
|
||||
Ranking players lets us have more accurate rankings in these conditions.
|
||||
<br />
|
||||
<br />
|
||||
If you always play with the same people the ranking system won't be able
|
||||
to make a difference between your scores. The difference only comes if
|
||||
you play with different people also.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
q: "DC rules?",
|
||||
a: (
|
||||
<>
|
||||
One DC replay per team per set is allowed. If DC happens before the game
|
||||
started (counted from players being able to move) room should be remade
|
||||
without DC replays being used. DC replay is only granted if it was less
|
||||
than 2 minutes 30 seconds into the match AND the team's score without DC
|
||||
didn't pass the 50 point mark AND the team with the DC stopped playing
|
||||
without delay.
|
||||
<br />
|
||||
<br /> If host DC's during game the hosting team has to use their DC
|
||||
replay if they have any left otherwise they lose the map.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
q: "Who hosts?",
|
||||
a: (
|
||||
<>
|
||||
If you can't agree which team should host then the team who is on the
|
||||
left side chooses a player in their team to host the room.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
q: "How do I report the score?",
|
||||
a: (
|
||||
<>
|
||||
You report the score by DM'ing the bot Lanista#5266. The bot can be
|
||||
found on our{" "}
|
||||
<MyLink isExternal href="https://discord.gg/sendou">
|
||||
Discord
|
||||
</MyLink>
|
||||
. If you have not registed your Nintendo account with Lanista you have
|
||||
to use the <code>!register</code> command before. After that reporting
|
||||
the score is done with the <code>!sendoureport</code> command. <br />
|
||||
<br />
|
||||
If you have problems please contact Lean#3146 on Discord.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
q: "Can we get a sub?",
|
||||
a: (
|
||||
<>
|
||||
You need to play the sets with the 4 people you sign up with. If a
|
||||
player becomes unable to play then the rest of the matches need to be
|
||||
forfeited.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
q: "How do I report a forfeit?",
|
||||
a: (
|
||||
<>
|
||||
You can do on our{" "}
|
||||
<MyLink isExternal href="https://discord.gg/sendou">
|
||||
Discord
|
||||
</MyLink>{" "}
|
||||
on the #helpdesk channel.
|
||||
</>
|
||||
),
|
||||
},
|
||||
] as const;
|
||||
|
||||
const FAQTab = () => {
|
||||
return (
|
||||
<Accordion>
|
||||
{questionsAndAnswers.map(({ q, a }) => (
|
||||
<AccordionItem key={q}>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box flex="1" textAlign="left" fontWeight="bold">
|
||||
{q}
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel pb={4}>{a}</AccordionPanel>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
export default FAQTab;
|
||||
|
|
@ -79,7 +79,7 @@ const RegisterHeader: React.FC<Props> = ({}) => {
|
|||
const ownTeamFullyRegisted = !!ownTeam && ownTeam.roster.length >= 4;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box my={6}>
|
||||
{ownTeam ? (
|
||||
<Alert
|
||||
status={ownTeamFullyRegisted ? "success" : "warning"}
|
||||
|
|
@ -89,7 +89,7 @@ const RegisterHeader: React.FC<Props> = ({}) => {
|
|||
justifyContent="center"
|
||||
textAlign="center"
|
||||
p={6}
|
||||
mt={4}
|
||||
rounded="lg"
|
||||
>
|
||||
<AlertTitle mb={1} fontSize="lg">
|
||||
{ownTeam.roster.length >= 4 ? (
|
||||
|
|
@ -116,7 +116,7 @@ const RegisterHeader: React.FC<Props> = ({}) => {
|
|||
h="1.75rem"
|
||||
size="sm"
|
||||
isDisabled={sending || copied}
|
||||
my={2}
|
||||
my={4}
|
||||
variant="outline"
|
||||
width={36}
|
||||
>
|
||||
|
|
@ -156,12 +156,7 @@ const RegisterHeader: React.FC<Props> = ({}) => {
|
|||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={createNewTeam}
|
||||
isLoading={sending}
|
||||
>
|
||||
<Button onClick={createNewTeam} isLoading={sending}>
|
||||
<Trans>Register new team</Trans>
|
||||
</Button>
|
||||
)}
|
||||
|
|
|
|||
64
components/play/RegisterTab.tsx
Normal file
64
components/play/RegisterTab.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { Box, Heading } from "@chakra-ui/layout";
|
||||
import MyError from "components/common/MyError";
|
||||
import MySpinner from "components/common/MySpinner";
|
||||
import { trpc } from "utils/trpc";
|
||||
import LadderTeam from "./LadderTeam";
|
||||
import RegisterHeader from "./RegisterHeader";
|
||||
|
||||
const RegisterTab = () => {
|
||||
const registeredTeamsQuery = trpc.useQuery(["play.allRegisteredTeams"]);
|
||||
const nextLadderDayQuery = trpc.useQuery(["play.nextLadderDay"]);
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
return (
|
||||
<Box fontWeight="bold">
|
||||
<>
|
||||
First ladder test date is planned for 8th May. Follow Sendou's Twitter
|
||||
for updates!
|
||||
</>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (nextLadderDayQuery.isLoading) return <MySpinner />;
|
||||
if (nextLadderDayQuery.error)
|
||||
return <MyError message={nextLadderDayQuery.error.message} />;
|
||||
|
||||
const nextLadderDay = nextLadderDayQuery.data!;
|
||||
|
||||
return (
|
||||
<>
|
||||
{!nextLadderDay?.matches.length && (
|
||||
<Box fontWeight="bold">
|
||||
{nextLadderDay ? (
|
||||
<>
|
||||
Next ladder event takes place at{" "}
|
||||
{new Date(nextLadderDay.date).toLocaleString()}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Next ladder date is not confirmed. Follow this page for updates!
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
{nextLadderDay && (
|
||||
<>
|
||||
<RegisterHeader />
|
||||
<Box mt={4}>
|
||||
<Heading size="sm" as="h3">
|
||||
Registered teams
|
||||
</Heading>
|
||||
{registeredTeamsQuery.data
|
||||
?.sort((a, b) => b.roster.length - a.roster.length)
|
||||
.map((team) => (
|
||||
<LadderTeam key={team.id} roster={team.roster} my={2} />
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisterTab;
|
||||
|
|
@ -83,6 +83,11 @@ const extendedTheme = extendTheme({
|
|||
colorScheme: "theme",
|
||||
},
|
||||
},
|
||||
Tabs: {
|
||||
defaultProps: {
|
||||
colorScheme: "theme",
|
||||
},
|
||||
},
|
||||
Modal: {
|
||||
baseStyle: (props) => ({
|
||||
dialog: {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { inferAsyncReturnType, inferProcedureOutput } from "@trpc/server";
|
|||
import * as trpcNext from "@trpc/server/adapters/next";
|
||||
import calendarApi from "routers/calendar";
|
||||
import freeAgentsApi from "routers/freeagents";
|
||||
import playApi from "routers/play";
|
||||
import plusApi from "routers/plus";
|
||||
import superjson from "superjson";
|
||||
import { getMySession } from "utils/api";
|
||||
|
|
@ -22,7 +23,8 @@ export function createRouter() {
|
|||
export const appRouter = createRouter()
|
||||
.merge("plus.", plusApi)
|
||||
.merge("calendar.", calendarApi)
|
||||
.merge("freeAgents.", freeAgentsApi);
|
||||
.merge("freeAgents.", freeAgentsApi)
|
||||
.merge("play.", playApi);
|
||||
|
||||
// Exporting type _type_ AppRouter only exposes types that can be used for inference
|
||||
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export
|
||||
|
|
|
|||
|
|
@ -1,211 +1,183 @@
|
|||
import { Box, Center, Flex, Grid, Heading } from "@chakra-ui/react";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import ModeImage from "components/common/ModeImage";
|
||||
import SubText from "components/common/SubText";
|
||||
import LadderTeam from "components/play/LadderTeam";
|
||||
import MatchUp from "components/play/MatchUp";
|
||||
import RegisterHeader from "components/play/RegisterHeader";
|
||||
import { useMyTheme } from "hooks/common";
|
||||
import { useLadderTeams } from "hooks/play";
|
||||
import { GetStaticProps } from "next";
|
||||
import prisma from "prisma/client";
|
||||
import { getAllLadderRegisteredTeamsForMatches } from "prisma/queries/getAllLadderRegisteredTeamsForMatches";
|
||||
import { getLadderDay, GetLadderDayData } from "prisma/queries/getLadderDay";
|
||||
import { Fragment } from "react";
|
||||
import { shuffleArray } from "utils/arrays";
|
||||
import { getLadderRounds } from "utils/play";
|
||||
import { Tab, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react";
|
||||
import ActiveMatchesTab from "components/play/ActiveMatchesTab";
|
||||
import FAQTab from "components/play/FAQTab";
|
||||
import RegisterTab from "components/play/RegisterTab";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { setSearchParams } from "utils/setSearchParams";
|
||||
|
||||
interface Props {
|
||||
ladderDay: GetLadderDayData;
|
||||
}
|
||||
const TAB_MAX_INDEX = 5;
|
||||
|
||||
const PlayPage: React.FC<Props> = ({ ladderDay }) => {
|
||||
const { gray } = useMyTheme();
|
||||
const { data } = useLadderTeams(!ladderDay);
|
||||
const PlayPage = () => {
|
||||
const router = useRouter();
|
||||
const [tabIndex, setTabIndex] = useState(() => {
|
||||
const tab = Number(router.query.tab);
|
||||
|
||||
if (Number.isNaN(tab) || tab < 0 || tab > TAB_MAX_INDEX) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return tab;
|
||||
});
|
||||
|
||||
const handleTabIndexChange = (index: number) => {
|
||||
setSearchParams("tab", "" + index);
|
||||
setTabIndex(index);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!ladderDay?.matches.length && (
|
||||
<Box fontSize="lg" fontWeight="bold">
|
||||
{ladderDay ? (
|
||||
<>Next event: {new Date(ladderDay.date).toLocaleString()}</>
|
||||
) : (
|
||||
<>
|
||||
<Trans>
|
||||
Next ladder date is not confirmed. Follow this page for updates!
|
||||
</Trans>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
{ladderDay && ladderDay.matches.length === 0 && (
|
||||
<>
|
||||
<RegisterHeader />
|
||||
<Box mt={4}>
|
||||
{data
|
||||
?.sort((a, b) => b.roster.length - a.roster.length)
|
||||
.map((team) => (
|
||||
<LadderTeam key={team.id} roster={team.roster} my={2} />
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
{ladderDay && ladderDay.matches.length > 0 && (
|
||||
<>
|
||||
{[1, 2].map((round) => (
|
||||
<Fragment key={round}>
|
||||
<Heading size="md">Round {round}</Heading>
|
||||
<SubText mt={4}>Maplist</SubText>
|
||||
{(ladderDay.matches.find((match) => match.order === round)!
|
||||
.maplist as any[]).map(({ stage, mode }, i) => {
|
||||
return (
|
||||
<Flex
|
||||
key={stage + mode}
|
||||
align="center"
|
||||
color={gray}
|
||||
fontSize="sm"
|
||||
my={2}
|
||||
>
|
||||
{i + 1}){" "}
|
||||
<Center mx={1}>
|
||||
<ModeImage mode={mode} size={24} />
|
||||
</Center>{" "}
|
||||
{stage}
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
<Grid
|
||||
templateColumns="3fr 1fr 3fr"
|
||||
placeItems="center"
|
||||
rowGap={4}
|
||||
mt={8}
|
||||
mb={8}
|
||||
>
|
||||
{ladderDay.matches
|
||||
.filter((match) => match.order === round)
|
||||
.map((match) => (
|
||||
<MatchUp
|
||||
key={match.players[0].user.discordId}
|
||||
matchUp={match}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
<Tabs index={tabIndex} onChange={handleTabIndexChange}>
|
||||
<TabList mb="1em" flexDir={["column", null, "row"]}>
|
||||
<Tab>Register</Tab>
|
||||
<Tab isDisabled={process.env.NODE_ENV === "production"}>
|
||||
Active Matches
|
||||
</Tab>
|
||||
<Tab isDisabled>Match History</Tab>
|
||||
<Tab isDisabled>Leaderboards</Tab>
|
||||
<Tab isDisabled>Stats</Tab>
|
||||
<Tab>FAQ</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel p={1}>
|
||||
<RegisterTab />
|
||||
</TabPanel>
|
||||
<TabPanel p={1}>
|
||||
<ActiveMatchesTab />
|
||||
</TabPanel>
|
||||
<TabPanel p={1}>
|
||||
<p>three!</p>
|
||||
</TabPanel>
|
||||
<TabPanel p={1}>
|
||||
<p>four!</p>
|
||||
</TabPanel>
|
||||
<TabPanel p={1}>
|
||||
<p>Stats!</p>
|
||||
</TabPanel>
|
||||
<TabPanel p={1}>
|
||||
<FAQTab />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const getStaticProps: GetStaticProps<Props> = async () => {
|
||||
const ladderDay = await getLadderDay();
|
||||
// export const getStaticProps: GetStaticProps<Props> = async () => {
|
||||
// const [ladderDay, previousLadderDay] = await Promise.all([
|
||||
// playService.nextLadderDay(),
|
||||
// playService.previousLadderDay(),
|
||||
// ]);
|
||||
|
||||
let ladderDayAfterGeneration: GetLadderDayData | undefined;
|
||||
if (
|
||||
ladderDay &&
|
||||
!ladderDay.matches.length &&
|
||||
ladderDay.date.getTime() < new Date().getTime()
|
||||
) {
|
||||
const teams = (await getAllLadderRegisteredTeamsForMatches()).filter(
|
||||
(team) => team.roster.length === 4
|
||||
);
|
||||
// let ladderDayAfterGeneration: NextLadderDay | undefined;
|
||||
// if (
|
||||
// ladderDay &&
|
||||
// !ladderDay.matches.length &&
|
||||
// ladderDay.date.getTime() < new Date().getTime()
|
||||
// ) {
|
||||
// const teams = (await getAllLadderRegisteredTeamsForMatches()).filter(
|
||||
// (team) => team.roster.length === 4
|
||||
// );
|
||||
|
||||
if (teams.length < 4) {
|
||||
return {
|
||||
props: { ladderDay: JSON.parse(JSON.stringify(ladderDay)) },
|
||||
revalidate: 30,
|
||||
}; // FIX
|
||||
}
|
||||
// if (teams.length < 4) {
|
||||
// return {
|
||||
// props: {
|
||||
// ladderDay: JSON.parse(JSON.stringify(ladderDay)),
|
||||
// previousLadderDay,
|
||||
// },
|
||||
// revalidate: 30,
|
||||
// }; // FIX
|
||||
// }
|
||||
|
||||
const matches = getLadderRounds(teams);
|
||||
// const matches = getLadderRounds(teams);
|
||||
|
||||
const eighteenMaps = getMaplist();
|
||||
// const eighteenMaps = getMaplist();
|
||||
|
||||
const dateWeekFromNow = new Date(ladderDay.date);
|
||||
dateWeekFromNow.setHours(
|
||||
ladderDay.date.getFullYear(),
|
||||
ladderDay.date.getMonth(),
|
||||
ladderDay.date.getDate() + 7
|
||||
);
|
||||
// const dateWeekFromNow = new Date(ladderDay.date);
|
||||
// dateWeekFromNow.setHours(
|
||||
// ladderDay.date.getFullYear(),
|
||||
// ladderDay.date.getMonth(),
|
||||
// ladderDay.date.getDate() + 7
|
||||
// );
|
||||
|
||||
const createMatches = matches.flatMap((round, i) =>
|
||||
round.map((match) =>
|
||||
prisma.ladderMatch.create({
|
||||
data: {
|
||||
dayId: ladderDay.id,
|
||||
maplist:
|
||||
i === 0 ? eighteenMaps.slice(0, 9) : eighteenMaps.slice(9, 18),
|
||||
order: i + 1,
|
||||
players: {
|
||||
create: match.flatMap((team, teamI) =>
|
||||
team.roster.map((user) => ({
|
||||
userId: user.id,
|
||||
team: teamI === 0 ? "ALPHA" : "BRAVO",
|
||||
}))
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
const createLadderDay = prisma.ladderDay.create({
|
||||
data: { date: dateWeekFromNow },
|
||||
});
|
||||
const deleteLadderRegisteredTeams = prisma.ladderRegisteredTeam.deleteMany();
|
||||
// const createMatches = matches.flatMap((round, i) =>
|
||||
// round.map((match) =>
|
||||
// prisma.ladderMatch.create({
|
||||
// data: {
|
||||
// dayId: ladderDay.id,
|
||||
// maplist:
|
||||
// i === 0 ? eighteenMaps.slice(0, 9) : eighteenMaps.slice(9, 18),
|
||||
// order: i + 1,
|
||||
// players: {
|
||||
// create: match.flatMap((team, teamI) =>
|
||||
// team.roster.map((user) => ({
|
||||
// userId: user.id,
|
||||
// team: teamI === 0 ? "ALPHA" : "BRAVO",
|
||||
// }))
|
||||
// ),
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
// )
|
||||
// );
|
||||
// const createLadderDay = prisma.ladderDay.create({
|
||||
// data: { date: dateWeekFromNow },
|
||||
// });
|
||||
// const deleteLadderRegisteredTeams = prisma.ladderRegisteredTeam.deleteMany();
|
||||
|
||||
await prisma.$transaction([
|
||||
...createMatches,
|
||||
createLadderDay,
|
||||
deleteLadderRegisteredTeams,
|
||||
] as any);
|
||||
// await prisma.$transaction([
|
||||
// ...createMatches,
|
||||
// createLadderDay,
|
||||
// deleteLadderRegisteredTeams,
|
||||
// ] as any);
|
||||
|
||||
ladderDayAfterGeneration = await getLadderDay();
|
||||
}
|
||||
// ladderDayAfterGeneration = await playService.nextLadderDay();
|
||||
// }
|
||||
|
||||
return {
|
||||
props: {
|
||||
ladderDay: JSON.parse(
|
||||
JSON.stringify(ladderDayAfterGeneration ?? ladderDay)
|
||||
),
|
||||
},
|
||||
revalidate: 30,
|
||||
};
|
||||
};
|
||||
// return {
|
||||
// props: {
|
||||
// ladderDay: JSON.parse(
|
||||
// JSON.stringify(ladderDayAfterGeneration ?? ladderDay)
|
||||
// ),
|
||||
// previousLadderDay,
|
||||
// },
|
||||
// revalidate: 30,
|
||||
// };
|
||||
// };
|
||||
|
||||
function getMaplist() {
|
||||
const modes = shuffleArray(["SZ", "TC", "RM", "CB"]);
|
||||
const stages = shuffleArray([
|
||||
"The Reef",
|
||||
"Musselforge Fitness",
|
||||
"Starfish Mainstage",
|
||||
"Humpback Pump Track",
|
||||
"Inkblot Art Academy",
|
||||
"Sturgeon Shipyard",
|
||||
"Manta Maria",
|
||||
"Snapper Canal",
|
||||
"Blackbelly Skatepark",
|
||||
"MakoMart",
|
||||
"Shellendorf Institute",
|
||||
"Goby Arena",
|
||||
"Piranha Pit",
|
||||
"Camp Triggerfish",
|
||||
"Wahoo World",
|
||||
"New Albacore Hotel",
|
||||
"Ancho-V Games",
|
||||
"Skipper Pavilion",
|
||||
//"Moray Towers",
|
||||
//"Port Mackerel",
|
||||
//"Walleye Warehouse",
|
||||
//"Arowana Mall",
|
||||
//"Kelp Dome"
|
||||
]);
|
||||
// function getMaplist() {
|
||||
// const modes = shuffleArray(["SZ", "TC", "RM", "CB"]);
|
||||
// const stages = shuffleArray([
|
||||
// "The Reef",
|
||||
// "Musselforge Fitness",
|
||||
// "Starfish Mainstage",
|
||||
// "Humpback Pump Track",
|
||||
// "Inkblot Art Academy",
|
||||
// "Sturgeon Shipyard",
|
||||
// "Manta Maria",
|
||||
// "Snapper Canal",
|
||||
// "Blackbelly Skatepark",
|
||||
// "MakoMart",
|
||||
// "Shellendorf Institute",
|
||||
// "Goby Arena",
|
||||
// "Piranha Pit",
|
||||
// "Camp Triggerfish",
|
||||
// "Wahoo World",
|
||||
// "New Albacore Hotel",
|
||||
// "Ancho-V Games",
|
||||
// "Skipper Pavilion",
|
||||
// //"Moray Towers",
|
||||
// //"Port Mackerel",
|
||||
// //"Walleye Warehouse",
|
||||
// //"Arowana Mall",
|
||||
// //"Kelp Dome"
|
||||
// ]);
|
||||
|
||||
return stages.map((stage) => {
|
||||
const mode = modes.pop();
|
||||
modes.unshift(mode!);
|
||||
return { stage, mode };
|
||||
});
|
||||
}
|
||||
// return stages.map((stage) => {
|
||||
// const mode = modes.pop();
|
||||
// modes.unshift(mode!);
|
||||
// return { stage, mode };
|
||||
// });
|
||||
// }
|
||||
|
||||
export default PlayPage;
|
||||
|
|
|
|||
18
prisma/factories/ladderRegisteredTeam.ts
Normal file
18
prisma/factories/ladderRegisteredTeam.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { Prisma } from "@prisma/client";
|
||||
import { Factory } from "fishery";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import prisma from "../client";
|
||||
|
||||
export default Factory.define<Prisma.LadderRegisteredTeamCreateManyInput>(
|
||||
({ sequence, onCreate }) => {
|
||||
onCreate((data) => {
|
||||
return prisma.ladderRegisteredTeam.create({ data });
|
||||
});
|
||||
|
||||
return {
|
||||
id: sequence,
|
||||
inviteCode: uuidv4(),
|
||||
ownerId: sequence,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
import { Prisma } from "@prisma/client";
|
||||
import prisma from "prisma/client";
|
||||
|
||||
export type GetLadderDayData = Prisma.PromiseReturnType<typeof getLadderDay>;
|
||||
|
||||
export const getLadderDay = async () => {
|
||||
const d = new Date();
|
||||
d.setHours(d.getHours() - 6);
|
||||
|
||||
return prisma.ladderDay.findFirst({
|
||||
where: { date: { gte: d } },
|
||||
include: {
|
||||
matches: {
|
||||
select: {
|
||||
order: true,
|
||||
maplist: true,
|
||||
teamAScore: true,
|
||||
teamBScore: true,
|
||||
players: {
|
||||
select: {
|
||||
team: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
discordAvatar: true,
|
||||
discordId: true,
|
||||
discriminator: true,
|
||||
username: true,
|
||||
team: {
|
||||
select: {
|
||||
name: true,
|
||||
nameForUrl: true,
|
||||
twitterName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
224
prisma/seed.ts
224
prisma/seed.ts
|
|
@ -2,6 +2,7 @@ import fs from "fs";
|
|||
import path from "path";
|
||||
import prisma from "./client";
|
||||
import calendarEventFactory from "./factories/calendarEvent";
|
||||
import ladderRegisteredTeamFactory from "./factories/ladderRegisteredTeam";
|
||||
import userFactory from "./factories/user";
|
||||
import {
|
||||
getPlusStatusesData,
|
||||
|
|
@ -59,6 +60,7 @@ async function dropAllData() {
|
|||
async function seedNewData() {
|
||||
await seedUsers();
|
||||
await seedEvents();
|
||||
await seedLadderData();
|
||||
await prisma.plusStatus.createMany({ data: getPlusStatusesData() });
|
||||
await prisma.plusSuggestion.createMany({ data: getPlusSuggestionsData() });
|
||||
await prisma.plusVotingSummary.createMany({
|
||||
|
|
@ -115,6 +117,228 @@ async function seedEvents() {
|
|||
});
|
||||
}
|
||||
|
||||
async function seedLadderData() {
|
||||
const twentyFourHoursFromNow = new Date(
|
||||
new Date().getTime() + 24 * 60 * 60 * 1000
|
||||
);
|
||||
twentyFourHoursFromNow.setHours(12, 0, 0);
|
||||
await prisma.ladderDay.createMany({
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
date: new Date(twentyFourHoursFromNow.getTime() - 24 * 60 * 60 * 1000),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
date: twentyFourHoursFromNow,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await prisma.ladderRegisteredTeam.createMany({
|
||||
data: [...Array(2)].map((_, _i) => {
|
||||
return ladderRegisteredTeamFactory.build();
|
||||
}),
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
prisma.user.updateMany({
|
||||
where: { id: { in: [1, 2, 3, 4] } },
|
||||
data: { ladderTeamId: 1 },
|
||||
}),
|
||||
prisma.user.updateMany({
|
||||
where: { id: { in: [5, 6, 7] } },
|
||||
data: { ladderTeamId: 2 },
|
||||
}),
|
||||
]);
|
||||
|
||||
await prisma.ladderMatch.create({
|
||||
data: {
|
||||
id: 1,
|
||||
dayId: 1,
|
||||
maplist: [
|
||||
{
|
||||
stage: "The Reef",
|
||||
mode: "SZ",
|
||||
},
|
||||
{
|
||||
stage: "Musselforge Fitness",
|
||||
mode: "CB",
|
||||
},
|
||||
{
|
||||
stage: "Starfish Mainstage",
|
||||
mode: "SZ",
|
||||
},
|
||||
{
|
||||
stage: "Humpback Pump Track",
|
||||
mode: "TC",
|
||||
},
|
||||
{
|
||||
stage: "Inkblot Art Academy",
|
||||
mode: "SZ",
|
||||
},
|
||||
{
|
||||
stage: "Sturgeon Shipyard",
|
||||
mode: "RM",
|
||||
},
|
||||
{
|
||||
stage: "Manta Maria",
|
||||
mode: "SZ",
|
||||
},
|
||||
{
|
||||
stage: "Snapper Canal",
|
||||
mode: "CB",
|
||||
},
|
||||
{
|
||||
stage: "Blackbelly Skatepark",
|
||||
mode: "SZ",
|
||||
},
|
||||
],
|
||||
order: 1,
|
||||
teamAScore: 5,
|
||||
teamBScore: 0,
|
||||
},
|
||||
});
|
||||
|
||||
// await prisma.ladderMatch.createMany({
|
||||
// data: [
|
||||
// {
|
||||
// id: 1,
|
||||
// dayId: 1,
|
||||
// maplist: [
|
||||
// {
|
||||
// stage: "The Reef",
|
||||
// mode: "SZ",
|
||||
// },
|
||||
// {
|
||||
// stage: "Musselforge Fitness",
|
||||
// mode: "CB",
|
||||
// },
|
||||
// {
|
||||
// stage: "Starfish Mainstage",
|
||||
// mode: "SZ",
|
||||
// },
|
||||
// {
|
||||
// stage: "Humpback Pump Track",
|
||||
// mode: "TC",
|
||||
// },
|
||||
// {
|
||||
// stage: "Inkblot Art Academy",
|
||||
// mode: "SZ",
|
||||
// },
|
||||
// {
|
||||
// stage: "Sturgeon Shipyard",
|
||||
// mode: "RM",
|
||||
// },
|
||||
// {
|
||||
// stage: "Manta Maria",
|
||||
// mode: "SZ",
|
||||
// },
|
||||
// {
|
||||
// stage: "Snapper Canal",
|
||||
// mode: "CB",
|
||||
// },
|
||||
// {
|
||||
// stage: "Blackbelly Skatepark",
|
||||
// mode: "SZ",
|
||||
// },
|
||||
// ],
|
||||
// order: 1,
|
||||
// teamAScore: 5,
|
||||
// teamBScore: 0,
|
||||
// },
|
||||
// {
|
||||
// id: 2,
|
||||
// dayId: 1,
|
||||
// maplist: [
|
||||
// {
|
||||
// stage: "MakoMart",
|
||||
// mode: "SZ",
|
||||
// },
|
||||
// {
|
||||
// stage: "Shellendorf Institute",
|
||||
// mode: "TC",
|
||||
// },
|
||||
// {
|
||||
// stage: "Goby Arena",
|
||||
// mode: "SZ",
|
||||
// },
|
||||
// {
|
||||
// stage: "Piranha Pit",
|
||||
// mode: "CB",
|
||||
// },
|
||||
// {
|
||||
// stage: "Camp Triggerfish",
|
||||
// mode: "SZ",
|
||||
// },
|
||||
// {
|
||||
// stage: "Wahoo World",
|
||||
// mode: "RM",
|
||||
// },
|
||||
// {
|
||||
// stage: "New Albacore Hotel",
|
||||
// mode: "SZ",
|
||||
// },
|
||||
// {
|
||||
// stage: "Ancho-V Games",
|
||||
// mode: "TC",
|
||||
// },
|
||||
// {
|
||||
// stage: "Skipper Pavilion",
|
||||
// mode: "SZ",
|
||||
// },
|
||||
// ],
|
||||
// order: 2,
|
||||
// },
|
||||
// ],
|
||||
// });
|
||||
|
||||
await prisma.ladderMatchPlayer.createMany({
|
||||
data: [
|
||||
{
|
||||
matchId: 1,
|
||||
team: "ALPHA",
|
||||
userId: 1,
|
||||
},
|
||||
{
|
||||
matchId: 1,
|
||||
team: "ALPHA",
|
||||
userId: 2,
|
||||
},
|
||||
{
|
||||
matchId: 1,
|
||||
team: "ALPHA",
|
||||
userId: 3,
|
||||
},
|
||||
{
|
||||
matchId: 1,
|
||||
team: "ALPHA",
|
||||
userId: 4,
|
||||
},
|
||||
{
|
||||
matchId: 1,
|
||||
team: "BRAVO",
|
||||
userId: 5,
|
||||
},
|
||||
{
|
||||
matchId: 1,
|
||||
team: "BRAVO",
|
||||
userId: 6,
|
||||
},
|
||||
{
|
||||
matchId: 1,
|
||||
team: "BRAVO",
|
||||
userId: 7,
|
||||
},
|
||||
{
|
||||
matchId: 1,
|
||||
team: "BRAVO",
|
||||
userId: 8,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
|
|
|
|||
20
routers/play.ts
Normal file
20
routers/play.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { createRouter } from "pages/api/trpc/[trpc]";
|
||||
import service from "services/play";
|
||||
|
||||
const playApi = createRouter()
|
||||
.query("allRegisteredTeams", {
|
||||
resolve() {
|
||||
return service.allRegisteredTeams();
|
||||
},
|
||||
})
|
||||
.query("nextLadderDay", {
|
||||
resolve() {
|
||||
return service.nextLadderDay();
|
||||
},
|
||||
})
|
||||
.query("previousLadderDay", {
|
||||
resolve() {
|
||||
return service.previousLadderDay();
|
||||
},
|
||||
});
|
||||
export default playApi;
|
||||
126
services/play.ts
Normal file
126
services/play.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import { Prisma } from ".prisma/client";
|
||||
import prisma from "prisma/client";
|
||||
|
||||
const allRegisteredTeams = async () =>
|
||||
prisma.ladderRegisteredTeam.findMany({
|
||||
include: {
|
||||
roster: {
|
||||
select: {
|
||||
id: true,
|
||||
discordAvatar: true,
|
||||
discordId: true,
|
||||
discriminator: true,
|
||||
username: true,
|
||||
team: {
|
||||
select: {
|
||||
name: true,
|
||||
nameForUrl: true,
|
||||
twitterName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const allLadderRegisteredTeamsForMatches = async () =>
|
||||
prisma.ladderRegisteredTeam.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
roster: {
|
||||
select: {
|
||||
id: true,
|
||||
trueSkill: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export type NextLadderDay = Prisma.PromiseReturnType<typeof nextLadderDay>;
|
||||
|
||||
const nextLadderDay = async () => {
|
||||
return prisma.ladderDay.findFirst({
|
||||
where: { date: { gte: new Date() } },
|
||||
include: {
|
||||
matches: {
|
||||
select: {
|
||||
order: true,
|
||||
maplist: true,
|
||||
teamAScore: true,
|
||||
teamBScore: true,
|
||||
players: {
|
||||
select: {
|
||||
team: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
discordAvatar: true,
|
||||
discordId: true,
|
||||
discriminator: true,
|
||||
username: true,
|
||||
team: {
|
||||
select: {
|
||||
name: true,
|
||||
nameForUrl: true,
|
||||
twitterName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export type PreviousLadderDay = Prisma.PromiseReturnType<
|
||||
typeof previousLadderDay
|
||||
>;
|
||||
|
||||
const previousLadderDay = async () => {
|
||||
return prisma.ladderDay.findFirst({
|
||||
where: { date: { lt: new Date() } },
|
||||
orderBy: { date: "desc" },
|
||||
select: {
|
||||
date: true,
|
||||
matches: {
|
||||
select: {
|
||||
order: true,
|
||||
maplist: true,
|
||||
teamAScore: true,
|
||||
teamBScore: true,
|
||||
players: {
|
||||
select: {
|
||||
team: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
discordAvatar: true,
|
||||
discordId: true,
|
||||
discriminator: true,
|
||||
username: true,
|
||||
team: {
|
||||
select: {
|
||||
name: true,
|
||||
nameForUrl: true,
|
||||
twitterName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
allRegisteredTeams,
|
||||
allLadderRegisteredTeamsForMatches,
|
||||
nextLadderDay,
|
||||
previousLadderDay,
|
||||
};
|
||||
|
|
@ -1 +1,3 @@
|
|||
export const truncuateFloat = (float: number) => parseFloat(float.toFixed(1));
|
||||
|
||||
export const hoursToMilliseconds = (hours: number) => hours * 1000 * 3600;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user