Initial commit (ugly/incomplete)

This commit is contained in:
Peter 2023-08-06 21:43:52 -04:00
parent 545f4aca7e
commit be2cf17a9e
39 changed files with 5668 additions and 119 deletions

136
.gitignore vendored
View File

@ -4,127 +4,25 @@ logs
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
pnpm-debug.log*
lerna-debug.log* lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html) node_modules
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json .DS_Store
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist dist
dist-ssr
coverage
*.local
# Gatsby files /cypress/videos/
.cache/ /cypress/screenshots/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output # Editor directories and files
.vuepress/dist .vscode/*
!.vscode/extensions.json
# vuepress v2.x temp and cache directory .idea
.temp *.suo
.cache *.ntvs*
*.njsproj
# Docusaurus cache and generated files *.sln
.docusaurus *.sw?
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}

27
components.d.ts vendored Normal file
View File

@ -0,0 +1,27 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
FrontPage: typeof import('./src/components/FrontPage.vue')['default']
IconCommunity: typeof import('./src/components/icons/IconCommunity.vue')['default']
IconDocumentation: typeof import('./src/components/icons/IconDocumentation.vue')['default']
IconEcosystem: typeof import('./src/components/icons/IconEcosystem.vue')['default']
IconSupport: typeof import('./src/components/icons/IconSupport.vue')['default']
IconTooling: typeof import('./src/components/icons/IconTooling.vue')['default']
NavBar: typeof import('./src/components/NavBar.vue')['default']
NButton: typeof import('naive-ui')['NButton']
NMenu: typeof import('naive-ui')['NMenu']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
StandingsTable: typeof import('./src/components/StandingsTable.vue')['default']
TheWelcome: typeof import('./src/components/TheWelcome.vue')['default']
WelcomeItem: typeof import('./src/components/WelcomeItem.vue')['default']
}
}

1
env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

22
index.html Normal file
View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en" style="height: 100%">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<meta property="og:title" content="SBa: Season 7" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://sombaseball.ddns.net" />
<meta property="og:image" content="https://sombaseball.ddns.net/static/images/sba-network.png" />
<meta property="og:description" content="The home of the Strat-o-matic Baseball Association" />
<meta name="theme-color" content="#A6CE39">
<title>SBa TEST</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

2730
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "sba2",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "run-p type-check build-only",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --noEmit"
},
"dependencies": {
"vue": "^3.2.45",
"vue-router": "^4.1.6"
},
"devDependencies": {
"@types/node": "^18.11.12",
"@vicons/fluent": "0.12.0",
"@vitejs/plugin-vue": "^4.0.0",
"@vue/tsconfig": "^0.1.3",
"naive-ui": "2.34.3",
"npm-run-all": "^4.1.5",
"typescript": "~4.7.4",
"unplugin-vue-components": "0.24.1",
"vfonts": "0.0.3",
"vite": "^4.0.0",
"vue-tsc": "^1.0.12"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

102
src/App.vue Normal file
View File

@ -0,0 +1,102 @@
<template>
<html lang="en" style="height: 100%">
<!-- <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootswatch/4.5.2/yeti/bootstrap.min.css">
<link rel="stylesheet" href="/src/assets/oldSite.css"> -->
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta property="og:title" content="SBa: Season 8" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://sombaseball.ddns.net" />
<meta property="og:image" content="https://sombaseball.ddns.net/static/images/sba-network.png" />
<meta property="og:description" content="The home of the Strat-o-matic Baseball Association" />
<meta name="theme-color" content="#A6CE39">
<title>SBa Season {{ seasonNumber }}</title>
</head>
<NavBar />
<RouterView />
</html>
</template>
<script lang="ts">
import { RouterView } from 'vue-router'
import NavBar from './components/NavBar.vue'
import { CURRENT_SEASON } from '@/services/utilities'
export default {
name: "App",
data() {
return {
seasonNumber: CURRENT_SEASON as number
}
}
}
</script>
<style scoped>
header {
line-height: 1.5;
max-height: 100vh;
}
.logo {
display: block;
margin: 0 auto 2rem;
}
nav {
width: 100%;
font-size: 12px;
text-align: center;
margin-top: 2rem;
}
nav a.router-link-exact-active {
color: var(--color-text);
}
nav a.router-link-exact-active:hover {
background-color: transparent;
}
nav a {
display: inline-block;
padding: 0 1rem;
border-left: 1px solid var(--color-border);
}
nav a:first-of-type {
border: 0;
}
@media (min-width: 1024px) {
header {
display: flex;
place-items: center;
padding-right: calc(var(--section-gap) / 2);
}
.logo {
margin: 0 2rem 0 0;
}
header .wrapper {
display: flex;
place-items: flex-start;
flex-wrap: wrap;
}
nav {
text-align: left;
margin-left: -1rem;
font-size: 1rem;
padding: 1rem 0;
margin-top: 1rem;
}
}
</style>

74
src/assets/base.css Normal file
View File

@ -0,0 +1,74 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
position: relative;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition: color 0.5s, background-color 0.5s;
line-height: 1.6;
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

1
src/assets/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69" xmlns:v="https://vecta.io/nano"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 308 B

35
src/assets/main.css Normal file
View File

@ -0,0 +1,35 @@
@import './base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
}
}

102
src/assets/oldSite.css Normal file
View File

@ -0,0 +1,102 @@
.centerDiv {
width: 92%;
height: 100%;
margin: 0 auto;
}
.sticky-dark {
position: sticky;
left: 0em;
background-color: #E5E5E5
}
.sticky-light {
position: sticky;
left: 0em;
background-color: #F4F4F4
}
.col-small {
width: 1em
}
.col-med {
width: 10em
}
.col-large {
width: 20em
}
code {
color: black
}
.navbar-dark {
background-color: black;
color: black;
margin-bottom: -5px
}
.dark-mode {
background-color: black;
color: #E5E5E5;
}
.dropdown-toggle.active-dropdown::after {
transform: rotate(-90deg);
}
[data-theme="dark"] {
background-color: black !important;
color: #E5E5E5;
}
[data-theme="dark"] .bg-light {
background-color: #333 !important;
}
[data-theme="dark"] .bg-white {
background-color: #000 !important;
}
[data-theme="dark"] .bg-black {
background-color: #eee !important;
}
[data-theme="dark"] .table td {
color: #E5E5E5 !important;
}
[data-theme="dark"] .table th {
color: #ffffff !important;
}
.news-preview {
max-width: 950px;
padding-bottom: 75px;
height: 100%;
margin: 0 auto;
}
.news-subtitle {
font-size: 11px;
line-height: 10px;
padding-bottom: 10px;
vertical-align: middle;
}
.news-article {
max-width: 700px;
padding-bottom: 75px;
height: 100%;
margin: 0 auto;
}
.news-subtitle-large {
/* font-size: 22px;
line-height: 20px; */
vertical-align: middle;
padding-top: 25px;
padding-bottom: 50px;
}

