add basic toaster

This commit is contained in:
Nate Kelley 2025-02-21 15:26:57 -07:00
parent 59beb494c4
commit c301821c4f
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
6 changed files with 129 additions and 65 deletions

12
web/package-lock.json generated
View File

@ -87,6 +87,7 @@
"react-virtualized-auto-sizer": "^1.0.25", "react-virtualized-auto-sizer": "^1.0.25",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
"sonner": "^2.0.1",
"split-pane-react": "^0.1.3", "split-pane-react": "^0.1.3",
"tailwind-merge": "^3.0.1", "tailwind-merge": "^3.0.1",
"utility-types": "^3.11.0", "utility-types": "^3.11.0",
@ -2412,6 +2413,7 @@
}, },
"node_modules/@clack/prompts/node_modules/is-unicode-supported": { "node_modules/@clack/prompts/node_modules/is-unicode-supported": {
"version": "1.3.0", "version": "1.3.0",
"extraneous": true,
"inBundle": true, "inBundle": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -23496,6 +23498,16 @@
"node": ">=10.0.0" "node": ">=10.0.0"
} }
}, },
"node_modules/sonner": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.1.tgz",
"integrity": "sha512-FRBphaehZ5tLdLcQ8g2WOIRE+Y7BCfWi5Zyd8bCvBjiW8TxxAyoWZIxS661Yz6TGPqFQ4VLzOF89WEYhfynSFQ==",
"license": "MIT",
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/source-map": { "node_modules/source-map": {
"version": "0.5.7", "version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",

View File

@ -96,6 +96,7 @@
"react-virtualized-auto-sizer": "^1.0.25", "react-virtualized-auto-sizer": "^1.0.25",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
"sonner": "^2.0.1",
"split-pane-react": "^0.1.3", "split-pane-react": "^0.1.3",
"tailwind-merge": "^3.0.1", "tailwind-merge": "^3.0.1",
"utility-types": "^3.11.0", "utility-types": "^3.11.0",

View File

@ -0,0 +1,30 @@
'use client';
import { useTheme } from 'next-themes';
import { Toaster } from 'sonner';
type ToasterProps = React.ComponentProps<typeof Toaster>;
const AppToaster = ({ ...props }: ToasterProps) => {
const { theme = 'system' } = useTheme();
return (
<Toaster
position="top-center"
theme={theme as ToasterProps['theme']}
className="toaster group"
toastOptions={{
classNames: {
toast:
'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
description: 'group-[.toast]:text-muted-foreground',
actionButton: 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
cancelButton: 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground'
}
}}
{...props}
/>
);
};
export { AppToaster };

View File

@ -0,0 +1 @@
export * from './AppToaster';

View File

