mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-07 19:55:46 -05:00
Add fields to the user public API endpoint Closes #2425
This commit is contained in:
@@ -3,6 +3,8 @@ import { jsonArrayFrom } from "kysely/helpers/sqlite";
|
||||
import { cors } from "remix-utils/cors";
|
||||
import { z } from "zod/v4";
|
||||
import { db } from "~/db/sql";
|
||||
import * 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";
|
||||
@@ -68,6 +70,19 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
|
||||
.select(["XRankPlacement.power"])
|
||||
.whereRef("SplatoonPlayer.userId", "=", "User.id"),
|
||||
).as("xRankPlacements"),
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom("TeamMemberWithSecondary")
|
||||
.innerJoin("Team", "Team.id", "TeamMemberWithSecondary.teamId")
|
||||
.select([
|
||||
"Team.name",
|
||||
"Team.customUrl",
|
||||
"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)
|
||||
@@ -82,6 +97,11 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
|
||||
.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,
|
||||
@@ -98,6 +118,12 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
|
||||
bsky: user.bsky,
|
||||
twitter: null, // deprecated field
|
||||
},
|
||||
currentRank: skill?.tier
|
||||
? {
|
||||
season,
|
||||
tier: skill.tier,
|
||||
}
|
||||
: null,
|
||||
peakXp:
|
||||
user.xRankPlacements.length > 0
|
||||
? user.xRankPlacements.reduce((acc, cur) => {
|
||||
@@ -116,6 +142,11 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
|
||||
gifUrl: `https://sendou.ink/static-assets/badges/${badge.code}.gif`,
|
||||
imageUrl: `https://sendou.ink/static-assets/badges/${badge.code}.png`,
|
||||
})),
|
||||
teams: user.teams.map((team) => ({
|
||||
name: team.name,
|
||||
role: team.role,
|
||||
teamPageUrl: `https://sendou.ink/t/${team.customUrl}`,
|
||||
})),
|
||||
};
|
||||
|
||||
return await cors(request, json(result));
|
||||
|
||||
@@ -35,7 +35,11 @@ export interface GetUserResponse {
|
||||
plusServerTier: 1 | 2 | 3 | null;
|
||||
weaponPool: Array<ProfileWeapon>;
|
||||
badges: Array<Badge>;
|
||||
/** Teams user is member of. The main team is always first in the array. */
|
||||
teams: Array<GlobalTeamMembership>;
|
||||
peakXp: number | null;
|
||||
/** Users current (or previous if it's off-season) ranked season (SendouQ & ranked tournaments) rank. Null if no rank for the season in question or the season does not have yet enough players on the leaderboard. */
|
||||
currentRank: SeasonalRank | null;
|
||||
}
|
||||
|
||||
/** GET /api/calendar/{year}/{week} */
|
||||
@@ -329,6 +333,61 @@ type Weapon = {
|
||||
|
||||
type ProfileWeapon = Weapon & { isFiveStar: boolean };
|
||||
|
||||
interface GlobalTeamMembership {
|
||||
/**
|
||||
* Name of the global team.
|
||||
*
|
||||
* @example "Moonlight"
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Role of the user in the team.
|
||||
*/
|
||||
role: TeamMemberRole | null;
|
||||
/**
|
||||
* URL for the global team page.
|
||||
*
|
||||
* @example "https://sendou.ink/t/moonlight"
|
||||
*/
|
||||
teamPageUrl: string;
|
||||
}
|
||||
|
||||
type TeamMemberRole =
|
||||
| "CAPTAIN"
|
||||
| "CO_CAPTAIN"
|
||||
| "FRONTLINE"
|
||||
| "SLAYER"
|
||||
| "SKIRMISHER"
|
||||
| "SUPPORT"
|
||||
| "MIDLINE"
|
||||
| "BACKLINE"
|
||||
| "FLEX"
|
||||
| "SUB"
|
||||
| "COACH"
|
||||
| "CHEERLEADER";
|
||||
|
||||
interface SeasonalRank {
|
||||
tier: {
|
||||
name: RankTierName;
|
||||
isPlus: boolean;
|
||||
};
|
||||
/**
|
||||
* Which season this rank is for.
|
||||
*
|
||||
* @example 7
|
||||
*/
|
||||
season: number;
|
||||
}
|
||||
|
||||
type RankTierName =
|
||||
| "LEVIATHAN"
|
||||
| "DIAMOND"
|
||||
| "PLATINUM"
|
||||
| "GOLD"
|
||||
| "SILVER"
|
||||
| "BRONZE"
|
||||
| "IRON";
|
||||
|
||||
type Badge = {
|
||||
/**
|
||||
* @example "Monday Afterparty"
|
||||
|
||||
Reference in New Issue
Block a user