mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-22 11:05:09 -05:00
join ladder team backend frontend
This commit is contained in:
147
components/play/RegisterHeader.tsx
Normal file
147
components/play/RegisterHeader.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
Box,
|
||||
Button,
|
||||
HStack,
|
||||
Input,
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import UserAvatar from "components/common/UserAvatar";
|
||||
import { useLadderTeams } from "hooks/play";
|
||||
import { getToastOptions } from "lib/getToastOptions";
|
||||
import { sendData } from "lib/postData";
|
||||
import useUser from "lib/useUser";
|
||||
import { useState } from "react";
|
||||
import { FiTrash } from "react-icons/fi";
|
||||
|
||||
interface Props {}
|
||||
|
||||
const RegisterHeader: React.FC<Props> = ({}) => {
|
||||
const toast = useToast();
|
||||
const [user] = useUser();
|
||||
const { data, mutate } = useLadderTeams();
|
||||
|
||||
const [sending, setSending] = useState(false);
|
||||
|
||||
const createNewTeam = async () => {
|
||||
setSending(true);
|
||||
const success = await sendData("POST", "/api/play/teams");
|
||||
setSending(false);
|
||||
if (!success) return;
|
||||
|
||||
mutate();
|
||||
|
||||
toast(getToastOptions(t`Team created`, "success"));
|
||||
};
|
||||
|
||||
const deleteTeam = async () => {
|
||||
if (!window.confirm(t`Delete registration?`)) return;
|
||||
setSending(true);
|
||||
const success = await sendData("DELETE", "/api/play/teams");
|
||||
setSending(false);
|
||||
if (!success) return;
|
||||
|
||||
mutate();
|
||||
|
||||
toast(getToastOptions(t`Registration canceled`, "success"));
|
||||
};
|
||||
|
||||
const leaveTeam = async () => {
|
||||
if (!window.confirm(t`Leave team?`)) return;
|
||||
setSending(true);
|
||||
const success = await sendData("POST", "/api/play/teams/leave");
|
||||
setSending(false);
|
||||
if (!success) return;
|
||||
|
||||
mutate();
|
||||
|
||||
toast(getToastOptions(t`Team left`, "success"));
|
||||
};
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
const ownTeam = data?.find((team) =>
|
||||
team.roster.some((member) => member.id === user.id)
|
||||
);
|
||||
|
||||
const ownTeamFullyRegisted = !!ownTeam && ownTeam.roster.length >= 4;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{ownTeam ? (
|
||||
<Alert
|
||||
status={ownTeamFullyRegisted ? "success" : "warning"}
|
||||
variant="subtle"
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
textAlign="center"
|
||||
p={6}
|
||||
mt={4}
|
||||
>
|
||||
<AlertTitle mb={1} fontSize="lg">
|
||||
{ownTeam.roster.length >= 4 ? (
|
||||
<Trans>Team fully registered</Trans>
|
||||
) : (
|
||||
<Trans>Add players to complete registration</Trans>
|
||||
)}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{ownTeam.inviteCode && (
|
||||
<Box>
|
||||
<Input
|
||||
name="code"
|
||||
value={`https://sendou.ink/play/join?code=${ownTeam.inviteCode}`}
|
||||
readOnly
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<HStack my={2} justify="center">
|
||||
{ownTeam.roster.map((member) => (
|
||||
<UserAvatar user={member} />
|
||||
))}
|
||||
</HStack>
|
||||
<Box mt={4}>
|
||||
{ownTeam.ownerId === user.id ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
leftIcon={<FiTrash />}
|
||||
colorScheme="red"
|
||||
onClick={deleteTeam}
|
||||
isLoading={sending}
|
||||
>
|
||||
<Trans>Delete registration</Trans>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
colorScheme="red"
|
||||
onClick={leaveTeam}
|
||||
isLoading={sending}
|
||||
>
|
||||
<Trans>Leave team</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={createNewTeam}
|
||||
isLoading={sending}
|
||||
>
|
||||
<Trans>Register new team</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisterHeader;
|
||||
5
hooks/play.ts
Normal file
5
hooks/play.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { GetAllLadderRegisteredTeamsData } from "prisma/queries/getAllLadderRegisteredTeams";
|
||||
import useSWR from "swr";
|
||||
|
||||
export const useLadderTeams = () =>
|
||||
useSWR<GetAllLadderRegisteredTeamsData>("/api/play/teams");
|
||||
@@ -8,3 +8,4 @@ export const SALMON_RUN_ADMIN_DISCORD_IDS = [
|
||||
];
|
||||
|
||||
export const TEAM_ROSTER_LIMIT = 10;
|
||||
export const LADDER_ROSTER_LIMIT = 4;
|
||||
|
||||
68
pages/api/play/teams/index.ts
Normal file
68
pages/api/play/teams/index.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { getMySession } from "lib/getMySession";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import prisma from "prisma/client";
|
||||
import { getAllLadderRegisteredTeams } from "prisma/queries/getAllLadderRegisteredTeams";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
const teamsHandler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
switch (req.method) {
|
||||
case "GET":
|
||||
await getHandler(req, res);
|
||||
break;
|
||||
case "POST":
|
||||
await postHandler(req, res);
|
||||
break;
|
||||
case "DELETE":
|
||||
await deleteHandler(req, res);
|
||||
break;
|
||||
default:
|
||||
res.status(405).end();
|
||||
}
|
||||
};
|
||||
|
||||
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const user = await getMySession(req);
|
||||
const teams = await getAllLadderRegisteredTeams(user?.id);
|
||||
|
||||
res.status(200).json(teams);
|
||||
}
|
||||
|
||||
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const user = await getMySession(req);
|
||||
if (!user) return res.status(401).end();
|
||||
|
||||
const userFromDb = await prisma.user.findUnique({
|
||||
where: { id: user.id },
|
||||
select: { ladderTeamId: true },
|
||||
});
|
||||
|
||||
if (!userFromDb || userFromDb.ladderTeamId) {
|
||||
return res.status(400).end();
|
||||
}
|
||||
|
||||
await prisma.ladderRegisteredTeam.create({
|
||||
data: {
|
||||
inviteCode: uuidv4(),
|
||||
ownerId: user.id,
|
||||
roster: { connect: { id: user.id } },
|
||||
},
|
||||
});
|
||||
|
||||
res.status(200).end();
|
||||
}
|
||||
|
||||
async function deleteHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const user = await getMySession(req);
|
||||
if (!user) return res.status(401).end();
|
||||
|
||||
const team = await prisma.ladderRegisteredTeam.findUnique({
|
||||
where: { ownerId: user.id },
|
||||
});
|
||||
if (!team) return res.status(400).end();
|
||||
|
||||
await prisma.ladderRegisteredTeam.delete({ where: { id: team.id } });
|
||||
|
||||
res.status(200).end();
|
||||
}
|
||||
|
||||
export default teamsHandler;
|
||||
53
pages/api/play/teams/join.ts
Normal file
53
pages/api/play/teams/join.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { LADDER_ROSTER_LIMIT } from "lib/constants";
|
||||
import { getMySession } from "lib/getMySession";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import prisma from "prisma/client";
|
||||
|
||||
const joinHandler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
switch (req.method) {
|
||||
case "POST":
|
||||
await postHandler(req, res);
|
||||
break;
|
||||
default:
|
||||
res.status(405).end();
|
||||
}
|
||||
};
|
||||
|
||||
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const user = await getMySession(req);
|
||||
if (!user) return res.status(401).end();
|
||||
|
||||
const { code } = req.body;
|
||||
|
||||
if (typeof code !== "string") {
|
||||
return res.status(400).end();
|
||||
}
|
||||
|
||||
const userFromDb = await prisma.user.findUnique({ where: { id: user.id } });
|
||||
|
||||
if (userFromDb?.ladderTeamId) {
|
||||
return res.status(400).json({ message: "Already in a team" });
|
||||
}
|
||||
|
||||
const team = await prisma.ladderRegisteredTeam.findUnique({
|
||||
where: { inviteCode: code },
|
||||
include: { roster: true },
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
return res.status(400).json({ message: "Invalid invite code" });
|
||||
}
|
||||
|
||||
if (team.roster.length >= LADDER_ROSTER_LIMIT) {
|
||||
return res.status(400).json({ message: "Team already has 4 members" });
|
||||
}
|
||||
|
||||
await prisma.ladderRegisteredTeam.update({
|
||||
where: { id: team.id },
|
||||
data: { roster: { connect: { id: user.id } } },
|
||||
});
|
||||
|
||||
res.status(200).end();
|
||||
}
|
||||
|
||||
export default joinHandler;
|
||||
35
pages/api/play/teams/leave.ts
Normal file
35
pages/api/play/teams/leave.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { getMySession } from "lib/getMySession";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import prisma from "prisma/client";
|
||||
|
||||
const playLeaveHandler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
switch (req.method) {
|
||||
case "POST":
|
||||
await postHandler(req, res);
|
||||
break;
|
||||
default:
|
||||
res.status(405).end();
|
||||
}
|
||||
};
|
||||
|
||||
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const user = await getMySession(req);
|
||||
if (!user) return res.status(401).end();
|
||||
|
||||
const userFromDb = await prisma.user.findUnique({
|
||||
where: { id: user.id },
|
||||
include: { ladderTeam: true },
|
||||
});
|
||||
if (!userFromDb?.ladderTeam || userFromDb.ladderTeam.ownerId === user.id) {
|
||||
return res.status(400).end();
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { ladderTeam: { disconnect: true } },
|
||||
});
|
||||
|
||||
res.status(200).end();
|
||||
}
|
||||
|
||||
export default playLeaveHandler;
|
||||
23
pages/play/index.tsx
Normal file
23
pages/play/index.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Box } from "@chakra-ui/react";
|
||||
import { t } from "@lingui/macro";
|
||||
import Breadcrumbs from "components/common/Breadcrumbs";
|
||||
import MyContainer from "components/common/MyContainer";
|
||||
import RegisterHeader from "components/play/RegisterHeader";
|
||||
import { useLadderTeams } from "hooks/play";
|
||||
|
||||
const PlayPage = () => {
|
||||
const { data } = useLadderTeams();
|
||||
|
||||
console.log({ data });
|
||||
return (
|
||||
<MyContainer>
|
||||
<Breadcrumbs pages={[{ name: t`Play` }]} />
|
||||
<Box fontSize="lg" fontWeight="bold">
|
||||
Next event: testing
|
||||
</Box>
|
||||
<RegisterHeader />
|
||||
</MyContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlayPage;
|
||||
49
pages/play/join.tsx
Normal file
49
pages/play/join.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { useLingui } from "@lingui/react";
|
||||
import MyContainer from "components/common/MyContainer";
|
||||
import { useLadderTeams } from "hooks/play";
|
||||
import { sendData } from "lib/postData";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const statusMessage = {
|
||||
LOADING: t`Please wait...`,
|
||||
ERROR: t`There is an error that needs to be fixed before you can join the team.`,
|
||||
REDIRECTING: t`Joined the team. Redirecting.`,
|
||||
} as const;
|
||||
|
||||
const JoinLadderTeamPage = ({}) => {
|
||||
const { i18n } = useLingui();
|
||||
const router = useRouter();
|
||||
const { mutate } = useLadderTeams();
|
||||
const [joiningStatus, setJoiningStatus] = useState<
|
||||
"LOADING" | "ERROR" | "REDIRECTING"
|
||||
>("LOADING");
|
||||
|
||||
useEffect(() => {
|
||||
const joinTeam = async () => {
|
||||
const { code } = router.query;
|
||||
if (typeof code !== "string" || typeof name !== "string") {
|
||||
router.push("/404");
|
||||
}
|
||||
|
||||
const success = await sendData("POST", "/api/play/teams/join", {
|
||||
code,
|
||||
});
|
||||
if (!success) {
|
||||
return setJoiningStatus("ERROR");
|
||||
}
|
||||
|
||||
mutate();
|
||||
|
||||
setJoiningStatus("REDIRECTING");
|
||||
router.push(`/play`);
|
||||
};
|
||||
|
||||
joinTeam();
|
||||
}, []);
|
||||
|
||||
return <MyContainer>{i18n._(statusMessage[joiningStatus])}</MyContainer>;
|
||||
};
|
||||
|
||||
export default JoinLadderTeamPage;
|
||||
34
prisma/queries/getAllLadderRegisteredTeams.ts
Normal file
34
prisma/queries/getAllLadderRegisteredTeams.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import prisma from "prisma/client";
|
||||
|
||||
export type GetAllLadderRegisteredTeamsData = Prisma.PromiseReturnType<
|
||||
typeof getAllLadderRegisteredTeams
|
||||
>;
|
||||
|
||||
export const getAllLadderRegisteredTeams = async (userId?: number) => {
|
||||
const teams = await prisma.ladderRegisteredTeam.findMany({
|
||||
include: {
|
||||
roster: {
|
||||
select: {
|
||||
id: true,
|
||||
discordAvatar: true,
|
||||
discordId: true,
|
||||
discriminator: true,
|
||||
username: true,
|
||||
team: {
|
||||
select: {
|
||||
name: true,
|
||||
nameForUrl: true,
|
||||
twitterName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return teams.map((team) => ({
|
||||
...team,
|
||||
inviteCode: team.ownerId === userId ? team.inviteCode : undefined,
|
||||
}));
|
||||
};
|
||||
Reference in New Issue
Block a user