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