feat: integrate Sentry for error tracking and enhance user notifications
- Added Sentry integration for error tracking across various components, including error boundaries and user actions. - Updated global error handling to capture exceptions and provide detailed feedback to users. - Enhanced user notifications with toast messages for actions such as credit management, image generation, and canvas exports. - Improved user experience by displaying relevant messages during interactions, ensuring better visibility of system states and errors.
This commit is contained in:
169
lib/toast.ts
169
lib/toast.ts
@@ -1,82 +1,109 @@
|
||||
import type { ReactNode } from "react"
|
||||
import { isValidElement } from "react"
|
||||
import { toast as sonnerToast, type ExternalToast } from "sonner"
|
||||
import { gooeyToast, type GooeyPromiseData } from "goey-toast";
|
||||
|
||||
const SUCCESS_DURATION = 4000
|
||||
const ERROR_DURATION = 6000
|
||||
const DURATION = {
|
||||
success: 4000,
|
||||
successShort: 2000,
|
||||
error: 6000,
|
||||
warning: 5000,
|
||||
info: 4000,
|
||||
} as const;
|
||||
|
||||
type SonnerPromiseInput<T> = Parameters<typeof sonnerToast.promise<T>>[0]
|
||||
type SonnerPromiseOptions<T> = Parameters<typeof sonnerToast.promise<T>>[1]
|
||||
type SonnerPromiseData<T> = NonNullable<SonnerPromiseOptions<T>>
|
||||
|
||||
function hasMessage(
|
||||
value: unknown,
|
||||
): value is {
|
||||
message: ReactNode
|
||||
duration?: number
|
||||
} {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
!isValidElement(value) &&
|
||||
"message" in value
|
||||
)
|
||||
}
|
||||
|
||||
function withStateDuration<T>(state: unknown, duration: number): unknown {
|
||||
if (state === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (typeof state === "function") {
|
||||
return async (value: T) => {
|
||||
const result = await state(value)
|
||||
return withStateDuration(result, duration)
|
||||
}
|
||||
}
|
||||
|
||||
if (hasMessage(state)) {
|
||||
return {
|
||||
...state,
|
||||
duration: state.duration ?? duration,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
message: state as ReactNode,
|
||||
duration,
|
||||
}
|
||||
}
|
||||
export type ToastDurationOverrides = {
|
||||
duration?: number;
|
||||
};
|
||||
|
||||
export const toast = {
|
||||
success(message: ReactNode, options?: ExternalToast) {
|
||||
return sonnerToast.success(message, {
|
||||
...options,
|
||||
duration: options?.duration ?? SUCCESS_DURATION,
|
||||
})
|
||||
success(
|
||||
message: string,
|
||||
description?: string,
|
||||
opts?: ToastDurationOverrides,
|
||||
) {
|
||||
return gooeyToast.success(message, {
|
||||
description,
|
||||
duration: opts?.duration ?? DURATION.success,
|
||||
});
|
||||
},
|
||||
error(message: ReactNode, options?: ExternalToast) {
|
||||
return sonnerToast.error(message, {
|
||||
...options,
|
||||
duration: options?.duration ?? ERROR_DURATION,
|
||||
})
|
||||
|
||||
error(
|
||||
message: string,
|
||||
description?: string,
|
||||
opts?: ToastDurationOverrides,
|
||||
) {
|
||||
return gooeyToast.error(message, {
|
||||
description,
|
||||
duration: opts?.duration ?? DURATION.error,
|
||||
});
|
||||
},
|
||||
loading(message: ReactNode, options?: ExternalToast) {
|
||||
return sonnerToast.loading(message, options)
|
||||
|
||||
warning(
|
||||
message: string,
|
||||
description?: string,
|
||||
opts?: ToastDurationOverrides,
|
||||
) {
|
||||
return gooeyToast.warning(message, {
|
||||
description,
|
||||
duration: opts?.duration ?? DURATION.warning,
|
||||
});
|
||||
},
|
||||
dismiss(id?: number | string) {
|
||||
return sonnerToast.dismiss(id)
|
||||
|
||||
info(
|
||||
message: string,
|
||||
description?: string,
|
||||
opts?: ToastDurationOverrides,
|
||||
) {
|
||||
return gooeyToast.info(message, {
|
||||
description,
|
||||
duration: opts?.duration ?? DURATION.info,
|
||||
});
|
||||
},
|
||||
promise<T>(promise: SonnerPromiseInput<T>, options?: SonnerPromiseOptions<T>) {
|
||||
return sonnerToast.promise(promise, {
|
||||
...options,
|
||||
success: withStateDuration<T>(options?.success, SUCCESS_DURATION) as SonnerPromiseData<T>["success"],
|
||||
error: withStateDuration<T>(options?.error, ERROR_DURATION) as SonnerPromiseData<T>["error"],
|
||||
})
|
||||
|
||||
promise<T>(promise: Promise<T>, data: GooeyPromiseData<T>) {
|
||||
return gooeyToast.promise(promise, data);
|
||||
},
|
||||
}
|
||||
|
||||
action(
|
||||
message: string,
|
||||
opts: {
|
||||
description?: string;
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
successLabel?: string;
|
||||
type?: "success" | "info" | "warning";
|
||||
duration?: number;
|
||||
},
|
||||
) {
|
||||
const t = opts.type ?? "info";
|
||||
return gooeyToast[t](message, {
|
||||
description: opts.description,
|
||||
duration: opts.duration ?? (t === "success" ? DURATION.success : DURATION.info),
|
||||
action: {
|
||||
label: opts.label,
|
||||
onClick: opts.onClick,
|
||||
successLabel: opts.successLabel,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
update(
|
||||
id: string | number,
|
||||
opts: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
type?: "default" | "success" | "error" | "warning" | "info";
|
||||
},
|
||||
) {
|
||||
gooeyToast.update(id, opts);
|
||||
},
|
||||
|
||||
dismiss(id?: string | number) {
|
||||
gooeyToast.dismiss(id);
|
||||
},
|
||||
};
|
||||
|
||||
export const toastDuration = {
|
||||
success: SUCCESS_DURATION,
|
||||
error: ERROR_DURATION,
|
||||
} as const
|
||||
success: DURATION.success,
|
||||
successShort: DURATION.successShort,
|
||||
error: DURATION.error,
|
||||
warning: DURATION.warning,
|
||||
info: DURATION.info,
|
||||
} as const;
|
||||
|
||||
Reference in New Issue
Block a user