Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FE] 토스트 생성 #166

Merged
merged 15 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,067 changes: 709 additions & 1,358 deletions HDesign/package-lock.json

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions HDesign/src/assets/confirm.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion HDesign/src/assets/error.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions HDesign/src/components/Button/Button.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const getButtonDefaultStyle = (theme: Theme) =>
display: 'flex',
justifyContent: 'center',
lineHeight: '1',
whiteSpace: 'nowrap',

'&:disabled': {
backgroundColor: theme.colors.tertiary,
Expand Down
53 changes: 49 additions & 4 deletions HDesign/src/components/Toast/Toast.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,61 @@ const meta = {
// layout: 'centered',
},
args: {
top: 80,
type: 'confirm',
position: 'top',
top: '80px',
message: `서버 오류로 인해 인원을 설정하는데 실패했어요.
두글자면 이렇게 보여요.`,
showingTime: 1000,
alwaysShow: false,
onUndo: () => alert('되돌리기 버튼이 눌렸습니다. 실행할 로직을 전달해주세요'),
},
} satisfies Meta<typeof Toast>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Playground: Story = {};
export const ConfirmToast: Story = {
args: {
...meta.args,
type: 'confirm',
top: '80px',
message: `이 첫번째 토스트 그림자 짙은거 두 개 떠서 그런거임 css 잘못한거 아닙니다. 잘못 없습니다. 스토리북이 잘못한거에요. 저희는 최선을 다했어요.. `,
},
};

export const ConfirmToastWithoutUndo: Story = {
args: {
...meta.args,
onUndo: undefined,
top: '160px',
},
};

export const ErrorToast: Story = {
args: {
...meta.args,
top: '240px',
type: 'error',
message: `님 이거 다 작성했는데, 혹시 되돌림?
되돌릴 수도 있음 ㅇㅇ 굿`,
},
};

export const ErrorToastWithoutUndo: Story = {
args: {
...meta.args,
top: '320px',
onUndo: undefined,
type: 'error',
},
};

export const NoneToast: Story = {
args: {
...meta.args,
top: '400px',
onUndo: undefined,
type: 'none',
message: '웨디는 커비의 먹잇감인가요? 그치만 감자는 웨디한테 먹힘 쿠스쿠스 ㅋ',
},
};
29 changes: 20 additions & 9 deletions HDesign/src/components/Toast/Toast.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,31 @@ import {css} from '@emotion/react';

import {Theme} from '@/theme/theme.type';

