Skip to content

Commit

Permalink
feat: 캐러셀을 적용한 랜딩 페이지 디자인 수정
Browse files Browse the repository at this point in the history
  • Loading branch information
hwinkr committed Oct 24, 2024
1 parent 16c907a commit 4c6ab32
Show file tree
Hide file tree
Showing 14 changed files with 270 additions and 20 deletions.
Binary file added frontend/src/assets/images/carousel-first.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/src/assets/images/carousel-fourth.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/src/assets/images/carousel-second.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/src/assets/images/carousel-third.png
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,47 @@
import { css } from '@emotion/react';

import theme from '@styles/theme';

export const carouselContainerStyles = css`
position: relative;
overflow: hidden;
width: 100%;
max-width: 600px;
margin: 0 auto;
`;

export const getSlideContainerStyles = (currentIndex: number, isTransitioning: boolean) => css`
transform: translateX(${-currentIndex * 100}%);
display: flex;
transition: ${isTransitioning ? 'transform 0.5s ease-in-out' : 'none'};
`;

export const carouselSlideStyles = css`
display: flex;
flex-direction: column;
flex-shrink: 0;
align-items: center;
width: 100%;
`;

export const indicatorContainerStyles = css`
display: flex;
gap: 8px;
justify-content: center;
margin-top: 16px;
`;

export const getIndicatorStyles = (active: boolean) => css`
cursor: pointer;
width: 8px;
height: 8px;
background-color: ${active ? theme.colors.primary : theme.colors.grey.primary};
border-radius: 50%;
transition: background-color 0.3s ease;
`;
76 changes: 76 additions & 0 deletions frontend/src/components/ComponentCarousel/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type { ReactNode } from 'react';
import { useCallback, useEffect, useState } from 'react';

import {
carouselContainerStyles,
carouselSlideStyles,
getIndicatorStyles,
getSlideContainerStyles,
indicatorContainerStyles,
} from './CompnentCarousel.styles';

interface CarouselProps {
slides: ReactNode[];
interval?: number;
}

export default function ComponentCarousel({ slides, interval = 2000 }: CarouselProps) {
const extendedSlides = [...slides.slice(-2), ...slides, ...slides.slice(0, 2)];

const [currentIndex, setCurrentIndex] = useState(2);
const [isTransitioning, setIsTransitioning] = useState(true);

const getRealIndex = () => {
if (currentIndex <= 1) return slides.length + currentIndex - 2;
if (currentIndex >= slides.length + 2) return currentIndex - slides.length - 2;
return currentIndex - 2;
};

const resetPosition = useCallback(() => {
if (currentIndex <= 1) {
setIsTransitioning(false);
setCurrentIndex(slides.length + currentIndex);
} else if (currentIndex >= slides.length + 2) {
setIsTransitioning(false);
setCurrentIndex(currentIndex - slides.length);
}
}, [currentIndex, slides.length]);

useEffect(() => {
const slider = document.querySelector('.carousel-slider');
const handleTransitionEnd = () => {
resetPosition();
};

slider?.addEventListener('transitionend', handleTransitionEnd);
return () => {
slider?.removeEventListener('transitionend', handleTransitionEnd);
};
}, [resetPosition]);

useEffect(() => {
const timer = setInterval(() => {
setIsTransitioning(true);
setCurrentIndex((prev) => prev + 1);
}, interval);

return () => clearInterval(timer);
}, [interval]);

return (
<div css={carouselContainerStyles}>
<div className="carousel-slider" css={getSlideContainerStyles(currentIndex, isTransitioning)}>
{extendedSlides.map((slide, index) => (
<div key={index} css={carouselSlideStyles}>
{slide}
</div>
))}
</div>
<div css={indicatorContainerStyles}>
{slides.map((_, index) => (
<div key={index} css={getIndicatorStyles(index === getRealIndex())} />
))}
</div>
</div>
);
}
24 changes: 24 additions & 0 deletions frontend/src/components/LandingCarousel/FirstCarousel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Text from '@components/_common/Text';

import firstCarouselImage from '@assets/images/carousel-first.png';

import { s_carouselImage, s_textContainer } from './LandingCarousels.styles';

export default function FirstCarousel() {
return (
<>
<img src={firstCarouselImage} alt="랜딩 첫 번째 이미지" css={s_carouselImage} />
<div css={s_textContainer}>
<Text typo="titleBold">약속 시간을 결정하기 힘드신가요?</Text>
<div>
<Text typo="captionMedium" textAlign="center">
약속 시간을 결정하느라 친구들의 답장을 하염없이 기다리거나,
</Text>
<Text typo="captionMedium" textAlign="center">
중요한 일들을 미루고 만날 시간만 고민한 적 있나요?
</Text>
</div>
</div>
</>
);
}
27 changes: 27 additions & 0 deletions frontend/src/components/LandingCarousel/FourthCarousel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Text from '@components/_common/Text';

import fourthCarouselImage from '@assets/images/carousel-fourth.png';

import { s_carouselImage, s_textContainer } from './LandingCarousels.styles';

export default function FourthCarousel() {
return (
<>
<img src={fourthCarouselImage} alt="랜딩 네 번째 이미지" css={s_carouselImage} />
<div css={s_textContainer}>
<Text typo="titleBold">
<Text.Accent text="쉽게 " />
만들고, 공유하고, 모이고!
</Text>
<div>
<Text typo="captionMedium" textAlign="center">
약속 시간 조율 스트레스는 이제 없어요!
</Text>
<Text typo="captionMedium" textAlign="center">
빠르게 모이고 친구들과 시간을 보내봐요
</Text>
</div>
</div>
</>
);
}
19 changes: 19 additions & 0 deletions frontend/src/components/LandingCarousel/LandingCarousels.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { css } from '@emotion/react';

