Compare commits

...

4 Commits

Author SHA1 Message Date
John Montagu, the 4th Earl of Sandvich 3c83eca380
feat: Implement user settings 2024-12-07 17:33:22 -08:00
John Montagu, the 4th Earl of Sandvich ad45e1530e
fix(backend): Ensure event details are updated
Fixed an issue where event name and description were not being updated
correctly from the provided JSON data in the update_event function.
2024-12-07 17:25:09 -08:00
John Montagu, the 4th Earl of Sandvich 42b7e603f0
Improve user experience quality of life 2024-12-07 17:21:57 -08:00
John Montagu, the 4th Earl of Sandvich d3abf67d88
Use tzdb timezones library 2024-12-07 17:18:44 -08:00
20 changed files with 332 additions and 187 deletions

View File

@ -11,6 +11,7 @@
"@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",
@ -2121,6 +2122,12 @@
} }
} }
}, },
"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",

View File

@ -18,6 +18,7 @@
"@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",

View File

@ -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;

View File

@ -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);

View File

@ -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>

View File

@ -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>

View File

@ -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] }}

View File

@ -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;

View File

@ -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>

View File

@ -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";

View File

@ -11,6 +11,7 @@ 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),
@ -72,6 +73,11 @@ const router = createRouter({
}, },
], ],
}, },
{
path: "/settings",
name: "user-settings",
component: UserSettingsView,
},
] ]
}); });

View File

@ -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 type { LocationQuery } from "vue-router"; import { useRouter, type LocationQuery } from "vue-router";
export const useAuthStore = defineStore("auth", () => { export const useAuthStore = defineStore("auth", () => {
const clientStore = useClientStore(); const clientStore = useClientStore();
@ -13,6 +13,8 @@ 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(
@ -48,8 +50,16 @@ export const useAuthStore = defineStore("auth", () => {
}); });
} }
async function setUsername(username: string) { async function logout() {
return client.default.setUsername({ username }); return client.default.deleteApiLogin()
.then(() => router.push("/"));
}
async function setUsername(name: string) {
return client.default.setUsername({ username: name })
.then((response) => {
username.value = response.username;
});
} }
return { return {
@ -60,6 +70,7 @@ export const useAuthStore = defineStore("auth", () => {
isRegistering, isRegistering,
getUser, getUser,
login, login,
logout,
setUsername, setUsername,
} }
}); });

View File

@ -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 &&

View File

@ -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" />

View File

@ -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>

View File

@ -1,8 +1,19 @@
<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();
@ -10,10 +21,9 @@ const router = useRouter();
const teamName = ref(""); const teamName = ref("");
const timezone = ref( const timezone = ref<TimeZone>(timezones.find((tz) => tz.name === "America/New_York")!);
Intl.DateTimeFormat().resolvedOptions().timeZone ??
"Etc/UTC" const timezoneStr = ref("");
);
const minuteOffset = ref(0); const minuteOffset = ref(0);
@ -21,10 +31,8 @@ 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, minuteOffset.value) teams.createTeam(teamName.value, timezone.value.name, minuteOffset.value)
.then(() => { .then(() => {
router.push("/"); router.push("/");
}); });
@ -56,7 +64,7 @@ function createTeam() {
(view all timezones) (view all timezones)
</a> </a>
</h3> </h3>
<v-select :options="timezones" v-model="timezone" /> <v-select :options="timezones" label="name" 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>
@ -64,7 +72,7 @@ function createTeam() {
</div> </div>
</div> </div>
<em class="aside"> <em class="aside">
Matches will be scheduled based on {{ timezone }} at Matches will be scheduled based on {{ timezone.alternativeName }} at
{{ minuteOffset }} {{ minuteOffset }}
<span v-if="minuteOffset == 1"> <span v-if="minuteOffset == 1">
minute minute

View File

@ -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>

View File

@ -0,0 +1,43 @@
<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>

View File

@ -292,6 +292,9 @@ 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()

View File

@ -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] = mapped_column(Text, nullable=True) description: Mapped[str | None] = 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)