feat: Add consume invite dialog
parent
242562d662
commit
36bc19c96d
|
@ -307,6 +307,22 @@ hr {
|
|||
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 {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 10pt;
|
||||
|
|
|
@ -13,6 +13,7 @@ export type { OpenAPIConfig } from './core/OpenAPI';
|
|||
export type { AddPlayerJson } from './models/AddPlayerJson';
|
||||
export type { AttendanceJson } from './models/AttendanceJson';
|
||||
export type { AvailabilitySchema } from './models/AvailabilitySchema';
|
||||
export type { ConsumeInviteResponse } from './models/ConsumeInviteResponse';
|
||||
export type { CreateEventJson } from './models/CreateEventJson';
|
||||
export type { CreateTeamJson } from './models/CreateTeamJson';
|
||||
export type { EditMemberRolesJson } from './models/EditMemberRolesJson';
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type ConsumeInviteResponse = {
|
||||
teamId: number;
|
||||
};
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
/* eslint-disable */
|
||||
import type { AddPlayerJson } from '../models/AddPlayerJson';
|
||||
import type { AttendanceJson } from '../models/AttendanceJson';
|
||||
import type { ConsumeInviteResponse } from '../models/ConsumeInviteResponse';
|
||||
import type { CreateEventJson } from '../models/CreateEventJson';
|
||||
import type { CreateTeamJson } from '../models/CreateTeamJson';
|
||||
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>
|
||||
* @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>
|
||||
* @param teamId
|
||||
|
|
|
@ -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>
|
|
@ -2,9 +2,13 @@
|
|||
import { onMounted } from "vue";
|
||||
import { useTeamsStore } from "../stores/teams";
|
||||
import { RouterLink } from "vue-router";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import InviteKeyDialog from "./InviteKeyDialog.vue";
|
||||
|
||||
const teams = useTeamsStore();
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
onMounted(() => {
|
||||
teams.fetchTeams();
|
||||
});
|
||||
|
@ -17,11 +21,8 @@ onMounted(() => {
|
|||
<i class="bi bi-people-fill margin"></i>
|
||||
Your Teams
|
||||
</h2>
|
||||
<div class="button-group">
|
||||
<button class="small">
|
||||
<i class="bi bi-person-plus-fill margin" />
|
||||
Join a team
|
||||
</button>
|
||||
<div class="button-group" v-if="authStore.isLoggedIn">
|
||||
<InviteKeyDialog />
|
||||
<RouterLink class="button" to="/team/register">
|
||||
<button class="small accent">
|
||||
<i class="bi bi-plus-circle-fill margin"></i>
|
||||
|
@ -30,6 +31,9 @@ onMounted(() => {
|
|||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!authStore.isLoggedIn">
|
||||
Log in to view your teams.
|
||||
</div>
|
||||
<div
|
||||
v-if="teams.teamsWithRole"
|
||||
v-for="(team, _, i) in teams.teamsWithRole"
|
||||
|
|
|
@ -26,9 +26,13 @@ export const useInvitesStore = defineStore("invites", () => {
|
|||
return response;
|
||||
}
|
||||
|
||||
async function consumeInvite(teamId: number, key: string) {
|
||||
const response = await client.default.consumeInvite(teamId.toString(), key);
|
||||
teamInvites[teamId] = teamInvites[teamId].filter((invite) => invite.key != key);
|
||||
async function consumeInvite(key: string) {
|
||||
const response = await client.default.consumeInvite(key);
|
||||
const teamId = response.teamId;
|
||||
if (teamInvites[teamId]) {
|
||||
teamInvites[teamId] = teamInvites[teamId]
|
||||
.filter((invite) => invite.key != key);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ onMounted(() => {
|
|||
};
|
||||
|
||||
if (key.value) {
|
||||
invitesStore.consumeInvite(teamId.value, key.value.toString())
|
||||
invitesStore.consumeInvite(key.value.toString())
|
||||
.finally(doFetchTeam);
|
||||
} else {
|
||||
doFetchTeam();
|
||||
|
@ -73,7 +73,7 @@ onMounted(() => {
|
|||
</div>
|
||||
<div class="right">
|
||||
<h2>Upcoming Events</h2>
|
||||
<EventList :events="events" />
|
||||
<EventList :events="events" :team-context="team" />
|
||||
<h2 id="recent-matches-header">
|
||||
Recent Matches
|
||||
<RouterLink class="button" to="/">
|
||||
|
@ -82,7 +82,7 @@ onMounted(() => {
|
|||
</button>
|
||||
</RouterLink>
|
||||
</h2>
|
||||
<em class="subtext" v-if="false">No recent matches.</em>
|
||||
<em class="subtext" v-if="true">No recent matches.</em>
|
||||
<MatchCard v-else />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -9,7 +9,7 @@ from models.player import Player, PlayerSchema
|
|||
from models.player_team import PlayerTeam
|
||||
from models.player_team_availability import PlayerTeamAvailability
|
||||
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 spec import spec, BaseModel
|
||||
from team_invite import api_team_invite
|
||||
|
@ -51,7 +51,7 @@ class ViewTeamResponse(BaseModel):
|
|||
team: TeamSchema
|
||||
|
||||
class ViewTeamsResponse(BaseModel):
|
||||
teams: list[TeamSchema]
|
||||
teams: list[TeamWithRoleSchema]
|
||||
|
||||
@api_team.post("/")
|
||||
@spec.validate(
|
||||
|
@ -290,7 +290,7 @@ def view_team(team_id: int, **kwargs):
|
|||
|
||||
def fetch_teams_for_player(player: Player, team_id: int | None):
|
||||
q = db.session.query(
|
||||
Team
|
||||
Team, PlayerTeam
|
||||
).join(
|
||||
PlayerTeam
|
||||
).join(
|
||||
|
@ -303,15 +303,15 @@ def fetch_teams_for_player(player: Player, team_id: int | None):
|
|||
q = q.where(PlayerTeam.team_id == team_id)
|
||||
|
||||
if team_id is None:
|
||||
teams = q.all()
|
||||
players_teams = list(map(lambda x: x.tuple()[1], q.all()))
|
||||
return ViewTeamsResponse(
|
||||
teams=list(map(TeamSchema.from_model, teams))
|
||||
teams=list(map(TeamWithRoleSchema.from_player_team, players_teams))
|
||||
)
|
||||
else:
|
||||
team = q.one_or_none()
|
||||
if team:
|
||||
return ViewTeamResponse(
|
||||
team=TeamSchema.from_model(team)
|
||||
team=TeamSchema.from_model(team.tuple()[0])
|
||||
)
|
||||
|
||||
class ViewTeamMembersResponse(PlayerSchema):
|
||||
|
|
|
@ -8,7 +8,7 @@ from middleware import requires_authentication, requires_team_membership
|
|||
from models.player import Player
|
||||
from models.player_team import PlayerTeam
|
||||
from models.team_invite import TeamInvite, TeamInviteSchema
|
||||
from spec import spec
|
||||
from spec import BaseModel, spec
|
||||
|
||||
|
||||
api_team_invite = Blueprint("team_invite", __name__)
|
||||
|
@ -75,27 +75,31 @@ def create_invite(team_id: int, **_):
|
|||
|
||||
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(
|
||||
resp=Response(
|
||||
HTTP_204=None,
|
||||
HTTP_200=ConsumeInviteResponse,
|
||||
HTTP_404=None,
|
||||
),
|
||||
operation_id="consume_invite"
|
||||
)
|
||||
@requires_authentication
|
||||
def consume_invite(player: Player, team_id: int, key: str, **_):
|
||||
def consume_invite(player: Player, key: str, **_):
|
||||
invite = db.session.query(
|
||||
TeamInvite
|
||||
).where(
|
||||
TeamInvite.team_id == team_id
|
||||
).where(
|
||||
TeamInvite.key == key
|
||||
).one_or_none()
|
||||
|
||||
|
||||
if not invite:
|
||||
abort(404)
|
||||
|
||||
team_id = invite.team_id
|
||||
|
||||
player_team = db.session.query(
|
||||
PlayerTeam
|
||||
).where(
|
||||
|
@ -118,7 +122,7 @@ def consume_invite(player: Player, team_id: int, key: str, **_):
|
|||
|
||||
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>")
|
||||
@spec.validate(
|
||||
|
|
Loading…
Reference in New Issue