Skip to content

Commit

Permalink
chore: rework notification system (#449)
Browse files Browse the repository at this point in the history
**Describe the pull request**
This pull request is dedicated to a comprehensive overhaul of the
current notification system to leverage React's Portal system and to
better handle Apollo errors. By integrating React Portal, we can benefit
from its capability to render children into a DOM node that exists
outside the root app and prevent rerender on show and hide notification.
Additionally, the improved handling of Apollo errors will ensure more
robust error detection and management within the notification system.
Through this significant rework, we aim to improve the reliability,
performance, and user experience of the notification system.

**Checklist**

- [x] I have made the modifications or added tests related to my PR
- [x] I have run the tests and linters locally and they pass
- [x] I have added/updated the documentation for my RP

Signed-off-by: 42Atomys <[email protected]>
  • Loading branch information
42atomys authored May 26, 2023
1 parent a90a6eb commit d174917
Show file tree
Hide file tree
Showing 15 changed files with 306 additions and 218 deletions.
12 changes: 8 additions & 4 deletions internal/api/api.resolvers.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 4 additions & 11 deletions web/ui/src/components/Form/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import {
DetailedHTMLProps,
Dispatch,
InputHTMLAttributes,
KeyboardEventHandler,
} from 'react';
import { DetailedHTMLProps, Dispatch, InputHTMLAttributes } from 'react';
import { Maybe } from 'types/globals';

interface InputProps<S> {
Expand All @@ -24,10 +19,6 @@ type InputTextType =
| 'url'
| 'search';

interface KeyDownEvent {
onKeyDown?: KeyboardEventHandler<HTMLInputElement>;
}

interface SelectInputProps<S> extends InputProps<S>, KeyDownEvent {
name?: string;
selectedValue: S;
Expand All @@ -45,7 +36,9 @@ interface FileInputProps<S>
keyof InputProps<S> | 'type' | 'value' | 'id'
> {}

interface TextInputProps<S> extends InputProps<S>, KeyDownEvent {
interface TextInputProps<S>
extends InputProps<S>,
Omit<React.HTMLProps<HTMLInputElement>, 'onChange'> {
type: InputTextType = 'text';
// debounce is in milliseconds (ms) if you want to use it to avoid update the
// state on every key press
Expand Down
151 changes: 78 additions & 73 deletions web/ui/src/components/Notification/Notification.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { useNotification } from '@ctx/notifications';
import classNames from 'classnames';
import { useCallback, useEffect, useState } from 'react';
import useNotification from './hooks';
import { AnimatePresence, motion } from 'framer-motion';
import { startTransition, useCallback, useEffect, useState } from 'react';
import { NotificationComponent } from './types';

const animationDuration = 400;

/**
* Notification is the component that is used to render a notification on the
* UI. The notification is managed by the NotificationProvider and
Expand All @@ -15,28 +18,28 @@ export const Notification: NotificationComponent = (notification) => {
// State used to know if the notification is visible or not by the user.
const [visible, setVisible] = useState(false);
const { removeNotification } = useNotification();
const { type = 'default', title, message, children } = notification;
const { type = 'default', title, message, duration, children } = notification;

// hide the notification after the user click on the close button
const hideNotification = useCallback(() => {
setVisible(false);
setTimeout(() => {
removeNotification(notification);
}, 400);
startTransition(() => {
setVisible(false);
setTimeout(() => {
removeNotification(notification);
}, animationDuration);
});
}, [removeNotification, notification]);

// Useeffect to let appear the notification from right to left in 400 ms
// Useeffect to let appear the notification from right to left
useEffect(() => {
setTimeout(() => {
setVisible(true);
}, 400);
setVisible(true);

if (notification.duration && notification.duration != 0) {
if (duration && duration != 0) {
setTimeout(() => {
hideNotification();
}, Math.max(notification.duration, 4000));
}, Math.max(duration, 4000));
}
}, [hideNotification, notification.duration]);
}, [hideNotification, duration]);

const containesClasses = classNames({
'from-teal-500/40 dark:from-teal-500/20 to-teal-400/40 dark:to-teal-700/20 border-teal-400 dark:border-teal-700 hover:ring-teal-400/75 dark:hover:ring-teal-700/75 [&_button]:bg-teal-300 [&_button]:dark:bg-teal-700 [&_button:hover]:ring-teal-500 [&_a]:text-teal-500 [&_a:hover]:text-teal-400':
Expand All @@ -49,8 +52,6 @@ export const Notification: NotificationComponent = (notification) => {
type === 'warning',
'from-slate-500/40 dark:from-slate-500/20 to-slate-400/40 dark:to-slate-700/20 border-slate-400 dark:border-slate-700 hover:ring-slate-400/75 dark:hover:ring-slate-700/75 [&_button]:bg-slate-300 [&_button]:dark:bg-slate-700 [&_button:hover]:ring-slate-500 [&_a]:text-slate-500 [&_a:hover]:text-slate-400':
type === 'default',
'right-[-120%] max-h-[0px] p-0 my-0 border-none': !visible,
'right-0 max-h-[200px] p-4 mt-2 border-2': visible,
});

const titleClasses = classNames({
Expand All @@ -70,64 +71,68 @@ export const Notification: NotificationComponent = (notification) => {
});

return (
<div
className={classNames(
'bg-white dark:bg-slate-900 rounded-lg shadow-md shadow-slate-400/50 dark:shadow-slate-900/50 border-solid select-none',
'bg-gradient-to-r dark:bg-gradient-to-l relative border-transparent transition-all duration-800 ease-in-out hover:ring-2 overflow-hidden',
'[&_button]:rounded-lg [&_button]:py-2 [&_button]:px-4 [&_button]:mt-2 [&_button]:text-white [&_button]:transition-all [&_button]:ring-2 [&_button]:ring-transparent',
'[&_a]:underline',
containesClasses
<AnimatePresence>
{visible && (
<motion.div
initial={{ opacity: 0, x: 100, scale: 0.5 }}
animate={{ opacity: 1, x: 0, scale: 1 }}
exit={{ opacity: 0, x: 0, scale: 0.5 }}
transition={{
duration: animationDuration / 1000,
type: 'spring',
bounce: 0.5,
}}
className={classNames(
'bg-white dark:bg-slate-900 rounded-lg shadow-md shadow-slate-400/50 dark:shadow-slate-900/50 border-solid select-none',
'bg-gradient-to-r dark:bg-gradient-to-l relative border-transparent hover:ring-2 overflow-hidden',
'[&_button]:rounded-lg [&_button]:py-2 [&_button]:px-4 [&_button]:mt-2 [&_button]:text-white [&_button]:transition-all [&_button]:ring-2 [&_button]:ring-transparent',
'[&_a]:underline max-h-[200px] p-4 mt-2 border-2 relative',
containesClasses
)}
>
{duration && (
<div className="absolute w-full top-0 left-0">
<div
className={classNames(
'border-2 h-1 animate-progress w-0',
containesClasses
)}
style={{
animationDuration: `${duration}ms`,
animationDirection: 'reverse',
}}
/>
</div>
)}
<div
className={classNames(
'p-1 absolute top-2 right-2 cursor-pointer',
textClasses
)}
onClick={hideNotification}
>
<i className="fa-light fa-xmark"></i>
</div>
<b
className={classNames(
'font-display inline-block first-letter:uppercase',
titleClasses
)}
>
{title}
</b>
<p
className={classNames(
'font-light block first-letter:uppercase',
textClasses
)}
>
{message}
</p>
{children}
</motion.div>
)}
>
<div
className={classNames(
'p-1 absolute top-2 right-2 cursor-pointer',
textClasses
)}
onClick={hideNotification}
>
<i className="fa-light fa-xmark"></i>
</div>
<b
className={classNames(
'font-display inline-block first-letter:uppercase',
titleClasses
)}
>
{title}
</b>
<p
className={classNames(
'font-light block first-letter:uppercase',
textClasses
)}
>
{message}
</p>
{children}
</div>
);
};

/**
* NotificationContainer is a component that will render the notifications
* that are stored in the state of the application and will be used to
* display the notifications to the user at the bottom right of the screen.
*
* NOTE: This component requires to be used inside the NotificationProvider.
*/
export const NotificationContainer = () => {
const { notifications } = useNotification();

return (
<div className="fixed bottom-0 right-0 p-4 min-w-min w-1/4 max-w-[400px]">
{notifications.map((notification) => (
<Notification
key={`notification-${notification.id}`}
{...notification}
/>
))}
</div>
</AnimatePresence>
);
};

Expand Down
11 changes: 0 additions & 11 deletions web/ui/src/components/Notification/NotificationContext.tsx

This file was deleted.

25 changes: 0 additions & 25 deletions web/ui/src/components/Notification/NotificationProvider.tsx

This file was deleted.

46 changes: 0 additions & 46 deletions web/ui/src/components/Notification/hooks.ts

This file was deleted.

2 changes: 1 addition & 1 deletion web/ui/src/components/Notification/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { useNotification as default, useNotification } from './hooks';
export { Notification } from './Notification';
export type { NotificationProps } from './types';
Loading

0 comments on commit d174917

Please sign in to comment.