feat(frontend): Implement matches
							parent
							
								
									45ac071a7f
								
							
						
					
					
						commit
						caaee983f2
					
				| 
						 | 
				
			
			@ -21,6 +21,7 @@ export type { EventSchema } from './models/EventSchema';
 | 
			
		|||
export type { EventWithPlayerSchema } from './models/EventWithPlayerSchema';
 | 
			
		||||
export type { EventWithPlayerSchemaList } from './models/EventWithPlayerSchemaList';
 | 
			
		||||
export type { GetEventPlayersResponse } from './models/GetEventPlayersResponse';
 | 
			
		||||
export type { MatchSchema } from './models/MatchSchema';
 | 
			
		||||
export type { PlayerEventRolesSchema } from './models/PlayerEventRolesSchema';
 | 
			
		||||
export type { PlayerRoleSchema } from './models/PlayerRoleSchema';
 | 
			
		||||
export type { PlayerSchema } from './models/PlayerSchema';
 | 
			
		||||
| 
						 | 
				
			
			@ -28,11 +29,14 @@ export type { PlayerTeamAvailabilityRoleSchema } from './models/PlayerTeamAvaila
 | 
			
		|||
export type { PutScheduleForm } from './models/PutScheduleForm';
 | 
			
		||||
export type { RoleSchema } from './models/RoleSchema';
 | 
			
		||||
export type { SetUsernameJson } from './models/SetUsernameJson';
 | 
			
		||||
export type { SubmitMatchJson } from './models/SubmitMatchJson';
 | 
			
		||||
export type { TeamDiscordIntegrationSchema } from './models/TeamDiscordIntegrationSchema';
 | 
			
		||||
export type { TeamIntegrationSchema } from './models/TeamIntegrationSchema';
 | 
			
		||||
export type { TeamInviteSchema } from './models/TeamInviteSchema';
 | 
			
		||||
export type { TeamInviteSchemaList } from './models/TeamInviteSchemaList';
 | 
			
		||||
export type { TeamLogsTfIntegrationSchema } from './models/TeamLogsTfIntegrationSchema';
 | 
			
		||||
export type { TeamMatchSchema } from './models/TeamMatchSchema';
 | 
			
		||||
export type { TeamMatchSchemaList } from './models/TeamMatchSchemaList';
 | 
			
		||||
export { TeamRole } from './models/TeamRole';
 | 
			
		||||
export type { TeamSchema } from './models/TeamSchema';
 | 
			
		||||
export type { TeamWithRoleSchema } from './models/TeamWithRoleSchema';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,14 @@
 | 
			
		|||
/* generated using openapi-typescript-codegen -- do not edit */
 | 
			
		||||
/* istanbul ignore file */
 | 
			
		||||
/* tslint:disable */
 | 
			
		||||
/* eslint-disable */
 | 
			
		||||
