210 lines
4.9 KiB
Vue
210 lines
4.9 KiB
Vue
<script setup lang="ts">
|
|
import type { Team } from '~/pages/hunt/[huntSlug]/teams.vue';
|
|
import { fullDate } from '~~/server/utils/date';
|
|
|
|
const { confirm } = useModal();
|
|
const { init } = useToast();
|
|
const { t, d } = useI18n();
|
|
|
|
const props = defineProps<{
|
|
team: Team;
|
|
huntId: number;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
refresh: [];
|
|
}>();
|
|
const loading = ref(false);
|
|
|
|
const search = ref('');
|
|
const safeSearch = ref('');
|
|
const newUser = ref<User>();
|
|
watchDebounced(
|
|
search,
|
|
(newSearch) => {
|
|
if (newSearch) {
|
|
safeSearch.value = newSearch;
|
|
}
|
|
},
|
|
{ debounce: 500, maxWait: 1000 }
|
|
);
|
|
|
|
type User = {
|
|
id: number;
|
|
name: string;
|
|
email: string;
|
|
};
|
|
const { data: users, pending: searchPending } = await useFetch(
|
|
() => `/api/admin/hunt/${props.huntId}/teamless?email=${safeSearch.value}`,
|
|
{
|
|
immediate: false,
|
|
lazy: true,
|
|
default: () => [] as User[]
|
|
}
|
|
);
|
|
|
|
async function kick(member: Team['members'][number]) {
|
|
const ok = await confirm({
|
|
title: 'Are you sure?',
|
|
message: `Are you sure you want to kick ${member.member.name} (${member.member.email}) from this team?`,
|
|
okText: 'Yes'
|
|
});
|
|
|
|
if (!ok) {
|
|
return;
|
|
}
|
|
|
|
loading.value = true;
|
|
|
|
try {
|
|
const { success } = await $fetch(`/api/admin/team/${props.team.id}/kick`, {
|
|
method: 'POST',
|
|
body: { userId: member.member.id }
|
|
});
|
|
init({
|
|
message: success ? 'Successfully kicked user!' : 'Error while kicking',
|
|
color: success ? 'success' : 'danger'
|
|
});
|
|
} catch (e) {
|
|
init({
|
|
title: 'Error while kicking',
|
|
color: 'danger',
|
|
// @ts-expect-error weird error stuff
|
|
message: e?.message || e?.statusMessage || t(parseError(e))
|
|
});
|
|
}
|
|
|
|
emit('refresh');
|
|
// await refresh();
|
|
loading.value = false;
|
|
}
|
|
|
|
async function addUser() {
|
|
if (!newUser.value) {
|
|
return;
|
|
}
|
|
loading.value = true;
|
|
|
|
try {
|
|
const { success } = await $fetch(`/api/admin/team/${props.team.id}/add`, {
|
|
method: 'POST',
|
|
body: { userId: newUser.value.id }
|
|
});
|
|
init({
|
|
message: success ? 'Successfully added user!' : 'Error while adding',
|
|
color: success ? 'success' : 'danger'
|
|
});
|
|
} catch (e) {
|
|
init({
|
|
title: 'Error while adding',
|
|
color: 'danger',
|
|
// @ts-expect-error weird error stuff
|
|
message: e?.message || e?.statusMessage || t(parseError(e))
|
|
});
|
|
}
|
|
|
|
emit('refresh');
|
|
newUser.value = undefined;
|
|
loading.value = false;
|
|
}
|
|
|
|
async function deleteTeam() {
|
|
const ok = await confirm({
|
|
title: 'Are you sure?',
|
|
message:
|
|
`Are you sure you want to delete this team?` +
|
|
(props.team._count.answers > 0
|
|
? ` This team has submitted answers!`
|
|
: ''),
|
|
okText: 'Yes'
|
|
});
|
|
|
|
if (!ok) {
|
|
return;
|
|
}
|
|
|
|
loading.value = true;
|
|
|
|
try {
|
|
const { success } = await $fetch(`/api/admin/team/${props.team.id}`, {
|
|
method: 'DELETE'
|
|
});
|
|
init({
|
|
message: success ? 'Successfully deleted team!' : 'Error while deleting',
|
|
color: success ? 'success' : 'danger'
|
|
});
|
|
} catch (e) {
|
|
init({
|
|
title: 'Error while deleting',
|
|
color: 'danger',
|
|
// @ts-expect-error weird error stuff
|
|
message: e?.message || e?.statusMessage || t(parseError(e))
|
|
});
|
|
}
|
|
|
|
emit('refresh');
|
|
loading.value = false;
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<VaCollapse
|
|
:key="team.id"
|
|
:header="`${team.name} — ${team.members.length} members — ${team._count.answers} answers — ${team.score} points`"
|
|
>
|
|
<div class="flex flex-col gap-2">
|
|
<h2 class="text-xl">
|
|
{{ team.name }}
|
|
<VaButton
|
|
color="danger"
|
|
:disabled="loading"
|
|
:loading="loading"
|
|
@click="deleteTeam"
|
|
>
|
|
Delete team</VaButton
|
|
>
|
|
</h2>
|
|
<p>
|
|
Created at:
|
|
{{ d(team.createdAt, fullDate) }}
|
|
by {{ team.owner.name }} ({{ team.owner.email }})
|
|
</p>
|
|
<p>{{ team._count.answers }} answers</p>
|
|
<div class="grid grid-cols-1 gap-2 md:grid-cols-3 lg:grid-cols-5">
|
|
<VaButton
|
|
v-for="member in team.members"
|
|
:key="member.member.id"
|
|
color="danger"
|
|
:disabled="loading"
|
|
:loading="loading"
|
|
@click="kick(member)"
|
|
>Kick {{ member.member.name }} ({{ member.member.email }})</VaButton
|
|
>
|
|
</div>
|
|
<div class="flex flex-col gap-2 md:flex-row">
|
|
<VaSelect
|
|
v-model="newUser"
|
|
v-model:search="search"
|
|
label="New Member"
|
|
placeholder="Start typing their email..."
|
|
autocomplete
|
|
highlight-matched-text
|
|
:options="users"
|
|
:loading="searchPending"
|
|
track-by="id"
|
|
text-by="email"
|
|
/><VaButton
|
|
icon="add"
|
|
color="success"
|
|
:disabled="!newUser || loading"
|
|
:loading="loading"
|
|
@click="addUser"
|
|
>Add user</VaButton
|
|
>
|
|
</div>
|
|
</div>
|
|
</VaCollapse>
|
|
</template>
|
|
|
|
<style scoped></style>
|