Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add player activity in sidebar + move ranks #37

Merged
merged 2 commits into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/Widget.astro
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const { title, label } = Astro.props

<div class="relative overflow-auto rounded-lg border border-gray-800/80 bg-gray-800/30 p-3 backdrop-blur-md sm:p-4">
<div class="sticky left-0 top-0 flex flex-wrap items-center gap-x-5 gap-y-1">
<h3 class="font-display flex-auto text-xl font-bold text-gray-200">
<h3 class="font-display flex-auto text-lg font-bold text-gray-200">
{title}
</h3>
<p class="text-md font-bold text-gray-400">
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function Tooltip(props: ParentProps<{ content: string; class?: string }>)
<KTooltip.Portal>
<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">
<KTooltip.Arrow />
<p>{props.content}</p>
<p class="whitespace-pre-wrap">{props.content}</p>
</KTooltip.Content>
</KTooltip.Portal>
</KTooltip.Root>
Expand Down
81 changes: 81 additions & 0 deletions src/components/widgets/PlayerActivity.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
---
import type { PlayerActivityStats, PlayerResponse } from "../../lib/api"
import Widget from "../Widget.astro"
import { Tooltip } from "../ui/Tooltip"

interface Props {
player: PlayerResponse
activity: PlayerActivityStats
}

const { player, activity } = Astro.props

function getWinrateClass(winrate: number) {
if (winrate >= 0.65) return "bg-green-500"
if (winrate > 0.5) return "bg-lime-500"
if (winrate >= 0.45) return "bg-yellow-500"
if (winrate > 0) return "bg-orange-500"
return "bg-red-500"
}

const maxMatches = Math.max(...activity.history.map((d) => d.matches))
type Day = { date: Date; win_rate: number; games_count: number; size: number; tooltip: string }
type Week = Day[]
const today = new Date()
// get the date of monday 12 weeks ago, fully including this week
const start = new Date(today.getFullYear(), today.getMonth(), today.getDate() - today.getDay() - 11 * 7)
const weeks: Week[] = []
for (let i = 0; i < 12; i++) {
const week: Week = []
for (let j = 0; j < 7; j++) {
const date = new Date(start.getFullYear(), start.getMonth(), start.getDate() + i * 7 + j)
if (date > today) break
const day = activity.history.find((d) => d.date === date.toISOString().slice(0, 10))
week.push({
date,
win_rate: (day?.win_rate ?? 0) / 100,
games_count: day?.matches ?? 0,
size: day?.matches ? Math.max(50, Math.min((day.matches / maxMatches) * 150, 100)) : 0,
tooltip: day?.matches
? `${date.toLocaleDateString("en", { dateStyle: "long" })}\n${day.matches} Games\n${Math.round(
day.win_rate
)}% Win rate`
: "No games",
})
}
weeks.push(week)
}
function formatMonth(date: Date) {
if (!date) return ""
return date.getDate() <= 7 ? date.toLocaleString("en", { month: "short" }) : ""
}
---

