From ea521ee75c3762916edabfba8ff46890460b102a Mon Sep 17 00:00:00 2001 From: Dico <65105537+ha3158987@users.noreply.github.com> Date: Tue, 4 May 2021 22:04:25 +0900 Subject: [PATCH] =?UTF-8?q?[Team19][Json,=20Dico]=20Sidedish=202=EC=A3=BC?= =?UTF-8?q?=EC=B0=A8=20PR=20(Final)=20(#73)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#1] init: :tada: 개발 환경 구축 * [#3] feat: :sparkles: Header 만들기 * [#5] feat: :sparkles: BestTab UI 구현 - BestTab UI - 상수 파일 - const.js - 재사용 컴포넌트 - Label.jsx - ItemCard.jsx * [#7] feat: :sparkles: Slide UI 구현 - App.jsx - SlideContainer import - BestItem.jsx - 삭제 (미사용) - BestItems.jsx - ItemCard prop 추가 - BestTab.jsx - 스타일 수정 - SlideContainer.jsx - UI 구현 - SlideItems.jsx - UI 구현 - SlideArrowBtn.jsx - UI 구현 - ItemCard.jsx - 스타일 수정 - prop 추가 - Label.jsx - prop 추가 - 기본 값 추가 * [#8] feat: :sparkles: ShowMoreBtn UI 구현 - App.js - ShowMoreBtn import - ShowMoreBtn.jsx - UI 구현 * [#10] feat: :sparkles: Header Dropdown 구현 - App.js - Header 경로 수정 - Header.jsx - 경로 변경 - HeaderLeft & Right 분리 - HeaderLeft.jsx - 컴포넌트화 - Navigations 컴포넌트화 - HeaderRight.jsx - 컴포넌트화 - Navigations.jsx - 컴포넌트화 - Dropdown 구현 * [#13] feat: :sparkles: API에 fetch 요청 로직 구현 * [#13] feat: :sparkles: API 요청, 베스트 기능구현 - BestItems.jsx - API 데이터 동기화 - BestTab.jsx - useState, useEffect, API 요청 - BestTabContainer.jsx - API 데이터 동기화 - BestTabNavigator.jsx - API 데이터 동기화 - ItemCard.jsx - prop 변경 - Label.jsx - COLOR변수 추가 * [#15] feat: :sparkles: 상세 modal 페이지 UI 구현 - App.js - PopUpContainer import - PopUpContainer.jsx - UI 구현 - PopUpImages.jsx - UI 구현 - PopUpInformations.jsx - UI 구현 * [#16] feat: :sparkles: 모달 페이지 이벤트 구현중 * [#16] feat: :sparkles: 수량정보 컴포넌트 분리 * [#19] refactor: :hammer: 리팩토링, 부족한 부분 추가 구현 - 파일 및 폴더 구조 변경 - common 폴더 생성 - Context.jsx - useContext 사용하여 prop drilling 개선 - 팝업 이벤트 구현 - 수량 변경 - 주문하기 - 주문결과 안내 메시지 UI * [#19] refactor: :hammer: 리팩토링 * [#17] feat: :sparkles: dj-slider 폴더구조 구축 * [#17] feat: :sparkles: 슬라이드 1/2 구현 중 * [#23] refactor: :hammer: 코드 리뷰 코멘트 반영 및 개선 * [#17] feat: :sparkles: 슬라이드 구현중/일부사항 수정 - util.js - price에 comma 붙이는 기능 구현 - PopUpItemCountContainer.jsx - price에 comma 붙이는 기능 import - ItemCard.jsx - price에 comma 붙이는 기능 import - 이미지 background로 수정 - Label.jsx - 라벨 배경색상 적용 * [#17] feat: :sparkles: 슬라이드 구현중 - 모듈화 - 시연을 위한 기능 구현을 위해 보류 * [#25] feat: :sparkles: 슬라이드 2/2 구현, API 데이터 동기화 - 슬라이드 명칭을 캐로셀로 변경 - API 데이터 동기화 - 캐로셀의 ItemCard를 children으로 변경 - 모듈화를 위함! - 아이템카드 mini, large 프로퍼티 추가 - 상세모달 캐로셀 추가 - 상세모달 스크롤 추가 - 모든 카테고리 보기 기능 구현 * [#27] feat: :sparkles: BestTab Skeleton UI 만들기 - Main.jsx - 시연을 위한 loop 설정 추가 - BestTab.jsx - SkeletonTab import - BestTabNavigator.jsx - 주석 제거 - SkeletonTab.jsx - Skeleton UI 구현 - DicoJsonCarousel.jsx - Carousel 구현중 * [#29] feat: ✨ PopUp Skeleton UI 만들기 - Context.jsx - 주석 제거 - BestTab.jsx - 주석 제거 - PopUpContainer.jsx - Skeleton import - PopUpItemsSlide.jsx - 주석 제거 - SkeletonPopUpContainerBody.jsx - Skeleton UI 구현 * feat: :sparkles: carousel loop 기능 구현 * [#31] feat: :sparkles: README.md 작성완료 Co-authored-by: kowoohyuk --- README.md | 37 +- src/App.js | 17 +- src/common/api.js | 28 ++ src/common/const.js | 15 + src/common/style.js | 27 + src/common/util.js | 1 + src/component/Context.jsx | 82 +++ src/component/main/Main.jsx | 78 +++ src/component/main/ShowMoreBtn.jsx | 17 + src/component/main/bestTab/BestItems.jsx | 42 ++ src/component/main/bestTab/BestTab.jsx | 50 ++ .../main/bestTab/BestTabContainer.jsx | 12 + .../main/bestTab/BestTabNavigator.jsx | 36 ++ src/component/main/bestTab/SkeletonTab.jsx | 210 ++++++++ src/component/main/popUp/PopUpContainer.jsx | 105 ++++ src/component/main/popUp/PopUpImages.jsx | 62 +++ .../main/popUp/PopUpInformations.jsx | 120 +++++ .../main/popUp/PopUpItemCountContainer.jsx | 121 +++++ .../main/popUp/PopUpItemOrderResult.jsx | 63 +++ src/component/main/popUp/PopUpItemsSlide.jsx | 61 +++ .../main/popUp/SkeletonPopUpContainerBody.jsx | 470 ++++++++++++++++++ .../main/slideContainer/SlideContainer.jsx | 35 ++ src/component/util/ItemCard.jsx | 77 ++- src/component/util/Label.jsx | 6 +- .../util/dj-slider/DicoJsonCarousel.jsx | 313 ++++++++++++ 25 files changed, 2044 insertions(+), 41 deletions(-) create mode 100644 src/common/api.js create mode 100644 src/common/const.js create mode 100644 src/common/style.js create mode 100644 src/common/util.js create mode 100644 src/component/Context.jsx create mode 100644 src/component/main/Main.jsx create mode 100644 src/component/main/ShowMoreBtn.jsx create mode 100644 src/component/main/bestTab/BestItems.jsx create mode 100644 src/component/main/bestTab/BestTab.jsx create mode 100644 src/component/main/bestTab/BestTabContainer.jsx create mode 100644 src/component/main/bestTab/BestTabNavigator.jsx create mode 100644 src/component/main/bestTab/SkeletonTab.jsx create mode 100644 src/component/main/popUp/PopUpContainer.jsx create mode 100644 src/component/main/popUp/PopUpImages.jsx create mode 100644 src/component/main/popUp/PopUpInformations.jsx create mode 100644 src/component/main/popUp/PopUpItemCountContainer.jsx create mode 100644 src/component/main/popUp/PopUpItemOrderResult.jsx create mode 100644 src/component/main/popUp/PopUpItemsSlide.jsx create mode 100644 src/component/main/popUp/SkeletonPopUpContainerBody.jsx create mode 100644 src/component/main/slideContainer/SlideContainer.jsx create mode 100644 src/component/util/dj-slider/DicoJsonCarousel.jsx diff --git a/README.md b/README.md index 0a569832f..18d853095 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,36 @@ -# sidedish +# 🥑 sidedish ## Team19 (a.k.a DJ) -### 조원 -Json +### Members +[Json😇](https://github.com/kowoohyuk), [Dico🌷](https://github.com/ha3158987) + +---- +## 📝 Table of Contents +- [Links](#links) +- [Customized NPM Module](#module) +- [Skeleton UI](#Skeleton) +- [UI](#ui) +---- +## 🔗 [Links](#Links) +### [Git Strategy](https://github.com/ha3158987/sidedish/wiki/Git-Strategy-%F0%9F%8C%B3) +### [Naming Rule](https://github.com/ha3158987/sidedish/wiki/Naming-Rule-%F0%9F%8D%92) +### [Reference](https://github.com/ha3158987/sidedish/wiki/Reference-%F0%9F%93%94) +### Blueprint +- [Component](https://www.notion.so/Component-f3ea0a6769d14acf97b0f2bc13b63ca9) +- [Carousel](https://www.notion.so/DicoJsonCarousel-4a8ccb91cb9e4edb9b4e3bfffae1f7ca) +- [Notion](https://www.notion.so/Sidedish-Json-Dico-cb7c0605253840da922952c6e910ea60) + +---- +## 🎠 [Customized NPM Module](#module) +[dico-json-carousel NPM](https://npmjs.com/package/dico-json-carousel) +![Carousel](https://s3.us-west-2.amazonaws.com/secure.notion-static.com/1f61e9d5-f75b-428c-af74-52357bf1522f/Untitled.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAT73L2G45O3KS52Y5%2F20210502%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20210502T134930Z&X-Amz-Expires=86400&X-Amz-Signature=e6c4ff35984ebe0977a127959b9494172e7c61802dfbac4029af6c88ab330ee3&X-Amz-SignedHeaders=host&response-content-disposition=filename%20%3D%22Untitled.png%22) + +----- +## ☠️ [Skeleton UI](#Skeleton) +![Skeleton UI](https://s3.us-west-2.amazonaws.com/secure.notion-static.com/eadb8270-8871-4b16-b63e-f233ca888319/_2021_04_30_16_54_31_768.gif?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAT73L2G45O3KS52Y5%2F20210502%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20210502T134428Z&X-Amz-Expires=86400&X-Amz-Signature=6c68fa970a8b048efbf29f5166f9d224b9d355a0be969d6cad45081a68ed1ad7&X-Amz-SignedHeaders=host) + +---- +## 🏠 [UI](#ui) +![screencapture-localhost-3000-2021-05-02-22_42_10](https://user-images.githubusercontent.com/65105537/116815304-d52f7280-ab97-11eb-93c2-220afdd5c943.png) + -Dico diff --git a/src/App.js b/src/App.js index f779f9c92..43d07f380 100644 --- a/src/App.js +++ b/src/App.js @@ -1,20 +1,15 @@ -import BestTab from "./component/bestTab/BestTab.jsx"; import Header from "./component/header/Header.jsx"; -import PopUpContainer from "./component/popUp/PopUpContainer.jsx"; -import ShowMoreBtn from "./component/ShowMoreBtn.jsx"; -import SlideContainer from "./component/slideContainer/SlideContainer.jsx"; -import GlobalStyle from "./style.js"; +import Main from "./component/main/Main.jsx"; +import GlobalStyle from "./common/style.js"; +import { ContextProvider } from "./component/Context.jsx"; function App() { return ( - <> +
- - - - - +
+ ); } diff --git a/src/common/api.js b/src/common/api.js new file mode 100644 index 000000000..58f77d997 --- /dev/null +++ b/src/common/api.js @@ -0,0 +1,28 @@ +const API = () => { + const URL = "https://codesquad-2021-api.herokuapp.com/sidedish"; + const API = path => { + const req = { + method : 'GET', + headers: { + "Content-Type": "application/json", + } + }; + return fetch(URL + path, req); + } + return async (path) => { + let json; + try { + const result = await API(path); + json = await result.json(); + } catch(e) { + console.log(e); + } finally { + if(!json || json.length === 0) { + json = null; + } + } + return json; + } +} + +export default API(); diff --git a/src/common/const.js b/src/common/const.js new file mode 100644 index 000000000..4078ab76b --- /dev/null +++ b/src/common/const.js @@ -0,0 +1,15 @@ +const TEXT = { + "bestTab-title": "후기가 증명하는 베스트 반찬", + "order" : { + true : { + "title": "주문 성공", + "content": "감사합니다! \r\n맛있게 준비해드릴게요!" + }, + false : { + "title": "주문 실패", + "content": "재고가 부족합니다!" + } + } +}; + +export { TEXT }; diff --git a/src/common/style.js b/src/common/style.js new file mode 100644 index 000000000..5aa240192 --- /dev/null +++ b/src/common/style.js @@ -0,0 +1,27 @@ +import { createGlobalStyle } from "styled-components"; + +const GlobalStyle = createGlobalStyle` + body { + overflow-x: hidden; + color: #333; + } + button { + background-color: transparent; + border:none; + cursor: pointer; + } + input { + border: none; + } + input:focus { + outline:none; + } + button:focus { + outline:none; + } + * { + box-sizing: border-box; + } +`; + +export default GlobalStyle; diff --git a/src/common/util.js b/src/common/util.js new file mode 100644 index 000000000..8e5f59536 --- /dev/null +++ b/src/common/util.js @@ -0,0 +1 @@ +export const addCommaToNumber = (number) => number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); diff --git a/src/component/Context.jsx b/src/component/Context.jsx new file mode 100644 index 000000000..6791faff9 --- /dev/null +++ b/src/component/Context.jsx @@ -0,0 +1,82 @@ +import { createContext, useContext, useEffect, useState } from "react"; +import API from "../common/api.js"; + +const DetailDataContext = createContext(); +const PopUpToggleContext = createContext(); +const OnFetchDetailDataContext = createContext(); +const SetPopUpToggleContext = createContext(); +const MainItemsContext = createContext(); +const MainItemsActiveContext = createContext(); +const SetMainItemsActiveContext = createContext(); + +export function ContextProvider({ children }) { + const [detailData, setDetailData] = useState(null); + const [popUpToggle, setPopUpToggle] = useState(false); + const [mainItems, setMainItems] = useState(null); + const [mainItemsActive, setMainItemsActive] = useState(false); + + useEffect(() => { + if (!mainItems) { + (async () => { + const data = await API("/main"); + setMainItems(data); + })(); + } + return; + }, [mainItems]); + + const onFetchDetailData = async (id) => { + setPopUpToggle(true); + setDetailData(null); + const data = await API(`/detail/${id}`); + if (data) { + setTimeout(() => setDetailData(data), 1000); + } + }; + + return ( + + + + + + + + {children} + + + + + + + + ); +} + +export function useDetailContext() { + return useContext(DetailDataContext); +} + +export function useOnFetchDetailDataContext() { + return useContext(OnFetchDetailDataContext); +} + +export function usePopUpToggleContext() { + return useContext(PopUpToggleContext); +} + +export function useSetPopUpToggleContext() { + return useContext(SetPopUpToggleContext); +} + +export function useMainItemsContext() { + return useContext(MainItemsContext); +} + +export function useMainItemsActiveContext() { + return useContext(MainItemsActiveContext); +} + +export function useSetMainItemsActiveContext() { + return useContext(SetMainItemsActiveContext); +} diff --git a/src/component/main/Main.jsx b/src/component/main/Main.jsx new file mode 100644 index 000000000..384ae5e1d --- /dev/null +++ b/src/component/main/Main.jsx @@ -0,0 +1,78 @@ +import { useEffect, useState } from "react"; +import BestTab from "./bestTab/BestTab.jsx"; +import PopUpContainer from "./popUp/PopUpContainer.jsx"; +import ShowMoreBtn from "./ShowMoreBtn.jsx"; +import API from "../../common/api.js"; +import { DicoJsonCarousel } from "../util/dj-slider/DicoJsonCarousel.jsx"; +import ItemCard from "../util/ItemCard"; +import styled from "styled-components"; + +import { + useMainItemsContext, + useMainItemsActiveContext, + useSetMainItemsActiveContext, +} from "../Context"; + +const CarouselContainer = styled.div` + margin-bottom: 5rem; +`; + +export default function Main() { + const mainItems = useMainItemsContext(); + const active = useMainItemsActiveContext(); + const setActive = useSetMainItemsActiveContext(); + + const onShowMoreItems = () => { + setActive(true); + }; + const getSalePrice = (price, discountRate) => { + return price - price * (discountRate / 100); + }; + if (!mainItems) return null; + return ( + <> + + {active ? ( + mainItems.map((mainItem, id) => ( + +

{mainItem.title}

+ + {mainItem.childs.map((item, idx) => ( + + ))} + +
+ )) + ) : ( + +

{mainItems[0].title}

+ + {mainItems[0].childs.map((item, idx) => ( + + ))} + +
+ )} + + + + ); +} diff --git a/src/component/main/ShowMoreBtn.jsx b/src/component/main/ShowMoreBtn.jsx new file mode 100644 index 000000000..6e4a7c8ef --- /dev/null +++ b/src/component/main/ShowMoreBtn.jsx @@ -0,0 +1,17 @@ +import styled from "styled-components"; + +const ShowMoreBtnStyle = styled.div` + margin: 0 -4rem; + font-size: 1.125rem; + font-weight: 600; + padding: 2.321225rem; + text-align: center; + background-color: #F5F5F7; + box-shadow: inset 0px 4px 4px rgba(0, 0, 0, 0.05); + cursor: pointer; + display: ${props => props.active ? 'none' : 'block'}; +`; + +export default function ShowMoreBtn({ active, onShowMoreItems }) { + return 모든 카테고리 보기; +} \ No newline at end of file diff --git a/src/component/main/bestTab/BestItems.jsx b/src/component/main/bestTab/BestItems.jsx new file mode 100644 index 000000000..801e31a52 --- /dev/null +++ b/src/component/main/bestTab/BestItems.jsx @@ -0,0 +1,42 @@ +import styled from "styled-components"; +import ItemCard from "../../util/ItemCard"; + +const BestItemsStyle = styled.div` + background-color: #eef4fa; + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-gap: 1.5rem; + padding: 2.5rem; +`; + +export default function BestItems({ childs }) { + const getRandom = (n, max) => { + const set = new Set(); + while (set.size < n) { + set.add(Math.floor(Math.random() * max)); + } + return Array.from(set); + }; + + const getSalePrice = (price, discountRate) => { + return price - price * (discountRate / 100); + }; + + return ( + + {getRandom(3, childs.length).map((idx) => ( + + ))} + + ); +} diff --git a/src/component/main/bestTab/BestTab.jsx b/src/component/main/bestTab/BestTab.jsx new file mode 100644 index 000000000..5166e8c93 --- /dev/null +++ b/src/component/main/bestTab/BestTab.jsx @@ -0,0 +1,50 @@ +import { useState, useEffect } from "react"; +import styled from "styled-components"; +import BestTabContainer from "./BestTabContainer"; +import BestTabNavigator from "./BestTabNavigator"; +import api from "../../../common/api.js"; +import SkeletonTab from "./SkeletonTab"; + +const BestTabStyle = styled.div` + h2 { + margin-bottom: 2rem; + } + margin-bottom: 5rem; +`; + +export default function BestTab() { + const [active, setActive] = useState(0); + const [bestItems, setBestItems] = useState(null); + + useEffect(() => { + if (!bestItems) { + try { + (async () => { + const data = await api("/best"); + setTimeout(() => setBestItems(data), 1000); + })(); + } catch (e) { + console.log(e); + } + } + return; + }, [bestItems]); + + return ( + <> + {bestItems ? ( + +

후기가 증명하는 베스트 반찬

+ + +
+ ) : ( + + )} + + ); +} diff --git a/src/component/main/bestTab/BestTabContainer.jsx b/src/component/main/bestTab/BestTabContainer.jsx new file mode 100644 index 000000000..d60b99315 --- /dev/null +++ b/src/component/main/bestTab/BestTabContainer.jsx @@ -0,0 +1,12 @@ +import styled from "styled-components"; +import BestItems from "./BestItems"; + +const BestTabContainerStyle = styled.div``; + +export default function BestTabContainer({ bestItem }) { + return ( + + + + ) +} \ No newline at end of file diff --git a/src/component/main/bestTab/BestTabNavigator.jsx b/src/component/main/bestTab/BestTabNavigator.jsx new file mode 100644 index 000000000..b061a66eb --- /dev/null +++ b/src/component/main/bestTab/BestTabNavigator.jsx @@ -0,0 +1,36 @@ +import styled from "styled-components"; + +const BestTabNavigatorStyle = styled.div` + display: grid; + grid-gap: 0.5rem; + grid-template-columns: repeat(5, 13rem); +`; + +const BestTabNavigatorItemStyle = styled.div` + font-size: 1.125rem; + background-color: ${(props) => (props.active ? "#EEF4FA" : "#F5F5F5")}; + padding: 1rem 2rem; + text-align: center; + font-weight: ${(props) => (props.active ? 600 : 400)}; + color: ${(props) => (props.active ? "#333" : "#828282")}; + cursor: pointer; + &:hover { + color: #333; + } +`; + +export default function BestTabNavigator({ bestItems, active, setActive }) { + return ( + + {bestItems.map((bestItem, i) => ( + setActive(i)} + active={i === active} + > + {bestItem.title} + + ))} + + ); +} diff --git a/src/component/main/bestTab/SkeletonTab.jsx b/src/component/main/bestTab/SkeletonTab.jsx new file mode 100644 index 000000000..faeba1a6e --- /dev/null +++ b/src/component/main/bestTab/SkeletonTab.jsx @@ -0,0 +1,210 @@ +import styled, { keyframes } from "styled-components"; + +const loading = keyframes` + 0% { + transform: translateX(-100px); + } + 50%, + 100% { + transform: translateX(500px); + } +`; + +const SkeletonTabStyle = styled.div` + h2 { + margin-bottom: 2rem; + background-color: #eef4fa; + display: inline-block; + width: 18rem; + margin-top: 0; + } + margin-bottom: 5rem; + .skeleton-ui::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(to right, #f2f2f2, #ddd, #f2f2f2); + animation: ${loading} 1s infinite linear; + } + .skeleton-ui { + overflow: hidden; + position: relative; + } +`; + +const SkeletonTabNavigatorStyle = styled.div` + display: grid; + grid-gap: 0.5rem; + grid-template-columns: repeat(5, 13rem); +`; + +const SkeletonTabNavigatorItemStyle = styled.div` + font-size: 1.125rem; + background-color: #f5f5f5; + padding: 1rem 2rem; + &:first-child { + background-color: #eef4fa; + } +`; +const SkeletonTabContainer = styled.div``; + +const SkeletonItemsStyle = styled.div` + background-color: #eef4fa; + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-gap: 1.5rem; + padding: 2.5rem; +`; + +const SkeletonItemCardStyle = styled.div` + & > div:last-child { + display: flex; + flex-flow: column; + } +`; +const SkeletonImgContainerStyle = styled.div` + position: relative; + margin-bottom: 1rem; + line-height: 0; + border-radius: 0.3rem; + overflow: hidden; +`; +const SkeletonImgStyle = styled.div` + width: 100%; + height: 25rem; + background-color: #eee; +`; +const SkeletonTitleStyle = styled.div` + display: inline-block; + width: 15rem; + margin-bottom: 0.5rem; + background-color: #eee; +`; +const SkeletonDescriptionStyle = styled.div` + display: inline-block; + width: 8rem; + margin-bottom: 1rem; + background-color: #eee; + font-size: 0.875rem; +`; +const SkeletonPricesStyle = styled.div` + margin-bottom: 1rem; +`; +const SkeletonSalePriceStyle = styled.div` + display: inline-block; + width: 5rem; + margin-right: 1rem; + font-size: 1.25rem; + background-color: #eee; +`; +const SkeletonNormalPriceStyle = styled.div` + display: inline-block; + width: 5rem; + background-color: #eee; +`; +const SkeletonLabel = styled.div` + display: inline-block; + width: 5.25rem; + background-color: #eee; + font-size: 0.875rem; + border-radius: 0.3rem; + padding: 0.25rem 1rem; +`; + +export default function SkeletonTab() { + return ( + +

 

+ + +   + + +   + + +   + + +   + + +   + + + + + + + + +
+ +   + + +   + + + +   + + +   + + +   +
+
+ + + + +
+ +   + + +   + + + +   + + +   + + +   +
+
+ + + + +
+ +   + + +   + + + +   + + +   + + +   +
+
+
+
+
+ ); +} diff --git a/src/component/main/popUp/PopUpContainer.jsx b/src/component/main/popUp/PopUpContainer.jsx new file mode 100644 index 000000000..180f4fefa --- /dev/null +++ b/src/component/main/popUp/PopUpContainer.jsx @@ -0,0 +1,105 @@ +import styled from "styled-components"; +import { FaTimes } from "react-icons/fa"; +import PopUpImages from "./PopUpImages"; +import PopUpInformations from "./PopUpInformations"; +import PopUpItemsSlide from "./PopUpItemsSlide"; +import SkeletonPopUpContainerBody from "./SkeletonPopUpContainerBody"; +import { + usePopUpToggleContext, + useSetPopUpToggleContext, + useDetailContext, +} from "../../Context"; + +const PopUpContainerStyle = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + z-index: 1; + &:before { + content: ""; + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background-color: rgba(0, 0, 0, 0.5); + z-index: -1; + } + & > div { + position: relative; + width: 60rem; + margin: 6vh auto; + } +`; + +const PopUpContainerBody = styled.div` + background-color: #fff; + width: 100%; + height: 88vh; + overflow-y: auto; + &::-webkit-scrollbar { + width: 0.875rem; + } + &::-webkit-scrollbar-thumb { + background-color: #cacaca; + border-radius: 0.375rem; + &:hover { + background-color: #b2b2b2; + } + } + &::-webkit-scrollbar-track { + background-color: #eee; + } +`; + +const PopUpDetailContainerStyle = styled.div` + padding: 3rem; + display: grid; + grid-template-columns: 24.5rem auto; + grid-gap: 2rem; + position: relative; +`; + +const CloseBtnStyle = styled.button` + color: #fff; + opacity: 0.8; + position: absolute; + right: -2.5rem; + font-size: 1.25rem; + cursor: pointer; + &:hover { + opacity: 1; + } +`; + +export default function PopUpContainer() { + const toggle = usePopUpToggleContext(); + const dispatch = useSetPopUpToggleContext(); + const detailData = useDetailContext(); + const onClick = () => { + dispatch(false); + }; + if (!toggle) return null; + return ( + +
+ + + + {detailData ? ( + + + + + + + + ) : ( + + )} +
+
+ ); +} diff --git a/src/component/main/popUp/PopUpImages.jsx b/src/component/main/popUp/PopUpImages.jsx new file mode 100644 index 000000000..3275a8a4f --- /dev/null +++ b/src/component/main/popUp/PopUpImages.jsx @@ -0,0 +1,62 @@ +import styled from "styled-components"; +import { useState, useEffect } from "react"; +import { useDetailContext } from "../../Context"; + +const PopUpImagesStyle = styled.div``; + +const PopUpMainImageStyle = styled.div` + width: 100%; + height: 24.5rem; + background-image: ${(props) => `url(${props.src})`}; + background-size: cover; + margin-bottom: 0.5rem; + border-radius: 0.3125rem; +`; + +const PopUpThumbnailImagesStyle = styled.div` + display: grid; + grid-template-columns: repeat(5, 1fr); + grid-gap: 0.5rem; +`; + +const PopUpThumbnailImageStyle = styled.img.attrs((props) => ({ + src: props.src, +}))` + width: 100%; + border-radius: 0.25rem; + cursor: pointer; + border: ${(props) => + props.active ? "2px solid #82D32D" : "2px solid transparent"}; + height: 4.5rem; +`; + +export default function PopUpImages() { + const detailData = useDetailContext(); + const { main_image, thumbnail_images } = detailData; + const images = [main_image, ...thumbnail_images]; + const [activeIndex, setActiveIndex] = useState(0); + + const onChangeMainImage = (idx) => { + setActiveIndex(idx); + }; + + useEffect(() => { + setActiveIndex(0); + }, [detailData]); + + return ( + + + + {images.map((URL, idx) => ( + onChangeMainImage(idx)} + /> + ))} + + + ); +} diff --git a/src/component/main/popUp/PopUpInformations.jsx b/src/component/main/popUp/PopUpInformations.jsx new file mode 100644 index 000000000..b5e61b277 --- /dev/null +++ b/src/component/main/popUp/PopUpInformations.jsx @@ -0,0 +1,120 @@ +import styled from "styled-components"; +import Label from "../../util/Label"; +import PopUpItemCountContainer from "./PopUpItemCountContainer"; +import { useDetailContext } from "../../Context"; +import { addCommaToNumber } from "../../../common/util.js"; + +const PopUpInformationsStyle = styled.div``; + +const PopUpItemTitle = styled.div` + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 1rem; +`; + +const PopUpItemDescription = styled.div` + font-size: 1.125rem; + color: #828282; + margin-bottom: 1rem; +`; + +const PopUpItemPriceContainer = styled.div` + margin-bottom: 1.5rem; + display: flex; + align-items: flex-end; +`; + +const SalePriceStyle = styled.div` + font-size: 1.5rem; + font-weight: 600; + margin-left: 0.5rem; + margin-right: 0.5rem; +`; + +const NormalPriceStyle = styled.div` + color: #828282; + text-decoration: line-through; +`; + +const PopUpItemBuyingInformations = styled.div` + border-top: 1px solid #e0e0e0; + border-bottom: 1px solid #e0e0e0; + padding: 0.5rem 0; +`; + +const PopUpItemBuyingInformation = styled.div` + display: grid; + grid-template-columns: 5rem auto; + margin-top: 1rem; + &:last-child { + margin-bottom: 1rem; + } +`; + +const PopUpItemBuyingInformationTitle = styled.div` + color: #828282; + line-height: 1.5rem; +`; + +const PopUpItemBuyingInformationContent = styled.div` + line-height: 1.5rem; +`; + +export default function PopUpInformations() { + const detailData = useDetailContext(); + const { + _id, + title, + description, + price, + discount, + stock, + point, + delivery_fee, + delivery_info, + } = detailData; + + return ( + + {title} + {description} + + + + + + 적립금 + + + {point}원 + + + + + 배송정보 + + + {delivery_info} + + + + + 배송비 + + + {delivery_fee} + + + + + + ); +} diff --git a/src/component/main/popUp/PopUpItemCountContainer.jsx b/src/component/main/popUp/PopUpItemCountContainer.jsx new file mode 100644 index 000000000..4bf8a4194 --- /dev/null +++ b/src/component/main/popUp/PopUpItemCountContainer.jsx @@ -0,0 +1,121 @@ +import styled from 'styled-components'; +import { FaAngleUp, FaAngleDown } from 'react-icons/fa'; +import { useState } from 'react'; +import PopUpItemOrderResult from "./PopUpItemOrderResult"; +import API from "../../../common/api.js"; +import { addCommaToNumber } from '../../../common/util.js'; + +const PopUpItemCountContainerStyle = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + padding: 1.5rem 0; + border-bottom: 1px solid #e0e0e0; + & > *:last-child { + display: grid; + grid-template-columns: 5rem 2rem; + line-height: 0; + } +`; + +const PopUpItemCountTitle = styled.div` +color: #828282; +width: 5rem; +`; + +const PopUpItemCount = styled.input.attrs({ +type: 'text' +})` +font-size: 1rem; +padding: 0rem 1rem; +border: 1px solid #E0E0E0; +text-align: center; +`; + +const PopUpItemCountBtns = styled.div` +border: 1px solid #E0E0E0; +border-left: 0px; +`; + +const PopUpItemCountBtn = styled.button` +width: 100%; +&:first-child { + border-bottom: 1px solid #E0E0E0; +} +`; + +const PopUpItemTotalPriceContainer = styled.div` + display: flex; + justify-content: flex-end; + padding: 2rem 0; +`; + +const PopUpItemTotalPriceTitle = styled.div` + margin-right: 1.5rem; + color: #828282; + font-size: 1.125rem; + align-self: center; +`; + +const PopUpTotalPrice = styled.div` + font-size: 2rem; + font-weight: 600; +`; + +const PopUpItemOrderBtn = styled.button` + background-color: #82d32d; + color: #fff; + font-size: 1.125rem; + font-weight: 600; + border-radius: 0.3125rem; + border: 1px solid #82d32d; + padding: 1rem; + text-align: center; + cursor: pointer; + width: 100%; + transition: all 0.2s ease-in-out; + &:hover { + background-color: #fff; + color: #82d32d; + } +`; + +export default function PopUpItemCountContainer ({ price, id }) { + const [ count, setCount ] = useState(1); + const [ orderResult, setOrderResult ] = useState(null); + + const onChangeCount = count => { + setOrderResult(null); + setCount(count); + }; + + const onMakeOrder = async () => { + const resultData = await API(`/buy/${id}/${count}`); + setOrderResult(resultData.result); + setTimeout(() => setOrderResult(null), 2500); + } + + return ( + <> + + 수량 +
+ + + onChangeCount(count + 1)}> + count > 0 ? onChangeCount(count - 1) : null }> + +
+
+ + 총 주문금액 + {addCommaToNumber(price * count)}원 + + 주문하기 + { orderResult !== null ? : null} + + ) +} + + + diff --git a/src/component/main/popUp/PopUpItemOrderResult.jsx b/src/component/main/popUp/PopUpItemOrderResult.jsx new file mode 100644 index 000000000..df3c9cb28 --- /dev/null +++ b/src/component/main/popUp/PopUpItemOrderResult.jsx @@ -0,0 +1,63 @@ +import styled from 'styled-components'; +import { TEXT } from '../../../common/const.js'; +import { FaCheck, FaTimes } from 'react-icons/fa'; + +const PopUpItemOrderResultStyle = styled.div` + position: fixed; + width: 100vw; + height: 100vh; + top: 0; + left: 0; + z-index: 1; + &::after { + content: ""; + position: fixed; + width: 100vw; + height: 100vh; + left: 0; + top: 0; + background-color: rgba(0, 0, 0, 0.3); + z-index: -1; + } +`; + +const OrderMessage = styled.div` + margin: 35vh auto; + width: 20rem; + padding: 2rem; + background-color: #fff; + h3 { + text-align: center; + font-size: 1.25rem; + font-weight: 600; + margin-bottom: 1rem; + * { + position: relative; + top: 0.125rem; + margin-right: 0.25rem; + } + .success { + color: #82D32D; + } + .fail { + color: #ff4545; + } + } + p { + color: #999; + white-space: break-spaces; + text-align: center; + } +`; + +export default function PopUpItemOrderResult({ result }) { + const { title, content } = TEXT.order[result]; + return ( + + +

{result ? : } {title}

+

{content}

+
+
+ ) +} \ No newline at end of file diff --git a/src/component/main/popUp/PopUpItemsSlide.jsx b/src/component/main/popUp/PopUpItemsSlide.jsx new file mode 100644 index 000000000..f3258ed66 --- /dev/null +++ b/src/component/main/popUp/PopUpItemsSlide.jsx @@ -0,0 +1,61 @@ +import styled from "styled-components"; +import { DicoJsonCarousel } from "../../util/dj-slider/DicoJsonCarousel"; +import { useMainItemsContext } from "../../Context"; +import ItemCard from "../../util/ItemCard"; + +const PopUpItemsSlideStyle = styled.div` + padding: 1rem 3rem; + background-color: #f2f2f2; + & > h2 { + font-size: 1.25rem; + } + .carousel-arrows { + width: 100%; + display: block; + text-align: right; + top: -3rem; + left: 1rem; + & > * { + display: inline-block; + font-size: 1rem; + } + } + .carousel-count { + position: absolute; + right: 1.375rem; + top: -2.1875rem; + font-size: 0.875rem; + } +`; + +export default function PopUpItemsSlide() { + const options = { + perPanel: 5, + count: true, + }; + const getSalePrice = (price, discountRate) => { + return price - price * (discountRate / 100); + }; + const items = useMainItemsContext(); + const childs = items[Math.floor(Math.random() * items.length)].childs; + return ( + +

함께하면 더욱 맛있는 상품

+ + {childs.map((item, idx) => ( + + ))} + +
+ ); +} diff --git a/src/component/main/popUp/SkeletonPopUpContainerBody.jsx b/src/component/main/popUp/SkeletonPopUpContainerBody.jsx new file mode 100644 index 000000000..6209d4727 --- /dev/null +++ b/src/component/main/popUp/SkeletonPopUpContainerBody.jsx @@ -0,0 +1,470 @@ +import styled, { keyframes } from "styled-components"; + +const loading = keyframes` + 0% { + transform: translateX(-100px); + } + 50%, + 100% { + transform: translateX(500px); + } +`; + +const SkeletonPopUpContainerBodyStyle = styled.div` + background-color: #fff; + width: 100%; + height: 88vh; + overflow-y: auto; + &::-webkit-scrollbar { + width: 0.875rem; + } + &::-webkit-scrollbar-thumb { + background-color: #cacaca; + border-radius: 0.375rem; + &:hover { + background-color: #b2b2b2; + } + } + &::-webkit-scrollbar-track { + background-color: #eee; + } + .skeleton-ui::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(to right, #f2f2f2, #ddd, #f2f2f2); + animation: ${loading} 1s infinite linear; + } + .skeleton-ui { + overflow: hidden; + position: relative; + } +`; + +const SkeletonPopUpDetailContainerStyle = styled.div` + padding: 3rem; + display: grid; + grid-template-columns: 24.5rem auto; + grid-gap: 2rem; + position: relative; +`; + +const SkeletonPopUpImagesStyle = styled.div``; +const SkeletonPopUpMainImageStyle = styled.div` + width: 100%; + height: 24.5rem; + background-color: #eee; + margin-bottom: 0.5rem; + border-radius: 0.3125rem; +`; +const SkeletonPopUpThumbnailImagesStyle = styled.div` + display: grid; + grid-template-columns: repeat(5, 1fr); + grid-gap: 0.5rem; +`; +const SkeletonPopUpThumbnailImageStyle = styled.div` + background-color: #eee; + width: 100%; + border-radius: 0.25rem; + cursor: pointer; + height: 4.5rem; +`; + +const SkeletonPopUpInformationsStyle = styled.div` + display: flex; + flex-flow: column; +`; + +const SkeletonPopUpItemTitle = styled.div` + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 1rem; + background-color: #eee; + display: inline-block; + width: 20rem; +`; + +const SkeletonPopUpItemDescription = styled.div` + font-size: 1.125rem; + color: #828282; + margin-bottom: 1rem; + background-color: #eee; + display: inline-block; + width: 15rem; +`; + +const SkeletonPopUpItemPriceContainer = styled.div` + margin-bottom: 1.5rem; + display: flex; + align-items: flex-end; +`; + +const SkeletonSalePriceStyle = styled.div` + font-size: 1.5rem; + font-weight: 600; + margin-left: 0.5rem; + margin-right: 0.5rem; + background-color: #eee; + display: inline-block; + width: 7rem; +`; + +const SkeletonNormalPriceStyle = styled.div` + color: #828282; + background-color: #eee; + display: inline-block; + width: 3rem; +`; + +const SkeletonPopUpItemBuyingInformations = styled.div` + border-top: 1px solid #e0e0e0; + border-bottom: 1px solid #e0e0e0; + padding: 0.5rem 0; +`; + +const SkeletonPopUpItemBuyingInformation = styled.div` + display: grid; + grid-template-columns: 5rem auto; + margin-top: 1rem; + &:last-child { + margin-bottom: 1rem; + } + &:nth-of-type(2) > * { + height: 1.5rem; + } + &:nth-of-type(2) > * + * { + height: 4.5rem; + } +`; + +const SkeletonPopUpItemBuyingInformationTitle = styled.div` + color: #828282; + line-height: 1.5rem; + background-color: #eee; + display: inline-block; + width: 4rem; +`; + +const SkeletonPopUpItemBuyingInformationContent = styled.div` + line-height: 1.5rem; + background-color: #eee; + display: inline-block; + width: 15rem; +`; + +const SkeletonLabel = styled.div` + display: inline-block; + width: 5.25rem; + background-color: #eee; + font-size: 0.875rem; + border-radius: 0.3rem; + padding: 0.25rem 1rem; +`; + +const SkeletonPopUpItemCountContainer = styled.div``; + +const SkeletonPopUpItemCountContainerStyle = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + padding: 1.5rem 0; + border-bottom: 1px solid #e0e0e0; + & > *:last-child { + display: grid; + grid-template-columns: 5rem 2rem; + line-height: 0; + } +`; + +const SkeletonPopUpItemCountTitle = styled.div` + color: #828282; + width: 3rem; + background-color: #eee; + display: inline-block; +`; + +const SkeletonPopUpItemCount = styled.input.attrs({ + type: "text", +})` + font-size: 1rem; + padding: 0rem 1rem; + border: 1px solid #e0e0e0; + text-align: center; +`; + +const SkeletonPopUpItemCountBtns = styled.div` + border: 1px solid #e0e0e0; + border-left: 0px; +`; + +const SkeletonPopUpItemCountBtn = styled.button` + width: 100%; + &:first-child { + border-bottom: 1px solid #e0e0e0; + } +`; + +const SkeletonPopUpItemTotalPriceContainer = styled.div` + display: flex; + justify-content: flex-end; + padding: 2rem 0; +`; + +const SkeletonPopUpItemTotalPriceTitle = styled.div` + margin-right: 1.5rem; + color: #828282; + font-size: 1.125rem; + align-self: center; + background-color: #eee; + display: inline-block; + width: 7rem; +`; + +const SkeletonPopUpTotalPrice = styled.div` + font-size: 2rem; + font-weight: 600; + background-color: #eee; + display: inline-block; + width: 8rem; +`; + +const SkeletonPopUpItemOrderBtn = styled.button` + background-color: #eee; + color: #fff; + font-size: 1.125rem; + font-weight: 600; + border-radius: 0.3125rem; + border: 1px solid #eee; + padding: 1rem; + text-align: center; + cursor: pointer; + width: 100%; +`; + +const SkletonCarouselStyle = styled.div` + background-color: #eef4fa; + padding: 2.5rem; + h2 { + margin-bottom: 2rem; + background-color: #eef4fa; + display: inline-block; + width: 18rem; + margin-top: 0; + } +`; + +const SkeletonItemsStyle = styled.div` + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-gap: 1.5rem; +`; + +const SkeletonItemCardStyle = styled.div` + & > div:last-child { + display: flex; + flex-flow: column; + } +`; +const SkeletonImgContainerStyle = styled.div` + position: relative; + margin-bottom: 1rem; + line-height: 0; + border-radius: 0.3rem; + overflow: hidden; +`; +const SkeletonImgStyle = styled.div` + width: 100%; + height: 10rem; + background-color: #eee; +`; +const SkeletonTitleStyle = styled.div` + display: inline-block; + width: 15rem; + margin-bottom: 0.5rem; + background-color: #eee; +`; +const SkeletonDescriptionStyle = styled.div` + display: inline-block; + width: 8rem; + margin-bottom: 1rem; + background-color: #eee; + font-size: 0.875rem; +`; +const SkeletonPopUpPricesStyle = styled.div` + margin-bottom: 1rem; +`; +const SkeletonPopUpSalePriceStyle = styled.div` + display: inline-block; + width: 5rem; + margin-right: 1rem; + font-size: 1.25rem; + background-color: #eee; +`; +const SkeletonPopUpNormalPriceStyle = styled.div` + display: inline-block; + width: 5rem; + background-color: #eee; +`; + +export default function SkeletonPopUpContainerBody() { + return ( + + + + + + + + + + + + +   + + +   + + +   + +   + + +   + + + + + +   + + +   + + + + +   + + +   + + + + +   + + +   + + + + + + +   + +
+ + + +   + + +   + + +
+
+ + +   + + +   + + + +   + +
+
+
+ +

 

