feat: Add consume invite dialog

master
John Montagu, the 4th Earl of Sandvich 2024-12-08 12:06:38 -08:00
parent 242562d662
commit 36bc19c96d
Signed by: sandvich
GPG Key ID: 9A39BE37E602B22D
10 changed files with 148 additions and 48 deletions

View File

@ -307,6 +307,22 @@ hr {
width: 100%; width: 100%;
} }
.dialog-overlay {
background-color: #00000055;
z-index: 1;
position: fixed;
inset: 0;
}
[role="dialog"] {
position: fixed;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
background-color: var(--base);
z-index: 10;
}
div.banner { div.banner {
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
font-size: 10pt; font-size: 10pt;

View File

@ -13,6 +13,7 @@ export type { OpenAPIConfig } from './core/OpenAPI';
export type { AddPlayerJson } from './models/AddPlayerJson'; export type { AddPlayerJson } from './models/AddPlayerJson';
export type { AttendanceJson } from './models/AttendanceJson'; export type { AttendanceJson } from './models/AttendanceJson';
export type { AvailabilitySchema } from './models/AvailabilitySchema'; export type { AvailabilitySchema } from './models/AvailabilitySchema';
export type { ConsumeInviteResponse } from './models/ConsumeInviteResponse';
export type { CreateEventJson } from './models/CreateEventJson'; export type { CreateEventJson } from './models/CreateEventJson';
export type { CreateTeamJson } from './models/CreateTeamJson'; export type { CreateTeamJson } from './models/CreateTeamJson';
export type { EditMemberRolesJson } from './models/EditMemberRolesJson'; export type { EditMemberRolesJson } from './models/EditMemberRolesJson';

View File

@ -0,0 +1,8 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ConsumeInviteResponse = {
teamId: number;
};

View File

@ -4,6 +4,7 @@
/* eslint-disable */ /* eslint-disable */
import type { AddPlayerJson } from '../models/AddPlayerJson'; import type { AddPlayerJson } from '../models/AddPlayerJson';
import type { AttendanceJson } from '../models/AttendanceJson'; import type { AttendanceJson } from '../models/AttendanceJson';
import type { ConsumeInviteResponse } from '../models/ConsumeInviteResponse';
import type { CreateEventJson } from '../models/CreateEventJson'; import type { CreateEventJson } from '../models/CreateEventJson';
import type { CreateTeamJson } from '../models/CreateTeamJson'; import type { CreateTeamJson } from '../models/CreateTeamJson';
import type { EditMemberRolesJson } from '../models/EditMemberRolesJson'; import type { EditMemberRolesJson } from '../models/EditMemberRolesJson';
@ -415,6 +416,27 @@ export class DefaultService {
}, },
}); });
} }
/**
* consume_invite <POST>
* @param key
* @returns ConsumeInviteResponse OK
* @throws ApiError
*/
public consumeInvite(
key: string,
): CancelablePromise<ConsumeInviteResponse> {
return this.httpRequest.request({
method: 'POST',
url: '/api/team/consume-invite/{key}',
path: {
'key': key,
},
errors: {
404: `Not Found`,
422: `Unprocessable Content`,
},
});
}
/** /**
* delete_team <DELETE> * delete_team <DELETE>
* @param teamId * @param teamId
@ -483,30 +505,6 @@ export class DefaultService {
}, },
}); });
} }
/**
* consume_invite <POST>
* @param teamId
* @param key
* @returns void
* @throws ApiError
*/
public consumeInvite(
teamId: string,
key: string,
): CancelablePromise<void> {
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 Content`,
},
});
}
/** /**
* edit_member_roles <PATCH> * edit_member_roles <PATCH>
* @param teamId * @param teamId

View File

@ -0,0 +1,65 @@
<script setup lang="ts">
import { useInvitesStore } from "@/stores/teams/invites";
import { DialogClose, DialogContent, DialogDescription, DialogOverlay, DialogPortal, DialogRoot, DialogTitle, DialogTrigger } from "radix-vue";
import { ref } from "vue";
import { useRouter } from "vue-router";
const invitesStore = useInvitesStore();
const key = ref("");
const router = useRouter();
function submit() {
invitesStore.consumeInvite(key.value)
.then((response) => {
console.log(response);
router.push({
name: "team-details",
params: {
id: response.teamId,
}
});
});
}
</script>
<template>
<DialogRoot>
<DialogTrigger>
<i class="bi bi-person-plus-fill margin" />
Join a team
</DialogTrigger>
<DialogPortal>
<DialogOverlay class="dialog-overlay" />
<DialogContent>
<DialogTitle>Join a team</DialogTitle>
<DialogDescription>
<p>
Enter the invite key to join a team. Don't have an invite key? Ask
your team leader to send you one.
</p>
</DialogDescription>
<div class="form-group margin">
<h3>Invite key</h3>
<input type="text" placeholder="Invite key or URL" v-model="key" />
</div>
<div class="form-group">
<div class="action-buttons">
<DialogClose class="accent" aria-label="Close" @click="submit">
<i class="bi bi-check" />
Join
</DialogClose>
</div>
</div>
</DialogContent>
</DialogPortal>
</DialogRoot>
</template>
<style scoped>
[role="dialog"] {
padding: 2rem;
border-radius: 0.5rem;
}
</style>

View File

@ -2,9 +2,13 @@
import { onMounted } from "vue"; import { onMounted } from "vue";
import { useTeamsStore } from "../stores/teams"; import { useTeamsStore } from "../stores/teams";
import { RouterLink } from "vue-router"; import { RouterLink } from "vue-router";
import { useAuthStore } from "@/stores/auth";
import InviteKeyDialog from "./InviteKeyDialog.vue";
const teams = useTeamsStore(); const teams = useTeamsStore();
const authStore = useAuthStore();
onMounted(() => { onMounted(() => {
teams.fetchTeams(); teams.fetchTeams();
}); });
@ -17,11 +21,8 @@ onMounted(() => {
<i class="bi bi-people-fill margin"></i> <i class="bi bi-people-fill margin"></i>
Your Teams Your Teams
</h2> </h2>
<div class="button-group"> <div class="button-group" v-if="authStore.isLoggedIn">
<button class="small"> <InviteKeyDialog />
<i class="bi bi-person-plus-fill margin" />
Join a team
</button>
<RouterLink class="button" to="/team/register"> <RouterLink class="button" to="/team/register">
<button class="small accent"> <button class="small accent">
<i class="bi bi-plus-circle-fill margin"></i> <i class="bi bi-plus-circle-fill margin"></i>
@ -30,6 +31,9 @@ onMounted(() => {
</RouterLink> </RouterLink>
</div> </div>
</div> </div>
<div v-if="!authStore.isLoggedIn">
Log in to view your teams.
</div>
<div <div
v-if="teams.teamsWithRole" v-if="teams.teamsWithRole"
v-for="(team, _, i) in teams.teamsWithRole" v-for="(team, _, i) in teams.teamsWithRole"

View File

@ -26,9 +26,13 @@ export const useInvitesStore = defineStore("invites", () => {
return response; return response;
} }
async function consumeInvite(teamId: number, key: string) { async function consumeInvite(key: string) {
const response = await client.default.consumeInvite(teamId.toString(), key); const response = await client.default.consumeInvite(key);
teamInvites[teamId] = teamInvites[teamId].filter((invite) => invite.key != key); const teamId = response.teamId;
if (teamInvites[teamId]) {
teamInvites[teamId] = teamInvites[teamId]
.filter((invite) => invite.key != key);
}
return response; return response;
} }

View File

@ -36,7 +36,7 @@ onMounted(() => {
}; };
if (key.value) { if (key.value) {
invitesStore.consumeInvite(teamId.value, key.value.toString()) invitesStore.consumeInvite(key.value.toString())
.finally(doFetchTeam); .finally(doFetchTeam);
} else { } else {
doFetchTeam(); doFetchTeam();
@ -73,7 +73,7 @@ onMounted(() => {
</div> </div>
<div class="right"> <div class="right">
<h2>Upcoming Events</h2> <h2>Upcoming Events</h2>
<EventList :events="events" /> <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="/">
@ -82,7 +82,7 @@ onMounted(() => {
</button> </button>
</RouterLink> </RouterLink>
</h2> </h2>
<em class="subtext" v-if="false">No recent matches.</em> <em class="subtext" v-if="true">No recent matches.</em>
<MatchCard v-else /> <MatchCard v-else />
</div> </div>
</div> </div>

View File

@ -9,7 +9,7 @@ from models.player import Player, PlayerSchema
from models.player_team import PlayerTeam from models.player_team import PlayerTeam
from models.player_team_availability import PlayerTeamAvailability from models.player_team_availability import PlayerTeamAvailability
from models.player_team_role import PlayerTeamRole, RoleSchema from models.player_team_role import PlayerTeamRole, RoleSchema
from models.team import Team, TeamSchema from models.team import Team, TeamSchema, TeamWithRoleSchema
from middleware import assert_team_authority, requires_authentication, requires_team_membership from middleware import assert_team_authority, requires_authentication, requires_team_membership
from spec import spec, BaseModel from spec import spec, BaseModel
from team_invite import api_team_invite from team_invite import api_team_invite
@ -51,7 +51,7 @@ class ViewTeamResponse(BaseModel):
team: TeamSchema team: TeamSchema
class ViewTeamsResponse(BaseModel): class ViewTeamsResponse(BaseModel):
teams: list[TeamSchema] teams: list[TeamWithRoleSchema]
@api_team.post("/") @api_team.post("/")
@spec.validate( @spec.validate(
@ -290,7 +290,7 @@ def view_team(team_id: int, **kwargs):
def fetch_teams_for_player(player: Player, team_id: int | None): def fetch_teams_for_player(player: Player, team_id: int | None):
q = db.session.query( q = db.session.query(
Team Team, PlayerTeam
).join( ).join(
PlayerTeam PlayerTeam
).join( ).join(
@ -303,15 +303,15 @@ def fetch_teams_for_player(player: Player, team_id: int | None):
q = q.where(PlayerTeam.team_id == team_id) q = q.where(PlayerTeam.team_id == team_id)
if team_id is None: if team_id is None:
teams = q.all() players_teams = list(map(lambda x: x.tuple()[1], q.all()))
return ViewTeamsResponse( return ViewTeamsResponse(
teams=list(map(TeamSchema.from_model, teams)) teams=list(map(TeamWithRoleSchema.from_player_team, players_teams))
) )
else: else:
team = q.one_or_none() team = q.one_or_none()
if team: if team:
return ViewTeamResponse( return ViewTeamResponse(
team=TeamSchema.from_model(team) team=TeamSchema.from_model(team.tuple()[0])
) )
class ViewTeamMembersResponse(PlayerSchema): class ViewTeamMembersResponse(PlayerSchema):

View File

@ -8,7 +8,7 @@ from middleware import requires_authentication, requires_team_membership
from models.player import Player from models.player import Player
from models.player_team import PlayerTeam from models.player_team import PlayerTeam
from models.team_invite import TeamInvite, TeamInviteSchema from models.team_invite import TeamInvite, TeamInviteSchema
from spec import spec from spec import BaseModel, spec
api_team_invite = Blueprint("team_invite", __name__) api_team_invite = Blueprint("team_invite", __name__)
@ -75,27 +75,31 @@ def create_invite(team_id: int, **_):
return response.dict(by_alias=True), 200 return response.dict(by_alias=True), 200
@api_team_invite.post("/id/<team_id>/consume-invite/<key>") class ConsumeInviteResponse(BaseModel):
team_id: int
@api_team_invite.post("/consume-invite/<key>")
@spec.validate( @spec.validate(
resp=Response( resp=Response(
HTTP_204=None, HTTP_200=ConsumeInviteResponse,
HTTP_404=None, HTTP_404=None,
), ),
operation_id="consume_invite" operation_id="consume_invite"
) )
@requires_authentication @requires_authentication
def consume_invite(player: Player, team_id: int, key: str, **_): def consume_invite(player: Player, key: str, **_):
invite = db.session.query( invite = db.session.query(
TeamInvite TeamInvite
).where(
TeamInvite.team_id == team_id
).where( ).where(
TeamInvite.key == key TeamInvite.key == key
).one_or_none() ).one_or_none()
if not invite: if not invite:
abort(404) abort(404)
team_id = invite.team_id
player_team = db.session.query( player_team = db.session.query(
PlayerTeam PlayerTeam
).where( ).where(
@ -118,7 +122,7 @@ def consume_invite(player: Player, team_id: int, key: str, **_):
db.session.commit() db.session.commit()
return make_response({ }, 204) return ConsumeInviteResponse(team_id=team_id).dict(by_alias=True), 200
@api_team_invite.delete("/id/<team_id>/invite/<key>") @api_team_invite.delete("/id/<team_id>/invite/<key>")
@spec.validate( @spec.validate(