().as("teamsCount")])
+ .as("teamsCount"),
eb
.selectFrom("UserSubmittedImage")
.select(["UserSubmittedImage.url"])
.whereRef("CalendarEvent.avatarImgId", "=", "UserSubmittedImage.id")
.as("logoUrl"),
- "CalendarEvent.avatarMetadata",
+ jsonObjectFrom(
+ eb
+ .selectFrom("TournamentOrganization")
+ .select([
+ "TournamentOrganization.name",
+ "TournamentOrganization.slug",
+ ])
+ .whereRef(
+ "TournamentOrganization.id",
+ "=",
+ "CalendarEvent.organizationId",
+ ),
+ ).as("organization"),
jsonArrayFrom(
eb
.selectFrom("TournamentResult")
@@ -332,40 +416,40 @@ export async function forShowcase() {
"TournamentResult.tournamentTeamId",
"TournamentTeam.id",
)
+ .leftJoin("AllTeam", "TournamentTeam.teamId", "AllTeam.id")
+ .leftJoin(
+ "UserSubmittedImage as TeamAvatar",
+ "AllTeam.avatarImgId",
+ "TeamAvatar.id",
+ )
+ .leftJoin(
+ "UserSubmittedImage as TournamentTeamAvatar",
+ "TournamentTeam.avatarImgId",
+ "TournamentTeamAvatar.id",
+ )
.whereRef("TournamentResult.tournamentId", "=", "Tournament.id")
.where("TournamentResult.placement", "=", 1)
.select([
- "User.id",
- "User.username",
+ ...COMMON_USER_FIELDS,
+ "User.country",
"TournamentTeam.name as teamName",
+ "TeamAvatar.url as teamLogoUrl",
+ "TournamentTeamAvatar.url as pickupAvatarUrl",
]),
).as("firstPlacers"),
])
- .orderBy("CalendarEventDate.startTime desc")
+ .where("CalendarEventDate.startTime", ">", databaseTimestampWeekAgo())
+ .orderBy("CalendarEventDate.startTime asc")
+ .$narrowType<{ teamsCount: NotNull }>()
.execute();
+}
- const latestWinners = rows.find((r) => r.firstPlacers.length > 0);
- const next: typeof rows = [];
-
- const nextTournamentsCount = latestWinners
- ? NEXT_TOURNAMENTS_TO_SHOW_WITH_UPCOMING
- : NEXT_TOURNAMENTS_TO_SHOW_WITH_UPCOMING + 1;
-
- for (const row of rows) {
- if (row.id === latestWinners?.id) break;
-
- // if they did not finalize the tournament for whatever reason, lets just stop showing it after 6 hours
- if (
- new Date() > add(databaseTimestampToDate(row.startTime), { hours: 6 })
- ) {
- continue;
- }
- next.unshift(row);
+function databaseTimestampWeekAgo() {
+ const now = new Date();
- if (next.length > nextTournamentsCount) next.pop();
- }
+ now.setDate(now.getDate() - 7);
- return [latestWinners, ...next].filter(Boolean);
+ return dateToDatabaseTimestamp(now);
}
export function topThreeResultsByTournamentId(tournamentId: number) {
diff --git a/app/features/tournament/actions/to.$id.register.server.ts b/app/features/tournament/actions/to.$id.register.server.ts
index 7880a84d0f..fa11c1f49c 100644
--- a/app/features/tournament/actions/to.$id.register.server.ts
+++ b/app/features/tournament/actions/to.$id.register.server.ts
@@ -1,5 +1,6 @@
import type { ActionFunction } from "@remix-run/node";
import { requireUser } from "~/features/auth/core/user.server";
+import * as ShowcaseTournaments from "~/features/front-page/core/ShowcaseTournaments.server";
import { MapPool } from "~/features/map-list-generator/core/map-pool";
import * as QRepository from "~/features/sendouq/QRepository.server";
import * as TeamRepository from "~/features/team/TeamRepository.server";
@@ -15,7 +16,7 @@ import {
parseFormData,
uploadImageIfSubmitted,
validate,
-} from "~/utils/remix";
+} from "~/utils/remix.server";
import { booleanToInt } from "~/utils/sql";
import { assertUnreachable } from "~/utils/types";
import { checkIn } from "../queries/checkIn.server";
@@ -110,6 +111,12 @@ export const action: ActionFunction = async ({ request, params }) => {
tournamentId,
avatarFileName,
});
+
+ ShowcaseTournaments.addToParticipationInfoMap({
+ tournamentId,
+ type: "participant",
+ userId: user.id,
+ });
}
break;
}
@@ -131,6 +138,12 @@ export const action: ActionFunction = async ({ request, params }) => {
);
deleteTeamMember({ tournamentTeamId: ownTeam.id, userId: data.userId });
+
+ ShowcaseTournaments.removeFromParticipationInfoMap({
+ tournamentId,
+ type: "participant",
+ userId: data.userId,
+ });
break;
}
case "LEAVE_TEAM": {
@@ -148,6 +161,12 @@ export const action: ActionFunction = async ({ request, params }) => {
userId: user.id,
});
+ ShowcaseTournaments.removeFromParticipationInfoMap({
+ tournamentId,
+ type: "participant",
+ userId: user.id,
+ });
+
break;
}
case "UPDATE_MAP_POOL": {
@@ -220,6 +239,13 @@ export const action: ActionFunction = async ({ request, params }) => {
userId: data.userId,
}),
});
+
+ ShowcaseTournaments.addToParticipationInfoMap({
+ tournamentId,
+ type: "participant",
+ userId: data.userId,
+ });
+
break;
}
case "UNREGISTER": {
@@ -227,6 +253,9 @@ export const action: ActionFunction = async ({ request, params }) => {
validate(!ownTeamCheckedIn, "You cannot unregister after checking in");
deleteTeam(ownTeam.id);
+
+ ShowcaseTournaments.clearParticipationInfoMap();
+
break;
}
case "DELETE_LOGO": {
diff --git a/app/features/tournament/routes/to.$id.admin.tsx b/app/features/tournament/routes/to.$id.admin.tsx
index 1b0d89824f..1ad863df70 100644
--- a/app/features/tournament/routes/to.$id.admin.tsx
+++ b/app/features/tournament/routes/to.$id.admin.tsx
@@ -19,6 +19,7 @@ import { USER } from "~/constants";
import { useUser } from "~/features/auth/core/user";
import { requireUserId } from "~/features/auth/core/user.server";
import { userIsBanned } from "~/features/ban/core/banned.server";
+import * as ShowcaseTournaments from "~/features/front-page/core/ShowcaseTournaments.server";
import type { TournamentData } from "~/features/tournament-bracket/core/Tournament.server";
import {
clearTournamentDataCache,
@@ -32,11 +33,11 @@ import {
badRequestIfFalsy,
parseRequestPayload,
validate,
-} from "~/utils/remix";
+} from "~/utils/remix.server";
import { assertUnreachable } from "~/utils/types";
import {
- calendarEditPage,
calendarEventPage,
+ tournamentEditPage,
tournamentPage,
} from "~/utils/urls";
import * as TournamentRepository from "../TournamentRepository.server";
@@ -90,6 +91,12 @@ export const action: ActionFunction = async ({ request, params }) => {
tournamentId,
});
+ ShowcaseTournaments.addToParticipationInfoMap({
+ tournamentId,
+ type: "participant",
+ userId: data.userId,
+ });
+
break;
}
case "CHANGE_TEAM_OWNER": {
@@ -192,6 +199,13 @@ export const action: ActionFunction = async ({ request, params }) => {
userId: data.memberId,
teamId: team.id,
});
+
+ ShowcaseTournaments.removeFromParticipationInfoMap({
+ tournamentId,
+ type: "participant",
+ userId: data.memberId,
+ });
+
break;
}
case "ADD_MEMBER": {
@@ -227,6 +241,13 @@ export const action: ActionFunction = async ({ request, params }) => {
userId: data.userId,
}),
});
+
+ ShowcaseTournaments.addToParticipationInfoMap({
+ tournamentId,
+ type: "participant",
+ userId: data.userId,
+ });
+
break;
}
case "DELETE_TEAM": {
@@ -236,23 +257,44 @@ export const action: ActionFunction = async ({ request, params }) => {
validate(!tournament.hasStarted, "Tournament has started");
deleteTeam(team.id);
+
+ ShowcaseTournaments.clearParticipationInfoMap();
+
break;
}
case "ADD_STAFF": {
validateIsTournamentAdmin();
+
await TournamentRepository.addStaff({
role: data.role,
tournamentId: tournament.ctx.id,
userId: data.userId,
});
+
+ if (data.role === "ORGANIZER") {
+ ShowcaseTournaments.addToParticipationInfoMap({
+ tournamentId,
+ type: "organizer",
+ userId: data.userId,
+ });
+ }
+
break;
}
case "REMOVE_STAFF": {
validateIsTournamentAdmin();
+
await TournamentRepository.removeStaff({
tournamentId: tournament.ctx.id,
userId: data.userId,
});
+
+ ShowcaseTournaments.removeFromParticipationInfoMap({
+ tournamentId,
+ type: "organizer",
+ userId: data.userId,
+ });
+
break;
}
case "UPDATE_CAST_TWITCH_ACCOUNTS": {
@@ -351,7 +393,7 @@ export default function TournamentAdminPage() {
{tournament.isAdmin(user) && !tournament.hasStarted ? (
{
userId: user.id,
}),
});
+
+ ShowcaseTournaments.addToParticipationInfoMap({
+ tournamentId,
+ type: "participant",
+ userId: user.id,
+ });
+
if (data.trust) {
const inviterUserId = teamToJoin.members.find(
(member) => member.isOwner,
diff --git a/app/features/tournament/routes/to.$id.seeds.tsx b/app/features/tournament/routes/to.$id.seeds.tsx
index 17f590cf5a..fca419bd5e 100644
--- a/app/features/tournament/routes/to.$id.seeds.tsx
+++ b/app/features/tournament/routes/to.$id.seeds.tsx
@@ -46,7 +46,7 @@ import {
} from "~/features/tournament-bracket/core/Tournament.server";
import { useTimeoutState } from "~/hooks/useTimeoutState";
import invariant from "~/utils/invariant";
-import { parseRequestPayload, validate } from "~/utils/remix";
+import { parseRequestPayload, validate } from "~/utils/remix.server";
import {
navIconUrl,
tournamentBracketsPage,
diff --git a/app/features/tournament/routes/to.$id.teams.$tid.tsx b/app/features/tournament/routes/to.$id.teams.$tid.tsx
index 8290209076..fc7a2c2c76 100644
--- a/app/features/tournament/routes/to.$id.teams.$tid.tsx
+++ b/app/features/tournament/routes/to.$id.teams.$tid.tsx
@@ -10,7 +10,7 @@ import { Redirect } from "~/components/Redirect";
import type { TournamentDataTeam } from "~/features/tournament-bracket/core/Tournament.server";
import { tournamentTeamPageParamsSchema } from "~/features/tournament-bracket/tournament-bracket-schemas.server";
import type { TournamentMaplistSource } from "~/modules/tournament-map-list-generator";
-import { parseParams } from "~/utils/remix";
+import { parseParams } from "~/utils/remix.server";
import {
teamPage,
tournamentMatchPage,
diff --git a/app/features/tournament/routes/to.$id.tsx b/app/features/tournament/routes/to.$id.tsx
index a26923c279..e157006266 100644
--- a/app/features/tournament/routes/to.$id.tsx
+++ b/app/features/tournament/routes/to.$id.tsx
@@ -21,7 +21,7 @@ import * as TournamentRepository from "~/features/tournament/TournamentRepositor
import { useIsMounted } from "~/hooks/useIsMounted";
import { isAdmin } from "~/permissions";
import { databaseTimestampToDate } from "~/utils/dates";
-import type { SendouRouteHandle } from "~/utils/remix";
+import type { SendouRouteHandle } from "~/utils/remix.server";
import { makeTitle } from "~/utils/strings";
import { assertUnreachable } from "~/utils/types";
import {
diff --git a/app/features/tournament/tournament-utils.server.ts b/app/features/tournament/tournament-utils.server.ts
index 3ce24c9edf..53f08b118d 100644
--- a/app/features/tournament/tournament-utils.server.ts
+++ b/app/features/tournament/tournament-utils.server.ts
@@ -1,5 +1,5 @@
import * as UserRepository from "~/features/user-page/UserRepository.server";
-import { validate } from "~/utils/remix";
+import { validate } from "~/utils/remix.server";
import type { Tournament } from "../tournament-bracket/core/Tournament";
export const inGameNameIfNeeded = async ({
diff --git a/app/features/tournament/tournament-utils.ts b/app/features/tournament/tournament-utils.ts
index af1b36641e..e50e29033d 100644
--- a/app/features/tournament/tournament-utils.ts
+++ b/app/features/tournament/tournament-utils.ts
@@ -5,6 +5,7 @@ import { rankedModesShort } from "~/modules/in-game-lists/modes";
import invariant from "~/utils/invariant";
import { tournamentLogoUrl } from "~/utils/urls";
import { MapPool } from "../map-list-generator/core/map-pool";
+import { currentSeason } from "../mmr/season";
import { BANNED_MAPS } from "../sendouq-settings/banned-maps";
import type { TournamentData } from "../tournament-bracket/core/Tournament.server";
import type { PlayedSet } from "./core/sets.server";
@@ -180,131 +181,6 @@ export function HACKY_resolvePicture(event: { name: string }) {
return tournamentLogoUrl("default");
}
-// legacy approach, new tournament should use the avatarMetadata column in CalendarEvent
-const BLACK = "#1e1e1e";
-const WHITE = "#fffcfc";
-export function HACKY_resolveThemeColors(event: { name: string }) {
- const normalizedEventName = event.name.toLowerCase();
-
- if (normalizedEventName.includes("sendouq")) {
- return { backgroundColor: "#1e1e1e", textColor: WHITE };
- }
-
- if (normalizedEventName.includes("paddling pool")) {
- return { backgroundColor: "#fff", textColor: BLACK };
- }
-
- if (normalizedEventName.includes("in the zone")) {
- return { backgroundColor: "#8b0000", textColor: WHITE };
- }
-
- if (normalizedEventName.includes("picnic")) {
- return { backgroundColor: "#e3fefe", textColor: BLACK };
- }
-
- if (normalizedEventName.includes("proving grounds")) {
- return { backgroundColor: "#ffe809", textColor: BLACK };
- }
-
- if (normalizedEventName.includes("triton")) {
- return { backgroundColor: "#aee8ff", textColor: BLACK };
- }
-
- if (normalizedEventName.includes("swim or sink")) {
- return { backgroundColor: "#d7f8ea", textColor: BLACK };
- }
-
- if (normalizedEventName.includes("from the ink up")) {
- return { backgroundColor: "#ffdfc6", textColor: BLACK };
- }
-
- if (normalizedEventName.includes("coral clash")) {
- return { backgroundColor: "#f0f4ff", textColor: BLACK };
- }
-
- if (normalizedEventName.includes("level up")) {
- return { backgroundColor: "#383232", textColor: WHITE };
- }
-
- if (normalizedEventName.includes("all 4 one")) {
- return { backgroundColor: "#2b262a", textColor: WHITE };
- }
-
- if (normalizedEventName.includes("fry basket")) {
- return { backgroundColor: "#fff", textColor: BLACK };
- }
-
- if (normalizedEventName.includes("the depths")) {
- return { backgroundColor: "#183e42", textColor: WHITE };
- }
-
- if (normalizedEventName.includes("eclipse")) {
- return { backgroundColor: "#191919", textColor: WHITE };
- }
-
- if (normalizedEventName.includes("homecoming")) {
- return { backgroundColor: "#1c1c1c", textColor: WHITE };
- }
-
- if (normalizedEventName.includes("bad ideas")) {
- return { backgroundColor: "#000000", textColor: WHITE };
- }
-
- if (normalizedEventName.includes("tenoch")) {
- return { backgroundColor: "#425969", textColor: WHITE };
- }
-
- if (normalizedEventName.includes("megalodon monday")) {
- return { backgroundColor: "#288eb5", textColor: WHITE };
- }
-
- if (normalizedEventName.includes("heaven 2 ocean")) {
- return { backgroundColor: "#8cf1ff", textColor: BLACK };
- }
-
- if (normalizedEventName.includes("kraken royale")) {
- return { backgroundColor: "#32333a", textColor: WHITE };
- }
-
- if (normalizedEventName.includes("menu royale")) {
- return { backgroundColor: "#000", textColor: WHITE };
- }
-
- if (normalizedEventName.includes("barracuda co")) {
- return { backgroundColor: "#47b6fe", textColor: BLACK };
- }
-
- if (normalizedEventName.includes("crimson ink")) {
- return { backgroundColor: "#000000", textColor: WHITE };
- }
-
- if (normalizedEventName.includes("mesozoic mayhem")) {
- return { backgroundColor: "#ccd5da", textColor: BLACK };
- }
-
- if (normalizedEventName.includes("rain or shine")) {
- return { backgroundColor: "#201c3b", textColor: WHITE };
- }
-
- if (normalizedEventName.includes("squid junction")) {
- return { backgroundColor: "#fed09f", textColor: BLACK };
- }
-
- if (normalizedEventName.includes("silly sausage")) {
- return { backgroundColor: "#ffd76f", textColor: BLACK };
- }
-
- if (normalizedEventName.includes("united-lan")) {
- return { backgroundColor: "#fff", textColor: BLACK };
- }
-
- if (normalizedEventName.includes("soul cup")) {
- return { backgroundColor: "#101011", textColor: WHITE };
- }
-
- return { backgroundColor: "#3430ad", textColor: WHITE };
-}
-
export type CounterPickValidationStatus =
| "PICKING"
| "VALID"
@@ -379,3 +255,17 @@ export function validateCounterPickMapPool(
return "VALID";
}
+
+export function tournamentIsRanked({
+ isSetAsRanked,
+ startTime,
+ minMembersPerTeam,
+}: { isSetAsRanked?: boolean; startTime: Date; minMembersPerTeam: number }) {
+ const seasonIsActive = Boolean(currentSeason(startTime));
+ if (!seasonIsActive) return false;
+
+ // 1v1, 2v2 and 3v3 are always considered "gimmicky"
+ if (minMembersPerTeam !== 4) return false;
+
+ return isSetAsRanked ?? true;
+}
diff --git a/app/features/user-page/actions/u.$identifier.builds.new.server.ts b/app/features/user-page/actions/u.$identifier.builds.new.server.ts
index 47c7e4ffa3..75a2878ae6 100644
--- a/app/features/user-page/actions/u.$identifier.builds.new.server.ts
+++ b/app/features/user-page/actions/u.$identifier.builds.new.server.ts
@@ -18,7 +18,7 @@ import type {
import { removeDuplicates } from "~/utils/arrays";
import { unJsonify } from "~/utils/kysely.server";
import { logger } from "~/utils/logger";
-import { parseRequestPayload, validate } from "~/utils/remix";
+import { parseRequestPayload, validate } from "~/utils/remix.server";
import type { Nullish } from "~/utils/types";
import { userBuildsPage } from "~/utils/urls";
import {
diff --git a/app/features/user-page/actions/u.$identifier.builds.server.ts b/app/features/user-page/actions/u.$identifier.builds.server.ts
index 05a40b1ea8..1e0c990f90 100644
--- a/app/features/user-page/actions/u.$identifier.builds.server.ts
+++ b/app/features/user-page/actions/u.$identifier.builds.server.ts
@@ -6,7 +6,7 @@ import * as BuildRepository from "~/features/builds/BuildRepository.server";
import { refreshBuildsCacheByWeaponSplIds } from "~/features/builds/core/cached-builds.server";
import * as UserRepository from "~/features/user-page/UserRepository.server";
import { logger } from "~/utils/logger";
-import { parseRequestPayload, validate } from "~/utils/remix";
+import { parseRequestPayload, validate } from "~/utils/remix.server";
import { assertUnreachable } from "~/utils/types";
import { userBuildsPage } from "~/utils/urls";
import {
diff --git a/app/features/user-page/loaders/u.$identifier.builds.server.ts b/app/features/user-page/loaders/u.$identifier.builds.server.ts
index 3aa38c6f4e..635ae783e8 100644
--- a/app/features/user-page/loaders/u.$identifier.builds.server.ts
+++ b/app/features/user-page/loaders/u.$identifier.builds.server.ts
@@ -4,7 +4,7 @@ import * as BuildRepository from "~/features/builds/BuildRepository.server";
import { sortAbilities } from "~/features/builds/core/ability-sorting.server";
import * as UserRepository from "~/features/user-page/UserRepository.server";
import type { MainWeaponId } from "~/modules/in-game-lists";
-import { notFoundIfFalsy, privatelyCachedJson } from "~/utils/remix";
+import { notFoundIfFalsy, privatelyCachedJson } from "~/utils/remix.server";
import { sortBuilds } from "../core/build-sorting.server";
import { userParamsSchema } from "../user-page-schemas.server";
diff --git a/app/features/user-page/loaders/u.$identifier.index.server.ts b/app/features/user-page/loaders/u.$identifier.index.server.ts
index 919be870ef..4d4c201d18 100644
--- a/app/features/user-page/loaders/u.$identifier.index.server.ts
+++ b/app/features/user-page/loaders/u.$identifier.index.server.ts
@@ -3,7 +3,7 @@ import { getUserId } from "~/features/auth/core/user.server";
import { userIsBanned } from "~/features/ban/core/banned.server";
import * as UserRepository from "~/features/user-page/UserRepository.server";
import { isAdmin } from "~/permissions";
-import { notFoundIfFalsy } from "~/utils/remix";
+import { notFoundIfFalsy } from "~/utils/remix.server";
export const loader = async ({ params, request }: LoaderFunctionArgs) => {
const loggedInUser = await getUserId(request);
diff --git a/app/features/user-page/loaders/u.$identifier.results.server.ts b/app/features/user-page/loaders/u.$identifier.results.server.ts
index 11639b5512..9a98eed67e 100644
--- a/app/features/user-page/loaders/u.$identifier.results.server.ts
+++ b/app/features/user-page/loaders/u.$identifier.results.server.ts
@@ -1,6 +1,6 @@
import type { LoaderFunctionArgs, SerializeFrom } from "@remix-run/node";
import * as UserRepository from "~/features/user-page/UserRepository.server";
-import { notFoundIfFalsy } from "~/utils/remix";
+import { notFoundIfFalsy } from "~/utils/remix.server";
export type UserResultsLoaderData = SerializeFrom;
diff --git a/app/features/user-page/loaders/u.$identifier.vods.server.ts b/app/features/user-page/loaders/u.$identifier.vods.server.ts
index 885a360cc3..488de8cabf 100644
--- a/app/features/user-page/loaders/u.$identifier.vods.server.ts
+++ b/app/features/user-page/loaders/u.$identifier.vods.server.ts
@@ -1,7 +1,7 @@
import type { LoaderFunctionArgs } from "@remix-run/node";
import * as UserRepository from "~/features/user-page/UserRepository.server";
import { findVods } from "~/features/vods/queries/findVods.server";
-import { notFoundIfFalsy } from "~/utils/remix";
+import { notFoundIfFalsy } from "~/utils/remix.server";
export const loader = async ({ params }: LoaderFunctionArgs) => {
const userId = notFoundIfFalsy(
diff --git a/app/features/user-page/routes/u.$identifier.art.tsx b/app/features/user-page/routes/u.$identifier.art.tsx
index a7fcc26305..7c520c1803 100644
--- a/app/features/user-page/routes/u.$identifier.art.tsx
+++ b/app/features/user-page/routes/u.$identifier.art.tsx
@@ -1,8 +1,6 @@
import type { ActionFunction, LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData, useMatches } from "@remix-run/react";
import { useTranslation } from "react-i18next";
-import { LinkButton } from "~/components/Button";
-import { Popover } from "~/components/Popover";
import { deleteArtSchema } from "~/features/art/art-schemas.server";
import { ART_SOURCES, type ArtSource } from "~/features/art/art-types";
import { ArtGrid } from "~/features/art/components/ArtGrid";
@@ -20,8 +18,7 @@ import {
notFoundIfFalsy,
parseRequestPayload,
validate,
-} from "~/utils/remix";
-import { newArtPage } from "~/utils/urls";
+} from "~/utils/remix.server";
import { userParamsSchema } from "../user-page-schemas.server";
import type { UserPageLoaderData } from "./u.$identifier";
@@ -122,9 +119,6 @@ export default function UserArtPage() {
? t("art:pendingApproval", { count: data.unvalidatedArtCount })
: null}
- {layoutData.user.id === user?.id ? (
-
- ) : null}