diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html new file mode 100644 index 0000000..2e3ae13 --- /dev/null +++ b/.storybook/preview-head.html @@ -0,0 +1 @@ + diff --git a/index.html b/index.html index be6dbad..6ccc736 100644 --- a/index.html +++ b/index.html @@ -7,6 +7,7 @@
+ diff --git a/src/components/Home/Home.tsx b/src/components/Home/Home.tsx index b0eac9e..61811e8 100644 --- a/src/components/Home/Home.tsx +++ b/src/components/Home/Home.tsx @@ -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 (
@@ -17,9 +22,11 @@ const Home = () => { mainLogo
- +
+ +
); }; diff --git a/src/components/HomeNavigateConfirmModal/HomeNavigateConfirmModal.module.scss b/src/components/HomeNavigateConfirmModal/HomeNavigateConfirmModal.module.scss new file mode 100644 index 0000000..c8a4771 --- /dev/null +++ b/src/components/HomeNavigateConfirmModal/HomeNavigateConfirmModal.module.scss @@ -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); + } + } +} diff --git a/src/components/HomeNavigateConfirmModal/HomeNavigateConfirmModal.stories.tsx b/src/components/HomeNavigateConfirmModal/HomeNavigateConfirmModal.stories.tsx new file mode 100644 index 0000000..da92eed --- /dev/null +++ b/src/components/HomeNavigateConfirmModal/HomeNavigateConfirmModal.stories.tsx @@ -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 ( + +
+ + 홈으로 가시겠어요? + + + 복사하지 않은 리뷰는 삭제돼요. + +
+
+ +
+
+ ); +}; + +const meta: Meta = { + title: "Example/HomeNavigateConfirmModal", + component: HomeNavigateConfirmModalStory, + parameters: { + layout: "centered", + }, + tags: ["!autodocs"], +}; + +export default meta; + +const ModalTemplate = () => { + const { isOpen, handleOpen, handleClose } = useOverlay(); + + return ( + <> + + + + ); +}; + +export default HomeNavigateConfirmModal; diff --git a/src/components/ui/Modal/Modal.module.scss b/src/components/ui/Modal/Modal.module.scss new file mode 100644 index 0000000..61d0d3d --- /dev/null +++ b/src/components/ui/Modal/Modal.module.scss @@ -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; + } +} diff --git a/src/components/ui/Modal/Modal.tsx b/src/components/ui/Modal/Modal.tsx new file mode 100644 index 0000000..4c35293 --- /dev/null +++ b/src/components/ui/Modal/Modal.tsx @@ -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 && ( + +
+ +
+ {children} +
+ + )} + + ); +}; + +export default Modal; diff --git a/src/components/ui/Modal/Portal.tsx b/src/components/ui/Modal/Portal.tsx new file mode 100644 index 0000000..5d31ca6 --- /dev/null +++ b/src/components/ui/Modal/Portal.tsx @@ -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; diff --git a/src/hooks/common/useOverlay.ts b/src/hooks/common/useOverlay.ts new file mode 100644 index 0000000..4fdcb63 --- /dev/null +++ b/src/hooks/common/useOverlay.ts @@ -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 }; +};