Refactor HACKY_resolvePicture away (#2606)

This commit is contained in:
Kalle 2025-11-08 13:54:02 +02:00 committed by GitHub
parent 62808206c1
commit 9296319d23
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
73 changed files with 880 additions and 584 deletions

View File

@ -17,6 +17,8 @@ STORAGE_REGION=us-east-1
STORAGE_BUCKET=sendou
STORAGE_URL=http://127.0.0.1:9000
VITE_TOURNAMENT_DEFAULT_LOGO="tournament-logo-default.png"
// Twitch integration for fetching streams
TWITCH_CLIENT_ID=
TWITCH_CLIENT_SECRET=

View File

@ -6,3 +6,5 @@ BASE_URL=https://example.com
SKALOP_SYSTEM_MESSAGE_URL=http://skalop.test
SKALOP_TOKEN=test
VITE_TOURNAMENT_DEFAULT_LOGO="tournament-logo-test.png"

View File

@ -169,7 +169,7 @@ function TournamentItem({
}
data-testid="tournament-search-item"
>
<img src={item.logoSrc} alt="" className={tournamentSearchStyles.logo} />
<img src={item.logoUrl} alt="" className={tournamentSearchStyles.logo} />
<div className={tournamentSearchStyles.itemTextsContainer}>
<span>{item.name}</span>
{additionalText() ? (

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
app/db/seed/img/default.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
app/db/seed/img/luti.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
app/db/seed/img/picnic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@ -75,6 +75,12 @@ import {
import { shortNanoid } from "~/utils/id";
import invariant from "~/utils/invariant";
import { mySlugify } from "~/utils/urls";
import {
getArtFilename,
SEED_ART_URLS,
SEED_TEAM_IMAGES,
SEED_TOURNAMENT_IMAGES,
} from "../../../scripts/seed-art-urls";
import type { QWeaponPool, Tables, UserMapModePreferences } from "../tables";
import {
ADMIN_TEST_AVATAR,
@ -131,6 +137,7 @@ const basicSeeds = (variation?: SeedVariation | null) => [
badgesToUsers,
badgeManagers,
patrons,
insertTeamAndTournamentImages,
organization,
calendarEvents,
calendarEventBadges,
@ -1108,7 +1115,8 @@ function calendarEventWithToTools(
"bracketUrl",
"authorId",
"tournamentId",
"organizationId"
"organizationId",
"avatarImgId"
) values (
$id,
$name,
@ -1117,7 +1125,8 @@ function calendarEventWithToTools(
$bracketUrl,
$authorId,
$tournamentId,
$organizationId
$organizationId,
$avatarImgId
)
`,
)
@ -1130,6 +1139,7 @@ function calendarEventWithToTools(
authorId: ADMIN_ID,
tournamentId,
organizationId: event === "PICNIC" ? 1 : null,
avatarImgId: getTournamentImageId(tournamentId),
});
const halfAnHourFromNow = new Date(Date.now() + 1000 * 60 * 30);
@ -1622,12 +1632,13 @@ const detailedTeam = (seedVariation?: SeedVariation | null) => () => {
sql
.prepare(
/* sql */ `
insert into "AllTeam" ("name", "customUrl", "inviteCode", "bio")
insert into "AllTeam" ("name", "customUrl", "inviteCode", "bio", "avatarImgId")
values (
'Alliance Rogue',
'alliance-rogue',
'${shortNanoid()}',
'${faker.lorem.paragraph()}'
'${faker.lorem.paragraph()}',
${getTeamImageId(1)}
)
`,
)
@ -1907,6 +1918,42 @@ const addUnvalidatedUserSubmittedImageStm = sql.prepare(/* sql */ `
@submitterUserId
) returning *
`);
const teamAndTournamentImages = new Map<string, number>();
function insertTeamAndTournamentImages() {
for (const { filename } of SEED_TEAM_IMAGES) {
const result = addUnvalidatedUserSubmittedImageStm.get({
validatedAt: dateToDatabaseTimestamp(new Date()),
url: filename,
submitterUserId: ADMIN_ID,
}) as Tables["UserSubmittedImage"];
teamAndTournamentImages.set(filename, result.id);
}
for (const { filename } of SEED_TOURNAMENT_IMAGES) {
const result = addUnvalidatedUserSubmittedImageStm.get({
validatedAt: dateToDatabaseTimestamp(new Date()),
url: filename,
submitterUserId: ADMIN_ID,
}) as Tables["UserSubmittedImage"];
teamAndTournamentImages.set(filename, result.id);
}
}
function getTeamImageId(teamId: number): number | null {
const teamImage = SEED_TEAM_IMAGES.find((img) => img.teamId === teamId);
if (!teamImage) return null;
return teamAndTournamentImages.get(teamImage.filename) ?? null;
}
function getTournamentImageId(tournamentId: number): number | null {
const tournamentImage = SEED_TOURNAMENT_IMAGES.find(
(img) => img.tournamentId === tournamentId,
);
if (!tournamentImage) return null;
return teamAndTournamentImages.get(tournamentImage.filename) ?? null;
}
const addArtUserMetadataStm = sql.prepare(/* sql */ `
insert into "ArtUserMetadata" (
"artId",
@ -1917,58 +1964,25 @@ const addArtUserMetadataStm = sql.prepare(/* sql */ `
@userId
)
`);
// get random image url: https://source.unsplash.com/random/?dog&1
const artImgUrls = [
"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",
];
const artImgFilenames = Array.from({ length: SEED_ART_URLS.length }, (_, i) =>
getArtFilename(i),
);
function arts() {
const artUsers = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17];
const allUsers = userIdsInRandomOrder();
const urls = [...artImgUrls];
const urls = [...artImgFilenames];
for (const userId of artUsers) {
for (let i = 0; i < faker.helpers.arrayElement([1, 2, 3, 3, 3, 4]); i++) {
const getUrl = () => {
if (urls.length === 0) {
return faker.image.url();
}
return urls.pop() ?? null;
};
const url = urls.pop()!;
if (!url) break;
const addedArt = addArtStm.get({
imgId: (
addUnvalidatedUserSubmittedImageStm.get({
validatedAt: dateToDatabaseTimestamp(new Date()),
url: getUrl(),
url,
submitterUserId: userId,
}) as Tables["UserSubmittedImage"]
).id,

View File

@ -3,8 +3,8 @@ import { jsonArrayFrom } from "kysely/helpers/sqlite";
import { cors } from "remix-utils/cors";
import { z } from "zod/v4";
import { db } from "~/db/sql";
import { concatUserSubmittedImagePrefix } from "~/utils/kysely.server";
import { notFoundIfFalsy, parseParams } from "~/utils/remix.server";
import { userSubmittedImage } from "~/utils/urls-img";
import { id } from "~/utils/zod";
import {
handleOptionsRequest,
@ -36,7 +36,9 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
"TournamentOrganization.description",
"TournamentOrganization.socials",
"TournamentOrganization.slug",
"UserSubmittedImage.url as logoUrl",
concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as(
"logoUrl",
),
jsonArrayFrom(
eb
.selectFrom("TournamentOrganizationMember")
@ -59,9 +61,7 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
id: organization.id,
name: organization.name,
description: organization.description,
logoUrl: organization.logoUrl
? userSubmittedImage(organization.logoUrl)
: null,
logoUrl: organization.logoUrl,
socialLinkUrls: organization.socials ?? [],
url: `https://sendou.ink/org/${organization.slug}`,
members: organization.members.map((member) => ({

View File

@ -2,8 +2,8 @@ import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { cors } from "remix-utils/cors";
import { z } from "zod/v4";
import { db } from "~/db/sql";
import { concatUserSubmittedImagePrefix } from "~/utils/kysely.server";
import { notFoundIfFalsy, parseParams } from "~/utils/remix.server";
import { userSubmittedImage } from "~/utils/urls-img";
import { id } from "~/utils/zod";
import {
handleOptionsRequest,
@ -29,23 +29,23 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
"UserSubmittedImage.id",
"Team.avatarImgId",
)
.select([
.select((eb) => [
"Team.id",
"Team.name",
"Team.customUrl",
"UserSubmittedImage.url as logoUrl",
concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as(
"logoUrl",
),
])
.where("Team.id", "=", teamId)
.executeTakeFirst(),
);
const logoUrl = team.logoUrl ? userSubmittedImage(team.logoUrl) : null;
const result: GetTeamResponse = {
id: team.id,
name: team.name,
logoUrl: team.logoUrl,
teamPageUrl: `https://sendou.ink/t/${team.customUrl}`,
logoUrl,
};
return await cors(request, json(result));

View File

@ -8,8 +8,8 @@ import * as TournamentRepository from "~/features/tournament/TournamentRepositor
import i18next from "~/modules/i18n/i18next.server";
import { nullifyingAvg } from "~/utils/arrays";
import { databaseTimestampToDate } from "~/utils/dates";
import { concatUserSubmittedImagePrefix } from "~/utils/kysely.server";
import { parseParams } from "~/utils/remix.server";
import { userSubmittedImage } from "~/utils/urls-img";
import { id } from "~/utils/zod";
import {
handleOptionsRequest,
@ -49,7 +49,9 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
"TournamentTeam.seed",
"TournamentTeam.createdAt",
"TournamentTeamCheckIn.checkedInAt",
"UserSubmittedImage.url as avatarUrl",
concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as(
"avatarUrl",
),
jsonObjectFrom(
eb
.selectFrom("AllTeam")
@ -61,7 +63,9 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
.whereRef("AllTeam.id", "=", "TournamentTeam.teamId")
.select([
"AllTeam.customUrl",
"UserSubmittedImage.url as logoUrl",
concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as(
"logoUrl",
),
"AllTeam.deletedAt",
]),
).as("team"),
@ -112,13 +116,6 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
const friendCodes = await TournamentRepository.friendCodesByTournamentId(id);
const logoUrl = (team: (typeof teams)[number]) => {
const url = team.team?.logoUrl ?? team.avatarUrl;
if (!url) return null;
return userSubmittedImage(url);
};
const result: GetTournamentTeamsResponse = teams.map((team) => {
return {
id: team.id,
@ -155,7 +152,7 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
joinedAt: databaseTimestampToDate(member.createdAt).toISOString(),
};
}),
logoUrl: logoUrl(team),
logoUrl: team.team?.logoUrl ?? team.avatarUrl,
mapPool:
team.mapPool.length > 0
? team.mapPool.map((map) => {

View File

@ -3,10 +3,8 @@ import { jsonArrayFrom } from "kysely/helpers/sqlite";
import { cors } from "remix-utils/cors";
import { z } from "zod/v4";
import { db } from "~/db/sql";
import { HACKY_resolvePicture } from "~/features/tournament/tournament-utils";
import { databaseTimestampToDate } from "~/utils/dates";
import { notFoundIfFalsy, parseParams } from "~/utils/remix.server";
import { userSubmittedImage } from "~/utils/urls-img";
import { id } from "~/utils/zod";
import {
handleOptionsRequest,
@ -72,9 +70,7 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
name: tournament.name,
startTime: databaseTimestampToDate(tournament.startTime).toISOString(),
url: `https://sendou.ink/to/${id}/brackets`,
logoUrl: tournament.logoUrl
? userSubmittedImage(tournament.logoUrl)
: `https://sendou.ink${HACKY_resolvePicture(tournament)}`,
logoUrl: tournament.logoUrl,
teams: {
checkedInCount: tournament.teams.filter((team) => team.checkedInAt)
.length,

View File

@ -2,6 +2,7 @@ import type { Transaction } from "kysely";
import { jsonArrayFrom } from "kysely/helpers/sqlite";
import { db } from "~/db/sql";
import type { DB, Tables } from "~/db/tables";
import { concatUserSubmittedImagePrefix } from "~/utils/kysely.server";
import { seededRandom } from "~/utils/random";
import type { ListedArt } from "./art-types";
@ -32,7 +33,7 @@ export async function findShowcaseArts(): Promise<ListedArt[]> {
.selectFrom("Art")
.innerJoin("User", "User.id", "Art.authorId")
.innerJoin("UserSubmittedImage", "UserSubmittedImage.id", "Art.imgId")
.select([
.select((eb) => [
"Art.id",
"Art.createdAt",
"Art.isShowcase",
@ -41,7 +42,9 @@ export async function findShowcaseArts(): Promise<ListedArt[]> {
"User.username",
"User.discordAvatar",
"User.commissionsOpen",
"UserSubmittedImage.url",
concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as(
"url",
),
])
.orderBy("Art.isShowcase", "desc")
.orderBy("Art.createdAt", "desc")
@ -85,7 +88,7 @@ export async function findShowcaseArtsByTag(
.innerJoin("Art", "Art.id", "TaggedArt.artId")
.innerJoin("User", "User.id", "Art.authorId")
.innerJoin("UserSubmittedImage", "UserSubmittedImage.id", "Art.imgId")
.select([
.select((eb) => [
"Art.id",
"Art.createdAt",
"Art.isShowcase",
@ -94,7 +97,9 @@ export async function findShowcaseArtsByTag(
"User.username",
"User.discordAvatar",
"User.commissionsOpen",
"UserSubmittedImage.url",
concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as(
"url",
),
])
.where("TaggedArt.tagId", "=", tagId)
.orderBy("Art.isShowcase", "desc")
@ -132,7 +137,7 @@ export async function findRecentlyUploadedArts(): Promise<ListedArt[]> {
.selectFrom("Art")
.innerJoin("User", "User.id", "Art.authorId")
.innerJoin("UserSubmittedImage", "UserSubmittedImage.id", "Art.imgId")
.select([
.select((eb) => [
"Art.id",
"Art.createdAt",
"Art.isShowcase",
@ -140,7 +145,9 @@ export async function findRecentlyUploadedArts(): Promise<ListedArt[]> {
"User.username",
"User.discordAvatar",
"User.commissionsOpen",
"UserSubmittedImage.url",
concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as(
"url",
),
])
.orderBy("Art.createdAt", "desc")
.limit(100)
@ -179,7 +186,9 @@ export async function findArtsByUserId(
"Art.description",
"Art.createdAt",
"Art.isShowcase",
"UserSubmittedImage.url",
concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as(
"url",
),
"User.discordId",
"User.username",
"User.discordAvatar",
@ -222,7 +231,9 @@ export async function findArtsByUserId(
"Art.description",
"Art.createdAt",
"Art.isShowcase",
"UserSubmittedImage.url",
concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as(
"url",
),
jsonArrayFrom(
eb
.selectFrom("TaggedArt")

View File

@ -1,7 +1,9 @@
export function previewUrl(url: string) {
// images with https are not hosted on spaces, this is used for local development
if (url.includes("https")) return url;
const lastDotIndex = url.lastIndexOf(".");
if (lastDotIndex === -1) return url;
const parts = url.split(".");
return `${parts[0]}-small.${parts[1]}`;
const urlWithoutExtension = url.slice(0, lastDotIndex);
const extension = url.slice(lastDotIndex + 1);
return `${urlWithoutExtension}-small.${extension}`;
}

View File

@ -17,7 +17,6 @@ import { usePagination } from "~/hooks/usePagination";
import { useSearchParamState } from "~/hooks/useSearchParamState";
import { databaseTimestampToDate } from "~/utils/dates";
import { artPage, newArtPage, userArtPage, userPage } from "~/utils/urls";
import { conditionalUserSubmittedImage } from "~/utils/urls-img";
import { ResponsiveMasonry } from "../../../modules/responsive-masonry/components/ResponsiveMasonry";
import { ART_PER_PAGE } from "../art-constants";
import type { ListedArt } from "../art-types";
@ -109,7 +108,7 @@ function BigImageDialog({ close, art }: { close: () => void; art: ListedArt }) {
>
<img
alt=""
src={conditionalUserSubmittedImage(art.url)}
src={art.url}
loading="lazy"
className="art__dialog__img"
onLoad={() => setImageLoaded(true)}
@ -179,7 +178,7 @@ function ImagePreview({
// biome-ignore lint/a11y/noStaticElementInteractions: Biome v2 migration
<img
alt=""
src={conditionalUserSubmittedImage(previewUrl(art.url))}
src={previewUrl(art.url)}
loading="lazy"
onClick={onClick}
onLoad={() => setImageLoaded(true)}

View File

@ -18,7 +18,6 @@ import invariant from "~/utils/invariant";
import { logger } from "~/utils/logger";
import type { SendouRouteHandle } from "~/utils/remix.server";
import { artPage, navIconUrl } from "~/utils/urls";
import { conditionalUserSubmittedImage } from "~/utils/urls-img";
import { metaTitle } from "../../../utils/remix";
import { action } from "../actions/art.new.server";
import { ART } from "../art-constants";
@ -113,12 +112,7 @@ function ImageUpload({
const id = React.useId();
if (data.art) {
return (
<img
src={conditionalUserSubmittedImage(previewUrl(data.art.url))}
alt=""
/>
);
return <img src={previewUrl(data.art.url)} alt="" />;
}
return (

View File

@ -569,7 +569,6 @@ export async function create(args: CreateArgs) {
? await createSubmittedImageInTrx({
trx,
avatarFileName: args.avatarFileName,
autoValidateAvatar: args.autoValidateAvatar,
userId: args.authorId,
})
: null;
@ -610,20 +609,18 @@ export async function create(args: CreateArgs) {
async function createSubmittedImageInTrx({
trx,
autoValidateAvatar,
avatarFileName,
userId,
}: {
trx: Transaction<DB>;
avatarFileName: string;
autoValidateAvatar?: boolean;
userId: number;
}) {
const result = await trx
.insertInto("UnvalidatedUserSubmittedImage")
.values({
url: avatarFileName,
validatedAt: autoValidateAvatar ? databaseTimestampNow() : null,
validatedAt: databaseTimestampNow(),
submitterUserId: userId,
})
.returning("id")
@ -644,7 +641,6 @@ export async function update(args: UpdateArgs) {
? await createSubmittedImageInTrx({
trx,
avatarFileName: args.avatarFileName,
autoValidateAvatar: args.autoValidateAvatar,
userId: args.authorId,
})
: null;

View File

@ -8,11 +8,9 @@ import { Image, ModeImage } from "~/components/Image";
import { TrophyIcon } from "~/components/icons/Trophy";
import { UsersIcon } from "~/components/icons/Users";
import { BadgeDisplay } from "~/features/badges/components/BadgeDisplay";
import { HACKY_resolvePicture } from "~/features/tournament/tournament-utils";
import { useIsMounted } from "~/hooks/useIsMounted";
import { databaseTimestampToDate } from "~/utils/dates";
import { navIconUrl } from "~/utils/urls";
import { userSubmittedImage } from "~/utils/urls-img";
import type { CalendarEvent, ShowcaseCalendarEvent } from "../calendar-types";
import { Tags } from "./Tags";
import styles from "./TournamentCard.module.css";
@ -54,14 +52,10 @@ export function TournamentCard({
>
<Link to={tournament.url} className={styles.card}>
<div className="stack horizontal justify-between">
{isHostedOnSendouInk ? (
{tournament.logoUrl ? (
<div className={styles.imgContainer}>
<img
src={
tournament.logoUrl
? userSubmittedImage(tournament.logoUrl)
: HACKY_resolvePicture(tournament)
}
src={tournament.logoUrl}
width={32}
height={32}
className={styles.avatarImg}
@ -142,7 +136,7 @@ function TournamentFirstPlacers({
<div className="stack xs horizontal items-center text-xs">
{firstPlacer.logoUrl ? (
<img
src={userSubmittedImage(firstPlacer.logoUrl)}
src={firstPlacer.logoUrl}
alt=""
width={24}
className="rounded-full"

View File

@ -33,7 +33,6 @@ import invariant from "~/utils/invariant";
import type { SendouRouteHandle } from "~/utils/remix.server";
import { pathnameFromPotentialURL } from "~/utils/strings";
import { CREATING_TOURNAMENT_DOC_LINK, FAQ_PAGE } from "~/utils/urls";
import { userSubmittedImage } from "~/utils/urls-img";
import {
CALENDAR_EVENT,
REG_CLOSES_AT_OPTIONS,
@ -719,14 +718,12 @@ function AvatarImageInput({
baseEvent?.tournament?.ctx.logoUrl &&
showPrevious
) {
const logoImgUrl = userSubmittedImage(baseEvent.tournament.ctx.logoUrl);
return (
<div className="stack horizontal md flex-wrap">
<input type="hidden" name="avatarImgId" value={baseEvent.avatarImgId} />
<div className="stack md items-center">
<img
src={logoImgUrl}
src={baseEvent.tournament.ctx.logoUrl}
alt=""
className="calendar-new__avatar-preview"
/>

View File

@ -8,7 +8,6 @@ import * as LeaderboardRepository from "~/features/leaderboards/LeaderboardRepos
import * as Seasons from "~/features/mmr/core/Seasons";
import { cache, IN_MILLISECONDS, ttl } from "~/utils/cache.server";
import { discordAvatarUrl, teamPage, userPage } from "~/utils/urls";
import { userSubmittedImage } from "~/utils/urls-img";
import * as ShowcaseTournaments from "../core/ShowcaseTournaments.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
@ -90,9 +89,7 @@ function cachedLeaderboards(): Promise<{
power: entry.power,
name: team.name,
url: teamPage(team.customUrl),
avatarUrl: team.avatarUrl
? userSubmittedImage(team.avatarUrl)
: null,
avatarUrl: team.avatarUrl,
};
}),
};

View File

@ -19,7 +19,7 @@ const createImage = async ({
validatedAt?: number | null;
}) => {
imageCounter++;
const url = `https://example.com/image-${submitterUserId}-${imageCounter}.png`;
const url = `image-${submitterUserId}-${imageCounter}.png`;
return ImageRepository.addNewImage({
submitterUserId,
@ -130,7 +130,7 @@ describe("deleteImageById", () => {
imageCounter++;
const art = await ArtRepository.insert({
authorId: 1,
url: `https://example.com/art-${imageCounter}.png`,
url: `art-${imageCounter}.png`,
validatedAt: Date.now(),
description: null,
linkedUsers: [],
@ -167,7 +167,7 @@ describe("countUnvalidatedArt", () => {
imageCounter++;
await ArtRepository.insert({
authorId: 1,
url: `https://example.com/art-${imageCounter}.png`,
url: `art-${imageCounter}.png`,
validatedAt: null,
description: null,
linkedUsers: [],
@ -177,7 +177,7 @@ describe("countUnvalidatedArt", () => {
imageCounter++;
await ArtRepository.insert({
authorId: 1,
url: `https://example.com/art-${imageCounter}.png`,
url: `art-${imageCounter}.png`,
validatedAt: null,
description: null,
linkedUsers: [],
@ -193,7 +193,7 @@ describe("countUnvalidatedArt", () => {
imageCounter++;
await ArtRepository.insert({
authorId: 1,
url: `https://example.com/art-${imageCounter}.png`,
url: `art-${imageCounter}.png`,
validatedAt: null,
description: null,
linkedUsers: [],
@ -203,7 +203,7 @@ describe("countUnvalidatedArt", () => {
imageCounter++;
await ArtRepository.insert({
authorId: 1,
url: `https://example.com/art-${imageCounter}.png`,
url: `art-${imageCounter}.png`,
validatedAt: Date.now(),
description: null,
linkedUsers: [],
@ -238,7 +238,7 @@ describe("countAllUnvalidated", () => {
imageCounter++;
await ImageRepository.addNewImage({
submitterUserId: 1,
url: `https://example.com/team-avatar-${imageCounter}.png`,
url: `team-avatar-${imageCounter}.png`,
validatedAt: null,
teamId: team.id,
type: "team-pfp",
@ -253,7 +253,7 @@ describe("countAllUnvalidated", () => {
imageCounter++;
await ArtRepository.insert({
authorId: 1,
url: `https://example.com/art-${imageCounter}.png`,
url: `art-${imageCounter}.png`,
validatedAt: null,
description: null,
linkedUsers: [],
@ -279,7 +279,7 @@ describe("countAllUnvalidated", () => {
imageCounter++;
await ImageRepository.addNewImage({
submitterUserId: 1,
url: `https://example.com/team-avatar-${imageCounter}.png`,
url: `team-avatar-${imageCounter}.png`,
validatedAt: Date.now(),
teamId: team.id,
type: "team-pfp",
@ -295,7 +295,7 @@ describe("countAllUnvalidated", () => {
imageCounter++;
await ImageRepository.addNewImage({
submitterUserId: 1,
url: `https://example.com/team-avatar-${imageCounter}.png`,
url: `team-avatar-${imageCounter}.png`,
validatedAt: null,
teamId: team.id,
type: "team-pfp",
@ -304,7 +304,7 @@ describe("countAllUnvalidated", () => {
imageCounter++;
await ArtRepository.insert({
authorId: 1,
url: `https://example.com/art-${imageCounter}.png`,
url: `art-${imageCounter}.png`,
validatedAt: null,
description: null,
linkedUsers: [],
@ -339,7 +339,7 @@ describe("countUnvalidatedBySubmitterUserId", () => {
imageCounter++;
await ImageRepository.addNewImage({
submitterUserId: 1,
url: `https://example.com/team-avatar-${imageCounter}.png`,
url: `team-avatar-${imageCounter}.png`,
validatedAt: null,
teamId: team.id,
type: "team-pfp",
@ -355,7 +355,7 @@ describe("countUnvalidatedBySubmitterUserId", () => {
imageCounter++;
await ImageRepository.addNewImage({
submitterUserId: 1,
url: `https://example.com/team-avatar-${imageCounter}.png`,
url: `team-avatar-${imageCounter}.png`,
validatedAt: Date.now(),
teamId: team.id,
type: "team-pfp",
@ -373,7 +373,7 @@ describe("countUnvalidatedBySubmitterUserId", () => {
imageCounter++;
await ImageRepository.addNewImage({
submitterUserId: 1,
url: `https://example.com/team1-avatar-${imageCounter}.png`,
url: `team1-avatar-${imageCounter}.png`,
validatedAt: null,
teamId: team1.id,
type: "team-pfp",
@ -382,7 +382,7 @@ describe("countUnvalidatedBySubmitterUserId", () => {
imageCounter++;
await ImageRepository.addNewImage({
submitterUserId: 2,
url: `https://example.com/team2-avatar-${imageCounter}.png`,
url: `team2-avatar-${imageCounter}.png`,
validatedAt: null,
teamId: team2.id,
type: "team-pfp",
@ -424,7 +424,7 @@ describe("validateImage", () => {
imageCounter++;
const img = await ImageRepository.addNewImage({
submitterUserId: 1,
url: `https://example.com/team-avatar-${imageCounter}.png`,
url: `team-avatar-${imageCounter}.png`,
validatedAt: null,
teamId: team.id,
type: "team-pfp",
@ -454,10 +454,10 @@ describe("unvalidatedImages", () => {
test("fetches unvalidated images with submitter info", async () => {
const team = await createTeam(1);
imageCounter++;
const url = `https://example.com/team-avatar-${imageCounter}.png`;
const filename = `team-avatar-${imageCounter}.png`;
await ImageRepository.addNewImage({
submitterUserId: 1,
url,
url: filename,
validatedAt: null,
teamId: team.id,
type: "team-pfp",
@ -468,7 +468,7 @@ describe("unvalidatedImages", () => {
expect(result).toHaveLength(1);
expect(result[0].submitterUserId).toBe(1);
expect(result[0].username).toBe("user1");
expect(result[0].url).toBe(url);
expect(result[0].url).toBe(`http://127.0.0.1:9000/sendou/${filename}`);
});
test("does not fetch validated images", async () => {
@ -476,7 +476,7 @@ describe("unvalidatedImages", () => {
imageCounter++;
await ImageRepository.addNewImage({
submitterUserId: 1,
url: `https://example.com/team-avatar-${imageCounter}.png`,
url: `team-avatar-${imageCounter}.png`,
validatedAt: Date.now(),
teamId: team.id,
type: "team-pfp",
@ -492,7 +492,7 @@ describe("unvalidatedImages", () => {
imageCounter++;
await ImageRepository.addNewImage({
submitterUserId: 1,
url: `https://example.com/team-avatar-${imageCounter}.png`,
url: `team-avatar-${imageCounter}.png`,
validatedAt: null,
teamId: team.id,
type: "team-pfp",
@ -501,7 +501,7 @@ describe("unvalidatedImages", () => {
imageCounter++;
await ArtRepository.insert({
authorId: 2,
url: `https://example.com/art-${imageCounter}.png`,
url: `art-${imageCounter}.png`,
validatedAt: null,
description: null,
linkedUsers: [],
@ -523,7 +523,7 @@ describe("unvalidatedImages", () => {
await ImageRepository.addNewImage({
submitterUserId: teamOwnerId,
url: `https://example.com/team-avatar-${i}.png`,
url: `team-avatar-${i}.png`,
validatedAt: null,
teamId: team.id,
type: "team-pfp",
@ -557,17 +557,17 @@ describe("addNewImage", () => {
test("creates image for team avatar", async () => {
const team = await createTeam(1);
imageCounter++;
const url = `https://example.com/team-avatar-${imageCounter}.png`;
const filename = `team-avatar-${imageCounter}.png`;
const img = await ImageRepository.addNewImage({
submitterUserId: 1,
url,
url: filename,
validatedAt: null,
teamId: team.id,
type: "team-pfp",
});
expect(img.url).toBe(url);
expect(img.url).toBe(filename);
expect(img.submitterUserId).toBe(1);
expect(img.validatedAt).toBeNull();
@ -578,17 +578,17 @@ describe("addNewImage", () => {
test("creates image for team banner", async () => {
const team = await createTeam(1);
imageCounter++;
const url = `https://example.com/team-banner-${imageCounter}.png`;
const filename = `team-banner-${imageCounter}.png`;
const img = await ImageRepository.addNewImage({
submitterUserId: 1,
url,
url: filename,
validatedAt: null,
teamId: team.id,
type: "team-banner",
});
expect(img.url).toBe(url);
expect(img.url).toBe(filename);
expect(img.submitterUserId).toBe(1);
expect(img.validatedAt).toBeNull();
@ -599,17 +599,17 @@ describe("addNewImage", () => {
test("creates image for organization avatar", async () => {
const org = await createOrganization(1);
imageCounter++;
const url = `https://example.com/org-avatar-${imageCounter}.png`;
const filename = `org-avatar-${imageCounter}.png`;
const img = await ImageRepository.addNewImage({
submitterUserId: 1,
url,
url: filename,
validatedAt: null,
organizationId: org.id,
type: "org-pfp",
});
expect(img.url).toBe(url);
expect(img.url).toBe(filename);
expect(img.submitterUserId).toBe(1);
expect(img.validatedAt).toBeNull();
@ -624,7 +624,7 @@ describe("addNewImage", () => {
const img = await ImageRepository.addNewImage({
submitterUserId: 1,
url: `https://example.com/team-avatar-${imageCounter}.png`,
url: `team-avatar-${imageCounter}.png`,
validatedAt,
teamId: team.id,
type: "team-pfp",
@ -642,7 +642,7 @@ describe("addNewImage", () => {
await ImageRepository.addNewImage({
submitterUserId: 1,
url: `https://example.com/team-avatar-${imageCounter}.png`,
url: `team-avatar-${imageCounter}.png`,
validatedAt: null,
teamId: team.id,
type: "team-pfp",

View File

@ -1,5 +1,6 @@
import { db } from "~/db/sql";
import { databaseTimestampNow } from "~/utils/dates";
import { concatUserSubmittedImagePrefix } from "~/utils/kysely.server";
import { IMAGES_TO_VALIDATE_AT_ONCE } from "./upload-constants";
import type { ImageUploadType } from "./upload-types";
@ -81,9 +82,11 @@ export function unvalidatedImages() {
"UnvalidatedUserSubmittedImage.submitterUserId",
"User.id",
)
.select([
.select((eb) => [
"UnvalidatedUserSubmittedImage.id",
"UnvalidatedUserSubmittedImage.url",
concatUserSubmittedImagePrefix(
eb.ref("UnvalidatedUserSubmittedImage.url"),
).as("url"),
"UnvalidatedUserSubmittedImage.submitterUserId",
"User.username",
])

View File

@ -5,7 +5,6 @@ import { FormWithConfirm } from "~/components/FormWithConfirm";
import { TrashIcon } from "~/components/icons/Trash";
import { Main } from "~/components/Main";
import { SubmitButton } from "~/components/SubmitButton";
import { userSubmittedImage } from "~/utils/urls-img";
import { action } from "../actions/upload.admin.server";
import { loader } from "../loaders/upload.admin.server";
@ -55,7 +54,7 @@ function ImageValidator() {
/>
</FormWithConfirm>
</div>
<img src={userSubmittedImage(image.url)} alt="" />
<img src={image.url} alt="" />
<Link to={`/u/${image.submitterUserId}`} className="text-xs">
From: {image.username}
</Link>

View File

@ -3,7 +3,10 @@ import type { InferResult } from "kysely";
import { jsonArrayFrom } from "kysely/helpers/sqlite";
import * as R from "remeda";
import { db } from "~/db/sql";
import { COMMON_USER_FIELDS } from "~/utils/kysely.server";
import {
COMMON_USER_FIELDS,
concatUserSubmittedImagePrefix,
} from "~/utils/kysely.server";
import { dateToDatabaseTimestamp } from "../../utils/dates";
import invariant from "../../utils/invariant";
import * as Seasons from "../mmr/core/Seasons";
@ -72,10 +75,12 @@ const teamLeaderboardBySeasonQuery = (season: number) =>
"UserSubmittedImage.id",
"Team.avatarImgId",
)
.select([
.select((eb) => [
"Team.id",
"Team.name",
"UserSubmittedImage.url as avatarUrl",
concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as(
"avatarUrl",
),
"Team.customUrl",
"TeamMemberWithSecondary.isMainTeam",
"TeamMemberWithSecondary.userId",

View File

@ -20,7 +20,6 @@ import {
userPage,
userSeasonsPage,
} from "~/utils/urls";
import { userSubmittedImage } from "~/utils/urls-img";
import { InfoPopover } from "../../../components/InfoPopover";
import { TopTenPlayer } from "../components/TopTenPlayer";
import {
@ -374,14 +373,12 @@ function TeamTable({
</div>
{entry.team?.avatarUrl ? (
<Link
// TODO: can be made better when $narrowNotNull lands
to={teamPage(entry.team.customUrl!)}
// TODO: can be made better when $narrowNotNull lands
title={entry.team.name!}
to={teamPage(entry.team.customUrl)}
title={entry.team.name}
>
<Avatar
size="xxs"
url={userSubmittedImage(entry.team.avatarUrl)}
url={entry.team.avatarUrl}
className="placements__avatar"
/>
</Link>

View File

@ -4,7 +4,10 @@ import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/sqlite";
import { db } from "~/db/sql";
import type { DB, TablesInsertable } from "~/db/tables";
import { databaseTimestampNow, dateToDatabaseTimestamp } from "~/utils/dates";
import { COMMON_USER_FIELDS } from "~/utils/kysely.server";
import {
COMMON_USER_FIELDS,
concatUserSubmittedImagePrefix,
} from "~/utils/kysely.server";
import { LFG } from "./lfg-constants";
export async function posts(user?: { id: number; plusTier: number | null }) {
@ -51,7 +54,9 @@ export async function posts(user?: { id: number; plusTier: number | null }) {
.select(({ eb: innerEb }) => [
"Team.id",
"Team.name",
"UserSubmittedImage.url as avatarUrl",
concatUserSubmittedImagePrefix(
innerEb.ref("UserSubmittedImage.url"),
).as("avatarUrl"),
jsonArrayFrom(
innerEb
.selectFrom("TeamMemberWithSecondary")

View File

@ -18,7 +18,6 @@ import { useIsMounted } from "~/hooks/useIsMounted";
import { useHasRole } from "~/modules/permissions/hooks";
import { databaseTimestampToDate } from "~/utils/dates";
import { lfgNewPostPage, navIconUrl, userPage } from "~/utils/urls";
import { userSubmittedImage } from "~/utils/urls-img";
import { hourDifferenceBetweenTimezones } from "../core/timezone";
import type { LFGLoaderData, TiersMap } from "../routes/lfg";
@ -145,9 +144,7 @@ function TeamLFGPost({
function PostTeamLogoHeader({ team }: { team: NonNullable<Post["team"]> }) {
return (
<div className="stack horizontal sm items-center font-bold">
{team.avatarUrl ? (
<Avatar size="xs" url={userSubmittedImage(team.avatarUrl)} />
) : null}
{team.avatarUrl ? <Avatar size="xs" url={team.avatarUrl} /> : null}
{team.name}
</div>
);

View File

@ -4,13 +4,15 @@ import { jsonArrayFrom, jsonBuildObject } from "kysely/helpers/sqlite";
import type { Tables, TablesInsertable } from "~/db/tables";
import { databaseTimestampNow, dateToDatabaseTimestamp } from "~/utils/dates";
import { shortNanoid } from "~/utils/id";
import { COMMON_USER_FIELDS } from "~/utils/kysely.server";
import { userSubmittedImage } from "~/utils/urls-img";
import {
COMMON_USER_FIELDS,
concatUserSubmittedImagePrefix,
tournamentLogoWithDefault,
} from "~/utils/kysely.server";
import { db } from "../../db/sql";
import invariant from "../../utils/invariant";
import type { Unwrapped } from "../../utils/types";
import type { AssociationVisibility } from "../associations/associations-types";
import { HACKY_resolvePicture } from "../tournament/tournament-utils";
import * as Scrim from "./core/Scrim";
import type { ScrimPost, ScrimPostUser } from "./scrims-types";
import { getPostRequestCensor, parseLutiDiv } from "./scrims-utils";
@ -117,11 +119,6 @@ const baseFindQuery = db
"ScrimPost.mapsTournamentId",
"CalendarEvent.tournamentId",
)
.leftJoin(
"UserSubmittedImage as TournamentAvatar",
"CalendarEvent.avatarImgId",
"TournamentAvatar.id",
)
.select((eb) => [
"ScrimPost.id",
"ScrimPost.at",
@ -141,12 +138,14 @@ const baseFindQuery = db
jsonBuildObject({
name: eb.ref("Team.name"),
customUrl: eb.ref("Team.customUrl"),
avatarUrl: eb.ref("UserSubmittedImage.url"),
avatarUrl: concatUserSubmittedImagePrefix(
eb.ref("UserSubmittedImage.url"),
),
}).as("team"),
jsonBuildObject({
id: eb.ref("CalendarEvent.tournamentId"),
name: eb.ref("CalendarEvent.name"),
avatarUrl: eb.ref("TournamentAvatar.url"),
avatarUrl: tournamentLogoWithDefault(eb),
}).as("mapsTournament"),
jsonArrayFrom(
eb
@ -173,7 +172,9 @@ const baseFindQuery = db
jsonBuildObject({
name: innerEb.ref("Team.name"),
customUrl: innerEb.ref("Team.customUrl"),
avatarUrl: innerEb.ref("UserSubmittedImage.url"),
avatarUrl: concatUserSubmittedImagePrefix(
innerEb.ref("UserSubmittedImage.url"),
),
}).as("team"),
jsonArrayFrom(
innerEb
@ -258,9 +259,7 @@ const mapDBRowToScrimPost = (
? {
id: row.mapsTournament.id,
name: row.mapsTournament.name!,
avatarUrl: row.mapsTournament.avatarUrl
? userSubmittedImage(row.mapsTournament.avatarUrl)
: HACKY_resolvePicture({ name: row.mapsTournament.name! }),
avatarUrl: row.mapsTournament.avatarUrl,
}
: null,
chatCode: row.chatCode ?? null,

View File

@ -22,7 +22,6 @@ import { useUser } from "~/features/auth/core/user";
import type { ModeShort } from "~/modules/in-game-lists/types";
import { databaseTimestampToDate } from "~/utils/dates";
import { scrimPage, tournamentRegisterPage, userPage } from "~/utils/urls";
import { userSubmittedImage } from "~/utils/urls-img";
import type { ScrimPost, ScrimPostRequest } from "../scrims-types";
import { formatFlexTimeDisplay } from "../scrims-utils";
import styles from "./ScrimCard.module.css";
@ -140,13 +139,7 @@ function ScrimTeamAvatar({
owner: ScrimPost["users"][number];
}) {
if (teamAvatarUrl) {
return (
<Avatar
size="xs"
url={userSubmittedImage(teamAvatarUrl)}
alt={teamName}
/>
);
return <Avatar size="xs" url={teamAvatarUrl} alt={teamName} />;
}
return <Avatar size="xs" user={owner} alt={owner.username} />;

View File

@ -21,7 +21,6 @@ import { SPLATTERCOLOR_SCREEN_ID } from "~/modules/in-game-lists/weapon-ids";
import { useHasPermission } from "~/modules/permissions/hooks";
import type { SerializeFrom } from "~/utils/remix";
import type { SendouRouteHandle } from "~/utils/remix.server";
import { userSubmittedImage } from "~/utils/urls-img";
import { Avatar } from "../../../components/Avatar";
import { Main } from "../../../components/Main";
import { databaseTimestampToDate } from "../../../utils/dates";
@ -192,10 +191,7 @@ function GroupCard({
className="stack horizontal items-center xs font-bold text-xs"
>
{group.team.avatarUrl ? (
<Avatar
url={userSubmittedImage(group.team.avatarUrl)}
size="xxs"
/>
<Avatar url={group.team.avatarUrl} size="xxs" />
) : null}
{group.team.name}
</Link>

View File

@ -13,7 +13,12 @@ import type {
import * as Seasons from "~/features/mmr/core/Seasons";
import { mostPopularArrayElement } from "~/utils/arrays";
import { dateToDatabaseTimestamp } from "~/utils/dates";
import { COMMON_USER_FIELDS, userChatNameColor } from "~/utils/kysely.server";
import {
COMMON_USER_FIELDS,
concatUserSubmittedImagePrefix,
tournamentLogoWithDefault,
userChatNameColor,
} from "~/utils/kysely.server";
import type { Unpacked } from "~/utils/types";
import { MATCHES_PER_SEASONS_PAGE } from "../user-page/user-page-constants";
@ -112,10 +117,12 @@ export async function findGroupById({
"AllTeam.avatarImgId",
"UserSubmittedImage.id",
)
.select([
.select((eb) => [
"AllTeam.name",
"AllTeam.customUrl",
"UserSubmittedImage.url as avatarUrl",
concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as(
"avatarUrl",
),
])
.where("AllTeam.id", "=", eb.ref("Group.teamId")),
).as("team"),
@ -229,19 +236,14 @@ const tournamentResultsSubQuery = (
"CalendarEvent.id",
"CalendarEventDate.eventId",
)
.leftJoin(
"UserSubmittedImage",
"CalendarEvent.avatarImgId",
"UserSubmittedImage.id",
)
.select([
.select((eb) => [
"TournamentResult.spDiff",
"TournamentResult.setResults",
"TournamentResult.tournamentId",
"TournamentResult.tournamentTeamId",
"CalendarEventDate.startTime as tournamentStartTime",
"CalendarEvent.name as tournamentName",
"UserSubmittedImage.url as logoUrl",
tournamentLogoWithDefault(eb).as("logoUrl"),
])
.whereRef("TournamentResult.tournamentId", "=", "Skill.tournamentId")
.where("TournamentResult.userId", "=", userId);

View File

@ -69,7 +69,6 @@ import {
specialWeaponImageUrl,
teamPage,
} from "~/utils/urls";
import { userSubmittedImage } from "~/utils/urls-img";
import { action } from "../actions/q.match.$id.server";
import { matchEndedAtIndex } from "../core/match";
import { loader } from "../loaders/q.match.$id.server";
@ -208,10 +207,7 @@ export default function QMatchPage() {
className="stack horizontal items-center xs font-bold"
>
{group.team.avatarUrl ? (
<Avatar
url={userSubmittedImage(group.team.avatarUrl)}
size="xxs"
/>
<Avatar url={group.team.avatarUrl} size="xxs" />
) : null}
{group.team.name}
</Link>

View File

@ -312,7 +312,7 @@ export async function usersThatTrusted(userId: number) {
.select([
...COMMON_USER_FIELDS,
"User.inGameName",
sql.raw<any>("null").as("teamId"),
sql<any>`null`.as("teamId"),
])
.where("TrustRelationship.trustReceiverUserId", "=", userId),
)

View File

@ -7,19 +7,22 @@ import { subsOfResult } from "~/features/team/team-utils";
import { databaseTimestampNow } from "~/utils/dates";
import { shortNanoid } from "~/utils/id";
import invariant from "~/utils/invariant";
import { COMMON_USER_FIELDS } from "~/utils/kysely.server";
import {
COMMON_USER_FIELDS,
concatUserSubmittedImagePrefix,
tournamentLogoOrNull,
} from "~/utils/kysely.server";
export function findAllUndisbanded() {
return db
.selectFrom("Team")
.leftJoin("UserSubmittedImage", "UserSubmittedImage.id", "Team.avatarImgId")
.select(({ eb }) => [
"Team.customUrl",
"Team.name",
eb
.selectFrom("UserSubmittedImage")
.whereRef("UserSubmittedImage.id", "=", "Team.avatarImgId")
.select("UserSubmittedImage.url")
.as("avatarSrc"),
concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as(
"avatarUrl",
),
jsonArrayFrom(
eb
.selectFrom("TeamMemberWithSecondary")
@ -74,8 +77,8 @@ export function findByCustomUrl(
"Team.bio",
"Team.customUrl",
"Team.css",
"AvatarImage.url as avatarSrc",
"BannerImage.url as bannerSrc",
concatUserSubmittedImagePrefix(eb.ref("AvatarImage.url")).as("avatarUrl"),
concatUserSubmittedImagePrefix(eb.ref("BannerImage.url")).as("bannerUrl"),
jsonArrayFrom(
eb
.selectFrom("TeamMemberWithSecondary")
@ -166,11 +169,7 @@ export async function findResultsById(teamId: number) {
"results.tournamentTeamId",
"CalendarEvent.name as tournamentName",
"CalendarEventDate.startTime",
eb
.selectFrom("UserSubmittedImage")
.select(["UserSubmittedImage.url"])
.whereRef("CalendarEvent.avatarImgId", "=", "UserSubmittedImage.id")
.as("logoUrl"),
tournamentLogoOrNull(eb).as("logoUrl"),
jsonArrayFrom(
eb
.selectFrom("results as results2")

View File

@ -7,10 +7,8 @@ import { UsersIcon } from "~/components/icons/Users";
import { Placement } from "~/components/Placement";
import { Table } from "~/components/Table";
import type { TeamResultsLoaderData } from "~/features/team/loaders/t.$customUrl.results.server";
import { HACKY_resolvePicture } from "~/features/tournament/tournament-utils";
import { databaseTimestampToDate } from "~/utils/dates";
import { tournamentLogoUrl, tournamentTeamPage, userPage } from "~/utils/urls";
import { userSubmittedImage } from "~/utils/urls-img";
import { tournamentTeamPage, userPage } from "~/utils/urls";
import styles from "./TeamResultsTable.module.css";
@ -33,10 +31,6 @@ export function TeamResultsTable({ results }: TeamResultsTableProps) {
</thead>
<tbody>
{results.map((result) => {
const logoUrl = result.logoUrl
? userSubmittedImage(result.logoUrl)
: HACKY_resolvePicture({ name: result.tournamentName });
return (
<tr key={result.tournamentId}>
<td className="pl-4 whitespace-nowrap">
@ -59,9 +53,9 @@ export function TeamResultsTable({ results }: TeamResultsTableProps) {
</td>
<td>
<div className="stack horizontal xs items-center">
{logoUrl !== tournamentLogoUrl("default") ? (
{result.logoUrl ? (
<img
src={logoUrl}
src={result.logoUrl}
alt=""
width={18}
height={18}

View File

@ -112,11 +112,11 @@ function ImageRemoveButtons() {
const { t } = useTranslation(["common", "team"]);
const { team } = useLoaderData<typeof loader>();
return team.avatarSrc || team.bannerSrc ? (
return team.avatarUrl || team.bannerUrl ? (
<div>
<Label>{t("team:forms.fields.removeImages")}</Label>
<ol className="team__image-links-list">
{team.avatarSrc ? (
{team.avatarUrl ? (
<li>
<FormWithConfirm
dialogHeading={t("team:deleteTeam.profilePicture.header", {
@ -131,7 +131,7 @@ function ImageRemoveButtons() {
</FormWithConfirm>
</li>
) : null}
{team.bannerSrc ? (
{team.bannerUrl ? (
<li>
<FormWithConfirm
dialogHeading={t("team:deleteTeam.banner.header", {

View File

@ -8,7 +8,6 @@ import { Main } from "~/components/Main";
import { metaTags } from "~/utils/remix";
import type { SendouRouteHandle } from "~/utils/remix.server";
import { bskyUrl, navIconUrl, TEAM_SEARCH_PAGE, teamPage } from "~/utils/urls";
import { userSubmittedImage } from "~/utils/urls-img";
import { loader } from "../loaders/t.$customUrl.server";
export { loader };
@ -21,9 +20,9 @@ export const meta: MetaFunction<typeof loader> = (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() {
<>
<div
className={clsx("team__banner", {
team__banner__placeholder: !team.bannerSrc,
team__banner__placeholder: !team.bannerUrl,
})}
style={{
"--team-banner-img": team.bannerSrc
? `url("${userSubmittedImage(team.bannerSrc)}")`
"--team-banner-img": team.bannerUrl
? `url("${team.bannerUrl}")`
: undefined,
}}
>
{team.avatarSrc ? (
{team.avatarUrl ? (
<div className="team__banner__avatar">
<div>
<img src={userSubmittedImage(team.avatarSrc)} alt="" />
<img src={team.avatarUrl} alt="" />
</div>
</div>
) : null}
@ -102,7 +101,7 @@ function TeamBanner() {
{team.name} <BskyLink />
</div>
</div>
{team.avatarSrc ? <div className="team__banner__avatar__spacer" /> : null}
{team.avatarUrl ? <div className="team__banner__avatar__spacer" /> : null}
</>
);
}

View File

@ -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 ? (
<img
src={userSubmittedImage(team.avatarSrc)}
src={team.avatarUrl}
alt=""
width={64}
height={64}

View File

@ -1,5 +1,4 @@
import * as TournamentRepository from "~/features/tournament/TournamentRepository.server";
import { HACKY_resolvePicture } from "~/features/tournament/tournament-utils";
import type { TournamentManagerDataSet } from "~/modules/brackets-manager/types";
import { isAdmin } from "~/modules/permissions/utils";
import { notFoundIfFalsy } from "~/utils/remix.server";
@ -53,19 +52,12 @@ function dataMapped({
(staff) => 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,

View File

@ -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. */

View File

@ -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",
},
});

View File

@ -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",
},
});

View File

@ -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",
},
});

View File

@ -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: {

View File

@ -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,

View File

@ -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,
};
};

View File

@ -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<typeof loader> = (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 (
<div className="stack horizontal md">
<Avatar
size="lg"
url={
data.organization.avatarUrl
? userSubmittedImage(data.organization.avatarUrl)
: undefined
}
/>
<Avatar size="lg" url={data.organization.avatarUrl ?? undefined} />
<div className="stack sm">
<div className="text-xl font-bold">{data.organization.name}</div>
{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({
<Placement placement={1} size={24} />
{winner.avatarUrl ? (
<img
src={userSubmittedImage(winner.avatarUrl)}
src={winner.avatarUrl}
alt=""
width={24}
height={24}

View File

@ -14,10 +14,13 @@ import { modesShort } from "~/modules/in-game-lists/modes";
import { nullFilledArray, nullifyingAvg } from "~/utils/arrays";
import { databaseTimestampNow, dateToDatabaseTimestamp } from "~/utils/dates";
import { shortNanoid } from "~/utils/id";
import { COMMON_USER_FIELDS, userChatNameColor } from "~/utils/kysely.server";
import {
COMMON_USER_FIELDS,
concatUserSubmittedImagePrefix,
tournamentLogoWithDefault,
userChatNameColor,
} from "~/utils/kysely.server";
import type { Unwrapped } from "~/utils/types";
import { userSubmittedImage } from "~/utils/urls-img";
import { HACKY_resolvePicture } from "./tournament-utils";
export type FindById = NonNullable<Unwrapped<typeof findById>>;
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<number>().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();
}

View File

@ -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,

View File

@ -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,
},
});
}

View File

@ -43,7 +43,7 @@ export function TournamentStream({
</div>
) : (
<div className="tournament__stream__user-container">
<Avatar size="xxs" url={tournament.ctx.logoSrc} />
<Avatar size="xxs" url={tournament.ctx.logoUrl} />
Cast <span className="text-lighter">{stream.twitchUserName}</span>
</div>
)}

View File

@ -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 (
<div className={clsx("stack lg", containerClassName("normal"))}>
<div className="tournament__logo-container">
<img
src={tournament.ctx.logoSrc}
src={tournament.ctx.logoUrl}
alt=""
className="tournament__logo"
width={124}
@ -102,13 +95,7 @@ export default function TournamentRegisterPage() {
className="stack horizontal sm items-center text-xs text-main-forced"
>
<Avatar
url={
tournament.ctx.organization.avatarUrl
? userSubmittedImage(
tournament.ctx.organization.avatarUrl,
)
: undefined
}
url={tournament.ctx.organization.avatarUrl ?? undefined}
size="xxs"
/>
{tournament.ctx.organization.name}
@ -159,12 +146,6 @@ export default function TournamentRegisterPage() {
</div>
</div>
</div>
{showAvatarPendingApprovalText ? (
<div className="text-warning text-sm font-semi-bold">
Tournament logo pending moderator review. Will be shown publicly once
approved.
</div>
) : null}
<TournamentRegisterInfoTabs />
</div>
);
@ -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 =

View File

@ -394,14 +394,12 @@ function RowContents({
}) {
const tournament = useTournament();
const logoUrl = tournament.tournamentTeamLogoSrc(team);
return (
<>
<div>{seed}</div>
<div>
{team.team?.logoUrl ? (
<Avatar url={tournament.tournamentTeamLogoSrc(team)} size="xxs" />
) : null}
</div>
<div>{logoUrl ? <Avatar url={logoUrl} size="xxs" /> : null}</div>
<div className="tournament__seeds__team-name">
{team.checkIns.length > 0 ? "✅ " : "❌ "} {team.name}
</div>

View File

@ -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<typeof loader> = (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,

View File

@ -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,

View File

@ -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"

View File

@ -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<number>("Art.id").distinct().as("count"))
.where((innerEb) =>
innerEb.or([
innerEb("Art.authorId", "=", sql.raw<any>("User.id")),
innerEb("ArtUserMetadata.userId", "=", sql.raw<any>("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",

View File

@ -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 (
<tr key={result.teamId}>
{hasHighlightCheckboxes && (
@ -107,9 +100,9 @@ export function UserResultsTable({
) : null}
{result.tournamentId ? (
<>
{logoUrl !== tournamentLogoUrl("default") ? (
{result.logoUrl ? (
<img
src={logoUrl}
src={result.logoUrl}
alt=""
width={18}
height={18}

View File

@ -24,7 +24,6 @@ import {
teamPage,
topSearchPlayerPage,
} from "~/utils/urls";
import { userSubmittedImage } from "~/utils/urls-img";
import { loader } from "../loaders/u.$identifier.index.server";
import type { UserPageLoaderData } from "../loaders/u.$identifier.server";
export { loader };
@ -94,7 +93,7 @@ function TeamInfo() {
{data.user.team.avatarUrl ? (
<img
alt=""
src={userSubmittedImage(data.user.team.avatarUrl)}
src={data.user.team.avatarUrl}
width={32}
height={32}
className="rounded-full"
@ -151,7 +150,7 @@ function SecondaryTeamsPopover() {
{team.avatarUrl ? (
<img
alt=""
src={userSubmittedImage(team.avatarUrl)}
src={team.avatarUrl}
width={24}
height={24}
className="rounded-full"

View File

@ -40,7 +40,6 @@ import type {
SeasonGroupMatch,
SeasonTournamentResult,
} from "~/features/sendouq-match/QMatchRepository.server";
import { HACKY_resolvePicture } from "~/features/tournament/tournament-utils";
import { useWeaponUsage } from "~/hooks/swr";
import { useIsMounted } from "~/hooks/useIsMounted";
import { modesShort } from "~/modules/in-game-lists/modes";
@ -57,7 +56,6 @@ import {
tournamentTeamPage,
userSeasonsPage,
} from "~/utils/urls";
import { userSubmittedImage } from "~/utils/urls-img";
import {
loader,
type UserSeasonsPageLoaderData,
@ -845,10 +843,6 @@ function GroupMatchResult({ match }: { match: SeasonGroupMatch }) {
}
function TournamentResult({ result }: { result: SeasonTournamentResult }) {
const logoUrl = result.logoUrl
? userSubmittedImage(result.logoUrl)
: HACKY_resolvePicture({ name: result.tournamentName });
return (
<div data-testid="seasons-tournament-result">
<Link
@ -859,7 +853,7 @@ function TournamentResult({ result }: { result: SeasonTournamentResult }) {
>
<div className="stack font-bold items-center text-lg text-center">
<img
src={logoUrl}
src={result.logoUrl}
width={36}
height={36}
alt=""

View File

@ -34,7 +34,7 @@ export const NotifyCheckInStartRoutine = new Routine({
tournamentId: tournament.ctx.id,
tournamentName: tournament.ctx.name,
},
pictureUrl: tournament.ctx.logoSrc,
pictureUrl: tournament.ctx.logoUrl,
},
userIds: tournament.ctx.teams
.flatMap((team) => team.members.map((member) => member.userId))

View File

@ -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<Tables, "User">) {
});
}
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<Tables, "CalendarEvent">,
) {
return eb.fn<string | null>("iif", [
eb("CalendarEvent.avatarImgId", "is not", null),
eb.fn<string>("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<Tables, "CalendarEvent">,
) {
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<T extends string | null>(
expr: Expression<T>,
) {
const eb = expressionBuilder<DB>();
return eb.fn<T extends null ? string | null : string>("iif", [
eb(expr, "is not", null),
eb.fn<string>("concat", [sql.lit(`${USER_SUBMITTED_IMAGE_ROOT}/`), expr]),
sql`null`,
]);
}
/** Prevents ParseJSONResultsPlugin from trying to parse this as JSON */
export function unJsonify<T>(value: T) {
if (typeof value !== "string") {

View File

@ -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}`;

View File

@ -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 = ({

View File

@ -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<string, string[]> = {
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<string> {
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<number> {
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<number> {
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);
});

48
scripts/seed-art-urls.ts Normal file
View File

@ -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 },
];

244
scripts/seed-images.ts Normal file
View File

@ -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<boolean> {
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<Buffer> {
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<Buffer> {
const imagePath = join(process.cwd(), "app", "db", "seed", "img", filename);
return await readFile(imagePath);
}
async function uploadToMinio(
imageBuffer: Buffer,
filename: string,
): Promise<string> {
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<boolean> {
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<void> {
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`,
);
}

View File

@ -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();

1
types/vite.d.ts vendored
View File

@ -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;
}