-
Notifications
You must be signed in to change notification settings - Fork 8
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] 토스트 UI 구현 #391
Merged
Merged
[FE] 토스트 UI 구현 #391
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
39141fa
chore: 체크 svg를 재사용할 수 있도록 current 속성으로 변경
hwinkr 11dd9a2
chore: 경고 토스트를 보여줄 때, 사용할 svg 추가
hwinkr 8c08776
chore: 필요없는 공백 제거
hwinkr 134f526
feat: 토스트 컴포넌트 구현
hwinkr 4ac9ffa
design: 토스트 컴포넌트를 스타일링하기 위한 css 추가
hwinkr a145ff7
test: 토스트 컴포넌트 UI 테스트
hwinkr b04edfe
feat: 토스트를 감싸는 컴포넌트 구현
hwinkr ea057dd
feat: 여러개의 토스트를 보여주기 위한 컴포넌트 구현
hwinkr 1a3ba4e
design: 토스트 목록 컴포넌트 스타일링을 위한 css 추가
hwinkr cc97375
feat: 전역으로 토스트의 렌더링 상태를 관리하기 위한 컨텍스트, 프로바이더 컴포넌트 구현
hwinkr c15ef17
test: 토스트 컴포넌트 폴더 구조 이동, 공통 스타일 추출, 버튼을 클릭했을 때 토스트 UI가 렌더링되는 스토리 추가
hwinkr 87c6600
chore: 토스트 UI 기본 시간 추가
hwinkr File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
158 changes: 158 additions & 0 deletions
158
frontend/src/components/_common/Toast/Toast.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
import { css } from '@emotion/react'; | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
|
||
import ToastProvider from '@contexts/ToastProvider'; | ||
|
||
import useToast from '@hooks/useToast/useToast'; | ||
|
||
import Toast from '.'; | ||
import { Button } from '../Buttons/Button'; | ||
|
||
const meta = { | ||
title: 'Components/Toast', | ||
component: Toast, | ||
parameters: { | ||
layout: 'centered', | ||
}, | ||
argTypes: { | ||
message: { | ||
control: { | ||
type: 'text', | ||
}, | ||
description: '토스트 UI를 통해 사용자에게 전달할 메시지입니다.', | ||
}, | ||
type: { | ||
control: { | ||
type: 'select', | ||
}, | ||
options: ['default', 'warning', 'success'], | ||
}, | ||
isOpen: { | ||
control: { | ||
type: 'boolean', | ||
}, | ||
description: '토스트 UI에 애니메이션을 적용하기 위한 추가 상태입니다.', | ||
}, | ||
}, | ||
|
||
decorators: [ | ||
(Story) => { | ||
return ( | ||
<ToastProvider> | ||
<div | ||
css={css` | ||
width: 43rem; | ||
`} | ||
> | ||
<Story /> | ||
</div> | ||
</ToastProvider> | ||
); | ||
}, | ||
], | ||
} satisfies Meta<typeof Toast>; | ||
|
||
export default meta; | ||
|
||
type Story = StoryObj<typeof Toast>; | ||
|
||
export const Default: Story = { | ||
args: { | ||
isOpen: true, | ||
type: 'default', | ||
message: '안녕하세요, 내 이름은 기본 토스트입니다.', | ||
}, | ||
|
||
render: (args) => { | ||
return <Toast {...args} />; | ||
}, | ||
}; | ||
|
||
export const Warning: Story = { | ||
args: { | ||
isOpen: true, | ||
type: 'warning', | ||
message: '안녕하세요, 내 이름은 경고 토스트입니다.', | ||
}, | ||
|
||
render: (args) => { | ||
return <Toast {...args} />; | ||
}, | ||
}; | ||
|
||
export const Success: Story = { | ||
args: { | ||
isOpen: true, | ||
type: 'success', | ||
message: '안녕하세요, 내 이름은 성공 토스트입니다.', | ||
}, | ||
|
||
render: (args) => { | ||
return <Toast {...args} />; | ||
}, | ||
}; | ||
|
||
export const ToastPlayground: Story = { | ||
render: () => { | ||
const { addToast } = useToast(); | ||
|
||
const renderDefaultToast = () => { | ||
addToast({ | ||
type: 'default', | ||
message: '안녕하세요, 내 이름은 기본 토스트입니다', | ||
duration: 3000, | ||
}); | ||
}; | ||
|
||
const renderSuccessToast = () => { | ||
addToast({ | ||
type: 'success', | ||
message: '안녕하세요, 내 이름은 성공 토스트입니다', | ||
duration: 3000, | ||
}); | ||
}; | ||
|
||
const renderWarningToast = () => { | ||
addToast({ | ||
type: 'warning', | ||
message: '안녕하세요, 내 이름은 경고 토스트입니다', | ||
duration: 3000, | ||
}); | ||
}; | ||
|
||
return ( | ||
<div | ||
css={css` | ||
position: relative; | ||
|
||
display: flex; | ||
justify-content: center; | ||
|
||
width: 100%; | ||
height: 100vh; | ||
`} | ||
> | ||
<div | ||
css={css` | ||
position: absolute; | ||
bottom: 2.4rem; | ||
|
||
display: flex; | ||
flex-direction: column; | ||
row-gap: 1.2rem; | ||
`} | ||
> | ||
<Button variant="primary" size="s" onClick={renderDefaultToast}> | ||
기본 토스트 렌더링하기 | ||
</Button> | ||
<Button variant="primary" size="s" onClick={renderSuccessToast}> | ||
성공 토스트 렌더링하기 | ||
</Button> | ||
<Button variant="primary" size="s" onClick={renderWarningToast}> | ||
경고 토스트 렌더링하기 | ||
</Button> | ||
</div> | ||
</div> | ||
); | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import type { SerializedStyles } from '@emotion/react'; | ||
import { css, keyframes } from '@emotion/react'; | ||
|
||
import theme from '@styles/theme'; | ||
|
||
import type { ToastType } from './Toast.type'; | ||
|
||
const toastSlideIn = keyframes` | ||
from{ | ||
opacity: 0; | ||
}to{ | ||
opacity: 1; | ||
} | ||
`; | ||
|
||
const toastSlideOut = keyframes` | ||
from{ | ||
opacity: 1; | ||
}to{ | ||
opacity: 0; | ||
} | ||
`; | ||
|
||
export const s_toastContainer = (isOpen: boolean) => css` | ||
display: flex; | ||
gap: 1.2rem; | ||
align-items: center; | ||
|
||
width: 100%; | ||
height: 4.8rem; | ||
padding: 1.2rem; | ||
|
||
background-color: #a1a1aa; | ||
border-radius: 1.6rem; | ||
box-shadow: 0 0.4rem 0.4rem rgb(0 0 0 / 20%); | ||
|
||
animation: ${isOpen ? toastSlideIn : toastSlideOut} 0.5s ease-in-out forwards; | ||
`; | ||
|
||
export const s_toastText = css` | ||
${theme.typography.captionBold} | ||
color: ${theme.colors.white}; | ||
`; | ||
|
||
const ICON_BACKGROUND_COLORS: Record<Exclude<ToastType, 'default'>, SerializedStyles> = { | ||
warning: css` | ||
background-color: #ef4545; | ||
`, | ||
success: css` | ||
background-color: ${theme.colors.green.mediumDark}; | ||
`, | ||
}; | ||
|
||
export const s_iconBackgroundColor = (type: Exclude<ToastType, 'default'>) => { | ||
return ICON_BACKGROUND_COLORS[type]; | ||
}; | ||
|
||
export const s_iconContainer = css` | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
|
||
width: 2.4rem; | ||
height: 2.4rem; | ||
|
||
border-radius: 50%; | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export type ToastType = 'default' | 'warning' | 'success'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { useEffect, useState } from 'react'; | ||
|
||
import Toast from '.'; | ||
import type { ToastType } from './Toast.type'; | ||
|
||
interface ToastContainerProps { | ||
duration?: number; | ||
type: ToastType; | ||
message: string; | ||
} | ||
|
||
const TOAST_ANIMATION_DURATION_TIME = 500; | ||
|
||
export default function ToastContainer({ type, message, duration = 3000 }: ToastContainerProps) { | ||
const [isOpen, setIsOpen] = useState(true); | ||
|
||
useEffect(() => { | ||
const animationTimer = setTimeout(() => { | ||
setIsOpen(false); | ||
}, duration - TOAST_ANIMATION_DURATION_TIME); | ||
|
||
return () => { | ||
clearTimeout(animationTimer); | ||
}; | ||
}, [duration]); | ||
|
||
return <Toast isOpen={isOpen} type={type} message={message} />; | ||
} |
19 changes: 19 additions & 0 deletions
19
frontend/src/components/_common/Toast/ToastList/ToastList.styles.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { css } from '@emotion/react'; | ||
|
||
export const s_toastListContainer = css` | ||
position: fixed; | ||
z-index: 3; | ||
top: 9rem; | ||
left: 50%; | ||
transform: translateX(-50%); | ||
|
||
display: flex; | ||
flex-direction: column; | ||
gap: 1.2rem; | ||
align-items: center; | ||
justify-content: center; | ||
|
||
width: 100%; | ||
max-width: 43rem; | ||
padding: 1.6rem; | ||
`; |
20 changes: 20 additions & 0 deletions
20
frontend/src/components/_common/Toast/ToastList/ToastList.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { createPortal } from 'react-dom'; | ||
|
||
import useToast from '@hooks/useToast/useToast'; | ||
|
||
import ToastContainer from '../ToastContainer'; | ||
import { s_toastListContainer } from './ToastList.styles'; | ||
|
||
export default function ToastList() { | ||
const { toasts } = useToast(); | ||
|
||
return createPortal( | ||
<div css={s_toastListContainer}> | ||
{toasts && | ||
toasts.map(({ id, type, message, duration }) => ( | ||
<ToastContainer key={id} type={type} message={message} duration={duration} /> | ||
))} | ||
</div>, | ||
document.body, | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import Check from '@assets/images/attendeeCheck.svg'; | ||
import Exclamation from '@assets/images/exclamation.svg'; | ||
|
||
import theme from '@styles/theme'; | ||
|
||
import { | ||
s_iconBackgroundColor, | ||
s_iconContainer, | ||
s_toastContainer, | ||
s_toastText, | ||
} from './Toast.styles'; | ||
import type { ToastType } from './Toast.type'; | ||
|
||
interface ToastProps { | ||
isOpen: boolean; | ||
type: ToastType; | ||
message: string; | ||
} | ||
|
||
const iconMap: Record<ToastType, React.FC<React.SVGProps<SVGSVGElement>> | null> = { | ||
default: null, | ||
success: Check, | ||
warning: Exclamation, | ||
}; | ||
|
||
// 토스트 컴포넌트는 UI를 보여주는 책임만 가질 수 있도록 최대한 책임을 분리하고 스토리북을 활용한 UI 테스트를 쉽게할 수 있도록 한다.(@해리) | ||
export default function Toast({ isOpen, type = 'default', message }: ToastProps) { | ||
const ToastIcon = iconMap[type]; | ||
|
||
return ( | ||
<div css={s_toastContainer(isOpen)}> | ||
{type !== 'default' && ( | ||
<div css={[s_iconContainer, s_iconBackgroundColor(type)]}> | ||
{ToastIcon && <ToastIcon width={16} height={16} stroke={theme.colors.white} />} | ||
</div> | ||
)} | ||
<p css={s_toastText}>{message}</p> | ||
</div> | ||
); | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍👍👍💯💯💯