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 });
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};