a tool for shared writing and social publishing
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};