feat(webapp): add location distance in group profile (#8846)

* Add distance to group profile if location is defined

* Fix snapshot tests in 'webapp/pages/groups/_id/_slug.spec.js'

* Fix prop Vue warning in test 'webapp/pages/groups/_id/_slug.spec.js'

* reuse locationFragement for groups

* use better order on locationFragement parameters

* moved LocationInfo Component to correct place as its used in Group & User related context

* use size prop

* reduce changeset

* update snapshots

* remove computed property & simplify component

* more tests & updated snapshots

---------

Co-authored-by: Ulf Gebhardt <ulf.gebhardt@webcraft-media.de>
This commit is contained in:
Wolfgang Huß 2025-08-26 10:34:30 +02:00 committed by GitHub
parent f61850980e
commit c1a05bc73b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 320 additions and 198 deletions

View File

@ -0,0 +1,45 @@
import { render } from '@testing-library/vue'
import LocationInfo from './LocationInfo.vue'
const localVue = global.localVue
describe('LocationInfo', () => {
const Wrapper = ({ withDistance }) => {
return render(LocationInfo, {
localVue,
propsData: {
locationData: {
name: 'Paris',
distanceToMe: withDistance ? 100 : null,
},
},
mocks: {
$t: jest.fn((t) => t),
},
})
}
describe('distance', () => {
it('renders with distance', () => {
const wrapper = Wrapper({ withDistance: true })
expect(wrapper.container).toMatchSnapshot()
})
it('renders without distance', () => {
const wrapper = Wrapper({ withDistance: false })
expect(wrapper.container).toMatchSnapshot()
})
})
describe('size', () => {
it('renders in base size', () => {
const wrapper = Wrapper({ size: 'base' })
expect(wrapper.container).toMatchSnapshot()
})
it('renders in small size', () => {
const wrapper = Wrapper({ size: 'small' })
expect(wrapper.container).toMatchSnapshot()
})
})
})

View File

@ -1,10 +1,12 @@
<template> <template>
<div class="location-info"> <div :class="`location-info size-${size}`">
<div class="location"> <div class="location">
<base-icon name="map-marker" /> <base-icon name="map-marker" />
{{ locationData.name }} {{ locationData.name }}
</div> </div>
<div v-if="distance" class="distance">{{ distance }}</div> <div v-if="locationData.distanceToMe !== null" class="distance">
{{ $t('location.distance', { distance: locationData.distanceToMe }) }}
</div>
</div> </div>
</template> </template>
@ -13,12 +15,12 @@ export default {
name: 'LocationInfo', name: 'LocationInfo',
props: { props: {
locationData: { type: Object, default: null }, locationData: { type: Object, default: null },
}, size: {
computed: { type: String,
distance() { default: 'base',
return this.locationData.distanceToMe === null validator: (value) => {
? null return value.match(/(small|base)/)
: this.$t('location.distance', { distance: this.locationData.distanceToMe }) },
}, },
}, },
} }
@ -36,9 +38,21 @@ export default {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
}
.distance { .size-base {
> .distance {
margin-top: 8px; margin-top: 8px;
} }
} }
.size-small {
font-size: 0.8rem;
color: #70677e;
margin-bottom: 12px;
> .distance {
margin-top: 2px;
}
}
</style> </style>

View File

