From 3ffb95ebfc7d0e138aec941cbb8632f5b849a92f Mon Sep 17 00:00:00 2001 From: nhanluongoe Date: Tue, 9 Jan 2024 13:24:25 +0700 Subject: [PATCH 1/8] build: gen *.d.ts when building --- src/components/toaster.tsx | 2 +- tsconfig.json | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/toaster.tsx b/src/components/toaster.tsx index 50e0259..76f89ef 100644 --- a/src/components/toaster.tsx +++ b/src/components/toaster.tsx @@ -7,7 +7,7 @@ import ToastIcon from './toast-icon'; setup(React.createElement); -interface ToasterProps { +export interface ToasterProps { position?: 'left' | 'center' | 'right'; toastOptions?: ToastsOptions; } diff --git a/tsconfig.json b/tsconfig.json index 78efe6c..729ec70 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,9 @@ "compilerOptions": { "esModuleInterop": true, "jsx": "react-jsx", + "lib": ["dom", "esnext"], + "declaration": true, + "sourceMap": true, "module": "ESNext", "moduleResolution": "node", "skipLibCheck": true, From ef7c5ac9ac741e3adadb9e47508daf4c663e325c Mon Sep 17 00:00:00 2001 From: nhanluongoe Date: Tue, 9 Jan 2024 13:30:20 +0700 Subject: [PATCH 2/8] refactor: move const value to top of the file --- src/core/use-toast.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/use-toast.ts b/src/core/use-toast.ts index 7642070..c9ff26f 100644 --- a/src/core/use-toast.ts +++ b/src/core/use-toast.ts @@ -10,6 +10,8 @@ import { } from './types'; import { genId } from './utils'; +const DEFAULT_DURATION = 3 * 1000; + function createToast(type: ToasterType = 'default', arg: ToastArg): Toast { if (isFunction(arg)) { const id = genId(); @@ -63,8 +65,6 @@ toast.dismiss = (toastId?: string) => { }); }; -const DEFAULT_DURATION = 3 * 1000; - const pause = () => { dispatch({ type: 'PAUSE', From 0d319cdd29e826b7ed188f6df85c10c9cb28b3d2 Mon Sep 17 00:00:00 2001 From: nhanluongoe Date: Tue, 9 Jan 2024 13:30:34 +0700 Subject: [PATCH 3/8] fix: add border for close icon --- src/components/icons.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/icons.tsx b/src/components/icons.tsx index 8fcdd68..13af6b9 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -44,6 +44,8 @@ const StyledClose = styled('i')` border: 2px solid transparent; border-radius: 40px; color: #ff4b4b; + border: 2px solid red; + border-radius: 9999px; } &::after, &::before { @@ -56,8 +58,8 @@ const StyledClose = styled('i')` height: 3px; background: currentColor; transform: rotate(45deg); - top: 10px; - left: 4px; + top: 12px; + left: 3px; } &::after { transform: rotate(-45deg); From 55cee5c6960c5381ddc49d91ef754b2417fe6e4c Mon Sep 17 00:00:00 2001 From: nhanluongoe Date: Tue, 9 Jan 2024 15:36:30 +0700 Subject: [PATCH 4/8] feat: add loading toast --- src/components/icons.tsx | 34 +++++++++++++++++++++++++++++++++- src/components/toast-icon.tsx | 5 ++++- src/core/types.ts | 2 +- src/core/use-toast.ts | 1 + 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/components/icons.tsx b/src/components/icons.tsx index 13af6b9..3d22281 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { styled } from 'goober'; +import { keyframes, styled } from 'goober'; const StyledCheck = styled('i')` & { @@ -68,3 +68,35 @@ const StyledClose = styled('i')` export const Close: React.FC = () => { return ; }; + +const spinnerTwoAltAnimation = keyframes` + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +`; +const StyledSpinner = styled('i')` + position: relative; + &, + &::before { + box-sizing: border-box; + display: block; + width: 30px; + height: 30px; + } + &::before { + content: ''; + position: absolute; + border-radius: 100px; + animation: ${spinnerTwoAltAnimation} 1s cubic-bezier(0.6, 0, 0.4, 1) + infinite; + border: 3px solid #5fbdff; + border-bottom-color: transparent; + border-top-color: transparent; + } +`; +export const Spinner: React.FC = () => { + return ; +}; diff --git a/src/components/toast-icon.tsx b/src/components/toast-icon.tsx index 6aaf2db..11cdb6b 100644 --- a/src/components/toast-icon.tsx +++ b/src/components/toast-icon.tsx @@ -1,7 +1,7 @@ import { keyframes, styled } from 'goober'; import React from 'react'; import { Toast } from '../core/types'; -import { Check, Close } from './icons'; +import { Check, Close, Spinner } from './icons'; interface ToastIconProps { icon?: Toast['icon']; @@ -42,6 +42,9 @@ const ToastIcon: React.FC = (props) => { if (type === 'error') { return ; } + if (type === 'loading') { + return ; + } return null; }; diff --git a/src/core/types.ts b/src/core/types.ts index a2b4246..f03fac2 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -1,6 +1,6 @@ import React, { CSSProperties } from 'react'; -export type ToasterType = 'success' | 'error' | 'default'; +export type ToasterType = 'success' | 'error' | 'default' | 'loading'; export type Toast = { id: string; diff --git a/src/core/use-toast.ts b/src/core/use-toast.ts index c9ff26f..fd559bc 100644 --- a/src/core/use-toast.ts +++ b/src/core/use-toast.ts @@ -57,6 +57,7 @@ function createHandler(type?: ToasterType) { const toast = (opts: ToastArg) => createHandler('default')(opts); toast.error = createHandler('error'); toast.success = createHandler('success'); +toast.loading = createHandler('loading'); toast.dismiss = (toastId?: string) => { dispatch({ From c9f39fc4d69e3eae9491fe4cf1c0dc2294e06cfd Mon Sep 17 00:00:00 2001 From: nhanluongoe Date: Tue, 9 Jan 2024 17:00:53 +0700 Subject: [PATCH 5/8] feat: support show toast using promise --- src/core/store.ts | 43 +++++++++++++++++++++++++++++++++++++++---- src/core/use-toast.ts | 23 ++++++++++++++++++++++- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/src/core/store.ts b/src/core/store.ts index b20c9c4..7868640 100644 --- a/src/core/store.ts +++ b/src/core/store.ts @@ -1,10 +1,15 @@ import React from 'react'; import { Toast, ToastsOptions } from './types'; +export const TOAST_EXPIRE_DISMISS_DELAY = 1000; +const TOAST_LIMIT = 3; + type ActionType = { ADD_TOAST: 'ADD_TOAST'; REMOVE_TOAST: 'REMOVE_TOAST'; DISMISS_TOAST: 'DISMISS_TOAST'; + UPSERT_TOAST: 'UPSERT_TOAST'; + UPDATE_TOAST: 'UPDATE_TOAST'; PAUSE: 'PAUSE'; RESUME: 'RESUME'; }; @@ -22,6 +27,14 @@ type Action = toastId?: Toast['id']; type: ActionType['REMOVE_TOAST']; } + | { + type: ActionType['UPSERT_TOAST']; + toast: Toast; + } + | { + type: ActionType['UPDATE_TOAST']; + toast: Partial; + } | { type: ActionType['PAUSE']; time: number; @@ -39,9 +52,6 @@ interface State { const toastTimeouts = new Map>(); -export const TOAST_EXPIRE_DISMISS_DELAY = 0; -const TOAST_LIMIT = 3; - const addToRemoveQueue = (toastId: string) => { if (toastTimeouts.has(toastId)) { return; @@ -58,6 +68,13 @@ const addToRemoveQueue = (toastId: string) => { toastTimeouts.set(toastId, timeout); }; +const clearFromRemoveQueue = (toastId: string) => { + const timeout = toastTimeouts.get(toastId); + if (timeout) { + clearTimeout(timeout); + } +}; + export const reducer = (state: State, action: Action): State => { switch (action.type) { case 'ADD_TOAST': { @@ -70,7 +87,6 @@ export const reducer = (state: State, action: Action): State => { case 'DISMISS_TOAST': { const { toastId } = action; - // ! Side effects ! - This could be execrated into a dismissToast() action, but I'll keep it here for simplicity if (toastId) { addToRemoveQueue(toastId); } else { @@ -107,6 +123,25 @@ export const reducer = (state: State, action: Action): State => { }; } + case 'UPSERT_TOAST': + const { toast } = action; + return state.toasts.find((t) => t.id === toast.id) + ? reducer(state, { type: 'UPDATE_TOAST', toast }) + : reducer(state, { type: 'ADD_TOAST', toast }); + + case 'UPDATE_TOAST': { + if (action.toast.id) { + clearFromRemoveQueue(action.toast.id); + } + + return { + ...state, + toasts: state.toasts.map((t) => + t.id === action.toast.id ? { ...t, ...action.toast } : t + ), + }; + } + case 'PAUSE': { return { ...state, diff --git a/src/core/use-toast.ts b/src/core/use-toast.ts index fd559bc..9410653 100644 --- a/src/core/use-toast.ts +++ b/src/core/use-toast.ts @@ -4,6 +4,7 @@ import { dispatch, useStore } from './store'; import { Toast, ToastArg, + ToastOptions, ToasterType, ToastsOptions, isFunction, @@ -47,7 +48,7 @@ function createHandler(type?: ToasterType) { return (options: ToastArg) => { const toast = createToast(type, options); dispatch({ - type: 'ADD_TOAST', + type: 'UPSERT_TOAST', toast, }); return toast.id; @@ -59,6 +60,26 @@ toast.error = createHandler('error'); toast.success = createHandler('success'); toast.loading = createHandler('loading'); +toast.promise = ( + promise: Promise, + content: { + loading: ToastOptions; + success: ToastOptions; + error: ToastOptions; + } +) => { + const id = toast.loading(content.loading); + promise + .then((p) => { + toast.success({ ...content.success, id }); + return p; + }) + .catch(() => { + toast.error({ ...content.error, id }); + }); + return promise; +}; + toast.dismiss = (toastId?: string) => { dispatch({ type: 'DISMISS_TOAST', From 7375c2a7acce7ed7a504fb33208be088e07969d0 Mon Sep 17 00:00:00 2001 From: nhanluongoe Date: Tue, 9 Jan 2024 17:01:24 +0700 Subject: [PATCH 6/8] chore: update stories --- src/stories/Toast.stories.tsx | 62 ++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/src/stories/Toast.stories.tsx b/src/stories/Toast.stories.tsx index db64a91..513f5a6 100644 --- a/src/stories/Toast.stories.tsx +++ b/src/stories/Toast.stories.tsx @@ -71,7 +71,30 @@ export const Error: StoryFn = () => { toast.error({ title: `Scheduled: Catch up ${count}`, description: 'Friday, February 10, 2023 at 5:57 PM', - duration: 3 * 1000, + duration: 300 * 1000, + }); + setCount((prev) => prev + 1); + }} + > + Show Toast + + + ); +}; + +export const Loading: StoryFn = () => { + const [count, setCount] = useState(0); + + return ( +
+ +
); }; + +export const PromiseSupport: StoryFn = () => { + return ( +
+ + +
+ ); +}; From 233c580dda12a93e4397ef45b0e4fe9bf92ba26d Mon Sep 17 00:00:00 2001 From: nhanluongoe Date: Wed, 10 Jan 2024 10:22:33 +0700 Subject: [PATCH 7/8] feat: add warning toast --- src/components/icons.tsx | 38 +++++++++++++++++++++++++++++++++++ src/components/toast-icon.tsx | 5 ++++- src/core/types.ts | 7 ++++++- src/core/use-toast.ts | 1 + 4 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/components/icons.tsx b/src/components/icons.tsx index 3d22281..f4fd2da 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -100,3 +100,41 @@ const StyledSpinner = styled('i')` export const Spinner: React.FC = () => { return ; }; + +const StyledWarning = styled('i')` + & { + box-sizing: border-box; + position: relative; + display: block; + width: 30px; + height: 30px; + border: 2px solid; + border-radius: 40px; + color: #f6d776; + } + &::after, + &::before { + content: ''; + display: block; + box-sizing: border-box; + position: absolute; + border-radius: 3px; + width: 3px; + background: currentColor; + left: 11.5px; + color: #f6d776; + } + &::after { + top: 4px; + height: 14px; + } + &::before { + height: 3px; + bottom: 3px; + width: 3px; + border-radius: 9999px; + } +`; +export const Warning: React.FC = () => { + return ; +}; diff --git a/src/components/toast-icon.tsx b/src/components/toast-icon.tsx index 11cdb6b..2c5f6b0 100644 --- a/src/components/toast-icon.tsx +++ b/src/components/toast-icon.tsx @@ -1,7 +1,7 @@ import { keyframes, styled } from 'goober'; import React from 'react'; import { Toast } from '../core/types'; -import { Check, Close, Spinner } from './icons'; +import { Check, Close, Spinner, Warning } from './icons'; interface ToastIconProps { icon?: Toast['icon']; @@ -45,6 +45,9 @@ const ToastIcon: React.FC = (props) => { if (type === 'loading') { return ; } + if (type === 'warning') { + return ; + } return null; }; diff --git a/src/core/types.ts b/src/core/types.ts index f03fac2..0a3adfb 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -1,6 +1,11 @@ import React, { CSSProperties } from 'react'; -export type ToasterType = 'success' | 'error' | 'default' | 'loading'; +export type ToasterType = + | 'success' + | 'error' + | 'default' + | 'loading' + | 'warning'; export type Toast = { id: string; diff --git a/src/core/use-toast.ts b/src/core/use-toast.ts index 9410653..0efc2f0 100644 --- a/src/core/use-toast.ts +++ b/src/core/use-toast.ts @@ -59,6 +59,7 @@ const toast = (opts: ToastArg) => createHandler('default')(opts); toast.error = createHandler('error'); toast.success = createHandler('success'); toast.loading = createHandler('loading'); +toast.warning = createHandler('warning'); toast.promise = ( promise: Promise, From 0611393d20d86a4f4c6904da1e706970bfe65ec4 Mon Sep 17 00:00:00 2001 From: nhanluongoe Date: Wed, 10 Jan 2024 10:22:42 +0700 Subject: [PATCH 8/8] chore: update stories --- src/stories/Toast.stories.tsx | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/stories/Toast.stories.tsx b/src/stories/Toast.stories.tsx index 513f5a6..14fe067 100644 --- a/src/stories/Toast.stories.tsx +++ b/src/stories/Toast.stories.tsx @@ -105,6 +105,30 @@ export const Loading: StoryFn = () => { ); }; +export const Warning: StoryFn = () => { + return ( +
+ + +
+ ); +}; + export const CustomIcon: StoryFn = () => { const [count, setCount] = useState(0);