a tool for shared writing and social publishing
at test/unknown-marks 182 lines 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 mass: 8, 100 friction: 150, 101 tension: 2000, 102 }, 103 }); 104 105 return transitions((style, item) => { 106 return item ? ( 107 <animated.div 108 style={style} 109 className={`toastAnimationWrapper fixed bottom-0 right-0 left-0 z-50 h-fit`} 110 > 111 <div 112 className={`toast absolute right-2 w-max shadow-md px-3 py-1 flex flex-row gap-2 rounded-full border text-center ${ 113 props.toast?.type === "error" 114 ? "border-white bg-[#dc143c] text-white border font-bold" 115 : props.toast?.type === "success" 116 ? "bg-accent-1 text-accent-2 border border-accent-2" 117 : "bg-accent-1 text-accent-2 border border-accent-2" 118 }`} 119 > 120 <div className="flex gap-2 grow justify-center">{item.content}</div> 121 <button 122 className="shrink-0" 123 onClick={() => { 124 props.setToast(null); 125 }} 126 > 127 <CloseTiny /> 128 </button> 129 </div> 130 </animated.div> 131 ) : null; 132 }); 133}; 134 135const Smoke: React.FC< 136 React.PropsWithChildren<{ 137 x: number; 138 y: number; 139 error?: boolean; 140 static?: boolean; 141 alignOnMobile?: "left" | "right" | "center" | undefined; 142 }> 143> = (props) => { 144 return ( 145 <div 146 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 ${ 147 props.alignOnMobile === "left" 148 ? "-translate-x-full" 149 : props.alignOnMobile === "right" 150 ? "" 151 : "-translate-x-1/2" 152 } 153 ${ 154 props.error 155 ? "border-white bg-[#dc143c] text-white border font-bold" 156 : "bg-accent-1 text-accent-2" 157 }`} 158 > 159 <style jsx>{` 160 .smoke { 161 left: ${props.x}px; 162 top: ${props.y}px; 163 animation-name: fadeout; 164 animation-duration: 2s; 165 } 166 167 @keyframes fadeout { 168 from { 169 ${props.static ? "" : `top: ${props.y - 20}px;`} 170 opacity: 100%; 171 } 172 173 to { 174 ${props.static ? "" : `top: ${props.y - 60}px;`} 175 opacity: 0%; 176 } 177 } 178 `}</style> 179 {props.children} 180 </div> 181 ); 182};