diff --git a/availabili.tf/package-lock.json b/availabili.tf/package-lock.json index 5cd61f8..f2db542 100644 --- a/availabili.tf/package-lock.json +++ b/availabili.tf/package-lock.json @@ -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", diff --git a/availabili.tf/package.json b/availabili.tf/package.json index 72e9569..829c0b7 100644 --- a/availabili.tf/package.json +++ b/availabili.tf/package.json @@ -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" }, diff --git a/availabili.tf/src/assets/base.css b/availabili.tf/src/assets/base.css index 0864268..60a8f37 100644 --- a/availabili.tf/src/assets/base.css +++ b/availabili.tf/src/assets/base.css @@ -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 */ :root { --vt-c-white: #ffffff; diff --git a/availabili.tf/src/assets/fonts/tf2-classicons.eot b/availabili.tf/src/assets/fonts/tf2-classicons.eot new file mode 100644 index 0000000..31312e3 Binary files /dev/null and b/availabili.tf/src/assets/fonts/tf2-classicons.eot differ diff --git a/availabili.tf/src/assets/fonts/tf2-classicons.svg b/availabili.tf/src/assets/fonts/tf2-classicons.svg new file mode 100644 index 0000000..952a1b4 --- /dev/null +++ b/availabili.tf/src/assets/fonts/tf2-classicons.svg @@ -0,0 +1,58 @@ + + + + + + +{ + "fontFamily": "tf2-classicons", + "majorVersion": 1, + "minorVersion": 0, + "fontURL": "https://github.com/Qixalite/tf2-classfont", + "description": "A web icon font representing TF2 core gamemodes classes\nFont generated by IcoMoon.", + "copyright": "Qixalite / qix.tf", + "designer": "Sickday", + "license": "MIT", + "licenseURL": "https://github.com/Qixalite/tf2-classfont/blob/master/LICENSE", + "version": "Version 1.0", + "fontId": "tf2-classicons", + "psName": "tf2-classicons", + "subFamily": "Regular", + "fullName": "tf2-classicons" +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/availabili.tf/src/assets/fonts/tf2-classicons.ttf b/availabili.tf/src/assets/fonts/tf2-classicons.ttf new file mode 100644 index 0000000..816bb76 Binary files /dev/null and b/availabili.tf/src/assets/fonts/tf2-classicons.ttf differ diff --git a/availabili.tf/src/assets/fonts/tf2-classicons.woff b/availabili.tf/src/assets/fonts/tf2-classicons.woff new file mode 100644 index 0000000..4e1e7e5 Binary files /dev/null and b/availabili.tf/src/assets/fonts/tf2-classicons.woff differ diff --git a/availabili.tf/src/assets/main.css b/availabili.tf/src/assets/main.css index 5924315..b4051ce 100644 --- a/availabili.tf/src/assets/main.css +++ b/availabili.tf/src/assets/main.css @@ -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); } diff --git a/availabili.tf/src/assets/tf2icons.css b/availabili.tf/src/assets/tf2icons.css new file mode 100644 index 0000000..6220b4c --- /dev/null +++ b/availabili.tf/src/assets/tf2icons.css @@ -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"; +} diff --git a/availabili.tf/src/components/PlayerCard.vue b/availabili.tf/src/components/PlayerCard.vue index 978264a..2c2fc95 100644 --- a/availabili.tf/src/components/PlayerCard.vue +++ b/availabili.tf/src/components/PlayerCard.vue @@ -9,21 +9,47 @@ const props = defineProps({ roleTitle: String, player: Object as PropType, 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); + } + } } }; @@ -31,45 +57,81 @@ function onClick() { diff --git a/availabili.tf/src/player.ts b/availabili.tf/src/player.ts index 2a0ba6d..38ba1b3 100644 --- a/availabili.tf/src/player.ts +++ b/availabili.tf/src/player.ts @@ -9,4 +9,5 @@ export interface PlayerTeamRole { role: string; main: boolean; availability: number; + playtime: number; } diff --git a/availabili.tf/src/router/index.ts b/availabili.tf/src/router/index.ts index 983edb8..4c87438 100644 --- a/availabili.tf/src/router/index.ts +++ b/availabili.tf/src/router/index.ts @@ -20,7 +20,7 @@ const router = createRouter({ path: "/schedule/roster", name: "roster-builder", component: RosterBuilderView - } + }, ] }) diff --git a/availabili.tf/src/stores/roster.ts b/availabili.tf/src/stores/roster.ts index c3b0adb..c45d0c2 100644 --- a/availabili.tf/src/stores/roster.ts +++ b/availabili.tf/src/stores/roster.ts @@ -14,7 +14,7 @@ export const useRosterStore = defineStore("roster", () => { const selectedPlayers: Reactive<{ [key: string]: PlayerTeamRole }> = reactive({}); - const selectedRole: Ref = ref("Pocket Scout"); + const selectedRole: Ref = ref(undefined); const availablePlayers: Reactive> = 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, } }); diff --git a/availabili.tf/src/views/RosterBuilderView.vue b/availabili.tf/src/views/RosterBuilderView.vue index 34b5239..c8108d5 100644 --- a/availabili.tf/src/views/RosterBuilderView.vue +++ b/availabili.tf/src/views/RosterBuilderView.vue @@ -10,11 +10,27 @@ const rosterStore = useRosterStore(); const hasAvailablePlayers = computed(() => { return rosterStore.availablePlayerRoles.length > 0; }); + +const hasAlternates = computed(() => { + return rosterStore.alternateRoles.length > 0; +}); diff --git a/availabili.tf/src/views/RosterBuilderViewDragDrop.vue b/availabili.tf/src/views/RosterBuilderViewDragDrop.vue new file mode 100644 index 0000000..fe132fe --- /dev/null +++ b/availabili.tf/src/views/RosterBuilderViewDragDrop.vue @@ -0,0 +1,79 @@ + + + + +