11"use client" ;
22import { useEffect , useState } from "react" ;
3+ import { createPortal } from "react-dom" ;
34import { XMarkIcon } from "@heroicons/react/20/solid" ;
45
56const HIDE_DELAY_MS = 2500 ;
7+ const HIDE_TRANSITION_MS = 200 ;
68
79const Notification = ( { show = false , setShow = ( ) => { } , message = "" } ) => {
8- const [ isVisible , setIsVisible ] = useState ( show ) ;
10+ const [ isMounted , setIsMounted ] = useState ( false ) ;
11+ const [ isVisible , setIsVisible ] = useState ( false ) ;
12+
13+ useEffect ( ( ) => {
14+ setIsMounted ( true ) ;
15+ return ( ) => setIsMounted ( false ) ;
16+ } , [ ] ) ;
917
1018 useEffect ( ( ) => {
1119 if ( show ) {
@@ -17,40 +25,45 @@ const Notification = ( { show = false, setShow = () => { }, message = "" } ) =>
1725 return ( ) => clearTimeout ( timer ) ;
1826 }
1927
20- const hideTimer = setTimeout ( ( ) => setIsVisible ( false ) , 200 ) ;
21- return ( ) => clearTimeout ( hideTimer ) ;
22- } , [ show , setShow ] ) ;
28+ if ( isVisible ) {
29+ const hideTimer = setTimeout ( ( ) => setIsVisible ( false ) , HIDE_TRANSITION_MS ) ;
30+ return ( ) => clearTimeout ( hideTimer ) ;
31+ }
32+ return undefined ;
33+ } , [ show , setShow , isVisible ] ) ;
2334
24- if ( ! isVisible && ! show ) {
35+ if ( ! isMounted || ( ! isVisible && ! show ) ) {
2536 return null ;
2637 }
2738
28- return (
39+ const toast = (
2940 < div
3041 aria-live = "assertive"
31- className = "pointer-events-none fixed inset-0 z-[2000] flex items-center justify-center p-4 "
42+ className = "pointer-events-none fixed inset-0 z-[2000] flex items-start justify-end px-4 pt-6 pb-4 sm:items-start sm:px-6 "
3243 >
3344 < div
34- className = { `pointer-events-auto w-full max-w-md transform rounded-lg bg-white/10 glass backdrop px-6 py-8 text-white shadow-2xl ring-1 ring-black/20 transition-all duration-200 ${ show ? "opacity-100 translate-y-0" : "opacity-0 - translate-y-3" } ` }
45+ className = { `pointer-events-auto w-full max-w-sm transform rounded-2xl glass bg-white backdrop px-6 py-4 text-slate-900 shadow-2xl ring-1 ring-black/20 transition-all duration-200 ${ show ? "translate-y-0 opacity-100 " : "- translate-y-3 opacity-0 " } ` }
3546 role = "status"
3647 >
3748 < div className = "flex items-start gap-4" >
38- < p className = "flex-1 text-base font-semibold leading-6" > { message } </ p >
49+ < p className = "flex-1 text-lg font-medium leading-6" > { message } </ p >
3950 < button
4051 type = "button"
41- className = "rounded-md p-1 text-slate-300 transition hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-slate-200 focus-visible:ring-offset-slate-900"
52+ className = "rounded-md p-1 text-slate-600 transition hover:text-slate-900 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-slate-200 focus-visible:ring-offset-slate-900"
4253 onClick = { ( ) => {
4354 setIsVisible ( false ) ;
4455 setShow ( false ) ;
4556 } }
4657 >
4758 < span className = "sr-only" > Dismiss notification</ span >
48- < XMarkIcon className = "h-5 w-5" aria-hidden = "true" />
59+ < XMarkIcon className = "h-5 w-5 text-red-700/80 " aria-hidden = "true" />
4960 </ button >
5061 </ div >
5162 </ div >
5263 </ div >
5364 ) ;
65+
66+ return createPortal ( toast , document . body ) ;
5467} ;
5568
5669export default Notification ;
0 commit comments