a tool for shared writing and social publishing
at main 4.8 kB view raw
1"use client"; 2import { animated, useTransition } from "@react-spring/web"; 3import { 4 createContext, 5 useCallback, 6 useContext, 7 useRef, 8 useState, 9} from "react"; 10import { CloseTiny } from "./Icons/CloseTiny"; 11 12type Toast = { 13 content: React.ReactNode; 14 type: "info" | "error" | "success"; 15 duration?: number; 16}; 17 18type Smoke = { 19 position: { x: number; y: number }; 20 text: React.ReactNode; 21 static?: boolean; 22 error?: boolean; 23 alignOnMobile?: "left" | "right" | "center" | undefined; 24}; 25 26type Smokes = Array<Smoke & { key: string }>; 27 28let PopUpContext = createContext({ 29 setSmokeState: (_f: (t: Smokes) => Smokes) => {}, 30 setToastState: (_t: Toast | null) => {}, 31}); 32 33export const useSmoker = () => { 34 let { setSmokeState: setState } = useContext(PopUpContext); 35 return (smoke: Smoke) => { 36 let key = Date.now().toString(); 37 setState((smokes) => smokes.concat([{ ...smoke, key }])); 38 setTimeout(() => { 39 setState((smokes) => smokes.filter((t) => t.key !== key)); 40 }, 2000); 41 }; 42}; 43export const useToaster = () => { 44 let { setToastState: toaster } = useContext(PopUpContext); 45 return toaster; 46}; 47export const PopUpProvider: React.FC<React.PropsWithChildren<unknown>> = ( 48 props, 49) => { 50 let [smokes, setState] = useState<Smokes>([]); 51 let [toastState, setToastState] = useState<Toast | null>(null); 52 let toastTimeout = useRef<number | null>(null); 53 let toaster = useCallback( 54 (toast: Toast | null) => { 55 if (toastTimeout.current) { 56 window.clearTimeout(toastTimeout.current); 57 toastTimeout.current = null; 58 } 59 setToastState(toast); 60 toastTimeout.current = window.setTimeout( 61 () => { 62 setToastState(null); 63 }, 64 toast?.duration ? toast.duration : 6000, 65 ); 66 }, 67 [setToastState], 68 ); 69 return ( 70 <PopUpContext.Provider 71 value={{ setSmokeState: setState, setToastState: toaster }} 72 > 73 {props.children} 74 {smokes.map((smoke) => ( 75 <Smoke 76 {...smoke.position} 77 error={smoke.error} 78 key={smoke.key} 79 static={smoke.static} 80 alignOnMobile={smoke.alignOnMobile} 81 > 82 {smoke.text} 83 </Smoke> 84 ))} 85 <Toast toast={toastState} setToast={setToastState} /> 86 </PopUpContext.Provider> 87 ); 88}; 89 90const Toast = (props: { 91 toast: Toast | null; 92 setToast: (t: Toast | null) => void; 93}) => { 94 let transitions = useTransition(props.toast ? [props.toast] : [], { 95 from: { top: -40 }, 96 enter: { top: 8 }, 97 leave: { top: -40 }, 98 config: {}, 99 }); 100 101 return transitions((style, item) => { 102 return item ? ( 103 <animated.div 104 style={style} 105 className={`toastAnimationWrapper fixed top-0 bottom-0 right-0 left-0 z-50 h-fit`} 106 > 107 <div 108 className={`toast absolute right-2 w-max shadow-md px-3 py-1 flex flex-row gap-2 rounded-full border text-center ${ 109 props.toast?.type === "error" 110 ? "border-white bg-[#dc143c] text-white border font-bold" 111 : props.toast?.type === "success" 112 ? "bg-accent-1 text-accent-2 border border-accent-2" 113 : "bg-accent-1 text-accent-2 border border-accent-2" 114 }`} 115 > 116 <div className="flex gap-2 grow justify-center">{item.content}</div> 117 <button 118 className="shrink-0" 119 onClick={() => { 120 props.setToast(null); 121 }} 122 > 123 <CloseTiny /> 124 </button> 125 </div> 126 </animated.div> 127 ) : null; 128 }); 129}; 130 131const Smoke: React.FC< 132 React.PropsWithChildren<{ 133 x: number; 134 y: number; 135 error?: boolean; 136 static?: boolean; 137 alignOnMobile?: "left" | "right" | "center" | undefined; 138 }> 139> = (props) => { 140 return ( 141 <div 142 className={`smoke w-max text-center pointer-events-none absolute z-50 rounded-full px-2 py-1 text-sm sm:-translate-x-1/2 ${ 143 props.alignOnMobile === "left" 144 ? "-translate-x-full" 145 : props.alignOnMobile === "right" 146 ? "" 147 : "-translate-x-1/2" 148 } 149 ${ 150 props.error 151 ? "border-white bg-[#dc143c] text-white border font-bold" 152 : "bg-accent-1 text-accent-2" 153 }`} 154 > 155 <style jsx>{` 156 .smoke { 157 left: ${props.x}px; 158 top: ${props.y}px; 159 animation-name: fadeout; 160 animation-duration: 2s; 161 } 162 163 @keyframes fadeout { 164 from { 165 ${props.static ? "" : `top: ${props.y - 20}px;`} 166 opacity: 100%; 167 } 168 169 to { 170 ${props.static ? "" : `top: ${props.y - 60}px;`} 171 opacity: 0%; 172 } 173 } 174 `}</style> 175 {props.children} 176 </div> 177 ); 178};