Skip to content

Instantly share code, notes, and snippets.

@fullstackjedi
Created September 2, 2025 12:56
Show Gist options
  • Select an option

  • Save fullstackjedi/40aa6acb9755044732cdc39db3b8e569 to your computer and use it in GitHub Desktop.

Select an option

Save fullstackjedi/40aa6acb9755044732cdc39db3b8e569 to your computer and use it in GitHub Desktop.
Novu Component
import { Fragment, useMemo, useState } from 'react'
import { useCounts, useNotifications, useNovu } from '@novu/react'
import { useFetchAllChangelogPosts } from '@pulse-modules/dashboard/queries/use-fetch-changelogs'
import { useAppStore } from '@pulse-modules/system/store/app-store-provider'
import { TChangelog } from '@pulse-types/changelog'
import { InAppNotificationEvents, TNotification } from '@pulse-types/notification'
import { Icons } from '@pulse-ui/icons/base'
import { Badge } from '@pulse-ui/primitives/badge'
import { Button } from '@pulse-ui/primitives/button'
import { InfiniteScroll } from '@pulse-ui/primitives/infinite-scroll'
import { Box, Center, HStack } from '@pulse-ui/primitives/layout'
import { Popover, PopoverContent, PopoverTrigger } from '@pulse-ui/primitives/popover'
import { TabContent, Tabs, TabsList, TabTrigger } from '@pulse-ui/primitives/tabs'
import { Span, Text } from '@pulse-ui/primitives/text'
import { ChangelogCard } from './changelog/changelog-card'
import { AccessGrant } from './templates/features/access-grant'
import { EntityRecordUpdate } from './templates/features/entity-record-update'
import { EntityViewExport } from './templates/features/entity-view-export'
import { FallbackNotificationCard } from './templates/features/fallback-notification-card'
import { TaskAssignment } from './templates/features/task-assignment'
import { TaskStatusUpdate } from './templates/features/task-status'
import { NoNotifications } from './templates/primitives/notifications-empty'
import { NotificationsLoader } from './templates/primitives/notifications-loader'
import { ViewChangelog } from './view-changelog'
export const Notifications = () => {
const novu = useNovu()
const { counts } = useCounts({ filters: [{ read: false }] })
const handleMarkAllAsRead = async () => {
await novu.notifications.readAll()
}
const unreadCount = counts?.[0].count ?? 0
const hasUnreadNotifications = unreadCount > 0
const { data: changelog = { docs: [] }, isLoading } = useFetchAllChangelogPosts()
const [selectedChangelog, setSelectedChangelog] = useState<TChangelog | null>(null)
const [openViewChangelog, setOpenViewChangelog] = useState(false)
return (
<Popover>
<PopoverTrigger asChild>
<Button
colorPalette="brand.grayA"
look="outline"
pos="relative"
px="px"
py="px"
rounded="2xl"
shadow={hasUnreadNotifications ? 'none' : 'xs'}
size="auto"
>
<Center bg="background.app" gap="1" h="2.8rem" px="1" rounded="0.9rem" w="100%" zIndex="2">
<Box pos="relative">
<Icons.notificationBell />
{unreadCount > 0 && (
<Badge
h="0.8rem"
look="solid"
pos="absolute"
right="-0.1rem"
rounded="full"
size="auto"
top="-0.2rem"
w="0.8rem"
/>
)}
</Box>
<Span color="text.muted" fontSize="1">
For you
</Span>
</Center>
<Box
animation="animatedGradient"
bgSize="300% 300%"
h="100%"
layerStyle={hasUnreadNotifications ? 'primary' : undefined}
pos="absolute"
right="0"
rounded="xl"
top="0"
w="100%"
zIndex="1"
/>
</Button>
</PopoverTrigger>
<PopoverContent borderBottom="none" borderRadius="none" h="calc(100vh - 5rem)" sideOffset={8} w="40rem">
<HStack h="4.9rem" justify="space-between" px="3">
<Text fontWeight="600">Notifications</Text>
<Button color="text.muted" look="transparent" onClick={handleMarkAllAsRead} size="xs">
<Icons.checkCheck size={12} />
<Span>Mark as read</Span>
</Button>
</HStack>
<Tabs defaultValue="all">
<TabsList borderBottom="subtle" gap="1">
{TAGS.map((tag) => (
<TabTrigger asChild key={tag.value} value={tag.value}>
<Button
borderBottom="subtle"
borderBottomColor="transparent"
color="text.muted"
css={{
'&[data-state=active]': { borderBottom: 'focused' }
}}
look="ghost"
minW="4rem"
px="2"
rounded="none"
size="xs"
>
{tag.label}
</Button>
</TabTrigger>
))}
<TabTrigger asChild value="changelog">
<Button
borderBottom="subtle"
borderBottomColor="transparent"
color="text.muted"
css={{
'&[data-state=active]': { borderBottom: 'focused' }
}}
look="ghost"
minW="4rem"
px="2"
rounded="none"
size="xs"
>
Changelog
</Button>
</TabTrigger>
</TabsList>
<Box mt="3">
{TAGS.map((tag) => (
<TabContent key={tag.value} value={tag.value}>
<NotificationContent tags={tag.value === 'all' ? undefined : [tag.value]} />
</TabContent>
))}
<TabContent value="changelog">
{isLoading ? (
<NotificationsLoader />
) : (
<Fragment>
{changelog.docs.map((changelogItem) => (
<ChangelogCard
changelog={changelogItem}
key={changelogItem.id}
setOpenViewChangelog={setOpenViewChangelog}
setSelectedChangelog={setSelectedChangelog}
/>
))}
</Fragment>
)}
<ViewChangelog
changelog={selectedChangelog}
onOpenChange={setOpenViewChangelog}
open={openViewChangelog}
/>
</TabContent>
</Box>
</Tabs>
</PopoverContent>
</Popover>
)
}
const NotificationContent = ({ tags }: { tags?: string[] }) => {
const userWorkspace = useAppStore((st) => st.userWorkspace)
const { notifications, isLoading, fetchMore: fetchNextPage, hasMore: hasNextPage } = useNotifications({ tags })
const notificationsForThisWorkspace = useMemo(() => {
const impulseNotifications = notifications as unknown as TNotification[]
return impulseNotifications?.filter((notification) => notification.data.tenant.id === userWorkspace.workspaceId)
}, [notifications, userWorkspace])
if (!userWorkspace || !notifications) {
return <NoNotifications />
}
if (!isLoading && notificationsForThisWorkspace.length === 0) {
return <NoNotifications />
}
return (
<InfiniteScroll
h="calc(100vh - 13rem)"
hasMore={hasNextPage}
isLoading={isLoading}
loadMore={async () => fetchNextPage()}
overflowY="auto"
pb="20"
>
{notificationsForThisWorkspace.map((notification) => {
const impulseNotification = notification as unknown as TNotification
const { actor, tenant } = impulseNotification.data
if (!actor || !tenant) return null
// task-related notifications
if (notification.workflow?.name === InAppNotificationEvents.taskAssignment) {
return <TaskAssignment key={notification.id} notification={impulseNotification} />
}
if (notification.workflow?.name === InAppNotificationEvents.taskStatusUpdate) {
return <TaskStatusUpdate key={notification.id} notification={impulseNotification} />
}
// entity-related notifications
if (notification.workflow?.name === InAppNotificationEvents.entityAttributeValueUpdate) {
return <EntityRecordUpdate key={notification.id} notification={impulseNotification} />
}
if (notification.workflow?.name === InAppNotificationEvents.entityViewExport) {
return <EntityViewExport key={notification.id} notification={impulseNotification} />
}
// access control notifications
if (notification.workflow?.name === InAppNotificationEvents.accessGrant) {
return <AccessGrant key={notification.id} notification={impulseNotification} />
}
return <FallbackNotificationCard key={notification.id} notification={impulseNotification} />
})}
</InfiniteScroll>
)
}
const TAGS = [
{
label: 'All',
value: 'all'
},
{
label: 'Tasks',
value: 'tasks'
},
{
label: 'Calendar',
value: 'calendar'
},
{
label: 'Updates',
value: 'updates'
}
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment