Skip to content

Commit 2bb1777

Browse files
committed
feat: Use sonner for toast instead of chakra
1 parent ff723c0 commit 2bb1777

19 files changed

+288
-95
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
"react-select": "5.8.0",
9595
"remeda": "2.5.0",
9696
"sharp": "0.33.4",
97+
"sonner": "1.5.0",
9798
"superjson": "2.2.1",
9899
"trpc-openapi": "1.2.0",
99100
"ts-pattern": "5.2.0",

pnpm-lock.yaml

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

src/app/Providers.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import React, { FC } from 'react';
33
import { CacheProvider } from '@chakra-ui/next-js';
44
import { ChakraProvider, createLocalStorageManager } from '@chakra-ui/react';
55
import { useTranslation } from 'react-i18next';
6+
import { Toaster } from 'sonner';
67

78
import '@/lib/dayjs/config';
89
import '@/lib/i18n/client';
@@ -28,6 +29,7 @@ export const Providers: FC<React.PropsWithChildren<unknown>> = ({
2829
}}
2930
>
3031
{children}
32+
<Toaster position="top-right" offset={16} />
3133
</ChakraProvider>
3234
</CacheProvider>
3335
);

src/components/Toast/docs.stories.tsx

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import React from 'react';
2+
3+
import { Box, Button, Flex } from '@chakra-ui/react';
4+
import { Meta } from '@storybook/react';
5+
import { toast } from 'sonner';
6+
7+
import { toastCustom } from '@/components/Toast';
8+
9+
export default {
10+
title: 'Components/Toast',
11+
decorators: [
12+
(Story) => (
13+
<Box h="10rem">
14+
<Story />
15+
</Box>
16+
),
17+
],
18+
} satisfies Meta;
19+
20+
export const Default = () => {
21+
const handleOpenToast = (props: { status: 'success' | 'error' | 'info' }) => {
22+
toastCustom({
23+
status: props.status,
24+
title: 'Ceci est un titre de toast',
25+
});
26+
};
27+
28+
return (
29+
<Flex gap={4}>
30+
<Button size="md" onClick={() => handleOpenToast({ status: 'success' })}>
31+
Success toast
32+
</Button>
33+
<Button size="md" onClick={() => handleOpenToast({ status: 'error' })}>
34+
Error toast
35+
</Button>
36+
<Button size="md" onClick={() => handleOpenToast({ status: 'info' })}>
37+
Info toast
38+
</Button>
39+
</Flex>
40+
);
41+
};
42+
43+
export const WithDescription = () => {
44+
const handleOpenToast = (props: { status: 'success' | 'error' | 'info' }) => {
45+
toastCustom({
46+
status: props.status,
47+
title: 'Ceci est un titre de toast',
48+
description:
49+
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis id porta lacus. Nunc tellus ipsum, blandit commodo neque at, eleifend facilisis arcu. Phasellus nec pretium sapien.',
50+
});
51+
};
52+
return (
53+
<Flex gap={4}>
54+
<Button size="md" onClick={() => handleOpenToast({ status: 'success' })}>
55+
Success toast with descrition
56+
</Button>
57+
<Button size="md" onClick={() => handleOpenToast({ status: 'error' })}>
58+
Error toast with descrition
59+
</Button>
60+
<Button size="md" onClick={() => handleOpenToast({ status: 'info' })}>
61+
Info toast with descrition
62+
</Button>
63+
</Flex>
64+
);
65+
};
66+
67+
export const WithActions = () => {
68+
const handleOpenToast = (props: { status: 'success' | 'error' | 'info' }) => {
69+
toastCustom({
70+
status: props.status,
71+
title: 'Ceci est un titre de toast',
72+
actions: (
73+
<Button onClick={() => toast.dismiss()}>Fermer les toasts</Button>
74+
),
75+
});
76+
};
77+
return (
78+
<Flex gap={4}>
79+
<Button size="md" onClick={() => handleOpenToast({ status: 'success' })}>
80+
Success toast with actions
81+
</Button>
82+
<Button size="md" onClick={() => handleOpenToast({ status: 'error' })}>
83+
Error toast with actions
84+
</Button>
85+
<Button size="md" onClick={() => handleOpenToast({ status: 'info' })}>
86+
Info toast with with actions
87+
</Button>
88+
</Flex>
89+
);
90+
};
91+
92+
export const HideIcon = () => {
93+
const handleOpenToast = (props: { status: 'success' | 'error' | 'info' }) => {
94+
toastCustom({
95+
status: props.status,
96+
title: 'Ceci est un titre de toast',
97+
hideIcon: true,
98+
});
99+
};
100+
return (
101+
<Flex gap={4}>
102+
<Button size="md" onClick={() => handleOpenToast({ status: 'success' })}>
103+
Success toast without icon
104+
</Button>
105+
<Button size="md" onClick={() => handleOpenToast({ status: 'error' })}>
106+
Error toast with without icon
107+
</Button>
108+
<Button size="md" onClick={() => handleOpenToast({ status: 'info' })}>
109+
Info toast with without icon
110+
</Button>
111+
</Flex>
112+
);
113+
};

src/components/Toast/index.ts

-33
This file was deleted.

