Skip to content

Commit

Permalink
Refactor/#54 Modal 컴포넌트 재설계 (#55)
Browse files Browse the repository at this point in the history
* chore: add loadable component package

* feat: implement modal management system with context and dynamic loading
  • Loading branch information
kang-kibong authored Oct 22, 2024
1 parent a20fddc commit 2d40e4f
Show file tree
Hide file tree
Showing 13 changed files with 178 additions and 39 deletions.
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export default tseslint.config(
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
'react-refresh/only-export-components': 'off',
'react-hooks/rules-of-hooks': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
},
);
42 changes: 40 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@emotion/css": "^11.13.0",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@loadable/component": "^5.16.4",
"@tanstack/react-query": "^5.56.2",
"csstype": "^3.1.3",
"react": "^18.3.1",
Expand All @@ -46,6 +47,7 @@
"@storybook/react": "^8.3.0",
"@storybook/react-vite": "^8.3.0",
"@storybook/test": "^8.3.0",
"@types/loadable__component": "^5.13.9",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/react-slick": "^0.23.13",
Expand Down
30 changes: 30 additions & 0 deletions src/components/common/Modal/Modals.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useContext } from 'react';
import { ModalsDispatchContext, ModalsStateContext } from './index.context';
import loadable from '@loadable/component';

export interface ModalProps {
[key: string]: unknown;
}

export const modals = {
roleModal: loadable(() => import('@features/auth/SignUp/components/common/RoleModal')),
};

export default function Modals() {
const openedModals = useContext(ModalsStateContext);
const { close } = useContext(ModalsDispatchContext);

return openedModals.map((modal, index) => {
const { Component, props } = modal;
const { onSubmit, ...restProps } = props;

const handleClose = () => close(Component);

const handleSubmit = async () => {
if (typeof onSubmit === 'function') await onSubmit();
handleClose();
};

return <Component {...restProps} key={index} onClose={handleClose} onSubmit={handleSubmit} />;
});
}
19 changes: 19 additions & 0 deletions src/components/common/Modal/hooks/useModals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useContext } from 'react';
import { ModalsDispatchContext } from '../index.context';

export default function useModals() {
const { open, close } = useContext(ModalsDispatchContext);

const openModal = (Component: React.ComponentType<any>, props: any) => {
open(Component, props);
};

const closeModal = (Component: React.ComponentType<any>) => {
close(Component);
};

return {
openModal,
closeModal,
};
}
18 changes: 18 additions & 0 deletions src/components/common/Modal/index.context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { createContext } from 'react';

interface ModalsDispatchContextProps {
open: (Component: React.ComponentType<any>, props: any) => void;
close: (Component: React.ComponentType<any>) => void;
}

export const ModalsDispatchContext = createContext<ModalsDispatchContextProps>({
open: () => {},
close: () => {},
});

interface ModalState {
Component: React.ComponentType<any>;
props: any;
}

export const ModalsStateContext = createContext<ModalState[]>([]);
5 changes: 2 additions & 3 deletions src/components/common/Modal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@ import { ReactNode } from 'react';
export type Props = {
textChildren: ReactNode;
buttonChildren: ReactNode;
borderRadius?: string;
onClose: () => void;
} & React.HTMLAttributes<HTMLDivElement>;