270
src/components/NavBar.vue Normal file
View File

@ -0,0 +1,270 @@
<template>
<div class="navbar navbar-expand-sm navbar-dark bg-primary ">
<div class="nav-bar navbar-brand mb-0 h1">
<RouterLink to="/">SBa Season {{ seasonNumber }}</RouterLink>
<n-menu mode="horizontal" :options="menuOptions" />
<li class="nav-item"> <a id="login" class="nav-link">Login with Discord</a></li>
<li class="nav-item">
<a id="team-login-link" href="/teams?abbrev=">
<img id="team-login" style="max-height:35px" hidden />
</a>
</li>
<form class="form-inline" action="/players">
<input type="text" name="name" placeholder="Player Search" list="player-names" id="player-choice">
<datalist id="player-names">
<option v-for="name in playerNames" :value="name">{{ name }}</option>
</datalist>
</form>
<ul class="navbar-nav navbar-right">
<li class="nav-item">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="darkSwitch" />
<label class="custom-control-label" for="darkSwitch">Dark Mode</label>
</div>
<!-- <script src="/static/dark-mode-switch.js"></script> -->
</li>
</ul>
</div>
</div>
<!-- <nav class="container">
<RouterLink to="/">SBa Season {{ seasonNumber }}</RouterLink>
<RouterLink to="/schedule">Schedule</RouterLink>
<RouterLink to="/standings">Standings</RouterLink>
<RouterLink to="/stats">Stats & Leaders</RouterLink>
<RouterLink to="/teams">Teams</RouterLink>
<RouterLink to="/transactions">Transactions</RouterLink>
<RouterLink to="/history">History</RouterLink>
<RouterLink to="/rules">Rules Ref</RouterLink>
<RouterLink to="/news">News</RouterLink>
</nav> -->
</template>
<script lang="ts">
import { RouterLink } from 'vue-router'
import type { MenuOption } from 'naive-ui'
import { fetchPlayers, type Player } from '@/services/playersService'
import { CURRENT_SEASON } from '@/services/utilities'
export default {
name: 'NavBar',
data() {
return {
playerNames: [] as string[],
}
},
created() {
this.populatePlayerNames()
},
computed: {
menuOptions(): MenuOption[] {
return [
{
label: 'Schedule',
key: 'Schedule'
},
{
label: 'Standings',
key: 'Standings',
},
{
label: 'Stats & Leaders',
key: 'Stats & Leaders',
children: [
{
label: 'Season Leaders',
key: 'Season Leaders',
children: [
{
label: 'Batting Leaders',
key: 'Batting Leaders'
},
{
label: 'Pitching Leaders',
key: 'Pitching Leaders'
},
{
label: 'Fielding Leaders',
key: 'Fielding Leaders'
}
]
},
{
label: 'Team Stats',
key: 'Team Stats',
children: [
{
label: 'Team Batting',
key: 'Team Batting'
},
{
label: 'Team Pitching',
key: 'Team Pitching'
},
{
label: 'Team Fielding',
key: 'Team Fielding'
}
]
},
{
label: 'Single-Game Records',
key: 'Single-Game Records',
children: [
{
label: 'Single-Game Batting',
key: 'Single-Game Batting'
},
{
label: 'Single-Game Pitching',
key: 'Single-Game Pitching'
},
{
label: 'Single-Game Fielding',
key: 'Single-Game Fielding'
}
]
},
{
label: 'Single-Season Stats',
key: 'Single-Season Stats',
children: [
{
label: 'Single-Season Batting',
key: 'Single-Season Batting'
},
{
label: 'Single-Season Pitching',
key: 'Single-Season Pitching'
},
{
label: 'Single-Season Fielding',
key: 'Single-Season Fielding'
}
]
},
{
label: 'Career Leaders',
key: 'Career Leaders',
children: [
{
label: 'Career Batting',
key: 'Career Batting'
},
{
label: 'Career Pitching',
key: 'Career Pitching'
},
{
label: 'Career Fielding',
key: 'Career Fielding'
}
]
}]
},
{
label: 'Teams',
key: 'Teams',
children: [
{
label: 'AL East',
key: 'AL East',
children: [
{ label: 'Gators', key: 'Gators' },
{ label: 'Honeybees', keys: 'Honeybees' },
{ label: 'Kaiju', key: 'Kaiju' },
{ label: 'Phantoms', key: 'Phantoms' }
]
},
{
label: 'AL West',
key: 'AL West',
children: [
{ label: 'Cyclones', key: 'Cyclones' },
{ label: 'Mussels', keys: 'Mussels' },
{ label: 'Whale Sharks', key: 'Whale Sharks' },
{ label: 'Wu Xia', key: 'Wu Xia' }
]
},
{
label: 'NL East',
key: 'NL East',
children: [
{ label: 'Drillers', key: 'Drillers' },
{ label: 'Macho Men', keys: 'Macho Men' },
{ label: 'Shoebills', key: 'Shoebills' },
{ label: 'Zephyr', key: 'Zephyr' }
]
},
{
label: 'NL West',
key: 'NL West',
children: [
{ label: 'Angels', key: 'Angels' },
{ label: 'Black Bears', keys: 'Black Bears' },
{ label: 'Bovines', key: 'Bovines' },
{ label: 'Snow Geese', key: 'Snow Geese' }
]
}
]
},
{
label: 'Transactions',
key: 'Transactions',
children: [
{ label: 'Free Agents', key: 'Free Agents' },
{ label: 'Transactions', key: 'Transactions' }
]
},
{
label: 'History',
key: 'History',
children: [
{ label: 'Awards', key: 'Awards' },
{
label: 'Managers',
key: 'Managers',
// TODO figure out a way to pull active/retired into children here
children: [{ label: 'Active', key: 'Active' }, { label: 'Retired', key: 'Retired' }]
},
{
label: 'Season Recaps',
key: 'Season Recaps',
children: [
{ label: 'Season 4', key: 'Season 4' },
{ label: 'Season 3', key: 'Season 3' },
{ label: 'Season 2', key: 'Season 2' },
{ label: 'Season 1', key: 'Season 1' },
]
}
]
},
{ label: 'Rules Ref', key: 'Rules Ref' },
{ label: 'News', key: 'News' }
]
},
seasonNumber(): number {
// TODO pull this from DB?
return CURRENT_SEASON
}
},
methods: {
async getPlayers(): Promise<Player[]> {
return await fetchPlayers(this.seasonNumber)
},
async populatePlayerNames(): Promise<void> {
const players: Player[] = await this.getPlayers()
this.playerNames = players.sort(p => p.wara).map(p => p.name)
}
}
}
</script>
<style>
nav {
margin: 0%;
display: flex;
justify-content: flex-start;
gap: 8px;
}
</style>

