From 050a0123183f4df3b31e647d35edcb3160fc48c4 Mon Sep 17 00:00:00 2001 From: HumanoidSandvichDispenser Date: Sat, 9 Nov 2024 15:24:30 -0800 Subject: [PATCH] Implement basic team features --- availabili.tf/src/assets/base.css | 5 +- availabili.tf/src/assets/main.css | 14 +- availabili.tf/src/client/index.ts | 2 + .../src/client/models/TeamInviteSchema.ts | 10 + .../src/client/models/TeamInviteSchemaList.ts | 6 + .../src/client/services/DefaultService.ts | 92 +++++++++ availabili.tf/src/components/InviteEntry.vue | 83 +++++++++ .../src/components/PlayerTeamCard.vue | 30 ++- availabili.tf/src/stores/teams.ts | 51 ++++- availabili.tf/src/views/TeamDetailsView.vue | 100 +++++++++- .../versions/65714d7e78f8_add_teaminvite.py | 35 ++++ backend-flask/models.py | 16 ++ backend-flask/team.py | 174 +++++++++++++++++- 13 files changed, 590 insertions(+), 28 deletions(-) create mode 100644 availabili.tf/src/client/models/TeamInviteSchema.ts create mode 100644 availabili.tf/src/client/models/TeamInviteSchemaList.ts create mode 100644 availabili.tf/src/components/InviteEntry.vue create mode 100644 backend-flask/migrations/versions/65714d7e78f8_add_teaminvite.py diff --git a/availabili.tf/src/assets/base.css b/availabili.tf/src/assets/base.css index 4659743..d484b7e 100644 --- a/availabili.tf/src/assets/base.css +++ b/availabili.tf/src/assets/base.css @@ -35,9 +35,12 @@ --mantle: #e6e9ef; --crust: #dce0e8; - --flamingo: #f0c6c6; + --red: #d20f39; + + --flamingo: #dd7878; --flamingo-transparent: #f0c6c655; --green: #40a02b; + --peach: #fe640b; --yellow: #df8e1d; --lavender: #7287fd; --accent: var(--lavender); diff --git a/availabili.tf/src/assets/main.css b/availabili.tf/src/assets/main.css index 74c736c..e3f2ecc 100644 --- a/availabili.tf/src/assets/main.css +++ b/availabili.tf/src/assets/main.css @@ -70,6 +70,12 @@ button.accent.dark { color: var(--base); } +button.destructive { + background-color: var(--flamingo); + color: var(--base); +} + + button.accent:hover { background-color: var(--text); color: var(--base); @@ -108,10 +114,14 @@ h1 { } h2 { - font-weight: 700; + font-weight: 800; } -em.aside { +span.small { + font-size: 9pt; +} + +em.aside, span.aside { color: var(--overlay-0); } diff --git a/availabili.tf/src/client/index.ts b/availabili.tf/src/client/index.ts index 9cf86c0..9d500db 100644 --- a/availabili.tf/src/client/index.ts +++ b/availabili.tf/src/client/index.ts @@ -15,6 +15,8 @@ export type { CreateTeamJson } from './models/CreateTeamJson'; export type { EditMemberRolesJson } from './models/EditMemberRolesJson'; export type { PutScheduleForm } from './models/PutScheduleForm'; export type { RoleSchema } from './models/RoleSchema'; +export type { TeamInviteSchema } from './models/TeamInviteSchema'; +export type { TeamInviteSchemaList } from './models/TeamInviteSchemaList'; export { TeamRole } from './models/TeamRole'; export type { TeamSchema } from './models/TeamSchema'; export type { ValidationError } from './models/ValidationError'; diff --git a/availabili.tf/src/client/models/TeamInviteSchema.ts b/availabili.tf/src/client/models/TeamInviteSchema.ts new file mode 100644 index 0000000..7119c8a --- /dev/null +++ b/availabili.tf/src/client/models/TeamInviteSchema.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type TeamInviteSchema = { + createdAt: string; + key: string; + teamId: number; +}; + diff --git a/availabili.tf/src/client/models/TeamInviteSchemaList.ts b/availabili.tf/src/client/models/TeamInviteSchemaList.ts new file mode 100644 index 0000000..ff2d763 --- /dev/null +++ b/availabili.tf/src/client/models/TeamInviteSchemaList.ts @@ -0,0 +1,6 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { TeamInviteSchema } from './TeamInviteSchema'; +export type TeamInviteSchemaList = Array; diff --git a/availabili.tf/src/client/services/DefaultService.ts b/availabili.tf/src/client/services/DefaultService.ts index 55e9562..8b2166b 100644 --- a/availabili.tf/src/client/services/DefaultService.ts +++ b/availabili.tf/src/client/services/DefaultService.ts @@ -6,6 +6,8 @@ import type { AddPlayerJson } from '../models/AddPlayerJson'; import type { CreateTeamJson } from '../models/CreateTeamJson'; import type { EditMemberRolesJson } from '../models/EditMemberRolesJson'; import type { PutScheduleForm } from '../models/PutScheduleForm'; +import type { TeamInviteSchema } from '../models/TeamInviteSchema'; +import type { TeamInviteSchemaList } from '../models/TeamInviteSchemaList'; import type { ViewScheduleResponse } from '../models/ViewScheduleResponse'; import type { ViewTeamMembersResponseList } from '../models/ViewTeamMembersResponseList'; import type { ViewTeamResponse } from '../models/ViewTeamResponse'; @@ -211,6 +213,30 @@ export class DefaultService { }, }); } + /** + * consume_invite + * @param teamId + * @param key + * @returns void + * @throws ApiError + */ + public consumeInvite( + teamId: string, + key: string, + ): CancelablePromise { + return this.httpRequest.request({ + method: 'POST', + url: '/api/team/id/{team_id}/consume-invite/{key}', + path: { + 'team_id': teamId, + 'key': key, + }, + errors: { + 404: `Not Found`, + 422: `Unprocessable Entity`, + }, + }); + } /** * edit_member_roles * @param teamId @@ -240,6 +266,72 @@ export class DefaultService { }, }); } + /** + * get_invites + * @param teamId + * @returns TeamInviteSchemaList OK + * @throws ApiError + */ + public getInvites( + teamId: string, + ): CancelablePromise { + return this.httpRequest.request({ + method: 'GET', + url: '/api/team/id/{team_id}/invite', + path: { + 'team_id': teamId, + }, + errors: { + 404: `Not Found`, + 422: `Unprocessable Entity`, + }, + }); + } + /** + * create_invite + * @param teamId + * @returns TeamInviteSchema OK + * @throws ApiError + */ + public createInvite( + teamId: string, + ): CancelablePromise { + return this.httpRequest.request({ + method: 'POST', + url: '/api/team/id/{team_id}/invite', + path: { + 'team_id': teamId, + }, + errors: { + 404: `Not Found`, + 422: `Unprocessable Entity`, + }, + }); + } + /** + * revoke_invite + * @param teamId + * @param key + * @returns void + * @throws ApiError + */ + public revokeInvite( + teamId: string, + key: string, + ): CancelablePromise { + return this.httpRequest.request({ + method: 'DELETE', + url: '/api/team/id/{team_id}/invite/{key}', + path: { + 'team_id': teamId, + 'key': key, + }, + errors: { + 404: `Not Found`, + 422: `Unprocessable Entity`, + }, + }); + } /** * add_player * @param teamId diff --git a/availabili.tf/src/components/InviteEntry.vue b/availabili.tf/src/components/InviteEntry.vue new file mode 100644 index 0000000..a8c21af --- /dev/null +++ b/availabili.tf/src/components/InviteEntry.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/availabili.tf/src/components/PlayerTeamCard.vue b/availabili.tf/src/components/PlayerTeamCard.vue index 79fc350..f15fed6 100644 --- a/availabili.tf/src/components/PlayerTeamCard.vue +++ b/availabili.tf/src/components/PlayerTeamCard.vue @@ -75,7 +75,10 @@ function updateRoles() {
- +
+ + +

