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:
Matthias
2026-03-27 18:14:04 +01:00
parent 5da0204163
commit 2f89465e82
35 changed files with 2822 additions and 186 deletions

View File

@@ -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;