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

[SP2] Carousel 컴포넌트를 만들고 Project Page에서 최근 출시된 프로젝트 목록에 적용 #235

Merged
merged 14 commits into from
Oct 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/assets/icons/arrow_left_28x28.svg
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/assets/icons/arrow_right_28x28.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/icons/ic_arrow_stick_right.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 8 additions & 4 deletions src/components/Layout/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import styled from '@emotion/styled';
import { CSSProperties, PropsWithChildren } from 'react';
import { SerializedStyles } from '@emotion/react';
Copy link
Member Author

Choose a reason for hiding this comment

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

((지금 피쳐랑 큰 관련은 없는 부분입니다))

레이아웃에 moreStyle이라는 프로퍼티를 주었는데, CSSProperties로 들어가면 매번 객체로 들어가는 것이라 좋아 보이지 않아서 css`` 로 넣을 수 있도록 바꿨습니다!!

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
Contributor

Choose a reason for hiding this comment

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

emotion css props 대한 아티클인데 같이 읽어보면 좋을 것 같아요!

Copy link
Member Author

Choose a reason for hiding this comment

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

객체가 매번 새롭게 생성되다 보니 성능상으로 좋지 않아 바꾸신 걸로 이해하면 될까요?

맞습니다!! 그런데 우영언니가 준 글을 읽어보니 이것 또한 그렇게 좋지는 않은 방법이네요 ..!!

emotion css props 대한 아티클인데 같이 읽어보면 좋을 것 같아요!

음 css 프롭을 styled component로 넘기지 말고 바로 css로 넘기는 방식으로 일단은 개선하겠습니다!!

import { PropsWithChildren } from 'react';