View File

@ -0,0 +1,66 @@
<template>
<div class="standings-table">
<table>
<thead>
<tr>
<th>Team</th>
<th>Record</th>
<th>Win %</th>
<th>GB</th>
<th>E#</th>
<th>Run Diff</th>
</tr>
</thead>
<tbody>
<tr v-for="(team, index) in teams" :key="index">
<td>
<RouterLink
:to="{ name: 'team', params: { seasonNumber: seasonNumber(), teamAbbreviation: team.teamAbbreviation } }">
{{
team.teamName }}
</RouterLink>
</td>
<td>{{ record(team) }}</td>
<td>{{ team.winPercentage }}</td>
<td>{{ gamesBack(team) }}</td>
<td>{{ eliminationNumber(team) }}</td>
<td>{{ team.runDifferential }}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script lang="ts">
import type { TeamStanding } from '@/services/standingsService'
import { CURRENT_SEASON } from '@/services/utilities'
export default {
name: "StandingsTable",
props: {
teams: {
type: Array<TeamStanding>,
required: true,
},
isDivisional: {
type: Boolean,
required: true
}
},
methods: {
eliminationNumber(teamStanding: TeamStanding): string {
return this.isDivisional ? teamStanding.divisionEliminationNumber : teamStanding.wildcardEliminationNumber
},
gamesBack(teamStanding: TeamStanding): string {
return this.isDivisional ? teamStanding.divisionGamesBack : teamStanding.wildcardGamesBack
},
record(teamStanding: TeamStanding): string {
return `${teamStanding.wins}-${teamStanding.losses}`
},
seasonNumber(): number {
return CURRENT_SEASON
}
}
}
</script>

View File

@ -0,0 +1,16 @@
<template>
<button @click="players">Log Players</button>
</template>
<script lang="ts">
import { fetchPlayers } from '@/services/playersService'
import { CURRENT_SEASON } from '@/services/utilities'
export default {
methods: {
async players(): Promise<void> {
await fetchPlayers(CURRENT_SEASON)
}
}
}
</script>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
<path
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
<path
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
/>
</svg>
</template>

View File

@ -0,0 +1,19 @@
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--mdi"
width="24"
height="24"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
>
<path
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
fill="currentColor"
></path>
</svg>
</template>

11
src/main.ts Normal file
View File

