Redesign team page

master
John Montagu, the 4th Earl of Sandvich 2024-11-16 21:21:29 -08:00
parent b4deeddfba
commit a0fadfca94
Signed by: sandvich
GPG Key ID: 9A39BE37E602B22D
14 changed files with 356 additions and 94 deletions

View File

@ -59,6 +59,10 @@ button > i.bi.margin {
margin-right: 4px; margin-right: 4px;
} }
i.bi.margin {
margin-right: 0.5em;
}
button:hover { button:hover {
background-color: var(--surface-0); background-color: var(--surface-0);
} }
@ -121,6 +125,50 @@ h2 {
font-weight: 800; font-weight: 800;
} }
details > summary {
cursor: pointer;
list-style: none;
}
details.accordion {
padding: 16px;
background-color: var(--mantle);
border: 1px solid var(--mantle);
border-radius: 8px;
}
details.accordion[open] {
background-color: var(--base);
border-color: var(--overlay-0);
}
details.accordion > summary {
display: flex;
align-items: center;
gap: 1em;
}
details > summary::after {
content: "";
font-size: 1.5rem;
transition-duration: 200ms;
margin-left: auto;
}
details[open] > summary::after {
transform: rotate(90deg);
transition-duration: 200ms;
}
details > summary > h2 {
display: inline-block;
}
h3 {
font-size: 11pt;
font-weight: 700;
}
span.small { span.small {
font-size: 9pt; font-size: 9pt;
} }
@ -145,7 +193,7 @@ input {
display: block; display: block;
width: 100%; width: 100%;
color: var(--text); color: var(--text);
padding: 6px 9px; padding: 7px 9px;
border: none; border: none;
/*outline: 1px solid var(--overlay-0);*/ /*outline: 1px solid var(--overlay-0);*/
border: 1px solid var(--overlay-0); border: 1px solid var(--overlay-0);

View File

@ -2,9 +2,12 @@
import { type TeamInviteSchema } from "../client"; import { type TeamInviteSchema } from "../client";
import { useTeamsStore } from "../stores/teams"; import { useTeamsStore } from "../stores/teams";
import { computed, type PropType } from "vue"; import { computed, type PropType } from "vue";
import moment from "moment";
const teamsStore = useTeamsStore(); const teamsStore = useTeamsStore();
const createdAt = computed(() => moment(props.invite.createdAt).format("L LT"));
const props = defineProps({ const props = defineProps({
invite: { invite: {
type: Object as PropType<TeamInviteSchema>, type: Object as PropType<TeamInviteSchema>,
@ -37,7 +40,7 @@ function revokeInvite() {
</a> </a>
</td> </td>
<td> <td>
{{ invite.createdAt }} {{ createdAt }}
</td> </td>
<td class="buttons"> <td class="buttons">
<button @click="copyLink"> <button @click="copyLink">
@ -45,8 +48,7 @@ function revokeInvite() {
Copy Link Copy Link
</button> </button>
<button class="destructive" @click="revokeInvite"> <button class="destructive" @click="revokeInvite">
<i class="bi bi-trash margin" /> <i class="bi bi-trash" />
Revoke
</button> </button>
</td> </td>
</tr> </tr>

View File

@ -4,7 +4,6 @@ import { useRoute, useRouter, RouterLink } from "vue-router";
import { computed } from "vue"; import { computed } from "vue";
import { useTeamDetails } from "../composables/team-details"; import { useTeamDetails } from "../composables/team-details";
import PlayerTeamCard from "../components/PlayerTeamCard.vue"; import PlayerTeamCard from "../components/PlayerTeamCard.vue";
import InviteEntry from "../components/InviteEntry.vue";
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@ -17,10 +16,6 @@ const {
teamMembers, teamMembers,
} = useTeamDetails(); } = useTeamDetails();
function createInvite() {
teamsStore.createInvite(team.value.id);
}
function leaveTeam() { function leaveTeam() {
teamsStore.leaveTeam(team.value.id) teamsStore.leaveTeam(team.value.id)
.then(() => { .then(() => {
@ -34,7 +29,10 @@ function leaveTeam() {
<template> <template>
<div class="member-list-header"> <div class="member-list-header">
<h2>Members</h2> <h2>
<i class="bi bi-people-fill margin" />
Members
</h2>
<em class="aside" v-if="teamMembers"> <em class="aside" v-if="teamMembers">
{{ teamMembers?.length }} member(s), {{ teamMembers?.length }} member(s),
{{ availableMembers?.length }} currently available, {{ availableMembers?.length }} currently available,
@ -65,42 +63,6 @@ function leaveTeam() {
/> />
</tbody> </tbody>
</table> </table>
<h2>Active Invites</h2>
<div>
<details>
<summary>View all invites</summary>
<span v-if="invites?.length == 0">
There are currently no active invites to this team.
</span>
<table id="invite-table" v-else>
<thead>
<tr>
<th>
Key (hover to reveal)
</th>
<th>
Creation time
</th>
</tr>
</thead>
<tbody>
<InviteEntry
v-for="invite in invites"
:invite="invite"
/>
</tbody>
</table>
<div class="create-invite-group">
<button class="accent" @click="createInvite">
<i class="bi bi-person-fill-add margin" />
Create Invite
</button>
<span class="small aside">
Invites are usable once and expire after 24 hours.
</span>
</div>
</details>
</div>
</template> </template>
<style scoped> <style scoped>
@ -125,18 +87,6 @@ table.member-table th {
font-weight: 700; font-weight: 700;
} }
th {
text-align: left;
font-weight: 600;
padding: 8px;
}
#invite-table {
width: 100%;
border: 1px solid var(--text);
margin: 8px 0;
}
.team-details-button-group { .team-details-button-group {
flex: 1; flex: 1;
display: flex; display: flex;

View File

@ -3,14 +3,20 @@ import type { PlayerTeamRole } from "../player";
import { computed, type PropType, ref, watch } from "vue"; import { computed, type PropType, ref, watch } from "vue";
import { useTeamsStore } from "../stores/teams"; import { useTeamsStore } from "../stores/teams";
import { useRosterStore } from "../stores/roster"; import { useRosterStore } from "../stores/roster";
import { type ViewTeamMembersResponse, type TeamSchema } from "@/client"; import { type ViewTeamMembersResponse, type TeamSchema, RoleSchema } from "@/client";
import SvgIcon from "@jamescoyle/vue-icon"; import SvgIcon from "@jamescoyle/vue-icon";
import { mdiCrown } from "@mdi/js"; import { mdiCrown } from "@mdi/js";
import RoleTag from "../components/RoleTag.vue"; import RoleTag from "../components/RoleTag.vue";
const props = defineProps({ const props = defineProps({
player: Object as PropType<ViewTeamMembersResponse>, player: {
team: Object as PropType<TeamSchema>, type: Object as PropType<ViewTeamMembersResponse>,
required: true,
},
team: {
type: Object as PropType<TeamSchema>,
required: true,
},
}); });
const teamsStore = useTeamsStore(); const teamsStore = useTeamsStore();
@ -31,8 +37,8 @@ const rosterStore = useRosterStore();
const isEditing = ref(false); const isEditing = ref(false);
// this is the roles of the player we are editing // this is the roles of the player we are editing
const roles = ref([]); const roles = ref<(RoleSchema | undefined)[]>([]);
const updatedRoles = ref([]); const updatedRoles = ref<RoleSchema[]>([]);
//const rolesMap = reactive({ //const rolesMap = reactive({
// "Role.PocketScout": undefined, // "Role.PocketScout": undefined,
@ -55,17 +61,18 @@ const possibleRoles = [
watch(isEditing, (newValue) => { watch(isEditing, (newValue) => {
if (newValue) { if (newValue) {
// editing // editing
roles.value = possibleRoles.map((roleName) => { roles.value = possibleRoles
console.log(roleName); .map((roleName) => {
return props.player.roles console.log(roleName);
.find((playerRole) => playerRole.role == roleName) ?? undefined; return props.player.roles
}); .find((playerRole) => playerRole.role == roleName) ?? undefined;
});
} }
}); });
function updateRoles() { function updateRoles() {
isEditing.value = false; isEditing.value = false;
updatedRoles.value = roles.value.filter(x => x); updatedRoles.value = roles.value.filter((x): x is RoleSchema => !!x);
props.player.roles = updatedRoles.value; props.player.roles = updatedRoles.value;
console.log(roles.value); console.log(roles.value);
console.log(updatedRoles.value); console.log(updatedRoles.value);

View File

@ -1,4 +1,4 @@
import { ref } from "vue"; import { ref, watch } from "vue";
export function useTeamSettings() { export function useTeamSettings() {
const teamName = ref(""); const teamName = ref("");
@ -7,4 +7,16 @@ export function useTeamSettings() {
Intl.DateTimeFormat().resolvedOptions().timeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone ??
"Etc/UTC" "Etc/UTC"
); );
const minuteOffset = ref(0);
watch(minuteOffset, (newValue) => {
minuteOffset.value = Math.min(Math.max(0, newValue), 59);
});
return {
teamName,
timezone,
minuteOffset,
}
} }

View File

@ -6,7 +6,11 @@ import LoginView from "../views/LoginView.vue";
import TeamRegistrationView from "../views/TeamRegistrationView.vue"; import TeamRegistrationView from "../views/TeamRegistrationView.vue";
import TeamDetailsView from "../views/TeamDetailsView.vue"; import TeamDetailsView from "../views/TeamDetailsView.vue";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
import TeamDetailsMembersListView from "../views/TeamDetailsMembersListView.vue"; //import TeamDetailsMembersListView from "../views/TeamDetailsMembersListView.vue";
import TeamSettingsView from "@/views/TeamSettingsView.vue";
import TeamSettingsGeneralView from "@/views/TeamSettings/GeneralView.vue";
import TeamSettingsIntegrationsView from "@/views/TeamSettings/IntegrationsView.vue";
import TeamSettingsInvitesView from "@/views/TeamSettings/InvitesView.vue";
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
@ -40,21 +44,32 @@ const router = createRouter({
path: "/team/id/:id", path: "/team/id/:id",
name: "team-details", name: "team-details",
component: TeamDetailsView, component: TeamDetailsView,
},
{
path: "/team/id/:id/settings",
name: "team-settings",
component: TeamSettingsView,
children: [ children: [
{ {
path: "", path: "",
component: TeamDetailsMembersListView, name: "team-settings/",
component: TeamSettingsGeneralView,
}, },
{ {
path: "", path: "integrations",
component: TeamDetailsMembersListView, name: "team-settings/integrations",
component: TeamSettingsIntegrationsView,
},
{
path: "invites",
name: "team-settings/invites",
component: TeamSettingsInvitesView,
}, },
], ],
}, },
] ]
}); });
router router
.beforeEach(async (to, from) => { .beforeEach(async (to, from) => {
const authStore = useAuthStore(); const authStore = useAuthStore();

View File

@ -71,7 +71,7 @@ export const useRosterStore = defineStore("roster", () => {
.sort(comparator); .sort(comparator);
}); });
const roleIcons = reactive({ const roleIcons = reactive<{ [key: string]: string }>({
"PocketScout": "tf2-PocketScout", "PocketScout": "tf2-PocketScout",
"FlankScout": "tf2-FlankScout", "FlankScout": "tf2-FlankScout",
"PocketSoldier": "tf2-PocketSoldier", "PocketSoldier": "tf2-PocketSoldier",
@ -80,7 +80,7 @@ export const useRosterStore = defineStore("roster", () => {
"Medic": "tf2-Medic", "Medic": "tf2-Medic",
}); });
const roleNames = reactive({ const roleNames = reactive<{ [key: string]: string }>({
"PocketScout": "Pocket Scout", "PocketScout": "Pocket Scout",
"FlankScout": "Flank Scout", "FlankScout": "Flank Scout",
"PocketSoldier": "Pocket Soldier", "PocketSoldier": "Pocket Soldier",

View File

@ -73,9 +73,9 @@ export const useTeamsStore = defineStore("teams", () => {
}); });
} }
async function updateRoles(teamId: number, playerId: number, roles: RoleSchema[]) { async function updateRoles(teamId: number, playerId: string, roles: RoleSchema[]) {
return await client.default return await client.default
.editMemberRoles(teamId.toString(), playerId.toString(), { .editMemberRoles(teamId.toString(), playerId, {
roles, roles,
}); });
} }

View File

@ -3,6 +3,7 @@ import { useRoute, useRouter, RouterLink, RouterView } from "vue-router";
import { useTeamsStore } from "../stores/teams"; import { useTeamsStore } from "../stores/teams";
import { computed, onMounted, ref } from "vue"; import { computed, onMounted, ref } from "vue";
import { useTeamDetails } from "../composables/team-details"; import { useTeamDetails } from "../composables/team-details";
import MembersList from "../components/MembersList.vue";
import moment from "moment"; import moment from "moment";
const route = useRoute(); const route = useRoute();
@ -37,7 +38,7 @@ onMounted(() => {
<template> <template>
<main> <main>
<template v-if="team"> <template v-if="team">
<center class="team-info"> <center class="margin">
<h1> <h1>
{{ team.teamName }} {{ team.teamName }}
</h1> </h1>
@ -45,13 +46,18 @@ onMounted(() => {
Formed on {{ creationDate }} Formed on {{ creationDate }}
</span> </span>
</center> </center>
<RouterView /> <center class="margin">
<RouterLink :to="{ name: 'team-settings' }">
Settings
</RouterLink>
</center>
<MembersList />
</template> </template>
</main> </main>
</template> </template>
<style scoped> <style scoped>
.team-info { .margin {
margin: 4em; margin: 4em;
} }
</style> </style>

View File

@ -75,13 +75,6 @@ function createTeam() {
past the hour. past the hour.
</em> </em>
</div> </div>
<div class="form-group margin">
<h3>
Announcements Webhook URL
<span class="aside">(optional)</span>
</h3>
<input v-model="webhook" />
</div>
<div class="form-group margin"> <div class="form-group margin">
<div class="action-buttons"> <div class="action-buttons">
<button class="accent" @click="createTeam">Create team</button> <button class="accent" @click="createTeam">Create team</button>
@ -98,11 +91,6 @@ function createTeam() {
margin: auto; margin: auto;
} }
.team-registration-container h3 {
font-size: 11pt;
font-weight: 700;
}
.team-registration-container .aside { .team-registration-container .aside {
font-size: 9pt; font-size: 9pt;
} }

View File

@ -0,0 +1,58 @@
<script setup lang="ts">
import { useTeamSettings } from '@/composables/team-settings';
import timezones from "@/assets/timezones.json";
const {
teamName,
timezone,
minuteOffset,
} = useTeamSettings();
</script>
<template>
<div class="team-general-settings">
<div>
<div class="form-group margin">
<h3 class="closer">Team Name</h3>
<input v-model="teamName" />
</div>
<div class="form-group margin">
<div class="form-group row">
<div class="form-group">
<h3>
Timezone
<a
class="aside"
href="https://nodatime.org/TimeZones"
target="_blank"
>
(view all timezones)
</a>
</h3>
<v-select :options="timezones" v-model="timezone" />
</div>
<div class="form-group" id="minute-offset-group">
<h3>Minute Offset</h3>
<input type="number" v-model="minuteOffset" min="0" max="59" />
</div>
</div>
<em class="aside">
Matches will be scheduled based on {{ timezone }} at
{{ minuteOffset }}
<span v-if="minuteOffset == 1">
minute
</span>
<span v-else>
minutes
</span>
past the hour.
</em>
</div>
<div class="form-group margin">
<div class="action-buttons">
<button class="accent" @click="updateTeamSettings">Save</button>
</div>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,8 @@
<script setup lang="ts">
</script>
<template>
<div class="team-integrations">
This team currently does not have any integrations.
</div>
</template>

View File

@ -0,0 +1,71 @@
<script setup lang="ts">
import { useTeamDetails } from "@/composables/team-details";
import { useTeamsStore } from "@/stores/teams";
import { onMounted } from "vue";
import InviteEntry from "@/components/InviteEntry.vue";
const teamsStore = useTeamsStore();
const {
team,
teamId,
invites,
} = useTeamDetails();
function createInvite() {
teamsStore.createInvite(team.value.id);
}
onMounted(() => {
teamsStore.fetchTeam(teamId.value)
.then(() => teamsStore.getInvites(teamId.value));
});
</script>
<template>
<div class="invites" v-if="team">
<table id="invite-table" v-if="invites?.length > 0">
<thead>
<tr>
<th>
Key (hover to reveal)
</th>
<th>
Creation time
</th>
</tr>
</thead>
<tbody>
<InviteEntry
v-for="invite in invites"
:invite="invite"
/>
</tbody>
</table>
<div class="create-invite-group">
<button class="accent" @click="createInvite">
<i class="bi bi-person-fill-add margin" />
Create Invite
</button>
<span class="small aside">
Invites are usable once and expire after 24 hours.
</span>
</div>
</div>
</template>
<style scoped>
#invite-table {
width: 100%;
border: 1px solid var(--overlay-0);
border-radius: 8px;
margin: 8px 0;
}
#invite-table th {
text-align: left;
font-weight: 600;
padding: 8px;
}
</style>

View File

@ -0,0 +1,97 @@
<script setup lang="ts">
import { useTeamsStore } from "@/stores/teams";
import { useTeamDetails } from "@/composables/team-details";
import { onMounted } from "vue";
import { RouterLink, RouterView } from "vue-router";
const teamsStore = useTeamsStore();
const {
team,
teamId,
} = useTeamDetails();
onMounted(() => {
teamsStore.fetchTeam(teamId.value);
});
</script>
<template>
<main class="team-settings" v-if="team">
<nav class="sidebar">
<div class="categories">
<div class="back-link">
<RouterLink :to="{ name: 'team-details' }">
<i class="bi bi-arrow-left-short" />
{{ team.teamName }}
</RouterLink>
</div>
<h3>Settings</h3>
<RouterLink class="tab" :to="{ name: 'team-settings/' }">
Overview
</RouterLink>
<RouterLink class="tab" :to="{ name: 'team-settings/integrations' }">
Integrations
</RouterLink>
<RouterLink class="tab" :to="{ name: 'team-settings/invites' }">
Invites
</RouterLink>
</div>
</nav>
<div class="view">
<RouterView />
</div>
</main>
</template>
<style scoped>
.team-settings {
display: flex;
gap: 16px;
justify-content: center;
}
.team-settings nav.sidebar {
display: flex;
justify-content: end;
}
.team-settings .view {
width: 60%;
}
.back-link {
padding: 8px 16px;
}
nav.sidebar h3 {
text-transform: uppercase;
color: var(--overlay-0);
padding: 0.5em 16px;
}
nav.sidebar > .categories {
display: flex;
flex-direction: column;
width: 256px;
gap: 4px;
}
nav.sidebar a.tab {
font-size: 12pt;
color: var(--overlay-0);
padding: 8px 16px;
font-weight: 500;
border-radius: 4px;
}
nav.sidebar a.tab:hover {
background-color: var(--crust);
color: var(--text);
}
nav.sidebar a.tab.router-link-exact-active {
background-color: var(--crust);
color: var(--text);
}
</style>