213 lines
5.4 KiB
Vue
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>
|