sendou.ink/app/features/api-public/routes/user.$identifier.ts
dependabot[bot] 938d7efd96
build(deps): bump remix-i18next from 7.5.0 to 8.0.0 (#3215)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kalle <38327916+Sendouc@users.noreply.github.com>
2026-07-10 17:40:24 +03:00

123 lines
3.8 KiB
TypeScript

import { jsonArrayFrom } from "kysely/helpers/sqlite";
import type { LoaderFunctionArgs } from "react-router";
import { z } from "zod";
import { db } from "~/db/sql";
import * as Seasons from "~/features/mmr/core/Seasons";
import { userSkills as _userSkills } from "~/features/mmr/tiered.server";
import { peakXpOverallSql } from "~/features/top-search/XRankPlacementRepository.server";
import * as UserRepository from "~/features/user-page/UserRepository.server";
import { getFixedTForLanguage } from "~/modules/i18n/i18next.server";
import { safeNumberParse } from "~/utils/number";
import { notFoundIfFalsy, parseParams } from "~/utils/remix.server";
import { badgeUrl } from "~/utils/urls";
import type { GetUserResponse } from "../schema";
const paramsSchema = z.object({
identifier: z.string(),
});
export const loader = async ({ params }: LoaderFunctionArgs) => {
const t = await getFixedTForLanguage("en", ["weapons"]);
const { identifier } = parseParams({ params, schema: paramsSchema });
const user = notFoundIfFalsy(
await db
.selectFrom("User")
.leftJoin("PlusTier", "PlusTier.userId", "User.id")
.leftJoin("SplatoonPlayer", "SplatoonPlayer.userId", "User.id")
.select(({ eb }) => [
"User.id",
"User.country",
"User.discordName",
"User.twitch",
"User.battlefy",
"User.bsky",
"User.customUrl",
"User.discordId",
"User.discordAvatar",
"User.inGameName",
"User.pronouns",
"PlusTier.tier",
jsonArrayFrom(
eb
.selectFrom("UserWeapon")
.select(["UserWeapon.isFavorite", "UserWeapon.weaponSplId"])
.whereRef("UserWeapon.userId", "=", "User.id")
.orderBy("UserWeapon.order", "asc"),
).as("weapons"),
peakXpOverallSql().as("peakXp"),
jsonArrayFrom(
eb
.selectFrom("TeamMemberWithSecondary")
.innerJoin("Team", "Team.id", "TeamMemberWithSecondary.teamId")
.select(["Team.id", "TeamMemberWithSecondary.role"])
.whereRef("TeamMemberWithSecondary.userId", "=", "User.id")
.orderBy("TeamMemberWithSecondary.isMainTeam", "desc")
.orderBy("TeamMemberWithSecondary.createdAt", "asc"),
).as("teams"),
])
.where((eb) => {
// we don't want to parse discord id's as numbers (length = 18)
const parsedId =
identifier.length < 10 ? safeNumberParse(identifier) : null;
if (parsedId) {
return eb("User.id", "=", parsedId);
}
return eb("User.discordId", "=", identifier);
})
.executeTakeFirst(),
);
const badges = await UserRepository.ownedBadgesByUserId(user.id);
const season = Seasons.currentOrPrevious(new Date())!.nth;
const { isAccurateTiers, userSkills } = _userSkills(season);
const skill = isAccurateTiers ? userSkills[user.id] : null;
const result: GetUserResponse = {
id: user.id,
name: user.discordName,
discordId: user.discordId,
avatarUrl: user.discordAvatar
? `https://cdn.discordapp.com/avatars/${user.discordId}/${user.discordAvatar}.png`
: null,
url: `https://sendou.ink/u/${user.customUrl ?? user.discordId}`,
country: user.country,
inGameName: user.inGameName,
pronouns: user.pronouns,
plusServerTier: user.tier as GetUserResponse["plusServerTier"],
socials: {
twitch: user.twitch,
battlefy: user.battlefy,
bsky: user.bsky,
twitter: null, // deprecated field
},
currentRank: skill?.tier
? {
season,
tier: skill.tier,
}
: null,
peakXp: user.peakXp,
weaponPool: user.weapons.map((weapon) => ({
id: weapon.weaponSplId,
name: t(`weapons:MAIN_${weapon.weaponSplId}`),
isFiveStar: Boolean(weapon.isFavorite),
})),
badges: badges.map((badge) => ({
name: badge.displayName,
count: badge.count,
gifUrl: badgeUrl({ code: badge.code, extension: "gif" }),
imageUrl: badgeUrl({ code: badge.code }),
})),
teams: user.teams.map((team) => ({
id: team.id,
role: team.role,
})),
};
return Response.json(result);
};