Skip to content

Commit 66aee39

Browse files
Merge pull request #37 from stormgateworld:pr/player-activity
Add player activity in sidebar + move ranks
2 parents 658c359 + cbd4f98 commit 66aee39

11 files changed

+217
-44
lines changed

src/components/Widget.astro

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const { title, label } = Astro.props
99

1010
<div class="relative overflow-auto rounded-lg border border-gray-800/80 bg-gray-800/30 p-3 backdrop-blur-md sm:p-4">
1111
<div class="sticky left-0 top-0 flex flex-wrap items-center gap-x-5 gap-y-1">
12-
<h3 class="font-display flex-auto text-xl font-bold text-gray-200">
12+
<h3 class="font-display flex-auto text-lg font-bold text-gray-200">
1313
{title}
1414
</h3>
1515
<p class="text-md font-bold text-gray-400">

src/components/ui/Tooltip.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export function Tooltip(props: ParentProps<{ content: string; class?: string }>)
88
<KTooltip.Portal>
99
<KTooltip.Content class="rounded-sm border border-gray-700/50 bg-gray-800 px-1.5 py-1 text-gray-200 shadow-sm animate-in fade-in slide-in-from-bottom-1">
1010
<KTooltip.Arrow />
11-
<p>{props.content}</p>
11+
<p class="whitespace-pre-wrap">{props.content}</p>
1212
</KTooltip.Content>
1313
</KTooltip.Portal>
1414
</KTooltip.Root>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
---
2+
import type { PlayerActivityStats, PlayerResponse } from "../../lib/api"
3+
import Widget from "../Widget.astro"
4+
import { Tooltip } from "../ui/Tooltip"
5+
6+
interface Props {
7+
player: PlayerResponse
8+
activity: PlayerActivityStats
9+
}
10+
11+
const { player, activity } = Astro.props
12+
13+
function getWinrateClass(winrate: number) {
14+
if (winrate >= 0.65) return "bg-green-500"
15+
if (winrate > 0.5) return "bg-lime-500"
16+
if (winrate >= 0.45) return "bg-yellow-500"
17+
if (winrate > 0) return "bg-orange-500"
18+
return "bg-red-500"
19+
}
20+
21+
const maxMatches = Math.max(...activity.history.map((d) => d.matches))
22+
type Day = { date: Date; win_rate: number; games_count: number; size: number; tooltip: string }
23+
type Week = Day[]
24+
const today = new Date()
25+
// get the date of monday 12 weeks ago, fully including this week
26+
const start = new Date(today.getFullYear(), today.getMonth(), today.getDate() - today.getDay() - 11 * 7)
27+
const weeks: Week[] = []
28+
for (let i = 0; i < 12; i++) {
29+
const week: Week = []
30+
for (let j = 0; j < 7; j++) {
31+
const date = new Date(start.getFullYear(), start.getMonth(), start.getDate() + i * 7 + j)
32+
if (date > today) break
33+
const day = activity.history.find((d) => d.date === date.toISOString().slice(0, 10))
34+
week.push({
35+
date,
36+
win_rate: (day?.win_rate ?? 0) / 100,
37+
games_count: day?.matches ?? 0,
38+
size: day?.matches ? Math.max(50, Math.min((day.matches / maxMatches) * 150, 100)) : 0,
39+
tooltip: day?.matches
40+
? `${date.toLocaleDateString("en", { dateStyle: "long" })}\n${day.matches} Games\n${Math.round(
41+
day.win_rate
42+
)}% Win rate`
43+
: "No games",
44+
})
45+
}
46+
weeks.push(week)
47+
}
48+
function formatMonth(date: Date) {
49+
if (!date) return ""
50+
return date.getDate() <= 7 ? date.toLocaleString("en", { month: "short" }) : ""
51+
}
52+
---
53+
54+
<Widget title="Activity" label={`${activity.aggregated?.matches ?? 0} Matches`}>
55+
<div class="flex flex-row gap-1">
56+
{
57+
weeks.map((week, x) => (
58+
<div class="flex flex-col gap-1">
59+
{week.map((day, y) => {
60+
const { size, win_rate, games_count, tooltip } = day
61+
return games_count == 0 ? (
62+
<div class="h-4 w-4 rounded-sm bg-gray-800/50" />
63+
) : (
64+
<Tooltip content={tooltip} client:idle>
65+
<div class="relative flex h-4 w-4 items-center justify-center">
66+
<div
67+
class:list={[" rounded-sm", getWinrateClass(win_rate)]}
68+
style={`width: ${size}%; height: ${size}%;`}
69+
/>
70+
</div>
71+
</Tooltip>
72+
)
73+
})}
74+
<div class="relative h-4 text-xs text-gray-400">
75+
<span class="absolute top-0">{formatMonth(week[6]?.date)}</span>
76+
</div>
77+
</div>
78+
))
79+
}
80+
</div>
81+
</Widget>

