Skip to content

Commit

Permalink
feat: added basic notification center that works
Browse files Browse the repository at this point in the history
  • Loading branch information
v1s10n-4 committed Sep 16, 2024
1 parent 7d30c2a commit 322e8f4
Show file tree
Hide file tree
Showing 10 changed files with 330 additions and 94 deletions.
7 changes: 5 additions & 2 deletions app/ClientProviders.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
"use client";

import { NovuProvider } from "@novu/react";
import { FC, PropsWithChildren } from "react";
import React, { FC, PropsWithChildren } from "react";
import NotificationFilterStatusProvider from "@/app/NotificationFilterStatus";

const ClientProviders: FC<PropsWithChildren> = ({ children }) => {
return (
<NovuProvider
applicationIdentifier="Q3MtSymCOQDP"
subscriberId="on-boarding-subscriber-id-123"
>
{children}
<NotificationFilterStatusProvider>
{children}
</NotificationFilterStatusProvider>
</NovuProvider>
);
};
Expand Down
102 changes: 102 additions & 0 deletions app/NotificationCenter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"use client";
import {
Badge,
Button,
Card,
IconButton,
Inset,
ScrollArea,
Separator,
VisuallyHidden,
} from "@v1s10n_4/radix-ui-themes";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/Drawer";
import BellIcon from "@/app/bell.svg";
import BellRingIcon from "@/app/bell-ring.svg";
import React, { ComponentProps, FC } from "react";
import { useCounts } from "@novu/react";
import NotificationList from "@/app/NotificationList";

const NotificationTrigger: FC<ComponentProps<typeof IconButton>> = (props) => {
const { counts, isFetching, isLoading } = useCounts({
filters: [{ read: false }],
});
return (
<IconButton
size="3"
variant="ghost"
color="gray"
mr="2"
className="relative"
{...props}
>
<div>
{counts && counts[0].count > 0 && (isLoading || isFetching) ? (
<BellRingIcon className="h-8 w-8" />
) : (
<BellIcon className="h-8 w-8" />
)}

{counts && counts[0].count > 0 && (
<Badge color="red" className="absolute right-0 top-0">
{counts[0].count}
</Badge>
)}
<VisuallyHidden>
Notification Center ({counts && counts[0].count} unread)
</VisuallyHidden>
</div>
</IconButton>
);
};

const NotificationCenter = () => {
return (
<Drawer direction="top" modal={false}>
<DrawerTrigger asChild>
<NotificationTrigger />
</DrawerTrigger>
<DrawerContent
className="fixed right-0 top-2 max-h-[98dvh] focus-visible:outline-0 sm:right-2 sm:top-24"
asChild
>
<Card className="mx-2 flex max-w-screen-xs flex-col rounded-t-none pb-[env(safe-area-inset-top)] [box-shadow:--shadow-5] before:rounded-b-none after:!inset-[--base-card-border-width] after:rounded-t-none">
<DrawerHeader pt="2">
<DrawerTitle align="center" size="1" mb="0">
Notifications
</DrawerTitle>
<VisuallyHidden>
<DrawerDescription></DrawerDescription>
</VisuallyHidden>
</DrawerHeader>
<Inset side="x">
<Separator size="4" mb="3" />
</Inset>
<ScrollArea>
<NotificationList />
</ScrollArea>
<Inset side="x">
<Separator size="4" mt="3" />
</Inset>
<DrawerFooter px="0" py="3">
<DrawerClose asChild>
<Button variant="surface" color="gray">
Close
</Button>
</DrawerClose>
</DrawerFooter>
</Card>
</DrawerContent>
</Drawer>
);
};

export default NotificationCenter;
38 changes: 38 additions & 0 deletions app/NotificationFilterStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"use client";
import React, { ReactNode, useState } from "react";

type StatusContextProps = {
status: "all" | "unread" | "archived";
setStatus: (status: "all" | "unread" | "archived") => void;
};

const StatusContext = React.createContext<StatusContextProps | undefined>(
undefined
);

export const useNotificationFilterStatus = () => {
const context = React.useContext(StatusContext);
if (context === undefined) {
throw new Error(
"useNotificationFilterStatus must be used within a StatusProvider"
);
}

return context;
};

export const NotificationFilterStatusProvider = ({
children,
}: {
children: ReactNode;
}) => {
const [status, setStatus] = useState<StatusContextProps["status"]>("all");

return (
<StatusContext.Provider value={{ status, setStatus }}>
{children}
</StatusContext.Provider>
);
};

export default NotificationFilterStatusProvider;
88 changes: 0 additions & 88 deletions app/NotificationInbox.tsx

This file was deleted.

57 changes: 57 additions & 0 deletions app/NotificationItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"use client";
import { Avatar, Box, Card, IconButton, Text } from "@v1s10n_4/radix-ui-themes";
import type { Notification } from "@novu/react";
import MailDeleteIcon from "pixelarticons/svg/mail-delete.svg";
import MailCheckIcon from "pixelarticons/svg/mail-check.svg";
import React, { FC } from "react";
import { timeAgo } from "@/lib/utils";

const NotificationItem: FC<{ notification: Notification }> = ({
notification,
}) => {
return (
<Card
key={notification.id}
className="flex gap-3"
variant="classic"
style={{
backgroundColor: notification.isRead ? "" : "var(--color-panel-solid)",
}}
>
{notification.avatar && (
<Avatar
size="3"
src={notification.avatar}
fallback={notification.body[0] || "N"}
/>
)}
<Box style={{ flex: 1 }}>
<Text size="2" weight={notification.isRead ? "regular" : "bold"}>
{notification.body}
</Text>
<Text size="1" color="gray" asChild>
<time> ({timeAgo(notification.createdAt)})</time>
</Text>
</Box>
{notification.isRead ? (
<IconButton
size="1"
variant="outline"
onClick={() => notification.unread()}
>
<MailDeleteIcon />
</IconButton>
) : (
<IconButton
size="1"
variant="outline"
onClick={() => notification.read()}
>
<MailCheckIcon />
</IconButton>
)}
</Card>
);
};

export default NotificationItem;
47 changes: 47 additions & 0 deletions app/NotificationList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"use client";
import React, { useMemo } from "react";
import { useNotifications } from "@novu/react";
import { Button, Flex, ScrollArea, Spinner } from "@v1s10n_4/radix-ui-themes";
import { useNotificationFilterStatus } from "@/app/NotificationFilterStatus";
import NotificationItem from "@/app/NotificationItem";

export default function NotificationList() {
const { status } = useNotificationFilterStatus();
const filter = useMemo(() => {
if (status === "unread") {
return { read: false };
} else if (status === "archived") {
return { archived: true };
}

return { archived: false };
}, [status]);
const { notifications, isLoading, isFetching, hasMore, fetchMore, error } =
useNotifications(filter);

const handleLoadMore = async () => {
if (hasMore && !isLoading) {
await fetchMore();
}
};

return (
<ScrollArea style={{ flex: 1 }}>
<Flex direction="column" gap="1">
{isLoading && (
<Spinner size="3" className="aspect-square h-full w-full" />
)}
{notifications?.map((notification) => (
<NotificationItem key={notification.id} notification={notification} />
))}
{hasMore && (
<Flex justify="center" p="4">
<Button onClick={handleLoadMore} disabled={isLoading}>
{isLoading ? "Loading..." : "Load More"}
</Button>
</Flex>
)}
</Flex>
</ScrollArea>
);
}
3 changes: 3 additions & 0 deletions app/bell-ring.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions app/bell.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 322e8f4

Please sign in to comment.