style: improved UX with toast alerts and other interactions

refactor: changed classnames according to new theme structure, changed all icons to material icons
This commit is contained in:
Dakshesh Jain 2023-07-14 19:23:56 +05:30
parent be39b097ab
commit 7045f80ec6
9 changed files with 3176 additions and 118 deletions

View File

@ -34,7 +34,7 @@ import {
SidebarEstimateSelect,
} from "components/issues";
// ui
import { Input, Spinner, CustomDatePicker } from "components/ui";
import { Input, Spinner, CustomDatePicker, Icon } from "components/ui";
// icons
import {
TagIcon,
@ -293,16 +293,19 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
{issueDetail?.project_detail?.identifier}-{issueDetail?.sequence_id}
</h4>
<div className="flex flex-wrap items-center gap-2">
<button
type="button"
className="rounded-md border border-custom-primary-100 px-4 py-1 text-xs text-custom-primary-100 shadow-sm duration-300 focus:outline-none"
onClick={() => {
if (subscribed) handleUnsubscribe();
else handleSubscribe();
}}
>
{loading ? "Loading..." : subscribed ? "Unsubscribe" : "Subscribe"}
</button>
{(fieldsToShow.includes("all") || fieldsToShow.includes("link")) && (
<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"
onClick={() => {
if (subscribed) handleUnsubscribe();
else handleSubscribe();
}}
>
<Icon iconName="notifications" />
{loading ? "Loading..." : subscribed ? "Unsubscribe" : "Subscribe"}
</button>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("link")) && (
<button
type="button"

View File

@ -5,9 +5,10 @@ import Image from "next/image";
import { useRouter } from "next/router";
// hooks
import useUserNotification from "hooks/use-user-notifications";
import useToast from "hooks/use-toast";
// icons
import { ArchiveIcon, ClockIcon, SingleCommentCard } from "components/icons";
import { Icon } from "components/ui";
// helper
import { stripHTML, replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
@ -18,9 +19,10 @@ import type { IUserNotification } from "types";
type NotificationCardProps = {
notification: IUserNotification;
markNotificationReadStatus: (notificationId: string) => void;
markNotificationArchivedStatus: (notificationId: string) => void;
markNotificationReadStatus: (notificationId: string) => Promise<void>;
markNotificationArchivedStatus: (notificationId: string) => Promise<void>;
setSelectedNotificationForSnooze: (notificationId: string) => void;
markSnoozeNotification: (notificationId: string, dateTime?: Date | undefined) => Promise<void>;
};
export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
@ -29,11 +31,16 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
markNotificationReadStatus,
markNotificationArchivedStatus,
setSelectedNotificationForSnooze,
markSnoozeNotification,
} = props;
const router = useRouter();
const { workspaceSlug } = router.query;
const { setToastAlert } = useToast();
if (notification.data.issue_activity.field === "None") return null;
return (
<div
key={notification.id}
@ -79,7 +86,8 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
{notification.triggered_by_details.first_name}{" "}
{notification.triggered_by_details.last_name}{" "}
</span>
{notification.data.issue_activity.verb}{" "}
{notification.data.issue_activity.field !== "comment" &&
notification.data.issue_activity.verb}{" "}
{notification.data.issue_activity.field !== "comment"
? replaceUnderscoreIfSnakeCase(notification.data.issue_activity.field)
: "commented"}{" "}
@ -89,13 +97,17 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
{notification.data.issue_activity.field !== "comment" ? (
notification.data.issue_activity.field === "target_date" ? (
renderShortDateWithYearFormat(notification.data.issue_activity.new_value)
) : notification.data.issue_activity.field === "attachment" ? (
"the issue"
) : stripHTML(notification.data.issue_activity.new_value).length > 55 ? (
stripHTML(notification.data.issue_activity.new_value).slice(0, 50) + "..."
) : (
stripHTML(notification.data.issue_activity.new_value)
)
) : (
<span>
{`"`}
{notification.data.issue_activity.new_value.length > 50
{notification.data.issue_activity.new_value.length > 55
? notification?.data?.issue_activity?.issue_comment?.slice(0, 50) + "..."
: notification.data.issue_activity.issue_comment}
{`"`}
@ -122,25 +134,43 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
{
id: 1,
name: notification.read_at ? "Mark as Unread" : "Mark as Read",
icon: SingleCommentCard,
icon: "chat_bubble",
onClick: () => {
markNotificationReadStatus(notification.id);
markNotificationReadStatus(notification.id).then(() => {
setToastAlert({
title: notification.read_at
? "Notification marked as unread"
: "Notification marked as read",
type: "success",
});
});
},
},
{
id: 2,
name: notification.archived_at ? "Unarchive Notification" : "Archive Notification",
icon: ArchiveIcon,
icon: "archive",
onClick: () => {
markNotificationArchivedStatus(notification.id);
markNotificationArchivedStatus(notification.id).then(() => {
setToastAlert({
title: notification.archived_at
? "Notification un-archived"
: "Notification archived",
type: "success",
});
});
},
},
{
id: 3,
name: notification.snoozed_till ? "Unsnooze Notification" : "Snooze Notification",
icon: ClockIcon,
icon: "schedule",
onClick: () => {
setSelectedNotificationForSnooze(notification.id);
if (notification.snoozed_till)
markSnoozeNotification(notification.id).then(() => {
setToastAlert({ title: "Notification un-snoozed", type: "success" });
});
else setSelectedNotificationForSnooze(notification.id);
},
},
].map((item) => (
@ -153,7 +183,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
key={item.id}
className="text-sm flex w-full items-center gap-x-2 hover:bg-custom-background-100 p-0.5 rounded"
>
<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>
))}
</div>

View File

@ -4,23 +4,14 @@ import Image from "next/image";
// hooks
import useTheme from "hooks/use-theme";
// icons
import {
XMarkIcon,
ArchiveIcon,
ClockIcon,
SortIcon,
BellNotificationIcon,
} from "components/icons";
import { Popover, Transition, Menu } from "@headlessui/react";
import { ArrowLeftIcon } from "@heroicons/react/20/solid";
// hooks
import useUserNotification from "hooks/use-user-notifications";
// components
import { Spinner } from "components/ui";
import { Spinner, Icon } from "components/ui";
import { SnoozeNotificationModal, NotificationCard } from "components/notifications";
// type
@ -60,6 +51,7 @@ export const NotificationPopover = () => {
notificationsMutate,
markNotificationArchivedStatus,
markNotificationReadStatus,
markSnoozeNotification,
} = useUserNotification();
// theme context
@ -70,7 +62,12 @@ export const NotificationPopover = () => {
<SnoozeNotificationModal
isOpen={selectedNotificationForSnooze !== null}
onClose={() => setSelectedNotificationForSnooze(null)}
notificationId={selectedNotificationForSnooze}
onSubmit={markSnoozeNotification}
notification={
notifications?.find(
(notification) => notification.id === selectedNotificationForSnooze
) || null
}
onSuccess={() => {
notificationsMutate();
setSelectedNotificationForSnooze(null);
@ -80,26 +77,13 @@ export const NotificationPopover = () => {
{({ open: isActive, close: closePopover }) => (
<>
<Popover.Button
className={`${
className={`group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${
isActive
? "bg-custom-sidebar-background-90 text-custom-sidebar-text-100"
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-90"
} group flex w-full items-center gap-3 rounded-md p-2 text-sm font-medium outline-none ${
sidebarCollapse ? "justify-center" : ""
}`}
? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
} ${sidebarCollapse ? "justify-center" : ""}`}
>
<span className="grid h-5 w-5 flex-shrink-0 place-items-center">
<BellNotificationIcon
color={
isActive
? "rgb(var(--color-sidebar-text-100))"
: "rgb(var(--color-sidebar-text-200))"
}
aria-hidden="true"
height="20"
width="20"
/>
</span>
<Icon iconName="notifications" />
{sidebarCollapse ? null : <span>Notifications</span>}
</Popover.Button>
<Transition
@ -117,6 +101,19 @@ export const NotificationPopover = () => {
Notifications
</h2>
<div className="flex gap-x-2 justify-center items-center">
<button
type="button"
onClick={(e) => {
notificationsMutate();
const target = e.target as HTMLButtonElement;
target?.classList.add("animate-spin");
setTimeout(() => {
target?.classList.remove("animate-spin");
}, 1000);
}}
>
<Icon iconName="refresh" className="h-6 w-6 text-custom-text-300" />
</button>
<button
type="button"
onClick={() => {
@ -125,7 +122,7 @@ export const NotificationPopover = () => {
setReadNotification((prev) => !prev);
}}
>
<SortIcon className="h-6 w-6 text-custom-text-300" />
<Icon iconName="filter_list" className="h-6 w-6 text-custom-text-300" />
</button>
<button
type="button"
@ -135,7 +132,7 @@ export const NotificationPopover = () => {
setSnoozed((prev) => !prev);
}}
>
<ClockIcon className="h-6 w-6 text-custom-text-300" />
<Icon iconName="schedule" className="h-6 w-6 text-custom-text-300" />
</button>
<button
type="button"
@ -145,10 +142,10 @@ export const NotificationPopover = () => {
setArchived((prev) => !prev);
}}
>
<ArchiveIcon className="h-6 w-6 text-custom-text-300" />
<Icon iconName="archive" className="h-6 w-6 text-custom-text-300" />
</button>
<button type="button" onClick={() => closePopover()}>
<XMarkIcon className="h-6 w-6 text-custom-text-300" />
<Icon iconName="close" className="h-6 w-6 text-custom-text-300" />
</button>
</div>
</div>
@ -166,7 +163,7 @@ export const NotificationPopover = () => {
}}
>
<h4 className="text-custom-text-300 text-center flex items-center">
<ArrowLeftIcon className="h-5 w-5 text-custom-text-300" />
<Icon iconName="arrow_back" className="h-5 w-5 text-custom-text-300" />
<span className="ml-2 font-semibold">
{snoozed
? "Snoozed Notifications"
@ -202,7 +199,9 @@ export const NotificationPopover = () => {
<div className="w-full flex-1 overflow-y-auto">
{notifications ? (
notifications.length > 0 ? (
notifications.filter(
(notification) => notification.data.issue_activity.field !== "None"
).length > 0 ? (
notifications.map((notification) => (
<NotificationCard
key={notification.id}
@ -210,6 +209,7 @@ export const NotificationPopover = () => {
markNotificationArchivedStatus={markNotificationArchivedStatus}
markNotificationReadStatus={markNotificationReadStatus}
setSelectedNotificationForSnooze={setSelectedNotificationForSnooze}
markSnoozeNotification={markSnoozeNotification}
/>
))
) : (

View File

@ -7,35 +7,37 @@ import { useRouter } from "next/router";
import { useForm, Controller } from "react-hook-form";
import { Transition, Dialog, Listbox } from "@headlessui/react";
import { ChevronDownIcon, CheckIcon } from "@heroicons/react/20/solid";
// date helper
import { getDatesAfterCurrentDate, getTimestampAfterCurrentTime } from "helpers/date-time.helper";
// services
import userNotificationServices from "services/notifications.service";
// hooks
import useToast from "hooks/use-toast";
// components
import { PrimaryButton, SecondaryButton } from "components/ui";
import { PrimaryButton, SecondaryButton, Icon } from "components/ui";
// icons
import { XMarkIcon } from "components/icons";
// types
import type { IUserNotification } from "types";
type SnoozeModalProps = {
isOpen: boolean;
onClose: () => void;
onSuccess: () => void;
notificationId: string | null;
notification: IUserNotification | null;
onSubmit: (notificationId: string, dateTime?: Date | undefined) => Promise<void>;
};
const dates = getDatesAfterCurrentDate();
const timeStamps = getTimestampAfterCurrentTime();
const defaultValues = {
time: null,
date: null,
};
export const SnoozeNotificationModal: React.FC<SnoozeModalProps> = (props) => {
const { isOpen, onClose, notificationId, onSuccess } = props;
const { isOpen, onClose, notification, onSuccess, onSubmit: handleSubmitSnooze } = props;
const router = useRouter();
const { workspaceSlug } = router.query;
@ -47,28 +49,26 @@ export const SnoozeNotificationModal: React.FC<SnoozeModalProps> = (props) => {
reset,
handleSubmit,
control,
} = useForm<any>();
} = useForm<any>({
defaultValues,
});
const onSubmit = async (formData: any) => {
if (!workspaceSlug || !notificationId) return;
if (!workspaceSlug || !notification) return;
const dateTime = new Date(
`${formData.date.toLocaleDateString()} ${formData.time.toLocaleTimeString()}`
);
await userNotificationServices
.patchUserNotification(workspaceSlug.toString(), notificationId, {
snoozed_till: dateTime,
})
.then(() => {
onClose();
onSuccess();
setToastAlert({
title: "Notification snoozed",
message: "Notification snoozed successfully",
type: "success",
});
await handleSubmitSnooze(notification.id, dateTime).then(() => {
onClose();
onSuccess();
setToastAlert({
title: "Notification snoozed",
message: "Notification snoozed successfully",
type: "success",
});
});
};
const handleClose = () => {
@ -91,7 +91,7 @@ export const SnoozeNotificationModal: React.FC<SnoozeModalProps> = (props) => {
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-brand-backdrop bg-opacity-50 transition-opacity" />
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto">
@ -117,7 +117,7 @@ export const SnoozeNotificationModal: React.FC<SnoozeModalProps> = (props) => {
<div>
<button type="button">
<XMarkIcon className="w-5 h-5 text-custom-text-100" />
<Icon iconName="close" className="w-5 h-5 text-custom-text-100" />
</button>
</div>
</div>
@ -133,18 +133,21 @@ export const SnoozeNotificationModal: React.FC<SnoozeModalProps> = (props) => {
{({ open }) => (
<>
<div className="relative mt-2">
<Listbox.Button className="relative w-full cursor-default rounded-md bg-white py-1.5 pl-3 pr-10 text-left text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 sm:text-sm sm:leading-6">
<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?.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
}) || "Select Time"}
{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">
<ChevronDownIcon
className="h-5 w-5 text-gray-400"
<Icon
iconName="expand_more"
className="h-5 w-5 text-custom-text-100"
aria-hidden="true"
/>
</span>
@ -157,14 +160,14 @@ export const SnoozeNotificationModal: React.FC<SnoozeModalProps> = (props) => {
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute z-10 mt-1 max-h-56 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
<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 text-custom-text-100"
? "bg-custom-primary-100/80 text-custom-text-100"
: "text-custom-text-700"
}`
}
@ -190,7 +193,8 @@ export const SnoozeNotificationModal: React.FC<SnoozeModalProps> = (props) => {
: "text-custom-primary-100"
}`}
>
<CheckIcon
<Icon
iconName="done"
className="h-5 w-5"
aria-hidden="true"
/>
@ -219,19 +223,22 @@ export const SnoozeNotificationModal: React.FC<SnoozeModalProps> = (props) => {
{({ open }) => (
<>
<div className="relative mt-2">
<Listbox.Button className="relative w-full cursor-default rounded-md bg-white py-1.5 pl-3 pr-10 text-left text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 sm:text-sm sm:leading-6">
<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?.toLocaleDateString([], {
day: "numeric",
month: "long",
year: "numeric",
}) || "Select Date"}
{value
? new Date(value)?.toLocaleDateString([], {
day: "numeric",
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">
<ChevronDownIcon
className="h-5 w-5 text-gray-400"
<Icon
iconName="expand_more"
className="h-5 w-5 text-custom-text-100"
aria-hidden="true"
/>
</span>
@ -244,14 +251,14 @@ export const SnoozeNotificationModal: React.FC<SnoozeModalProps> = (props) => {
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute z-10 mt-1 max-h-56 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
<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 text-custom-text-100"
? "bg-custom-primary-100/80 text-custom-text-100"
: "text-custom-text-700"
}`
}
@ -277,7 +284,8 @@ export const SnoozeNotificationModal: React.FC<SnoozeModalProps> = (props) => {
: "text-custom-primary-100"
}`}
>
<CheckIcon
<Icon
iconName="done"
className="h-5 w-5"
aria-hidden="true"
/>
@ -302,7 +310,7 @@ export const SnoozeNotificationModal: React.FC<SnoozeModalProps> = (props) => {
<div className="w-full flex items-center gap-2 justify-end">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<PrimaryButton type="submit" loading={isSubmitting}>
Submit
{isSubmitting ? "Submitting..." : "Submit"}
</PrimaryButton>
</div>
</div>

2991
apps/app/components/ui/icon-name-type.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -5,34 +5,32 @@ import { useRouter } from "next/router";
// hooks
import useTheme from "hooks/use-theme";
// icons
import { ChartBarIcon } from "@heroicons/react/24/outline";
import { GridViewIcon, AssignmentClipboardIcon, TickMarkIcon } from "components/icons";
import { NotificationPopover } from "components/notifications";
const workspaceLinks = (workspaceSlug: string) => [
{
icon: GridViewIcon,
icon: "grid_view",
name: "Dashboard",
href: `/${workspaceSlug}`,
},
{
icon: ChartBarIcon,
icon: "bar_chart",
name: "Analytics",
href: `/${workspaceSlug}/analytics`,
},
{
icon: AssignmentClipboardIcon,
icon: "work",
name: "Projects",
href: `/${workspaceSlug}/projects`,
},
{
icon: TickMarkIcon,
icon: "task_alt",
name: "My Issues",
href: `/${workspaceSlug}/me/my-issues`,
},
];
// components
import { Icon, Tooltip } from "components/ui";

View File

@ -310,7 +310,7 @@ export const getDatesAfterCurrentDate = (): Array<{
const current = new Date();
const date = [];
for (let i = 0; i < 7; i++) {
const newDate = new Date(current.getTime() + i * 24 * 60 * 60 * 1000);
const newDate = new Date(Math.round(current.getTime() / (30 * 60 * 1000)) * 30 * 60 * 1000);
date.push({
label: newDate.toLocaleDateString([], {
day: "numeric",

View File

@ -1,4 +1,4 @@
import { useCallback, useState } from "react";
import { useState } from "react";
import { useRouter } from "next/router";
@ -32,7 +32,7 @@ const useUserNotification = () => {
type: selectedTab,
snoozed,
archived,
read: readNotification,
read: selectedTab === null ? !readNotification : undefined,
})
: null,
workspaceSlug
@ -41,7 +41,7 @@ const useUserNotification = () => {
type: selectedTab,
snoozed,
archived,
read: readNotification,
read: selectedTab === null ? !readNotification : undefined,
})
: null
);
@ -121,11 +121,39 @@ const useUserNotification = () => {
}
};
const markSnoozeNotification = async (notificationId: string, dateTime?: Date) => {
if (!workspaceSlug) return;
const isSnoozed =
notifications?.find((notification) => notification.id === notificationId)?.snoozed_till !==
null;
if (isSnoozed) {
await userNotificationServices
.patchUserNotification(workspaceSlug.toString(), notificationId, {
snoozed_till: null,
})
.then(() => {
notificationsMutate();
});
} else
await userNotificationServices
.patchUserNotification(workspaceSlug.toString(), notificationId, {
snoozed_till: dateTime,
})
.then(() => {
notificationsMutate(
(prevData) => prevData?.filter((prev) => prev.id !== notificationId) || []
);
});
};
return {
notifications,
notificationsMutate,
markNotificationReadStatus,
markNotificationArchivedStatus,
markSnoozeNotification,
snoozed,
setSnoozed,
archived,

View File

@ -46,7 +46,7 @@ export interface IIssueLite {
state_group: string;
}
export type NotificationType = "created" | "assigned" | "watching";
export type NotificationType = "created" | "assigned" | "watching" | null;
export interface INotificationParams {
snoozed?: boolean;