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