index.tsx 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. 'use client'
  2. import classNames from 'classnames'
  3. import type { ReactNode } from 'react'
  4. import React, { useEffect, useState } from 'react'
  5. import { createRoot } from 'react-dom/client'
  6. import {
  7. CheckCircleIcon,
  8. ExclamationTriangleIcon,
  9. InformationCircleIcon,
  10. XCircleIcon,
  11. } from '@heroicons/react/20/solid'
  12. import { createContext } from 'use-context-selector'
  13. export type IToastProps = {
  14. type?: 'success' | 'error' | 'warning' | 'info'
  15. duration?: number
  16. message: string
  17. children?: ReactNode
  18. onClose?: () => void
  19. }
  20. type IToastContext = {
  21. notify: (props: IToastProps) => void
  22. }
  23. const defaultDuring = 3000
  24. export const ToastContext = createContext<IToastContext>({} as IToastContext)
  25. const Toast = ({
  26. type = 'info',
  27. duration,
  28. message,
  29. children,
  30. }: IToastProps) => {
  31. // sometimes message is react node array. Not handle it.
  32. if (typeof message !== 'string') {
  33. return null
  34. }
  35. return <div className={classNames(
  36. 'fixed rounded-md p-4 my-4 mx-8 z-50',
  37. 'top-0',
  38. 'right-0',
  39. type === 'success' ? 'bg-green-50' : '',
  40. type === 'error' ? 'bg-red-50' : '',
  41. type === 'warning' ? 'bg-yellow-50' : '',
  42. type === 'info' ? 'bg-blue-50' : '',
  43. )}>
  44. <div className="flex">
  45. <div className="flex-shrink-0">
  46. {type === 'success' && <CheckCircleIcon className="w-5 h-5 text-green-400" aria-hidden="true" />}
  47. {type === 'error' && <XCircleIcon className="w-5 h-5 text-red-400" aria-hidden="true" />}
  48. {type === 'warning' && <ExclamationTriangleIcon className="w-5 h-5 text-yellow-400" aria-hidden="true" />}
  49. {type === 'info' && <InformationCircleIcon className="w-5 h-5 text-blue-400" aria-hidden="true" />}
  50. </div>
  51. <div className="ml-3">
  52. <h3 className={
  53. classNames(
  54. 'text-sm font-medium',
  55. type === 'success' ? 'text-green-800' : '',
  56. type === 'error' ? 'text-red-800' : '',
  57. type === 'warning' ? 'text-yellow-800' : '',
  58. type === 'info' ? 'text-blue-800' : '',
  59. )
  60. }>{message}</h3>
  61. {children && <div className={
  62. classNames(
  63. 'mt-2 text-sm',
  64. type === 'success' ? 'text-green-700' : '',
  65. type === 'error' ? 'text-red-700' : '',
  66. type === 'warning' ? 'text-yellow-700' : '',
  67. type === 'info' ? 'text-blue-700' : '',
  68. )
  69. }>
  70. {children}
  71. </div>
  72. }
  73. </div>
  74. </div>
  75. </div>
  76. }
  77. export const ToastProvider = ({
  78. children,
  79. }: {
  80. children: ReactNode
  81. }) => {
  82. const placeholder: IToastProps = {
  83. type: 'info',
  84. message: 'Toast message',
  85. duration: 3000,
  86. }
  87. const [params, setParams] = React.useState<IToastProps>(placeholder)
  88. const [mounted, setMounted] = useState(false)
  89. useEffect(() => {
  90. if (mounted) {
  91. setTimeout(() => {
  92. setMounted(false)
  93. }, params.duration || defaultDuring)
  94. }
  95. }, [mounted])
  96. return <ToastContext.Provider value={{
  97. notify: (props) => {
  98. setMounted(true)
  99. setParams(props)
  100. },
  101. }}>
  102. {mounted && <Toast {...params} />}
  103. {children}
  104. </ToastContext.Provider>
  105. }
  106. Toast.notify = ({
  107. type,
  108. message,
  109. duration,
  110. }: Pick<IToastProps, 'type' | 'message' | 'duration'>) => {
  111. if (typeof window === 'object') {
  112. const holder = document.createElement('div')
  113. const root = createRoot(holder)
  114. root.render(<Toast type={type} message={message} duration={duration} />)
  115. document.body.appendChild(holder)
  116. setTimeout(() => {
  117. if (holder)
  118. holder.remove()
  119. }, duration || defaultDuring)
  120. }
  121. }
  122. export default Toast