diff --git a/src/components/widgets/FactionDropdown.tsx b/src/components/widgets/FactionDropdown.tsx
new file mode 100644
index 0000000..81c0c79
--- /dev/null
+++ b/src/components/widgets/FactionDropdown.tsx
@@ -0,0 +1,31 @@
+import { createEffect, createSignal, onMount } from "solid-js"
+import { SelectButton, type SelectButtonOption } from "../ui/SelectButton"
+import infernals from "../../assets/game/factions/infernals-small-glow.png"
+import vanguard from "../../assets/game/factions/vanguard-small-glow.png"
+import { Race } from "../../lib/api"
+
+const factionOptions: SelectButtonOption[] = [
+ {
+ label: "Infernals",
+ value: Race.INFERNALS,
+ icon: infernals.src,
+ },
+ { label: "Vanguard", value: Race.VANGUARD, icon: vanguard.src },
+]
+
+export function FactionDropdown(props: { queryParam: string; selected: (typeof factionOptions)[number]["value"] }) {
+ const selected = props.selected ?? "all"
+ const [faction, setFaction] = createSignal(
+ factionOptions.find((option) => option.value === selected) || factionOptions[0]
+ )
+
+ createEffect(() => {
+ const urlParams = new URLSearchParams(window.location.search)
+ if (faction()?.value === selected) return
+ if (faction()?.value && faction()?.value != "all") urlParams.set(props.queryParam, faction()?.value!)
+ else urlParams.delete(props.queryParam)
+ window.location.href = `${window.location.pathname}${urlParams.size ? "?" + urlParams.toString() : ""}`
+ })
+
+ return
+}
diff --git a/src/components/widgets/GameLengthChart.tsx b/src/components/widgets/GameLengthChart.tsx
index 486d869..989cbf9 100644
--- a/src/components/widgets/GameLengthChart.tsx
+++ b/src/components/widgets/GameLengthChart.tsx
@@ -55,6 +55,7 @@ export function GameLengthChart(props: GameLengthProps) {
intersect: false,
mode: "index",
},
+ maxBarThickness: 50,
plugins: {
title: {
display: false,
diff --git a/src/components/widgets/MmrHistoryChart.tsx b/src/components/widgets/MmrHistoryChart.tsx
new file mode 100644
index 0000000..168413c
--- /dev/null
+++ b/src/components/widgets/MmrHistoryChart.tsx
@@ -0,0 +1,70 @@
+// @ts-nocheck
+
+import { onMount } from "solid-js"
+import { Chart, Title, Tooltip, Colors, TimeScale, type ChartOptions, type ChartData } from "chart.js"
+import { Line } from "solid-chartjs"
+import "chartjs-adapter-date-fns"
+
+type MmrHistoryChartProps = {
+ labels: string[]
+ data: number[]
+}
+
+const longNumberFormatter = new Intl.NumberFormat("en-us")
+
+export function MmrHistoryChart(props: MmrHistoryChartProps) {
+ let canvas: HTMLCanvasElement
+ onMount(() => {
+ Chart.register(Title, Tooltip, Colors, TimeScale)
+ })
+
+ const chartData: ChartData = {
+ labels: props.labels,
+ datasets: [
+ {
+ label: "MMR",
+ data: props.data,
+ tension: 0.3,
+ borderColor: "#9E9E9E",
+ pointRadius: 5,
+ pointHoverRadius: 2,
+ pointBorderColor: "transparent",
+ },
+ ],
+ }
+
+ const chartOptions: ChartOptions = {
+ responsive: true,
+ maintainAspectRatio: true,
+ interaction: {
+ intersect: false,
+ mode: "index",
+ },
+ scales: {
+ y: {
+ ticks: {
+ precision: 0,
+ },
+ offset: true,
+ },
+ x: {
+ type: "time",
+ time: { unit: "day", tooltipFormat: "d MMM, y" },
+ },
+ },
+ plugins: {
+ tooltip: {
+ displayColors: false,
+ callbacks: {
+ label: (c) => `MMR: ${Math.round(c.parsed.y)}`,
+ },
+ },
+ },
+ }
+
+ return (
+
+ (canvas = c)} />
+
+ )
+}
diff --git a/src/layouts/PlayerLayout.astro b/src/layouts/PlayerLayout.astro
index 752dd8d..b21072f 100644
--- a/src/layouts/PlayerLayout.astro
+++ b/src/layouts/PlayerLayout.astro
@@ -44,6 +44,7 @@ const playerInfo = (await getCollection("players")).find((p) => p.data.playerId
current={[
{ href: `/players/${slug}`, label: "Overview" },
{ href: `/players/${slug}/matches`, label: "Match History" },
+ { href: `/players/${slug}/statistics`, label: "Statistics" },
]}
/>
diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts
index 160a6e9..28f4604 100644
--- a/src/lib/api/index.ts
+++ b/src/lib/api/index.ts
@@ -11,11 +11,12 @@ export type { ActivityStatistics } from "./models/ActivityStatistics"
export type { ActivityStatisticsActivity } from "./models/ActivityStatisticsActivity"
export type { ActivityStatisticsEntry } from "./models/ActivityStatisticsEntry"
export type { ActivityStatisticsServerEntry } from "./models/ActivityStatisticsServerEntry"
+export { Aggregation } from "./models/Aggregation"
export type { ErrorResponse } from "./models/ErrorResponse"
export { Leaderboard } from "./models/Leaderboard"
export type { LeaderboardDumpResponse } from "./models/LeaderboardDumpResponse"
export type { LeaderboardEntryHistory } from "./models/LeaderboardEntryHistory"
-export type { LeaderboardEntryHistoryEntry } from "./models/LeaderboardEntryHistoryEntry"
+export type { LeaderboardEntryHistoryRow } from "./models/LeaderboardEntryHistoryRow"
export type { LeaderboardEntryResponse } from "./models/LeaderboardEntryResponse"
export { LeaderboardOrder } from "./models/LeaderboardOrder"
export type { LeaderboardResponse } from "./models/LeaderboardResponse"
@@ -35,13 +36,13 @@ export type { PlayerMatchupsStatsEntry } from "./models/PlayerMatchupsStatsEntry
export type { PlayerMatchupsStatsMatchup } from "./models/PlayerMatchupsStatsMatchup"
export type { PlayerOpponentsStats } from "./models/PlayerOpponentsStats"
export type { PlayerOpponentsStatsOpponent } from "./models/PlayerOpponentsStatsOpponent"
-export type { PlayerPreferences } from "./models/PlayerPreferences"
export type { PlayerResponse } from "./models/PlayerResponse"
export type { PlayerStatsEntry } from "./models/PlayerStatsEntry"
export type { PlayerStatsEntryAggregated } from "./models/PlayerStatsEntryAggregated"
export type { PlayerStatsEntryNumBreakdown } from "./models/PlayerStatsEntryNumBreakdown"
export { ProfilePrivacy } from "./models/ProfilePrivacy"
export { Race } from "./models/Race"
+export { Resolution } from "./models/Resolution"
export type { StatsByTime } from "./models/StatsByTime"
export type { StatsByTimeEntry } from "./models/StatsByTimeEntry"
export type { StatsByTimeHistoryPoint } from "./models/StatsByTimeHistoryPoint"
diff --git a/src/lib/api/models/PlayerPreferences.ts b/src/lib/api/models/Aggregation.ts
similarity index 50%
rename from src/lib/api/models/PlayerPreferences.ts
rename to src/lib/api/models/Aggregation.ts
index 84ead43..1ab7c5d 100644
--- a/src/lib/api/models/PlayerPreferences.ts
+++ b/src/lib/api/models/Aggregation.ts
@@ -2,7 +2,8 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
-import type { ProfilePrivacy } from "./ProfilePrivacy"
-export type PlayerPreferences = {
- privacy_profile?: ProfilePrivacy | null
+export enum Aggregation {
+ LAST = "last",
+ MAX_MMR = "max_mmr",
+ MAX_POINTS = "max_points",
}
diff --git a/src/lib/api/models/LeaderboardEntryHistory.ts b/src/lib/api/models/LeaderboardEntryHistory.ts
index ca8c7bf..9113eac 100644
--- a/src/lib/api/models/LeaderboardEntryHistory.ts
+++ b/src/lib/api/models/LeaderboardEntryHistory.ts
@@ -2,7 +2,12 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
-import type { LeaderboardEntryHistoryEntry } from "./LeaderboardEntryHistoryEntry"
+import type { Aggregation } from "./Aggregation"
+import type { LeaderboardEntryHistoryRow } from "./LeaderboardEntryHistoryRow"
+import type { Resolution } from "./Resolution"
export type LeaderboardEntryHistory = {
- history: Array
+ cached_at: string
+ resolution: Resolution
+ aggregation: Aggregation
+ history: Array
}
diff --git a/src/lib/api/models/LeaderboardEntryHistoryEntry.ts b/src/lib/api/models/LeaderboardEntryHistoryRow.ts
similarity index 64%
rename from src/lib/api/models/LeaderboardEntryHistoryEntry.ts
rename to src/lib/api/models/LeaderboardEntryHistoryRow.ts
index 4c76d12..85192e0 100644
--- a/src/lib/api/models/LeaderboardEntryHistoryEntry.ts
+++ b/src/lib/api/models/LeaderboardEntryHistoryRow.ts
@@ -2,9 +2,8 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
-export type LeaderboardEntryHistoryEntry = {
+export type LeaderboardEntryHistoryRow = {
time: string
- mmr: number
- max_confirmed_mmr?: number | null
+ mmr?: number | null
points?: number | null
}
diff --git a/src/lib/api/models/Race.ts b/src/lib/api/models/Race.ts
index 14a9e77..3dc3f6a 100644
--- a/src/lib/api/models/Race.ts
+++ b/src/lib/api/models/Race.ts
@@ -5,5 +5,4 @@
export enum Race {
INFERNALS = "infernals",
VANGUARD = "vanguard",
- RANDOM = "random",
}
diff --git a/src/lib/api/models/Resolution.ts b/src/lib/api/models/Resolution.ts
new file mode 100644
index 0000000..580dd6d
--- /dev/null
+++ b/src/lib/api/models/Resolution.ts
@@ -0,0 +1,10 @@
+/* generated using openapi-typescript-codegen -- do no edit */
+/* istanbul ignore file */
+/* tslint:disable */
+/* eslint-disable */
+export enum Resolution {
+ MINUTE = "minute",
+ HOUR = "hour",
+ DAY = "day",
+ WEEK = "week",
+}
diff --git a/src/lib/api/services/LeaderboardEntriesApi.ts b/src/lib/api/services/LeaderboardEntriesApi.ts
index 3b0e4b9..0a7dcd6 100644
--- a/src/lib/api/services/LeaderboardEntriesApi.ts
+++ b/src/lib/api/services/LeaderboardEntriesApi.ts
@@ -2,7 +2,9 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
+import type { Aggregation } from "../models/Aggregation"
import type { LeaderboardEntryHistory } from "../models/LeaderboardEntryHistory"
+import type { Resolution } from "../models/Resolution"
import type { CancelablePromise } from "../core/CancelablePromise"
import { OpenAPI } from "../core/OpenAPI"
import { request as __request } from "../core/request"
@@ -13,11 +15,15 @@ export class LeaderboardEntriesApi {
*/
public static getLeaderboardEntryHistory({
leaderboardEntryId,
+ resolution,
+ aggregation,
}: {
/**
* Player Leaderboard Entry ID
*/
leaderboardEntryId: string
+ resolution?: Resolution | null
+ aggregation?: Aggregation | null
}): CancelablePromise {
return __request(OpenAPI, {
method: "GET",
@@ -25,6 +31,10 @@ export class LeaderboardEntriesApi {
path: {
leaderboard_entry_id: leaderboardEntryId,
},
+ query: {
+ resolution: resolution,
+ aggregation: aggregation,
+ },
errors: {
404: `Player leaderboard entry was not found`,
500: `Server error`,
diff --git a/src/lib/api/services/PlayersApi.ts b/src/lib/api/services/PlayersApi.ts
index a3e5596..f177161 100644
--- a/src/lib/api/services/PlayersApi.ts
+++ b/src/lib/api/services/PlayersApi.ts
@@ -7,7 +7,6 @@ import type { PlayerActivityStats } from "../models/PlayerActivityStats"
import type { PlayerMatchesResponse } from "../models/PlayerMatchesResponse"
import type { PlayerMatchupsStats } from "../models/PlayerMatchupsStats"
import type { PlayerOpponentsStats } from "../models/PlayerOpponentsStats"
-import type { PlayerPreferences } from "../models/PlayerPreferences"
import type { PlayerResponse } from "../models/PlayerResponse"
import type { Race } from "../models/Race"
import type { CancelablePromise } from "../core/CancelablePromise"
@@ -45,6 +44,7 @@ export class PlayersApi {
public static getPlayerMatches({
playerId,
race,
+ opponentPlayerId,
page,
count,
}: {
@@ -53,6 +53,7 @@ export class PlayersApi {
*/
playerId: string
race?: Race | null
+ opponentPlayerId?: string | null
page?: number | null
count?: number | null
}): CancelablePromise {
@@ -64,6 +65,7 @@ export class PlayersApi {
},
query: {
race: race,
+ opponent_player_id: opponentPlayerId,
page: page,
count: count,
},
@@ -97,58 +99,6 @@ export class PlayersApi {
},
})
}
- /**
- * @returns PlayerPreferences Player found successfully
- * @throws ApiError
- */
- public static getPlayerPreferences({
- playerId,
- }: {
- /**
- * Player ID
- */
- playerId: string
- }): CancelablePromise {
- return __request(OpenAPI, {
- method: "GET",
- url: "/v0/players/{player_id}/preferences",
- path: {
- player_id: playerId,
- },
- errors: {
- 404: `Player was not found`,
- 500: `Server error`,
- },
- })
- }
- /**
- * @returns PlayerPreferences Player preferences updated successfully
- * @throws ApiError
- */
- public static updatePlayerPreferences({
- playerId,
- requestBody,
- }: {
- /**
- * Player ID
- */
- playerId: string
- requestBody: PlayerPreferences
- }): CancelablePromise {
- return __request(OpenAPI, {
- method: "PUT",
- url: "/v0/players/{player_id}/preferences",
- path: {
- player_id: playerId,
- },
- body: requestBody,
- mediaType: "application/json",
- errors: {
- 404: `Player was not found`,
- 500: `Server error`,
- },
- })
- }
/**
* @returns PlayerActivityStats Player found successfully
* @throws ApiError
diff --git a/src/lib/labels.ts b/src/lib/labels.ts
index 387b9e2..d1b904e 100644
--- a/src/lib/labels.ts
+++ b/src/lib/labels.ts
@@ -3,7 +3,6 @@ import { League, Race } from "./api"
export const raceLabels: Record = {
infernals: "Infernals",
vanguard: "Vanguard",
- random: "Random",
}
export const leagueLabels: Record = {
diff --git a/src/pages/players/[id]-[username]/matches.astro b/src/pages/players/[id]-[username]/matches.astro
index 9875209..8838063 100644
--- a/src/pages/players/[id]-[username]/matches.astro
+++ b/src/pages/players/[id]-[username]/matches.astro
@@ -4,27 +4,13 @@ import PlayerLayout from "../../../layouts/PlayerLayout.astro"
import { MatchHistory } from "../../../components/widgets/MatchHistory.tsx"
import { PlayersApi, Race } from "../../../lib/api"
import Section from "../../../components/layout/Section.astro"
+import { getDataOrErrorResponse } from "../../../lib/utils"
const { page = 1, faction } = Object.fromEntries(new URL(Astro.request.url).searchParams.entries())
-// to be moved to own file
-async function getDataOrErrorResponse(
- ...values: T
-): Promise<[{ -readonly [P in keyof T]: Awaited }, error: Response | null]> {
- try {
- const result = await Promise.all(values)
- return [result, null]
- } catch (e) {
- return [[] as any, new Response(null, { status: 500, statusText: `${e}` })]
- }
-}
-
const playerId = Astro.params.id!
-const [[player], error] = await getDataOrErrorResponse(
- PlayersApi.getPlayer({ playerId }),
- PlayersApi.getPlayerMatches({ playerId })
-)
+const [[player], error] = await getDataOrErrorResponse(PlayersApi.getPlayer({ playerId }))
if (error) return error
---
diff --git a/src/pages/players/[id]-[username]/statistics.astro b/src/pages/players/[id]-[username]/statistics.astro
new file mode 100644
index 0000000..34f750d
--- /dev/null
+++ b/src/pages/players/[id]-[username]/statistics.astro
@@ -0,0 +1,121 @@
+---
+export const prerender = false
+
+import {
+ Aggregation,
+ LeaderboardEntriesApi,
+ LeaderboardEntryHistoryRow,
+ PlayersApi,
+ Resolution,
+} from "../../../lib/api"
+import Section from "../../../components/layout/Section.astro"
+import Main from "../../../components/layout/Main.astro"
+import { MmrHistoryChart } from "../../../components/widgets/MmrHistoryChart"
+import Widget from "../../../components/Widget.astro"
+import { FactionDropdown } from "../../../components/widgets/FactionDropdown"
+import { getDataOrErrorResponse } from "../../../lib/utils"
+import Sidebar from "../../../components/layout/Sidebar.astro"
+import PlayerActivity from "../../../components/widgets/PlayerActivity.astro"
+import PlayerMatchupStats from "../../../components/widgets/PlayerMatchupStats.astro"
+import PlayerGameLengthStats from "../../../components/widgets/PlayerGameLengthStats.astro"
+import PlayerOpponents from "../../../components/widgets/PlayerOpponents.astro"
+import PlayerLayout from "../../../layouts/PlayerLayout.astro"
+import { GameLengthChart } from "../../../components/widgets/GameLengthChart"
+import { Image } from "astro:assets"
+import infernals from "../../../assets/game/factions/infernals-small.png"
+import vanguard from "../../../assets/game/factions/vanguard-small.png"
+import { raceLabels } from "../../../lib/labels"
+
+const playerId = Astro.params.id!
+
+const [[player, playerActivity, playerMatchupStats, playerOpponents], error] = await getDataOrErrorResponse(
+ PlayersApi.getPlayer({ playerId }),
+ PlayersApi.getPlayerStatisticsActivity({ playerId }),
+ PlayersApi.getPlayerStatisticsMatchups({ playerId }),
+ PlayersApi.getPlayerStatisticsOpponents({ playerId, count: 10 })
+)
+if (error) return error
+
+const leaderboardEntries = player?.leaderboard_entries
+ .filter((x) => x.matches > 0)
+ .sort((a, b) => (b.points ?? 0) - (a.points ?? 0))
+
+const faction = new URLSearchParams(Astro.url.search).get("faction") || leaderboardEntries?.[0].race
+const leaderboard = leaderboardEntries.find((leaderboard) => leaderboard.race === faction) || leaderboardEntries?.[0]
+
+let leaderboardHistory: LeaderboardEntryHistoryRow[] = []
+
+if (leaderboard?.leaderboard_entry_id) {
+ const [[leaderboardEntryHistory], error2] = await getDataOrErrorResponse(
+ LeaderboardEntriesApi.getLeaderboardEntryHistory({
+ leaderboardEntryId: leaderboard?.leaderboard_entry_id,
+ resolution: Resolution.DAY,
+ aggregation: Aggregation.LAST,
+ })
+ )
+ leaderboardHistory = leaderboardEntryHistory.history
+ if (error2) return error
+}
+
+const mmrEntries: number[] = []
+const dateLabels: string[] = []
+for (const entry of leaderboardHistory) {
+ if (entry.mmr) {
+ dateLabels.push(entry.time)
+ mmrEntries.push(Math.round(entry.mmr))
+ }
+}
+
+const today = new Date().toISOString().slice(0, 10)
+if (dateLabels[dateLabels.length - 1] !== today) {
+ dateLabels.push(today)
+ mmrEntries.push(Math.round(leaderboard.mmr))
+}
+---
+
+
+
+
+
+
+
+ {
+ (
+
+
+ {mmrEntries.length > 1 ? (
+
+ ) : (
+
Not enough data
+ )}
+
+
+ )
+ }
+ {
+ playerMatchupStats.matchups
+ .filter((m) => m.race == faction)
+ .map((matchup) => (
+
+
+
+
+ {raceLabels[matchup.race]}
+ vs
+
+ {raceLabels[matchup.opponent_race]}
+
+
+ ))
+ }
+
+
+ {playerActivity?.history && }
+ {playerMatchupStats.matchups.length > 0 && }
+
+
+