Cancel match

This commit is contained in:
Kalle 2023-08-19 16:06:23 +03:00
parent 082d732b3f
commit 97aa02dc14
6 changed files with 91 additions and 51 deletions

View File

@ -1,5 +1,5 @@
import { useFetcher } from "@remix-run/react";
import React from "react";
import { type FetcherWithComponents, useFetcher } from "@remix-run/react";
import * as React from "react";
import invariant from "tiny-invariant";
import { useTranslation } from "~/hooks/useTranslation";
import { Button, type ButtonProps } from "./Button";
@ -11,19 +11,25 @@ export function FormWithConfirm({
children,
dialogHeading,
deleteButtonText,
cancelButtonText,
action,
submitButtonTestId = "submit-button",
submitButtonVariant = "destructive",
fetcher: _fetcher,
}: {
fields?: [name: string, value: string | number][];
children: React.ReactNode;
dialogHeading: string;
deleteButtonText?: string;
cancelButtonText?: string;
action?: string;
submitButtonTestId?: string;
submitButtonVariant?: ButtonProps["variant"];
fetcher?: FetcherWithComponents<any>;
}) {
const fetcher = useFetcher();
const componentsFetcher = useFetcher();
const fetcher = _fetcher ?? componentsFetcher;
const { t } = useTranslation(["common"]);
const [dialogOpen, setDialogOpen] = React.useState(false);
const formRef = React.useRef<HTMLFormElement>(null);
@ -34,6 +40,12 @@ export function FormWithConfirm({
invariant(React.isValidElement(children));
React.useEffect(() => {
if (fetcher.state === "loading") {
closeDialog();
}
}, [fetcher.state]);
return (
<>
<fetcher.Form
@ -58,7 +70,9 @@ export function FormWithConfirm({
>
{deleteButtonText ?? t("common:actions.delete")}
</SubmitButton>
<Button onClick={closeDialog}>{t("common:actions.cancel")}</Button>
<Button onClick={closeDialog}>
{cancelButtonText ?? t("common:actions.cancel")}
</Button>
</div>
</div>
</Dialog>

View File

@ -85,6 +85,14 @@ export function compareMatchToReportedScores({
const sameGroupReporting = newReporterGroupId === previousReporterGroupId;
const differentConstant = sameGroupReporting ? "FIX_PREVIOUS" : "DIFFERENT";
if (
previousReporterGroupId &&
match.mapList.filter((m) => m.winnerGroupId).length !== winners.length
) {
return differentConstant;
}
for (const [
i,
{ winnerGroupId: previousWinnerGroupId },

View File

@ -80,9 +80,10 @@ const winners = z.preprocess(
safeJSONParse,
z
.array(z.enum(["ALPHA", "BRAVO"]))
.min(Math.ceil(SENDOUQ_BEST_OF / 2))
.max(SENDOUQ_BEST_OF)
.refine((val) => {
if (val.length === 0) return true;
const matchEndedAt = matchEndedAtIndex(val);
// match did end
@ -101,9 +102,6 @@ export const matchSchema = z.union([
z.boolean().nullish().default(false)
),
}),
z.object({
_action: _action("CANCEL_MATCH"),
}),
z.object({
_action: _action("LOOK_AGAIN"),
previousGroupId: id,

View File

@ -85,5 +85,5 @@ export function winnersArrayToWinner(winners: ("ALPHA" | "BRAVO")[]) {
if (alphaCount > bravoCount) return "ALPHA";
if (bravoCount > alphaCount) return "BRAVO";
throw new Error("no winner");
return null;
}

View File

@ -121,6 +121,7 @@
padding-inline-start: var(--s-4);
font-size: var(--fonts-sm);
color: var(--text-lighter);
margin-block-start: var(--s-1);
}
.q-match__members-container {

View File

@ -151,12 +151,18 @@ export const action = async ({ request, params }: ActionArgs) => {
if (compared === "DUPLICATE") {
return null;
}
const matchIsBeingCanceled = data.winners.length === 0;
if (compared === "DIFFERENT") {
return { error: "different" as const };
return {
error: matchIsBeingCanceled
? ("cant-cancel" as const)
: ("different" as const),
};
}
const newSkills =
compared === "SAME"
compared === "SAME" && !matchIsBeingCanceled
? calculateMatchSkills({
groupMatchId: match.id,
winner: groupForMatch(winnerTeamId)!.members.map((m) => m.id),
@ -164,6 +170,9 @@ export const action = async ({ request, params }: ActionArgs) => {
})
: null;
const shouldLockMatchWithoutChangingRecords =
compared === "SAME" && matchIsBeingCanceled;
sql.transaction(() => {
reportScore({
matchId,
@ -183,6 +192,9 @@ export const action = async ({ request, params }: ActionArgs) => {
addSkills(newSkills);
cache.delete(USER_SKILLS_CACHE_KEY);
}
if (shouldLockMatchWithoutChangingRecords) {
addDummySkill(match.id);
}
// fix edge case where they 1) report score 2) report weapons 3) report score again, but with different amount of maps played
if (compared === "FIX_PREVIOUS") {
deleteReporterWeaponsByMatchId(matchId);
@ -196,28 +208,6 @@ export const action = async ({ request, params }: ActionArgs) => {
break;
}
case "CANCEL_MATCH": {
const match = notFoundIfFalsy(findMatchById(matchId));
validate(
!match.isLocked,
"Match has already been reported by both teams"
);
validate(isAdmin(user), "Only admin can cancel the match");
sql.transaction(() => {
reportScore({
matchId,
reportedByUserId: user.id,
winners: [],
});
deleteReporterWeaponsByMatchId(matchId);
setGroupAsInactive(match.alphaGroupId);
setGroupAsInactive(match.bravoGroupId);
addDummySkill(match.id);
})();
break;
}
case "LOOK_AGAIN": {
const previousGroup = groupForMatch(data.previousGroupId);
validate(previousGroup, "Previous group not found");
@ -310,6 +300,7 @@ export default function QMatchPage() {
const data = useLoaderData<typeof loader>();
const [showWeaponsForm, setShowWeaponsForm] = React.useState(false);
const submitScoreFetcher = useFetcher<typeof action>();
const cancelScoreFetcher = useFetcher<typeof action>();
React.useEffect(() => {
setShowWeaponsForm(false);
@ -396,10 +387,34 @@ export default function QMatchPage() {
</div>
{!data.match.isLocked && ownMember ? (
<div>
<div className="stack items-end">
<Link to={SENDOUQ_RULES_PAGE} className="text-xs font-bold">
<div className="stack horizontal justify-between">
<Link to={SENDOUQ_RULES_PAGE} className="text-xxs font-bold">
Read the rules
</Link>
{canReportScore && !data.match.isLocked ? (
<FormWithConfirm
dialogHeading="Cancel match? (Check rules)"
fields={[
["_action", "REPORT_SCORE"],
["winners", "[]"],
]}
deleteButtonText="Cancel"
cancelButtonText="Nevermind"
fetcher={cancelScoreFetcher}
>
<Button
className="build__small-text"
variant="minimal-destructive"
size="tiny"
type="submit"
disabled={
ownTeamReported && !data.match.mapList[0].winnerGroupId
}
>
Cancel match
</Button>
</FormWithConfirm>
) : null}
</div>
<div className="q-match__join-discord-section">
If needed, contact your opponent on the <b>#match-meetup</b>{" "}
@ -417,6 +432,11 @@ export default function QMatchPage() {
</div>
</div>
) : null}
{cancelScoreFetcher.data?.error === "cant-cancel" ? (
<div className="text-xs text-warning font-semi-bold text-center">
Opponent has already reported score for this match.
</div>
) : null}
<MapList
key={data.match.id}
canReportScore={canReportScore}
@ -432,21 +452,6 @@ export default function QMatchPage() {
) : null}
</>
) : null}
{isAdmin(user) && !data.match.isLocked ? (
<FormWithConfirm
dialogHeading={"Cancel match"}
fields={[["_action", "CANCEL_MATCH"]]}
>
<Button
className="build__small-text"
variant="minimal-destructive"
size="tiny"
type="submit"
>
Cancel match
</Button>
</FormWithConfirm>
) : null}
</Main>
);
}
@ -472,6 +477,19 @@ function Score({ reportedAt }: { reportedAt: number }) {
[0, 0]
);
if (score[0] === 0 && score[1] === 0) {
return (
<div className="stack items-center line-height-tight">
<div className="text-sm font-bold text-warning">Match canceled</div>
{!data.match.isLocked ? (
<div className="text-xs text-lighter">
Pending other team&apos;s confirmation
</div>
) : null}
</div>
);
}
return (
<div className="stack items-center line-height-tight">
<div className="text-lg font-bold">{score.join(" - ")}</div>
@ -555,7 +573,8 @@ function AfterMatchActions({
const wasReportedInTheLastWeek =
databaseTimestampToDate(reportedAt).getTime() >
Date.now() - 7 * 24 * 3600 * 1000;
const showWeaponsFormButton = wasReportedInTheLastWeek;
const showWeaponsFormButton =
wasReportedInTheLastWeek && data.match.mapList[0].winnerGroupId;
const winners = playedMaps.map((m) =>
m.winnerGroupId === data.match.alphaGroupId ? "ALPHA" : "BRAVO"