Implement leaving team
parent
050a012318
commit
f5bcbb85b5
|
@ -8,8 +8,11 @@
|
||||||
"name": "availabili.tf",
|
"name": "availabili.tf",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@jamescoyle/vue-icon": "^0.1.2",
|
||||||
|
"@mdi/js": "^7.4.47",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"bootstrap-icons": "^1.11.3",
|
"bootstrap-icons": "^1.11.3",
|
||||||
|
"css.gg": "^2.1.4",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"moment-timezone": "^0.5.46",
|
"moment-timezone": "^0.5.46",
|
||||||
"pinia": "^2.2.4",
|
"pinia": "^2.2.4",
|
||||||
|
@ -838,6 +841,12 @@
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@jamescoyle/vue-icon": {
|
||||||
|
"version": "0.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jamescoyle/vue-icon/-/vue-icon-0.1.2.tgz",
|
||||||
|
"integrity": "sha512-KFrImXx5TKIi6iQXlnyLEBl4rNosNKbTeRnr70ucTdUaciVmd9qK9d/pZAaKt1Ob/8xNnX2GMp8LisyHdKtEgw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.5",
|
"version": "0.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
|
||||||
|
@ -897,6 +906,12 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@mdi/js": {
|
||||||
|
"version": "7.4.47",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.4.47.tgz",
|
||||||
|
"integrity": "sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
|
@ -2816,6 +2831,12 @@
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css.gg": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/css.gg/-/css.gg-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-7eyhXQLNJus5q3AVlYhDFjvVkB1ng1D9EjaBJzvboLfNx60RcFdZ1NinEgJMEA8bkwPwRLfbZ0ADTBXsbdrRgw==",
|
||||||
|
"license": "SEE LICENSE"
|
||||||
|
},
|
||||||
"node_modules/cssesc": {
|
"node_modules/cssesc": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||||
|
|
|
@ -15,8 +15,11 @@
|
||||||
"openapi-generate": "openapi --input 'http://localhost:8000/apidoc/openapi.json' --output src/client --name AvailabilitfClient"
|
"openapi-generate": "openapi --input 'http://localhost:8000/apidoc/openapi.json' --output src/client --name AvailabilitfClient"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@jamescoyle/vue-icon": "^0.1.2",
|
||||||
|
"@mdi/js": "^7.4.47",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"bootstrap-icons": "^1.11.3",
|
"bootstrap-icons": "^1.11.3",
|
||||||
|
"css.gg": "^2.1.4",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"moment-timezone": "^0.5.46",
|
"moment-timezone": "^0.5.46",
|
||||||
"pinia": "^2.2.4",
|
"pinia": "^2.2.4",
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
@import url("tf2icons.css");
|
@import url("tf2icons.css");
|
||||||
@import url("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css");
|
@import url("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css");
|
||||||
|
/*@import url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css");*/
|
||||||
|
@import url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/solid.min.css");
|
||||||
|
|
||||||
/* color palette from <https://github.com/vuejs/theme> */
|
/* color palette from <https://github.com/vuejs/theme> */
|
||||||
:root {
|
:root {
|
||||||
|
@ -39,9 +41,20 @@
|
||||||
|
|
||||||
--flamingo: #dd7878;
|
--flamingo: #dd7878;
|
||||||
--flamingo-transparent: #f0c6c655;
|
--flamingo-transparent: #f0c6c655;
|
||||||
--green: #40a02b;
|
|
||||||
--peach: #fe640b;
|
--peach: #fe640b;
|
||||||
|
--green: #40a02b;
|
||||||
--yellow: #df8e1d;
|
--yellow: #df8e1d;
|
||||||
|
/*
|
||||||
|
--green: #a6e3a1;
|
||||||
|
--yellow: #f9e2af;
|
||||||
|
*/
|
||||||
|
--green-transparent: color-mix(in srgb, var(--green), transparent 80%);
|
||||||
|
--yellow-transparent: color-mix(in srgb, var(--yellow), transparent 80%);
|
||||||
|
/*
|
||||||
|
--green-transparent-50: #a6e3a1;
|
||||||
|
--yellow-transparent-50: #f9e2af;
|
||||||
|
*/
|
||||||
|
|
||||||
--lavender: #7287fd;
|
--lavender: #7287fd;
|
||||||
--accent: var(--lavender);
|
--accent: var(--lavender);
|
||||||
--accent-transparent-80: color-mix(in srgb, var(--accent), transparent 80%);
|
--accent-transparent-80: color-mix(in srgb, var(--accent), transparent 80%);
|
||||||
|
|
|
@ -15,6 +15,10 @@ a,
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.button {
|
||||||
|
padding: unset;
|
||||||
|
}
|
||||||
|
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
a:hover {
|
a:hover {
|
||||||
background-color: var(--accent-transparent);
|
background-color: var(--accent-transparent);
|
||||||
|
|
|
@ -13,6 +13,7 @@ export type { OpenAPIConfig } from './core/OpenAPI';
|
||||||
export type { AddPlayerJson } from './models/AddPlayerJson';
|
export type { AddPlayerJson } from './models/AddPlayerJson';
|
||||||
export type { CreateTeamJson } from './models/CreateTeamJson';
|
export type { CreateTeamJson } from './models/CreateTeamJson';
|
||||||
export type { EditMemberRolesJson } from './models/EditMemberRolesJson';
|
export type { EditMemberRolesJson } from './models/EditMemberRolesJson';
|
||||||
|
export type { PlayerSchema } from './models/PlayerSchema';
|
||||||
export type { PutScheduleForm } from './models/PutScheduleForm';
|
export type { PutScheduleForm } from './models/PutScheduleForm';
|
||||||
export type { RoleSchema } from './models/RoleSchema';
|
export type { RoleSchema } from './models/RoleSchema';
|
||||||
export type { TeamInviteSchema } from './models/TeamInviteSchema';
|
export type { TeamInviteSchema } from './models/TeamInviteSchema';
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
/* generated using openapi-typescript-codegen -- do not edit */
|
||||||
|
/* istanbul ignore file */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export type PlayerSchema = {
|
||||||
|
steamId: string;
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
|
|
@ -4,8 +4,9 @@
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
import type { RoleSchema } from './RoleSchema';
|
import type { RoleSchema } from './RoleSchema';
|
||||||
export type ViewTeamMembersResponse = {
|
export type ViewTeamMembersResponse = {
|
||||||
availability: number;
|
availability: Array<number>;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
isTeamLeader?: boolean;
|
||||||
playtime: number;
|
playtime: number;
|
||||||
roles: Array<RoleSchema>;
|
roles: Array<RoleSchema>;
|
||||||
steamId: string;
|
steamId: string;
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
import type { AddPlayerJson } from '../models/AddPlayerJson';
|
import type { AddPlayerJson } from '../models/AddPlayerJson';
|
||||||
import type { CreateTeamJson } from '../models/CreateTeamJson';
|
import type { CreateTeamJson } from '../models/CreateTeamJson';
|
||||||
import type { EditMemberRolesJson } from '../models/EditMemberRolesJson';
|
import type { EditMemberRolesJson } from '../models/EditMemberRolesJson';
|
||||||
|
import type { PlayerSchema } from '../models/PlayerSchema';
|
||||||
import type { PutScheduleForm } from '../models/PutScheduleForm';
|
import type { PutScheduleForm } from '../models/PutScheduleForm';
|
||||||
import type { TeamInviteSchema } from '../models/TeamInviteSchema';
|
import type { TeamInviteSchema } from '../models/TeamInviteSchema';
|
||||||
import type { TeamInviteSchemaList } from '../models/TeamInviteSchemaList';
|
import type { TeamInviteSchemaList } from '../models/TeamInviteSchemaList';
|
||||||
|
@ -71,6 +72,21 @@ export class DefaultService {
|
||||||
url: '/api/login/authenticate',
|
url: '/api/login/authenticate',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* get_user <GET>
|
||||||
|
* @returns PlayerSchema OK
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public getUser(): CancelablePromise<PlayerSchema> {
|
||||||
|
return this.httpRequest.request({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/login/get-user',
|
||||||
|
errors: {
|
||||||
|
401: `Unauthorized`,
|
||||||
|
422: `Unprocessable Entity`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* get <GET>
|
* get <GET>
|
||||||
* @param windowStart
|
* @param windowStart
|
||||||
|
@ -361,6 +377,31 @@ export class DefaultService {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* remove_player_from_team <DELETE>
|
||||||
|
* @param teamId
|
||||||
|
* @param targetPlayerId
|
||||||
|
* @returns any OK
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public removePlayerFromTeam(
|
||||||
|
teamId: string,
|
||||||
|
targetPlayerId: string,
|
||||||
|
): CancelablePromise<any> {
|
||||||
|
return this.httpRequest.request({
|
||||||
|
method: 'DELETE',
|
||||||
|
url: '/api/team/id/{team_id}/player/{target_player_id}/',
|
||||||
|
path: {
|
||||||
|
'team_id': teamId,
|
||||||
|
'target_player_id': targetPlayerId,
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
403: `Forbidden`,
|
||||||
|
404: `Not Found`,
|
||||||
|
422: `Unprocessable Entity`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* view_team_members <GET>
|
* view_team_members <GET>
|
||||||
* @param teamId
|
* @param teamId
|
||||||
|
|
|
@ -272,10 +272,12 @@ onUnmounted(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-slot[selection="1"] {
|
.time-slot[selection="1"] {
|
||||||
background-color: var(--accent-transparent-50);
|
/*background-color: var(--accent-transparent-50);*/
|
||||||
|
background-color: var(--yellow-transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-slot[selection="2"] {
|
.time-slot[selection="2"] {
|
||||||
background-color: var(--accent);
|
/*background-color: var(--accent);*/
|
||||||
|
background-color: var(--green-transparent);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -4,6 +4,8 @@ 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 } from "@/client";
|
||||||
|
import SvgIcon from "@jamescoyle/vue-icon";
|
||||||
|
import { mdiCrown } from "@mdi/js";
|
||||||
import RoleTag from "../components/RoleTag.vue";
|
import RoleTag from "../components/RoleTag.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
@ -69,19 +71,34 @@ function updateRoles() {
|
||||||
console.log(updatedRoles.value);
|
console.log(updatedRoles.value);
|
||||||
teamsStore.updateRoles(props.team.id, props.player.steamId, updatedRoles.value);
|
teamsStore.updateRoles(props.team.id, props.player.steamId, updatedRoles.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isUnavailable = computed(() => {
|
||||||
|
return props.player?.availability[0] == 0 &&
|
||||||
|
props.player?.availability[1] == 0;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<tr class="player-card">
|
<tr class="player-card">
|
||||||
<td>
|
<td>
|
||||||
<div class="status flex-middle" :availability="player.availability">
|
<div
|
||||||
|
class="status flex-middle"
|
||||||
|
:is-unavailable="isUnavailable"
|
||||||
|
>
|
||||||
<div class="status-indicators">
|
<div class="status-indicators">
|
||||||
<span class="indicator left-indicator" />
|
<span
|
||||||
<span class="indicator right-indicator" />
|
class="indicator left-indicator"
|
||||||
|
:availability="player.availability[0]"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="indicator right-indicator"
|
||||||
|
:availability="player.availability[1]"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h3>
|
<h3>
|
||||||
{{ player.username }}
|
{{ player.username }}
|
||||||
</h3>
|
</h3>
|
||||||
|
<svg-icon v-if="player.isTeamLeader" type="mdi" :path="mdiCrown" />
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -164,16 +181,16 @@ function updateRoles() {
|
||||||
border-radius: 0 8px 8px 0;
|
border-radius: 0 8px 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status[availability="0"] h3 {
|
.status[is-unavailable="true"] {
|
||||||
color: var(--overlay-0);
|
color: var(--overlay-0);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status[availability="1"] .indicator {
|
.status .indicator[availability="1"] {
|
||||||
background-color: var(--yellow);
|
background-color: var(--yellow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status[availability="2"] .indicator {
|
.status .indicator[availability="2"] {
|
||||||
background-color: var(--green);
|
background-color: var(--green);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import RosterBuilderView from "../views/RosterBuilderView.vue";
|
||||||
import LoginView from "../views/LoginView.vue";
|
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";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
@ -40,6 +41,16 @@ const router = createRouter({
|
||||||
component: TeamDetailsView
|
component: TeamDetailsView
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
})
|
});
|
||||||
|
|
||||||
export default router
|
|
||||||
|
router
|
||||||
|
.beforeEach(async (to, from) => {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
console.log("test");
|
||||||
|
if (!authStore.isLoggedIn && !authStore.hasCheckedAuth) {
|
||||||
|
await authStore.getUser();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
|
|
|
@ -1,11 +1,29 @@
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
|
import { useClientStore } from "./client";
|
||||||
|
|
||||||
export const useAuthStore = defineStore("auth", () => {
|
export const useAuthStore = defineStore("auth", () => {
|
||||||
const steamId = ref(NaN);
|
const clientStore = useClientStore();
|
||||||
|
const client = clientStore.client;
|
||||||
|
|
||||||
|
const steamId = ref("");
|
||||||
const username = ref("");
|
const username = ref("");
|
||||||
const isLoggedIn = ref(false);
|
const isLoggedIn = ref(false);
|
||||||
const isRegistering = ref(false);
|
const isRegistering = ref(false);
|
||||||
|
const hasCheckedAuth = ref(false);
|
||||||
|
|
||||||
|
async function getUser() {
|
||||||
|
hasCheckedAuth.value = true;
|
||||||
|
return clientStore.call(
|
||||||
|
getUser.name,
|
||||||
|
() => client.default.getUser(),
|
||||||
|
(response) => {
|
||||||
|
steamId.value = response.steamId;
|
||||||
|
username.value = username.value;
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function login(queryParams: { [key: string]: string }) {
|
async function login(queryParams: { [key: string]: string }) {
|
||||||
return fetch(import.meta.env.VITE_API_BASE_URL + "/login/authenticate", {
|
return fetch(import.meta.env.VITE_API_BASE_URL + "/login/authenticate", {
|
||||||
|
@ -32,7 +50,9 @@ export const useAuthStore = defineStore("auth", () => {
|
||||||
steamId,
|
steamId,
|
||||||
username,
|
username,
|
||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
|
hasCheckedAuth,
|
||||||
isRegistering,
|
isRegistering,
|
||||||
|
getUser,
|
||||||
login,
|
login,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { AvailabilitfClient } from "@/client";
|
import { AvailabilitfClient, CancelablePromise } from "@/client";
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
export const useClientStore = defineStore("client", () => {
|
export const useClientStore = defineStore("client", () => {
|
||||||
|
@ -10,7 +10,7 @@ export const useClientStore = defineStore("client", () => {
|
||||||
|
|
||||||
function call<T>(
|
function call<T>(
|
||||||
key: string,
|
key: string,
|
||||||
apiCall: () => Promise<T>,
|
apiCall: () => CancelablePromise<T>,
|
||||||
thenOnce?: (result: T) => T
|
thenOnce?: (result: T) => T
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
console.log("Fetching call " + key);
|
console.log("Fetching call " + key);
|
||||||
|
|
|
@ -3,10 +3,13 @@ import { AvailabilitfClient, type TeamInviteSchema, type RoleSchema, type TeamSc
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { computed, reactive, ref, type Reactive, type Ref } from "vue";
|
import { computed, reactive, ref, type Reactive, type Ref } from "vue";
|
||||||
import { useClientStore } from "./client";
|
import { useClientStore } from "./client";
|
||||||
|
import { useAuthStore } from "./auth";
|
||||||
|
|
||||||
export type TeamMap = { [id: number]: TeamSchema };
|
export type TeamMap = { [id: number]: TeamSchema };
|
||||||
|
|
||||||
export const useTeamsStore = defineStore("teams", () => {
|
export const useTeamsStore = defineStore("teams", () => {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
const clientStore = useClientStore();
|
const clientStore = useClientStore();
|
||||||
const client = clientStore.client;
|
const client = clientStore.client;
|
||||||
|
|
||||||
|
@ -14,8 +17,6 @@ export const useTeamsStore = defineStore("teams", () => {
|
||||||
const teamMembers: Reactive<{ [id: number]: ViewTeamMembersResponse[] }> = reactive({ });
|
const teamMembers: Reactive<{ [id: number]: ViewTeamMembersResponse[] }> = reactive({ });
|
||||||
const teamInvites: Reactive<{ [id: number]: TeamInviteSchema[] }> = reactive({ });
|
const teamInvites: Reactive<{ [id: number]: TeamInviteSchema[] }> = reactive({ });
|
||||||
|
|
||||||
const isFetchingTeams = ref(false);
|
|
||||||
|
|
||||||
async function fetchTeams() {
|
async function fetchTeams() {
|
||||||
return clientStore.call(
|
return clientStore.call(
|
||||||
fetchTeams.name,
|
fetchTeams.name,
|
||||||
|
@ -116,6 +117,11 @@ export const useTeamsStore = defineStore("teams", () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function leaveTeam(teamId: number) {
|
||||||
|
return client.default
|
||||||
|
.removePlayerFromTeam(teamId.toString(), authStore.steamId);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
teams,
|
teams,
|
||||||
teamInvites,
|
teamInvites,
|
||||||
|
@ -129,5 +135,6 @@ export const useTeamsStore = defineStore("teams", () => {
|
||||||
createInvite,
|
createInvite,
|
||||||
consumeInvite,
|
consumeInvite,
|
||||||
revokeInvite,
|
revokeInvite,
|
||||||
|
leaveTeam,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -164,17 +164,27 @@ button.radio:hover {
|
||||||
|
|
||||||
button.radio.selected {
|
button.radio.selected {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
background-color: var(--accent-transparent);
|
color: var(--accent-transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
button.left {
|
button.left {
|
||||||
border-radius: 4px 0 0 4px;
|
border-radius: 4px 0 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.radio.left.selected {
|
||||||
|
color: var(--yellow);
|
||||||
|
background-color: var(--yellow-transparent);
|
||||||
|
}
|
||||||
|
|
||||||
button.right {
|
button.right {
|
||||||
border-radius: 0 4px 4px 0;
|
border-radius: 0 4px 4px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.right.radio.selected {
|
||||||
|
color: var(--green);
|
||||||
|
background-color: var(--green-transparent);
|
||||||
|
}
|
||||||
|
|
||||||
.v-select {
|
.v-select {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
|
|
@ -19,7 +19,12 @@ const invites = computed(() => {
|
||||||
|
|
||||||
const availableMembers = computed(() => {
|
const availableMembers = computed(() => {
|
||||||
return teamsStore.teamMembers[route.params.id]
|
return teamsStore.teamMembers[route.params.id]
|
||||||
.filter((member) => member.availability > 0);
|
.filter((member) => member.availability[0] > 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const availableMembersNextHour = computed(() => {
|
||||||
|
return teamsStore.teamMembers[route.params.id]
|
||||||
|
.filter((member) => member.availability[1] > 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
function createInvite() {
|
function createInvite() {
|
||||||
|
@ -30,17 +35,33 @@ function revokeInvite(key) {
|
||||||
teamsStore.revokeInvite(team.value.id, key)
|
teamsStore.revokeInvite(team.value.id, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function leaveTeam() {
|
||||||
|
teamsStore.leaveTeam(team.value.id)
|
||||||
|
.then(() => {
|
||||||
|
teamsStore.fetchTeams()
|
||||||
|
.then(() => {
|
||||||
|
router.push("/");
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
let key = route.query.key;
|
let key = route.query.key;
|
||||||
let teamId = route.params.id;
|
let teamId = route.params.id;
|
||||||
|
|
||||||
|
let doFetchTeam = () => {
|
||||||
|
teamsStore.fetchTeam(teamId)
|
||||||
|
.then(() => teamsStore.fetchTeamMembers(teamId))
|
||||||
|
.then(() => teamsStore.getInvites(teamId));
|
||||||
|
};
|
||||||
|
|
||||||
if (key) {
|
if (key) {
|
||||||
await teamsStore.consumeInvite(teamId, key);
|
teamsStore.consumeInvite(teamId, key)
|
||||||
|
.finally(doFetchTeam);
|
||||||
|
} else {
|
||||||
|
doFetchTeam();
|
||||||
}
|
}
|
||||||
|
|
||||||
teamsStore.fetchTeam(teamId)
|
|
||||||
.then(() => teamsStore.fetchTeamMembers(teamId))
|
|
||||||
.then(() => teamsStore.getInvites(teamId));
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -49,16 +70,23 @@ onMounted(async () => {
|
||||||
<template v-if="team">
|
<template v-if="team">
|
||||||
<h1>
|
<h1>
|
||||||
{{ team.teamName }}
|
{{ team.teamName }}
|
||||||
<RouterLink :to="'/schedule?teamId=' + team.id">
|
|
||||||
</RouterLink>
|
|
||||||
<em class="aside" v-if="teamsStore.teamMembers[route.params.id]">
|
<em class="aside" v-if="teamsStore.teamMembers[route.params.id]">
|
||||||
{{ teamsStore.teamMembers[route.params.id]?.length }} member(s),
|
{{ teamsStore.teamMembers[route.params.id]?.length }} member(s),
|
||||||
{{ availableMembers?.length }} currently available
|
{{ availableMembers?.length }} currently available,
|
||||||
|
{{ availableMembersNextHour?.length }} available in the next hour
|
||||||
</em>
|
</em>
|
||||||
<div class="team-details-button-group">
|
<div class="team-details-button-group">
|
||||||
<button class="accent">
|
<RouterLink class="button" :to="'/schedule?teamId=' + team.id">
|
||||||
<i class="bi bi-calendar-fill margin"></i>
|
<button class="accent">
|
||||||
View schedule
|
<i class="bi bi-calendar-fill margin"></i>
|
||||||
|
View schedule
|
||||||
|
</button>
|
||||||
|
</RouterLink>
|
||||||
|
<button
|
||||||
|
class="destructive"
|
||||||
|
@click="leaveTeam"
|
||||||
|
>
|
||||||
|
Leave
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</h1>
|
</h1>
|
||||||
|
@ -175,6 +203,7 @@ th {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-invite-group {
|
.create-invite-group {
|
||||||
|
|
|
@ -4,8 +4,10 @@ import string
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from flask import Blueprint, abort, make_response, redirect, request, url_for
|
from flask import Blueprint, abort, make_response, redirect, request, url_for
|
||||||
import requests
|
import requests
|
||||||
|
from spectree import Response
|
||||||
|
from spec import spec
|
||||||
import models
|
import models
|
||||||
from models import AuthSession, Player, db
|
from models import AuthSession, Player, PlayerSchema, db
|
||||||
from middleware import requires_authentication
|
from middleware import requires_authentication
|
||||||
|
|
||||||
api_login = Blueprint("login", __name__, url_prefix="/login")
|
api_login = Blueprint("login", __name__, url_prefix="/login")
|
||||||
|
@ -16,6 +18,21 @@ STEAM_OPENID_URL = "https://steamcommunity.com/openid/login"
|
||||||
def index():
|
def index():
|
||||||
return "test"
|
return "test"
|
||||||
|
|
||||||
|
@api_login.get("/get-user")
|
||||||
|
@requires_authentication
|
||||||
|
@spec.validate(
|
||||||
|
resp=Response(
|
||||||
|
HTTP_200=PlayerSchema,
|
||||||
|
HTTP_401=None,
|
||||||
|
),
|
||||||
|
operation_id="get_user"
|
||||||
|
)
|
||||||
|
def get_user(player: Player, auth_session: AuthSession):
|
||||||
|
return PlayerSchema(
|
||||||
|
steam_id=str(player.steam_id),
|
||||||
|
username=player.username,
|
||||||
|
).dict(by_alias=True), 200
|
||||||
|
|
||||||
@api_login.post("/authenticate")
|
@api_login.post("/authenticate")
|
||||||
def steam_authenticate():
|
def steam_authenticate():
|
||||||
params = request.get_json()
|
params = request.get_json()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from random import randint, random
|
from random import randint, random
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
@ -116,6 +116,69 @@ def delete_team(player: Player, team_id: int):
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return make_response(200)
|
return make_response(200)
|
||||||
|
|
||||||
|
@api_team.delete("/id/<team_id>/player/<target_player_id>/")
|
||||||
|
@spec.validate(
|
||||||
|
resp=Response(
|
||||||
|
HTTP_200=None,
|
||||||
|
HTTP_403=None,
|
||||||
|
HTTP_404=None,
|
||||||
|
),
|
||||||
|
operation_id="remove_player_from_team"
|
||||||
|
)
|
||||||
|
@requires_authentication
|
||||||
|
def remove_player_from_team(player: Player, team_id: int, target_player_id: int, **kwargs):
|
||||||
|
player_team = db.session.query(
|
||||||
|
PlayerTeam
|
||||||
|
).where(
|
||||||
|
PlayerTeam.team_id == team_id
|
||||||
|
).where(
|
||||||
|
PlayerTeam.player_id == player.steam_id
|
||||||
|
).one_or_none()
|
||||||
|
|
||||||
|
if not player_team:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
target_player_team = db.session.query(
|
||||||
|
PlayerTeam
|
||||||
|
).where(
|
||||||
|
PlayerTeam.team_id == team_id
|
||||||
|
).where(
|
||||||
|
PlayerTeam.player_id == target_player_id
|
||||||
|
).one_or_none()
|
||||||
|
|
||||||
|
if not target_player_team:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
is_team_leader = player_team.is_team_leader
|
||||||
|
|
||||||
|
if not is_team_leader and player_team != target_player_team:
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
team = target_player_team.team
|
||||||
|
|
||||||
|
db.session.delete(target_player_team)
|
||||||
|
db.session.refresh(team)
|
||||||
|
|
||||||
|
if len(team.players) == 0:
|
||||||
|
# delete the team if the only member
|
||||||
|
db.session.delete(team)
|
||||||
|
else:
|
||||||
|
# if there doesn't exist another team leader, promote the first player
|
||||||
|
team_leaders = db.session.query(
|
||||||
|
PlayerTeam
|
||||||
|
).where(
|
||||||
|
PlayerTeam.team_id == team_id
|
||||||
|
).where(
|
||||||
|
PlayerTeam.is_team_leader == True
|
||||||
|
).all()
|
||||||
|
|
||||||
|
if len(team_leaders) == 0:
|
||||||
|
team.players[0].is_team_leader = True
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return make_response({ }, 200)
|
||||||
|
|
||||||
class AddPlayerJson(BaseModel):
|
class AddPlayerJson(BaseModel):
|
||||||
team_role: PlayerTeam.TeamRole = PlayerTeam.TeamRole.Player
|
team_role: PlayerTeam.TeamRole = PlayerTeam.TeamRole.Player
|
||||||
is_team_leader: bool = False
|
is_team_leader: bool = False
|
||||||
|
@ -238,9 +301,10 @@ class ViewTeamMembersResponse(PlayerSchema):
|
||||||
is_main: bool
|
is_main: bool
|
||||||
|
|
||||||
roles: list[RoleSchema]
|
roles: list[RoleSchema]
|
||||||
availability: int
|
availability: list[int]
|
||||||
playtime: float
|
playtime: float
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
is_team_leader: bool = False
|
||||||
|
|
||||||
@api_team.get("/id/<team_id>/players")
|
@api_team.get("/id/<team_id>/players")
|
||||||
@spec.validate(
|
@spec.validate(
|
||||||
|
@ -254,6 +318,7 @@ class ViewTeamMembersResponse(PlayerSchema):
|
||||||
@requires_authentication
|
@requires_authentication
|
||||||
def view_team_members(player: Player, team_id: int, **kwargs):
|
def view_team_members(player: Player, team_id: int, **kwargs):
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
|
next_hour = now + timedelta(hours=1)
|
||||||
|
|
||||||
player_teams_query = db.session.query(
|
player_teams_query = db.session.query(
|
||||||
PlayerTeam
|
PlayerTeam
|
||||||
|
@ -264,7 +329,7 @@ def view_team_members(player: Player, team_id: int, **kwargs):
|
||||||
joinedload(PlayerTeam.player),
|
joinedload(PlayerTeam.player),
|
||||||
joinedload(PlayerTeam.player_roles),
|
joinedload(PlayerTeam.player_roles),
|
||||||
joinedload(PlayerTeam.availability.and_(
|
joinedload(PlayerTeam.availability.and_(
|
||||||
(PlayerTeamAvailability.start_time <= now) &
|
(PlayerTeamAvailability.start_time <= next_hour) &
|
||||||
(PlayerTeamAvailability.end_time > now)
|
(PlayerTeamAvailability.end_time > now)
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
@ -284,10 +349,13 @@ def view_team_members(player: Player, team_id: int, **kwargs):
|
||||||
roles = player_team.player_roles
|
roles = player_team.player_roles
|
||||||
player = player_team.player
|
player = player_team.player
|
||||||
|
|
||||||
availability = 0
|
availability = [0, 0]
|
||||||
if len(player_team.availability) > 0:
|
|
||||||
print(player_team.availability)
|
for record in player_team.availability:
|
||||||
availability = player_team.availability[0].availability
|
if record.start_time <= now < record.end_time:
|
||||||
|
availability[0] = record.availability
|
||||||
|
if record.start_time <= next_hour < record.end_time:
|
||||||
|
availability[1] = record.availability
|
||||||
|
|
||||||
return ViewTeamMembersResponse(
|
return ViewTeamMembersResponse(
|
||||||
username=player.username,
|
username=player.username,
|
||||||
|
@ -296,6 +364,7 @@ def view_team_members(player: Player, team_id: int, **kwargs):
|
||||||
availability=availability,
|
availability=availability,
|
||||||
playtime=player_team.playtime.total_seconds() / 3600,
|
playtime=player_team.playtime.total_seconds() / 3600,
|
||||||
created_at=player_team.created_at,
|
created_at=player_team.created_at,
|
||||||
|
is_team_leader=player_team.is_team_leader,
|
||||||
).dict(by_alias=True)
|
).dict(by_alias=True)
|
||||||
|
|
||||||
return list(map(map_to_response, player_teams))
|
return list(map(map_to_response, player_teams))
|
||||||
|
|
Loading…
Reference in New Issue