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

[Feat/#40] 홈 이동 안내 팝업 모달 제작 #41

Merged
merged 4 commits into from
Feb 2, 2025
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
1 change: 1 addition & 0 deletions .storybook/preview-head.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div id="modal"></div>
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
</head>
<body>
<div id="root"></div>
<div id="modal"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
9 changes: 8 additions & 1 deletion src/components/Home/Home.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import styles from "@/components/Home/Home.module.scss";
import HomeNavigateConfirmModal from "@/components/HomeNavigateConfirmModal/HomeNavigateConfirmModal";
import IconButton from "@/components/ui/IconButton/IconButton";
import Text from "@/components/ui/Text/Text";

import { useOverlay } from "@/hooks/common/useOverlay";

const Home = () => {
const { isOpen, handleClose, handleOpen } = useOverlay();

return (
<div className={styles.Home}>
<div className={styles.HomeTitle}>
Expand All @@ -17,9 +22,11 @@ const Home = () => {
<img src="/assets/img/img-graphic-logo.png" alt="mainLogo" />
</div>
<div className={styles.HomeBottom}>
<IconButton text="갤러리" iconName="gallery" />
<IconButton text="갤러리" iconName="gallery" onClick={handleOpen} />
<IconButton text="카메라" iconName="camera" />
</div>

<HomeNavigateConfirmModal isOpen={isOpen} handleClose={handleClose} />
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
.Modal {
padding: 1.875rem 1.25rem 1.25rem;

& > span {
margin-top: 0.375rem;
}
}

.ButtonWrapper {
display: flex;
align-items: center;
gap: 0.625rem;
margin-top: 1.5rem;

& > button {
width: 8.75rem;
}
}

.ShowButtonWrapper {
display: flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
margin-top: 0.75rem;
width: 100%;
cursor: pointer;

& > svg > path {
fill: rgba(0, 0, 0, 0.15);
}

&.isChecked {
& > svg > path {
fill: rgb(54, 54, 66);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { useState } from "react";

import classNames from "classnames";

import styles from "@/components/HomeNavigateConfirmModal/HomeNavigateConfirmModal.module.scss";
import Button from "@/components/ui/Button/Button";
import Icon from "@/components/ui/Icon/Icon";
import Modal from "@/components/ui/Modal/Modal";
import Text from "@/components/ui/Text/Text";

import { useOverlay } from "@/hooks/common/useOverlay";

import type { Meta, StoryObj, StoryFn } from "@storybook/react";

interface HomeNavigateConfirmModalProps {
isOpen: boolean;
handleClose: () => void;
}

const HomeNavigateConfirmModalStory = ({ isOpen, handleClose }: HomeNavigateConfirmModalProps) => {
const [isShowButtonChecked, setIsShowButtonChecked] = useState(false);

const handleShowButtonClick = () => {
setIsShowButtonChecked((prev) => !prev);
};
return (
<Modal isOpen={isOpen}>
<div className={styles.Modal}>
<Text variant="titleSm" color="primary" align="center" as="h2">
홈으로 가시겠어요?
</Text>
<Text variant="bodyM" color="secondary" align="center" as="p">
복사하지 않은 리뷰는 삭제돼요.
</Text>
<div className={styles.ButtonWrapper}>
<Button text="아니요" variant="tertiary" onClick={handleClose} />
<Button text="네" variant="primary" onClick={handleClose} />
</div>
<button
className={classNames(styles.ShowButtonWrapper, {
[styles.isChecked]: isShowButtonChecked,
})}
onClick={handleShowButtonClick}
>
<Icon name="checkCircle" />
<Text variant="bodyXsm" color={isShowButtonChecked ? "primary" : "tertiary"}>
다시 안볼래요
</Text>
</button>
</div>
</Modal>
);
};

const meta: Meta<typeof HomeNavigateConfirmModalStory> = {
title: "Example/HomeNavigateConfirmModal",
component: HomeNavigateConfirmModalStory,
parameters: {
layout: "centered",
},
tags: ["!autodocs"],
};

export default meta;

const ModalTemplate = () => {
const { isOpen, handleOpen, handleClose } = useOverlay();

return (
<>
<Button text="open modal" onClick={handleOpen} />
<HomeNavigateConfirmModalStory isOpen={isOpen} handleClose={handleClose} />
</>
);
};

const Template: StoryFn<typeof ModalTemplate> = ModalTemplate;

export const ModalStory: StoryObj<HomeNavigateConfirmModalProps> = {
render: Template,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";

import classNames from "classnames";

import styles from "@/components/HomeNavigateConfirmModal/HomeNavigateConfirmModal.module.scss";
import Button from "@/components/ui/Button/Button";
import Icon from "@/components/ui/Icon/Icon";
import Modal from "@/components/ui/Modal/Modal";
import Text from "@/components/ui/Text/Text";

interface HomeNavigateConfirmModalProps {
isOpen: boolean;
handleClose: () => void;
}

const HomeNavigateConfirmModal = ({ isOpen, handleClose }: HomeNavigateConfirmModalProps) => {
const navigate = useNavigate();

// 이후 상태 초기값 재설정
const [isShowButtonChecked, setIsShowButtonChecked] = useState(false);

const handleShowButtonClick = () => {
setIsShowButtonChecked((prev) => !prev);
};

const handleNavigateHome = () => {
handleClose();
navigate("/");
};

return (
<Modal isOpen={isOpen}>
<div className={styles.Modal}>
<Text variant="titleSm" color="primary" align="center" as="h2">
홈으로 가시겠어요?
</Text>
<Text variant="bodyM" color="secondary" align="center" as="p">
복사하지 않은 리뷰는 삭제돼요.
</Text>
<div className={styles.ButtonWrapper}>
<Button text="아니요" variant="tertiary" onClick={handleClose} />
<Button text="네" variant="primary" onClick={handleNavigateHome} />
</div>
<button
className={classNames(styles.ShowButtonWrapper, {
[styles.isChecked]: isShowButtonChecked,
})}
onClick={handleShowButtonClick}
>
<Icon name="checkCircle" />
<Text variant="bodyXsm" color={isShowButtonChecked ? "primary" : "tertiary"}>
다시 안볼래요
</Text>
</button>
</div>
</Modal>
);
};

export default HomeNavigateConfirmModal;
34 changes: 34 additions & 0 deletions src/components/ui/Modal/Modal.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
.ModalBackdrop {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.3);
z-index: 99;

&.Open {
animation: animation-show 300ms cubic-bezier(0.3, 0, 0, 1);
}
}

.Modal {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
z-index: 100;
background-color: var(--color-white);
border-radius: 1.25rem;

&.Open {
animation: animation-show 300ms cubic-bezier(0.3, 0, 0, 1);
}
}

@keyframes animation-show {
from {
opacity: 0;
}

to {
opacity: 1;
}
}
45 changes: 45 additions & 0 deletions src/components/ui/Modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { PropsWithChildren } from "react";
import { useEffect } from "react";

import classNames from "classnames";

import styles from "@/components/ui/Modal/Modal.module.scss";
import Portal from "@/components/ui/Modal/Portal";

interface ModalProps extends PropsWithChildren {
isOpen: boolean;
}

const Modal = ({ isOpen, children }: ModalProps) => {
useEffect(() => {
document.body.style.overflow = "hidden";

return () => {
document.body.style.overflow = "auto";
};
}, []);

return (
<>
{isOpen && (
<Portal elementId="modal">
<div
className={classNames(styles.ModalBackdrop, {
[styles.Open]: isOpen,
})}
/>

<div
className={classNames(styles.Modal, {
[styles.Open]: isOpen,
})}
>
{children}
</div>
</Portal>
)}
</>
);
};

export default Modal;
15 changes: 15 additions & 0 deletions src/components/ui/Modal/Portal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useMemo } from "react";
import type { PropsWithChildren } from "react";
import { createPortal } from "react-dom";

interface PortalProps extends PropsWithChildren {
elementId: string;
}

const Portal = ({ children, elementId }: PortalProps) => {
const rootElement = useMemo(() => document.getElementById(elementId), [elementId]);

return createPortal(children, rootElement as HTMLElement);
};

export default Portal;
19 changes: 19 additions & 0 deletions src/hooks/common/useOverlay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useCallback, useState } from "react";

export const useOverlay = () => {
const [isOpen, setIsOpen] = useState(false);

const handleOpen = useCallback(() => {
setIsOpen(true);
}, [setIsOpen]);

const handleClose = useCallback(() => {
setIsOpen(false);
}, [setIsOpen]);

const handleToggle = useCallback(() => {
setIsOpen((prev) => !prev);
}, []);

return { isOpen, handleOpen, handleClose, handleToggle };
};