export type MatchSchema = {
 | 
			
		||||
    blueScore: number;
 | 
			
		||||
    createdAt: string;
 | 
			
		||||
    duration: string;
 | 
			
		||||
    logsTfId: number;
 | 
			
		||||
    logsTfTitle: string;
 | 
			
		||||
    matchTime: string;
 | 
			
		||||
    redScore: number;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
/* generated using openapi-typescript-codegen -- do not edit */
 | 
			
		||||
/* istanbul ignore file */
 | 
			
		||||
/* tslint:disable */
 | 
			
		||||
/* eslint-disable */
 | 
			
		||||
export type SubmitMatchJson = {
 | 
			
		||||
    matchIds: Array<number>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
/* generated using openapi-typescript-codegen -- do not edit */
 | 
			
		||||
/* istanbul ignore file */
 | 
			
		||||
/* tslint:disable */
 | 
			
		||||
/* eslint-disable */
 | 
			
		||||
import type { MatchSchema } from './MatchSchema';
 | 
			
		||||
export type TeamMatchSchema = {
 | 
			
		||||
    match: MatchSchema;
 | 
			
		||||
    ourScore: number;
 | 
			
		||||
    theirScore: number;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
/* generated using openapi-typescript-codegen -- do not edit */
 | 
			
		||||
/* istanbul ignore file */
 | 
			
		||||
/* tslint:disable */
 | 
			
		||||
/* eslint-disable */
 | 
			
		||||
import type { TeamMatchSchema } from './TeamMatchSchema';
 | 
			
		||||
export type TeamMatchSchemaList = Array<TeamMatchSchema>;
 | 
			
		||||
| 
						 | 
				
			
			@ -12,12 +12,15 @@ import type { EventSchema } from '../models/EventSchema';
 | 
			
		|||
import type { EventWithPlayerSchema } from '../models/EventWithPlayerSchema';
 | 
			
		||||
import type { EventWithPlayerSchemaList } from '../models/EventWithPlayerSchemaList';
 | 
			
		||||
import type { GetEventPlayersResponse } from '../models/GetEventPlayersResponse';
 | 
			
		||||
import type { MatchSchema } from '../models/MatchSchema';
 | 
			
		||||
import type { PlayerSchema } from '../models/PlayerSchema';
 | 
			
		||||
import type { PutScheduleForm } from '../models/PutScheduleForm';
 | 
			
		||||
import type { SetUsernameJson } from '../models/SetUsernameJson';
 | 
			
		||||
import type { SubmitMatchJson } from '../models/SubmitMatchJson';
 | 
			
		||||
import type { TeamIntegrationSchema } from '../models/TeamIntegrationSchema';
 | 
			
		||||
import type { TeamInviteSchema } from '../models/TeamInviteSchema';
 | 
			
		||||
import type { TeamInviteSchemaList } from '../models/TeamInviteSchemaList';
 | 
			
		||||
import type { TeamMatchSchemaList } from '../models/TeamMatchSchemaList';
 | 
			
		||||
import type { TeamSchema } from '../models/TeamSchema';
 | 
			
		||||
import type { UpdateEventJson } from '../models/UpdateEventJson';
 | 
			
		||||
import type { ViewAvailablePlayersResponse } from '../models/ViewAvailablePlayersResponse';
 | 
			
		||||
| 
						 | 
				
			
			@ -289,6 +292,79 @@ export class DefaultService {
 | 
			
		|||
            },
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    /**
 | 
			
		||||
     * submit_match <PUT>
 | 
			
		||||
     * @param requestBody
 | 
			
		||||
     * @returns void
 | 
			
		||||
     * @throws ApiError
 | 
			
		||||
     */
 | 
			
		||||
    public submitMatch(
 | 
			
		||||
        requestBody?: SubmitMatchJson,
 | 
			
		||||
    ): CancelablePromise<void> {
 | 
			
		||||
        return this.httpRequest.request({
 | 
			
		||||
            method: 'PUT',
 | 
			
		||||
            url: '/api/match/',
 | 
			
		||||
            body: requestBody,
 | 
			
		||||
            mediaType: 'application/json',
 | 
			
		||||
            errors: {
 | 
			
		||||
                422: `Unprocessable Content`,
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    /**
 | 
			
		||||
     * get_match <GET>
 | 
			
		||||
     * @param matchId
 | 
			
		||||
     * @returns MatchSchema OK
 | 
			
		||||
     * @throws ApiError
 | 
			
		||||
     */
 | 
			
		||||
    public getApiMatchIdMatchId(
 | 
			
		||||
        matchId: number,
 | 
			
		||||
    ): CancelablePromise<MatchSchema> {
 | 
			
		||||
        return this.httpRequest.request({
 | 
			
		||||
            method: 'GET',
 | 
			
		||||
            url: '/api/match/id/{match_id}',
 | 
			
		||||
            path: {
 | 
			
		||||
                'match_id': matchId,
 | 
			
		||||
            },
 | 
			
		||||
            errors: {
 | 
			
		||||
                422: `Unprocessable Content`,
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    /**
 | 
			
		||||
     * get_matches_for_player_teams <GET>
 | 
			
		||||
     * @returns TeamMatchSchemaList OK
 | 
			
		||||
     * @throws ApiError
 | 
			
		||||
     */
 | 
			
		||||
    public getMatchesForPlayerTeams(): CancelablePromise<TeamMatchSchemaList> {
 | 
			
		||||
        return this.httpRequest.request({
 | 
			
		||||
            method: 'GET',
 | 
			
		||||
            url: '/api/match/player',
 | 
			
		||||
            errors: {
 | 
			
		||||
                422: `Unprocessable Content`,
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    /**
 | 
			
		||||
     * get_matches_for_team <GET>
 | 
			
		||||
     * @param teamId
 | 
			
		||||
     * @returns TeamMatchSchemaList OK
 | 
			
		||||
     * @throws ApiError
 | 
			
		||||
     */
 | 
			
		||||
    public getMatchesForTeam(
 | 
			
		||||
        teamId: number,
 | 
			
		||||
    ): CancelablePromise<TeamMatchSchemaList> {
 | 
			
		||||
        return this.httpRequest.request({
 | 
			
		||||
            method: 'GET',
 | 
			
		||||
            url: '/api/match/team/{team_id}',
 | 
			
		||||
            path: {
 | 
			
		||||
                'team_id': teamId,
 | 
			
		||||
            },
 | 
			
		||||
            errors: {
 | 
			
		||||
                422: `Unprocessable Content`,
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    /**
 | 
			
		||||
     * get <GET>
 | 
			
		||||
     * @param windowStart
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,63 @@
 | 
			
		|||
<script setup lang="ts">
 | 
			
		||||
import { useMatchesStore } from "@/stores/matches";
 | 
			
		||||
import { DialogClose, DialogContent, DialogDescription, DialogOverlay, DialogPortal, DialogRoot, DialogTitle, DialogTrigger } from "radix-vue";
 | 
			
		||||
import { ref } from "vue";
 | 
			
		||||
 | 
			
		||||
const matchesStore = useMatchesStore();
 | 
			
		||||
 | 
			
		||||
const urlsText = ref("");
 | 
			
		||||
 | 
			
		||||
function submit() {
 | 
			
		||||
  const ids = urlsText.value.split("\n")
 | 
			
		||||
    .map((url) => {
 | 
			
		||||
      const matchId = url.match(/logs\.tf\/(\d+)/);
 | 
			
		||||
      return matchId ? Number(matchId[1]) : NaN;
 | 
			
		||||
    })
 | 
			
		||||
    .filter((id) => !isNaN(id));
 | 
			
		||||
 | 
			
		||||
  matchesStore.submitMatches(ids);
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <DialogRoot>
 | 
			
		||||
    <DialogTrigger>
 | 
			
		||||
      <i class="bi bi-file-earmark-plus-fill margin" />
 | 
			
		||||
      Submit logs.tf matches
 | 
			
		||||
    </DialogTrigger>
 | 
			
		||||
    <DialogPortal>
 | 
			
		||||
      <DialogOverlay class="dialog-overlay" />
 | 
			
		||||
      <DialogContent>
 | 
			
		||||
        <DialogTitle>Submit logs.tf matches</DialogTitle>
 | 
			
		||||
        <DialogDescription>
 | 
			
		||||
          <p>
 | 
			
		||||
            Enter up to 10 logs.tf URLs (or match IDs) to submit them. This
 | 
			
		||||
            allows you to track your match stats and view them later.
 | 
			
		||||
          </p>
 | 
			
		||||
        </DialogDescription>
 | 
			
		||||
        <div class="form-group margin">
 | 
			
		||||
          <h3>logs.tf URLs</h3>
 | 
			
		||||
          <textarea
 | 
			
		||||
            v-model="urlsText"
 | 
			
		||||
            placeholder="Paste logs.tf URLs here (limit: 10)"
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="form-group">
 | 
			
		||||
          <div class="action-buttons">
 | 
			
		||||
            <DialogClose class="accent" aria-label="Close" @click="submit">
 | 
			
		||||
              <i class="bi bi-check" />
 | 
			
		||||
              Submit
 | 
			
		||||
            </DialogClose>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </DialogContent>
 | 
			
		||||
    </DialogPortal>
 | 
			
		||||
  </DialogRoot>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
[role="dialog"] {
 | 
			
		||||
  padding: 2rem;
 | 
			
		||||
  border-radius: 0.5rem;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -12,6 +12,7 @@ import TeamSettingsGeneralView from "@/views/TeamSettings/GeneralView.vue";
 | 
			
		|||
import TeamSettingsIntegrationsView from "@/views/TeamSettings/IntegrationsView.vue";
 | 
			
		||||
import TeamSettingsInvitesView from "@/views/TeamSettings/InvitesView.vue";
 | 
			
		||||
import UserSettingsView from "@/views/UserSettingsView.vue";
 | 
			
		||||
import MatchesView from "@/views/MatchesView.vue";
 | 
			
		||||
 | 
			
		||||
const router = createRouter({
 | 
			
		||||
  history: createWebHistory(import.meta.env.BASE_URL),
 | 
			
		||||
| 
						 | 
				
			
			@ -78,6 +79,11 @@ const router = createRouter({
 | 
			
		|||
      name: "user-settings",
 | 
			
		||||
      component: UserSettingsView,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: "/matches",
 | 
			
		||||
      name: "matches",
 | 
			
		||||
      component: MatchesView,
 | 
			
		||||
    },
 | 
			
		||||
  ]
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,38 @@
 | 
			
		|||
import type { MatchSchema, TeamMatchSchema } from "@/client";
 | 
			
		||||
import { defineStore } from "pinia";
 | 
			
		||||
import { ref } from "vue";
 | 
			
		||||
import { useClientStore } from "./client";
 | 
			
		||||
 | 
			
		||||
export const useMatchesStore = defineStore("matches", () => {
 | 
			
		||||
  const clientStore = useClientStore();
 | 
			
		||||
  const client = clientStore.client;
 | 
			
		||||
 | 
			
		||||
  const matches = ref<{ [id: number]: MatchSchema }>({ });
 | 
			
		||||
 | 
			
		||||
  const teamMatches = ref<{ [id: number]: TeamMatchSchema }>({ });
 | 
			
		||||
 | 
			
		||||
  function fetchMatches() {
 | 
			
		||||
    return clientStore.call(
 | 
			
		||||
      fetchMatches.name,
 | 
			
		||||
      () => client.default.getMatchesForPlayerTeams(),
 | 
			
		||||
      (response) => {
 | 
			
		||||
        response.forEach((match) => {
 | 
			
		||||
          matches.value[match.match.logsTfId] = match.match;
 | 
			
		||||
          teamMatches.value[match.match.logsTfId] = match;
 | 
			
		||||
        });
 | 
			
		||||
        return response;
 | 
			
		||||
      }
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function submitMatches(logsTfIds: number[]) {
 | 
			
		||||
    return client.default.submitMatch({ matchIds: logsTfIds });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    matches,
 | 
			
		||||
    teamMatches,
 | 
			
		||||
    fetchMatches,
 | 
			
		||||
    submitMatches,
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,70 @@
 | 
			
		|||
<script setup lang="ts">
 | 
			
		||||
import AddMatchDialog from "@/components/AddMatchDialog.vue";
 | 
			
		||||
import { useMatchesStore } from "@/stores/matches";
 | 
			
		||||
import { onMounted } from "vue";
 | 
			
		||||
 | 
			
		||||
const matchesStore = useMatchesStore();
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  matchesStore.fetchMatches();
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <main>
 | 
			
		||||
    <div class="header">
 | 
			
		||||
      <h1>
 | 
			
		||||
        <i class="bi bi-trophy-fill margin"></i>
 | 
			
		||||
        Matches you've played
 | 
			
		||||
      </h1>
 | 
			
		||||
      <div class="button-group">
 | 
			
		||||
        <AddMatchDialog />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <table>
 | 
			
		||||
      <thead>
 | 
			
		||||
        <tr>
 | 
			
		||||
          <th>RED</th>
 | 
			
		||||
          <th>BLU</th>
 | 
			
		||||
          <th>Match Date</th>
 | 
			
		||||
          <th>logs.tf URL</th>
 | 
			
		||||
        </tr>
 | 
			
		||||
      </thead>
 | 
			
		||||
      <tbody>
 | 
			
		||||
        <tr v-for="match in matchesStore.matches" :key="match.logsTfId">
 | 
			
		||||
          <td>{{ match.redScore }}</td>
 | 
			
		||||
          <td>{{ match.blueScore }}</td>
 | 
			
		||||
          <td>{{ match.matchTime }}</td>
 | 
			
		||||
          <td>
 | 
			
		||||
            <a :href="`https://logs.tf/${match.logsTfId}`" target="_blank">
 | 
			
		||||
              #{{ match.logsTfId }}
 | 
			
		||||
            </a>
 | 
			
		||||
          </td>
 | 
			
		||||
        </tr>
 | 
			
		||||
      </tbody>
 | 
			
		||||
    </table>
 | 
			
		||||
  </main>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.header {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.button-group {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: flex-end;
 | 
			
		||||
  margin-bottom: 0.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
table {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
th {
 | 
			
		||||
  text-align: left;
 | 
			
		||||
  font-weight: 800;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
		Loading…
	
		Reference in New Issue