@ -0,0 +1,99 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LocationInfo distance renders with distance 1`] = `
<div>
<div
class="location-info size-base"
>
<div
class="location"
>
<span
class="base-icon"
>
<!---->
</span>
Paris
</div>
<div
class="distance"
>
location.distance
</div>
</div>
</div>
`;
exports[`LocationInfo distance renders without distance 1`] = `
<div>
<div
class="location-info size-base"
>
<div
class="location"
>
<span
class="base-icon"
>
<!---->
</span>
Paris
</div>
<!---->
</div>
</div>
`;
exports[`LocationInfo size renders in base size 1`] = `
<div>
<div
class="location-info size-base"
>
<div
class="location"
>
<span
class="base-icon"
>
<!---->
</span>
Paris
</div>
<!---->
</div>
</div>
`;
exports[`LocationInfo size renders in small size 1`] = `
<div>
<div
class="location-info size-base"
>
<div
class="location"
>
<span
class="base-icon"
>
<!---->
</span>
Paris
</div>
<!---->
</div>
</div>
`;

View File

@ -1,31 +0,0 @@
import { render } from '@testing-library/vue'
import LocationInfo from './LocationInfo.vue'
const localVue = global.localVue
describe('LocationInfo', () => {
const Wrapper = ({ withDistance }) => {
return render(LocationInfo, {
localVue,
propsData: {
locationData: {
name: 'Paris',
distanceToMe: withDistance ? 100 : null,
},
},
mocks: {
$t: jest.fn((t) => t),
},
})
}
it('renders with distance', () => {
const wrapper = Wrapper({ withDistance: true })
expect(wrapper.container).toMatchSnapshot()
})
it('renders without distance', () => {
const wrapper = Wrapper({ withDistance: false })
expect(wrapper.container).toMatchSnapshot()
})
})

View File

@ -30,7 +30,7 @@
<script> <script>
import Badges from '~/components/Badges.vue' import Badges from '~/components/Badges.vue'
import LocationInfo from '~/components/UserTeaser/LocationInfo.vue' import LocationInfo from '~/components/LocationInfo/LocationInfo.vue'
import { isTouchDevice } from '~/components/utils/isTouchDevice' import { isTouchDevice } from '~/components/utils/isTouchDevice'
import { userTeaserQuery } from '~/graphql/User.js' import { userTeaserQuery } from '~/graphql/User.js'

View File

@ -1,51 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LocationInfo renders with distance 1`] = `
<div>
<div
class="location-info"
>
<div
class="location"
>
<span
class="base-icon"
>
<!---->
</span>
Paris
</div>
<div
class="distance"
>
location.distance
</div>
</div>
</div>
`;
exports[`LocationInfo renders without distance 1`] = `
<div>
<div
class="location-info"
>
<div
class="location"
>
<span
class="base-icon"
>
<!---->
</span>
Paris
</div>
<!---->
</div>
</div>
`;

View File

