Improve user experience quality of life
							parent
							
								
									d3abf67d88
								
							
						
					
					
						commit
						42b7e603f0
					
				| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import { RouterLink, RouterView } from "vue-router";
 | 
					import { RouterLink, RouterView } from "vue-router";
 | 
				
			||||||
import { useAuthStore } from "./stores/auth";
 | 
					import { useAuthStore } from "./stores/auth";
 | 
				
			||||||
 | 
					import ProfileDropdown from "./components/ProfileDropdown.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const baseUrl = window.location.origin;
 | 
					const baseUrl = window.location.origin;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,26 +12,41 @@ const authStore = useAuthStore();
 | 
				
			||||||
  <header>
 | 
					  <header>
 | 
				
			||||||
    <div class="wrapper">
 | 
					    <div class="wrapper">
 | 
				
			||||||
      <nav>
 | 
					      <nav>
 | 
				
			||||||
        <h1>availabili.tf</h1>
 | 
					        <h1>
 | 
				
			||||||
        <RouterLink to="/">Home</RouterLink>
 | 
					          <RouterLink class="header-link" to="/">availabili.tf</RouterLink>
 | 
				
			||||||
        <RouterLink to="/schedule">Schedule</RouterLink>
 | 
					        </h1>
 | 
				
			||||||
        <div v-if="authStore.isLoggedIn">
 | 
					        <div class="nav-links">
 | 
				
			||||||
          Welcome {{ authStore.username }}
 | 
					          <a
 | 
				
			||||||
 | 
					            class="button"
 | 
				
			||||||
 | 
					            href="https://github.com/HumanoidSandvichDispenser/availabili.tf"
 | 
				
			||||||
 | 
					            v-tooltip="'View on GitHub'"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <button class="icon">
 | 
				
			||||||
 | 
					              <i class="bi bi-github" />
 | 
				
			||||||
 | 
					            </button>
 | 
				
			||||||
 | 
					          </a>
 | 
				
			||||||
 | 
					          <ProfileDropdown v-if="authStore.isLoggedIn" />
 | 
				
			||||||
 | 
					          <!--button v-if="authStore.isLoggedIn" class="profile-button">
 | 
				
			||||||
 | 
					            Welcome {{ authStore.username }}
 | 
				
			||||||
 | 
					          </button-->
 | 
				
			||||||
 | 
					          <form
 | 
				
			||||||
 | 
					            v-else
 | 
				
			||||||
 | 
					            action="https://steamcommunity.com/openid/login"
 | 
				
			||||||
 | 
					            method="get"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <input type="hidden" name="openid.identity"
 | 
				
			||||||
 | 
					                   value="http://specs.openid.net/auth/2.0/identifier_select" />
 | 
				
			||||||
 | 
					            <input type="hidden" name="openid.claimed_id"
 | 
				
			||||||
 | 
					                   value="http://specs.openid.net/auth/2.0/identifier_select" />
 | 
				
			||||||
 | 
					            <input type="hidden" name="openid.ns" value="http://specs.openid.net/auth/2.0" />
 | 
				
			||||||
 | 
					            <input type="hidden" name="openid.mode" value="checkid_setup" />
 | 
				
			||||||
 | 
					            <input type="hidden" name="openid.return_to" :value="baseUrl + '/login'" />
 | 
				
			||||||
 | 
					            <!--button type="submit">Log in through Steam</button-->
 | 
				
			||||||
 | 
					            <button type="submit" class="sign-in-button">
 | 
				
			||||||
 | 
					              <img src="https://community.fastly.steamstatic.com/public/images/signinthroughsteam/sits_01.png" />
 | 
				
			||||||
 | 
					            </button>
 | 
				
			||||||
 | 
					          </form>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <form
 | 
					 | 
				
			||||||
          v-else
 | 
					 | 
				
			||||||
          action="https://steamcommunity.com/openid/login"
 | 
					 | 
				
			||||||
          method="get"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <input type="hidden" name="openid.identity"
 | 
					 | 
				
			||||||
                 value="http://specs.openid.net/auth/2.0/identifier_select" />
 | 
					 | 
				
			||||||
          <input type="hidden" name="openid.claimed_id"
 | 
					 | 
				
			||||||
                 value="http://specs.openid.net/auth/2.0/identifier_select" />
 | 
					 | 
				
			||||||
          <input type="hidden" name="openid.ns" value="http://specs.openid.net/auth/2.0" />
 | 
					 | 
				
			||||||
          <input type="hidden" name="openid.mode" value="checkid_setup" />
 | 
					 | 
				
			||||||
          <input type="hidden" name="openid.return_to" :value="baseUrl + '/login'" />
 | 
					 | 
				
			||||||
          <button type="submit">Log in through Steam</button>
 | 
					 | 
				
			||||||
        </form>
 | 
					 | 
				
			||||||
      </nav>
 | 
					      </nav>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </header>
 | 
					  </header>
 | 
				
			||||||