@ -1,6 +1,7 @@
import React, { PropsWithChildren } from 'react'; import React, { PropsWithChildren } from 'react';
import { App, ModalFuncProps } from 'antd'; // import { App, ModalFuncProps } from 'antd';
import { createStyles } from 'antd-style'; import { toast, type ExternalToast } from 'sonner';
// import { createStyles } from 'antd-style';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { import {
useContextSelector, useContextSelector,
@ -18,39 +19,57 @@ export interface NotificationProps {
duration?: number; duration?: number;
} }
const useStyles = createStyles(({ token, css }) => ({ // const useStyles = createStyles(({ token, css }) => ({
modal: css` // modal: css`
.busterv2-modal-body { // .busterv2-modal-body {
padding: 0px !important; // padding: 0px !important;
} // }
.busterv2-modal-confirm-body { // .busterv2-modal-confirm-body {
padding: 24px 32px 16px 32px !important; // padding: 24px 32px 16px 32px !important;
} // }
.busterv2-modal-confirm-btns { // .busterv2-modal-confirm-btns {
margin-top: 0px !important; // margin-top: 0px !important;
padding: 12px 32px !important; // padding: 12px 32px !important;
border-top: 0.5px solid ${token.colorBorder}; // border-top: 0.5px solid ${token.colorBorder};
display: flex; // display: flex;
align-items: center; // align-items: center;
justify-content: flex-end; // justify-content: flex-end;
} // }
.busterv2-modal-confirm-content { // .busterv2-modal-confirm-content {
color: ${token.colorTextSecondary} !important; // color: ${token.colorTextSecondary} !important;
} // }
` // `
})); // }));
export const useBusterNotificationsInternal = () => { export const useBusterNotificationsInternal = () => {
const { message, notification, modal } = App.useApp(); // const { message, notification, modal } = App.useApp();
// const { cx, styles } = useStyles();
const { cx, styles } = useStyles();
const openNotification = useMemoizedFn( const openNotification = useMemoizedFn(
(props: { title?: string; message: string; type: NotificationType }) => { (props: { title?: string; message: string; type: NotificationType }) => {
notification?.open?.({ ...props, description: props.message, message: props.title }); const { title, message, type } = props;
const toastOptions: ExternalToast = {
description: message,
position: 'top-center'
};
switch (type) {
case 'success':
return toast.success(title, toastOptions);
case 'info':
return toast.info(title, toastOptions);
case 'warning':
return toast.warning(title, toastOptions);
case 'error':
return toast.error(title, toastOptions);
default:
const _never: never = type;
return '';
}
} }
); );
@ -59,24 +78,24 @@ export const useBusterNotificationsInternal = () => {
const type = values.type || 'error'; const type = values.type || 'error';
const title = values.title || 'Error'; const title = values.title || 'Error';
const message = values.message || 'Something went wrong. Please try again.'; const message = values.message || 'Something went wrong. Please try again.';
openNotification({ ...values, message, title, type }); return openNotification({ ...values, message, title, type });
}); });
const openInfoNotification = useMemoizedFn( const openInfoNotification = useMemoizedFn(
({ type = 'info', message = 'Info', title = 'Info', ...props }: NotificationProps) => { ({ type = 'info', message = 'Info', title = 'Info', ...props }: NotificationProps) => {
openNotification({ ...props, title, message, type }); return openNotification({ ...props, title, message, type });
} }
); );
const openSuccessNotification = useMemoizedFn( const openSuccessNotification = useMemoizedFn(
({ type = 'success', title = 'Success', message = 'success', ...props }: NotificationProps) => { ({ type = 'success', title = 'Success', message = 'success', ...props }: NotificationProps) => {
openNotification({ ...props, message, title, type }); return openNotification({ ...props, message, title, type });
} }
); );
const openWarningNotification = useMemoizedFn( const openWarningNotification = useMemoizedFn(
({ type = 'warning', title = 'Warning', message = 'Warning', ...props }: NotificationProps) => { ({ type = 'warning', title = 'Warning', message = 'Warning', ...props }: NotificationProps) => {
openNotification({ ...props, message, title, type }); return openNotification({ ...props, message, title, type });
} }
); );
@ -86,28 +105,27 @@ export const useBusterNotificationsInternal = () => {
(props: { (props: {
type: NotificationType; type: NotificationType;
message: string; message: string;
loading?: boolean;
onClose?: () => void; onClose?: () => void;
duration?: number; duration?: number;
}) => { }) => {
if (props.loading) { return openNotification({
message.loading(props.message, props.duration, props.onClose); ...props,
} else { title: props.message,
message?.[props.type]?.(props.message, props.duration, props.onClose); message: ''
} });
} }
); );
const openErrorMessage = useMemoizedFn((message: string) => { const openErrorMessage = useMemoizedFn((message: string) => {
openMessage({ type: 'error', message }); return openMessage({ type: 'error', message });
}); });
const openInfoMessage = useMemoizedFn((message: string, duration?: number) => { const openInfoMessage = useMemoizedFn((message: string, duration?: number) => {
openMessage({ type: 'info', message, duration }); return openMessage({ type: 'info', message, duration });
}); });
const openSuccessMessage = useMemoizedFn((message: string) => { const openSuccessMessage = useMemoizedFn((message: string) => {
openMessage({ type: 'success', message }); return openMessage({ type: 'success', message });
}); });
const openConfirmModal = useMemoizedFn( const openConfirmModal = useMemoizedFn(
@ -117,36 +135,34 @@ export const useBusterNotificationsInternal = () => {
onOk: () => void; onOk: () => void;
onCancel?: () => void; onCancel?: () => void;
icon?: React.ReactNode; icon?: React.ReactNode;
okButtonProps?: ModalFuncProps['okButtonProps'];
cancelButtonProps?: ModalFuncProps['cancelButtonProps'];
width?: string | number; width?: string | number;
useReject?: boolean; useReject?: boolean;
}): Promise<void> => { }): Promise<void> => {
const useReject = props.useReject ?? true; const useReject = props.useReject ?? true;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
modal.confirm({ // modal.confirm({
icon: props.icon || <></>, // icon: props.icon || <></>,
...props, // ...props,
className: cx(styles.modal, ''), // className: cx(styles.modal, ''),
cancelButtonProps: { // cancelButtonProps: {
...props.cancelButtonProps, // ...props.cancelButtonProps,
type: 'text' // type: 'text'
}, // },
okButtonProps: { // okButtonProps: {
...props.okButtonProps, // ...props.okButtonProps,
type: 'default' // type: 'default'
}, // },
onOk: async () => { // onOk: async () => {
await props.onOk(); // await props.onOk();
resolve(); // resolve();
}, // },
onCancel: async () => { // onCancel: async () => {
await props.onCancel?.(); // await props.onCancel?.();
if (useReject) reject(); // if (useReject) reject();
else resolve(); // else resolve();
} // }
}); // });
}); });
} }
); );

View File

@ -9,6 +9,7 @@ import {
createContext, createContext,
ContextSelector ContextSelector
} from '@fluentui/react-context-selector'; } from '@fluentui/react-context-selector';
import { AppToaster } from '@/components/ui/toaster';
export const ENABLE_DARK_MODE = false; export const ENABLE_DARK_MODE = false;
@ -31,7 +32,10 @@ export const BusterStyleProvider: React.FC<PropsWithChildren<{}>> = ({ children
enableSystem={ENABLE_DARK_MODE} enableSystem={ENABLE_DARK_MODE}
themes={['light', 'dark']} themes={['light', 'dark']}
disableTransitionOnChange> disableTransitionOnChange>
<BaseBusterStyleProvider>{children}</BaseBusterStyleProvider> <BaseBusterStyleProvider>
{children}
<AppToaster />
</BaseBusterStyleProvider>
</NextThemeProvider> </NextThemeProvider>
); );
}; };