Skip to content

Commit

Permalink
[Feature/BAR-37] 토스트 컴포넌트 개발 (#16)
Browse files Browse the repository at this point in the history
* feat: grid, flex에서 style 관련된 요소는 모두 className을 통해 들어오도록 변경

* feat: deps 룰 제거

* feat: toastStore 작성

* feat: Toast 컴포넌트 작성

* feat: Toast 테스트 환경 구축

* feat: 토스트 사용 형태 변경

* feat: 토스트 스토리 반영

* feat: clearInterval 추가

* feat: recipe를 사용하는 형태로 변경

* feat: deps lint rule 추가

* refactor: message 변경

* feat: 잘못 추가된 파일 제거

* feat: 두 종류의 토스트 duration 반영

* feat: toast story 작성

* fix: lintstage 실행시 bash 추가

* refactor: 스토리명 변경

* feat: style 사용 변경
  • Loading branch information
miro-ring authored Dec 30, 2023
1 parent 8c6b82e commit 27ed97b
Show file tree
Hide file tree
Showing 9 changed files with 194 additions and 6 deletions.
2 changes: 1 addition & 1 deletion .lintstagedrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"*.{js,jsx,ts,tsx}": ["prettier --write", "eslint --fix"],
"*.{ts,tsx}": "tsc --noEmit"
"*.{ts,tsx}": "bash -c tsc --noEmit"
}
2 changes: 2 additions & 0 deletions pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import type { AppProps } from 'next/app';
import '@/src/styles/global.css';

import TanstackQueryProvider from '@/src/components/Providers/TanstackQueryProvider';
import Toast from '@/src/components/Toast/Toast';

const App = ({ Component, pageProps }: AppProps) => {
return (
<TanstackQueryProvider dehydratedState={pageProps.dehydratedState}>
<Component {...pageProps} />
<Toast />
</TanstackQueryProvider>
);
};
Expand Down
6 changes: 2 additions & 4 deletions pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import type { NextPage } from 'next';

const HomePage: NextPage = () => {
return <>바로</>;
const HomePage = () => {
return <div>바로</div>;
};

export default HomePage;
61 changes: 61 additions & 0 deletions src/components/Toast/Toast.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useEffect } from 'react';
import type { Meta, StoryObj } from '@storybook/react';

import { TOAST_DURATION_TIME } from '@/src/models/toastModel';
import { useToast } from '@/src/stores/toastStore';

import Toast from './Toast';

const meta: Meta<typeof Toast> = {
title: 'Toast',
component: Toast,
decorators: [
(Story) => (
<div style={{ height: '500px' }}>
<Story />
</div>
),
],
};

export default meta;
type Story = StoryObj<typeof Toast>;

export const Basic: Story = {
render: function Render() {
const { showToast } = useToast();

useEffect(() => {
showToast({ message: '테스트', type: TOAST_DURATION_TIME.SHOW });

const interval = setInterval(
() => showToast({ message: '테스트', type: TOAST_DURATION_TIME.SHOW }),
TOAST_DURATION_TIME.SHOW + 1000,
);

return () => clearInterval(interval);
}, [showToast]);

return <Toast />;
},
};

export const WithAction: Story = {
render: function Render() {
const { showToast } = useToast();

useEffect(() => {
showToast({ message: '테스트', type: TOAST_DURATION_TIME.ACTION });

const interval = setInterval(
() =>
showToast({ message: '테스트', type: TOAST_DURATION_TIME.ACTION }),
TOAST_DURATION_TIME.ACTION + 1000,
);

return () => clearInterval(interval);
}, [showToast]);

return <Toast />;
},
};
29 changes: 29 additions & 0 deletions src/components/Toast/Toast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useEffect } from 'react';

import { useToast } from '@/src/stores/toastStore';

import * as styles from './style.css';

const Toast = () => {
const { toastData, hideToast } = useToast();

const { message, type, isToastVisible } = toastData;

useEffect(() => {
let timer;

if (isToastVisible) {
timer = setTimeout(() => hideToast(), type);
}

return () => clearTimeout(timer);
}, [hideToast, isToastVisible, type]);

return (
<div className={styles.toast({ isActive: isToastVisible })} role="alert">
<span>{message}</span>
</div>
);
};

export default Toast;
36 changes: 36 additions & 0 deletions src/components/Toast/style.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { recipe } from '@vanilla-extract/recipes';

import { sprinkles } from '@/src/styles/sprinkles.css';
import { theme } from '@/src/styles/theme.css';
import { modalLayer } from '@/src/styles/utils.css';

export const toast = recipe({
base: [
modalLayer,
sprinkles({
typography: '14/Body/Regular',
}),
{
backgroundColor: theme.colors['Dim/70'],
position: 'fixed',
color: theme.colors['Grey/White'],
bottom: 0,
left: '50%',
transform: 'translateX(-50%) translateY(30px)',
borderRadius: '6px',
textAlign: 'center',
padding: '23px 32px',
transition: 'transform 400ms ease, opacity 400ms',
opacity: 0,
pointerEvents: 'none',
},
],
variants: {
isActive: {
true: {
opacity: 1,
transform: 'translateX(-50%) translateY(-30px)',
},
},
},
});
7 changes: 7 additions & 0 deletions src/models/toastModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const TOAST_DURATION_TIME = {
SHOW: 3500,
ACTION: 5000,
} as const;

export type ToastDurationTime =
(typeof TOAST_DURATION_TIME)[keyof typeof TOAST_DURATION_TIME];
55 changes: 55 additions & 0 deletions src/stores/toastStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { createStore } from 'zustand';
import { shallow } from 'zustand/shallow';
import { useStoreWithEqualityFn as useStore } from 'zustand/traditional';

import {
TOAST_DURATION_TIME,
type ToastDurationTime,
} from '../models/toastModel';

interface ToastData {
message: string;
type: ToastDurationTime;
}

interface ToastState {
isToastVisible: boolean;
}

interface State {
toastData: ToastData & ToastState;
}

interface Action {
showToast: (data: ToastData) => void;
hideToast: () => void;
}

const INITIAL_TOAST_DATA = {
message: '',
type: TOAST_DURATION_TIME.SHOW,
isToastVisible: false,
};

export const toastStore = createStore<State & Action>((set, get) => ({
toastData: INITIAL_TOAST_DATA,
showToast: (data) => set({ toastData: { ...data, isToastVisible: true } }),
hideToast: () => {
set({ toastData: { ...get().toastData, isToastVisible: false } });
},
}));

const useToastStore = <T>(
selector: (state: State & Action) => T,
equals?: (a: T, b: T) => boolean,
) => useStore(toastStore, selector, equals);

export const useToast = () =>
useToastStore(
(state) => ({
toastData: state.toastData,
showToast: state.showToast,
hideToast: state.hideToast,
}),
shallow,
);
2 changes: 1 addition & 1 deletion src/styles/sprinkles.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { COLORS, TYPOGRAPHY } from './tokens';

const fontProperties = defineProperties({
properties: {
fontSize: TYPOGRAPHY,
typography: TYPOGRAPHY,
},
});

Expand Down

0 comments on commit 27ed97b

Please sign in to comment.