src/lib/api/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,13 @@ export type { MatchParticipantResponse } from "./models/MatchParticipantResponse
1919
export type { MatchResponse } from "./models/MatchResponse"
2020
export { MatchResult } from "./models/MatchResult"
2121
export { MatchState } from "./models/MatchState"
22+
export type { PlayerActivityStats } from "./models/PlayerActivityStats"
23+
export type { PlayerActivityStatsRace } from "./models/PlayerActivityStatsRace"
2224
export type { PlayerMatchesResponse } from "./models/PlayerMatchesResponse"
2325
export type { PlayerPreferences } from "./models/PlayerPreferences"
2426
export type { PlayerResponse } from "./models/PlayerResponse"
27+
export type { PlayerStatsEntry } from "./models/PlayerStatsEntry"
28+
export type { PlayerStatsEntryNumBreakdown } from "./models/PlayerStatsEntryNumBreakdown"
2529
export { ProfilePrivacy } from "./models/ProfilePrivacy"
2630
export { Race } from "./models/Race"
2731
export type { StatsByTime } from "./models/StatsByTime"

src/lib/api/models/MatchParticipantPlayerLeaderboardEntryResponse.ts

+5
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@
33
/* tslint:disable */
44
/* eslint-disable */
55
export type MatchParticipantPlayerLeaderboardEntryResponse = {
6+
leaderboard_entry_id: string
67
league?: string | null
78
tier?: number | null
89
rank?: number | null
10+
wins: number
11+
losses: number
12+
ties?: number | null
13+
win_rate: number
914
}
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/* generated using openapi-typescript-codegen -- do no edit */
2+
/* istanbul ignore file */
3+
/* tslint:disable */
4+
/* eslint-disable */
5+
import type { PlayerActivityStatsRace } from "./PlayerActivityStatsRace"
6+
import type { PlayerStatsEntry } from "./PlayerStatsEntry"
7+
export type PlayerActivityStats = {
8+
updated_at: string
9+
aggregated?: PlayerStatsEntry | null
10+
history: Array<PlayerStatsEntry>
11+
races: Array<PlayerActivityStatsRace>
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/* generated using openapi-typescript-codegen -- do no edit */
2+
/* istanbul ignore file */
3+
/* tslint:disable */
4+
/* eslint-disable */
5+
import type { PlayerStatsEntry } from "./PlayerStatsEntry"
6+
export type PlayerActivityStatsRace = {
7+
aggregated?: PlayerStatsEntry | null
8+
history: Array<PlayerStatsEntry>
9+
}
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/* generated using openapi-typescript-codegen -- do no edit */
2+
/* istanbul ignore file */
3+
/* tslint:disable */
4+
/* eslint-disable */
5+
import type { PlayerStatsEntryNumBreakdown } from "./PlayerStatsEntryNumBreakdown"
6+
import type { Race } from "./Race"
7+
export type PlayerStatsEntry = {
8+
date?: string | null
9+
race?: Race | null
10+
matches: number
11+
wins: number
12+
losses: number
13+
win_rate: number
14+
mmr: PlayerStatsEntryNumBreakdown
15+
points: PlayerStatsEntryNumBreakdown
16+
match_length: PlayerStatsEntryNumBreakdown
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/* generated using openapi-typescript-codegen -- do no edit */
2+
/* istanbul ignore file */
3+
/* tslint:disable */
4+
/* eslint-disable */
5+
export type PlayerStatsEntryNumBreakdown = {
6+
max?: number | null
7+
min?: number | null
8+
average?: number | null
9+
}

src/lib/api/services/PlayersApi.ts

+23
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,27 @@ export class PlayersApi {
129129
},
130130
})
131131
}
132+
/**
133+
* @returns MatchResponse Player found successfully
134+
* @throws ApiError
135+
*/
136+
public static getPlayerStatisticsActivity({
137+
playerId,
138+
}: {
139+
/**
140+
* Player ID
141+
*/
142+
playerId: string
143+
}): CancelablePromise<MatchResponse> {
144+
return __request(OpenAPI, {
145+
method: "GET",
146+
url: "/v0/players/{player_id}/statistics/activity",
147+
path: {
148+
player_id: playerId,
149+
},
150+
errors: {
151+
404: `Player was not found`,
152+
},
153+
})
154+
}
132155
}

src/pages/players/[id]-[username].astro

