mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-04-17 02:35:27 -05:00
Cancel match
This commit is contained in:
parent
082d732b3f
commit
97aa02dc14
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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'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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user