Skip to content

Commit 498f56d

Browse files
authored
Feat: Replaicing react hot toast with Sonner toasts (#600)
* Feat: Replaicing react hot toast with Sonner toasts * pnpm lock file fix * Quickfix
1 parent b23696b commit 498f56d

File tree

4 files changed

+101
-65
lines changed

4 files changed

+101
-65
lines changed
Lines changed: 41 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ExclamationCircleIcon, XMarkIcon } from "@heroicons/react/20/solid";
22
import { CheckCircleIcon } from "@heroicons/react/24/solid";
3-
import { AnimatePresence, motion } from "framer-motion";
4-
import toast, { Toaster, resolveValue, useToasterStore } from "react-hot-toast";
3+
import { Toaster, toast } from "sonner";
4+
55
import { useTypedLoaderData } from "remix-typedjson";
66
import { loader } from "~/root";
77
import { useEffect } from "react";
@@ -11,79 +11,55 @@ const permanentToastDuration = 60 * 60 * 24 * 1000;
1111

1212
export function Toast() {
1313
const { toastMessage } = useTypedLoaderData<typeof loader>();
14-
1514
useEffect(() => {
1615
if (!toastMessage) {
1716
return;
1817
}
1918
const { message, type, options } = toastMessage;
2019

21-
switch (type) {
22-
case "success":
23-
toast.success(message, {
24-
duration: options.ephemeral ? defaultToastDuration : permanentToastDuration,
25-
});
26-
break;
27-
case "error":
28-
toast.error(message, {
29-
duration: options.ephemeral ? defaultToastDuration : permanentToastDuration,
30-
});
31-
break;
32-
default:
33-
throw new Error(`${type} is not handled`);
34-
}
20+
toast.custom((t) => <ToastUI variant={type} message={message} t={t as string} />, {
21+
duration: options.ephemeral ? defaultToastDuration : permanentToastDuration,
22+
});
3523
}, [toastMessage]);
3624

25+
return <Toaster />;
26+
}
27+
28+
export function ToastUI({
29+
variant,
30+
message,
31+
t,
32+
toastWidth = 356, // Default width, matches what sonner provides by default
33+
}: {
34+
variant: "error" | "success";
35+
message: string;
36+
t: string;
37+
toastWidth?: string | number;
38+
}) {
3739
return (
38-
<Toaster
39-
position="bottom-right"
40-
toastOptions={{
41-
success: {
42-
icon: <CheckCircleIcon className="h-6 w-6 text-green-600" />,
43-
},
44-
error: {
45-
icon: <ExclamationCircleIcon className="h-6 w-6 text-rose-600" />,
46-
},
40+
<div
41+
className={`self-end rounded-lg border border-slate-750 bg-midnight-900 shadow-md`}
42+
style={{
43+
width: toastWidth,
4744
}}
4845
>
49-
{(t) => (
50-
<AnimatePresence>
51-
<motion.div
52-
className="flex gap-2 rounded-lg border border-slate-750 bg-no-repeat p-4 text-bright shadow-md"
53-
style={{
54-
opacity: t.visible ? 1 : 0,
55-
background:
56-
"radial-gradient(at top, hsla(271, 91%, 65%, 0.18), hsla(221, 83%, 53%, 0.18)) hsla(221, 83%, 53%, 0.18)",
57-
}}
58-
initial={{ opacity: 0, y: 100 }}
59-
animate={t.visible ? "visible" : "hidden"}
60-
variants={{
61-
hidden: {
62-
opacity: 0,
63-
y: 0,
64-
transition: {
65-
duration: 0.15,
66-
ease: "easeInOut",
67-
},
68-
},
69-
visible: {
70-
opacity: 1,
71-
y: 0,
72-
transition: {
73-
duration: 0.3,
74-
ease: "easeInOut",
75-
},
76-
},
77-
}}
78-
>
79-
{t.icon}
80-
{resolveValue(t.message, t)}
81-
<button className="p-1" onClick={() => toast.dismiss(t.id)}>
82-
<XMarkIcon className="h-4 w-4 text-bright" />
83-
</button>
84-
</motion.div>
85-
</AnimatePresence>
86-
)}
87-
</Toaster>
46+
<div
47+
className="flex w-full gap-2 rounded-lg bg-no-repeat p-4 text-bright"
48+
style={{
49+
background:
50+
"radial-gradient(at top, hsla(271, 91%, 65%, 0.18), hsla(221, 83%, 53%, 0.18)) hsla(221, 83%, 53%, 0.18)",
51+
}}
52+
>
53+
{variant === "success" ? (
54+
<CheckCircleIcon className="h-6 w-6 text-green-600" />
55+
) : (
56+
<ExclamationCircleIcon className="h-6 w-6 text-rose-600" />
57+
)}
58+
{message}
59+
<button className="ms-auto p-1" onClick={() => toast.dismiss(t)}>
60+
<XMarkIcon className="h-4 w-4 text-bright" />
61+
</button>
62+
</div>
63+
</div>
8864
);
8965
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { Toaster, toast } from "sonner";
3+
import { ToastUI } from "../primitives/Toast";
4+
import { Button } from "../primitives/Buttons";
5+
6+
const meta: Meta = {
7+
title: "Primitives/Toast",
8+
};
9+
10+
export default meta;
11+
12+
type Story = StoryObj<typeof Collection>;
13+
14+
export const Toasts: Story = {
15+
render: () => <Collection />,
16+
};
17+
18+
function Collection() {
19+
return (
20+
<div className="flex flex-col items-start gap-y-4 p-4">
21+
<ToastUI variant="success" message="Success UI" t="-" />
22+
<ToastUI variant="error" message="Error UI" t="-" />
23+
<br />
24+
<Button
25+
variant="primary/large"
26+
onClick={() =>
27+
toast.custom((t) => <ToastUI variant="success" message="Success" t={t as string} />, {
28+
duration: Infinity, // Prevents auto-dismissal for demo purposes
29+
})
30+
}
31+
>
32+
Success
33+
</Button>
34+
<Button
35+
variant="primary/large"
36+
onClick={() =>
37+
toast.custom((t) => <ToastUI variant="error" message="Error" t={t as string} />, {
38+
duration: Infinity,
39+
})
40+
}
41+
>
42+
Error
43+
</Button>
44+
<Toaster />
45+
</div>
46+
);
47+
}

apps/webapp/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@
106106
"simple-oauth2": "^5.0.0",
107107
"simplur": "^3.0.1",
108108
"slug": "^6.0.0",
109+
"sonner": "^1.0.3",
109110
"tailwind-merge": "^1.12.0",
110111
"tailwind-scrollbar-hide": "^1.1.7",
111112
"tailwindcss-animate": "^1.0.5",

pnpm-lock.yaml

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)