@ -0,0 +1,11 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import './assets/main.css'
const app = createApp(App)
app.use(router)
app.mount('#app')

59
src/router/index.ts Normal file
View File

@ -0,0 +1,59 @@
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
const HomeView = () => import('../views/HomeView.vue')
export const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/teams/:seasonNumber/:teamAbbreviation',
name: 'team',
component: () => import('../views/TeamView.vue'),
props: castTeamsRouteParams
},
{
path: '/managers/:managerName',
name: 'manager',
component: () => import('../views/ManagerView.vue'),
props: true
},
{
path: '/players/:seasonNumber/:playerName',
name: 'player',
component: () => import('../views/PlayerView.vue'),
props: castPlayersRouteParams
}
]
function castTeamsRouteParams(route: any) {
return {
teamAbbreviation: route.params.teamAbbreviation,
seasonNumber: Number(route.params.seasonNumber)
}
}
function castPlayersRouteParams(route: any) {
return {
playerName: route.params.playerName,
seasonNumber: Number(route.params.seasonNumber)
}
}
export const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
})
// export function routerPush (name: AppRouteNames, params?: RouteParams): ReturnType<typeof router.push> {
// return params !== undefined
// ? router.push({
// name,
// params,
// })
// : router.push({ name })
// }
export default router

View File

@ -0,0 +1,38 @@
export interface Team {
abbrev: string
auto_draft: any
color: string // hex code
dice_color: string // hex code
division: Division
division_legacy: any
gmid: number
gmid2: number
gsheet: any
id: number
lname: string
manager1: Manager
manager2: Manager
manager_legacy: any
mascot: any
season: number
sname: string
stadium: string
thumbnail: string
}
export interface Manager {
id: number
name: string
image: string
headline: string
bio: string
}
export interface Division {
division_abbrev: 'W' | 'E'
division_name: 'West' | 'East'
id: number
league_abbrev: 'NL' | 'AL'
league_name: 'National League' | 'American League'
season: number
}

View File

@ -0,0 +1,25 @@
import { SITE_URL } from './utilities'
export interface LeagueInfo {
id: number,
week: number,
freeze: boolean,
season: number,
transcount: number,
bstatcount: number,
pstatcount: number,
bet_week: number,
trade_deadline: number,
pick_trade_start: number,
pick_trade_end: number,
playoffs_begin: number,
injury_count: number
}
export async function fetchCurrentLeagueInfo(): Promise<LeagueInfo> {
const response = await fetch(`${SITE_URL}/api/v3/current`)
const leagueInfoResponse: LeagueInfo = await response.json()
return leagueInfoResponse
}

View File

@ -0,0 +1,9 @@
// const api = new GhostContentAPI({
// url: 'https://sombaseball.ddns.net/writers',
// key: 'cfc331bb02beaebc0c62211b10',
// version: "v3"
// });
export function getPosts() {
}

View File

@ -0,0 +1,68 @@
import type { Team } from './apiResponseTypes'
import { SITE_URL } from './utilities'
export interface Player {
bbref_id: string
demotion_week: number
headshot: any
id: number
il_return: any
image: string
image2: string
injury_rating: string //could strongly type as XpYY
last_game: string // could strongly type as XX.X IP wXXgX
last_game2: string
name: string
pitcher_injury: string
pos_1: string
pos_2: string
pos_3: string
pos_4: string
pos_5: string
pos_6: string
pos_7: string
pos_8: string
season: number
strat_code: string
team: Team,
vanity_card: string
wara: number
}
export async function fetchPlayers(seasonNumber: number): Promise<Player[]> {
const response = await fetch(`${SITE_URL}/api/v3/players?season=${seasonNumber}`)
const playersResponse: {
count: number
players: Player[]
} = await response.json()
return playersResponse.players
}
// TODO should/can this use team abbrev instead of id?
export async function fetchPlayersByTeam(seasonNumber: number, teamId: number): Promise<Player[]> {
const response = await fetch(`${SITE_URL}/api/v3/players?season=${seasonNumber}&team_id=${teamId}`)
const playersResponse: {
count: number
players: Player[]
} = await response.json()
return playersResponse.players
}
export async function fetchPlayerByName(seasonNumber: number, playerName: string): Promise<Player> {
const response = await fetch(`${SITE_URL}/api/v3/players?season=${seasonNumber}&name=${playerName}`)
const playersResponse: {
count: number
players: Player[]
} = await response.json()
if (playersResponse.count !== 1) {
throw new Error('playersServices.fetchPlayerByName - Expected one player, return contained none or many')
}
return playersResponse.players[0]
}

View File

