Skip to content

Commit 81a90d2

Browse files
authored
feat(Toast): add Toast nearly identical to ChatGPT's (danny-avila#1108)
1 parent ba5ab86 commit 81a90d2

File tree

10 files changed

+281
-3
lines changed

10 files changed

+281
-3
lines changed

client/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"@radix-ui/react-slider": "^1.1.1",
3838
"@radix-ui/react-switch": "^1.0.3",
3939
"@radix-ui/react-tabs": "^1.0.3",
40+
"@radix-ui/react-toast": "^1.1.5",
4041
"@radix-ui/react-tooltip": "^1.0.6",
4142
"@tanstack/react-query": "^4.28.0",
4243
"@zattoo/use-double-click": "1.2.0",

client/src/App.jsx

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { RecoilRoot } from 'recoil';
2+
import * as RadixToast from '@radix-ui/react-toast';
23
import { RouterProvider } from 'react-router-dom';
34
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
45
import { QueryClient, QueryClientProvider, QueryCache } from '@tanstack/react-query';
56
import { ScreenshotProvider, ThemeProvider, useApiErrorBoundary } from './hooks';
7+
import Toast from './components/ui/Toast';
68
import { router } from './routes';
79

810
const App = () => {
@@ -22,8 +24,12 @@ const App = () => {
2224
<QueryClientProvider client={queryClient}>
2325
<RecoilRoot>
2426
<ThemeProvider>
25-
<RouterProvider router={router} />
26-
<ReactQueryDevtools initialIsOpen={false} position="top-right" />
27+
<RadixToast.Provider>
28+
<RouterProvider router={router} />
29+
<ReactQueryDevtools initialIsOpen={false} position="top-right" />
30+
<Toast />
31+
<RadixToast.Viewport className="pointer-events-none fixed inset-0 z-[60] mx-auto my-2 flex max-w-[560px] flex-col items-stretch justify-start md:pb-5" />
32+
</RadixToast.Provider>
2733
</ThemeProvider>
2834
</RecoilRoot>
2935
</QueryClientProvider>

client/src/common/types.ts

+7
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ export enum ESide {
2121
Left = 'left',
2222
}
2323

24+
export enum NotificationSeverity {
25+
INFO = 'info',
26+
SUCCESS = 'success',
27+
WARNING = 'warning',
28+
ERROR = 'error',
29+
}
30+
2431
export type TBaseSettingsProps = {
2532
conversation: TConversation | TPreset | null;
2633
className?: string;

client/src/components/ui/Toast.tsx

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import * as RadixToast from '@radix-ui/react-toast';
2+
import { NotificationSeverity } from '~/common/types';
3+
import { useToast } from '~/hooks';
4+
5+
export default function Toast() {
6+
const { toast, onOpenChange } = useToast();
7+
const severityClassName = {
8+
[NotificationSeverity.INFO]: 'border-gray-500 bg-gray-500',
9+
[NotificationSeverity.SUCCESS]: 'border-green-500 bg-green-500',
10+
[NotificationSeverity.WARNING]: 'border-orange-500 bg-orange-500',
11+
[NotificationSeverity.ERROR]: 'border-red-500 bg-red-500',
12+
};
13+
14+
return (
15+
<RadixToast.Root
16+
open={toast.open}
17+
onOpenChange={onOpenChange}
18+
className="toast-root"
19+
style={{
20+
height: '74px',
21+
marginBottom: '0px',
22+
}}
23+
>
24+
<div className="w-full p-1 text-center md:w-auto md:text-justify">
25+
<div
26+
className={`alert-root pointer-events-auto inline-flex flex-row gap-2 rounded-md border px-3 py-2 text-white ${
27+
severityClassName[toast.severity]
28+
}`}
29+
role="alert"
30+
>
31+
{toast.showIcon && (
32+
<div className="mt-1 flex-shrink-0 flex-grow-0">
33+
<svg
34+
stroke="currentColor"
35+
fill="none"
36+
strokeWidth="2"
37+
viewBox="0 0 24 24"
38+
strokeLinecap="round"
39+
strokeLinejoin="round"
40+
className="icon-sm"
41+
height="1em"
42+
width="1em"
43+
xmlns="http://www.w3.org/2000/svg"
44+
>
45+
<polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2" />
46+
<line x1="12" y1="8" x2="12" y2="12" />
47+
<line x1="12" y1="16" x2="12.01" y2="16" />
48+
</svg>
49+
</div>
50+
)}
51+
<RadixToast.Description className="flex-1 justify-center gap-2">
52+
<div className="whitespace-pre-wrap text-left">{toast.message}</div>
53+
</RadixToast.Description>
54+
</div>
55+
</div>
56+
</RadixToast.Root>
57+
);
58+
}

client/src/hooks/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export * from './AuthContext';
22
export * from './ThemeContext';
33
export * from './ScreenshotContext';
44
export * from './ApiErrorBoundaryContext';
5+
export { default as useToast } from './useToast';
56
export { default as useTimeout } from './useTimeout';
67
export { default as useUserKey } from './useUserKey';
78
export { default as useDebounce } from './useDebounce';

client/src/hooks/useToast.ts

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { useRef, useEffect } from 'react';
2+
import { useRecoilState } from 'recoil';
3+
import { NotificationSeverity } from '~/common';
4+
import store from '~/store';
5+
6+
export default function useToast(timeoutDuration = 100) {
7+
const [toast, setToast] = useRecoilState(store.toastState);
8+
const timerRef = useRef<number | null>(null);
9+
10+
useEffect(() => {
11+
return () => {
12+
if (timerRef.current !== null) {
13+
clearTimeout(timerRef.current);
14+
}
15+
};
16+
}, []);
17+
18+
type TShowToast = {
19+
message: string;
20+
severity?: NotificationSeverity;
21+
showIcon?: boolean;
22+
};
23+
24+
const showToast = ({
25+
message,
26+
severity = NotificationSeverity.SUCCESS,
27+
showIcon = true,
28+
}: TShowToast) => {
29+
setToast({ ...toast, open: false });
30+
if (timerRef.current !== null) {
31+
clearTimeout(timerRef.current);
32+
}
33+
timerRef.current = window.setTimeout(() => {
34+
setToast({ open: true, message, severity, showIcon });
35+
}, timeoutDuration);
36+
};
37+
38+
return {
39+
toast,
40+
onOpenChange: (open: boolean) => setToast({ ...toast, open }),
41+
showToast,
42+
};
43+
}

client/src/store/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import endpoints from './endpoints';
44
import models from './models';
55
import user from './user';
66
import text from './text';
7+
import toast from './toast';
78
import submission from './submission';
89
import search from './search';
910
import preset from './preset';
@@ -17,6 +18,7 @@ export default {
1718
...models,
1819
...user,
1920
...text,
21+
...toast,
2022
...submission,
2123
...search,
2224
...preset,

client/src/store/toast.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { atom } from 'recoil';
2+
import { NotificationSeverity } from '~/common';
3+
4+
const toastState = atom({
5+
key: 'toastState',
6+
default: {
7+
open: false,
8+
message: '',
9+
severity: NotificationSeverity.SUCCESS,
10+
showIcon: true,
11+
},
12+
});
13+
14+
export default { toastState };

client/src/style.css

+61
Original file line numberDiff line numberDiff line change
@@ -1532,3 +1532,64 @@ button.scroll-convo {
15321532
.hidden-visibility {
15331533
visibility: hidden;
15341534
}
1535+
1536+
.toast-root {
1537+
align-items:center;
1538+
display:flex;
1539+
flex-direction:column;
1540+
height:0;
1541+
transition:all .24s cubic-bezier(0,0,.2,1)
1542+
}
1543+
1544+
.toast-root[data-state=open] {
1545+
-webkit-animation:toast-open .24s cubic-bezier(.175,.885,.32,1.175) both;
1546+
animation:toast-open .24s cubic-bezier(.175,.885,.32,1.175) both
1547+
}
1548+
.toast-root[data-state=closed] {
1549+
-webkit-animation:toast-close .12s cubic-bezier(.4,0,1,1) both;
1550+
animation:toast-close .12s cubic-bezier(.4,0,1,1) both
1551+
}
1552+
.toast-root .alert-root {
1553+
box-shadow:0 0 1px rgba(67,90,111,.3),0 5px 8px -4px rgba(67,90,111,.3);
1554+
flex-shrink:0;
1555+
pointer-events:all
1556+
}
1557+
1558+
@-webkit-keyframes toast-open {
1559+
0% {
1560+
opacity:0;
1561+
-webkit-transform:translateY(-100%);
1562+
transform:translateY(-100%)
1563+
}
1564+
to {
1565+
-webkit-transform:translateY(0);
1566+
transform:translateY(0)
1567+
}
1568+
}
1569+
@keyframes toast-open {
1570+
0% {
1571+
opacity:0;
1572+
-webkit-transform:translateY(-100%);
1573+
transform:translateY(-100%)
1574+
}
1575+
to {
1576+
-webkit-transform:translateY(0);
1577+
transform:translateY(0)
1578+
}
1579+
}
1580+
@-webkit-keyframes toast-close {
1581+
0% {
1582+
opacity:1
1583+
}
1584+
to {
1585+
opacity:0
1586+
}
1587+
}
1588+
@keyframes toast-close {
1589+
0% {
1590+
opacity:1
1591+
}
1592+
to {
1593+
opacity:0
1594+
}
1595+
}

package-lock.json

+86-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)