const Modal = ({ textChildren, buttonChildren, borderRadius = '12px', onClose, ...props }: Props) => {
const Modal = ({ textChildren, buttonChildren, onClose, ...props }: Props) => {
return (
<Overlay onClick={onClose}>
<Card borderRadius={borderRadius} {...props} onClick={(e) => e.stopPropagation()}>
<Card borderRadius="12px" {...props} onClick={(e) => e.stopPropagation()}>
<Wrapper>
<TextWrapper>{textChildren}</TextWrapper>
<ButtonWrapper>{buttonChildren}</ButtonWrapper>
Expand Down
29 changes: 29 additions & 0 deletions src/components/providers/Modals.provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { ModalsDispatchContext, ModalsStateContext } from '../common/Modal/index.context';
import { PropsWithChildren, useState, useMemo } from 'react';

interface ModalState {
Component: React.ComponentType<any>;
props: any;
}

const ModalsProvider = ({ children }: PropsWithChildren<unknown>) => {
const [openedModals, setOpenedModals] = useState<ModalState[]>([]);

const open = (Component: React.ComponentType<any>, props: any) => {
setOpenedModals((modals) => [...modals, { Component, props }]);
};

const close = (Component: React.ComponentType<any>) => {
setOpenedModals((modals) => modals.filter((modal) => modal.Component !== Component));
};

const dispatch = useMemo(() => ({ open, close }), []);

return (
<ModalsStateContext.Provider value={openedModals}>
<ModalsDispatchContext.Provider value={dispatch}>{children}</ModalsDispatchContext.Provider>
</ModalsStateContext.Provider>
);
};

export default ModalsProvider;
11 changes: 10 additions & 1 deletion src/components/providers/index.provider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { ReactNode } from 'react';
import GlobalStylesProvider from './GlobalStylesProvider/index.provider';
import ModalsProvider from './Modals.provider';
import Modals from '../common/Modal/Modals';

export default function AppProviders({ children }: { children: ReactNode }) {
return <GlobalStylesProvider>{children}</GlobalStylesProvider>;
return (
<GlobalStylesProvider>
<ModalsProvider>
{children}
<Modals />
</ModalsProvider>
</GlobalStylesProvider>
);
}
13 changes: 5 additions & 8 deletions src/features/auth/SignUp/components/RoleSelection.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import { Flex } from '@components/common';
import RoleSelector from './common/RoleSelector';
import { ReactNode } from 'react';

type Props = {
onRoleSelect: (modalContent: ReactNode) => void;
};
const FLEX_GAP_CONFIG = { x: '30px' };

export default function RoleSelection({ onRoleSelect }: Props) {
export default function RoleSelection() {
return (
<Flex justifyContent="center" alignItems="center" gap={{ x: '30px' }}>
<RoleSelector role="employer" onClick={onRoleSelect} />
<RoleSelector role="worker" onClick={onRoleSelect} />
<Flex justifyContent="center" alignItems="center" gap={FLEX_GAP_CONFIG}>
<RoleSelector role="employer" />
<RoleSelector role="worker" />
</Flex>
);
}
11 changes: 8 additions & 3 deletions src/features/auth/SignUp/components/common/RoleModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ const FLEX_GAP_CONFIG = { x: '16px' };

type Props = {
content: ReactNode;
onSubmit: () => void;
onClose: () => void;
};

export default function RoleModal({ content, onClose }: Props) {
export default function RoleModal({ content, onSubmit, onClose }: Props) {
return (
<Modal
textChildren={
Expand All @@ -27,8 +28,12 @@ export default function RoleModal({ content, onClose }: Props) {
}
buttonChildren={
<Flex gap={FLEX_GAP_CONFIG}>
<Button theme="default">등록할게요</Button>
<Button theme="outlined">괜찮아요</Button>
<Button theme="default" onClick={() => onSubmit()}>
등록할게요
</Button>
<Button theme="outlined" onClick={() => onClose()}>
괜찮아요
</Button>
</Flex>
}
onClose={onClose}
Expand Down
22 changes: 13 additions & 9 deletions src/features/auth/SignUp/components/common/RoleSelector/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Card, Flex, Typo } from '@components/common';
import { roleConfig } from './index.config';
import { bounceAnimation } from '@assets/styles/animations';
import { ReactNode } from 'react';
import { responsiveStyle } from '@utils/responsive';
import useModals from '@components/common/Modal/hooks/useModals';
import { modals } from '@/components/common/Modal/Modals';

const cardStyle = responsiveStyle({
default: { padding: '60px 120px', cursor: 'pointer' },
Expand All @@ -18,17 +19,20 @@ const iconStyle = responsiveStyle({

type Props = {
role: 'employer' | 'worker';
onClick: (modalContent: ReactNode) => void;
};

export default function RoleSelector({ role, onClick }: Props) {
export default function RoleSelector({ role }: Props) {
const { openModal } = useModals();

const handleClick = () => {
openModal(modals.roleModal, {
content: roleConfig[role].modalContent,
onSubmit: () => console.log('이력서 등록 페이지로 이동'),
});
};

return (
<Card
borderColor="blue"
borderRadius="12px"
css={[bounceAnimation, cardStyle]}
onClick={() => onClick(roleConfig[role].modalContent)}
>
<Card borderColor="blue" borderRadius="12px" css={[bounceAnimation, cardStyle]} onClick={handleClick}>
<Flex direction="column" alignItems="center">
<div css={iconStyle}>{roleConfig[role].icon}</div>
<Typo element="h2" color="blue" size="18px" bold>
Expand Down
14 changes: 1 addition & 13 deletions src/pages/auth/SignUp/index.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,20 @@
import { ReactNode, useState } from 'react';
import Layout from '@features/layout';
import { InnerContainer } from '@components/common';
import { responsiveStyle, responsiveSectionPadding } from '@utils/responsive';
import RoleSelection from '@/features/auth/SignUp/components/RoleSelection';
import RoleModal from '@/features/auth/SignUp/components/common/RoleModal';
import useToggle from '@hooks/useToggle';
import SignUpText from '@/features/auth/SignUp/components/SignUpText';

const sectionStyle = responsiveStyle(responsiveSectionPadding);

export default function SignUp() {
const [isToggle, toggle] = useToggle();
const [modalContent, setModalContent] = useState<ReactNode>();

const handleRoleSelect = (modalContent: ReactNode) => {
toggle();
setModalContent(modalContent);
};

return (
<Layout>
<section css={sectionStyle}>
<InnerContainer>
<SignUpText />
<RoleSelection onRoleSelect={handleRoleSelect} />
<RoleSelection />
</InnerContainer>
</section>
{isToggle && <RoleModal content={modalContent} onClose={toggle} />}
</Layout>
);
}

0 comments on commit 2d40e4f

Please sign in to comment.