import { useCallback } from 'react'
import {
ActionIcon,
Indicator,
Popover,
Drawer,
Stack,
Text,
Group,
Box,
UnstyledButton,
Anchor,
Divider,
} from '@pikku/mantine/core'
import { useDisclosure, useMediaQuery } from '@mantine/hooks'
import { asI18n } from '@pikku/react'
import { m } from '@/i18n/messages'
import { Bell, BellOff } from 'lucide-react'
import { useNavigate } from '@tanstack/react-router'
import { useQueryClient } from '@tanstack/react-query'
import { usePikkuQuery, usePikkuMutation } from '@perauset/functions-sdk/pikku/api.gen'
interface NotificationItem {
notificationId: string
type: string
title: string
body: string | null
entityType: string | null
entityId: string | null
readAt: string | Date | null
createdAt: string | Date
}
function entityLink(
entityType: string | null,
entityId: string | null,
): string | null {
if (!entityType || !entityId) return null
switch (entityType) {
case 'task':
return '/tasks'
case 'stay':
return '/stays'
case 'boat_trip':
return '/boats'
case 'retreat':
return '/retreats'
default:
return null
}
}
function formatTimestamp(val: string | Date): string {
const d = typeof val === 'string' ? new Date(val) : val
const now = new Date()
const diffMs = now.getTime() - d.getTime()
const diffMin = Math.floor(diffMs / 60000)
if (diffMin < 1) return 'Just now'
if (diffMin < 60) return `${diffMin}m ago`
const diffHr = Math.floor(diffMin / 60)
if (diffHr < 24) return `${diffHr}h ago`
const diffDays = Math.floor(diffHr / 24)
if (diffDays < 7) return `${diffDays}d ago`
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
}
function NotificationEntry({
n,
onRead,
}: {
n: NotificationItem
onRead: (id: string, link: string | null) => void
}) {
const isRead = !!n.readAt
const link = entityLink(n.entityType, n.entityId)
return (
onRead(n.notificationId, link)}
style={{
display: 'block',
width: '100%',
padding: '10px 12px',
borderRadius: 8,
backgroundColor: isRead ? 'transparent' : 'rgba(42, 123, 136, 0.04)',
transition: 'background-color 0.15s ease',
}}
>
{asI18n(n.title)}
{asI18n(formatTimestamp(n.createdAt))}
{n.body && (
{asI18n(n.body)}
)}
)
}
export function NotificationBell() {
const navigate = useNavigate()
const queryClient = useQueryClient()
const isMobile = useMediaQuery('(max-width: 768px)')
const [popoverOpened, { toggle: togglePopover, close: closePopover }] =
useDisclosure()
const [drawerOpened, { toggle: toggleDrawer, close: closeDrawer }] =
useDisclosure()
const { data } = usePikkuQuery(
'listNotifications',
{ limit: 5, offset: 0 },
{ refetchInterval: 30_000 },
)
const items = (data?.items ?? []) as NotificationItem[]
const unreadCount = items.filter((n) => !n.readAt).length
const markReadMutation = usePikkuMutation('markNotificationRead', {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['listNotifications'] })
},
})
const handleRead = useCallback(
(notificationId: string, link: string | null) => {
// Optimistic update
queryClient.setQueryData<{ items?: NotificationItem[] }>(
['listNotifications', { limit: 5, offset: 0 }],
(old) => {
if (!old?.items) return old
return {
...old,
items: old.items.map((n) =>
n.notificationId === notificationId
? { ...n, readAt: new Date().toISOString() }
: n,
),
}
},
)
markReadMutation.mutate({ notificationId })
if (link) {
closePopover()
closeDrawer()
navigate({ to: link })
}
},
[queryClient, markReadMutation, closePopover, closeDrawer, navigate],
)
const handleToggle = () => {
if (isMobile) {
toggleDrawer()
} else {
togglePopover()
}
}
const bellButton = (
0 ? asI18n(String(unreadCount)) : undefined}
disabled={unreadCount === 0}
color="red"
offset={4}
processing={unreadCount > 0}
>
)
const content = (
{m.notifications_title()}
{unreadCount > 0 && (
{m.notifications_unread_count({ count: unreadCount })}
)}
{items.length === 0 ? (
{m.notifications_empty()}
) : (
{items.map((n) => (
))}
)}
{
closePopover()
closeDrawer()
navigate({ to: '/notifications' })
}}
>
{m.notifications_view_all()}
)
if (isMobile) {
return (
<>
{bellButton}
{content}
>
)
}
return (
{
if (!o) closePopover()
}}
width={360}
position="bottom-end"
shadow="lg"
radius="md"
>
{bellButton}
{content}
)
}