Implement group profile, first step

This commit is contained in:
Wolfgang Huß 2022-08-29 11:26:27 +02:00
parent 623af5f47b
commit 39487778bf
6 changed files with 325 additions and 219 deletions

95
webapp/graphql/groups.js Normal file
View File

@ -0,0 +1,95 @@
import gql from 'graphql-tag'
// ------ mutations
export const createGroupMutation = gql`
mutation(
$id: ID
$name: String!
$slug: String
$about: String
$description: String!
$groupType: GroupType!
$actionRadius: GroupActionRadius!
$categoryIds: [ID]
) {
CreateGroup(
id: $id
name: $name
slug: $slug
about: $about
description: $description
groupType: $groupType
actionRadius: $actionRadius
categoryIds: $categoryIds
) {
id
name
slug
createdAt
updatedAt
disabled
deleted
about
description
groupType
actionRadius
myRole
}
}
`
// ------ queries
export const groupQuery = gql`
query(
$isMember: Boolean
$id: ID
$name: String
$slug: String
$createdAt: String
$updatedAt: String
$about: String
$description: String
$locationName: String
$first: Int
$offset: Int
$orderBy: [_GroupOrdering]
$filter: _GroupFilter
) {
Group(
isMember: $isMember
id: $id
name: $name
slug: $slug
createdAt: $createdAt
updatedAt: $updatedAt
about: $about
description: $description
locationName: $locationName
first: $first
offset: $offset
orderBy: $orderBy
filter: $filter
) {
id
name
slug
createdAt
updatedAt
disabled
deleted
about
description
groupType
actionRadius
myRole
categories {
id
slug
name
icon
}
}
}
`

View File

