diff --git a/app/features/admin/actions/admin.server.ts b/app/features/admin/actions/admin.server.ts index 4f63512f4..32c9ca08c 100644 --- a/app/features/admin/actions/admin.server.ts +++ b/app/features/admin/actions/admin.server.ts @@ -39,6 +39,8 @@ export const action = async ({ request }: ActionFunctionArgs) => { errorToast(`Migration failed. Reason: ${errorMessage}`); } + await refreshBannedCache(); + message = "Account migrated"; break; } catch (err) { diff --git a/app/features/art/routes/art.new.tsx b/app/features/art/routes/art.new.tsx index efff2f512..44f585e6c 100644 --- a/app/features/art/routes/art.new.tsx +++ b/app/features/art/routes/art.new.tsx @@ -65,7 +65,7 @@ export default function NewArtPage() { const submitButtonDisabled = () => { if (fetcher.state !== "idle") return true; - return !img && !data.art; + return (!img || !smallImg) && !data.art; }; if (!isArtist) { @@ -121,7 +121,6 @@ function ImageUpload({ { const uploadedFile = e.target.files?.[0]; diff --git a/app/features/ban/loaders/suspended.server.ts b/app/features/ban/loaders/suspended.server.ts index 2dbed9b32..c8a878cbb 100644 --- a/app/features/ban/loaders/suspended.server.ts +++ b/app/features/ban/loaders/suspended.server.ts @@ -6,14 +6,19 @@ import { } from "~/features/auth/core/authenticator.server"; import { authSessionStorage } from "~/features/auth/core/session.server"; import type { Nullish } from "~/utils/types"; -import { userIsBanned } from "../core/banned.server"; +import { refreshBannedCache, userIsBanned } from "../core/banned.server"; export const loader = async ({ request }: LoaderFunctionArgs) => { const userId = await getUserIdEvenIfBanned(request); if (!userId || !userIsBanned(userId)) return redirect("/"); - const bannedStatus = (await AdminRepository.allBannedUsers()).get(userId)!; + const bannedStatus = (await AdminRepository.allBannedUsers()).get(userId); + + if (!bannedStatus) { + await refreshBannedCache(); + return redirect("/"); + } return { banned: bannedStatus.banned, diff --git a/app/features/builds/BuildRepository.server.ts b/app/features/builds/BuildRepository.server.ts index 0441ef29a..3c3f7e96e 100644 --- a/app/features/builds/BuildRepository.server.ts +++ b/app/features/builds/BuildRepository.server.ts @@ -257,9 +257,9 @@ export async function ownerIdById(buildId: number) { .selectFrom("Build") .select("ownerId") .where("id", "=", buildId) - .executeTakeFirstOrThrow(); + .executeTakeFirst(); - return result.ownerId; + return result?.ownerId ?? null; } export async function abilityPointAverages(weaponSplId?: MainWeaponId | null) { diff --git a/app/features/tournament-bracket/TournamentMatchRepository.server.test.ts b/app/features/tournament-bracket/TournamentMatchRepository.server.test.ts new file mode 100644 index 000000000..2d90faac6 --- /dev/null +++ b/app/features/tournament-bracket/TournamentMatchRepository.server.test.ts @@ -0,0 +1,178 @@ +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { db } from "~/db/sql"; +import { dbInsertUsers, dbReset } from "~/utils/Test"; +import * as TournamentMatchRepository from "./TournamentMatchRepository.server"; + +const createTournament = () => + db + .insertInto("Tournament") + .values({ + mapPickingStyle: "TO", + settings: JSON.stringify({ bracketProgression: [] }), + }) + .returning("id") + .executeTakeFirstOrThrow(); + +const createTeam = (tournamentId: number, name: string) => + db + .insertInto("TournamentTeam") + .values({ + tournamentId, + name, + inviteCode: `inv-${tournamentId}-${name}`, + }) + .returning("id") + .executeTakeFirstOrThrow(); + +const createStage = (tournamentId: number, name: string, number: number) => + db + .insertInto("TournamentStage") + .values({ + tournamentId, + name, + number, + type: "double_elimination", + settings: "{}", + }) + .returning("id") + .executeTakeFirstOrThrow(); + +const createGroup = (stageId: number, number: number) => + db + .insertInto("TournamentGroup") + .values({ stageId, number }) + .returning("id") + .executeTakeFirstOrThrow(); + +const createRound = (stageId: number, groupId: number, number: number) => + db + .insertInto("TournamentRound") + .values({ + stageId, + groupId, + number, + maps: JSON.stringify({ count: 3, type: "BEST_OF" }), + }) + .returning("id") + .executeTakeFirstOrThrow(); + +const createMatch = async (args: { + stageId: number; + groupId: number; + roundId: number; + number: number; + teamOneId: number; + teamTwoId: number; +}) => { + const match = await db + .insertInto("TournamentMatch") + .values({ + stageId: args.stageId, + groupId: args.groupId, + roundId: args.roundId, + number: args.number, + status: 4, + opponentOne: JSON.stringify({ id: args.teamOneId, score: 2 }), + opponentTwo: JSON.stringify({ id: args.teamTwoId, score: 0 }), + }) + .returning("id") + .executeTakeFirstOrThrow(); + + await db + .insertInto("TournamentMatchGameResult") + .values({ + matchId: match.id, + mode: "SZ", + number: 1, + reporterId: 1, + source: "TO", + stageId: 1, + winnerTeamId: args.teamOneId, + }) + .returning("id") + .executeTakeFirstOrThrow() + .then((result) => + db + .insertInto("TournamentMatchGameResultParticipant") + .values([ + { + matchGameResultId: result.id, + userId: 1, + tournamentTeamId: args.teamOneId, + }, + { + matchGameResultId: result.id, + userId: 2, + tournamentTeamId: args.teamTwoId, + }, + ]) + .execute(), + ); + + return match; +}; + +describe("findByTournamentTeamId", () => { + beforeEach(async () => { + await dbInsertUsers(2); + }); + + afterEach(() => { + dbReset(); + }); + + test("preserves stage order: matches from an earlier stage come first even when later stage has lower group numbers", async () => { + // Tournament with two stages. The first stage has a high group number + // (think: round-robin pool 8) and the second stage has group number 1 + // (think: DE bracket winners). The team page should show stage 1's + // matches first, then stage 2's. + const tournament = await createTournament(); + const teamA = await createTeam(tournament.id, "A"); + const teamB = await createTeam(tournament.id, "B"); + + // Insert team members so we have someone to attribute results to + for (const userId of [1, 2]) { + await db + .insertInto("TournamentTeamMember") + .values({ tournamentTeamId: teamA.id, userId, role: "OWNER" }) + .execute(); + await db + .insertInto("TournamentTeamMember") + .values({ tournamentTeamId: teamB.id, userId, role: "OWNER" }) + .execute(); + } + + const stage1 = await createStage(tournament.id, "Stage 1", 1); + const stage1Group = await createGroup(stage1.id, 8); + const stage1Round = await createRound(stage1.id, stage1Group.id, 1); + const stage1Match = await createMatch({ + stageId: stage1.id, + groupId: stage1Group.id, + roundId: stage1Round.id, + number: 1, + teamOneId: teamA.id, + teamTwoId: teamB.id, + }); + + const stage2 = await createStage(tournament.id, "Stage 2", 2); + const stage2Group = await createGroup(stage2.id, 1); + const stage2Round = await createRound(stage2.id, stage2Group.id, 1); + const stage2Match = await createMatch({ + stageId: stage2.id, + groupId: stage2Group.id, + roundId: stage2Round.id, + number: 1, + teamOneId: teamA.id, + teamTwoId: teamB.id, + }); + + const result = await TournamentMatchRepository.findByTournamentTeamId( + teamA.id, + ); + + expect(result.map((s) => s.tournamentMatchId)).toEqual([ + stage1Match.id, + stage2Match.id, + ]); + }); +}); diff --git a/app/features/tournament-match/TournamentMatchRepository.server.ts b/app/features/tournament-match/TournamentMatchRepository.server.ts index 433078cf9..24f230816 100644 --- a/app/features/tournament-match/TournamentMatchRepository.server.ts +++ b/app/features/tournament-match/TournamentMatchRepository.server.ts @@ -237,6 +237,7 @@ export function findByTournamentTeamId(tournamentTeamId: number) { ), ), ) + .orderBy("TournamentRound.stageId", "asc") .orderBy("TournamentGroup.number", "asc") .orderBy("TournamentRound.number", "asc") .execute(); diff --git a/app/features/tournament/routes/to.$id.register.tsx b/app/features/tournament/routes/to.$id.register.tsx index d631cc5b4..2066edc73 100644 --- a/app/features/tournament/routes/to.$id.register.tsx +++ b/app/features/tournament/routes/to.$id.register.tsx @@ -130,6 +130,11 @@ export default function TournamentRegisterPage() { hour: "numeric", day: "numeric", month: "numeric", + year: + tournament.ctx.startTime.getFullYear() !== + new Date().getFullYear() + ? "2-digit" + : undefined, }} /> ) : null} diff --git a/app/features/tournament/routes/to.$id.teams.$tid.tsx b/app/features/tournament/routes/to.$id.teams.$tid.tsx index 23cb3d281..684501d96 100644 --- a/app/features/tournament/routes/to.$id.teams.$tid.tsx +++ b/app/features/tournament/routes/to.$id.teams.$tid.tsx @@ -29,7 +29,7 @@ import { useTournament } from "./to.$id"; export { loader }; export const meta: MetaFunction = (args) => { - const tournamentData = (args.matches[1].data as any) + const tournamentData = JSON.parse(args.matches[1].data as any) ?.tournament as TournamentData; if (!args.data || !tournamentData) return [];