Compare commits
No commits in common. "3c83eca3802b3f53accb7b74a273d31b33df56c1" and "40641f80a389c1a8b600a5cbf1eda1570b85c905" have entirely different histories.
3c83eca380
...
40641f80a3
|
@ -11,7 +11,6 @@
|
||||||
"@jamescoyle/vue-icon": "^0.1.2",
|
"@jamescoyle/vue-icon": "^0.1.2",
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@programic/vue3-tooltip": "^1.0.0",
|
"@programic/vue3-tooltip": "^1.0.0",
|
||||||
"@vvo/tzdb": "^6.152.0",
|
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"bootstrap-icons": "^1.11.3",
|
"bootstrap-icons": "^1.11.3",
|
||||||
"css.gg": "^2.1.4",
|
"css.gg": "^2.1.4",
|
||||||
|
@ -2122,12 +2121,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vvo/tzdb": {
|
|
||||||
"version": "6.152.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@vvo/tzdb/-/tzdb-6.152.0.tgz",
|
|
||||||
"integrity": "sha512-PSHIgDk6LjYTyAK7fPLZIliB1vSQg2OXxfkAkRJzUkwuR/Xp5FzmQNx9SmHVZhw/W/Y1x6TE6yO89PFPossswQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/abbrev": {
|
"node_modules/abbrev": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
"@jamescoyle/vue-icon": "^0.1.2",
|
"@jamescoyle/vue-icon": "^0.1.2",
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@programic/vue3-tooltip": "^1.0.0",
|
"@programic/vue3-tooltip": "^1.0.0",
|
||||||
"@vvo/tzdb": "^6.152.0",
|
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"bootstrap-icons": "^1.11.3",
|
"bootstrap-icons": "^1.11.3",
|
||||||
"css.gg": "^2.1.4",
|
"css.gg": "^2.1.4",
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<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;
|
||||||
|
|
||||||
|
@ -12,23 +11,12 @@ const authStore = useAuthStore();
|
||||||
<header>
|
<header>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<nav>
|
<nav>
|
||||||
<h1>
|
<h1>availabili.tf</h1>
|
||||||
<RouterLink class="header-link" to="/">availabili.tf</RouterLink>
|
<RouterLink to="/">Home</RouterLink>
|
||||||
</h1>
|
<RouterLink to="/schedule">Schedule</RouterLink>
|
||||||
<div class="nav-links">
|
<div v-if="authStore.isLoggedIn">
|
||||||
<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 }}
|
Welcome {{ authStore.username }}
|
||||||
</button-->
|
</div>
|
||||||
<form
|
<form
|
||||||
v-else
|
v-else
|
||||||
action="https://steamcommunity.com/openid/login"
|
action="https://steamcommunity.com/openid/login"
|
||||||
|
@ -41,12 +29,8 @@ const authStore = useAuthStore();
|
||||||
<input type="hidden" name="openid.ns" value="http://specs.openid.net/auth/2.0" />
|
<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.mode" value="checkid_setup" />
|
||||||
<input type="hidden" name="openid.return_to" :value="baseUrl + '/login'" />
|
<input type="hidden" name="openid.return_to" :value="baseUrl + '/login'" />
|
||||||
<!--button type="submit">Log in through Steam</button-->
|
<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>
|
</form>
|
||||||
</div>
|
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
@ -62,10 +46,6 @@ 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;
|
||||||
|
@ -73,36 +53,28 @@ a.header-link {
|
||||||
|
|
||||||
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(--text);
|
color: var(--crust);
|
||||||
|
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 {
|
||||||
background-color: transparent;
|
color: var(--accent);
|
||||||
|
background-color: var(--accent-transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
nav > h1 {
|
nav > h1 {
|
||||||
|
@ -110,12 +82,6 @@ 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;
|
||||||
|
@ -136,6 +102,7 @@ button.sign-in-button {
|
||||||
|
|
||||||
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="listbox"] {
|
[role="menu"] {
|
||||||
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,16 +279,6 @@ 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%;
|
||||||
|
@ -307,6 +297,17 @@ 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,53 +1,91 @@
|
||||||
<script setup lang="ts" generic="T extends AcceptableValue">
|
<script setup lang="ts">
|
||||||
import { defineModel, defineProps, ref } from "vue";
|
import { computed, 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 selectedValue = defineModel<T>();
|
const model = defineModel();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
options: Array<String>,
|
||||||
|
isDisabled: Boolean,
|
||||||
|
});
|
||||||
|
|
||||||
const isOpen = ref(false);
|
const isOpen = ref(false);
|
||||||
|
const selectedOption = computed(() => props.options[model.value]);
|
||||||
|
|
||||||
withDefaults(defineProps<{
|
function selectOption(index) {
|
||||||
values: T[];
|
model.value = index;
|
||||||
//mapper? (value: T): string;
|
isOpen.value = false;
|
||||||
//keyMapper? (value: T): string;
|
}
|
||||||
display: string;
|
|
||||||
keyField: string;
|
|
||||||
}>(), {
|
|
||||||
//mapper: (value: T) => value.toString(),
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ComboboxRoot
|
<div :class="{ 'dropdown-container': true, 'is-open': isOpen }">
|
||||||
v-model="selectedValue"
|
<button @click="isOpen = !isOpen" :disabled="isDisabled">
|
||||||
v-model:open="isOpen"
|
{{ selectedOption }}
|
||||||
defaultOpen
|
<i class="bi bi-caret-down-fill"></i>
|
||||||
>
|
</button>
|
||||||
<ComboboxAnchor>
|
<ul class="dropdown" v-if="isOpen" @blur="isOpen = false">
|
||||||
<ComboboxInput />
|
<li v-for="(option, i) in options" :key="i" @click="selectOption(i)">
|
||||||
<ComboboxTrigger>
|
<button :class="{ 'is-selected': i == model }">
|
||||||
hi
|
{{ option }}
|
||||||
</ComboboxTrigger>
|
</button>
|
||||||
</ComboboxAnchor>
|
</li>
|
||||||
|
</ul>
|
||||||
<ComboboxPortal>
|
</div>
|
||||||
<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 Auto-Tracking</h2>
|
<h2>logs.tf Integration</h2>
|
||||||
<p>Automatically fetch and track match history from logs.tf.</p>
|
<p>Automatically 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,12 +5,11 @@ import { useRosterStore } from "../stores/roster";
|
||||||
|
|
||||||
const rosterStore = useRosterStore();
|
const rosterStore = useRosterStore();
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = defineProps({
|
||||||
roleTitle: string,
|
roleTitle: String,
|
||||||
player: PlayerTeamRoleFlat | undefined,
|
player: Object as PropType<PlayerTeamRoleFlat>,
|
||||||
isRoster: boolean,
|
isRoster: Boolean,
|
||||||
}>(), {
|
isRinger: Boolean,
|
||||||
isRoster: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const isSelected = computed(() => {
|
const isSelected = computed(() => {
|
||||||
|
@ -18,9 +17,11 @@ const isSelected = computed(() => {
|
||||||
return rosterStore.selectedRole == props.roleTitle;
|
return rosterStore.selectedRole == props.roleTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedPlayers = rosterStore.selectedPlayers;
|
if (props.isRinger) {
|
||||||
|
return rosterStore.selectedPlayers[props.roleTitle]?.playtime == -1;
|
||||||
|
}
|
||||||
|
|
||||||
return selectedPlayers[props.roleTitle]?.steamId == props.player?.steamId;
|
return Object.values(rosterStore.selectedPlayers).includes(props.player);
|
||||||
});
|
});
|
||||||
|
|
||||||
function onClick() {
|
function onClick() {
|
||||||
|
@ -34,14 +35,26 @@ function onClick() {
|
||||||
// we are selecting the player
|
// we are selecting the player
|
||||||
if (isSelected.value) {
|
if (isSelected.value) {
|
||||||
rosterStore.selectPlayerForRole(undefined, props.roleTitle);
|
rosterStore.selectPlayerForRole(undefined, props.roleTitle);
|
||||||
|
} else {
|
||||||
|
if (props.isRinger) {
|
||||||
|
const ringerPlayer: PlayerTeamRoleFlat = {
|
||||||
|
steamId: "0",
|
||||||
|
name: "Ringer",
|
||||||
|
role: props.roleTitle ?? "",
|
||||||
|
isMain: false,
|
||||||
|
availability: 1,
|
||||||
|
playtime: -1,
|
||||||
|
};
|
||||||
|
rosterStore.selectPlayerForRole(ringerPlayer, props.roleTitle);
|
||||||
} else {
|
} else {
|
||||||
rosterStore.selectPlayerForRole(props.player, props.roleTitle);
|
rosterStore.selectPlayerForRole(props.player, props.roleTitle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const playtime = computed(() => {
|
const playtime = computed(() => {
|
||||||
let hours = props.player?.playtime ?? 0 / 3600;
|
let hours = props.player?.playtime / 3600 ?? 0;
|
||||||
return hours.toFixed(1);
|
return hours.toFixed(1);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -49,7 +62,7 @@ const playtime = computed(() => {
|
||||||
<template>
|
<template>
|
||||||
<button :class="{
|
<button :class="{
|
||||||
'player-card': true,
|
'player-card': true,
|
||||||
'no-player': !player,
|
'no-player': !player && !isRinger,
|
||||||
'selected': isSelected,
|
'selected': isSelected,
|
||||||
'can-be-available': player?.availability == 1
|
'can-be-available': player?.availability == 1
|
||||||
}" @click="onClick">
|
}" @click="onClick">
|
||||||
|
@ -66,12 +79,21 @@ const playtime = computed(() => {
|
||||||
(alternate)
|
(alternate)
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="Number(playtime) > 0">
|
<span v-if="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,14 +131,9 @@ const rightIndicator = computed(() => {
|
||||||
:availability="player.availability[1]"
|
:availability="player.availability[1]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<a
|
|
||||||
class="player-name"
|
|
||||||
:href="`https://steamcommunity.com/profiles/${player.steamId}`"
|
|
||||||
>
|
|
||||||
<h3>
|
<h3>
|
||||||
{{ player.username }}
|
{{ player.username }}
|
||||||
</h3>
|
</h3>
|
||||||
</a>
|
|
||||||
<svg-icon
|
<svg-icon
|
||||||
v-if="player.isTeamLeader"
|
v-if="player.isTeamLeader"
|
||||||
:class="[
|
:class="[
|
||||||
|
@ -261,14 +256,6 @@ 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;
|
||||||
|
|
|
@ -1,63 +0,0 @@
|
||||||
<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,7 +6,6 @@ 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";
|
||||||
|
|
|
@ -11,7 +11,6 @@ import TeamSettingsView from "@/views/TeamSettingsView.vue";
|
||||||
import TeamSettingsGeneralView from "@/views/TeamSettings/GeneralView.vue";
|
import TeamSettingsGeneralView from "@/views/TeamSettings/GeneralView.vue";
|
||||||
import TeamSettingsIntegrationsView from "@/views/TeamSettings/IntegrationsView.vue";
|
import TeamSettingsIntegrationsView from "@/views/TeamSettings/IntegrationsView.vue";
|
||||||
import TeamSettingsInvitesView from "@/views/TeamSettings/InvitesView.vue";
|
import TeamSettingsInvitesView from "@/views/TeamSettings/InvitesView.vue";
|
||||||
import UserSettingsView from "@/views/UserSettingsView.vue";
|
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
@ -73,11 +72,6 @@ const router = createRouter({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "/settings",
|
|
||||||
name: "user-settings",
|
|
||||||
component: UserSettingsView,
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { useClientStore } from "./client";
|
import { useClientStore } from "./client";
|
||||||
import { useRouter, type LocationQuery } from "vue-router";
|
import type { LocationQuery } from "vue-router";
|
||||||
|
|
||||||
export const useAuthStore = defineStore("auth", () => {
|
export const useAuthStore = defineStore("auth", () => {
|
||||||
const clientStore = useClientStore();
|
const clientStore = useClientStore();
|
||||||
|
@ -13,8 +13,6 @@ export const useAuthStore = defineStore("auth", () => {
|
||||||
const isRegistering = ref(false);
|
const isRegistering = ref(false);
|
||||||
const hasCheckedAuth = ref(false);
|
const hasCheckedAuth = ref(false);
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
async function getUser() {
|
async function getUser() {
|
||||||
hasCheckedAuth.value = true;
|
hasCheckedAuth.value = true;
|
||||||
return clientStore.call(
|
return clientStore.call(
|
||||||
|
@ -50,16 +48,8 @@ export const useAuthStore = defineStore("auth", () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logout() {
|
async function setUsername(username: string) {
|
||||||
return client.default.deleteApiLogin()
|
return client.default.setUsername({ username });
|
||||||
.then(() => router.push("/"));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setUsername(name: string) {
|
|
||||||
return client.default.setUsername({ username: name })
|
|
||||||
.then((response) => {
|
|
||||||
username.value = response.username;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -70,7 +60,6 @@ export const useAuthStore = defineStore("auth", () => {
|
||||||
isRegistering,
|
isRegistering,
|
||||||
getUser,
|
getUser,
|
||||||
login,
|
login,
|
||||||
logout,
|
|
||||||
setUsername,
|
setUsername,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 = ref([
|
const neededRoles: Reactive<Array<String>> = reactive([
|
||||||
"PocketScout",
|
"PocketScout",
|
||||||
"FlankScout",
|
"FlankScout",
|
||||||
"PocketSoldier",
|
"PocketSoldier",
|
||||||
|
@ -95,7 +95,7 @@ export const useRosterStore = defineStore("roster", () => {
|
||||||
"Spy": "Spy",
|
"Spy": "Spy",
|
||||||
});
|
});
|
||||||
|
|
||||||
function selectPlayerForRole(player: PlayerTeamRoleFlat | undefined, role: string) {
|
function selectPlayerForRole(player: PlayerTeamRoleFlat, 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,10 +79,13 @@ 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="hasAlternates">Alternates</h3>
|
<h3 v-if="hasAvailablePlayers">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,24 +27,18 @@ const hasAvailablePlayers = computed(() => {
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<h3 v-if="hasAvailablePlayers">Available</h3>
|
<h3 v-if="hasAvailablePlayers">Available</h3>
|
||||||
<PlayerCard
|
<PlayerCard v-for="player in rosterStore.definitelyAvailableAll"
|
||||||
v-for="player in rosterStore.definitelyAvailable"
|
|
||||||
:player="player"
|
:player="player"
|
||||||
:role-title="player.role"
|
: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
|
<PlayerCard v-for="player in rosterStore.canBeAvailableAll"
|
||||||
v-for="player in rosterStore.canBeAvailable"
|
|
||||||
:player="player"
|
:player="player"
|
||||||
:role-title="player.role"
|
:role-title="player.role" />
|
||||||
:is-roster="false"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
@ -1,19 +1,8 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, watch } from "vue";
|
import { ref, watch } from "vue";
|
||||||
import { useTeamsStore } from "../stores/teams";
|
import { useTeamsStore } from "../stores/teams";
|
||||||
//import timezones from "../assets/timezones.json";
|
import timezones from "../assets/timezones.json";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import ComboBox from "../components/ComboBox.vue";
|
|
||||||
import { getTimeZones, type TimeZone } from "@vvo/tzdb";
|
|
||||||
import moment from "moment";
|
|
||||||
|
|
||||||
const timezones = getTimeZones({
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(timezones.length);
|
|
||||||
|
|
||||||
console.log(moment.tz.names());
|
|
||||||
|
|
||||||
const teams = useTeamsStore();
|
const teams = useTeamsStore();
|
||||||
|
|
||||||
|
@ -21,9 +10,10 @@ const router = useRouter();
|
||||||
|
|
||||||
const teamName = ref("");
|
const teamName = ref("");
|
||||||
|
|
||||||
const timezone = ref<TimeZone>(timezones.find((tz) => tz.name === "America/New_York")!);
|
const timezone = ref(
|
||||||
|
Intl.DateTimeFormat().resolvedOptions().timeZone ??
|
||||||
const timezoneStr = ref("");
|
"Etc/UTC"
|
||||||
|
);
|
||||||
|
|
||||||
const minuteOffset = ref(0);
|
const minuteOffset = ref(0);
|
||||||
|
|
||||||
|
@ -31,8 +21,10 @@ watch(minuteOffset, (newValue) => {
|
||||||
minuteOffset.value = Math.min(Math.max(0, newValue), 59);
|
minuteOffset.value = Math.min(Math.max(0, newValue), 59);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const webhook = ref("");
|
||||||
|
|
||||||
function createTeam() {
|
function createTeam() {
|
||||||
teams.createTeam(teamName.value, timezone.value.name, minuteOffset.value)
|
teams.createTeam(teamName.value, timezone.value, minuteOffset.value)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
router.push("/");
|
router.push("/");
|
||||||
});
|
});
|
||||||
|
@ -64,7 +56,7 @@ function createTeam() {
|
||||||
(view all timezones)
|
(view all timezones)
|
||||||
</a>
|
</a>
|
||||||
</h3>
|
</h3>
|
||||||
<v-select :options="timezones" label="name" v-model="timezone" />
|
<v-select :options="timezones" v-model="timezone" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" id="minute-offset-group">
|
<div class="form-group" id="minute-offset-group">
|
||||||
<h3>Minute Offset</h3>
|
<h3>Minute Offset</h3>
|
||||||
|
@ -72,7 +64,7 @@ function createTeam() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<em class="aside">
|
<em class="aside">
|
||||||
Matches will be scheduled based on {{ timezone.alternativeName }} at
|
Matches will be scheduled based on {{ timezone }} at
|
||||||
{{ minuteOffset }}
|
{{ minuteOffset }}
|
||||||
<span v-if="minuteOffset == 1">
|
<span v-if="minuteOffset == 1">
|
||||||
minute
|
minute
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<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";
|
||||||
|
@ -12,33 +11,20 @@ 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(() => {
|
.then(() => integrationsStore.getIntegrations(teamId.value));
|
||||||
integrationsStore.getIntegrations(teamId.value)
|
|
||||||
.then(() => {
|
|
||||||
isLoading.value = false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="team-integrations">
|
<div class="team-integrations">
|
||||||
<div v-if="isLoading">
|
|
||||||
<LoaderContainer />
|
|
||||||
</div>
|
|
||||||
<template v-else>
|
|
||||||
<DiscordIntegrationForm v-model="integrationsStore.discordIntegration" />
|
<DiscordIntegrationForm v-model="integrationsStore.discordIntegration" />
|
||||||
<LogsTfIntegrationForm v-model="integrationsStore.logsTfIntegration" />
|
<LogsTfIntegrationForm v-model="integrationsStore.logsTfIntegration" />
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { useAuthStore } from "@/stores/auth";
|
|
||||||
import { onMounted, ref } from "vue";
|
|
||||||
|
|
||||||
const displayName = ref("");
|
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
|
||||||
|
|
||||||
function save() {
|
|
||||||
authStore.setUsername(displayName.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
displayName.value = authStore.username;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<main>
|
|
||||||
<div class="user-settings-container">
|
|
||||||
<h1>User Settings</h1>
|
|
||||||
<div class="form-group margin">
|
|
||||||
<h3>
|
|
||||||
Display Name
|
|
||||||
</h3>
|
|
||||||
<input v-model="displayName" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group margin">
|
|
||||||
<div class="action-buttons">
|
|
||||||
<button class="accent" @click="save">Save</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.user-settings-container {
|
|
||||||
align-items: center;
|
|
||||||
max-width: 500px;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -292,9 +292,6 @@ def update_event(player: Player, event_id: int, json: UpdateEventJson, **_):
|
||||||
else:
|
else:
|
||||||
player_event.role = None
|
player_event.role = None
|
||||||
|
|
||||||
event.name = json.name
|
|
||||||
event.description = json.description
|
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
event.update_discord_message()
|
event.update_discord_message()
|
||||||
|
|
|
@ -23,7 +23,7 @@ class Event(app_db.BaseModel):
|
||||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
start_time: Mapped[datetime] = mapped_column(UtcDateTime, nullable=False)
|
start_time: Mapped[datetime] = mapped_column(UtcDateTime, nullable=False)
|
||||||
|
|
||||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
description: Mapped[str] = mapped_column(Text, nullable=True)
|
||||||
created_at: Mapped[datetime] = mapped_column(TIMESTAMP, server_default=func.now())
|
created_at: Mapped[datetime] = mapped_column(TIMESTAMP, server_default=func.now())
|
||||||
discord_message_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
|
discord_message_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue