From 646cc048994866a44cab0094083da01c8e2b0b83 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Tue, 21 Apr 2026 21:10:57 +0300 Subject: [PATCH] Wire up tournament join tab --- .../tournament-bracket-schemas.server.ts | 4 + .../actions/to.$id.matches.$mid.server.ts | 5 ++ .../components/TournamentMatchTabs.tsx | 84 +++++++++++++++++-- .../loaders/to.$id.matches.$mid.server.ts | 14 ++++ 4 files changed, 100 insertions(+), 7 deletions(-) diff --git a/app/features/tournament-bracket/tournament-bracket-schemas.server.ts b/app/features/tournament-bracket/tournament-bracket-schemas.server.ts index 93b6680d4..cbf818337 100644 --- a/app/features/tournament-bracket/tournament-bracket-schemas.server.ts +++ b/app/features/tournament-bracket/tournament-bracket-schemas.server.ts @@ -101,6 +101,10 @@ export const matchSchema = z.union([ _action: _action("END_SET"), winnerTeamId: z.preprocess(nullLiteraltoNull, id.nullable()), }), + // xxx: one central API for confirm room? + z.object({ + _action: _action("CONFIRM_ROOM"), + }), ]); export const bracketIdx = z.coerce.number().int().min(0).max(100); diff --git a/app/features/tournament-match/actions/to.$id.matches.$mid.server.ts b/app/features/tournament-match/actions/to.$id.matches.$mid.server.ts index 2a97992a6..b465f0b01 100644 --- a/app/features/tournament-match/actions/to.$id.matches.$mid.server.ts +++ b/app/features/tournament-match/actions/to.$id.matches.$mid.server.ts @@ -3,6 +3,7 @@ import { sql } from "~/db/sql"; import { TournamentMatchStatus } from "~/db/tables"; import { requireUser } from "~/features/auth/core/user.server"; import * as ChatSystemMessage from "~/features/chat/ChatSystemMessage.server"; +import * as RoomLinkRepository from "~/features/chat/RoomLinkRepository.server"; import * as TournamentRepository from "~/features/tournament/TournamentRepository.server"; import * as TournamentTeamRepository from "~/features/tournament/TournamentTeamRepository.server"; import { endDroppedTeamMatches } from "~/features/tournament/tournament-utils.server"; @@ -728,6 +729,10 @@ export const action: ActionFunction = async ({ params, request }) => { break; } + case "CONFIRM_ROOM": { + await RoomLinkRepository.refreshTimestamp(user.id); + break; + } default: { assertUnreachable(data); } diff --git a/app/features/tournament-match/components/TournamentMatchTabs.tsx b/app/features/tournament-match/components/TournamentMatchTabs.tsx index 71b7c8455..71ccbc736 100644 --- a/app/features/tournament-match/components/TournamentMatchTabs.tsx +++ b/app/features/tournament-match/components/TournamentMatchTabs.tsx @@ -1,3 +1,4 @@ +import { differenceInMinutes } from "date-fns"; import { useFetcher } from "react-router"; import { MatchJoinTab } from "~/components/match-page/MatchJoinTab"; import { MatchResultTab } from "~/components/match-page/MatchResultTab"; @@ -6,9 +7,14 @@ import { MatchTabs } from "~/components/match-page/MatchTabs"; import type { TimelineMap } from "~/components/match-page/MatchTimeline"; import { useUser } from "~/features/auth/core/user"; import { useTournament } from "~/features/tournament/routes/to.$id"; -import { tournamentTeamToActiveRosterUserIds } from "~/features/tournament-bracket/tournament-bracket-utils"; +import { + groupNumberToLetters, + tournamentTeamToActiveRosterUserIds, +} from "~/features/tournament-bracket/tournament-bracket-utils"; +import { databaseTimestampToDate } from "~/utils/dates"; import { tournamentTeamPage } from "~/utils/urls"; import type { TournamentMatchLoaderData } from "../loaders/to.$id.matches.$mid.server"; +import { resolveHostingTeam, resolveRoomPass } from "../tournament-match-utils"; import { TournamentMatchActionTab } from "./TournamentMatchActionTab"; export function TournamentMatchTabs({ @@ -62,7 +68,7 @@ export function TournamentMatchTabs({ maps={resolveTimelineMaps(data, opponentOneId, opponentTwoId)} /> ) : null} - {tabs.includes("join") ? : null} + {tabs.includes("join") ? : null} {tabs.includes("action") && currentMap ? ( b.type === "round_robin", + ); + const bracketIdx = tournament.brackets.findIndex((b) => + b.data.match.some((m) => m.id === data.match.id), + ); + const bracket = tournament.brackets[bracketIdx]; + const bracketMatch = bracket?.data.match.find((m) => m.id === data.match.id); + const group = bracket?.data.group.find( + (g) => g.id === bracketMatch?.group_id, + ); + + const poolCode = tournament.resolvePoolCode({ + hostingTeamId: hostingTeam.id, + groupLetters: + group && bracket?.type === "round_robin" + ? groupNumberToLetters(group.number) + : undefined, + bracketNumber: + hasRoundRobin && bracket?.type !== "round_robin" + ? bracketIdx + 1 + : undefined, + }); + + // xxx: maybe some shared util? + const freshnessCutoff = data.match.startedAt ?? 0; + const validRoomLink = data.roomLinks.find( + (rl) => rl.refreshedAt >= freshnessCutoff, + ); + const ownStaleRoomLink = validRoomLink + ? undefined + : data.roomLinks.find((rl) => rl.userId === user?.id); + const activeRoomLink = validRoomLink ?? ownStaleRoomLink; + const isStale = activeRoomLink ? !validRoomLink : undefined; + const staleMinutesAgo = ownStaleRoomLink + ? differenceInMinutes( + new Date(), + databaseTimestampToDate(ownStaleRoomLink.refreshedAt), + ) + : 0; + const roomLinkUsername = activeRoomLink + ? data.match.players.find((p) => p.id === activeRoomLink.userId)?.username + : undefined; + return ( { + confirmFetcher.submit({ _action: "CONFIRM_ROOM" }, { method: "post" }); + }} + isConfirming={confirmFetcher.state !== "idle"} + pool={`${poolCode.prefix}${poolCode.suffix}`} + pass={resolveRoomPass(hostingTeam.id)} + showNoSplatnetAlert={data.anyUserPrefersNoSplatnet} /> ); } diff --git a/app/features/tournament-match/loaders/to.$id.matches.$mid.server.ts b/app/features/tournament-match/loaders/to.$id.matches.$mid.server.ts index 1ffade3d5..37753ad09 100644 --- a/app/features/tournament-match/loaders/to.$id.matches.$mid.server.ts +++ b/app/features/tournament-match/loaders/to.$id.matches.$mid.server.ts @@ -3,6 +3,7 @@ import type { LoaderFunctionArgs } from "react-router"; import { getUser } from "~/features/auth/core/user.server"; import * as ChatSystemMessage from "~/features/chat/ChatSystemMessage.server"; import { chatAccessible } from "~/features/chat/chat-utils"; +import * as RoomLinkRepository from "~/features/chat/RoomLinkRepository.server"; import * as TournamentRepository from "~/features/tournament/TournamentRepository.server"; import * as TournamentTeamRepository from "~/features/tournament/TournamentTeamRepository.server"; import * as PickBan from "~/features/tournament-bracket/core/PickBan"; @@ -176,6 +177,17 @@ export const loader = async ({ params }: LoaderFunctionArgs) => { const visibleChatCode = shouldSeeChat && !chatCodeExpired ? match.chatCode : undefined; + // xxx: optimization, can be skipped if user can't join anyway + const [roomLinks, anyUserPrefersNoSplatnet] = matchIsOver + ? ([[], false] as const) + : await Promise.all([ + RoomLinkRepository.findByUserIds( + match.players.map((p) => p.id), + 3, + ), + UserRepository.anyUserPrefersNoSplatnet(match.players.map((p) => p.id)), + ]); + return { match: shouldSeeChat ? match : { ...match, chatCode: undefined }, results, @@ -184,6 +196,8 @@ export const loader = async ({ params }: LoaderFunctionArgs) => { endedEarly, noScreen, chatCode: visibleChatCode, + roomLinks, + anyUserPrefersNoSplatnet, pickBanEventCount: pickBanEvents.length, pickBanEvents: pickBanEvents.map((e) => ({ type: e.type,