Skip to content

Commit

Permalink
feat: Use sonner for toast instead of chakra (#538)
Browse files Browse the repository at this point in the history
* feat: Use sonner for toast instead of chakra

* fix: toast clear in demo modal interceptor

* fix: language in documentation

* fix: dark mode issue

* fix: remove unused import
  • Loading branch information
ivan-dalmet authored Oct 18, 2024
1 parent ff723c0 commit 6b6f3b2
Show file tree
Hide file tree
Showing 24 changed files with 312 additions and 103 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"react-select": "5.8.0",
"remeda": "2.5.0",
"sharp": "0.33.4",
"sonner": "1.5.0",
"superjson": "2.2.1",
"trpc-openapi": "1.2.0",
"ts-pattern": "5.2.0",
Expand Down
14 changes: 14 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/app/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React, { FC } from 'react';
import { CacheProvider } from '@chakra-ui/next-js';
import { ChakraProvider, createLocalStorageManager } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { Toaster } from 'sonner';

import '@/lib/dayjs/config';
import '@/lib/i18n/client';
Expand All @@ -28,6 +29,7 @@ export const Providers: FC<React.PropsWithChildren<unknown>> = ({
}}
>
{children}
<Toaster position="top-right" offset={16} />
</ChakraProvider>
</CacheProvider>
);
Expand Down
113 changes: 113 additions & 0 deletions src/components/Toast/docs.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React from 'react';

import { Box, Button, Flex } from '@chakra-ui/react';
import { Meta } from '@storybook/react';
import { toast } from 'sonner';

import { toastCustom } from '@/components/Toast';

export default {
title: 'Components/Toast',
decorators: [
(Story) => (
<Box h="10rem">
<Story />
</Box>
),
],
} satisfies Meta;

export const Default = () => {
const handleOpenToast = (props: { status: 'success' | 'error' | 'info' }) => {
toastCustom({
status: props.status,
title: 'This is a toast',
});
};

return (
<Flex gap={4}>
<Button size="md" onClick={() => handleOpenToast({ status: 'success' })}>
Success toast
</Button>
<Button size="md" onClick={() => handleOpenToast({ status: 'error' })}>
Error toast
</Button>
<Button size="md" onClick={() => handleOpenToast({ status: 'info' })}>
Info toast
</Button>
</Flex>
);
};

export const WithDescription = () => {
const handleOpenToast = (props: { status: 'success' | 'error' | 'info' }) => {
toastCustom({
status: props.status,
title: 'This is a toast',
description:
'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.',
});
};
return (
<Flex gap={4}>
<Button size="md" onClick={() => handleOpenToast({ status: 'success' })}>
Success toast with description
</Button>
<Button size="md" onClick={() => handleOpenToast({ status: 'error' })}>
Error toast with description
</Button>
<Button size="md" onClick={() => handleOpenToast({ status: 'info' })}>
Info toast with description
</Button>
</Flex>
);
};

export const WithActions = () => {
const handleOpenToast = (props: { status: 'success' | 'error' | 'info' }) => {
toastCustom({
status: props.status,
title: 'This is a toast',
actions: (
<Button onClick={() => toast.dismiss()}>Close all toasts</Button>
),
});
};
return (
<Flex gap={4}>
<Button size="md" onClick={() => handleOpenToast({ status: 'success' })}>
Success toast with actions
</Button>
<Button size="md" onClick={() => handleOpenToast({ status: 'error' })}>
Error toast with actions
</Button>
<Button size="md" onClick={() => handleOpenToast({ status: 'info' })}>
Info toast with with actions
</Button>
</Flex>
);
};

export const HideIcon = () => {
const handleOpenToast = (props: { status: 'success' | 'error' | 'info' }) => {
toastCustom({
status: props.status,
title: 'This is a toast',
hideIcon: true,
});
};
return (
<Flex gap={4}>
<Button size="md" onClick={() => handleOpenToast({ status: 'success' })}>
Success toast without icon
</Button>
<Button size="md" onClick={() => handleOpenToast({ status: 'error' })}>
Error toast without icon
</Button>
<Button size="md" onClick={() => handleOpenToast({ status: 'info' })}>
Info toast without icon
</Button>
</Flex>
);
};
33 changes: 0 additions & 33 deletions src/components/Toast/index.ts

This file was deleted.

