Add better player card details and ringers

master
John Montagu, the 4th Earl of Sandvich 2024-10-25 09:51:36 -07:00
parent c08f2434e6
commit 849b628130
Signed by: sandvich
GPG Key ID: 9A39BE37E602B22D
15 changed files with 530 additions and 38 deletions

View File

@ -9,7 +9,9 @@
"version": "0.0.0",
"dependencies": {
"axios": "^1.7.7",
"bootstrap-icons": "^1.11.3",
"pinia": "^2.2.4",
"v-tooltip": "^2.1.3",
"vue": "^3.5.12",
"vue-router": "^4.4.5"
},
@ -68,6 +70,18 @@
"node": ">=6.0.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.9.tgz",
"integrity": "sha512-4zpTHZ9Cm6L9L+uIqghQX8ZXg8HKFcjYO3qHoO8zTmRm6HQUJ8SSJ+KRvbMBZn0EGVlT4DRYeQ/6hjlyXBh+Kg==",
"license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/types": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.9.tgz",
@ -2064,6 +2078,22 @@
"dev": true,
"license": "ISC"
},
"node_modules/bootstrap-icons": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.11.3.tgz",
"integrity": "sha512-+3lpHrCw/it2/7lBL15VR0HEumaBss0+f/Lb6ZvHISn1mlK83jjFpooTLsMWbIjJMDjDjOExMsTxnXSIT4k4ww==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/twbs"
},
{
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
}
],
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@ -4442,7 +4472,6 @@
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.merge": {
@ -5171,6 +5200,17 @@
}
}
},
"node_modules/popper.js": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
"integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==",
"deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/postcss": {
"version": "8.4.47",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
@ -5374,6 +5414,12 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"license": "MIT"
},
"node_modules/request-progress": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz",
@ -5676,6 +5722,16 @@
"node": ">=8"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"license": "BSD-3-Clause",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@ -6195,6 +6251,73 @@
"uuid": "dist/bin/uuid"
}
},
"node_modules/v-tooltip": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/v-tooltip/-/v-tooltip-2.1.3.tgz",
"integrity": "sha512-xXngyxLQTOx/yUEy50thb8te7Qo4XU6h4LZB6cvEfVd9mnysUxLEoYwGWDdqR+l69liKsy3IPkdYff3J1gAJ5w==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"lodash": "^4.17.21",
"popper.js": "^1.16.1",
"vue-resize": "^1.0.1"
}
},
"node_modules/v-tooltip/node_modules/@vue/compiler-sfc": {
"version": "2.7.16",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.16.tgz",
"integrity": "sha512-KWhJ9k5nXuNtygPU7+t1rX6baZeqOYLEforUPjgNDBnLicfHCoi48H87Q8XyLZOrNNsmhuwKqtpDQWjEFe6Ekg==",
"peer": true,
"dependencies": {
"@babel/parser": "^7.23.5",
"postcss": "^8.4.14",
"source-map": "^0.6.1"
},
"optionalDependencies": {
"prettier": "^1.18.2 || ^2.0.0"
}
},
"node_modules/v-tooltip/node_modules/prettier": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
"license": "MIT",
"optional": true,
"peer": true,
"bin": {
"prettier": "bin-prettier.js"
},
"engines": {
"node": ">=10.13.0"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/v-tooltip/node_modules/vue": {
"version": "2.7.16",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.7.16.tgz",
"integrity": "sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==",
"deprecated": "Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-sfc": "2.7.16",
"csstype": "^3.1.0"
}
},
"node_modules/v-tooltip/node_modules/vue-resize": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/vue-resize/-/vue-resize-1.0.1.tgz",
"integrity": "sha512-z5M7lJs0QluJnaoMFTIeGx6dIkYxOwHThlZDeQnWZBizKblb99GSejPnK37ZbNE/rVwDcYcHY+Io+AxdpY952w==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"vue": "^2.6.0"
}
},
"node_modules/verror": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",

