add spaces
This commit is contained in:
343
src/components/spaces/SpacesList.vue
Normal file
343
src/components/spaces/SpacesList.vue
Normal file
@@ -0,0 +1,343 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, onMounted, ref, watch, reactive, onUnmounted} from 'vue';
|
||||
import {Space, SpaceInvite} from "@/models/Space";
|
||||
import {
|
||||
createSpaceInvite,
|
||||
deleteSpaceRequest,
|
||||
getSpaces,
|
||||
kickMemberFromSpaceRequest,
|
||||
leaveSpaceRequest
|
||||
} from "@/services/spaceService";
|
||||
import LoadingView from "@/components/LoadingView.vue";
|
||||
import {formatDate, formatDateTime} from "../../utils/utils";
|
||||
import Button from "primevue/button";
|
||||
import Toast from "primevue/toast";
|
||||
import Dialog from "primevue/dialog";
|
||||
import InputText from "primevue/inputtext";
|
||||
import SpaceCreationDialog from "@/components/spaces/SpaceCreationDialog.vue";
|
||||
import {useToast} from "primevue/usetoast";
|
||||
import {deleteBudgetRequest, getBudgetInfos} from "@/services/budgetsService";
|
||||
import ConfirmDialog from "primevue/confirmdialog";
|
||||
import {useConfirm} from "primevue/useconfirm";
|
||||
import {useSpaceStore} from "@/stores/spaceStore";
|
||||
import {EventBus} from "@/utils/EventBus";
|
||||
import {useUserStore} from "@/stores/userStore";
|
||||
import {User} from "@/models/User";
|
||||
|
||||
const toast = useToast()
|
||||
const confirm = useConfirm();
|
||||
const loading = ref(true);
|
||||
const creationOpened = ref(false);
|
||||
const spaceCreated = async () => {
|
||||
creationOpened.value = false;
|
||||
await spaceStore.fetchSpaces();
|
||||
}
|
||||
const invite = ref<SpaceInvite | null>(null);
|
||||
const inviteUrl = computed(() => {
|
||||
if (invite.value) {
|
||||
return 'https://luminic.space/spaces/invite/' + invite.value.code;
|
||||
}
|
||||
})
|
||||
|
||||
const inviteCreatedDialog = ref<boolean | null>(null);
|
||||
|
||||
const createInvite = async (space: Space) => {
|
||||
confirm.require({
|
||||
message: `Вы действительно хотите создать приглашение в пространство ${space.name}?`,
|
||||
header: 'Создание приграшения',
|
||||
icon: 'pi pi-info-circle',
|
||||
rejectLabel: 'Отмена',
|
||||
rejectProps: {
|
||||
label: 'Отмена',
|
||||
severity: 'secondary',
|
||||
outlined: true
|
||||
},
|
||||
acceptProps: {
|
||||
label: 'Создать',
|
||||
severity: 'success'
|
||||
},
|
||||
accept: async () => {
|
||||
try {
|
||||
await createSpaceInvite().then(async (res) => {
|
||||
await spaceStore.fetchSpaces();
|
||||
invite.value = res;
|
||||
inviteCreatedDialog.value = true;
|
||||
|
||||
})
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка!',
|
||||
detail: error.response?.data?.message || 'Ошибка при создании приглашения',
|
||||
life: 3000
|
||||
});
|
||||
}
|
||||
},
|
||||
reject: () => {
|
||||
toast.add({severity: 'info', summary: 'Отменено', detail: 'Вы отменили создание приглашения', life: 3000});
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
const leaveSpace = async (space: Space) => {
|
||||
|
||||
confirm.require({
|
||||
message: `Вы действительно хотите выйти из пространства ${space.name}?`,
|
||||
header: 'Выход из пространства',
|
||||
icon: 'pi pi-info-circle',
|
||||
rejectLabel: 'Отмена',
|
||||
rejectProps: {
|
||||
label: 'Отмена',
|
||||
severity: 'secondary',
|
||||
outlined: true
|
||||
},
|
||||
acceptProps: {
|
||||
label: 'Выйти',
|
||||
severity: 'danger'
|
||||
},
|
||||
accept: async () => {
|
||||
try {
|
||||
await leaveSpaceRequest(space.id).then((res) => {
|
||||
spaceStore.fetchSpaces();
|
||||
})
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка!',
|
||||
detail: error.response?.data?.message || 'Ошибка при выходе из пространства',
|
||||
life: 3000
|
||||
});
|
||||
}
|
||||
},
|
||||
reject: () => {
|
||||
toast.add({severity: 'info', summary: 'Отменено', detail: 'Вы отменили выход', life: 3000});
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
const kickMember = async (space: Space, user: User) => {
|
||||
|
||||
confirm.require({
|
||||
message: `Вы действительно хотите исключить пользователя ${user.firstName} из пространства ${space.name}?`,
|
||||
header: 'Исключить из пространства',
|
||||
icon: 'pi pi-info-circle',
|
||||
rejectLabel: 'Отмена',
|
||||
rejectProps: {
|
||||
label: 'Отмена',
|
||||
severity: 'secondary',
|
||||
outlined: true
|
||||
},
|
||||
acceptProps: {
|
||||
label: 'Исключить',
|
||||
severity: 'danger'
|
||||
},
|
||||
accept: async () => {
|
||||
try {
|
||||
await kickMemberFromSpaceRequest(space.id, user.username).then((res) => {
|
||||
spaceStore.fetchSpaces();
|
||||
})
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка!',
|
||||
detail: error.response?.data?.message || 'Ошибка при исключении из пространства',
|
||||
life: 3000
|
||||
});
|
||||
}
|
||||
},
|
||||
reject: () => {
|
||||
toast.add({severity: 'info', summary: 'Отменено', detail: 'Вы отменили исключение', life: 3000});
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
const deleteSpace = async (space: Space) => {
|
||||
|
||||
confirm.require({
|
||||
message: `Вы действительно хотите удалить пространство ${space.name}? Будут удалены все бюджеты!`,
|
||||
header: 'Удаление пространства',
|
||||
icon: 'pi pi-info-circle',
|
||||
rejectLabel: 'Отмена',
|
||||
rejectProps: {
|
||||
label: 'Отмена',
|
||||
severity: 'secondary',
|
||||
outlined: true
|
||||
},
|
||||
acceptProps: {
|
||||
label: 'Удалить',
|
||||
severity: 'danger'
|
||||
},
|
||||
accept: async () => {
|
||||
try {
|
||||
await deleteSpaceRequest(space.id).then((res) => {
|
||||
spaceStore.fetchSpaces();
|
||||
})
|
||||
} catch (error: Error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Ошибка!',
|
||||
detail: error.response?.data?.message || 'Ошибка при удалении пространства',
|
||||
life: 3000
|
||||
});
|
||||
}
|
||||
},
|
||||
reject: () => {
|
||||
toast.add({severity: 'info', summary: 'Отменено', detail: 'Вы отменили удаление', life: 3000});
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
const copied = ref(false);
|
||||
|
||||
const copyToClipboard = async (text) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
copied.value = true;
|
||||
// setTimeout(() => copied.value = false, 1500); // Убираем сообщение через 1.5 сек
|
||||
} catch (err) {
|
||||
console.error('Ошибка копирования:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const spaces = computed(() => spaceStore.spaces)
|
||||
|
||||
|
||||
const spaceStore = useSpaceStore()
|
||||
const selectedSpace = computed(() => spaceStore.space)
|
||||
|
||||
const selectSpace = (space: Space) => {
|
||||
spaceStore.setSpace(space);
|
||||
localStorage.setItem("spaceId", space.id);
|
||||
}
|
||||
const loadingValue = computed(() => {
|
||||
return !spaces.value
|
||||
})
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<LoadingView v-if="loadingValue"/>
|
||||
|
||||
<div v-else class="p-4 bg-gray-100 h-full grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-4 ">
|
||||
<Toast/>
|
||||
<ConfirmDialog/>
|
||||
<Dialog :visible="inviteCreatedDialog" header="Приглашение" @hide="inviteCreatedDialog=false"
|
||||
@update:visible="inviteCreatedDialog=false">
|
||||
<div class="flex flex-col justify-start">
|
||||
<div class="flex flex-row gap-2 items-center"> Ссылка приглашения:
|
||||
<input class="p-2 border min-w-fit w-80" v-model="inviteUrl" disabled></input>
|
||||
<button @click="copyToClipboard('https://luminic.space/spaces/invite/' + invite.code)">
|
||||
{{ !copied ? 'Копировать' : 'Скопировано!' }}
|
||||
</button>
|
||||
</div>
|
||||
<p>Действует до {{ formatDateTime(invite.activeTill) }} и только на 1 использование.</p>
|
||||
</div>
|
||||
</Dialog>
|
||||
<div v-for="space in spaces" class="w-full ">
|
||||
|
||||
<div
|
||||
class="bg-white p-4 shadow-lg rounded-lg flex flex-col gap-2 items-start justify-between h-[200px] ">
|
||||
<div class="flex flex-col w-full h-full justify-between">
|
||||
|
||||
<div class="flex flex-row justify-between"><p class="text-xl font-bold line-clamp-1">{{ space.name }}</p>
|
||||
<button @click="selectSpace(space)">
|
||||
|
||||
<div class="flex p-1 rounded border"
|
||||
:class="selectedSpace? selectedSpace.id === space.id ? 'bg-green-100' : 'bg-gray-100':'bg-gray-100'"
|
||||
@click="spaceStore.setSpace(space)">
|
||||
<span v-if="selectedSpace? space.id === selectedSpace.id : false"
|
||||
class="font-bold text-gray-500 items-center"><i
|
||||
class="pi pi-check"/> Выбрано</span>
|
||||
<span v-else class="">Выбрать</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="line-clamp-2 items-start flex ">{{ space.description }}</p>
|
||||
<div class="flex flex-row gap-2">
|
||||
<div v-for="user in space.users"
|
||||
:key="user.id"
|
||||
@mouseover="user.isHovered = true"
|
||||
@mouseleave="user.isHovered = false">
|
||||
<div
|
||||
|
||||
class="relative flex bg-emerald-300 rounded-full w-10 h-10 items-center justify-center"
|
||||
|
||||
>
|
||||
<!-- Первая буква имени -->
|
||||
<span class="text-white text-center">
|
||||
{{ user.firstName.substring(0, 1) }}
|
||||
</span>
|
||||
|
||||
<!-- Иконка короны для владельца -->
|
||||
<i
|
||||
v-if="space.owner.id === user.id"
|
||||
class="pi pi-crown absolute -top-1 -right-1 text-yellow-400 z-10 bg-white rounded-full p-[0.2rem] border"
|
||||
style="font-size: 0.8rem"
|
||||
></i>
|
||||
|
||||
<!-- Всплывающее окно -->
|
||||
<div
|
||||
v-if="user.isHovered"
|
||||
class="absolute top-10 left-20 transform -translate-x-1/2 bg-white shadow-lg rounded-lg p-2 w-40 z-50 border"
|
||||
>
|
||||
<p class="text-sm font-semibold text-gray-800">{{ user.firstName }} {{ user.lastName }}</p>
|
||||
<p class="text-xs text-gray-500">Роль: {{ user.role || 'Пользователь' }}</p>
|
||||
|
||||
<!-- Кнопка удаления (только если это не текущий пользователь) -->
|
||||
<button
|
||||
v-if="user.id !== useUserStore().user.id"
|
||||
@click="kickMember(space, user)"
|
||||
class="mt-2 bg-red-500 text-white text-xs rounded p-1 w-full hover:bg-red-600 transition"
|
||||
>
|
||||
Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="space.owner.id == useUserStore().user.id"
|
||||
class="flex bg-gray-300 hover:bg-emerald-300 rounded-full w-10 h-10 items-center justify-center relative"
|
||||
>
|
||||
<i class="text-white text-center pi pi-plus" @click="createInvite(space)"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row justify-between items-center w-full">
|
||||
<p class="text-sm text-gray-600">Создано: {{ formatDate(space.createdAt) }}</p>
|
||||
<button v-if="space.owner.id == useUserStore().user.id" @click="deleteSpace(space)"
|
||||
class="flex justify-end"><i class="pi pi-trash"
|
||||
style="color:red; font-size: 1.2rem"/>
|
||||
</button>
|
||||
<button v-else class="flex items-center gap-2 text-sm" @click="leaveSpace(space)">Выйти<i
|
||||
class="pi pi-sign-out" style="font-size: 0.7rem"/></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="w-full h-full">
|
||||
<div class="bg-white p-4 shadow-lg rounded-lg flex flex-col gap-2 justify-center items-center h-[200px]">
|
||||
<button class="flex-col" @click="creationOpened = !creationOpened">
|
||||
<i class="pi pi-plus-circle text-emerald-300" style="font-size: 2.5rem"></i>
|
||||
<p class="text-emerald-600">Создать пространство</p>
|
||||
</button>
|
||||
<SpaceCreationDialog :opened="creationOpened" @spaceCreated="spaceCreated"
|
||||
@close-modal="creationOpened = false"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user