diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx
index 7b989b0047471..254eed6f2e912 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..0c8df3f71de6a
--- /dev/null
+++ b/hooks/useNotification.ts
@@ -0,0 +1,5 @@
+import { useContext } from 'react';
+
+import { NotificationDispatch } from '@/providers/notificationProvider';
+
+export const useNotification = () => useContext(NotificationDispatch);
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..ce53248b74309
--- /dev/null
+++ b/providers/notificationProvider.tsx
@@ -0,0 +1,56 @@
+import * as Toast from '@radix-ui/react-toast';
+import type {
+ Dispatch,
+ FC,
+ PropsWithChildren,
+ ReactNode,
+ SetStateAction,
+} from 'react';
+import { createContext, useEffect, useState } from 'react';
+
+import Notification from '@/components/Common/Notification';
+
+type NotificationContextType = {
+ message: string | ReactNode;
+ duration: number;
+} | null;
+
+type NotificationProps = {
+ viewportClassName?: string;
+};
+
+const NotificationContext = createContext(null);
+
+export const NotificationDispatch = createContext<
+ Dispatch>
+>(() => {});
+
+export const NotificationProvider: FC> = ({
+ viewportClassName,
+ children,
+}) => {
+ const [notification, dispatch] = useState(null);
+
+ useEffect(() => {
+ const timeout = setTimeout(() => dispatch(null), notification?.duration);
+
+ return () => clearTimeout(timeout);
+ }, [notification]);
+
+ return (
+
+
+
+ {children}
+
+
+ {notification && (
+
+ {notification.message}
+
+ )}
+
+
+
+ );
+};