Skip to content

Commit

Permalink
feat(app): delete all notifications button (#3018)
Browse files Browse the repository at this point in the history
  • Loading branch information
tim-schultz authored Nov 4, 2024
1 parent ce61f68 commit 77dfcd7
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 27 deletions.
62 changes: 53 additions & 9 deletions app/__tests__/components/Notifications.test.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,43 @@
import React from "react";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { Notifications } from "../../components/Notifications";
import { useNotifications, useDismissNotification } from "../../hooks/useNotifications";
import { useNotifications, useDismissNotification, useDeleteAllNotifications } from "../../hooks/useNotifications";
import { StampClaimingContext } from "../../context/stampClaimingContext";
import { CeramicContext } from "../../context/ceramicContext";

// Mock the hooks and contexts
jest.mock("../../hooks/useNotifications", () => ({
useNotifications: jest.fn(),
useDismissNotification: jest.fn(),
useDeleteAllNotifications: jest.fn(),
}));
jest.mock("../../context/stampClaimingContext");
jest.mock("../../context/ceramicContext");

const mockSetShowSidebar = jest.fn();
const mockDeleteMutation = { mutate: jest.fn() };

describe("Notifications Component", () => {
beforeEach(() => {
jest.clearAllMocks();
(useDeleteAllNotifications as jest.Mock).mockReturnValue(mockDeleteMutation);
});

it("should render without notifications", async () => {
(useNotifications as jest.Mock).mockReturnValue({ notifications: [] });

render(<Notifications setShowSidebar={mockSetShowSidebar} />);
await waitFor(() => {});

const noteBell = screen.getByTestId("notification-bell");
fireEvent.click(noteBell);

await waitFor(() => {
expect(screen.getByText("Congrats! You don't have any notifications.")).toBeInTheDocument();
});
await waitFor(() => {});

expect(
screen.getByText("You have no notifications. We’ll let you know when there’s something.")
).toBeInTheDocument();
});

it("should render read notification", async () => {
(useNotifications as jest.Mock).mockReturnValue({
notifications: [
Expand All @@ -46,14 +53,14 @@ describe("Notifications Component", () => {

render(<Notifications setShowSidebar={mockSetShowSidebar} />);

await waitFor(() => {});

const noteBell = screen.getByTestId("notification-bell");
fireEvent.click(noteBell);

await waitFor(() => {});
expect(screen.getByText(`Test notification`)).toBeInTheDocument();
expect(screen.getByTestId("read-indicator")).toHaveClass("bg-background-5");
});

it("should render unread notification", async () => {
(useNotifications as jest.Mock).mockReturnValue({
notifications: [
Expand All @@ -69,12 +76,49 @@ describe("Notifications Component", () => {

render(<Notifications setShowSidebar={mockSetShowSidebar} />);

await waitFor(() => {});

const noteBell = screen.getByTestId("notification-bell");
fireEvent.click(noteBell);

await waitFor(() => {});
expect(screen.getByText(`Test notification`)).toBeInTheDocument();
expect(screen.getByTestId("read-indicator")).toHaveClass("bg-transparent");
});

it("should handle delete all notifications", async () => {
(useNotifications as jest.Mock).mockReturnValue({
notifications: [
{
notification_id: "1",
type: "on_chain_expiry",
content: "Test notification",
is_read: false,
link: "https://example.com",
},
],
});

render(<Notifications setShowSidebar={mockSetShowSidebar} />);

const noteBell = screen.getByTestId("notification-bell");
fireEvent.click(noteBell);

const deleteButton = screen.getByText("Delete All");
fireEvent.click(deleteButton);

await waitFor(() => {
expect(mockDeleteMutation.mutate).toHaveBeenCalled();
});
});

it("should not show delete all button when there are no notifications", async () => {
(useNotifications as jest.Mock).mockReturnValue({ notifications: [] });
render(<Notifications setShowSidebar={mockSetShowSidebar} />);

const noteBell = screen.getByTestId("notification-bell");
fireEvent.click(noteBell);

await waitFor(() => {
expect(screen.queryByText("Delete All")).not.toBeInTheDocument();
});
});
});
75 changes: 57 additions & 18 deletions app/components/Notifications.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import React, { Fragment, useCallback, useContext, useMemo, useState } from "react";
import { Popover, Transition } from "@headlessui/react";
import { useNotifications, useDismissNotification, Notification } from "../hooks/useNotifications";
import {
useNotifications,
useDismissNotification,
Notification,
useDeleteAllNotifications,
} from "../hooks/useNotifications";
import { StampClaimForPlatform, StampClaimingContext } from "../context/stampClaimingContext";
import { PLATFORM_ID, PROVIDER_ID } from "@gitcoin/passport-types";
import { CeramicContext } from "../context/ceramicContext";
Expand Down Expand Up @@ -172,12 +177,28 @@ const NotificationComponent: React.FC<NotificationProps> = ({ notification, setS
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute w-48 right-1 bg-background flex flex-col justify-start text-left p-4 rounded">
<button onClick={() => dismissMutation.mutate()} className="w-full text-left">
Mark as Read
</button>
<button className="w-full text-left text-color-7" onClick={() => deleteMutation.mutate()}>
Delete
</button>
{({ close }) => (
<>
<button
onClick={() => {
dismissMutation.mutate();
close();
}}
className="w-full text-left"
>
Mark as Read
</button>
<button
className="w-full text-left text-color-7"
onClick={() => {
deleteMutation.mutate();
close();
}}
>
Delete
</button>
</>
)}
</Popover.Panel>
</Transition>
</>
Expand All @@ -194,6 +215,7 @@ export type NotificationsProps = {

export const Notifications: React.FC<NotificationsProps> = ({ setShowSidebar }) => {
const { notifications } = useNotifications();
const deleteMutation = useDeleteAllNotifications();
const hasNotifications = notifications.length > 0;
return (
<div className="flex justify-end z-20">
Expand Down Expand Up @@ -228,19 +250,36 @@ export const Notifications: React.FC<NotificationsProps> = ({ setShowSidebar })
<div className="w-full relative">
<div className="absolute top-[-6px] w-[10px] h-[10px] right-7 border-l bg-background border-b border-foreground-5 transform rotate-[135deg]"></div>
</div>
<div className="overflow-y-auto min-h-[120px] max-h-[40vh]">
<div className="absolute w-full pl-8 py-3 text-foreground-2 bg-background z-20 rounded-t">
Notifications
</div>
<div
className={`overflow-y-auto min-h-[120px] max-h-[40vh] ${notifications.length > 0 ? "pt-10 pb-10" : "pt-6"}`}
>
{notifications.length > 0 ? (
notifications
.sort((a, b) => (a.is_read === b.is_read ? 0 : a.is_read ? 1 : -1))
.map((notification) => (
<NotificationComponent
key={notification.notification_id}
notification={notification}
setShowSidebar={() => setShowSidebar(true)}
/>
))
<>
{notifications
.sort((a, b) => (a.is_read === b.is_read ? 0 : a.is_read ? 1 : -1))
.map((notification) => (
<NotificationComponent
key={notification.notification_id}
notification={notification}
setShowSidebar={() => setShowSidebar(true)}
/>
))}
<div
onClick={() => {
deleteMutation.mutate();
}}
className="cursor-pointer absolute bottom-0 w-full pl-8 py-3 text-foreground-2 bg-background z-20 rounded-b bg-gradient-to-b from-background via-background to-[#082F2A]"
>
Delete All
</div>
</>
) : (
<p className="p-2">Congrats! You don&apos;t have any notifications.</p>
<p className="p-8 text-foreground-2">
You have no notifications. We’ll let you know when there’s something.
</p>
)}
</div>
</Popover.Panel>
Expand Down
22 changes: 22 additions & 0 deletions app/hooks/useNotifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,28 @@ export const useDismissNotification = (notification_id: string, dismissalType: "
});
};

const deleteAllNotifications = async (dbAccessToken?: string) => {
if (!dbAccessToken) return;
const res = await axios.delete(`${process.env.NEXT_PUBLIC_SCORER_ENDPOINT}/passport-admin/notifications`, {
headers: {
Authorization: `Bearer ${dbAccessToken}`,
},
});
return res.data;
};

export const useDeleteAllNotifications = () => {
const { dbAccessToken } = useDatastoreConnectionContext();
const queryClient = useQueryClient();

return useMutation({
mutationFn: () => deleteAllNotifications(dbAccessToken),
onSuccess: () => {
queryClient.setQueryData(["notifications"], { items: [] });
},
});
};

export const useNotifications = () => {
const { dbAccessTokenStatus, dbAccessToken } = useDatastoreConnectionContext();
const { scorer } = useCustomization();
Expand Down

0 comments on commit 77dfcd7

Please sign in to comment.