Skip to content

Commit

Permalink
15기 모집 알람 모달 추가 (#341)
Browse files Browse the repository at this point in the history
* feat: 모집안내 -> 디프만 카카오톡 연결

* refactor: 버튼 분리

* feat: 지원하기 버튼 없애기

* fix: 주석 제거

---------

Co-authored-by: byun sumi <[email protected]>
  • Loading branch information
sumi-0011 and byun sumi authored Feb 21, 2024
1 parent 60e83ad commit 30bdb41
Show file tree
Hide file tree
Showing 11 changed files with 315 additions and 91 deletions.
Binary file added public/images/kakao-qr.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/components/GNB/MobileMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function MobileMenu({ onClickMenu }: MobileMenuProps) {
return (
<m.article
initial={{ height: 0, opacity: 0 }}
animate={{ height: '280px', opacity: 1 }}
animate={{ height: '234px', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
css={mobileMenuCss}
>
Expand Down
100 changes: 100 additions & 0 deletions src/components/GuideModalButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React, { useState } from 'react';
import Image from 'next/image';
import { css, Theme } from '@emotion/react';

import { Button } from '~/components/Button';
import CloseIcon from '~/components/Icons/CloseIcon';
import Modal from '~/components/Modal/Modal';
import { mediaQuery } from '~/styles/media';

function GuideModalButton() {
const [isAlertOpen, setIsAlertOpen] = useState(false);

const onAlertClose = () => setIsAlertOpen(false);

const onAlertButtonClick = () => {
setIsAlertOpen(true);
};
return (
<>
<Button size="lg" overrideCss={buttonCss} onClick={onAlertButtonClick}>
15기 알림신청
</Button>

<Modal isShowing={isAlertOpen} onClickOutside={onAlertClose} mode="wait">
<article css={modalCss}>
<h2>15기 알림 신청</h2>
<p>디프만 카카오톡 채널 친구 추가 시 기수 모집 알림을 보내드립니다</p>
<Image src={'/images/kakao-qr.png'} alt="kakao qr link" width={90} height={90} />
<div css={buttonWrapperCss}>
<a href="https://pf.kakao.com/_xoxmcxed">
<Button size="md">카카오톡 채널 바로가기</Button>
</a>
</div>
<div className="close-icon" onClick={onAlertClose}>
<CloseIcon />
</div>
</article>
</Modal>
</>
);
}

export default GuideModalButton;

const buttonCss = css`
max-width: 443px;
margin: 0 auto;
${mediaQuery('mobile')} {
max-width: 266px;
}
`;

const buttonWrapperCss = css`
margin-top: 39px;
margin-bottom: 24px;
padding: 0 24px;
width: 100%;
button {
height: 46px;
width: 100%;
${mediaQuery('mobile')} {
height: 46px;
}
}
`;

const modalCss = (theme: Theme) => css`
background-color: ${theme.colors.black400};
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
width: 320px;
position: relative;
h2 {
${theme.typos.pretendard.body1};
color: ${theme.colors.white};
margin-bottom: 8px;
margin-top: 47px;
}
p {
${theme.typos.pretendard.body2};
color: ${theme.colors.gray100};
margin-bottom: 39px;
max-width: 221px;
}
.close-icon {
position: absolute;
top: 16px;
right: 16px;
cursor: pointer;
}
`;
24 changes: 24 additions & 0 deletions src/components/Icons/CloseIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Props, Svg } from '~/components/Icons/Svg';

function CloseIcon({ color = '#606475', ...props }: Props) {
return (
<Svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
{...props}
>
<path
d="M18 18L12 12M12 12L6 6M12 12L18 6M12 12L6 18"
stroke={color}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</Svg>
);
}

export default CloseIcon;
29 changes: 29 additions & 0 deletions src/components/Modal/AnimatePortal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { type ComponentProps } from 'react';
import { AnimatePresence } from 'framer-motion';

import Portal from '~/components/Modal/Portal';

interface Props extends ComponentProps<typeof Portal> {
/**
* children의 렌더링 여부
*/
isShowing: boolean;
/**
* framer-motion AnimatePresence의 mode
* @default 'wait'
*/
mode?: ComponentProps<typeof AnimatePresence>['mode'];
}

/**
* @description Portal을 AnimatePresence와 함께 사용합니다
*/
const AnimatePortal = ({ children, isShowing, mode = 'wait' }: Props) => {
return (
<Portal>
<AnimatePresence mode={mode}>{isShowing && children}</AnimatePresence>
</Portal>
);
};

export default AnimatePortal;
121 changes: 121 additions & 0 deletions src/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { type ComponentProps, type MouseEvent, type PropsWithChildren } from 'react';
import { css } from '@emotion/react';
import { m, Variants } from 'framer-motion';

import AnimatePortal from '~/components/Modal/AnimatePortal';
import useScrollLock from '~/components/Modal/useScrollLock';

interface Props {
/**
* 외부영역 클릭시 호출될 함수
*/
onClickOutside?: VoidFunction;
}

/**
*
* @param isShowing 열림/닫힘 상태
* @param mode AnimatePresence mode
* @param onClickOutside 외부영역 클릭시 호출될 함수
*/
const Modal = ({
isShowing,
mode,
onClickOutside,
children,
}: PropsWithChildren<Props> & ComponentProps<typeof AnimatePortal>) => {
useScrollLock({ lock: isShowing });

return (
<AnimatePortal isShowing={isShowing} mode={mode}>
<div css={dialogPositionCss}>
<ModalBlur onClickOutside={onClickOutside} />
<m.div
variants={defaultFadeInVariants}
initial="initial"
animate="animate"
exit="exit"
css={containerCss}
>
{children}
</m.div>
</div>
</AnimatePortal>
);
};
const defaultEasing = [0.6, -0.05, 0.01, 0.99];

const defaultFadeInVariants: Variants = {
initial: {
opacity: 0,
transition: { duration: 0.3, ease: defaultEasing },
willChange: 'opacity',
},
animate: {
opacity: 1,
transition: { duration: 0.3, ease: defaultEasing },
willChange: 'opacity',
},
exit: {
opacity: 0,
transition: { duration: 0.3, ease: defaultEasing },
willChange: 'opacity',
},
};

const dialogPositionCss = css`
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100vh;
`;

const ModalBlur = ({ onClickOutside }: Pick<Props, 'onClickOutside'>) => {
const onClickOutsideDefault = (e: MouseEvent) => {
if (e.target !== e.currentTarget) return;
if (onClickOutside) onClickOutside();
};

return (
<m.div
onClick={onClickOutsideDefault}
css={blurCss}
variants={defaultFadeInVariants}
initial="initial"
animate="animate"
exit="exit"
style={{
cursor: onClickOutside ? 'pointer' : 'default',
}}
/>
);
};

const blurCss = css`
position: fixed;
z-index: 9999;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--DIM-70, rgba(19, 28, 40, 0.7));
`;

const containerCss = css`
position: fixed;
z-index: 10000;
display: flex;
flex-direction: column;
border-radius: 16px;
`;

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

/**
* @description react.createPortal을 이용해 document.body에 children을 렌더링합니다
*/
const Portal = ({ children }: PropsWithChildren) => {
const [container, setContainer] = useState<Element | null>(null);

useEffect(() => {
if (document) {
setContainer(document.body);
}
}, []);

if (!container) return null;

return createPortal(children, container);
};

export default Portal;
14 changes: 14 additions & 0 deletions src/components/Modal/useScrollLock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useEffect } from 'react';

const useScrollLock = ({ lock }: { lock: boolean }): void => {
useEffect((): (() => void) => {
const originalStyle: string = window.getComputedStyle(document.body).overflow;
if (lock) {
document.body.style.overflow = 'hidden';
}

return () => (document.body.style.overflow = originalStyle);
}, [lock]);
};

export default useScrollLock;
Loading

0 comments on commit 30bdb41

Please sign in to comment.