Compare commits
	
		
			5 Commits 
		
	
	
		
			52d8ea5988
			...
			b41dbefa55
		
	
	| Author | SHA1 | Date | 
|---|---|---|
| 
							
							
								
									
								
								 | 
						b41dbefa55 | |
| 
							
							
								
									
								
								 | 
						c7874d6d11 | |
| 
							
							
								
									
								
								 | 
						c9547c43f6 | |
| 
							
							
								
									
								
								 | 
						1d6dce5088 | |
| 
							
							
								
									
								
								 | 
						13fd7fdfc0 | 
| 
						 | 
					@ -18,7 +18,7 @@ Scheduling for TF2
 | 
				
			||||||
- **Database:** [PostgreSQL 17.1](https://www.postgresql.org/docs/17/index.html)
 | 
					- **Database:** [PostgreSQL 17.1](https://www.postgresql.org/docs/17/index.html)
 | 
				
			||||||
  (production) / SQLite (development)
 | 
					  (production) / SQLite (development)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Setup (dev)
 | 
					## Setup (development, SQLite3)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```sh
 | 
					```sh
 | 
				
			||||||
docker compose build
 | 
					docker compose build
 | 
				
			||||||
| 
						 | 
					@ -28,7 +28,7 @@ DATABASE_URI=sqlite:///db.sqlite3 flask db upgrade
 | 
				
			||||||
 | 
					
 | 
				
			||||||
App will run at port 8000.
 | 
					App will run at port 8000.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Setup (production)
 | 
					## Setup (production, Postgres)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Build the frontend app:
 | 
					Build the frontend app:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,6 +21,7 @@ export type { EventSchema } from './models/EventSchema';
 | 
				
			||||||
export type { EventWithPlayerSchema } from './models/EventWithPlayerSchema';
 | 
					export type { EventWithPlayerSchema } from './models/EventWithPlayerSchema';
 | 
				
			||||||
export type { EventWithPlayerSchemaList } from './models/EventWithPlayerSchemaList';
 | 
					export type { EventWithPlayerSchemaList } from './models/EventWithPlayerSchemaList';
 | 
				
			||||||
export type { GetEventPlayersResponse } from './models/GetEventPlayersResponse';
 | 
					export type { GetEventPlayersResponse } from './models/GetEventPlayersResponse';
 | 
				
			||||||
 | 
					export type { GetMatchQuery } from './models/GetMatchQuery';
 | 
				
			||||||
export type { MatchSchema } from './models/MatchSchema';
 | 
					export type { MatchSchema } from './models/MatchSchema';
 | 
				
			||||||
export type { PlayerEventRolesSchema } from './models/PlayerEventRolesSchema';
 | 
					export type { PlayerEventRolesSchema } from './models/PlayerEventRolesSchema';
 | 
				
			||||||
export type { PlayerRoleSchema } from './models/PlayerRoleSchema';
 | 
					export type { PlayerRoleSchema } from './models/PlayerRoleSchema';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,8 @@
 | 
				
			||||||
 | 
					/* generated using openapi-typescript-codegen -- do not edit */
 | 
				
			||||||
 | 
					/* istanbul ignore file */
 | 
				
			||||||
 | 
					/* tslint:disable */
 | 
				
			||||||
 | 
					/* eslint-disable */
 | 
				
			||||||
 | 
					export type GetMatchQuery = {
 | 
				
			||||||
 | 
					    limit: (number | null);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,7 +5,7 @@
 | 
				
			||||||
export type MatchSchema = {
 | 
					export type MatchSchema = {
 | 
				
			||||||
    blueScore: number;
 | 
					    blueScore: number;
 | 
				
			||||||
    createdAt: string;
 | 
					    createdAt: string;
 | 
				
			||||||
    duration: string;
 | 
					    duration: number;
 | 
				
			||||||
    logsTfId: number;
 | 
					    logsTfId: number;
 | 
				
			||||||
    logsTfTitle: string;
 | 
					    logsTfTitle: string;
 | 
				
			||||||
    matchTime: string;
 | 
					    matchTime: string;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,5 +4,6 @@
 | 
				
			||||||
/* eslint-disable */
 | 
					/* eslint-disable */
 | 
				
			||||||
export type SubmitMatchJson = {
 | 
					export type SubmitMatchJson = {
 | 
				
			||||||
    matchIds: Array<number>;
 | 
					    matchIds: Array<number>;
 | 
				
			||||||
 | 
					    teamId: number;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,6 +6,7 @@ import type { MatchSchema } from './MatchSchema';
 | 
				
			||||||
export type TeamMatchSchema = {
 | 
					export type TeamMatchSchema = {
 | 
				
			||||||
    match: MatchSchema;
 | 
					    match: MatchSchema;
 | 
				
			||||||
    ourScore: number;
 | 
					    ourScore: number;
 | 
				
			||||||
 | 
					    teamColor: string;
 | 
				
			||||||
    theirScore: number;
 | 
					    theirScore: number;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -348,11 +348,13 @@ export class DefaultService {
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * get_matches_for_team <GET>
 | 
					     * get_matches_for_team <GET>
 | 
				
			||||||
     * @param teamId
 | 
					     * @param teamId
 | 
				
			||||||
 | 
					     * @param limit
 | 
				
			||||||
     * @returns TeamMatchSchemaList OK
 | 
					     * @returns TeamMatchSchemaList OK
 | 
				
			||||||
     * @throws ApiError
 | 
					     * @throws ApiError
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public getMatchesForTeam(
 | 
					    public getMatchesForTeam(
 | 
				
			||||||
        teamId: number,
 | 
					        teamId: number,
 | 
				
			||||||
 | 
					        limit: (number | null),
 | 
				
			||||||
    ): CancelablePromise<TeamMatchSchemaList> {
 | 
					    ): CancelablePromise<TeamMatchSchemaList> {
 | 
				
			||||||
        return this.httpRequest.request({
 | 
					        return this.httpRequest.request({
 | 
				
			||||||
            method: 'GET',
 | 
					            method: 'GET',
 | 
				
			||||||
| 
						 | 
					@ -360,6 +362,9 @@ export class DefaultService {
 | 
				
			||||||
            path: {
 | 
					            path: {
 | 
				
			||||||
                'team_id': teamId,
 | 
					                'team_id': teamId,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
 | 
					            query: {
 | 
				
			||||||
 | 
					                'limit': limit,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
            errors: {
 | 
					            errors: {
 | 
				
			||||||
                422: `Unprocessable Content`,
 | 
					                422: `Unprocessable Content`,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
| 
						 | 
					@ -520,7 +525,7 @@ export class DefaultService {
 | 
				
			||||||
     * @throws ApiError
 | 
					     * @throws ApiError
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public deleteTeam(
 | 
					    public deleteTeam(
 | 
				
			||||||
        teamId: string,
 | 
					        teamId: number,
 | 
				
			||||||
    ): CancelablePromise<any> {
 | 
					    ): CancelablePromise<any> {
 | 
				
			||||||
        return this.httpRequest.request({
 | 
					        return this.httpRequest.request({
 | 
				
			||||||
            method: 'DELETE',
 | 
					            method: 'DELETE',
 | 
				
			||||||
| 
						 | 
					@ -542,7 +547,7 @@ export class DefaultService {
 | 
				
			||||||
     * @throws ApiError
 | 
					     * @throws ApiError
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public getTeam(
 | 
					    public getTeam(
 | 
				
			||||||
        teamId: string,
 | 
					        teamId: number,
 | 
				
			||||||
    ): CancelablePromise<ViewTeamResponse> {
 | 
					    ): CancelablePromise<ViewTeamResponse> {
 | 
				
			||||||
        return this.httpRequest.request({
 | 
					        return this.httpRequest.request({
 | 
				
			||||||
            method: 'GET',
 | 
					            method: 'GET',
 | 
				
			||||||
| 
						 | 
					@ -590,7 +595,7 @@ export class DefaultService {
 | 
				
			||||||
     * @throws ApiError
 | 
					     * @throws ApiError
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public editMemberRoles(
 | 
					    public editMemberRoles(
 | 
				
			||||||
        teamId: string,
 | 
					        teamId: number,
 | 
				
			||||||
        targetPlayerId: string,
 | 
					        targetPlayerId: string,
 | 
				
			||||||
        requestBody?: EditMemberRolesJson,
 | 
					        requestBody?: EditMemberRolesJson,
 | 
				
			||||||
    ): CancelablePromise<void> {
 | 
					    ): CancelablePromise<void> {
 | 
				
			||||||
| 
						 | 
					@ -617,7 +622,7 @@ export class DefaultService {
 | 
				
			||||||
     * @throws ApiError
 | 
					     * @throws ApiError
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public getIntegrations(
 | 
					    public getIntegrations(
 | 
				
			||||||
        teamId: string,
 | 
					        teamId: number,
 | 
				
			||||||
    ): CancelablePromise<TeamIntegrationSchema> {
 | 
					    ): CancelablePromise<TeamIntegrationSchema> {
 | 
				
			||||||
        return this.httpRequest.request({
 | 
					        return this.httpRequest.request({
 | 
				
			||||||
            method: 'GET',
 | 
					            method: 'GET',
 | 
				
			||||||
| 
						 | 
					@ -638,7 +643,7 @@ export class DefaultService {
 | 
				
			||||||
     * @throws ApiError
 | 
					     * @throws ApiError
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public updateIntegrations(
 | 
					    public updateIntegrations(
 | 
				
			||||||
        teamId: string,
 | 
					        teamId: number,
 | 
				
			||||||
        requestBody?: TeamIntegrationSchema,
 | 
					        requestBody?: TeamIntegrationSchema,
 | 
				
			||||||
    ): CancelablePromise<TeamIntegrationSchema> {
 | 
					    ): CancelablePromise<TeamIntegrationSchema> {
 | 
				
			||||||
        return this.httpRequest.request({
 | 
					        return this.httpRequest.request({
 | 
				
			||||||
| 
						 | 
					@ -661,7 +666,7 @@ export class DefaultService {
 | 
				
			||||||
     * @throws ApiError
 | 
					     * @throws ApiError
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public getInvites(
 | 
					    public getInvites(
 | 
				
			||||||
        teamId: string,
 | 
					        teamId: number,
 | 
				
			||||||
    ): CancelablePromise<TeamInviteSchemaList> {
 | 
					    ): CancelablePromise<TeamInviteSchemaList> {
 | 
				
			||||||
        return this.httpRequest.request({
 | 
					        return this.httpRequest.request({
 | 
				
			||||||
            method: 'GET',
 | 
					            method: 'GET',
 | 
				
			||||||
| 
						 | 
					@ -682,7 +687,7 @@ export class DefaultService {
 | 
				
			||||||
     * @throws ApiError
 | 
					     * @throws ApiError
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public createInvite(
 | 
					    public createInvite(
 | 
				
			||||||
        teamId: string,
 | 
					        teamId: number,
 | 
				
			||||||
    ): CancelablePromise<TeamInviteSchema> {
 | 
					    ): CancelablePromise<TeamInviteSchema> {
 | 
				
			||||||
        return this.httpRequest.request({
 | 
					        return this.httpRequest.request({
 | 
				
			||||||
            method: 'POST',
 | 
					            method: 'POST',
 | 
				
			||||||
| 
						 | 
					@ -704,7 +709,7 @@ export class DefaultService {
 | 
				
			||||||
     * @throws ApiError
 | 
					     * @throws ApiError
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public revokeInvite(
 | 
					    public revokeInvite(
 | 
				
			||||||
        teamId: string,
 | 
					        teamId: number,
 | 
				
			||||||
        key: string,
 | 
					        key: string,
 | 
				
			||||||
    ): CancelablePromise<void> {
 | 
					    ): CancelablePromise<void> {
 | 
				
			||||||
        return this.httpRequest.request({
 | 
					        return this.httpRequest.request({
 | 
				
			||||||
| 
						 | 
					@ -729,7 +734,7 @@ export class DefaultService {
 | 
				
			||||||
     * @throws ApiError
 | 
					     * @throws ApiError
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public createOrUpdatePlayer(
 | 
					    public createOrUpdatePlayer(
 | 
				
			||||||
        teamId: string,
 | 
					        teamId: number,
 | 
				
			||||||
        playerId: string,
 | 
					        playerId: string,
 | 
				
			||||||
        requestBody?: AddPlayerJson,
 | 
					        requestBody?: AddPlayerJson,
 | 
				
			||||||
    ): CancelablePromise<any> {
 | 
					    ): CancelablePromise<any> {
 | 
				
			||||||
| 
						 | 
					@ -757,7 +762,7 @@ export class DefaultService {
 | 
				
			||||||
     * @throws ApiError
 | 
					     * @throws ApiError
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public removePlayerFromTeam(
 | 
					    public removePlayerFromTeam(
 | 
				
			||||||
        teamId: string,
 | 
					        teamId: number,
 | 
				
			||||||
        targetPlayerId: string,
 | 
					        targetPlayerId: string,
 | 
				
			||||||
    ): CancelablePromise<any> {
 | 
					    ): CancelablePromise<any> {
 | 
				
			||||||
        return this.httpRequest.request({
 | 
					        return this.httpRequest.request({
 | 
				
			||||||
| 
						 | 
					@ -781,7 +786,7 @@ export class DefaultService {
 | 
				
			||||||
     * @throws ApiError
 | 
					     * @throws ApiError
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public getTeamMembers(
 | 
					    public getTeamMembers(
 | 
				
			||||||
        teamId: string,
 | 
					        teamId: number,
 | 
				
			||||||
    ): CancelablePromise<ViewTeamMembersResponseList> {
 | 
					    ): CancelablePromise<ViewTeamMembersResponseList> {
 | 
				
			||||||
        return this.httpRequest.request({
 | 
					        return this.httpRequest.request({
 | 
				
			||||||
            method: 'GET',
 | 
					            method: 'GET',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,10 +1,13 @@
 | 
				
			||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { useTeamDetails } from "@/composables/team-details";
 | 
				
			||||||
import { useMatchesStore } from "@/stores/matches";
 | 
					import { useMatchesStore } from "@/stores/matches";
 | 
				
			||||||
import { DialogClose, DialogContent, DialogDescription, DialogOverlay, DialogPortal, DialogRoot, DialogTitle, DialogTrigger } from "radix-vue";
 | 
					import { DialogClose, DialogContent, DialogDescription, DialogOverlay, DialogPortal, DialogRoot, DialogTitle, DialogTrigger } from "radix-vue";
 | 
				
			||||||
import { ref } from "vue";
 | 
					import { ref } from "vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const matchesStore = useMatchesStore();
 | 
					const matchesStore = useMatchesStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { teamId } = useTeamDetails();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const urlsText = ref("");
 | 
					const urlsText = ref("");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function submit() {
 | 
					function submit() {
 | 
				
			||||||
| 
						 | 
					@ -15,7 +18,10 @@ function submit() {
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    .filter((id) => !isNaN(id));
 | 
					    .filter((id) => !isNaN(id));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  matchesStore.submitMatches(ids);
 | 
					  matchesStore.submitMatches(ids, teamId.value)
 | 
				
			||||||
 | 
					    .then(() => {
 | 
				
			||||||
 | 
					      urlsText.value = "";
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -28,7 +28,7 @@ function disableIntegration() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <h2>logs.tf Auto-Tracking</h2>
 | 
					  <h2>logs.tf Auto-Tracking</h2>
 | 
				
			||||||
  <p>Automatically fetch and track match history from logs.tf. (CURRENTLY NOT IMPLEMENTED)</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>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,37 +1,91 @@
 | 
				
			||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import type { TeamMatchSchema, TeamSchema } from '@/client';
 | 
				
			||||||
 | 
					import moment from 'moment';
 | 
				
			||||||
 | 
					import { computed } from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const matchTime = computed(() => moment(props.teamMatch.match.matchTime)
 | 
				
			||||||
 | 
					  .format("LL LT"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps<{
 | 
				
			||||||
 | 
					  team: TeamSchema,
 | 
				
			||||||
 | 
					  teamMatch: TeamMatchSchema,
 | 
				
			||||||
 | 
					}>();
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div class="match-card">
 | 
					  <div class="match-card">
 | 
				
			||||||
 | 
					    <div class="match-title">
 | 
				
			||||||
 | 
					      <h3>
 | 
				
			||||||
 | 
					        {{ teamMatch.match.logsTfTitle }}
 | 
				
			||||||
 | 
					      </h3>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
    <div class="match-scores">
 | 
					    <div class="match-scores">
 | 
				
			||||||
      <div class="team-and-score">
 | 
					      <div class="team-and-score">
 | 
				
			||||||
        <span class="team-name">
 | 
					        <span class="team-name">
 | 
				
			||||||
          NVBLU
 | 
					          <span v-if="teamMatch.teamColor == 'Blue'">
 | 
				
			||||||
 | 
					            BLU
 | 
				
			||||||
 | 
					          </span>
 | 
				
			||||||
 | 
					          <span v-else>
 | 
				
			||||||
 | 
					            RED
 | 
				
			||||||
 | 
					          </span>
 | 
				
			||||||
        </span>
 | 
					        </span>
 | 
				
			||||||
        <span class="score">
 | 
					        <span class="score">
 | 
				
			||||||
          3
 | 
					          {{ teamMatch.ourScore }}
 | 
				
			||||||
        </span>
 | 
					        </span>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <span class="score">-</span>
 | 
					 | 
				
			||||||
      <div class="team-and-score">
 | 
					      <div class="team-and-score">
 | 
				
			||||||
        <span class="score">
 | 
					        <span class="score">
 | 
				
			||||||
          2
 | 
					          {{ teamMatch.theirScore }}
 | 
				
			||||||
        </span>
 | 
					        </span>
 | 
				
			||||||
        <span class="team-name">
 | 
					        <span class="team-name">
 | 
				
			||||||
          RED
 | 
					          <span v-if="teamMatch.teamColor == 'Blue'">
 | 
				
			||||||
 | 
					            RED
 | 
				
			||||||
 | 
					          </span>
 | 
				
			||||||
 | 
					          <span v-else>
 | 
				
			||||||
 | 
					            BLU
 | 
				
			||||||
 | 
					          </span>
 | 
				
			||||||
        </span>
 | 
					        </span>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div class="bottom-row">
 | 
				
			||||||
 | 
					      <div class="subtext">
 | 
				
			||||||
 | 
					        {{ matchTime }}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div>
 | 
				
			||||||
 | 
					        <a :href="'https://logs.tf/' + teamMatch.match.logsTfId" target="_blank" class="button">
 | 
				
			||||||
 | 
					          #{{ teamMatch.match.logsTfId }}
 | 
				
			||||||
 | 
					        </a>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style scoped>
 | 
					<style scoped>
 | 
				
			||||||
.match-card {
 | 
					.match-card {
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  justify-content: space-between;
 | 
					  flex-direction: column;
 | 
				
			||||||
  padding: 1rem;
 | 
					  padding: 1rem;
 | 
				
			||||||
  border: 1px solid var(--text);
 | 
					  border: 1px solid var(--text);
 | 
				
			||||||
  border-radius: 8px;
 | 
					  border-radius: 8px;
 | 
				
			||||||
 | 
					  gap: 0.5rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.match-title {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  gap: 0.5rem;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.bottom-row {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: space-between;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.bottom-row > div {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  gap: 1rem;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.match-scores {
 | 
					.match-scores {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,8 +11,8 @@ 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 TeamSettingsMatchesView from "@/views/TeamSettings/MatchesView.vue";
 | 
				
			||||||
import UserSettingsView from "@/views/UserSettingsView.vue";
 | 
					import UserSettingsView from "@/views/UserSettingsView.vue";
 | 
				
			||||||
import MatchesView from "@/views/MatchesView.vue";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const router = createRouter({
 | 
					const router = createRouter({
 | 
				
			||||||
  history: createWebHistory(import.meta.env.BASE_URL),
 | 
					  history: createWebHistory(import.meta.env.BASE_URL),
 | 
				
			||||||
| 
						 | 
					@ -72,6 +72,11 @@ const router = createRouter({
 | 
				
			||||||
          name: "team-settings/invites",
 | 
					          name: "team-settings/invites",
 | 
				
			||||||
          component: TeamSettingsInvitesView,
 | 
					          component: TeamSettingsInvitesView,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          path: "matches",
 | 
				
			||||||
 | 
					          name: "team-settings/matches",
 | 
				
			||||||
 | 
					          component: TeamSettingsMatchesView,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
| 
						 | 
					@ -79,11 +84,6 @@ const router = createRouter({
 | 
				
			||||||
      name: "user-settings",
 | 
					      name: "user-settings",
 | 
				
			||||||
      component: UserSettingsView,
 | 
					      component: UserSettingsView,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      path: "/matches",
 | 
					 | 
				
			||||||
      name: "matches",
 | 
					 | 
				
			||||||
      component: MatchesView,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  ]
 | 
					  ]
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,7 +9,9 @@ export const useMatchesStore = defineStore("matches", () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const matches = ref<{ [id: number]: MatchSchema }>({ });
 | 
					  const matches = ref<{ [id: number]: MatchSchema }>({ });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const teamMatches = ref<{ [id: number]: TeamMatchSchema }>({ });
 | 
					  const teamMatches = ref<{ [teamId: number]: TeamMatchSchema[] }>({ });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const recentMatches = ref<TeamMatchSchema[]>([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function fetchMatches() {
 | 
					  function fetchMatches() {
 | 
				
			||||||
    return clientStore.call(
 | 
					    return clientStore.call(
 | 
				
			||||||
| 
						 | 
					@ -18,21 +20,52 @@ export const useMatchesStore = defineStore("matches", () => {
 | 
				
			||||||
      (response) => {
 | 
					      (response) => {
 | 
				
			||||||
        response.forEach((match) => {
 | 
					        response.forEach((match) => {
 | 
				
			||||||
          matches.value[match.match.logsTfId] = match.match;
 | 
					          matches.value[match.match.logsTfId] = match.match;
 | 
				
			||||||
          teamMatches.value[match.match.logsTfId] = match;
 | 
					          //teamMatches.value[match.match.logsTfId] = match;
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        return response;
 | 
					        return response;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function submitMatches(logsTfIds: number[]) {
 | 
					  function fetchMatchesForTeam(teamId: number) {
 | 
				
			||||||
    return client.default.submitMatch({ matchIds: logsTfIds });
 | 
					    return clientStore.call(
 | 
				
			||||||
 | 
					      fetchMatchesForTeam.name,
 | 
				
			||||||
 | 
					      () => client.default.getMatchesForTeam(teamId, 1024),
 | 
				
			||||||
 | 
					      (response) => {
 | 
				
			||||||
 | 
					        teamMatches.value[teamId] = [];
 | 
				
			||||||
 | 
					        response.forEach((match) => {
 | 
				
			||||||
 | 
					          matches.value[match.match.logsTfId] = match.match;
 | 
				
			||||||
 | 
					          teamMatches.value[teamId].push(match);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        return response;
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function fetchRecentMatchesForTeam(teamId: number, limit: number) {
 | 
				
			||||||
 | 
					    return clientStore.call(
 | 
				
			||||||
 | 
					      fetchMatchesForTeam.name,
 | 
				
			||||||
 | 
					      () => client.default.getMatchesForTeam(teamId, limit),
 | 
				
			||||||
 | 
					      (response) => {
 | 
				
			||||||
 | 
					        recentMatches.value = [];
 | 
				
			||||||
 | 
					        response.forEach((match) => {
 | 
				
			||||||
 | 
					          matches.value[match.match.logsTfId] = match.match;
 | 
				
			||||||
 | 
					          recentMatches.value.push(match);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        return response;
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function submitMatches(logsTfIds: number[], teamId: number) {
 | 
				
			||||||
 | 
					    return client.default.submitMatch({ matchIds: logsTfIds, teamId });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    matches,
 | 
					    matches,
 | 
				
			||||||
    teamMatches,
 | 
					    teamMatches,
 | 
				
			||||||
 | 
					    recentMatches,
 | 
				
			||||||
    fetchMatches,
 | 
					    fetchMatches,
 | 
				
			||||||
 | 
					    fetchMatchesForTeam,
 | 
				
			||||||
 | 
					    fetchRecentMatchesForTeam,
 | 
				
			||||||
    submitMatches,
 | 
					    submitMatches,
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -35,7 +35,7 @@ export const useTeamsStore = defineStore("teams", () => {
 | 
				
			||||||
  async function fetchTeam(id: number) {
 | 
					  async function fetchTeam(id: number) {
 | 
				
			||||||
    const response = await clientStore.call(
 | 
					    const response = await clientStore.call(
 | 
				
			||||||
      fetchTeam.name,
 | 
					      fetchTeam.name,
 | 
				
			||||||
      () => client.default.getTeam(id.toString())
 | 
					      () => client.default.getTeam(id)
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    teams[response.team.id] = response.team;
 | 
					    teams[response.team.id] = response.team;
 | 
				
			||||||
    return response;
 | 
					    return response;
 | 
				
			||||||
| 
						 | 
					@ -44,7 +44,7 @@ export const useTeamsStore = defineStore("teams", () => {
 | 
				
			||||||
  async function fetchTeamMembers(id: number) {
 | 
					  async function fetchTeamMembers(id: number) {
 | 
				
			||||||
    const response = await clientStore.call(
 | 
					    const response = await clientStore.call(
 | 
				
			||||||
      fetchTeamMembers.name,
 | 
					      fetchTeamMembers.name,
 | 
				
			||||||
      () => client.default.getTeamMembers(id.toString())
 | 
					      () => client.default.getTeamMembers(id)
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    teamMembers[id] = response.map((member): ViewTeamMembersResponse => {
 | 
					    teamMembers[id] = response.map((member): ViewTeamMembersResponse => {
 | 
				
			||||||
      member.roles = member.roles.sort((a, b) => (a.isMain === b.isMain ? 0 : a.isMain ? -1 : 1));
 | 
					      member.roles = member.roles.sort((a, b) => (a.isMain === b.isMain ? 0 : a.isMain ? -1 : 1));
 | 
				
			||||||
| 
						 | 
					@ -68,11 +68,11 @@ export const useTeamsStore = defineStore("teams", () => {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async function updateRoles(teamId: number, playerId: string, roles: RoleSchema[]) {
 | 
					  async function updateRoles(teamId: number, playerId: string, roles: RoleSchema[]) {
 | 
				
			||||||
    return await client.default.editMemberRoles(teamId.toString(), playerId, { roles });
 | 
					    return await client.default.editMemberRoles(teamId, playerId, { roles });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async function leaveTeam(teamId: number) {
 | 
					  async function leaveTeam(teamId: number) {
 | 
				
			||||||
    return client.default.removePlayerFromTeam(teamId.toString(), authStore.steamId);
 | 
					    return client.default.removePlayerFromTeam(teamId, authStore.steamId);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,7 +18,7 @@ export const useIntegrationsStore = defineStore("integrations", () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async function getIntegrations(teamId: number) {
 | 
					  async function getIntegrations(teamId: number) {
 | 
				
			||||||
    hasLoaded.value = false;
 | 
					    hasLoaded.value = false;
 | 
				
			||||||
    const response = await client.default.getIntegrations(teamId.toString());
 | 
					    const response = await client.default.getIntegrations(teamId);
 | 
				
			||||||
    setIntegrations(response);
 | 
					    setIntegrations(response);
 | 
				
			||||||
    return response;
 | 
					    return response;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -34,7 +34,7 @@ export const useIntegrationsStore = defineStore("integrations", () => {
 | 
				
			||||||
      discordIntegration: discordIntegration.value ?? null,
 | 
					      discordIntegration: discordIntegration.value ?? null,
 | 
				
			||||||
      logsTfIntegration: logsTfIntegration.value ?? null,
 | 
					      logsTfIntegration: logsTfIntegration.value ?? null,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    const response = await client.default.updateIntegrations(teamId.toString(), body);
 | 
					    const response = await client.default.updateIntegrations(teamId, body);
 | 
				
			||||||
    setIntegrations(response);
 | 
					    setIntegrations(response);
 | 
				
			||||||
    return response;
 | 
					    return response;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,7 +12,7 @@ export const useInvitesStore = defineStore("invites", () => {
 | 
				
			||||||
  async function getInvites(teamId: number) {
 | 
					  async function getInvites(teamId: number) {
 | 
				
			||||||
    return clientStore.call(
 | 
					    return clientStore.call(
 | 
				
			||||||
      getInvites.name,
 | 
					      getInvites.name,
 | 
				
			||||||
      () => client.default.getInvites(teamId.toString()),
 | 
					      () => client.default.getInvites(teamId),
 | 
				
			||||||
      (response) => {
 | 
					      (response) => {
 | 
				
			||||||
        teamInvites[teamId] = response;
 | 
					        teamInvites[teamId] = response;
 | 
				
			||||||
        return response;
 | 
					        return response;
 | 
				
			||||||
| 
						 | 
					@ -21,7 +21,7 @@ export const useInvitesStore = defineStore("invites", () => {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async function createInvite(teamId: number) {
 | 
					  async function createInvite(teamId: number) {
 | 
				
			||||||
    const response = await client.default.createInvite(teamId.toString());
 | 
					    const response = await client.default.createInvite(teamId);
 | 
				
			||||||
    teamInvites[teamId].push(response);
 | 
					    teamInvites[teamId].push(response);
 | 
				
			||||||
    return response;
 | 
					    return response;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -37,7 +37,7 @@ export const useInvitesStore = defineStore("invites", () => {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async function revokeInvite(teamId: number, key: string) {
 | 
					  async function revokeInvite(teamId: number, key: string) {
 | 
				
			||||||
    const response = await client.default.revokeInvite(teamId.toString(), key);
 | 
					    const response = await client.default.revokeInvite(teamId, key);
 | 
				
			||||||
    teamInvites[teamId] = teamInvites[teamId].filter((invite) => invite.key != key);
 | 
					    teamInvites[teamId] = teamInvites[teamId].filter((invite) => invite.key != key);
 | 
				
			||||||
    return response;
 | 
					    return response;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,10 +9,12 @@ import moment from "moment";
 | 
				
			||||||
import EventList from "@/components/EventList.vue";
 | 
					import EventList from "@/components/EventList.vue";
 | 
				
			||||||
import { useTeamsEventsStore } from "@/stores/teams/events";
 | 
					import { useTeamsEventsStore } from "@/stores/teams/events";
 | 
				
			||||||
import MatchCard from "@/components/MatchCard.vue";
 | 
					import MatchCard from "@/components/MatchCard.vue";
 | 
				
			||||||
 | 
					import { useMatchesStore } from "@/stores/matches";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const route = useRoute();
 | 
					const route = useRoute();
 | 
				
			||||||
const teamsStore = useTeamsStore();
 | 
					const teamsStore = useTeamsStore();
 | 
				
			||||||
const invitesStore = useInvitesStore();
 | 
					const invitesStore = useInvitesStore();
 | 
				
			||||||
 | 
					const matchesStore = useMatchesStore();
 | 
				
			||||||
const { team, teamId } = useTeamDetails();
 | 
					const { team, teamId } = useTeamDetails();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const creationDate = computed(() => {
 | 
					const creationDate = computed(() => {
 | 
				
			||||||
| 
						 | 
					@ -25,6 +27,7 @@ const key = computed(() => route.query.key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const teamsEventsStore = useTeamsEventsStore();
 | 
					const teamsEventsStore = useTeamsEventsStore();
 | 
				
			||||||
const events = computed(() => teamsEventsStore.teamEvents[teamId.value]);
 | 
					const events = computed(() => teamsEventsStore.teamEvents[teamId.value]);
 | 
				
			||||||
 | 
					const matches = computed(() => matchesStore.recentMatches);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
onMounted(() => {
 | 
					onMounted(() => {
 | 
				
			||||||
  let doFetchTeam = () => {
 | 
					  let doFetchTeam = () => {
 | 
				
			||||||
| 
						 | 
					@ -32,6 +35,7 @@ onMounted(() => {
 | 
				
			||||||
      .then(() => {
 | 
					      .then(() => {
 | 
				
			||||||
        teamsStore.fetchTeamMembers(teamId.value);
 | 
					        teamsStore.fetchTeamMembers(teamId.value);
 | 
				
			||||||
        teamsEventsStore.fetchTeamEvents(teamId.value);
 | 
					        teamsEventsStore.fetchTeamEvents(teamId.value);
 | 
				
			||||||
 | 
					        matchesStore.fetchRecentMatchesForTeam(teamId.value, 5);
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -76,14 +80,21 @@ onMounted(() => {
 | 
				
			||||||
          <EventList :events="events" :team-context="team" />
 | 
					          <EventList :events="events" :team-context="team" />
 | 
				
			||||||
          <h2 id="recent-matches-header">
 | 
					          <h2 id="recent-matches-header">
 | 
				
			||||||
            Recent Matches
 | 
					            Recent Matches
 | 
				
			||||||
            <!--RouterLink class="button" to="/">
 | 
					            <RouterLink class="button" :to="{ name: 'team-settings/matches' }">
 | 
				
			||||||
              <button class="icon" v-tooltip="'View all'">
 | 
					              <button class="icon" v-tooltip="'View all'">
 | 
				
			||||||
                <i class="bi bi-arrow-right-circle-fill"></i>
 | 
					                <i class="bi bi-arrow-right-circle-fill"></i>
 | 
				
			||||||
              </button>
 | 
					              </button>
 | 
				
			||||||
            </RouterLink-->
 | 
					            </RouterLink>
 | 
				
			||||||
          </h2>
 | 
					          </h2>
 | 
				
			||||||
          <em class="subtext" v-if="true">No recent matches.</em>
 | 
					          <em class="subtext" v-if="!matches">
 | 
				
			||||||
          <MatchCard v-else />
 | 
					            No recent matches.
 | 
				
			||||||
 | 
					          </em>
 | 
				
			||||||
 | 
					          <MatchCard
 | 
				
			||||||
 | 
					            v-else
 | 
				
			||||||
 | 
					            v-for="match in matches"
 | 
				
			||||||
 | 
					            :team-match="match"
 | 
				
			||||||
 | 
					            :team="team"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </template>
 | 
					    </template>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,12 +1,21 @@
 | 
				
			||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import AddMatchDialog from "@/components/AddMatchDialog.vue";
 | 
					import AddMatchDialog from "@/components/AddMatchDialog.vue";
 | 
				
			||||||
 | 
					import { useTeamDetails } from "@/composables/team-details";
 | 
				
			||||||
import { useMatchesStore } from "@/stores/matches";
 | 
					import { useMatchesStore } from "@/stores/matches";
 | 
				
			||||||
import { onMounted } from "vue";
 | 
					import { useTeamsStore } from "@/stores/teams";
 | 
				
			||||||
 | 
					import moment from "moment";
 | 
				
			||||||
 | 
					import { computed, onMounted } from "vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const matchesStore = useMatchesStore();
 | 
					const matchesStore = useMatchesStore();
 | 
				
			||||||
 | 
					const teamsStore = useTeamsStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { team, teamId } = useTeamDetails();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const matches = computed(() => matchesStore.teamMatches[teamId.value]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
onMounted(() => {
 | 
					onMounted(() => {
 | 
				
			||||||
  matchesStore.fetchMatches();
 | 
					  teamsStore.fetchTeam(teamId.value)
 | 
				
			||||||
 | 
					    .then(() => matchesStore.fetchMatchesForTeam(teamId.value));
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,7 +24,7 @@ onMounted(() => {
 | 
				
			||||||
    <div class="header">
 | 
					    <div class="header">
 | 
				
			||||||
      <h1>
 | 
					      <h1>
 | 
				
			||||||
        <i class="bi bi-trophy-fill margin"></i>
 | 
					        <i class="bi bi-trophy-fill margin"></i>
 | 
				
			||||||
        Matches you've played
 | 
					        Matches
 | 
				
			||||||
      </h1>
 | 
					      </h1>
 | 
				
			||||||
      <div class="button-group">
 | 
					      <div class="button-group">
 | 
				
			||||||
        <AddMatchDialog />
 | 
					        <AddMatchDialog />
 | 
				
			||||||
| 
						 | 
					@ -26,18 +35,20 @@ onMounted(() => {
 | 
				
			||||||
        <tr>
 | 
					        <tr>
 | 
				
			||||||
          <th>RED</th>
 | 
					          <th>RED</th>
 | 
				
			||||||
          <th>BLU</th>
 | 
					          <th>BLU</th>
 | 
				
			||||||
 | 
					          <th>Team</th>
 | 
				
			||||||
          <th>Match Date</th>
 | 
					          <th>Match Date</th>
 | 
				
			||||||
          <th>logs.tf URL</th>
 | 
					          <th>logs.tf URL</th>
 | 
				
			||||||
        </tr>
 | 
					        </tr>
 | 
				
			||||||
      </thead>
 | 
					      </thead>
 | 
				
			||||||
      <tbody>
 | 
					      <tbody>
 | 
				
			||||||
        <tr v-for="match in matchesStore.matches" :key="match.logsTfId">
 | 
					        <tr v-for="teamMatch in matches">
 | 
				
			||||||
          <td>{{ match.redScore }}</td>
 | 
					          <td>{{ teamMatch.match.redScore }}</td>
 | 
				
			||||||
          <td>{{ match.blueScore }}</td>
 | 
					          <td>{{ teamMatch.match.blueScore }}</td>
 | 
				
			||||||
          <td>{{ match.matchTime }}</td>
 | 
					          <td>{{ teamMatch.teamColor == 'Blue' ? 'BLU' : 'RED' }}</td>
 | 
				
			||||||
 | 
					          <td>{{ moment(teamMatch.match.matchTime).format("LL LT") }}</td>
 | 
				
			||||||
          <td>
 | 
					          <td>
 | 
				
			||||||
            <a :href="`https://logs.tf/${match.logsTfId}`" target="_blank">
 | 
					            <a :href="`https://logs.tf/${teamMatch.match.logsTfId}`" target="_blank">
 | 
				
			||||||
              #{{ match.logsTfId }}
 | 
					              #{{ teamMatch.match.logsTfId }}
 | 
				
			||||||
            </a>
 | 
					            </a>
 | 
				
			||||||
          </td>
 | 
					          </td>
 | 
				
			||||||
        </tr>
 | 
					        </tr>
 | 
				
			||||||
| 
						 | 
					@ -42,6 +42,9 @@ onMounted(() => {
 | 
				
			||||||
        <RouterLink class="tab" :to="{ name: 'team-settings/invites' }">
 | 
					        <RouterLink class="tab" :to="{ name: 'team-settings/invites' }">
 | 
				
			||||||
          Invites
 | 
					          Invites
 | 
				
			||||||
        </RouterLink>
 | 
					        </RouterLink>
 | 
				
			||||||
 | 
					        <RouterLink class="tab" :to="{ name: 'team-settings/matches' }">
 | 
				
			||||||
 | 
					          Matches
 | 
				
			||||||
 | 
					        </RouterLink>
 | 
				
			||||||
        <hr>
 | 
					        <hr>
 | 
				
			||||||
        <button class="destructive-on-hover icon-end" @click="leaveTeam">
 | 
					        <button class="destructive-on-hover icon-end" @click="leaveTeam">
 | 
				
			||||||
          Leave team
 | 
					          Leave team
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,7 @@ from flask_migrate import Migrate
 | 
				
			||||||
from flask_sqlalchemy import SQLAlchemy
 | 
					from flask_sqlalchemy import SQLAlchemy
 | 
				
			||||||
from sqlalchemy import MetaData
 | 
					from sqlalchemy import MetaData
 | 
				
			||||||
from sqlalchemy.orm import DeclarativeBase
 | 
					from sqlalchemy.orm import DeclarativeBase
 | 
				
			||||||
 | 
					from celery import Celery, Task
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BaseModel(DeclarativeBase):
 | 
					class BaseModel(DeclarativeBase):
 | 
				
			||||||
    pass
 | 
					    pass
 | 
				
			||||||
| 
						 | 
					@ -33,9 +34,11 @@ def connect_db_with_app(database_uri: str | None, include_migrate=True):
 | 
				
			||||||
        print("Creating tables if they do not exist")
 | 
					        print("Creating tables if they do not exist")
 | 
				
			||||||
        db.create_all()
 | 
					        db.create_all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def connect_celery_with_app():
 | 
					def connect_celery_with_app() -> Celery:
 | 
				
			||||||
 | 
					    if "celery" in app.extensions:
 | 
				
			||||||
 | 
					        return app.extensions["celery"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def celery_init_app(app):
 | 
					    def celery_init_app(app):
 | 
				
			||||||
        from celery import Celery, Task
 | 
					 | 
				
			||||||
        class FlaskTask(Task):
 | 
					        class FlaskTask(Task):
 | 
				
			||||||
            def __call__(self, *args: object, **kwargs: object) -> object:
 | 
					            def __call__(self, *args: object, **kwargs: object) -> object:
 | 
				
			||||||
                with app.app_context():
 | 
					                with app.app_context():
 | 
				
			||||||
| 
						 | 
					@ -55,7 +58,7 @@ def connect_celery_with_app():
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    app.config.from_prefixed_env()
 | 
					    app.config.from_prefixed_env()
 | 
				
			||||||
    celery_init_app(app)
 | 
					    return celery_init_app(app)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def create_app() -> Flask:
 | 
					def create_app() -> Flask:
 | 
				
			||||||
    return Flask(__name__)
 | 
					    return Flask(__name__)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,6 +14,11 @@ from models.player_team import PlayerTeam
 | 
				
			||||||
from models.team_integration import TeamLogsTfIntegration
 | 
					from models.team_integration import TeamLogsTfIntegration
 | 
				
			||||||
from celery import shared_task
 | 
					from celery import shared_task
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					celery = app_db.connect_celery_with_app()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@celery.on_after_configure.connect
 | 
				
			||||||
 | 
					def setup_periodic_task(sender, **kwargs):
 | 
				
			||||||
 | 
					    sender.add_periodic_task(300.0, etl_periodic.s(), name="Fetch logs every 5 minutes")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
FETCH_URL = "https://logs.tf/api/v1/log/{}"
 | 
					FETCH_URL = "https://logs.tf/api/v1/log/{}"
 | 
				
			||||||
SEARCH_URL = "https://logs.tf/api/v1/log?limit=25?offset={}"
 | 
					SEARCH_URL = "https://logs.tf/api/v1/log?limit=25?offset={}"
 | 
				
			||||||
| 
						 | 
					@ -26,10 +31,13 @@ def get_log_ids(last_log_id: int):
 | 
				
			||||||
        for summary in response.json()["logs"]:
 | 
					        for summary in response.json()["logs"]:
 | 
				
			||||||
            id: int = summary["id"]
 | 
					            id: int = summary["id"]
 | 
				
			||||||
            if id == last_log_id:
 | 
					            if id == last_log_id:
 | 
				
			||||||
                break
 | 
					                print("Reached last log ID", id)
 | 
				
			||||||
 | 
					                return
 | 
				
			||||||
            # yield models.match.RawLogSummary.from_response(summary)
 | 
					            # yield models.match.RawLogSummary.from_response(summary)
 | 
				
			||||||
            yield id
 | 
					            yield id
 | 
				
			||||||
            current = id
 | 
					            current = id
 | 
				
			||||||
 | 
					        sleep(5)
 | 
				
			||||||
 | 
					        break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def extract(log_id: int) -> models.match.RawLogDetails:
 | 
					def extract(log_id: int) -> models.match.RawLogDetails:
 | 
				
			||||||
    response = requests.get(FETCH_URL.format(log_id))
 | 
					    response = requests.get(FETCH_URL.format(log_id))
 | 
				
			||||||
| 
						 | 
					@ -68,20 +76,6 @@ def extract_steam_ids(players: dict[str, models.match.LogPlayer]):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@shared_task
 | 
					@shared_task
 | 
				
			||||||
def update_playtime(steam_ids: list[int]):
 | 
					def update_playtime(steam_ids: list[int]):
 | 
				
			||||||
    # update players with playtime (recalculate through aggregation)
 | 
					 | 
				
			||||||
    #subquery = (
 | 
					 | 
				
			||||||
    #    app_db.db.session.query(
 | 
					 | 
				
			||||||
    #        PlayerTeam.id,
 | 
					 | 
				
			||||||
    #        func.sum(Match.duration).label("total_playtime")
 | 
					 | 
				
			||||||
    #    )
 | 
					 | 
				
			||||||
    #    .join(PlayerMatch, PlayerMatch.player_id == PlayerTeam.player_id)
 | 
					 | 
				
			||||||
    #    .join(Match, PlayerMatch.match_id == Match.logs_tf_id)
 | 
					 | 
				
			||||||
    #    .join(TeamMatch, TeamMatch.match_id == Match.logs_tf_id)
 | 
					 | 
				
			||||||
    #    .where(PlayerTeam.player_id.in_(steam_ids))
 | 
					 | 
				
			||||||
    #    .where(PlayerTeam.team_id == TeamMatch.team_id)
 | 
					 | 
				
			||||||
    #    .group_by(PlayerTeam.id)
 | 
					 | 
				
			||||||
    #    .subquery()
 | 
					 | 
				
			||||||
    #)
 | 
					 | 
				
			||||||
    steam_ids_int = list(map(lambda x: int(x), steam_ids))
 | 
					    steam_ids_int = list(map(lambda x: int(x), steam_ids))
 | 
				
			||||||
    ptp = (
 | 
					    ptp = (
 | 
				
			||||||
        select(
 | 
					        select(
 | 
				
			||||||
| 
						 | 
					@ -121,18 +115,13 @@ def update_playtime(steam_ids: list[int]):
 | 
				
			||||||
    app_db.db.session.commit()
 | 
					    app_db.db.session.commit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_common_teams(steam_ids: list[str]):
 | 
					def get_common_teams(steam_ids: list[int]):
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
        app_db.db.session.query(
 | 
					        app_db.db.session.query(
 | 
				
			||||||
            PlayerTeam.team_id,
 | 
					            PlayerTeam.team_id,
 | 
				
			||||||
            func.count(PlayerTeam.team_id),
 | 
					            func.count(PlayerTeam.team_id),
 | 
				
			||||||
            TeamLogsTfIntegration.min_team_member_count,
 | 
					 | 
				
			||||||
            #aggregate_func
 | 
					            #aggregate_func
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        .outerjoin(
 | 
					 | 
				
			||||||
            TeamLogsTfIntegration,
 | 
					 | 
				
			||||||
            TeamLogsTfIntegration.team_id == PlayerTeam.team_id
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .where(PlayerTeam.player_id.in_(steam_ids))
 | 
					        .where(PlayerTeam.player_id.in_(steam_ids))
 | 
				
			||||||
        .group_by(PlayerTeam.team_id)
 | 
					        .group_by(PlayerTeam.team_id)
 | 
				
			||||||
        .order_by(func.count(PlayerTeam.team_id).desc())
 | 
					        .order_by(func.count(PlayerTeam.team_id).desc())
 | 
				
			||||||
| 
						 | 
					@ -154,9 +143,6 @@ def transform(
 | 
				
			||||||
        .all()
 | 
					        .all()
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if len(players) == 0:
 | 
					 | 
				
			||||||
        return
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if not existing_match:
 | 
					    if not existing_match:
 | 
				
			||||||
        match = Match()
 | 
					        match = Match()
 | 
				
			||||||
        match.logs_tf_id = log_id
 | 
					        match.logs_tf_id = log_id
 | 
				
			||||||
| 
						 | 
					@ -169,7 +155,8 @@ def transform(
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        match = existing_match
 | 
					        match = existing_match
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    #app_db.db.session.add(match)
 | 
					    if len(players) == 0:
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for player in players:
 | 
					    for player in players:
 | 
				
			||||||
        player_data = details["players"][steam64_to_steam3(player.steam_id)]
 | 
					        player_data = details["players"][steam64_to_steam3(player.steam_id)]
 | 
				
			||||||
| 
						 | 
					@ -196,7 +183,9 @@ def transform(
 | 
				
			||||||
            row_tuple = tuple(row)
 | 
					            row_tuple = tuple(row)
 | 
				
			||||||
            team_id = row_tuple[0]
 | 
					            team_id = row_tuple[0]
 | 
				
			||||||
            player_count = row_tuple[1]
 | 
					            player_count = row_tuple[1]
 | 
				
			||||||
            log_min_player_count = row_tuple[2] or 100
 | 
					            log_min_player_count = app_db.db.session.query(
 | 
				
			||||||
 | 
					                TeamLogsTfIntegration.min_team_member_count
 | 
				
			||||||
 | 
					            ).where(TeamLogsTfIntegration.team_id == team_id).one_or_none() or 100
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            should_create_team_match = False
 | 
					            should_create_team_match = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -230,18 +219,22 @@ def load_specific_match(id: int, team_id: int | None):
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    raw_match = extract(id)
 | 
					    raw_match = extract(id)
 | 
				
			||||||
 | 
					    print("Loading match: " + str(id))
 | 
				
			||||||
    app_db.db.session.bulk_save_objects(transform(id, raw_match, match, team_id))
 | 
					    app_db.db.session.bulk_save_objects(transform(id, raw_match, match, team_id))
 | 
				
			||||||
    app_db.db.session.commit()
 | 
					    app_db.db.session.commit()
 | 
				
			||||||
    sleep(3) # avoid rate limiting if multiple tasks are queued
 | 
					    sleep(3)  # avoid rate limiting if multiple tasks are queued
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def main():
 | 
					@celery.task
 | 
				
			||||||
 | 
					def etl_periodic():
 | 
				
			||||||
    last: int = (
 | 
					    last: int = (
 | 
				
			||||||
        app_db.db.session.query(
 | 
					        app_db.db.session.query(
 | 
				
			||||||
            func.max(models.match.Match.logs_tf_id)
 | 
					            func.max(models.match.Match.logs_tf_id)
 | 
				
			||||||
        ).scalar()
 | 
					        ).scalar()
 | 
				
			||||||
    ) or 3767233
 | 
					    ) or 3768715
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for summary in get_log_ids(last):
 | 
					    print("Last log ID: " + str(last))
 | 
				
			||||||
        print(summary)
 | 
					
 | 
				
			||||||
        sleep(3)
 | 
					    for id in get_log_ids(last):
 | 
				
			||||||
 | 
					        print("Found log: " + str(id))
 | 
				
			||||||
 | 
					        load_specific_match.delay(id, None)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,4 @@
 | 
				
			||||||
 | 
					from typing import Optional
 | 
				
			||||||
from flask import Blueprint, abort
 | 
					from flask import Blueprint, abort
 | 
				
			||||||
from pydantic.v1 import validator
 | 
					from pydantic.v1 import validator
 | 
				
			||||||
#from pydantic.functional_validators import field_validator
 | 
					#from pydantic.functional_validators import field_validator
 | 
				
			||||||
| 
						 | 
					@ -41,6 +42,7 @@ def get_match(player: Player, match_id: int, **_):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SubmitMatchJson(BaseModel):
 | 
					class SubmitMatchJson(BaseModel):
 | 
				
			||||||
    match_ids: list[int]
 | 
					    match_ids: list[int]
 | 
				
			||||||
 | 
					    team_id: int
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @validator("match_ids")
 | 
					    @validator("match_ids")
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
| 
						 | 
					@ -59,15 +61,13 @@ class SubmitMatchJson(BaseModel):
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@requires_authentication
 | 
					@requires_authentication
 | 
				
			||||||
def submit_match(json: SubmitMatchJson, **_):
 | 
					def submit_match(json: SubmitMatchJson, **_):
 | 
				
			||||||
    import sys
 | 
					 | 
				
			||||||
    print(json, file=sys.stderr)
 | 
					 | 
				
			||||||
    if json.match_ids is None:
 | 
					 | 
				
			||||||
        print("json.match_ids is None", file=sys.stderr)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for id in json.match_ids:
 | 
					    for id in json.match_ids:
 | 
				
			||||||
        load_specific_match.delay(id, None)
 | 
					        load_specific_match.delay(id, json.team_id)
 | 
				
			||||||
    return { }, 204
 | 
					    return { }, 204
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class GetMatchQuery(BaseModel):
 | 
				
			||||||
 | 
					    limit: Optional[int]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@api_match.get("/team/<int:team_id>")
 | 
					@api_match.get("/team/<int:team_id>")
 | 
				
			||||||
@spec.validate(
 | 
					@spec.validate(
 | 
				
			||||||
    resp=Response(
 | 
					    resp=Response(
 | 
				
			||||||
| 
						 | 
					@ -77,14 +77,19 @@ def submit_match(json: SubmitMatchJson, **_):
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@requires_authentication
 | 
					@requires_authentication
 | 
				
			||||||
@requires_team_membership()
 | 
					@requires_team_membership()
 | 
				
			||||||
def get_matches_for_team(team_id: Team, **_):
 | 
					def get_matches_for_team(team_id: Team, query: GetMatchQuery, **_):
 | 
				
			||||||
    matches = (
 | 
					    q = (
 | 
				
			||||||
        db.session.query(TeamMatch)
 | 
					        db.session.query(TeamMatch)
 | 
				
			||||||
        .where(TeamMatch.team_id == team_id)
 | 
					        .where(TeamMatch.team_id == team_id)
 | 
				
			||||||
        .options(joinedload(TeamMatch.match))
 | 
					        .options(joinedload(TeamMatch.match))
 | 
				
			||||||
        .all()
 | 
					        .order_by(TeamMatch.match_id.desc())
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if query and query.limit:
 | 
				
			||||||
 | 
					        q = q.limit(query.limit)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    matches = q.all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return [TeamMatchSchema.from_model(match).dict(by_alias=True) for match in matches], 200
 | 
					    return [TeamMatchSchema.from_model(match).dict(by_alias=True) for match in matches], 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@api_match.get("/player")
 | 
					@api_match.get("/player")
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,6 +19,7 @@ class TeamMatchSchema(spec.BaseModel):
 | 
				
			||||||
    match: "MatchSchema"
 | 
					    match: "MatchSchema"
 | 
				
			||||||
    our_score: int
 | 
					    our_score: int
 | 
				
			||||||
    their_score: int
 | 
					    their_score: int
 | 
				
			||||||
 | 
					    team_color: str
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def from_model(cls, model: "TeamMatch"):
 | 
					    def from_model(cls, model: "TeamMatch"):
 | 
				
			||||||
| 
						 | 
					@ -29,6 +30,7 @@ class TeamMatchSchema(spec.BaseModel):
 | 
				
			||||||
            match=MatchSchema.from_model(model.match),
 | 
					            match=MatchSchema.from_model(model.match),
 | 
				
			||||||
            our_score=our_score,
 | 
					            our_score=our_score,
 | 
				
			||||||
            their_score=their_score,
 | 
					            their_score=their_score,
 | 
				
			||||||
 | 
					            team_color=model.team_color,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -135,7 +135,7 @@ 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/<int:team_id>/player/<int:target_player_id>/")
 | 
					@api_team.delete("/id/<int:team_id>/player/<target_player_id>/")
 | 
				
			||||||
@spec.validate(
 | 
					@spec.validate(
 | 
				
			||||||
    resp=Response(
 | 
					    resp=Response(
 | 
				
			||||||
        HTTP_200=None,
 | 
					        HTTP_200=None,
 | 
				
			||||||
| 
						 | 
					@ -145,7 +145,8 @@ def delete_team(player: Player, team_id: int):
 | 
				
			||||||
    operation_id="remove_player_from_team"
 | 
					    operation_id="remove_player_from_team"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@requires_authentication
 | 
					@requires_authentication
 | 
				
			||||||
def remove_player_from_team(player: Player, team_id: int, target_player_id: int, **kwargs):
 | 
					def remove_player_from_team(player: Player, team_id: int, target_player_id: str, **kwargs):
 | 
				
			||||||
 | 
					    target_player_id: int = int(target_player_id)
 | 
				
			||||||
    player_team = db.session.query(
 | 
					    player_team = db.session.query(
 | 
				
			||||||
        PlayerTeam
 | 
					        PlayerTeam
 | 
				
			||||||
    ).where(
 | 
					    ).where(
 | 
				
			||||||
| 
						 | 
					@ -202,7 +203,7 @@ 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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@api_team.put("/id/<int:team_id>/player/<int:player_id>/")
 | 
					@api_team.put("/id/<int:team_id>/player/<player_id>/")
 | 
				
			||||||
@spec.validate(
 | 
					@spec.validate(
 | 
				
			||||||
    resp=Response(
 | 
					    resp=Response(
 | 
				
			||||||
        HTTP_200=None,
 | 
					        HTTP_200=None,
 | 
				
			||||||
| 
						 | 
					@ -211,7 +212,8 @@ class AddPlayerJson(BaseModel):
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    operation_id="create_or_update_player"
 | 
					    operation_id="create_or_update_player"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
def add_player(player: Player, team_id: int, player_id: int, json: AddPlayerJson):
 | 
					def add_player(player: Player, team_id: int, player_id: str, json: AddPlayerJson):
 | 
				
			||||||
 | 
					    player_id: int = int(player_id)
 | 
				
			||||||
    player_team = db.session.query(
 | 
					    player_team = db.session.query(
 | 
				
			||||||
        PlayerTeam
 | 
					        PlayerTeam
 | 
				
			||||||
    ).where(
 | 
					    ).where(
 | 
				
			||||||
| 
						 | 
					@ -387,7 +389,7 @@ def view_team_members(player: Player, team_id: int, **kwargs):
 | 
				
			||||||
class EditMemberRolesJson(BaseModel):
 | 
					class EditMemberRolesJson(BaseModel):
 | 
				
			||||||
    roles: list[RoleSchema]
 | 
					    roles: list[RoleSchema]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@api_team.patch("/id/<int:team_id>/edit-player/<int:target_player_id>")
 | 
					@api_team.patch("/id/<int:team_id>/edit-player/<target_player_id>")
 | 
				
			||||||
@spec.validate(
 | 
					@spec.validate(
 | 
				
			||||||
    resp=Response(
 | 
					    resp=Response(
 | 
				
			||||||
        HTTP_204=None,
 | 
					        HTTP_204=None,
 | 
				
			||||||
| 
						 | 
					@ -401,9 +403,10 @@ def edit_member_roles(
 | 
				
			||||||
    json: EditMemberRolesJson,
 | 
					    json: EditMemberRolesJson,
 | 
				
			||||||
    player: Player,
 | 
					    player: Player,
 | 
				
			||||||
    team_id: int,
 | 
					    team_id: int,
 | 
				
			||||||
    target_player_id: int,
 | 
					    target_player_id: str,
 | 
				
			||||||
    **kwargs,
 | 
					    **kwargs,
 | 
				
			||||||
):
 | 
					):
 | 
				
			||||||
 | 
					    target_player_id: int = int(target_player_id)
 | 
				
			||||||
    target_player = db.session.query(
 | 
					    target_player = db.session.query(
 | 
				
			||||||
        PlayerTeam
 | 
					        PlayerTeam
 | 
				
			||||||
    ).where(
 | 
					    ).where(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,7 +20,7 @@ services:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # Flask service
 | 
					  # Flask service
 | 
				
			||||||
  backend:
 | 
					  backend:
 | 
				
			||||||
    container_name: backend
 | 
					    container_name: backend-production
 | 
				
			||||||
    command: ["gunicorn", "-w", "4", "app:app", "-b", "0.0.0.0:5000"]
 | 
					    command: ["gunicorn", "-w", "4", "app:app", "-b", "0.0.0.0:5000"]
 | 
				
			||||||
    image: backend-flask-production
 | 
					    image: backend-flask-production
 | 
				
			||||||
    ports:
 | 
					    ports:
 | 
				
			||||||
| 
						 | 
					@ -42,13 +42,13 @@ services:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # ETL job (runs with the same source as the backend)
 | 
					  # ETL job (runs with the same source as the backend)
 | 
				
			||||||
  celery-worker:
 | 
					  celery-worker:
 | 
				
			||||||
    container_name: worker
 | 
					    container_name: worker-production
 | 
				
			||||||
    command: celery -A make_celery.celery_app worker --loglevel=info --concurrency=1
 | 
					    command: celery -A make_celery.celery_app worker --loglevel=info --concurrency=1 -B
 | 
				
			||||||
 | 
					    image: backend-flask-production
 | 
				
			||||||
    environment:
 | 
					    environment:
 | 
				
			||||||
      - CELERY_BROKER_URL=redis://redis:6379/0
 | 
					      - CELERY_BROKER_URL=redis://redis:6379/0
 | 
				
			||||||
      - CELERY_RESULT_BACKEND=redis://redis:6379/0
 | 
					      - CELERY_RESULT_BACKEND=redis://redis:6379/0
 | 
				
			||||||
      - DATABASE_URI=postgresql+psycopg://db:5432
 | 
					      - DATABASE_URI=postgresql+psycopg://postgres:password@db:5432/availabilitf
 | 
				
			||||||
    image: backend-flask-production
 | 
					 | 
				
			||||||
    volumes:
 | 
					    volumes:
 | 
				
			||||||
      - ./backend-flask:/app
 | 
					      - ./backend-flask:/app
 | 
				
			||||||
    networks:
 | 
					    networks:
 | 
				
			||||||
| 
						 | 
					@ -68,7 +68,8 @@ services:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # Vue + Vite service
 | 
					  # Vue + Vite service
 | 
				
			||||||
  frontend:
 | 
					  frontend:
 | 
				
			||||||
    container_name: frontend
 | 
					    container_name: frontend-production
 | 
				
			||||||
 | 
					    image: frontend-production
 | 
				
			||||||
    build:
 | 
					    build:
 | 
				
			||||||
      context: ./availabili.tf
 | 
					      context: ./availabili.tf
 | 
				
			||||||
      dockerfile: Dockerfile.prod
 | 
					      dockerfile: Dockerfile.prod
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,7 +22,7 @@ services:
 | 
				
			||||||
  # ETL job (runs with the same source as the backend)
 | 
					  # ETL job (runs with the same source as the backend)
 | 
				
			||||||
  celery-worker:
 | 
					  celery-worker:
 | 
				
			||||||
    container_name: worker
 | 
					    container_name: worker
 | 
				
			||||||
    command: celery -A make_celery.celery_app worker --loglevel=info --concurrency=1
 | 
					    command: celery -A make_celery.celery_app worker --loglevel=info --concurrency=1 -B
 | 
				
			||||||
    environment:
 | 
					    environment:
 | 
				
			||||||
      - CELERY_BROKER_URL=redis://redis:6379/0
 | 
					      - CELERY_BROKER_URL=redis://redis:6379/0
 | 
				
			||||||
      - CELERY_RESULT_BACKEND=redis://redis:6379/0
 | 
					      - CELERY_RESULT_BACKEND=redis://redis:6379/0
 | 
				
			||||||
| 
						 | 
					@ -51,6 +51,8 @@ services:
 | 
				
			||||||
      context: ./availabili.tf
 | 
					      context: ./availabili.tf
 | 
				
			||||||
    environment:
 | 
					    environment:
 | 
				
			||||||
      VITE_API_URL: http://localhost:8000  # API endpoint
 | 
					      VITE_API_URL: http://localhost:8000  # API endpoint
 | 
				
			||||||
 | 
					    ports:
 | 
				
			||||||
 | 
					      - 5173:5173
 | 
				
			||||||
    volumes:
 | 
					    volumes:
 | 
				
			||||||
      - ./availabili.tf:/app
 | 
					      - ./availabili.tf:/app
 | 
				
			||||||
    networks:
 | 
					    networks:
 | 
				
			||||||
| 
						 | 
					@ -62,7 +64,7 @@ services:
 | 
				
			||||||
    ports:
 | 
					    ports:
 | 
				
			||||||
      - "8000:80"
 | 
					      - "8000:80"
 | 
				
			||||||
    volumes:
 | 
					    volumes:
 | 
				
			||||||
      - ./nginx:/etc/nginx/conf.d
 | 
					      - ./nginx/development.conf:/etc/nginx/nginx.conf
 | 
				
			||||||
    depends_on:
 | 
					    depends_on:
 | 
				
			||||||
      - backend
 | 
					      - backend
 | 
				
			||||||
      - frontend
 | 
					      - frontend
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,24 +0,0 @@
 | 
				
			||||||
server {
 | 
					 | 
				
			||||||
    listen 80;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Proxy for the Vite frontend
 | 
					 | 
				
			||||||
    location / {
 | 
					 | 
				
			||||||
        proxy_pass http://frontend:5173;
 | 
					 | 
				
			||||||
        proxy_set_header Host $host;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        proxy_http_version 1.1;
 | 
					 | 
				
			||||||
        proxy_set_header Upgrade $http_upgrade;
 | 
					 | 
				
			||||||
        proxy_set_header Connection "upgrade";
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Proxy for the Flask backend API
 | 
					 | 
				
			||||||
    location /api/ {
 | 
					 | 
				
			||||||
        proxy_pass http://backend:5000;
 | 
					 | 
				
			||||||
        proxy_set_header Host $host;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    location /apidoc/ {
 | 
					 | 
				
			||||||
        proxy_pass http://backend:5000;
 | 
					 | 
				
			||||||
        proxy_set_header Host $host;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,30 @@
 | 
				
			||||||
 | 
					events {
 | 
				
			||||||
 | 
					    worker_connections 1024;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					http {
 | 
				
			||||||
 | 
					    server {
 | 
				
			||||||
 | 
					        listen 80;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Proxy for the Vite frontend
 | 
				
			||||||
 | 
					        location / {
 | 
				
			||||||
 | 
					            proxy_pass http://frontend:5173;
 | 
				
			||||||
 | 
					            proxy_set_header Host $host;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            proxy_http_version 1.1;
 | 
				
			||||||
 | 
					            proxy_set_header Upgrade $http_upgrade;
 | 
				
			||||||
 | 
					            proxy_set_header Connection "upgrade";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Proxy for the Flask backend API
 | 
				
			||||||
 | 
					        location /api/ {
 | 
				
			||||||
 | 
					            proxy_pass http://backend:5000;
 | 
				
			||||||
 | 
					            proxy_set_header Host $host;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        location /apidoc/ {
 | 
				
			||||||
 | 
					            proxy_pass http://backend:5000;
 | 
				
			||||||
 | 
					            proxy_set_header Host $host;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
		Reference in New Issue