src/components/Toast/index.tsx

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { ReactNode } from 'react';
2+
3+
import {
4+
Box,
5+
ButtonGroup,
6+
Card,
7+
CardBody,
8+
Flex,
9+
Heading,
10+
IconButton,
11+
} from '@chakra-ui/react';
12+
import { LuCheckCircle2, LuInfo, LuX, LuXCircle } from 'react-icons/lu';
13+
import { ExternalToast, toast } from 'sonner';
14+
import { match } from 'ts-pattern';
15+
16+
import { Icon } from '@/components/Icons';
17+
18+
export const toastCustom = (params: {
19+
status?: 'info' | 'success' | 'error';
20+
hideIcon?: boolean;
21+
title: ReactNode;
22+
description?: ReactNode;
23+
actions?: ReactNode;
24+
}) => {
25+
const status = params.status ?? 'info';
26+
const icon = match(status)
27+
.with('info', () => LuInfo)
28+
.with('success', () => LuCheckCircle2)
29+
.with('error', () => LuXCircle)
30+
.exhaustive();
31+
32+
const options: ExternalToast = {
33+
duration: status === 'error' ? Infinity : 3000,
34+
};
35+
36+
toast.custom(
37+
(t) => (
38+
<Flex>
39+
<IconButton
40+
zIndex={1}
41+
size="xs"
42+
aria-label="Fermer le message"
43+
icon={<LuX />}
44+
onClick={() => toast.dismiss(t)}
45+
position="absolute"
46+
top={-2.5}
47+
right={-2.5}
48+
borderRadius="full"
49+
/>
50+
<Card
51+
w="356px"
52+
position="relative"
53+
overflow="hidden"
54+
boxShadow="layout"
55+
>
56+
<Box
57+
position="absolute"
58+
top={0}
59+
left={0}
60+
bottom={0}
61+
w="3px"
62+
bg={`${status}.600`}
63+
/>
64+
<CardBody
65+
display="flex"
66+
flexDirection="column"
67+
gap={1.5}
68+
p={4}
69+
color="gray.800"
70+
_dark={{
71+
color: 'white',
72+
}}
73+
>
74+
<Flex alignItems="center" gap={2}>
75+
<Heading size="xs" flex={1}>
76+
{!params.hideIcon && (
77+
<Icon
78+
icon={icon}
79+
mr={2}
80+
fontSize="1.2em"
81+
color={`${status}.500`}
82+
/>
83+
)}
84+
{params.title}
85+
</Heading>
86+
{!!params.actions && (
87+
<ButtonGroup size="xs">{params.actions}</ButtonGroup>
88+
)}
89+
</Flex>
90+
{!!params.description && (
91+
<Flex direction="column" fontSize="xs" color="text-dimmed">
92+
{params.description}
93+
</Flex>
94+
)}
95+
</CardBody>
96+
</Card>
97+
</Flex>
98+
),
99+
options
100+
);
101+
};

src/features/account/AccountDeleteButton.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
44
import { LuTrash2 } from 'react-icons/lu';
55

66
import { ConfirmModal } from '@/components/ConfirmModal';
7-
import { useToastError } from '@/components/Toast';
7+
import { toastCustom } from '@/components/Toast';
88
import {
99
AccountDeleteVerificationCodeModale,
1010
SEARCH_PARAM_VERIFY_EMAIL,
@@ -25,7 +25,6 @@ export const AccountDeleteButton = () => {
2525
);
2626
const account = trpc.account.get.useQuery();
2727

28-
const toastError = useToastError();
2928
const deleteAccountValidate = searchParams[SEARCH_PARAM_VERIFY_EMAIL];
3029

3130
const deleteAccount = trpc.account.deleteRequest.useMutation({
@@ -37,7 +36,8 @@ export const AccountDeleteButton = () => {
3736
});
3837
},
3938
onError: () => {
40-
toastError({
39+
toastCustom({
40+
status: 'error',
4141
title: t('account:deleteAccount.feedbacks.updateError.title'),
4242
});
4343
},

src/features/account/AccountEmailForm.tsx

+3-4
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
FormFieldLabel,
1515
} from '@/components/Form';
1616
import { LoaderFull } from '@/components/LoaderFull';
17-
import { useToastError } from '@/components/Toast';
17+
import { toastCustom } from '@/components/Toast';
1818
import { EmailVerificationCodeModale } from '@/features/account/EmailVerificationCodeModal';
1919
import {
2020
FormFieldsAccountEmail,
@@ -33,8 +33,6 @@ export const AccountEmailForm = () => {
3333
staleTime: Infinity,
3434
});
3535

36-
const toastError = useToastError();
37-
3836
const updateEmail = trpc.account.updateEmail.useMutation({
3937
onSuccess: async ({ token }, { email }) => {
4038
setSearchParams({
@@ -43,7 +41,8 @@ export const AccountEmailForm = () => {
4341
});
4442
},
4543
onError: () => {
46-
toastError({
44+
toastCustom({
45+
status: 'error',
4746
title: t('account:email.feedbacks.updateError.title'),
4847
});
4948
},

src/features/account/AccountProfileForm.tsx

+5-6
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
FormFieldLabel,
1414
} from '@/components/Form';
1515
import { LoaderFull } from '@/components/LoaderFull';
16-
import { useToastError, useToastSuccess } from '@/components/Toast';
16+
import { toastCustom } from '@/components/Toast';
1717
import {
1818
FormFieldsAccountProfile,
1919
zFormFieldsAccountProfile,
@@ -31,18 +31,17 @@ export const AccountProfileForm = () => {
3131
staleTime: Infinity,
3232
});
3333

34-
const toastSuccess = useToastSuccess();
35-
const toastError = useToastError();
36-
3734
const updateAccount = trpc.account.update.useMutation({
3835
onSuccess: async () => {
3936
await trpcUtils.account.invalidate();
40-
toastSuccess({
37+
toastCustom({
38+
status: 'success',
4139
title: t('account:profile.feedbacks.updateSuccess.title'),
4240
});
4341
},
4442
onError: () => {
45-
toastError({
43+
toastCustom({
44+
status: 'error',
4645
title: t('account:profile.feedbacks.updateError.title'),
4746
});
4847
},

0 commit comments

Comments
 (0)