Skip to content

Commit

Permalink
feat: Toast 컴포넌트 추가 (#106)
Browse files Browse the repository at this point in the history
* chore: @radix-ui/react-portal 패키지 설치

* feat: Toast 컴포넌트 추가

* chore: position을 고정

* feat: ToastOptionAtom으로 position 관리할 수 있도록

* feat: 부가적인 옵션만을 받아 default + 부가옵션을 설정할 수 있도록

* refactor: sequance 대신 length로 id 부여
  • Loading branch information
Doeunnkimm authored Jan 12, 2024
1 parent 57b1a17 commit bdf4350
Show file tree
Hide file tree
Showing 11 changed files with 267 additions and 34 deletions.
96 changes: 62 additions & 34 deletions .pnp.cjs

Large diffs are not rendered by default.

Binary file not shown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
},
"dependencies": {
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-portal": "^1.0.4",
"@radix-ui/react-tabs": "^1.0.4",
"@sentry/nextjs": "^7.91.0",
"@sentry/utils": "^7.91.0",
Expand Down
5 changes: 5 additions & 0 deletions src/assets/icons/toast-success.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/icons/toast-warning.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 41 additions & 0 deletions src/components/atoms/toast/Toast.atom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { atom } from 'jotai';

import type { ToastProps } from './Toast';

type ToastsProps = ToastProps[];

export interface ToastOptionProps {
position?: string;
}

export const toastsAtom = atom<ToastsProps>([]);

export const toastOptionAtom = atom<ToastOptionProps>({
position: 'bottom-[84px]',
});

export const removeToastAtom = atom(null, (get, set, id: number) => {
const prev = get(toastsAtom);
set(
toastsAtom,
prev.filter((toast) => toast.id !== id),
);
});

export const toastAtom = atom(
(get) => get(toastsAtom),
(get, set, type: ToastProps['type']) => (title: string) => () => {
const prev = get(toastsAtom);
const newToast = { type, title, id: prev.length };
set(toastsAtom, [...prev, newToast]);
},
);

export const toastOptionChangeAtom = atom(
(get) => get(toastOptionAtom),
(get, set, changeOption: ToastOptionProps) => {
const prev = get(toastOptionAtom);
const updatedOption = { ...prev, ...changeOption };
set(toastOptionAtom, updatedOption);
},
);
38 changes: 38 additions & 0 deletions src/components/atoms/toast/Toast.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { Meta, StoryObj } from '@storybook/react';

import { useToast } from '@/hooks/useToast';

import { Button } from '..';

import { Toast } from './Toast';
import { ToastProvider } from './ToastProvider';

const meta: Meta<typeof Toast> = {
title: 'components/atoms/toast',
component: Toast,
decorators: [
(Story) => (
<div className="w-[300px] h-[100vh]">
<ToastProvider />
<Story />
</div>
),
],
};

export default meta;

type Story = StoryObj<typeof Toast>;

export const Basic: Story = {
args: {
title: '세부 목표를 삭제했어요.',
type: 'success',
},
};

export const ClickToToast = () => {
const toast = useToast();

return <Button onClick={toast.success('세부 목표를 삭제했어요.')}>누르면 토스트가 나와요</Button>;
};
57 changes: 57 additions & 0 deletions src/components/atoms/toast/Toast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
'use client';

import { useEffect, useState } from 'react';
import { useSetAtom } from 'jotai';

import SuccessIcon from '@/assets/icons/toast-success.svg';
import WarningIcon from '@/assets/icons/toast-warning.svg';

import { Typography } from '../typography';

import { removeToastAtom } from './Toast.atom';

export interface ToastProps {
id: number;
title: string;
type?: 'success' | 'warning';
}

const toastIcon = {
success: <SuccessIcon />,
warning: <WarningIcon />,
};

const TOAST_DURATION = 3000;
const ANIMATION_DURATION = 350;

export const Toast = ({ id, title, type = 'success' }: ToastProps) => {
const [opacity, setOpacity] = useState('opacity-[0.2]');
const removeToastItem = useSetAtom(removeToastAtom);

useEffect(() => {
setOpacity('opacity-[0.8]');
const timeoutForRemove = setTimeout(() => {
removeToastItem(id);
}, TOAST_DURATION);

const timeoutForVisible = setTimeout(() => {
setOpacity('opacity-0');
}, TOAST_DURATION - ANIMATION_DURATION);

return () => {
clearTimeout(timeoutForRemove);
clearTimeout(timeoutForVisible);
};
}, [id, removeToastItem]);

return (
<div
className={`w-fit flex gap-5xs justify-center items-center px-3xs py-5xs bg-gray-60 rounded-[12px] mb-5xs transition-all duration-350 ease-in-out ${opacity}`}
>
{toastIcon[type]}
<Typography type="body3" className="text-white">
{title}
</Typography>
</div>
);
};
20 changes: 20 additions & 0 deletions src/components/atoms/toast/ToastProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as Portal from '@radix-ui/react-portal';
import { useAtomValue } from 'jotai';

import { Toast } from './Toast';
import { toastOptionAtom, toastsAtom } from './Toast.atom';

export const ToastProvider = () => {
const toasts = useAtomValue(toastsAtom);
const { position } = useAtomValue(toastOptionAtom);

return (
<Portal.Root>
<div className={`fixed ${position} left-1/2 transform translate-x-[-50%]`}>
{toasts.map((toast) => (
<Toast key={toast.id} {...toast} />
))}
</div>
</Portal.Root>
);
};
19 changes: 19 additions & 0 deletions src/hooks/useToast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useEffect } from 'react';
import { useSetAtom } from 'jotai';

import type { ToastOptionProps } from '@/components/atoms/toast/Toast.atom';
import { toastAtom, toastOptionChangeAtom } from '@/components/atoms/toast/Toast.atom';

export const useToast = (option?: ToastOptionProps) => {
const addToast = useSetAtom(toastAtom);
const setOption = useSetAtom(toastOptionChangeAtom);

useEffect(() => {
if (option) setOption(option);
}, [option, setOption]);

return {
success: addToast('success'),
warning: addToast('warning'),
};
};
21 changes: 21 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3109,6 +3109,26 @@ __metadata:
languageName: node
linkType: hard

"@radix-ui/react-portal@npm:^1.0.4":
version: 1.0.4
resolution: "@radix-ui/react-portal@npm:1.0.4"
dependencies:
"@babel/runtime": "npm:^7.13.10"
"@radix-ui/react-primitive": "npm:1.0.3"
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: fed32f8148b833fe852fb5e2f859979ffdf2fb9a9ef46583b9b52915d764ad36ba5c958a64e61d23395628ccc09d678229ee94cd112941e8fe2575021f820c29
languageName: node
linkType: hard

"@radix-ui/react-presence@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-presence@npm:1.0.1"
Expand Down Expand Up @@ -6420,6 +6440,7 @@ __metadata:
dependencies:
"@hookform/devtools": "npm:^4.3.1"
"@radix-ui/react-icons": "npm:^1.3.0"
"@radix-ui/react-portal": "npm:^1.0.4"
"@radix-ui/react-tabs": "npm:^1.0.4"
"@sentry/nextjs": "npm:^7.91.0"
"@sentry/utils": "npm:^7.91.0"
Expand Down

0 comments on commit bdf4350

Please sign in to comment.