464 lines
19 KiB
Vue
464 lines
19 KiB
Vue
<template>
|
|
<div class="player-view">
|
|
<div class="centerDiv">
|
|
<!-- Heading -->
|
|
<div class="row">
|
|
<div class="col-sm">
|
|
<h1 id="player-name">{{ playerName }}{{ injuryReturnDate }}</h1>
|
|
<h2 v-if="isCurrentPlayer" id="player-wara">{{ player?.wara }} sWAR</h2>
|
|
</div>
|
|
|
|
<div v-if="isCurrentPlayer" class="col-sm-1">
|
|
<RouterLink v-if="teamAbbreviation && teamThumbnail"
|
|
:to="{ name: 'team', params: { seasonNumber: seasonNumber, teamAbbreviation: teamAbbreviation } }">
|
|
<img id="thumbnail" height="125" style="float:right; vertical-align:middle; max-height:100%;"
|
|
:src="teamThumbnail">
|
|
</RouterLink>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="isCurrentPlayer" class="row">
|
|
<div class="col-sm-auto">
|
|
<img style="max-height:485px; max-width: 100%;" id="team-image" :src="playerImageUrl" :alt="playerName">
|
|
<p><a id="bbref-link" target="_blank" :href="baseballReferenceUrl">Baseball Reference Page</a></p>
|
|
</div>
|
|
|
|
<div class="col-sm-auto">
|
|
<img v-if="playerCardImage1Url" style="max-height:485px; max-width: 100%;" id="card-image"
|
|
:src="playerCardImage1Url">
|
|
<img v-if="playerCardImage2Url" style="max-height:485px; max-width: 100%;" id="card-image"
|
|
:src="playerCardImage2Url">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Batter Summary -->
|
|
<div v-if="currentSeasonBatting" class="row" id="batter-summary">
|
|
<div class="col-sm-12">
|
|
<h3>Summary</h3>
|
|
</div>
|
|
<div class="col-sm-8">
|
|
<div class="table-responsive-xl" style="max-width:45rem">
|
|
<table class="table table-sm table-striped">
|
|
<thead class="thead-dark">
|
|
<tr>
|
|
<th>Summary</th>
|
|
<th>AB</th>
|
|
<th>H</th>
|
|
<th>HR</th>
|
|
<th>BA</th>
|
|
<th>R</th>
|
|
<th>RBI</th>
|
|
<th>SB</th>
|
|
<th>OBP</th>
|
|
<th>SLG</th>
|
|
<th>OPS</th>
|
|
<th>wOBA</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="batter-summary-table">
|
|
<tr>
|
|
<td>Season {{ currentSeasonBatting.player.season }}</td>
|
|
<td>{{ currentSeasonBatting.ab }}</td>
|
|
<td>{{ currentSeasonBatting.hit }}</td>
|
|
<td>{{ currentSeasonBatting.hr }}</td>
|
|
<td>{{ currentSeasonBatting.avg.toFixed(3) }}</td>
|
|
<td>{{ currentSeasonBatting.run }}</td>
|
|
<td>{{ currentSeasonBatting.rbi }}</td>
|
|
<td>{{ currentSeasonBatting.sb }}</td>
|
|
<td>{{ currentSeasonBatting.obp.toFixed(3) }}</td>
|
|
<td>{{ currentSeasonBatting.slg.toFixed(3) }}</td>
|
|
<td>{{ currentSeasonBatting.ops.toFixed(3) }}</td>
|
|
<td>{{ currentSeasonBatting.woba.toFixed(3) }}</td>
|
|
</tr>
|
|
<tr v-if="currentPostSeasonBatting">
|
|
<td>S{{ currentPostSeasonBatting.player.season }} / Playoffs</td>
|
|
<td>{{ currentPostSeasonBatting.ab }}</td>
|
|
<td>{{ currentPostSeasonBatting.hit }}</td>
|
|
<td>{{ currentPostSeasonBatting.hr }}</td>
|
|
<td>{{ currentPostSeasonBatting.avg.toFixed(3) }}</td>
|
|
<td>{{ currentPostSeasonBatting.run }}</td>
|
|
<td>{{ currentPostSeasonBatting.rbi }}</td>
|
|
<td>{{ currentPostSeasonBatting.sb }}</td>
|
|
<td>{{ currentPostSeasonBatting.obp.toFixed(3) }}</td>
|
|
<td>{{ currentPostSeasonBatting.slg.toFixed(3) }}</td>
|
|
<td>{{ currentPostSeasonBatting.ops.toFixed(3) }}</td>
|
|
<td>{{ currentPostSeasonBatting.woba.toFixed(3) }}</td>
|
|
</tr>
|
|
</tbody>
|
|
<tfoot v-if="careerBattingStat" id="batter-summary-footer">
|
|
<tr>
|
|
<th>Career</th>
|
|
<th>{{ careerBattingStat.ab }}</th>
|
|
<th>{{ careerBattingStat.hit }}</th>
|
|
<th>{{ careerBattingStat.hr }}</th>
|
|
<th>{{ careerBattingStat.avg.toFixed(3) }}</th>
|
|
<th>{{ careerBattingStat.run }}</th>
|
|
<th>{{ careerBattingStat.rbi }}</th>
|
|
<th>{{ careerBattingStat.sb }}</th>
|
|
<th>{{ careerBattingStat.obp.toFixed(3) }}</th>
|
|
<th>{{ careerBattingStat.slg.toFixed(3) }}</th>
|
|
<th>{{ careerBattingStat.ops.toFixed(3) }}</th>
|
|
<th>{{ careerBattingStat.woba.toFixed(3) }}</th>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="col-sm-4">
|
|
<div class="table-responsive-xl" style="max-width:20rem">
|
|
<table class="table table-sm table-striped">
|
|
<thead class="thead-dark">
|
|
<tr>
|
|
<th>Inj</th>
|
|
<th v-if="lastAppearance">Last App</th>
|
|
<th v-if="secondLastAppearance">2nd Last App</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="batter-summary-helper">
|
|
<tr>
|
|
<td>{{ injuryRating }}</td>
|
|
<td v-if="lastAppearance">{{ lastAppearance }}</td>
|
|
<td v-if="secondLastAppearance">{{ secondLastAppearance }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<!-- Last 4 Games -->
|
|
<div class="col-small-12">
|
|
<h3>Last 4 Games</h3>
|
|
<div class="table-responsive-xl">
|
|
<table class="table table-sm table-striped">
|
|
<thead class="thead-dark">
|
|
<tr>
|
|
<th>Game</th>
|
|
<th>PA</th>
|
|
<th>AB</th>
|
|
<th>R</th>
|
|
<th>H</th>
|
|
<th>2B</th>
|
|
<th>3B</th>
|
|
<th>HR</th>
|
|
<th>RBI</th>
|
|
<th>SB</th>
|
|
<th>CS</th>
|
|
<th>BB</th>
|
|
<th>SO</th>
|
|
<th>BPHR</th>
|
|
<th>BPFO</th>
|
|
<th>BP1B</th>
|
|
<th>BPLO</th>
|
|
<th>GIDP</th>
|
|
<th>HBP</th>
|
|
<th>SAC</th>
|
|
<th>IBB</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="last4-batting">
|
|
<tr v-for="gameStat in last4Games">
|
|
<td>{{ makeWxGyFromGame(gameStat.game) }}</td>
|
|
<td>{{ gameStat.pa }}</td>
|
|
<td>{{ gameStat.ab }}</td>
|
|
<td>{{ gameStat.run }}</td>
|
|
<td>{{ gameStat.hit }}</td>
|
|
<td>{{ gameStat.double }}</td>
|
|
<td>{{ gameStat.triple }}</td>
|
|
<td>{{ gameStat.hr }}</td>
|
|
<td>{{ gameStat.rbi }}</td>
|
|
<td>{{ gameStat.sb }}</td>
|
|
<td>{{ gameStat.cs }}</td>
|
|
<td>{{ gameStat.bb }}</td>
|
|
<td>{{ gameStat.so }}</td>
|
|
<td>{{ gameStat.bphr }}</td>
|
|
<td>{{ gameStat.bpfo }}</td>
|
|
<td>{{ gameStat.bp1b }}</td>
|
|
<td>{{ gameStat.bplo }}</td>
|
|
<td>{{ gameStat.gidp }}</td>
|
|
<td>{{ gameStat.hbp }}</td>
|
|
<td>{{ gameStat.sac }}</td>
|
|
<td>{{ gameStat.ibb }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Career Batting -->
|
|
<div class="row" id="career-batting-row">
|
|
<div class="col-sm-12">
|
|
<h3>Batting Stats</h3>
|
|
<div class="table-responsive-xl">
|
|
<table class="table table-sm table-striped" id="career-batting">
|
|
<thead class="thead-dark">
|
|
<tr>
|
|
<th>Season</th>
|
|
<th>PA</th>
|
|
<th>AB</th>
|
|
<th>R</th>
|
|
<th>H</th>
|
|
<th>2B</th>
|
|
<th>3B</th>
|
|
<th>HR</th>
|
|
<th>RBI</th>
|
|
<th>SB</th>
|
|
<th>CS</th>
|
|
<th>BB</th>
|
|
<th>SO</th>
|
|
<th>BA</th>
|
|
<th>OBP</th>
|
|
<th>SLG</th>
|
|
<th>OPS</th>
|
|
<th>wOBA</th>
|
|
<th>K%</th>
|
|
<th>BPHR</th>
|
|
<th>BPFO</th>
|
|
<th>BP1B</th>
|
|
<th>BPLO</th>
|
|
<th>GIDP</th>
|
|
<th>HBP</th>
|
|
<th>SAC</th>
|
|
<th>IBB</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="career-batting-table">
|
|
<tr v-for="stat in sortedRegularAndPostSeasonBatting">
|
|
<td>S{{ stat.seasonNumber }}{{ stat.isRegularSeason ? '' : ' / Playoffs' }}</td>
|
|
<td>{{ stat.pa }}</td>
|
|
<td>{{ stat.ab }}</td>
|
|
<td>{{ stat.run }}</td>
|
|
<td>{{ stat.hit }}</td>
|
|
<td>{{ stat.double }}</td>
|
|
<td>{{ stat.triple }}</td>
|
|
<td>{{ stat.hr }}</td>
|
|
<td>{{ stat.rbi }}</td>
|
|
<td>{{ stat.sb }}</td>
|
|
<td>{{ stat.cs }}</td>
|
|
<td>{{ stat.bb }}</td>
|
|
<td>{{ stat.so }}</td>
|
|
<td>{{ stat.avg.toFixed(3) }}</td>
|
|
<td>{{ stat.obp.toFixed(3) }}</td>
|
|
<td>{{ stat.slg.toFixed(3) }}</td>
|
|
<td>{{ stat.ops.toFixed(3) }}</td>
|
|
<td>{{ stat.woba.toFixed(3) }}</td>
|
|
<td>{{ calculateStrikeoutPercent(stat) }}</td>
|
|
<td>{{ stat.bphr }}</td>
|
|
<td>{{ stat.bpfo }}</td>
|
|
<td>{{ stat.bp1b }}</td>
|
|
<td>{{ stat.bplo }}</td>
|
|
<td>{{ stat.gidp }}</td>
|
|
<td>{{ stat.hbp }}</td>
|
|
<td>{{ stat.sac }}</td>
|
|
<td>{{ stat.ibb }}</td>
|
|
</tr>
|
|
</tbody>
|
|
<tfoot>
|
|
<tr v-if="careerBattingStat" id="career-batting-footer">
|
|
<th>Career</th>
|
|
<th>{{ careerBattingStat.pa }}</th>
|
|
<th>{{ careerBattingStat.ab }}</th>
|
|
<th>{{ careerBattingStat.run }}</th>
|
|
<th>{{ careerBattingStat.hit }}</th>
|
|
<th>{{ careerBattingStat.double }}</th>
|
|
<th>{{ careerBattingStat.triple }}</th>
|
|
<th>{{ careerBattingStat.hr }}</th>
|
|
<th>{{ careerBattingStat.rbi }}</th>
|
|
<th>{{ careerBattingStat.sb }}</th>
|
|
<th>{{ careerBattingStat.cs }}</th>
|
|
<th>{{ careerBattingStat.bb }}</th>
|
|
<th>{{ careerBattingStat.so }}</th>
|
|
<th>{{ careerBattingStat.avg.toFixed(3) }}</th>
|
|
<th>{{ careerBattingStat.obp.toFixed(3) }}</th>
|
|
<th>{{ careerBattingStat.slg.toFixed(3) }}</th>
|
|
<th>{{ careerBattingStat.ops.toFixed(3) }}</th>
|
|
<th>{{ careerBattingStat.woba.toFixed(3) }}</th>
|
|
<th>{{ calculateStrikeoutPercent(careerBattingStat) }}</th>
|
|
<th>{{ careerBattingStat.bphr }}</th>
|
|
<th>{{ careerBattingStat.bpfo }}</th>
|
|
<th>{{ careerBattingStat.bp1b }}</th>
|
|
<th>{{ careerBattingStat.bplo }}</th>
|
|
<th>{{ careerBattingStat.gidp }}</th>
|
|
<th>{{ careerBattingStat.hbp }}</th>
|
|
<th>{{ careerBattingStat.sac }}</th>
|
|
<th>{{ careerBattingStat.ibb }}</th>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import type { Game } from '@/services/apiResponseTypes'
|
|
import { isDiscordAuthenticated } from '@/services/authenticationService'
|
|
import { fetchLast2DecisionsByPlayerId, type Decision } from '@/services/decisionsService'
|
|
import { type Player, fetchPlayerByName } from '@/services/playersService'
|
|
import { fetchBattingStatsBySeasonAndPlayerId, fetchBattingStatsForLastFourGamesBySeasonAndPlayerId, aggregateBattingStats, type BattingStat } from '@/services/statsService'
|
|
import { CURRENT_SEASON, isNotUndefined } from '@/services/utilities'
|
|
|
|
interface BattingStatWithSeason extends BattingStat {
|
|
seasonNumber: number
|
|
isRegularSeason: boolean
|
|
}
|
|
|
|
export default {
|
|
name: "PlayerView",
|
|
data() {
|
|
return {
|
|
isAuthenticated: false as Boolean,
|
|
player: undefined as Player | undefined,
|
|
last2Decisions: [] as Decision[],
|
|
regularSeasonBattingStats: [] as BattingStat[],
|
|
postSeasonBattingStats: [] as BattingStat[],
|
|
last4Games: [] as BattingStat[]
|
|
}
|
|
},
|
|
props: {
|
|
seasonNumber: { type: Number, required: true },
|
|
playerName: { type: String, required: true }
|
|
},
|
|
computed: {
|
|
playerSeasonNumber(): number | undefined {
|
|
return this.player?.season
|
|
},
|
|
isCurrentPlayer(): boolean {
|
|
return this.seasonNumber === this.playerSeasonNumber
|
|
},
|
|
// TODO use to determine order of stats to display
|
|
isBatter(): boolean {
|
|
return !this.player?.pos_1.includes('P')
|
|
},
|
|
teamAbbreviation(): string | undefined {
|
|
return this.player?.team?.abbrev
|
|
},
|
|
teamThumbnail(): string | undefined {
|
|
return this.player?.team?.thumbnail
|
|
},
|
|
baseballReferenceUrl(): string | undefined {
|
|
if (!this.player?.bbref_id) return undefined
|
|
const firstChar = this.player.bbref_id.slice(0, 1)
|
|
return `https://www.baseball-reference.com/players/${firstChar}/${this.player.bbref_id}.shtml`
|
|
},
|
|
playerImageUrl(): string | undefined {
|
|
return this.player?.vanity_card ?? this.teamThumbnail
|
|
},
|
|
playerCardImage1Url(): string | undefined {
|
|
if (!this.isAuthenticated) return undefined
|
|
return this.player?.image
|
|
},
|
|
playerCardImage2Url(): string | undefined {
|
|
if (!this.isAuthenticated) return undefined
|
|
return this.player?.image2
|
|
},
|
|
injuryReturnDate(): string | undefined {
|
|
if (!this.player?.il_return) return undefined
|
|
return ` 🏥 (${this.player.il_return})`
|
|
},
|
|
injuryRating(): string | undefined {
|
|
return this.player?.injury_rating
|
|
},
|
|
currentSeasonBatting(): BattingStat | undefined {
|
|
if (!this.regularSeasonBattingStats.length) return undefined
|
|
return this.regularSeasonBattingStats.find(stat => stat.player.season === CURRENT_SEASON)
|
|
},
|
|
currentPostSeasonBatting(): BattingStat | undefined {
|
|
if (!this.postSeasonBattingStats.length) return undefined
|
|
return this.postSeasonBattingStats.find(stat => stat.player.season === CURRENT_SEASON)
|
|
},
|
|
careerBattingStat(): BattingStat | undefined {
|
|
if (this.regularSeasonBattingStats.length > 0) {
|
|
// old site behavior just summed regular season stats for the career line total
|
|
return aggregateBattingStats(this.regularSeasonBattingStats)
|
|
}
|
|
|
|
return undefined
|
|
},
|
|
sortedRegularAndPostSeasonBatting(): BattingStatWithSeason[] {
|
|
let seasonStats: BattingStatWithSeason[] = this.regularSeasonBattingStats.map(stat => {
|
|
return {
|
|
...stat,
|
|
seasonNumber: stat.player.season,
|
|
isRegularSeason: true
|
|
}
|
|
})
|
|
// TODO: here would be where you could filter out postseason stats if desired (like Josef requested)
|
|
seasonStats = seasonStats.concat(this.postSeasonBattingStats.map(stat => {
|
|
return {
|
|
...stat,
|
|
seasonNumber: stat.player.season,
|
|
isRegularSeason: false
|
|
}
|
|
}))
|
|
|
|
return seasonStats.sort((s1, s2) => {
|
|
return s1.seasonNumber - s2.seasonNumber === 0
|
|
? s1.isRegularSeason
|
|
? -1
|
|
: 1
|
|
: s1.seasonNumber - s2.seasonNumber
|
|
})
|
|
},
|
|
lastAppearance(): string | undefined {
|
|
if (!this.last2Decisions?.length) return undefined
|
|
if (this.last2Decisions.length <= 0) return undefined
|
|
|
|
return this.formatDecisionToAppearance(this.last2Decisions[0])
|
|
},
|
|
secondLastAppearance(): string | undefined {
|
|
if (!this.last2Decisions?.length) return undefined
|
|
if (this.last2Decisions.length <= 1) return '-'
|
|
|
|
return this.formatDecisionToAppearance(this.last2Decisions[1])
|
|
}
|
|
},
|
|
created() {
|
|
this.fetchData()
|
|
},
|
|
watch: {
|
|
seasonNumber(newValue, oldValue) {
|
|
if (newValue !== oldValue) this.fetchData()
|
|
},
|
|
playerName(newName, oldName) {
|
|
if (newName !== oldName) this.fetchData()
|
|
}
|
|
},
|
|
methods: {
|
|
async fetchData(): Promise<void> {
|
|
this.isAuthenticated = await isDiscordAuthenticated()
|
|
this.player = await this.tryFetchPlayerByNameForAnySeason(this.seasonNumber, this.playerName)
|
|
if (!this.player) return
|
|
|
|
this.last2Decisions = await fetchLast2DecisionsByPlayerId(this.seasonNumber, this.player.id)
|
|
this.last4Games = await fetchBattingStatsForLastFourGamesBySeasonAndPlayerId(this.seasonNumber, this.player.id)
|
|
|
|
// TODO: this should change, either with an api that can take a player name for every season, a way
|
|
// to get multiple seasons stats at once, or a players ids across all seasons at once
|
|
const playerSeasons = await Promise.all(Array.from(Array(CURRENT_SEASON), (element, index) => index + 1).map(seasonNumber => fetchPlayerByName(seasonNumber, this.player!.name)))
|
|
this.regularSeasonBattingStats = (await Promise.all(playerSeasons.filter(isNotUndefined).map(player => fetchBattingStatsBySeasonAndPlayerId(player!.season, player!.id, true)))).filter(isNotUndefined)
|
|
this.postSeasonBattingStats = (await Promise.all(playerSeasons.filter(isNotUndefined).map(player => fetchBattingStatsBySeasonAndPlayerId(player!.season, player!.id, false)))).filter(isNotUndefined)
|
|
},
|
|
async tryFetchPlayerByNameForAnySeason(seasonNumber: number, playerName: string): Promise<Player | undefined> {
|
|
do {
|
|
const player: Player | undefined = await fetchPlayerByName(seasonNumber, playerName)
|
|
if (player) return player
|
|
seasonNumber--
|
|
} while (seasonNumber > 0)
|
|
},
|
|
formatDecisionToAppearance(decision: Decision): string {
|
|
return `${decision.rest_ip.toFixed(1)}IP w${decision.week}g${decision.game_num}`
|
|
},
|
|
makeWxGyFromGame(game: Game | 'TOT'): string {
|
|
if (game === 'TOT') return 'TOT'
|
|
return `w${game.week}g${game.game_num}`
|
|
},
|
|
calculateStrikeoutPercent(stat: BattingStat): string {
|
|
if (!stat.pa) return 'N/A'
|
|
return (stat.so * 100 / stat.pa).toFixed(1)
|
|
}
|
|
}
|
|
}
|
|
</script>
|