export const s_carouselContainer = css`
display: flex;
flex-direction: column;
row-gap: 0.8rem;
justify-content: center;
`;

export const s_textContainer = css`
display: flex;
flex-direction: column;
row-gap: 1.2rem;
`;

export const s_carouselImage = css`
width: 16rem;
height: 16rem;
`;
23 changes: 23 additions & 0 deletions frontend/src/components/LandingCarousel/SecondCarousel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Text from '@components/_common/Text';

import secondCarouselImage from '@assets/images/carousel-second.png';

import { s_carouselImage, s_textContainer } from './LandingCarousels.styles';

export default function SecondCarousel() {
return (
<>
<img src={secondCarouselImage} alt="랜딩 두 번째 이미지" css={s_carouselImage} />
<div css={s_textContainer}>
<Text typo="titleBold" textAlign="center">
<Text.Accent text="모모" />가 도와드릴게요
</Text>
<div>
<Text typo="captionMedium">
이제는 손쉽게 약속 시간을 결정하고, 기다리는 시간을 줄여보세요!
</Text>
</div>
</div>
</>
);
}
25 changes: 25 additions & 0 deletions frontend/src/components/LandingCarousel/ThirdCarousel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Text from '@components/_common/Text';

import thirdCarouselImage from '@assets/images/carousel-third.png';

import { s_carouselImage, s_textContainer } from './LandingCarousels.styles';

export default function ThirdCarousel() {
return (
<>
<img src={thirdCarouselImage} alt="랜딩 첫 번째 이미지" css={s_carouselImage} />
<div css={s_textContainer}>
<Text typo="titleBold">
약속 일정을 <Text.Accent text="간편하게" /> 등록해요
</Text>
<div>
<Text typo="captionMedium" textAlign="center">
{
'시간을 등록할 때는 드래그로 원하는 시간대를 쉽게 선택해요.\n 날짜를 등록할 때는 달력을 클릭해 간편하게 선택할 수 있어요'
}
</Text>
</div>
</div>
</>
);
}
5 changes: 4 additions & 1 deletion frontend/src/components/_common/Text/Text.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,17 @@ const TEXT_TYPOGRAPHIES: Record<keyof typeof TYPOGRAPHY, SerializedStyles> = {
export const s_textStyles = ({
variant,
typo,
textAlign,
}: {
variant: TextVariant;
typo: keyof typeof TYPOGRAPHY;
textAlign: CSSProperties['textAlign'];
}) => {
return css`
${TEXT_TYPOGRAPHIES[typo]};
color: ${TEXT_COLOR_STYLES[variant]};
text-align: ${textAlign};
white-space: pre-line;
vertical-align: middle;
${TEXT_TYPOGRAPHIES[typo]}
`;
};
16 changes: 9 additions & 7 deletions frontend/src/components/_common/Text/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { PropsWithChildren } from 'react';
import type { CSSProperties, PropsWithChildren } from 'react';

import Accent from './Accent';
import { s_textStyles } from './Text.styles';
Expand All @@ -7,14 +7,16 @@ import type { TextTypo, TextVariant } from './Text.types';
interface TextProps extends PropsWithChildren {
variant?: TextVariant;
typo?: TextTypo;
textAlign?: CSSProperties['textAlign'];
}

export default function Text({ variant = 'default', typo = 'bodyMedium', children }: TextProps) {
return (
<p css={s_textStyles({ variant, typo })} role="text">
{children}
</p>
);
export default function Text({
variant = 'default',
typo = 'bodyMedium',
textAlign = 'left',
children,
}: TextProps) {
return <p css={s_textStyles({ variant, typo, textAlign })}>{children}</p>;
}

Text.Accent = Accent;
28 changes: 16 additions & 12 deletions frontend/src/pages/LandingPage/index.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,35 @@
import ContentLayout from '@layouts/ContentLayout';

import { Button } from '@components/_common/Buttons/Button';
import Text from '@components/_common/Text';
import ComponentCarousel from '@components/ComponentCarousel';
import FirstCarousel from '@components/LandingCarousel/FirstCarousel';
import FourthCarousel from '@components/LandingCarousel/FourthCarousel';
import SecondCarousel from '@components/LandingCarousel/SecondCarousel';
import ThirdCarousel from '@components/LandingCarousel/ThirdCarousel';
import BottomFixedButton from '@components/_common/Buttons/BottomFixedButton';

import useRouter from '@hooks/useRouter/useRouter';

import MomoCharacter from '@assets/images/momoCharacter.svg';

import { MEETING_CREATE_PATH } from '@constants/routes/meeting';

import { s_container } from './LandingPage.styles';

const slides = [
<FirstCarousel key="first" />,
<SecondCarousel key="second" />,
<ThirdCarousel key="third" />,
<FourthCarousel key="fourth" />,
];

export default function LandingPage() {
const { routeTo } = useRouter();

return (
<ContentLayout>
<div css={s_container}>
<h1>
<Text typo="titleBold">
모두 쉽게 모이자! <Text.Accent text="모모" /> 🍑
</Text>
</h1>
<MomoCharacter width="128" height="180" />
<Button onClick={() => routeTo(MEETING_CREATE_PATH)} variant="primary" size="full">
<ComponentCarousel slides={slides} />
<BottomFixedButton onClick={() => routeTo(MEETING_CREATE_PATH)}>
약속 생성하기
</Button>
</BottomFixedButton>
</div>
</ContentLayout>
);
Expand Down

0 comments on commit 4c6ab32

Please sign in to comment.