+55-42
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import { Image } from "astro:assets"
66
import infernals from "../../assets/game/factions/infernals-small.png"
77
import vanguard from "../../assets/game/factions/vanguard-small.png"
88
import MatchPreview from "../../components/widgets/MatchPreview.astro"
9-
import { PlayersApi } from "../../lib/api"
10-
import { leagues } from "../../assets/game/leagues/leagues"
9+
import { PlayersApi, type PlayerActivityStats } from "../../lib/api"
1110
import { RankedBadge } from "../../components/ui/RankedBadge"
11+
import PlayerActivity from "../../components/widgets/PlayerActivity.astro"
1212
const themes = {
1313
infernals: {
1414
badge: "bg-red-800/20 border-red-500/50",
@@ -33,55 +33,68 @@ const highestLeague = player.leaderboard_entries.reduce(
3333
(acc, entry) => (entry.points > acc.points ? entry : acc),
3434
player.leaderboard_entries[0]
3535
)
36+
37+
const playerActivity = (await PlayersApi.getPlayerStatisticsActivity({ playerId })) as any as PlayerActivityStats
3638
---
3739

3840
<Layout title={player.nickname}>
3941
<div class="w-full border-b border-gray-700/50 bg-gray-800/50 backdrop-blur-lg">
40-
<div class="mx-auto flex max-w-screen-md flex-wrap items-center gap-4 px-4 py-4">
42+
<div class="mx-auto flex max-w-screen-lg flex-wrap items-center gap-4 px-4 py-4">
4143
<RankedBadge entry={highestLeague} class="w-20" client:load />
4244
<h1 class="flex-auto text-2xl font-bold text-gray-50">{player.nickname}</h1>
45+
</div>
46+
</div>
47+
<section class="relative mx-auto flex max-w-screen-lg flex-col gap-6 px-4 py-8 md:px-7 lg:flex-row">
48+
<div class="flex-auto">
49+
<Widget title="Recent Matches" label="Closed Beta Ranked">
50+
<div class="">
51+
{playerMatches.matches.map((match) => <MatchPreview match={match} mainPlayerId={playerId} />)}
52+
</div>
53+
</Widget>
54+
</div>
55+
<div class="order-first flex basis-1/4 flex-col gap-6 sm:flex-row lg:order-none lg:flex-col">
56+
<Widget title="Top Ranks">
57+
<div class="flex flex-col gap-2">
58+
{
59+
player.leaderboard_entries.map((entry) => (
60+
<div
61+
class:list={[
62+
"rounded-lg pl-3 pr-1 py-2 -mx-2 flex items-center gap-3 text-sm sm:text-base",
63+
themes[entry.race as Theme].badge,
64+
]}
65+
>
66+
<Image
67+
src={entry.race === "infernals" ? infernals : vanguard}
68+
alt={entry.race}
69+
class="size-6 sm:size-10"
70+
/>
71+
<div>
72+
<span class:list={["text-xs", themes[entry.race as Theme].badgeLabel]}>Rank</span>
73+
<p>#{entry.rank}</p>
74+
</div>
75+
<div>
76+
<span class:list={["text-xs", themes[entry.race as Theme].badgeLabel]}>Points</span>
77+
<p>
78+
{Math.round(entry.points)}
4379

44-
<div class="flex flex-wrap gap-4">
45-
{
46-
player.leaderboard_entries.map((entry) => (
47-
<div
48-
class:list={[
49-
"border rounded-lg px-4 py-2 flex items-center gap-3 text-sm sm:text-base",
50-
themes[entry.race as Theme].badge,
51-
]}
52-
>
53-
<Image
54-
src={entry.race === "infernals" ? infernals : vanguard}
55-
alt={entry.race}
56-
class="size-6 sm:size-10"
57-
/>
58-
<div>
59-
<span class:list={["text-xs", themes[entry.race as Theme].badgeLabel]}>Rank</span>
60-
<p>#{entry.rank}</p>
61-
</div>
62-
<div>
63-
<span class:list={["text-xs", themes[entry.race as Theme].badgeLabel]}>Points</span>
64-
<p>
65-
{Math.round(entry.points)}
66-
67-
<RankedBadge entry={entry} class="inline-block w-4" client:load />
68-
</p>
69-
</div>
70-
<div>
71-
<span class:list={["text-xs", themes[entry.race as Theme].badgeLabel]}>MMR</span>
72-
<p>{Math.round(entry.mmr)}</p>
80+
<RankedBadge entry={entry} class="inline-block w-4" client:load />
81+
</p>
82+
</div>
83+
<div>
84+
<span class:list={["text-xs", themes[entry.race as Theme].badgeLabel]}>MMR</span>
85+
<p>{Math.round(entry.mmr)}</p>
86+
</div>
7387
</div>
74-
</div>
75-
))
76-
}
88+
))
89+
}
90+
</div>
91+
</Widget>
92+
<div class="hidden sm:block">
93+
<PlayerActivity activity={playerActivity} {player} />
7794
</div>
7895
</div>
79-
</div>
80-
<section class="relative mx-auto max-w-screen-md px-4 py-8">
81-
<Widget title="Recent Matches" label="Closed Beta Ranked">
82-
<div class="-mx-3 sm:-mx-4">
83-
{playerMatches.matches.map((match) => <MatchPreview match={match} mainPlayerId={playerId} />)}
84-
</div>
85-
</Widget>
96+
<div class="sm:hidden">
97+
<PlayerActivity activity={playerActivity} {player} />
98+
</div>
8699
</section>
87100
</Layout>

0 commit comments

Comments
 (0)