Add player profiles and community settings
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,6 +5,7 @@ const player: RawPlayer = {
|
||||
id: 'player-1',
|
||||
accountId: 'account-1',
|
||||
displayName: 'Nova',
|
||||
battleTag: 'Nova#1234',
|
||||
ratings: { tank: 36, damage: 28, support: 24 },
|
||||
preferredRoles: ['Tank'],
|
||||
preferredPlayerIds: ['player-2'],
|
||||
@@ -19,6 +20,7 @@ describe('backend DTO adapters', () => {
|
||||
it('adapts backend role ratings without changing their values', () => {
|
||||
expect(adaptPlayer(player)).toMatchObject({
|
||||
displayName: 'Nova',
|
||||
battleTag: 'Nova#1234',
|
||||
ratings: { tank: 36, damage: 28, support: 24 },
|
||||
preferredRoles: ['tank'],
|
||||
preferredPlayerIds: ['player-2'],
|
||||
@@ -118,10 +120,38 @@ describe('actual backend routes', () => {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}))
|
||||
await api.updateRatings('Nova', { tank: 37, damage: 29, support: 25 }, ['tank', 'damage'], ['player-2'], ['player-3'])
|
||||
await api.updateRatings('Nova Prime', 'Nova#5678', { tank: 37, damage: 29, support: 25 }, ['tank', 'damage'], ['player-2'], ['player-3'])
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/me/player', expect.objectContaining({ method: 'PATCH' }))
|
||||
expect(fetchMock.mock.calls[0][1]?.body).toContain('"preferredRoles":["Tank","Damage"]')
|
||||
expect(fetchMock.mock.calls[0][1]?.body).toContain('"avoidedPlayerIds":["player-3"]')
|
||||
expect(fetchMock.mock.calls[0][1]?.body).toContain('"displayName":"Nova Prime"')
|
||||
expect(fetchMock.mock.calls[0][1]?.body).toContain('"battleTag":"Nova#5678"')
|
||||
})
|
||||
|
||||
it('loads public profile history and updates community settings', async () => {
|
||||
const publicProfile = {
|
||||
player: {
|
||||
id: player.id,
|
||||
displayName: player.displayName,
|
||||
battleTag: player.battleTag,
|
||||
ratings: player.ratings,
|
||||
isGuest: false,
|
||||
},
|
||||
summary: { seriesPlayed: 3, seriesWon: 2, seriesLost: 1, winRate: 2 / 3, mapsPlayed: 7, mapsWon: 4, mapsLost: 2, mapsDrawn: 1 },
|
||||
recentMatches: [{ seriesId: 'series-1', eventId: 'event-1', eventName: 'Cup', eventDate: '2026-07-18T00:00:00Z', ownTeamId: 'a', ownTeamName: 'A', opponentTeamId: 'b', opponentTeamName: 'B', ownScore: 2, opponentScore: 1, outcome: 'win' }],
|
||||
}
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch')
|
||||
.mockResolvedValueOnce(new Response(JSON.stringify(publicProfile), { status: 200, headers: { 'Content-Type': 'application/json' } }))
|
||||
.mockResolvedValueOnce(new Response(JSON.stringify({ discordInviteUrl: 'https://discord.gg/test' }), { status: 200, headers: { 'Content-Type': 'application/json' } }))
|
||||
const profile = await api.playerProfile(player.id)
|
||||
expect(profile.stats.mapsDrawn).toBe(1)
|
||||
expect(profile.recentMatches[0].outcome).toBe('win')
|
||||
await api.updateCommunitySettings('https://discord.gg/test')
|
||||
expect(fetchMock.mock.calls[0][0]).toBe('/api/players/player-1')
|
||||
expect(fetchMock.mock.calls[1]).toEqual(['/api/community/settings', expect.objectContaining({
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ discordInviteUrl: 'https://discord.gg/test' }),
|
||||
})])
|
||||
})
|
||||
|
||||
it('sends backend RSVP enum values to the player-specific route', async () => {
|
||||
|
||||
@@ -18,6 +18,35 @@ export interface Player {
|
||||
preferredPlayerIds: string[]
|
||||
avoidedPlayerIds: string[]
|
||||
}
|
||||
export interface PlayerStats {
|
||||
seriesPlayed: number
|
||||
seriesWon: number
|
||||
seriesLost: number
|
||||
winRate: number
|
||||
mapsPlayed: number
|
||||
mapsWon: number
|
||||
mapsLost: number
|
||||
mapsDrawn: number
|
||||
}
|
||||
export interface RecentMatch {
|
||||
seriesId: string
|
||||
eventId: string
|
||||
eventName: string
|
||||
startedAt: string
|
||||
teamId: string
|
||||
teamName: string
|
||||
opponentTeamId: string
|
||||
opponentTeamName: string
|
||||
scoreFor: number
|
||||
scoreAgainst: number
|
||||
outcome: 'win' | 'loss'
|
||||
}
|
||||
export interface PublicPlayerProfile extends Player {
|
||||
isGuest: boolean
|
||||
stats: PlayerStats
|
||||
recentMatches: RecentMatch[]
|
||||
}
|
||||
export interface CommunitySettings { discordInviteUrl: string }
|
||||
export interface Session {
|
||||
account: { id: string; discordId: string; displayName: string; avatarUrl?: string; role: AccountRole }
|
||||
player: Player
|
||||
@@ -154,13 +183,36 @@ export interface RawAccount {
|
||||
}
|
||||
export interface RawRatings { tank: number; damage: number; support: number }
|
||||
export interface RawPlayer {
|
||||
id: string; accountId: string; displayName: string; ratings: RawRatings
|
||||
id: string; accountId: string; displayName: string; battleTag?: string; ratings: RawRatings
|
||||
preferredRoles?: Array<'Tank' | 'Damage' | 'Support'>
|
||||
preferredPlayerIds?: string[]
|
||||
avoidedPlayerIds?: string[]
|
||||
createdAt: string; updatedAt: string
|
||||
}
|
||||
export interface RawSession { account: RawAccount; player: RawPlayer }
|
||||
export interface RawPlayerProfile {
|
||||
player: {
|
||||
id: string
|
||||
displayName: string
|
||||
battleTag: string
|
||||
ratings: RawRatings
|
||||
isGuest: boolean
|
||||
}
|
||||
summary: PlayerStats
|
||||
recentMatches: Array<{
|
||||
seriesId: string
|
||||
eventId: string
|
||||
eventName: string
|
||||
eventDate: string
|
||||
ownTeamId: string
|
||||
ownTeamName: string
|
||||
opponentTeamId: string
|
||||
opponentTeamName: string
|
||||
ownScore: number
|
||||
opponentScore: number
|
||||
outcome: 'win' | 'loss'
|
||||
}>
|
||||
}
|
||||
export interface RawEvent {
|
||||
id: string; name: string; description: string; startsAt: string; endsAt: string
|
||||
registrationDeadline: string; createdBy: string; createdAt: string; updatedAt: string
|
||||
@@ -223,6 +275,7 @@ export function adaptPlayer(raw: RawPlayer): Player {
|
||||
id: raw.id,
|
||||
accountId: raw.accountId,
|
||||
displayName: raw.displayName,
|
||||
battleTag: raw.battleTag || undefined,
|
||||
ratings,
|
||||
preferredRoles,
|
||||
preferredPlayerIds: raw.preferredPlayerIds ?? [],
|
||||
@@ -529,6 +582,7 @@ export const api = {
|
||||
profile: async () => adaptPlayer((await raw.session()).player),
|
||||
updateRatings: async (
|
||||
displayName: string,
|
||||
battleTag: string,
|
||||
ratings: Pick<Ratings, PlayerRole>,
|
||||
preferredRoles: PlayerRole[],
|
||||
preferredPlayerIds: string[],
|
||||
@@ -538,13 +592,50 @@ export const api = {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({
|
||||
displayName,
|
||||
battleTag: battleTag.trim(),
|
||||
ratings: { tank: ratings.tank, damage: ratings.damage, support: ratings.support },
|
||||
preferredRoles: preferredRoles.map((role) => role === 'tank' ? 'Tank' : role === 'damage' ? 'Damage' : 'Support'),
|
||||
preferredPlayerIds,
|
||||
avoidedPlayerIds,
|
||||
}),
|
||||
})),
|
||||
playerProfile: async (playerId: string): Promise<PublicPlayerProfile> => {
|
||||
const profile = await request<RawPlayerProfile>(`/players/${playerId}`)
|
||||
return {
|
||||
id: profile.player.id,
|
||||
accountId: profile.player.isGuest ? undefined : 'linked',
|
||||
displayName: profile.player.displayName,
|
||||
battleTag: profile.player.battleTag || undefined,
|
||||
ratings: {
|
||||
tank: toRankOrdinal(profile.player.ratings.tank),
|
||||
damage: toRankOrdinal(profile.player.ratings.damage),
|
||||
support: toRankOrdinal(profile.player.ratings.support),
|
||||
updatedAt: '',
|
||||
},
|
||||
preferredRoles: [],
|
||||
preferredPlayerIds: [],
|
||||
avoidedPlayerIds: [],
|
||||
isGuest: profile.player.isGuest,
|
||||
stats: profile.summary,
|
||||
recentMatches: profile.recentMatches.map((match) => ({
|
||||
seriesId: match.seriesId,
|
||||
eventId: match.eventId,
|
||||
eventName: match.eventName,
|
||||
startedAt: match.eventDate,
|
||||
teamId: match.ownTeamId,
|
||||
teamName: match.ownTeamName,
|
||||
opponentTeamId: match.opponentTeamId,
|
||||
opponentTeamName: match.opponentTeamName,
|
||||
scoreFor: match.ownScore,
|
||||
scoreAgainst: match.opponentScore,
|
||||
outcome: match.outcome,
|
||||
})),
|
||||
} satisfies PublicPlayerProfile
|
||||
},
|
||||
players: async () => (await raw.players()).map(adaptPlayer),
|
||||
communitySettings: () => request<CommunitySettings>('/community/settings'),
|
||||
updateCommunitySettings: (discordInviteUrl: string) =>
|
||||
request<CommunitySettings>('/community/settings', { method: 'PUT', body: JSON.stringify({ discordInviteUrl }) }),
|
||||
accounts: () => request<RawAccount[]>('/accounts'),
|
||||
setModerator: (accountId: string, moderator: boolean) =>
|
||||
request<RawAccount>(`/accounts/${accountId}/moderator`, { method: 'PATCH', body: JSON.stringify({ moderator }) }),
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
BracketDraft,
|
||||
MixEvent,
|
||||
Player,
|
||||
PublicPlayerProfile,
|
||||
Registration,
|
||||
Series,
|
||||
Session,
|
||||
@@ -307,3 +308,50 @@ export const demoBracketDraft: BracketDraft = {
|
||||
{ id: 'draft-m3', round: 2, order: 0, slotA: { kind: 'Winner', matchId: 'draft-m1' }, slotB: { kind: 'Winner', matchId: 'draft-m2' } },
|
||||
],
|
||||
}
|
||||
|
||||
export const demoCommunitySettings = {
|
||||
discordInviteUrl: 'https://discord.gg/mixmaker',
|
||||
}
|
||||
|
||||
export const demoPlayerProfiles: PublicPlayerProfile[] = players.map((player, index) => ({
|
||||
...player,
|
||||
isGuest: !player.accountId,
|
||||
stats: {
|
||||
seriesPlayed: 18 + index,
|
||||
seriesWon: 11 + (index % 4),
|
||||
seriesLost: 7 + (index % 3),
|
||||
winRate: Math.round(((11 + (index % 4)) / (18 + index)) * 100),
|
||||
mapsPlayed: 42 + index * 2,
|
||||
mapsWon: 25 + index,
|
||||
mapsLost: 14 + index,
|
||||
mapsDrawn: 3,
|
||||
},
|
||||
recentMatches: [
|
||||
{
|
||||
seriesId: 'series-1',
|
||||
eventId: 'event-3',
|
||||
eventName: 'Summer Clash #4',
|
||||
startedAt: daysFromNow(-2, 20),
|
||||
teamId: 'team-alpha',
|
||||
teamName: 'Ember Wolves',
|
||||
opponentTeamId: 'team-beta',
|
||||
opponentTeamName: 'Azure Phantoms',
|
||||
scoreFor: 2,
|
||||
scoreAgainst: 1,
|
||||
outcome: 'win',
|
||||
},
|
||||
{
|
||||
seriesId: 'series-2',
|
||||
eventId: 'event-5',
|
||||
eventName: 'Completed Friday Mix',
|
||||
startedAt: daysFromNow(-7, 20),
|
||||
teamId: 'team-alpha',
|
||||
teamName: 'Ember Wolves',
|
||||
opponentTeamId: 'team-beta',
|
||||
opponentTeamName: 'Neon Foxes',
|
||||
scoreFor: 1,
|
||||
scoreAgainst: 2,
|
||||
outcome: 'loss',
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user