| 1 | export type ToastType = "error" | "success"; |
| 2 | |
| 3 | export interface ToastOptions { |
| 4 | message: string; |
| 5 | type?: ToastType; |
| 6 | duration?: number; |
| 7 | } |
| 8 | |
| 9 | export interface ToastState { |
| 10 | message: string; |
| 11 | type: ToastType; |
| 12 | visible: boolean; |
| 13 | } |
| 14 | |
| 15 | type ToastListener = (state: ToastState | null) => void; |
| 16 | |
| 17 | const DEFAULT_DURATION = 3000; |
| 18 | |
| 19 | let listener: ToastListener | null = null; |
| 20 | let timer: number | null = null; |
| 21 | let currentToast: ToastState | null = null; |
| 22 | |
| 23 | function clearTimer() { |
| 24 | if (timer !== null) { |
| 25 | window.clearTimeout(timer); |
| 26 | timer = null; |
| 27 | } |
| 28 | } |
| 29 | |
| 30 | function emit(state: ToastState | null) { |
| 31 | listener?.(state); |
| 32 | } |
| 33 | |
| 34 | function normalizeOptions( |
| 35 | options: ToastOptions | string, |
| 36 | type?: ToastType, |
| 37 | duration?: number, |
| 38 | ): Required<Pick<ToastOptions, "message" | "type" | "duration">> { |
| 39 | if (typeof options === "string") { |
| 40 | return { |
| 41 | message: options, |
| 42 | type: type ?? "error", |
| 43 | duration: duration ?? DEFAULT_DURATION, |
| 44 | }; |
| 45 | } |
| 46 | return { |
| 47 | message: options.message, |
| 48 | type: options.type ?? "error", |
| 49 | duration: options.duration ?? DEFAULT_DURATION, |
| 50 | }; |
| 51 | } |
| 52 | |
| 53 | function showToast(options: ToastOptions | string) { |
| 54 | const { message, type, duration } = normalizeOptions(options); |
| 55 | |
| 56 | clearTimer(); |
| 57 | currentToast = { message, type, visible: true }; |
| 58 | emit(currentToast); |
| 59 | |
| 60 | timer = window.setTimeout(() => { |
| 61 | if (currentToast) { |
| 62 | currentToast = { ...currentToast, visible: false }; |
| 63 | emit(currentToast); |
| 64 | } |
| 65 | timer = null; |
| 66 | }, duration); |
| 67 | } |
| 68 | |
| 69 | export function registerToastListener(fn: ToastListener): () => void { |
| 70 | listener = fn; |
| 71 | return () => { |
| 72 | if (listener === fn) { |
| 73 | listener = null; |
| 74 | } |
| 75 | }; |
| 76 | } |
| 77 | |
| 78 | export const toast = { |
| 79 | show(options: ToastOptions | string) { |
| 80 | showToast(options); |
| 81 | }, |
| 82 | error(message: string, duration?: number) { |
| 83 | showToast({ message, type: "error", duration }); |
| 84 | }, |
| 85 | success(message: string, duration?: number) { |
| 86 | showToast({ message, type: "success", duration }); |
| 87 | }, |
| 88 | }; |
| 89 |