The Node.js® Website

Create Notification Component (#5955)

feat: create Notification component

authored by

Farhan Alam and committed by
GitHub
7e90b954 6109d750

+217 -3
+6 -3
.storybook/preview.tsx
··· 3 3 import { withThemeByDataAttribute } from '@storybook/addon-themes'; 4 4 import { SiteProvider } from '../providers/siteProvider'; 5 5 import { LocaleProvider } from '../providers/localeProvider'; 6 + import { NotificationProvider } from '../providers/notificationProvider'; 6 7 import * as constants from './constants'; 7 8 import type { Preview, ReactRenderer } from '@storybook/react'; 8 9 ··· 30 31 Story => ( 31 32 <SiteProvider> 32 33 <LocaleProvider> 33 - <div className={rootClasses}> 34 - <Story /> 35 - </div> 34 + <NotificationProvider viewportClassName="absolute top-0 left-0 list-none"> 35 + <div className={rootClasses}> 36 + <Story /> 37 + </div> 38 + </NotificationProvider> 36 39 </LocaleProvider> 37 40 </SiteProvider> 38 41 ),
+18
components/Common/Notification/index.module.css
··· 1 + .root { 2 + @apply m-6 3 + rounded 4 + border 5 + border-neutral-200 6 + bg-white 7 + px-4 8 + py-3 9 + shadow-lg 10 + dark:border-neutral-800 11 + dark:bg-neutral-900; 12 + } 13 + 14 + .message { 15 + @apply font-medium 16 + text-green-600 17 + dark:text-white; 18 + }
+37
components/Common/Notification/index.stories.tsx
··· 1 + import { CodeBracketIcon } from '@heroicons/react/24/solid'; 2 + import type { Meta as MetaObj, StoryObj } from '@storybook/react'; 3 + import { FormattedMessage } from 'react-intl'; 4 + 5 + import Notification from './index'; 6 + 7 + type Story = StoryObj<typeof Notification>; 8 + type Meta = MetaObj<typeof Notification>; 9 + 10 + export const Default: Story = { 11 + args: { 12 + open: true, 13 + duration: 5000, 14 + children: 'Copied to clipboard!', 15 + }, 16 + }; 17 + 18 + export const TimedNotification: Story = { 19 + args: { 20 + duration: 5000, 21 + children: 'Copied to clipboard!', 22 + }, 23 + }; 24 + 25 + export const WithJSX: Story = { 26 + args: { 27 + open: true, 28 + children: ( 29 + <div className="flex items-center gap-3"> 30 + <CodeBracketIcon className="h-4 w-4" /> 31 + <FormattedMessage id="components.common.codebox.copied" /> 32 + </div> 33 + ), 34 + }, 35 + }; 36 + 37 + export default { component: Notification } as Meta;
+34
components/Common/Notification/index.tsx
··· 1 + import * as ToastPrimitive from '@radix-ui/react-toast'; 2 + import classNames from 'classnames'; 3 + import type { FC } from 'react'; 4 + 5 + import styles from './index.module.css'; 6 + 7 + type NotificationProps = { 8 + open?: boolean; 9 + duration?: number; 10 + onChange?: (value: boolean) => void; 11 + children?: React.ReactNode; 12 + className?: string; 13 + }; 14 + 15 + const Notification: FC<NotificationProps> = ({ 16 + open, 17 + duration = 5000, 18 + onChange, 19 + children, 20 + className, 21 + }: NotificationProps) => ( 22 + <ToastPrimitive.Root 23 + open={open} 24 + duration={duration} 25 + onOpenChange={onChange} 26 + className={classNames(styles.root, className)} 27 + > 28 + <ToastPrimitive.Title className={styles.message}> 29 + {children} 30 + </ToastPrimitive.Title> 31 + </ToastPrimitive.Root> 32 + ); 33 + 34 + export default Notification;
+5
hooks/useNotification.ts
··· 1 + import { useContext } from 'react'; 2 + 3 + import { NotificationDispatch } from '@/providers/notificationProvider'; 4 + 5 + export const useNotification = () => useContext(NotificationDispatch);
+1
i18n/locales/en.json
··· 36 36 "components.pagination.previous": "Older", 37 37 "components.common.crossLink.previous": "Prev", 38 38 "components.common.crossLink.next": "Next", 39 + "components.common.codebox.copied": "Copied to clipboard!", 39 40 "layouts.blogPost.author.byLine": "{author, select, null {} other {By {author}, }}", 40 41 "layouts.blogIndex.currentYear": "News from {year}", 41 42 "components.api.jsonLink.title": "View as JSON",
+59
package-lock.json
··· 13 13 "@mdx-js/react": "^2.3.0", 14 14 "@nodevu/core": "~0.1.0", 15 15 "@radix-ui/react-select": "^2.0.0", 16 + "@radix-ui/react-toast": "^1.1.5", 16 17 "@types/node": "18.18.3", 17 18 "@vcarl/remark-headings": "~0.1.0", 18 19 "@vercel/analytics": "^1.0.2", ··· 4732 4733 } 4733 4734 } 4734 4735 }, 4736 + "node_modules/@radix-ui/react-presence": { 4737 + "version": "1.0.1", 4738 + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", 4739 + "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", 4740 + "dependencies": { 4741 + "@babel/runtime": "^7.13.10", 4742 + "@radix-ui/react-compose-refs": "1.0.1", 4743 + "@radix-ui/react-use-layout-effect": "1.0.1" 4744 + }, 4745 + "peerDependencies": { 4746 + "@types/react": "*", 4747 + "@types/react-dom": "*", 4748 + "react": "^16.8 || ^17.0 || ^18.0", 4749 + "react-dom": "^16.8 || ^17.0 || ^18.0" 4750 + }, 4751 + "peerDependenciesMeta": { 4752 + "@types/react": { 4753 + "optional": true 4754 + }, 4755 + "@types/react-dom": { 4756 + "optional": true 4757 + } 4758 + } 4759 + }, 4735 4760 "node_modules/@radix-ui/react-primitive": { 4736 4761 "version": "1.0.3", 4737 4762 "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", ··· 4868 4893 }, 4869 4894 "peerDependenciesMeta": { 4870 4895 "@types/react": { 4896 + "optional": true 4897 + } 4898 + } 4899 + }, 4900 + "node_modules/@radix-ui/react-toast": { 4901 + "version": "1.1.5", 4902 + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.1.5.tgz", 4903 + "integrity": "sha512-fRLn227WHIBRSzuRzGJ8W+5YALxofH23y0MlPLddaIpLpCDqdE0NZlS2NRQDRiptfxDeeCjgFIpexB1/zkxDlw==", 4904 + "dependencies": { 4905 + "@babel/runtime": "^7.13.10", 4906 + "@radix-ui/primitive": "1.0.1", 4907 + "@radix-ui/react-collection": "1.0.3", 4908 + "@radix-ui/react-compose-refs": "1.0.1", 4909 + "@radix-ui/react-context": "1.0.1", 4910 + "@radix-ui/react-dismissable-layer": "1.0.5", 4911 + "@radix-ui/react-portal": "1.0.4", 4912 + "@radix-ui/react-presence": "1.0.1", 4913 + "@radix-ui/react-primitive": "1.0.3", 4914 + "@radix-ui/react-use-callback-ref": "1.0.1", 4915 + "@radix-ui/react-use-controllable-state": "1.0.1", 4916 + "@radix-ui/react-use-layout-effect": "1.0.1", 4917 + "@radix-ui/react-visually-hidden": "1.0.3" 4918 + }, 4919 + "peerDependencies": { 4920 + "@types/react": "*", 4921 + "@types/react-dom": "*", 4922 + "react": "^16.8 || ^17.0 || ^18.0", 4923 + "react-dom": "^16.8 || ^17.0 || ^18.0" 4924 + }, 4925 + "peerDependenciesMeta": { 4926 + "@types/react": { 4927 + "optional": true 4928 + }, 4929 + "@types/react-dom": { 4871 4930 "optional": true 4872 4931 } 4873 4932 }
+1
package.json
··· 44 44 "@mdx-js/react": "^2.3.0", 45 45 "@nodevu/core": "~0.1.0", 46 46 "@radix-ui/react-select": "^2.0.0", 47 + "@radix-ui/react-toast": "^1.1.5", 47 48 "@types/node": "18.18.3", 48 49 "@vcarl/remark-headings": "~0.1.0", 49 50 "@vercel/analytics": "^1.0.2",
+56
providers/notificationProvider.tsx
··· 1 + import * as Toast from '@radix-ui/react-toast'; 2 + import type { 3 + Dispatch, 4 + FC, 5 + PropsWithChildren, 6 + ReactNode, 7 + SetStateAction, 8 + } from 'react'; 9 + import { createContext, useEffect, useState } from 'react'; 10 + 11 + import Notification from '@/components/Common/Notification'; 12 + 13 + type NotificationContextType = { 14 + message: string | ReactNode; 15 + duration: number; 16 + } | null; 17 + 18 + type NotificationProps = { 19 + viewportClassName?: string; 20 + }; 21 + 22 + const NotificationContext = createContext<NotificationContextType>(null); 23 + 24 + export const NotificationDispatch = createContext< 25 + Dispatch<SetStateAction<NotificationContextType>> 26 + >(() => {}); 27 + 28 + export const NotificationProvider: FC<PropsWithChildren<NotificationProps>> = ({ 29 + viewportClassName, 30 + children, 31 + }) => { 32 + const [notification, dispatch] = useState<NotificationContextType>(null); 33 + 34 + useEffect(() => { 35 + const timeout = setTimeout(() => dispatch(null), notification?.duration); 36 + 37 + return () => clearTimeout(timeout); 38 + }, [notification]); 39 + 40 + return ( 41 + <NotificationContext.Provider value={notification}> 42 + <NotificationDispatch.Provider value={dispatch}> 43 + <Toast.Provider> 44 + {children} 45 + 46 + <Toast.Viewport className={viewportClassName} /> 47 + {notification && ( 48 + <Notification duration={notification.duration}> 49 + {notification.message} 50 + </Notification> 51 + )} 52 + </Toast.Provider> 53 + </NotificationDispatch.Provider> 54 + </NotificationContext.Provider> 55 + ); 56 + };