| 
						 | 
					@ -46,6 +62,10 @@ header {
 | 
				
			||||||
  max-height: 100vh;
 | 
					  max-height: 100vh;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					a.header-link {
 | 
				
			||||||
 | 
					  font-weight: 800;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.logo {
 | 
					.logo {
 | 
				
			||||||
  display: block;
 | 
					  display: block;
 | 
				
			||||||
  margin: 0 auto 2rem;
 | 
					  margin: 0 auto 2rem;
 | 
				
			||||||
| 
						 | 
					@ -53,28 +73,36 @@ header {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
nav {
 | 
					nav {
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  gap: 8px;
 | 
					 | 
				
			||||||
  width: 100%;
 | 
					  width: 100%;
 | 
				
			||||||
  font-size: 12px;
 | 
					 | 
				
			||||||
  text-align: center;
 | 
					  text-align: center;
 | 
				
			||||||
  margin: 0;
 | 
					  margin: 0;
 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  justify-content: space-between;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					nav .nav-links {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: end;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  font-size: 11pt;
 | 
				
			||||||
 | 
					  gap: 1rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					button.profile-button {
 | 
				
			||||||
 | 
					  background-color: transparent;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
nav a.router-link-exact-active {
 | 
					nav a.router-link-exact-active {
 | 
				
			||||||
  color: var(--crust);
 | 
					  color: var(--text);
 | 
				
			||||||
  background-color: var(--accent);
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
nav a {
 | 
					nav a {
 | 
				
			||||||
  padding: 0.5rem 1rem;
 | 
					 | 
				
			||||||
  color: var(--subtext-0);
 | 
					  color: var(--subtext-0);
 | 
				
			||||||
  border-radius: 8px;
 | 
					  border-radius: 8px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
nav a:hover {
 | 
					nav a:hover {
 | 
				
			||||||
  color: var(--accent);
 | 
					  background-color: transparent;
 | 
				
			||||||
  background-color: var(--accent-transparent);
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
nav > h1 {
 | 
					nav > h1 {
 | 
				
			||||||
| 
						 | 
					@ -82,6 +110,12 @@ nav > h1 {
 | 
				
			||||||
  margin-right: 1rem;
 | 
					  margin-right: 1rem;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					button.sign-in-button {
 | 
				
			||||||
 | 
					  background-color: transparent;
 | 
				
			||||||
 | 
					  border: none;
 | 
				
			||||||
 | 
					  padding: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@media (min-width: 1024px) {
 | 
					@media (min-width: 1024px) {
 | 
				
			||||||
  header {
 | 
					  header {
 | 
				
			||||||
    display: flex;
 | 
					    display: flex;
 | 
				
			||||||
| 
						 | 
					@ -102,7 +136,6 @@ nav > h1 {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  nav {
 | 
					  nav {
 | 
				
			||||||
    text-align: left;
 | 
					    text-align: left;
 | 
				
			||||||
    margin-left: -1rem;
 | 
					 | 
				
			||||||
    font-size: 1rem;
 | 
					    font-size: 1rem;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    padding: 1rem 0;
 | 
					    padding: 1rem 0;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -271,7 +271,7 @@ hr {
 | 
				
			||||||
  /*box-shadow: 0 0 4px var(--text);*/
 | 
					  /*box-shadow: 0 0 4px var(--text);*/
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[role="menu"] {
 | 
					[role="menu"], [role="listbox"] {
 | 
				
			||||||
  background-color: var(--base);
 | 
					  background-color: var(--base);
 | 
				
			||||||
  border: 1px solid var(--overlay-0);
 | 
					  border: 1px solid var(--overlay-0);
 | 
				
			||||||
  border-radius: 4px;
 | 
					  border-radius: 4px;
 | 
				
			||||||
| 
						 | 
					@ -279,6 +279,16 @@ hr {
 | 
				
			||||||
  min-width: 8rem;
 | 
					  min-width: 8rem;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[role="listbox"] [role="option"] {
 | 
				
			||||||
 | 
					  padding: 0.5rem 1rem;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[role="listbox"] [role="option"][data-highlighted] {
 | 
				
			||||||
 | 
					  background-color: var(--surface-0);
 | 
				
			||||||
 | 
					  padding: 4px 10px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[role="menu"] button {
 | 
					[role="menu"] button {
 | 
				
			||||||
  background-color: var(--base);
 | 
					  background-color: var(--base);
 | 
				
			||||||
  width: 100%;
 | 
					  width: 100%;
 | 
				
			||||||
| 
						 | 
					@ -297,17 +307,6 @@ hr {
 | 
				
			||||||
[role="menu"] button > i.bi.margin {
 | 
					[role="menu"] button > i.bi.margin {
 | 
				
			||||||
  margin-right: 0.5em;
 | 
					  margin-right: 0.5em;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
/*
 | 
					 | 
				
			||||||
div[role="menu"] div[role="menuitem"]:first-child button {
 | 
					 | 
				
			||||||
  border-top-left-radius: var(--border-radius);
 | 
					 | 
				
			||||||
  border-top-right-radius: var(--border-radius);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div[role="menu"] div[role="menuitem"]:last-child button {
 | 
					 | 
				
			||||||
  border-bottom-left-radius: var(--border-radius);
 | 
					 | 
				
			||||||
  border-bottom-right-radius: var(--border-radius);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
[role="menu"] button:hover {
 | 
					[role="menu"] button:hover {
 | 
				
			||||||
  background-color: var(--surface-0);
 | 
					  background-color: var(--surface-0);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,91 +1,53 @@
 | 
				
			||||||
<script setup lang="ts">
 | 
					<script setup lang="ts" generic="T extends AcceptableValue">
 | 
				
			||||||
import { computed, defineModel, defineProps, ref } from "vue";
 | 
					import { defineModel, defineProps, ref } from "vue";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ComboboxContent,
 | 
				
			||||||
 | 
					  ComboboxInput,
 | 
				
			||||||
 | 
					  ComboboxItem,
 | 
				
			||||||
 | 
					  ComboboxRoot,
 | 
				
			||||||
 | 
					  ComboboxPortal,
 | 
				
			||||||
 | 
					  ComboboxTrigger,
 | 
				
			||||||
 | 
					  ComboboxAnchor,
 | 
				
			||||||
 | 
					  ComboboxViewport,
 | 
				
			||||||
 | 
					} from "radix-vue";
 | 
				
			||||||
 | 
					import type { AcceptableValue } from "node_modules/radix-vue/dist/shared/types";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const model = defineModel();
 | 
					const selectedValue = defineModel<T>();
 | 
				
			||||||
 | 
					 | 
				
			||||||
const props = defineProps({
 | 
					 | 
				
			||||||
  options: Array<String>,
 | 
					 | 
				
			||||||
  isDisabled: Boolean,
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const isOpen = ref(false);
 | 
					const isOpen = ref(false);
 | 
				
			||||||
const selectedOption = computed(() => props.options[model.value]);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
function selectOption(index) {
 | 
					withDefaults(defineProps<{
 | 
				
			||||||
  model.value = index;
 | 
					  values: T[];
 | 
				
			||||||
  isOpen.value = false;
 | 
					  //mapper? (value: T): string;
 | 
				
			||||||
}
 | 
					  //keyMapper? (value: T): string;
 | 
				
			||||||
 | 
					  display: string;
 | 
				
			||||||
 | 
					  keyField: string;
 | 
				
			||||||
 | 
					}>(), {
 | 
				
			||||||
 | 
					  //mapper: (value: T) => value.toString(),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div :class="{ 'dropdown-container': true, 'is-open': isOpen }">
 | 
					  <ComboboxRoot
 | 
				
			||||||
    <button @click="isOpen = !isOpen" :disabled="isDisabled">
 | 
					    v-model="selectedValue"
 | 
				
			||||||
      {{ selectedOption }}
 | 
					    v-model:open="isOpen"
 | 
				
			||||||
      <i class="bi bi-caret-down-fill"></i>
 | 
					    defaultOpen
 | 
				
			||||||
    </button>
 | 
					  >
 | 
				
			||||||
    <ul class="dropdown" v-if="isOpen" @blur="isOpen = false">
 | 
					    <ComboboxAnchor>
 | 
				
			||||||
      <li v-for="(option, i) in options" :key="i" @click="selectOption(i)">
 | 
					      <ComboboxInput />
 | 
				
			||||||
        <button :class="{ 'is-selected': i == model }">
 | 
					      <ComboboxTrigger>
 | 
				
			||||||
          {{ option }}
 | 
					        hi
 | 
				
			||||||
        </button>
 | 
					      </ComboboxTrigger>
 | 
				
			||||||
      </li>
 | 
					    </ComboboxAnchor>
 | 
				
			||||||
    </ul>
 | 
					
 | 
				
			||||||
  </div>
 | 
					    <ComboboxPortal>
 | 
				
			||||||
 | 
					      <ComboboxContent position="popper">
 | 
				
			||||||
 | 
					        <ComboboxViewport>
 | 
				
			||||||
 | 
					        </ComboboxViewport>
 | 
				
			||||||
 | 
					      </ComboboxContent>
 | 
				
			||||||
 | 
					    </ComboboxPortal>
 | 
				
			||||||
 | 
					  </ComboboxRoot>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style scoped>
 | 
					<style scoped>
 | 
				
			||||||
.dropdown-container {
 | 
					 | 
				
			||||||
  display: inline-block;
 | 
					 | 
				
			||||||
  border-radius: 8px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.dropdown-container button {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  flex-direction: row;
 | 
					 | 
				
			||||||
  align-items: center;
 | 
					 | 
				
			||||||
  justify-content: space-between;
 | 
					 | 
				
			||||||
  width: 100%;
 | 
					 | 
				
			||||||
  text-align: left;
 | 
					 | 
				
			||||||
  font-weight: 700;
 | 
					 | 
				
			||||||
  font-size: 16px;
 | 
					 | 
				
			||||||
  padding: 4px;
 | 
					 | 
				
			||||||
  transition-duration: 200ms;
 | 
					 | 
				
			||||||
  background-color: transparent;
 | 
					 | 
				
			||||||
  cursor: pointer;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.dropdown-container button:hover {
 | 
					 | 
				
			||||||
  background-color: var(--crust);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.dropdown-container.is-open ul.dropdown {
 | 
					 | 
				
			||||||
  box-shadow: 1px 1px 8px var(--shadow);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ul.dropdown {
 | 
					 | 
				
			||||||
  display: block;
 | 
					 | 
				
			||||||
  background-color: var(--base);
 | 
					 | 
				
			||||||
  position: absolute;
 | 
					 | 
				
			||||||
  margin-top: 8px;
 | 
					 | 
				
			||||||
  padding: 0;
 | 
					 | 
				
			||||||
  z-index: 2;
 | 
					 | 
				
			||||||
  border-radius: 8px;
 | 
					 | 
				
			||||||
  overflow: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ul.dropdown > li {
 | 
					 | 
				
			||||||
  list-style-type: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.dropdown li > button {
 | 
					 | 
				
			||||||
  padding: 8px 16px;
 | 
					 | 
				
			||||||
  font-weight: 500;
 | 
					 | 
				
			||||||
  font-size: 14px;
 | 
					 | 
				
			||||||
  border-radius: 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.dropdown li > button.is-selected {
 | 
					 | 
				
			||||||
  background-color: var(--accent-transparent);
 | 
					 | 
				
			||||||
  color: var(--accent);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -27,8 +27,8 @@ function disableIntegration() {
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <h2>logs.tf Integration</h2>
 | 
					  <h2>logs.tf Auto-Tracking</h2>
 | 
				
			||||||
  <p>Automatically track match history from logs.tf.</p>
 | 
					  <p>Automatically fetch and track match history from logs.tf.</p>
 | 
				
			||||||
  <div v-if="model">
 | 
					  <div v-if="model">
 | 
				
			||||||
    <div class="form-group margin">
 | 
					    <div class="form-group margin">
 | 
				
			||||||
      <h3>logs.tf API key (optional)</h3>
 | 
					      <h3>logs.tf API key (optional)</h3>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,11 +5,12 @@ import { useRosterStore } from "../stores/roster";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const rosterStore = useRosterStore();
 | 
					const rosterStore = useRosterStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const props = defineProps({
 | 
					const props = withDefaults(defineProps<{
 | 
				
			||||||
  roleTitle: String,
 | 
					  roleTitle: string,
 | 
				
			||||||
  player: Object as PropType<PlayerTeamRoleFlat>,
 | 
					  player: PlayerTeamRoleFlat | undefined,
 | 
				
			||||||
  isRoster: Boolean,
 | 
					  isRoster: boolean,
 | 
				
			||||||
  isRinger: Boolean,
 | 
					}>(), {
 | 
				
			||||||
 | 
					  isRoster: false,
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const isSelected = computed(() => {
 | 
					const isSelected = computed(() => {
 | 
				
			||||||
| 
						 | 
					@ -17,11 +18,9 @@ const isSelected = computed(() => {
 | 
				
			||||||
    return rosterStore.selectedRole == props.roleTitle;
 | 
					    return rosterStore.selectedRole == props.roleTitle;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (props.isRinger) {
 | 
					  const selectedPlayers = rosterStore.selectedPlayers;
 | 
				
			||||||
    return rosterStore.selectedPlayers[props.roleTitle]?.playtime == -1;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return Object.values(rosterStore.selectedPlayers).includes(props.player);
 | 
					  return selectedPlayers[props.roleTitle]?.steamId == props.player?.steamId;
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function onClick() {
 | 
					function onClick() {
 | 
				
			||||||
| 
						 | 
					@ -36,25 +35,13 @@ function onClick() {
 | 
				
			||||||
    if (isSelected.value) {
 | 
					    if (isSelected.value) {
 | 
				
			||||||
      rosterStore.selectPlayerForRole(undefined, props.roleTitle);
 | 
					      rosterStore.selectPlayerForRole(undefined, props.roleTitle);
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      if (props.isRinger) {
 | 
					      rosterStore.selectPlayerForRole(props.player, props.roleTitle);
 | 
				
			||||||
        const ringerPlayer: PlayerTeamRoleFlat = {
 | 
					 | 
				
			||||||
          steamId: "0",
 | 
					 | 
				
			||||||
          name: "Ringer",
 | 
					 | 
				
			||||||
          role: props.roleTitle ?? "",
 | 
					 | 
				
			||||||
          isMain: false,
 | 
					 | 
				
			||||||
          availability: 1,
 | 
					 | 
				
			||||||
          playtime: -1,
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
        rosterStore.selectPlayerForRole(ringerPlayer, props.roleTitle);
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        rosterStore.selectPlayerForRole(props.player, props.roleTitle);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const playtime = computed(() => {
 | 
					const playtime = computed(() => {
 | 
				
			||||||
  let hours = props.player?.playtime / 3600 ?? 0;
 | 
					  let hours = props.player?.playtime ?? 0 / 3600;
 | 
				
			||||||
  return hours.toFixed(1);
 | 
					  return hours.toFixed(1);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					@ -62,7 +49,7 @@ const playtime = computed(() => {
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <button :class="{
 | 
					  <button :class="{
 | 
				
			||||||
    'player-card': true,
 | 
					    'player-card': true,
 | 
				
			||||||
    'no-player': !player && !isRinger,
 | 
					    'no-player': !player,
 | 
				
			||||||
    'selected': isSelected,
 | 
					    'selected': isSelected,
 | 
				
			||||||
    'can-be-available': player?.availability == 1
 | 
					    'can-be-available': player?.availability == 1
 | 
				
			||||||
  }" @click="onClick">
 | 
					  }" @click="onClick">
 | 
				
			||||||
| 
						 | 
					@ -79,21 +66,12 @@ const playtime = computed(() => {
 | 
				
			||||||
              (alternate)
 | 
					              (alternate)
 | 
				
			||||||
            </span>
 | 
					            </span>
 | 
				
			||||||
          </span>
 | 
					          </span>
 | 
				
			||||||
          <span v-if="playtime > 0">
 | 
					          <span v-if="Number(playtime) > 0">
 | 
				
			||||||
            {{ playtime }} hours
 | 
					            {{ playtime }} hours
 | 
				
			||||||
          </span>
 | 
					          </span>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </span>
 | 
					      </span>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div v-else-if="isRinger" class="role-info">
 | 
					 | 
				
			||||||
      <span>
 | 
					 | 
				
			||||||
        <h4 class="player-name">Ringer</h4>
 | 
					 | 
				
			||||||
        <div class="subtitle">
 | 
					 | 
				
			||||||
          <span>{{ rosterStore.roleNames[roleTitle] }}</span>
 | 
					 | 
				
			||||||
          <!--span>nobody likes to play {{ roleTitle }}</span-->
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </span>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
    <div v-else class="role-info">
 | 
					    <div v-else class="role-info">
 | 
				
			||||||
      <span>
 | 
					      <span>
 | 
				
			||||||
        {{ rosterStore.roleNames[roleTitle] }}
 | 
					        {{ rosterStore.roleNames[roleTitle] }}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -131,9 +131,14 @@ const rightIndicator = computed(() => {
 | 
				
			||||||
            :availability="player.availability[1]"
 | 
					            :availability="player.availability[1]"
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <h3>
 | 
					        <a
 | 
				
			||||||
          {{ player.username }}
 | 
					          class="player-name"
 | 
				
			||||||
        </h3>
 | 
					          :href="`https://steamcommunity.com/profiles/${player.steamId}`"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <h3>
 | 
				
			||||||
 | 
					            {{ player.username }}
 | 
				
			||||||
 | 
					          </h3>
 | 
				
			||||||
 | 
					        </a>
 | 
				
			||||||
        <svg-icon
 | 
					        <svg-icon
 | 
				
			||||||
          v-if="player.isTeamLeader"
 | 
					          v-if="player.isTeamLeader"
 | 
				
			||||||
          :class="[
 | 
					          :class="[
 | 
				
			||||||
| 
						 | 
					@ -256,6 +261,14 @@ const rightIndicator = computed(() => {
 | 
				
			||||||
  background-color: var(--green);
 | 
					  background-color: var(--green);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					a.player-name {
 | 
				
			||||||
 | 
					  color: unset;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					a.player-name:hover {
 | 
				
			||||||
 | 
					  background-color: unset;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.flex-middle {
 | 
					.flex-middle {
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  gap: 8px;
 | 
					  gap: 8px;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,63 @@
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { useAuthStore } from "@/stores/auth";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  DropdownMenuRoot,
 | 
				
			||||||
 | 
					  DropdownMenuTrigger,
 | 
				
			||||||
 | 
					  DropdownMenuContent,
 | 
				
			||||||
 | 
					  DropdownMenuItem,
 | 
				
			||||||
 | 
					  DropdownMenuPortal,
 | 
				
			||||||
 | 
					} from "radix-vue";
 | 
				
			||||||
 | 
					import { RouterLink } from "vue-router";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const authStore = useAuthStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function logout() {
 | 
				
			||||||
 | 
					  authStore.logout();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <DropdownMenuRoot>
 | 
				
			||||||
 | 
					    <DropdownMenuTrigger className="profile-button">
 | 
				
			||||||
 | 
					      {{ authStore.username }}
 | 
				
			||||||
 | 
					      <i class="bi bi-chevron-down" />
 | 
				
			||||||
 | 
					    </DropdownMenuTrigger>
 | 
				
			||||||
 | 
					    <DropdownMenuPortal>
 | 
				
			||||||
 | 
					      <DropdownMenuContent className="shadow">
 | 
				
			||||||
 | 
					        <DropdownMenuItem>
 | 
				
			||||||
 | 
					          <RouterLink class="button" :to="{ 'name': 'user-settings' }">
 | 
				
			||||||
 | 
					            <button>
 | 
				
			||||||
 | 
					              <i class="bi bi-gear margin" />
 | 
				
			||||||
 | 
					              Settings
 | 
				
			||||||
 | 
					            </button>
 | 
				
			||||||
 | 
					          </RouterLink>
 | 
				
			||||||
 | 
					        </DropdownMenuItem>
 | 
				
			||||||
 | 
					        <DropdownMenuItem>
 | 
				
			||||||
 | 
					          <RouterLink class="button" to="/teams">
 | 
				
			||||||
 | 
					            <button>
 | 
				
			||||||
 | 
					              <i class="bi bi-people margin" />
 | 
				
			||||||
 | 
					              Teams
 | 
				
			||||||
 | 
					            </button>
 | 
				
			||||||
 | 
					          </RouterLink>
 | 
				
			||||||
 | 
					        </DropdownMenuItem>
 | 
				
			||||||
 | 
					        <DropdownMenuItem>
 | 
				
			||||||
 | 
					          <button class="destructive" @click="logout">
 | 
				
			||||||
 | 
					            <i class="bi bi-box-arrow-right margin" />
 | 
				
			||||||
 | 
					            Log out
 | 
				
			||||||
 | 
					          </button>
 | 
				
			||||||
 | 
					        </DropdownMenuItem>
 | 
				
			||||||
 | 
					      </DropdownMenuContent>
 | 
				
			||||||
 | 
					    </DropdownMenuPortal>
 | 
				
			||||||
 | 
					  </DropdownMenuRoot>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					.profile-button {
 | 
				
			||||||
 | 
					  background-color: transparent;
 | 
				
			||||||
 | 
					  font-size: inherit;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.profile-button:hover {
 | 
				
			||||||
 | 
					  background-color: var(--surface-0);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					@ -6,6 +6,7 @@ import { createPinia } from "pinia";
 | 
				
			||||||
import VueSelect from "vue-select";
 | 
					import VueSelect from "vue-select";
 | 
				
			||||||
import { TooltipDirective } from "vue3-tooltip";
 | 
					import { TooltipDirective } from "vue3-tooltip";
 | 
				
			||||||
import "vue3-tooltip/tooltip.css";
 | 
					import "vue3-tooltip/tooltip.css";
 | 
				
			||||||
 | 
					import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import App from "./App.vue";
 | 
					import App from "./App.vue";
 | 
				
			||||||
import router from "./router";
 | 
					import router from "./router";
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,7 +16,7 @@ export const useRosterStore = defineStore("roster", () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // TODO: move roster state to a composable
 | 
					  // TODO: move roster state to a composable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const neededRoles: Reactive<Array<String>> = reactive([
 | 
					  const neededRoles  = ref([
 | 
				
			||||||
    "PocketScout",
 | 
					    "PocketScout",
 | 
				
			||||||
    "FlankScout",
 | 
					    "FlankScout",
 | 
				
			||||||
    "PocketSoldier",
 | 
					    "PocketSoldier",
 | 
				
			||||||
| 
						 | 
					@ -95,7 +95,7 @@ export const useRosterStore = defineStore("roster", () => {
 | 
				
			||||||
    "Spy": "Spy",
 | 
					    "Spy": "Spy",
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function selectPlayerForRole(player: PlayerTeamRoleFlat, role: string) {
 | 
					  function selectPlayerForRole(player: PlayerTeamRoleFlat | undefined, role: string) {
 | 
				
			||||||
    if (player && player.steamId) {
 | 
					    if (player && player.steamId) {
 | 
				
			||||||
      const existingRole = Object.keys(selectedPlayers).find((selectedRole) => {
 | 
					      const existingRole = Object.keys(selectedPlayers).find((selectedRole) => {
 | 
				
			||||||
        return selectedPlayers[selectedRole]?.steamId == player.steamId &&
 | 
					        return selectedPlayers[selectedRole]?.steamId == player.steamId &&
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -79,13 +79,10 @@ onMounted(async () => {
 | 
				
			||||||
        <span v-if="!hasAvailablePlayers && rosterStore.selectedRole">
 | 
					        <span v-if="!hasAvailablePlayers && rosterStore.selectedRole">
 | 
				
			||||||
          No players are currently available for this role.
 | 
					          No players are currently available for this role.
 | 
				
			||||||
        </span>
 | 
					        </span>
 | 
				
			||||||
        <h3 v-if="hasAvailablePlayers">Alternates</h3>
 | 
					        <h3 v-if="hasAlternates">Alternates</h3>
 | 
				
			||||||
        <PlayerCard v-for="player in rosterStore.alternateRoles"
 | 
					        <PlayerCard v-for="player in rosterStore.alternateRoles"
 | 
				
			||||||
                    :player="player"
 | 
					                    :player="player"
 | 
				
			||||||
                    :role-title="player.role" />
 | 
					                    :role-title="player.role" />
 | 
				
			||||||
        <PlayerCard v-if="rosterStore.selectedRole"
 | 
					 | 
				
			||||||
                    is-ringer
 | 
					 | 
				
			||||||
                    :role-title="rosterStore.selectedRole" />
 | 
					 | 
				
			||||||
        <div class="action-buttons">
 | 
					        <div class="action-buttons">
 | 
				
			||||||
          <button class="accent" @click="closeSelection">
 | 
					          <button class="accent" @click="closeSelection">
 | 
				
			||||||
            <i class="bi bi-check" />
 | 
					            <i class="bi bi-check" />
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -27,18 +27,24 @@ const hasAvailablePlayers = computed(() => {
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div class="column">
 | 
					      <div class="column">
 | 
				
			||||||
        <h3 v-if="hasAvailablePlayers">Available</h3>
 | 
					        <h3 v-if="hasAvailablePlayers">Available</h3>
 | 
				
			||||||
        <PlayerCard v-for="player in rosterStore.definitelyAvailableAll"
 | 
					        <PlayerCard
 | 
				
			||||||
                    :player="player"
 | 
					          v-for="player in rosterStore.definitelyAvailable"
 | 
				
			||||||
                    :role-title="player.role" />
 | 
					          :player="player"
 | 
				
			||||||
 | 
					          :role-title="player.role"
 | 
				
			||||||
 | 
					          :is-roster="false"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
        <span v-if="!hasAvailablePlayers">
 | 
					        <span v-if="!hasAvailablePlayers">
 | 
				
			||||||
          No players are currently available for this role.
 | 
					          No players are currently available for this role.
 | 
				
			||||||
        </span>
 | 
					        </span>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div class="column">
 | 
					      <div class="column">
 | 
				
			||||||
        <h3 v-if="hasAvailablePlayers">Available if needed</h3>
 | 
					        <h3 v-if="hasAvailablePlayers">Available if needed</h3>
 | 
				
			||||||
        <PlayerCard v-for="player in rosterStore.canBeAvailableAll"
 | 
					        <PlayerCard
 | 
				
			||||||
                    :player="player"
 | 
					          v-for="player in rosterStore.canBeAvailable"
 | 
				
			||||||
                    :role-title="player.role" />
 | 
					          :player="player"
 | 
				
			||||||
 | 
					          :role-title="player.role"
 | 
				
			||||||
 | 
					          :is-roster="false"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </main>
 | 
					  </main>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import DiscordIntegrationForm from "@/components/DiscordIntegrationForm.vue";
 | 
					import DiscordIntegrationForm from "@/components/DiscordIntegrationForm.vue";
 | 
				
			||||||
import IntegrationDetails from "@/components/IntegrationDetails.vue";
 | 
					import IntegrationDetails from "@/components/IntegrationDetails.vue";
 | 
				
			||||||
 | 
					import LoaderContainer from "@/components/LoaderContainer.vue";
 | 
				
			||||||
import LogsTfIntegrationForm from "@/components/LogsTfIntegrationForm.vue";
 | 
					import LogsTfIntegrationForm from "@/components/LogsTfIntegrationForm.vue";
 | 
				
			||||||
import { useTeamDetails } from "@/composables/team-details";
 | 
					import { useTeamDetails } from "@/composables/team-details";
 | 
				
			||||||
import { useTeamsStore } from "@/stores/teams";
 | 
					import { useTeamsStore } from "@/stores/teams";
 | 
				
			||||||
| 
						 | 
					@ -11,20 +12,33 @@ const teamsStore = useTeamsStore();
 | 
				
			||||||
const integrationsStore = useIntegrationsStore();
 | 
					const integrationsStore = useIntegrationsStore();
 | 
				
			||||||
const { teamId } = useTeamDetails();
 | 
					const { teamId } = useTeamDetails();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const isLoading = ref(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
//function createIntegration() {
 | 
					//function createIntegration() {
 | 
				
			||||||
//  integrationsStore.createIntegration(teamId.value, "discord");
 | 
					//  integrationsStore.createIntegration(teamId.value, "discord");
 | 
				
			||||||
//}
 | 
					//}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
onMounted(() => {
 | 
					onMounted(() => {
 | 
				
			||||||
 | 
					  isLoading.value = true;
 | 
				
			||||||
  teamsStore.fetchTeam(teamId.value)
 | 
					  teamsStore.fetchTeam(teamId.value)
 | 
				
			||||||
    .then(() => integrationsStore.getIntegrations(teamId.value));
 | 
					    .then(() => {
 | 
				
			||||||
 | 
					      integrationsStore.getIntegrations(teamId.value)
 | 
				
			||||||
 | 
					        .then(() => {
 | 
				
			||||||
 | 
					          isLoading.value = false;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div class="team-integrations">
 | 
					  <div class="team-integrations">
 | 
				
			||||||
    <DiscordIntegrationForm v-model="integrationsStore.discordIntegration" />
 | 
					    <div v-if="isLoading">
 | 
				
			||||||
    <LogsTfIntegrationForm v-model="integrationsStore.logsTfIntegration" />
 | 
					      <LoaderContainer />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <template v-else>
 | 
				
			||||||
 | 
					      <DiscordIntegrationForm v-model="integrationsStore.discordIntegration" />
 | 
				
			||||||
 | 
					      <LogsTfIntegrationForm v-model="integrationsStore.logsTfIntegration" />
 | 
				
			||||||
 | 
					    </template>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue