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>
<div class="location-info">
<div :class="`location-info size-${size}`">
<div class="location">
<base-icon name="map-marker" />
{{ locationData.name }}
</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>
</template>
@ -13,12 +15,12 @@ export default {
name: 'LocationInfo',
props: {
locationData: { type: Object, default: null },
},
computed: {
distance() {
return this.locationData.distanceToMe === null
? null
: this.$t('location.distance', { distance: this.locationData.distanceToMe })
size: {
type: String,
default: 'base',
validator: (value) => {
return value.match(/(small|base)/)
},
},
},
}
@ -36,9 +38,21 @@ export default {
align-items: center;
justify-content: center;
}
}
.distance {
.size-base {
> .distance {
margin-top: 8px;
}
}
.size-small {
font-size: 0.8rem;
color: #70677e;
margin-bottom: 12px;
> .distance {
margin-top: 2px;
}
}
</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>
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 { 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`
fragment location on User {
export const locationFragment = (type, lang) => gql`
fragment location on ${type} {
locationName
location {
id
@ -57,7 +57,7 @@ export const userCountsFragment = gql`
export const userTeaserFragment = (lang) => gql`
${badgesFragment}
${locationFragment(lang)}
${locationFragment('User', lang)}
fragment userTeaser on User {
followedByCount

View File

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

View File

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

View File

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

View File

@ -199,19 +199,30 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a close
</p>
<p
class="ds-text ds-text-size-small ds-text-soft ds-text-center"
<div
class="location-info size-small"
>
<span
class="base-icon"
data-test="map-marker"
<div
class="location"
>
<!---->
</span>
Paris
</p>
<span
class="base-icon"
>
<!---->
</span>
Paris
</div>
<div
class="distance"
>
location.distance
</div>
</div>
<p
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
class="ds-text ds-text-size-small ds-text-soft ds-text-center"
<div
class="location-info size-small"
>
<span
class="base-icon"
data-test="map-marker"
<div
class="location"
>
<!---->
</span>
Paris
</p>
<span
class="base-icon"
>
<!---->
</span>
Paris
</div>
<div
class="distance"
>
location.distance
</div>
</div>
<p
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
class="ds-text ds-text-size-small ds-text-soft ds-text-center"
<div
class="location-info size-small"
>
<span
class="base-icon"
data-test="map-marker"
<div
class="location"
>
<!---->
</span>
Paris
</p>
<span
class="base-icon"
>
<!---->
</span>
Paris
</div>
<div
class="distance"
>
location.distance
</div>
</div>
<p
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
class="ds-text ds-text-size-small ds-text-soft ds-text-center"
<div
class="location-info size-small"
>
<span
class="base-icon"
data-test="map-marker"
<div
class="location"
>
<!---->
</span>
Paris
</p>
<span
class="base-icon"
>
<!---->
</span>
Paris
</div>
<div
class="distance"
>
location.distance
</div>
</div>
<p
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
class="ds-text ds-text-size-small ds-text-soft ds-text-center"
<div
class="location-info size-small"
>
<span
class="base-icon"
data-test="map-marker"
<div
class="location"
>
<!---->
</span>
Hamburg
</p>
<span
class="base-icon"
>
<!---->
</span>
Hamburg
</div>
<div
class="distance"
>
location.distance
</div>
</div>
<p
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
class="ds-text ds-text-size-small ds-text-soft ds-text-center"
<div
class="location-info size-small"
>
<span
class="base-icon"
data-test="map-marker"
<div
class="location"
>
<!---->
</span>
Hamburg
</p>
<span
class="base-icon"
>
<!---->
</span>
Hamburg
</div>
<div
class="distance"
>
location.distance
</div>
</div>
<p
class="ds-text ds-text-size-small ds-text-soft ds-text-center"

View File

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

View File

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