@ -0,0 +1,101 @@
import type { Team } from './apiResponseTypes'
import { SITE_URL } from './utilities'
interface TeamStandingRaw {
away_losses: number
away_wins: number
div1_losses: number
div1_wins: number
div2_losses: number
div2_wins: number
div3_losses: number
div3_wins: number
div4_losses: number
div4_wins: number
div_e_num: number
div_gb: number
home_losses: number
home_wins: number
id: number
last8_losses: number
last8_wins: number
losses: number
one_run_losses: number
one_run_wins: number
pythag_losses: number
pythag_wins: number
run_diff: number
streak_num: number
streak_wl: 'w' | 'l'
team: Team
wc_e_num: number | null
wc_gb: number
wins: number
}
export interface TeamStanding {
teamName: string
teamAbbreviation: string
divisionAbbreviation: 'W' | 'E'
leagueAbbreviation: 'NL' | 'AL'
wins: number
losses: number
winPercentage: string,
last8Record: string,
streak: string
divisionGamesBack: string
divisionEliminationNumber: string
wildcardGamesBack: string
wildcardEliminationNumber: string
runDifferential: string
isWildcardTeam: boolean
}
export async function fetchStandings(seasonNumber: number): Promise<TeamStanding[]> {
const response = await fetch(`${SITE_URL}/api/v3/standings?season=${seasonNumber}`)//?league_abbrev=AL&division_abbrev=W`)
const standingsResponse: {
count: number
standings: TeamStandingRaw[]
} = await response.json()
const standings: TeamStandingRaw[] = standingsResponse.standings
return standings.map(normalizeStanding)
}
function normalizeStanding(standing: TeamStandingRaw): TeamStanding {
const totalGamesPlayed = standing.wins + standing.losses
// round win percentage to 3 decimal places
const winPercentage: string = totalGamesPlayed ? `${Math.round(standing.wins / totalGamesPlayed * 1000) / 1000}%` : '-'
const formattedRunDiff = `${standing.run_diff > 0 ? '+' : ''}${standing.run_diff}`
const isWildcardTeam = standing.wc_gb !== null || standing.wc_e_num !== null
return {
teamName: standing.team.sname,
teamAbbreviation: standing.team.abbrev,
divisionAbbreviation: standing.team.division.division_abbrev,
leagueAbbreviation: standing.team.division.league_abbrev,
wins: standing.wins,
losses: standing.losses,
winPercentage,
last8Record: `${standing.last8_wins}-${standing.last8_losses}`,
streak: `${standing.streak_wl.toUpperCase()}${standing.streak_num}`,
divisionGamesBack: parseGamesBack(standing.div_gb),
divisionEliminationNumber: parseEliminationNumber(standing.div_e_num),
wildcardGamesBack: parseGamesBack(standing.wc_gb),
wildcardEliminationNumber: parseEliminationNumber(standing.wc_e_num),
runDifferential: formattedRunDiff,
isWildcardTeam
}
}
function parseGamesBack(gamesBack: number | null): string {
if (gamesBack == null) return '-'
return `${gamesBack < 0 ? '+' : ''}${Math.abs(gamesBack).toFixed(1)}`
}
function parseEliminationNumber(eliminationNumber: number | null): string {
if (eliminationNumber == null) return '-'
return eliminationNumber <= 0 ? 'E' : `${eliminationNumber}`
}

View File

@ -0,0 +1,17 @@
import type { Team } from './apiResponseTypes'
import { SITE_URL } from './utilities'
export async function fetchTeam(seasonNumber: number, teamAbbreviation: string): Promise<Team> {
const response = await fetch(`${SITE_URL}/api/v3/teams/?season=${seasonNumber}&team_abbrev=${teamAbbreviation}`)
const teamResponse: {
count: number
teams: Team[]
} = await response.json()
if (teamResponse.count !== 1) {
throw new Error('teamServices.fetchTeam - Expected one team, return contained none or many')
}
return teamResponse.teams[0]
}

View File

@ -0,0 +1,43 @@
import type { Team } from './apiResponseTypes'
import type { Player } from './playersService'
import { SITE_URL } from './utilities'
export interface Transaction {
id: number
week: number
player: Player
oldteam: Team
newteam: Team
season: number
moveid: number
cancelled: boolean
frozen: boolean
}
export async function fetchTransactionsByTeamAndWeek(seasonNumber: number, teamAbbreviation: string, weekNumber: number): Promise<Transaction[]> {
const response = await fetch(`${SITE_URL}/api/v3/transactions?season=${seasonNumber}&team_abbrev=${teamAbbreviation}&week_start=${weekNumber}`)
const transactionsResponse: {
count: number
transactions: Transaction[]
} = await response.json()
console.log('fetchTransactionsByTeam', seasonNumber, teamAbbreviation, transactionsResponse)
return transactionsResponse.transactions
}
export async function fetchPlayerByName(seasonNumber: number, playerName: string): Promise<Player> {
const response = await fetch(`${SITE_URL}/api/v3/players?season=${seasonNumber}&name=${playerName}`)
const playersResponse: {
count: number
players: Player[]
} = await response.json()
if (playersResponse.count !== 1) {
throw new Error('playersServices.fetchPlayerByName - Expected one player, return contained none or many')
}
return playersResponse.players[0]
}

View File

@ -0,0 +1,3 @@
export const SITE_URL = 'https://sba.manticorum.com'
export const CURRENT_SEASON = 8

4
src/shims-vue.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module "*.vue" {
import Vue from 'vue'
export default Vue
}

92
src/views/HomeView.vue Normal file
View File

