Social: Refactor Invites, Add Group Search, Move Promoted Tab - refs BT#21101

pull/5641/head
christianbeeznst 5 months ago
parent 4bf62d1bdc
commit 795cd9686d
  1. 77
      assets/css/scss/_social.scss
  2. 20
      assets/vue/components/social/MyFriendsCard.vue
  3. 26
      assets/vue/components/social/MyGroupsCard.vue
  4. 93
      assets/vue/components/usergroup/Layout.vue
  5. 58
      assets/vue/components/userreluser/InvitationList.vue
  6. 72
      assets/vue/components/userreluser/Layout.vue
  7. 4
      assets/vue/components/userreluser/UserRelUserRequestsList.vue
  8. 5
      assets/vue/composables/sidebarMenu.js
  9. 3
      assets/vue/composables/useSocialMenuItems.js
  10. 1
      assets/vue/router/userreluser.js
  11. 50
      assets/vue/views/social/SocialWall.vue
  12. 322
      assets/vue/views/usergroup/List.vue
  13. 98
      assets/vue/views/userreluser/UserRelUserList.vue

@ -400,19 +400,21 @@
background: none;
}
.tab-header {
.tab {
padding: 0.5rem 1rem;
cursor: pointer;
padding: 0.75rem 1rem;
border-bottom: 3px solid transparent;
border-bottom: 2px solid transparent;
transition: border-color 0.3s;
}
.tab-header:hover {
border-bottom: 3px solid #64B5F6;
.tab:hover {
border-bottom: 2px solid #d1d5db;
}
.active-tab:hover {
border-bottom: 3px solid #1976D2;
.tab-active {
border-bottom: 2px solid #3b82f6;
color: #3b82f6;
font-weight: bold;
}
}
@ -1036,3 +1038,64 @@
.circle-gray {
color: gray;
}
.icon-spacing {
margin-right: 8px;
}
#social-wall-container {
.tab {
padding: 0.5rem 1rem;
border-radius: 9999px;
transition: background-color 0.3s, color 0.3s;
cursor: pointer;
}
.tab:hover {
background-color: #e2e8f0;
}
.tab-active {
background-color: #3b82f6;
color: white;
}
}
#social-group-container {
.user-invite-card {
max-width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.user-invite-card .flex {
display: flex;
justify-content: space-between;
align-items: center;
}
.user-invite-card .flex h4 {
margin: 0;
}
.user-invite-card .flex span {
display: block;
margin-top: 0.25rem;
color: #6b7280;
}
.user-invite-card .flex .space-x-2 {
display: flex;
gap: 0.5rem;
}
.user-invite-card .flex .space-x-2 button {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 50%;
}
}

@ -1,30 +1,30 @@
<template>
<BaseCard plain class="my-groups-card bg-white mb-3">
<template #header>
<div class="px-4 py-2 -mb-2 bg-gray-15">
<h2 class="text-h5">{{ t('My friends') }}</h2>
<div class="px-4 py-3 bg-gray-200">
<h2 class="text-xl font-semibold">{{ t('My friends') }}</h2>
</div>
</template>
<hr class="-mt-2 mb-4 -mx-4">
<div>
<div v-if="isCurrentUser" class="input-group mb-3">
<hr class="my-2">
<div class="px-4">
<div v-if="isCurrentUser" class="flex items-center mb-4">
<input
type="search"
class="form-control"
class="flex-grow p-2 h-[44px] border border-gray-300 rounded-l-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Search"
v-model="searchQuery"
@input="fetchFriends"
>
<button
class="btn btn-outline-secondary"
class="p-2 h-[44px] bg-gray-200 border border-gray-300 rounded-r-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500"
type="button"
@click="search"
>
<i class="mdi mdi-magnify"></i>
<i class="mdi mdi-magnify text-gray-700"></i>
</button>
</div>
<ul class="list-group">
<li v-for="friend in limitedFriends" :key="friend.id" class="list-group-item friend-item d-flex align-items-center">
<ul class="list-group mb-4">
<li v-for="friend in limitedFriends" :key="friend.id" class="list-group-item friend-item d-flex align-items-center mb-2">
<a :href="`/social?id=${friend.friend.id}`" class="d-flex align-items-center text-decoration-none">
<BaseUserAvatar :image-url="friend.friend.illustrationUrl" class="mr-2" :alt="t('Picture')" />
<span>{{ friend.friend.firstname }} {{ friend.friend.lastname }} <small class="text-muted">({{ friend.friend.username }})</small></span>

@ -1,39 +1,39 @@
<template>
<BaseCard plain class="my-groups-card bg-white mb-3">
<BaseCard plain class="my-groups-card bg-white mt-3 mb-3">
<template #header>
<div class="px-2 py-2 -mb-2 bg-gray-15">
<h2 class="text-h5">{{ t('My communities') }}</h2>
<div class="px-4 py-3 bg-gray-200">
<h2 class="text-xl font-semibold">{{ t('My communities') }}</h2>
</div>
</template>
<hr class="-mt-2 mb-4 -mx-4">
<div class="px-2">
<ul class="mb-3">
<hr class="my-2">
<div class="px-4">
<ul class="mb-4">
<li
class="list-group-item"
class="mb-2"
v-for="group in groups"
:key="group.id"
>
<a :href="group.url || '#'" v-if="group.url">{{ group.name }}</a>
<a :href="group.url || '#'" v-if="group.url" class="text-blue-600 hover:underline">{{ group.name }}</a>
<span v-else>{{ group.name }}</span>
</li>
</ul>
<div v-if="isValidGlobalForumsCourse" class="text-center mb-3">
<div v-if="isValidGlobalForumsCourse" class="text-center mb-4">
<a :href="goToUrl" class="btn btn-primary">{{ t('See all communities') }}</a>
</div>
<div v-else >
<div v-if="isCurrentUser" class="input-group mb-3">
<div v-if="isCurrentUser" class="flex items-center mb-4">
<input
type="search"
class="form-control"
class="flex-grow p-2 h-[44px] border border-gray-300 rounded-l-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Search"
v-model="searchQuery"
>
<button
class="btn btn-outline-secondary"
class="p-2 h-[44px] bg-gray-200 border border-gray-300 rounded-r-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500"
type="button"
@click="search"
>
<i class="mdi mdi-magnify"></i>
<i class="mdi mdi-magnify text-gray-700"></i>
</button>
</div>
</div>

@ -1,9 +1,52 @@
<template>
<div class="flex flex-col md:flex-row gap-4">
<div class="flex flex-col md:flex-row gap-4" id="social-group-container">
<div class="md:basis-1/3 lg:basis-1/4 2xl:basis-1/6 flex flex-col">
<UserProfileCard v-if="!isLoading && !isGroup" />
<GroupInfoCard v-if="!isLoading && isGroup" />
<SocialGroupMenu v-if="!isLoading && isGroup" />
<BaseCard v-if="isCurrentUser" plain class="mt-4 invite-friends">
<div class="flex flex-col items-center p-2 user-invite-card">
<div class="w-full">
<div class=" bg-gray-200 border-b border-gray-300 rounded-t-lg text-center">
<h2 class="text-xl font-semibold">{{ t('Pending Group Invitations') }}</h2>
</div>
<div class="pbg-white">
<div v-if="pendingInvitations.length > 0" class="space-y-4">
<div v-for="invitation in pendingInvitations" :key="invitation.id" class="flex items-center border rounded-lg shadow-sm bg-white">
<div class="ml-4 flex-grow text-center">
<h4 class="text-lg font-semibold">
<a :href="'profile.php?u=' + invitation.itemId" class="text-blue-600 hover:underline">{{ invitation.itemName }}</a>
</h4>
<span class="text-sm text-gray-500">{{ invitation.date }}</span>
</div>
<div class="flex space-x-2">
<BaseButton
v-if="invitation.canAccept"
icon="mdi-check"
type="success"
size="small"
only-icon
@click="() => acceptGroupInvitation(invitation.itemId)"
/>
<button
v-if="invitation.canDeny"
class="remove-btn"
@click="() => denyGroupInvitation(invitation.itemId)"
>
-
</button>
</div>
</div>
</div>
<div v-else class="p-4 text-center text-gray-500">
<p>{{ t("No invitations or records found") }}</p>
</div>
</div>
</div>
</div>
</BaseCard>
</div>
<div class="md:basis-2/3 lg:basis-3/4 2xl:basis-5/6">
<router-view></router-view>
@ -12,17 +55,61 @@
</template>
<script setup>
import UserProfileCard from "../social/UserProfileCard.vue"
import { provide } from "vue"
import { onMounted, provide, ref } from "vue"
import { useSocialInfo } from "../../composables/useSocialInfo"
import SocialGroupMenu from "../social/SocialGroupMenu.vue"
import GroupInfoCard from "../social/GroupInfoCard.vue"
import BaseButton from "../../components/basecomponents/BaseButton.vue"
import socialService from "../../services/socialService"
import { useNotification } from "../../composables/notification"
import { useI18n } from "vue-i18n"
import BaseCard from "../../components/basecomponents/BaseCard.vue"
const { t } = useI18n()
const { user, isCurrentUser, groupInfo, isGroup, loadGroup, loadUser, isLoading } = useSocialInfo()
const notification = useNotification()
const pendingInvitations = ref([])
const fetchInvitations = async (userId) => {
if (!userId) return
try {
const data = await socialService.fetchInvitations(userId)
pendingInvitations.value = data.pendingGroupInvitations
} catch (error) {
console.error('Error fetching invitations:', error)
}
}
const acceptGroupInvitation = async (groupId) => {
try {
await socialService.acceptGroupInvitation(user.value.id, groupId)
console.log('Group invitation accepted successfully')
await fetchInvitations(user.value.id)
} catch (error) {
console.error('Error accepting group invitation:', error)
}
}
const denyGroupInvitation = async (groupId) => {
try {
await socialService.denyGroupInvitation(user.value.id, groupId)
console.log('Group invitation denied successfully')
await fetchInvitations(user.value.id)
} catch (error) {
console.error('Error denying group invitation:', error)
}
}
provide("social-user", user)
provide("is-current-user", isCurrentUser)
provide("group-info", groupInfo)
provide("is-group", isGroup)
onMounted(async () => {
await loadUser()
if (user.value && user.value.id) {
await fetchInvitations(user.value.id)
}
})
</script>

@ -2,40 +2,40 @@
<div class="friends-invitations">
<BaseCard plain class="bg-white mt-4">
<template #header>
<div class="px-4 py-2 bg-gray-15">
<h2 class="text-h5">{{ title }}</h2>
<div class="px-4 py-2 bg-gray-100 border-b border-gray-300">
<h2 class="text-xl font-semibold">{{ title }}</h2>
</div>
</template>
<hr class="my-4">
<div v-if="invitations && invitations.length > 0" class="invitation-list">
<div v-for="invitation in invitations" :key="invitation.id" class="invitation-item">
<div class="invitation-content">
<img :src="invitation.itemPicture" class="item-picture" alt="Item picture">
<div class="invitation-info">
<h4><a :href="'profile.php?u=' + invitation.itemId">{{ invitation.itemName }}</a></h4>
<p>{{ invitation.content }}</p>
<span>{{ invitation.date }}</span>
</div>
<div class="invitation-actions">
<BaseButton
v-if="invitation.canAccept"
label="Accept"
icon="check"
type="success"
@click="emitEvent('accept', invitation.itemId)"
/>
<BaseButton
v-if="invitation.canDeny"
label="Deny"
icon="times"
type="danger"
@click="emitEvent('deny', invitation.itemId)"
/>
</div>
<div v-if="invitations && invitations.length > 0" class="space-y-4">
<div v-for="invitation in invitations" :key="invitation.id" class="flex items-center p-4 border rounded-lg shadow-sm bg-white">
<img :src="invitation.itemPicture" class="w-16 h-16 rounded-full" alt="Item picture">
<div class="ml-4 flex-grow">
<h4 class="text-lg font-semibold"><a :href="'profile.php?u=' + invitation.itemId" class="text-blue-600 hover:underline">{{ invitation.itemName }}</a></h4>
<p class="text-gray-600">{{ invitation.content }}</p>
<span class="text-sm text-gray-500">{{ invitation.date }}</span>
</div>
<div class="flex space-x-2">
<BaseButton
v-if="invitation.canAccept"
label="Accept"
icon="check"
type="success"
@click="emitEvent('accept', invitation.itemId)"
class="ml-2"
/>
<BaseButton
v-if="invitation.canDeny"
label="Deny"
icon="times"
type="danger"
@click="emitEvent('deny', invitation.itemId)"
class="ml-2"
/>
</div>
</div>
</div>
<div v-else class="no-invitations-message">
<div v-else class="p-4 text-center text-gray-500">
<p>{{ t("No invitations or records found") }}</p>
</div>
</BaseCard>
@ -46,10 +46,8 @@
import BaseCard from "../basecomponents/BaseCard.vue"
import BaseButton from "../basecomponents/BaseButton.vue"
import { useI18n } from "vue-i18n"
import { useFormatDate } from "../../composables/formatDate"
const { t } = useI18n()
const { relativeDatetime } = useFormatDate()
const props = defineProps({
invitations: Array,
title: String

@ -1,18 +1,39 @@
<template>
<div class="flex flex-col md:flex-row gap-4">
<div class="md:basis-1/3 lg:basis-1/4 2xl:basis-1/6 flex flex-col">
<div class="md:basis-1/4 lg:basis-1/6 2xl:basis-1/8 flex flex-col">
<UserProfileCard />
<BaseCard plain class="mt-4">
<template #header>
<div class="px-4 py-3 bg-gray-200 border-b border-gray-300">
<h3 class="text-xl font-semibold">{{ t('Requests') }}</h3>
</div>
</template>
<div class="px-4 py-3">
<UserRelUserRequestsList
v-if="isCurrentUser"
ref="requestList"
@accept-friend="reloadHandler"
/>
</div>
</BaseCard>
</div>
<div class="md:basis-2/3 lg:basis-3/4 2xl:basis-5/6">
<div class="md:basis-3/4 lg:basis-5/6 2xl:basis-7/8">
<router-view></router-view>
</div>
</div>
</template>
<script setup>
import UserProfileCard from "../social/UserProfileCard.vue"
import { onMounted, provide } from "vue"
import { nextTick, onMounted, provide, ref } from "vue"
import { useSocialInfo } from "../../composables/useSocialInfo"
import UserRelUserRequestsList from "./UserRelUserRequestsList.vue"
import BaseCard from "../../components/basecomponents/BaseCard.vue"
import { useNotification } from "../../composables/notification"
import userRelUserService from "../../services/userreluser"
import { useI18n } from "vue-i18n"
const { t } = useI18n()
const { user, isCurrentUser, groupInfo, isGroup, loadUser } = useSocialInfo()
provide("social-user", user)
@ -20,5 +41,48 @@ provide("is-current-user", isCurrentUser)
provide("group-info", groupInfo)
provide("is-group", isGroup)
onMounted(loadUser)
const requestList = ref(null)
const items = ref([])
const loadingFriends = ref(true)
const notification = useNotification()
const friendFilter = {
user: user.id,
relationType: 3, // friend status
}
const friendBackFilter = {
friend: user.id,
relationType: 3, // friend status
}
const reloadHandler = async () => {
loadingFriends.value = true
items.value = []
try {
const [friendshipResponse, friendshipBackResponse] = await Promise.all([
userRelUserService.findAll({ params: friendFilter }),
userRelUserService.findAll({ params: friendBackFilter }),
])
const [friendshipJson, friendshipBackJson] = await Promise.all([
friendshipResponse.json(),
friendshipBackResponse.json(),
])
items.value.push(...friendshipJson["hydra:member"], ...friendshipBackJson["hydra:member"])
} catch (e) {
notification.showErrorNotification(e)
} finally {
loadingFriends.value = false
if (requestList.value) {
await nextTick()
requestList.value.loadRequests()
}
}
}
onMounted(async () => {
await loadUser()
await reloadHandler()
})
</script>

@ -1,8 +1,4 @@
<template>
<h3 v-t="'Requests'" />
<hr />
<div
v-if="loading"
class="space-y-4"

@ -132,8 +132,9 @@ export function useSidebarMenu() {
const styledSocialItems = socialItems.value.map((item) => {
const newItem = {
...item,
class: `sub-item-indent${isActive(item) ? " active" : ""}`,
}
class: `sub-item-indent${isActive(item) ? ' active' : ''}`,
icon: item.icon ? `${item.icon} icon-spacing` : null
};
if (newItem.isLink && newItem.route) {
newItem.url = newItem.route

@ -60,13 +60,10 @@ export function useSocialMenuItems() {
return isCurrentUser.value ? [
{ icon: 'mdi mdi-home', label: t("Home"), route: '/social' },
{ icon: 'mdi mdi-email', label: t("Messages"), route: '/resources/messages', badgeCount: unreadMessagesCount.value },
{ icon: 'mdi mdi-mailbox', label: t("Invitations"), route: { name: 'Invitations' }, badgeCount: invitationsCount.value },
{ icon: 'mdi mdi-handshake', label: t("My friends"), route: { name: 'UserRelUserList' } },
{ icon: 'mdi mdi-group', label: t("Social groups"), route: groupLink.value, isLink: isValidGlobalForumsCourse.value },
{ icon: 'mdi mdi-magnify', label: t("Search"), route: '/social/search' },
{ icon: 'mdi mdi-briefcase', label: t("My files"), route: { name: 'PersonalFileList', params: { node: securityStore.user.resourceNode.id } } },
{ icon: 'mdi mdi-account', label: t("Personal data"), route: '/resources/users/personal_data' },
{ icon: 'mdi mdi-star', label: t("Promoted messages"), route: { path: '/social', query: { filterType: 'promoted' } } }
] : [
{ icon: 'mdi mdi-home', label: t("Home"), route: '/social' },
{ icon: 'mdi mdi-email', label: t("Send message"), link: `/main/inc/ajax/user_manager.ajax.php?a=get_user_popup&user_id=${user.value.id}`, isExternal: true }

@ -12,7 +12,6 @@ export default {
},
{
name: 'UserRelUserAdd',
//path: ':id',
path: 'add',
component: () => import('../views/userreluser/UserRelUserAdd.vue')
},

@ -1,14 +1,33 @@
<template>
<div>
<SocialWallPostForm v-if="!hidePostForm && isCurrentUser" @post-created="refreshPosts" class="mb-6" />
<div id="social-wall-container">
<div class="flex justify-center mb-6 space-x-4">
<button
:class="['tab', { 'tab-active': filterType === null }]"
@click="filterMessages(null)"
>
{{ t('All Messages') }}
</button>
<button
:class="['tab', { 'tab-active': filterType === 'promoted' }]"
@click="filterMessages('promoted')"
>
{{ t('Promoted Messages') }}
</button>
</div>
<SocialWallPostForm v-if="!hidePostForm && isCurrentUser" @post-created="refreshPosts" class="mb-6" />
<SocialWallPostList ref="postListRef" class="mb-6" />
</div>
</template>
<script setup>
import { inject, ref } from "vue"
import SocialWallPostForm from "../../components/social/SocialWallPostForm.vue"
import SocialWallPostList from "../../components/social/SocialWallPostList.vue"
import { inject, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import SocialWallPostForm from "../../components/social/SocialWallPostForm.vue";
import SocialWallPostList from "../../components/social/SocialWallPostList.vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const props = defineProps({
hidePostForm: {
@ -18,11 +37,30 @@ const props = defineProps({
});
const postListRef = ref(null);
const isCurrentUser = inject('is-current-user')
const isCurrentUser = inject('is-current-user');
const route = useRoute();
const router = useRouter();
const filterType = ref(route.query.filterType || null);
watch(
() => route.query.filterType,
(newFilterType) => {
filterType.value = newFilterType;
refreshPosts();
}
);
function refreshPosts() {
if (postListRef.value) {
postListRef.value.refreshPosts();
}
}
function filterMessages(type) {
router.push({ path: '/social', query: { filterType: type } });
}
</script>
<style scoped>
</style>

@ -1,122 +1,135 @@
<template>
<div class="p-grid p-nogutter social-groups">
<div class="p-col-12">
<div class="p-d-flex p-jc-between p-ai-center p-mb-4">
<h1>Social groups</h1>
<Button
class="create-group-button"
icon="pi pi-plus"
label="Create a social group"
@click="showCreateGroupDialog = true"
/>
</div>
<TabView class="social-group-tabs">
<TabPanel
:class="{ 'active-tab': activeTab === 'Newest' }"
header="Newest"
header-class="tab-header"
<div class="p-4 social-groups">
<div class="flex justify-between items-center mb-4">
<h1 class="text-2xl font-semibold">Social Groups</h1>
<Button
class="bg-blue-500 text-white rounded-md px-4 py-2"
icon="pi pi-plus"
label="Create a social group"
@click="showCreateGroupDialog = true"
/>
</div>
<div class="tabs mb-4">
<button
:class="['tab', { 'tab-active': activeTab === 'Newest' }]"
@click="activeTab = 'Newest'"
>
Newest
</button>
<button
:class="['tab', { 'tab-active': activeTab === 'Popular' }]"
@click="activeTab = 'Popular'"
>
Popular
</button>
<button
:class="['tab', { 'tab-active': activeTab === 'My groups' }]"
@click="activeTab = 'My groups'"
>
My Groups
</button>
<button
:class="['tab', { 'tab-active': activeTab === 'Search Groups' }]"
@click="activeTab = 'Search Groups'"
>
Search Groups
</button>
</div>
<div v-show="activeTab === 'Newest'">
<div class="group-list">
<div
v-for="group in newestGroups"
:key="group['@id']"
class="group-item flex items-center p-4 bg-white shadow-md rounded-md mb-4"
>
<div class="group-list">
<div
v-for="group in newestGroups"
:key="group['@id']"
class="group-item"
<img
v-if="group.pictureUrl"
:src="group.pictureUrl"
alt="Group Image"
class="w-16 h-16 rounded-full mr-4"
/>
<i
v-else
class="mdi mdi-account-group-outline text-4xl text-gray-500 mr-4"
></i>
<div class="group-details">
<a
:href="`/resources/usergroups/show/${extractGroupId(group)}`"
class="text-lg font-semibold text-blue-600 hover:underline"
>{{ group.title }}</a
>
<img
v-if="group.pictureUrl"
:src="group.pictureUrl"
alt="Group Image"
class="group-image"
/>
<i
v-else
class="mdi mdi-account-group-outline group-icon"
></i>
<div class="group-details">
<a
:href="`/resources/usergroups/show/${extractGroupId(group)}`"
class="group-title"
>{{ group.title }}</a
>
<div class="group-info">
<span class="group-member-count">{{ group.memberCount }} {{ t("Member") }}</span>
<span class="group-description">{{ group.description }}</span>
</div>
</div>
<div class="group-info text-gray-500">
<span class="group-member-count">{{ group.memberCount }} Members</span>
<span class="group-description">{{ group.description }}</span>
</div>
</div>
</TabPanel>
<TabPanel
:class="{ 'active-tab': activeTab === 'Popular' }"
header="Popular"
header-class="tab-header"
</div>
</div>
</div>
<div v-show="activeTab === 'Popular'">
<div class="group-list">
<div
v-for="group in popularGroups"
:key="group['@id']"
class="group-item flex items-center p-4 bg-white shadow-md rounded-md mb-4"
>
<div class="group-list">
<div
v-for="group in popularGroups"
:key="group['@id']"
class="group-item"
<img
v-if="group.pictureUrl"
:src="group.pictureUrl"
alt="Group Image"
class="w-16 h-16 rounded-full mr-4"
/>
<i
v-else
class="mdi mdi-account-group-outline text-4xl text-gray-500 mr-4"
></i>
<div class="group-details">
<a
:href="`/resources/usergroups/show/${extractGroupId(group)}`"
class="text-lg font-semibold text-blue-600 hover:underline"
>{{ group.title }}</a
>
<img
v-if="group.pictureUrl"
:src="group.pictureUrl"
alt="Group Image"
class="group-image"
/>
<i
v-else
class="mdi mdi-account-group-outline group-icon"
></i>
<div class="group-details">
<a
:href="`/resources/usergroups/show/${extractGroupId(group)}`"
class="group-title"
>{{ group.title }}</a
>
<div class="group-info">
<span class="group-member-count">{{ group.memberCount }} {{ t("Member") }}</span>
<span class="group-description">{{ group.description }}</span>
</div>
</div>
<div class="group-info text-gray-500">
<span class="group-member-count">{{ group.memberCount }} Members</span>
<span class="group-description">{{ group.description }}</span>
</div>
</div>
</TabPanel>
<TabPanel
:class="{ 'active-tab': activeTab === 'My groups' }"
header="My groups"
header-class="tab-header"
</div>
</div>
</div>
<div v-show="activeTab === 'My groups'">
<div class="group-list">
<div
v-for="group in myGroups"
:key="group['@id']"
class="group-item flex items-center p-4 bg-white shadow-md rounded-md mb-4"
>
<div class="group-list">
<div
v-for="group in myGroups"
:key="group['@id']"
class="group-item"
<img
v-if="group.pictureUrl"
:src="group.pictureUrl"
alt="Group Image"
class="w-16 h-16 rounded-full mr-4"
/>
<i
v-else
class="mdi mdi-account-group-outline text-4xl text-gray-500 mr-4"
></i>
<div class="group-details">
<a
:href="`/resources/usergroups/show/${extractGroupId(group)}`"
class="text-lg font-semibold text-blue-600 hover:underline"
>{{ group.title }}</a
>
<img
v-if="group.pictureUrl"
:src="group.pictureUrl"
alt="Group Image"
class="group-image"
/>
<i
v-else
class="mdi mdi-account-group-outline group-icon"
></i>
<div class="group-details">
<a
:href="`/resources/usergroups/show/${extractGroupId(group)}`"
class="group-title"
>{{ group.title }}</a
>
<div class="group-info">
<span class="group-member-count">{{ group.memberCount }} {{ t("Member") }}</span>
<span class="group-description">{{ group.description }}</span>
</div>
</div>
<div class="group-info text-gray-500">
<span class="group-member-count">{{ group.memberCount }} Members</span>
<span class="group-description">{{ group.description }}</span>
</div>
</div>
</TabPanel>
</TabView>
</div>
</div>
</div>
<div v-show="activeTab === 'Search Groups'">
<GroupSearch />
</div>
</div>
@ -188,32 +201,33 @@
</template>
<script setup>
import Button from "primevue/button"
import TabView from "primevue/tabview"
import TabPanel from "primevue/tabpanel"
import { onMounted, ref } from "vue"
import useVuelidate from "@vuelidate/core"
import { required } from "@vuelidate/validators"
import BaseInputTextWithVuelidate from "../../components/basecomponents/BaseInputTextWithVuelidate.vue"
import BaseFileUpload from "../../components/basecomponents/BaseFileUpload.vue"
import BaseCheckbox from "../../components/basecomponents/BaseCheckbox.vue"
import { useI18n } from "vue-i18n"
import usergroupService from "../../services/usergroupService"
import Button from "primevue/button";
import TabView from "primevue/tabview";
import TabPanel from "primevue/tabpanel";
import { onMounted, ref } from "vue";
import useVuelidate from "@vuelidate/core";
import { required } from "@vuelidate/validators";
import BaseInputTextWithVuelidate from "../../components/basecomponents/BaseInputTextWithVuelidate.vue";
import BaseFileUpload from "../../components/basecomponents/BaseFileUpload.vue";
import BaseCheckbox from "../../components/basecomponents/BaseCheckbox.vue";
import { useI18n } from "vue-i18n";
import usergroupService from "../../services/usergroupService";
import GroupSearch from "../../components/usergroup/GroupSearch.vue"
const { t } = useI18n()
const newestGroups = ref([])
const popularGroups = ref([])
const myGroups = ref([])
const activeTab = ref("Newest")
const showCreateGroupDialog = ref(false)
const selectedFile = ref(null)
const { t } = useI18n();
const newestGroups = ref([]);
const popularGroups = ref([]);
const myGroups = ref([]);
const activeTab = ref("Newest");
const showCreateGroupDialog = ref(false);
const selectedFile = ref(null);
const groupForm = ref({
name: "",
description: "",
url: "",
picture: null,
})
});
const v$ = useVuelidate(
{
groupForm: {
@ -222,14 +236,14 @@ const v$ = useVuelidate(
url: {},
},
},
{ groupForm },
)
{ groupForm }
);
const permissionsOptions = [
{ label: "Open", value: "1" },
{ label: "Closed", value: "2" },
]
];
const createGroup = async () => {
v$.value.$touch()
v$.value.$touch();
if (!v$.value.$invalid) {
const groupData = {
title: groupForm.value.name,
@ -238,45 +252,45 @@ const createGroup = async () => {
visibility: groupForm.value.permissions.value,
allowMembersToLeaveGroup: groupForm.value.allowLeave ? 1 : 0,
groupType: 1,
}
};
try {
const newGroup = await usergroupService.createGroup(groupData)
const newGroup = await usergroupService.createGroup(groupData);
if (selectedFile.value && newGroup && newGroup.id) {
await usergroupService.uploadPicture(newGroup.id, {
picture: selectedFile.value,
})
});
}
showCreateGroupDialog.value = false
resetForm()
updateGroupsList()
showCreateGroupDialog.value = false;
resetForm();
updateGroupsList();
} catch (error) {
console.error("Failed to create group or upload picture:", error.response.data)
console.error("Failed to create group or upload picture:", error.response.data);
}
}
}
};
const updateGroupsList = () => {
usergroupService.listNewest().then((newest) => (newestGroups.value = newest))
usergroupService.listPopular().then((popular) => (popularGroups.value = popular))
usergroupService.listMine().then((mine) => (myGroups.value = mine))
}
usergroupService.listNewest().then((newest) => (newestGroups.value = newest));
usergroupService.listPopular().then((popular) => (popularGroups.value = popular));
usergroupService.listMine().then((mine) => (myGroups.value = mine));
};
const extractGroupId = (group) => {
const match = group["@id"].match(/\/api\/usergroup\/(\d+)/)
return match ? match[1] : null
}
const match = group["@id"].match(/\/api\/usergroup\/(\d+)/);
return match ? match[1] : null;
};
const redirectToGroupDetails = (groupId) => {
router.push({ name: "UserGroupShow", params: { group_id: groupId } })
}
router.push({ name: "UserGroupShow", params: { group_id: groupId } });
};
onMounted(async () => {
updateGroupsList()
})
updateGroupsList();
});
const closeDialog = () => {
showCreateGroupDialog.value = false
}
showCreateGroupDialog.value = false;
};
const resetForm = () => {
groupForm.value = {
name: "",
@ -285,8 +299,8 @@ const resetForm = () => {
picture: null,
permissions: "",
allowLeave: false,
}
selectedFile.value = null
v$.value.$reset()
}
};
selectedFile.value = null;
v$.value.$reset();
};
</script>