View File

@ -15,7 +15,9 @@
},
"dependencies": {
"axios": "^1.7.7",
"bootstrap-icons": "^1.11.3",
"pinia": "^2.2.4",
"v-tooltip": "^2.1.3",
"vue": "^3.5.12",
"vue-router": "^4.4.5"
},

View File

@ -1,3 +1,6 @@
@import url("tf2icons.css");
@import url("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css");
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 93 KiB

View File

@ -23,12 +23,26 @@ a,
}
button {
color: var(--text);
border: none;
font-weight: 700;
color: var(--text);
background-color: var(--surface-0);
border: none;
padding: 8px;
border-radius: 4px;
}
button.accent {
background-color: var(--accent);
color: var(--base);
text-transform: uppercase;
}
h1 {
font-weight: 800;
font-size: 300%;
font-size: 200%;
line-height: 2em;
}
em.aside {
color: var(--overlay-0);
}

View File

@ -0,0 +1,75 @@
@font-face {
font-family: 'tf2-classicons';
src: url('fonts/tf2-classicons.eot?bv99da');
src: url('fonts/tf2-classicons.eot?bv99da#iefix') format('embedded-opentype'),
url('fonts/tf2-classicons.ttf?bv99da') format('truetype'),
url('fonts/tf2-classicons.woff?bv99da') format('woff'),
url('fonts/tf2-classicons.svg?bv99da#tf2-classicons') format('svg');
font-weight: normal;
font-style: normal;
}
i, .icomoon-liga {
/* use !important to prevent issues with browser extensions that change fonts */
font-family: 'tf2-classicons' !important;
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
/* Enable Ligatures ================ */
letter-spacing: 0;
-webkit-font-feature-settings: "liga";
-moz-font-feature-settings: "liga=1";
-moz-font-feature-settings: "liga";
-ms-font-feature-settings: "liga" 1;
font-feature-settings: "liga";
-webkit-font-variant-ligatures: discretionary-ligatures;
font-variant-ligatures: discretionary-ligatures;
/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.tf2-Heavy:before {
content: "\1f4aa";
}
.tf2-Medic:before {
content: "\1f497";
}
.tf2-Pyro:before {
content: "\1f525";
}
.tf2-Scout:before {
content: "\1f407";
}
.tf2-Sniper:before {
content: "\1f3b7";
}
.tf2-Soldier:before {
content: "\1f4a5";
}
.tf2-Spy:before {
content: "\1f4e6";
}
.tf2-Demo:before {
content: "\1f4a3";
}
.tf2-Engineer:before {
content: "\1f527";
}
.tf2-FlankSoldier:before {
content: "\1f400";
}
.tf2-PocketSoldier:before {
content: "\1f418";
}
.tf2-FlankScout:before {
content: "\1f43f";
}
.tf2-PocketScout:before {
content: "\1f416";
}

View File

@ -9,21 +9,47 @@ const props = defineProps({
roleTitle: String,
player: Object as PropType<PlayerTeamRole>,
isRoster: Boolean,
isRinger: Boolean,
});
const isSelected = computed(() => {
if (props.isRoster) {
return rosterStore.selectedRole == props.roleTitle;
}
if (props.isRinger) {
return rosterStore.selectedPlayers[props.roleTitle]?.playtime == -1;
}
return Object.values(rosterStore.selectedPlayers).includes(props.player);
});
function onClick() {
if (props.isRoster) {
rosterStore.selectedRole = props.roleTitle;
if (rosterStore.selectedRole == props.roleTitle) {
rosterStore.selectedRole = undefined;
} else {
rosterStore.selectedRole = props.roleTitle;
}
} else {
// we are selecting the player
rosterStore.selectPlayerForRole(props.player, props.roleTitle);
if (isSelected.value) {
rosterStore.selectPlayerForRole(undefined, props.roleTitle);
} else {
if (props.isRinger) {
const ringerPlayer: PlayerTeamRole = {
steamId: -1,
name: "Ringer",
role: props.roleTitle,
main: false,
availability: 1,
playtime: -1,
};
rosterStore.selectPlayerForRole(ringerPlayer, props.roleTitle);
} else {
rosterStore.selectPlayerForRole(props.player, props.roleTitle);
}
}
}
};
</script>
@ -31,45 +57,81 @@ function onClick() {
<template>
<button :class="{
'player-card': true,
'no-player': !player,
'no-player': !player && !isRinger,
'selected': isSelected,
}" @click="onClick">
<div v-if="player">
<h1>{{ player.name }}</h1>
<span v-if="roleTitle != player.role">
Subbing in as
</span>
{{ player.role }}
<span v-if="!player.main">
(alternate role)
'can-be-available': player?.availability == 2
}" @click="onClick">
<div class="role-icon">
<i :class="rosterStore.roleIcons[roleTitle]" />
</div>
<div v-if="player" class="role-info">
<span>
<h4 class="player-name">{{ player.name }}</h4>
<span v-if="roleTitle != player.role">
Subbing in as
</span>
{{ player.role }}
<span v-if="!player.main && isRoster">
(alternate)
</span>
</span>
</div>
<div v-else>
{{ roleTitle }}
<div v-else-if="isRinger" class="role-info">
<span>
<h4 class="player-name">Ringer</h4>
{{ roleTitle }}
</span>
</div>
<div v-else class="role-info">
<span>
{{ roleTitle }}
</span>
</div>
</button>
</template>
<style scoped>
.player-card {
background-color: var(--crust);
background-color: white;
padding: 1em;
border-radius: 8px;
user-select: none;
display: flex;
gap: 1em;
align-items: center;
border: 2px solid white;
box-shadow: 1px 1px 8px var(--surface-0);
}
.player-card.can-be-available {
color: var(--overlay-0);
}
.player-card .role-icon {
display: flex;
flex-direction: row;
align-items: center;
font-size: 2em;
}
.player-card .role-info {
text-align: left;
}
.player-card:hover {
background-color: var(--surface-0);
transition-duration: 200ms;
border-color: var(--surface-0);
}
.player-card.no-player {
border: 2px dashed var(--overlay-0);
border: 2px solid var(--overlay-0);
box-shadow: none;
}
.player-card.no-player.selected {
background-color: var(--accent-transparent);
border: 2px dashed var(--accent);
border: 2px solid var(--accent);
color: var(--accent);
}
@ -89,8 +151,8 @@ function onClick() {
color: var(--accent);
}
h1 {
font-size: 24px;
font-weight: 700;
.player-name {
font-size: 16px;
font-weight: 600;
}
</style>

View File

@ -9,4 +9,5 @@ export interface PlayerTeamRole {
role: string;
main: boolean;
availability: number;
playtime: number;
}

View File

@ -20,7 +20,7 @@ const router = createRouter({
path: "/schedule/roster",
name: "roster-builder",
component: RosterBuilderView
}
},
]
})

