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} ) }