From 081f9c23a90a05ed3a4a2b8723a6635706ea1b20 Mon Sep 17 00:00:00 2001 From: Mike Barkmin Date: Fri, 8 Oct 2021 09:47:28 +0200 Subject: [PATCH] feat(notifications): add notify and NotificationsProvider Notifications can be dispatch by calling notify.info, notify.warn, notify.error or notify.success. The NotificationsProvider is already integrated into the PatchesProvider. --- .storybook/preview.tsx | 1 + docs/introduction/notifications.stories.mdx | 26 + package.json | 1 + src/NotificationsProvider.stories.tsx | 37 + src/NotificationsProvider.tsx | 675 ++++++++++++++++++ src/PatchesProvider.tsx | 13 +- src/__snapshots__/ButtonLink.test.tsx.snap | 3 + .../ButtonOutlineLink.test.tsx.snap | 3 + .../ButtonPrimaryLink.test.tsx.snap | 3 + .../ButtonSecondaryLink.test.tsx.snap | 3 + .../PatchesProvider.test.tsx.snap | 6 +- src/index.ts | 1 + yarn.lock | 9 +- 13 files changed, 774 insertions(+), 7 deletions(-) create mode 100644 docs/introduction/notifications.stories.mdx create mode 100644 src/NotificationsProvider.stories.tsx create mode 100644 src/NotificationsProvider.tsx diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index f4299778..f7a2d716 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -11,6 +11,7 @@ const SORT_ORDER = { "Getting Started", "Design System", "Responsive Design", + "Notifications", "Contributing", ], Recipes: [], diff --git a/docs/introduction/notifications.stories.mdx b/docs/introduction/notifications.stories.mdx new file mode 100644 index 00000000..5753401c --- /dev/null +++ b/docs/introduction/notifications.stories.mdx @@ -0,0 +1,26 @@ +import { Meta } from "@storybook/addon-docs/blocks"; + + + +# Notifications + +Notifications are an integral part of any application. Patches uses +[react-toastify](https://github.com/fkhadra/react-toastify) under the hood. We +reexport some parts of react-toastify and apply our own styling. + +You need to wrap your application in a PatchesProvider, which you already should +have. Then you can use the `notify` object and dispatch notifications like so: + +``` +import { notify } from "@openpatch/patches" + +notify.info("Info") +notify.warn("Warn") +notify.error("Error") +notify.success("Success") +``` + +If you want to overwrite the default behaviour of the Notifications, you can +pass an option object to the different functions. Take a look at the +[react-toastify Repository](https://fkhadra.github.io/react-toastify/api/toast) +for more information about that. diff --git a/package.json b/package.json index 1784a4e8..6ba61e81 100644 --- a/package.json +++ b/package.json @@ -104,6 +104,7 @@ "react-modal": "^3.12.1", "react-syntax-highlighter": "^15.4.4", "react-tabs": "^3.2.1", + "react-toastify": "^8.0.3", "remark-gfm": "^1.0.0", "styled-system": "^5.1.5" }, diff --git a/src/NotificationsProvider.stories.tsx b/src/NotificationsProvider.stories.tsx new file mode 100644 index 00000000..13990eb1 --- /dev/null +++ b/src/NotificationsProvider.stories.tsx @@ -0,0 +1,37 @@ +import { Meta } from "@storybook/react/types-6-0"; +import { ButtonPrimary } from "./ButtonPrimary"; +import { NotificationsProvider, notify } from "./NotificationsProvider"; + +export default { + title: "Components/NotificationsProvider", + component: NotificationsProvider, + argTypes: {}, +} as Meta; + +export const Info = () => { + const dispatch = () => { + notify.info("Info"); + }; + return Dispatch; +}; + +export const Warn = () => { + const dispatch = () => { + notify.warn("Warn"); + }; + return Dispatch; +}; + +export const Success = () => { + const dispatch = () => { + notify.success("Success"); + }; + return Dispatch; +}; + +export const Error = () => { + const dispatch = () => { + notify.error("Error"); + }; + return Dispatch; +}; diff --git a/src/NotificationsProvider.tsx b/src/NotificationsProvider.tsx new file mode 100644 index 00000000..130c66a3 --- /dev/null +++ b/src/NotificationsProvider.tsx @@ -0,0 +1,675 @@ +import { css, Global } from "@emotion/react"; +import { Fragment, ReactNode } from "react"; +import { toast, ToastContainer, ToastContainerProps } from "react-toastify"; + +export type NotificationsProviderProps = { + children?: ReactNode; +} & ToastContainerProps; + +export const notify = { + info: toast.info, + warn: toast.warn, + success: toast.success, + error: toast.error, + dismiss: toast.dismiss, + isActive: toast.isActive, + update: toast.update, + clearWaitingQueue: toast.clearWaitingQueue, + done: toast.done, +}; + +export const NotificationsProvider = ({ + children, + ...props +}: NotificationsProviderProps) => { + return ( + + css` + :root { + --toastify-color-light: ${theme.colors.card}; + --toastify-color-dark: ${theme.colors.neutral["900"]}; + --toastify-color-info: ${theme.colors.info["500"]}; + --toastify-color-success: ${theme.colors.success["500"]}; + --toastify-color-warning: ${theme.colors.warning["500"]}; + --toastify-color-error: ${theme.colors.error["500"]}; + --toastify-color-transparent: rgba(255, 255, 255, 0.7); + --toastify-icon-color-info: var(--toastify-color-info); + --toastify-icon-color-success: var(--toastify-color-success); + --toastify-icon-color-warning: var(--toastify-color-warning); + --toastify-icon-color-error: var(--toastify-color-error); + --toastify-toast-width: 320px; + --toastify-toast-background: ${theme.colors.card}; + --toastify-toast-min-height: 64px; + --toastify-toast-max-height: 800px; + --toastify-font-family: sans-serif; + --toastify-z-index: 9999; + --toastify-text-color-light: ${theme.colors.text}; + --toastify-text-color-dark: ${theme.colors.text}; + --toastify-text-color-info: ${theme.colors.text}; + --toastify-text-color-success: ${theme.colors.text}; + --toastify-text-color-warning: ${theme.colors.text}; + --toastify-text-color-error: ${theme.colors.text}; + --toastify-spinner-color: #616161; + --toastify-spinner-color-empty-area: #e0e0e0; + --toastify-color-progress-light: ${theme.colors.accent["500"]}; + --toastify-color-progress-dark: ${theme.colors.accent["500"]}; + --toastify-color-progress-info: var(--toastify-color-info); + --toastify-color-progress-success: var(--toastify-color-success); + --toastify-color-progress-warning: var(--toastify-color-warning); + --toastify-color-progress-error: var(--toastify-color-error); + } + + .Toastify__toast-container { + z-index: var(--toastify-z-index); + -webkit-transform: translate3d(0, 0, var(--toastify-z-index) px); + position: fixed; + padding: 4px; + width: var(--toastify-toast-width); + box-sizing: border-box; + color: #fff; + } + .Toastify__toast-container--top-left { + top: 1em; + left: 1em; + } + .Toastify__toast-container--top-center { + top: 1em; + left: 50%; + transform: translateX(-50%); + } + .Toastify__toast-container--top-right { + top: 1em; + right: 1em; + } + .Toastify__toast-container--bottom-left { + bottom: 1em; + left: 1em; + } + .Toastify__toast-container--bottom-center { + bottom: 1em; + left: 50%; + transform: translateX(-50%); + } + .Toastify__toast-container--bottom-right { + bottom: 1em; + right: 1em; + } + + @media only screen and (max-width: 480px) { + .Toastify__toast-container { + width: 100vw; + padding: 0; + left: 0; + margin: 0; + } + .Toastify__toast-container--top-left, + .Toastify__toast-container--top-center, + .Toastify__toast-container--top-right { + top: 0; + transform: translateX(0); + } + .Toastify__toast-container--bottom-left, + .Toastify__toast-container--bottom-center, + .Toastify__toast-container--bottom-right { + bottom: 0; + transform: translateX(0); + } + .Toastify__toast-container--rtl { + right: 0; + left: initial; + } + } + .Toastify__toast { + position: relative; + min-height: var(--toastify-toast-min-height); + box-sizing: border-box; + margin-bottom: 1rem; + padding: 8px; + border-radius: 4px; + box-shadow: 0 1px 10px 0 rgba(0, 0, 0, 0.1), + 0 2px 15px 0 rgba(0, 0, 0, 0.05); + display: -ms-flexbox; + display: flex; + -ms-flex-pack: justify; + justify-content: space-between; + max-height: var(--toastify-toast-max-height); + overflow: hidden; + font-family: var(--toastify-font-family); + cursor: pointer; + direction: ltr; + } + .Toastify__toast--rtl { + direction: rtl; + } + .Toastify__toast-body { + margin: auto 0; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + padding: 6px; + display: -ms-flexbox; + display: flex; + -ms-flex-align: center; + align-items: center; + } + .Toastify__toast-body > div:last-child { + -ms-flex: 1; + flex: 1; + } + .Toastify__toast-icon { + -webkit-margin-end: 10px; + margin-inline-end: 10px; + width: 20px; + -ms-flex-negative: 0; + flex-shrink: 0; + display: -ms-flexbox; + display: flex; + } + + .Toastify--animate { + animation-fill-mode: both; + animation-duration: 0.7s; + } + + .Toastify--animate-icon { + animation-fill-mode: both; + animation-duration: 0.3s; + } + + @media only screen and (max-width: 480px) { + .Toastify__toast { + margin-bottom: 0; + border-radius: 0; + } + } + .Toastify__toast-theme--dark { + background: var(--toastify-color-dark); + color: var(--toastify-text-color-dark); + } + .Toastify__toast-theme--light { + background: var(--toastify-color-light); + color: var(--toastify-text-color-light); + } + .Toastify__toast-theme--colored.Toastify__toast--default { + background: var(--toastify-color-light); + color: var(--toastify-text-color-light); + } + .Toastify__toast-theme--colored.Toastify__toast--info { + color: var(--toastify-text-color-info); + background: var(--toastify-color-info); + } + .Toastify__toast-theme--colored.Toastify__toast--success { + color: var(--toastify-text-color-success); + background: var(--toastify-color-success); + } + .Toastify__toast-theme--colored.Toastify__toast--warning { + color: var(--toastify-text-color-warning); + background: var(--toastify-color-warning); + } + .Toastify__toast-theme--colored.Toastify__toast--error { + color: var(--toastify-text-color-error); + background: var(--toastify-color-error); + } + + .Toastify__progress-bar-theme--light { + background: var(--toastify-color-progress-light); + } + .Toastify__progress-bar-theme--dark { + background: var(--toastify-color-progress-dark); + } + .Toastify__progress-bar--info { + background: var(--toastify-color-progress-info); + } + .Toastify__progress-bar--success { + background: var(--toastify-color-progress-success); + } + .Toastify__progress-bar--warning { + background: var(--toastify-color-progress-warning); + } + .Toastify__progress-bar--error { + background: var(--toastify-color-progress-error); + } + .Toastify__progress-bar-theme--colored.Toastify__progress-bar--info, + .Toastify__progress-bar-theme--colored.Toastify__progress-bar--success, + .Toastify__progress-bar-theme--colored.Toastify__progress-bar--warning, + .Toastify__progress-bar-theme--colored.Toastify__progress-bar--error { + background: var(--toastify-color-transparent); + } + + .Toastify__close-button { + color: #fff; + background: transparent; + outline: none; + border: none; + padding: 0; + cursor: pointer; + opacity: 0.7; + transition: 0.3s ease; + -ms-flex-item-align: start; + align-self: flex-start; + } + .Toastify__close-button--light { + color: #000; + opacity: 0.3; + } + .Toastify__close-button > svg { + fill: currentColor; + height: 16px; + width: 14px; + } + .Toastify__close-button:hover, + .Toastify__close-button:focus { + opacity: 1; + } + + @keyframes Toastify__trackProgress { + 0% { + transform: scaleX(1); + } + 100% { + transform: scaleX(0); + } + } + .Toastify__progress-bar { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 5px; + z-index: var(--toastify-z-index); + opacity: 0.7; + transform-origin: left; + } + .Toastify__progress-bar--animated { + animation: Toastify__trackProgress linear 1 forwards; + } + .Toastify__progress-bar--controlled { + transition: transform 0.2s; + } + .Toastify__progress-bar--rtl { + right: 0; + left: initial; + transform-origin: right; + } + + .Toastify__spinner { + width: 20px; + height: 20px; + box-sizing: border-box; + border: 2px solid; + border-radius: 100%; + border-color: var(--toastify-spinner-color-empty-area); + border-right-color: var(--toastify-spinner-color); + animation: Toastify__spin 0.65s linear infinite; + } + + @keyframes Toastify__bounceInRight { + from, + 60%, + 75%, + 90%, + to { + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + from { + opacity: 0; + transform: translate3d(3000px, 0, 0); + } + 60% { + opacity: 1; + transform: translate3d(-25px, 0, 0); + } + 75% { + transform: translate3d(10px, 0, 0); + } + 90% { + transform: translate3d(-5px, 0, 0); + } + to { + transform: none; + } + } + @keyframes Toastify__bounceOutRight { + 20% { + opacity: 1; + transform: translate3d(-20px, 0, 0); + } + to { + opacity: 0; + transform: translate3d(2000px, 0, 0); + } + } + @keyframes Toastify__bounceInLeft { + from, + 60%, + 75%, + 90%, + to { + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + 0% { + opacity: 0; + transform: translate3d(-3000px, 0, 0); + } + 60% { + opacity: 1; + transform: translate3d(25px, 0, 0); + } + 75% { + transform: translate3d(-10px, 0, 0); + } + 90% { + transform: translate3d(5px, 0, 0); + } + to { + transform: none; + } + } + @keyframes Toastify__bounceOutLeft { + 20% { + opacity: 1; + transform: translate3d(20px, 0, 0); + } + to { + opacity: 0; + transform: translate3d(-2000px, 0, 0); + } + } + @keyframes Toastify__bounceInUp { + from, + 60%, + 75%, + 90%, + to { + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + from { + opacity: 0; + transform: translate3d(0, 3000px, 0); + } + 60% { + opacity: 1; + transform: translate3d(0, -20px, 0); + } + 75% { + transform: translate3d(0, 10px, 0); + } + 90% { + transform: translate3d(0, -5px, 0); + } + to { + transform: translate3d(0, 0, 0); + } + } + @keyframes Toastify__bounceOutUp { + 20% { + transform: translate3d(0, -10px, 0); + } + 40%, + 45% { + opacity: 1; + transform: translate3d(0, 20px, 0); + } + to { + opacity: 0; + transform: translate3d(0, -2000px, 0); + } + } + @keyframes Toastify__bounceInDown { + from, + 60%, + 75%, + 90%, + to { + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + 0% { + opacity: 0; + transform: translate3d(0, -3000px, 0); + } + 60% { + opacity: 1; + transform: translate3d(0, 25px, 0); + } + 75% { + transform: translate3d(0, -10px, 0); + } + 90% { + transform: translate3d(0, 5px, 0); + } + to { + transform: none; + } + } + @keyframes Toastify__bounceOutDown { + 20% { + transform: translate3d(0, 10px, 0); + } + 40%, + 45% { + opacity: 1; + transform: translate3d(0, -20px, 0); + } + to { + opacity: 0; + transform: translate3d(0, 2000px, 0); + } + } + .Toastify__bounce-enter--top-left, + .Toastify__bounce-enter--bottom-left { + animation-name: Toastify__bounceInLeft; + } + .Toastify__bounce-enter--top-right, + .Toastify__bounce-enter--bottom-right { + animation-name: Toastify__bounceInRight; + } + .Toastify__bounce-enter--top-center { + animation-name: Toastify__bounceInDown; + } + .Toastify__bounce-enter--bottom-center { + animation-name: Toastify__bounceInUp; + } + + .Toastify__bounce-exit--top-left, + .Toastify__bounce-exit--bottom-left { + animation-name: Toastify__bounceOutLeft; + } + .Toastify__bounce-exit--top-right, + .Toastify__bounce-exit--bottom-right { + animation-name: Toastify__bounceOutRight; + } + .Toastify__bounce-exit--top-center { + animation-name: Toastify__bounceOutUp; + } + .Toastify__bounce-exit--bottom-center { + animation-name: Toastify__bounceOutDown; + } + + @keyframes Toastify__zoomIn { + from { + opacity: 0; + transform: scale3d(0.3, 0.3, 0.3); + } + 50% { + opacity: 1; + } + } + @keyframes Toastify__zoomOut { + from { + opacity: 1; + } + 50% { + opacity: 0; + transform: scale3d(0.3, 0.3, 0.3); + } + to { + opacity: 0; + } + } + .Toastify__zoom-enter { + animation-name: Toastify__zoomIn; + } + + .Toastify__zoom-exit { + animation-name: Toastify__zoomOut; + } + + @keyframes Toastify__flipIn { + from { + transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + animation-timing-function: ease-in; + opacity: 0; + } + 40% { + transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + animation-timing-function: ease-in; + } + 60% { + transform: perspective(400px) rotate3d(1, 0, 0, 10deg); + opacity: 1; + } + 80% { + transform: perspective(400px) rotate3d(1, 0, 0, -5deg); + } + to { + transform: perspective(400px); + } + } + @keyframes Toastify__flipOut { + from { + transform: perspective(400px); + } + 30% { + transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + opacity: 1; + } + to { + transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + opacity: 0; + } + } + .Toastify__flip-enter { + animation-name: Toastify__flipIn; + } + + .Toastify__flip-exit { + animation-name: Toastify__flipOut; + } + + @keyframes Toastify__slideInRight { + from { + transform: translate3d(110%, 0, 0); + visibility: visible; + } + to { + transform: translate3d(0, 0, 0); + } + } + @keyframes Toastify__slideInLeft { + from { + transform: translate3d(-110%, 0, 0); + visibility: visible; + } + to { + transform: translate3d(0, 0, 0); + } + } + @keyframes Toastify__slideInUp { + from { + transform: translate3d(0, 110%, 0); + visibility: visible; + } + to { + transform: translate3d(0, 0, 0); + } + } + @keyframes Toastify__slideInDown { + from { + transform: translate3d(0, -110%, 0); + visibility: visible; + } + to { + transform: translate3d(0, 0, 0); + } + } + @keyframes Toastify__slideOutRight { + from { + transform: translate3d(0, 0, 0); + } + to { + visibility: hidden; + transform: translate3d(110%, 0, 0); + } + } + @keyframes Toastify__slideOutLeft { + from { + transform: translate3d(0, 0, 0); + } + to { + visibility: hidden; + transform: translate3d(-110%, 0, 0); + } + } + @keyframes Toastify__slideOutDown { + from { + transform: translate3d(0, 0, 0); + } + to { + visibility: hidden; + transform: translate3d(0, 500px, 0); + } + } + @keyframes Toastify__slideOutUp { + from { + transform: translate3d(0, 0, 0); + } + to { + visibility: hidden; + transform: translate3d(0, -500px, 0); + } + } + .Toastify__slide-enter--top-left, + .Toastify__slide-enter--bottom-left { + animation-name: Toastify__slideInLeft; + } + .Toastify__slide-enter--top-right, + .Toastify__slide-enter--bottom-right { + animation-name: Toastify__slideInRight; + } + .Toastify__slide-enter--top-center { + animation-name: Toastify__slideInDown; + } + .Toastify__slide-enter--bottom-center { + animation-name: Toastify__slideInUp; + } + + .Toastify__slide-exit--top-left, + .Toastify__slide-exit--bottom-left { + animation-name: Toastify__slideOutLeft; + } + .Toastify__slide-exit--top-right, + .Toastify__slide-exit--bottom-right { + animation-name: Toastify__slideOutRight; + } + .Toastify__slide-exit--top-center { + animation-name: Toastify__slideOutUp; + } + .Toastify__slide-exit--bottom-center { + animation-name: Toastify__slideOutDown; + } + + @keyframes Toastify__spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } + } + `} + /> + + {children} + + ); +}; diff --git a/src/PatchesProvider.tsx b/src/PatchesProvider.tsx index 667ae1be..ac616e94 100644 --- a/src/PatchesProvider.tsx +++ b/src/PatchesProvider.tsx @@ -4,6 +4,7 @@ import { LinkComponentProvider, LinkComponentProviderProps, } from "./LinkComponentProvider"; +import { NotificationsProvider } from "./NotificationsProvider"; import { ThemeProvider, ThemeProviderProps } from "./ThemeProvider"; export type PatchesProviderProps = { @@ -19,11 +20,13 @@ export const PatchesProvider = ({ const linkComponentFromContext = useContext(LinkComponentContext); return ( - - {children} - + + + {children} + + ); }; diff --git a/src/__snapshots__/ButtonLink.test.tsx.snap b/src/__snapshots__/ButtonLink.test.tsx.snap index 0f5d8e72..a236b236 100644 --- a/src/__snapshots__/ButtonLink.test.tsx.snap +++ b/src/__snapshots__/ButtonLink.test.tsx.snap @@ -126,6 +126,9 @@ exports[`should match snapshot 1`] = `
+
+
+
+ `; diff --git a/src/index.ts b/src/index.ts index 6b16b561..4b21d362 100644 --- a/src/index.ts +++ b/src/index.ts @@ -58,6 +58,7 @@ export { Markdown, MarkdownProps } from "./Markdown"; export { MarkdownEditor, MarkdownEditorProps } from "./MarkdownEditor"; export { Modal, ModalProps } from "./Modal"; export { Nav, NavProps } from "./Nav"; +export { NotificationsProvider, NotificationsProviderProps } from "./NotificationsProvider"; export { NumberInput, NumberInputProps } from "./NumberInput"; export { PageHeader, PageHeaderProps } from "./PageHeader"; export { PasswordInput, PasswordInputProps } from "./PasswordInput"; diff --git a/yarn.lock b/yarn.lock index dc501499..7f6a9fc2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5905,7 +5905,7 @@ clone@^1.0.2: resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= -clsx@^1.1.0: +clsx@^1.1.0, clsx@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== @@ -12835,6 +12835,13 @@ react-textarea-autosize@^8.3.0: use-composed-ref "^1.0.0" use-latest "^1.0.0" +react-toastify@^8.0.3: + version "8.0.3" + resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-8.0.3.tgz#7fbf65f69ec357aab8dd03c1496f9177aa92409a" + integrity sha512-rv3koC7f9lKKSkdpYgo/TGzgWlrB/aaiUInF1DyV7BpiM4kyTs+uhu6/r8XDMtBY2FOIHK+FlK3Iv7OzpA/tCA== + dependencies: + clsx "^1.1.1" + react@17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"