Skip to content

Commit e645f85

Browse files
committed
Update notifications, add user hover card
1 parent 1bfda73 commit e645f85

File tree

3 files changed

+415
-15
lines changed

3 files changed

+415
-15
lines changed

resources/js/components/NotificationItem.vue

Lines changed: 113 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,6 @@
2525
class="w-10 h-10 rounded-full object-cover ring-2 ring-white dark:ring-gray-900 transition-transform group-hover/avatar:scale-105"
2626
@error="handleImageError"
2727
/>
28-
<span
29-
class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 text-xs text-white bg-gray-900 dark:bg-gray-700 rounded opacity-0 pointer-events-none group-hover/avatar:opacity-100 transition-opacity whitespace-nowrap z-10"
30-
>
31-
@{{ notification.actor.username }}
32-
</span>
3328
<div
3429
class="absolute -bottom-1 -right-1 flex items-center justify-center w-5 h-5 rounded-full ring-2 ring-white dark:ring-gray-900"
3530
:class="getIconBackgroundClass()"
@@ -44,19 +39,16 @@
4439

4540
<div class="flex-1 min-w-0 pr-2">
4641
<p class="text-sm leading-snug text-gray-900 dark:text-gray-100">
47-
<router-link
48-
:to="`/@${notification.actor.username}`"
49-
@click.stop="markAsRead"
50-
class="hover:underline group/name relative"
42+
<span
43+
ref="nameRef"
44+
@mouseenter="handleNameHover"
45+
@mouseleave="handleNameLeave"
46+
class="hover:underline cursor-pointer"
5147
:class="!notification.read_at ? 'font-bold' : 'font-semibold'"
48+
@click.stop="navigateToProfile"
5249
>
5350
{{ notification.actor.name }}
54-
<span
55-
class="absolute bottom-full left-0 mb-2 px-2 py-1 text-xs text-white bg-gray-900 dark:bg-gray-700 rounded opacity-0 pointer-events-none group-hover/name:opacity-100 transition-opacity whitespace-nowrap z-10"
56-
>
57-
@{{ notification.actor.username }}
58-
</span>
59-
</router-link>
51+
</span>
6052
<span class="text-gray-600 dark:text-gray-400 font-normal">
6153
{{ ' ' + getNotificationMessage() }}
6254
</span>
@@ -89,10 +81,24 @@
8981

9082
<div v-else-if="!notification.read_at" class="w-2 h-2 bg-blue-500 rounded-full"></div>
9183
</div>
84+
85+
<Teleport to="body">
86+
<UserHoverCard
87+
v-if="!isMobile && showHoverCard"
88+
:show="showHoverCard"
89+
:account="notification.actor"
90+
:account-id="notification.actor.id"
91+
:position="hoverCardPosition"
92+
:current-user-id="currentUserId"
93+
@mouseenter="handleCardEnter"
94+
@mouseleave="handleCardLeave"
95+
/>
96+
</Teleport>
9297
</div>
9398
</template>
9499

95100
<script setup>
101+
import { ref, onMounted, onUnmounted } from 'vue'
96102
import { useRouter } from 'vue-router'
97103
import {
98104
HeartIcon,
@@ -104,11 +110,16 @@ import {
104110
} from '@heroicons/vue/24/solid'
105111
import { useHashids } from '@/composables/useHashids'
106112
import { useI18n } from 'vue-i18n'
113+
import UserHoverCard from '@/components/ProfileHoverCard.vue'
107114
108115
const props = defineProps({
109116
notification: {
110117
type: Object,
111118
required: true
119+
},
120+
currentUserId: {
121+
type: Number,
122+
default: null
112123
}
113124
})
114125
@@ -117,6 +128,14 @@ const router = useRouter()
117128
const { encodeHashid } = useHashids()
118129
const { t } = useI18n()
119130
131+
const isMobile = ref(false)
132+
133+
const showHoverCard = ref(false)
134+
const hoverCardPosition = ref({ top: '0px', left: '0px' })
135+
const hoverTimeout = ref(null)
136+
const isOverCard = ref(false)
137+
const nameRef = ref(null)
138+
120139
const notificationConfig = {
121140
'video.like': {
122141
message: t('notifications.messageTypes.videoLike'),
@@ -180,6 +199,85 @@ const notificationConfig = {
180199
}
181200
}
182201
202+
// Check if device is mobile
203+
const checkMobile = () => {
204+
isMobile.value =
205+
'ontouchstart' in window || navigator.maxTouchPoints > 0 || window.innerWidth < 768
206+
}
207+
208+
onMounted(() => {
209+
checkMobile()
210+
window.addEventListener('resize', checkMobile)
211+
})
212+
213+
onUnmounted(() => {
214+
window.removeEventListener('resize', checkMobile)
215+
clearTimeout(hoverTimeout.value)
216+
})
217+
218+
const calculatePosition = () => {
219+
if (!nameRef.value) return { top: '0px', left: '0px' }
220+
221+
const rect = nameRef.value.getBoundingClientRect()
222+
const cardWidth = 288
223+
const cardHeight = 220
224+
const spacing = 12
225+
226+
let left = rect.left + rect.width / 2 - cardWidth / 2
227+
228+
const margin = 16
229+
if (left < margin) {
230+
left = margin
231+
} else if (left + cardWidth > window.innerWidth - margin) {
232+
left = window.innerWidth - cardWidth - margin
233+
}
234+
235+
let top = rect.top - cardHeight - spacing
236+
237+
if (top < margin) {
238+
top = rect.bottom + spacing
239+
}
240+
241+
return {
242+
top: `${top}px`,
243+
left: `${left}px`
244+
}
245+
}
246+
247+
const handleNameHover = () => {
248+
if (isMobile.value) return
249+
250+
clearTimeout(hoverTimeout.value)
251+
hoverTimeout.value = setTimeout(() => {
252+
hoverCardPosition.value = calculatePosition()
253+
showHoverCard.value = true
254+
}, 500)
255+
}
256+
257+
const handleNameLeave = () => {
258+
clearTimeout(hoverTimeout.value)
259+
hoverTimeout.value = setTimeout(() => {
260+
if (!isOverCard.value) {
261+
showHoverCard.value = false
262+
}
263+
}, 200)
264+
}
265+
266+
const handleCardEnter = () => {
267+
clearTimeout(hoverTimeout.value)
268+
isOverCard.value = true
269+
}
270+
271+
const handleCardLeave = () => {
272+
isOverCard.value = false
273+
showHoverCard.value = false
274+
}
275+
276+
const navigateToProfile = () => {
277+
markAsRead()
278+
router.push(`/@${props.notification.actor.username}`)
279+
}
280+
183281
const getNotificationMessage = () => {
184282
const config = notificationConfig[props.notification.type]
185283
return config?.message || t('notifications.messageTypes.default')

0 commit comments

Comments
 (0)