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

[SP0] 모집 안내 플로팅 배너 구현 #161

Merged
merged 12 commits into from
Sep 11, 2023
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@types/qs": "6.9.7",
"axios": "^0.27.2",
"classcat": "^5.0.4",
"dayjs": "^1.11.3",
"nanoid": "4.0.2",
"next": "13.3.0",
"qs": "6.11.1",
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 47 additions & 0 deletions src/components/common/Timer/Timer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { FC, useState } from 'react';
import useInterval from '@src/hooks/useInterval';
import { convertMsIntoDate } from '@src/utils/convertMsIntoDate';

interface TimerProps {
targetDate: Date;
prefix?: string;
suffix?: string;
endMessage: string;
}

type TimeObj = ReturnType<typeof convertMsIntoDate>;

const Timer: FC<TimerProps> = (props) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

우왕 FC로 하신 이유는 무엇인가용!?

const [timeDiff, setTimeDiff] = useState<TimeObj | 'prepare' | 'timeEnd'>('prepare');

useInterval(() => {
const diff = props.targetDate.getTime() - Date.now();
if (diff >= 0) {
setTimeDiff(convertMsIntoDate(diff));
} else {
setTimeDiff('timeEnd');
}
}, 100);

if (timeDiff === 'prepare') {
return <> </>;
}
if (timeDiff === 'timeEnd') {
return <>{props.endMessage}</>;
}

return (
<>
{props.prefix}
{timeDiff.days}일 {padZero(timeDiff.hours)}:{padZero(timeDiff.minutes)}:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timeDiff객체가 이 스트링을 다 뽑아주면 어떨까용!?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

convertMsIntoDate 가 뽑아준 timeDiff를 갖고 이 스트링을 뽑아주는 함수를 만들어 쓴다면 좋을 것 같아요!!

{padZero(timeDiff.seconds)}
{props.suffix}
</>
Comment on lines +26 to +39
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion;

이렇게 작성하면 timeDiff에 대한 return 값을 더 쉽게 파악할 수 있을 것 같아요!

  if (timeDiff === 'prepare') {
    return <> </>;
  }else if (timeDiff === 'timeEnd') {
    return <>{props.endMessage}</>;
  }else{
    return (
      <>
        {props.prefix}
        {timeDiff.days}{padZero(timeDiff.hours)}:{padZero(timeDiff.minutes)}:
        {padZero(timeDiff.seconds)}
        {props.suffix}
      </>
    )
  }

);
};

export default Timer;

const padZero = (num: number) => {
return `${num}`.padStart(2, '0');
};
1 change: 1 addition & 0 deletions src/components/common/Timer/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './Timer';
24 changes: 24 additions & 0 deletions src/hooks/useInterval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useEffect, useRef } from 'react';

type IntervalId = ReturnType<typeof setInterval>;

