Skip to content

Commit

Permalink
feat(app): remaining updates for notifcations
Browse files Browse the repository at this point in the history
  • Loading branch information
tim-schultz committed Jun 21, 2024
1 parent 2fa580c commit d36589e
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 82 deletions.
1 change: 0 additions & 1 deletion app/components/Category.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ export const Category = ({
const [dropDownOpen, setDropDownOpen] = useState<boolean>(false);
const openRef = React.useRef(dropDownOpen);
openRef.current = dropDownOpen;
const customization = useCustomization();

const [panelMounted, setPanelMounted] = useState<boolean>(false);

Expand Down
168 changes: 103 additions & 65 deletions app/components/Notifications.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,90 @@
import React, { Fragment } from "react";
import React, { Fragment, useContext, useMemo } from "react";
import { Popover, Transition } from "@headlessui/react";
import { useNotifications, useDismissNotification, Notification } from "../hooks/useNotifications";
import { OnchainSidebar } from "./OnchainSidebar";
import { StampClaimForPlatform, StampClaimingContext } from "../context/stampClaimingContext";
import { PLATFORM_ID, PROVIDER_ID } from "@gitcoin/passport-types";
import { CeramicContext } from "../context/ceramicContext";

export type NotificationProps = {
notification_id: string;
type: "Custom" | "Expiry" | "OnChainExpiry" | "Deduplication";
content: string;
dismissed: boolean;
notification: Notification;
} & {
setShowSidebar: (show: boolean) => void;
};

const Content = ({ type, content }: NotificationProps) => {
const ExpiryAction = ({
content,
provider,
notification_id,
}: {
content: string;
provider: PROVIDER_ID;
notification_id: string;
}) => {
const { claimCredentials } = useContext(StampClaimingContext);
const { expiredPlatforms } = useContext(CeramicContext);

const platformId = Object.values(expiredPlatforms)
.filter((platform) => {
const providers = platform.platFormGroupSpec
.map((spec) => spec.providers.map((provider) => provider.name))
.flat();

return providers.includes(provider);
})
.map((platform) => platform.platform.platformId)[0];

const deleteMutation = useDismissNotification(notification_id, "delete");

const refreshStamp = async (stamp: StampClaimForPlatform) => {
await claimCredentials(async () => await Promise.resolve(), [stamp]);
deleteMutation.mutate();
};

const message = useMemo(() => {
const claim: StampClaimForPlatform = {
platformId: platformId as PLATFORM_ID,
selectedProviders: [provider],
};

const parts = content.split(/(reverify)/i);
return parts.map((part, index) =>
part.toLowerCase() === "reverify" ? (
<div className="inline-block underline cursor-pointer" key={index} onClick={() => refreshStamp(claim)}>
{part}
</div>
) : (
part
)
);
}, [platformId, provider, content, claimCredentials]);

return <>{message}</>;
};

const Content = ({ notification }: { notification: Notification }) => {
const { content, link, type } = notification;
switch (type) {
case "Custom":
case "custom":
return (
<span>
{content}. Learn more{" "}
<a className="underline" href={link} target="_blank">
here
</a>
.
</span>
);
case "stamp_expiry":
return (
<ExpiryAction content={content} provider={link as PROVIDER_ID} notification_id={notification.notification_id} />
);
case "on_chain_expiry":
return <span>{content}</span>;
case "Expiry":
return <span>Your {content} stamp has expired. Please reverify to keep your Passport up to date.</span>;
case "OnChainExpiry":
return <span>Your on-chain Passport on {content} has expired. Update now to maintain your active status.</span>;
case "Deduplication":
case "deduplication":
return (
<span>
{/* TODO: content might need to be an object/array? */}
You have claimed the same {content} stamp in two Passports. We only count your stamp once. This duplicate is
in your wallet {content}. Learn more about deduplication{" "}
<a href="link-to-deduplication" target="_blank">
{content}{" "}
<a className="underline" href="link-to-deduplication" target="_blank">
here
</a>
.
Expand All @@ -35,8 +93,9 @@ const Content = ({ type, content }: NotificationProps) => {
}
};

const Notification: React.FC<NotificationProps> = ({ notification_id, content, type, dismissed, setShowSidebar }) => {
const messageClasses = `text-sm w-5/6 ${dismissed ? "text-foreground-4" : "text-foreground-2"}`;
const NotificationComponent: React.FC<NotificationProps> = ({ notification, setShowSidebar }) => {
const { notification_id, content, is_read, link, type } = notification;
const messageClasses = `text-sm w-5/6 ${is_read ? "text-foreground-4" : "text-foreground-2"}`;

const dismissMutation = useDismissNotification(notification_id, "read");
const deleteMutation = useDismissNotification(notification_id, "delete");
Expand All @@ -45,15 +104,17 @@ const Notification: React.FC<NotificationProps> = ({ notification_id, content, t
<>
{/* ${index > 0 && "border-t border-foreground-5"} */}
<div
className={`flex justify-start items-center p-4 relative ${type === "OnChainExpiry" && "cursor-pointer"}`}
className={`flex justify-start items-center p-4 relative ${type === "on_chain_expiry" && "cursor-pointer"}`}
onClick={() => {
if (type === "OnChainExpiry") {
if (type === "on_chain_expiry") {
setShowSidebar(true);
}
}}
>
<span className={`p-1 mr-2 text-xs rounded-full ${dismissed ? "bg-transparent" : "bg-background-5 "}`}></span>
<span className={messageClasses}>{content}</span>
<span className={`p-1 mr-2 text-xs rounded-full ${is_read ? "bg-transparent" : "bg-background-5 "}`}></span>
<span className={messageClasses}>
<Content notification={notification} />
</span>
<div className="absolute top-1 right-3 z-10">
<Popover className="relative">
<>
Expand Down Expand Up @@ -95,37 +156,11 @@ export type NotificationsProps = {
};

export const Notifications: React.FC<NotificationsProps> = ({ setShowSidebar }) => {
const { notifications: _notifications } = useNotifications();
const notifications = [
{
notification_id: "n1",
type: "Custom",
content: "Welcome to our service! Get started by visiting your dashboard.",
dismissed: false,
},
{
notification_id: "n2",
type: "Expiry",
content: "Your subscription is expiring soon. Renew now to keep using all features.",
dismissed: false,
},
{
notification_id: "n3",
type: "OnChainExpiry",
content: "Your on-chain credentials will expire in 3 days. Update your details to stay active.",
dismissed: true,
},
{
notification_id: "n4",
type: "Deduplication",
content: "We have detected duplicate entries in your data. Please review them.",
dismissed: false,
},
];
const { notifications } = useNotifications();
const hasNotifications = notifications.length > 0;

return (
<div className="w-full flex justify-end">
<div className="w-full flex justify-end z-10">
<Popover className="relative">
<>
<Popover.Button className="ml-auto p-6">
Expand All @@ -134,7 +169,7 @@ export const Notifications: React.FC<NotificationsProps> = ({ setShowSidebar })
<div
className={`${notifications.length > 10 ? "-right-5" : "-right-2"} absolute -top-3 rounded-full bg-background-5 px-1 border-4 border-background text-[10px] text-background leading-2`}
>
{notifications.filter((not) => !not.dismissed).length}
{notifications.filter((not) => !not.is_read).length}
</div>
)}
<img
Expand All @@ -153,22 +188,25 @@ export const Notifications: React.FC<NotificationsProps> = ({ setShowSidebar })
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute w-60 right-1 flex flex-col border-foreground-5 border rounded bg-gradient-to-b from-background via-background to-[#082F2A]">
<Popover.Panel className="absolute w-96 md:w-72 right-1 flex flex-col border-foreground-5 border rounded bg-gradient-to-b from-background via-background to-[#082F2A]">
<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>
{notifications
.sort((a, b) => (a.dismissed === b.dismissed ? 0 : a.dismissed ? 1 : -1))
.map((notification, index) => (
<Notification
key={notification.notification_id}
notification_id={notification.notification_id}
content={notification.content}
dismissed={notification.dismissed}
type={notification.type as Notification["type"]}
setShowSidebar={() => setShowSidebar(true)}
/>
))}
<div className="overflow-y-auto max-h-[40vh]">
{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)}
/>
))
) : (
<p className="p-2">Congrats! You have no notifications.</p>
)}
</div>
</Popover.Panel>
</Transition>
</>
Expand Down
31 changes: 18 additions & 13 deletions app/hooks/useNotifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,25 @@ import { useDatastoreConnectionContext } from "../context/datastoreConnectionCon
import { useOnChainData } from "./useOnChainData";

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { debug } from "console";

export type Notification = {
notification_id: string;
type: "Custom" | "Expiry" | "OnChainExpiry" | "Deduplication";
type: "custom" | "stamp_expiry" | "on_chain_expiry" | "deduplication";
content: string;
dismissed: boolean;
is_read: boolean;
link: string;
};

type Notifications = {
items: Notification[];
};

const fetchNotifications = async (expiredChainIds?: string[], dbAccessToken?: string) => {
if (!dbAccessToken || !expiredChainIds) return;
const res = await axios.post(
`${process.env.NEXT_PUBLIC_SCORER_ENDPOINT}/passport-admin/notifications`,
{
expired_chain_ids: expiredChainIds,
expired_chain_ids: expiredChainIds || [],
},
{
headers: {
Expand All @@ -34,7 +39,7 @@ const dismissNotification = async (
dbAccessToken?: string
) => {
if (!dbAccessToken) return;
const res = await axios.patch(
const res = await axios.post(
`${process.env.NEXT_PUBLIC_SCORER_ENDPOINT}/passport-admin/notifications/${notification_id}`,
{ dismissal_type: dismissalType },
{
Expand All @@ -54,15 +59,15 @@ export const useDismissNotification = (notification_id: string, dismissalType: "
return useMutation({
mutationFn: () => dismissNotification(notification_id, dismissalType, dbAccessToken),
onSuccess: () => {
const currentNotifications: Notification[] = queryClient.getQueryData(["notifications"]) || [];
const { items }: Notifications = queryClient.getQueryData(["notifications"]) || { items: [] };
const updatedNotifications =
dismissalType === "delete"
? currentNotifications.filter((notification) => notification.notification_id !== notification_id)
: currentNotifications.map((notification) =>
notification.notification_id === notification_id ? { ...notification, dismissed: true } : notification
? items.filter((notification) => notification.notification_id !== notification_id)
: items.map((notification) =>
notification.notification_id === notification_id ? { ...notification, is_read: true } : notification
);

queryClient.setQueryData(["notifications"], updatedNotifications);
queryClient.setQueryData(["notifications"], { items: updatedNotifications });
},
});
};
Expand All @@ -86,14 +91,14 @@ export const useNotifications = () => {
setExpiredChainIds(expiredIds);
}, [onChainData]);

const { data: notifications, error } = useQuery<Notification[], Error>({
const { data: notifications, error } = useQuery<Notifications, Error>({
queryKey: ["notifications"],
queryFn: () => fetchNotifications(expiredChainIds, dbAccessToken),
enabled: !!dbAccessToken && !!expiredChainIds?.length && dbAccessTokenStatus === "connected",
enabled: !!dbAccessToken && dbAccessTokenStatus === "connected",
});

return {
error,
notifications,
notifications: notifications?.items || [],
};
};
6 changes: 3 additions & 3 deletions app/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
:root {
--onboard-shadow-3: none;
--account-center-position-top: -14px;
--account-center-position-right: 10rem;
--account-center-position-right: 8.5rem;
--account-center-border-radius: 16px;
--onboard-action-required-btn-text-color: #fff;
--onboard-font-family-normal: var(--font-body);
Expand All @@ -43,13 +43,13 @@

@media (max-width: 1020px /* md */) {
:root {
--account-center-position-right: 24px;
--account-center-position-right: 80px;
}
}

@media (max-width: 480px /* base */) {
:root {
--account-center-position-right: 0px;
--account-center-position-right: 60px;
--onboard-modal-top: 0;
}
}
Expand Down

0 comments on commit d36589e

Please sign in to comment.