feat: notifications (#1566)

* feat: added new issue subscriber table

* dev: notification model

* feat: added CRUD operation for issue subscriber

* Revert "feat: added CRUD operation for issue subscriber"

This reverts commit b22e062576.

* feat: added CRUD operation for issue subscriber

* dev: notification models and operations

* dev: remove delete endpoint response data

* dev: notification endpoints and fix bg worker for saving notifications

* feat: added list and unsubscribe function in issue subscriber

* dev: filter by snoozed and response update for list and permissions

* dev: update issue notifications

* dev: notification  segregation

* dev: update notifications

* dev: notification filtering

* dev: add issue name in notifications

* dev: notification new endpoints

* fix: pushing local settings

* feat: notification workflow setup and made basic UI

* style: improved UX with toast alerts and other interactions

refactor: changed classnames according to new theme structure, changed all icons to material icons

* feat: showing un-read notification count

* feat: not showing 'subscribe' button on issue created by user & assigned to user

not showing 'Create by you' for view & guest of the workspace

* fix: 'read' -> 'unread' heading, my issue wrong filter

* feat: made snooze dropdown & modal

feat: switched to calendar

* fix: minor ui fixes

* feat: snooze modal date/time select

* fix: params for read/un-read notification

* style: snooze notification modal

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
This commit is contained in:
Dakshesh Jain 2023-07-19 14:44:04 +05:30 committed by GitHub
parent 98b9957753
commit 53e443d816
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 518 additions and 448 deletions

View File

@ -61,6 +61,7 @@ type Props = {
| "link" | "link"
| "delete" | "delete"
| "all" | "all"
| "subscribe"
)[]; )[];
uneditable?: boolean; uneditable?: boolean;
}; };
@ -232,7 +233,8 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
{issueDetail?.created_by !== user?.id && {issueDetail?.created_by !== user?.id &&
!issueDetail?.assignees.includes(user?.id ?? "") && !issueDetail?.assignees.includes(user?.id ?? "") &&
(fieldsToShow.includes("all") || fieldsToShow.includes("link")) && ( !router.pathname.includes("[archivedIssueId]") &&
(fieldsToShow.includes("all") || fieldsToShow.includes("subscribe")) && (
<button <button
type="button" type="button"
className="rounded-md flex items-center gap-2 border border-custom-primary-100 px-2 py-1 text-xs text-custom-primary-100 shadow-sm duration-300 focus:outline-none" className="rounded-md flex items-center gap-2 border border-custom-primary-100 px-2 py-1 text-xs text-custom-primary-100 shadow-sm duration-300 focus:outline-none"

View File

@ -1,6 +1,5 @@
import React from "react"; import React from "react";
// next
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
@ -8,11 +7,17 @@ import { useRouter } from "next/router";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// icons // icons
import { Icon } from "components/ui"; import { CustomMenu, Icon, Tooltip } from "components/ui";
// helper // helper
import { stripHTML, replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; import { stripHTML, replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
import { formatDateDistance, renderShortDateWithYearFormat } from "helpers/date-time.helper"; import {
formatDateDistance,
render12HourFormatTime,
renderLongDateFormat,
renderShortDate,
renderShortDateWithYearFormat,
} from "helpers/date-time.helper";
// type // type
import type { IUserNotification } from "types"; import type { IUserNotification } from "types";
@ -25,6 +30,33 @@ type NotificationCardProps = {
markSnoozeNotification: (notificationId: string, dateTime?: Date | undefined) => Promise<void>; markSnoozeNotification: (notificationId: string, dateTime?: Date | undefined) => Promise<void>;
}; };
const snoozeOptions = [
{
label: "1 days",
value: new Date(new Date().getTime() + 24 * 60 * 60 * 1000),
},
{
label: "3 days",
value: new Date(new Date().getTime() + 3 * 24 * 60 * 60 * 1000),
},
{
label: "5 days",
value: new Date(new Date().getTime() + 5 * 24 * 60 * 60 * 1000),
},
{
label: "1 week",
value: new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000),
},
{
label: "2 weeks",
value: new Date(new Date().getTime() + 14 * 24 * 60 * 60 * 1000),
},
{
label: "Custom",
value: null,
},
];
export const NotificationCard: React.FC<NotificationCardProps> = (props) => { export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
const { const {
notification, notification,
@ -41,23 +73,20 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
return ( return (
<div <div
key={notification.id}
onClick={() => { onClick={() => {
markNotificationReadStatus(notification.id); markNotificationReadStatus(notification.id);
router.push( router.push(
`/${workspaceSlug}/projects/${notification.project}/issues/${notification.data.issue.id}` `/${workspaceSlug}/projects/${notification.project}/issues/${notification.data.issue.id}`
); );
}} }}
className={`px-4 ${ className={`group relative py-3 px-6 cursor-pointer ${
notification.read_at === null ? "bg-custom-primary-70/10" : "hover:bg-custom-background-200" notification.read_at === null ? "bg-custom-primary-70/5" : "hover:bg-custom-background-200"
}`} }`}
> >
<div className="relative group flex items-center gap-3 py-3 cursor-pointer border-b-2 border-custom-border-200">
{notification.read_at === null && ( {notification.read_at === null && (
<span className="absolute top-1/2 -left-2 -translate-y-1/2 w-1.5 h-1.5 bg-custom-primary-100 rounded-full" /> <span className="absolute top-1/2 left-2 -translate-y-1/2 w-1.5 h-1.5 bg-custom-primary-100 rounded-full" />
)} )}
<div className="flex w-full pl-2"> <div className="flex items-center gap-4 w-full">
<div className="pl-0 p-2">
<div className="relative w-12 h-12 rounded-full"> <div className="relative w-12 h-12 rounded-full">
{notification.triggered_by_details.avatar && {notification.triggered_by_details.avatar &&
notification.triggered_by_details.avatar !== "" ? ( notification.triggered_by_details.avatar !== "" ? (
@ -70,17 +99,15 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
/> />
) : ( ) : (
<div className="w-12 h-12 bg-custom-background-100 rounded-full flex justify-center items-center"> <div className="w-12 h-12 bg-custom-background-100 rounded-full flex justify-center items-center">
<span className="text-custom-text-100 font-semibold text-lg"> <span className="text-custom-text-100 font-medium text-lg">
{notification.triggered_by_details.first_name[0].toUpperCase()} {notification.triggered_by_details.first_name[0].toUpperCase()}
</span> </span>
</div> </div>
)} )}
</div> </div>
</div> <div className="w-full space-y-2.5">
<div className="w-full flex flex-col overflow-hidden"> <div className="text-sm">
<div> <span className="font-semibold">
<p>
<span className="font-semibold text-custom-text-200">
{notification.triggered_by_details.first_name}{" "} {notification.triggered_by_details.first_name}{" "}
{notification.triggered_by_details.last_name}{" "} {notification.triggered_by_details.last_name}{" "}
</span> </span>
@ -95,7 +122,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
notification.data.issue_activity.field !== "None" notification.data.issue_activity.field !== "None"
? "to" ? "to"
: ""} : ""}
<span className="font-semibold text-custom-text-200"> <span className="font-semibold">
{" "} {" "}
{notification.data.issue_activity.field !== "None" ? ( {notification.data.issue_activity.field !== "None" ? (
notification.data.issue_activity.field !== "comment" ? ( notification.data.issue_activity.field !== "comment" ? (
@ -121,22 +148,29 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
"the issue and assigned it to you." "the issue and assigned it to you."
)} )}
</span> </span>
</p>
</div> </div>
<div className="w-full flex items-center justify-between mt-3"> <div className="w-full flex justify-between text-xs">
<p className="truncate inline max-w-lg text-custom-text-300 text-sm mr-3"> <p className="truncate inline max-w-lg text-custom-text-300 mr-3">
{notification.data.issue.identifier}-{notification.data.issue.sequence_id}{" "} {notification.data.issue.identifier}-{notification.data.issue.sequence_id}{" "}
{notification.data.issue.name} {notification.data.issue.name}
</p> </p>
<p className="text-custom-text-300 text-xs"> {notification.snoozed_till ? (
{formatDateDistance(notification.created_at)} <p className="text-custom-text-300 flex items-center gap-x-1">
<Icon iconName="schedule" />
<span>
Till {renderShortDate(notification.snoozed_till)},{" "}
{render12HourFormatTime(notification.snoozed_till)}
</span>
</p> </p>
) : (
<p className="text-custom-text-300">{formatDateDistance(notification.created_at)}</p>
)}
</div> </div>
</div> </div>
</div> </div>
<div className="absolute py-1 flex gap-x-3 right-0 top-3 opacity-0 group-hover:opacity-100 pointer-events-none group-hover:pointer-events-auto"> <div className="absolute py-1 gap-x-3 right-3 top-3 hidden group-hover:flex">
{[ {[
{ {
id: 1, id: 1,
@ -168,19 +202,8 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
}); });
}, },
}, },
{
id: 3,
name: notification.snoozed_till ? "Unsnooze Notification" : "Snooze Notification",
icon: "schedule",
onClick: () => {
if (notification.snoozed_till)
markSnoozeNotification(notification.id).then(() => {
setToastAlert({ title: "Notification un-snoozed", type: "success" });
});
else setSelectedNotificationForSnooze(notification.id);
},
},
].map((item) => ( ].map((item) => (
<Tooltip tooltipContent={item.name} position="top-left">
<button <button
type="button" type="button"
onClick={(e) => { onClick={(e) => {
@ -188,12 +211,53 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
item.onClick(); item.onClick();
}} }}
key={item.id} key={item.id}
className="text-sm flex w-full items-center gap-x-2 hover:bg-custom-background-100 p-0.5 rounded" className="text-sm flex w-full items-center gap-x-2 bg-custom-background-80 hover:bg-custom-background-100 p-0.5 rounded"
> >
<Icon iconName={item.icon} className="h-5 w-5 text-custom-text-300" /> <Icon iconName={item.icon} className="h-5 w-5 text-custom-text-300" />
</button> </button>
</Tooltip>
))} ))}
</div>
<Tooltip tooltipContent="Snooze Notification" position="top-left">
<CustomMenu
menuButtonOnClick={(e) => {
e.stopPropagation();
}}
customButton={
<button
type="button"
className="text-sm flex w-full items-center gap-x-2 bg-custom-background-80 hover:bg-custom-background-100 p-0.5 rounded"
>
<Icon iconName="schedule" className="h-5 w-5 text-custom-text-300" />
</button>
}
optionsClassName="!z-20"
>
{snoozeOptions.map((item) => (
<CustomMenu.MenuItem
key={item.label}
renderAs="button"
onClick={(e) => {
e.stopPropagation();
if (!item.value) {
setSelectedNotificationForSnooze(notification.id);
return;
}
markSnoozeNotification(notification.id, item.value).then(() => {
setToastAlert({
title: `Notification snoozed till ${renderLongDateFormat(item.value)}`,
type: "success",
});
});
}}
>
{item.label}
</CustomMenu.MenuItem>
))}
</CustomMenu>
</Tooltip>
</div> </div>
</div> </div>
); );

View File

@ -1,6 +1,5 @@
import React, { Fragment } from "react"; import React, { Fragment } from "react";
import Image from "next/image";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// hooks // hooks
@ -13,9 +12,10 @@ import useWorkspaceMembers from "hooks/use-workspace-members";
import useUserNotification from "hooks/use-user-notifications"; import useUserNotification from "hooks/use-user-notifications";
// components // components
import { Spinner, Icon } from "components/ui"; import { Icon, Loader, EmptyState } from "components/ui";
import { SnoozeNotificationModal, NotificationCard } from "components/notifications"; import { SnoozeNotificationModal, NotificationCard } from "components/notifications";
// images
import emptyNotification from "public/empty-state/notification.svg";
// helpers // helpers
import { getNumberCount } from "helpers/string.helper"; import { getNumberCount } from "helpers/string.helper";
@ -116,16 +116,15 @@ export const NotificationPopover = () => {
leaveFrom="opacity-100 translate-y-0" leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1" leaveTo="opacity-0 translate-y-1"
> >
<Popover.Panel className="absolute bg-custom-background-100 flex flex-col left-0 md:left-full ml-8 z-10 top-0 pt-5 md:w-[36rem] w-[20rem] h-[27rem] border border-custom-background-90 shadow-lg rounded"> <Popover.Panel className="absolute bg-custom-background-100 flex flex-col left-0 md:left-full ml-8 z-10 top-0 md:w-[36rem] w-[20rem] h-[27rem] border border-custom-border-300 shadow-lg rounded-xl">
<div className="flex justify-between items-center md:px-6 px-2"> <div className="flex items-center justify-between px-5 pt-5">
<h2 className="text-custom-sidebar-text-100 text-lg font-semibold mb-2"> <h2 className="text-xl font-semibold mb-2">Notifications</h2>
Notifications <div className="flex gap-x-4 justify-center items-center text-custom-text-200">
</h2>
<div className="flex gap-x-2 justify-center items-center">
<button <button
type="button" type="button"
onClick={(e) => { onClick={(e) => {
notificationsMutate(); notificationsMutate();
const target = e.target as HTMLButtonElement; const target = e.target as HTMLButtonElement;
target?.classList.add("animate-spin"); target?.classList.add("animate-spin");
setTimeout(() => { setTimeout(() => {
@ -133,7 +132,7 @@ export const NotificationPopover = () => {
}, 1000); }, 1000);
}} }}
> >
<Icon iconName="refresh" className="h-6 w-6 text-custom-text-300" /> <Icon iconName="refresh" />
</button> </button>
<button <button
type="button" type="button"
@ -143,7 +142,7 @@ export const NotificationPopover = () => {
setReadNotification((prev) => !prev); setReadNotification((prev) => !prev);
}} }}
> >
<Icon iconName="filter_list" className="h-6 w-6 text-custom-text-300" /> <Icon iconName="filter_list" />
</button> </button>
<button <button
type="button" type="button"
@ -153,7 +152,7 @@ export const NotificationPopover = () => {
setSnoozed((prev) => !prev); setSnoozed((prev) => !prev);
}} }}
> >
<Icon iconName="schedule" className="h-6 w-6 text-custom-text-300" /> <Icon iconName="schedule" />
</button> </button>
<button <button
type="button" type="button"
@ -163,18 +162,15 @@ export const NotificationPopover = () => {
setArchived((prev) => !prev); setArchived((prev) => !prev);
}} }}
> >
<Icon iconName="archive" className="h-6 w-6 text-custom-text-300" /> <Icon iconName="archive" />
</button> </button>
<button type="button" onClick={() => closePopover()}> <button type="button" onClick={() => closePopover()}>
<Icon iconName="close" className="h-6 w-6 text-custom-text-300" /> <Icon iconName="close" />
</button> </button>
</div> </div>
</div> </div>
<div className="border-b border-custom-border-300 w-full px-5 mt-5">
<div className="mt-5 flex flex-col items-center">
{snoozed || archived || readNotification ? ( {snoozed || archived || readNotification ? (
<div className="w-full mb-3">
<div className="flex flex-col flex-1 px-2 md:px-6 overflow-y-auto">
<button <button
type="button" type="button"
onClick={() => { onClick={() => {
@ -183,22 +179,19 @@ export const NotificationPopover = () => {
setReadNotification(false); setReadNotification(false);
}} }}
> >
<h4 className="text-custom-text-300 text-center flex items-center"> <h4 className="flex items-center gap-2 pb-4">
<Icon iconName="arrow_back" className="h-5 w-5 text-custom-text-300" /> <Icon iconName="arrow_back" />
<span className="ml-2 font-semibold"> <span className="ml-2 font-medium">
{snoozed {snoozed
? "Snoozed Notifications" ? "Snoozed Notifications"
: readNotification : readNotification
? "Read Notifications" ? "Unread Notifications"
: "Archived Notifications"} : "Archived Notifications"}
</span> </span>
</h4> </h4>
</button> </button>
</div>
</div>
) : ( ) : (
<div className="border-b border-custom-border-300 md:px-6 px-2 w-full"> <nav className="flex space-x-5 overflow-x-auto" aria-label="Tabs">
<nav className="-mb-px flex space-x-8" aria-label="Tabs">
{notificationTabs.map((tab) => {notificationTabs.map((tab) =>
tab.value === "created" ? ( tab.value === "created" ? (
isMember || isOwner ? ( isMember || isOwner ? (
@ -206,15 +199,20 @@ export const NotificationPopover = () => {
type="button" type="button"
key={tab.value} key={tab.value}
onClick={() => setSelectedTab(tab.value)} onClick={() => setSelectedTab(tab.value)}
className={`whitespace-nowrap border-b-2 pb-4 px-1 text-sm font-medium ${ className={`whitespace-nowrap border-b-2 pb-4 px-1 text-sm font-medium outline-none ${
tab.value === selectedTab tab.value === selectedTab
? "border-custom-primary-100 text-custom-primary-100" ? "border-custom-primary-100 text-custom-primary-100"
: "border-transparent text-custom-text-500 hover:border-custom-border-300 hover:text-custom-text-200" : "border-transparent text-custom-text-200"
}`} }`}
> >
{tab.label}
{tab.unreadCount && tab.unreadCount > 0 ? ( {tab.unreadCount && tab.unreadCount > 0 ? (
<span className="ml-3 bg-custom-background-1000/5 rounded-full text-custom-text-100 text-xs px-1.5"> <span
className={`ml-2 rounded-full text-xs px-2 py-0.5 ${
tab.value === selectedTab
? "bg-custom-primary-100 text-white"
: "bg-custom-background-80 text-custom-text-200"
}`}
>
{getNumberCount(tab.unreadCount)} {getNumberCount(tab.unreadCount)}
</span> </span>
) : null} ) : null}
@ -228,12 +226,18 @@ export const NotificationPopover = () => {
className={`whitespace-nowrap border-b-2 pb-4 px-1 text-sm font-medium ${ className={`whitespace-nowrap border-b-2 pb-4 px-1 text-sm font-medium ${
tab.value === selectedTab tab.value === selectedTab
? "border-custom-primary-100 text-custom-primary-100" ? "border-custom-primary-100 text-custom-primary-100"
: "border-transparent text-custom-text-500 hover:border-custom-border-300 hover:text-custom-text-200" : "border-transparent text-custom-text-200"
}`} }`}
> >
{tab.label} {tab.label}
{tab.unreadCount && tab.unreadCount > 0 ? ( {tab.unreadCount && tab.unreadCount > 0 ? (
<span className="ml-3 bg-custom-background-1000/5 rounded-full text-custom-text-100 text-xs px-1.5"> <span
className={`ml-2 rounded-full text-xs px-2 py-0.5 ${
tab.value === selectedTab
? "bg-custom-primary-100 text-white"
: "bg-custom-background-80 text-custom-text-200"
}`}
>
{getNumberCount(tab.unreadCount)} {getNumberCount(tab.unreadCount)}
</span> </span>
) : null} ) : null}
@ -241,16 +245,13 @@ export const NotificationPopover = () => {
) )
)} )}
</nav> </nav>
</div>
)} )}
</div> </div>
<div className="w-full flex-1 overflow-y-auto">
{notifications ? ( {notifications ? (
notifications.filter( notifications.length > 0 ? (
(notification) => notification.data.issue_activity.field !== "None" <div className="divide-y divide-custom-border-100 overflow-y-auto">
).length > 0 ? ( {notifications.map((notification) => (
notifications.map((notification) => (
<NotificationCard <NotificationCard
key={notification.id} key={notification.id}
notification={notification} notification={notification}
@ -259,30 +260,27 @@ export const NotificationPopover = () => {
setSelectedNotificationForSnooze={setSelectedNotificationForSnooze} setSelectedNotificationForSnooze={setSelectedNotificationForSnooze}
markSnoozeNotification={markSnoozeNotification} markSnoozeNotification={markSnoozeNotification}
/> />
)) ))}
</div>
) : ( ) : (
<div className="flex flex-col w-full h-full justify-center items-center"> <div className="grid h-full w-full place-items-center overflow-hidden">
<Image <EmptyState
src="/empty-state/empty-notification.svg" title="You're updated with all the notifications"
alt="Empty" description="You have read all the notifications."
width={200} image={emptyNotification}
height={200} isFullScreen={false}
layout="fixed"
/> />
<h4 className="text-custom-text-300 text-lg font-semibold">
You{"'"}re updated with all the notifications
</h4>
<p className="text-custom-text-300 text-sm mt-2">
You have read all the notifications.
</p>
</div> </div>
) )
) : ( ) : (
<div className="flex w-full h-full justify-center items-center"> <Loader className="p-5 space-y-4">
<Spinner /> <Loader.Item height="50px" />
</div> <Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
<Loader.Item height="50px" />
</Loader>
)} )}
</div>
</Popover.Panel> </Popover.Panel>
</Transition> </Transition>
</> </>

View File

@ -9,13 +9,19 @@ import { useForm, Controller } from "react-hook-form";
import { Transition, Dialog, Listbox } from "@headlessui/react"; import { Transition, Dialog, Listbox } from "@headlessui/react";
// date helper // date helper
import { getDatesAfterCurrentDate, getTimestampAfterCurrentTime } from "helpers/date-time.helper"; import { getAllTimeIn30MinutesInterval } from "helpers/date-time.helper";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// components // components
import { PrimaryButton, SecondaryButton, Icon } from "components/ui"; import {
PrimaryButton,
SecondaryButton,
Icon,
CustomDatePicker,
CustomSelect,
} from "components/ui";
// types // types
import type { IUserNotification } from "types"; import type { IUserNotification } from "types";
@ -28,14 +34,20 @@ type SnoozeModalProps = {
onSubmit: (notificationId: string, dateTime?: Date | undefined) => Promise<void>; onSubmit: (notificationId: string, dateTime?: Date | undefined) => Promise<void>;
}; };
const dates = getDatesAfterCurrentDate(); type FormValues = {
const timeStamps = getTimestampAfterCurrentTime(); time: string | null;
date: Date | null;
period: "AM" | "PM";
};
const defaultValues = { const defaultValues: FormValues = {
time: null, time: null,
date: null, date: null,
period: "AM",
}; };
const timeStamps = getAllTimeIn30MinutesInterval();
export const SnoozeNotificationModal: React.FC<SnoozeModalProps> = (props) => { export const SnoozeNotificationModal: React.FC<SnoozeModalProps> = (props) => {
const { isOpen, onClose, notification, onSuccess, onSubmit: handleSubmitSnooze } = props; const { isOpen, onClose, notification, onSuccess, onSubmit: handleSubmitSnooze } = props;
@ -49,19 +61,57 @@ export const SnoozeNotificationModal: React.FC<SnoozeModalProps> = (props) => {
reset, reset,
handleSubmit, handleSubmit,
control, control,
} = useForm<any>({ watch,
setValue,
} = useForm({
defaultValues, defaultValues,
}); });
const onSubmit = async (formData: any) => { const getTimeStamp = () => {
if (!workspaceSlug || !notification) return; const today = new Date();
const formDataDate = watch("date");
const dateTime = new Date( if (!formDataDate) return timeStamps;
`${formData.date.toLocaleDateString()} ${formData.time.toLocaleTimeString()}`
const isToday = today.toDateString() === new Date(formDataDate).toDateString();
if (!isToday) return timeStamps;
const hours = today.getHours();
const minutes = today.getMinutes();
return timeStamps.filter((optionTime) => {
let optionHours = parseInt(optionTime.value.split(":")[0]);
const optionMinutes = parseInt(optionTime.value.split(":")[1]);
const period = watch("period");
if (period === "PM" && optionHours !== 12) optionHours += 12;
if (optionHours < hours) return false;
if (optionHours === hours && optionMinutes < minutes) return false;
return true;
});
};
const onSubmit = async (formData: FormValues) => {
if (!workspaceSlug || !notification || !formData.date || !formData.time) return;
const period = formData.period;
const time = formData.time.split(":");
const hours = parseInt(
`${period === "AM" ? time[0] : parseInt(time[0]) + 12 === 24 ? "00" : parseInt(time[0]) + 12}`
); );
const minutes = parseInt(time[1]);
const dateTime = new Date(formData.date);
dateTime.setHours(hours);
dateTime.setMinutes(minutes);
await handleSubmitSnooze(notification.id, dateTime).then(() => { await handleSubmitSnooze(notification.id, dateTime).then(() => {
onClose(); handleClose();
onSuccess(); onSuccess();
setToastAlert({ setToastAlert({
title: "Notification snoozed", title: "Notification snoozed",
@ -74,7 +124,7 @@ export const SnoozeNotificationModal: React.FC<SnoozeModalProps> = (props) => {
const handleClose = () => { const handleClose = () => {
onClose(); onClose();
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
reset(); reset({ ...defaultValues });
clearTimeout(timeout); clearTimeout(timeout);
}, 500); }, 500);
}; };
@ -105,7 +155,7 @@ export const SnoozeNotificationModal: React.FC<SnoozeModalProps> = (props) => {
leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
> >
<Dialog.Panel className="relative transform rounded-lg border border-custom-border-100 bg-custom-background-80 p-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl"> <Dialog.Panel className="relative transform rounded-lg border border-custom-border-100 bg-custom-background-100 p-5 text-left shadow-xl transition-all sm:w-full sm:max-w-2xl">
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<Dialog.Title <Dialog.Title
@ -116,191 +166,104 @@ export const SnoozeNotificationModal: React.FC<SnoozeModalProps> = (props) => {
</Dialog.Title> </Dialog.Title>
<div> <div>
<button type="button"> <button type="button" onClick={handleClose}>
<Icon iconName="close" className="w-5 h-5 text-custom-text-100" /> <Icon iconName="close" className="w-5 h-5 text-custom-text-100" />
</button> </button>
</div> </div>
</div> </div>
<div className="mt-5 flex gap-3"> <div className="mt-5 flex items-center gap-3">
<div className="flex-1">
<Controller
control={control}
name="time"
rules={{ required: "Please select a time" }}
render={({ field: { value, onChange } }) => (
<Listbox value={value} onChange={onChange}>
{({ open }) => (
<>
<div className="relative mt-2">
<Listbox.Button className="relative w-full cursor-default rounded-md border border-custom-border-100 bg-custom-background-100 py-1.5 pl-3 pr-10 text-left text-custom-text-100 shadow-sm focus:outline-none sm:text-sm sm:leading-6">
<span className="flex items-center">
<span className="ml-3 block truncate">
{value
? new Date(value)?.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})
: "Select Time"}
</span>
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 ml-3 flex items-center pr-2">
<Icon
iconName="expand_more"
className="h-5 w-5 text-custom-text-100"
aria-hidden="true"
/>
</span>
</Listbox.Button>
<Transition
show={open}
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute z-10 mt-1 max-h-56 w-full overflow-auto rounded-md bg-custom-background-100 py-1 text-base shadow-lg focus:outline-none sm:text-sm">
{timeStamps.map((time, index) => (
<Listbox.Option
key={`${time.label}-${index}`}
className={({ active }) =>
`relative cursor-default select-none py-2 pl-3 pr-9 ${
active
? "bg-custom-primary-100/80 text-custom-text-100"
: "text-custom-text-700"
}`
}
value={time.value}
>
{({ selected, active }) => (
<>
<div className="flex items-center">
<span
className={`ml-3 block truncate ${
selected ? "font-semibold" : "font-normal"
}`}
>
{time.label}
</span>
</div>
{selected ? (
<span
className={`absolute inset-y-0 right-0 flex items-center pr-4 ${
active
? "text-custom-text-100"
: "text-custom-primary-100"
}`}
>
<Icon
iconName="done"
className="h-5 w-5"
aria-hidden="true"
/>
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</>
)}
</Listbox>
)}
/>
</div>
<div className="flex-1"> <div className="flex-1">
<h6 className="block text-sm font-medium text-custom-text-400 mb-2">
Pick a date
</h6>
<Controller <Controller
name="date" name="date"
control={control} control={control}
rules={{ required: "Please select a date" }} rules={{ required: "Please select a date" }}
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<Listbox value={value} onChange={onChange}> <CustomDatePicker
{({ open }) => ( placeholder="Select date"
<> value={value}
<div className="relative mt-2"> onChange={(val) => {
<Listbox.Button className="relative w-full cursor-default rounded-md border border-custom-border-100 bg-custom-background-100 py-1.5 pl-3 pr-10 text-left text-custom-text-100 shadow-sm focus:outline-none sm:text-sm sm:leading-6"> setValue("time", null);
<span className="flex items-center"> onChange(val);
<span className="ml-3 block truncate"> }}
{value className="px-3 py-2 w-full rounded-md border border-custom-border-300 bg-custom-background-100 text-custom-text-100 focus:outline-none !text-sm"
? new Date(value)?.toLocaleDateString([], { noBorder
day: "numeric", minDate={new Date()}
month: "long",
year: "numeric",
})
: "Select Date"}
</span>
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 ml-3 flex items-center pr-2">
<Icon
iconName="expand_more"
className="h-5 w-5 text-custom-text-100"
aria-hidden="true"
/> />
)}
/>
</div>
<div className="flex-1">
<h6 className="block text-sm font-medium text-custom-text-400 mb-2">
Pick a time
</h6>
<Controller
control={control}
name="time"
rules={{ required: "Please select a time" }}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
onChange={onChange}
label={
<div className="truncate">
{value ? (
<span>
{value} {watch("period").toLowerCase()}
</span> </span>
</Listbox.Button> ) : (
<span className="text-custom-text-400 text-sm">
<Transition Select a time
show={open} </span>
as={Fragment} )}
leave="transition ease-in duration-100" </div>
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute z-10 mt-1 max-h-56 w-full overflow-auto rounded-md bg-custom-background-100 py-1 text-base shadow-lg focus:outline-none sm:text-sm">
{dates.map((date, index) => (
<Listbox.Option
key={`${date.label}-${index}`}
className={({ active }) =>
`relative cursor-default select-none py-2 pl-3 pr-9 ${
active
? "bg-custom-primary-100/80 text-custom-text-100"
: "text-custom-text-700"
}`
} }
value={date.value} width="w-full"
input
> >
{({ selected, active }) => ( <div className="w-full rounded overflow-hidden h-9 mb-2 flex">
<> <div
onClick={() => {
setValue("period", "AM");
}}
className={`w-1/2 h-full cursor-pointer flex justify-center items-center text-center ${
watch("period") === "AM"
? "bg-custom-primary-100/90 text-custom-primary-0"
: "bg-custom-background-80"
}`}
>
AM
</div>
<div
onClick={() => {
setValue("period", "PM");
}}
className={`w-1/2 h-full cursor-pointer flex justify-center items-center text-center ${
watch("period") === "PM"
? "bg-custom-primary-100/90 text-custom-primary-0"
: "bg-custom-background-80"
}`}
>
PM
</div>
</div>
{getTimeStamp().length > 0 ? (
getTimeStamp().map((time, index) => (
<CustomSelect.Option key={`${time}-${index}`} value={time.value}>
<div className="flex items-center"> <div className="flex items-center">
<span <span className="ml-3 block truncate">{time.label}</span>
className={`ml-3 block truncate ${
selected ? "font-semibold" : "font-normal"
}`}
>
{date.label}
</span>
</div> </div>
</CustomSelect.Option>
{selected ? ( ))
<span ) : (
className={`absolute inset-y-0 right-0 flex items-center pr-4 ${ <p className="text-custom-text-200 text-center p-3">
active No available time for this date.
? "text-custom-text-100" </p>
: "text-custom-primary-100"
}`}
>
<Icon
iconName="done"
className="h-5 w-5"
aria-hidden="true"
/>
</span>
) : null}
</>
)} )}
</Listbox.Option> </CustomSelect>
))}
</Listbox.Options>
</Transition>
</div>
</>
)}
</Listbox>
)} )}
/> />
</div> </div>

View File

@ -15,6 +15,7 @@ type Props = {
className?: string; className?: string;
isClearable?: boolean; isClearable?: boolean;
disabled?: boolean; disabled?: boolean;
minDate?: Date;
}; };
export const CustomDatePicker: React.FC<Props> = ({ export const CustomDatePicker: React.FC<Props> = ({
@ -28,6 +29,7 @@ export const CustomDatePicker: React.FC<Props> = ({
className = "", className = "",
isClearable = true, isClearable = true,
disabled = false, disabled = false,
minDate,
}) => ( }) => (
<DatePicker <DatePicker
placeholderText={placeholder} placeholderText={placeholder}
@ -52,5 +54,6 @@ export const CustomDatePicker: React.FC<Props> = ({
dateFormat="MMM dd, yyyy" dateFormat="MMM dd, yyyy"
isClearable={isClearable} isClearable={isClearable}
disabled={disabled} disabled={disabled}
minDate={minDate}
/> />
); );

View File

@ -13,6 +13,7 @@ export type CustomMenuProps = DropdownProps & {
ellipsis?: boolean; ellipsis?: boolean;
noBorder?: boolean; noBorder?: boolean;
verticalEllipsis?: boolean; verticalEllipsis?: boolean;
menuButtonOnClick?: (...args: any) => void;
}; };
const CustomMenu = ({ const CustomMenu = ({
@ -32,17 +33,21 @@ const CustomMenu = ({
verticalEllipsis = false, verticalEllipsis = false,
verticalPosition = "bottom", verticalPosition = "bottom",
width = "auto", width = "auto",
menuButtonOnClick,
}: CustomMenuProps) => ( }: CustomMenuProps) => (
<Menu as="div" className={`${selfPositioned ? "" : "relative"} w-min text-left ${className}`}> <Menu as="div" className={`${selfPositioned ? "" : "relative"} w-min text-left ${className}`}>
{({ open }) => ( {({ open }) => (
<> <>
{customButton ? ( {customButton ? (
<Menu.Button as="div">{customButton}</Menu.Button> <Menu.Button as="button" onClick={menuButtonOnClick}>
{customButton}
</Menu.Button>
) : ( ) : (
<> <>
{ellipsis || verticalEllipsis ? ( {ellipsis || verticalEllipsis ? (
<Menu.Button <Menu.Button
type="button" type="button"
onClick={menuButtonOnClick}
disabled={disabled} disabled={disabled}
className={`relative grid place-items-center rounded p-1 text-custom-text-200 outline-none ${ className={`relative grid place-items-center rounded p-1 text-custom-text-200 outline-none ${
disabled ? "cursor-not-allowed" : "cursor-pointer hover:bg-custom-background-80" disabled ? "cursor-not-allowed" : "cursor-pointer hover:bg-custom-background-80"
@ -59,7 +64,7 @@ const CustomMenu = ({
className={`flex items-center justify-between gap-1 rounded-md px-2.5 py-1 text-xs whitespace-nowrap duration-300 ${ className={`flex items-center justify-between gap-1 rounded-md px-2.5 py-1 text-xs whitespace-nowrap duration-300 ${
open ? "bg-custom-background-90 text-custom-text-100" : "text-custom-text-200" open ? "bg-custom-background-90 text-custom-text-100" : "text-custom-text-200"
} ${ } ${
noBorder ? "" : "border border-custom-border-100 shadow-sm focus:outline-none" noBorder ? "" : "border border-custom-border-300 shadow-sm focus:outline-none"
} ${ } ${
disabled disabled
? "cursor-not-allowed text-custom-text-200" ? "cursor-not-allowed text-custom-text-200"
@ -83,7 +88,7 @@ const CustomMenu = ({
leaveTo="transform opacity-0 scale-95" leaveTo="transform opacity-0 scale-95"
> >
<Menu.Items <Menu.Items
className={`absolute z-10 overflow-y-scroll whitespace-nowrap rounded-md border border-custom-border-100 p-1 text-xs shadow-lg focus:outline-none bg-custom-background-90 ${ className={`absolute z-10 overflow-y-scroll whitespace-nowrap rounded-md border border-custom-border-300 p-1 text-xs shadow-lg focus:outline-none bg-custom-background-90 ${
position === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right" position === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"
} ${verticalPosition === "top" ? "bottom-full mb-1" : "mt-1"} ${ } ${verticalPosition === "top" ? "bottom-full mb-1" : "mt-1"} ${
maxHeight === "lg" maxHeight === "lg"

View File

@ -69,7 +69,7 @@ export const CustomSearchSelect = ({
<Combobox.Button as="div">{customButton}</Combobox.Button> <Combobox.Button as="div">{customButton}</Combobox.Button>
) : ( ) : (
<Combobox.Button <Combobox.Button
className={`flex items-center justify-between gap-1 w-full rounded-md shadow-sm border border-custom-border-100 duration-300 focus:outline-none ${ className={`flex items-center justify-between gap-1 w-full rounded-md shadow-sm border border-custom-border-300 duration-300 focus:outline-none ${
input ? "px-3 py-2 text-sm" : "px-2.5 py-1 text-xs" input ? "px-3 py-2 text-sm" : "px-2.5 py-1 text-xs"
} ${ } ${
disabled disabled
@ -94,7 +94,7 @@ export const CustomSearchSelect = ({
leaveTo="opacity-0 translate-y-1" leaveTo="opacity-0 translate-y-1"
> >
<Combobox.Options <Combobox.Options
className={`absolute z-10 min-w-[10rem] border border-custom-border-100 p-2 rounded-md bg-custom-background-90 text-xs shadow-lg focus:outline-none ${ className={`absolute z-10 min-w-[10rem] border border-custom-border-300 p-2 rounded-md bg-custom-background-90 text-xs shadow-lg focus:outline-none ${
position === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right" position === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"
} ${verticalPosition === "top" ? "bottom-full mb-1" : "mt-1"} ${ } ${verticalPosition === "top" ? "bottom-full mb-1" : "mt-1"} ${
width === "auto" ? "min-w-[8rem] whitespace-nowrap" : width width === "auto" ? "min-w-[8rem] whitespace-nowrap" : width

View File

@ -44,7 +44,7 @@ const CustomSelect = ({
<Listbox.Button as="div">{customButton}</Listbox.Button> <Listbox.Button as="div">{customButton}</Listbox.Button>
) : ( ) : (
<Listbox.Button <Listbox.Button
className={`flex items-center justify-between gap-1 w-full rounded-md border border-custom-border-100 shadow-sm duration-300 focus:outline-none ${ className={`flex items-center justify-between gap-1 w-full rounded-md border border-custom-border-300 shadow-sm duration-300 focus:outline-none ${
input ? "px-3 py-2 text-sm" : "px-2.5 py-1 text-xs" input ? "px-3 py-2 text-sm" : "px-2.5 py-1 text-xs"
} ${ } ${
disabled disabled
@ -68,7 +68,7 @@ const CustomSelect = ({
leaveTo="transform opacity-0 scale-95" leaveTo="transform opacity-0 scale-95"
> >
<Listbox.Options <Listbox.Options
className={`absolute z-10 border border-custom-border-100 mt-1 origin-top-right overflow-y-auto rounded-md bg-custom-background-90 text-xs shadow-lg focus:outline-none ${ className={`absolute z-10 border border-custom-border-300 mt-1 origin-top-right overflow-y-auto rounded-md bg-custom-background-90 text-xs shadow-lg focus:outline-none ${
position === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right" position === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"
} ${verticalPosition === "top" ? "bottom-full mb-1" : "mt-1"} ${ } ${verticalPosition === "top" ? "bottom-full mb-1" : "mt-1"} ${
maxHeight === "lg" maxHeight === "lg"

View File

@ -9,7 +9,7 @@ type Props = {
title: string; title: string;
description: React.ReactNode | string; description: React.ReactNode | string;
image: any; image: any;
buttonText: string; buttonText?: string;
buttonIcon?: any; buttonIcon?: any;
onClick?: () => void; onClick?: () => void;
isFullScreen?: boolean; isFullScreen?: boolean;
@ -33,10 +33,12 @@ export const EmptyState: React.FC<Props> = ({
<Image src={image} className="w-52 sm:w-60" alt={buttonText} /> <Image src={image} className="w-52 sm:w-60" alt={buttonText} />
<h6 className="text-xl font-semibold mt-6 sm:mt-8 mb-3">{title}</h6> <h6 className="text-xl font-semibold mt-6 sm:mt-8 mb-3">{title}</h6>
<p className="text-custom-text-300 mb-7 sm:mb-8">{description}</p> <p className="text-custom-text-300 mb-7 sm:mb-8">{description}</p>
{buttonText && (
<PrimaryButton className="flex items-center gap-1.5" onClick={onClick}> <PrimaryButton className="flex items-center gap-1.5" onClick={onClick}>
{buttonIcon} {buttonIcon}
{buttonText} {buttonText}
</PrimaryButton> </PrimaryButton>
)}
</div> </div>
</div> </div>
); );

View File

@ -36,7 +36,7 @@ export const IssuesPieChart: React.FC<Props> = ({ groupedIssues }) => (
activeInnerRadiusOffset={5} activeInnerRadiusOffset={5}
colors={(datum) => datum.data.color} colors={(datum) => datum.data.color}
tooltip={(datum) => ( tooltip={(datum) => (
<div className="flex items-center gap-2 rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs"> <div className="flex items-center gap-2 rounded-md border border-custom-border-200 bg-custom-background-90 p-2 text-xs">
<span className="text-custom-text-200 capitalize">{datum.datum.label} issues:</span>{" "} <span className="text-custom-text-200 capitalize">{datum.datum.label} issues:</span>{" "}
{datum.datum.value} {datum.datum.value}
</div> </div>
@ -59,7 +59,7 @@ export const IssuesPieChart: React.FC<Props> = ({ groupedIssues }) => (
className="h-2 w-2" className="h-2 w-2"
style={{ backgroundColor: STATE_GROUP_COLORS[cell.state_group] }} style={{ backgroundColor: STATE_GROUP_COLORS[cell.state_group] }}
/> />
<div className="capitalize text-custom-text-200 text-xs"> <div className="capitalize text-custom-text-200 text-xs whitespace-nowrap">
{cell.state_group}- {cell.state_count} {cell.state_group}- {cell.state_count}
</div> </div>
</div> </div>

View File

@ -243,7 +243,7 @@ export const isDateGreaterThanToday = (dateStr: string) => {
return date > today; return date > today;
}; };
export const renderLongDateFormat = (dateString: string) => { export const renderLongDateFormat = (dateString: string | Date) => {
const date = new Date(dateString); const date = new Date(dateString);
const day = date.getDate(); const day = date.getDate();
const year = date.getFullYear(); const year = date.getFullYear();
@ -333,3 +333,44 @@ export const getDatesAfterCurrentDate = (): Array<{
export const checkIfStringIsDate = (date: string): boolean => export const checkIfStringIsDate = (date: string): boolean =>
new Date(date).toString() !== "Invalid Date"; new Date(date).toString() !== "Invalid Date";
// return an array of dates starting from 12:00 to 23:30 with 30 minutes interval as dates
export const getDatesWith30MinutesInterval = (): Array<Date> => {
const dates = [];
const current = new Date();
for (let i = 0; i < 24; i++) {
const newDate = new Date(current.getTime() + i * 60 * 60 * 1000);
dates.push(newDate);
}
return dates;
};
export const getAllTimeIn30MinutesInterval = (): Array<{
label: string;
value: string;
}> => [
{ label: "12:00", value: "12:00" },
{ label: "12:30", value: "12:30" },
{ label: "01:00", value: "01:00" },
{ label: "01:30", value: "01:30" },
{ label: "02:00", value: "02:00" },
{ label: "02:30", value: "02:30" },
{ label: "03:00", value: "03:00" },
{ label: "03:30", value: "03:30" },
{ label: "04:00", value: "04:00" },
{ label: "04:30", value: "04:30" },
{ label: "05:00", value: "05:00" },
{ label: "05:30", value: "05:30" },
{ label: "06:00", value: "06:00" },
{ label: "06:30", value: "06:30" },
{ label: "07:00", value: "07:00" },
{ label: "07:30", value: "07:30" },
{ label: "08:00", value: "08:00" },
{ label: "08:30", value: "08:30" },
{ label: "09:00", value: "09:00" },
{ label: "09:30", value: "09:30" },
{ label: "10:00", value: "10:00" },
{ label: "10:30", value: "10:30" },
{ label: "11:00", value: "11:00" },
{ label: "11:30", value: "11:30" },
];

View File

@ -45,8 +45,6 @@ const useUserIssueNotificationSubscription = (
}, [workspaceSlug, projectId, issueId, mutate]); }, [workspaceSlug, projectId, issueId, mutate]);
const handleSubscribe = useCallback(() => { const handleSubscribe = useCallback(() => {
console.log(workspaceSlug, projectId, issueId, user);
if (!workspaceSlug || !projectId || !issueId || !user) return; if (!workspaceSlug || !projectId || !issueId || !user) return;
userNotificationServices userNotificationServices

View File

@ -26,23 +26,17 @@ const useUserNotification = () => {
); );
const [selectedTab, setSelectedTab] = useState<NotificationType>("assigned"); const [selectedTab, setSelectedTab] = useState<NotificationType>("assigned");
const params = {
type: snoozed || archived || readNotification ? undefined : selectedTab,
snoozed,
archived,
read: !readNotification ? undefined : false,
};
const { data: notifications, mutate: notificationsMutate } = useSWR( const { data: notifications, mutate: notificationsMutate } = useSWR(
workspaceSlug ? USER_WORKSPACE_NOTIFICATIONS(workspaceSlug.toString(), params) : null,
workspaceSlug workspaceSlug
? USER_WORKSPACE_NOTIFICATIONS(workspaceSlug.toString(), { ? () => userNotificationServices.getUserNotifications(workspaceSlug.toString(), params)
type: selectedTab,
snoozed,
archived,
read: selectedTab === null ? !readNotification : undefined,
})
: null,
workspaceSlug
? () =>
userNotificationServices.getUserNotifications(workspaceSlug.toString(), {
type: selectedTab,
snoozed,
archived,
read: selectedTab === null ? !readNotification : undefined,
})
: null : null
); );

View File

@ -69,13 +69,13 @@ const WorkspacePage: NextPage = () => {
return ( return (
<WorkspaceAuthorizationLayout <WorkspaceAuthorizationLayout
left={ left={
<div className="flex items-center gap-2 px-3"> <div className="flex items-center gap-2 pl-3">
<Icon iconName="grid_view" /> <Icon iconName="grid_view" />
Dashboard Dashboard
</div> </div>
} }
right={ right={
<div className="flex items-center gap-3"> <div className="flex items-center gap-3 px-3">
<button <button
onClick={() => setIsProductUpdatesModalOpen(true)} onClick={() => setIsProductUpdatesModalOpen(true)}
className="flex items-center gap-1.5 bg-custom-background-80 text-xs font-medium py-1.5 px-3 rounded" className="flex items-center gap-1.5 bg-custom-background-80 text-xs font-medium py-1.5 px-3 rounded"

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB