mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-28 14:20:46 -05:00
Admin can edit scores
This commit is contained in:
@@ -4,8 +4,8 @@ import clone from "just-clone";
|
||||
import { useState } from "react";
|
||||
import { Form } from "remix";
|
||||
import { scoreValid } from "~/core/play/validators";
|
||||
import { Button } from "../Button";
|
||||
import { ModeImage } from "../ModeImage";
|
||||
import { SubmitButton } from "../SubmitButton";
|
||||
|
||||
const NO_RESULT = "NO_RESULT";
|
||||
|
||||
@@ -14,14 +14,20 @@ interface MapListProps {
|
||||
name: string;
|
||||
mode: Mode;
|
||||
}[];
|
||||
reportedWinnerIds: string[];
|
||||
canSubmitScore: boolean;
|
||||
groupIds: {
|
||||
our: string;
|
||||
their: string;
|
||||
};
|
||||
}
|
||||
export function MapList({ mapList, canSubmitScore, groupIds }: MapListProps) {
|
||||
const [winners, setWinners] = useState<string[]>([]);
|
||||
export function MapList({
|
||||
mapList,
|
||||
reportedWinnerIds,
|
||||
canSubmitScore,
|
||||
groupIds,
|
||||
}: MapListProps) {
|
||||
const [winners, setWinners] = useState<string[]>(reportedWinnerIds);
|
||||
|
||||
const updateWinners = (winnerId: string, index: number) => {
|
||||
const newWinners = clone(winners);
|
||||
@@ -79,7 +85,12 @@ export function MapList({ mapList, canSubmitScore, groupIds }: MapListProps) {
|
||||
);
|
||||
})}
|
||||
{canSubmitScore && (
|
||||
<Submitter mapList={mapList} winners={winners} groupIds={groupIds} />
|
||||
<Submitter
|
||||
mapList={mapList}
|
||||
winners={winners}
|
||||
groupIds={groupIds}
|
||||
isFirstTimeReporting={reportedWinnerIds.length === 0}
|
||||
/>
|
||||
)}
|
||||
</ol>
|
||||
);
|
||||
@@ -89,6 +100,7 @@ function Submitter({
|
||||
mapList,
|
||||
winners,
|
||||
groupIds,
|
||||
isFirstTimeReporting,
|
||||
}: {
|
||||
mapList: MapListProps["mapList"];
|
||||
winners: string[];
|
||||
@@ -96,6 +108,7 @@ function Submitter({
|
||||
our: string;
|
||||
their: string;
|
||||
};
|
||||
isFirstTimeReporting: boolean;
|
||||
}) {
|
||||
const warningText = scoreValid(winners, mapList.length)
|
||||
? undefined
|
||||
@@ -119,9 +132,17 @@ function Submitter({
|
||||
<div className="play-match__score-submit-button">
|
||||
<Form method="post">
|
||||
<input type="hidden" name="winnerIds" value={JSON.stringify(winners)} />
|
||||
<Button type="submit" name="_action" value="REPORT_SCORE">
|
||||
<SubmitButton
|
||||
type="submit"
|
||||
name="_action"
|
||||
value={isFirstTimeReporting ? "REPORT_SCORE" : "EDIT_REPORTED_SCORE"}
|
||||
actionType={
|
||||
isFirstTimeReporting ? "REPORT_SCORE" : "EDIT_REPORTED_SCORE"
|
||||
}
|
||||
loadingText="Submitting..."
|
||||
>
|
||||
Submit {score.join("-")}
|
||||
</Button>
|
||||
</SubmitButton>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,8 @@ import { Ability } from "@prisma/client";
|
||||
|
||||
export const DISCORD_URL = "https://discord.gg/sendou";
|
||||
|
||||
export const ADMIN_TEST_UUID = "846e12eb-d373-4002-a0c3-e23077e1c88c";
|
||||
export const ADMIN_UUID = "ee2d82dd-624f-4b07-9d8d-ddee1f8fb36f";
|
||||
|
||||
export const ADMIN_TEST_DISCORD_ID = "79237403620945920";
|
||||
export const ADMIN_TEST_AVATAR = "fcfd65a3bea598905abb9ca25296816b";
|
||||
export const NZAP_TEST_UUID = "fab649fe-3421-46e9-88cf-d10aa2153821";
|
||||
|
||||
5
app/core/common/permissions.ts
Normal file
5
app/core/common/permissions.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { ADMIN_UUID } from "~/constants";
|
||||
|
||||
export function isAdmin(userId?: string) {
|
||||
return userId === ADMIN_UUID;
|
||||
}
|
||||
@@ -103,12 +103,7 @@ export function recentOfUser(userId: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function reportScore({
|
||||
UNSAFE_matchId,
|
||||
UNSAFE_winnerGroupIds,
|
||||
playerIds,
|
||||
groupIds,
|
||||
}: {
|
||||
interface ReportScoreArgs {
|
||||
UNSAFE_matchId: string;
|
||||
/** Group ID's in order of stages win */
|
||||
UNSAFE_winnerGroupIds: string[];
|
||||
@@ -117,7 +112,13 @@ export async function reportScore({
|
||||
losing: string[];
|
||||
};
|
||||
groupIds: string[];
|
||||
}) {
|
||||
}
|
||||
export async function reportScore({
|
||||
UNSAFE_matchId,
|
||||
UNSAFE_winnerGroupIds,
|
||||
playerIds,
|
||||
groupIds,
|
||||
}: ReportScoreArgs) {
|
||||
const allPlayerIds = [...playerIds.winning, ...playerIds.losing];
|
||||
const skills = await db.skill.findMany({
|
||||
where: { userId: { in: allPlayerIds } },
|
||||
@@ -143,8 +144,32 @@ export async function reportScore({
|
||||
status: "INACTIVE",
|
||||
},
|
||||
}),
|
||||
// https://stackoverflow.com/a/26715934
|
||||
db.$executeRawUnsafe(`
|
||||
insertScores({ UNSAFE_matchId, UNSAFE_winnerGroupIds }),
|
||||
]);
|
||||
}
|
||||
|
||||
export async function overrideScores({
|
||||
UNSAFE_matchId,
|
||||
UNSAFE_winnerGroupIds,
|
||||
}: Pick<ReportScoreArgs, "UNSAFE_matchId" | "UNSAFE_winnerGroupIds">) {
|
||||
return db.$transaction([
|
||||
db.lfgGroupMatchStage.updateMany({
|
||||
where: { lfgGroupMatchId: UNSAFE_matchId },
|
||||
data: { winnerGroupId: null },
|
||||
}),
|
||||
insertScores({
|
||||
UNSAFE_matchId,
|
||||
UNSAFE_winnerGroupIds,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
function insertScores({
|
||||
UNSAFE_matchId,
|
||||
UNSAFE_winnerGroupIds,
|
||||
}: Pick<ReportScoreArgs, "UNSAFE_matchId" | "UNSAFE_winnerGroupIds">) {
|
||||
// https://stackoverflow.com/a/26715934
|
||||
return db.$executeRawUnsafe(`
|
||||
update "LfgGroupMatchStage" as lfg set
|
||||
"winnerGroupId" = lfg2.winner_id
|
||||
from (values
|
||||
@@ -153,6 +178,5 @@ export async function reportScore({
|
||||
).join(",")}
|
||||
) as lfg2(lfg_group_match_id, "order", winner_id)
|
||||
where lfg2.lfg_group_match_id = lfg."lfgGroupMatchId" and lfg2.order = lfg.order;
|
||||
`),
|
||||
]);
|
||||
`);
|
||||
}
|
||||
|
||||
@@ -21,11 +21,13 @@ import { CheckmarkIcon } from "~/components/icons/Checkmark";
|
||||
import { ModeImage } from "~/components/ModeImage";
|
||||
import { MapList } from "~/components/play/MapList";
|
||||
import { DISCORD_URL, LFG_AMOUNT_OF_STAGES_TO_GENERATE } from "~/constants";
|
||||
import { isAdmin } from "~/core/common/permissions";
|
||||
import {
|
||||
groupsToWinningAndLosingPlayerIds,
|
||||
scoresAreIdentical,
|
||||
} from "~/core/play/utils";
|
||||
import { isGroupAdmin, matchIsUnranked } from "~/core/play/validators";
|
||||
import { useUser } from "~/hooks/common";
|
||||
import * as LFGGroup from "~/models/LFGGroup.server";
|
||||
import * as LFGMatch from "~/models/LFGMatch.server";
|
||||
import styles from "~/styles/play-match.css";
|
||||
@@ -70,7 +72,7 @@ const matchActionSchema = z.union([
|
||||
_action: z.literal("LOOK_AGAIN"),
|
||||
}),
|
||||
z.object({
|
||||
_action: z.literal("REPORT_SCORE"),
|
||||
_action: z.enum(["REPORT_SCORE", "EDIT_REPORTED_SCORE"]),
|
||||
winnerIds: z.preprocess(
|
||||
safeJSONParse,
|
||||
z
|
||||
@@ -87,7 +89,7 @@ const matchActionSchema = z.union([
|
||||
}),
|
||||
]);
|
||||
|
||||
type ActionData = {
|
||||
export type MatchActionData = {
|
||||
error?: "DIFFERENT_SCORE" | "ALREADY_IN_GROUP";
|
||||
ok?: z.infer<typeof matchActionSchema>["_action"];
|
||||
};
|
||||
@@ -96,7 +98,7 @@ export const action: ActionFunction = async ({
|
||||
request,
|
||||
context,
|
||||
params,
|
||||
}): Promise<ActionData | Response> => {
|
||||
}): Promise<MatchActionData | Response> => {
|
||||
invariant(typeof params.id === "string", "Expected params.id to be string");
|
||||
const data = await parseRequestFormData({
|
||||
request,
|
||||
@@ -143,6 +145,16 @@ export const action: ActionFunction = async ({
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "EDIT_REPORTED_SCORE": {
|
||||
validate(isAdmin(user.id), "Not admin");
|
||||
|
||||
await LFGMatch.overrideScores({
|
||||
UNSAFE_matchId: params.id,
|
||||
UNSAFE_winnerGroupIds: data.winnerIds,
|
||||
});
|
||||
|
||||
return { ok: "EDIT_REPORTED_SCORE" };
|
||||
}
|
||||
case "LOOK_AGAIN": {
|
||||
validate(matchIsUnranked(match), "Score reporting required");
|
||||
await LFGGroup.setInactive(ownGroup.id);
|
||||
@@ -268,19 +280,17 @@ export const loader: LoaderFunction = async ({ params, context }) => {
|
||||
groups,
|
||||
scores,
|
||||
createdAtTimestamp: new Date(match.createdAt).getTime(),
|
||||
mapList: match.stages
|
||||
.map(({ stage, winnerGroupId }) => {
|
||||
const winner = () => {
|
||||
if (!winnerGroupId) return undefined;
|
||||
mapList: match.stages.map(({ stage, winnerGroupId }) => {
|
||||
const winner = () => {
|
||||
if (!winnerGroupId) return undefined;
|
||||
|
||||
return groups[0].id === winnerGroupId ? 0 : 1;
|
||||
};
|
||||
return {
|
||||
...stage,
|
||||
winner: winner(),
|
||||
};
|
||||
})
|
||||
.filter((stage) => !scores || typeof stage.winner === "number"),
|
||||
return groups[0].id === winnerGroupId ? 0 : 1;
|
||||
};
|
||||
return {
|
||||
...stage,
|
||||
winner: winner(),
|
||||
};
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -288,7 +298,9 @@ export const loader: LoaderFunction = async ({ params, context }) => {
|
||||
export default function LFGMatchPage() {
|
||||
const data = useLoaderData<LFGMatchLoaderData>();
|
||||
const transition = useTransition();
|
||||
const actionData = useActionData<ActionData>();
|
||||
const actionData = useActionData<MatchActionData>();
|
||||
const user = useUser();
|
||||
const [adminEditActive, setAdminEditActive] = React.useState(false);
|
||||
|
||||
const showPlayAgainSection = () => {
|
||||
if (!data.isCaptain) return false;
|
||||
@@ -302,6 +314,12 @@ export default function LFGMatchPage() {
|
||||
return true;
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (actionData?.ok === "EDIT_REPORTED_SCORE") {
|
||||
setAdminEditActive(false);
|
||||
}
|
||||
}, [actionData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{actionData?.error === "DIFFERENT_SCORE" && (
|
||||
@@ -388,35 +406,48 @@ export default function LFGMatchPage() {
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
{data.scores && (
|
||||
{data.scores && !adminEditActive && (
|
||||
<div className="play-match__played-map-list">
|
||||
{data.mapList.map((stage) => {
|
||||
return (
|
||||
<React.Fragment key={`${stage.name}-${stage.mode}`}>
|
||||
<div
|
||||
className={clsx("play-match__checkmark", "left", {
|
||||
invisible: stage.winner !== 0,
|
||||
})}
|
||||
>
|
||||
<CheckmarkIcon />
|
||||
</div>
|
||||
<div className="play-match__played-stage">
|
||||
<ModeImage
|
||||
className="play-match__played-mode"
|
||||
mode={stage.mode}
|
||||
/>
|
||||
{stage.name}
|
||||
</div>
|
||||
<div
|
||||
className={clsx("play-match__checkmark", {
|
||||
invisible: stage.winner !== 1,
|
||||
})}
|
||||
>
|
||||
<CheckmarkIcon />
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
{data.mapList
|
||||
.filter((stage) => typeof stage.winner === "number")
|
||||
.map((stage) => {
|
||||
return (
|
||||
<React.Fragment key={`${stage.name}-${stage.mode}`}>
|
||||
<div
|
||||
className={clsx("play-match__checkmark", "left", {
|
||||
invisible: stage.winner !== 0,
|
||||
})}
|
||||
>
|
||||
<CheckmarkIcon />
|
||||
</div>
|
||||
<div className="play-match__played-stage">
|
||||
<ModeImage
|
||||
className="play-match__played-mode"
|
||||
mode={stage.mode}
|
||||
/>
|
||||
{stage.name}
|
||||
</div>
|
||||
<div
|
||||
className={clsx("play-match__checkmark", {
|
||||
invisible: stage.winner !== 1,
|
||||
})}
|
||||
>
|
||||
<CheckmarkIcon />
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{isAdmin(user?.id) && data.scores && (
|
||||
<div className="flex justify-center mt-4">
|
||||
<Button
|
||||
tiny
|
||||
variant={adminEditActive ? "destructive" : undefined}
|
||||
onClick={() => setAdminEditActive((isActive) => !isActive)}
|
||||
>
|
||||
{adminEditActive ? "Cancel" : "Edit"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -438,9 +469,16 @@ export default function LFGMatchPage() {
|
||||
</Form>
|
||||
</div>
|
||||
)}
|
||||
{!data.scores && data.isRanked && (
|
||||
{(!data.scores || adminEditActive) && data.isRanked && (
|
||||
<MapList
|
||||
mapList={data.mapList}
|
||||
reportedWinnerIds={data.mapList
|
||||
.filter((m) => typeof m.winner === "number")
|
||||
.map((m) => {
|
||||
const winnerGroup = data.groups[m.winner as number];
|
||||
invariant(winnerGroup, "Unexpected winnerGroup is undefined");
|
||||
return winnerGroup.id;
|
||||
})}
|
||||
canSubmitScore={data.isCaptain}
|
||||
groupIds={{
|
||||
our: data.groups[0].id,
|
||||
|
||||
@@ -11,7 +11,7 @@ import { SeedVariations } from "~/utils/schemas";
|
||||
import {
|
||||
ADMIN_TEST_AVATAR,
|
||||
ADMIN_TEST_DISCORD_ID,
|
||||
ADMIN_TEST_UUID,
|
||||
ADMIN_UUID,
|
||||
NZAP_TEST_AVATAR,
|
||||
NZAP_TEST_DISCORD_ID,
|
||||
NZAP_TEST_UUID,
|
||||
@@ -71,7 +71,7 @@ export async function seed(variation?: SeedVariations) {
|
||||
|
||||
const userIdsInTheSystem = (await prisma.user.findMany())
|
||||
.map((u) => u.id)
|
||||
.filter((id) => id !== ADMIN_TEST_UUID && id !== NZAP_TEST_UUID);
|
||||
.filter((id) => id !== ADMIN_UUID && id !== NZAP_TEST_UUID);
|
||||
const remainingUserIdsForGroups = await lfgGroups(userIdsInTheSystem);
|
||||
|
||||
if (variation === "match" || variation === "tournament-start") {
|
||||
@@ -88,7 +88,7 @@ export async function seed(variation?: SeedVariations) {
|
||||
async function adminUser() {
|
||||
return prisma.user.create({
|
||||
data: {
|
||||
id: ADMIN_TEST_UUID,
|
||||
id: ADMIN_UUID,
|
||||
discordDiscriminator: "4059",
|
||||
discordId: ADMIN_TEST_DISCORD_ID,
|
||||
discordName: "Sendou",
|
||||
@@ -376,7 +376,7 @@ export async function seed(variation?: SeedVariations) {
|
||||
data: {
|
||||
mu: 30 + i,
|
||||
sigma: 15 - i,
|
||||
userId: ADMIN_TEST_UUID,
|
||||
userId: ADMIN_UUID,
|
||||
createdAt: new Date(now + i * 10000),
|
||||
},
|
||||
});
|
||||
@@ -447,7 +447,7 @@ export async function seed(variation?: SeedVariations) {
|
||||
|
||||
const members = [
|
||||
{
|
||||
memberId: ADMIN_TEST_UUID,
|
||||
memberId: ADMIN_UUID,
|
||||
captain: true,
|
||||
},
|
||||
{ memberId: NZAP_TEST_UUID },
|
||||
@@ -510,6 +510,9 @@ export async function seed(variation?: SeedVariations) {
|
||||
{ order: 2, stageId: 40, winnerGroupId: PAST_OUR_GROUP_UUID },
|
||||
{ order: 3, stageId: 60, winnerGroupId: PAST_OUR_GROUP_UUID },
|
||||
{ order: 4, stageId: 101, winnerGroupId: PAST_OUR_GROUP_UUID },
|
||||
{ order: 5, stageId: 30 },
|
||||
{ order: 6, stageId: 50 },
|
||||
{ order: 7, stageId: 90 },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user