107 changes: 107 additions & 0 deletions src/components/Toast/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { ReactNode } from 'react';

import {
Box,
ButtonGroup,
Card,
CardBody,
Flex,
Heading,
IconButton,
} from '@chakra-ui/react';
import i18n from 'i18next';
import { LuCheckCircle2, LuInfo, LuX, LuXCircle } from 'react-icons/lu';
import { ExternalToast, toast } from 'sonner';
import { match } from 'ts-pattern';

import { Icon } from '@/components/Icons';

export const toastCustom = (params: {
status?: 'info' | 'success' | 'error';
hideIcon?: boolean;
title: ReactNode;
description?: ReactNode;
actions?: ReactNode;
}) => {
const status = params.status ?? 'info';
const icon = match(status)
.with('info', () => LuInfo)
.with('success', () => LuCheckCircle2)
.with('error', () => LuXCircle)
.exhaustive();

const options: ExternalToast = {
duration: status === 'error' ? Infinity : 3000,
};

toast.custom(
(t) => (
<Flex>
<IconButton
zIndex={1}
size="xs"
aria-label={i18n.t('components:toast.closeToast')}
icon={<LuX />}
onClick={() => toast.dismiss(t)}
position="absolute"
top={-2.5}
right={-2.5}
borderRadius="full"
/>
<Card
w="356px"
position="relative"
overflow="hidden"
boxShadow="layout"
>
<Box
position="absolute"
top={0}
left={0}
bottom={0}
w="3px"
bg={`${status}.600`}
/>
<CardBody
display="flex"
flexDirection="column"
gap={1.5}
p={4}
color="gray.800"
_dark={{
color: 'white',
}}
>
<Flex alignItems="center" gap={2}>
<Heading size="xs" flex={1}>
{!params.hideIcon && (
<Icon
icon={icon}
mr={2}
fontSize="1.2em"
color={`${status}.500`}
/>
)}
{params.title}
</Heading>
{!!params.actions && (
<ButtonGroup size="xs">{params.actions}</ButtonGroup>
)}
</Flex>
{!!params.description && (
<Flex
direction="column"
fontSize="xs"
color="gray.600"
_dark={{ color: 'gray.400' }}
>
{params.description}
</Flex>
)}
</CardBody>
</Card>
</Flex>
),
options
);
};
6 changes: 3 additions & 3 deletions src/features/account/AccountDeleteButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import { LuTrash2 } from 'react-icons/lu';

import { ConfirmModal } from '@/components/ConfirmModal';
import { useToastError } from '@/components/Toast';
import { toastCustom } from '@/components/Toast';
import {
AccountDeleteVerificationCodeModale,
SEARCH_PARAM_VERIFY_EMAIL,
Expand All @@ -25,7 +25,6 @@ export const AccountDeleteButton = () => {
);
const account = trpc.account.get.useQuery();

const toastError = useToastError();
const deleteAccountValidate = searchParams[SEARCH_PARAM_VERIFY_EMAIL];

const deleteAccount = trpc.account.deleteRequest.useMutation({
Expand All @@ -37,7 +36,8 @@ export const AccountDeleteButton = () => {
});
},
onError: () => {
toastError({
toastCustom({
status: 'error',
title: t('account:deleteAccount.feedbacks.updateError.title'),
});
},
Expand Down
7 changes: 3 additions & 4 deletions src/features/account/AccountEmailForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
FormFieldLabel,
} from '@/components/Form';
import { LoaderFull } from '@/components/LoaderFull';
import { useToastError } from '@/components/Toast';
import { toastCustom } from '@/components/Toast';
import { EmailVerificationCodeModale } from '@/features/account/EmailVerificationCodeModal';
import {
FormFieldsAccountEmail,
Expand All @@ -33,8 +33,6 @@ export const AccountEmailForm = () => {
staleTime: Infinity,
});

const toastError = useToastError();

const updateEmail = trpc.account.updateEmail.useMutation({
onSuccess: async ({ token }, { email }) => {
setSearchParams({
Expand All @@ -43,7 +41,8 @@ export const AccountEmailForm = () => {
});
},
onError: () => {
toastError({
toastCustom({
status: 'error',
title: t('account:email.feedbacks.updateError.title'),
});
},
Expand Down
Loading

0 comments on commit 6b6f3b2

Please sign in to comment.