Max 33274e5b9a
feat(webapp): user teaser popover (#8450)
* calculate distance between current user and queried user

* fix query for unset location

* use database to calculate distance

* rename distance to distance to me, 100% calculation done in DB

* distanceToMe tests

* lint fixes

* remove comments

* Show user teaser popover with badges, Desktop

* Refactor UserTeaser and add mobile popover support

* Avoid click propagation (WIP)

* Prevent event propagation

* Adjust alignment and font sizes

* More spacing for statistics

* Add distance, simplify user link

* Refactor location info into own component

* Add tests for UserTeaserPopup

* Refactor and test LocationInfo

* Query distanceToMe, rename distance to distanceToMe

* Update test

* Improve tests for UserTeaser, WIP

* Fix tests

* DistanceToMe on User instead of Location

* Revert "DistanceToMe on User instead of Location"

This reverts commit 96c9db00a44cd120e47bfe9534d3e066a194744c.

* Fix notifications

* Refactor UserTeaser and fix location info

* Fix group member crash

* Show 0 distance

* Fit in popover on small screens

* Allow access to profile on desktop

* Revert backend changes

* Load user teaser popover data only when needed

* Fix type mismatch

* Refactor for clarity and accessibility

* Litte refactorings and improvements

* Fix popover test

* Adapt and fix tests

* Fix tests and bugs

* Add placeholder

* cypress: adapt user teaser locator to changes

* Remove delays and scrolling

* Disable popovers in notification list and fix layout

* Remove flickering

* Make overlay catch all pointer events on touch devices

* Re-add attribute for E2E test

* Fix test, return to mouseover

* fix snapshot

---------

Co-authored-by: Ulf Gebhardt <ulf.gebhardt@webcraft-media.de>
Co-authored-by: Wolfgang Huß <wolle.huss@pjannto.com>
Co-authored-by: mahula <lenzmath@posteo.de>
2025-05-05 23:54:13 +00:00

177 lines
4.4 KiB
Vue

<template>
<nuxt-link
v-if="!unreadNotificationsCount"
class="notifications-menu"
:to="{ name: 'notifications' }"
>
<base-button
icon="bell"
ghost
circle
v-tooltip="{
content: $t('header.notifications.tooltip'),
placement: 'bottom-start',
}"
/>
</nuxt-link>
<dropdown v-else class="notifications-menu" offset="8" :placement="placement">
<template #default="{ toggleMenu }">
<base-button
ghost
circle
v-tooltip="{
content: $t('header.notifications.tooltip'),
placement: 'bottom-start',
}"
@click="toggleMenu"
>
<counter-icon icon="bell" :count="unreadNotificationsCount" danger />
</base-button>
</template>
<template #popover="{ closeMenu }">
<div class="notifications-menu-popover">
<notification-list :notifications="notifications" @markAsRead="markAsRead" />
</div>
<ds-flex class="notifications-link-container">
<ds-flex-item class="notifications-link-container-item" :width="{ base: '100%' }" centered>
<nuxt-link :to="{ name: 'notifications' }">
<base-button ghost primary>
{{ $t('notifications.pageLink') }}
</base-button>
</nuxt-link>
</ds-flex-item>
<ds-flex-item class="notifications-link-container-item" :width="{ base: '100%' }" centered>
<base-button
ghost
primary
@click="markAllAsRead(closeMenu)"
data-test="markAllAsRead-button"
>
{{ $t('notifications.markAllAsRead') }}
</base-button>
</ds-flex-item>
</ds-flex>
</template>
</dropdown>
</template>
<script>
import { mapGetters } from 'vuex'
import unionBy from 'lodash/unionBy'
import {
notificationQuery,
markAsReadMutation,
notificationAdded,
markAllAsReadMutation,
} from '~/graphql/User'
import CounterIcon from '~/components/_new/generic/CounterIcon/CounterIcon'
import Dropdown from '~/components/Dropdown'
import NotificationList from '../NotificationList/NotificationList'
export default {
name: 'NotificationMenu',
components: {
CounterIcon,
Dropdown,
NotificationList,
},
data() {
return {
notifications: [],
}
},
props: {
placement: { type: String },
},
methods: {
async markAsRead(notificationSourceId) {
const variables = { id: notificationSourceId }
try {
await this.$apollo.mutate({
mutation: markAsReadMutation(this.$i18n),
variables,
})
} catch (error) {
this.$toast.error(error.message)
}
},
async markAllAsRead(closeMenu) {
if (!this.hasNotifications) {
return
}
closeMenu()
try {
await this.$apollo.mutate({
mutation: markAllAsReadMutation(this.$i18n),
})
} catch (error) {
this.$toast.error(error.message)
}
},
},
computed: {
...mapGetters({
user: 'auth/user',
}),
unreadNotificationsCount() {
const result = this.notifications.reduce((count, notification) => {
return notification.read ? count : count + 1
}, 0)
return result
},
hasNotifications() {
return this.notifications.length
},
},
apollo: {
notifications: {
query() {
return notificationQuery()
},
variables() {
return {
read: false,
orderBy: 'updatedAt_desc',
}
},
subscribeToMore: {
document: notificationAdded(),
updateQuery: (previousResult, { subscriptionData }) => {
const {
data: { notificationAdded: newNotification },
} = subscriptionData
return {
notifications: unionBy(
[newNotification],
previousResult?.notifications,
(notification) => notification.id,
).sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)),
}
},
},
error(error) {
this.$toast.error(error.message)
},
},
},
}
</script>
<style lang="scss">
.notifications-menu-popover {
max-width: 500px;
}
.notifications-link-container {
background-color: $background-color-softer-active;
justify-content: center;
padding: $space-x-small;
flex-direction: row;
}
.notifications-link-container-item {
justify-content: center;
display: flex;
}
</style>