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; background: none;
} }
.tab-header { .tab {
padding: 0.5rem 1rem;
cursor: pointer; cursor: pointer;
padding: 0.75rem 1rem; border-bottom: 2px solid transparent;
border-bottom: 3px solid transparent;
transition: border-color 0.3s; transition: border-color 0.3s;
} }
.tab-header:hover { .tab:hover {
border-bottom: 3px solid #64B5F6; border-bottom: 2px solid #d1d5db;
} }
.active-tab:hover { .tab-active {
border-bottom: 3px solid #1976D2; border-bottom: 2px solid #3b82f6;
color: #3b82f6;
font-weight: bold;
} }
} }
@ -1036,3 +1038,64 @@
.circle-gray { .circle-gray {
color: 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> <template>
<BaseCard plain class="my-groups-card bg-white mb-3"> <BaseCard plain class="my-groups-card bg-white mb-3">
<template #header> <template #header>
<div class="px-4 py-2 -mb-2 bg-gray-15"> <div class="px-4 py-3 bg-gray-200">
<h2 class="text-h5">{{ t('My friends') }}</h2> <h2 class="text-xl font-semibold">{{ t('My friends') }}</h2>
</div> </div>
</template> </template>
<hr class="-mt-2 mb-4 -mx-4"> <hr class="my-2">
<div> <div class="px-4">
<div v-if="isCurrentUser" class="input-group mb-3"> <div v-if="isCurrentUser" class="flex items-center mb-4">
<input <input
type="search" 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" placeholder="Search"
v-model="searchQuery" v-model="searchQuery"
@input="fetchFriends" @input="fetchFriends"
> >
<button <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" type="button"
@click="search" @click="search"
> >
<i class="mdi mdi-magnify"></i> <i class="mdi mdi-magnify text-gray-700"></i>
</button> </button>
</div> </div>
<ul class="list-group"> <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"> <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"> <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')" /> <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> <span>{{ friend.friend.firstname }} {{ friend.friend.lastname }} <small class="text-muted">({{ friend.friend.username }})</small></span>

@ -1,39 +1,39 @@
<template> <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> <template #header>
<div class="px-2 py-2 -mb-2 bg-gray-15"> <div class="px-4 py-3 bg-gray-200">
<h2 class="text-h5">{{ t('My communities') }}</h2> <h2 class="text-xl font-semibold">{{ t('My communities') }}</h2>
</div> </div>
</template> </template>
<hr class="-mt-2 mb-4 -mx-4"> <hr class="my-2">
<div class="px-2"> <div class="px-4">
<ul class="mb-3"> <ul class="mb-4">
<li <li
class="list-group-item" class="mb-2"
v-for="group in groups" v-for="group in groups"
:key="group.id" :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> <span v-else>{{ group.name }}</span>
</li> </li>
</ul> </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> <a :href="goToUrl" class="btn btn-primary">{{ t('See all communities') }}</a>
</div> </div>
<div v-else > <div v-else >
<div v-if="isCurrentUser" class="input-group mb-3"> <div v-if="isCurrentUser" class="flex items-center mb-4">
<input <input
type="search" 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" placeholder="Search"
v-model="searchQuery" v-model="searchQuery"
> >
<button <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" type="button"
@click="search" @click="search"
> >
<i class="mdi mdi-magnify"></i> <i class="mdi mdi-magnify text-gray-700"></i>
</button> </button>
</div> </div>
</div> </div>

@ -1,9 +1,52 @@
<template> <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"> <div class="md:basis-1/3 lg:basis-1/4 2xl:basis-1/6 flex flex-col">
<UserProfileCard v-if="!isLoading && !isGroup" /> <UserProfileCard v-if="!isLoading && !isGroup" />
<GroupInfoCard v-if="!isLoading && isGroup" /> <GroupInfoCard v-if="!isLoading && isGroup" />
<SocialGroupMenu 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>
<div class="md:basis-2/3 lg:basis-3/4 2xl:basis-5/6"> <div class="md:basis-2/3 lg:basis-3/4 2xl:basis-5/6">
<router-view></router-view> <router-view></router-view>
@ -12,17 +55,61 @@
</template> </template>
<script setup> <script setup>
import UserProfileCard from "../social/UserProfileCard.vue" import UserProfileCard from "../social/UserProfileCard.vue"
import { provide } from "vue" import { onMounted, provide, ref } from "vue"
import { useSocialInfo } from "../../composables/useSocialInfo" import { useSocialInfo } from "../../composables/useSocialInfo"
import SocialGroupMenu from "../social/SocialGroupMenu.vue" import SocialGroupMenu from "../social/SocialGroupMenu.vue"
import GroupInfoCard from "../social/GroupInfoCard.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 { 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("social-user", user)
provide("is-current-user", isCurrentUser) provide("is-current-user", isCurrentUser)
provide("group-info", groupInfo) provide("group-info", groupInfo)
provide("is-group", isGroup) provide("is-group", isGroup)
onMounted(async () => {
await loadUser()
if (user.value && user.value.id) {
await fetchInvitations(user.value.id)
}
})
</script> </script>

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

@ -1,18 +1,39 @@
<template> <template>
<div class="flex flex-col md:flex-row gap-4"> <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 /> <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>
<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> <router-view></router-view>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import UserProfileCard from "../social/UserProfileCard.vue" import UserProfileCard from "../social/UserProfileCard.vue"
import { onMounted, provide } from "vue" import { nextTick, onMounted, provide, ref } from "vue"
import { useSocialInfo } from "../../composables/useSocialInfo" 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() const { user, isCurrentUser, groupInfo, isGroup, loadUser } = useSocialInfo()
provide("social-user", user) provide("social-user", user)
@ -20,5 +41,48 @@ provide("is-current-user", isCurrentUser)
provide("group-info", groupInfo) provide("group-info", groupInfo)
provide("is-group", isGroup) 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> </script>

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

@ -132,8 +132,9 @@ export function useSidebarMenu() {
const styledSocialItems = socialItems.value.map((item) => { const styledSocialItems = socialItems.value.map((item) => {
const newItem = { const newItem = {
...item, ...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) { if (newItem.isLink && newItem.route) {
newItem.url = newItem.route newItem.url = newItem.route

@ -60,13 +60,10 @@ export function useSocialMenuItems() {
return isCurrentUser.value ? [ return isCurrentUser.value ? [
{ icon: 'mdi mdi-home', label: t("Home"), route: '/social' }, { 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-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-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-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-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-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-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 } { 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', name: 'UserRelUserAdd',
//path: ':id',
path: 'add', path: 'add',
component: () => import('../views/userreluser/UserRelUserAdd.vue') component: () => import('../views/userreluser/UserRelUserAdd.vue')
}, },

@ -1,14 +1,33 @@
<template> <template>
<div> <div id="social-wall-container">
<SocialWallPostForm v-if="!hidePostForm && isCurrentUser" @post-created="refreshPosts" class="mb-6" /> <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" /> <SocialWallPostList ref="postListRef" class="mb-6" />
</div> </div>
</template> </template>
<script setup> <script setup>
import { inject, ref } from "vue" import { inject, ref, watch } from "vue";
import SocialWallPostForm from "../../components/social/SocialWallPostForm.vue" import { useRoute, useRouter } from "vue-router";
import SocialWallPostList from "../../components/social/SocialWallPostList.vue" 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({ const props = defineProps({
hidePostForm: { hidePostForm: {
@ -18,11 +37,30 @@ const props = defineProps({
}); });
const postListRef = ref(null); 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() { function refreshPosts() {
if (postListRef.value) { if (postListRef.value) {
postListRef.value.refreshPosts(); postListRef.value.refreshPosts();
} }
} }
function filterMessages(type) {
router.push({ path: '/social', query: { filterType: type } });
}
</script> </script>
<style scoped>
</style>

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

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

Loading…
Cancel
Save