diff --git a/README.md b/README.md index 9fa347d00..055e1a4a0 100644 --- a/README.md +++ b/README.md @@ -13,3 +13,113 @@ ## Week 2. 1단계 - 페이지 만들기 [🔗 link](https://edu.nextstep.camp/s/hazAC9xa/ls/QzV1ncxk) + +--- + +# 구현 목록 + +## 1단계 + +- [x] react router dom 세팅 + +- 공통 컴포넌트 만들기 + - [x] Header + - [x] Footer + +- 페이지 만들기 + - 메인 페이지 (/) + - [x] Theme 카테고리 섹션 추가 + - [x] Theme 페이지 이동 + - [x] 실시간 급상승 선물랭킹 + - [x] 필터 기능을 hooks를 사용하여 구현 (ex. 전체, 여성이, 남성이, 청소년이 / 받고 싶어한, 많이 선물한, 위시로 받은) + - [x] 상품 목록 접기 + - [x] 상품 목록 펼치기 + - Theme 페이지(/theme :themeKey) + - [x] Header 섹션 추가 + - [x] 재사용성을 고려하여 Header 섹션 만들기 (themeKey에 따라 label, title, description, backgroundColor가 달라짐) + - [x] 상품 목록 섹션을 추가. + - 로그인 페이지(/login) + - [x] 로그인 기능 추가 (ID와 PW는 아무 값을 입력해도 통과되도록) + - 나의 페이지(/my-account) + - [x] 로그아웃 버튼 추가 + - [x] 로그아웃 기능 추가 + + +## 2단계 + +- [ ] 로그인 페이지에서 ID와 PW를 입력하면 직전 페이지로 Redirect 되도록 해요. +- [x] Fake 로그인 기능을 구현해요. + - [x] 로그인 페이지에서 ID와 PW를 입력하면 ID를 sessionStorage의 authToken key에 저장해요. + - [x] 모든 페이지 진입 시 authToken을 토대로 로그인 여부를 판단하는 로직을 추가해요. (ContextAPI 활용) + - [x] Header에서 로그인 한 경우 내 계정을 로그인 하지 않은 경우 로그인 버튼을 추가해요. + - [x] 내 계정(/my-account) 페이지는 로그인 한 사람만 접근 가능하게 해요. (로그인 하지 않은 유저는 로그인 페이지로 연결해요) + - [x] 내 계정 페이지에서 로그아웃을 할 수 있도록 해요. (로그아웃 후 메인 페이지(/) 로 Redirect 되도록 해요) + + + +--- + + + +# 폴더 구조 + +```text +src +├── components +│ ├── common +│ │ ├── Button +│ │ │ ├── index.stories.tsx +│ │ │ ├── index.tsx +│ │ ├── FilterButton +│ │ │ ├── FilterButton.tsx +│ │ ├── Form +│ │ │ ├── Input +│ │ │ │ ├── UnderlineTextField.stories.tsx +│ │ │ │ ├── UnderlineTextField.tsx +│ │ ├── GoodsItem +│ │ │ ├── Default.stories.tsx +│ │ │ ├── Default.tsx +│ │ │ ├── Ranking.stories.tsx +│ │ │ ├── Ranking.tsx +│ │ ├── Image +│ │ │ ├── index.stories.tsx +│ │ │ ├── index.tsx +│ │ ├── layouts +│ │ │ ├── Container +│ │ │ │ ├── index.stories.tsx +│ │ │ │ ├── index.tsx +│ │ │ ├── Grid +│ │ │ │ ├── index.stories.tsx +│ │ │ │ ├── index.tsx +│ ├── Footer +│ │ ├── Footer.tsx +│ ├── GoodsCategory +│ │ ├── GoodsCategory.tsx +│ ├── Header +│ │ ├── Header.tsx +│ ├── Items +│ │ ├── Items.tsx +│ ├── Ranking +│ │ ├── Detail +│ │ │ ├── DetailButton.tsx +│ │ ├── Filter +│ │ │ ├── Filter.tsx +│ │ │ ├── FilterTab.tsx +│ │ │ ├── FilterTabs.tsx +│ │ ├── RankingItems +│ │ │ ├── RankingItems.tsx +│ │ │ ├── RankingHeader.tsx +│ ├── SelectFriend +│ │ ├── SelectFriend.tsx +│ ├── ThemeGoods +│ │ ├── ThemeGoods.tsx +│ ├── ThemeHeader +│ │ ├── ThemeHeader.tsx +├── pages +│ ├── Home.tsx +│ ├── Login.tsx +│ ├── MyAccount.tsx +├── routes +│ ├── index.tsx + +``` \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8f100a3a8..ab0ccb04b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,8 @@ "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^6.24.0" }, "devDependencies": { "@craco/craco": "^7.1.0", @@ -6135,6 +6136,14 @@ "@babel/runtime": "^7.13.10" } }, + "node_modules/@remix-run/router": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.17.1.tgz", + "integrity": "sha512-mCOMec4BKd6BRGBZeSnGiIgwsbLGp3yhVqAD8H+PxiRNEHgDpZb8J1TnrSDlg97t0ySKMQJTHCWBCmBpSmkF6Q==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -28318,6 +28327,36 @@ } } }, + "node_modules/react-router": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.24.1.tgz", + "integrity": "sha512-PTXFXGK2pyXpHzVo3rR9H7ip4lSPZZc0bHG5CARmj65fTT6qG7sTngmb6lcYu1gf3y/8KxORoy9yn59pGpCnpg==", + "dependencies": { + "@remix-run/router": "1.17.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.24.1.tgz", + "integrity": "sha512-U19KtXqooqw967Vw0Qcn5cOvrX5Ejo9ORmOtJMzYWtCT4/WOfFLIZGGsVLxcd9UkBO0mSTZtXqhZBsWlHr7+Sg==", + "dependencies": { + "@remix-run/router": "1.17.1", + "react-router": "6.24.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", diff --git a/package.json b/package.json index 8a3e091c7..21b33e338 100644 --- a/package.json +++ b/package.json @@ -25,11 +25,10 @@ "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^6.24.0" }, "devDependencies": { - "react-scripts": "5.0.1", - "typescript": "^4.9.5", "@craco/craco": "^7.1.0", "@emotion/eslint-plugin": "^11.11.0", "@storybook/addon-essentials": "^7.6.17", @@ -65,8 +64,10 @@ "eslint-plugin-storybook": "^0.8.0", "prettier": "^3.2.5", "prop-types": "^15.8.1", + "react-scripts": "5.0.1", "storybook": "^7.6.17", "tsconfig-paths-webpack-plugin": "^4.1.0", + "typescript": "^4.9.5", "webpack": "^5.90.3" }, "overrides": { diff --git a/src/App.tsx b/src/App.tsx index 1df5ce256..9f4a99451 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,18 +1,11 @@ -import styled from '@emotion/styled'; +import Router from './routes'; const App = () => { - const name = 'Josh Perez'; - return ( -
- Hello, {name} -
+ <> + + ); }; export default App; - -const Title = styled.h1` - font-size: 1.5em; - color: gray; -`; diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx new file mode 100644 index 000000000..c45ee818b --- /dev/null +++ b/src/components/Footer/Footer.tsx @@ -0,0 +1,21 @@ +/** @jsxImportSource @emotion/react */ +import styled from '@emotion/styled'; +import React from "react"; + +const FooterContainer = styled.footer` + display: flex; + justify-content: space-between; + width: 100%; + height: 150px; + background-color: #f5f5f5; +`; + +const Footer: React.FC = () => { + return ( + +

카카오톡 선물하기

+
+ ); +} + +export default Footer; \ No newline at end of file diff --git a/src/components/GoodsCategory/GoodsCategory.tsx b/src/components/GoodsCategory/GoodsCategory.tsx new file mode 100644 index 000000000..ab8ea5637 --- /dev/null +++ b/src/components/GoodsCategory/GoodsCategory.tsx @@ -0,0 +1,52 @@ +import styled from "@emotion/styled"; +import React from "react"; + +import { Grid } from "@/components/common/layouts/Grid"; +import Item from "@/components/Items/items"; + +interface ItemType { + image: string; + label: string; + themekey: string; + radius?: 'circle' | number; + } + + const imageUrls = { + img: 'https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fgift%2Fhome%2Ftheme%2F292020231106_MXMUB.png', + friendImg: 'https://gift-s.kakaocdn.net/dn/gift/images/m640/bg_profile_default.png' + }; + + const items: ItemType[] = [ + { image: imageUrls.img, label: '생일', themekey: 'birthday' }, + { image: imageUrls.img, label: '졸업선물', themekey: 'graduation'}, + { image: imageUrls.img, label: '명품선물', themekey: 'luxury'}, + { image: imageUrls.img, label: '스몰럭셔리', themekey: 'luxury'}, + { image: imageUrls.img, label: '결혼/집들이', themekey: 'wedding'}, + { image: imageUrls.img, label: '따뜻한선물' , themekey: 'warm'}, + { image: imageUrls.img, label: '가벼운선물' , themekey: 'light'}, + { image: imageUrls.img, label: '팬심저격', themekey: 'fan'}, + { image: imageUrls.img, label: '교환권', themekey: 'exchange'}, + { image: imageUrls.img, label: '건강/비타민', themekey: 'health'}, + { image: imageUrls.img, label: '과일/한우', themekey: 'fruit'}, + { image: imageUrls.img, label: '출산/키즈', themekey: 'kids'}, + ]; + +const GridWrapper = styled.div` + width: 95%; + margin: 0 auto; + align-items: center; +`; + +const GoodsCatygory: React.FC = () => { + return ( + + + {items.map((item, index) => ( + + ))} + + + ); +}; + +export default GoodsCatygory; \ No newline at end of file diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx new file mode 100644 index 000000000..bd15a5daa --- /dev/null +++ b/src/components/Header/Header.tsx @@ -0,0 +1,52 @@ +import styled from '@emotion/styled'; +import React from 'react'; +import { Link } from 'react-router-dom'; + +const HeaderContainer = styled.header` + display: flex; + justify-content: space-between; + padding: 10px 20px; + background-color: #fff; + border-bottom: 1px solid #ccc; +`; + +const Nav = styled.nav` + display: flex; + width: 100%; + justify-content: space-between; +`; + +const NavItem = styled.div` + display: flex; + align-items: center; +`; + +const StyledLink = styled(Link)` + text-decoration: none; + color: black; + font-weight: bold; + outline: none; +`; + +interface HeaderProps { + isLoggedIn: boolean; +} + +const Header: React.FC = ({isLoggedIn}) => { + return ( + + + + ); +}; + +export default Header; diff --git a/src/components/Items/items.tsx b/src/components/Items/items.tsx new file mode 100644 index 000000000..514411cd0 --- /dev/null +++ b/src/components/Items/items.tsx @@ -0,0 +1,47 @@ +/** @jsxImportSource @emotion/react */ +import { css } from '@emotion/react'; +import React from 'react'; +import { Link } from 'react-router-dom'; + +import { Image } from '@/components/common/Image/index'; + +const itemStyle = css` + display: flex; + flex-direction: column; + align-items: center; + margin: 10px; + span { + margin-top: 10px; + font-size: 14px; + } +`; + +const linkStyle = css` + outline: none; + &:focus, + &:active { + outline: none; + } +`; + +interface ItemProps { + image: string; + label: string; + themekey: string; + radius?: 'circle' | number; +} + +const Item: React.FC = ({ image, label, radius, themekey}) => { + radius = 20; + return ( +
+ + {label} + + {label} +
+ ); +}; + +export default Item; + diff --git a/src/components/PrivateRoute/PrivateRoute.tsx b/src/components/PrivateRoute/PrivateRoute.tsx new file mode 100644 index 000000000..0b7a9bb5d --- /dev/null +++ b/src/components/PrivateRoute/PrivateRoute.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { Navigate, useLocation } from 'react-router-dom'; + +import { useAuth } from '@/contexts/Authcontext'; + + +interface PrivateRouteProps { + children: JSX.Element; + } + +const PrivateRoute: React.FC = ({ children}) => { + const {isLoggedIn} = useAuth(); + const location = useLocation(); + + if (!isLoggedIn) { + console.log(location) + return ; + } + + return children; +}; + +export default PrivateRoute; diff --git a/src/components/Ranking/Detail/DetailButton.tsx b/src/components/Ranking/Detail/DetailButton.tsx new file mode 100644 index 000000000..c38ef9026 --- /dev/null +++ b/src/components/Ranking/Detail/DetailButton.tsx @@ -0,0 +1,35 @@ +import styled from '@emotion/styled'; +import React from 'react'; + +const Button = styled.button` + background-color: white; + color: black; + border: 1px solid #ddd; + border-radius: 10px; + padding: 20px 25px; + cursor: pointer; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + width: 50%; + + &:focus { + outline: none; + } +`; + +interface DetailButtonProps { + onClick: () => void; + text: string; +} + +const DetailButton: React.FC = ({ onClick, text }) => { + return ( +
+ +
+ ); +}; + +export default DetailButton; diff --git a/src/components/Ranking/Filter/Filter.tsx b/src/components/Ranking/Filter/Filter.tsx new file mode 100644 index 000000000..174f54018 --- /dev/null +++ b/src/components/Ranking/Filter/Filter.tsx @@ -0,0 +1,81 @@ +import styled from '@emotion/styled'; +import React, { useState } from 'react'; + +import FilterButton from '../../common/FilterButton/FilterButton'; +import RankingItems from '../RankingItems/RankingItems'; +import FilterTabs from './FilterTabs'; + +const FilterContent = styled.div` + margin: 20px auto; + `; + +const FilterWrapper = styled.div` + display: flex; + justify-content: space-around; +`; + +const items = [ + { id: 1, category: 'ALL', imageSrc: 'https://st.kakaocdn.net/product/gift/product/20231030175450_53e90ee9708f45ffa45b3f7b4bc01c7c.jpg', subtitle: 'BBQ', title: 'BBQ 양념치킨+크림치즈볼+콜라1.25L', amount: 10000 }, + { id: 2, category: '여성', imageSrc: 'https://st.kakaocdn.net/product/gift/product/20231030175450_53e90ee9708f45ffa45b3f7b4bc01c7c.jpg', subtitle: 'BBQ', title: 'BBQ 양념치킨+크림치즈볼+콜라1.25L', amount: 20000 }, + { id: 3, category: '남성', imageSrc: 'https://st.kakaocdn.net/product/gift/product/20231030175450_53e90ee9708f45ffa45b3f7b4bc01c7c.jpg', subtitle: 'BBQ', title: 'BBQ 양념치킨+크림치즈볼+콜라1.25L', amount: 20000 }, + { id: 4, category: '청소년', imageSrc: 'https://st.kakaocdn.net/product/gift/product/20231030175450_53e90ee9708f45ffa45b3f7b4bc01c7c.jpg', subtitle: 'BBQ', title: 'BBQ 양념치킨+크림치즈볼+콜라1.25L', amount: 20000 }, + { id: 5, category: '여성', imageSrc: 'https://st.kakaocdn.net/product/gift/product/20231030175450_53e90ee9708f45ffa45b3f7b4bc01c7c.jpg', subtitle: 'BBQ', title: 'BBQ 양념치킨+크림치즈볼+콜라1.25L', amount: 20000 }, + { id: 6, category: '남성', imageSrc: 'https://st.kakaocdn.net/product/gift/product/20231030175450_53e90ee9708f45ffa45b3f7b4bc01c7c.jpg', subtitle: 'BBQ', title: 'BBQ 양념치킨+크림치즈볼+콜라1.25L', amount: 20000 }, + { id: 7, category: '남성', imageSrc: 'https://st.kakaocdn.net/product/gift/product/20231030175450_53e90ee9708f45ffa45b3f7b4bc01c7c.jpg', subtitle: 'BBQ', title: 'BBQ 양념치킨+크림치즈볼+콜라1.25L', amount: 20000 }, + { id: 8, category: '남성', imageSrc: 'https://st.kakaocdn.net/product/gift/product/20231030175450_53e90ee9708f45ffa45b3f7b4bc01c7c.jpg', subtitle: 'BBQ', title: 'BBQ 양념치킨+크림치즈볼+콜라1.25L', amount: 20000 }, + { id: 9, category: '남성', imageSrc: 'https://st.kakaocdn.net/product/gift/product/20231030175450_53e90ee9708f45ffa45b3f7b4bc01c7c.jpg', subtitle: 'BBQ', title: 'BBQ 양념치킨+크림치즈볼+콜라1.25L', amount: 20000 }, + { id: 10, category: '남성', imageSrc: 'https://st.kakaocdn.net/product/gift/product/20231030175450_53e90ee9708f45ffa45b3f7b4bc01c7c.jpg', subtitle: 'BBQ', title: 'BBQ 양념치킨+크림치즈볼+콜라1.25L', amount: 20000 }, + { id: 11, category: '남성', imageSrc: 'https://st.kakaocdn.net/product/gift/product/20231030175450_53e90ee9708f45ffa45b3f7b4bc01c7c.jpg', subtitle: 'BBQ', title: 'BBQ 양념치킨+크림치즈볼+콜라1.25L', amount: 20000 }, + { id: 12, category: '남성', imageSrc: 'https://st.kakaocdn.net/product/gift/product/20231030175450_53e90ee9708f45ffa45b3f7b4bc01c7c.jpg', subtitle: 'BBQ', title: 'BBQ 양념치킨+크림치즈볼+콜라1.25L', amount: 20000 }, + { id: 13, category: '남성', imageSrc: 'https://st.kakaocdn.net/product/gift/product/20231030175450_53e90ee9708f45ffa45b3f7b4bc01c7c.jpg', subtitle: 'BBQ', title: 'BBQ 양념치킨+크림치즈볼+콜라1.25L', amount: 20000 }, + { id: 14, category: '남성', imageSrc: 'https://st.kakaocdn.net/product/gift/product/20231030175450_53e90ee9708f45ffa45b3f7b4bc01c7c.jpg', subtitle: 'BBQ', title: 'BBQ 양념치킨+크림치즈볼+콜라1.25L', amount: 20000 }, + { id: 15, category: '남성', imageSrc: 'https://st.kakaocdn.net/product/gift/product/20231030175450_53e90ee9708f45ffa45b3f7b4bc01c7c.jpg', subtitle: 'BBQ', title: 'BBQ 양념치킨+크림치즈볼+콜라1.25L', amount: 20000 }, + { id: 16, category: '남성', imageSrc: 'https://st.kakaocdn.net/product/gift/product/20231030175450_53e90ee9708f45ffa45b3f7b4bc01c7c.jpg', subtitle: 'BBQ', title: 'BBQ 양념치킨+크림치즈볼+콜라1.25L', amount: 20000 }, + { id: 17, category: '남성', imageSrc: 'https://st.kakaocdn.net/product/gift/product/20231030175450_53e90ee9708f45ffa45b3f7b4bc01c7c.jpg', subtitle: 'BBQ', title: 'BBQ 양념치킨+크림치즈볼+콜라1.25L', amount: 20000 }, + { id: 18, category: '남성', imageSrc: 'https://st.kakaocdn.net/product/gift/product/20231030175450_53e90ee9708f45ffa45b3f7b4bc01c7c.jpg', subtitle: 'BBQ', title: 'BBQ 양념치킨+크림치즈볼+콜라1.25L', amount: 20000 }, + { id: 19, category: '남성', imageSrc: 'https://st.kakaocdn.net/product/gift/product/20231030175450_53e90ee9708f45ffa45b3f7b4bc01c7c.jpg', subtitle: 'BBQ', title: 'BBQ 양념치킨+크림치즈볼+콜라1.25L', amount: 20000 }, + { id: 20, category: '남성', imageSrc: 'https://st.kakaocdn.net/product/gift/product/20231030175450_53e90ee9708f45ffa45b3f7b4bc01c7c.jpg', subtitle: 'BBQ', title: 'BBQ 양념치킨+크림치즈볼+콜라1.25L', amount: 20000 }, + { id: 21, category: '남성', imageSrc: 'https://st.kakaocdn.net/product/gift/product/20231030175450_53e90ee9708f45ffa45b3f7b4bc01c7c.jpg', subtitle: 'BBQ', title: 'BBQ 양념치킨+크림치즈볼+콜라1.25L', amount: 20000 }, +]; + +const Filter: React.FC = () => { + const [activeFilters, setActiveFilters] = useState<{ [key: string]: boolean }>({ + ALL: true, + 여성: false, + 남성: false, + 청소년: false, + }); + const [tab, setTab] = useState('받고 싶어한'); + + const handleFilterChange = (newFilter: string) => { + setActiveFilters({ + ALL: newFilter === 'ALL', + 여성: newFilter === '여성', + 남성: newFilter === '남성', + 청소년: newFilter === '청소년', + }); + }; + + const handleTabChange = (newTab: string) => { + setTab(newTab); + }; + + const filteredItems = items.filter(item => activeFilters.ALL || activeFilters[item.category]); + + return ( + + + handleFilterChange('ALL')} buttonText='ALL'>전체 + handleFilterChange('여성')} buttonText='👩🏻‍🦳'>여성이 + handleFilterChange('남성')} buttonText='👨🏻‍🦳'>남성이 + handleFilterChange('청소년')} buttonText='👦🏻'>청소년이 + +
+ +
+ +
+ ); +}; + +export default Filter; diff --git a/src/components/Ranking/Filter/FilterTab.tsx b/src/components/Ranking/Filter/FilterTab.tsx new file mode 100644 index 000000000..35552daef --- /dev/null +++ b/src/components/Ranking/Filter/FilterTab.tsx @@ -0,0 +1,24 @@ +import styled from '@emotion/styled'; +import React from 'react'; + +const Tab = styled.div<{ active: boolean }>` + color: ${(props) => (props.active ? '#4285f4' : '#7fa1e7')}; + font-size: 16px; + font-weight: bold; + cursor: pointer; + padding: 10px; + margin: 0 10px; + width: 95%; +`; + +interface FilterTabProps { + active: boolean; + onClick: () => void; + children: React.ReactNode; +} + +const FilterTab: React.FC = ({ active, onClick, children }) => { + return {children}; +}; + +export default FilterTab; diff --git a/src/components/Ranking/Filter/FilterTabs.tsx b/src/components/Ranking/Filter/FilterTabs.tsx new file mode 100644 index 000000000..2a8f00c06 --- /dev/null +++ b/src/components/Ranking/Filter/FilterTabs.tsx @@ -0,0 +1,59 @@ +import styled from '@emotion/styled'; +import React from 'react'; + +const TabsWrapper = styled.div` + display: flex; + justify-content: space-around; + background-color: #daeefb; + border-radius: 10px; + border: 1px solid #dae5fb; + padding: 10px; + width: 85%; + margin: 20px auto; +`; + +const Tab = styled.div<{ active: boolean }>` + color: ${(props) => (props.active ? '#4285f4' : '#7fa1e7')}; + font-size: 24px; + font-weight: bold; + cursor: pointer; + padding: 10px; + margin: 0 10px; + width: 95%; + text-align: center; + pointer-events: auto; +`; + +const TabsContent = styled.div` + margin: 50px auto; +`; + + +interface FilterTabProps { + active: boolean; + onClick: () => void; + children: React.ReactNode; +} + +const FilterTab: React.FC = ({ active, onClick, children }) => { + return {children}; +}; + +interface FilterTabsProps { + activeTab: string; + onTabChange: (tab: string) => void; +} + +const FilterTabs: React.FC = ({ activeTab, onTabChange }) => { + return ( + + + onTabChange('받고 싶어한')}>받고 싶어한 + onTabChange('많이 선물한')}>많이 선물한 + onTabChange('위시로 받은')}>위시로 받은 + + + ); +}; + +export default FilterTabs; diff --git a/src/components/Ranking/RankingHeader.tsx b/src/components/Ranking/RankingHeader.tsx new file mode 100644 index 000000000..35fa561db --- /dev/null +++ b/src/components/Ranking/RankingHeader.tsx @@ -0,0 +1,21 @@ +/** @jsxImportSource @emotion/react */ +import { css } from '@emotion/react'; +import React from "react"; + +const headerStyle = css` + font-size: 40px; + font-weight: bold; + text-align: center; + margin-top: 120px; + margin-bottom: 50px; +`; + +const RankingHeader: React.FC = () => { + return ( +
+ 실시간 급상승 선물랭킹 +
+ ); + }; + + export default RankingHeader; \ No newline at end of file diff --git a/src/components/Ranking/RankingItems/RankingItems.tsx b/src/components/Ranking/RankingItems/RankingItems.tsx new file mode 100644 index 000000000..f242bfe17 --- /dev/null +++ b/src/components/Ranking/RankingItems/RankingItems.tsx @@ -0,0 +1,60 @@ +import styled from '@emotion/styled'; +import React, { useState } from 'react'; + +import { RankingGoodsItems } from '../../common/GoodsItem/Ranking'; +import { Grid } from '../../common/layouts/Grid'; +import DetailButton from '../Detail/DetailButton'; + +interface Item { + id: number; + category: string; + imageSrc: string; + subtitle: string; + title: string; + amount: number; +} + +interface RankingItemsProps { + items: Item[]; +} + +const GridWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: 85%; + margin: 80px auto; +`; + +const RankingItems: React.FC = ({ items }) => { + const [visibleCount, setVisibleCount] = useState(6); + + + const handleShowMore = () => { + setVisibleCount(prevCount => (prevCount < items.length ? prevCount + 15 : 6)); + }; + + const buttonText = visibleCount < items.length ? '더보기' : '접기'; + + return ( + <> + + + {items.slice(0, visibleCount).map((item, index) => ( + + ))} + + + + + ); +}; + +export default RankingItems; diff --git a/src/components/SelectFriend/SelectFriend.tsx b/src/components/SelectFriend/SelectFriend.tsx new file mode 100644 index 000000000..a76adbfa7 --- /dev/null +++ b/src/components/SelectFriend/SelectFriend.tsx @@ -0,0 +1,36 @@ +import styled from '@emotion/styled'; +import React from 'react'; + +import { Image } from '../common/Image'; + + +const SelectFriendStyle = styled.div` + display: flex; + align-items: center; + width: 100%; + height: 180px; + margin-bottom: 30px; + background-color: #f5f5f5; +`; + +const SelectFriendTextStyle = styled.span` + font-size: 30px; + font-weight: 600; +`; + +const SelectFriendImageStyle = styled(Image)` + width: 70px; + height: 70px; + padding: 20px; +`; + +const SelectFriend : React.FC = () => { + return ( + + + 선물 받을 친구를 선택해주세요. + +); +} + +export default SelectFriend; \ No newline at end of file diff --git a/src/components/ThemeGoods/ThemeGoods.tsx b/src/components/ThemeGoods/ThemeGoods.tsx new file mode 100644 index 000000000..9d9a1e223 --- /dev/null +++ b/src/components/ThemeGoods/ThemeGoods.tsx @@ -0,0 +1,56 @@ +import styled from "@emotion/styled"; +import React from "react"; + +import { Grid } from "@/components/common/layouts/Grid/index"; + +import { DefaultGoodsItems } from "../common/GoodsItem/Default"; + +interface ItemType { + imageSrc: string; + subtitle: string; + title: string; + amount: number; +} + +const imageUrls = { + img: "https://img1.daumcdn.net/thumb/S104x104/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fgift%2Fhome%2Ftheme%2F292020231106_MXMUB.png", + friendImg: "https://gift-s.kakaocdn.net/dn/gift/images/m640/bg_profile_default.png", + chickenImg : "https://st.kakaocdn.net/product/gift/product/20231030175450_53e90ee9708f45ffa45b3f7b4bc01c7c.jpg" +}; + +const items: ItemType[] = [ + { imageSrc: imageUrls.chickenImg, title: "BBQ 양념치킨+크림치즈볼+콜라1.25L", subtitle: "BBQ", amount:29000}, + { imageSrc: imageUrls.chickenImg, title: "BBQ 양념치킨+크림치즈볼+콜라1.25L", subtitle: "BBQ", amount:29000}, + { imageSrc: imageUrls.chickenImg, title: "BBQ 양념치킨+크림치즈볼+콜라1.25L", subtitle: "BBQ", amount:29000}, + { imageSrc: imageUrls.chickenImg, title: "BBQ 양념치킨+크림치즈볼+콜라1.25L", subtitle: "BBQ", amount:29000}, + { imageSrc: imageUrls.chickenImg, title: "BBQ 양념치킨+크림치즈볼+콜라1.25L", subtitle: "BBQ", amount:29000}, + { imageSrc: imageUrls.chickenImg, title: "BBQ 양념치킨+크림치즈볼+콜라1.25L", subtitle: "BBQ", amount:29000}, + { imageSrc: imageUrls.chickenImg, title: "BBQ 양념치킨+크림치즈볼+콜라1.25L", subtitle: "BBQ", amount:29000}, + { imageSrc: imageUrls.chickenImg, title: "BBQ 양념치킨+크림치즈볼+콜라1.25L", subtitle: "BBQ", amount:29000}, + { imageSrc: imageUrls.chickenImg, title: "BBQ 양념치킨+크림치즈볼+콜라1.25L", subtitle: "BBQ", amount:29000}, + { imageSrc: imageUrls.chickenImg, title: "BBQ 양념치킨+크림치즈볼+콜라1.25L", subtitle: "BBQ", amount:29000}, + { imageSrc: imageUrls.chickenImg, title: "BBQ 양념치킨+크림치즈볼+콜라1.25L", subtitle: "BBQ", amount:29000}, + { imageSrc: imageUrls.chickenImg, title: "BBQ 양념치킨+크림치즈볼+콜라1.25L", subtitle: "BBQ", amount:29000}, + { imageSrc: imageUrls.chickenImg, title: "BBQ 양념치킨+크림치즈볼+콜라1.25L", subtitle: "BBQ", amount:29000}, + { imageSrc: imageUrls.chickenImg, title: "BBQ 양념치킨+크림치즈볼+콜라1.25L", subtitle: "BBQ", amount:29000}, + { imageSrc: imageUrls.chickenImg, title: "BBQ 양념치킨+크림치즈볼+콜라1.25L", subtitle: "BBQ", amount:29000}, + { imageSrc: imageUrls.chickenImg, title: "BBQ 양념치킨+크림치즈볼+콜라1.25L", subtitle: "BBQ", amount:29000}, +]; + +const GridWrapper = styled.div` + padding: 20px; +`; + +const ThemeGoods: React.FC = () => { + return ( + + + {items.map((item, index) => ( + + ))} + + + ); + }; + +export default ThemeGoods; diff --git a/src/components/ThemeHeader/ThemeHeader.tsx b/src/components/ThemeHeader/ThemeHeader.tsx new file mode 100644 index 000000000..6ec714d92 --- /dev/null +++ b/src/components/ThemeHeader/ThemeHeader.tsx @@ -0,0 +1,130 @@ +/** @jsxImportSource @emotion/react */ +import { css } from '@emotion/react'; +import React from 'react'; + +interface ThemeData { + label: string; + title: string; + description: string; + backgroundColor: string; + themeKey: string; +} + +const headerStyles = (backgroundColor: string) => css` + background-color: ${backgroundColor}; + color: white; + padding: 30px; + margin-bottom: 20px; + .title { + font-size: 30px; + font-weight: bold; + } + .description { + font-size: 24px; + } + p { + margin: 20px 0; + } +`; + +const themeData: { [key: string]: ThemeData } = { + birthday: { + label: '생일', + title: '생일 선물 추천', + description: '특별한 생일을 위한 완벽한 선물들', + backgroundColor: '#f5a623', + themeKey: 'birthday', + }, + graduation: { + label: '졸업선물', + title: '졸업 선물 추천', + description: '졸업을 축하하는 최고의 선물', + backgroundColor: '#4a90e2', + themeKey: 'graduation', + }, + luxury: { + label: '명품선물', + title: '명품 선물 추천', + description: '럭셔리하고 고급스러운 선물들', + backgroundColor: '#bd10e0', + themeKey: 'luxury', + }, + wedding: { + label: '결혼/집들이', + title: '결혼 및 집들이 선물 추천', + description: '새로운 시작을 축하하는 최고의 선물들', + backgroundColor: '#ff6347', + themeKey: 'wedding', + }, + warm: { + label: '따뜻한선물', + title: '따뜻한 마음을 전하는 선물 추천', + description: '따뜻함을 전달하는 선물들', + backgroundColor: '#ffb6c1', + themeKey: 'warm', + }, + light: { + label: '가벼운선물', + title: '가벼운 선물 추천', + description: '부담 없이 주고받을 수 있는 선물들', + backgroundColor: '#87cefa', + themeKey: 'light', + }, + fan: { + label: '팬심저격', + title: '팬심을 저격하는 선물 추천', + description: '팬들을 위한 최고의 선물들', + backgroundColor: '#dda0dd', + themeKey: 'fan', + }, + exchange: { + label: '교환권', + title: '교환권 선물 추천', + description: '다양하게 사용 가능한 교환권 선물들', + backgroundColor: '#8fbc8f', + themeKey: 'exchange', + }, + health: { + label: '건강/비타민', + title: '건강을 위한 선물 추천', + description: '건강을 챙기는 선물들', + backgroundColor: '#3cb371', + themeKey: 'health', + }, + food: { + label: '과일/한우', + title: '과일 및 한우 선물 추천', + description: '맛있고 건강한 선물들', + backgroundColor: '#ffa07a', + themeKey: 'food', + }, + kids: { + label: '출산/키즈', + title: '출산 및 키즈 선물 추천', + description: '아이들을 위한 선물들', + backgroundColor: '#ff69b4', + themeKey: 'kids', + }, +}; + +interface HeaderProps { + themeKey: keyof typeof themeData; +} + +const ThemeHeader: React.FC = ({ themeKey }) => { + const theme = themeData[themeKey]; + + if (!theme) { + return
Invalid theme key: {themeKey}
; + } + + return ( +
+

{theme.label}

+

{theme.title}

+

{theme.description}

+
+ ); +}; + +export default ThemeHeader; diff --git a/src/components/common/FilterButton/FilterButton.tsx b/src/components/common/FilterButton/FilterButton.tsx new file mode 100644 index 000000000..8c776aa88 --- /dev/null +++ b/src/components/common/FilterButton/FilterButton.tsx @@ -0,0 +1,47 @@ +import styled from '@emotion/styled'; +import React from 'react'; + +const Button = styled.button<{ active: boolean }>` + background-color: ${(props) => (props.active ? '#4285f4' : '#e6effe')}; + color: #FFF; + text-weight: bold; + border: none; + border-radius: 25px; + padding: 20px; + cursor: pointer; + font-size: 20px; + width: 25px; + text-align: center; + transition: background-color 0.3s; + + &:focus { + outline: none; + } +`; + +const Text = styled.span<{ active: boolean }>` + display: block; + margin-top: 10px; + font-size: 25px; + color: ${(props) => (props.active ? '#4285f4' : '#000')}; + text-align: center; + transition: color 0.3s; +`; + +interface FilterButtonProps { + active: boolean; + onClick: () => void; + buttonText: string; + children: React.ReactNode; +} + +const FilterButton: React.FC = ({ active, onClick, buttonText, children }) => { + return( +
+ + {children} +
+ ); +}; + +export default FilterButton; diff --git a/src/contexts/Authcontext.tsx b/src/contexts/Authcontext.tsx new file mode 100644 index 000000000..37ace94ec --- /dev/null +++ b/src/contexts/Authcontext.tsx @@ -0,0 +1,46 @@ +import type { ReactNode} from 'react'; +import React, { createContext, useContext, useEffect,useState } from 'react'; + +interface AuthContextType { + isLoggedIn: boolean; + user: string | null; + login: (username: string) => void; + logout: () => void; +} + +const AuthContext = createContext(undefined); + +export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + const [user, setUser] = useState(null); + + useEffect(() => { + const storedUser = sessionStorage.getItem('authToken'); + if (storedUser) { + setUser(storedUser); + } + }, []); + + const login = (username: string) => { + sessionStorage.setItem('authToken', username); + setUser(username); + }; + + const logout = () => { + sessionStorage.removeItem('authToken'); + setUser(null); + }; + + return ( + + {children} + + ); +}; + +export const useAuth = () => { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +}; diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx new file mode 100644 index 000000000..e5b52c57a --- /dev/null +++ b/src/pages/Home.tsx @@ -0,0 +1,44 @@ +/** @jsxImportSource @emotion/react */ +import { css } from '@emotion/react'; +import React from 'react'; + +import GoodsCatygory from '@/components/GoodsCategory/GoodsCategory'; +import RankingHeader from '@/components/Ranking/RankingHeader'; +import SelectFriend from '@/components/SelectFriend/SelectFriend'; + +import { Button } from '../components/common/Button/index'; +import Filter from '../components/Ranking/Filter/Filter'; + + + +const buttonStyle = css` + display: flex; + flex-direction: column; + align-items: center; + outline: none; + width: 85%; + margin: 40px auto; + padding: 5px; + + p { + margin: 3px 0; + } +`; + + +const Home: React.FC = () => { + return ( +
+ + + + + +
+ ); + }; + +export default Home; \ No newline at end of file diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx new file mode 100644 index 000000000..4293a23dc --- /dev/null +++ b/src/pages/Login.tsx @@ -0,0 +1,85 @@ +import styled from "@emotion/styled"; +import React, { useState } from "react"; +import { useLocation,useNavigate} from "react-router-dom"; + +import { Button } from "@/components/common/Button/index"; +import { UnderlineTextField } from "@/components/common/Form/Input/UnderlineTextField"; +import { useAuth } from "@/contexts/Authcontext"; + +const Container = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100vh; +`; + +const LoginForm = styled.div` + display: flex; + flex-direction: column; + align-items: center; + border: 1px solid #c7c7c7; + padding: 30px; + width: 400px; + height: 250px; + margin-top: 20px; +`; + +const ButtonStyle = styled(Button)` + margin-top: 70px; + height: 50px; +`; + +const LoginTextStyle = styled.h1` + font-size: 30px; + font-weight: 600; + margin-bottom: 10px; +`; + +const UnderlineTextFieldStyle = styled(UnderlineTextField)` + margin-top: 20px; + width: 100%; +`; + +interface LoginProps { + onLogin: (username: string) => void; +} + +const Login: React.FC = ({}) => { + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const navigate = useNavigate(); + const location = useLocation(); + const { login } = useAuth(); + + const from = (location.state as { from?: Location })?.from?.pathname || "/"; + const handleLogin = () => { + if (username && password) { + login(username); + navigate(from, {replace: true}); + } else { + alert('아이디와 비밀번호를 입력해주세요.'); + } + } + return ( + + Kakao + + setUsername(e.target.value)} + placeholder='이름' + /> + setPassword(e.target.value)} + placeholder='비밀번호' + /> + 로그인 + + + ); +} + +export default Login; diff --git a/src/pages/MyAccount.tsx b/src/pages/MyAccount.tsx new file mode 100644 index 000000000..49d0dc533 --- /dev/null +++ b/src/pages/MyAccount.tsx @@ -0,0 +1,55 @@ +import styled from "@emotion/styled"; +import { useNavigate } from "react-router-dom"; + +import { Button } from "@/components/common/Button"; +import { useAuth } from "@/contexts/Authcontext"; + + +const Container = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 50vh; +`; + +const StyledText = styled.h1` + font-size: 30px; + font-weight: 600; + margin-bottom: 50px; +`; + +const StyledButton = styled(Button)` + margin-top: 40px; + height: 50px; + width: 200px; + background-color: #636061; + color: #FFF; + outline: none; + :hover { + background-color: #777475; + } +`; + +interface MyAccountProps { + username: string; + onLogout: () => void; +} + +const MyAccount: React.FC= () => { + const navigate = useNavigate(); + const { user: username, logout } = useAuth(); + + const handleLogout = () => { + logout(); + navigate('/'); + }; + return ( + + {username}님 안녕하세요! + 로그아웃 + + ); +} + +export default MyAccount; \ No newline at end of file diff --git a/src/pages/Theme.tsx b/src/pages/Theme.tsx new file mode 100644 index 000000000..e453391ad --- /dev/null +++ b/src/pages/Theme.tsx @@ -0,0 +1,22 @@ + +import { useParams } from 'react-router-dom'; + +import ThemeGoods from '@/components/ThemeGoods/ThemeGoods'; +import ThemeHeader from '@/components/ThemeHeader/ThemeHeader'; + +const Theme = () => { + const { themeKey } = useParams<{ themeKey: string }>(); + + if (!themeKey) { + return
Invalid theme key
; + } + + return ( +
+ + +
+ ); +} + +export default Theme; diff --git a/src/routes/index.tsx b/src/routes/index.tsx new file mode 100644 index 000000000..b3f2d1557 --- /dev/null +++ b/src/routes/index.tsx @@ -0,0 +1,46 @@ + +import { useState } from 'react'; +import { BrowserRouter as Router, Route,Routes } from 'react-router-dom'; + +import Footer from '@/components/Footer/Footer'; +import Header from '@/components/Header/Header'; +import PrivateRoute from '@/components/PrivateRoute/PrivateRoute'; +import { AuthProvider} from '@/contexts/Authcontext'; + +import Home from '../pages/Home'; +import Login from '../pages/Login'; +import MyAccount from '../pages/MyAccount'; +import Theme from '../pages/Theme'; + +const AppRoutes = () => { + + const[isLoggedIn, setIsLoggedIn] = useState(false); + const [user, setuser] = useState(''); + + const handleLogin = (username:string) => { + setuser(username); + setIsLoggedIn(true); + }; + + const handleLogout = () => { + setuser(''); + setIsLoggedIn(false); + }; + + return ( + + +
+ + } /> + } /> + } /> + } /> + +