export function Layout({ children, moreStyle }: PropsWithChildren<{ moreStyle?: CSSProperties }>) {
return <Main style={moreStyle}>{children}</Main>;
export function Layout({
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion;

컨벤션에 맞게 export default function~ 으로 적어주면 좋을 것 같아요!

Copy link
Member Author

Choose a reason for hiding this comment

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

이것 제가 많이 어겼네요 .. 다 고치겠습니다!!

Copy link
Member Author

Choose a reason for hiding this comment

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

이건 이번에 추가된게 아니라 저번에 추가된 것이고, 한 번 바꾸면 너무나 많은 파일을 건드리게 되네요 ..
다른 이슈에서 해야 할 것 같습니다!!

children,
moreStyle,
}: PropsWithChildren<{ moreStyle?: SerializedStyles }>) {
return <Main css={moreStyle}>{children}</Main>;
}

const Main = styled.div<{ moreStyle?: CSSProperties }>`
const Main = styled.div`
width: 100%;

@media (max-width: 1279px) {
Expand Down
91 changes: 91 additions & 0 deletions src/components/common/Carousel/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React, { useEffect, useRef, useState } from 'react';
import { CarouselArrowType, CarouselOverflowType } from '@src/lib/types/universal';
import { S } from './style';

interface CarouselProps {
itemWidth: number;
Copy link
Member Author

Choose a reason for hiding this comment

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

프롭 이름 하는 일 비고
itemWidth 아이템 너비
stride 보폭 - 한 번에 넘어가는 페이지 수
leftArrowType 왼쪽 Arrow의 타입 현재는 보여준다 / 안 보여준다 밖에 없지만 나중에 확장 가능!
rightArrowType 오른쪽 Arrow의 타입 위와 같다
overflowType 넘치는 부분을 어떻게 보여줄 것인가? 현재는 blur / show 둘 뿐
children React Children

stride?: number;
leftArrowType?: CarouselArrowType;
rightArrowType?: CarouselArrowType;
overflowType?: CarouselOverflowType;
children: JSX.Element[];
}

const SWIPE_THRESHOLD = 50;

export default function Carousel({
itemWidth,
stride = 1,
leftArrowType = CarouselArrowType.External,
rightArrowType = CarouselArrowType.External,
overflowType = CarouselOverflowType.Blur,
children,
}: CarouselProps) {
const [currentIndex, setCurrentIndex] = useState(0);
const [startX, setStartX] = useState(0);
const wrapperRef = useRef<HTMLDivElement>(null);

useEffect(() => {
Copy link
Member Author

Choose a reason for hiding this comment

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

stride가 달라지면 전체 페이지 수가 달라지므로, 현재 페이지를 0으로 초기화합니다

setCurrentIndex(0);
}, [stride, itemWidth]);

const handlePrev = () => {
setCurrentIndex(Math.max(0, currentIndex - stride));
};

const handleNext = () => {
setCurrentIndex(Math.min(children.length - 1, currentIndex + stride));
};

const handleTouchStart = (e: React.TouchEvent<HTMLDivElement>) => {
setStartX(e.touches[0].clientX);
};

const handleTouchEnd = (e: React.TouchEvent<HTMLDivElement>) => {
const endX = e.changedTouches[0].clientX;
const deltaX = startX - endX;

if (deltaX > SWIPE_THRESHOLD) {
handleNext();
} else if (deltaX < -SWIPE_THRESHOLD) {
handlePrev();
}
};

const translateX = -currentIndex * itemWidth;

return (
<S.Wrapper ref={wrapperRef}>
{overflowType === CarouselOverflowType.Blur && (
<>
<S.LeftBlur />
<S.RightBlur />
</>
)}
{currentIndex !== 0 && <S.LeftArrow type={leftArrowType} onClick={handlePrev} />}
<S.CarouselViewport>
<S.CarouselWrapper
translateX={translateX}
itemWidth={itemWidth}
itemCount={children.length}
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
>
{children}
</S.CarouselWrapper>
</S.CarouselViewport>
{currentIndex !== children.length - stride && (
<S.RightArrow type={rightArrowType} onClick={handleNext} />
)}
<S.DotWrapper>
{Array.from({ length: Math.ceil(children.length / stride) }).map((dot, index) => (
<S.Dot
key={index}
onClick={() => setCurrentIndex(index * stride)}
selected={index === Math.floor(currentIndex / stride)}
/>
))}
</S.DotWrapper>
</S.Wrapper>
);
}
114 changes: 114 additions & 0 deletions src/components/common/Carousel/style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import styled from '@emotion/styled';
import { colors } from '@sopt-makers/colors';
import arrowLeft from '@src/assets/icons/arrow_left_28x28.svg';
import arrowRight from '@src/assets/icons/arrow_right_28x28.svg';
import { HideScrollbar } from '@src/lib/styles/scrollbar';
import { CarouselArrowType } from '@src/lib/types/universal';

const Wrapper = styled(HideScrollbar)`
width: 100%;
position: relative;
`;
Comment on lines +8 to +11
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 Author

Choose a reason for hiding this comment

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

요건 프로젝트 목록이 담겨 있는 캐러셀이라서, overflow-x가 생기게 됩니다! 그럼 가로 방향 스크롤바가 생기게 되는데요..!
디자인 의도는 그것이 아닐 것 같아서 스크롤바를 숨겼어요!!

Copy link
Member

@solar3070 solar3070 Oct 26, 2023

Choose a reason for hiding this comment

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

캐러셀의 스크롤 뿐만이 아니라 페이지의 스크롤까지 같이 없어지는 거 같은데 맞나요..?!
...가 아니군요.. 저희 원래 스크롤이 없는 것 같네요.....? 공홈 초기부터 이렇게 했던 거 같은데 의도된 디자인인가요?


const Arrow = styled.div<{ type: CarouselArrowType }>`
${({ type }) => type === CarouselArrowType.None && 'display: hidden;'}
position: absolute;
width: 40px;
height: 40px;
border-radius: 20px;
background-color: ${colors.gray600};
color: white;
z-index: 2;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
background-repeat: no-repeat;
background-position: center;
`;

const LeftArrow = styled(Arrow)<{ type: CarouselArrowType }>`
left: -50px;
background-image: url(${arrowLeft});
`;

const RightArrow = styled(Arrow)<{ type: CarouselArrowType }>`
right: -50px;
background-image: url(${arrowRight});
`;

const CarouselWrapper = styled.div<{
translateX: number;
itemWidth: number;
itemCount: number;
}>`
width: ${({ itemWidth, itemCount }) => itemWidth * itemCount}px;
display: grid;
grid-template-columns: ${({ itemWidth, itemCount }) => `repeat(${itemCount}, ${itemWidth}px)`};
transition: transform 0.5s ease-in-out;
transform: ${({ translateX }) => `translateX(${translateX}px)`};
`;

const CarouselViewport = styled.div`
width: 100%;
`;

const Blur = styled.div`
z-index: 2;
position: absolute;
height: 100%;
width: calc(50vw - 50%);
top: 0;
background: linear-gradient(
to right,
transparent 10px,
${colors.background} 50px,
${colors.background}
);
`;

const LeftBlur = styled(Blur)`
left: calc(50% - 50vw);
transform: rotate(180deg);
`;

const RightBlur = styled(Blur)`
right: calc(50% - 50vw);
`;

const DotWrapper = styled.div`
margin-top: 24px;
display: flex;
justify-content: center;
gap: 12px;
`;

const Dot = styled.div<{ selected: boolean }>`
position: relative;
width: 8px;
height: 8px;
background-color: ${({ selected }) => (selected ? colors.white : colors.gray800)};
border-radius: 50%;
cursor: pointer;

::before {
content: '';
position: absolute;
top: -4px;
left: -4px;
right: -4px;
bottom: -4px;
border-radius: 50%;
}
`;

export const S = {
Wrapper,
LeftArrow,
RightArrow,
CarouselWrapper,
CarouselViewport,
LeftBlur,
RightBlur,
DotWrapper,
Dot,
};
4 changes: 2 additions & 2 deletions src/components/common/PageLayout/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import dynamic from 'next/dynamic';
import { CSSProperties } from 'react';
import { SerializedStyles } from '@emotion/react';
import { Header, Layout } from '@src/components';

type PageLayoutOwnProps = {
showScrollTopButton?: boolean;
moreStyle?: CSSProperties;
moreStyle?: SerializedStyles;
};

const DynamicFooter = dynamic(() => import('@src/components/Footer'));
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/Select/style.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import styled from '@emotion/styled';
import { colors } from '@sopt-makers/colors';
import arrowDown from '@src/assets/icons/arrow_down.svg';
import { colors } from '@src/lib/styles/colors';

const SelectWrapper = styled.div`
position: relative;
Expand Down
20 changes: 20 additions & 0 deletions src/hooks/useDevice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,23 @@ export function useIsMobile(maxWidth = '765.9px') {
}, [mobile]);
return isMobile;
}

type DeviceType = 'desktop' | 'iOS' | 'Android';

export function useDeviceType() {
Copy link
Member Author

@SeojinSeojin SeojinSeojin Oct 25, 2023

Choose a reason for hiding this comment

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

접속한 장치 판단하는 훅입니다!

하나 만들어 두었어요~

const [deviceType, setDeviceType] = useState<DeviceType | undefined>('desktop');

useEffect(() => {
const userAgent = navigator.userAgent || navigator.vendor;

if (/iPad|iPhone|iPod/.test(userAgent)) {
setDeviceType('iOS');
} else if (/android/i.test(userAgent)) {
setDeviceType('Android');
} else {
setDeviceType('desktop');
}
}, []);

return deviceType;
}
3 changes: 2 additions & 1 deletion src/lib/styles/global.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { colors } from '@sopt-makers/colors';
import { css } from '@emotion/react';
import font from './font';

Expand Down Expand Up @@ -80,7 +81,7 @@ export const global = css`
}

body {
background-color: #0f0f12;
background-color: ${colors.background};
line-height: 1;
}

Expand Down
14 changes: 14 additions & 0 deletions src/lib/styles/scrollbar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import styled from '@emotion/styled';

export const HideScrollbar = styled.div`
/* Chrome, Safari and Opera */
&::-webkit-scrollbar {
display: none;
}

/* Firefox */
scrollbar-width: none;

/* IE and Edge */
-ms-overflow-style: none;
`;
12 changes: 12 additions & 0 deletions src/lib/types/universal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,15 @@ export type TabType = TabTypeOption<Part>;
export type ExtraTabType = TabTypeOption<ExtraPart>;

export type LabelKeyType = string | number | symbol;

export enum CarouselArrowType {
External = 'external',
None = 'none',
// Internal = 'internal',
// Overlay = 'overlay',
}

export enum CarouselOverflowType {
Blur = 'blur',
Visible = 'visible',
}
13 changes: 13 additions & 0 deletions src/lib/utils/array.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export function sortBy<T>(array: T[], key: keyof T): T[] {
return [...array].sort((a, b) => {
const aValue = a[key];
const bValue = b[key];
if (typeof aValue === 'number' && typeof bValue === 'number') {
return aValue - bValue;
}
if (typeof aValue === 'string' && typeof bValue === 'string') {
return (aValue as string).localeCompare(bValue as string);
}
return 0;
});
}
15 changes: 10 additions & 5 deletions src/views/ProjectPage/ProjectPage.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { css } from '@emotion/react';
import { useState } from 'react';
import PageLayout from '@src/components/common/PageLayout';
import Select from '@src/components/common/Select';
import { activeProjectCategoryList, projectCategoryLabel } from '@src/lib/constants/project';
import { ProjectCategoryType } from '@src/lib/types/project';
import { ProjectList } from './components';
import RecentProjectList from './components/RecentProjectList';
import useFetch from './hooks/useFetch';
import { ContentWrapper, Root, SectionTitle } from './styles';

Expand All @@ -12,9 +14,15 @@ function Projects() {
const state = useFetch(selectedCategory);

return (
<PageLayout showScrollTopButton>
<PageLayout
showScrollTopButton
moreStyle={css`
overflow-x: hidden;
Copy link
Member Author

Choose a reason for hiding this comment

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

캐러셀이 가로로 길어서 overflow-x가 생겨 일단 숨겼습니다!!

`}
>
<Root>
<ContentWrapper>
<RecentProjectList />
<SectionTitle>SOPT에서 진행된 프로젝트 둘러보기</SectionTitle>
<Select
options={activeProjectCategoryList}
Expand All @@ -24,10 +32,7 @@ function Projects() {
setSelectedValue={setCategory}
baseValue={ProjectCategoryType.ALL}
/>
<ProjectList
state={state}
selectedCategory={selectedCategory ?? ProjectCategoryType.ALL}
/>
<ProjectList state={state} selectedCategory={selectedCategory} />
</ContentWrapper>
</Root>
</PageLayout>
Expand Down
Loading
Loading