Skip to content

Commit

Permalink
feat: new style for the homepage #4 (#6)
Browse files Browse the repository at this point in the history
* feat: added theme colors

* feat: added header and new home page #4

* feat: added navbar to the statistic screen #4

* fix: sidebar scrolling

* feat: show JSON parse error
  • Loading branch information
Swackles authored Jan 16, 2024
1 parent 8515034 commit dc05fd4
Show file tree
Hide file tree
Showing 30 changed files with 538 additions and 249 deletions.
Binary file added fonts/ExtraCondensed-ExtraBoldItalic.ttf
Binary file not shown.
105 changes: 105 additions & 0 deletions src/common/components/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import * as React from 'react';
import Sheet from '@mui/joy/Sheet';
import {Button, FormControl, FormHelperText, IconButton, Stack, Typography, useTheme} from "@mui/joy";
import InfoOutlined from "@mui/icons-material/InfoOutlined";
import {useGameStatsStore} from "@common/stores/gameStatsStore";
import {ChangeEvent, useState} from "react";
import MenuIcon from '@mui/icons-material/Menu';
import { toggleSidebar } from "./helper";
import {observer} from "mobx-react";

export interface HeaderProps {
compact?: boolean
}

const Header = observer(({ compact }: HeaderProps) => {
const theme = useTheme()
const { setJson, error } = useGameStatsStore()

const [fileError, setFileError] = useState<string>()

const onFileInput = (e: ChangeEvent<HTMLInputElement>) => {
const reader = new FileReader();

reader.readAsText(e.target.files![0], "UTF-8");
reader.onload = event => {
try {
setJson(JSON.parse(event.target!.result as string))
setFileError(undefined)
} catch (e) {
console.log(e)
setFileError((e as Error).message)
}
}
reader.onerror = event => {
console.error(event)
setFileError("Error occured reading the file")
}
}

return (
<Sheet
sx={{
position: 'fixed',
top: 0,
height: 'var(--Header-height)',
zIndex: 9995,
gap: 1,
width: "100vw",
backgroundColor: compact ? "#FFF" : theme.vars.palette.primary[600]
}}
>
<Stack
direction="row"
justifyContent={{ xs: compact ? "space-between" : "flex-end", md: "space-between" }}
alignItems="center"
spacing={0}
sx={{ p: compact ? 3 : 7 }}
>
{!compact && <Typography level="h1"
sx={{
color: "#FFF",
display: {
xs: "none",
md: "block"
}
}}>
THE FINALS TRACKER
</Typography>}
{compact && <IconButton
onClick={() => toggleSidebar()}
variant="outlined"
color="neutral"
size="sm"
>
<MenuIcon/>
</IconButton>}
<Stack
direction="row"
justifyContent="flex-end"
alignItems="center"
spacing={4}
>
<Button size="lg" component="a" target="_blank" href="https://the-finals-leaderboard.leonlarsson.com/" color="neutral">
Leaderboard
</Button>
<FormControl error={true} sx={{ width: "100%" }} >
<Button size="lg" color="neutral" component="label">
Upload JSON
<input type="file"
accept="application/json"
hidden
onChange={onFileInput} />
</Button>
{(error || fileError) && <FormHelperText><InfoOutlined/> {error || fileError}</FormHelperText>}
</FormControl>
<a href="https://github.com/Swackles/the-finals-tracker" target="_blank">
<img style={{ width: 35, height: 35 }} src={compact ? "/github-mark.svg" : "/github-mark-white.svg"}/>
</a>
</Stack>
</Stack>
</Sheet>
);
})

export default Header
26 changes: 26 additions & 0 deletions src/common/components/Header/helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export function openSidebar() {
if (typeof document !== 'undefined') {
document.body.style.overflow = 'hidden';
document.documentElement.style.setProperty('--SideNavigation-slideIn', '1');
}
}

export function closeSidebar() {
if (typeof document !== 'undefined') {
document.documentElement.style.removeProperty('--SideNavigation-slideIn');
document.body.style.removeProperty('overflow');
}
}

