From 9ee5a3122337941af8cbdca557835118ba53d144 Mon Sep 17 00:00:00 2001 From: "Farhan Mohd. Fokrul Alam" Date: Tue, 3 Oct 2023 01:31:38 +0600 Subject: [PATCH] feat: create Notification component --- .storybook/preview.tsx | 3 + .../Common/Notification/index.module.css | 18 ++++++ .../Common/Notification/index.stories.tsx | 37 ++++++++++++ components/Common/Notification/index.tsx | 34 +++++++++++ hooks/useNotification.ts | 9 +++ i18n/locales/en.json | 1 + package-lock.json | 59 +++++++++++++++++++ package.json | 1 + providers/notificationProvider.tsx | 55 +++++++++++++++++ 9 files changed, 217 insertions(+) create mode 100644 components/Common/Notification/index.module.css create mode 100644 components/Common/Notification/index.stories.tsx create mode 100644 components/Common/Notification/index.tsx create mode 100644 hooks/useNotification.ts create mode 100644 providers/notificationProvider.tsx diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 7b989b0047471..568abb85892b2 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -3,6 +3,7 @@ import classNames from 'classnames'; import { withThemeByDataAttribute } from '@storybook/addon-themes'; import { SiteProvider } from '../providers/siteProvider'; import { LocaleProvider } from '../providers/localeProvider'; +import { NotificationProvider } from '../providers/notificationProvider'; import * as constants from './constants'; import type { Preview, ReactRenderer } from '@storybook/react'; @@ -30,9 +31,11 @@ const preview: Preview = { Story => ( +
+
), diff --git a/components/Common/Notification/index.module.css b/components/Common/Notification/index.module.css new file mode 100644 index 0000000000000..981e1b9d03cb8 --- /dev/null +++ b/components/Common/Notification/index.module.css @@ -0,0 +1,18 @@ +.root { + @apply m-6 + rounded + border + border-neutral-200 + bg-white + px-4 + py-3 + shadow-lg + dark:border-neutral-800 + dark:bg-neutral-900; +} + +.message { + @apply font-medium + text-green-600 + dark:text-white; +} diff --git a/components/Common/Notification/index.stories.tsx b/components/Common/Notification/index.stories.tsx new file mode 100644 index 0000000000000..0aa59b809ce7a --- /dev/null +++ b/components/Common/Notification/index.stories.tsx @@ -0,0 +1,37 @@ +import { CodeBracketIcon } from '@heroicons/react/24/solid'; +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; +import { FormattedMessage } from 'react-intl'; + +import Notification from './index'; + +type Story = StoryObj; +type Meta = MetaObj; + +export const Default: Story = { + args: { + open: true, + duration: 5000, + children: 'Copied to clipboard!', + }, +}; + +export const TimedNotification: Story = { + args: { + duration: 5000, + children: 'Copied to clipboard!', + }, +}; + +export const WithJSX: Story = { + args: { + open: true, + children: ( +
+ + +
+ ), + }, +}; + +export default { component: Notification } as Meta; diff --git a/components/Common/Notification/index.tsx b/components/Common/Notification/index.tsx new file mode 100644 index 0000000000000..f84eb00373e0d --- /dev/null +++ b/components/Common/Notification/index.tsx @@ -0,0 +1,34 @@ +import * as ToastPrimitive from '@radix-ui/react-toast'; +import classNames from 'classnames'; +import type { FC } from 'react'; + +import styles from './index.module.css'; + +type NotificationProps = { + open?: boolean; + duration?: number; + onChange?: (value: boolean) => void; + children?: React.ReactNode; + className?: string; +}; + +const Notification: FC = ({ + open, + duration = 5000, + onChange, + children, + className, +}: NotificationProps) => ( + + + {children} + + +); + +export default Notification; diff --git a/hooks/useNotification.ts b/hooks/useNotification.ts new file mode 100644 index 0000000000000..674c1cd62564f --- /dev/null +++ b/hooks/useNotification.ts @@ -0,0 +1,9 @@ +import { useContext } from 'react'; + +import { NotificationDispatch } from '@/providers/notificationProvider'; + +export const useNotification = () => { + const dispatch = useContext(NotificationDispatch); + + return dispatch; +}; diff --git a/i18n/locales/en.json b/i18n/locales/en.json index aa9797b88f6b6..279a1958c6a84 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -36,6 +36,7 @@ "components.pagination.previous": "Older", "components.common.crossLink.previous": "Prev", "components.common.crossLink.next": "Next", + "components.common.codebox.copied": "Copied to clipboard!", "layouts.blogPost.author.byLine": "{author, select, null {} other {By {author}, }}", "layouts.blogIndex.currentYear": "News from {year}", "components.api.jsonLink.title": "View as JSON", diff --git a/package-lock.json b/package-lock.json index daa11275cb623..044c705a7af92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@mdx-js/react": "^2.3.0", "@nodevu/core": "~0.1.0", "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-toast": "^1.1.5", "@types/node": "18.18.3", "@vcarl/remark-headings": "~0.1.0", "@vercel/analytics": "^1.0.2", @@ -4732,6 +4733,30 @@ } } }, + "node_modules/@radix-ui/react-presence": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", + "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-primitive": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", @@ -4872,6 +4897,40 @@ } } }, + "node_modules/@radix-ui/react-toast": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.1.5.tgz", + "integrity": "sha512-fRLn227WHIBRSzuRzGJ8W+5YALxofH23y0MlPLddaIpLpCDqdE0NZlS2NRQDRiptfxDeeCjgFIpexB1/zkxDlw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1", + "@radix-ui/react-visually-hidden": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-toggle": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.0.3.tgz", diff --git a/package.json b/package.json index 482362fe71214..d302a1c1c33d0 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@mdx-js/react": "^2.3.0", "@nodevu/core": "~0.1.0", "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-toast": "^1.1.5", "@types/node": "18.18.3", "@vcarl/remark-headings": "~0.1.0", "@vercel/analytics": "^1.0.2", diff --git a/providers/notificationProvider.tsx b/providers/notificationProvider.tsx new file mode 100644 index 0000000000000..3adae236a2b00 --- /dev/null +++ b/providers/notificationProvider.tsx @@ -0,0 +1,55 @@ +import * as Toast from '@radix-ui/react-toast'; +import type { + CSSProperties, + Dispatch, + FC, + PropsWithChildren, + SetStateAction, +} from 'react'; +import { createContext, useEffect, useState } from 'react'; + +import Notification from '@/components/Common/Notification'; + +const viewportStyles = { + position: 'absolute', + bottom: 0, + right: 0, +} satisfies CSSProperties; + +type NotificationContextType = { + message: string | JSX.Element; + duration: number; +} | null; + +const NotificationContext = createContext(null); + +export const NotificationDispatch = createContext< + Dispatch> +>(() => {}); + +export const NotificationProvider: FC = ({ children }) => { + const [notification, dispatch] = useState(null); + + useEffect(() => { + const timeout = setTimeout(() => dispatch(null), notification?.duration); + + return () => clearTimeout(timeout); + }, [notification]); + + return ( + + + + {children} + + {notification && ( + + {notification.message} + + )} + + + + + ); +};