|
@@ -59,9 +53,9 @@ export function TeamResultsTable({ results }: TeamResultsTableProps) {
|
- {logoUrl !== tournamentLogoUrl("default") ? (
+ {result.logoUrl ? (
 ();
- return team.avatarSrc || team.bannerSrc ? (
+ return team.avatarUrl || team.bannerUrl ? (
- {team.avatarSrc ? (
+ {team.avatarUrl ? (
-
) : null}
- {team.bannerSrc ? (
+ {team.bannerUrl ? (
-
= (args) => {
title: args.data.team.name,
description: args.data.team.bio ?? undefined,
location: args.location,
- image: args.data.team.avatarSrc
+ image: args.data.team.avatarUrl
? {
- url: userSubmittedImage(args.data.team.avatarSrc),
+ url: args.data.team.avatarUrl,
dimensions: {
width: 124,
height: 124,
@@ -74,18 +73,18 @@ function TeamBanner() {
<>
- {team.avatarSrc ? (
+ {team.avatarUrl ? (
- })
+
) : null}
@@ -102,7 +101,7 @@ function TeamBanner() {
{team.name}
- {team.avatarSrc ? : null}
+ {team.avatarUrl ? : null}
>
);
}
diff --git a/app/features/team/routes/t.tsx b/app/features/team/routes/t.tsx
index 58de22326..f5e2e1b3b 100644
--- a/app/features/team/routes/t.tsx
+++ b/app/features/team/routes/t.tsx
@@ -23,7 +23,6 @@ import {
TEAM_SEARCH_PAGE,
teamPage,
} from "~/utils/urls";
-import { userSubmittedImage } from "~/utils/urls-img";
import { action } from "../actions/t.server";
import { loader } from "../loaders/t.server";
import { TEAM, TEAMS_PER_PAGE } from "../team-constants";
@@ -106,9 +105,9 @@ export default function TeamSearchPage() {
to={teamPage(team.customUrl)}
className="team-search__team"
>
- {team.avatarSrc ? (
+ {team.avatarUrl ? (
}) staff.id === user?.id && staff.role === "ORGANIZER",
) ||
isAdmin(user);
- const logoIsFromStaticAssets = ctx.logoSrc.includes("static-assets");
const revealInfo = tournamentHasStarted || isOrganizer;
- const defaultLogo = HACKY_resolvePicture({ name: "default" });
-
return {
data,
ctx: {
...ctx,
- logoSrc:
- isOrganizer || ctx.logoValidatedAt || logoIsFromStaticAssets
- ? ctx.logoSrc
- : defaultLogo,
teams: ctx.teams.map((team) => {
const isOwnTeam = team.members.some(
(member) => member.userId === user?.id,
diff --git a/app/features/tournament-bracket/core/Tournament.ts b/app/features/tournament-bracket/core/Tournament.ts
index ddb99eda4..0891c80e0 100644
--- a/app/features/tournament-bracket/core/Tournament.ts
+++ b/app/features/tournament-bracket/core/Tournament.ts
@@ -24,7 +24,6 @@ import {
import invariant from "~/utils/invariant";
import { logger } from "~/utils/logger";
import { assertUnreachable } from "~/utils/types";
-import { userSubmittedImage } from "~/utils/urls-img";
import {
fillWithNullTillPowerOfTwo,
groupNumberToLetters,
@@ -664,11 +663,7 @@ export class Tournament {
/** Tournament teams logo image path, either from the team or the pickup avatar uploaded specifically for this tournament */
tournamentTeamLogoSrc(team: TournamentDataTeam) {
- const url = team.team?.logoUrl ?? team.pickupAvatarUrl;
-
- if (!url) return;
-
- return userSubmittedImage(url);
+ return team.team?.logoUrl ?? team.pickupAvatarUrl;
}
/** Generates a Splatoon 3 pool code to join the tournament match. It tries to make it so that teams don't need to change the pool all the time, but provides different ones not to run into the in-game limit of max people in a pool at a time. */
diff --git a/app/features/tournament-bracket/core/tests/mocks-li.ts b/app/features/tournament-bracket/core/tests/mocks-li.ts
index 2c10dfb87..c1b18382b 100644
--- a/app/features/tournament-bracket/core/tests/mocks-li.ts
+++ b/app/features/tournament-bracket/core/tests/mocks-li.ts
@@ -7409,7 +7409,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({
],
},
logoUrl: "tournament-logo-0r1Pt_6Ho7qb6UF7VM4yD-1733156154857.webp",
- logoValidatedAt: 1733164146,
author: {
id: 4941,
username: "HoeenHero",
@@ -15061,7 +15060,5 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({
45194, 45250, 45290, 45295, 45391, 45778, 45806, 46245, 46289, 46305,
46394, 46451, 46501, 46518, 46523, 46538, 46586, 46637, 46648, 46771,
],
- logoSrc:
- "https://sendou.nyc3.cdn.digitaloceanspaces.com/tournament-logo-0r1Pt_6Ho7qb6UF7VM4yD-1733156154857.webp",
},
});
diff --git a/app/features/tournament-bracket/core/tests/mocks-sos.ts b/app/features/tournament-bracket/core/tests/mocks-sos.ts
index f70e87842..ec6b766dd 100644
--- a/app/features/tournament-bracket/core/tests/mocks-sos.ts
+++ b/app/features/tournament-bracket/core/tests/mocks-sos.ts
@@ -2256,8 +2256,7 @@ export const SWIM_OR_SINK_167 = (
},
],
},
- logoUrl: null,
- logoValidatedAt: null,
+ logoUrl: "test.png",
author: {
id: 1402,
username: "grace",
@@ -7626,6 +7625,5 @@ export const SWIM_OR_SINK_167 = (
40505, 41797, 42409, 42703, 43847, 43850, 44751, 45163, 45635, 45980,
46045, 46099, 46101,
],
- logoSrc: "/static-assets/img/tournament-logos/sos.png",
},
});
diff --git a/app/features/tournament-bracket/core/tests/mocks-zones-weekly.ts b/app/features/tournament-bracket/core/tests/mocks-zones-weekly.ts
index e5361e06c..d40e144d4 100644
--- a/app/features/tournament-bracket/core/tests/mocks-zones-weekly.ts
+++ b/app/features/tournament-bracket/core/tests/mocks-zones-weekly.ts
@@ -341,7 +341,6 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isFinalized: 0,
organization: null,
logoUrl: "tournament-logo-hfX5gzVyrt5QCV8fiQA4n-1716906622859.webp",
- logoValidatedAt: 1716949983,
author: {
id: 13370,
username: "Puma",
@@ -1080,7 +1079,5 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
17855, 21689, 26992, 30176, 31148, 33047, 33491, 33578, 33611, 37632,
37901, 43518, 43662, 45879, 46006, 46467, 46813,
],
- logoSrc:
- "https://sendou.nyc3.cdn.digitaloceanspaces.com/tournament-logo-hfX5gzVyrt5QCV8fiQA4n-1716906622859.webp",
},
});
diff --git a/app/features/tournament-bracket/core/tests/mocks.ts b/app/features/tournament-bracket/core/tests/mocks.ts
index fead35e04..5ad92e874 100644
--- a/app/features/tournament-bracket/core/tests/mocks.ts
+++ b/app/features/tournament-bracket/core/tests/mocks.ts
@@ -2039,9 +2039,7 @@ export const PADDLING_POOL_257 = () =>
description:
"Hosted by Dapple Productions.\n\nThe longest tournament series in Splatoon!\nEvery week a tournament!\n\n✓ DE or Groups into SE\n✓ All Modes (Picnic system)\n✓ Badge prize\n✓ A well-ran tournament experience\n\nCome join!",
rules: null,
- logoUrl: null,
- logoSrc: "/test.png",
- logoValidatedAt: null,
+ logoUrl: "/test.png",
subCounts: [],
startTime: 1709748000,
author: {
@@ -8067,9 +8065,7 @@ export const PADDLING_POOL_255 = () =>
name: "Paddling Pool 255",
description: null,
rules: null,
- logoUrl: null,
- logoSrc: "/test.png",
- logoValidatedAt: null,
+ logoUrl: "/test.png",
subCounts: [],
startTime: 1708538400,
author: {
@@ -14413,9 +14409,7 @@ export const IN_THE_ZONE_32 = ({
name: "In The Zone 32",
description: "Part of sendou.ink ranked season 2",
rules: null,
- logoUrl: null,
- logoSrc: "/test.png",
- logoValidatedAt: null,
+ logoUrl: "/test.png",
subCounts: [],
startTime: 1707588000,
author: {
diff --git a/app/features/tournament-bracket/core/tests/test-utils.ts b/app/features/tournament-bracket/core/tests/test-utils.ts
index 613a5871f..c0d35d959 100644
--- a/app/features/tournament-bracket/core/tests/test-utils.ts
+++ b/app/features/tournament-bracket/core/tests/test-utils.ts
@@ -65,9 +65,7 @@ export const testTournament = ({
organization: null,
parentTournamentId: null,
rules: null,
- logoUrl: null,
- logoSrc: "/test.png",
- logoValidatedAt: null,
+ logoUrl: "/test.png",
discordUrl: null,
startTime: 1705858842,
isFinalized: 0,
diff --git a/app/features/tournament-organization/TournamentOrganizationRepository.server.ts b/app/features/tournament-organization/TournamentOrganizationRepository.server.ts
index b2201e62e..c3c1407d0 100644
--- a/app/features/tournament-organization/TournamentOrganizationRepository.server.ts
+++ b/app/features/tournament-organization/TournamentOrganizationRepository.server.ts
@@ -8,10 +8,12 @@ import {
databaseTimestampToDate,
dateToDatabaseTimestamp,
} from "~/utils/dates";
-import { COMMON_USER_FIELDS } from "~/utils/kysely.server";
+import {
+ COMMON_USER_FIELDS,
+ concatUserSubmittedImagePrefix,
+ tournamentLogoWithDefault,
+} from "~/utils/kysely.server";
import { mySlugify } from "~/utils/urls";
-import { userSubmittedImage } from "~/utils/urls-img";
-import { HACKY_resolvePicture } from "../tournament/tournament-utils";
import { TOURNAMENT_SERIES_EVENTS_PER_PAGE } from "./tournament-organization-constants";
interface CreateArgs {
@@ -58,7 +60,9 @@ export async function findBySlug(slug: string) {
"TournamentOrganization.socials",
"TournamentOrganization.slug",
"TournamentOrganization.isEstablished",
- "UserSubmittedImage.url as avatarUrl",
+ concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as(
+ "avatarUrl",
+ ),
jsonArrayFrom(
eb
.selectFrom("TournamentOrganizationMember")
@@ -173,11 +177,7 @@ const findEventsBaseQuery = (organizationId: number) =>
"CalendarEvent.name",
"CalendarEvent.tournamentId",
eb.fn.min("CalendarEventDate.startTime").as("startTime"),
- eb
- .selectFrom("UserSubmittedImage")
- .select(["UserSubmittedImage.url"])
- .whereRef("CalendarEvent.avatarImgId", "=", "UserSubmittedImage.id")
- .as("logoUrl"),
+ tournamentLogoWithDefault(eb).as("logoUrl"),
jsonObjectFrom(
eb
.selectFrom("TournamentResult")
@@ -195,7 +195,9 @@ const findEventsBaseQuery = (organizationId: number) =>
)
.select(({ eb: innerEb }) => [
"TournamentTeam.name",
- innerEb.fn.coalesce("u1.url", "u2.url").as("avatarUrl"),
+ concatUserSubmittedImagePrefix(
+ innerEb.fn.coalesce("u1.url", "u2.url"),
+ ).as("avatarUrl"),
jsonArrayFrom(
innerEb
.selectFrom("TournamentTeamMember")
@@ -250,7 +252,7 @@ const findEventsBaseQuery = (organizationId: number) =>
const mapEvent = <
T extends {
tournamentId: number | null;
- logoUrl: string | null;
+ logoUrl: string;
name: string;
},
>(
@@ -258,11 +260,7 @@ const mapEvent = <
) => {
return {
...event,
- logoUrl: !event.tournamentId
- ? null
- : event.logoUrl
- ? userSubmittedImage(event.logoUrl)
- : HACKY_resolvePicture(event),
+ logoUrl: !event.tournamentId ? null : event.logoUrl,
};
};
diff --git a/app/features/tournament-organization/routes/org.$slug.tsx b/app/features/tournament-organization/routes/org.$slug.tsx
index c71122129..db7528f3c 100644
--- a/app/features/tournament-organization/routes/org.$slug.tsx
+++ b/app/features/tournament-organization/routes/org.$slug.tsx
@@ -39,7 +39,6 @@ import {
tournamentPage,
userPage,
} from "~/utils/urls";
-import { userSubmittedImage } from "~/utils/urls-img";
import { action } from "../actions/org.$slug.server";
import { EventCalendar } from "../components/EventCalendar";
import { SocialLinksList } from "../components/SocialLinksList";
@@ -58,7 +57,7 @@ export const meta: MetaFunction = (args) => {
description: args.data.organization.description ?? undefined,
image: args.data.organization.avatarUrl
? {
- url: userSubmittedImage(args.data.organization.avatarUrl),
+ url: args.data.organization.avatarUrl,
dimensions: { width: 124, height: 124 },
}
: undefined,
@@ -75,7 +74,7 @@ export const handle: SendouRouteHandle = {
return [
data.organization.avatarUrl
? {
- imgPath: userSubmittedImage(data.organization.avatarUrl),
+ imgPath: data.organization.avatarUrl,
href: tournamentOrganizationPage({
organizationSlug: data.organization.slug,
}),
@@ -121,14 +120,7 @@ function LogoHeader() {
return (
-
+
{data.organization.name}
{canEditOrganization ? (
@@ -276,7 +268,7 @@ function AllTournamentsView() {
events={data.events}
fallbackLogoUrl={
data.organization.avatarUrl
- ? userSubmittedImage(data.organization.avatarUrl)
+ ? data.organization.avatarUrl
: BLANK_IMAGE_URL
}
/>
@@ -510,7 +502,7 @@ function EventWinners({
{winner.avatarUrl ? (
}) >;
export async function findById(id: number) {
@@ -63,7 +66,9 @@ export async function findById(id: number) {
"TournamentOrganization.id",
"TournamentOrganization.name",
"TournamentOrganization.slug",
- "UserSubmittedImage.url as avatarUrl",
+ concatUserSubmittedImagePrefix(
+ innerEb.ref("UserSubmittedImage.url"),
+ ).as("avatarUrl"),
jsonArrayFrom(
innerEb
.selectFrom("TournamentOrganizationMember")
@@ -91,24 +96,7 @@ export async function findById(id: number) {
"CalendarEvent.organizationId",
),
).as("organization"),
- eb
- .selectFrom("UnvalidatedUserSubmittedImage")
- .select(["UnvalidatedUserSubmittedImage.url"])
- .whereRef(
- "CalendarEvent.avatarImgId",
- "=",
- "UnvalidatedUserSubmittedImage.id",
- )
- .as("logoUrl"),
- eb
- .selectFrom("UnvalidatedUserSubmittedImage")
- .select(["UnvalidatedUserSubmittedImage.validatedAt"])
- .whereRef(
- "CalendarEvent.avatarImgId",
- "=",
- "UnvalidatedUserSubmittedImage.id",
- )
- .as("logoValidatedAt"),
+ tournamentLogoWithDefault(eb).as("logoUrl"),
jsonObjectFrom(
eb
.selectFrom("User")
@@ -168,7 +156,9 @@ export async function findById(id: number) {
"TournamentTeam.createdAt",
"TournamentTeam.activeRosterUserIds",
"TournamentTeam.startingBracketIdx",
- "UserSubmittedImage.url as pickupAvatarUrl",
+ concatUserSubmittedImagePrefix(
+ innerEb.ref("UserSubmittedImage.url"),
+ ).as("pickupAvatarUrl"),
jsonArrayFrom(
innerEb
.selectFrom("TournamentTeamMember")
@@ -238,10 +228,12 @@ export async function findById(id: number) {
"UserSubmittedImage.id",
)
.whereRef("AllTeam.id", "=", "TournamentTeam.teamId")
- .select([
+ .select((eb) => [
"AllTeam.id",
"AllTeam.customUrl",
- "UserSubmittedImage.url as logoUrl",
+ concatUserSubmittedImagePrefix(
+ eb.ref("UserSubmittedImage.url"),
+ ).as("logoUrl"),
"AllTeam.deletedAt",
]),
).as("team"),
@@ -306,9 +298,6 @@ export async function findById(id: number) {
.filter((ordinal) => typeof ordinal === "number"),
),
})),
- logoSrc: result.logoUrl
- ? userSubmittedImage(result.logoUrl)
- : HACKY_resolvePicture(result),
participatedUsers: result.participatedUsers.map((user) => user.userId),
};
}
@@ -471,11 +460,7 @@ export function forShowcase() {
)
.select(({ fn }) => [fn.countAll ().as("teamsCount")])
.as("teamsCount"),
- eb
- .selectFrom("UserSubmittedImage")
- .select(["UserSubmittedImage.url"])
- .whereRef("CalendarEvent.avatarImgId", "=", "UserSubmittedImage.id")
- .as("logoUrl"),
+ tournamentLogoWithDefault(eb).as("logoUrl"),
jsonObjectFrom(
eb
.selectFrom("TournamentOrganization")
@@ -511,12 +496,16 @@ export function forShowcase() {
)
.whereRef("TournamentResult.tournamentId", "=", "Tournament.id")
.where("TournamentResult.placement", "=", 1)
- .select([
+ .select((eb) => [
...COMMON_USER_FIELDS,
"User.country",
"TournamentTeam.name as teamName",
- "TeamAvatar.url as teamLogoUrl",
- "TournamentTeamAvatar.url as pickupAvatarUrl",
+ concatUserSubmittedImagePrefix(eb.ref("TeamAvatar.url")).as(
+ "teamLogoUrl",
+ ),
+ concatUserSubmittedImagePrefix(
+ eb.ref("TournamentTeamAvatar.url"),
+ ).as("pickupAvatarUrl"),
]),
).as("firstPlacers"),
])
@@ -1153,16 +1142,11 @@ export async function searchByName({
"CalendarEvent.id",
"CalendarEventDate.eventId",
)
- .leftJoin(
- "UnvalidatedUserSubmittedImage",
- "CalendarEvent.avatarImgId",
- "UnvalidatedUserSubmittedImage.id",
- )
- .select([
+ .select((eb) => [
"Tournament.id",
"CalendarEvent.name",
"CalendarEventDate.startTime",
- "UnvalidatedUserSubmittedImage.url as logoUrl",
+ tournamentLogoWithDefault(eb).as("logoUrl"),
])
.where("CalendarEvent.name", "like", `%${query}%`)
.where("CalendarEvent.hidden", "=", 0)
@@ -1177,12 +1161,5 @@ export async function searchByName({
);
}
- const results = await sqlQuery.execute();
-
- return results.map((result) => ({
- ...result,
- logoSrc: result.logoUrl
- ? userSubmittedImage(result.logoUrl)
- : HACKY_resolvePicture({ name: result.name }),
- }));
+ return sqlQuery.execute();
}
diff --git a/app/features/tournament/actions/to.$id.admin.server.ts b/app/features/tournament/actions/to.$id.admin.server.ts
index 853363a98..4fd5fca31 100644
--- a/app/features/tournament/actions/to.$id.admin.server.ts
+++ b/app/features/tournament/actions/to.$id.admin.server.ts
@@ -259,7 +259,7 @@ export const action: ActionFunction = async ({ request, params }) => {
notification: {
type: "TO_ADDED_TO_TEAM",
pictureUrl:
- tournament.tournamentTeamLogoSrc(team) ?? tournament.ctx.logoSrc,
+ tournament.tournamentTeamLogoSrc(team) ?? tournament.ctx.logoUrl,
meta: {
adderUsername: user.username,
teamName: team.name,
diff --git a/app/features/tournament/actions/to.$id.register.server.ts b/app/features/tournament/actions/to.$id.register.server.ts
index ee367f5da..9214fa102 100644
--- a/app/features/tournament/actions/to.$id.register.server.ts
+++ b/app/features/tournament/actions/to.$id.register.server.ts
@@ -301,7 +301,7 @@ export const action: ActionFunction = async ({ request, params }) => {
tournamentName: tournament.ctx.name,
tournamentTeamId: ownTeam.id,
},
- pictureUrl: tournament.ctx.logoSrc,
+ pictureUrl: tournament.ctx.logoUrl,
},
});
}
diff --git a/app/features/tournament/components/TournamentStream.tsx b/app/features/tournament/components/TournamentStream.tsx
index 99390932e..f27165067 100644
--- a/app/features/tournament/components/TournamentStream.tsx
+++ b/app/features/tournament/components/TournamentStream.tsx
@@ -43,7 +43,7 @@ export function TournamentStream({
) : (
-
+
Cast {stream.twitchUserName}
)}
diff --git a/app/features/tournament/routes/to.$id.register.tsx b/app/features/tournament/routes/to.$id.register.tsx
index ee4e2e94a..db034d560 100644
--- a/app/features/tournament/routes/to.$id.register.tsx
+++ b/app/features/tournament/routes/to.$id.register.tsx
@@ -55,7 +55,6 @@ import {
userEditProfilePage,
userPage,
} from "~/utils/urls";
-import { userSubmittedImage } from "~/utils/urls-img";
import { AlertIcon } from "../../../components/icons/Alert";
import { action } from "../actions/to.$id.register.server";
import type { TournamentRegisterPageLoader } from "../loaders/to.$id.register.server";
@@ -69,22 +68,16 @@ import { useTournament } from "./to.$id";
export { loader, action };
export default function TournamentRegisterPage() {
- const user = useUser();
const isMounted = useIsMounted();
const tournament = useTournament();
const startsAtEvenHour = tournament.ctx.startTime.getMinutes() === 0;
- const showAvatarPendingApprovalText =
- tournament.ctx.logoUrl &&
- !tournament.ctx.logoValidatedAt &&
- tournament.isOrganizer(user);
-
return (
{tournament.ctx.organization.name}
@@ -159,12 +146,6 @@ export default function TournamentRegisterPage() {
- {showAvatarPendingApprovalText ? (
-
- Tournament logo pending moderator review. Will be shown publicly once
- approved.
-
- ) : null}
);
@@ -659,16 +640,11 @@ function TeamInfo({
const teamToSignUpWith = data?.teams.find(
(team) => team.id === signUpWithTeamId,
);
- return teamToSignUpWith?.logoUrl
- ? userSubmittedImage(teamToSignUpWith.logoUrl)
- : null;
+ return teamToSignUpWith?.logoUrl;
}
if (uploadedAvatar) return URL.createObjectURL(uploadedAvatar);
- if (ownTeam?.pickupAvatarUrl) {
- return userSubmittedImage(ownTeam.pickupAvatarUrl);
- }
- return null;
+ return ownTeam?.pickupAvatarUrl;
})();
const canEditAvatar =
diff --git a/app/features/tournament/routes/to.$id.seeds.tsx b/app/features/tournament/routes/to.$id.seeds.tsx
index 24d0cb4f5..5c011c8ad 100644
--- a/app/features/tournament/routes/to.$id.seeds.tsx
+++ b/app/features/tournament/routes/to.$id.seeds.tsx
@@ -394,14 +394,12 @@ function RowContents({
}) {
const tournament = useTournament();
+ const logoUrl = tournament.tournamentTeamLogoSrc(team);
+
return (
<>
{seed}
-
- {team.team?.logoUrl ? (
-
- ) : null}
-
+
{team.checkIns.length > 0 ? "✅ " : "❌ "} {team.name}
diff --git a/app/features/tournament/routes/to.$id.teams.$tid.tsx b/app/features/tournament/routes/to.$id.teams.$tid.tsx
index 23ee0b7c8..fb7ab40eb 100644
--- a/app/features/tournament/routes/to.$id.teams.$tid.tsx
+++ b/app/features/tournament/routes/to.$id.teams.$tid.tsx
@@ -19,7 +19,6 @@ import {
tournamentTeamPage,
userPage,
} from "~/utils/urls";
-import { userSubmittedImage } from "~/utils/urls-img";
import { TeamWithRoster } from "../components/TeamWithRoster";
import * as Standings from "../core/Standings";
import type { PlayedSet } from "../core/sets.server";
@@ -42,7 +41,7 @@ export const meta: MetaFunction = (args) => {
description: `${team.name} roster (${team.members.map((m) => m.username).join(", ")}) and sets in ${tournamentData.ctx.name}.`,
image: teamLogoUrl
? {
- url: userSubmittedImage(teamLogoUrl),
+ url: teamLogoUrl,
dimensions: { width: 124, height: 124 },
}
: undefined,
diff --git a/app/features/tournament/routes/to.$id.tsx b/app/features/tournament/routes/to.$id.tsx
index 30f6a9bd6..8224fafa6 100644
--- a/app/features/tournament/routes/to.$id.tsx
+++ b/app/features/tournament/routes/to.$id.tsx
@@ -22,7 +22,6 @@ import {
tournamentPage,
tournamentRegisterPage,
} from "~/utils/urls";
-import { userSubmittedImage } from "~/utils/urls-img";
import { metaTags } from "../../../utils/remix";
import { loader, type TournamentLoaderData } from "../loaders/to.$id.server";
@@ -53,7 +52,7 @@ export const meta: MetaFunction = (args) => {
? removeMarkdown(data.tournament.ctx.description)
: undefined,
image: {
- url: data.tournament.ctx.logoSrc,
+ url: data.tournament.ctx.logoUrl,
dimensions: { width: 124, height: 124 },
},
location: args.location,
@@ -71,9 +70,7 @@ export const handle: SendouRouteHandle = {
return [
data.tournament.ctx.organization?.avatarUrl
? {
- imgPath: userSubmittedImage(
- data.tournament.ctx.organization.avatarUrl,
- ),
+ imgPath: data.tournament.ctx.organization.avatarUrl,
href: tournamentOrganizationPage({
organizationSlug: data.tournament.ctx.organization.slug,
}),
@@ -83,7 +80,7 @@ export const handle: SendouRouteHandle = {
}
: null,
{
- imgPath: data.tournament.ctx.logoSrc,
+ imgPath: data.tournament.ctx.logoUrl,
href: tournamentPage(data.tournament.ctx.id),
type: "IMAGE" as const,
text: data.tournament.ctx.name,
diff --git a/app/features/tournament/tournament-utils.ts b/app/features/tournament/tournament-utils.ts
index 1345e5dc9..5f3265af8 100644
--- a/app/features/tournament/tournament-utils.ts
+++ b/app/features/tournament/tournament-utils.ts
@@ -3,7 +3,6 @@ import { modesShort, rankedModesShort } from "~/modules/in-game-lists/modes";
import type { ModeShort, StageId } from "~/modules/in-game-lists/types";
import { weekNumberToDate } from "~/utils/dates";
import { SHORT_NANOID_LENGTH } from "~/utils/id";
-import { tournamentLogoUrl } from "~/utils/urls";
import type { Tables, TournamentStageSettings } from "../../db/tables";
import { assertUnreachable } from "../../utils/types";
import { MapPool } from "../map-list-generator/core/map-pool";
@@ -69,129 +68,6 @@ export function tournamentRoundI18nKey(round: PlayedSet["round"]) {
return `bracket.${round.type}` as const;
}
-// legacy approach, new tournament should use the avatarImgId column in CalendarEvent
-export function HACKY_resolvePicture(event: { name: string }) {
- const normalizedEventName = event.name.toLowerCase();
-
- if (normalizedEventName.includes("sendouq")) {
- return tournamentLogoUrl("sf");
- }
-
- if (normalizedEventName.includes("paddling pool")) {
- return tournamentLogoUrl("pp");
- }
-
- if (normalizedEventName.includes("in the zone")) {
- return tournamentLogoUrl("itz");
- }
-
- if (normalizedEventName.includes("picnic")) {
- return tournamentLogoUrl("pn");
- }
-
- if (normalizedEventName.includes("proving grounds")) {
- return tournamentLogoUrl("pg");
- }
-
- if (normalizedEventName.includes("triton")) {
- return tournamentLogoUrl("tc");
- }
-
- if (normalizedEventName.includes("swim or sink")) {
- return tournamentLogoUrl("sos");
- }
-
- if (normalizedEventName.includes("from the ink up")) {
- return tournamentLogoUrl("ftiu");
- }
-
- if (normalizedEventName.includes("coral clash")) {
- return tournamentLogoUrl("cc");
- }
-
- if (normalizedEventName.includes("level up")) {
- return tournamentLogoUrl("lu");
- }
-
- if (normalizedEventName.includes("all 4 one")) {
- return tournamentLogoUrl("a41");
- }
-
- if (normalizedEventName.includes("fry basket")) {
- return tournamentLogoUrl("fb");
- }
-
- if (normalizedEventName.includes("the depths")) {
- return tournamentLogoUrl("d");
- }
-
- if (normalizedEventName.includes("eclipse")) {
- return tournamentLogoUrl("e");
- }
-
- if (normalizedEventName.includes("homecoming")) {
- return tournamentLogoUrl("hc");
- }
-
- if (normalizedEventName.includes("bad ideas")) {
- return tournamentLogoUrl("bio");
- }
-
- if (normalizedEventName.includes("tenoch")) {
- return tournamentLogoUrl("ai");
- }
-
- if (normalizedEventName.includes("megalodon monday")) {
- return tournamentLogoUrl("mm");
- }
-
- if (normalizedEventName.includes("heaven 2 ocean")) {
- return tournamentLogoUrl("ho");
- }
-
- if (normalizedEventName.includes("kraken royale")) {
- return tournamentLogoUrl("kr");
- }
-
- if (normalizedEventName.includes("menu royale")) {
- return tournamentLogoUrl("mr");
- }
-
- if (normalizedEventName.includes("barracuda co")) {
- return tournamentLogoUrl("bc");
- }
-
- if (normalizedEventName.includes("crimson ink")) {
- return tournamentLogoUrl("ci");
- }
-
- if (normalizedEventName.includes("mesozoic mayhem")) {
- return tournamentLogoUrl("me");
- }
-
- if (normalizedEventName.includes("rain or shine")) {
- return tournamentLogoUrl("ros");
- }
-
- if (normalizedEventName.includes("squid junction")) {
- return tournamentLogoUrl("sj");
- }
-
- if (normalizedEventName.includes("silly sausage")) {
- return tournamentLogoUrl("ss");
- }
-
- if (normalizedEventName.includes("united-lan")) {
- return tournamentLogoUrl("ul");
- }
-
- if (normalizedEventName.includes("soul cup")) {
- return tournamentLogoUrl("sc");
- }
-
- return tournamentLogoUrl("default");
-}
-
export type CounterPickValidationStatus =
| "PICKING"
| "VALID"
diff --git a/app/features/user-page/UserRepository.server.ts b/app/features/user-page/UserRepository.server.ts
index 8ec82715d..1a61b190f 100644
--- a/app/features/user-page/UserRepository.server.ts
+++ b/app/features/user-page/UserRepository.server.ts
@@ -15,7 +15,12 @@ import { isSupporter } from "~/modules/permissions/utils";
import { databaseTimestampNow, dateToDatabaseTimestamp } from "~/utils/dates";
import invariant from "~/utils/invariant";
import type { CommonUser } from "~/utils/kysely.server";
-import { COMMON_USER_FIELDS, userChatNameColor } from "~/utils/kysely.server";
+import {
+ COMMON_USER_FIELDS,
+ concatUserSubmittedImagePrefix,
+ tournamentLogoOrNull,
+ userChatNameColor,
+} from "~/utils/kysely.server";
import { safeNumberParse } from "~/utils/number";
import type { ChatUser } from "../chat/chat-types";
@@ -123,8 +128,8 @@ export function findLayoutDataByIdentifier(
.select(({ fn }) => fn.count("Art.id").distinct().as("count"))
.where((innerEb) =>
innerEb.or([
- innerEb("Art.authorId", "=", sql.raw("User.id")),
- innerEb("ArtUserMetadata.userId", "=", sql.raw("User.id")),
+ innerEb("Art.authorId", "=", eb.ref("User.id")),
+ innerEb("ArtUserMetadata.userId", "=", eb.ref("User.id")),
]),
)
.as("artCount"),
@@ -178,13 +183,15 @@ export async function findProfileByIdentifier(
"UserSubmittedImage.id",
"Team.avatarImgId",
)
- .select([
+ .select((eb) => [
"Team.name",
"Team.customUrl",
"Team.id",
"TeamMemberWithSecondary.isMainTeam",
"TeamMemberWithSecondary.role as userTeamRole",
- "UserSubmittedImage.url as avatarUrl",
+ concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as(
+ "avatarUrl",
+ ),
])
.whereRef("TeamMemberWithSecondary.userId", "=", "User.id"),
).as("teams"),
@@ -521,12 +528,8 @@ export function findResultsByUserId(
"TournamentResult.placement",
"TournamentResult.participantCount",
"TournamentResult.setResults",
+ tournamentLogoOrNull(eb).as("logoUrl"),
"TournamentResult.div",
- eb
- .selectFrom("UserSubmittedImage")
- .select(["UserSubmittedImage.url"])
- .whereRef("CalendarEvent.avatarImgId", "=", "UserSubmittedImage.id")
- .as("logoUrl"),
"CalendarEvent.name as eventName",
"TournamentTeam.id as teamId",
"TournamentTeam.name as teamName",
diff --git a/app/features/user-page/components/UserResultsTable.tsx b/app/features/user-page/components/UserResultsTable.tsx
index ac05b7522..da8180aa3 100644
--- a/app/features/user-page/components/UserResultsTable.tsx
+++ b/app/features/user-page/components/UserResultsTable.tsx
@@ -6,16 +6,13 @@ import { SendouPopover } from "~/components/elements/Popover";
import { UsersIcon } from "~/components/icons/Users";
import { Placement } from "~/components/Placement";
import { Table } from "~/components/Table";
-import { HACKY_resolvePicture } from "~/features/tournament/tournament-utils";
import { databaseTimestampToDate } from "~/utils/dates";
import {
calendarEventPage,
tournamentBracketsPage,
- tournamentLogoUrl,
tournamentTeamPage,
userPage,
} from "~/utils/urls";
-import { userSubmittedImage } from "~/utils/urls-img";
import type { UserResultsLoaderData } from "../loaders/u.$identifier.results.server";
import { ParticipationPill } from "./ParticipationPill";
@@ -59,10 +56,6 @@ export function UserResultsTable({
const nameCellId = `${id}-${result.teamId}-name`;
const checkboxLabelIds = `${nameCellId} ${placementHeaderId} ${placementCellId}`;
- const logoUrl = result.logoUrl
- ? userSubmittedImage(result.logoUrl)
- : HACKY_resolvePicture({ name: result.eventName });
-
return (
{hasHighlightCheckboxes && (
@@ -107,9 +100,9 @@ export function UserResultsTable({
) : null}
{result.tournamentId ? (
<>
- {logoUrl !== tournamentLogoUrl("default") ? (
+ {result.logoUrl ? (
 team.members.map((member) => member.userId))
diff --git a/app/utils/kysely.server.ts b/app/utils/kysely.server.ts
index 4c5d90720..be4b6d751 100644
--- a/app/utils/kysely.server.ts
+++ b/app/utils/kysely.server.ts
@@ -1,6 +1,13 @@
-import { type ColumnType, type ExpressionBuilder, sql } from "kysely";
+import {
+ type ColumnType,
+ type Expression,
+ type ExpressionBuilder,
+ expressionBuilder,
+ sql,
+} from "kysely";
import { jsonBuildObject } from "kysely/helpers/sqlite";
-import type { Tables } from "~/db/tables";
+import type { DB, Tables } from "~/db/tables";
+import { IS_E2E_TEST_RUN } from "./e2e";
export const COMMON_USER_FIELDS = [
"User.id",
@@ -31,6 +38,85 @@ export function commonUserJsonObject(eb: ExpressionBuilder ) {
});
}
+const USER_SUBMITTED_IMAGE_ROOT =
+ (process.env.NODE_ENV === "development" &&
+ import.meta.env.VITE_PROD_MODE !== "true") ||
+ IS_E2E_TEST_RUN ||
+ process.env.NODE_ENV === "test"
+ ? "http://127.0.0.1:9000/sendou"
+ : "https://sendou.nyc3.cdn.digitaloceanspaces.com";
+
+/**
+ * Constructs a SQL expression that returns the full URL for a tournament's logo.
+ * If the tournament has a custom logo (via avatarImgId), returns that logo's URL.
+ * Otherwise, returns null.
+ *
+ * @returns A SQL expression that concatenates the image root URL with either the custom logo URL or default logo
+ */
+export function tournamentLogoOrNull(
+ eb: ExpressionBuilder,
+) {
+ return eb.fn("iif", [
+ eb("CalendarEvent.avatarImgId", "is not", null),
+ eb.fn("concat", [
+ sql.lit(`${USER_SUBMITTED_IMAGE_ROOT}/`),
+ eb
+ .selectFrom("UnvalidatedUserSubmittedImage")
+ .select(["UnvalidatedUserSubmittedImage.url"])
+ .whereRef(
+ "CalendarEvent.avatarImgId",
+ "=",
+ "UnvalidatedUserSubmittedImage.id",
+ ),
+ ]),
+ sql`null`,
+ ]);
+}
+
+/**
+ * Constructs a SQL expression that returns the full URL for a tournament's logo.
+ * If the tournament has a custom logo (via avatarImgId), returns that logo's URL.
+ * Otherwise, falls back to the default tournament logo.
+ *
+ * @returns A SQL expression that concatenates the image root URL with either the custom logo URL or default logo
+ */
+export function tournamentLogoWithDefault(
+ /** Expression builder scoped to the CalendarEvent table */
+ eb: ExpressionBuilder,
+) {
+ return concatUserSubmittedImagePrefix(
+ eb
+ .selectFrom("UnvalidatedUserSubmittedImage")
+ .select((eb) => [
+ eb.fn
+ .coalesce(
+ "UnvalidatedUserSubmittedImage.url",
+ sql.lit(`${import.meta.env.VITE_TOURNAMENT_DEFAULT_LOGO}`),
+ )
+ .as("url"),
+ ])
+ .whereRef(
+ "CalendarEvent.avatarImgId",
+ "=",
+ "UnvalidatedUserSubmittedImage.id",
+ )
+ .$asScalar(),
+ );
+}
+
+/** Concats the file name (a bit misleadingly called `url` in the DB schema) with the root URL, giving the full URL for the image */
+export function concatUserSubmittedImagePrefix(
+ expr: Expression,
+) {
+ const eb = expressionBuilder();
+
+ return eb.fn("iif", [
+ eb(expr, "is not", null),
+ eb.fn("concat", [sql.lit(`${USER_SUBMITTED_IMAGE_ROOT}/`), expr]),
+ sql`null`,
+ ]);
+}
+
/** Prevents ParseJSONResultsPlugin from trying to parse this as JSON */
export function unJsonify(value: T) {
if (typeof value !== "string") {
diff --git a/app/utils/urls-img.ts b/app/utils/urls-img.ts
deleted file mode 100644
index ab23ec9a6..000000000
--- a/app/utils/urls-img.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-// TODO: separating this file from urls.ts is a temporary solution. The reason is that import.meta.env cannot currently be used in files that are consumed by plain Node.js
-
-import { IS_E2E_TEST_RUN } from "./e2e";
-
-const USER_SUBMITTED_IMAGE_ROOT =
- (process.env.NODE_ENV === "development" &&
- import.meta.env.VITE_PROD_MODE !== "true") ||
- IS_E2E_TEST_RUN
- ? "http://127.0.0.1:9000/sendou"
- : "https://sendou.nyc3.cdn.digitaloceanspaces.com";
-
-// TODO: move development images to minio and deprecate this hack
-// images with https are not hosted on spaces, this is used for local development
-export const conditionalUserSubmittedImage = (fileName: string) =>
- fileName.includes("https") ? fileName : userSubmittedImage(fileName);
-
-export const userSubmittedImage = (fileName: string) =>
- `${USER_SUBMITTED_IMAGE_ROOT}/${fileName}`;
diff --git a/app/utils/urls.ts b/app/utils/urls.ts
index 4656be612..8cba32663 100644
--- a/app/utils/urls.ts
+++ b/app/utils/urls.ts
@@ -498,8 +498,6 @@ export const preferenceEmojiUrl = (preference?: Preference) => {
return `/static-assets/img/emoji/${emoji}.svg`;
};
-export const tournamentLogoUrl = (identifier: string) =>
- `/static-assets/img/tournament-logos/${identifier}.png`;
export const TIER_PLUS_URL = "/static-assets/img/tiers/plus";
export const winnersImageUrl = ({
diff --git a/scripts/migrate-tournament-logos-to-s3.ts b/scripts/migrate-tournament-logos-to-s3.ts
new file mode 100644
index 000000000..61197a1a9
--- /dev/null
+++ b/scripts/migrate-tournament-logos-to-s3.ts
@@ -0,0 +1,171 @@
+import "dotenv/config";
+import fs from "node:fs";
+import path from "node:path";
+import { Readable } from "node:stream";
+import { db } from "~/db/sql";
+import { uploadStreamToS3 } from "~/features/img-upload/s3.server";
+import { databaseTimestampNow } from "~/utils/dates";
+import invariant from "~/utils/invariant";
+import { logger } from "~/utils/logger";
+
+const TOURNAMENT_LOGO_PATH = "public/static-assets/img/tournament-logos";
+const ADMIN_ID = 1;
+
+const LOGO_IDENTIFIER_TO_PATTERN: Record = {
+ sf: ["sendouq"],
+ pp: ["paddling pool"],
+ itz: ["in the zone"],
+ pn: ["picnic"],
+ pg: ["proving grounds"],
+ tc: ["triton"],
+ sos: ["swim or sink"],
+ ftiu: ["from the ink up"],
+ cc: ["coral clash"],
+ lu: ["level up"],
+ a41: ["all 4 one"],
+ fb: ["fry basket"],
+ d: ["the depths"],
+ e: ["eclipse"],
+ hc: ["homecoming"],
+ bio: ["bad ideas"],
+ ai: ["tenoch"],
+ mm: ["megalodon monday"],
+ ho: ["heaven 2 ocean"],
+ kr: ["kraken royale"],
+ mr: ["menu royale"],
+ bc: ["barracuda co"],
+ ci: ["crimson ink"],
+ me: ["mesozoic mayhem"],
+ ros: ["rain or shine"],
+ sj: ["squid junction"],
+ ss: ["silly sausage"],
+ ul: ["united-lan"],
+ sc: ["soul cup"],
+};
+
+async function uploadLogoFile(
+ filePath: string,
+ identifier: string,
+): Promise {
+ logger.info(`Uploading ${identifier}.png to S3...`);
+
+ const fileBuffer = fs.readFileSync(filePath);
+ const stream = Readable.from(fileBuffer);
+
+ const fileName = `tournament-logo-${identifier}.png`;
+
+ const s3Url = await uploadStreamToS3(stream, fileName);
+
+ invariant(s3Url, `Failed to upload ${identifier}.png to S3`);
+
+ logger.info(`Uploaded ${identifier}.png to ${s3Url}`);
+ return fileName;
+}
+
+async function createImageRecord(url: string): Promise {
+ const result = await db
+ .insertInto("UnvalidatedUserSubmittedImage")
+ .values({
+ url,
+ validatedAt: databaseTimestampNow(),
+ submitterUserId: ADMIN_ID,
+ })
+ .returning("id")
+ .executeTakeFirstOrThrow();
+
+ return result.id;
+}
+
+async function updateCalendarEvents(
+ imageId: number,
+ patterns: string[],
+): Promise {
+ const events = await db
+ .selectFrom("CalendarEvent")
+ .select(["id", "name"])
+ .where("avatarImgId", "is", null)
+ .execute();
+
+ let updateCount = 0;
+
+ for (const event of events) {
+ const normalizedEventName = event.name.toLowerCase();
+ const matches = patterns.some((pattern) =>
+ normalizedEventName.includes(pattern),
+ );
+
+ if (matches) {
+ await db
+ .updateTable("CalendarEvent")
+ .set({ avatarImgId: imageId })
+ .where("id", "=", event.id)
+ .execute();
+
+ logger.info(`Updated CalendarEvent ${event.id}: "${event.name}"`);
+ updateCount++;
+ }
+ }
+
+ return updateCount;
+}
+
+async function main() {
+ logger.info("Starting tournament logo migration to S3...");
+
+ const defaultLogoPath = path.join(TOURNAMENT_LOGO_PATH, "default.png");
+ if (fs.existsSync(defaultLogoPath)) {
+ logger.info("\n=== Uploading default tournament logo ===");
+ const defaultS3Url = await uploadLogoFile(defaultLogoPath, "default");
+ const defaultImageId = await createImageRecord(defaultS3Url);
+ logger.info(
+ `Created UnvalidatedUserSubmittedImage record for default logo with ID ${defaultImageId}\n`,
+ );
+ }
+
+ const logoFiles = fs
+ .readdirSync(TOURNAMENT_LOGO_PATH)
+ .filter((file) => file.endsWith(".png") && file !== "default.png");
+
+ logger.info(`Found ${logoFiles.length} logo files to migrate`);
+
+ let totalUpdated = 0;
+
+ for (const file of logoFiles) {
+ const identifier = file.replace(".png", "");
+ const patterns = LOGO_IDENTIFIER_TO_PATTERN[identifier];
+
+ if (!patterns) {
+ logger.warn(`No pattern mapping found for ${identifier}, skipping...`);
+ continue;
+ }
+
+ const filePath = path.join(TOURNAMENT_LOGO_PATH, file);
+
+ const s3Url = await uploadLogoFile(filePath, identifier);
+
+ const imageId = await createImageRecord(s3Url);
+ logger.info(
+ `Created UnvalidatedUserSubmittedImage record with ID ${imageId}`,
+ );
+
+ const updatedCount = await updateCalendarEvents(imageId, patterns);
+ totalUpdated += updatedCount;
+
+ logger.info(
+ `Updated ${updatedCount} CalendarEvent records for ${identifier}\n`,
+ );
+ }
+
+ logger.info("\n=== Migration Complete ===");
+ logger.info(`Total CalendarEvent records updated: ${totalUpdated}`);
+}
+
+main()
+ .then(() => {
+ logger.info("Script completed successfully");
+ process.exit(0);
+ })
+ .catch((error) => {
+ logger.error("Script failed:", error);
+ process.exit(1);
+ });
diff --git a/scripts/seed-art-urls.ts b/scripts/seed-art-urls.ts
new file mode 100644
index 000000000..79361cca1
--- /dev/null
+++ b/scripts/seed-art-urls.ts
@@ -0,0 +1,48 @@
+export const SEED_ART_URLS = [
+ "https://images.unsplash.com/photo-1611627474565-2367887415d1?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU1NTA2&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
+ "https://images.unsplash.com/photo-1625120742520-3f085b6894ba?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU1NTI2&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
+ "https://images.unsplash.com/photo-1656695607245-9686ce8e1a91?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU1NTQ1&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
+ "https://images.unsplash.com/photo-1673011526786-c7bf154d2c6d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU1NTY5&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
+ "https://images.unsplash.com/photo-1643833994700-059713434c71?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU1NTc2&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
+ "https://images.unsplash.com/photo-1541425284102-3d2c49dcb2bc?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU1NTk5&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
+ "https://images.unsplash.com/photo-1526946366170-7a81b443c4e6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU1NjA4&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
+ "https://images.unsplash.com/photo-1551368003-4d96079d0a99?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU1NjIz&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
+ "https://images.unsplash.com/photo-1595960684234-49d2a004e753?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU1NjMw&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
+ "https://images.unsplash.com/photo-1676275062470-4b628cf1ce01?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU1NjM5&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
+ "https://images.unsplash.com/photo-1602099081031-767e09dfdbad?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU2NDYz&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
+ "https://images.unsplash.com/photo-1547532182-bf296f6be875?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU2NDY5&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
+ "https://images.unsplash.com/photo-1601549838695-57580707e367?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU2NDc2&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
+ "https://images.unsplash.com/photo-1595361315899-72a291112b7b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU2NDgy&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
+ "https://images.unsplash.com/photo-1676275061266-a28f2f3f4552?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU2NDg4&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
+ "https://images.unsplash.com/44/C3EWdWzT8imxs0fKeKoC_blackforrest.JPG?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU2NDk1&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
+ "https://images.unsplash.com/photo-1628425242605-a0039d89e8b2?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU2NTAx&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
+ "https://images.unsplash.com/photo-1542917118-105d7d34b9ca?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU2NTEw&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
+ "https://images.unsplash.com/photo-1601549838695-57580707e367?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU2NTE3&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
+ "https://images.unsplash.com/photo-1660583490803-75c0307c805b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8ZG9nLDF8fHx8fHwxNjg4NTU2NTQ2&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
+ "https://images.unsplash.com/photo-1490042706304-06c664f6fd9a?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8ZG9nLDF8fHx8fHwxNjg4NTU2NTU4&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
+ "https://images.unsplash.com/photo-1676998652985-fd74c7b2a8d5?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8ZG9nLDF8fHx8fHwxNjg4NTU2NTY0&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
+ "https://images.unsplash.com/photo-1470390356535-d19bbf47bacb?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8ZG9nLDF8fHx8fHwxNjg4NTU2NTcx&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
+ "https://images.unsplash.com/photo-1503256207526-0d5d80fa2f47?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8ZG9nLDF8fHx8fHwxNjg4NTU2Njcy&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
+ "https://images.unsplash.com/photo-1604495589307-973bd87d7fa3?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8ZG9nLDF8fHx8fHwxNjg4NTU2Njg2&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
+ "https://images.unsplash.com/photo-1563476651637-3e5c5941d432?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU2NzEw&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
+ "https://images.unsplash.com/photo-1551567819-eef106c515b6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU2NzE2&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
+ "https://images.unsplash.com/photo-1553536590-d28c5d5dee92?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLDF8fHx8fHwxNjg4NTU2NzIx&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
+];
+
+export function getArtFilename(index: number): string {
+ return `art-${index}.jpg`;
+}
+
+export const SEED_TEAM_IMAGES = [
+ { filename: "alliance-rogue.png", teamId: 1 },
+ { filename: "default.png", teamId: null },
+];
+
+export const SEED_TOURNAMENT_IMAGES = [
+ { filename: "picnic.png", tournamentId: 1 },
+ { filename: "in-the-zone.png", tournamentId: 2 },
+ { filename: "paddling-pool.png", tournamentId: 3 },
+ { filename: "swim-or-sink.png", tournamentId: 4 },
+ { filename: "the-depths.png", tournamentId: 5 },
+ { filename: "luti.png", tournamentId: 6 },
+];
diff --git a/scripts/seed-images.ts b/scripts/seed-images.ts
new file mode 100644
index 000000000..dc98cb80a
--- /dev/null
+++ b/scripts/seed-images.ts
@@ -0,0 +1,244 @@
+import "dotenv/config";
+import { readFile } from "node:fs/promises";
+import { join } from "node:path";
+import { Readable } from "node:stream";
+import { S3 } from "@aws-sdk/client-s3";
+import { Upload } from "@aws-sdk/lib-storage";
+import { logger } from "~/utils/logger";
+import {
+ getArtFilename,
+ SEED_ART_URLS,
+ SEED_TEAM_IMAGES,
+ SEED_TOURNAMENT_IMAGES,
+} from "./seed-art-urls";
+
+async function checkMinioConnection(): Promise {
+ try {
+ const {
+ STORAGE_END_POINT,
+ STORAGE_ACCESS_KEY,
+ STORAGE_SECRET,
+ STORAGE_REGION,
+ STORAGE_BUCKET,
+ } = process.env;
+
+ if (
+ !(
+ STORAGE_ACCESS_KEY &&
+ STORAGE_END_POINT &&
+ STORAGE_SECRET &&
+ STORAGE_REGION &&
+ STORAGE_BUCKET
+ )
+ ) {
+ logger.warn("Storage configuration not found in environment");
+ return false;
+ }
+
+ const s3 = new S3({
+ endpoint: STORAGE_END_POINT,
+ forcePathStyle: false,
+ credentials: {
+ accessKeyId: STORAGE_ACCESS_KEY,
+ secretAccessKey: STORAGE_SECRET,
+ },
+ region: STORAGE_REGION,
+ });
+
+ await s3.headBucket({ Bucket: STORAGE_BUCKET });
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+async function downloadImage(url: string): Promise {
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(`Failed to download image: ${response.statusText}`);
+ }
+ const arrayBuffer = await response.arrayBuffer();
+ return Buffer.from(arrayBuffer);
+}
+
+async function readLocalImage(filename: string): Promise {
+ const imagePath = join(process.cwd(), "app", "db", "seed", "img", filename);
+ return await readFile(imagePath);
+}
+
+async function uploadToMinio(
+ imageBuffer: Buffer,
+ filename: string,
+): Promise {
+ const {
+ STORAGE_END_POINT,
+ STORAGE_ACCESS_KEY,
+ STORAGE_SECRET,
+ STORAGE_REGION,
+ STORAGE_BUCKET,
+ } = process.env;
+
+ const s3 = new S3({
+ endpoint: STORAGE_END_POINT,
+ forcePathStyle: false,
+ credentials: {
+ accessKeyId: STORAGE_ACCESS_KEY!,
+ secretAccessKey: STORAGE_SECRET!,
+ },
+ region: STORAGE_REGION,
+ });
+
+ const stream = Readable.from(imageBuffer);
+
+ const upload = new Upload({
+ client: s3,
+ params: {
+ Bucket: STORAGE_BUCKET!,
+ Key: filename,
+ Body: stream,
+ ACL: "public-read",
+ },
+ });
+
+ await upload.done();
+
+ return filename;
+}
+
+async function fileExistsInMinio(filename: string): Promise {
+ try {
+ const {
+ STORAGE_END_POINT,
+ STORAGE_ACCESS_KEY,
+ STORAGE_SECRET,
+ STORAGE_REGION,
+ STORAGE_BUCKET,
+ } = process.env;
+
+ const s3 = new S3({
+ endpoint: STORAGE_END_POINT,
+ forcePathStyle: false,
+ credentials: {
+ accessKeyId: STORAGE_ACCESS_KEY!,
+ secretAccessKey: STORAGE_SECRET!,
+ },
+ region: STORAGE_REGION,
+ });
+
+ await s3.headObject({
+ Bucket: STORAGE_BUCKET!,
+ Key: filename,
+ });
+
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+export async function seedImages(): Promise {
+ const minioAvailable = await checkMinioConnection();
+
+ if (!minioAvailable) {
+ logger.warn(
+ "⚠️ Minio is not available. Skipping image seeding. Make sure Docker is running if you want to seed images.",
+ );
+ return;
+ }
+
+ logger.info(`📥 Processing ${SEED_ART_URLS.length} art images`);
+
+ let successCount = 0;
+ let failCount = 0;
+ let skippedCount = 0;
+
+ for (let i = 0; i < SEED_ART_URLS.length; i++) {
+ const url = SEED_ART_URLS[i];
+ const filename = getArtFilename(i);
+ const smallFilename = filename.replace(/\.(\w+)$/, "-small.$1");
+
+ try {
+ const regularExists = await fileExistsInMinio(filename);
+ const smallExists = await fileExistsInMinio(smallFilename);
+
+ if (regularExists && smallExists) {
+ skippedCount++;
+ logger.info(
+ ` ↷ Files ${filename} and ${smallFilename} already exist in Minio`,
+ );
+ } else {
+ const imageBuffer = await downloadImage(url);
+
+ if (!regularExists) {
+ logger.info(` Uploading ${filename} to Minio...`);
+ await uploadToMinio(imageBuffer, filename);
+ }
+
+ if (!smallExists) {
+ logger.info(` Uploading ${smallFilename} to Minio...`);
+ await uploadToMinio(imageBuffer, smallFilename);
+ }
+
+ successCount++;
+ }
+ } catch (err) {
+ failCount++;
+ logger.error(
+ ` ✗ Failed to process ${filename}: ${(err as Error).message}`,
+ );
+ }
+ }
+
+ logger.info(
+ `\n✅ Art image seeding complete: ${successCount} uploaded, ${skippedCount} already existed, ${failCount} failed`,
+ );
+
+ logger.info(
+ `\n📥 Processing ${SEED_TEAM_IMAGES.length} team images and ${SEED_TOURNAMENT_IMAGES.length} tournament images`,
+ );
+
+ const localImages = [...SEED_TEAM_IMAGES, ...SEED_TOURNAMENT_IMAGES];
+
+ let localSuccessCount = 0;
+ let localFailCount = 0;
+ let localSkippedCount = 0;
+
+ for (const { filename } of localImages) {
+ const smallFilename = filename.replace(/\.(\w+)$/, "-small.$1");
+
+ try {
+ const regularExists = await fileExistsInMinio(filename);
+ const smallExists = await fileExistsInMinio(smallFilename);
+
+ if (regularExists && smallExists) {
+ localSkippedCount++;
+ logger.info(
+ ` ↷ Files ${filename} and ${smallFilename} already exist in Minio`,
+ );
+ } else {
+ const imageBuffer = await readLocalImage(filename);
+
+ if (!regularExists) {
+ logger.info(` Uploading ${filename} to Minio...`);
+ await uploadToMinio(imageBuffer, filename);
+ }
+
+ if (!smallExists) {
+ logger.info(` Uploading ${smallFilename} to Minio...`);
+ await uploadToMinio(imageBuffer, smallFilename);
+ }
+
+ localSuccessCount++;
+ }
+ } catch (err) {
+ localFailCount++;
+ logger.error(
+ ` ✗ Failed to process ${filename}: ${(err as Error).message}`,
+ );
+ }
+ }
+
+ logger.info(
+ `\n✅ Local image seeding complete: ${localSuccessCount} uploaded, ${localSkippedCount} already existed, ${localFailCount} failed`,
+ );
+}
diff --git a/scripts/setup.ts b/scripts/setup.ts
index 70893c414..e93700a31 100644
--- a/scripts/setup.ts
+++ b/scripts/setup.ts
@@ -3,6 +3,7 @@ import fs from "node:fs";
import { seed } from "~/db/seed";
import { db } from "~/db/sql";
import { logger } from "~/utils/logger";
+import { seedImages } from "./seed-images";
async function main() {
// Step 1: Create .env if it doesn't exist
@@ -33,6 +34,14 @@ async function main() {
process.exit(1);
}
}
+
+ // Step 3: Seed images to Minio
+ logger.info("🖼️ Seeding images to Minio...");
+ try {
+ await seedImages();
+ } catch (err) {
+ logger.error("Error seeding images:", (err as Error).message);
+ }
}
main();
diff --git a/types/vite.d.ts b/types/vite.d.ts
index ddffec0b7..b08a46a53 100644
--- a/types/vite.d.ts
+++ b/types/vite.d.ts
@@ -3,4 +3,5 @@ interface ImportMetaEnv {
VITE_PLAYWIRE_PUBLISHER_ID?: string;
VITE_PLAYWIRE_WEBSITE_ID?: string;
VITE_SITE_DOMAIN: string;
+ VITE_TOURNAMENT_DEFAULT_LOGO: string;
}
|