Implement leaving team
							parent
							
								
									050a012318
								
							
						
					
					
						commit
						f5bcbb85b5
					
				| 
						 | 
				
			
			@ -8,8 +8,11 @@
 | 
			
		|||
      "name": "availabili.tf",
 | 
			
		||||
      "version": "0.0.0",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@jamescoyle/vue-icon": "^0.1.2",
 | 
			
		||||
        "@mdi/js": "^7.4.47",
 | 
			
		||||
        "axios": "^1.7.7",
 | 
			
		||||
        "bootstrap-icons": "^1.11.3",
 | 
			
		||||
        "css.gg": "^2.1.4",
 | 
			
		||||
        "moment": "^2.30.1",
 | 
			
		||||
        "moment-timezone": "^0.5.46",
 | 
			
		||||
        "pinia": "^2.2.4",
 | 
			
		||||
| 
						 | 
				
			
			@ -838,6 +841,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": {
 | 
			
		||||
      "version": "0.3.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -897,6 +906,12 @@
 | 
			
		|||
      "dev": true,
 | 
			
		||||
      "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": {
 | 
			
		||||
      "version": "2.1.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -2816,6 +2831,12 @@
 | 
			
		|||
        "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": {
 | 
			
		||||
      "version": "3.0.0",
 | 
			
		||||
      "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"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@jamescoyle/vue-icon": "^0.1.2",
 | 
			
		||||
    "@mdi/js": "^7.4.47",
 | 
			
		||||
    "axios": "^1.7.7",
 | 
			
		||||
    "bootstrap-icons": "^1.11.3",
 | 
			
		||||
    "css.gg": "^2.1.4",
 | 
			
		||||
    "moment": "^2.30.1",
 | 
			
		||||
    "moment-timezone": "^0.5.46",
 | 
			
		||||
    "pinia": "^2.2.4",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,7 @@
 | 
			
		|||
@import url("tf2icons.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> */
 | 
			
		||||
:root {
 | 
			
		||||
| 
						 | 
				
			
			@ -39,9 +41,20 @@
 | 
			
		|||
 | 
			
		||||
  --flamingo: #dd7878;
 | 
			
		||||
  --flamingo-transparent: #f0c6c655;
 | 
			
		||||
  --green: #40a02b;
 | 
			
		||||
  --peach: #fe640b;
 | 
			
		||||
  --green: #40a02b;
 | 
			
		||||
  --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;
 | 
			
		||||
  --accent: var(--lavender);
 | 
			
		||||
  --accent-transparent-80: color-mix(in srgb, var(--accent), transparent 80%);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,6 +15,10 @@ a,
 | 
			
		|||
  padding: 3px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
a.button {
 | 
			
		||||
  padding: unset;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (hover: hover) {
 | 
			
		||||
  a:hover {
 | 
			
		||||
    background-color: var(--accent-transparent);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,6 +13,7 @@ export type { OpenAPIConfig } from './core/OpenAPI';
 | 
			
		|||
export type { AddPlayerJson } from './models/AddPlayerJson';
 | 
			
		||||
export type { CreateTeamJson } from './models/CreateTeamJson';
 | 
			
		||||
export type { EditMemberRolesJson } from './models/EditMemberRolesJson';
 | 
			
		||||
export type { PlayerSchema } from './models/PlayerSchema';
 | 
			
		||||
export type { PutScheduleForm } from './models/PutScheduleForm';
 | 
			
		||||
export type { RoleSchema } from './models/RoleSchema';
 | 
			
		||||
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 */
 | 
			
		||||
import type { RoleSchema } from './RoleSchema';
 | 
			
		||||
export type ViewTeamMembersResponse = {
 | 
			
		||||
    availability: number;
 | 
			
		||||
    availability: Array<number>;
 | 
			
		||||
    createdAt: string;
 | 
			
		||||
    isTeamLeader?: boolean;
 | 
			
		||||
    playtime: number;
 | 
			
		||||
    roles: Array<RoleSchema>;
 | 
			
		||||
    steamId: string;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,7 @@
 | 
			
		|||
import type { AddPlayerJson } from '../models/AddPlayerJson';
 | 
			
		||||
import type { CreateTeamJson } from '../models/CreateTeamJson';
 | 
			
		||||
import type { EditMemberRolesJson } from '../models/EditMemberRolesJson';
 | 
			
		||||
import type { PlayerSchema } from '../models/PlayerSchema';
 | 
			
		||||
import type { PutScheduleForm } from '../models/PutScheduleForm';
 | 
			
		||||
import type { TeamInviteSchema } from '../models/TeamInviteSchema';
 | 
			
		||||
import type { TeamInviteSchemaList } from '../models/TeamInviteSchemaList';
 | 
			
		||||
| 
						 | 
				
			
			@ -71,6 +72,21 @@ export class DefaultService {
 | 
			
		|||
            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>
 | 
			
		||||
     * @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>
 | 
			
		||||
     * @param teamId
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -272,10 +272,12 @@ onUnmounted(() => {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
.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"] {
 | 
			
		||||
  background-color: var(--accent);
 | 
			
		||||
  /*background-color: var(--accent);*/
 | 
			
		||||
  background-color: var(--green-transparent);
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,6 +4,8 @@ import { computed, type PropType, ref, watch } from "vue";
 | 
			
		|||
import { useTeamsStore } from "../stores/teams";
 | 
			
		||||
import { useRosterStore } from "../stores/roster";
 | 
			
		||||
import { type ViewTeamMembersResponse, type TeamSchema } from "@/client";
 | 
			
		||||
import SvgIcon from "@jamescoyle/vue-icon";
 | 
			
		||||
import { mdiCrown } from "@mdi/js";
 | 
			
		||||
import RoleTag from "../components/RoleTag.vue";
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
| 
						 | 
				
			
			@ -69,19 +71,34 @@ function updateRoles() {
 | 
			
		|||
  console.log(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>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <tr class="player-card">
 | 
			
		||||
    <td>
 | 
			
		||||
      <div class="status flex-middle" :availability="player.availability">
 | 
			
		||||
      <div
 | 
			
		||||
        class="status flex-middle"
 | 
			
		||||
        :is-unavailable="isUnavailable"
 | 
			
		||||
      >
 | 
			
		||||
        <div class="status-indicators">
 | 
			
		||||
          <span class="indicator left-indicator" />
 | 
			
		||||
          <span class="indicator right-indicator" />
 | 
			
		||||
          <span
 | 
			
		||||
            class="indicator left-indicator"
 | 
			
		||||
            :availability="player.availability[0]"
 | 
			
		||||
          />
 | 
			
		||||
          <span
 | 
			
		||||
            class="indicator right-indicator"
 | 
			
		||||
            :availability="player.availability[1]"
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
        <h3>
 | 
			
		||||
          {{ player.username }}
 | 
			
		||||
        </h3>
 | 
			
		||||
        <svg-icon v-if="player.isTeamLeader" type="mdi" :path="mdiCrown" />
 | 
			
		||||
      </div>
 | 
			
		||||
    </td>
 | 
			
		||||
    <td>
 | 
			
		||||
| 
						 | 
				
			
			@ -164,16 +181,16 @@ function updateRoles() {
 | 
			
		|||
  border-radius: 0 8px 8px 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.status[availability="0"] h3 {
 | 
			
		||||
.status[is-unavailable="true"] {
 | 
			
		||||
  color: var(--overlay-0);
 | 
			
		||||
  font-weight: 400;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.status[availability="1"] .indicator {
 | 
			
		||||
.status .indicator[availability="1"] {
 | 
			
		||||
  background-color: var(--yellow);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.status[availability="2"] .indicator {
 | 
			
		||||
.status .indicator[availability="2"] {
 | 
			
		||||
  background-color: var(--green);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,7 @@ import RosterBuilderView from "../views/RosterBuilderView.vue";
 | 
			
		|||
import LoginView from "../views/LoginView.vue";
 | 
			
		||||
import TeamRegistrationView from "../views/TeamRegistrationView.vue";
 | 
			
		||||
import TeamDetailsView from "../views/TeamDetailsView.vue";
 | 
			
		||||
import { useAuthStore } from "@/stores/auth";
 | 
			
		||||
 | 
			
		||||
const router = createRouter({
 | 
			
		||||
  history: createWebHistory(import.meta.env.BASE_URL),
 | 
			
		||||
| 
						 | 
				
			
			@ -40,6 +41,16 @@ const router = createRouter({
 | 
			
		|||
      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 { ref } from "vue";
 | 
			
		||||
import { useClientStore } from "./client";
 | 
			
		||||
 | 
			
		||||
export const useAuthStore = defineStore("auth", () => {
 | 
			
		||||
  const steamId = ref(NaN);
 | 
			
		||||
  const clientStore = useClientStore();
 | 
			
		||||
  const client = clientStore.client;
 | 
			
		||||
 | 
			
		||||
  const steamId = ref("");
 | 
			
		||||
  const username = ref("");
 | 
			
		||||
  const isLoggedIn = 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 }) {
 | 
			
		||||
    return fetch(import.meta.env.VITE_API_BASE_URL + "/login/authenticate", {
 | 
			
		||||
| 
						 | 
				
			
			@ -32,7 +50,9 @@ export const useAuthStore = defineStore("auth", () => {
 | 
			
		|||
    steamId,
 | 
			
		||||
    username,
 | 
			
		||||
    isLoggedIn,
 | 
			
		||||
    hasCheckedAuth,
 | 
			
		||||
    isRegistering,
 | 
			
		||||
    getUser,
 | 
			
		||||
    login,
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import { AvailabilitfClient } from "@/client";
 | 
			
		||||
import { AvailabilitfClient, CancelablePromise } from "@/client";
 | 
			
		||||
import { defineStore } from "pinia";
 | 
			
		||||
 | 
			
		||||
export const useClientStore = defineStore("client", () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -10,7 +10,7 @@ export const useClientStore = defineStore("client", () => {
 | 
			
		|||
 | 
			
		||||
  function call<T>(
 | 
			
		||||
    key: string,
 | 
			
		||||
    apiCall: () => Promise<T>,
 | 
			
		||||
    apiCall: () => CancelablePromise<T>,
 | 
			
		||||
    thenOnce?: (result: T) => T
 | 
			
		||||
  ): Promise<T> {
 | 
			
		||||
    console.log("Fetching call " + key);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,10 +3,13 @@ import { AvailabilitfClient, type TeamInviteSchema, type RoleSchema, type TeamSc
 | 
			
		|||
import { defineStore } from "pinia";
 | 
			
		||||
import { computed, reactive, ref, type Reactive, type Ref } from "vue";
 | 
			
		||||
import { useClientStore } from "./client";
 | 
			
		||||
import { useAuthStore } from "./auth";
 | 
			
		||||
 | 
			
		||||
export type TeamMap = { [id: number]: TeamSchema };
 | 
			
		||||
 | 
			
		||||
export const useTeamsStore = defineStore("teams", () => {
 | 
			
		||||
  const authStore = useAuthStore();
 | 
			
		||||
 | 
			
		||||
  const clientStore = useClientStore();
 | 
			
		||||
  const client = clientStore.client;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -14,8 +17,6 @@ export const useTeamsStore = defineStore("teams", () => {
 | 
			
		|||
  const teamMembers: Reactive<{ [id: number]: ViewTeamMembersResponse[] }> = reactive({ });
 | 
			
		||||
  const teamInvites: Reactive<{ [id: number]: TeamInviteSchema[] }> = reactive({ });
 | 
			
		||||
 | 
			
		||||
  const isFetchingTeams = ref(false);
 | 
			
		||||
 | 
			
		||||
  async function fetchTeams() {
 | 
			
		||||
    return clientStore.call(
 | 
			
		||||
      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 {
 | 
			
		||||
    teams,
 | 
			
		||||
    teamInvites,
 | 
			
		||||
| 
						 | 
				
			
			@ -129,5 +135,6 @@ export const useTeamsStore = defineStore("teams", () => {
 | 
			
		|||
    createInvite,
 | 
			
		||||
    consumeInvite,
 | 
			
		||||
    revokeInvite,
 | 
			
		||||
    leaveTeam,
 | 
			
		||||
  };
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -164,17 +164,27 @@ button.radio:hover {
 | 
			
		|||
 | 
			
		||||
button.radio.selected {
 | 
			
		||||
  color: var(--accent);
 | 
			
		||||
  background-color: var(--accent-transparent);
 | 
			
		||||
  color: var(--accent-transparent);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
button.left {
 | 
			
		||||
  border-radius: 4px 0 0 4px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
button.radio.left.selected {
 | 
			
		||||
  color: var(--yellow);
 | 
			
		||||
  background-color: var(--yellow-transparent);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
button.right {
 | 
			
		||||
  border-radius: 0 4px 4px 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
button.right.radio.selected {
 | 
			
		||||
  color: var(--green);
 | 
			
		||||
  background-color: var(--green-transparent);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.v-select {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  width: auto;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,7 +19,12 @@ const invites = computed(() => {
 | 
			
		|||
 | 
			
		||||
const availableMembers = computed(() => {
 | 
			
		||||
  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() {
 | 
			
		||||
| 
						 | 
				
			
			@ -30,17 +35,33 @@ function revokeInvite(key) {
 | 
			
		|||
  teamsStore.revokeInvite(team.value.id, key)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function leaveTeam() {
 | 
			
		||||
  teamsStore.leaveTeam(team.value.id)
 | 
			
		||||
    .then(() => {
 | 
			
		||||
      teamsStore.fetchTeams()
 | 
			
		||||
        .then(() => {
 | 
			
		||||
          router.push("/");
 | 
			
		||||
        })
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
  let key = route.query.key;
 | 
			
		||||
  let teamId = route.params.id;
 | 
			
		||||
 | 
			
		||||
  let doFetchTeam = () => {
 | 
			
		||||
    teamsStore.fetchTeam(teamId)
 | 
			
		||||
      .then(() => teamsStore.fetchTeamMembers(teamId))
 | 
			
		||||
      .then(() => teamsStore.getInvites(teamId));
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  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>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -49,16 +70,23 @@ onMounted(async () => {
 | 
			
		|||
    <template v-if="team">
 | 
			
		||||
      <h1>
 | 
			
		||||
        {{ team.teamName }}
 | 
			
		||||
        <RouterLink :to="'/schedule?teamId=' + team.id">
 | 
			
		||||
        </RouterLink>
 | 
			
		||||
        <em class="aside" v-if="teamsStore.teamMembers[route.params.id]">
 | 
			
		||||
          {{ teamsStore.teamMembers[route.params.id]?.length }} member(s),
 | 
			
		||||
          {{ availableMembers?.length }} currently available
 | 
			
		||||
          {{ availableMembers?.length }} currently available,
 | 
			
		||||
          {{ availableMembersNextHour?.length }} available in the next hour
 | 
			
		||||
        </em>
 | 
			
		||||
        <div class="team-details-button-group">
 | 
			
		||||
          <button class="accent">
 | 
			
		||||
            <i class="bi bi-calendar-fill margin"></i>
 | 
			
		||||
            View schedule
 | 
			
		||||
          <RouterLink class="button" :to="'/schedule?teamId=' + team.id">
 | 
			
		||||
            <button class="accent">
 | 
			
		||||
              <i class="bi bi-calendar-fill margin"></i>
 | 
			
		||||
              View schedule
 | 
			
		||||
            </button>
 | 
			
		||||
          </RouterLink>
 | 
			
		||||
          <button
 | 
			
		||||
            class="destructive"
 | 
			
		||||
            @click="leaveTeam"
 | 
			
		||||
          >
 | 
			
		||||
            Leave
 | 
			
		||||
          </button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </h1>
 | 
			
		||||
| 
						 | 
				
			
			@ -175,6 +203,7 @@ th {
 | 
			
		|||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: end;
 | 
			
		||||
  gap: 4px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.create-invite-group {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,8 +4,10 @@ import string
 | 
			
		|||
import urllib.parse
 | 
			
		||||
from flask import Blueprint, abort, make_response, redirect, request, url_for
 | 
			
		||||
import requests
 | 
			
		||||
from spectree import Response
 | 
			
		||||
from spec import spec
 | 
			
		||||
import models
 | 
			
		||||
from models import AuthSession, Player, db
 | 
			
		||||
from models import AuthSession, Player, PlayerSchema, db
 | 
			
		||||
from middleware import requires_authentication
 | 
			
		||||
 | 
			
		||||
api_login = Blueprint("login", __name__, url_prefix="/login")
 | 
			
		||||
| 
						 | 
				
			
			@ -16,6 +18,21 @@ STEAM_OPENID_URL = "https://steamcommunity.com/openid/login"
 | 
			
		|||
def index():
 | 
			
		||||
    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")
 | 
			
		||||
def steam_authenticate():
 | 
			
		||||
    params = request.get_json()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
from datetime import datetime, timezone
 | 
			
		||||
from datetime import datetime, timedelta, timezone
 | 
			
		||||
from random import randint, random
 | 
			
		||||
import sys
 | 
			
		||||
import time
 | 
			
		||||
| 
						 | 
				
			
			@ -116,6 +116,69 @@ def delete_team(player: Player, team_id: int):
 | 
			
		|||
    db.session.commit()
 | 
			
		||||
    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):
 | 
			
		||||
    team_role: PlayerTeam.TeamRole = PlayerTeam.TeamRole.Player
 | 
			
		||||
    is_team_leader: bool = False
 | 
			
		||||
| 
						 | 
				
			
			@ -238,9 +301,10 @@ class ViewTeamMembersResponse(PlayerSchema):
 | 
			
		|||
        is_main: bool
 | 
			
		||||
 | 
			
		||||
    roles: list[RoleSchema]
 | 
			
		||||
    availability: int
 | 
			
		||||
    availability: list[int]
 | 
			
		||||
    playtime: float
 | 
			
		||||
    created_at: datetime
 | 
			
		||||
    is_team_leader: bool = False
 | 
			
		||||
 | 
			
		||||
@api_team.get("/id/<team_id>/players")
 | 
			
		||||
@spec.validate(
 | 
			
		||||
| 
						 | 
				
			
			@ -254,6 +318,7 @@ class ViewTeamMembersResponse(PlayerSchema):
 | 
			
		|||
@requires_authentication
 | 
			
		||||
def view_team_members(player: Player, team_id: int, **kwargs):
 | 
			
		||||
    now = datetime.now(timezone.utc)
 | 
			
		||||
    next_hour = now + timedelta(hours=1)
 | 
			
		||||
 | 
			
		||||
    player_teams_query = db.session.query(
 | 
			
		||||
        PlayerTeam
 | 
			
		||||
| 
						 | 
				
			
			@ -264,7 +329,7 @@ def view_team_members(player: Player, team_id: int, **kwargs):
 | 
			
		|||
        joinedload(PlayerTeam.player),
 | 
			
		||||
        joinedload(PlayerTeam.player_roles),
 | 
			
		||||
        joinedload(PlayerTeam.availability.and_(
 | 
			
		||||
            (PlayerTeamAvailability.start_time <= now) &
 | 
			
		||||
            (PlayerTeamAvailability.start_time <= next_hour) &
 | 
			
		||||
                (PlayerTeamAvailability.end_time > now)
 | 
			
		||||
        )),
 | 
			
		||||
    )
 | 
			
		||||
| 
						 | 
				
			
			@ -284,10 +349,13 @@ def view_team_members(player: Player, team_id: int, **kwargs):
 | 
			
		|||
        roles = player_team.player_roles
 | 
			
		||||
        player = player_team.player
 | 
			
		||||
 | 
			
		||||
        availability = 0
 | 
			
		||||
        if len(player_team.availability) > 0:
 | 
			
		||||
            print(player_team.availability)
 | 
			
		||||
            availability = player_team.availability[0].availability
 | 
			
		||||
        availability = [0, 0]
 | 
			
		||||
 | 
			
		||||
        for record in player_team.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(
 | 
			
		||||
            username=player.username,
 | 
			
		||||
| 
						 | 
				
			
			@ -296,6 +364,7 @@ def view_team_members(player: Player, team_id: int, **kwargs):
 | 
			
		|||
            availability=availability,
 | 
			
		||||
            playtime=player_team.playtime.total_seconds() / 3600,
 | 
			
		||||
            created_at=player_team.created_at,
 | 
			
		||||
            is_team_leader=player_team.is_team_leader,
 | 
			
		||||
        ).dict(by_alias=True)
 | 
			
		||||
 | 
			
		||||
    return list(map(map_to_response, player_teams))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue