Implement basic team features
parent
2fda11bc9a
commit
050a012318
|
@ -35,9 +35,12 @@
|
||||||
--mantle: #e6e9ef;
|
--mantle: #e6e9ef;
|
||||||
--crust: #dce0e8;
|
--crust: #dce0e8;
|
||||||
|
|
||||||
--flamingo: #f0c6c6;
|
--red: #d20f39;
|
||||||
|
|
||||||
|
--flamingo: #dd7878;
|
||||||
--flamingo-transparent: #f0c6c655;
|
--flamingo-transparent: #f0c6c655;
|
||||||
--green: #40a02b;
|
--green: #40a02b;
|
||||||
|
--peach: #fe640b;
|
||||||
--yellow: #df8e1d;
|
--yellow: #df8e1d;
|
||||||
--lavender: #7287fd;
|
--lavender: #7287fd;
|
||||||
--accent: var(--lavender);
|
--accent: var(--lavender);
|
||||||
|
|
|
@ -70,6 +70,12 @@ button.accent.dark {
|
||||||
color: var(--base);
|
color: var(--base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.destructive {
|
||||||
|
background-color: var(--flamingo);
|
||||||
|
color: var(--base);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
button.accent:hover {
|
button.accent:hover {
|
||||||
background-color: var(--text);
|
background-color: var(--text);
|
||||||
color: var(--base);
|
color: var(--base);
|
||||||
|
@ -108,10 +114,14 @@ h1 {
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
em.aside {
|
span.small {
|
||||||
|
font-size: 9pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
em.aside, span.aside {
|
||||||
color: var(--overlay-0);
|
color: var(--overlay-0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,8 @@ export type { CreateTeamJson } from './models/CreateTeamJson';
|
||||||
export type { EditMemberRolesJson } from './models/EditMemberRolesJson';
|
export type { EditMemberRolesJson } from './models/EditMemberRolesJson';
|
||||||
export type { PutScheduleForm } from './models/PutScheduleForm';
|
export type { PutScheduleForm } from './models/PutScheduleForm';
|
||||||
export type { RoleSchema } from './models/RoleSchema';
|
export type { RoleSchema } from './models/RoleSchema';
|
||||||
|
export type { TeamInviteSchema } from './models/TeamInviteSchema';
|
||||||
|
export type { TeamInviteSchemaList } from './models/TeamInviteSchemaList';
|
||||||
export { TeamRole } from './models/TeamRole';
|
export { TeamRole } from './models/TeamRole';
|
||||||
export type { TeamSchema } from './models/TeamSchema';
|
export type { TeamSchema } from './models/TeamSchema';
|
||||||
export type { ValidationError } from './models/ValidationError';
|
export type { ValidationError } from './models/ValidationError';
|
||||||
|
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
|
@ -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<TeamInviteSchema>;
|
|
@ -6,6 +6,8 @@ import type { AddPlayerJson } from '../models/AddPlayerJson';
|
||||||
import type { CreateTeamJson } from '../models/CreateTeamJson';
|
import type { CreateTeamJson } from '../models/CreateTeamJson';
|
||||||
import type { EditMemberRolesJson } from '../models/EditMemberRolesJson';
|
import type { EditMemberRolesJson } from '../models/EditMemberRolesJson';
|
||||||
import type { PutScheduleForm } from '../models/PutScheduleForm';
|
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 { ViewScheduleResponse } from '../models/ViewScheduleResponse';
|
||||||
import type { ViewTeamMembersResponseList } from '../models/ViewTeamMembersResponseList';
|
import type { ViewTeamMembersResponseList } from '../models/ViewTeamMembersResponseList';
|
||||||
import type { ViewTeamResponse } from '../models/ViewTeamResponse';
|
import type { ViewTeamResponse } from '../models/ViewTeamResponse';
|
||||||
|
@ -211,6 +213,30 @@ 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 Entity`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* edit_member_roles <PATCH>
|
* edit_member_roles <PATCH>
|
||||||
* @param teamId
|
* @param teamId
|
||||||
|
@ -240,6 +266,72 @@ export class DefaultService {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* get_invites <GET>
|
||||||
|
* @param teamId
|
||||||
|
* @returns TeamInviteSchemaList OK
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public getInvites(
|
||||||
|
teamId: string,
|
||||||
|
): CancelablePromise<TeamInviteSchemaList> {
|
||||||
|
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 <POST>
|
||||||
|
* @param teamId
|
||||||
|
* @returns TeamInviteSchema OK
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public createInvite(
|
||||||
|
teamId: string,
|
||||||
|
): CancelablePromise<TeamInviteSchema> {
|
||||||
|
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 <DELETE>
|
||||||
|
* @param teamId
|
||||||
|
* @param key
|
||||||
|
* @returns void
|
||||||
|
* @throws ApiError
|
||||||
|
*/
|
||||||
|
public revokeInvite(
|
||||||
|
teamId: string,
|
||||||
|
key: string,
|
||||||
|
): CancelablePromise<void> {
|
||||||
|
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 <PUT>
|
* add_player <PUT>
|
||||||
* @param teamId
|
* @param teamId
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { type TeamInviteSchema } from "../client";
|
||||||
|
import { useTeamsStore } from "../stores/teams";
|
||||||
|
import { computed } from "vue";
|
||||||
|
|
||||||
|
const teamsStore = useTeamsStore();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
invite: Object as PropType<TeamInviteSchema>,
|
||||||
|
});
|
||||||
|
|
||||||
|
const inviteLink = computed(() => {
|
||||||
|
let teamId = props.invite.teamId;
|
||||||
|
let key = props.invite.key;
|
||||||
|
return `${window.location.origin}/team/id/${teamId}?key=${key}`;
|
||||||
|
})
|
||||||
|
|
||||||
|
function copyLink() {
|
||||||
|
navigator.clipboard.writeText(inviteLink.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function revokeInvite() {
|
||||||
|
teamsStore.revokeInvite(props.invite.teamId, props.invite.key);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a class="key" :href="inviteLink">
|
||||||
|
<code>
|
||||||
|
{{ invite.key }}
|
||||||
|
</code>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ invite.createdAt }}
|
||||||
|
</td>
|
||||||
|
<td class="buttons">
|
||||||
|
<button @click="copyLink">
|
||||||
|
<i class="bi bi-link margin" />
|
||||||
|
Copy Link
|
||||||
|
</button>
|
||||||
|
<button class="destructive" @click="revokeInvite">
|
||||||
|
<i class="bi bi-trash margin" />
|
||||||
|
Revoke
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
tr .buttons {
|
||||||
|
opacity: 0;
|
||||||
|
transition-duration: 200ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover .buttons {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key {
|
||||||
|
color: var(--text);
|
||||||
|
background-color: var(--text);
|
||||||
|
padding: 2px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key:hover {
|
||||||
|
color: var(--base);
|
||||||
|
transition-duration: 200ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
align-content: center;
|
||||||
|
justify-content: end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -75,7 +75,10 @@ function updateRoles() {
|
||||||
<tr class="player-card">
|
<tr class="player-card">
|
||||||
<td>
|
<td>
|
||||||
<div class="status flex-middle" :availability="player.availability">
|
<div class="status flex-middle" :availability="player.availability">
|
||||||
<span class="dot"></span>
|
<div class="status-indicators">
|
||||||
|
<span class="indicator left-indicator" />
|
||||||
|
<span class="indicator right-indicator" />
|
||||||
|
</div>
|
||||||
<h3>
|
<h3>
|
||||||
{{ player.username }}
|
{{ player.username }}
|
||||||
</h3>
|
</h3>
|
||||||
|
@ -129,8 +132,6 @@ function updateRoles() {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
gap: 1em;
|
gap: 1em;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border: 2px solid white;
|
|
||||||
box-shadow: 1px 1px 8px var(--surface-0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-card > td {
|
.player-card > td {
|
||||||
|
@ -142,24 +143,37 @@ function updateRoles() {
|
||||||
font-size: 12pt;
|
font-size: 12pt;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dot {
|
.status-indicators {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicators > .indicator {
|
||||||
display: block;
|
display: block;
|
||||||
border-radius: 50%;
|
|
||||||
height: 8px;
|
height: 8px;
|
||||||
width: 8px;
|
width: 12px;
|
||||||
background-color: var(--overlay-0);
|
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 {
|
.status[availability="0"] h3 {
|
||||||
color: var(--overlay-0);
|
color: var(--overlay-0);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status[availability="1"] .dot {
|
.status[availability="1"] .indicator {
|
||||||
background-color: var(--yellow);
|
background-color: var(--yellow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status[availability="2"] .dot {
|
.status[availability="2"] .indicator {
|
||||||
background-color: var(--green);
|
background-color: var(--green);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
import Cacheable from "@/cacheable";
|
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 { defineStore } from "pinia";
|
||||||
import { computed, reactive, ref, type Reactive, type Ref } from "vue";
|
import { computed, reactive, ref, type Reactive, type Ref } from "vue";
|
||||||
import { useClientStore } from "./client";
|
import { useClientStore } from "./client";
|
||||||
|
|
||||||
export type TeamMap = { [id: number]: TeamSpec };
|
export type TeamMap = { [id: number]: TeamSchema };
|
||||||
|
|
||||||
export const useTeamsStore = defineStore("teams", () => {
|
export const useTeamsStore = defineStore("teams", () => {
|
||||||
const clientStore = useClientStore();
|
const clientStore = useClientStore();
|
||||||
const client = clientStore.client;
|
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 teamMembers: Reactive<{ [id: number]: ViewTeamMembersResponse[] }> = reactive({ });
|
||||||
|
const teamInvites: Reactive<{ [id: number]: TeamInviteSchema[] }> = reactive({ });
|
||||||
|
|
||||||
const isFetchingTeams = ref(false);
|
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 {
|
return {
|
||||||
teams,
|
teams,
|
||||||
|
teamInvites,
|
||||||
teamMembers,
|
teamMembers,
|
||||||
fetchTeams,
|
fetchTeams,
|
||||||
fetchTeam,
|
fetchTeam,
|
||||||
fetchTeamMembers,
|
fetchTeamMembers,
|
||||||
createTeam,
|
createTeam,
|
||||||
updateRoles
|
updateRoles,
|
||||||
|
getInvites,
|
||||||
|
createInvite,
|
||||||
|
consumeInvite,
|
||||||
|
revokeInvite,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRoute, useRouter, RouterLink } from "vue-router";
|
import { useRoute, useRouter, RouterLink } from "vue-router";
|
||||||
import { useTeamsStore } from "../stores/teams";
|
import { useTeamsStore } from "../stores/teams";
|
||||||
import { computed, onMounted } from "vue";
|
import { computed, onMounted, ref } from "vue";
|
||||||
import PlayerTeamCard from "../components/PlayerTeamCard.vue";
|
import PlayerTeamCard from "../components/PlayerTeamCard.vue";
|
||||||
|
import InviteEntry from "../components/InviteEntry.vue";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
@ -12,14 +13,34 @@ const team = computed(() => {
|
||||||
return teamsStore.teams[route.params.id];
|
return teamsStore.teams[route.params.id];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const invites = computed(() => {
|
||||||
|
return teamsStore.teamInvites[route.params.id];
|
||||||
|
});
|
||||||
|
|
||||||
const availableMembers = computed(() => {
|
const availableMembers = computed(() => {
|
||||||
return teamsStore.teamMembers[route.params.id]
|
return teamsStore.teamMembers[route.params.id]
|
||||||
.filter((member) => member.availability > 0);
|
.filter((member) => member.availability > 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
function createInvite() {
|
||||||
teamsStore.fetchTeam(route.params.id)
|
teamsStore.createInvite(team.value.id);
|
||||||
.then(() => teamsStore.fetchTeamMembers(route.params.id));
|
}
|
||||||
|
|
||||||
|
function revokeInvite(key) {
|
||||||
|
teamsStore.revokeInvite(team.value.id, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
let key = route.query.key;
|
||||||
|
let teamId = route.params.id;
|
||||||
|
|
||||||
|
if (key) {
|
||||||
|
await teamsStore.consumeInvite(teamId, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
teamsStore.fetchTeam(teamId)
|
||||||
|
.then(() => teamsStore.fetchTeamMembers(teamId))
|
||||||
|
.then(() => teamsStore.getInvites(teamId));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -29,15 +50,17 @@ onMounted(() => {
|
||||||
<h1>
|
<h1>
|
||||||
{{ team.teamName }}
|
{{ team.teamName }}
|
||||||
<RouterLink :to="'/schedule?teamId=' + team.id">
|
<RouterLink :to="'/schedule?teamId=' + team.id">
|
||||||
<button class="accent">
|
|
||||||
<i class="bi bi-calendar-fill margin"></i>
|
|
||||||
View schedule
|
|
||||||
</button>
|
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<em class="aside" v-if="teamsStore.teamMembers[route.params.id]">
|
<em class="aside" v-if="teamsStore.teamMembers[route.params.id]">
|
||||||
{{ teamsStore.teamMembers[route.params.id]?.length }} member(s),
|
{{ teamsStore.teamMembers[route.params.id]?.length }} member(s),
|
||||||
{{ availableMembers?.length }} currently available
|
{{ availableMembers?.length }} currently available
|
||||||
</em>
|
</em>
|
||||||
|
<div class="team-details-button-group">
|
||||||
|
<button class="accent">
|
||||||
|
<i class="bi bi-calendar-fill margin"></i>
|
||||||
|
View schedule
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</h1>
|
</h1>
|
||||||
<table class="member-table">
|
<table class="member-table">
|
||||||
<!--thead>
|
<!--thead>
|
||||||
|
@ -65,6 +88,42 @@ onMounted(() => {
|
||||||
/>
|
/>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<h2>Active Invites</h2>
|
||||||
|
<div>
|
||||||
|
<details>
|
||||||
|
<summary>View all invites</summary>
|
||||||
|
<span v-if="invites?.length == 0">
|
||||||
|
There are currently no active invites to this team.
|
||||||
|
</span>
|
||||||
|
<table id="invite-table" v-else>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
Key (hover to reveal)
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Creation time
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<InviteEntry
|
||||||
|
v-for="invite in invites"
|
||||||
|
:invite="invite"
|
||||||
|
/>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="create-invite-group">
|
||||||
|
<button class="accent" @click="createInvite">
|
||||||
|
<i class="bi bi-person-fill-add margin" />
|
||||||
|
Create Invite
|
||||||
|
</button>
|
||||||
|
<span class="small aside">
|
||||||
|
Invites are usable once and expire after 24 hours.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
@ -98,4 +157,29 @@ div.member-grid {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
th {
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#invite-table {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid var(--text);
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-details-button-group {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-invite-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
"""Add TeamInvite
|
||||||
|
|
||||||
|
Revision ID: 65714d7e78f8
|
||||||
|
Revises: f50a79c4ae22
|
||||||
|
Create Date: 2024-11-08 23:16:04.669526
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '65714d7e78f8'
|
||||||
|
down_revision = 'f50a79c4ae22'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('team_invites',
|
||||||
|
sa.Column('key', sa.String(length=31), nullable=False),
|
||||||
|
sa.Column('team_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.TIMESTAMP(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
||||||
|
sa.Column('delete_on_use', sa.Boolean(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['team_id'], ['teams.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('key')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('team_invites')
|
||||||
|
# ### end Alembic commands ###
|
|
@ -50,6 +50,7 @@ class Team(db.Model):
|
||||||
minute_offset: Mapped[int] = mapped_column(SmallInteger, default=0)
|
minute_offset: Mapped[int] = mapped_column(SmallInteger, default=0)
|
||||||
|
|
||||||
players: Mapped[List["PlayerTeam"]] = relationship(back_populates="team")
|
players: Mapped[List["PlayerTeam"]] = relationship(back_populates="team")
|
||||||
|
invites: Mapped[List["TeamInvite"]] = relationship(back_populates="team")
|
||||||
|
|
||||||
created_at: Mapped[datetime] = mapped_column(TIMESTAMP, server_default=func.now())
|
created_at: Mapped[datetime] = mapped_column(TIMESTAMP, server_default=func.now())
|
||||||
|
|
||||||
|
@ -145,6 +146,21 @@ class PlayerTeamAvailability(db.Model):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class TeamInvite(db.Model):
|
||||||
|
__tablename__ = "team_invites"
|
||||||
|
|
||||||
|
key: Mapped[str] = mapped_column(String(31), primary_key=True)
|
||||||
|
team_id: Mapped[int] = mapped_column(ForeignKey("teams.id"))
|
||||||
|
delete_on_use: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(TIMESTAMP, server_default=func.now())
|
||||||
|
|
||||||
|
team: Mapped["Team"] = relationship(back_populates="invites")
|
||||||
|
|
||||||
|
class TeamInviteSchema(spec.BaseModel):
|
||||||
|
key: str
|
||||||
|
team_id: int
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
class AuthSession(db.Model):
|
class AuthSession(db.Model):
|
||||||
__tablename__ = "auth_sessions"
|
__tablename__ = "auth_sessions"
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import datetime
|
from datetime import datetime, timezone
|
||||||
|
from random import randint, random
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
from typing import List
|
from typing import List
|
||||||
from flask import Blueprint, abort, jsonify, make_response, request
|
from flask import Blueprint, abort, jsonify, make_response, request
|
||||||
from pydantic.v1 import validator
|
from pydantic.v1 import validator
|
||||||
from spectree import Response
|
from spectree import Response
|
||||||
from sqlalchemy.orm import joinedload, subqueryload
|
from sqlalchemy.orm import joinedload, subqueryload
|
||||||
from models import Player, PlayerSchema, PlayerTeam, PlayerTeamAvailability, PlayerTeamRole, PlayerTeamSchema, Team, TeamSchema, db
|
from models import Player, PlayerSchema, PlayerTeam, PlayerTeamAvailability, PlayerTeamRole, PlayerTeamSchema, Team, TeamInvite, TeamInviteSchema, TeamSchema, db
|
||||||
from middleware import requires_authentication
|
from middleware import requires_authentication
|
||||||
import models
|
import models
|
||||||
from spec import spec, BaseModel
|
from spec import spec, BaseModel
|
||||||
|
@ -238,7 +240,7 @@ class ViewTeamMembersResponse(PlayerSchema):
|
||||||
roles: list[RoleSchema]
|
roles: list[RoleSchema]
|
||||||
availability: int
|
availability: int
|
||||||
playtime: float
|
playtime: float
|
||||||
created_at: datetime.datetime
|
created_at: datetime
|
||||||
|
|
||||||
@api_team.get("/id/<team_id>/players")
|
@api_team.get("/id/<team_id>/players")
|
||||||
@spec.validate(
|
@spec.validate(
|
||||||
|
@ -251,7 +253,7 @@ class ViewTeamMembersResponse(PlayerSchema):
|
||||||
)
|
)
|
||||||
@requires_authentication
|
@requires_authentication
|
||||||
def view_team_members(player: Player, team_id: int, **kwargs):
|
def view_team_members(player: Player, team_id: int, **kwargs):
|
||||||
now = datetime.datetime.now(datetime.timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
player_teams_query = db.session.query(
|
player_teams_query = db.session.query(
|
||||||
PlayerTeam
|
PlayerTeam
|
||||||
|
@ -318,7 +320,6 @@ def edit_member_roles(
|
||||||
target_player_id: int,
|
target_player_id: int,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
print("hiiii lol")
|
|
||||||
target_player = db.session.query(
|
target_player = db.session.query(
|
||||||
PlayerTeam
|
PlayerTeam
|
||||||
).where(
|
).where(
|
||||||
|
@ -353,3 +354,166 @@ def edit_member_roles(
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return make_response({ }, 204)
|
return make_response({ }, 204)
|
||||||
|
|
||||||
|
@api_team.get("/id/<team_id>/invite")
|
||||||
|
@spec.validate(
|
||||||
|
resp=Response(
|
||||||
|
HTTP_200=list[TeamInviteSchema],
|
||||||
|
HTTP_404=None,
|
||||||
|
),
|
||||||
|
operation_id="get_invites"
|
||||||
|
)
|
||||||
|
@requires_authentication
|
||||||
|
def get_invites(player: Player, team_id: int, **kwargs):
|
||||||
|
player_team = db.session.query(
|
||||||
|
PlayerTeam
|
||||||
|
).where(
|
||||||
|
PlayerTeam.player_id == player.steam_id
|
||||||
|
).where(
|
||||||
|
PlayerTeam.team_id == team_id
|
||||||
|
).one_or_none()
|
||||||
|
|
||||||
|
if not player_team:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
invites = db.session.query(
|
||||||
|
TeamInvite
|
||||||
|
).where(
|
||||||
|
TeamInvite.team_id == team_id
|
||||||
|
).all()
|
||||||
|
|
||||||
|
def map_invite_to_schema(invite: TeamInvite):
|
||||||
|
return TeamInviteSchema(
|
||||||
|
key=invite.key,
|
||||||
|
team_id=invite.team_id,
|
||||||
|
created_at=invite.created_at,
|
||||||
|
).dict(by_alias=True)
|
||||||
|
|
||||||
|
return list(map(map_invite_to_schema, invites)), 200
|
||||||
|
|
||||||
|
@api_team.post("/id/<team_id>/invite")
|
||||||
|
@spec.validate(
|
||||||
|
resp=Response(
|
||||||
|
HTTP_200=TeamInviteSchema,
|
||||||
|
HTTP_404=None,
|
||||||
|
),
|
||||||
|
operation_id="create_invite"
|
||||||
|
)
|
||||||
|
@requires_authentication
|
||||||
|
def create_invite(player: Player, team_id: int, **kwargs):
|
||||||
|
player_team = db.session.query(
|
||||||
|
PlayerTeam
|
||||||
|
).where(
|
||||||
|
PlayerTeam.player_id == player.steam_id
|
||||||
|
).where(
|
||||||
|
PlayerTeam.team_id == team_id
|
||||||
|
).one_or_none()
|
||||||
|
|
||||||
|
if not player_team:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
team_id_shifted = int(team_id) << 48
|
||||||
|
random_value_shifted = int(randint(0, (1 << 16) - 1)) << 32
|
||||||
|
timestamp = int(time.time()) & ((1 << 32) - 1)
|
||||||
|
|
||||||
|
key_int = timestamp | team_id_shifted | random_value_shifted
|
||||||
|
key_hex = "%0.16X" % key_int
|
||||||
|
|
||||||
|
invite = TeamInvite()
|
||||||
|
invite.team_id = team_id
|
||||||
|
invite.key = key_hex
|
||||||
|
|
||||||
|
db.session.add(invite)
|
||||||
|
db.session.flush()
|
||||||
|
db.session.refresh(invite)
|
||||||
|
|
||||||
|
response = TeamInviteSchema(
|
||||||
|
key=key_hex,
|
||||||
|
team_id=team_id,
|
||||||
|
created_at=invite.created_at
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return response.dict(by_alias=True), 200
|
||||||
|
|
||||||
|
@api_team.post("/id/<team_id>/consume-invite/<key>")
|
||||||
|
@spec.validate(
|
||||||
|
resp=Response(
|
||||||
|
HTTP_204=None,
|
||||||
|
HTTP_404=None,
|
||||||
|
),
|
||||||
|
operation_id="consume_invite"
|
||||||
|
)
|
||||||
|
@requires_authentication
|
||||||
|
def consume_invite(player: Player, team_id: int, key: str, **kwargs):
|
||||||
|
invite = db.session.query(
|
||||||
|
TeamInvite
|
||||||
|
).where(
|
||||||
|
TeamInvite.team_id == team_id
|
||||||
|
).where(
|
||||||
|
TeamInvite.key == key
|
||||||
|
).one_or_none()
|
||||||
|
|
||||||
|
if not invite:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
player_team = db.session.query(
|
||||||
|
PlayerTeam
|
||||||
|
).where(
|
||||||
|
PlayerTeam.player_id == player.steam_id
|
||||||
|
).where(
|
||||||
|
PlayerTeam.team_id == team_id
|
||||||
|
).one_or_none()
|
||||||
|
|
||||||
|
if player_team:
|
||||||
|
abort(409)
|
||||||
|
|
||||||
|
player_team = PlayerTeam()
|
||||||
|
player_team.player = player
|
||||||
|
player_team.team_id = team_id
|
||||||
|
|
||||||
|
db.session.add(player_team)
|
||||||
|
|
||||||
|
if invite.delete_on_use:
|
||||||
|
db.session.delete(invite)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return make_response({ }, 204)
|
||||||
|
|
||||||
|
@api_team.delete("/id/<team_id>/invite/<key>")
|
||||||
|
@spec.validate(
|
||||||
|
resp=Response(
|
||||||
|
HTTP_204=None,
|
||||||
|
HTTP_404=None,
|
||||||
|
),
|
||||||
|
operation_id="revoke_invite"
|
||||||
|
)
|
||||||
|
@requires_authentication
|
||||||
|
def revoke_invite(player: Player, team_id: int, key: str, **kwargs):
|
||||||
|
player_team = db.session.query(
|
||||||
|
PlayerTeam
|
||||||
|
).where(
|
||||||
|
PlayerTeam.player_id == player.steam_id
|
||||||
|
).where(
|
||||||
|
PlayerTeam.team_id == team_id
|
||||||
|
).one_or_none()
|
||||||
|
|
||||||
|
if not player_team:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
invite = db.session.query(
|
||||||
|
TeamInvite
|
||||||
|
).where(
|
||||||
|
TeamInvite.team_id == team_id
|
||||||
|
).where(
|
||||||
|
TeamInvite.key == key
|
||||||
|
).one_or_none()
|
||||||
|
|
||||||
|
if not invite:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
db.session.delete(invite)
|
||||||
|
db.session.commit()
|
||||||
|
return make_response({ }, 204)
|
||||||
|
|
Loading…
Reference in New Issue