@ -333,15 +333,16 @@
"placeholder": "Schreib etwas Inspirierendes …"
},
"error-pages": {
"403-default": "Kein Zugang zu dieser Seite",
"404-default": "Diese Seite konnte nicht gefunden werden",
"500-default": "Internal Server Error",
"503-default": "Dienst steht nicht zur Verfügung",
"back-to-index": "Zurück zur Startseite",
"cannot-edit-post": "Dieser Beitrag kann nicht editiert werden",
"default": "Ein Fehler ist aufgetreten",
"post-not-found": "Dieser Beitrag konnte nicht gefunden werden",
"profile-not-found": "Dieses Profil konnte nicht gefunden werden"
"403-default": "Kein Zugang zu dieser Seite!",
"404-default": "Diese Seite konnte nicht gefunden werden!",
"500-default": "Internal Server Error!",
"503-default": "Dienst steht nicht zur Verfügung!",
"back-to-index": "Zurück zur Startseite!",
"cannot-edit-post": "Dieser Beitrag kann nicht editiert werden!",
"default": "Ein Fehler ist aufgetreten!",
"group-not-found": "Dieses Gruppenprofil konnte nicht gefunden werden!",
"post-not-found": "Dieser Beitrag konnte nicht gefunden werden!",
"profile-not-found": "Dieses Profil konnte nicht gefunden werden!"
},
"filter-menu": {
"all": "Alle",

View File

@ -333,15 +333,16 @@
"placeholder": "Leave your inspirational thoughts …"
},
"error-pages": {
"403-default": "Not authorized to this page",
"404-default": "This page could not be found",
"500-default": "Internal Server Error",
"503-default": "Service Unavailable",
"back-to-index": "Back to index page",
"cannot-edit-post": "This post cannot be edited",
"default": "An error occurred",
"post-not-found": "This post could not be found",
"profile-not-found": "This profile could not be found"
"403-default": "Not authorized to this page!",
"404-default": "This page could not be found!",
"500-default": "Internal Server Error!",
"503-default": "Service Unavailable!",
"back-to-index": "Back to index page!",
"cannot-edit-post": "This post cannot be edited!",
"default": "An error occurred!",
"group-not-found": "This group profile could not be found!",
"post-not-found": "This post could not be found!",
"profile-not-found": "This profile could not be found!"
},
"filter-menu": {
"all": "All",

View File

@ -5,7 +5,7 @@ const localVue = global.localVue
config.stubs['nuxt-child'] = '<span class="nuxt-child"><slot /></span>'
describe('Profile _id.vue', () => {
describe('Group profile _id.vue', () => {
let wrapper
let Wrapper
let mocks

View File

@ -9,7 +9,7 @@ import PersistentLinks from '~/mixins/persistentLinks.js'
const options = {
queryId: gql`
query($idOrSlug: ID) {
User(id: $idOrSlug) {
Group(id: $idOrSlug) {
id
slug
}
@ -17,14 +17,14 @@ const options = {
`,
querySlug: gql`
query($idOrSlug: String) {
User(slug: $idOrSlug) {
Group(slug: $idOrSlug) {
id
slug
}
}
`,
message: 'error-pages.profile-not-found',
path: 'profile',
message: 'error-pages.group-not-found',
path: 'group',
}
const persistentLinks = PersistentLinks(options)

View File

@ -1,23 +1,24 @@
<template>
<div>
<ds-space />
<ds-flex v-if="user" :width="{ base: '100%' }" gutter="base">
<ds-flex v-if="group" :width="{ base: '100%' }" gutter="base">
<ds-flex-item :width="{ base: '100%', sm: 2, md: 2, lg: 1 }">
<base-card
:class="{ 'disabled-content': user.disabled }"
:class="{ 'disabled-content': group.disabled }"
style="position: relative; height: auto; overflow: visible"
>
<hc-upload v-if="myProfile" :user="user">
<user-avatar :user="user" class="profile-avatar" size="large"></user-avatar>
<!-- Wolle: <hc-upload v-if="isMyGroup" :user="user"> -->
<hc-upload v-if="isMyGroup" :user="group">
<!-- Wolle: <user-avatar :user="user" class="profile-avatar" size="large"></user-avatar> -->
</hc-upload>
<user-avatar v-else :user="user" class="profile-avatar" size="large" />
<!-- Wolle: <user-avatar v-else :user="user" class="profile-avatar" size="large" /> -->
<!-- Menu -->
<client-only>
<!-- Wolle: <client-only>
<content-menu
placement="bottom-end"
resource-type="user"
:resource="user"
:is-owner="myProfile"
:is-owner="isMyGroup"
class="user-content-menu"
@mute="muteUser"
@unmute="unmuteUser"
@ -25,28 +26,28 @@
@unblock="unblockUser"
@delete="deleteUser"
/>
</client-only>
</client-only> -->
<ds-space margin="small">
<ds-heading tag="h3" align="center" no-margin>
{{ userName }}
{{ groupName }}
</ds-heading>
<ds-text align="center" color="soft">
{{ userSlug }}
{{ groupSlug }}
</ds-text>
<ds-text v-if="user.location" align="center" color="soft" size="small">
<!-- Wolle: <ds-text v-if="user.location" align="center" color="soft" size="small">
<base-icon name="map-marker" />
{{ user.location.name }}
</ds-text>
</ds-text> -->
<ds-text align="center" color="soft" size="small">
{{ $t('profile.memberSince') }} {{ user.createdAt | date('MMMM yyyy') }}
{{ $t('profile.memberSince') }} {{ group.createdAt | date('MMMM yyyy') }}
</ds-text>
</ds-space>
<ds-space v-if="user.badges && user.badges.length" margin="x-small">
<!-- Wolle: <ds-space v-if="user.badges && user.badges.length" margin="x-small">
<hc-badges :badges="user.badges" />
</ds-space>
</ds-space> -->
<ds-flex>
<ds-flex-item>
<client-only>
<!-- Wolle: <client-only>
<ds-number :label="$t('profile.followers')">
<hc-count-to
slot="count"
@ -54,17 +55,17 @@
:end-val="user.followedByCount"
/>
</ds-number>
</client-only>
</client-only> -->
</ds-flex-item>
<ds-flex-item>
<client-only>
<!-- Wolle: <client-only>
<ds-number :label="$t('profile.following')">
<hc-count-to slot="count" :end-val="user.followingCount" />
</ds-number>
</client-only>
</client-only> -->
</ds-flex-item>
</ds-flex>
<div v-if="!myProfile" class="action-buttons">
<!-- Wolle: <div v-if="!isMyGroup" class="action-buttons">
<base-button v-if="user.isBlocked" @click="unblockUser(user)">
{{ $t('settings.blocked-users.unblock') }}
</base-button>
@ -78,11 +79,11 @@
@optimistic="optimisticFollow"
@update="updateFollow"
/>
</div>
<template v-if="user.about">
</div> -->
<template v-if="group.about">
<hr />
<ds-space margin-top="small" margin-bottom="small">
<ds-text color="soft" size="small" class="hyphenate-text">{{ user.about }}</ds-text>
<ds-text color="soft" size="small" class="hyphenate-text">{{ group.about }}</ds-text>
</ds-space>
</template>
</base-card>
@ -90,7 +91,7 @@
<ds-heading tag="h3" soft style="text-align: center; margin-bottom: 10px">
{{ $t('profile.network.title') }}
</ds-heading>
<follow-list
<!-- Wolle: <follow-list
:loading="$apollo.loading"
:user="user"
type="followedBy"
@ -102,21 +103,21 @@
:user="user"
type="following"
@fetchAllConnections="fetchAllConnections"
/>
<social-media :user-name="userName" :user="user" />
/> -->
<!-- Wolle: <social-media :user-name="groupName" :user="user" /> -->
</ds-flex-item>
<ds-flex-item :width="{ base: '100%', sm: 3, md: 5, lg: 3 }">
<masonry-grid>
<!-- TapNavigation -->
<tab-navigation :tabs="tabOptions" :activeTab="tabActive" @switch-tab="handleTab" />
<!-- Wolle: <tab-navigation :tabs="tabOptions" :activeTab="tabActive" @switch-tab="handleTab" /> -->
<!-- feed -->
<ds-grid-item :row-span="2" column-span="fullWidth">
<ds-space centered>
<nuxt-link :to="{ name: 'post-create' }">
<base-button
v-if="myProfile"
v-if="isMyGroup"
v-tooltip="{
content: $t('contribution.newPost'),
placement: 'left',
@ -132,7 +133,7 @@
</ds-space>
</ds-grid-item>
<template v-if="posts.length">
<template v-if="posts && posts.length">
<masonry-grid-item
v-for="post in posts"
:key="post.id"
@ -160,9 +161,9 @@
</ds-grid-item>
</template>
</masonry-grid>
<client-only>
<!-- Wolle: <client-only>
<infinite-loading v-if="hasMore" @infinite="showMoreContributions" />
</client-only>
</client-only> -->
</ds-flex-item>
</ds-flex>
</div>
@ -184,19 +185,20 @@ import MasonryGrid from '~/components/MasonryGrid/MasonryGrid.vue'
import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue'
import TabNavigation from '~/components/_new/generic/TabNavigation/TabNavigation'
import { profilePagePosts } from '~/graphql/PostQuery'
import UserQuery from '~/graphql/User'
import { groupQuery } from '~/graphql/groups'
import { muteUser, unmuteUser } from '~/graphql/settings/MutedUsers'
import { blockUser, unblockUser } from '~/graphql/settings/BlockedUsers'
import UpdateQuery from '~/components/utils/UpdateQuery'
import SocialMedia from '~/components/SocialMedia/SocialMedia'
const tabToFilterMapping = ({ tab, id }) => {
return {
post: { author: { id } },
comment: { comments_some: { author: { id } } },
shout: { shoutedBy_some: { id } },
}[tab]
}
// Wolle:
// const tabToFilterMapping = ({ tab, id }) => {
// return {
// post: { author: { id } },
// comment: { comments_some: { author: { id } } },
// shout: { shoutedBy_some: { id } },
// }[tab]
// }
export default {
components: {
@ -220,194 +222,201 @@ export default {
mode: 'out-in',
},
data() {
const filter = tabToFilterMapping({ tab: 'post', id: this.$route.params.id })
// Wolle:
// const filter = tabToFilterMapping({ tab: 'post', id: this.$route.params.id })
return {
User: [],
Group: [],
posts: [],
hasMore: true,
offset: 0,
pageSize: 6,
tabActive: 'post',
filter,
// Wolle: filter,
followedByCountStartValue: 0,
followedByCount: 7,
followingCount: 7,
}
},
computed: {
myProfile() {
return this.$route.params.id === this.$store.getters['auth/user'].id
isMyGroup() {
return this.group.myRole
},
user() {
return this.User ? this.User[0] : {}
group() {
return this.Group ? this.Group[0] : {}
},
userName() {
const { name } = this.user || {}
groupName() {
const { name } = this.group || {}
return name || this.$t('profile.userAnonym')
},
userSlug() {
const { slug } = this.user || {}
groupSlug() {
const { slug } = this.group || {}
return slug && `@${slug}`
},
tabOptions() {
return [
{
type: 'post',
title: this.$t('common.post', null, this.user.contributionsCount),
count: this.user.contributionsCount,
disabled: this.user.contributionsCount === 0,
},
{
type: 'comment',
title: this.$t('profile.commented'),
count: this.user.commentedCount,
disabled: this.user.commentedCount === 0,
},
{
type: 'shout',
title: this.$t('profile.shouted'),
count: this.user.shoutedCount,
disabled: this.user.shoutedCount === 0,
},
]
},
// tabOptions() {
// return [
// {
// type: 'post',
// title: this.$t('common.post', null, this.user.contributionsCount),
// count: this.user.contributionsCount,
// disabled: this.user.contributionsCount === 0,
// },
// {
// type: 'comment',
// title: this.$t('profile.commented'),
// count: this.user.commentedCount,
// disabled: this.user.commentedCount === 0,
// },
// {
// type: 'shout',
// title: this.$t('profile.shouted'),
// count: this.user.shoutedCount,
// disabled: this.user.shoutedCount === 0,
// },
// ]
// },
},
methods: {
handleTab(tab) {
if (this.tabActive !== tab) {
this.tabActive = tab
this.filter = tabToFilterMapping({ tab, id: this.$route.params.id })
this.resetPostList()
}
},
// Wolle: handleTab(tab) {
// if (this.tabActive !== tab) {
// this.tabActive = tab
// this.filter = tabToFilterMapping({ tab, id: this.$route.params.id })
// this.resetPostList()
// }
// },
uniq(items, field = 'id') {
return uniqBy(items, field)
},
showMoreContributions($state) {
const { profilePagePosts: PostQuery } = this.$apollo.queries
if (!PostQuery) return // seems this can be undefined on subpages
this.offset += this.pageSize
// Wolle:
// showMoreContributions($state) {
// const { profilePagePosts: PostQuery } = this.$apollo.queries
// if (!PostQuery) return // seems this can be undefined on subpages
// this.offset += this.pageSize
PostQuery.fetchMore({
variables: {
offset: this.offset,
filter: this.filter,
first: this.pageSize,
orderBy: 'createdAt_desc',
},
updateQuery: UpdateQuery(this, { $state, pageKey: 'profilePagePosts' }),
})
},
resetPostList() {
this.offset = 0
this.posts = []
this.hasMore = true
},
refetchPostList() {
this.resetPostList()
this.$apollo.queries.profilePagePosts.refetch()
},
async muteUser(user) {
try {
await this.$apollo.mutate({ mutation: muteUser(), variables: { id: user.id } })
} catch (error) {
this.$toast.error(error.message)
} finally {
this.$apollo.queries.User.refetch()
this.resetPostList()
this.$apollo.queries.profilePagePosts.refetch()
}
},
async unmuteUser(user) {
try {
this.$apollo.mutate({ mutation: unmuteUser(), variables: { id: user.id } })
} catch (error) {
this.$toast.error(error.message)
} finally {
this.$apollo.queries.User.refetch()
this.resetPostList()
this.$apollo.queries.profilePagePosts.refetch()
}
},
async blockUser(user) {
try {
await this.$apollo.mutate({ mutation: blockUser(), variables: { id: user.id } })
} catch (error) {
this.$toast.error(error.message)
} finally {
this.$apollo.queries.User.refetch()
}
},
async unblockUser(user) {
try {
this.$apollo.mutate({ mutation: unblockUser(), variables: { id: user.id } })
} catch (error) {
this.$toast.error(error.message)
} finally {
this.$apollo.queries.User.refetch()
}
},
async deleteUser(userdata) {
this.$store.commit('modal/SET_OPEN', {
name: 'delete',
data: {
userdata: userdata,
},
})
},
optimisticFollow({ followedByCurrentUser }) {
/*
* Note: followedByCountStartValue is updated to avoid counting from 0 when follow/unfollow
*/
this.followedByCountStartValue = this.user.followedByCount
const currentUser = this.$store.getters['auth/user']
if (followedByCurrentUser) {
this.user.followedByCount++
this.user.followedBy = [currentUser, ...this.user.followedBy]
} else {
this.user.followedByCount--
this.user.followedBy = this.user.followedBy.filter((user) => user.id !== currentUser.id)
}
this.user.followedByCurrentUser = followedByCurrentUser
},
updateFollow({ followedByCurrentUser, followedBy, followedByCount }) {
this.followedByCountStartValue = this.user.followedByCount
this.user.followedByCount = followedByCount
this.user.followedByCurrentUser = followedByCurrentUser
this.user.followedBy = followedBy
},
fetchAllConnections(type) {
if (type === 'following') this.followingCount = Infinity
if (type === 'followedBy') this.followedByCount = Infinity
},
// PostQuery.fetchMore({
// variables: {
// offset: this.offset,
// filter: this.filter,
// first: this.pageSize,
// orderBy: 'createdAt_desc',
// },
// updateQuery: UpdateQuery(this, { $state, pageKey: 'profilePagePosts' }),
// })
// },
// resetPostList() {
// this.offset = 0
// this.posts = []
// this.hasMore = true
// },
// refetchPostList() {
// this.resetPostList()
// this.$apollo.queries.profilePagePosts.refetch()
// },
// async muteUser(user) {
// try {
// await this.$apollo.mutate({ mutation: muteUser(), variables: { id: user.id } })
// } catch (error) {
// this.$toast.error(error.message)
// } finally {
// this.$apollo.queries.User.refetch()
// this.resetPostList()
// this.$apollo.queries.profilePagePosts.refetch()
// }
// },
// async unmuteUser(user) {
// try {
// this.$apollo.mutate({ mutation: unmuteUser(), variables: { id: user.id } })
// } catch (error) {
// this.$toast.error(error.message)
// } finally {
// this.$apollo.queries.User.refetch()
// this.resetPostList()
// this.$apollo.queries.profilePagePosts.refetch()
// }
// },
// async blockUser(user) {
// try {
// await this.$apollo.mutate({ mutation: blockUser(), variables: { id: user.id } })
// } catch (error) {
// this.$toast.error(error.message)
// } finally {
// this.$apollo.queries.User.refetch()
// }
// },
// async unblockUser(user) {
// try {
// this.$apollo.mutate({ mutation: unblockUser(), variables: { id: user.id } })
// } catch (error) {
// this.$toast.error(error.message)
// } finally {
// this.$apollo.queries.User.refetch()
// }
// },
// async deleteUser(userdata) {
// this.$store.commit('modal/SET_OPEN', {
// name: 'delete',
// data: {
// userdata: userdata,
// },
// })
// },
// Wolle:
// optimisticFollow({ followedByCurrentUser }) {
// /*
// * Note: followedByCountStartValue is updated to avoid counting from 0 when follow/unfollow
// */
// this.followedByCountStartValue = this.user.followedByCount
// const currentUser = this.$store.getters['auth/user']
// if (followedByCurrentUser) {
// this.user.followedByCount++
// this.user.followedBy = [currentUser, ...this.user.followedBy]
// } else {
// this.user.followedByCount--
// this.user.followedBy = this.user.followedBy.filter((user) => user.id !== currentUser.id)
// }
// this.user.followedByCurrentUser = followedByCurrentUser
// },
// Wolle:
// updateFollow({ followedByCurrentUser, followedBy, followedByCount }) {
// this.followedByCountStartValue = this.user.followedByCount
// this.user.followedByCount = followedByCount
// this.user.followedByCurrentUser = followedByCurrentUser
// this.user.followedBy = followedBy
// },
// Wolle:
// fetchAllConnections(type) {
// if (type === 'following') this.followingCount = Infinity
// if (type === 'followedBy') this.followedByCount = Infinity
// },
},
apollo: {
profilePagePosts: {
// Wolle:
// profilePagePosts: {
// query() {
// return profilePagePosts(this.$i18n)
// },
// variables() {
// return {
// filter: this.filter,
// first: this.pageSize,
// offset: 0,
// orderBy: 'createdAt_desc',
// }
// },
// update({ profilePagePosts }) {
// this.posts = profilePagePosts
// },
// fetchPolicy: 'cache-and-network',
// },
Group: {
query() {
return profilePagePosts(this.$i18n)
},
variables() {
return {
filter: this.filter,
first: this.pageSize,
offset: 0,
orderBy: 'createdAt_desc',
}
},
update({ profilePagePosts }) {
this.posts = profilePagePosts
},
fetchPolicy: 'cache-and-network',
},
User: {
query() {
return UserQuery(this.$i18n)
// Wolle: return groupQuery(this.$i18n) // language will be needed for lacations
return groupQuery
},
variables() {
return {
id: this.$route.params.id,
followedByCount: this.followedByCount,
followingCount: this.followingCount,
// followedByCount: this.followedByCount,
// followingCount: this.followingCount,
}
},
fetchPolicy: 'cache-and-network',