index.tsx 3.7 KB

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