From 355b4500bb01d4831337dc4fcc25dba766daeec2 Mon Sep 17 00:00:00 2001 From: SeojinSeojin <1106laura@naver.com> Date: Tue, 24 Oct 2023 17:45:01 +0900 Subject: [PATCH 01/13] feat: implement carousel component --- src/assets/icons/arrow_left_28x28.svg | 2 +- src/assets/icons/arrow_right_28x28.svg | 2 +- src/components/common/Carousel/index.tsx | 71 +++++++++++++++++++++++ src/components/common/Carousel/style.ts | 72 ++++++++++++++++++++++++ 4 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 src/components/common/Carousel/index.tsx create mode 100644 src/components/common/Carousel/style.ts diff --git a/src/assets/icons/arrow_left_28x28.svg b/src/assets/icons/arrow_left_28x28.svg index 6cd46c42..2d4b7027 100644 --- a/src/assets/icons/arrow_left_28x28.svg +++ b/src/assets/icons/arrow_left_28x28.svg @@ -1,3 +1,3 @@ - + diff --git a/src/assets/icons/arrow_right_28x28.svg b/src/assets/icons/arrow_right_28x28.svg index 68a60d2a..0c668aa3 100644 --- a/src/assets/icons/arrow_right_28x28.svg +++ b/src/assets/icons/arrow_right_28x28.svg @@ -1,3 +1,3 @@ - + diff --git a/src/components/common/Carousel/index.tsx b/src/components/common/Carousel/index.tsx new file mode 100644 index 00000000..c22d7044 --- /dev/null +++ b/src/components/common/Carousel/index.tsx @@ -0,0 +1,71 @@ +import React, { useRef, useState } from 'react'; +import { CarouselArrowType, CarouselOverflowType } from '@src/lib/types/universal'; +import { S } from './style'; + +interface CarouselProps { + itemWidth: number; + leftArrowType?: CarouselArrowType; + rightArrowType?: CarouselArrowType; + overflowType?: CarouselOverflowType; + children: JSX.Element[]; +} + +const Carousel: React.FC = ({ + itemWidth, + leftArrowType = CarouselArrowType.External, + rightArrowType = CarouselArrowType.External, + overflowType = CarouselOverflowType.Blur, + children, +}) => { + const [currentIndex, setCurrentIndex] = useState(0); + const [startX, setStartX] = useState(0); + const wrapperRef = useRef(null); + + const handlePrev = () => { + setCurrentIndex(Math.max(0, currentIndex - 1)); + }; + + const handleNext = () => { + setCurrentIndex(Math.min(children.length - 1, currentIndex + 1)); + }; + + const handleTouchStart = (e: React.TouchEvent) => { + setStartX(e.touches[0].clientX); + }; + + const handleTouchEnd = (e: React.TouchEvent) => { + const endX = e.changedTouches[0].clientX; + const deltaX = startX - endX; + + if (deltaX > 50) { + handleNext(); + } else if (deltaX < -50) { + handlePrev(); + } + }; + + const translateX = -currentIndex * itemWidth; + + return ( + + {overflowType === CarouselOverflowType.Blur && } + {currentIndex !== 0 && } + + + {children} + + + {currentIndex !== children.length - 1 && ( + + )} + + ); +}; + +export default Carousel; diff --git a/src/components/common/Carousel/style.ts b/src/components/common/Carousel/style.ts new file mode 100644 index 00000000..7dc3962f --- /dev/null +++ b/src/components/common/Carousel/style.ts @@ -0,0 +1,72 @@ +import styled from '@emotion/styled'; +import arrowLeft from '@src/assets/icons/arrow_left_28x28.svg'; +import arrowRight from '@src/assets/icons/arrow_right_28x28.svg'; +import { colors } from '@src/lib/styles/colors'; +import { HideScrollbar } from '@src/lib/styles/scrollbar'; +import { CarouselArrowType } from '@src/lib/types/universal'; + +const Wrapper = styled(HideScrollbar)` + width: 100%; + position: relative; +`; + +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%; + overflow: hidden; +`; + +const RightBlur = styled.div` + z-index: 2; + position: absolute; + height: 100%; + right: 0; + top: 0; + width: 160px; + background: linear-gradient(to right, transparent, ${colors.background}, ${colors.background}); + + /* 모바일 뷰 */ + @media (max-width: 765.9px) { + width: 40px; + background: linear-gradient(to right, transparent, ${colors.background}); + } +`; + +export const S = { Wrapper, LeftArrow, RightArrow, CarouselWrapper, CarouselViewport, RightBlur }; From f1ede9c38ea1467f4954254190e463621bb126e5 Mon Sep 17 00:00:00 2001 From: SeojinSeojin <1106laura@naver.com> Date: Tue, 24 Oct 2023 17:45:21 +0900 Subject: [PATCH 02/13] feat: implement carousel component in Project Page --- src/hooks/useDevice.ts | 20 ++++++ src/views/ProjectPage/ProjectPage.tsx | 7 +- .../RecentProjectList/Carousel/index.tsx | 25 +++++++ .../RecentProjectList/Item/index.tsx | 67 +++++++++++++++++++ .../RecentProjectList/Item/style.ts | 50 ++++++++++++++ .../components/RecentProjectList/index.tsx | 26 +++++++ src/views/ProjectPage/hooks/useFetch.ts | 8 ++- 7 files changed, 196 insertions(+), 7 deletions(-) create mode 100644 src/views/ProjectPage/components/RecentProjectList/Carousel/index.tsx create mode 100644 src/views/ProjectPage/components/RecentProjectList/Item/index.tsx create mode 100644 src/views/ProjectPage/components/RecentProjectList/Item/style.ts create mode 100644 src/views/ProjectPage/components/RecentProjectList/index.tsx diff --git a/src/hooks/useDevice.ts b/src/hooks/useDevice.ts index a809bf71..990a4cfa 100644 --- a/src/hooks/useDevice.ts +++ b/src/hooks/useDevice.ts @@ -33,3 +33,23 @@ export function useIsMobile(maxWidth = '765.9px') { }, [mobile]); return isMobile; } + +type DeviceType = 'desktop' | 'iOS' | 'Android'; + +export function useDeviceType() { + const [deviceType, setDeviceType] = useState('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; +} diff --git a/src/views/ProjectPage/ProjectPage.tsx b/src/views/ProjectPage/ProjectPage.tsx index 1a3b8bbb..ff34d22e 100644 --- a/src/views/ProjectPage/ProjectPage.tsx +++ b/src/views/ProjectPage/ProjectPage.tsx @@ -4,6 +4,7 @@ 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'; @@ -15,6 +16,7 @@ function Projects() { + SOPT에서 진행된 프로젝트 둘러보기 - + diff --git a/src/views/ProjectPage/components/RecentProjectList/Carousel/index.tsx b/src/views/ProjectPage/components/RecentProjectList/Carousel/index.tsx new file mode 100644 index 00000000..74f524d5 --- /dev/null +++ b/src/views/ProjectPage/components/RecentProjectList/Carousel/index.tsx @@ -0,0 +1,25 @@ +import Carousel from '@src/components/common/Carousel'; +import { useDeviceType, useIsTablet } from '@src/hooks/useDevice'; +import { CarouselArrowType, CarouselOverflowType } from '@src/lib/types/universal'; + +function RecentProjectListCarousel({ children }: { children: JSX.Element[] }) { + const isTabletSize = useIsTablet('767px'); + const deviceType = useDeviceType(); + + const arrowType = deviceType === 'desktop' ? CarouselArrowType.External : CarouselArrowType.None; + const overflowType = + deviceType === 'desktop' ? CarouselOverflowType.Blur : CarouselOverflowType.Visible; + + return ( + + {children} + + ); +} + +export default RecentProjectListCarousel; diff --git a/src/views/ProjectPage/components/RecentProjectList/Item/index.tsx b/src/views/ProjectPage/components/RecentProjectList/Item/index.tsx new file mode 100644 index 00000000..224500e7 --- /dev/null +++ b/src/views/ProjectPage/components/RecentProjectList/Item/index.tsx @@ -0,0 +1,67 @@ +import Image from 'next/image'; +import { useDeviceType } from '@src/hooks/useDevice'; +import { LinkType, ProjectLinkType, ProjectType } from '@src/lib/types/project'; +import { S } from './style'; + +type RecentProjectListItemProps = ProjectType; + +const linkToRecord = (links: ProjectLinkType[]): Record => { + const record: Record = { + [LinkType.Github]: undefined, + [LinkType.instagram]: undefined, + [LinkType.웹사이트]: undefined, + [LinkType.발표영상]: undefined, + [LinkType['구글 플레이스토어']]: undefined, + [LinkType['앱 스토어']]: undefined, + [LinkType['기타 관련자료']]: undefined, + }; + + for (const link of links) { + record[link.title] = link.url; + } + + return record; +}; + +const getTryLink = ( + link: ProjectLinkType[], + deviceType?: 'desktop' | 'iOS' | 'Android', +): string => { + const linkRecord = linkToRecord(link); + if (deviceType === 'iOS' && linkRecord['appStore']) { + return linkRecord['appStore']; + } + if (deviceType === 'Android' && linkRecord['googlePlay']) { + return linkRecord['googlePlay']; + } + return ( + linkRecord['website'] ?? + linkRecord['media'] ?? + linkRecord['github'] ?? + linkRecord['instagram'] ?? + '' + ); +}; + +function RecentProjectListItem(props: RecentProjectListItemProps) { + const deviceType = useDeviceType(); + const tryLink = getTryLink(props.link, deviceType); + + return ( + + + + + {props.name} + {props.summary} + + {props.generation}기 + 사용해보기 + + + + + ); +} + +export default RecentProjectListItem; diff --git a/src/views/ProjectPage/components/RecentProjectList/Item/style.ts b/src/views/ProjectPage/components/RecentProjectList/Item/style.ts new file mode 100644 index 00000000..08b5c1e4 --- /dev/null +++ b/src/views/ProjectPage/components/RecentProjectList/Item/style.ts @@ -0,0 +1,50 @@ +import styled from '@emotion/styled'; +import { colors } from '@src/lib/styles/colors'; + +const FlexWrapper = styled.div` + width: 568px; + display: flex; + background-color: ${colors.gray800}; + border-radius: 12px; + gap: 16px; + padding: 24px; + margin-left: 20px; + /* 모바일 뷰 */ + @media (max-width: 765.9px) { + width: 305px; + } +`; + +const MarginWrapper = styled.div` + padding-right: 20px; +`; + +const DetailWrapper = styled.div` + display: flex; + flex-direction: column; + flex: 1; +`; + +const DetailFooterWrapper = styled.div` + display: flex; + justify-content: space-between; + flex: 1; + align-items: flex-end; +`; + +const TextName = styled.div``; + +const TextSummary = styled.div``; +const Chip = styled.div``; +const TryLink = styled.a``; + +export const S = { + FlexWrapper, + MarginWrapper, + TextName, + TextSummary, + Chip, + TryLink, + DetailWrapper, + DetailFooterWrapper, +}; diff --git a/src/views/ProjectPage/components/RecentProjectList/index.tsx b/src/views/ProjectPage/components/RecentProjectList/index.tsx new file mode 100644 index 00000000..cce19a12 --- /dev/null +++ b/src/views/ProjectPage/components/RecentProjectList/index.tsx @@ -0,0 +1,26 @@ +import { ProjectCategoryType } from '@src/lib/types/project'; +import useFetch from '../../hooks/useFetch'; +import { SectionTitle } from '../../styles'; +import RecentProjectListCarousel from './Carousel'; +import RecentProjectListItem from './Item'; + +function RecentProjectList() { + const state = useFetch(ProjectCategoryType.ALL, 'updatedAt'); + + if (state._TAG !== 'OK') return null; + + if (state.data.length === 0) return null; + + return ( + <> + 최근 출시한 프로젝트 + + {state.data.slice(0, 5).map((d) => ( + + ))} + + + ); +} + +export default RecentProjectList; diff --git a/src/views/ProjectPage/hooks/useFetch.ts b/src/views/ProjectPage/hooks/useFetch.ts index 508d355b..3153cca4 100644 --- a/src/views/ProjectPage/hooks/useFetch.ts +++ b/src/views/ProjectPage/hooks/useFetch.ts @@ -1,13 +1,15 @@ import { useCallback } from 'react'; import useFetchBase from '@src/hooks/useFetchBase'; import { api } from '@src/lib/api'; -import { ProjectCategoryType } from '@src/lib/types/project'; +import { ProjectCategoryType, ProjectType } from '@src/lib/types/project'; +import { sortBy } from '@src/lib/utils/array'; -const useFetch = (selected: ProjectCategoryType) => { +const useFetch = (selected?: ProjectCategoryType, sortProp?: keyof ProjectType) => { const willFetch = useCallback(async () => { const response = await api.projectAPI.getProjectList(selected); + if (sortProp) return sortBy(response.projects, sortProp); return response.projects; - }, [selected]); + }, [selected, sortProp]); const state = useFetchBase(willFetch); return state; }; From 0d7dd6eedf77409e728876b3bbb22f52d936efb6 Mon Sep 17 00:00:00 2001 From: SeojinSeojin <1106laura@naver.com> Date: Tue, 24 Oct 2023 17:45:35 +0900 Subject: [PATCH 03/13] chore: misc styles and utils --- src/components/Header/header.module.scss | 2 +- src/lib/styles/global.ts | 3 ++- src/lib/styles/scrollbar.ts | 14 ++++++++++++++ src/lib/types/universal.ts | 12 ++++++++++++ src/lib/utils/array.ts | 13 +++++++++++++ 5 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 src/lib/styles/scrollbar.ts create mode 100644 src/lib/utils/array.ts diff --git a/src/components/Header/header.module.scss b/src/components/Header/header.module.scss index a4a41259..b40c1fca 100644 --- a/src/components/Header/header.module.scss +++ b/src/components/Header/header.module.scss @@ -8,7 +8,7 @@ justify-content: center; align-items: center; position: fixed; - background-color: rgba(22, 22, 28, 0.9); + background-color: #0F0F12; backdrop-filter: blur(20px); z-index: 100; diff --git a/src/lib/styles/global.ts b/src/lib/styles/global.ts index f38426db..b96697b8 100644 --- a/src/lib/styles/global.ts +++ b/src/lib/styles/global.ts @@ -1,4 +1,5 @@ import { css } from '@emotion/react'; +import { colors } from './colors'; import font from './font'; export const global = css` @@ -80,7 +81,7 @@ export const global = css` } body { - background-color: #16161c; + background-color: ${colors.background}; line-height: 1; } diff --git a/src/lib/styles/scrollbar.ts b/src/lib/styles/scrollbar.ts new file mode 100644 index 00000000..6ba90be5 --- /dev/null +++ b/src/lib/styles/scrollbar.ts @@ -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; +`; diff --git a/src/lib/types/universal.ts b/src/lib/types/universal.ts index abf8a68f..523a98d1 100644 --- a/src/lib/types/universal.ts +++ b/src/lib/types/universal.ts @@ -36,3 +36,15 @@ export type TabType = TabTypeOption; export type ExtraTabType = TabTypeOption; export type LabelKeyType = string | number | symbol; + +export enum CarouselArrowType { + External = 'external', + None = 'none', + // Internal = 'internal', + // Overlay = 'overlay', +} + +export enum CarouselOverflowType { + Blur = 'blur', + Visible = 'visible', +} diff --git a/src/lib/utils/array.ts b/src/lib/utils/array.ts new file mode 100644 index 00000000..749476f3 --- /dev/null +++ b/src/lib/utils/array.ts @@ -0,0 +1,13 @@ +export function sortBy(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; + }); +} From 47353506a9cedf5f7d7dc93d4255703ba6a6d88e Mon Sep 17 00:00:00 2001 From: SeojinSeojin <1106laura@naver.com> Date: Wed, 25 Oct 2023 09:41:16 +0900 Subject: [PATCH 04/13] fix: fix detail of recent project list style --- src/assets/icons/ic_arrow_stick_right.svg | 3 + .../RecentProjectList/Carousel/index.tsx | 2 +- .../RecentProjectList/Item/index.tsx | 29 ++-- .../RecentProjectList/Item/style.ts | 134 ++++++++++++++++-- .../components/RecentProjectList/index.tsx | 2 +- 5 files changed, 142 insertions(+), 28 deletions(-) create mode 100644 src/assets/icons/ic_arrow_stick_right.svg diff --git a/src/assets/icons/ic_arrow_stick_right.svg b/src/assets/icons/ic_arrow_stick_right.svg new file mode 100644 index 00000000..1152ae01 --- /dev/null +++ b/src/assets/icons/ic_arrow_stick_right.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/views/ProjectPage/components/RecentProjectList/Carousel/index.tsx b/src/views/ProjectPage/components/RecentProjectList/Carousel/index.tsx index 74f524d5..fb88595a 100644 --- a/src/views/ProjectPage/components/RecentProjectList/Carousel/index.tsx +++ b/src/views/ProjectPage/components/RecentProjectList/Carousel/index.tsx @@ -12,7 +12,7 @@ function RecentProjectListCarousel({ children }: { children: JSX.Element[] }) { return ( { +): string | undefined => { const linkRecord = linkToRecord(link); if (deviceType === 'iOS' && linkRecord['appStore']) { return linkRecord['appStore']; @@ -34,13 +33,7 @@ const getTryLink = ( if (deviceType === 'Android' && linkRecord['googlePlay']) { return linkRecord['googlePlay']; } - return ( - linkRecord['website'] ?? - linkRecord['media'] ?? - linkRecord['github'] ?? - linkRecord['instagram'] ?? - '' - ); + return linkRecord['website']; }; function RecentProjectListItem(props: RecentProjectListItemProps) { @@ -49,17 +42,21 @@ function RecentProjectListItem(props: RecentProjectListItemProps) { return ( - - + + {props.name} {props.summary} - - {props.generation}기 - 사용해보기 - - + + {props.generation}기 + {tryLink && ( + + 사용해보기 + + )} + + ); } diff --git a/src/views/ProjectPage/components/RecentProjectList/Item/style.ts b/src/views/ProjectPage/components/RecentProjectList/Item/style.ts index 08b5c1e4..5c375129 100644 --- a/src/views/ProjectPage/components/RecentProjectList/Item/style.ts +++ b/src/views/ProjectPage/components/RecentProjectList/Item/style.ts @@ -1,17 +1,25 @@ import styled from '@emotion/styled'; +import Image from 'next/image'; +import icArrowStickRight from '@src/assets/icons/ic_arrow_stick_right.svg'; import { colors } from '@src/lib/styles/colors'; +import { textSingularLineEllipsis } from '@src/lib/styles/textEllipsis'; -const FlexWrapper = styled.div` +const GridWrapper = styled.div` width: 568px; - display: flex; + display: grid; + grid-template-areas: 'img detail' 'img footer'; + grid-template-columns: 116px auto; + column-gap: 16px; background-color: ${colors.gray800}; border-radius: 12px; - gap: 16px; padding: 24px; - margin-left: 20px; + margin-right: 20px; /* 모바일 뷰 */ @media (max-width: 765.9px) { - width: 305px; + width: 325px; + grid-template-areas: 'img detail' 'footer footer'; + grid-template-columns: 48px auto; + column-gap: 10px; } `; @@ -20,27 +28,133 @@ const MarginWrapper = styled.div` `; const DetailWrapper = styled.div` + grid-area: detail; display: flex; flex-direction: column; flex: 1; `; +const ThumbnailImage = styled(Image)` + grid-area: img; + border-radius: 10px; + + /* 모바일 뷰 */ + @media (max-width: 765.9px) { + width: 48px; + height: 48px; + } +`; + const DetailFooterWrapper = styled.div` + grid-area: footer; display: flex; justify-content: space-between; flex: 1; align-items: flex-end; `; -const TextName = styled.div``; +const TextName = styled.div` + color: ${colors.gray30}; + font-size: 24px; + font-weight: 700; + line-height: 150%; + letter-spacing: -0.48px; + + /* 모바일 뷰 */ + @media (max-width: 765.9px) { + font-size: 16px; + letter-spacing: -0.24px; + } +`; + +const TextSummary = styled.div` + ${textSingularLineEllipsis} + color: ${colors.gray100}; + max-width: 408px; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 150%; /* 21px */ + letter-spacing: -0.21px; -const TextSummary = styled.div``; -const Chip = styled.div``; -const TryLink = styled.a``; + /* 모바일 뷰 */ + @media (max-width: 765.9px) { + font-size: 13px; + font-weight: 400; + letter-spacing: -0.195px; + max-width: 234px; + } +`; + +const Chip = styled.div` + padding: 5px 8px; + border-radius: 6px; + background-color: ${colors.gray700}; + color: ${colors.gray100}; + font-size: 12px; + font-weight: 500; + line-height: 135%; + letter-spacing: -0.18px; + + /* 모바일 뷰 */ + @media (max-width: 765.9px) { + height: 26px; + padding: 5px 8px; + font-size: 11px; + font-weight: 600; + letter-spacing: -0.165px; + } +`; + +const TryLink = styled.a` + cursor: pointer; + position: relative; + color: ${colors.gray30}; + font-size: 16px; + font-weight: 500; + line-height: 165%; + letter-spacing: -0.24px; + padding-top: 2px; + padding-bottom: 2px; + padding-right: 31px; + + &::after { + content: ''; + right: 6px; + width: 18px; + height: 18px; + position: absolute; + border-radius: 50%; + border: 1px solid ${colors.gray30}; + top: 50%; + transform: translateY(-50%); + background-image: url(${icArrowStickRight}); + } + + /* 모바일 뷰 */ + @media (max-width: 765.9px) { + font-size: 14px; + letter-spacing: -0.21px; + &::after { + content: ''; + right: 4px; + width: 14px; + height: 14px; + position: absolute; + border-radius: 50%; + border: 1px solid ${colors.gray30}; + top: 50%; + transform: translateY(-50%); + background-image: url(${icArrowStickRight}); + background-size: cover; + } + } +`; export const S = { - FlexWrapper, + GridWrapper, MarginWrapper, + ThumbnailImage, TextName, TextSummary, Chip, diff --git a/src/views/ProjectPage/components/RecentProjectList/index.tsx b/src/views/ProjectPage/components/RecentProjectList/index.tsx index cce19a12..49a2c134 100644 --- a/src/views/ProjectPage/components/RecentProjectList/index.tsx +++ b/src/views/ProjectPage/components/RecentProjectList/index.tsx @@ -15,7 +15,7 @@ function RecentProjectList() { <> 최근 출시한 프로젝트 - {state.data.slice(0, 5).map((d) => ( + {state.data.slice(0, 6).map((d) => ( ))} From 5ebe2d1d38b8f460d0e2527a599ec8a3ce3a4065 Mon Sep 17 00:00:00 2001 From: SeojinSeojin <1106laura@naver.com> Date: Wed, 25 Oct 2023 09:48:54 +0900 Subject: [PATCH 05/13] fix: change type of moreStyle property --- src/components/Layout/Layout.tsx | 13 +++++++++---- src/components/common/PageLayout/index.tsx | 4 ++-- src/views/ProjectPage/ProjectPage.tsx | 8 +++++++- src/views/RecruitPage/RecruitPage.tsx | 2 +- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx index 0243120d..5930be88 100644 --- a/src/components/Layout/Layout.tsx +++ b/src/components/Layout/Layout.tsx @@ -1,11 +1,16 @@ 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
{children}
; +export function Layout({ + children, + moreStyle, +}: PropsWithChildren<{ moreStyle?: SerializedStyles }>) { + return
{children}
; } -const Main = styled.div<{ moreStyle?: CSSProperties }>` +const Main = styled.div<{ moreStyle?: SerializedStyles }>` + ${({ moreStyle }) => moreStyle && moreStyle}; width: 100%; @media (max-width: 1279px) { diff --git a/src/components/common/PageLayout/index.tsx b/src/components/common/PageLayout/index.tsx index d7fd80c4..65d77105 100644 --- a/src/components/common/PageLayout/index.tsx +++ b/src/components/common/PageLayout/index.tsx @@ -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')); diff --git a/src/views/ProjectPage/ProjectPage.tsx b/src/views/ProjectPage/ProjectPage.tsx index ff34d22e..04c4c403 100644 --- a/src/views/ProjectPage/ProjectPage.tsx +++ b/src/views/ProjectPage/ProjectPage.tsx @@ -1,3 +1,4 @@ +import { css } from '@emotion/react'; import { useState } from 'react'; import PageLayout from '@src/components/common/PageLayout'; import Select from '@src/components/common/Select'; @@ -13,7 +14,12 @@ function Projects() { const state = useFetch(selectedCategory); return ( - + diff --git a/src/views/RecruitPage/RecruitPage.tsx b/src/views/RecruitPage/RecruitPage.tsx index a93d852e..9c6a393f 100644 --- a/src/views/RecruitPage/RecruitPage.tsx +++ b/src/views/RecruitPage/RecruitPage.tsx @@ -13,7 +13,7 @@ const BottomLogo = lazy(() => import('./components/BottomLogo')); function Recruit() { return ( - + From f1eb47949249e8ec2d00f9db2fd20b6b9986ce55 Mon Sep 17 00:00:00 2001 From: SeojinSeojin <1106laura@naver.com> Date: Wed, 25 Oct 2023 10:28:21 +0900 Subject: [PATCH 06/13] feat: implement page indicator dot in carousel --- src/components/common/Carousel/index.tsx | 31 ++++++++-- src/components/common/Carousel/style.ts | 62 ++++++++++++++++--- .../RecentProjectList/Carousel/index.tsx | 4 +- 3 files changed, 81 insertions(+), 16 deletions(-) diff --git a/src/components/common/Carousel/index.tsx b/src/components/common/Carousel/index.tsx index c22d7044..23201789 100644 --- a/src/components/common/Carousel/index.tsx +++ b/src/components/common/Carousel/index.tsx @@ -1,9 +1,10 @@ -import React, { useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { CarouselArrowType, CarouselOverflowType } from '@src/lib/types/universal'; import { S } from './style'; interface CarouselProps { itemWidth: number; + stride?: number; leftArrowType?: CarouselArrowType; rightArrowType?: CarouselArrowType; overflowType?: CarouselOverflowType; @@ -12,6 +13,7 @@ interface CarouselProps { const Carousel: React.FC = ({ itemWidth, + stride = 1, leftArrowType = CarouselArrowType.External, rightArrowType = CarouselArrowType.External, overflowType = CarouselOverflowType.Blur, @@ -21,12 +23,16 @@ const Carousel: React.FC = ({ const [startX, setStartX] = useState(0); const wrapperRef = useRef(null); + useEffect(() => { + setCurrentIndex(0); + }, [stride, itemWidth]); + const handlePrev = () => { - setCurrentIndex(Math.max(0, currentIndex - 1)); + setCurrentIndex(Math.max(0, currentIndex - stride)); }; const handleNext = () => { - setCurrentIndex(Math.min(children.length - 1, currentIndex + 1)); + setCurrentIndex(Math.min(children.length - 1, currentIndex + stride)); }; const handleTouchStart = (e: React.TouchEvent) => { @@ -45,10 +51,16 @@ const Carousel: React.FC = ({ }; const translateX = -currentIndex * itemWidth; + console.log(Array(children.length / stride)); return ( - {overflowType === CarouselOverflowType.Blur && } + {overflowType === CarouselOverflowType.Blur && ( + <> + + + + )} {currentIndex !== 0 && } = ({ {children} - {currentIndex !== children.length - 1 && ( + {currentIndex !== children.length - stride && ( )} + + {Array.from({ length: Math.ceil(children.length / stride) }).map((dot, index) => ( + setCurrentIndex(index * stride)} + selected={index === Math.floor(currentIndex / stride)} + /> + ))} + ); }; diff --git a/src/components/common/Carousel/style.ts b/src/components/common/Carousel/style.ts index 7dc3962f..f0486a42 100644 --- a/src/components/common/Carousel/style.ts +++ b/src/components/common/Carousel/style.ts @@ -50,23 +50,65 @@ const CarouselWrapper = styled.div<{ const CarouselViewport = styled.div` width: 100%; - overflow: hidden; `; -const RightBlur = styled.div` +const Blur = styled.div` z-index: 2; position: absolute; height: 100%; - right: 0; + width: calc(50vw - 50%); top: 0; - width: 160px; - background: linear-gradient(to right, transparent, ${colors.background}, ${colors.background}); + 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; - /* 모바일 뷰 */ - @media (max-width: 765.9px) { - width: 40px; - background: linear-gradient(to right, transparent, ${colors.background}); + ::before { + content: ''; + position: absolute; + top: -4px; + left: -4px; + right: -4px; + bottom: -4px; + border-radius: 50%; } `; -export const S = { Wrapper, LeftArrow, RightArrow, CarouselWrapper, CarouselViewport, RightBlur }; +export const S = { + Wrapper, + LeftArrow, + RightArrow, + CarouselWrapper, + CarouselViewport, + LeftBlur, + RightBlur, + DotWrapper, + Dot, +}; diff --git a/src/views/ProjectPage/components/RecentProjectList/Carousel/index.tsx b/src/views/ProjectPage/components/RecentProjectList/Carousel/index.tsx index fb88595a..ff9a4867 100644 --- a/src/views/ProjectPage/components/RecentProjectList/Carousel/index.tsx +++ b/src/views/ProjectPage/components/RecentProjectList/Carousel/index.tsx @@ -1,8 +1,9 @@ import Carousel from '@src/components/common/Carousel'; -import { useDeviceType, useIsTablet } from '@src/hooks/useDevice'; +import { useDeviceType, useIsDesktop, useIsTablet } from '@src/hooks/useDevice'; import { CarouselArrowType, CarouselOverflowType } from '@src/lib/types/universal'; function RecentProjectListCarousel({ children }: { children: JSX.Element[] }) { + const isDesktopSize = useIsDesktop('1239px'); const isTabletSize = useIsTablet('767px'); const deviceType = useDeviceType(); @@ -12,6 +13,7 @@ function RecentProjectListCarousel({ children }: { children: JSX.Element[] }) { return ( Date: Wed, 25 Oct 2023 10:31:30 +0900 Subject: [PATCH 07/13] chore: replace lib/colors to sopt-makers/colors --- src/components/common/Carousel/style.ts | 2 +- src/components/common/Select/style.ts | 2 +- .../ProjectPage/components/RecentProjectList/Item/style.ts | 2 +- src/views/ProjectPage/styles.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/common/Carousel/style.ts b/src/components/common/Carousel/style.ts index f0486a42..ee94c092 100644 --- a/src/components/common/Carousel/style.ts +++ b/src/components/common/Carousel/style.ts @@ -1,7 +1,7 @@ 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 { colors } from '@src/lib/styles/colors'; import { HideScrollbar } from '@src/lib/styles/scrollbar'; import { CarouselArrowType } from '@src/lib/types/universal'; diff --git a/src/components/common/Select/style.ts b/src/components/common/Select/style.ts index 16f58b4d..6a976fe1 100644 --- a/src/components/common/Select/style.ts +++ b/src/components/common/Select/style.ts @@ -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; diff --git a/src/views/ProjectPage/components/RecentProjectList/Item/style.ts b/src/views/ProjectPage/components/RecentProjectList/Item/style.ts index 5c375129..9bc7af21 100644 --- a/src/views/ProjectPage/components/RecentProjectList/Item/style.ts +++ b/src/views/ProjectPage/components/RecentProjectList/Item/style.ts @@ -1,7 +1,7 @@ import styled from '@emotion/styled'; +import { colors } from '@sopt-makers/colors'; import Image from 'next/image'; import icArrowStickRight from '@src/assets/icons/ic_arrow_stick_right.svg'; -import { colors } from '@src/lib/styles/colors'; import { textSingularLineEllipsis } from '@src/lib/styles/textEllipsis'; const GridWrapper = styled.div` diff --git a/src/views/ProjectPage/styles.ts b/src/views/ProjectPage/styles.ts index dc308a0c..2158ccbf 100644 --- a/src/views/ProjectPage/styles.ts +++ b/src/views/ProjectPage/styles.ts @@ -1,5 +1,5 @@ import styled from '@emotion/styled'; -import { colors } from '@src/lib/styles/colors'; +import { colors } from '@sopt-makers/colors'; export const SectionTitle = styled.div` color: ${colors.gray10}; From d828e3bc57514b7eb8a49251316abd9f1ad004f9 Mon Sep 17 00:00:00 2001 From: SeojinSeojin <1106laura@naver.com> Date: Wed, 25 Oct 2023 10:39:45 +0900 Subject: [PATCH 08/13] chore: remove console.log --- src/components/common/Carousel/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/common/Carousel/index.tsx b/src/components/common/Carousel/index.tsx index 23201789..7a11d0dc 100644 --- a/src/components/common/Carousel/index.tsx +++ b/src/components/common/Carousel/index.tsx @@ -51,7 +51,6 @@ const Carousel: React.FC = ({ }; const translateX = -currentIndex * itemWidth; - console.log(Array(children.length / stride)); return ( From cef6bc1db1bb3c2e0e828c07139f656295e67ec7 Mon Sep 17 00:00:00 2001 From: SeojinSeojin <1106laura@naver.com> Date: Thu, 26 Oct 2023 00:01:45 +0900 Subject: [PATCH 09/13] fix: alter moreStyle prop with css prop --- src/components/Layout/Layout.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx index 5930be88..dffde502 100644 --- a/src/components/Layout/Layout.tsx +++ b/src/components/Layout/Layout.tsx @@ -1,16 +1,15 @@ -import styled from '@emotion/styled'; import { SerializedStyles } from '@emotion/react'; +import styled from '@emotion/styled'; import { PropsWithChildren } from 'react'; export function Layout({ children, moreStyle, }: PropsWithChildren<{ moreStyle?: SerializedStyles }>) { - return
{children}
; + return
{children}
; } -const Main = styled.div<{ moreStyle?: SerializedStyles }>` - ${({ moreStyle }) => moreStyle && moreStyle}; +const Main = styled.div` width: 100%; @media (max-width: 1279px) { From 751a6b22fd4d29f605ffc67a061c60442beb93bd Mon Sep 17 00:00:00 2001 From: SeojinSeojin <1106laura@naver.com> Date: Thu, 26 Oct 2023 00:06:34 +0900 Subject: [PATCH 10/13] fix: apply convention of component default export --- src/components/Layout/Layout.tsx | 2 +- src/components/common/Carousel/index.tsx | 8 +++----- .../components/RecentProjectList/Carousel/index.tsx | 4 +--- .../components/RecentProjectList/Item/index.tsx | 4 +--- .../ProjectPage/components/RecentProjectList/index.tsx | 4 +--- 5 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx index dffde502..243087cb 100644 --- a/src/components/Layout/Layout.tsx +++ b/src/components/Layout/Layout.tsx @@ -1,5 +1,5 @@ -import { SerializedStyles } from '@emotion/react'; import styled from '@emotion/styled'; +import { SerializedStyles } from '@emotion/react'; import { PropsWithChildren } from 'react'; export function Layout({ diff --git a/src/components/common/Carousel/index.tsx b/src/components/common/Carousel/index.tsx index 7a11d0dc..78b83438 100644 --- a/src/components/common/Carousel/index.tsx +++ b/src/components/common/Carousel/index.tsx @@ -11,14 +11,14 @@ interface CarouselProps { children: JSX.Element[]; } -const Carousel: React.FC = ({ +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(null); @@ -86,6 +86,4 @@ const Carousel: React.FC = ({
); -}; - -export default Carousel; +} diff --git a/src/views/ProjectPage/components/RecentProjectList/Carousel/index.tsx b/src/views/ProjectPage/components/RecentProjectList/Carousel/index.tsx index ff9a4867..ff802469 100644 --- a/src/views/ProjectPage/components/RecentProjectList/Carousel/index.tsx +++ b/src/views/ProjectPage/components/RecentProjectList/Carousel/index.tsx @@ -2,7 +2,7 @@ import Carousel from '@src/components/common/Carousel'; import { useDeviceType, useIsDesktop, useIsTablet } from '@src/hooks/useDevice'; import { CarouselArrowType, CarouselOverflowType } from '@src/lib/types/universal'; -function RecentProjectListCarousel({ children }: { children: JSX.Element[] }) { +export default function RecentProjectListCarousel({ children }: { children: JSX.Element[] }) { const isDesktopSize = useIsDesktop('1239px'); const isTabletSize = useIsTablet('767px'); const deviceType = useDeviceType(); @@ -23,5 +23,3 @@ function RecentProjectListCarousel({ children }: { children: JSX.Element[] }) {
); } - -export default RecentProjectListCarousel; diff --git a/src/views/ProjectPage/components/RecentProjectList/Item/index.tsx b/src/views/ProjectPage/components/RecentProjectList/Item/index.tsx index e47a546d..75996cfe 100644 --- a/src/views/ProjectPage/components/RecentProjectList/Item/index.tsx +++ b/src/views/ProjectPage/components/RecentProjectList/Item/index.tsx @@ -36,7 +36,7 @@ const getTryLink = ( return linkRecord['website']; }; -function RecentProjectListItem(props: RecentProjectListItemProps) { +export default function RecentProjectListItem(props: RecentProjectListItemProps) { const deviceType = useDeviceType(); const tryLink = getTryLink(props.link, deviceType); @@ -60,5 +60,3 @@ function RecentProjectListItem(props: RecentProjectListItemProps) { ); } - -export default RecentProjectListItem; diff --git a/src/views/ProjectPage/components/RecentProjectList/index.tsx b/src/views/ProjectPage/components/RecentProjectList/index.tsx index 49a2c134..2bccf808 100644 --- a/src/views/ProjectPage/components/RecentProjectList/index.tsx +++ b/src/views/ProjectPage/components/RecentProjectList/index.tsx @@ -4,7 +4,7 @@ import { SectionTitle } from '../../styles'; import RecentProjectListCarousel from './Carousel'; import RecentProjectListItem from './Item'; -function RecentProjectList() { +export default function RecentProjectList() { const state = useFetch(ProjectCategoryType.ALL, 'updatedAt'); if (state._TAG !== 'OK') return null; @@ -22,5 +22,3 @@ function RecentProjectList() { ); } - -export default RecentProjectList; From a65f4cc2a354d927a497a4d68c6310564954e6fb Mon Sep 17 00:00:00 2001 From: SeojinSeojin <1106laura@naver.com> Date: Thu, 26 Oct 2023 00:08:59 +0900 Subject: [PATCH 11/13] refactor: make magic number into const --- src/components/common/Carousel/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/common/Carousel/index.tsx b/src/components/common/Carousel/index.tsx index 78b83438..73554e18 100644 --- a/src/components/common/Carousel/index.tsx +++ b/src/components/common/Carousel/index.tsx @@ -11,6 +11,8 @@ interface CarouselProps { children: JSX.Element[]; } +const SWIPE_THRESHOLD = 50; + export default function Carousel({ itemWidth, stride = 1, @@ -43,9 +45,9 @@ export default function Carousel({ const endX = e.changedTouches[0].clientX; const deltaX = startX - endX; - if (deltaX > 50) { + if (deltaX > SWIPE_THRESHOLD) { handleNext(); - } else if (deltaX < -50) { + } else if (deltaX < -SWIPE_THRESHOLD) { handlePrev(); } }; From 7df93c12d1a0a9ec3f7a7b02bc1a5e27aca5e668 Mon Sep 17 00:00:00 2001 From: SeojinSeojin <1106laura@naver.com> Date: Thu, 26 Oct 2023 00:21:01 +0900 Subject: [PATCH 12/13] fix: fix responsive related style values --- .../components/RecentProjectList/Carousel/index.tsx | 6 +++--- .../components/RecentProjectList/Item/style.ts | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/views/ProjectPage/components/RecentProjectList/Carousel/index.tsx b/src/views/ProjectPage/components/RecentProjectList/Carousel/index.tsx index ff802469..8b600074 100644 --- a/src/views/ProjectPage/components/RecentProjectList/Carousel/index.tsx +++ b/src/views/ProjectPage/components/RecentProjectList/Carousel/index.tsx @@ -1,10 +1,10 @@ import Carousel from '@src/components/common/Carousel'; -import { useDeviceType, useIsDesktop, useIsTablet } from '@src/hooks/useDevice'; +import { useDeviceType, useIsDesktop, useIsMobile } from '@src/hooks/useDevice'; import { CarouselArrowType, CarouselOverflowType } from '@src/lib/types/universal'; export default function RecentProjectListCarousel({ children }: { children: JSX.Element[] }) { const isDesktopSize = useIsDesktop('1239px'); - const isTabletSize = useIsTablet('767px'); + const isMobileSize = useIsMobile('767px'); const deviceType = useDeviceType(); const arrowType = deviceType === 'desktop' ? CarouselArrowType.External : CarouselArrowType.None; @@ -14,7 +14,7 @@ export default function RecentProjectListCarousel({ children }: { children: JSX. return ( Date: Thu, 26 Oct 2023 00:44:18 +0900 Subject: [PATCH 13/13] feat: add interaction to try link button --- .../RecentProjectList/Item/style.ts | 50 +++++++++++++++---- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/src/views/ProjectPage/components/RecentProjectList/Item/style.ts b/src/views/ProjectPage/components/RecentProjectList/Item/style.ts index fa2eae66..7728459d 100644 --- a/src/views/ProjectPage/components/RecentProjectList/Item/style.ts +++ b/src/views/ProjectPage/components/RecentProjectList/Item/style.ts @@ -1,6 +1,7 @@ import styled from '@emotion/styled'; import { colors } from '@sopt-makers/colors'; import Image from 'next/image'; +import { css } from '@emotion/react'; import icArrowStickRight from '@src/assets/icons/ic_arrow_stick_right.svg'; import { textSingularLineEllipsis } from '@src/lib/styles/textEllipsis'; @@ -106,6 +107,16 @@ const Chip = styled.div` } `; +const tryArrowBaseCss = css` + content: ''; + position: absolute; + border-radius: 50%; + border: 1px solid ${colors.gray30}; + top: 50%; + transform: translateY(-50%); + background-image: url(${icArrowStickRight}); +`; + const TryLink = styled.a` cursor: pointer; position: relative; @@ -114,30 +125,40 @@ const TryLink = styled.a` font-weight: 500; line-height: 165%; letter-spacing: -0.24px; + padding-left: 10px; padding-top: 2px; padding-bottom: 2px; - padding-right: 31px; + padding-right: 36px; + border-radius: 15px; + transition: 200ms; &::after { - content: ''; - right: 6px; + right: 11px; width: 18px; height: 18px; - position: absolute; - border-radius: 50%; - border: 1px solid ${colors.gray30}; - top: 50%; - transform: translateY(-50%); - background-image: url(${icArrowStickRight}); + ${tryArrowBaseCss} + } + + &:hover { + background-color: ${colors.gray700}; + + &::after { + right: 11px; + width: 18px; + height: 18px; + ${tryArrowBaseCss} + transform: translateY(-50%) rotate(-45deg); + } } /* 모바일 뷰 */ @media (max-width: 767px) { font-size: 14px; letter-spacing: -0.21px; + border-radius: 13px; &::after { content: ''; - right: 4px; + right: 9px; width: 14px; height: 14px; position: absolute; @@ -148,6 +169,15 @@ const TryLink = styled.a` background-image: url(${icArrowStickRight}); background-size: cover; } + &:hover { + &::after { + right: 9px; + width: 14px; + height: 14px; + ${tryArrowBaseCss} + transform: translateY(-50%) rotate(-45deg); + } + } } `;