@ -16,8 +16,8 @@ export const userFragment = gql`
} }
` `
export const locationFragment = (lang) => gql` export const locationFragment = (type, lang) => gql`
fragment location on User { fragment location on ${type} {
locationName locationName
location { location {
id id
@ -57,7 +57,7 @@ export const userCountsFragment = gql`
export const userTeaserFragment = (lang) => gql` export const userTeaserFragment = (lang) => gql`
${badgesFragment} ${badgesFragment}
${locationFragment(lang)} ${locationFragment('User', lang)}
fragment userTeaser on User { fragment userTeaser on User {
followedByCount followedByCount

View File

@ -15,7 +15,7 @@ export default (i18n) => {
return gql` return gql`
${userFragment} ${userFragment}
${userCountsFragment} ${userCountsFragment}
${locationFragment(lang)} ${locationFragment('User', lang)}
${badgesFragment} ${badgesFragment}
${postFragment} ${postFragment}
${postCountsFragment} ${postCountsFragment}
@ -65,7 +65,7 @@ export const filterPosts = (i18n) => {
return gql` return gql`
${userFragment} ${userFragment}
${userCountsFragment} ${userCountsFragment}
${locationFragment(lang)} ${locationFragment('User', lang)}
${badgesFragment} ${badgesFragment}
${postFragment} ${postFragment}
${postCountsFragment} ${postCountsFragment}
@ -108,7 +108,7 @@ export const profilePagePosts = (i18n) => {
return gql` return gql`
${userFragment} ${userFragment}
${userCountsFragment} ${userCountsFragment}
${locationFragment(lang)} ${locationFragment('User', lang)}
${badgesFragment} ${badgesFragment}
${postFragment} ${postFragment}
${postCountsFragment} ${postCountsFragment}
@ -158,7 +158,7 @@ export const relatedContributions = (i18n) => {
return gql` return gql`
${userFragment} ${userFragment}
${userCountsFragment} ${userCountsFragment}
${locationFragment(lang)} ${locationFragment('User', lang)}
${badgesFragment} ${badgesFragment}
${postFragment} ${postFragment}
${postCountsFragment} ${postCountsFragment}

View File

@ -15,7 +15,7 @@ export const profileUserQuery = (i18n) => {
return gql` return gql`
${userFragment} ${userFragment}
${userCountsFragment} ${userCountsFragment}
${locationFragment(lang)} ${locationFragment('User', lang)}
${badgesFragment} ${badgesFragment}
query User($id: ID!, $followedByCount: Int!, $followingCount: Int!) { query User($id: ID!, $followedByCount: Int!, $followingCount: Int!) {
@ -112,7 +112,7 @@ export const mapUserQuery = (i18n) => {
const lang = i18n.locale().toUpperCase() const lang = i18n.locale().toUpperCase()
return gql` return gql`
${userFragment} ${userFragment}
${locationFragment(lang)} ${locationFragment('User', lang)}
${badgesFragment} ${badgesFragment}
query { query {

View File

@ -1,5 +1,5 @@
import gql from 'graphql-tag' import gql from 'graphql-tag'
// import { locationFragment } from './Fragments' import { locationFragment } from './Fragments'
// ------ mutations // ------ mutations
@ -160,9 +160,8 @@ export const removeUserFromGroupMutation = () => {
export const groupQuery = (i18n) => { export const groupQuery = (i18n) => {
const lang = i18n ? i18n.locale().toUpperCase() : 'EN' const lang = i18n ? i18n.locale().toUpperCase() : 'EN'
// ${locationFragment(lang)}
return gql` return gql`
${locationFragment('Group', lang)}
query ($isMember: Boolean, $id: ID, $slug: String, $first: Int, $offset: Int) { query ($isMember: Boolean, $id: ID, $slug: String, $first: Int, $offset: Int) {
Group(isMember: $isMember, id: $id, slug: $slug, first: $first, offset: $offset) { Group(isMember: $isMember, id: $id, slug: $slug, first: $first, offset: $offset) {
id id
@ -187,13 +186,7 @@ export const groupQuery = (i18n) => {
avatar { avatar {
url url
} }
locationName ...location
# ...location
location {
name: name${lang}
lng
lat
}
membersCount membersCount
myRole myRole
inviteCodes { inviteCodes {

View File

@ -199,19 +199,30 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a close
</p> </p>
<p <div
class="ds-text ds-text-size-small ds-text-soft ds-text-center" class="location-info size-small"
> >
<span <div
class="base-icon" class="location"
data-test="map-marker"
> >
<!----> <span
</span> class="base-icon"
>
Paris <!---->
</span>
</p>
Paris
</div>
<div
class="distance"
>
location.distance
</div>
</div>
<p <p
class="ds-text ds-text-size-small ds-text-soft ds-text-center" class="ds-text ds-text-size-small ds-text-soft ds-text-center"
@ -1072,19 +1083,30 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a close
</p> </p>
<p <div
class="ds-text ds-text-size-small ds-text-soft ds-text-center" class="location-info size-small"
> >
<span <div
class="base-icon" class="location"
data-test="map-marker"
> >
<!----> <span
</span> class="base-icon"
>
Paris <!---->
</span>
</p>
Paris
</div>
<div
class="distance"
>
location.distance
</div>
</div>
<p <p
class="ds-text ds-text-size-small ds-text-soft ds-text-center" class="ds-text ds-text-size-small ds-text-soft ds-text-center"
@ -1529,19 +1551,30 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a close
</p> </p>
<p <div
class="ds-text ds-text-size-small ds-text-soft ds-text-center" class="location-info size-small"
> >
<span <div
class="base-icon" class="location"
data-test="map-marker"
> >
<!----> <span
</span> class="base-icon"
>
Paris <!---->
</span>
</p>
Paris
</div>
<div
class="distance"
>
location.distance
</div>
</div>
<p <p
class="ds-text ds-text-size-small ds-text-soft ds-text-center" class="ds-text ds-text-size-small ds-text-soft ds-text-center"
@ -2075,19 +2108,30 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a close
</p> </p>
<p <div
class="ds-text ds-text-size-small ds-text-soft ds-text-center" class="location-info size-small"
> >
<span <div
class="base-icon" class="location"
data-test="map-marker"
> >
<!----> <span
</span> class="base-icon"
>
Paris <!---->
</span>
</p>
Paris
</div>
<div
class="distance"
>
location.distance
</div>
</div>
<p <p
class="ds-text ds-text-size-small ds-text-soft ds-text-center" class="ds-text ds-text-size-small ds-text-soft ds-text-center"
@ -6603,19 +6647,30 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a hidde
</p> </p>
<p <div
class="ds-text ds-text-size-small ds-text-soft ds-text-center" class="location-info size-small"
> >
<span <div
class="base-icon" class="location"
data-test="map-marker"
> >
<!----> <span
</span> class="base-icon"
>
Hamburg <!---->
</span>
</p>
Hamburg
</div>
<div
class="distance"
>
location.distance
</div>
</div>
<p <p
class="ds-text ds-text-size-small ds-text-soft ds-text-center" class="ds-text ds-text-size-small ds-text-soft ds-text-center"
@ -7582,19 +7637,30 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a hidde
</p> </p>
<p <div
class="ds-text ds-text-size-small ds-text-soft ds-text-center" class="location-info size-small"
> >
<span <div
class="base-icon" class="location"
data-test="map-marker"
> >
<!----> <span
</span> class="base-icon"
>
Hamburg <!---->
</span>
</p>
Hamburg
</div>
<div
class="distance"
>
location.distance
</div>
</div>
<p <p
class="ds-text ds-text-size-small ds-text-soft ds-text-center" class="ds-text ds-text-size-small ds-text-soft ds-text-center"

View File

@ -38,10 +38,7 @@
{{ `&${groupSlug}` }} {{ `&${groupSlug}` }}
</ds-text> </ds-text>
<!-- group location --> <!-- group location -->
<ds-text v-if="group && group.location" align="center" color="soft" size="small"> <location-info v-if="group.location" :location-data="group.location" size="small" />
<base-icon name="map-marker" data-test="map-marker" />
{{ group && group.location ? group.location.name : '' }}
</ds-text>
<!-- group created at --> <!-- group created at -->
<ds-text align="center" color="soft" size="small"> <ds-text align="center" color="soft" size="small">
{{ $t('group.foundation') }} {{ group.createdAt | date('MMMM yyyy') }} {{ $t('group.foundation') }} {{ group.createdAt | date('MMMM yyyy') }}
@ -176,7 +173,9 @@
? $t('group.membersListTitleNotAllowedSeeingGroupMembers') ? $t('group.membersListTitleNotAllowedSeeingGroupMembers')
: null : null
" "
:allProfilesCount="isAllowedSeeingGroupMembers ? group.membersCount : 0" :allProfilesCount="
isAllowedSeeingGroupMembers && group.membersCount ? group.membersCount : 0
"
:profiles="isAllowedSeeingGroupMembers ? groupMembers : []" :profiles="isAllowedSeeingGroupMembers ? groupMembers : []"
:loading="$apollo.loading" :loading="$apollo.loading"
@fetchAllProfiles="fetchAllMembers" @fetchAllProfiles="fetchAllMembers"
@ -280,6 +279,7 @@ import CountTo from '~/components/CountTo.vue'
import Empty from '~/components/Empty/Empty' import Empty from '~/components/Empty/Empty'
import GroupContentMenu from '~/components/ContentMenu/GroupContentMenu' import GroupContentMenu from '~/components/ContentMenu/GroupContentMenu'
import JoinLeaveButton from '~/components/Button/JoinLeaveButton' import JoinLeaveButton from '~/components/Button/JoinLeaveButton'
import LocationInfo from '~/components/LocationInfo/LocationInfo.vue'
import MasonryGrid from '~/components/MasonryGrid/MasonryGrid.vue' import MasonryGrid from '~/components/MasonryGrid/MasonryGrid.vue'
import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue' import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue'
import PostTeaser from '~/components/PostTeaser/PostTeaser.vue' import PostTeaser from '~/components/PostTeaser/PostTeaser.vue'
@ -308,6 +308,7 @@ export default {
Empty, Empty,
GroupContentMenu, GroupContentMenu,
JoinLeaveButton, JoinLeaveButton,
LocationInfo,
PostTeaser, PostTeaser,
ProfileAvatar, ProfileAvatar,
ProfileList, ProfileList,

View File

@ -34,11 +34,7 @@
<!-- <base-icon name="at" data-test="at" /> --> <!-- <base-icon name="at" data-test="at" /> -->
{{ `@${userSlug}` }} {{ `@${userSlug}` }}
</ds-text> </ds-text>
<location-info <location-info v-if="user.location" :location-data="user.location" size="small" />
v-if="user.location"
:location-data="user.location"
class="location-info"
/>
<ds-text align="center" color="soft" size="small"> <ds-text align="center" color="soft" size="small">
{{ $t('profile.memberSince') }} {{ user.createdAt | date('MMMM yyyy') }} {{ $t('profile.memberSince') }} {{ user.createdAt | date('MMMM yyyy') }}
</ds-text> </ds-text>
@ -211,7 +207,7 @@ import { muteUser, unmuteUser } from '~/graphql/settings/MutedUsers'
import { blockUser, unblockUser } from '~/graphql/settings/BlockedUsers' import { blockUser, unblockUser } from '~/graphql/settings/BlockedUsers'
import UpdateQuery from '~/components/utils/UpdateQuery' import UpdateQuery from '~/components/utils/UpdateQuery'
import SocialMedia from '~/components/SocialMedia/SocialMedia' import SocialMedia from '~/components/SocialMedia/SocialMedia'
import LocationInfo from '~/components/UserTeaser/LocationInfo.vue' import LocationInfo from '~/components/LocationInfo/LocationInfo.vue'
const tabToFilterMapping = ({ tab, id }) => { const tabToFilterMapping = ({ tab, id }) => {
return { return {
@ -493,14 +489,4 @@ export default {
margin-bottom: $space-x-small; margin-bottom: $space-x-small;
} }
} }
.location-info {
font-size: 0.8rem;
color: #70677e;
margin-bottom: 12px;
> .distance {
margin-top: 2px !important;
}
}
</style> </style>