diff --git a/src/components/modal/modal-dialog.jsx b/src/components/modal/modal-dialog.jsx new file mode 100644 index 0000000..45a9ddc --- /dev/null +++ b/src/components/modal/modal-dialog.jsx @@ -0,0 +1,40 @@ +import styled from "styled-components"; +import { PrimaryButton } from "../button/button"; +import BUTTON_SIZE from "../button/button-size"; +import Colors from "../color/colors"; + +const Title = styled.h2` + margin: 0; + color: ${Colors.gray(600)}; +`; + +const Content = styled.p` + margin: 0; + color: ${Colors.gray(600)}; +`; + +const Action = styled.div` + display: flex; + justify-content: flex-end; + gap: 16px; +`; + +const StyledAlertDialog = styled.div` + display: flex; + flex-direction: column; + gap: 24px; +`; + +function ModalDialog({ title, content, action }) { + return ( + + {title} + {content && {content}} + + {action ?? } + + + ); +} + +export default ModalDialog; diff --git a/src/components/modal/modal.jsx b/src/components/modal/modal.jsx index deb75b5..631f61a 100644 --- a/src/components/modal/modal.jsx +++ b/src/components/modal/modal.jsx @@ -1,7 +1,4 @@ import styled from "styled-components"; -import { useModal } from "../../hooks/use-modal"; -import { PrimaryButton } from "../button/button"; -import BUTTON_SIZE from "../button/button-size"; import Portal from "../portal/portal"; const Content = styled.div` @@ -34,34 +31,14 @@ const ModalContainer = styled.div` align-items: center; `; -const ActionButton = styled.div` - cursor: pointer; -`; - -function Modal({ id, action, children }) { - const { showsModal, setShowsModal } = useModal({ - id: id, - type: "modal", - }); - - const handleClick = () => setShowsModal(true); - const handleConfirmClick = () => setShowsModal(false); - +function Modal({ shows, children }) { return ( <> - {action} - {showsModal && ( + {shows && ( - - {children} - - + {children} diff --git a/src/features/message/components/message-card-detail.jsx b/src/features/message/components/message-card-detail.jsx index 2f90ae9..68f8ad0 100644 --- a/src/features/message/components/message-card-detail.jsx +++ b/src/features/message/components/message-card-detail.jsx @@ -1,9 +1,12 @@ import styled from "styled-components"; +import { PrimaryButton } from "../../../components/button/button"; +import BUTTON_SIZE from "../../../components/button/button-size"; import Colors from "../../../components/color/colors"; import { formatDate } from "../../../utils/formatter"; import MessageSender from "./message-sender"; const Header = styled.header` + width: 100%; display: flex; justify-content: space-between; align-items: center; @@ -12,6 +15,7 @@ const Header = styled.header` `; const Content = styled.div` + width: 100%; margin-top: 16px; font-size: 18px; font-weight: 400; @@ -34,6 +38,10 @@ const Content = styled.div` } `; +const Action = styled.div` + padding-top: 24px; +`; + const CreatedDate = styled.span` font-size: 14px; font-weight: 400; @@ -41,9 +49,13 @@ const CreatedDate = styled.span` color: ${Colors.gray(400)}; `; -const StyledMessageCardDetail = styled.div``; +const StyledMessageCardDetail = styled.div` + display: flex; + flex-direction: column; + align-items: center; +`; -function MessageCardDetail({ message }) { +function MessageCardDetail({ message, onConfirm }) { return (
@@ -55,6 +67,13 @@ function MessageCardDetail({ message }) { {formatDate(message.createdAt, ".")}
{message.content} + + +
); } diff --git a/src/features/message/components/message-card.jsx b/src/features/message/components/message-card.jsx index d20a7cd..1400b3c 100644 --- a/src/features/message/components/message-card.jsx +++ b/src/features/message/components/message-card.jsx @@ -50,11 +50,22 @@ const StyledMessageCard = styled.article` border-radius: 16px; background-color: white; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08); + cursor: ${({ $isEditing }) => ($isEditing ? "default" : "pointer")}; `; -function MessageCard({ isEditing, message, onDelete }) { +function MessageCard({ isEditing, message, onClick, onDelete }) { + const handleClick = () => { + if (isEditing) return; + onClick(message); + }; + + const handleDeleteClick = (event) => { + event.stopPropagation(); + onDelete(message); + }; + return ( - +
)}
diff --git a/src/features/message/components/messages-grid.jsx b/src/features/message/components/messages-grid.jsx index 0e42b5e..7aae610 100644 --- a/src/features/message/components/messages-grid.jsx +++ b/src/features/message/components/messages-grid.jsx @@ -1,8 +1,9 @@ -import { useRef } from "react"; +import { useRef, useState } from "react"; import { useNavigate, useParams } from "react-router"; import styled from "styled-components"; import Modal from "../../../components/modal/modal.jsx"; import { useIntersectionObserver } from "../../../hooks/use-intersection-observer.jsx"; +import { useModal } from "../../../hooks/use-modal.jsx"; import { media } from "../../../utils/media.js"; import MessageCardAdd from "./message-card-add.jsx"; import MessageCardDetail from "./message-card-detail.jsx"; @@ -28,6 +29,10 @@ function MessagesGrid({ isEditing, messages, onDelete, onInfiniteScroll }) { const navigate = useNavigate(); const { id } = useParams(); const infiniteScrollTargetRef = useRef(); + const { showsModal, setShowsModal } = useModal({ + key: "message-modal", + }); + const [modalMessage, setModalMessage] = useState(null); const observerCallback = (entry) => { if (!entry.isIntersecting) return; @@ -39,33 +44,42 @@ function MessagesGrid({ isEditing, messages, onDelete, onInfiniteScroll }) { navigate(`/post/${id}/message`); }; - const handleDeleteClick = (messageId) => { - onDelete(messageId); + const handleMessageClick = (message) => { + setShowsModal(true); + setModalMessage(message); }; - const messageCard = (message) => ( - handleDeleteClick(message.id)} - /> - ); + const handleDeleteClick = (message) => { + onDelete(message); + }; + + const handleModalConfirm = () => { + setShowsModal(false); + setModalMessage(null); + }; return ( - - - {messages.map((message) => - isEditing ? ( - messageCard(message) - ) : ( - - - - ) - )} -
-
+ <> + + + {messages.map((message) => ( + + ))} +
+
+ + + + ); } diff --git a/src/hooks/use-modal-dialog.jsx b/src/hooks/use-modal-dialog.jsx new file mode 100644 index 0000000..5bde450 --- /dev/null +++ b/src/hooks/use-modal-dialog.jsx @@ -0,0 +1,41 @@ +import { useState } from "react"; +import { useModal } from "./use-modal"; + +function useModalDialog() { + const { showsModal, setShowsModal } = useModal({ + key: "delete-modal", + }); + const [title, setTitle] = useState(""); + const [content, setContent] = useState(""); + const [primaryAction, setPrimaryAction] = useState(null); + + const openDialog = ({ title, content, primaryAction }) => { + setShowsModal(true); + setTitle(title); + setContent(content); + setPrimaryAction(() => primaryAction); + }; + + const closeDialog = () => { + setShowsModal(false); + setTitle(""); + setContent(""); + setPrimaryAction(null); + }; + + const onPrimaryAction = () => { + primaryAction(); + closeDialog(); + }; + + return { + showsDialog: showsModal, + dialogTitle: title, + dialogContent: content, + openDialog, + closeDialog, + onPrimaryAction, + }; +} + +export { useModalDialog }; diff --git a/src/hooks/use-modal.jsx b/src/hooks/use-modal.jsx index 9a3a1ad..964b050 100644 --- a/src/hooks/use-modal.jsx +++ b/src/hooks/use-modal.jsx @@ -1,7 +1,6 @@ import { usePortal } from "./use-portal"; -function useModal({ id, type }) { - const key = `${type}_${id}`; +function useModal({ key }) { const { isOpen, setIsOpen } = usePortal({ key }); return { showsModal: isOpen, setShowsModal: setIsOpen }; } diff --git a/src/pages/messages-page.jsx b/src/pages/messages-page.jsx index 5584178..41030f0 100644 --- a/src/pages/messages-page.jsx +++ b/src/pages/messages-page.jsx @@ -8,6 +8,8 @@ import { } from "../components/button/button"; import BUTTON_SIZE from "../components/button/button-size"; import BACKGROUND_COLOR from "../components/color/background-color"; +import Modal from "../components/modal/modal"; +import ModalDialog from "../components/modal/modal-dialog"; import { deleteMessage, getMessages, @@ -20,6 +22,7 @@ import { } from "../features/rolling-paper/api/recipients"; import RollingPaperHeader from "../features/rolling-paper/components/header/rolling-paper-header"; import { useMedia } from "../hooks/use-media"; +import { useModalDialog } from "../hooks/use-modal-dialog"; import ContentLayout from "../layouts/content-layout"; import { media } from "../utils/media"; @@ -75,19 +78,15 @@ function ViewerButtons({ onEdit }) { ); } -function EditingButtons({ onDelete, onCancel }) { +function EditingButtons({ onDelete, onDone }) { return ( - - + ); } @@ -99,6 +98,14 @@ function MessagesPage() { const location = useLocation(); const navigate = useNavigate(); const { id } = useParams(); + const { + showsDialog, + dialogTitle, + dialogContent, + openDialog, + closeDialog, + onPrimaryAction, + } = useModalDialog(); const isEditing = useMemo( () => location.pathname.includes("edit"), @@ -109,28 +116,50 @@ function MessagesPage() { navigate("edit"); }; - const handleRollingPaperDelete = async () => { - try { - await deleteRecipient({ id: recipient.id }); - navigate(`/list`); - } catch (error) { - // TODO: Error 처리 - console.log(error); - } + const handleRollingPaperDelete = () => { + openDialog({ + title: `${recipient.name} 님의 롤링 페이퍼를 삭제할까요?`, + content: "삭제한 롤링 페이퍼는 복원할 수 없어요.", + primaryAction: async () => { + try { + await deleteRecipient({ id: recipient.id }); + navigate(`/list`); + } catch (error) { + // TODO: Error 처리 + console.log(error); + } + }, + }); }; - const handleEditCancel = () => { + const handleEditDone = () => { navigate(-1); }; - const handleMessageDelete = async (messageId) => { - try { - await deleteMessage({ id: messageId }); - setMessages((prev) => prev.filter((message) => message.id !== messageId)); - } catch (error) { - // TODO: Error 처리 - console.log(error); - } + const handleMessageDelete = (message) => { + openDialog({ + title: `${message.sender} 님의 메시지를 삭제할까요?`, + content: "삭제한 메시지는 복원할 수 없어요.", + primaryAction: async () => { + try { + await deleteMessage({ id: message.id }); + setMessages((prev) => + prev.filter((prevMessage) => prevMessage.id !== message.id) + ); + } catch (error) { + // TODO: Error 처리 + console.log(error); + } + }, + }); + }; + + const handleDelete = () => { + onPrimaryAction(); + }; + + const handleDeleteCancel = () => { + closeDialog(); }; const handleInfiniteScroll = async () => { @@ -186,7 +215,7 @@ function MessagesPage() { {isEditing ? ( ) : ( @@ -204,7 +233,33 @@ function MessagesPage() { ); - return isMobile ? content : {content}; + return isMobile ? ( + content + ) : ( + + {content} + + + + + + } + /> + + + ); } export default MessagesPage; diff --git a/src/tests/test-components-page.jsx b/src/tests/test-components-page.jsx index 4fd191f..2a5444f 100644 --- a/src/tests/test-components-page.jsx +++ b/src/tests/test-components-page.jsx @@ -23,6 +23,7 @@ import POPOVER_ALIGNMENT from "../components/popover/popover-alignment"; import TextField from "../components/text-field/text-field"; import TEXT_FIELD_TYPE from "../components/text-field/text-field-type"; import Toast from "../components/toast/toast"; +import { useModal } from "../hooks/use-modal"; import { useToast } from "../hooks/use-toast"; const OutlinedHeader = styled(Header)` @@ -49,6 +50,13 @@ function TestComponentsPage() { const handleToastClick = () => setShowsToast(true); const handleToastDismiss = () => setShowsToast(false); + /* Modal */ + const { showsModal, setShowsModal } = useModal({ + key: "test-modal", + }); + const handleModalOpen = () => setShowsModal(true); + const handleModalClose = () => setShowsModal(false); + return (
- } - > + +

This is Modal.

+