Better UI when waiting on a team

This commit is contained in:
Kalle 2026-05-03 11:30:35 +03:00
parent 49f648cf98
commit 8b70e1b2c6
20 changed files with 79 additions and 13 deletions

View File

@ -1,5 +1,5 @@
import { differenceInMinutes } from "date-fns";
import { Lock, MousePointerClick, Users, X } from "lucide-react";
import { Hourglass, Lock, MousePointerClick, Users, X } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Avatar } from "~/components/Avatar";
import { SendouButton } from "~/components/elements/Button";
@ -40,6 +40,7 @@ export function TournamentMatchBanner({
const opponentOne = data.match.opponentOne;
const opponentTwo = data.match.opponentTwo;
const isMissingTeam = !opponentOne?.id || !opponentTwo?.id;
const leagueRoundLocked = isLeagueRoundLocked(tournament, data.match.roundId);
const leagueRoundStartDate = leagueRoundLocked
@ -81,6 +82,12 @@ export function TournamentMatchBanner({
: undefined
}
/>
) : isMissingTeam ? (
<IconBanner
icon={<Hourglass size={32} />}
header={t("tournament:match.waitingForTeams.header")}
subtitle={t("tournament:match.waitingForTeams.subtitle")}
/>
) : teamsMissingActiveRoster.length > 0 ? (
<IconBanner
icon={<Users size={32} />}

View File

@ -1,3 +1,4 @@
import { useTranslation } from "react-i18next";
import { useFetcher } from "react-router";
import { MatchJoinTab } from "~/components/match-page/MatchJoinTab";
import { MatchResultTab } from "~/components/match-page/MatchResultTab";
@ -42,12 +43,16 @@ export function TournamentMatchTabs({
isPickBanStep,
} = useMatch();
// Preview matches (participants TBD) only render the admin tab so organizers
// can pre-cast or pre-prepare; everything else needs both teams.
// When waiting on team(s) only a subset of tabs can be rendered
if (!teamOne || !teamTwo) {
return tabs.includes(TAB_KEYS.ADMIN) ? (
<MatchTabs tabs={[TAB_KEYS.ADMIN]}>
<TournamentMatchAdminTab data={data} />
return tabs.length > 0 ? (
<MatchTabs tabs={tabs}>
{tabs.includes(TAB_KEYS.ROSTERS) ? (
<TournamentMatchRosterTab data={data} />
) : null}
{tabs.includes(TAB_KEYS.ADMIN) ? (
<TournamentMatchAdminTab data={data} />
) : null}
</MatchTabs>
) : null;
}
@ -369,28 +374,33 @@ function TournamentMatchRosterTab({
}: {
data: TournamentMatchLoaderData;
}) {
const { t } = useTranslation(["tournament"]);
const tournament = useTournament();
const user = useUser();
const fetcher = useFetcher();
const {
teams: [teamOne, teamTwo],
} = useMatch();
if (!teamOne || !teamTwo) return null;
const tbdTeam = { defaultName: t("tournament:match.tbd"), members: [] };
return (
<MatchRosterTab
minMembersPerTeam={tournament.minMembersPerTeam}
canEditSubbedOut={[
canEditSubbedOutForTeam(teamOne),
canEditSubbedOutForTeam(teamTwo),
teamOne ? canEditSubbedOutForTeam(teamOne) : false,
teamTwo ? canEditSubbedOutForTeam(teamTwo) : false,
]}
defaultIsEditing={[
needsActiveRosterSelection(teamOne),
needsActiveRosterSelection(teamTwo),
teamOne ? needsActiveRosterSelection(teamOne) : false,
teamTwo ? needsActiveRosterSelection(teamTwo) : false,
]}
onSubbedOutChange={handleSubbedOutChange}
isSubmitting={fetcher.state !== "idle"}
teams={[rosterTeamData(teamOne), rosterTeamData(teamTwo)]}
teams={[
teamOne ? rosterTeamData(teamOne) : tbdTeam,
teamTwo ? rosterTeamData(teamTwo) : tbdTeam,
]}
/>
);

View File

@ -207,6 +207,8 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
const isParticipant = match.players.some((p) => p.id === user?.id);
const canJoin =
!matchIsOver &&
match.opponentOne?.id != null &&
match.opponentTwo?.id != null &&
(isParticipant || tournament.isOrganizerOrStreamer(user)) &&
!isLeagueRoundLocked(tournament, match.roundId);

View File

@ -15,7 +15,6 @@ export const handle: SendouRouteHandle = {
i18n: ["q"],
};
// xxx: check page when both teams are not resolved yet
export default function TournamentMatchPage() {
const data = useLoaderData<typeof loader>();

View File

@ -117,6 +117,9 @@
"match.activeRosterMissing.subtitle": "",
"match.leagueLocked.header": "",
"match.leagueLocked.subtitle": "",
"match.waitingForTeams.header": "",
"match.waitingForTeams.subtitle": "",
"match.tbd": "",
"match.action.undoLastScore": "Annuller sidste score",
"match.action.reopenMatch": "Genåbn kamp",
"match.action.endSet": "",

View File

@ -117,6 +117,9 @@
"match.activeRosterMissing.subtitle": "",
"match.leagueLocked.header": "",
"match.leagueLocked.subtitle": "",
"match.waitingForTeams.header": "",
"match.waitingForTeams.subtitle": "",
"match.tbd": "",
"match.action.undoLastScore": "Letztes Ergebnis widerrufen",
"match.action.reopenMatch": "Match erneut öffnen",
"match.action.endSet": "",

View File

@ -117,6 +117,9 @@
"match.activeRosterMissing.subtitle": "Waiting on {{teams}}",
"match.leagueLocked.header": "Waiting for league round to start",
"match.leagueLocked.subtitle": "Round playable from {{date}} onwards",
"match.waitingForTeams.header": "Waiting for teams",
"match.waitingForTeams.subtitle": "Teams will be resolved from earlier matches",
"match.tbd": "TBD",
"match.action.undoLastScore": "Undo last score",
"match.action.reopenMatch": "Reopen match",
"match.action.endSet": "End set",

View File

@ -119,6 +119,9 @@
"match.activeRosterMissing.subtitle": "",
"match.leagueLocked.header": "",
"match.leagueLocked.subtitle": "",
"match.waitingForTeams.header": "",
"match.waitingForTeams.subtitle": "",
"match.tbd": "",
"match.action.undoLastScore": "Anular resultado previo",
"match.action.reopenMatch": "Reabrir partido",
"match.action.endSet": "Finalizar set",

View File

@ -119,6 +119,9 @@
"match.activeRosterMissing.subtitle": "",
"match.leagueLocked.header": "",
"match.leagueLocked.subtitle": "",
"match.waitingForTeams.header": "",
"match.waitingForTeams.subtitle": "",
"match.tbd": "",
"match.action.undoLastScore": "Anular resultado previo",
"match.action.reopenMatch": "Reabrir partido",
"match.action.endSet": "",

View File

@ -119,6 +119,9 @@
"match.activeRosterMissing.subtitle": "",
"match.leagueLocked.header": "",
"match.leagueLocked.subtitle": "",
"match.waitingForTeams.header": "",
"match.waitingForTeams.subtitle": "",
"match.tbd": "",
"match.action.undoLastScore": "Annuler le dernier score",
"match.action.reopenMatch": "Rouvrir le match",
"match.action.endSet": "",

View File

@ -119,6 +119,9 @@
"match.activeRosterMissing.subtitle": "",
"match.leagueLocked.header": "",
"match.leagueLocked.subtitle": "",
"match.waitingForTeams.header": "",
"match.waitingForTeams.subtitle": "",
"match.tbd": "",
"match.action.undoLastScore": "Annuler le dernier score",
"match.action.reopenMatch": "Rouvrir le match",
"match.action.endSet": "",

View File

@ -119,6 +119,9 @@
"match.activeRosterMissing.subtitle": "",
"match.leagueLocked.header": "",
"match.leagueLocked.subtitle": "",
"match.waitingForTeams.header": "",
"match.waitingForTeams.subtitle": "",
"match.tbd": "",
"match.action.undoLastScore": "בטלו את התוצאה האחרונה",
"match.action.reopenMatch": "פתיחה מחדש של הקרב",
"match.action.endSet": "",

View File

@ -119,6 +119,9 @@
"match.activeRosterMissing.subtitle": "",
"match.leagueLocked.header": "",
"match.leagueLocked.subtitle": "",
"match.waitingForTeams.header": "",
"match.waitingForTeams.subtitle": "",
"match.tbd": "",
"match.action.undoLastScore": "Annulla ultimo punteggio",
"match.action.reopenMatch": "Riapri match",
"match.action.endSet": "",

View File

@ -113,6 +113,9 @@
"match.activeRosterMissing.subtitle": "",
"match.leagueLocked.header": "",
"match.leagueLocked.subtitle": "",
"match.waitingForTeams.header": "",
"match.waitingForTeams.subtitle": "",
"match.tbd": "",
"match.action.undoLastScore": "最後のスコアをやりなおす",
"match.action.reopenMatch": "対戦を再度開く",
"match.action.endSet": "",

View File

@ -113,6 +113,9 @@
"match.activeRosterMissing.subtitle": "",
"match.leagueLocked.header": "",
"match.leagueLocked.subtitle": "",
"match.waitingForTeams.header": "",
"match.waitingForTeams.subtitle": "",
"match.tbd": "",
"match.action.undoLastScore": "",
"match.action.reopenMatch": "",
"match.action.endSet": "",

View File

@ -117,6 +117,9 @@
"match.activeRosterMissing.subtitle": "",
"match.leagueLocked.header": "",
"match.leagueLocked.subtitle": "",
"match.waitingForTeams.header": "",
"match.waitingForTeams.subtitle": "",
"match.tbd": "",
"match.action.undoLastScore": "",
"match.action.reopenMatch": "",
"match.action.endSet": "",

View File

@ -121,6 +121,9 @@
"match.activeRosterMissing.subtitle": "",
"match.leagueLocked.header": "",
"match.leagueLocked.subtitle": "",
"match.waitingForTeams.header": "",
"match.waitingForTeams.subtitle": "",
"match.tbd": "",
"match.action.undoLastScore": "",
"match.action.reopenMatch": "",
"match.action.endSet": "",

View File

@ -119,6 +119,9 @@
"match.activeRosterMissing.subtitle": "",
"match.leagueLocked.header": "",
"match.leagueLocked.subtitle": "",
"match.waitingForTeams.header": "",
"match.waitingForTeams.subtitle": "",
"match.tbd": "",
"match.action.undoLastScore": "Desfazer última pontuação",
"match.action.reopenMatch": "Reabrir partida",
"match.action.endSet": "",

View File

@ -121,6 +121,9 @@
"match.activeRosterMissing.subtitle": "",
"match.leagueLocked.header": "",
"match.leagueLocked.subtitle": "",
"match.waitingForTeams.header": "",
"match.waitingForTeams.subtitle": "",
"match.tbd": "",
"match.action.undoLastScore": "Отменить последний результат",
"match.action.reopenMatch": "Открыть матч заново",
"match.action.endSet": "",

View File

@ -113,6 +113,9 @@
"match.activeRosterMissing.subtitle": "",
"match.leagueLocked.header": "",
"match.leagueLocked.subtitle": "",
"match.waitingForTeams.header": "",
"match.waitingForTeams.subtitle": "",
"match.tbd": "",
"match.action.undoLastScore": "撤销上次比分",
"match.action.reopenMatch": "重新开始对战",
"match.action.endSet": "",