Skip to content

Commit

Permalink
64 feat toast 컴포넌트를 구현 (#88)
Browse files Browse the repository at this point in the history
* feat : Toast 컴포넌트 구현

* feat : Toast 컨테이너 구현

* feat : 전역적으로 토스트 관리
  • Loading branch information
HelloWook authored Oct 31, 2024
1 parent d2c22b3 commit 489a925
Show file tree
Hide file tree
Showing 9 changed files with 235 additions and 2 deletions.
2 changes: 2 additions & 0 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import React from 'react';
import GlobalStyles from './styles/globalStyles';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import ToastContainer from '@/features/Toast/ui/ToastContainer';

const queryClient = new QueryClient();

const App: React.FC = () => {
return (
<QueryClientProvider client={queryClient}>
<GlobalStyles />
<ToastContainer />
</QueryClientProvider>
);
};
Expand Down
39 changes: 39 additions & 0 deletions src/features/Toast/hooks/useToastStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { create } from 'zustand';
import { Toast } from '../model/type';

interface ToastStore {
toasts: Toast[];
addToast: (
message: string,
variant: 'success' | 'warning' | 'error'
) => void;
removeToast: (id: number) => void;
}

/**
* addToasdt새로운 Toast 메시지를 추가하고 2초 후 자동 제거
* 호출 시
* const { addToast } = useToastStore(); 불러와서
* () => addToast('Success message!', 'success') 형식으로 사용해주세요
* @param message - 표시할 메시지 내용
* @param variant - 메시지 타입 ('success' | 'warning' | 'error')
*/
export const useToastStore = create<ToastStore>((set) => ({
toasts: [],
addToast: (message, variant) => {
const id = Date.now();
set((state) => ({
toasts: [...state.toasts, { id, message, variant }]
}));

setTimeout(() => {
set((state) => ({
toasts: state.toasts.filter((toast) => toast.id !== id)
}));
}, 2000);
},
removeToast: (id) =>
set((state) => ({
toasts: state.toasts.filter((toast) => toast.id !== id)
}))
}));
5 changes: 5 additions & 0 deletions src/features/Toast/model/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface Toast {
id: number;
variant: 'success' | 'warning' | 'error';
message: string;
}
25 changes: 25 additions & 0 deletions src/features/Toast/ui/Toast.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { Meta, StoryObj } from '@storybook/react';

import Toast from './Toast';

const meta: Meta<typeof Toast> = {
component: Toast,
title: 'features/UI/Toast',
tags: ['autodocs'],
argTypes: {},
};
export default meta;

type Story = StoryObj<typeof Toast>;

export const Default: Story = {
args: {children :"화이팅"},
};

export const Waring: Story = {
args: {children :"화이팅",variant: "warning"},
};

export const Error: Story = {
args: {children :"화이팅", variant : 'error'},
};
64 changes: 64 additions & 0 deletions src/features/Toast/ui/Toast.styeld.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react';
import styled, { keyframes } from 'styled-components';

interface ToastStyledProps {
variant: 'success' | 'warning' | 'error';
}

const slideIn = keyframes`
0% {
opacity: 0;
transform: translateY(-20px);
}
10% {
opacity: 1;
transform: translateY(0);
}
90% {
opacity: 1;
transform: translateY(0);
}
100% {
opacity: 0;
transform: translateY(-20px);
}
`;

export const ToastStyled = styled.div<ToastStyledProps>`
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
width: 514.05px;
height: 77px;
padding: 0 20px;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
border-radius: 6px;
font-family: 'Lato', sans-serif;
font-weight: 400;
font-size: 22px;
letter-spacing: 0.7125px;
position: relative;
margin-bottom: 10px;
background-color: ${({ variant }) => {
switch (variant) {
case 'success':
return '#3BDE86';
case 'warning':
return '#FB4242';
case 'error':
return '#FED046';
default:
return '#ffffff';
}
}};
animation: ${slideIn} 2s ease forwards;
`;
export const ToastMessageStlyed = styled.span`
text-align: center;
text-overflow: ellipsis;
display: inline-block;
white-space: nowrap;
overflow: hidden;
width: 350px;
`;
48 changes: 48 additions & 0 deletions src/features/Toast/ui/Toast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// components/Toast.tsx
import React from 'react';
import { ToastStyled, ToastMessageStlyed } from './Toast.styeld';
import { IoIosWarning, IoMdClose } from 'react-icons/io';
import { FaCheck } from 'react-icons/fa6';
import { MdOutlineError } from 'react-icons/md';

interface ToastProps {
children: React.ReactNode;
variant: 'success' | 'warning' | 'error';
onClose: () => void;
}

const IconStyle = {
width: 35,
height: 35,
position: 'absolute' as const,
left: 30,
top: 20
};

const CloseStlye = {
width: 35,
height: 35,
position: 'absolute' as const,
right: 30,
top: 20
};

const iconComponents = {
success: <FaCheck style={{ ...IconStyle }} />,
warning: <IoIosWarning style={{ ...IconStyle }} />,
error: <MdOutlineError style={{ ...IconStyle }} />
};

const Toast = ({ children, variant = 'success', onClose }: ToastProps) => {
const Icon = iconComponents[variant];

return (
<ToastStyled variant={variant}>
{Icon}
<ToastMessageStlyed>{children}</ToastMessageStlyed>
<IoMdClose onClick={onClose} style={CloseStlye} />
</ToastStyled>
);
};

export default Toast;
27 changes: 27 additions & 0 deletions src/features/Toast/ui/ToastContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import { useToastStore } from '../hooks/useToastStore';
import Toast from './Toast';
import { ToastContainerStyled } from './toastContainer.styled';

const ToastContainer = () => {
const { toasts, removeToast } = useToastStore();

return (
<ToastContainerStyled>
{toasts
.slice()
.reverse()
.map((toast) => (
<Toast
key={toast.id}
variant={toast.variant}
onClose={() => removeToast(toast.id)}
>
{toast.message}
</Toast>
))}
</ToastContainerStyled>
);
};

export default ToastContainer;
8 changes: 8 additions & 0 deletions src/features/Toast/ui/toastContainer.styled.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import styled from 'styled-components';

export const ToastContainerStyled = styled.div`
position: fixed;
margin-bottom: 20px;
top: 20px;
right: 20px;
`;
19 changes: 17 additions & 2 deletions src/shared/ui/Modal/Modal.styled.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,37 @@
import styled from 'styled-components';
import styled, { keyframes } from 'styled-components';

const fadeIn = keyframes`
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
`;

export const ModalStyled = styled.div`
background: white;
width: 370px;
height: 200px;
border-radius: 20px;
box-shadow: inset;
padding: 8px;
position: relative;
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.1);
animation: ${fadeIn} 2 ease forwards;
h1 {
margin-bottom: 30px;
}
svg {
font-size: 30px;
position: absolute;
top: 18px;
right: 20px;
color: #838383;
cursor: pointer;
}
`;

Expand Down

0 comments on commit 489a925

Please sign in to comment.