@ -0,0 +1,92 @@
<template>
<main class="home-view">
<TheWelcome />
<h1>Season {{ seasonNumber }} Standings</h1>
<h2 id="week-num">{{ weekNumber ? `Week ${weekNumber}` : '' }}</h2>
<!-- Division Standings -->
<div class="row">
<div class="column">
<h2>American League</h2>
<h3>East</h3>
<StandingsTable :teams="alEastTeams" :is-divisional=true />
<h3>West</h3>
<StandingsTable :teams="alWestTeams" :is-divisional=true />
<h3>AL Wildcard</h3>
<StandingsTable :teams="alWildcardTeams" :is-divisional=false />
<h2>National League</h2>
<h3>East</h3>
<StandingsTable :teams="nlEastTeams" :is-divisional=true />
<h3>West</h3>
<StandingsTable :teams="nlWestTeams" :is-divisional=true />
<h3>NL Wildcard</h3>
<StandingsTable :teams="nlWildcardTeams" :is-divisional=false />
</div>
<div class="column">
<h3>Latest News</h3>
<div id="news-posts"></div>
</div>
</div>
</main>
</template>
<script lang="ts">
import StandingsTable from '@/components/StandingsTable.vue'
import { fetchStandings, type TeamStanding } from '@/services/standingsService'
import { fetchCurrentLeagueInfo, type LeagueInfo } from '@/services/currentService'
import { CURRENT_SEASON } from '@/services/utilities'
import TheWelcome from '../components/TheWelcome.vue'
export default {
name: "HomeView",
data() {
return {
seasonNumber: CURRENT_SEASON as number,
weekNumber: undefined as number | undefined,
teamStandings: [] as TeamStanding[],
alEastTeams: [] as TeamStanding[],
alWestTeams: [] as TeamStanding[],
alWildcardTeams: [] as TeamStanding[],
nlEastTeams: [] as TeamStanding[],
nlWestTeams: [] as TeamStanding[],
nlWildcardTeams: [] as TeamStanding[],
}
},
created() {
this.fetchData()
},
methods: {
async fetchData(): Promise<void> {
const leagueInfo: LeagueInfo = await fetchCurrentLeagueInfo()
this.weekNumber = leagueInfo.week
// TODO CHANGE THIS
this.teamStandings = await fetchStandings(CURRENT_SEASON - 1)
this.alEastTeams = this.teamStandings.filter(ts => ts.divisionAbbreviation === 'E' && ts.leagueAbbreviation === 'AL')
this.alWestTeams = this.teamStandings.filter(ts => ts.divisionAbbreviation === 'W' && ts.leagueAbbreviation === 'AL')
this.nlEastTeams = this.teamStandings.filter(ts => ts.divisionAbbreviation === 'E' && ts.leagueAbbreviation === 'NL')
this.nlWestTeams = this.teamStandings.filter(ts => ts.divisionAbbreviation === 'W' && ts.leagueAbbreviation === 'NL')
this.alWildcardTeams = this.teamStandings.filter(ts => ts.leagueAbbreviation === 'AL' && ts.isWildcardTeam)
this.nlWildcardTeams = this.teamStandings.filter(ts => ts.leagueAbbreviation === 'NL' && ts.isWildcardTeam)
},
}
}
</script>
<style>
.row {
display: flex;
}
.column {
flex-basis: 50%;
/* Sets the initial width of each column to 50% */
padding: 10px;
box-sizing: border-box;
/* Includes padding within the width of each column */
}
</style>

118
src/views/ManagerView.vue Normal file
View File

@ -0,0 +1,118 @@
<template>
<div class="manager-view centerDiv">
<div class="row">
<div class="col-sm-12">
<h1 id="manager-name">{{ managerName }}</h1>
</div>
<div class="col-sm-12">
<h3 id="manager-record">Career Record: 294-218</h3>
</div>
<div class="col-sm-12">
<p id="manager-headline"></p>
</div>
</div>
<div class="row">
<div class="col-sm-4">
<h3 style="text-align: center">Teams Managed</h3>
<div class="table-responsive-xl">
<table style="text-align: center" class="table table-sm table-striped" id="manager-teams-table">
<thead class="thead-dark">
<tr>
<th>Season</th>
<th>Team</th>
<th>Record</th>
</tr>
<tr>
<td>7</td>
<td><a href="/teams?abbrev=TK&amp;season=7">Tokyo Kaiju</a></td>
<td id="season-7-record">47-41</td>
</tr>
<tr>
<td>6</td>
<td><a href="/teams?abbrev=TK&amp;season=6">Tokyo Kaiju</a></td>
<td id="season-6-record">51-37</td>
</tr>
<tr>
<td>5</td>
<td><a href="/teams?abbrev=TK&amp;season=5">Tokyo Kaiju</a></td>
<td id="season-5-record">55-33</td>
</tr>
<tr>
<td>4</td>
<td><a href="/teams?abbrev=TK&amp;season=4">Tokyo Kaiju</a></td>
<td id="season-4-record">55-33</td>
</tr>
<tr>
<td>3</td>
<td><a href="/teams?abbrev=TK&amp;season=3">Tokyo Kaiju</a></td>
<td id="season-3-record">49-39</td>
</tr>
<tr>
<td>2</td>
<td><a href="/teams?abbrev=TK&amp;season=2">Tokyo Kaiju</a></td>
<td id="season-2-record">37-35</td>
</tr>
</thead>
<tbody id="manager-teams"></tbody>
</table>
</div>
</div>
<div class="col-sm-5">
<h3 style="text-align: center">Awards Received</h3>
<div class="table-responsive-xl">
<table style="text-align: center" class="table table-sm table-striped" id="manager-awards-table">
<thead class="thead-dark">
<tr>
<th>Season</th>
<th>Award</th>
<th>Extras</th>
</tr>
<tr>
<td>3</td>
<td>AL Wildcard</td>
<td><a href="/teams?abbrev=TK&amp;season=3">Tokyo Kaiju</a></td>
</tr>
<tr>
<td>2</td>
<td>Most Hated Ballpark</td>
<td><a href="/teams?abbrev=TK&amp;season=2">Tokyo Kaiju</a></td>
</tr>
<tr>
<td>2</td>
<td>American League Pennant</td>
<td><a href="/teams?abbrev=TK&amp;season=2">Tokyo Kaiju</a></td>
</tr>
<tr>
<td>2</td>
<td>AL East Leader</td>
<td><a href="/teams?abbrev=TK&amp;season=2">Tokyo Kaiju</a></td>
</tr>
</thead>
<tbody id="manager-awards"></tbody>
</table>
</div>
</div>
<div class="col-sm-3">
<h3 style="text-align: center" id="manager-bio-header"></h3>
<p id="manager-bio"></p>
</div>
</div>
</div>
</template>
<script lang="ts">
export default {
name: "ManagerView",
props: {
managerName: { type: String, required: true },
}
}
</script>