View File

@ -14,7 +14,7 @@ export const useRosterStore = defineStore("roster", () => {
const selectedPlayers: Reactive<{ [key: string]: PlayerTeamRole }> = reactive({});
const selectedRole: Ref<String | undefined> = ref("Pocket Scout");
const selectedRole: Ref<String | undefined> = ref(undefined);
const availablePlayers: Reactive<Array<PlayerTeamRole>> = reactive([
{
@ -23,6 +23,7 @@ export const useRosterStore = defineStore("roster", () => {
role: "Flank Scout",
main: true,
availability: 1,
playtime: 35031,
},
{
steamId: 2839,
@ -30,6 +31,7 @@ export const useRosterStore = defineStore("roster", () => {
role: "Flank Scout",
main: false,
availability: 1,
playtime: 28811,
},
{
steamId: 2839,
@ -37,6 +39,7 @@ export const useRosterStore = defineStore("roster", () => {
role: "Pocket Scout",
main: true,
availability: 1,
playtime: 28811,
},
{
steamId: 2841,
@ -44,6 +47,7 @@ export const useRosterStore = defineStore("roster", () => {
role: "Pocket Soldier",
main: true,
availability: 2,
playtime: 98372,
},
{
steamId: 2841,
@ -51,6 +55,7 @@ export const useRosterStore = defineStore("roster", () => {
role: "Roamer",
main: false,
availability: 2,
playtime: 98372,
},
{
steamId: 2282,
@ -58,6 +63,7 @@ export const useRosterStore = defineStore("roster", () => {
role: "Demoman",
main: true,
availability: 2,
playtime: 47324,
},
{
steamId: 2842,
@ -65,6 +71,7 @@ export const useRosterStore = defineStore("roster", () => {
role: "Roamer",
main: false,
availability: 2,
playtime: 12028,
},
{
steamId: 2842,
@ -72,6 +79,7 @@ export const useRosterStore = defineStore("roster", () => {
role: "Demoman",
main: false,
availability: 2,
playtime: 12028,
},
{
steamId: 2842,
@ -79,6 +87,7 @@ export const useRosterStore = defineStore("roster", () => {
role: "Pocket Scout",
main: false,
availability: 2,
playtime: 12028,
},
//{
// steamId: 2843,
@ -93,6 +102,7 @@ export const useRosterStore = defineStore("roster", () => {
role: "Pocket Soldier",
main: false,
availability: 2,
playtime: 50201,
},
{
steamId: 2843,
@ -100,6 +110,7 @@ export const useRosterStore = defineStore("roster", () => {
role: "Roamer",
main: false,
availability: 2,
playtime: 50201,
},
{
steamId: 2844,
@ -107,6 +118,7 @@ export const useRosterStore = defineStore("roster", () => {
role: "Roamer",
main: true,
availability: 1,
playtime: 4732,
},
{
steamId: 2844,
@ -114,6 +126,7 @@ export const useRosterStore = defineStore("roster", () => {
role: "Pocket Soldier",
main: false,
availability: 1,
playtime: 4732,
},
]);
@ -129,9 +142,27 @@ export const useRosterStore = defineStore("roster", () => {
return availablePlayerRoles.value.filter((player) => player.availability == 1);
});
const mainRoles = computed(() => {
return availablePlayerRoles.value.filter((player) => player.main)
.sort((a, b) => b.playtime - a.playtime);
});
const alternateRoles = computed(() => {
return availablePlayerRoles.value.filter((player) => !player.main)
.sort((a, b) => b.playtime - a.playtime);
});
const roleIcons = reactive({
"Pocket Scout": "tf2-PocketScout",
"Flank Scout": "tf2-FlankScout",
"Pocket Soldier": "tf2-PocketSoldier",
"Roamer": "tf2-FlankSoldier",
"Demoman": "tf2-Demo",
"Medic": "tf2-Medic",
});
function selectPlayerForRole(player: PlayerTeamRole, role: string) {
console.log("selecting.");
if (player) {
if (player && player.steamId > 0) {
const existingRole = Object.keys(selectedPlayers).find((selectedRole) => {
return selectedPlayers[selectedRole]?.steamId == player.steamId &&
role != selectedRole;
@ -154,5 +185,8 @@ export const useRosterStore = defineStore("roster", () => {
selectPlayerForRole,
definitelyAvailable,
canBeAvailable,
roleIcons,
mainRoles,
alternateRoles,
}
});

View File

@ -10,11 +10,27 @@ const rosterStore = useRosterStore();
const hasAvailablePlayers = computed(() => {
return rosterStore.availablePlayerRoles.length > 0;
});
const hasAlternates = computed(() => {
return rosterStore.alternateRoles.length > 0;
});
</script>
<template>
<main>
<h1>Roster</h1>
<div class="top">
<h1 class="roster-title">
Roster for Snus Brotherhood
<em class="aside date">Aug. 13, 2036 @ 11:30 PM EST</em>
</h1>
<div class="button-group">
<button>
<i class="bi bi-box-arrow-left"></i>
Back
</button>
<button class="accent">Submit</button>
</div>
</div>
<div class="columns">
<div class="column">
<PlayerCard v-for="role in rosterStore.neededRoles"
@ -23,25 +39,38 @@ const hasAvailablePlayers = computed(() => {
is-roster />
</div>
<div class="column">
<h3 v-if="hasAvailablePlayers">Available</h3>
<PlayerCard v-for="player in rosterStore.definitelyAvailable"
<PlayerCard v-for="player in rosterStore.mainRoles"
:player="player"
:role-title="player.role" />
<span v-if="!hasAvailablePlayers">
<span v-if="!hasAvailablePlayers && rosterStore.selectedRole">
No players are currently available for this role.
</span>
</div>
<div class="column">
<h3 v-if="hasAvailablePlayers">Available if needed</h3>
<PlayerCard v-for="player in rosterStore.canBeAvailable"
<h3 v-if="hasAvailablePlayers">Alternates</h3>
<PlayerCard v-for="player in rosterStore.alternateRoles"
:player="player"
:role-title="player.role" />
<PlayerCard v-if="rosterStore.selectedRole"
is-ringer
:role-title="rosterStore.selectedRole" />
</div>
</div>
</main>
</template>
<style scoped>
.top {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.top .button-group {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.columns {
display: flex;
flex-direction: row;
@ -57,8 +86,20 @@ const hasAvailablePlayers = computed(() => {
width: 100%;
}
h3 {
.column h3 {
font-weight: 700;
color: var(--subtext-0);
font-size: 14px;
text-transform: uppercase;
color: var(--overlay-0);
}
.roster-title {
display: flex;
gap: 0.5em;
}
em.aside.date {
font-size: 14px;
vertical-align: middle;
}
</style>

View File

@ -0,0 +1,79 @@
<script setup lang="ts">
import PlayerCard from "../components/PlayerCard.vue";
import RoleSlot from "../components/RoleSlot.vue";
import PlayerTeamRole from "../player.ts";
import { computed, reactive } from "vue";
import { useRosterStore } from "../stores/roster";
const rosterStore = useRosterStore();
const hasAvailablePlayers = computed(() => {
return rosterStore.availablePlayerRoles.length > 0;
});
</script>
<template>
<main>
<h1 class="roster-title">
Roster for Snus Brotherhood
<emph class="aside date">Aug. 13, 2036 @ 11:30 PM EST</emph>
</h1>
<div class="columns">
<div class="column">
<PlayerCard v-for="role in rosterStore.neededRoles"
:player="rosterStore.selectedPlayers[role]"
:role-title="role"
is-roster />
</div>
<div class="column">
<h3 v-if="hasAvailablePlayers">Available</h3>
<PlayerCard v-for="player in rosterStore.definitelyAvailableAll"
:player="player"
:role-title="player.role" />
<span v-if="!hasAvailablePlayers">
No players are currently available for this role.
</span>
</div>
<div class="column">
<h3 v-if="hasAvailablePlayers">Available if needed</h3>
<PlayerCard v-for="player in rosterStore.canBeAvailableAll"
:player="player"
:role-title="player.role" />
</div>
</div>
</main>
</template>
<style scoped>
.columns {
display: flex;
flex-direction: row;
}
.column {
display: flex;
flex-grow: 1;
margin-left: 4em;
margin-right: 4em;
flex-direction: column;
row-gap: 8px;
width: 100%;
}
.column h3 {
font-weight: 700;
font-size: 14px;
text-transform: uppercase;
color: var(--overlay-0);
}
.roster-title {
display: flex;
gap: 0.5em;
}
emph.aside.date {
font-size: 14px;
vertical-align: middle;
}
</style>