plane/apps/app/components/notifications/select-snooze-till-modal.tsx
Dakshesh Jain 16a7bd3bda
feat: user issue notifications (#1523)
* 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

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2023-07-18 12:07:55 +05:30

326 lines
15 KiB
TypeScript

import React, { Fragment } from "react";
// next
import { useRouter } from "next/router";
// react hook form
import { useForm, Controller } from "react-hook-form";
import { Transition, Dialog, Listbox } from "@headlessui/react";
// date helper
import { getDatesAfterCurrentDate, getTimestampAfterCurrentTime } from "helpers/date-time.helper";
// hooks
import useToast from "hooks/use-toast";
// components
import { PrimaryButton, SecondaryButton, Icon } from "components/ui";
// types
import type { IUserNotification } from "types";
type SnoozeModalProps = {
isOpen: boolean;
onClose: () => void;
onSuccess: () => void;
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, notification, onSuccess, onSubmit: handleSubmitSnooze } = props;
const router = useRouter();
const { workspaceSlug } = router.query;
const { setToastAlert } = useToast();
const {
formState: { isSubmitting },
reset,
handleSubmit,
control,
} = useForm<any>({
defaultValues,
});
const onSubmit = async (formData: any) => {
if (!workspaceSlug || !notification) return;
const dateTime = new Date(
`${formData.date.toLocaleDateString()} ${formData.time.toLocaleTimeString()}`
);
await handleSubmitSnooze(notification.id, dateTime).then(() => {
onClose();
onSuccess();
setToastAlert({
title: "Notification snoozed",
message: "Notification snoozed successfully",
type: "success",
});
});
};
const handleClose = () => {
onClose();
const timeout = setTimeout(() => {
reset();
clearTimeout(timeout);
}, 500);
};
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<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">
<div className="flex items-center justify-center min-h-full p-4 text-center">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
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">
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex justify-between items-center">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-custom-text-100"
>
Customize Snooze Time
</Dialog.Title>
<div>
<button type="button">
<Icon iconName="close" className="w-5 h-5 text-custom-text-100" />
</button>
</div>
</div>
<div className="mt-5 flex 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">
<Controller
name="date"
control={control}
rules={{ required: "Please select a date" }}
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)?.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">
<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">
{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}
>
{({ selected, active }) => (
<>
<div className="flex items-center">
<span
className={`ml-3 block truncate ${
selected ? "font-semibold" : "font-normal"
}`}
>
{date.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>
<div className="mt-5 flex items-center justify-between gap-2">
<div className="w-full flex items-center gap-2 justify-end">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<PrimaryButton type="submit" loading={isSubmitting}>
{isSubmitting ? "Submitting..." : "Submit"}
</PrimaryButton>
</div>
</div>
</form>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
};