export function toggleSidebar() {
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
const slideIn = window
.getComputedStyle(document.documentElement)
.getPropertyValue('--SideNavigation-slideIn');
if (slideIn) {
closeSidebar();
} else {
openSidebar();
}
}
}
3 changes: 3 additions & 0 deletions src/common/components/Header/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Header from "./Header"

export default Header
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
export enum Map {
MONACO = "Monaco",
SOUL = "Seoul",
SKYWAY_STADIUM = "Skyway Stadium",
LAS_VEGAS = "Las Vegas"
}

export enum MapVariant {
MONACO_BASE = "DA_MV_Monaco_01_Base",
MONACO_DUCK_AND_COVER = "DA_MV_Monaco_01_DuckAndCover",
Expand Down
4 changes: 2 additions & 2 deletions src/common/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export * from "./gameStatsJson"

export * from "./Archetype"
export * from "./GameMode"
export * from "./MapVariant"
export * from "./RoundType"
129 changes: 43 additions & 86 deletions src/common/stores/gameStatsStore/GameStatsStore.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import {action, computed, makeObservable, observable} from "mobx";
import {
Archetype,
DiscoveryRoundStats,
GameMode,
GamePerArchetype,
GameStatsJson,
MapVariant,
RoundStatSummary,
RoundType
} from "@common/models";
import { getLoadoutAssetFromId, mapArchetype} from "@common/util";
import {ArchetypeGameStats, MatchesStats, TournamentRound, TournamentStat, WeaponStat, WinRates} from "./models";
import {MatchesStats, WinRates} from "./models";
import {Store} from "@common/stores";
import {
ArchetypeStats,
GameModeStats,
GameStats,
GameStatsJson, LoadoutItemStat,
mapGameStats,
Tournament
} from "@common/util/mapGameStats";

export class GameStatsStore implements Store {
@observable.ref protected _json?: GameStatsJson = undefined
@observable.ref protected _stats?: GameStats
@observable.ref protected _error?: string
@observable.ref protected _gameMode: GameMode = GameMode.TOTAL

static new = () => new GameStatsStore()
Expand All @@ -23,9 +26,27 @@ export class GameStatsStore implements Store {
makeObservable(this)
}

@action.bound
setJson(json: GameStatsJson | undefined) {
if (!json) return this.setStats(json)

try {
this.setStats(mapGameStats(json))
this.setError(undefined)
} catch (e) {
console.error(e)
this.setError("Unable to parse JSON")
}
}

@computed
get error(): string | undefined {
return this._error
}

@computed
get isJsonPresent(): boolean {
return this._json != undefined
return this._stats != undefined
}

@computed
Expand All @@ -35,43 +56,17 @@ export class GameStatsStore implements Store {

@computed
get profile() {
return this.json["v1-shared-profile"]
return this.stats.profile
}

@computed
get roundStatsSummary(): RoundStatSummary {
return this.json["v1-discovery-roundstatsummary"][this._gameMode]
get roundStatsSummary(): GameModeStats {
return this.stats.statsPerRoundType[this._gameMode]
}

@computed
get tournaments(): TournamentStat[] {
const tournamentMap: Record<string, DiscoveryRoundStats[]> = {}
for (const roundStat of this.json["v1-discovery-roundstats"].roundStats) {
if (tournamentMap[roundStat.tournamentId] === undefined) tournamentMap[roundStat.tournamentId] = [roundStat]
else tournamentMap[roundStat.tournamentId].push(roundStat)
}

const tournaments: TournamentStat[] = []
for (const [id, rounds] of Object.entries(tournamentMap)) {
tournaments.push({
id,
won: rounds[0].tournamentWon,
rounds: rounds.map<TournamentRound>(round => ({
id: round.roundId,
damageDone: round.damageDone,
deaths: round.deaths,
kills: round.kills,
respawns: round.respawns,
revives: round.revivesDone,
map: round.mapVariant as MapVariant,
start: new Date(round.startTime * 1000),
end: new Date(round.endTime * 1000),
won: round.roundWon
}))
})
}

return tournaments
get tournaments(): Tournament[] {
return this.stats.tournaments
}

@computed
Expand All @@ -87,17 +82,8 @@ export class GameStatsStore implements Store {
}

@computed
//TODO: Better name needed, returns both gadgets and weapons that can deal damage
get weapons(): WeaponStat[] {
const { damagePerItem, killsPerItem } = this.roundStatsSummary
const weaponIds = Object.keys(damagePerItem).concat(Object.keys(killsPerItem))
.filter((value, i, arr) => arr.indexOf(value) === i)

return weaponIds.map(id => ({
...getLoadoutAssetFromId(id),
damage: damagePerItem[id] || 0,
kills: killsPerItem[id] || 0
}))
get loadoutItems(): LoadoutItemStat[] {
return this.roundStatsSummary.loadoutItems
}

@computed
Expand Down Expand Up @@ -127,48 +113,19 @@ export class GameStatsStore implements Store {
}

@computed
get archetypes(): ArchetypeGameStats[] {
const {
roundWinRatePerArchetype,
timePlayedPerArchetype,
tournamentWinRatePerArchetype
} = this.roundStatsSummary

return ([
...Object.keys(roundWinRatePerArchetype),
...Object.keys(timePlayedPerArchetype),
...Object.keys(tournamentWinRatePerArchetype)
].filter((value, i, arr) => arr.indexOf(value) === i && value != "") as Array<keyof GamePerArchetype>)
.map(key => {
const type = mapArchetype(key)
const {kills, damage} = this.weapons
.filter(row => row.archetype === type)
.reduce((a, b) => ({
kills: a.kills + b.kills,
damage: a.damage + (b.damage * 100_000)
}), {kills: 0, damage: 0})

return {
type,
kills,
damage: Math.round(damage / 100_000),
roundWinRate: roundWinRatePerArchetype[key] || 0,
tournamentWinRate: roundWinRatePerArchetype[key] || 0,
timePlayed: timePlayedPerArchetype[key] || 0
}
}
)
get archetypes(): ArchetypeStats[] {
return this.roundStatsSummary.archetypes
}


@computed
private get json(): GameStatsJson {
private get stats(): GameStats {
if (!this.isJsonPresent) throw new Error("Trying to fetch JSON data when JSON is not available")

return this._json!
return this._stats!
}

@action setGameMode = (gameMode: GameMode) => this._gameMode = gameMode
@action
setJson = (json: GameStatsJson | undefined) => this._json = json
@action private setError = (error?: string) => this._error = error
@action private setStats = (stats?: GameStats) => this._stats = stats
}
5 changes: 2 additions & 3 deletions src/common/stores/gameStatsStore/GameStatsStoreProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React, {useContext} from "react";
import React, {useContext, useState} from "react";
import {GameStatsStore} from "./GameStatsStore";
import {useStore} from "@common/stores";
import {observer} from "mobx-react";

const GameStatsStoreContext = React.createContext<GameStatsStore>({
Expand All @@ -14,7 +13,7 @@ export interface GameStatsProviderProps {
}

export const GameStatsProvider = observer(({ homeView: HomeView, statsView: StatsView }: GameStatsProviderProps) => {
const store = useStore(GameStatsStore.new)
const [store] = useState(GameStatsStore.new)

return (
<GameStatsStoreContext.Provider value={store}>
Expand Down
10 changes: 0 additions & 10 deletions src/common/stores/gameStatsStore/models/ArchetypeGameStats.ts

This file was deleted.

1 change: 0 additions & 1 deletion src/common/stores/gameStatsStore/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from "./ArchetypeGameStats"
export * from "./MatchesStats"
export * from "./TournamentStat"
export * from "./WeaponStat"
Expand Down
1 change: 0 additions & 1 deletion src/common/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,4 @@ export * from './fameToLeague'
export * from './getLoadoutAssetFromId'
export * from './getMapName'
export * from './getWeaponName'
export * from "./mapArchetype"
export * from './msToTimeString'
14 changes: 0 additions & 14 deletions src/common/util/mapArchetype.ts

This file was deleted.

Loading

0 comments on commit dc05fd4

Please sign in to comment.