-
Notifications
You must be signed in to change notification settings - Fork 7
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
Changes from all commits
355b450
f1ede9c
0d7dd6e
4735350
5ebe2d1
f1eb479
f418ea7
78c5e4a
d828e3b
cef6bc1
751a6b2
a65f4cc
7df93c1
39c07bd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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'; | ||
import { PropsWithChildren } from 'react'; | ||
|
||
export function Layout({ children, moreStyle }: PropsWithChildren<{ moreStyle?: CSSProperties }>) { | ||
return <Main style={moreStyle}>{children}</Main>; | ||
export function Layout({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion; 컨벤션에 맞게 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이것 제가 많이 어겼네요 .. 다 고치겠습니다!! There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
|
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; | |||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
|||||||||||||||||||||||
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(() => { | |||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | |||||||||||||||||||||||
); | |||||||||||||||||||||||
} |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 프로젝트 탭에서 스크롤바를 숨기는 이유가 뭔가요? 궁금합니다! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 요건 프로젝트 목록이 담겨 있는 캐러셀이라서, overflow-x가 생기게 됩니다! 그럼 가로 방향 스크롤바가 생기게 되는데요..! There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,3 +33,23 @@ export function useIsMobile(maxWidth = '765.9px') { | |
}, [mobile]); | ||
return isMobile; | ||
} | ||
|
||
type DeviceType = 'desktop' | 'iOS' | 'Android'; | ||
|
||
export function useDeviceType() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} |
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; | ||
`; |
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; | ||
}); | ||
} |
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'; | ||
|
||
|
@@ -12,9 +14,15 @@ function Projects() { | |
const state = useFetch(selectedCategory); | ||
|
||
return ( | ||
<PageLayout showScrollTopButton> | ||
<PageLayout | ||
showScrollTopButton | ||
moreStyle={css` | ||
overflow-x: hidden; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 캐러셀이 가로로 길어서 overflow-x가 생겨 일단 숨겼습니다!! |
||
`} | ||
> | ||
<Root> | ||
<ContentWrapper> | ||
<RecentProjectList /> | ||
<SectionTitle>SOPT에서 진행된 프로젝트 둘러보기</SectionTitle> | ||
<Select | ||
options={activeProjectCategoryList} | ||
|
@@ -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> | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
((지금 피쳐랑 큰 관련은 없는 부분입니다))
레이아웃에 moreStyle이라는 프로퍼티를 주었는데, CSSProperties로 들어가면 매번 객체로 들어가는 것이라 좋아 보이지 않아서 css`` 로 넣을 수 있도록 바꿨습니다!!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
객체가 매번 새롭게 생성되다 보니 성능상으로 좋지 않아 바꾸신 걸로 이해하면 될까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
emotion css props 대한 아티클인데 같이 읽어보면 좋을 것 같아요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
맞습니다!! 그런데 우영언니가 준 글을 읽어보니 이것 또한 그렇게 좋지는 않은 방법이네요 ..!!
음 css 프롭을 styled component로 넘기지 말고 바로 css로 넘기는 방식으로 일단은 개선하겠습니다!!