diff --git a/src/components/common/ModalWindow.jsx b/src/components/common/ModalWindow.jsx new file mode 100644 index 0000000..e6aeb84 --- /dev/null +++ b/src/components/common/ModalWindow.jsx @@ -0,0 +1,95 @@ +import React from 'react'; + +import styled from '@emotion/styled'; + +import { css } from '@emotion/react'; +import Button from '../../styles/Button'; + +const ModalWindowWrapper = styled.div` + position: fixed; + z-index: 101; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.25); + display: flex; + justify-content: center; + align-items: center; + + ${(props) => props.visible && css` + &.animation { + animation-name: fade-in; + animation-fill-mode: both; + animation-duration: 0.3s; + } + + @keyframes fade-in { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } + } + `}; +`; + +const ModalBoxWrapper = styled.div` + display: flex; + flex-direction: column; + width: 320px; + background: white; + padding: 1.5rem; + border-radius: 6px; + box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.125); + h2 { + margin-top: 0; + margin-bottom: 1rem; + } + p { + margin-bottom: 2rem; + } + .buttons { + display: flex; + justify-content: flex-end; + } +`; + +const StyledButton = styled(Button)` + height: 2rem; + & + & { + margin-left: 0.75rem; + } +`; + +const ModalWindow = ({ + visible, + title, + description, + confirmText = '확인', + cancelText = '취소', + onConfirm, + onCancel, +}) => { + if (!visible) { + return null; + } + + return ( + + + {title} + {description} + + {cancelText} + {onConfirm && ( + {confirmText} + )} + + + + ); +}; + +export default ModalWindow; diff --git a/src/components/common/ModalWindow.test.jsx b/src/components/common/ModalWindow.test.jsx new file mode 100644 index 0000000..adf3bbc --- /dev/null +++ b/src/components/common/ModalWindow.test.jsx @@ -0,0 +1,49 @@ +import React from 'react'; + +import { render } from '@testing-library/react'; + +import ModalWindow from './ModalWindow'; + +describe('ModalWindow', () => { + const handleConfirm = jest.fn(); + const handleCancel = jest.fn(); + + const renderModalWindow = ({ visible, title, description }) => render(( + + )); + + context('with visible', () => { + const modal = { + visible: true, + title: '타이틀', + description: '내용', + }; + + it('renders Modal text', () => { + const { container } = renderModalWindow(modal); + + expect(container).toHaveTextContent('타이틀'); + expect(container).toHaveTextContent('내용'); + }); + }); + + context('without visible', () => { + const modal = { + visible: false, + title: '타이틀', + description: '내용', + }; + + it("doesn't renders Modal text", () => { + const { container } = renderModalWindow(modal); + + expect(container).toBeEmptyDOMElement(); + }); + }); +}); diff --git a/src/components/introduce/ApplyStatusButton.jsx b/src/components/introduce/ApplyStatusButton.jsx index 5d65916..a88dceb 100644 --- a/src/components/introduce/ApplyStatusButton.jsx +++ b/src/components/introduce/ApplyStatusButton.jsx @@ -44,7 +44,7 @@ const ApplyStatusButtonWrapper = styled.button` `; const ApplyStatusButton = ({ - timeStatus, onApply, user, applyStatus, + timeStatus, onApply, applyStatus, }) => { if (applyStatus) { return ( @@ -68,17 +68,6 @@ const ApplyStatusButton = ({ ); } - if (!user) { - return ( - - 로그인 후 신청 가능합니다. - - ); - } - return ( { const renderApplyStatusButton = ({ applyStatus = false, timeStatus = false, - user = true, }) => render(( )); @@ -36,14 +34,6 @@ describe('ApplyStatusButton', () => { }); }); - context('When not log in', () => { - it('renders "You can apply after logging in." text', () => { - const { container } = renderApplyStatusButton({ user: false }); - - expect(container).toHaveTextContent('로그인 후 신청 가능합니다.'); - }); - }); - context('When it is possible to apply', () => { it('renders "apply" text', () => { const { container } = renderApplyStatusButton({}); diff --git a/src/components/introduce/IntroduceHeader.jsx b/src/components/introduce/IntroduceHeader.jsx new file mode 100644 index 0000000..593cf9c --- /dev/null +++ b/src/components/introduce/IntroduceHeader.jsx @@ -0,0 +1,70 @@ +import React, { useState } from 'react'; + +import styled from '@emotion/styled'; + +import palette from '../../styles/palette'; +import { changeDateToTime, isCheckedTimeStatus } from '../../util/utils'; + +import ApplyStatusButton from './ApplyStatusButton'; +import AskLoginModal from './modals/AskLoginModal'; + +const IntroduceHeaderWrapper = styled.div` + border-bottom: 2px solid ${palette.gray[4]}; + padding-bottom: 1.5rem; + margin-bottom: 2rem; + display: flex; + justify-content: space-between; + h1 { + font-size: 2.3rem; + line-height: 1.5; + margin: 0; + } +`; + +const IntroduceHeader = ({ + group, onApply, user, realTime, +}) => { + const [modal, setModal] = useState(false); + + const { + title, moderatorId, participants, applyEndDate, + } = group; + + const applyEndTime = changeDateToTime(applyEndDate); + + const onApplyClick = () => { + setModal(true); + }; + + const handleCancel = () => { + setModal(false); + }; + + const handleApply = () => { + if (!user) { + onApplyClick(); + return; + } + + onApply(); + }; + + return ( + + {title} + {moderatorId !== user && ( + <> + + + > + )} + + ); +}; + +export default IntroduceHeader; diff --git a/src/components/introduce/IntroduceHeader.test.jsx b/src/components/introduce/IntroduceHeader.test.jsx new file mode 100644 index 0000000..e11207e --- /dev/null +++ b/src/components/introduce/IntroduceHeader.test.jsx @@ -0,0 +1,169 @@ +import React from 'react'; + +import { fireEvent, render } from '@testing-library/react'; + +import IntroduceHeader from './IntroduceHeader'; + +import STUDY_GROUP from '../../../fixtures/study-group'; + +describe('IntroduceHeader', () => { + const handleApply = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + const renderIntroduceHeader = ({ group, time, user }) => render(( + + )); + + it('renders study group title and contents', () => { + const { container } = renderIntroduceHeader({ group: STUDY_GROUP }); + + expect(container).toHaveTextContent('스터디를 소개합니다.2'); + }); + + context('When the author and the logged-in user have the same ID', () => { + it("doesn't renders apply button", () => { + const { container } = renderIntroduceHeader({ group: STUDY_GROUP, user: 'user2' }); + + expect(container).not.toHaveTextContent('신청하기'); + }); + }); + + context('When the study recruitment is closed', () => { + const time = Date.now(); + + describe('current time is after the recruitment deadline', () => { + const nowDate = new Date(); + const yesterday = nowDate.setDate(nowDate.getDate() - 1); + + const group = { + ...STUDY_GROUP, + applyEndDate: yesterday, + participants: [ + 'user2', + ], + personnel: 2, + }; + + it('renders recruitment closed text', () => { + const { container } = renderIntroduceHeader({ group, time }); + + expect(container).toHaveTextContent('모집 마감'); + }); + }); + + describe('When the number of study group participants equals the maximum number of participants', () => { + const nowDate = new Date(); + const tomorrow = nowDate.setDate(nowDate.getDate() + 1); + + const group = { + ...STUDY_GROUP, + applyEndDate: tomorrow, + participants: [ + 'user2', + 'user3', + ], + personnel: 2, + }; + + it('renders recruitment closed text', () => { + const { container } = renderIntroduceHeader({ group, time }); + + expect(container).toHaveTextContent('모집 마감'); + }); + }); + + describe('When the user clicks the Apply button without logging in', () => { + const nowDate = new Date(); + const tomorrow = nowDate.setDate(nowDate.getDate() + 1); + + const group = { + ...STUDY_GROUP, + applyEndDate: tomorrow, + participants: [ + 'user2', + ], + personnel: 2, + }; + + it('renders modal window appears and application failure message', () => { + const { container, getByText } = renderIntroduceHeader({ group, time }); + + const button = getByText('신청하기'); + + expect(button).not.toBeNull(); + + fireEvent.click(button); + + expect(handleApply).not.toBeCalled(); + + expect(container).toHaveTextContent('로그인 후 신청 가능합니다.'); + + fireEvent.click(getByText('확인')); + + expect(container).not.toHaveTextContent('로그인 후 신청 가능합니다.'); + }); + }); + }); + + context('When the study recruitment is opened', () => { + const time = Date.now(); + + describe(`current time is before the recruitment deadline and + when the number of study group participants is less than the maximum number of participants`, () => { + const nowDate = new Date(); + const tomorrow = nowDate.setDate(nowDate.getDate() + 1); + + const group = { + ...STUDY_GROUP, + applyEndDate: tomorrow, + participants: [ + 'user2', + ], + personnel: 2, + }; + + it('renders recruitment apply text', () => { + const { container } = renderIntroduceHeader({ group, time }); + + expect(container).toHaveTextContent('신청하기'); + expect(container).not.toHaveTextContent('모집마감'); + }); + }); + + describe('When the user clicks the Apply button after logging in', () => { + const nowDate = new Date(); + const tomorrow = nowDate.setDate(nowDate.getDate() + 1); + + const group = { + ...STUDY_GROUP, + applyEndDate: tomorrow, + participants: [ + 'user2', + ], + personnel: 2, + }; + + it('renders modal window appears and application failure message', () => { + const { container, getByText } = renderIntroduceHeader({ group, time, user: 'user' }); + + const button = getByText('신청하기'); + + expect(button).not.toBeNull(); + + fireEvent.click(button); + + expect(handleApply).toBeCalled(); + + expect(container).not.toHaveTextContent('로그인 후 신청 가능합니다.'); + }); + }); + }); +}); diff --git a/src/components/introduce/StudyIntroduceForm.jsx b/src/components/introduce/StudyIntroduceForm.jsx index 3c6f0a0..9d381fd 100644 --- a/src/components/introduce/StudyIntroduceForm.jsx +++ b/src/components/introduce/StudyIntroduceForm.jsx @@ -4,29 +4,11 @@ import styled from '@emotion/styled'; import Moment from 'react-moment'; -import { changeDateToTime, isCheckedTimeStatus } from '../../util/utils'; +import { changeDateToTime } from '../../util/utils'; import Tags from '../common/Tags'; import palette from '../../styles/palette'; import DateTimeChange from '../common/DateTimeChange'; -import ApplyStatusButton from './ApplyStatusButton'; - -const StudyIntroduceWrapper = styled.div` - margin-top: 6em; -`; - -const IntroduceHeaderWrapper = styled.div` - border-bottom: 2px solid ${palette.gray[4]}; - padding-bottom: 1.5rem; - margin-bottom: 2rem; - display: flex; - justify-content: space-between; - h1 { - font-size: 2.3rem; - line-height: 1.5; - margin: 0; - } -`; const IntroduceReferenceWrapper = styled.div` display: flex; @@ -85,27 +67,16 @@ const IntroduceContent = styled.div` `; const StudyIntroduceForm = ({ - group, realTime, onApply, user, + group, realTime, }) => { const { - title, contents, tags, moderatorId, personnel, participants, applyEndDate, createDate, + contents, tags, moderatorId, personnel, participants, applyEndDate, createDate, } = group; const applyEndTime = changeDateToTime(applyEndDate); return ( - - - {title} - {moderatorId !== user && ( - - )} - + <> {`🙋♂️ ${moderatorId}`} @@ -134,7 +105,7 @@ const StudyIntroduceForm = ({ - + > ); }; diff --git a/src/components/introduce/StudyIntroduceForm.test.jsx b/src/components/introduce/StudyIntroduceForm.test.jsx index 5f167d6..43bc2b9 100644 --- a/src/components/introduce/StudyIntroduceForm.test.jsx +++ b/src/components/introduce/StudyIntroduceForm.test.jsx @@ -9,10 +9,9 @@ import StudyIntroduceForm from './StudyIntroduceForm'; import STUDY_GROUP from '../../../fixtures/study-group'; describe('StudyIntroduceForm', () => { - const renderStudyIntroduceForm = ({ group, time, user = 'user' }) => render(( + const renderStudyIntroduceForm = ({ group, time }) => render(( @@ -25,96 +24,9 @@ describe('StudyIntroduceForm', () => { expect(container).toHaveTextContent('2020년 12월 06일'); }); - it('renders study group title and contents', () => { - const { container } = renderStudyIntroduceForm({ group: STUDY_GROUP }); - - expect(container).toHaveTextContent('스터디를 소개합니다.2'); - }); - it('renders links of tags', () => { const { container } = renderStudyIntroduceForm({ group: STUDY_GROUP }); expect(container.innerHTML).toContain(' { - it("doesn't renders apply button", () => { - const { container } = renderStudyIntroduceForm({ group: STUDY_GROUP, user: 'user2' }); - - expect(container).not.toHaveTextContent('신청하기'); - }); - }); - - context('When the study recruitment is closed', () => { - const time = Date.now(); - - describe('current time is after the recruitment deadline', () => { - const nowDate = new Date(); - const yesterday = nowDate.setDate(nowDate.getDate() - 1); - - const group = { - ...STUDY_GROUP, - applyEndDate: yesterday, - participants: [ - 'user2', - ], - personnel: 2, - }; - - it('renders recruitment closed text', () => { - const { container } = renderStudyIntroduceForm({ group, time }); - - expect(container).toHaveTextContent('모집마감'); - expect(container).not.toHaveTextContent('신청하기'); - }); - }); - - describe('When the number of study group participants equals the maximum number of participants', () => { - const nowDate = new Date(); - const tomorrow = nowDate.setDate(nowDate.getDate() + 1); - - const group = { - ...STUDY_GROUP, - applyEndDate: tomorrow, - participants: [ - 'user2', - 'user3', - ], - personnel: 2, - }; - - it('renders recruitment closed text', () => { - const { container } = renderStudyIntroduceForm({ group, time }); - - expect(container).toHaveTextContent('모집마감'); - expect(container).not.toHaveTextContent('신청하기'); - }); - }); - }); - - context('When the study recruitment is opened', () => { - const time = Date.now(); - - describe(`current time is before the recruitment deadline and - when the number of study group participants is less than the maximum number of participants`, () => { - const nowDate = new Date(); - const tomorrow = nowDate.setDate(nowDate.getDate() + 1); - - const group = { - ...STUDY_GROUP, - applyEndDate: tomorrow, - participants: [ - 'user2', - ], - personnel: 2, - }; - - it('renders recruitment apply text', () => { - const { container } = renderStudyIntroduceForm({ group, time }); - - expect(container).toHaveTextContent('신청하기'); - expect(container).not.toHaveTextContent('모집마감'); - }); - }); - }); }); diff --git a/src/components/introduce/modals/AskLoginModal.jsx b/src/components/introduce/modals/AskLoginModal.jsx new file mode 100644 index 0000000..e87ca57 --- /dev/null +++ b/src/components/introduce/modals/AskLoginModal.jsx @@ -0,0 +1,15 @@ +import React from 'react'; + +import ModalWindow from '../../common/ModalWindow'; + +const AskLoginModal = ({ visible, onCancel }) => ( + +); + +export default AskLoginModal; diff --git a/src/components/introduce/modals/AskLoginModal.test.jsx b/src/components/introduce/modals/AskLoginModal.test.jsx new file mode 100644 index 0000000..138012e --- /dev/null +++ b/src/components/introduce/modals/AskLoginModal.test.jsx @@ -0,0 +1,41 @@ +import React from 'react'; + +import { render } from '@testing-library/react'; + +import AskLoginModal from './AskLoginModal'; + +describe('AskLoginModal', () => { + const handleCancel = jest.fn(); + + const renderAskLoginModal = ({ visible }) => render(( + + )); + + context('with visible', () => { + const modal = { + visible: true, + }; + + it('renders Modal text', () => { + const { container } = renderAskLoginModal(modal); + + expect(container).toHaveTextContent('신청 실패'); + expect(container).toHaveTextContent('로그인 후 신청 가능합니다.'); + }); + }); + + context('without visible', () => { + const modal = { + visible: false, + }; + + it("doesn't renders Modal text", () => { + const { container } = renderAskLoginModal(modal); + + expect(container).toBeEmptyDOMElement(); + }); + }); +}); diff --git a/src/containers/introduce/IntroduceContainer.jsx b/src/containers/introduce/IntroduceContainer.jsx index 6f84171..62ef687 100644 --- a/src/containers/introduce/IntroduceContainer.jsx +++ b/src/containers/introduce/IntroduceContainer.jsx @@ -8,6 +8,7 @@ import { loadStudyGroup, updateStudyGroup } from '../../reducers/groupSlice'; import StudyIntroduceForm from '../../components/introduce/StudyIntroduceForm'; import GroupContentLoader from '../../components/introduce/GroupsContentLoader'; +import IntroduceHeader from '../../components/introduce/IntroduceHeader'; const IntroduceContainer = ({ groupId }) => { const [realTime, setRealTime] = useState(Date.now()); @@ -26,10 +27,8 @@ const IntroduceContainer = ({ groupId }) => { }, 1000); const onApplyStudy = useCallback(() => { - if (user) { - dispatch(updateStudyGroup()); - } - }, [dispatch, user]); + dispatch(updateStudyGroup()); + }, [dispatch]); if (!group) { return ( @@ -38,12 +37,18 @@ const IntroduceContainer = ({ groupId }) => { } return ( - + <> + + + > ); }; diff --git a/src/containers/introduce/IntroduceContainer.test.jsx b/src/containers/introduce/IntroduceContainer.test.jsx index 2bc9039..ed8b820 100644 --- a/src/containers/introduce/IntroduceContainer.test.jsx +++ b/src/containers/introduce/IntroduceContainer.test.jsx @@ -73,7 +73,7 @@ describe('IntroduceContainer', () => { }); }); - context('with user', () => { + describe('with user', () => { given('group', () => (STUDY_GROUP)); given('user', () => ('user')); @@ -89,21 +89,4 @@ describe('IntroduceContainer', () => { expect(dispatch).toBeCalledTimes(2); }); }); - - context('without user', () => { - given('group', () => (STUDY_GROUP)); - given('user', () => (null)); - - it("click event doesn't dispatches action call updateStudyGroup", () => { - const { getByText } = renderIntroduceContainer(1); - - const button = getByText('로그인 후 신청 가능합니다.'); - - expect(button).not.toBeNull(); - - fireEvent.click(button); - - expect(dispatch).toBeCalledTimes(1); - }); - }); }); diff --git a/src/pages/IntroducePage.jsx b/src/pages/IntroducePage.jsx index 444cfba..c285fac 100644 --- a/src/pages/IntroducePage.jsx +++ b/src/pages/IntroducePage.jsx @@ -2,17 +2,23 @@ import React from 'react'; import { useParams } from 'react-router-dom'; +import styled from '@emotion/styled'; + import Responsive from '../styles/Responsive'; import IntroduceContainer from '../containers/introduce/IntroduceContainer'; +const IntroducePageWrapper = styled(Responsive)` + margin-top: 6em; +`; + const IntroducePage = ({ params }) => { const { id } = params || useParams(); return ( - + - + ); }; export default React.memo(IntroducePage);
{description}