feat(frontend): Implement matches

master
John Montagu, the 4th Earl of Sandvich 2024-12-10 10:25:33 -08:00
parent 45ac071a7f
commit caaee983f2
Signed by: sandvich
GPG Key ID: 9A39BE37E602B22D
10 changed files with 296 additions and 0 deletions

View File

@ -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';

View File

@ -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;
};

View File

@ -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>;
};

View File

@ -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;
};

View File

@ -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>;

View File

@ -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

View File

@ -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>

View File

@ -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,
},
]
});

View File

@ -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,
}
});

View File

@ -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>