1231
src/views/PlayerView.vue Normal file

File diff suppressed because it is too large Load Diff

263
src/views/TeamView.vue Normal file
View File

@ -0,0 +1,263 @@
<template>
<div class="team-view centerDiv">
<div class="row">
<div class="col-sm">
<h1 id="team-record">{{ teamName }}</h1>
<h2 id="standings"></h2>
<h2 id="streak">Last 8: {{ lastEight }} / Streak: {{ streak }}</h2>
</div>
<div class="col-sm">
<h2 id="manager">
Manager{{ hasMultipleManagers ? 's' : '' }}:
<RouterLink v-if="manager1Name" :to="{ name: 'manager', params: { managerName: manager1Name } }">
{{ manager1Name }}
</RouterLink>
{{ hasMultipleManagers ? ' & ' : '' }}
<RouterLink v-if="hasMultipleManagers && manager2Name"
:to="{ name: 'manager', params: { managerName: manager2Name } }">
{{ manager2Name }}
</RouterLink>
</h2>
<h2 id="abbrev">Abbrev: {{ teamAbbreviation }}</h2>
</div>
<div class="col-sm-1">
<!-- TODO move thumbnail to computed and fallback on default pic? -->
<img id="thumbnail" height="125" style="float:right; vertical-align:middle" :src=team?.thumbnail>
</div>
</div>
<!-- Team Rosters -->
<div class="row">
<!-- Left Column -->
<div class="col-sm-6">
<h3 id="active-roster-count">({{ rosterCount }}/26) Active Roster ({{ swarTotal(players).toFixed(2) }})</h3>
<div class="table-responsive-xl">
<table class="table table-sm table-striped" id="table-active-roster">
<thead class="thead-dark">
<tr>
<th>Pos</th>
<th>Name</th>
<th>sWAR</th>
<th>Injury</th>
<th>Positions</th>
</tr>
</thead>
<tbody id="active-roster">
<tr v-for="player in sortedPlayers(players)">
<td>{{ player.pos_1 }}</td>
<td>
<RouterLink :to="{ name: 'player', params: { seasonNumber: seasonNumber, playerName: player.name } }">
{{ player.name }}
</RouterLink>
</td>
<td>{{ player.wara }}</td>
<td>{{ player.injury_rating }}</td>
<td>{{ allPositions(player) }}</td>
</tr>
</tbody>
<tfoot>
<tr>
<th>Pos</th>
<th>Name</th>
<th>sWAR</th>
<th>Injury</th>
<th>Positions</th>
</tr>
</tfoot>
</table>
</div>
</div>
<!-- Right Column -->
<div class="col-sm-6">
<h3 id="minors-header">{{ minorsTeamName }} ({{ swarTotal(playersMinors).toFixed(2) }})</h3>
<div class="table-responsive-xl">
<table class="table table-sm table-striped" id="table-minors-roster">
<thead class="thead-dark">
<tr>
<th>Pos</th>
<th>Name</th>
<th>WARa</th>
<th>Inj Return</th>
<th>Positions</th>
</tr>
</thead>
<tbody id="minors-roster">
<tr v-for="player in sortedPlayers(playersMinors)">
<td>{{ player.pos_1 }}</td>
<td>
<RouterLink :to="{ name: 'player', params: { seasonNumber: seasonNumber, playerName: player.name } }">
{{ player.name }}
</RouterLink>
</td>
<td>{{ player.wara }}</td>
<td>{{ player.il_return }}</td>
<td>{{ allPositions(player) }}</td>
</tr>
</tbody>
</table>
</div>
<h3 id="short-il-header">Injured List ({{ swarTotal(playersInjuredList).toFixed(2) }})</h3>
<div class="table-responsive-xl">
<table class="table table-sm table-striped" id="table-shortil-roster">
<thead class="thead-dark">
<tr>
<th>Pos</th>
<th>Name</th>
<th>WARa</th>
<th>Inj Return</th>
<th>Positions</th>
</tr>
</thead>
<tbody id="short-il-roster">
<tr v-for="player in sortedPlayers(playersInjuredList)">
<td>{{ player.pos_1 }}</td>
<td>
<RouterLink :to="{ name: 'player', params: { seasonNumber: seasonNumber, playerName: player.name } }">
{{ player.name }}
</RouterLink>
</td>
<td>{{ player.wara }}</td>
<td>{{ player.il_return }}</td>
<td>{{ allPositions(player) }}</td>
</tr>
</tbody>
</table>
</div>
<h3 id="transactions-header">Week {{ transactionsWeekNumber }} Transactions ({{ transactionsWar.toFixed(2) }})
</h3>
<div class="table-responsive-xl">
<table class="table table-sm table-striped" id="table-transactions">
<thead class="thead-dark">
<tr>
<th>Week</th>
<th>Player</th>
<th>Old Team</th>
<th>New Team</th>
</tr>
</thead>
<tbody id="transactions-body"></tbody>
<tr v-for="row in transactionRows">
<td>{{ row.week }}</td>
<td>{{ row.playerName }}</td>
<td>{{ row.oldTeam }}</td>
<td>{{ row.newTeam }}</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import type { Team } from '@/services/apiResponseTypes'
import { fetchCurrentLeagueInfo } from '@/services/currentService'
import { fetchPlayersByTeam, type Player } from '@/services/playersService'
import { fetchStandings, type TeamStanding } from '@/services/standingsService'
import { fetchTeam } from '@/services/teamsService'
import { fetchTransactionsByTeamAndWeek, type Transaction } from '@/services/transactionsService'
export default {
name: "TeamView",
data() {
return {
team: undefined as Team | undefined,
teamMinors: undefined as Team | undefined,
teamInjuredList: undefined as Team | undefined,
teamStanding: undefined as TeamStanding | undefined,
players: [] as Player[],
playersMinors: [] as Player[],
playersInjuredList: [] as Player[],
transactions: [] as Transaction[],
weekNumber: undefined as number | undefined
}
},
props: {
seasonNumber: { type: Number, required: true },
teamAbbreviation: { type: String, required: true },
},
computed: {
manager1Name(): string | undefined {
return this.team?.manager1?.name
},
manager2Name(): string | undefined {
return this.team?.manager2?.name
},
hasMultipleManagers(): boolean {
return !!(this.manager1Name && this.manager2Name)
},
teamName(): string | undefined {
return this.team?.lname
},
minorsTeamName(): string | undefined {
return this.teamMinors?.sname
},
lastEight(): string | undefined {
return this.teamStanding?.last8Record
},
streak(): string | undefined {
return this.teamStanding?.streak
},
rosterCount(): number {
return this.players.length
},
transactionsWeekNumber(): number | undefined {
if (this.weekNumber === undefined) return undefined
return this.weekNumber + 1
},
transactionsWar(): number {
const teamsWar = this.swarTotal(this.players)
if (!this.transactions.length) return teamsWar
const playersOut = this.transactions.filter(t => t.oldteam.abbrev === this.teamAbbreviation).map(t => t.player)
const playersIn = this.transactions.filter(t => t.newteam.abbrev === this.teamAbbreviation).map(t => t.player)
const swarOut = this.swarTotal(playersOut)
const swarIn = this.swarTotal(playersIn)
return teamsWar - swarOut + swarIn
},
transactionRows(): { week: number, playerName: string, oldTeam: string, newTeam: string }[] {
return this.transactions.map(t => {
return { week: t.week, playerName: t.player.name, oldTeam: t.oldteam.sname, newTeam: t.newteam.sname }
})
}
},
created() {
this.fetchData()
},
methods: {
async fetchData(): Promise<void> {
this.team = await fetchTeam(this.seasonNumber, this.teamAbbreviation)
this.teamMinors = await fetchTeam(this.seasonNumber, `${this.teamAbbreviation}MiL`)
this.teamInjuredList = await fetchTeam(this.seasonNumber, `${this.teamAbbreviation}IL`)
this.teamStanding = (await fetchStandings(this.seasonNumber - 1)).find(ts => ts.teamName === this.team?.sname)
this.weekNumber = (await fetchCurrentLeagueInfo()).week
this.players = await fetchPlayersByTeam(this.seasonNumber, this.team?.id)
this.playersMinors = await fetchPlayersByTeam(this.seasonNumber, this.teamMinors?.id)
this.playersInjuredList = await fetchPlayersByTeam(this.seasonNumber, this.teamInjuredList?.id)
this.transactions = (await fetchTransactionsByTeamAndWeek(this.seasonNumber, this.teamAbbreviation, this.transactionsWeekNumber!))
console.log(this.transactions)
},
allPositions(player: Player): string {
let positions = []
positions.push(player.pos_1, player.pos_2, player.pos_3, player.pos_4, player.pos_5, player.pos_6, player.pos_7)
return positions.join(" ")
},
swarTotal(players: Player[]): number {
return players.map(p => p.wara).reduce((prev, curr) => prev + curr, 0)
},
sortedPlayers(players: Player[]): Player[] {
return players.sort((a, b) => b.wara - a.wara)
}
}
}
</script>

8
tsconfig.config.json Normal file
View File

@ -0,0 +1,8 @@
{
"extends": "@vue/tsconfig/tsconfig.node.json",
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*"],
"compilerOptions": {
"composite": true,
"types": ["node"]
}
}

16
tsconfig.json Normal file
View File

@ -0,0 +1,16 @@
{
"extends": "@vue/tsconfig/tsconfig.web.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"references": [
{
"path": "./tsconfig.config.json"
}
]
}

20
vite.config.ts Normal file
View File

@ -0,0 +1,20 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
Components({
resolvers: [NaiveUiResolver()]
})],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})