<Widget title="Activity" label={`${activity.aggregated?.matches ?? 0} Matches`}>
<div class="flex flex-row gap-1">
{
weeks.map((week, x) => (
<div class="flex flex-col gap-1">
{week.map((day, y) => {
const { size, win_rate, games_count, tooltip } = day
return games_count == 0 ? (
<div class="h-4 w-4 rounded-sm bg-gray-800/50" />
) : (
<Tooltip content={tooltip} client:idle>
<div class="relative flex h-4 w-4 items-center justify-center">
<div
class:list={[" rounded-sm", getWinrateClass(win_rate)]}
style={`width: ${size}%; height: ${size}%;`}
/>
</div>
</Tooltip>
)
})}
<div class="relative h-4 text-xs text-gray-400">
<span class="absolute top-0">{formatMonth(week[6]?.date)}</span>
</div>
</div>
))
}
</div>
</Widget>
4 changes: 4 additions & 0 deletions src/lib/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@ export type { MatchParticipantResponse } from "./models/MatchParticipantResponse
export type { MatchResponse } from "./models/MatchResponse"
export { MatchResult } from "./models/MatchResult"
export { MatchState } from "./models/MatchState"
export type { PlayerActivityStats } from "./models/PlayerActivityStats"
export type { PlayerActivityStatsRace } from "./models/PlayerActivityStatsRace"
export type { PlayerMatchesResponse } from "./models/PlayerMatchesResponse"
export type { PlayerPreferences } from "./models/PlayerPreferences"
export type { PlayerResponse } from "./models/PlayerResponse"
export type { PlayerStatsEntry } from "./models/PlayerStatsEntry"
export type { PlayerStatsEntryNumBreakdown } from "./models/PlayerStatsEntryNumBreakdown"
export { ProfilePrivacy } from "./models/ProfilePrivacy"
export { Race } from "./models/Race"
export type { StatsByTime } from "./models/StatsByTime"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
/* tslint:disable */
/* eslint-disable */
export type MatchParticipantPlayerLeaderboardEntryResponse = {
leaderboard_entry_id: string
league?: string | null
tier?: number | null
rank?: number | null
wins: number
losses: number
ties?: number | null
win_rate: number
}
12 changes: 12 additions & 0 deletions src/lib/api/models/PlayerActivityStats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { PlayerActivityStatsRace } from "./PlayerActivityStatsRace"
import type { PlayerStatsEntry } from "./PlayerStatsEntry"
export type PlayerActivityStats = {
updated_at: string
aggregated?: PlayerStatsEntry | null
history: Array<PlayerStatsEntry>
races: Array<PlayerActivityStatsRace>
}
9 changes: 9 additions & 0 deletions src/lib/api/models/PlayerActivityStatsRace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { PlayerStatsEntry } from "./PlayerStatsEntry"
export type PlayerActivityStatsRace = {
aggregated?: PlayerStatsEntry | null
history: Array<PlayerStatsEntry>
}
17 changes: 17 additions & 0 deletions src/lib/api/models/PlayerStatsEntry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { PlayerStatsEntryNumBreakdown } from "./PlayerStatsEntryNumBreakdown"
import type { Race } from "./Race"
export type PlayerStatsEntry = {
date?: string | null
race?: Race | null
matches: number
wins: number
losses: number
win_rate: number
mmr: PlayerStatsEntryNumBreakdown
points: PlayerStatsEntryNumBreakdown
match_length: PlayerStatsEntryNumBreakdown
}
9 changes: 9 additions & 0 deletions src/lib/api/models/PlayerStatsEntryNumBreakdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type PlayerStatsEntryNumBreakdown = {
max?: number | null
min?: number | null
average?: number | null
}
23 changes: 23 additions & 0 deletions src/lib/api/services/PlayersApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,27 @@ export class PlayersApi {
},
})
}
/**
* @returns MatchResponse Player found successfully
* @throws ApiError
*/
public static getPlayerStatisticsActivity({
playerId,
}: {
/**
* Player ID
*/
playerId: string
}): CancelablePromise<MatchResponse> {
return __request(OpenAPI, {
method: "GET",
url: "/v0/players/{player_id}/statistics/activity",
path: {
player_id: playerId,
},
errors: {
404: `Player was not found`,
},
})
}
}
97 changes: 55 additions & 42 deletions src/pages/players/[id]-[username].astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import { Image } from "astro:assets"
import infernals from "../../assets/game/factions/infernals-small.png"
import vanguard from "../../assets/game/factions/vanguard-small.png"
import MatchPreview from "../../components/widgets/MatchPreview.astro"
import { PlayersApi } from "../../lib/api"
import { leagues } from "../../assets/game/leagues/leagues"
import { PlayersApi, type PlayerActivityStats } from "../../lib/api"
import { RankedBadge } from "../../components/ui/RankedBadge"
import PlayerActivity from "../../components/widgets/PlayerActivity.astro"
const themes = {
infernals: {
badge: "bg-red-800/20 border-red-500/50",
Expand All @@ -33,55 +33,68 @@ const highestLeague = player.leaderboard_entries.reduce(
(acc, entry) => (entry.points > acc.points ? entry : acc),
player.leaderboard_entries[0]
)

const playerActivity = (await PlayersApi.getPlayerStatisticsActivity({ playerId })) as any as PlayerActivityStats
---

<Layout title={player.nickname}>
<div class="w-full border-b border-gray-700/50 bg-gray-800/50 backdrop-blur-lg">
<div class="mx-auto flex max-w-screen-md flex-wrap items-center gap-4 px-4 py-4">
<div class="mx-auto flex max-w-screen-lg flex-wrap items-center gap-4 px-4 py-4">
<RankedBadge entry={highestLeague} class="w-20" client:load />
<h1 class="flex-auto text-2xl font-bold text-gray-50">{player.nickname}</h1>
</div>
</div>
<section class="relative mx-auto flex max-w-screen-lg flex-col gap-6 px-4 py-8 md:px-7 lg:flex-row">
<div class="flex-auto">
<Widget title="Recent Matches" label="Closed Beta Ranked">
<div class="">
{playerMatches.matches.map((match) => <MatchPreview match={match} mainPlayerId={playerId} />)}
</div>
</Widget>
</div>
<div class="order-first flex basis-1/4 flex-col gap-6 sm:flex-row lg:order-none lg:flex-col">
<Widget title="Top Ranks">
<div class="flex flex-col gap-2">
{
player.leaderboard_entries.map((entry) => (
<div
class:list={[
"rounded-lg pl-3 pr-1 py-2 -mx-2 flex items-center gap-3 text-sm sm:text-base",
themes[entry.race as Theme].badge,
]}
>
<Image
src={entry.race === "infernals" ? infernals : vanguard}
alt={entry.race}
class="size-6 sm:size-10"
/>
<div>
<span class:list={["text-xs", themes[entry.race as Theme].badgeLabel]}>Rank</span>
<p>#{entry.rank}</p>
</div>
<div>
<span class:list={["text-xs", themes[entry.race as Theme].badgeLabel]}>Points</span>
<p>
{Math.round(entry.points)}

<div class="flex flex-wrap gap-4">
{
player.leaderboard_entries.map((entry) => (
<div
class:list={[
"border rounded-lg px-4 py-2 flex items-center gap-3 text-sm sm:text-base",
themes[entry.race as Theme].badge,
]}
>
<Image
src={entry.race === "infernals" ? infernals : vanguard}
alt={entry.race}
class="size-6 sm:size-10"
/>
<div>
<span class:list={["text-xs", themes[entry.race as Theme].badgeLabel]}>Rank</span>
<p>#{entry.rank}</p>
</div>
<div>
<span class:list={["text-xs", themes[entry.race as Theme].badgeLabel]}>Points</span>
<p>
{Math.round(entry.points)}

<RankedBadge entry={entry} class="inline-block w-4" client:load />
</p>
</div>
<div>
<span class:list={["text-xs", themes[entry.race as Theme].badgeLabel]}>MMR</span>
<p>{Math.round(entry.mmr)}</p>
<RankedBadge entry={entry} class="inline-block w-4" client:load />
</p>
</div>
<div>
<span class:list={["text-xs", themes[entry.race as Theme].badgeLabel]}>MMR</span>
<p>{Math.round(entry.mmr)}</p>
</div>
</div>
</div>
))
}
))
}
</div>
</Widget>
<div class="hidden sm:block">
<PlayerActivity activity={playerActivity} {player} />
</div>
</div>
</div>
<section class="relative mx-auto max-w-screen-md px-4 py-8">
<Widget title="Recent Matches" label="Closed Beta Ranked">
<div class="-mx-3 sm:-mx-4">
{playerMatches.matches.map((match) => <MatchPreview match={match} mainPlayerId={playerId} />)}
</div>
</Widget>
<div class="sm:hidden">
<PlayerActivity activity={playerActivity} {player} />
</div>
</section>
</Layout>
Loading