Supporter uploaded avatar

This commit is contained in:
Kalle 2026-06-14 14:08:23 +03:00
parent 62f1b33321
commit bd14f88eaa
110 changed files with 1743 additions and 191 deletions

View File

@ -110,7 +110,9 @@ export function Avatar({
alt = "",
...rest
}: {
user?: Pick<Tables["User"], "discordId" | "discordAvatar">;
user?: Pick<Tables["User"], "discordId" | "discordAvatar"> & {
customAvatarUrl?: string | null;
};
url?: string | null;
identiconInput?: string;
className?: string;
@ -122,21 +124,25 @@ export function Avatar({
const isClient = useHydrated();
const isIdenticon =
!url && (!user?.discordAvatar || isErrored || identiconInput);
!url &&
(!user?.customAvatarUrl || isErrored) &&
(!user?.discordAvatar || isErrored || identiconInput);
const identiconSource = identiconInput ?? user?.discordId ?? "unknown";
const src = url
? url
: user?.discordAvatar && !isErrored
? discordAvatarUrl({
discordAvatar: user.discordAvatar,
discordId: user.discordId,
size: size === "lg" || size === "xmd" ? "lg" : "sm",
})
: isClient
? generateIdenticon(identiconSource, dimensions[size], 7)
: BLANK_IMAGE_URL;
: user?.customAvatarUrl && !isErrored
? user.customAvatarUrl
: user?.discordAvatar && !isErrored
? discordAvatarUrl({
discordAvatar: user.discordAvatar,
discordId: user.discordId,
size: size === "lg" || size === "xmd" ? "lg" : "sm",
})
: isClient
? generateIdenticon(identiconSource, dimensions[size], 7)
: BLANK_IMAGE_URL;
return (
<div className={clsx(styles.avatarWrapper, className)}>

View File

@ -83,7 +83,9 @@ function ListItemContent({
suppressSubtitleHydrationWarning,
}: {
children: React.ReactNode;
user?: Pick<Tables["User"], "discordId" | "discordAvatar">;
user?: Pick<Tables["User"], "discordId" | "discordAvatar"> & {
customAvatarUrl?: string | null;
};
imageUrl?: string;
overlayIconUrl?: string;
subtitle?: React.ReactNode;
@ -155,7 +157,9 @@ export function ListLink({
isActive?: boolean;
imageUrl?: string;
overlayIconUrl?: string;
user?: Pick<Tables["User"], "discordId" | "discordAvatar">;
user?: Pick<Tables["User"], "discordId" | "discordAvatar"> & {
customAvatarUrl?: string | null;
};
subtitle?: React.ReactNode;
badge?: React.ReactNode;
badgeVariant?: "default" | "warning";
@ -190,7 +194,9 @@ export function ListButton({
badgeVariant,
}: {
children: React.ReactNode;
user?: Pick<Tables["User"], "discordId" | "discordAvatar">;
user?: Pick<Tables["User"], "discordId" | "discordAvatar"> & {
customAvatarUrl?: string | null;
};
subtitle?: string | null;
badge?: string | null;
badgeVariant?: "default" | "warning";

View File

@ -9,6 +9,7 @@ function user(id: number): CommonUser {
discordId: `discord${id}`,
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
};
}

View File

@ -1099,6 +1099,7 @@ export interface User {
customTheme: JSONColumnTypeNullable<CustomTheme>;
customUrl: string | null;
discordAvatar: string | null;
customAvatarImgId: number | null;
discordId: string;
discordName: string;
customName: string | null;

View File

@ -3,7 +3,10 @@ import { jsonArrayFrom } from "kysely/helpers/sqlite";
import { db } from "~/db/sql";
import type { DB, Tables } from "~/db/tables";
import { actorId } from "~/features/auth/core/user.server";
import { concatUserSubmittedImagePrefix } from "~/utils/kysely.server";
import {
concatUserSubmittedImagePrefix,
customAvatarUrl,
} from "~/utils/kysely.server";
import { seededRandom } from "~/utils/random";
import type { ListedArt } from "./art-types";
@ -37,6 +40,7 @@ export async function findShowcaseArts(): Promise<ListedArt[]> {
"User.username",
"User.discordAvatar",
"User.commissionsOpen",
customAvatarUrl(eb).as("customAvatarUrl"),
concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as(
"url",
),
@ -66,6 +70,7 @@ export async function findShowcaseArts(): Promise<ListedArt[]> {
author: {
commissionsOpen: a.commissionsOpen,
discordAvatar: a.discordAvatar,
customAvatarUrl: a.customAvatarUrl,
discordId: a.discordId,
username: a.username,
},
@ -92,6 +97,7 @@ export async function findShowcaseArtsByTag(
"User.username",
"User.discordAvatar",
"User.commissionsOpen",
customAvatarUrl(eb).as("customAvatarUrl"),
concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as(
"url",
),
@ -121,6 +127,7 @@ export async function findShowcaseArtsByTag(
author: {
commissionsOpen: a.commissionsOpen,
discordAvatar: a.discordAvatar,
customAvatarUrl: a.customAvatarUrl,
discordId: a.discordId,
username: a.username,
},
@ -140,6 +147,7 @@ export async function findRecentlyUploadedArts(): Promise<ListedArt[]> {
"User.username",
"User.discordAvatar",
"User.commissionsOpen",
customAvatarUrl(eb).as("customAvatarUrl"),
concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as(
"url",
),
@ -156,6 +164,7 @@ export async function findRecentlyUploadedArts(): Promise<ListedArt[]> {
author: {
commissionsOpen: a.commissionsOpen,
discordAvatar: a.discordAvatar,
customAvatarUrl: a.customAvatarUrl,
discordId: a.discordId,
username: a.username,
},
@ -197,6 +206,7 @@ export async function findArtsByUserId(
"User.username",
"User.discordAvatar",
"User.commissionsOpen",
customAvatarUrl(eb).as("customAvatarUrl"),
jsonArrayFrom(
eb
.selectFrom("TaggedArt")
@ -279,6 +289,7 @@ export async function findArtsByUserId(
discordId: row.discordId,
username: row.username,
discordAvatar: row.discordAvatar,
customAvatarUrl: row.customAvatarUrl,
commissionsOpen: row.commissionsOpen ?? undefined,
},
})),

View File

@ -20,6 +20,7 @@ export interface ListedArt {
discordId: Tables["User"]["discordId"];
username: Tables["User"]["username"];
discordAvatar: Tables["User"]["discordAvatar"];
customAvatarUrl: string | null;
commissionsOpen?: Tables["User"]["commissionsOpen"];
};
}

View File

@ -6,7 +6,7 @@ import { ASSOCIATION } from "~/features/associations/associations-constants";
import * as FriendRepository from "~/features/friends/FriendRepository.server";
import { LimitReachedError } from "~/utils/errors";
import { shortNanoid } from "~/utils/id";
import { COMMON_USER_FIELDS } from "~/utils/kysely.server";
import { commonUserSelect } from "~/utils/kysely.server";
import { logger } from "~/utils/logger";
interface FindOptions {
@ -61,7 +61,10 @@ const baseFindQuery = (options: FindOptions) =>
.selectFrom("AssociationMember")
.innerJoin("User", "User.id", "AssociationMember.userId")
.whereRef("AssociationMember.associationId", "=", "Association.id")
.select([...COMMON_USER_FIELDS, "AssociationMember.role"]),
.select((eb) => [
...commonUserSelect(eb),
"AssociationMember.role",
]),
).as("members"),
),
);

View File

@ -4,7 +4,7 @@ import { db } from "~/db/sql";
import type { DB } from "~/db/tables";
import { sortBadgesByFavorites } from "~/features/user-page/core/badge-sorting.server";
import invariant from "~/utils/invariant";
import { COMMON_USER_FIELDS } from "~/utils/kysely.server";
import { commonUserSelect } from "~/utils/kysely.server";
import { SPLATOON_3_XP_BADGE_VALUES } from "./badges-constants";
import { findSplatoon3XpBadgeValue } from "./badges-utils";
@ -21,7 +21,7 @@ const withAuthor = (eb: ExpressionBuilder<DB, "Badge">) => {
return jsonObjectFrom(
eb
.selectFrom("User")
.select(COMMON_USER_FIELDS)
.select((eb) => commonUserSelect(eb))
.whereRef("User.id", "=", "Badge.authorId"),
).as("author");
};
@ -31,7 +31,7 @@ const withManagers = (eb: ExpressionBuilder<DB, "Badge">) => {
eb
.selectFrom("BadgeManager")
.innerJoin("User", "BadgeManager.userId", "User.id")
.select(["userId", ...COMMON_USER_FIELDS])
.select((eb) => ["userId", ...commonUserSelect(eb)])
.whereRef("BadgeManager.badgeId", "=", "Badge.id"),
).as("managers");
};

View File

@ -28,6 +28,7 @@ import {
import invariant from "~/utils/invariant";
import {
concatUserSubmittedImagePrefix,
customAvatarUrl,
tournamentLogoWithDefault,
} from "~/utils/kysely.server";
import { calendarEventPage, tournamentPage } from "~/utils/urls";
@ -396,13 +397,14 @@ export async function findResultsByEventId(eventId: number) {
eb
.selectFrom("CalendarEventResultPlayer")
.leftJoin("User", "User.id", "CalendarEventResultPlayer.userId")
.select([
.select((eb) => [
"CalendarEventResultPlayer.userId as id",
"CalendarEventResultPlayer.name",
"User.username",
"User.discordId",
"User.discordAvatar",
"User.customUrl",
customAvatarUrl(eb).as("customAvatarUrl"),
])
.whereRef(
"CalendarEventResultPlayer.teamId",

View File

@ -37,6 +37,7 @@ export type ChatUser = Pick<
Tables["User"],
"username" | "discordId" | "discordAvatar" | "pronouns"
> & {
customAvatarUrl: string | null;
chatNameHue: string | null;
title?: string;
};

View File

@ -3,7 +3,7 @@ import { db } from "~/db/sql";
import type { DB } from "~/db/tables";
import { actorId } from "~/features/auth/core/user.server";
import { dateToDatabaseTimestamp } from "~/utils/dates";
import { COMMON_USER_FIELDS } from "~/utils/kysely.server";
import { commonUserSelect, customAvatarUrl } from "~/utils/kysely.server";
export async function findByUserIdWithActivity(userId: number) {
const [friendRows, teamMemberRows] = await Promise.all([
@ -99,8 +99,8 @@ function withLfgJoins<QB extends SelectQueryBuilder<any, any, any>>(qb: QB) {
"CalendarEventDate.eventId",
"CalendarEvent.id",
)
.select([
...COMMON_USER_FIELDS,
.select((eb) => [
...commonUserSelect(eb),
"CalendarEvent.name as tournamentName",
"TournamentTeam.tournamentId",
"CalendarEventDate.startTime as tournamentStartTime",
@ -121,7 +121,7 @@ export async function findPendingSentRequests(senderId: number) {
return db
.selectFrom("FriendRequest")
.innerJoin("User", "User.id", "FriendRequest.receiverId")
.select([
.select((eb) => [
"FriendRequest.id",
"FriendRequest.createdAt",
"User.id as receiverId",
@ -129,6 +129,7 @@ export async function findPendingSentRequests(senderId: number) {
"User.discordId as receiverDiscordId",
"User.discordAvatar as receiverDiscordAvatar",
"User.customUrl as receiverCustomUrl",
customAvatarUrl(eb).as("receiverCustomAvatarUrl"),
])
.where("FriendRequest.senderId", "=", senderId)
.orderBy("FriendRequest.createdAt", "desc")
@ -205,7 +206,7 @@ export async function findPendingReceivedRequests(receiverId: number) {
return db
.selectFrom("FriendRequest")
.innerJoin("User", "User.id", "FriendRequest.senderId")
.select([
.select((eb) => [
"FriendRequest.id",
"FriendRequest.createdAt",
"User.id as senderId",
@ -213,6 +214,7 @@ export async function findPendingReceivedRequests(receiverId: number) {
"User.discordId as senderDiscordId",
"User.discordAvatar as senderDiscordAvatar",
"User.customUrl as senderCustomUrl",
customAvatarUrl(eb).as("senderCustomAvatarUrl"),
])
.where("FriendRequest.receiverId", "=", receiverId)
.orderBy("FriendRequest.createdAt", "desc")
@ -329,13 +331,7 @@ export async function findMutualFriends({
eb("f2.userTwoId", "=", targetUserId),
]),
)
.select([
"User.id",
"User.username",
"User.discordId",
"User.discordAvatar",
"User.customUrl",
])
.select((eb) => commonUserSelect(eb))
.execute();
}
@ -374,7 +370,7 @@ export async function findFriendsByUserId(userId: number) {
eb("Friendship.userTwoId", "=", userId),
]),
)
.select([...COMMON_USER_FIELDS])
.select((eb) => commonUserSelect(eb))
.orderBy("User.username", "asc")
.execute();
}

View File

@ -17,6 +17,7 @@ import { SENDOUQ_LOOKING_PAGE, tournamentSubsPage } from "~/utils/urls";
export function FriendMenu({
discordId,
discordAvatar,
customAvatarUrl,
name,
subtitle,
badge,
@ -28,6 +29,7 @@ export function FriendMenu({
}: {
discordId: string;
discordAvatar: string | null;
customAvatarUrl: string | null;
name: string;
subtitle: string | null;
badge: string | null;
@ -59,7 +61,7 @@ export function FriendMenu({
<SendouMenu
trigger={
<ListButton
user={{ discordId, discordAvatar }}
user={{ discordId, discordAvatar, customAvatarUrl }}
subtitle={subtitle}
badge={badge}
>

View File

@ -34,6 +34,7 @@ export const loader = async () => {
username: friend.username,
discordId: friend.discordId,
discordAvatar: friend.discordAvatar,
customAvatarUrl: friend.customAvatarUrl,
url: userPage({
discordId: friend.discordId,
customUrl: friend.customUrl,
@ -68,6 +69,7 @@ export const loader = async () => {
username: tm.username,
discordId: tm.discordId,
discordAvatar: tm.discordAvatar,
customAvatarUrl: tm.customAvatarUrl,
url: userPage({
discordId: tm.discordId,
customUrl: tm.customUrl,
@ -94,6 +96,7 @@ export const loader = async () => {
username: req.senderUsername,
discordId: req.senderDiscordId,
discordAvatar: req.senderDiscordAvatar,
customAvatarUrl: req.senderCustomAvatarUrl,
url: userPage({
discordId: req.senderDiscordId,
customUrl: req.senderCustomUrl,
@ -108,6 +111,7 @@ export const loader = async () => {
username: req.receiverUsername,
discordId: req.receiverDiscordId,
discordAvatar: req.receiverDiscordAvatar,
customAvatarUrl: req.receiverCustomAvatarUrl,
url: userPage({
discordId: req.receiverDiscordId,
customUrl: req.receiverCustomUrl,

View File

@ -69,6 +69,7 @@ function IncomingRequestsSection() {
user={{
discordId: request.sender.discordId,
discordAvatar: request.sender.discordAvatar,
customAvatarUrl: request.sender.customAvatarUrl,
}}
size="xxsm"
/>
@ -120,6 +121,7 @@ function PendingRequestsSection() {
user={{
discordId: request.receiver.discordId,
discordAvatar: request.receiver.discordAvatar,
customAvatarUrl: request.receiver.customAvatarUrl,
}}
size="xxsm"
/>

View File

@ -381,6 +381,7 @@ function buildFirstPlacerEntry(
const members = withMembers
? rows.slice(0, MEMBERS_TO_SHOW).map((row) => ({
customUrl: row.customUrl,
customAvatarUrl: row.customAvatarUrl,
discordAvatar: row.discordAvatar,
discordId: row.discordId,
id: row.id,

View File

@ -83,13 +83,15 @@ function cachedLeaderboards(): Promise<{
power: entry.power,
name: entry.username,
url: userPage(entry),
avatarUrl: entry.discordAvatar
? discordAvatarUrl({
discordAvatar: entry.discordAvatar,
discordId: entry.discordId,
size: "sm",
})
: null,
avatarUrl: entry.customAvatarUrl
? entry.customAvatarUrl
: entry.discordAvatar
? discordAvatarUrl({
discordAvatar: entry.discordAvatar,
discordId: entry.discordId,
size: "sm",
})
: null,
})),
team: team
.filter((entry) => entry.team)

View File

@ -81,6 +81,11 @@ const PERKS = [
name: "customizedColorsUser",
extraInfo: false,
},
{
tier: 2,
name: "customAvatar",
extraInfo: true,
},
{
tier: 2,
name: "favoriteBadges",

View File

@ -10,7 +10,7 @@ import type {
RankedModeShort,
} from "~/modules/in-game-lists/types";
import {
COMMON_USER_FIELDS,
commonUserSelect,
concatUserSubmittedImagePrefix,
} from "~/utils/kysely.server";
import { dateToDatabaseTimestamp } from "../../utils/dates";
@ -62,7 +62,7 @@ const teamLeaderboardBySeasonQuery = (season: number) =>
eb
.selectFrom("SkillTeamUser")
.innerJoin("User", "SkillTeamUser.userId", "User.id")
.select(COMMON_USER_FIELDS)
.select((eb) => commonUserSelect(eb))
.whereRef("SkillTeamUser.skillId", "=", "Skill.id"),
).as("members"),
jsonArrayFrom(
@ -318,8 +318,8 @@ function xpLeaderboardQuery(where?: {
})
.innerJoin("SplatoonPlayer", "SplatoonPlayer.id", "Placement.playerId")
.leftJoin("User", "User.id", "SplatoonPlayer.userId")
.select([
...COMMON_USER_FIELDS,
.select((eb) => [
...commonUserSelect(eb),
"Placement.entryId",
"Placement.playerId",
"Placement.weaponSplId",
@ -369,8 +369,8 @@ export async function userSPLeaderboard(season: number) {
.onRef("Latest.userId", "=", "Skill.userId")
.onRef("Latest.maxId", "=", "Skill.id"),
)
.select([
...COMMON_USER_FIELDS,
.select((eb) => [
...commonUserSelect(eb),
"Skill.id as entryId",
"Skill.ordinal",
"User.plusSkippedForSeasonNth",

View File

@ -5,7 +5,7 @@ import { db } from "~/db/sql";
import type { DB, TablesInsertable } from "~/db/tables";
import { databaseTimestampNow, dateToDatabaseTimestamp } from "~/utils/dates";
import {
COMMON_USER_FIELDS,
commonUserSelect,
concatUserSubmittedImagePrefix,
userProfileWeapons,
} from "~/utils/kysely.server";
@ -31,7 +31,7 @@ export async function posts(user?: { id: number; plusTier: number | null }) {
.selectFrom("User")
.leftJoin("PlusTier", "PlusTier.userId", "User.id")
.select(({ eb: innerEb }) => [
...COMMON_USER_FIELDS,
...commonUserSelect(innerEb),
"User.languages",
"User.country",
"PlusTier.tier as plusTier",
@ -59,7 +59,7 @@ export async function posts(user?: { id: number; plusTier: number | null }) {
.innerJoin("User", "User.id", "TeamMemberWithSecondary.userId")
.leftJoin("PlusTier", "PlusTier.userId", "User.id")
.select(({ eb: innestEb }) => [
...COMMON_USER_FIELDS,
...commonUserSelect(innestEb),
"User.languages",
"User.country",
"PlusTier.tier as plusTier",

View File

@ -1,6 +1,6 @@
import { db } from "~/db/sql";
import type { Tables, TablesInsertable } from "~/db/tables";
import { COMMON_USER_FIELDS } from "~/utils/kysely.server";
import { commonUserSelect } from "~/utils/kysely.server";
import * as StreamRanking from "../sidebar/core/StreamRanking";
export function replaceAll(
@ -40,8 +40,8 @@ export function findXRankStreams() {
StreamRanking.minXpForStreamToBeShown(),
)
.where("LiveStream.twitch", "is not", null)
.select([
...COMMON_USER_FIELDS,
.select((eb) => [
...commonUserSelect(eb),
"SplatoonPlayer.peakXp",
"LiveStream.viewerCount",
"LiveStream.thumbnailUrl",

View File

@ -107,6 +107,7 @@ export default function MatchPageTestRoute() {
discordId: "123",
discordAvatar: null,
customUrl: "sendou",
customAvatarUrl: null,
},
{
id: 2,
@ -114,6 +115,7 @@ export default function MatchPageTestRoute() {
discordId: "456",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
{
id: 3,
@ -121,6 +123,7 @@ export default function MatchPageTestRoute() {
discordId: "789",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
{
id: 4,
@ -128,6 +131,7 @@ export default function MatchPageTestRoute() {
discordId: "012",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
],
bravo: [
@ -137,6 +141,7 @@ export default function MatchPageTestRoute() {
discordId: "345",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
{
id: 6,
@ -144,6 +149,7 @@ export default function MatchPageTestRoute() {
discordId: "678",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
{
id: 7,
@ -151,6 +157,7 @@ export default function MatchPageTestRoute() {
discordId: "901",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
{
id: 8,
@ -158,6 +165,7 @@ export default function MatchPageTestRoute() {
discordId: "234",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
],
}}
@ -195,6 +203,7 @@ export default function MatchPageTestRoute() {
tier: { name: "LEVIATHAN", isPlus: true },
plusTier: 1,
weaponPool: [0, 2000, 4000],
customAvatarUrl: null,
},
{
id: 2,
@ -205,6 +214,7 @@ export default function MatchPageTestRoute() {
tier: { name: "DIAMOND", isPlus: false },
plusTier: 2,
weaponPool: [20, 1100],
customAvatarUrl: null,
},
{
id: 3,
@ -213,6 +223,7 @@ export default function MatchPageTestRoute() {
discordAvatar: null,
customUrl: null,
tier: "CALCULATING",
customAvatarUrl: null,
},
{
id: 4,
@ -220,6 +231,7 @@ export default function MatchPageTestRoute() {
discordId: "012",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
{
id: 9,
@ -228,6 +240,7 @@ export default function MatchPageTestRoute() {
discordAvatar: null,
customUrl: null,
tier: { name: "GOLD", isPlus: true },
customAvatarUrl: null,
},
],
subbedOut: [9],
@ -244,6 +257,7 @@ export default function MatchPageTestRoute() {
tier: { name: "PLATINUM", isPlus: false },
plusTier: 3,
weaponPool: [40, 3000],
customAvatarUrl: null,
},
{
id: 6,
@ -252,6 +266,7 @@ export default function MatchPageTestRoute() {
discordAvatar: null,
customUrl: null,
tier: { name: "SILVER", isPlus: true },
customAvatarUrl: null,
},
{
id: 7,
@ -259,6 +274,7 @@ export default function MatchPageTestRoute() {
discordId: "901",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
{
id: 8,
@ -267,6 +283,7 @@ export default function MatchPageTestRoute() {
discordAvatar: null,
customUrl: null,
tier: { name: "BRONZE", isPlus: false },
customAvatarUrl: null,
},
],
},
@ -376,6 +393,7 @@ export default function MatchPageTestRoute() {
discordId: "123",
discordAvatar: null,
customUrl: "sendou",
customAvatarUrl: null,
},
skillDifference: {
calculated: true,
@ -391,6 +409,7 @@ export default function MatchPageTestRoute() {
discordId: "456",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
skillDifference: {
calculated: true,
@ -406,6 +425,7 @@ export default function MatchPageTestRoute() {
discordId: "789",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
skillDifference: {
calculated: false,
@ -420,6 +440,7 @@ export default function MatchPageTestRoute() {
discordId: "012",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
skillDifference: {
calculated: false,
@ -444,6 +465,7 @@ export default function MatchPageTestRoute() {
discordId: "345",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
skillDifference: {
calculated: true,
@ -459,6 +481,7 @@ export default function MatchPageTestRoute() {
discordId: "678",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
skillDifference: {
calculated: true,
@ -474,6 +497,7 @@ export default function MatchPageTestRoute() {
discordId: "901",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
skillDifference: {
calculated: true,
@ -489,6 +513,7 @@ export default function MatchPageTestRoute() {
discordId: "234",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
skillDifference: {
calculated: true,
@ -523,6 +548,7 @@ export default function MatchPageTestRoute() {
discordId: "123",
discordAvatar: null,
customUrl: "sendou",
customAvatarUrl: null,
},
{
id: 2,
@ -530,6 +556,7 @@ export default function MatchPageTestRoute() {
discordId: "456",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
{
id: 3,
@ -537,6 +564,7 @@ export default function MatchPageTestRoute() {
discordId: "789",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
{
id: 4,
@ -544,6 +572,7 @@ export default function MatchPageTestRoute() {
discordId: "012",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
],
bravo: [
@ -553,6 +582,7 @@ export default function MatchPageTestRoute() {
discordId: "345",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
{
id: 6,
@ -560,6 +590,7 @@ export default function MatchPageTestRoute() {
discordId: "678",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
{
id: 7,
@ -567,6 +598,7 @@ export default function MatchPageTestRoute() {
discordId: "901",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
{
id: 8,
@ -574,6 +606,7 @@ export default function MatchPageTestRoute() {
discordId: "234",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
],
},
@ -595,6 +628,7 @@ export default function MatchPageTestRoute() {
discordId: "123",
discordAvatar: null,
customUrl: "sendou",
customAvatarUrl: null,
},
{
id: 2,
@ -602,6 +636,7 @@ export default function MatchPageTestRoute() {
discordId: "456",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
{
id: 3,
@ -609,6 +644,7 @@ export default function MatchPageTestRoute() {
discordId: "789",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
{
id: 4,
@ -616,6 +652,7 @@ export default function MatchPageTestRoute() {
discordId: "012",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
],
bravo: [
@ -625,6 +662,7 @@ export default function MatchPageTestRoute() {
discordId: "345",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
{
id: 6,
@ -632,6 +670,7 @@ export default function MatchPageTestRoute() {
discordId: "678",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
{
id: 7,
@ -639,6 +678,7 @@ export default function MatchPageTestRoute() {
discordId: "901",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
{
id: 8,
@ -646,6 +686,7 @@ export default function MatchPageTestRoute() {
discordId: "234",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
],
},
@ -668,6 +709,7 @@ export default function MatchPageTestRoute() {
discordId: "123",
discordAvatar: null,
customUrl: "sendou",
customAvatarUrl: null,
},
{
id: 2,
@ -675,6 +717,7 @@ export default function MatchPageTestRoute() {
discordId: "456",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
{
id: 3,
@ -682,6 +725,7 @@ export default function MatchPageTestRoute() {
discordId: "789",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
{
id: 4,
@ -689,6 +733,7 @@ export default function MatchPageTestRoute() {
discordId: "012",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
],
bravo: [
@ -698,6 +743,7 @@ export default function MatchPageTestRoute() {
discordId: "345",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
{
id: 6,
@ -705,6 +751,7 @@ export default function MatchPageTestRoute() {
discordId: "678",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
{
id: 9,
@ -712,6 +759,7 @@ export default function MatchPageTestRoute() {
discordId: "567",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
{
id: 8,
@ -719,6 +767,7 @@ export default function MatchPageTestRoute() {
discordId: "234",
discordAvatar: null,
customUrl: null,
customAvatarUrl: null,
},
],
},

View File

@ -5,7 +5,7 @@ import { db } from "~/db/sql";
import type { DB } from "~/db/tables";
import type { MonthYear } from "~/features/plus-voting/core";
import { databaseTimestampNow, databaseTimestampToDate } from "~/utils/dates";
import { COMMON_USER_FIELDS } from "~/utils/kysely.server";
import { commonUserSelect } from "~/utils/kysely.server";
import type { Unwrapped } from "~/utils/types";
export type FindAllByMonthItem = Unwrapped<typeof findAllByMonth>;
@ -21,15 +21,15 @@ export async function findAllByMonth(args: MonthYear) {
jsonObjectFrom(
eb
.selectFrom("User")
.select(COMMON_USER_FIELDS)
.select((eb) => commonUserSelect(eb))
.whereRef("PlusSuggestion.authorId", "=", "User.id"),
).as("author"),
jsonObjectFrom(
eb
.selectFrom("User")
.leftJoin("PlusTier", "PlusSuggestion.suggestedId", "PlusTier.userId")
.select([
...COMMON_USER_FIELDS,
.select((eb) => [
...commonUserSelect(eb),
"User.bio",
"PlusTier.tier as plusTier",
])

View File

@ -9,7 +9,7 @@ import {
rangeToMonthYear,
} from "~/features/plus-voting/core";
import invariant from "~/utils/invariant";
import { COMMON_USER_FIELDS } from "~/utils/kysely.server";
import { commonUserSelect } from "~/utils/kysely.server";
import type { Unwrapped } from "~/utils/types";
import * as PlusVoting from "./core/PlusVoting";
@ -17,8 +17,8 @@ const resultsByMonthYearQuery = (args: MonthYear) =>
db
.selectFrom("PlusVotingResult")
.innerJoin("User", "PlusVotingResult.votedId", "User.id")
.select([
...COMMON_USER_FIELDS,
.select((eb) => [
...commonUserSelect(eb),
"PlusVotingResult.wasSuggested",
"PlusVotingResult.tier",
"PlusVotingResult.score",
@ -136,7 +136,7 @@ export type UsersForVoting = {
user: Pick<
Tables["User"],
"id" | "discordId" | "username" | "discordAvatar" | "bio"
>;
> & { customAvatarUrl: string | null };
suggestion?: PlusSuggestionRepository.FindAllByMonthItem;
}[];
@ -147,7 +147,7 @@ export async function usersForVoting(loggedInUser: {
const members = await db
.selectFrom("User")
.innerJoin("PlusTier", "PlusTier.userId", "User.id")
.select([...COMMON_USER_FIELDS, "User.bio"])
.select((eb) => [...commonUserSelect(eb), "User.bio"])
.where("PlusTier.tier", "=", loggedInUser.plusTier)
.execute();
@ -167,6 +167,7 @@ export async function usersForVoting(loggedInUser: {
discordId: member.discordId,
username: member.username,
discordAvatar: member.discordAvatar,
customAvatarUrl: member.customAvatarUrl,
bio: member.bio,
},
});
@ -179,6 +180,7 @@ export async function usersForVoting(loggedInUser: {
discordId: suggestion.suggested.discordId,
username: suggestion.suggested.username,
discordAvatar: suggestion.suggested.discordAvatar,
customAvatarUrl: suggestion.suggested.customAvatarUrl,
bio: suggestion.suggested.bio,
},
suggestion,

View File

@ -7,7 +7,7 @@ import { databaseTimestampNow, dateToDatabaseTimestamp } from "~/utils/dates";
import { ConcurrentModificationError } from "~/utils/errors";
import { shortNanoid } from "~/utils/id";
import {
COMMON_USER_FIELDS,
commonUserSelect,
concatUserSubmittedImagePrefix,
tournamentLogoWithDefault,
} from "~/utils/kysely.server";
@ -153,8 +153,8 @@ const baseFindQuery = db
eb
.selectFrom("ScrimPostUser")
.innerJoin("User", "ScrimPostUser.userId", "User.id")
.select([
...COMMON_USER_FIELDS,
.select((eb) => [
...commonUserSelect(eb),
"User.inGameName",
"ScrimPostUser.isOwner",
])
@ -186,8 +186,8 @@ const baseFindQuery = db
innerEb
.selectFrom("ScrimPostRequestUser")
.innerJoin("User", "ScrimPostRequestUser.userId", "User.id")
.select([
...COMMON_USER_FIELDS,
.select((eb) => [
...commonUserSelect(eb),
"User.inGameName",
"ScrimPostRequestUser.isOwner",
])

View File

@ -12,7 +12,7 @@ import { dateToDatabaseTimestamp } from "~/utils/dates";
import { shortNanoid } from "~/utils/id";
import invariant from "~/utils/invariant";
import {
COMMON_USER_FIELDS,
commonUserSelect,
concatUserSubmittedImagePrefix,
matchProfileWeapons,
tournamentLogoWithDefault,
@ -138,7 +138,7 @@ function groupWithTeamAndMembers(
),
)
.select((arrayEb) => [
...COMMON_USER_FIELDS,
...commonUserSelect(arrayEb),
"GroupMember.role",
"GroupMember.note",
"User.inGameName",
@ -229,7 +229,7 @@ const groupMatchResultsSubQuery = (eb: ExpressionBuilder<DB, "Skill">) => {
eb
.selectFrom("GroupMember")
.innerJoin("User", "GroupMember.userId", "User.id")
.select([...COMMON_USER_FIELDS])
.select((eb) => commonUserSelect(eb))
.whereRef(
"GroupMember.groupId",
"=",

View File

@ -95,6 +95,7 @@ export function resolveTimelineSpChanges(
discordId: m.discordId,
discordAvatar: m.discordAvatar,
customUrl: m.customUrl,
customAvatarUrl: m.customAvatarUrl,
},
skillDifference: m.skillDifference!,
}));

View File

@ -1,7 +1,7 @@
import { jsonObjectFrom } from "kysely/helpers/sqlite";
import { db } from "~/db/sql";
import { dateToDatabaseTimestamp } from "~/utils/dates";
import { COMMON_USER_FIELDS } from "~/utils/kysely.server";
import { commonUserSelect } from "~/utils/kysely.server";
import type { Unwrapped } from "~/utils/types";
export type ActiveMatchPlayersItem = Unwrapped<typeof activeMatchPlayers>;
@ -29,7 +29,7 @@ export function activeMatchPlayers() {
jsonObjectFrom(
eb
.selectFrom("User")
.select([...COMMON_USER_FIELDS, "User.twitch"])
.select((eb) => [...commonUserSelect(eb), "User.twitch"])
.whereRef("GroupMember.userId", "=", "User.id"),
).as("user"),
])

View File

@ -7,7 +7,11 @@ import { actorId } from "~/features/auth/core/user.server";
import { databaseTimestampNow, dateToDatabaseTimestamp } from "~/utils/dates";
import { shortNanoid } from "~/utils/id";
import invariant from "~/utils/invariant";
import { COMMON_USER_FIELDS, matchProfileWeapons } from "~/utils/kysely.server";
import {
commonUserSelect,
customAvatarUrl,
matchProfileWeapons,
} from "~/utils/kysely.server";
import { errorIsSqliteForeignKeyConstraintFailure } from "~/utils/sql";
import { userIsBanned } from "../ban/core/banned.server";
import { FULL_GROUP_SIZE } from "./q-constants";
@ -54,6 +58,7 @@ export async function findCurrentGroups() {
username: Tables["User"]["username"];
discordId: Tables["User"]["discordId"];
discordAvatar: Tables["User"]["discordAvatar"];
customAvatarUrl: string | null;
customUrl: Tables["User"]["customUrl"];
pronouns: Tables["User"]["pronouns"] | null;
mapModePreferences: Tables["User"]["mapModePreferences"];
@ -99,6 +104,7 @@ export async function findCurrentGroups() {
username: eb.ref("User.username"),
discordId: eb.ref("User.discordId"),
discordAvatar: eb.ref("User.discordAvatar"),
customAvatarUrl: customAvatarUrl(eb),
customUrl: eb.ref("User.customUrl"),
mapModePreferences: eb.ref("User.mapModePreferences"),
noScreen: eb.ref("User.noScreen"),
@ -433,8 +439,8 @@ export async function friendsAndTeammates(userId: number) {
const rows = await db
.selectFrom("TeamMemberWithSecondary")
.innerJoin("User", "User.id", "TeamMemberWithSecondary.userId")
.select([
...COMMON_USER_FIELDS,
.select((eb) => [
...commonUserSelect(eb),
"User.inGameName",
"TeamMemberWithSecondary.teamId",
])
@ -460,8 +466,8 @@ export async function friendsAndTeammates(userId: number) {
]),
),
)
.select([
...COMMON_USER_FIELDS,
.select((eb) => [
...commonUserSelect(eb),
"User.inGameName",
sql<any>`null`.as("teamId"),
]),

View File

@ -22,6 +22,7 @@ function createMember(overrides: Partial<SQGroupMember> = {}): SQGroupMember {
discordId: "123456789",
username: "TestUser",
discordAvatar: null,
customAvatarUrl: null,
customUrl: null,
role: "OWNER",
vc: "NO",
@ -75,6 +76,7 @@ function createOwnGroupMember(
discordId: "123456789",
username: "TestUser",
discordAvatar: null,
customAvatarUrl: null,
customUrl: null,
role: "OWNER",
vc: "NO",

View File

@ -46,6 +46,7 @@ export type SidebarFriend = {
name: string;
discordId: string;
discordAvatar: string | null;
customAvatarUrl: string | null;
url: string;
subtitle: string;
badge: string;
@ -191,13 +192,15 @@ async function combinedStreams(): Promise<SidebarStream[]> {
stream: {
id: `xrank-${row.id}`,
name: row.username,
imageUrl: row.discordAvatar
? discordAvatarUrl({
discordId: row.discordId,
discordAvatar: row.discordAvatar,
size: "sm",
})
: BLANK_IMAGE_URL,
imageUrl: row.customAvatarUrl
? row.customAvatarUrl
: row.discordAvatar
? discordAvatarUrl({
discordId: row.discordId,
discordAvatar: row.discordAvatar,
size: "sm",
})
: BLANK_IMAGE_URL,
url: row.twitchUsername
? twitchUrl(row.twitchUsername)
: userPage({ discordId: row.discordId, customUrl: row.customUrl }),
@ -365,6 +368,7 @@ function rowToSidebarFriend(
name: row.username,
discordId: row.discordId,
discordAvatar: row.discordAvatar,
customAvatarUrl: row.customAvatarUrl,
url: userPage({ discordId: row.discordId, customUrl: row.customUrl }),
subtitle,
badge,

View File

@ -10,7 +10,7 @@ import { databaseTimestampNow } from "~/utils/dates";
import { shortNanoid } from "~/utils/id";
import invariant from "~/utils/invariant";
import {
COMMON_USER_FIELDS,
commonUserSelect,
concatUserSubmittedImagePrefix,
tournamentLogoOrNull,
userProfileWeapons,
@ -175,7 +175,7 @@ export function findByCustomUrl(
.selectFrom("TeamMemberWithSecondary")
.innerJoin("User", "User.id", "TeamMemberWithSecondary.userId")
.select(({ eb: innerEb }) => [
...COMMON_USER_FIELDS,
...commonUserSelect(innerEb),
"TeamMemberWithSecondary.role",
"TeamMemberWithSecondary.customRole",
"TeamMemberWithSecondary.roleType",
@ -274,7 +274,7 @@ export async function findResultsById(teamId: number) {
)
.innerJoin("User", "User.id", "TournamentResult.userId")
.whereRef("results2.tournamentId", "=", "results.tournamentId")
.select(COMMON_USER_FIELDS),
.select((eb) => commonUserSelect(eb)),
).as("participants"),
])
.orderBy("CalendarEventDate.startTime", "desc")
@ -320,7 +320,7 @@ export async function teamsByMemberUserId(
eb
.selectFrom("TeamMemberWithSecondary as m2")
.innerJoin("User", "User.id", "m2.userId")
.select([...COMMON_USER_FIELDS, "m2.role", "m2.roleType"])
.select((eb) => [...commonUserSelect(eb), "m2.role", "m2.roleType"])
.whereRef("TeamMemberWithSecondary.teamId", "=", "m2.teamId"),
).as("members"),
])

View File

@ -18,6 +18,7 @@ const createMember = (userId: number) =>
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
customAvatarUrl: null,
}) as const;
const createTestTournament = (

View File

@ -51,6 +51,7 @@ describe("tournamentSummary()", () => {
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
customAvatarUrl: null,
})),
name: `Team ${teamId}`,
prefersNotToHost: 0,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -336,6 +336,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
discordId: "308483655515373570",
discordAvatar: "a5fff2b4706d99364e646cab28c8085b",
customUrl: "puma",
customAvatarUrl: null,
},
staff: [
{
@ -346,6 +347,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
discordAvatar: "2a569302e9545c6a07f8f8aa337d139d",
customUrl: "penis",
role: "ORGANIZER",
customAvatarUrl: null,
},
{
id: 3147,
@ -355,6 +357,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
discordAvatar: "85090cfe2e0da693355bcec9740c1eaa",
customUrl: "cookie",
role: "ORGANIZER",
customAvatarUrl: null,
},
{
id: 5212,
@ -364,6 +367,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
discordAvatar: "bd634c91f7d0475f3671956fa9a2110a",
customUrl: null,
role: "ORGANIZER",
customAvatarUrl: null,
},
{
id: 23120,
@ -373,6 +377,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
discordAvatar: "f1191c94b1da5396a06b620408017c1f",
customUrl: "weizihao",
role: "ORGANIZER",
customAvatarUrl: null,
},
],
bracketProgressionOverrides: [],
@ -406,6 +411,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "OWNER",
customAvatarUrl: null,
},
{
userId: 2899,
@ -422,6 +428,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
customAvatarUrl: null,
},
{
userId: 6114,
@ -438,6 +445,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
customAvatarUrl: null,
},
{
userId: 33963,
@ -454,6 +462,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
customAvatarUrl: null,
},
{
userId: 30176,
@ -470,6 +479,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
customAvatarUrl: null,
},
],
checkIns: [
@ -517,6 +527,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "OWNER",
customAvatarUrl: null,
},
{
userId: 21689,
@ -533,6 +544,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
customAvatarUrl: null,
},
{
userId: 3147,
@ -549,6 +561,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
customAvatarUrl: null,
},
{
userId: 2072,
@ -565,6 +578,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
customAvatarUrl: null,
},
],
checkIns: [
@ -607,6 +621,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "OWNER",
customAvatarUrl: null,
},
{
userId: 13370,
@ -623,6 +638,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
customAvatarUrl: null,
},
{
userId: 45,
@ -639,6 +655,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
customAvatarUrl: null,
},
{
userId: 1843,
@ -655,6 +672,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
customAvatarUrl: null,
},
],
checkIns: [
@ -697,6 +715,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "OWNER",
customAvatarUrl: null,
},
{
userId: 13590,
@ -713,6 +732,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
customAvatarUrl: null,
},
{
userId: 10757,
@ -729,6 +749,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
customAvatarUrl: null,
},
{
userId: 33047,
@ -745,6 +766,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
customAvatarUrl: null,
},
{
userId: 41024,
@ -761,6 +783,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
customAvatarUrl: null,
},
],
checkIns: [
@ -808,6 +831,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "OWNER",
customAvatarUrl: null,
},
{
userId: 29665,
@ -824,6 +848,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
customAvatarUrl: null,
},
{
userId: 46006,
@ -840,6 +865,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
customAvatarUrl: null,
},
{
userId: 33483,
@ -856,6 +882,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
customAvatarUrl: null,
},
{
userId: 11780,
@ -872,6 +899,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
customAvatarUrl: null,
},
{
userId: 37901,
@ -888,6 +916,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
customAvatarUrl: null,
},
],
checkIns: [
@ -930,6 +959,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "OWNER",
customAvatarUrl: null,
},
{
userId: 43662,
@ -946,6 +976,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
customAvatarUrl: null,
},
{
userId: 33491,
@ -962,6 +993,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
customAvatarUrl: null,
},
{
userId: 46467,
@ -978,6 +1010,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
customAvatarUrl: null,
},
{
userId: 46813,
@ -994,6 +1027,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
customAvatarUrl: null,
},
],
checkIns: [
@ -1041,6 +1075,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "OWNER",
customAvatarUrl: null,
},
{
userId: 33611,
@ -1057,6 +1092,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
customAvatarUrl: null,
},
{
userId: 31148,
@ -1073,6 +1109,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
customAvatarUrl: null,
},
{
userId: 33578,
@ -1089,6 +1126,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
streamViewerCount: null,
streamThumbnailUrl: null,
role: "REGULAR",
customAvatarUrl: null,
},
],
checkIns: [

File diff suppressed because it is too large Load Diff

View File

@ -97,6 +97,7 @@ export const testTournament = ({
teams: nTeams(participant.length, Math.min(...participant)),
author: {
customUrl: null,
customAvatarUrl: null,
discordAvatar: null,
discordId: "123",
username: "test",

View File

@ -7,6 +7,7 @@ import { shortNanoid } from "~/utils/id";
import invariant from "~/utils/invariant";
import {
concatUserSubmittedImagePrefix,
customAvatarUrl,
matchProfileWeapons,
} from "~/utils/kysely.server";
import { errorIsSqliteForeignKeyConstraintFailure } from "~/utils/sql";
@ -64,6 +65,7 @@ type TournamentLFGMemberObject = {
username: Tables["User"]["username"];
discordId: Tables["User"]["discordId"];
discordAvatar: Tables["User"]["discordAvatar"];
customAvatarUrl: string | null;
customUrl: Tables["User"]["customUrl"];
languages: Tables["User"]["languages"];
vc: Tables["User"]["vc"];
@ -108,6 +110,7 @@ export async function findLookingTeamsByTournamentId(tournamentId: number) {
username: eb.ref("User.username"),
discordId: eb.ref("User.discordId"),
discordAvatar: eb.ref("User.discordAvatar"),
customAvatarUrl: customAvatarUrl(eb),
customUrl: eb.ref("User.customUrl"),
languages: eb.ref("User.languages"),
vc: eb.ref("User.vc"),
@ -147,6 +150,7 @@ export async function findSubGroups(tournamentId: number) {
username: eb.ref("User.username"),
discordId: eb.ref("User.discordId"),
discordAvatar: eb.ref("User.discordAvatar"),
customAvatarUrl: customAvatarUrl(eb),
customUrl: eb.ref("User.customUrl"),
languages: eb.ref("User.languages"),
vc: eb.ref("User.vc"),

View File

@ -28,6 +28,7 @@ export type LFGGroupMember = {
username: string;
discordId: string;
discordAvatar: string | null;
customAvatarUrl: string | null;
customUrl: string | null;
languages: string[];
vc: "YES" | "NO" | "LISTEN_ONLY" | null;

View File

@ -118,6 +118,7 @@ async function subsMode({
username: member.username,
discordId: member.discordId,
discordAvatar: member.discordAvatar,
customAvatarUrl: member.customAvatarUrl,
customUrl: member.customUrl,
vc: member.vc,
languages,
@ -160,6 +161,7 @@ async function resolveOwnTeam({
username: m.username,
discordId: m.discordId,
discordAvatar: m.discordAvatar,
customAvatarUrl: m.customAvatarUrl,
customUrl: m.customUrl,
languages: [],
vc: null,
@ -200,6 +202,7 @@ function transformMembers(
username: m.username,
discordId: m.discordId,
discordAvatar: m.discordAvatar,
customAvatarUrl: m.customAvatarUrl,
customUrl: m.customUrl,
languages,
vc: m.vc,

View File

@ -4,6 +4,7 @@ import { db } from "~/db/sql";
import { TournamentMatchStatus, type TournamentRoundMaps } from "~/db/tables";
import type { ModeShort, StageId } from "~/modules/in-game-lists/types";
import invariant from "~/utils/invariant";
import { customAvatarUrl } from "~/utils/kysely.server";
import type { Unwrapped } from "~/utils/types";
const opponentOneId = sql<number>`"TournamentMatch"."opponentOne" ->> '$.id'`;
@ -52,7 +53,7 @@ export async function findMatchById(id: number) {
eb
.selectFrom("TournamentTeamMember")
.innerJoin("User", "User.id", "TournamentTeamMember.userId")
.select([
.select((eb) => [
"User.id",
"User.username",
"TournamentTeamMember.tournamentTeamId",
@ -65,6 +66,7 @@ export async function findMatchById(id: number) {
"User.customUrl",
"User.discordAvatar",
"User.pronouns",
customAvatarUrl(eb).as("customAvatarUrl"),
])
.where(({ or, eb: innerEb }) =>
or([
@ -405,12 +407,13 @@ export function findByTournamentTeamId(tournamentTeamId: number) {
"otherTeam.id",
),
)
.select([
.select((eb) => [
"User.id",
"User.username",
"User.discordAvatar",
"User.discordId",
"User.customUrl",
customAvatarUrl(eb).as("customAvatarUrl"),
])
.whereRef(
"TournamentMatchGameResult.matchId",

View File

@ -223,12 +223,14 @@ function buildSetEndingData({
discordId: string;
discordAvatar: string | null;
customUrl: string | null;
customAvatarUrl: string | null;
}): CommonUser => ({
id: m.userId,
username: m.username,
discordId: m.discordId,
discordAvatar: m.discordAvatar,
customUrl: m.customUrl,
customAvatarUrl: m.customAvatarUrl,
});
const teamOneMembersMap = new Map(

View File

@ -156,6 +156,7 @@ function resolveTimelineMaps(
discordId: u.discordId,
discordAvatar: u.discordAvatar,
customUrl: u.customUrl,
customAvatarUrl: u.customAvatarUrl,
}));
return data.results.map((result, mapIndex) => {
@ -398,6 +399,7 @@ function TournamentMatchRosterTab({
discordId: m.discordId,
discordAvatar: m.discordAvatar,
customUrl: m.customUrl,
customAvatarUrl: m.customAvatarUrl,
inGameName: m.inGameName,
})),
subbedOut,

View File

@ -15,8 +15,9 @@ import {
dateToDatabaseTimestamp,
} from "~/utils/dates";
import {
COMMON_USER_FIELDS,
commonUserSelect,
concatUserSubmittedImagePrefix,
customAvatarUrl,
tournamentLogoWithDefault,
} from "~/utils/kysely.server";
import { mySlugify } from "~/utils/urls";
@ -74,10 +75,10 @@ export async function findBySlug(slug: string) {
eb
.selectFrom("TournamentOrganizationMember")
.innerJoin("User", "User.id", "TournamentOrganizationMember.userId")
.select([
.select((eb) => [
"TournamentOrganizationMember.role",
"TournamentOrganizationMember.roleDisplayName",
...COMMON_USER_FIELDS,
...commonUserSelect(eb),
])
.whereRef(
"TournamentOrganizationMember.organizationId",
@ -250,7 +251,11 @@ const findEventsBaseQuery = (organizationId: number) =>
innerEb
.selectFrom("TournamentResult as WinnerResult")
.innerJoin("User", "User.id", "WinnerResult.userId")
.select(["User.discordAvatar", "User.discordId"])
.select((winnerEb) => [
"User.discordAvatar",
"User.discordId",
customAvatarUrl(winnerEb).as("customAvatarUrl"),
])
.whereRef(
"WinnerResult.tournamentTeamId",
"=",
@ -284,7 +289,11 @@ const findEventsBaseQuery = (organizationId: number) =>
"User.id",
"CalendarEventResultPlayer.userId",
)
.select(["User.discordAvatar", "User.discordId"])
.select((playerEb) => [
"User.discordAvatar",
"User.discordId",
customAvatarUrl(playerEb).as("customAvatarUrl"),
])
.whereRef(
"CalendarEventResultPlayer.teamId",
"=",
@ -617,11 +626,11 @@ export function allBannedUsersByOrganizationId(organizationId: number) {
return db
.selectFrom("TournamentOrganizationBannedUser")
.innerJoin("User", "User.id", "TournamentOrganizationBannedUser.userId")
.select([
.select((eb) => [
"TournamentOrganizationBannedUser.privateNote",
"TournamentOrganizationBannedUser.updatedAt",
"TournamentOrganizationBannedUser.expiresAt",
...COMMON_USER_FIELDS,
...commonUserSelect(eb),
])
.where(
"TournamentOrganizationBannedUser.organizationId",

View File

@ -106,6 +106,7 @@ async function calendarEventPoints(
leaderboardInfo.set(player.id, {
user: {
customUrl: player.customUrl,
customAvatarUrl: player.customAvatarUrl,
discordAvatar: player.discordAvatar,
discordId: player.discordId!,
id: player.id,

View File

@ -5,7 +5,7 @@ import { db } from "~/db/sql";
import type { DB, Tables, TournamentAuditLogMetadata } from "~/db/tables";
import { actorId } from "~/features/auth/core/user.server";
import { databaseTimestampNow, dateToDatabaseTimestamp } from "~/utils/dates";
import { COMMON_USER_FIELDS } from "~/utils/kysely.server";
import { commonUserSelect } from "~/utils/kysely.server";
export const AUDIT_LOG_PAGE_SIZE = 30;
@ -138,13 +138,13 @@ export function findByTournamentId({
jsonObjectFrom(
eb
.selectFrom("User")
.select(COMMON_USER_FIELDS)
.select((eb) => commonUserSelect(eb))
.whereRef("User.id", "=", "TournamentAuditLog.actorUserId"),
).as("actor"),
jsonObjectFrom(
eb
.selectFrom("User")
.select(COMMON_USER_FIELDS)
.select((eb) => commonUserSelect(eb))
.whereRef("User.id", "=", "TournamentAuditLog.subjectUserId"),
).as("subject"),
jsonObjectFrom(

View File

@ -22,8 +22,9 @@ import { databaseTimestampNow, dateToDatabaseTimestamp } from "~/utils/dates";
import { shortNanoid } from "~/utils/id";
import invariant from "~/utils/invariant";
import {
COMMON_USER_FIELDS,
commonUserSelect,
concatUserSubmittedImagePrefix,
customAvatarUrl,
tournamentLogoWithDefault,
} from "~/utils/kysely.server";
import type { Unwrapped } from "~/utils/types";
@ -95,10 +96,10 @@ export async function findById(id: number) {
"TournamentOrganizationMember.userId",
"User.id",
)
.select([
.select((eb) => [
"TournamentOrganizationMember.userId",
"TournamentOrganizationMember.role",
...COMMON_USER_FIELDS,
...commonUserSelect(eb),
"User.pronouns",
])
.whereRef(
@ -128,15 +129,15 @@ export async function findById(id: number) {
jsonObjectFrom(
eb
.selectFrom("User")
.select([...COMMON_USER_FIELDS, "User.pronouns"])
.select((eb) => [...commonUserSelect(eb), "User.pronouns"])
.whereRef("User.id", "=", "CalendarEvent.authorId"),
).as("author"),
jsonArrayFrom(
eb
.selectFrom("TournamentStaff")
.innerJoin("User", "TournamentStaff.userId", "User.id")
.select([
...COMMON_USER_FIELDS,
.select((eb) => [
...commonUserSelect(eb),
"User.pronouns",
"TournamentStaff.role",
])
@ -194,7 +195,7 @@ export async function findById(id: number) {
)
.leftJoin("PlusTier", "PlusTier.userId", "User.id")
.leftJoin("LiveStream", "LiveStream.userId", "User.id")
.select([
.select((eb) => [
"User.id as userId",
"User.username",
"User.discordId",
@ -213,6 +214,7 @@ export async function findById(id: number) {
"LiveStream.twitch as streamTwitch",
"LiveStream.viewerCount as streamViewerCount",
"LiveStream.thumbnailUrl as streamThumbnailUrl",
customAvatarUrl(eb).as("customAvatarUrl"),
])
.whereRef(
"TournamentTeamMember.tournamentTeamId",
@ -584,7 +586,7 @@ export function forShowcase() {
.whereRef("TournamentResult.tournamentId", "=", "Tournament.id")
.where("TournamentResult.placement", "=", 1)
.select((eb) => [
...COMMON_USER_FIELDS,
...commonUserSelect(eb),
"User.country",
"TournamentResult.div",
"TournamentTeam.name as teamName",
@ -665,7 +667,7 @@ export function topThreeResultsByTournamentId(tournamentId: number) {
jsonObjectFrom(
eb
.selectFrom("User")
.select([...COMMON_USER_FIELDS])
.select((eb) => commonUserSelect(eb))
.whereRef("User.id", "=", "TournamentResult.userId"),
).as("user"),
])

View File

@ -36,7 +36,7 @@ export interface PlayedSet {
Pick<
Tables["User"],
"id" | "username" | "discordAvatar" | "discordId" | "customUrl"
>
> & { customAvatarUrl: string | null }
>;
};
}

View File

@ -1,4 +1,4 @@
import type { ExpressionBuilder, FunctionModule, NotNull } from "kysely";
import type { ExpressionBuilder, NotNull } from "kysely";
import { sql } from "kysely";
import { jsonArrayFrom } from "kysely/helpers/sqlite";
import * as R from "remeda";
@ -17,8 +17,9 @@ import { isSupporter } from "~/modules/permissions/utils";
import { databaseTimestampNow, dateToDatabaseTimestamp } from "~/utils/dates";
import invariant from "~/utils/invariant";
import {
COMMON_USER_FIELDS,
commonUserSelect,
concatUserSubmittedImagePrefix,
customAvatarUrl,
tournamentLogoOrNull,
userChatNameHue,
userProfileWeapons,
@ -87,7 +88,7 @@ export function findLayoutDataByIdentifier(
return identifierToUserIdQuery(identifier)
.leftJoin("PlusTier", "PlusTier.userId", "User.id")
.select((eb) => [
...COMMON_USER_FIELDS,
...commonUserSelect(eb),
"User.pronouns",
"User.country",
"User.inGameName",
@ -182,6 +183,8 @@ export async function findProfileByIdentifier(
"User.patronTier",
"PlusTier.tier as plusTier",
"User.pronouns",
"User.customAvatarImgId",
customAvatarUrl(eb).as("customAvatarUrl"),
userProfileWeapons(eb).as("weapons"),
jsonArrayFrom(
eb
@ -380,7 +383,7 @@ export function findByFriendCode(friendCode: string) {
return db
.selectFrom("UserFriendCode")
.innerJoin("User", "User.id", "UserFriendCode.userId")
.select([...COMMON_USER_FIELDS])
.select((eb) => commonUserSelect(eb))
.where("UserFriendCode.friendCode", "=", friendCode)
.execute();
}
@ -391,7 +394,7 @@ export async function findLeanById(id: number) {
.leftJoin("PlusTier", "PlusTier.userId", "User.id")
.where("User.id", "=", id)
.select(({ eb }) => [
...COMMON_USER_FIELDS,
...commonUserSelect(eb),
"User.customTheme",
"User.isArtist",
"User.isVideoAdder",
@ -439,11 +442,11 @@ export function findModInfoById(id: number) {
eb
.selectFrom("ModNote")
.innerJoin("User", "User.id", "ModNote.authorId")
.select([
.select((eb) => [
"ModNote.id as noteId",
"ModNote.text",
"ModNote.createdAt",
...COMMON_USER_FIELDS,
...commonUserSelect(eb),
])
.where("ModNote.isDeleted", "=", 0)
.where("ModNote.userId", "=", id)
@ -453,11 +456,11 @@ export function findModInfoById(id: number) {
eb
.selectFrom("BanLog")
.innerJoin("User", "User.id", "BanLog.bannedByUserId")
.select([
.select((eb) => [
"BanLog.banned",
"BanLog.bannedReason",
"BanLog.createdAt",
...COMMON_USER_FIELDS,
...commonUserSelect(eb),
])
.where("BanLog.userId", "=", id)
.orderBy("BanLog.createdAt", "desc"),
@ -492,14 +495,7 @@ export function findAllPlusServerMembers() {
export async function findChatUsersByUserIds(userIds: number[]) {
const users = await db
.selectFrom("User")
.select([
"User.id",
"User.discordId",
"User.discordAvatar",
"User.username",
"User.pronouns",
userChatNameHue,
])
.select((eb) => [...commonUserSelect(eb), "User.pronouns", userChatNameHue])
.where("User.id", "in", userIds)
.execute();
@ -601,7 +597,10 @@ export function findResultsByUserId(
eb
.selectFrom("CalendarEventResultPlayer")
.leftJoin("User", "User.id", "CalendarEventResultPlayer.userId")
.select([...COMMON_USER_FIELDS, "CalendarEventResultPlayer.name"])
.select((eb) => [
...commonUserSelect(eb),
"CalendarEventResultPlayer.name",
])
.whereRef(
"CalendarEventResultPlayer.teamId",
"=",
@ -636,7 +635,10 @@ export function findResultsByUserId(
eb
.selectFrom("TournamentResult as TournamentResult2")
.innerJoin("User", "User.id", "TournamentResult2.userId")
.select([...COMMON_USER_FIELDS, sql<string | null>`null`.as("name")])
.select((eb) => [
...commonUserSelect(eb),
sql<string | null>`null`.as("name"),
])
.whereRef(
"TournamentResult2.tournamentTeamId",
"=",
@ -777,16 +779,18 @@ export async function findResultPlacementsByUserId(userId: number) {
];
}
const searchSelectedFields = ({ fn }: { fn: FunctionModule<DB, "User"> }) =>
const searchSelectedFields = (eb: ExpressionBuilder<DB, "User">) =>
[
...COMMON_USER_FIELDS,
...commonUserSelect(eb),
"User.inGameName",
"PlusTier.tier as plusTier",
fn<string | null>("iif", [
"User.showDiscordUniqueName",
"User.discordUniqueName",
sql`null`,
]).as("discordUniqueName"),
eb
.fn<string | null>("iif", [
"User.showDiscordUniqueName",
"User.discordUniqueName",
sql`null`,
])
.as("discordUniqueName"),
] as const;
export async function search({
query,
@ -1069,12 +1073,31 @@ type UpdateProfileArgs = Pick<
> & {
weapons: Pick<TablesInsertable["UserWeapon"], "weaponSplId" | "isFavorite">[];
favoriteBadgeIds?: number[] | null;
customAvatarImgId?: number | null;
};
export function updateOwnProfile(args: UpdateProfileArgs) {
const userId = actorId();
return db.transaction().execute(async (trx) => {
await trx.deleteFrom("UserWeapon").where("userId", "=", userId).execute();
// a removed or replaced custom avatar is no longer referenced by anything,
// so its submitted image row is cleaned up
const current = await trx
.selectFrom("User")
.select("User.customAvatarImgId")
.where("id", "=", userId)
.executeTakeFirst();
if (
current?.customAvatarImgId &&
current.customAvatarImgId !== args.customAvatarImgId
) {
await trx
.deleteFrom("UnvalidatedUserSubmittedImage")
.where("id", "=", current.customAvatarImgId)
.where("UnvalidatedUserSubmittedImage.submitterUserId", "=", userId)
.execute();
}
if (args.weapons.length > 0) {
await trx
.insertInto("UserWeapon")
@ -1109,6 +1132,7 @@ export function updateOwnProfile(args: UpdateProfileArgs) {
commissionsOpen: args.commissionsOpen,
commissionsOpenedAt:
args.commissionsOpen === 1 ? databaseTimestampNow() : null,
customAvatarImgId: args.customAvatarImgId ?? null,
})
.where("id", "=", userId)
.returning(["User.id", "User.customUrl", "User.discordId"])

View File

@ -4,16 +4,16 @@ import { BADGE } from "~/features/badges/badges-constants";
import * as TournamentTeamRepository from "~/features/tournament/TournamentTeamRepository.server";
import { clearTournamentDataCache } from "~/features/tournament-bracket/core/Tournament.server";
import * as UserRepository from "~/features/user-page/UserRepository.server";
import { parseFormData } from "~/form/parse.server";
import { parseFormDataWithImages } from "~/form/parse.server";
import { userPage } from "~/utils/urls";
import { userEditProfileSchemaServer } from "../user-page-schemas.server";
import { userEditProfileBaseSchema } from "../user-page-schemas";
export const action: ActionFunction = async ({ request }) => {
const user = requireUser();
const result = await parseFormData({
const result = await parseFormDataWithImages({
request,
schema: userEditProfileSchemaServer,
schema: userEditProfileBaseSchema,
});
if (!result.success) {
@ -22,6 +22,17 @@ export const action: ActionFunction = async ({ request }) => {
const data = result.data;
if (data.customUrl) {
const existingUser = await UserRepository.findByCustomUrl(data.customUrl);
if (existingUser && existingUser.id !== user.id) {
return {
fieldErrors: {
customUrl: "forms:errors.profileCustomUrlDuplicate",
},
};
}
}
const [subjectPronoun, objectPronoun] = data.pronouns ?? [null, null];
const pronouns =
subjectPronoun && objectPronoun
@ -58,6 +69,7 @@ export const action: ActionFunction = async ({ request }) => {
showDiscordUniqueName: data.showDiscordUniqueName ? 1 : 0,
commissionsOpen: isArtist && data.commissionsOpen ? 1 : 0,
commissionText: isArtist ? data.commissionText : null,
customAvatarImgId: isSupporter ? data.customAvatar : null,
});
await UserRepository.updateOwnPreferences({

View File

@ -15,6 +15,7 @@ const DEFAULT_FIELDS = {
commissionsOpen: false,
commissionText: null,
country: "FI",
customAvatar: null,
customName: null,
customUrl: null,
favoriteBadgeIds: [],

View File

@ -3,6 +3,7 @@ import { Link, useLoaderData, useMatches } from "react-router";
import { FormMessage } from "~/components/FormMessage";
import { FriendCodePopover } from "~/components/FriendCodePopover";
import { BADGE } from "~/features/badges/badges-constants";
import { existingImage } from "~/form/image-field";
import { SendouForm } from "~/form/SendouForm";
import { useHydrated } from "~/hooks/useHydrated";
import { useHasRole } from "~/modules/permissions/hooks";
@ -41,6 +42,10 @@ export default function UserEditPage() {
}));
const defaultValues = {
customAvatar: existingImage(
data.user.customAvatarImgId,
data.user.customAvatarUrl,
),
customName: data.user.customName ?? "",
customUrl: layoutData.user.customUrl ?? "",
inGameName: data.user.inGameName ?? "",
@ -66,12 +71,14 @@ export default function UserEditPage() {
schema={userEditProfileBaseSchema}
defaultValues={defaultValues}
submitButtonText={t("common:actions.save")}
revalidateRoot
>
{({ FormField }) => (
<>
<FriendCodePopover />
<FormField name="customName" />
<FormField name="customUrl" />
<FormField name="customAvatar" disabled={!isSupporter} />
<FormField name="inGameName" />
<FormField name="sensitivity" />
<FormField name="pronouns" />

View File

@ -46,7 +46,17 @@ export const handle: SendouRouteHandle = {
if (!data) return [];
if (!data.user.discordAvatar) {
const imgPath = data.user.customAvatarUrl
? data.user.customAvatarUrl
: data.user.discordAvatar
? discordAvatarUrl({
discordId: data.user.discordId,
discordAvatar: data.user.discordAvatar,
size: "sm",
})
: null;
if (!imgPath) {
return {
text: data.user.username,
href: userPage(data.user),
@ -55,11 +65,7 @@ export const handle: SendouRouteHandle = {
}
return {
imgPath: discordAvatarUrl({
discordId: data.user.discordId,
discordAvatar: data.user.discordAvatar,
size: "sm",
}),
imgPath,
href: userPage(data.user),
type: "IMAGE",
text: data.user.username,

View File

@ -1,29 +1,6 @@
import { z } from "zod";
import { requireUser } from "~/features/auth/core/user.server";
import * as BuildRepository from "~/features/builds/BuildRepository.server";
import * as UserRepository from "./UserRepository.server";
import {
gearAllOrNoneRefine,
newBuildBaseSchema,
userEditProfileBaseSchema,
} from "./user-page-schemas";
export const userEditProfileSchemaServer =
userEditProfileBaseSchema.superRefine(async (data, ctx) => {
if (!data.customUrl) return;
const existingUser = await UserRepository.findByCustomUrl(data.customUrl);
if (!existingUser) return;
const currentUser = requireUser();
if (existingUser.id === currentUser.id) return;
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "forms:errors.profileCustomUrlDuplicate",
path: ["customUrl"],
});
});
import { gearAllOrNoneRefine, newBuildBaseSchema } from "./user-page-schemas";
export const newBuildSchemaServer = newBuildBaseSchema
.refine(gearAllOrNoneRefine.fn, gearAllOrNoneRefine.opts)

View File

@ -8,6 +8,7 @@ import {
customField,
dualSelectOptional,
idConstantOptional,
image,
selectDynamicOptional,
stringConstant,
textAreaOptional,
@ -61,6 +62,11 @@ const SENS_ITEMS = [
}));
export const userEditProfileBaseSchema = z.object({
customAvatar: image({
label: "labels.profileCustomAvatar",
bottomText: "bottomTexts.profileCustomAvatar",
autoValidate: true,
}),
customName: textFieldOptional({
label: "labels.profileCustomName",
bottomText: "bottomTexts.profileCustomName",

View File

@ -14,7 +14,11 @@ import {
dayMonthYearToDatabaseTimestamp,
} from "~/utils/dates";
import invariant from "~/utils/invariant";
import { type CommonUser, commonUserJsonObject } from "~/utils/kysely.server";
import {
type CommonUser,
commonUserJsonObject,
commonUserSelect,
} from "~/utils/kysely.server";
import { VODS_PAGE_BATCH_SIZE } from "./vods-constants";
import type { VideoBeingAdded, Vod } from "./vods-types";
import {
@ -65,12 +69,7 @@ export async function findVods({
jsonArrayFrom(
eb
.selectFrom("User")
.select([
"User.username",
"User.discordId",
"User.discordAvatar",
"User.customUrl",
])
.select((playerEb) => commonUserSelect(playerEb))
.whereRef("User.id", "=", "VideoMatchPlayer.playerUserId"),
).as("players"),
]);

View File

@ -8,10 +8,10 @@ export type VideoBeingAdded = z.infer<typeof videoSchema>;
export interface Vod {
id: Tables["Video"]["id"];
pov?:
| Pick<
| (Pick<
Tables["User"],
"username" | "discordId" | "discordAvatar" | "customUrl" | "id"
>
> & { customAvatarUrl: string | null })
| string;
title: Tables["Video"]["title"];
type: Tables["Video"]["type"];

View File

@ -333,6 +333,7 @@ export function FormField({
<ImageFormField
{...commonProps}
{...formField}
disabled={disabled}
value={value as ImageFieldValue}
onChange={handleChange as (v: ImageFieldValue) => void}
/>

View File

@ -84,6 +84,7 @@ function prefixItems<V extends string>(
export function image(args: {
label: FormsTranslationKey;
bottomText?: FormsTranslationKey;
dimensions?: "logo" | "thick-banner" | { width: number; height: number };
autoValidate?: boolean;
}) {
@ -91,6 +92,7 @@ export function image(args: {
// instance would otherwise have its metadata overwritten by later fields)
return imageValue.clone().register(formRegistry, {
label: prefixKey(args.label),
bottomText: prefixKey(args.bottomText),
dimensions: args.dimensions ?? "logo",
autoValidate: args.autoValidate ?? false,
type: "image",

View File

@ -15,16 +15,19 @@ import styles from "./ImageFormField.module.css";
type ImageFormFieldProps = Omit<FormFieldProps<"image">, "onBlur"> & {
value: ImageFieldValue;
onChange: (value: ImageFieldValue) => void;
disabled?: boolean;
};
export function ImageFormField({
name,
label,
bottomText,
dimensions,
autoValidate,
error,
value,
onChange,
disabled,
}: ImageFormFieldProps) {
const id = React.useId();
const { t } = useTranslation(["common"]);
@ -72,7 +75,8 @@ export function ImageFormField({
label={label}
error={error}
bottomText={
autoValidate ? undefined : "forms:bottomTexts.imageModeration"
bottomText ??
(autoValidate ? undefined : "forms:bottomTexts.imageModeration")
}
>
<div className="stack sm items-start">
@ -88,6 +92,7 @@ export function ImageFormField({
variant="minimal-destructive"
size="small"
onPress={() => onChange(null)}
isDisabled={disabled}
>
{t("common:actions.remove")}
</SendouButton>
@ -97,6 +102,7 @@ export function ImageFormField({
type="file"
accept="image/png, image/jpeg, image/webp"
onChange={handleFileChange}
disabled={disabled}
/>
)}
</div>

View File

@ -117,8 +117,7 @@ interface FormFieldMapPool<T extends string> extends FormFieldBase<T> {
disableBannedMaps?: boolean;
}
interface FormFieldImage<T extends string>
extends Omit<FormFieldBase<T>, "bottomText"> {
interface FormFieldImage<T extends string> extends FormFieldBase<T> {
dimensions?: ImageFieldDimensions;
/** Validate uploaded images immediately, bypassing the moderator queue (e.g. trusted org logos). */
autoValidate?: boolean;

View File

@ -124,6 +124,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
discordId: user.discordId,
id: user.id,
customUrl: user.customUrl,
customAvatarUrl: user.customAvatarUrl,
inGameName: user.inGameName,
friendCode: user.friendCode,
preferences: user.preferences ?? {},

View File

@ -108,6 +108,7 @@ describe("syncLiveStreams tournament streamers", () => {
streamTwitch: null,
streamViewerCount: null,
streamThumbnailUrl: null,
customAvatarUrl: null,
},
],
}),
@ -148,6 +149,7 @@ describe("syncLiveStreams tournament streamers", () => {
streamTwitch: null,
streamViewerCount: null,
streamThumbnailUrl: null,
customAvatarUrl: null,
},
],
}),
@ -201,6 +203,7 @@ describe("syncLiveStreams tournament streamers", () => {
streamTwitch: null,
streamViewerCount: null,
streamThumbnailUrl: null,
customAvatarUrl: null,
},
],
}),
@ -237,6 +240,7 @@ describe("syncLiveStreams tournament streamers", () => {
streamTwitch: null,
streamViewerCount: null,
streamThumbnailUrl: null,
customAvatarUrl: null,
},
],
}),
@ -276,6 +280,7 @@ describe("syncLiveStreams tournament streamers", () => {
streamTwitch: null,
streamViewerCount: null,
streamThumbnailUrl: null,
customAvatarUrl: null,
},
],
}),

View File

@ -9,18 +9,44 @@ import { jsonArrayFrom, jsonBuildObject } from "kysely/helpers/sqlite";
import type { DB, Tables } from "~/db/tables";
import { IS_E2E_TEST_RUN } from "./e2e";
export const COMMON_USER_FIELDS = [
"User.id",
"User.username",
"User.discordId",
"User.discordAvatar",
"User.customUrl",
] as const;
/**
* Select list for the fields shared by every user representation across the app. Includes
* `customAvatarUrl`, the full URL of the user's supporter custom avatar (resolved from
* `User.customAvatarImgId`), or `null` when they have none. `"User"` must be in scope at the call
* site (it always is, since the other fields reference `User.*`).
*/
export function commonUserSelect(eb: ExpressionBuilder<Tables, "User">) {
return [
"User.id",
"User.username",
"User.discordId",
"User.discordAvatar",
"User.customUrl",
customAvatarUrl(eb).as("customAvatarUrl"),
] as const;
}
/**
* SQL expression resolving to the full URL of a user's supporter custom avatar (from
* `User.customAvatarImgId`), or `null` when they have none. Alias it
* (`.as("customAvatarUrl")`) when selecting it directly. Prefer {@link commonUserSelect} /
* {@link commonUserJsonObject}; reach for this only when those don't fit (e.g. prefixed aliases or
* a hand-built `jsonBuildObject`).
*/
export function customAvatarUrl(eb: ExpressionBuilder<Tables, "User">) {
return concatUserSubmittedImagePrefix(
eb
.selectFrom("UserSubmittedImage")
.select("UserSubmittedImage.url")
.whereRef("UserSubmittedImage.id", "=", "User.customAvatarImgId")
.$asScalar(),
).$castTo<string | null>();
}
export type CommonUser = Pick<
Tables["User"],
"id" | "username" | "discordId" | "discordAvatar" | "customUrl"
>;
> & { customAvatarUrl: string | null };
const userChatNameHueRaw = sql<
string | null
@ -35,6 +61,13 @@ export function commonUserJsonObject(eb: ExpressionBuilder<Tables, "User">) {
discordId: eb.ref("User.discordId"),
discordAvatar: eb.ref("User.discordAvatar"),
customUrl: eb.ref("User.customUrl"),
customAvatarUrl: concatUserSubmittedImagePrefix(
eb
.selectFrom("UserSubmittedImage")
.select("UserSubmittedImage.url")
.whereRef("UserSubmittedImage.id", "=", "User.customAvatarImgId")
.$asScalar(),
).$castTo<string | null>(),
});
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -252,6 +252,8 @@
"support.perk.userShortLink": "",
"support.perk.userShortLink.extra": "",
"support.perk.customizedColorsUser": "Tilpas farver på brugerprofil",
"support.perk.customAvatar": "",
"support.perk.customAvatar.extra": "",
"support.perk.favoriteBadges": "",
"support.perk.favoriteBadges.extra": "",
"support.perk.customizedColorsTeam": "Tilpas far holdprofil",

View File

@ -236,6 +236,7 @@
"errors.plusAlreadySuggested": "",
"errors.plusAlreadyMember": "",
"errors.plusCannotSuggest": "",
"labels.profileCustomAvatar": "",
"labels.profileCustomName": "Brugerdefineret Navn",
"labels.profileCustomUrl": "Brugerdefineret URL",
"labels.inGameName": "Splatoon 3 Brugernavn",
@ -248,6 +249,7 @@
"labels.profileCommissionsOpen": "Åben for bestillinger",
"labels.profileCommissionText": "info om bestilling",
"labels.profileNewProfileEnabled": "",
"bottomTexts.profileCustomAvatar": "",
"bottomTexts.profileCustomName": "Hvis feltet ikke udfyldes bruges dit discordbrugernavn: \"{{discordName}}\"",
"bottomTexts.profileCustomUrl": "",
"bottomTexts.profileInGameName": "",

View File

@ -252,6 +252,8 @@
"support.perk.userShortLink": "",
"support.perk.userShortLink.extra": "",
"support.perk.customizedColorsUser": "Farben anpassen (User-Seite)",
"support.perk.customAvatar": "",
"support.perk.customAvatar.extra": "",
"support.perk.favoriteBadges": "",
"support.perk.favoriteBadges.extra": "",
"support.perk.customizedColorsTeam": "Farben anpassen (Team-Seite)",

View File

@ -236,6 +236,7 @@
"errors.plusAlreadySuggested": "",
"errors.plusAlreadyMember": "",
"errors.plusCannotSuggest": "",
"labels.profileCustomAvatar": "",
"labels.profileCustomName": "",
"labels.profileCustomUrl": "Benutzerdefinierte URL",
"labels.inGameName": "Name im Spiel",
@ -248,6 +249,7 @@
"labels.profileCommissionsOpen": "",
"labels.profileCommissionText": "",
"labels.profileNewProfileEnabled": "",
"bottomTexts.profileCustomAvatar": "",
"bottomTexts.profileCustomName": "",
"bottomTexts.profileCustomUrl": "",
"bottomTexts.profileInGameName": "",

View File

@ -252,6 +252,8 @@
"support.perk.userShortLink": "User page short link",
"support.perk.userShortLink.extra": "Instead of e.g. sendou.ink/u/sendou you can also use snd.ink/sendou when linking to your user profile.",
"support.perk.customizedColorsUser": "Customize colors (user page)",
"support.perk.customAvatar": "Custom avatar",
"support.perk.customAvatar.extra": "Upload a custom avatar to use instead of your Discord avatar.",
"support.perk.favoriteBadges": "Set profile first page badges",
"support.perk.favoriteBadges.extra": "You can set which badges appear on the first page and the order. Normally it's only possible to choose the first badge.",
"support.perk.customizedColorsTeam": "Customize colors (team page)",

View File

@ -236,6 +236,7 @@
"errors.plusAlreadySuggested": "This user has already been suggested",
"errors.plusAlreadyMember": "This user is already a member of this tier",
"errors.plusCannotSuggest": "Can't make a suggestion right now",
"labels.profileCustomAvatar": "Custom avatar",
"labels.profileCustomName": "Custom name",
"labels.profileCustomUrl": "Custom URL",
"labels.inGameName": "In-game name",
@ -248,6 +249,7 @@
"labels.profileCommissionsOpen": "Commissions open",
"labels.profileCommissionText": "Commission info",
"labels.profileNewProfileEnabled": "New profile page",
"bottomTexts.profileCustomAvatar": "Patrons (Supporter & above) can upload an image to be used instead of Discord avatar",
"bottomTexts.profileCustomName": "If empty, your Discord display name is shown",
"bottomTexts.profileCustomUrl": "For patrons (Supporter & above) short link is available. E.g. instead of sendou.ink/u/sendou, snd.ink/sendou can be used.",
"bottomTexts.profileInGameName": "Format: Name#disc (e.g. Player#1234)",

View File

@ -252,6 +252,8 @@
"support.perk.userShortLink": "Enlace corto de perfil",
"support.perk.userShortLink.extra": "En lugar de sendou.ink/u/sendou también puedes usar snd.ink/sendou al enlazar a tu perfil de usuario.",
"support.perk.customizedColorsUser": "Colores personalizados (perfil)",
"support.perk.customAvatar": "",
"support.perk.customAvatar.extra": "",
"support.perk.favoriteBadges": "Fijar insignias favoritas",
"support.perk.favoriteBadges.extra": "Puedes elegir qué insignias aparecen en la página principal de tu perfil y en qué orden.",
"support.perk.customizedColorsTeam": "Colores personalizados (equipo)",

View File

@ -236,6 +236,7 @@
"errors.plusAlreadySuggested": "",
"errors.plusAlreadyMember": "",
"errors.plusCannotSuggest": "",
"labels.profileCustomAvatar": "",
"labels.profileCustomName": "",
"labels.profileCustomUrl": "Enlace personalizado",
"labels.inGameName": "Nombre en el juego",
@ -248,6 +249,7 @@
"labels.profileCommissionsOpen": "Comisiones abiertas",
"labels.profileCommissionText": "Info de comisiones",
"labels.profileNewProfileEnabled": "",
"bottomTexts.profileCustomAvatar": "",
"bottomTexts.profileCustomName": "",
"bottomTexts.profileCustomUrl": "",
"bottomTexts.profileInGameName": "",

View File

@ -252,6 +252,8 @@
"support.perk.userShortLink": "",
"support.perk.userShortLink.extra": "",
"support.perk.customizedColorsUser": "Colores personalizados (perfil)",
"support.perk.customAvatar": "",
"support.perk.customAvatar.extra": "",
"support.perk.favoriteBadges": "",
"support.perk.favoriteBadges.extra": "",
"support.perk.customizedColorsTeam": "Colores personalizados (página de equipo)",

View File

@ -236,6 +236,7 @@
"errors.plusAlreadySuggested": "",
"errors.plusAlreadyMember": "",
"errors.plusCannotSuggest": "",
"labels.profileCustomAvatar": "",
"labels.profileCustomName": "Nombre personalizado",
"labels.profileCustomUrl": "Enlace personalizado",
"labels.inGameName": "Nombre en el juego",
@ -248,6 +249,7 @@
"labels.profileCommissionsOpen": "Comisiones abiertas",
"labels.profileCommissionText": "Info de comisiones",
"labels.profileNewProfileEnabled": "",
"bottomTexts.profileCustomAvatar": "",
"bottomTexts.profileCustomName": "Si vacío, se mostrará tu nombre de Discord: \"{{discordName}}\"",
"bottomTexts.profileCustomUrl": "",
"bottomTexts.profileInGameName": "",

View File

@ -252,6 +252,8 @@
"support.perk.userShortLink": "",
"support.perk.userShortLink.extra": "",
"support.perk.customizedColorsUser": "Personalisation des couleurs (page perso)",
"support.perk.customAvatar": "",
"support.perk.customAvatar.extra": "",
"support.perk.favoriteBadges": "",
"support.perk.favoriteBadges.extra": "",
"support.perk.customizedColorsTeam": "Personalisation des couleurs (page d'équipe)",

View File

@ -236,6 +236,7 @@
"errors.plusAlreadySuggested": "",
"errors.plusAlreadyMember": "",
"errors.plusCannotSuggest": "",
"labels.profileCustomAvatar": "",
"labels.profileCustomName": "",
"labels.profileCustomUrl": "URL personnalisée",
"labels.inGameName": "Pseudo en jeu",
@ -248,6 +249,7 @@
"labels.profileCommissionsOpen": "Commissions acceptées",
"labels.profileCommissionText": "Info pour les commissions",
"labels.profileNewProfileEnabled": "",
"bottomTexts.profileCustomAvatar": "",
"bottomTexts.profileCustomName": "",
"bottomTexts.profileCustomUrl": "",
"bottomTexts.profileInGameName": "",

View File

@ -252,6 +252,8 @@
"support.perk.userShortLink": "Lien de la page de l'utilisateur plus court",
"support.perk.userShortLink.extra": "Au lieu d'utiliser par exemple sendou.ink/u/sendou, vous pouvez également utiliser snd.ink/sendou lorsque vous créez un lien vers votre profil.",
"support.perk.customizedColorsUser": "Personalisation des couleurs (page personnelle)",
"support.perk.customAvatar": "",
"support.perk.customAvatar.extra": "",
"support.perk.favoriteBadges": "Choisissez le badge qui apparaitra en premier sur votre profil",
"support.perk.favoriteBadges.extra": "Vous pouvez choisir l'ordre des badges qui vont apparaitre sur la première page de votre profil. Normalement, il est seulement possible de choisir le premier badge.",
"support.perk.customizedColorsTeam": "Personalisation des couleurs (page d'équipe)",

View File

@ -236,6 +236,7 @@
"errors.plusAlreadySuggested": "",
"errors.plusAlreadyMember": "",
"errors.plusCannotSuggest": "",
"labels.profileCustomAvatar": "",
"labels.profileCustomName": "Nom personnalisée",
"labels.profileCustomUrl": "URL personnalisée",
"labels.inGameName": "Pseudo en jeu",
@ -248,6 +249,7 @@
"labels.profileCommissionsOpen": "Commissions acceptées",
"labels.profileCommissionText": "Info pour les commissions",
"labels.profileNewProfileEnabled": "",
"bottomTexts.profileCustomAvatar": "",
"bottomTexts.profileCustomName": "Si il n'est pas présent, votre pseudo discord est utilisé: \"{{discordName}}\"",
"bottomTexts.profileCustomUrl": "Pour les Supporter patrons (& plus), les liens courts sont disponibles. Exemple: Au mieux de sendou.ink/u/sendou, snd.ink/sendou peut être utilisé.",
"bottomTexts.profileInGameName": "",

View File

@ -252,6 +252,8 @@
"support.perk.userShortLink": "",
"support.perk.userShortLink.extra": "",
"support.perk.customizedColorsUser": "התאמה אישית של צבעים (דף משתמש)",
"support.perk.customAvatar": "",
"support.perk.customAvatar.extra": "",
"support.perk.favoriteBadges": "",
"support.perk.favoriteBadges.extra": "",
"support.perk.customizedColorsTeam": "התאמה אישית של צבעים (דף צוות)",

View File

@ -236,6 +236,7 @@
"errors.plusAlreadySuggested": "",
"errors.plusAlreadyMember": "",
"errors.plusCannotSuggest": "",
"labels.profileCustomAvatar": "",
"labels.profileCustomName": "",
"labels.profileCustomUrl": "כתובת URL מותאמת אישית",
"labels.inGameName": "שם במשחק",
@ -248,6 +249,7 @@
"labels.profileCommissionsOpen": "בקשות פתוחות",
"labels.profileCommissionText": "מידע עבור בקשות",
"labels.profileNewProfileEnabled": "",
"bottomTexts.profileCustomAvatar": "",
"bottomTexts.profileCustomName": "",
"bottomTexts.profileCustomUrl": "",
"bottomTexts.profileInGameName": "",

View File

@ -252,6 +252,8 @@
"support.perk.userShortLink": "Short link per la pagina utente",
"support.perk.userShortLink.extra": "Invece di es. sendou.ink/u/sendou puoi anche usare snd.ink/sendou quando linki la tua pagina utente.",
"support.perk.customizedColorsUser": "Personalizza colori (pagina utente)",
"support.perk.customAvatar": "",
"support.perk.customAvatar.extra": "",
"support.perk.favoriteBadges": "",
"support.perk.favoriteBadges.extra": "",
"support.perk.customizedColorsTeam": "Personalizza colori (Pagina team)",

View File

@ -236,6 +236,7 @@
"errors.plusAlreadySuggested": "",
"errors.plusAlreadyMember": "",
"errors.plusCannotSuggest": "",
"labels.profileCustomAvatar": "",
"labels.profileCustomName": "Nome personalizzato",
"labels.profileCustomUrl": "URL personalizzato",
"labels.inGameName": "Nome nel gioco",
@ -248,6 +249,7 @@
"labels.profileCommissionsOpen": "Commissioni aperte",
"labels.profileCommissionText": "Info sulle commissioni",
"labels.profileNewProfileEnabled": "",
"bottomTexts.profileCustomAvatar": "",
"bottomTexts.profileCustomName": "Se mancante, viene usato il tuo nome visualizzato Discord: \"{{discordName}}\"",
"bottomTexts.profileCustomUrl": "Per gli iscritti al Patreon (Supporter compreso in su) è disponibile il link corto. Es. invece di sendou.ink/u/sendou, può essere usato snd.ink/sendou.",
"bottomTexts.profileInGameName": "",

View File

@ -252,6 +252,8 @@
"support.perk.userShortLink": "",
"support.perk.userShortLink.extra": "",
"support.perk.customizedColorsUser": "カラーをカスタマイズする (ユーザーページ)",
"support.perk.customAvatar": "",
"support.perk.customAvatar.extra": "",
"support.perk.favoriteBadges": "",
"support.perk.favoriteBadges.extra": "",
"support.perk.customizedColorsTeam": "カラーをカスタマイズする (チームページ)",

View File

@ -236,6 +236,7 @@
"errors.plusAlreadySuggested": "",
"errors.plusAlreadyMember": "",
"errors.plusCannotSuggest": "",
"labels.profileCustomAvatar": "",
"labels.profileCustomName": "カスタム 名前",
"labels.profileCustomUrl": "カスタム URL",
"labels.inGameName": "ゲーム中の名前",
@ -248,6 +249,7 @@
"labels.profileCommissionsOpen": "依頼を受付中",
"labels.profileCommissionText": "依頼に関する情報",
"labels.profileNewProfileEnabled": "",
"bottomTexts.profileCustomAvatar": "",
"bottomTexts.profileCustomName": "記入されてない場合ディスコードの表示名 \"{{discordName}}\"を使います",
"bottomTexts.profileCustomUrl": "",
"bottomTexts.profileInGameName": "",

View File

@ -252,6 +252,8 @@
"support.perk.userShortLink": "",
"support.perk.userShortLink.extra": "",
"support.perk.customizedColorsUser": "",
"support.perk.customAvatar": "",
"support.perk.customAvatar.extra": "",
"support.perk.favoriteBadges": "",
"support.perk.favoriteBadges.extra": "",
"support.perk.customizedColorsTeam": "",

View File

@ -236,6 +236,7 @@
"errors.plusAlreadySuggested": "",
"errors.plusAlreadyMember": "",
"errors.plusCannotSuggest": "",
"labels.profileCustomAvatar": "",
"labels.profileCustomName": "",
"labels.profileCustomUrl": "",
"labels.inGameName": "",
@ -248,6 +249,7 @@
"labels.profileCommissionsOpen": "",
"labels.profileCommissionText": "",
"labels.profileNewProfileEnabled": "",
"bottomTexts.profileCustomAvatar": "",
"bottomTexts.profileCustomName": "",
"bottomTexts.profileCustomUrl": "",
"bottomTexts.profileInGameName": "",

View File

@ -252,6 +252,8 @@
"support.perk.userShortLink": "",
"support.perk.userShortLink.extra": "",
"support.perk.customizedColorsUser": "",
"support.perk.customAvatar": "",
"support.perk.customAvatar.extra": "",
"support.perk.favoriteBadges": "",
"support.perk.favoriteBadges.extra": "",
"support.perk.customizedColorsTeam": "",

Some files were not shown because too many files have changed in this diff Show More