export default function useInterval(callback: () => void, delay: number) {
const intervalId = useRef<IntervalId | null>(null);

const clear = () => {
if (intervalId.current !== null) {
clearInterval(intervalId.current);
}
};

useEffect(() => {
intervalId.current = setInterval(callback, delay);
return () => {
clear();
};
}, [callback, delay]);

return {
clear,
};
}
7 changes: 7 additions & 0 deletions src/utils/convertMsIntoDate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const convertMsIntoDate = (milliseconds: number) => {
const days = Math.floor(milliseconds / (1000 * 60 * 60 * 24));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1000 은 1초, 1000 * 60은 1분, 1000 * 60 * 60은 1시간 .. 등등 다 의미가 있는 값들이어서, 상수로 정의해서 써보면 어떨까요!?

const SECOND = 1000;
const MINUTE = SECOND * 60;
const HOUR = MINUTE * 60;
const DAY = HOUR * 24;

요런 식으로요!!

const hours = Math.floor((milliseconds % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((milliseconds % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((milliseconds % (1000 * 60)) / 1000);
return { days, hours, minutes, seconds };
};
7 changes: 6 additions & 1 deletion src/views/MainPage/MainPage.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import { Footer, Header, Layout, ScrollToTopButton } from '@src/components';
import { useIsMobile } from '@src/hooks/useDevice';
import {
ActivityDescription,
ActivityReview,
BannerImage,
DetailedInformation,
PartDescription,
RecruitFloatingBanner,
SoptHistory,
} from '@src/views/MainPage/components';
import styles from './main-page.module.scss';

function MainPage() {
const isMobile = useIsMobile();

return (
<>
<Layout>
<Header />
<ScrollToTopButton />
{!isMobile && <ScrollToTopButton />}
<RecruitFloatingBanner />
<BannerImage />
<div className={styles.container}>
<div className={styles.content}>
Expand Down
9 changes: 9 additions & 0 deletions src/views/MainPage/assets/sopt-symbol.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import styled from '@emotion/styled';
import Link from 'next/link';
import { useRouter } from 'next/router';
import Timer from '@src/components/common/Timer';
import { useIsMobile } from '@src/hooks/useDevice';
import SoptSymbol from '@src/views/MainPage/assets/sopt-symbol.svg';
import dayjs from 'dayjs';

const TARGET_DATE = dayjs('2023-09-08T15:00:00.000Z').toDate();

export function RecruitFloatingBanner() {
const isMobile = useIsMobile();
const router = useRouter();

return (
<Wrapper>
<Banner isMobile={isMobile} onClick={() => router.push('/recruit')}>
<Countdown>
<Symbol src={SoptSymbol} alt="솝트 심볼" />
{isMobile ? (
'33기 YB 지원하기'
) : (
<Timer
targetDate={TARGET_DATE}
prefix="33기 YB 모집 마감까지 "
endMessage="33기 YB 모집이 마감되었습니다"
/>
)}
</Countdown>
<ApplyButton>
<Link href="/recruit">{isMobile ? '→' : '지원하기→'}</Link>
</ApplyButton>
</Banner>
</Wrapper>
);
}

const Wrapper = styled.div`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment;

3기부터는 시멘틱 태그를 최대한 사용하면 좋을 것 같아요!

position: fixed;
left: 50%;
bottom: 120px;
transform: translate(-50%, -50%);
z-index: 9999;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion;

z-index 단위( ex. 10단위로 증가 )도 정하면 좋을 것 같아요!


@media (max-width: 1299px) {
bottom: 80px;
}
`;

const Symbol = styled.img`
width: 52px;
height: 26px;

@media (max-width: 1299px) and (min-width: 766px) {
width: 36px;
height: 18px;
}

@media (max-width: 766px) {
width: 30px;
height: 15px;
}
`;

const Banner = styled.div<{ isMobile: boolean }>`
display: flex;
justify-content: space-between;
align-items: center;

width: 753px;
height: 88px;
padding: 31px 40px 31px 48px;

border-radius: 20px;
border: 1px solid rgba(114, 222, 173, 0.6);
background: rgba(33, 50, 42, 0.7);

box-shadow: 0px 4px 20px 0px rgba(32, 39, 38, 0.15);
backdrop-filter: blur(50px);

@media (max-width: 1299px) and (min-width: 766px) {
width: 585px;
padding: 31px 32px;
}

@media (max-width: 766px) {
width: 312px;
height: 56px;
padding: 20px 24px;

border-radius: 12px;
}

cursor: ${({ isMobile }) => (isMobile ? 'pointer' : 'auto')};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

모바일일 때에는 커서 속성이 있으나 없으나 비슷하게 보여서 굳이 나누지 않아도 될듯합니다!!

Copy link
Member

@SeojinSeojin SeojinSeojin Sep 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그리고 모바일인지 판단해서 처리하는 건 @media 로도 할 수 있어서 프롭으로 넘겨주지 않아도 될 것 같아요!!

useIsMobile 훅이,

  • 기기 너비
  • 사용자가 접속한 브라우저 환경

을 보고 모바일인지 데스크탑인지 판단하는 거라고 저도 처음에 생각했으나,
기기 너비만 보기 때문에 @media(max-width: 766px)이랑 동작이 똑같습니다!

`;

const Countdown = styled.div`
display: flex;
gap: 16px;

color: #fcfcfc;
font-size: 26px;
font-weight: 700;
line-height: 100%; /* 26px */
letter-spacing: -0.52px;

@media (max-width: 1299px) and (min-width: 766px) {
font-size: 18px;
letter-spacing: -0.36px;
}

@media (max-width: 766px) {
gap: 8px;

font-size: 16px;
letter-spacing: -0.32px;
}
`;

const ApplyButton = styled.div`
color: #1deda2;
font-size: 26px;
font-weight: 700;
line-height: 100%; /* 26px */
letter-spacing: -0.52px;

cursor: pointer;

@media (max-width: 1299px) and (min-width: 766px) {
font-size: 18px;
letter-spacing: -0.36px;
}

@media (max-width: 766px) {
font-size: 16px;
letter-spacing: -0.32px;
}
`;
1 change: 1 addition & 0 deletions src/views/MainPage/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './ActivityDescription/ActivityDescription';
export * from './DetailedInformation/DetailedInformation';
export * from './PartDescription/PartDescription';
export * from './ActivityReview/ActivityReview';
export * from './RecruitFloatingBanner/RecruitFloatingBanner';
Loading