blob: 66b24fc441d11fcd212c0c962b4965c209fa8344 [file] [log] [blame]
giod0026612025-05-08 13:00:36 +00001"use client";
gio5f2f1002025-03-20 18:38:48 +04002
3// Inspired by react-hot-toast library
giod0026612025-05-08 13:00:36 +00004import * as React from "react";
gio5f2f1002025-03-20 18:38:48 +04005
giod0026612025-05-08 13:00:36 +00006import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
gio5f2f1002025-03-20 18:38:48 +04007
giod0026612025-05-08 13:00:36 +00008const TOAST_LIMIT = 1;
9const TOAST_REMOVE_DELAY = 1000000;
gio5f2f1002025-03-20 18:38:48 +040010
11type ToasterToast = ToastProps & {
giod0026612025-05-08 13:00:36 +000012 id: string;
13 title?: React.ReactNode;
14 description?: React.ReactNode;
15 action?: ToastActionElement;
16};
gio5f2f1002025-03-20 18:38:48 +040017
18const actionTypes = {
giod0026612025-05-08 13:00:36 +000019 ADD_TOAST: "ADD_TOAST",
20 UPDATE_TOAST: "UPDATE_TOAST",
21 DISMISS_TOAST: "DISMISS_TOAST",
22 REMOVE_TOAST: "REMOVE_TOAST",
23} as const;
gio5f2f1002025-03-20 18:38:48 +040024
giod0026612025-05-08 13:00:36 +000025let count = 0;
gio5f2f1002025-03-20 18:38:48 +040026
27function genId() {
giod0026612025-05-08 13:00:36 +000028 count = (count + 1) % Number.MAX_SAFE_INTEGER;
29 return count.toString();
gio5f2f1002025-03-20 18:38:48 +040030}
31
giod0026612025-05-08 13:00:36 +000032type ActionType = typeof actionTypes;
gio5f2f1002025-03-20 18:38:48 +040033
34type Action =
giod0026612025-05-08 13:00:36 +000035 | {
36 type: ActionType["ADD_TOAST"];
37 toast: ToasterToast;
38 }
39 | {
40 type: ActionType["UPDATE_TOAST"];
41 toast: Partial<ToasterToast>;
42 }
43 | {
44 type: ActionType["DISMISS_TOAST"];
45 toastId?: ToasterToast["id"];
46 }
47 | {
48 type: ActionType["REMOVE_TOAST"];
49 toastId?: ToasterToast["id"];
50 };
gio5f2f1002025-03-20 18:38:48 +040051
52interface State {
giod0026612025-05-08 13:00:36 +000053 toasts: ToasterToast[];
gio5f2f1002025-03-20 18:38:48 +040054}
55
giod0026612025-05-08 13:00:36 +000056const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
gio5f2f1002025-03-20 18:38:48 +040057
58const addToRemoveQueue = (toastId: string) => {
giod0026612025-05-08 13:00:36 +000059 if (toastTimeouts.has(toastId)) {
60 return;
61 }
gio5f2f1002025-03-20 18:38:48 +040062
giod0026612025-05-08 13:00:36 +000063 const timeout = setTimeout(() => {
64 toastTimeouts.delete(toastId);
65 dispatch({
66 type: "REMOVE_TOAST",
67 toastId: toastId,
68 });
69 }, TOAST_REMOVE_DELAY);
gio5f2f1002025-03-20 18:38:48 +040070
giod0026612025-05-08 13:00:36 +000071 toastTimeouts.set(toastId, timeout);
72};
gio5f2f1002025-03-20 18:38:48 +040073
74export const reducer = (state: State, action: Action): State => {
giod0026612025-05-08 13:00:36 +000075 switch (action.type) {
76 case "ADD_TOAST":
77 return {
78 ...state,
79 toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
80 };
gio5f2f1002025-03-20 18:38:48 +040081
giod0026612025-05-08 13:00:36 +000082 case "UPDATE_TOAST":
83 return {
84 ...state,
85 toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
86 };
gio5f2f1002025-03-20 18:38:48 +040087
giod0026612025-05-08 13:00:36 +000088 case "DISMISS_TOAST": {
89 const { toastId } = action;
gio5f2f1002025-03-20 18:38:48 +040090
giod0026612025-05-08 13:00:36 +000091 // ! Side effects ! - This could be extracted into a dismissToast() action,
92 // but I'll keep it here for simplicity
93 if (toastId) {
94 addToRemoveQueue(toastId);
95 } else {
96 state.toasts.forEach((toast) => {
97 addToRemoveQueue(toast.id);
98 });
99 }
gio5f2f1002025-03-20 18:38:48 +0400100
giod0026612025-05-08 13:00:36 +0000101 return {
102 ...state,
103 toasts: state.toasts.map((t) =>
104 t.id === toastId || toastId === undefined
105 ? {
106 ...t,
107 open: false,
108 }
109 : t,
110 ),
111 };
112 }
113 case "REMOVE_TOAST":
114 if (action.toastId === undefined) {
115 return {
116 ...state,
117 toasts: [],
118 };
119 }
120 return {
121 ...state,
122 toasts: state.toasts.filter((t) => t.id !== action.toastId),
123 };
124 }
125};
gio5f2f1002025-03-20 18:38:48 +0400126
giod0026612025-05-08 13:00:36 +0000127const listeners: Array<(state: State) => void> = [];
gio5f2f1002025-03-20 18:38:48 +0400128
giod0026612025-05-08 13:00:36 +0000129let memoryState: State = { toasts: [] };
gio5f2f1002025-03-20 18:38:48 +0400130
131function dispatch(action: Action) {
giod0026612025-05-08 13:00:36 +0000132 memoryState = reducer(memoryState, action);
133 listeners.forEach((listener) => {
134 listener(memoryState);
135 });
gio5f2f1002025-03-20 18:38:48 +0400136}
137
giod0026612025-05-08 13:00:36 +0000138type Toast = Omit<ToasterToast, "id">;
gio5f2f1002025-03-20 18:38:48 +0400139
140function toast({ ...props }: Toast) {
giod0026612025-05-08 13:00:36 +0000141 const id = genId();
gio5f2f1002025-03-20 18:38:48 +0400142
giod0026612025-05-08 13:00:36 +0000143 const update = (props: ToasterToast) =>
144 dispatch({
145 type: "UPDATE_TOAST",
146 toast: { ...props, id },
147 });
148 const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
gio5f2f1002025-03-20 18:38:48 +0400149
giod0026612025-05-08 13:00:36 +0000150 dispatch({
151 type: "ADD_TOAST",
152 toast: {
153 ...props,
154 id,
155 open: true,
156 onOpenChange: (open) => {
157 if (!open) dismiss();
158 },
159 },
160 });
gio5f2f1002025-03-20 18:38:48 +0400161
giod0026612025-05-08 13:00:36 +0000162 return {
163 id: id,
164 dismiss,
165 update,
166 };
gio5f2f1002025-03-20 18:38:48 +0400167}
168
169function useToast() {
giod0026612025-05-08 13:00:36 +0000170 const [state, setState] = React.useState<State>(memoryState);
gio5f2f1002025-03-20 18:38:48 +0400171
giod0026612025-05-08 13:00:36 +0000172 React.useEffect(() => {
173 listeners.push(setState);
174 return () => {
175 const index = listeners.indexOf(setState);
176 if (index > -1) {
177 listeners.splice(index, 1);
178 }
179 };
180 }, [state]);
gio5f2f1002025-03-20 18:38:48 +0400181
giod0026612025-05-08 13:00:36 +0000182 return {
183 ...state,
184 toast,
185 dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
186 };
gio5f2f1002025-03-20 18:38:48 +0400187}
188
giod0026612025-05-08 13:00:36 +0000189export { useToast, toast };