index.tsx 3.9 KB

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