@ -19,8 +19,8 @@
/>
</BaseToolbar>
<div class="flex flex-col lg:flex-row gap-4">
<div class="basis-auto lg:basis-3/4">
<div class="flex flex-col gap-4">
<div class="w-full">
<div
v-if="loadingFriends"
class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3"
@ -38,72 +38,42 @@
layout="grid"
>
<template #grid="slotProps">
<div
class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3"
>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
<div
v-for="(item, index) in slotProps.items"
:key="index"
class="friend-list__block"
>
<div
v-if="item.user['@id'] === user['@id']"
class="friend-info"
>
<div v-if="item.user['@id'] === user['@id']" class="friend-info">
<img
:alt="item.friend.username"
:src="item.friend.illustrationUrl"
class="friend-info__avatar"
/>
<div
class="friend-info__username"
v-text="item.friend.username"
/>
<div class="friend-info__username" v-text="item.friend.username" />
</div>
<div
v-else
class="friend-info"
>
<div v-else class="friend-info">
<img
:alt="item.user.username"
:src="item.user.illustrationUrl"
class="friend-info__avatar"
/>
<div
class="friend-info__username"
v-text="item.user.username"
/>
<div class="friend-info__username" v-text="item.user.username" />
</div>
<div class="friend-options" v-if="isCurrentUser">
<span
class="friend-options__time"
v-text="relativeDatetime(item.createdAt)"
/>
<BaseButton
icon="user-delete"
only-icon
type="danger"
@click="onClickDeleteFriend(item)"
/>
<span class="friend-options__time" v-text="relativeDatetime(item.createdAt)" />
<BaseButton icon="user-delete" only-icon type="danger" @click="onClickDeleteFriend(item)" />
</div>
</div>
</div>
</template>
</DataView>
</div>
<div v-if="isCurrentUser" class="basis-auto lg:basis-1/4">
<UserRelUserRequestsList
ref="requestList"
@accept-friend="reloadHandler"
/>
</div>
</div>
</template>
<script setup>
import { useStore } from "vuex"
import { inject, onMounted, ref } from "vue"
import { inject, ref, onMounted } from "vue"
import BaseToolbar from "../../components/basecomponents/BaseToolbar.vue"
import BaseButton from "../../components/basecomponents/BaseButton.vue"
import Skeleton from "primevue/skeleton"
@ -114,56 +84,40 @@ import { useConfirm } from "primevue/useconfirm"
import userRelUserService from "../../services/userreluser"
import { useFormatDate } from "../../composables/formatDate"
import { useNotification } from "../../composables/notification"
import UserRelUserRequestsList from "../../components/userreluser/UserRelUserRequestsList.vue"
const store = useStore()
const router = useRouter()
const { t } = useI18n()
const user = inject('social-user')
const isCurrentUser = inject('is-current-user')
const items = ref([])
const loadingFriends = ref(true)
const notification = useNotification()
const { relativeDatetime } = useFormatDate()
const loadingFriends = ref(true)
const friendFilter = {
user: user.id,
relationType: 3, // friend status
}
const friendBackFilter = {
friend: user.id,
relationType: 3, // friend status
}
const router = useRouter()
const confirm = useConfirm()
const requestList = ref()
function reloadHandler() {
loadingFriends.value = true
items.value = []
Promise.all([
userRelUserService.findAll({
params: friendFilter,
}),
userRelUserService.findAll({
params: friendBackFilter,
}),
userRelUserService.findAll({ params: { user: user.id, relationType: 3 } }),
userRelUserService.findAll({ params: { friend: user.id, relationType: 3 } }),
])
.then(([friendshipResponse, friendshipBackResponse]) =>
Promise.all([friendshipResponse.json(), friendshipBackResponse.json()]),
)
.then(([friendshipJson, friendshipBackJson]) =>
items.value.push(...friendshipJson["hydra:member"], ...friendshipBackJson["hydra:member"]),
Promise.all([friendshipResponse.json(), friendshipBackResponse.json()])
)
.then(([friendshipJson, friendshipBackJson]) => {
items.value.push(...friendshipJson["hydra:member"], ...friendshipBackJson["hydra:member"])
})
.catch((e) => notification.showErrorNotification(e))
.finally(() => (loadingFriends.value = false))
requestList.value.loadRequests()
.finally(() => {
loadingFriends.value = false
if (requestList.value) {
requestList.value.loadRequests()
}
})
}
onMounted(() => {
@ -174,8 +128,6 @@ const goToAdd = () => {
router.push({ name: "UserRelUserAdd" })
}
const confirm = useConfirm()
function onClickDeleteFriend(friendship) {
confirm.require({
icon: "mdi mdi-alert-outline",

Loading…
Cancel
Save