+ + + + + +
+ +   + + +   + + + +   + + +   + + +   +
+
+ + + + +
+ +   + + +   + + + +   + + +   + + +   +
+
+ + + + +
+ +   + + +   + + + +   + + +   + + +   +
+
+
+
+
+ ); +} diff --git a/src/component/main/slideContainer/SlideContainer.jsx b/src/component/main/slideContainer/SlideContainer.jsx new file mode 100644 index 000000000..5f7777d49 --- /dev/null +++ b/src/component/main/slideContainer/SlideContainer.jsx @@ -0,0 +1,35 @@ +import styled from "styled-components"; +import SlideItems from "./SlideItems"; +import SliderArrowBtn from "./SliderArrowBtn"; +import { FaChevronLeft, FaChevronRight } from "react-icons/fa"; + +const SlideContainerStyle = styled.div` + h2 { + margin-bottom: 2rem; + } + margin-bottom: 2.25rem; + position: relative; +`; + +const SliderArrowBtnsStyle = styled.div` + position: absolute; + width: calc(100% + 7rem); + top: 50%; + left: -3.5rem; + z-index: -1; + display: flex; + justify-content: space-between; +`; + +export default function SlideContainer({ onFetchDetailData }) { + return ( + +

후기가 증명하는 베스트 반찬

+ + + + + +
+ ) +} \ No newline at end of file diff --git a/src/component/util/ItemCard.jsx b/src/component/util/ItemCard.jsx index 2586ce062..3a392ab2e 100644 --- a/src/component/util/ItemCard.jsx +++ b/src/component/util/ItemCard.jsx @@ -1,7 +1,14 @@ import styled from "styled-components"; import Label from "./Label"; +import { useOnFetchDetailDataContext } from "../Context"; +import { addCommaToNumber } from "../../common/util.js"; -const ItemCardStyle = styled.div``; +const ItemCardStyle = styled.div` + cursor: pointer; + &:hover .title { + text-decoration: underline; + } +`; const ImgContainerStyle = styled.div` position: relative; @@ -11,43 +18,51 @@ const ImgContainerStyle = styled.div` overflow: hidden; `; -const ImgStyle = styled.img.attrs((props) => ({ - src: props.src +const ImgStyle = styled.div.attrs((props) => ({ + src: props.src, }))` + background-image: url(${(props) => props.src}); + background-size: cover; width: 100%; - height: 18rem; + height: ${(props) => props.large ? '25rem' : props.mini ? '9.625rem': '20rem'}; &:hover + div { - display: block; + display: flex; } `; const HoverStyle = styled.div` - font-size: 1.5rem; + font-size: ${(props) => props.mini ? '1rem': '1.5rem'}; text-align: center; font-weight: 600; color: #fff; - background-color: rgba(0,0,0,50%); + background-color: rgba(0, 0, 0, 50%); position: absolute; top: 0; width: 100%; height: 100%; - padding: calc(50% - 3rem) calc(50% - 4rem); display: none; + align-items: center; + justify-content: center; + flex-flow: column; div { margin: 1rem; line-height: 1rem; } div:first-child { + display: inline-block; padding-bottom: 1rem; + margin-bottom: 0; border-bottom: 1px solid #fff; } &:hover { - display: block; + display: flex; } `; const TitleStyle = styled.div` margin-bottom: 0.5rem; + font-size: ${(props) => props.mini ? '0.9375rem;': '1rem'}; + height: ${(props) => props.mini ? '2rem': 'auto'}; `; const DescriptionStyle = styled.div` @@ -63,38 +78,52 @@ const PricesStyle = styled.div` const NormalPriceStyle = styled.div` text-decoration: line-through; font-size: 0.875rem; - color: #BDBDBD; + color: #bdbdbd; display: inline-block; `; const SalePriceStyle = styled.div` - font-size: 1.25rem; + font-size: ${(props) => props.mini ? '0.8125rem': '1.25rem'}; font-weight: 600; margin-right: 0.5rem; display: inline-block; `; -export default function ItemCard({ src = "", title = "", description = "", salePrice, normalPrice, labels=[] }) { +export default function ItemCard({ + id, + src = "", + title = "", + description = "", + salePrice, + normalPrice, + labels = [], + mini = false, + large = false, +}) { + const onFetchDetailData = useOnFetchDetailDataContext(); + const onClick = () => { + onFetchDetailData(id); + }; return ( - + - - + +
새벽배송
전국택배
- {title} - {description} + {title} + {!mini && {description}} - {salePrice}원 - {normalPrice}원 + {addCommaToNumber(salePrice)}원 + {!mini && {addCommaToNumber(normalPrice)}원} - { - labels.map((label, idx) =>
- ) -} \ No newline at end of file + ); +} diff --git a/src/component/util/Label.jsx b/src/component/util/Label.jsx index 81190ee64..7f6ee0a5d 100644 --- a/src/component/util/Label.jsx +++ b/src/component/util/Label.jsx @@ -4,18 +4,20 @@ const LabelStyle = styled.div` border-radius: 0.3rem; padding: 0.25rem 1rem; color: #fff; + font-weight: 600; font-size: 0.875rem; display: inline-block; text-align: center; background-color: ${props => props.bgColor}; + margin-right: 0.5rem; `; // #86C6FF, #82D32D const COLOR = { "이벤트특가" : "#82D32D", "런칭특가" : "#86C6FF", - "베스트" : "#D980FA" + "베스트" : "#D980FA", } export default function Label({ text }) { - return {text} + return {text} } \ No newline at end of file diff --git a/src/component/util/dj-slider/DicoJsonCarousel.jsx b/src/component/util/dj-slider/DicoJsonCarousel.jsx new file mode 100644 index 000000000..cb3ff0e08 --- /dev/null +++ b/src/component/util/dj-slider/DicoJsonCarousel.jsx @@ -0,0 +1,313 @@ +import { useState, useEffect, createContext, useContext, useRef } from "react"; +import { FaChevronLeft, FaChevronRight } from "react-icons/fa"; +import styled from "styled-components"; + +export function DicoJsonCarousel({ children, options = {} }) { + const { + perPanel = 4, + speed = 800, + autoplay = false, + interval = 2500, + dots = false, + count = false, + loop = true, + } = options; + const maxPage = Math.ceil(children.length / perPanel); + const [page, setPage] = useState(0); + const transitionDefault = `transform ${speed}ms ease-in-out`; + const [x, setX] = useState(-100 / 3); + const [moving, setMoving] = useState(false); + // const [autoplayState, setAutoplayState] = useState(autoplay); + const [trasitionValue, setTransitionValue] = useState(transitionDefault); + const direction = useRef(0); + direction.current = 0; + + const onTransitionEnd = () => { + setMoving(false); + setTransitionValue("none"); + let next = x > -1 ? page - 1 : page + 1; + if (next < 0 && !loop) { + next = maxPage; + } + if (next > maxPage && !loop) { + next = 1; + } + setPage(next); + setX(-100 / 3); + }; + + const onMovePage = (count) => { + if (moving) return; + direction.current = 0; + setX(count > 0 ? (-100 / 3) * 2 : 0); + setMoving(true); + direction.current = count; + }; + + useEffect(() => { + if (trasitionValue === "none") setTransitionValue(transitionDefault); + }, [x]); + + const components = []; + if (loop) { + for(let i = (page - 1) * perPanel; i < (page + 2) * perPanel; i++) { + if(i < 0) + components.push( +
+ {children[((i % children.length) + children.length) % children.length]} +
+ ); + else + components.push( +
+ {children[i % children.length]} +
+ ); + } + } else { + for (let i = perPanel * (page - 1); i < children.length; i++) { + components.push(
{children[i]}
); + } + } + + // useEffect(() => { + // if(loop && autoplayState) { + // setAutoplayState(false); + // setTimeout(async () => { + // onMovePage(1); + // setAutoplayState(true); + // }, interval); + // } + // }, [autoplayState]); + + if (!children) return null; + + const slideStyles = { + transform: `translate3d(${x}%, 0, 0)`, + transition: trasitionValue, + }; + + return ( + + + + {components} + + + + onMovePage(-1)} + active={loop || page > 0} + > + + + onMovePage(1)} + active={loop || page < maxPage - 1} + > + + + + {dots && } + {count && ( + + {page + 1}/{maxPage + 1} + + )} + + ); +} + +const DicoJsonCarouselContainer = styled.div` + position: relative; + width: 100%; + min-height: 5vh; +`; + +const CarouselTrack = styled.div` + overflow: hidden; + position: relative; +`; + +const CarouselListStyle = styled.div` + display: grid; + width: 300%; + grid-template-columns: repeat(${(props) => props.perPanel * 3}, 1fr); + & > * { + margin-right: 0.5rem; + &:nth-child(${(props) => props.perPanel}n + 1) { + margin-right: 1rem; + } + &:not(:nth-child(${(props) => props.perPanel}n + 1)) { + margin-left: 0.5rem; + } + &:nth-child(${(props) => props.perPanel}n) { + margin-right: 0rem; + margin-left: 1rem; + } + } +`; + +const CarouselArrows = styled.div` + position: absolute; + width: calc(100% + 7rem); + top: calc(50% - 4rem); + left: -3.5rem; + display: flex; + justify-content: space-between; + & > * { + padding: 1rem; + line-height: 1rem; + font-size: 1.25rem; + cursor: pointer; + } +`; + +const CarouselLeftArrow = styled.div` + opacity: ${(props) => (props.active ? 1 : 0.5)}; + user-select: ${(props) => (props.active ? "initial" : "none")}; + pointer-events: ${(props) => (props.active ? "initial" : "none")}; +`; + +const CarouselRightArrow = styled.div` + opacity: ${(props) => (props.active ? 1 : 0.5)}; + user-select: ${(props) => (props.active ? "initial" : "none")}; + pointer-events: ${(props) => (props.active ? "initial" : "none")}; +`; + +const Dots = styled.div``; + +const Count = styled.div``; + + +// 디코 최고 +/* +페이지 / 시작점(-1페이지의 가장 첫번째 인덱스) +5(children.length) % 4(perPanel) = 1 +-3 4 +-2 3 +-1 2 +0 1 '1' 2 3 4 | 0 1 2 3 | 4 0 1 2 +1 0 index + 3페이지에 들어가는 총 요소 갯수 % +2 4 + +6(children.length) % 4(perPanel)= 2 +-2 0 +-1 4 +0 2 '2' 3 4 5 | 0 1 2 3 | 4 5 0 1 +1 0 +2 4 + +6(children.length) % 5(perPanel) = 1 + +0 1 '1' 2 3 4 5 | 0 1 2 3 4 | 5 0 1 2 3 + + +5(children.length) % 4(perPanel) = 1 + +-4 +1 2 3 4 | 0 1 2 3 | 4 1 2 3 + +1이상이면 +(page - 1) * perPanel(4) ~ (page + 2) * perPanel(4) + +0일 때 +(-1) * perPanel ~ (2) * perPanel +-4 ~ 8 // [0, 1, 2, 3, 4] + +page = 0 +범위 = -4 ~ 8 +// -4 와 1의 연관성, -3과 2의 연관성, -2와 3의 연관성, -1과 4의 연관성 +// 1 2 3 4 | 0 1 2 3 | 4 0 1 2 + +음수일 때, + +page = -4 +0 1 2 3 | 4 0 1 2 | 3 4 0 1 +page = -3 +4 0 1 2 | 3 4 0 1 | 2 3 4 0 +page = -2 +3 4 0 1 | 2 3 4 0 | 1 2 3 4 +page = -1 +2 3 4 0 | 1 2 3 4 | 0 1 2 3 + +(page - 1) * perPanel(4) ~ (page + 2) * perPanel(4) + +page = 0 +범위 = -4 ~ 8 // -4 % 5 = -4 + children.length 1 % children.length = 1 + +page = -1 +범위 = -8 ~ 4 // -8 % 5 = -3 + children.length 2 % children.length = 2 + +page = -2 +범위 = -12 ~ 0 // -12 % 5 = -2 + children.length 3 % children.length = 3 + +page = -3 +범위 = -16 ~ -4 // -16 % 5 = -1 + children.length 4 % children.length = 4 + +page = -4 +범위 = -20 ~ -8 // -20 % 5 = 0 + children.length 5 % children.length = 0 + +// page = 1 +// 0 1 2 3 | 4 0 1 2 | 3 4 0 1 +// page = 2 +// 4 0 1 2 | 3 4 0 1 | 2 3 4 0 +// page = 3 +// 3 4 0 1 | 2 3 4 0 | 1 2 3 4 + +/* +*/ + + /* 루프가 있다면??? */ + // 최소 페이지나 최대 페이지 외의 영역에도, 반대 영역의 요소들을 앞, 뒤로 붙여야 함! + // 5 + // 1 2 3 4 | 0 1 2 3 | 4 0 1 2 + // 음수 일 때 page -1, -2 + // page = -3 - -1 = -4 + // 4 0 1 2 | 3 4 0 1 | 2 3 4 0 + // page = -2 - -1 = -3 + // 3 4 0 1 | 2 3 4 0 | 1 2 3 4 + // page = -1 - -1 = -2 + // 2 3 4 0 | 1 2 3 4 | 0 1 2 3 + // page = 0 - 1 = 1 + // 1 2 3 4 | 0 1 2 3 | 4 0 1 2 + // page = 1 + // 0 1 2 3 | 4 0 1 2 | 3 4 0 1 + // page = 2 - 1 ? 1 + // 4 0 1 2 | 3 4 0 1 | 2 3 4 0 + + // children.length % perPanel = 나머지 + // Math.abs((page - 1) * perPanel - perPanel) % children.length + // page = -2 + // abs((-2 - -1) * 4 - 4) = 16 % 5 = 1 + // page = -1 + // abs((-1 - -1) * 4 - 4) = 12 % 5 = 2 + // page = 0 + // abs((0 - -1) * 4 - 4) = 8 % 5 = 3 + + // page * perPanel = -8 => abs(-8) = 8 % children.length = 3 ~ 15(미만) + // -3 * 4 = 12 = 2 ~ 14 + // -2 * 4 = -8 => abs(-8) = 8 % 5 = 3 ~ 15 + // -1 * 4 = -4 => 4 = 4 % 5 = 4 ~ 16 + + // 렌더링 해야 될 아이템의 갯수(12) = perPanel * 3 + // 시작점이 i일 때, i + 12 미만 까지 components 배열에 추가하면 됨! + // 나머지(1) = children.length % perPanel + // maxPage 이상일 때 + + // perPanel의 배수가 아닐 때 + + // perPanel의 배수일 때 + // const [start, end] = [ + // Math.abs(perPanel * (page - 1)), + // Math.abs(perPanel * (page - 1)) + perPanel * 2, + // ].sort((a, b) => a - b); + // for (let i = start; i < end; i++) { + // components.push( + //
{children[i % children.length]}
+ // ); + // } \ No newline at end of file