Replace migrate group solution

This commit is contained in:
Kalle 2026-05-01 17:08:24 +03:00
parent c802faf151
commit 072fdca641
23 changed files with 72 additions and 35 deletions

View File

@ -266,7 +266,6 @@ function ModeOnlyGrid({
);
}
// xxx: maybe we should just have a shared custom component for stage image + label
function StageTile({
option,
type,

View File

@ -1,9 +1,10 @@
import { Check, Clock, X } from "lucide-react";
import { Check, Clock, RotateCcw, X } from "lucide-react";
import { useTranslation } from "react-i18next";
import type { FetcherWithComponents } from "react-router";
import { type FetcherWithComponents, Link } from "react-router";
import { Avatar } from "~/components/Avatar";
import { SendouButton } from "~/components/elements/Button";
import { FormWithConfirm } from "~/components/FormWithConfirm";
import { SENDOUQ_LOOKING_PAGE } from "~/utils/urls";
import * as RejoinVote from "../core/RejoinVote";
import styles from "./RematchVotePanel.module.css";
@ -37,10 +38,18 @@ export function RematchVotePanel({
members.map((m) => m.id),
).length;
const voteResolved = RejoinVote.result(votes).type === "RESOLVED";
const viewerVotedYes =
RejoinVote.userContinueStatus(votes, viewerUserId) === true;
const viewerVotedNo =
RejoinVote.userContinueStatus(votes, viewerUserId) === false;
return (
<div className={styles.root}>
<div className={styles.prompt}>
{t("q:match.rematch.prompt", { count: currentRoundSize })}
{voteResolved
? t("q:match.rematch.resolved", { count: currentRoundSize })
: t("q:match.rematch.prompt", { count: currentRoundSize })}
</div>
<ul className={styles.list}>
{members.map((member) => {
@ -54,7 +63,15 @@ export function RematchVotePanel({
);
})}
</ul>
{RejoinVote.userContinueStatus(votes, viewerUserId) === false ? null : (
{voteResolved && viewerVotedYes ? (
<div className={styles.buttons}>
<Link to={SENDOUQ_LOOKING_PAGE}>
<SendouButton variant="primary" size="small" icon={<RotateCcw />}>
{t("q:match.rematch.backToQueue")}
</SendouButton>
</Link>
</div>
) : viewerVotedNo ? null : (
<div className={styles.buttons}>
<FormWithConfirm
fields={[
@ -76,10 +93,7 @@ export function RematchVotePanel({
<SendouButton
variant="primary"
size="small"
isDisabled={
isPending ||
RejoinVote.userContinueStatus(votes, viewerUserId) === true
}
isDisabled={isPending || viewerVotedYes}
onPress={() =>
fetcher.submit(
{

View File

@ -210,10 +210,14 @@ function RequeueTab({
reporterSide !== null &&
reporterSide !== viewerSide;
const showTimeline = !data.match.isLocked;
return (
<SendouTabPanel id={TAB_KEYS.ACTION}>
{isStaffOnly || !viewerGroup || !user ? (
<MatchTimeline compact teams={teams} score={score} maps={maps} />
showTimeline ? (
<MatchTimeline compact teams={teams} score={score} maps={maps} />
) : null
) : (
<div className={styles.rematchContent}>
{viewerGroup.matchmade ? (
@ -234,7 +238,9 @@ function RequeueTab({
) : null}
{isOnReporterTeam ? <hr className={styles.divider} /> : null}
<MatchTimeline compact teams={teams} score={score} maps={maps} />
{showTimeline ? (
<MatchTimeline compact teams={teams} score={score} maps={maps} />
) : null}
{isOnConfirmerTeam ? <ScoreConfirmerSection data={data} /> : null}
{isOnReporterTeam ? <ReporterUndoSection /> : null}
<WeaponReportSection data={data} viewerUserId={user.id} />

View File

@ -4,18 +4,17 @@ import { MatchJoinTab } from "~/components/match-page/MatchJoinTab";
import { MatchResultTab } from "~/components/match-page/MatchResultTab";
import { MatchRosterTab } from "~/components/match-page/MatchRosterTab";
import { MatchTabs } from "~/components/match-page/MatchTabs";
import { Redirect } from "~/components/Redirect";
import { useUser } from "~/features/auth/core/user";
import {
resolveActiveRoomLink,
useConfirmRoom,
} from "~/features/chat/room-link-utils";
import { DISPLAY_VOTE_RESULT_SECONDS } from "~/features/sendouq/q-constants";
import { ACTION_TAB_AFTER_LOCKED_SECONDS } from "~/features/sendouq/q-constants";
import { resolveRoomPass } from "~/features/tournament-match/tournament-match-utils";
import { useHasRole } from "~/modules/permissions/hooks";
import { databaseTimestampNow } from "~/utils/dates";
import { safeNumberParse } from "~/utils/number";
import { SENDOUQ_LOOKING_PAGE, sendouQMatchPage, teamPage } from "~/utils/urls";
import { sendouQMatchPage, teamPage } from "~/utils/urls";
import {
resolveTimelineMaps,
resolveTimelineSpChanges,
@ -57,17 +56,11 @@ export function SendouQMatchTabs({ data }: { data: SendouQMatchLoaderData }) {
const isCanceled = data.match.isCanceled;
const isParticipant = Boolean(userSide);
const migrated = data.migratedToGroupId != null && isParticipant;
// xxx: hmm is this really correct?
if (migrated) {
return <Redirect to={SENDOUQ_LOOKING_PAGE} />;
}
const lockedVoteVisible =
const lockedActionTabVisible =
data.match.confirmedAt !== null &&
databaseTimestampNow() <
data.match.confirmedAt + DISPLAY_VOTE_RESULT_SECONDS;
data.match.confirmedAt + ACTION_TAB_AFTER_LOCKED_SECONDS;
const matchInProgress = !isLocked && !awaitingConfirmation && currentMap;
@ -76,7 +69,7 @@ export function SendouQMatchTabs({ data }: { data: SendouQMatchLoaderData }) {
!isCanceled &&
(matchInProgress ||
awaitingConfirmation ||
(isLocked && lockedVoteVisible));
(isLocked && lockedActionTabVisible));
const hasReportedMaps = data.match.mapList.some(
(m) => m.winnerGroupId !== null,

View File

@ -40,18 +40,8 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
const match = SendouQ.mapMatch(matchUnmapped, user, privateNotes);
const ownCurrentGroup = user ? SendouQ.findOwnGroup(user.id) : undefined;
const migratedToGroupId =
ownCurrentGroup &&
ownCurrentGroup.id !== match.groupAlpha.id &&
ownCurrentGroup.id !== match.groupBravo.id
? ownCurrentGroup.id
: null;
return {
match,
// xxx: i don't think this is the correct idea
migratedToGroupId,
roomLinks,
anyUserPrefersNoSplatnet,
reportedWeapons,

View File

@ -14,7 +14,7 @@ export const FULL_GROUP_SIZE = 4;
export const SENDOUQ_BEST_OF = 7;
export const DISPLAY_VOTE_RESULT_SECONDS = 3 * 60 * 60;
export const ACTION_TAB_AFTER_LOCKED_SECONDS = 24 * 60 * 60; // 24 hours
export const JOIN_CODE_SEARCH_PARAM_KEY = "join";

View File

@ -91,7 +91,10 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
: null,
weapons:
info === "weapons"
? await ReportedWeaponRepository.seasonReportedWeaponsByUserId({ season, userId: user.id })
? await ReportedWeaponRepository.seasonReportedWeaponsByUserId({
season,
userId: user.id,
})
: null,
players:
info === "enemies" || info === "mates"

View File

@ -210,6 +210,7 @@
"match.confirmScore": "",
"match.confirmScore.wrongHint": "",
"match.rematch.prompt": "",
"match.rematch.resolved": "",
"match.rematch.vote.yes": "",
"match.rematch.vote.no": "",
"match.rematch.vote.noConfirm": "",
@ -217,6 +218,7 @@
"match.rematch.fizzled": "",
"match.rematch.waitingCaptain": "",
"match.rematch.rejoinQueue": "",
"match.rematch.backToQueue": "",
"preparing.joinQ": "",
"tiers.currentCriteria": "",
"tiers.info.p1": "",

View File

@ -210,6 +210,7 @@
"match.confirmScore": "",
"match.confirmScore.wrongHint": "",
"match.rematch.prompt": "",
"match.rematch.resolved": "",
"match.rematch.vote.yes": "",
"match.rematch.vote.no": "",
"match.rematch.vote.noConfirm": "",
@ -217,6 +218,7 @@
"match.rematch.fizzled": "",
"match.rematch.waitingCaptain": "",
"match.rematch.rejoinQueue": "",
"match.rematch.backToQueue": "",
"preparing.joinQ": "",
"tiers.currentCriteria": "",
"tiers.info.p1": "",

View File

@ -210,6 +210,7 @@
"match.confirmScore": "Confirm score",
"match.confirmScore.wrongHint": "Wrong score? Ask the other team to undo their report and adjust it.",
"match.rematch.prompt": "Continue queueing with the group of {{count}}?",
"match.rematch.resolved": "New group of {{count}} formed",
"match.rematch.vote.yes": "Yes, continue",
"match.rematch.vote.no": "No, I'm done",
"match.rematch.vote.noConfirm": "Vote no? You can't change your vote afterwards.",
@ -217,6 +218,7 @@
"match.rematch.fizzled": "Nobody wanted to continue",
"match.rematch.waitingCaptain": "Waiting for the captain to choose whether to re-queue",
"match.rematch.rejoinQueue": "Rejoin queue",
"match.rematch.backToQueue": "Back to queue",
"preparing.joinQ": "Join the queue",
"tiers.currentCriteria": "Current criteria",
"tiers.info.p1": "For example, Leviathan is the top 5% of players. Diamond is the 85th percentile etc.",

View File

@ -210,6 +210,7 @@
"match.confirmScore": "",
"match.confirmScore.wrongHint": "",
"match.rematch.prompt": "",
"match.rematch.resolved": "",
"match.rematch.vote.yes": "",
"match.rematch.vote.no": "",
"match.rematch.vote.noConfirm": "",
@ -217,6 +218,7 @@
"match.rematch.fizzled": "",
"match.rematch.waitingCaptain": "",
"match.rematch.rejoinQueue": "",
"match.rematch.backToQueue": "",
"preparing.joinQ": "Unirte a la fila",
"tiers.currentCriteria": "Criterios actuales",
"tiers.info.p1": "Por ejemplo, Leviathan se encuentra entre el 5% de los mejores jugadores. Diamond es el percentil 85, etc.",

View File

@ -210,6 +210,7 @@
"match.confirmScore": "",
"match.confirmScore.wrongHint": "",
"match.rematch.prompt": "",
"match.rematch.resolved": "",
"match.rematch.vote.yes": "",
"match.rematch.vote.no": "",
"match.rematch.vote.noConfirm": "",
@ -217,6 +218,7 @@
"match.rematch.fizzled": "",
"match.rematch.waitingCaptain": "",
"match.rematch.rejoinQueue": "",
"match.rematch.backToQueue": "",
"preparing.joinQ": "Unirte a la fila",
"tiers.currentCriteria": "Criterios actuales",
"tiers.info.p1": "Por ejemplo, Leviathan se encuentra entre el 5% de los mejores jugadores. Diamond es el percentil 85, etc.",

View File

@ -210,6 +210,7 @@
"match.confirmScore": "",
"match.confirmScore.wrongHint": "",
"match.rematch.prompt": "",
"match.rematch.resolved": "",
"match.rematch.vote.yes": "",
"match.rematch.vote.no": "",
"match.rematch.vote.noConfirm": "",
@ -217,6 +218,7 @@
"match.rematch.fizzled": "",
"match.rematch.waitingCaptain": "",
"match.rematch.rejoinQueue": "",
"match.rematch.backToQueue": "",
"preparing.joinQ": "",
"tiers.currentCriteria": "",
"tiers.info.p1": "",

View File

@ -210,6 +210,7 @@
"match.confirmScore": "",
"match.confirmScore.wrongHint": "",
"match.rematch.prompt": "",
"match.rematch.resolved": "",
"match.rematch.vote.yes": "",
"match.rematch.vote.no": "",
"match.rematch.vote.noConfirm": "",
@ -217,6 +218,7 @@
"match.rematch.fizzled": "",
"match.rematch.waitingCaptain": "",
"match.rematch.rejoinQueue": "",
"match.rematch.backToQueue": "",
"preparing.joinQ": "Rejoindre la queue",
"tiers.currentCriteria": "Critères actuels",
"tiers.info.p1": "Par exemple, Les Léviathans font partie des 5 % des meilleurs joueurs. Le diamant est le top 15%, etc.",

View File

@ -210,6 +210,7 @@
"match.confirmScore": "",
"match.confirmScore.wrongHint": "",
"match.rematch.prompt": "",
"match.rematch.resolved": "",
"match.rematch.vote.yes": "",
"match.rematch.vote.no": "",
"match.rematch.vote.noConfirm": "",
@ -217,6 +218,7 @@
"match.rematch.fizzled": "",
"match.rematch.waitingCaptain": "",
"match.rematch.rejoinQueue": "",
"match.rematch.backToQueue": "",
"preparing.joinQ": "",
"tiers.currentCriteria": "",
"tiers.info.p1": "",

View File

@ -210,6 +210,7 @@
"match.confirmScore": "",
"match.confirmScore.wrongHint": "",
"match.rematch.prompt": "",
"match.rematch.resolved": "",
"match.rematch.vote.yes": "",
"match.rematch.vote.no": "",
"match.rematch.vote.noConfirm": "",
@ -217,6 +218,7 @@
"match.rematch.fizzled": "",
"match.rematch.waitingCaptain": "",
"match.rematch.rejoinQueue": "",
"match.rematch.backToQueue": "",
"preparing.joinQ": "Unisciti alla coda",
"tiers.currentCriteria": "Criterio corrente",
"tiers.info.p1": "Per esempio Leviathan è la top 5% dei giocatori. Diamante è l' 85esimo percentile etc.",

View File

@ -210,6 +210,7 @@
"match.confirmScore": "",
"match.confirmScore.wrongHint": "",
"match.rematch.prompt": "",
"match.rematch.resolved": "",
"match.rematch.vote.yes": "",
"match.rematch.vote.no": "",
"match.rematch.vote.noConfirm": "",
@ -217,6 +218,7 @@
"match.rematch.fizzled": "",
"match.rematch.waitingCaptain": "",
"match.rematch.rejoinQueue": "",
"match.rematch.backToQueue": "",
"preparing.joinQ": "列に入る",
"tiers.currentCriteria": "現在の基準",
"tiers.info.p1": "例として、Leviathanはプレイヤーの上位5%、Diamondは上位15%",

View File

@ -210,6 +210,7 @@
"match.confirmScore": "",
"match.confirmScore.wrongHint": "",
"match.rematch.prompt": "",
"match.rematch.resolved": "",
"match.rematch.vote.yes": "",
"match.rematch.vote.no": "",
"match.rematch.vote.noConfirm": "",
@ -217,6 +218,7 @@
"match.rematch.fizzled": "",
"match.rematch.waitingCaptain": "",
"match.rematch.rejoinQueue": "",
"match.rematch.backToQueue": "",
"preparing.joinQ": "",
"tiers.currentCriteria": "",
"tiers.info.p1": "",

View File

@ -210,6 +210,7 @@
"match.confirmScore": "",
"match.confirmScore.wrongHint": "",
"match.rematch.prompt": "",
"match.rematch.resolved": "",
"match.rematch.vote.yes": "",
"match.rematch.vote.no": "",
"match.rematch.vote.noConfirm": "",
@ -217,6 +218,7 @@
"match.rematch.fizzled": "",
"match.rematch.waitingCaptain": "",
"match.rematch.rejoinQueue": "",
"match.rematch.backToQueue": "",
"preparing.joinQ": "",
"tiers.currentCriteria": "",
"tiers.info.p1": "",

View File

@ -210,6 +210,7 @@
"match.confirmScore": "",
"match.confirmScore.wrongHint": "",
"match.rematch.prompt": "",
"match.rematch.resolved": "",
"match.rematch.vote.yes": "",
"match.rematch.vote.no": "",
"match.rematch.vote.noConfirm": "",
@ -217,6 +218,7 @@
"match.rematch.fizzled": "",
"match.rematch.waitingCaptain": "",
"match.rematch.rejoinQueue": "",
"match.rematch.backToQueue": "",
"preparing.joinQ": "",
"tiers.currentCriteria": "",
"tiers.info.p1": "",

View File

@ -210,6 +210,7 @@
"match.confirmScore": "",
"match.confirmScore.wrongHint": "",
"match.rematch.prompt": "",
"match.rematch.resolved": "",
"match.rematch.vote.yes": "",
"match.rematch.vote.no": "",
"match.rematch.vote.noConfirm": "",
@ -217,6 +218,7 @@
"match.rematch.fizzled": "",
"match.rematch.waitingCaptain": "",
"match.rematch.rejoinQueue": "",
"match.rematch.backToQueue": "",
"preparing.joinQ": "Entrar na fila",
"tiers.currentCriteria": "Critérios atuais",
"tiers.info.p1": "Por exemplo, Leviathan é o top 5% dos jogadores. Diamond é top 15% e etc.",

View File

@ -210,6 +210,7 @@
"match.confirmScore": "",
"match.confirmScore.wrongHint": "",
"match.rematch.prompt": "",
"match.rematch.resolved": "",
"match.rematch.vote.yes": "",
"match.rematch.vote.no": "",
"match.rematch.vote.noConfirm": "",
@ -217,6 +218,7 @@
"match.rematch.fizzled": "",
"match.rematch.waitingCaptain": "",
"match.rematch.rejoinQueue": "",
"match.rematch.backToQueue": "",
"preparing.joinQ": "Присоединиться к очереди",
"tiers.currentCriteria": "Текущие критерии",
"tiers.info.p1": "Например, Leviathan - топ 5% игроков, Diamond - 85 процентиль и т.д.",

View File

@ -210,6 +210,7 @@
"match.confirmScore": "",
"match.confirmScore.wrongHint": "",
"match.rematch.prompt": "",
"match.rematch.resolved": "",
"match.rematch.vote.yes": "",
"match.rematch.vote.no": "",
"match.rematch.vote.noConfirm": "",
@ -217,6 +218,7 @@
"match.rematch.fizzled": "",
"match.rematch.waitingCaptain": "",
"match.rematch.rejoinQueue": "",
"match.rematch.backToQueue": "",
"preparing.joinQ": "开始匹配",
"tiers.currentCriteria": "当前规则",
"tiers.info.p1": "比如说Leviathan是前5%的玩家Diamond是前15%的玩家。",