{{ player.username }}

@@ -129,8 +132,6 @@ function updateRoles() { user-select: none; gap: 1em; align-items: center; - border: 2px solid white; - box-shadow: 1px 1px 8px var(--surface-0); } .player-card > td { @@ -142,24 +143,37 @@ function updateRoles() { font-size: 12pt; } -.dot { +.status-indicators { + display: flex; + flex-direction: row; + gap: 2px; +} + +.status-indicators > .indicator { display: block; - border-radius: 50%; height: 8px; - width: 8px; + width: 12px; background-color: var(--overlay-0); } +.left-indicator { + border-radius: 8px 0 0 8px; +} + +.right-indicator { + border-radius: 0 8px 8px 0; +} + .status[availability="0"] h3 { color: var(--overlay-0); font-weight: 400; } -.status[availability="1"] .dot { +.status[availability="1"] .indicator { background-color: var(--yellow); } -.status[availability="2"] .dot { +.status[availability="2"] .indicator { background-color: var(--green); } diff --git a/availabili.tf/src/stores/teams.ts b/availabili.tf/src/stores/teams.ts index 29ed857..34f1ce5 100644 --- a/availabili.tf/src/stores/teams.ts +++ b/availabili.tf/src/stores/teams.ts @@ -1,17 +1,18 @@ import Cacheable from "@/cacheable"; -import { AvailabilitfClient, type RoleSchema, type TeamSpec, type ViewTeamMembersResponse, type ViewTeamResponse, type ViewTeamsResponse } from "@/client"; +import { AvailabilitfClient, type TeamInviteSchema, type RoleSchema, type TeamSchema, type ViewTeamMembersResponse, type ViewTeamResponse, type ViewTeamsResponse } from "@/client"; import { defineStore } from "pinia"; import { computed, reactive, ref, type Reactive, type Ref } from "vue"; import { useClientStore } from "./client"; -export type TeamMap = { [id: number]: TeamSpec }; +export type TeamMap = { [id: number]: TeamSchema }; export const useTeamsStore = defineStore("teams", () => { const clientStore = useClientStore(); const client = clientStore.client; - const teams: Reactive<{ [id: number]: TeamSpec }> = reactive({ }); + const teams: Reactive<{ [id: number]: TeamSchema }> = reactive({ }); const teamMembers: Reactive<{ [id: number]: ViewTeamMembersResponse[] }> = reactive({ }); + const teamInvites: Reactive<{ [id: number]: TeamInviteSchema[] }> = reactive({ }); const isFetchingTeams = ref(false); @@ -78,13 +79,55 @@ export const useTeamsStore = defineStore("teams", () => { }); } + async function getInvites(teamId: number) { + return clientStore.call( + getInvites.name, + () => client.default.getInvites(teamId.toString()), + (response) => { + teamInvites[teamId] = response; + return response; + } + ); + } + + async function createInvite(teamId: number) { + return client.default.createInvite(teamId.toString()) + .then((response) => { + teamInvites[teamId].push(response); + return response; + }) + } + + async function consumeInvite(teamId: number, key: string) { + return client.default.consumeInvite(teamId.toString(), key) + .then((response) => { + teamInvites[teamId] = teamInvites[teamId] + .filter((invite) => invite.key != key); + return response; + }); + } + + async function revokeInvite(teamId: number, key: string) { + return client.default.revokeInvite(teamId.toString(), key) + .then((response) => { + teamInvites[teamId] = teamInvites[teamId] + .filter((invite) => invite.key != key); + return response; + }); + } + return { teams, + teamInvites, teamMembers, fetchTeams, fetchTeam, fetchTeamMembers, createTeam, - updateRoles + updateRoles, + getInvites, + createInvite, + consumeInvite, + revokeInvite, }; }); diff --git a/availabili.tf/src/views/TeamDetailsView.vue b/availabili.tf/src/views/TeamDetailsView.vue index 76da457..2dad788 100644 --- a/availabili.tf/src/views/TeamDetailsView.vue +++ b/availabili.tf/src/views/TeamDetailsView.vue @@ -1,8 +1,9 @@ @@ -29,15 +50,17 @@ onMounted(() => {

{{ team.teamName }} - {{ teamsStore.teamMembers[route.params.id]?.length }} member(s), {{ availableMembers?.length }} currently available +
+ +