harb/web-app/src/components/NotificationBell.vue
openhands 60d0859eb3 fix: Snatch notifications and position history (#151)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 08:47:24 +00:00

213 lines
5.4 KiB
Vue

<template>
<div class="notification-bell" ref="bellRef">
<button class="notification-bell__btn" @click="toggle" :aria-label="`Notifications${unseenCount > 0 ? `, ${unseenCount} unseen` : ''}`">
<Icon icon="mdi:bell-outline" class="notification-bell__icon" />
<span v-if="unseenCount > 0" class="notification-bell__badge">{{ unseenCount }}</span>
</button>
<Transition name="bell-panel">
<div v-if="isOpen" class="notification-bell__panel" role="dialog" aria-label="Snatch notifications">
<div class="notification-bell__panel-header">
<span class="notification-bell__panel-title">Snatch Notifications</span>
<button class="notification-bell__close" @click="close" aria-label="Close notifications">✕</button>
</div>
<div class="notification-bell__panel-body">
<div v-if="allRecentSnatches.length === 0" class="notification-bell__empty">No snatch events found.</div>
<div v-for="event in allRecentSnatches" :key="event.id" class="notification-bell__event">
<div class="notification-bell__event-icon">⚡</div>
<div class="notification-bell__event-details">
<div class="notification-bell__event-title">Position snatched</div>
<div class="notification-bell__event-meta">
<span>{{ formatKrk(event.tokenAmount) }} $KRK</span>
<span class="notification-bell__event-time">{{ relativeTime(event.timestamp) }}</span>
</div>
</div>
</div>
</div>
</div>
</Transition>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { Icon } from '@iconify/vue';
import { useSnatchNotifications } from '@/composables/useSnatchNotifications';
import { useWallet } from '@/composables/useWallet';
import { DEFAULT_CHAIN_ID } from '@/config';
const wallet = useWallet();
const chainId = wallet.account.chainId ?? DEFAULT_CHAIN_ID;
const { unseenCount, allRecentSnatches, isOpen, toggle, close } = useSnatchNotifications(chainId);
const bellRef = ref<HTMLElement | null>(null);
function formatKrk(raw: string): string {
try {
const val = Number(BigInt(raw)) / 1e18;
return val.toFixed(2);
} catch {
return '?';
}
}
function relativeTime(ts: string): string {
try {
const sec = Number(ts);
const nowSec = Math.floor(Date.now() / 1000);
const diff = nowSec - sec;
if (diff < 60) return 'just now';
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
return `${Math.floor(diff / 86400)}d ago`;
} catch {
return '';
}
}
function handleClickOutside(event: MouseEvent) {
if (bellRef.value && !bellRef.value.contains(event.target as Node)) {
close();
}
}
onMounted(() => {
document.addEventListener('mousedown', handleClickOutside);
});
onUnmounted(() => {
document.removeEventListener('mousedown', handleClickOutside);
});
</script>
<style lang="sass">
.notification-bell
position: relative
display: inline-flex
align-items: center
&__btn
background: none
border: none
cursor: pointer
color: var(--color-navbar-font, #fff)
padding: 8px
border-radius: 8px
display: flex
align-items: center
justify-content: center
position: relative
transition: background 0.15s
&:hover
background: rgba(255, 255, 255, 0.08)
&__icon
font-size: 22px
&__badge
position: absolute
top: 2px
right: 2px
background: #f59e0b
color: #000
border-radius: 50%
font-size: 10px
font-weight: 700
min-width: 16px
height: 16px
display: flex
align-items: center
justify-content: center
padding: 0 3px
line-height: 1
&__panel
position: absolute
top: calc(100% + 8px)
right: 0
width: 300px
background: #0F0F0F
border: 1px solid var(--color-collapse-border, #2D2D2D)
border-radius: 12px
z-index: 100
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5)
overflow: hidden
&__panel-header
display: flex
justify-content: space-between
align-items: center
padding: 12px 16px
border-bottom: 1px solid var(--color-collapse-border, #2D2D2D)
&__panel-title
font-size: 14px
font-weight: 600
color: #fff
&__close
background: none
border: none
color: #9A9898
cursor: pointer
font-size: 14px
padding: 2px 6px
border-radius: 4px
&:hover
color: #fff
&__panel-body
max-height: 320px
overflow-y: auto
padding: 8px 0
&__empty
padding: 16px
color: #9A9898
font-size: 13px
text-align: center
&__event
display: flex
gap: 10px
padding: 10px 16px
border-bottom: 1px solid rgba(255, 255, 255, 0.04)
&:last-child
border-bottom: none
&:hover
background: rgba(255, 255, 255, 0.03)
&__event-icon
font-size: 18px
flex-shrink: 0
margin-top: 1px
&__event-details
flex: 1
min-width: 0
&__event-title
font-size: 13px
font-weight: 500
color: #f59e0b
&__event-meta
display: flex
justify-content: space-between
font-size: 12px
color: #9A9898
margin-top: 2px
&__event-time
flex-shrink: 0
.bell-panel-enter-active,
.bell-panel-leave-active
transition: opacity 0.15s, transform 0.15s
.bell-panel-enter-from,
.bell-panel-leave-to
opacity: 0
transform: translateY(-6px)
</style>