sendou.ink/app/features/api-public/routes/user.$identifier.ts
Kalle e27260f88b Bye bye .png
... well mostly. Some places will still use it like PWA splash screens.
Otherwise browser support for .avif strong enough now to do this
now.
2026-03-29 16:48:47 +03:00

130 lines
4.0 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 { i18next } from "~/modules/i18n/i18next.server";
import { safeNumberParse } from "~/utils/number";
import { notFoundIfFalsy, parseParams } from "~/utils/remix.server";
import type { GetUserResponse } from "../schema";
const paramsSchema = z.object({
identifier: z.string(),
});
export const loader = async ({ params }: LoaderFunctionArgs) => {
const t = await i18next.getFixedT("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"),
jsonArrayFrom(
eb
.selectFrom("BadgeOwner")
.innerJoin("Badge", "Badge.id", "BadgeOwner.badgeId")
.select(({ fn }) => [
"Badge.displayName",
"Badge.code",
fn.count<number>("BadgeOwner.badgeId").as("count"),
])
.groupBy(["BadgeOwner.badgeId", "BadgeOwner.userId"])
.whereRef("BadgeOwner.userId", "=", "User.id"),
).as("badges"),
"SplatoonPlayer.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 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: user.badges.map((badge) => ({
name: badge.displayName,
count: badge.count,
gifUrl: `https://sendou.ink/static-assets/badges/${badge.code}.gif`,
imageUrl: `https://sendou.ink/static-assets/badges/${badge.code}.avif`,
})),
teams: user.teams.map((team) => ({
id: team.id,
role: team.role,
})),
};
return Response.json(result);
};