export const toastStyle = (top: number, isShow: boolean, theme: Theme) =>
css({
display: isShow ? 'flex' : 'none',
alignItems: 'center',
import {ToastPosition} from './Toast.type';

type ToastMarginStyle = {
position?: ToastPosition;
bottom?: string;
top?: string;
};

export const toastMarginStyle = ({position, bottom, top}: ToastMarginStyle) =>
css({
position: 'absolute',
top: `${top}%`,
left: 0,
gap: '0.5rem',
bottom: position === 'bottom' ? `${bottom}` : 'auto',
top: position === 'top' ? `${top}` : 'auto',
left: '50%',
transform: 'translate(-50%)',

width: '100%',
maxWidth: '48rem',
paddingInline: '0.5rem',
});

export const toastStyle = (theme: Theme) =>
css({
width: '100%',
margin: '1.25rem',
padding: '0.625rem',
padding: '0.625rem 1rem',

backgroundColor: theme.colors.gray,
boxShadow: '0 8px 12px rgba(0, 0, 0, 0.16);',
Expand Down
67 changes: 56 additions & 11 deletions HDesign/src/components/Toast/Toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,71 @@
import {createPortal} from 'react-dom';

import Text from '@components/Text/Text';
import Flex from '@components/Flex/Flex';

import ErrorIcon from '@assets/error.svg';
import ConfirmIcon from '@assets/confirm.svg';

import {useTheme} from '@theme/HDesignProvider';

import {toastStyle, textStyle} from './Toast.style';
import {ToastProps} from './Toast.type';
import useToast from './useToast';
import Button from '../Button/Button';

const Toast: React.FC<ToastProps> = ({top = 80, message, showingTime, alwaysShow}) => {
import {toastStyle, textStyle, toastMarginStyle} from './Toast.style';
import {ToastProps, ToastType} from './Toast.type';

const renderIcon = (type: ToastType) => {
switch (type) {
case 'error':
return <ErrorIcon />;

case 'confirm':
return <ConfirmIcon />;

case 'none':
return null;

default:
return null;
}
};

const Toast = ({
type = 'confirm',
top = '0px',
bottom = '0px',
isClickToClose = true,
position = 'bottom',
message,
onUndo,
onClose,
...htmlProps
}: ToastProps) => {
const {theme} = useTheme();
const {isShow} = useToast({message, showingTime, alwaysShow});
const styleProps = {position, top, bottom};

const handleClickToClose = () => {
if (!isClickToClose || !onClose) return;

onClose();
};

return createPortal(
// TODO: (@cookie) toast를 위한 시멘틱 태그 알아보기
<div css={toastStyle(top, isShow, theme)}>
<ErrorIcon />
<Text size="smallBodyBold" css={textStyle(theme)}>
{message}
</Text>
<div css={toastMarginStyle({...styleProps})} {...htmlProps} onClick={handleClickToClose}>
<div css={toastStyle(theme)}>
<Flex justifyContent="spaceBetween" alignItems="center">
<Flex alignItems="center" gap="0.5rem">
{renderIcon(type)}
<Text size="smallBodyBold" css={textStyle(theme)}>
{message}
</Text>
</Flex>
{onUndo && (
<Button variants="tertiary" size="small" onClick={onUndo}>
되돌리기
</Button>
)}
</Flex>
</div>
</div>,
document.body,
);
Expand Down
19 changes: 12 additions & 7 deletions HDesign/src/components/Toast/Toast.type.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
export type ToastPosition = 'bottom' | 'top';
export type ToastType = 'error' | 'confirm' | 'none';

export interface ToastStyleProps {
top?: number;
bottom?: string;
top?: string;
}

export interface ToastCustomProps {
showingTime?: number;
alwaysShow?: boolean;
export interface ToastOptionProps {
position?: ToastPosition;
type?: ToastType;
onUndo?: () => void;
isClickToClose?: boolean;
onClose?: () => void;
}

export interface ToastRequiredProps {
message: string;
}

export type ToastOptionProps = ToastStyleProps & ToastCustomProps;

export type ToastProps = React.ComponentProps<'aside'> & ToastOptionProps & ToastRequiredProps;
export type ToastProps = React.ComponentProps<'div'> & ToastStyleProps & ToastOptionProps & ToastRequiredProps;
46 changes: 46 additions & 0 deletions HDesign/src/components/Toast/ToastProvider.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/** @jsxImportSource @emotion/react */
import type {Meta, StoryObj} from '@storybook/react';

import Button from '../Button/Button';

import {ToastProvider, useToast} from './ToastProvider';

const meta = {
title: 'Components/ToastProvider',
component: ToastProvider,
tags: ['autodocs'],
decorators: [
Story => (
<ToastProvider>
<Story />
</ToastProvider>
),
],
} satisfies Meta<typeof ToastProvider>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Playground: Story = {
decorators: [
() => {
const {showToast} = useToast();

return (
<Button
onClick={() =>
showToast({
isAlwaysOn: true,
message: '이거자냥 (feat. 쿠키)',
type: 'confirm',
position: 'top',
})
}
>
토스트 열기
</Button>
);
},
],
};
59 changes: 59 additions & 0 deletions HDesign/src/components/Toast/ToastProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/** @jsxImportSource @emotion/react */
import {createContext, useCallback, useContext, useEffect, useState} from 'react';

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

export const ToastContext = createContext<ToastContextProps | null>(null);

interface ToastContextProps {
showToast: (args: ShowToast) => void;
}

type ShowToast = ToastProps & {
showingTime?: number;
isAlwaysOn?: boolean;
};

const ToastProvider = ({children}: React.PropsWithChildren) => {
const [currentToast, setCurrentToast] = useState<ShowToast | null>(null);

const showToast = useCallback(({showingTime = 3000, isAlwaysOn = false, ...toastProps}: ShowToast) => {
setCurrentToast({showingTime, isAlwaysOn, ...toastProps});
}, []);

const closeToast = () => {
setCurrentToast(null);
};

useEffect(() => {
if (!currentToast) return;

if (!currentToast.isAlwaysOn) {
const timer = setTimeout(() => {
setCurrentToast(null);
}, currentToast.showingTime);

return () => clearTimeout(timer);
}
}, [currentToast]);

return (
<ToastContext.Provider value={{showToast}}>
{currentToast && <Toast onClose={closeToast} {...currentToast} />}
{children}
</ToastContext.Provider>
);
};

const useToast = () => {
const context = useContext(ToastContext);

if (!context) {
throw new Error('useToast는 ToastProvider 내에서 사용되어야 합니다.');
}

return context;
};

export {ToastProvider, useToast};
34 changes: 0 additions & 34 deletions HDesign/src/components/Toast/useToast.ts

This file was deleted.

Loading
Loading