diff --git a/package-lock.json b/package-lock.json index 1583290..ab82091 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,9 +13,13 @@ "@iconify/react": "^5.2.0", "@mui/icons-material": "^6.4.1", "@mui/material": "^6.4.1", + "@types/react-slick": "^0.23.13", "axios": "^1.7.9", + "chart.js": "^4.4.7", + "pretendard": "^1.3.9", "lucide-react": "^0.474.0", "react": "^18.3.1", + "react-chartjs-2": "^5.3.0", "react-dom": "^18.3.1", "react-router-dom": "^7.1.1", "react-slick": "^0.30.3", @@ -26,6 +30,7 @@ }, "devDependencies": { "@eslint/js": "^9.17.0", + "@types/chart.js": "^2.9.41", "@svgr/rollup": "^8.1.0", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", @@ -2213,6 +2218,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@mui/core-downloads-tracker": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.1.tgz", @@ -2888,6 +2899,16 @@ "@swc/counter": "^0.1.3" } }, + "node_modules/@types/chart.js": { + "version": "2.9.41", + "resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.41.tgz", + "integrity": "sha512-3dvkDvueckY83UyUXtJMalYoH6faOLkWQoaTlJgB4Djde3oORmNP0Jw85HtzTuXyliUHcdp704s0mZFQKio/KQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "moment": "^2.10.2" + } + }, "node_modules/@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", @@ -2950,7 +2971,10 @@ "version": "0.23.13", "resolved": "https://registry.npmjs.org/@types/react-slick/-/react-slick-0.23.13.tgz", "integrity": "sha512-bNZfDhe/L8t5OQzIyhrRhBr/61pfBcWaYJoq6UDqFtv5LMwfg4NsVDD2J8N01JqdAdxLjOt66OZEp6PX+dGs/A==", + + "dev": true, + "license": "MIT", "dependencies": { "@types/react": "*" @@ -3463,6 +3487,21 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + + "node_modules/chart.js": { + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.7.tgz", + "integrity": "sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + + "node_modules/classnames": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", @@ -5391,6 +5430,7 @@ "string-convert": "^0.2.0" } }, + "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -5468,7 +5508,9 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -5567,6 +5609,16 @@ "node": "*" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5936,6 +5988,12 @@ "node": ">= 0.8.0" } }, + "node_modules/pretendard": { + "version": "1.3.9", + "resolved": "https://registry.npmjs.org/pretendard/-/pretendard-1.3.9.tgz", + "integrity": "sha512-PaQAADyLY5v4kYFwkpSJHbSSYIkiriY/1xXw75TKoZ9UQQqeU+tvP05yTdZAWibiIYoo8ZKtRv8PM7w0IaywSw==", + "license": "OFL-1.1" + }, "node_modules/prettier": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", @@ -6004,6 +6062,16 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz", + "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -6174,6 +6242,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT" + }, + "node_modules/regexpu-core": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", diff --git a/package.json b/package.json index 1983810..9f03202 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,13 @@ "@iconify/react": "^5.2.0", "@mui/icons-material": "^6.4.1", "@mui/material": "^6.4.1", + "@types/react-slick": "^0.23.13", "axios": "^1.7.9", + "chart.js": "^4.4.7", + "pretendard": "^1.3.9", "lucide-react": "^0.474.0", "react": "^18.3.1", + "react-chartjs-2": "^5.3.0", "react-dom": "^18.3.1", "react-router-dom": "^7.1.1", "react-slick": "^0.30.3", @@ -28,6 +32,7 @@ }, "devDependencies": { "@eslint/js": "^9.17.0", + "@types/chart.js": "^2.9.41", "@svgr/rollup": "^8.1.0", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", diff --git a/src/App.tsx b/src/App.tsx index 6a62217..6a0f324 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,10 +1,12 @@ import { ModalContextProvider } from './components/Modal/context/ModalContext'; import Router from './routes/router'; +//import { UserProvider } from './components/RaffleDetail/context/UserContext'; // import SearchBox from './components/SearchBox'; function App() { return ( <> + {/* */} diff --git a/src/assets/ProductCard/like.svg b/src/assets/ProductCard/like.svg new file mode 100644 index 0000000..a5f2313 --- /dev/null +++ b/src/assets/ProductCard/like.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/ProductCard/ticket.svg b/src/assets/ProductCard/ticket.svg new file mode 100644 index 0000000..10c9300 --- /dev/null +++ b/src/assets/ProductCard/ticket.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/ProductCard/unlike.svg b/src/assets/ProductCard/unlike.svg new file mode 100644 index 0000000..af67f7d --- /dev/null +++ b/src/assets/ProductCard/unlike.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/homePage/clock.svg b/src/assets/homePage/clock.svg new file mode 100644 index 0000000..d02d89b --- /dev/null +++ b/src/assets/homePage/clock.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/homePage/follow.svg b/src/assets/homePage/follow.svg new file mode 100644 index 0000000..930b720 --- /dev/null +++ b/src/assets/homePage/follow.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/homePage/like.svg b/src/assets/homePage/like.svg new file mode 100644 index 0000000..ac34296 --- /dev/null +++ b/src/assets/homePage/like.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/homePage/moreList.svg b/src/assets/homePage/moreList.svg new file mode 100644 index 0000000..4567267 --- /dev/null +++ b/src/assets/homePage/moreList.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/raffleDetail/icon-like.svg b/src/assets/raffleDetail/icon-like.svg new file mode 100644 index 0000000..4dadcc0 --- /dev/null +++ b/src/assets/raffleDetail/icon-like.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/raffleDetail/icon-mark.svg b/src/assets/raffleDetail/icon-mark.svg new file mode 100644 index 0000000..a4f03d9 --- /dev/null +++ b/src/assets/raffleDetail/icon-mark.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/assets/raffleDetail/icon-ticket.svg b/src/assets/raffleDetail/icon-ticket.svg new file mode 100644 index 0000000..55b1d04 --- /dev/null +++ b/src/assets/raffleDetail/icon-ticket.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/raffleDetail/icon-unlike.svg b/src/assets/raffleDetail/icon-unlike.svg new file mode 100644 index 0000000..74d99e7 --- /dev/null +++ b/src/assets/raffleDetail/icon-unlike.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/smallProductCard/smallLike.svg b/src/assets/smallProductCard/smallLike.svg new file mode 100644 index 0000000..276eba1 --- /dev/null +++ b/src/assets/smallProductCard/smallLike.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/smallProductCard/smallTicket.svg b/src/assets/smallProductCard/smallTicket.svg new file mode 100644 index 0000000..004c905 --- /dev/null +++ b/src/assets/smallProductCard/smallTicket.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/smallProductCard/smallUnlike.svg b/src/assets/smallProductCard/smallUnlike.svg new file mode 100644 index 0000000..e5e9c3d --- /dev/null +++ b/src/assets/smallProductCard/smallUnlike.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Modal/modals/DrawModal.tsx b/src/components/Modal/modals/DrawModal.tsx index 75e0c86..92a8b0e 100644 --- a/src/components/Modal/modals/DrawModal.tsx +++ b/src/components/Modal/modals/DrawModal.tsx @@ -1,30 +1,46 @@ import React from 'react'; import Modal from '../Modal'; import styled from 'styled-components'; -import ticket from '../../../assets/ticket.svg'; +import icTicket from '../../../assets/ticket.svg'; import { useModalContext } from '../context/ModalContext'; import DrawOkModal from './DrawOkModal'; interface ModalProps { onClose: () => void; + handleRoleChange: () => void; + countParticipant: () => void; + images: string[]; + name: string; + ticket: number; } -const DrawModal: React.FC = ({ onClose }) => { +const DrawModal: React.FC = ({ + onClose, + handleRoleChange, + countParticipant, + images, + name, + ticket, +}) => { + const itemImg = images[0]; + const { openModal } = useModalContext(); const handleSubmit = () => { openModal(({ onClose }) => ); + handleRoleChange(); + countParticipant(); }; return ( - - 로지텍 무소음 마우스 + + {name} 해당 상품에 응모하시겠습니까? - - 3 + + {ticket} 응모 후 취소할 수 없습니다. @@ -90,7 +106,7 @@ const Title = styled.div` font-weight: 700; `; -const Box = styled.div` +const Box = styled.img` width: 190px; height: 190px; flex-shrink: 0; diff --git a/src/components/Modal/modals/DrawOkModal.tsx b/src/components/Modal/modals/DrawOkModal.tsx index 1c00093..7c4e609 100644 --- a/src/components/Modal/modals/DrawOkModal.tsx +++ b/src/components/Modal/modals/DrawOkModal.tsx @@ -17,7 +17,7 @@ const DrawOkModal: React.FC = ({ onClose }) => { 0월 0일 00시 메일로 결과가 전송됩니다. - + ); diff --git a/src/components/ProductCard.tsx b/src/components/ProductCard.tsx new file mode 100644 index 0000000..cdd5317 --- /dev/null +++ b/src/components/ProductCard.tsx @@ -0,0 +1,194 @@ +import React, { useState } from 'react'; +import styled from 'styled-components'; +import ticket from '../assets/ProductCard/ticket.svg'; +import like from '../assets/ProductCard/like.svg'; +import unlike from '../assets/productCard/unlike.svg'; + +const ProductCard = () => { + const [isLiked, setIsLiked] = useState(false); + const toggleLike = () => { + setIsLiked((prevState) => !prevState); + }; + + return ( + + + 응모 마감 + 마감임박 + + {isLiked + + + + + 다영언니의 텀블러 + 5명 응모중 + + + + + ticket 3 + + 7시간 35분 27초뒤 마감 + + + + ); +}; + +export default ProductCard; + +const Wrapper = styled.div` + width: 228px; + height: 314px; + flex-shrink: 0; + border-radius: 2px; + background: #fff; +`; + +const ImageContainer = styled.div` + width: 228px; + height: 227px; + flex-shrink: 0; + border-radius: 5px; + background: #e4e4e4; + position: relative; + margin-top: 6px; +`; + +const RaffleClosingBox = styled.div` + width: 143.316px; + height: 47.272px; + transform: rotate(0.421deg); + + flex-shrink: 0; + border-radius: 4px; + border: 2px solid #c908ff; + + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + display: flex; + justify-content: center; + align-items: center; + + color: #c908ff; + text-align: center; + font-family: Pretendard; + font-size: 20px; + font-style: normal; + font-weight: 600; + line-height: 18px; +`; + +const TextBox = styled.div` + width: 78.929px; + height: 26px; + flex-shrink: 0; + border-radius: 42px; + background: rgba(201, 8, 255, 0.2); + + position: absolute; + top: 188px; + right: 135.07px; + display: flex; + justify-content: center; + align-items: center; + + color: #c908ff; + text-align: center; + font-family: Pretendard; + font-size: 11px; + font-style: normal; + font-weight: 600; + line-height: 18px; /* 163.636% */ +`; + +const LikeBox = styled.div` + width: 26px; + height: 26px; + flex-shrink: 0; + + position: absolute; + top: 188px; + right: 16px; + + /* cursor: pointer; + user-select: none; */ +`; + +const InfoContainer = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + + padding: 0px 5.6px 16px 5px; +`; + +const TitleContainer = styled.div` + display: flex; + height: 21px; + margin-top: 14px; + margin-bottom: 10px; + justify-content: space-between; + align-items: center; + flex-shrink: 0; +`; + +const TitleBox = styled.div` + display: flex; + align-items: center; + + color: #000; + font-family: Pretendard; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 112.5% */ +`; + +const ParticipantsBox = styled.div` + display: flex; + align-items: center; + + color: #c908ff; + text-align: right; + font-family: Pretendard; + font-size: 12px; + font-style: normal; + font-weight: 600; + line-height: 18px; /* 150% */ +`; + +const ContentContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; +`; + +const TicketBox = styled.div` + display: flex; + gap: 11px; + + color: #000; + font-family: Pretendard; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 150%; /* 24px */ +`; + +const TimeBox = styled.div` + color: #8f8e94; + text-align: right; + font-family: Pretendard; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 130%; /* 18.2px */ +`; diff --git a/src/components/SmallProductCard.tsx b/src/components/SmallProductCard.tsx new file mode 100644 index 0000000..9ea39b3 --- /dev/null +++ b/src/components/SmallProductCard.tsx @@ -0,0 +1,134 @@ +import React, { useState } from 'react'; +import styled from 'styled-components'; +import smallTicket from '../assets/smallProductCard/smallTicket.svg'; +import smallUnlike from '../assets/smallProductCard/smallUnlike.svg'; +import smallLike from '../assets/smallProductCard/smallLike.svg'; + +const SmallProductCard = () => { + const [isLiked, setIsLiked] = useState(false); + const toggleLike = () => { + setIsLiked((prevState) => !prevState); + }; + return ( + + + 마감임박 + + {isLiked + + + + 다영언니의 텀블러 + + + smallTicket 3 + + 7시간 35분 27초뒤 마감 + + + + ); +}; + +export default SmallProductCard; + +const Wrapper = styled.div` + width: 192px; + height: 261px; + background-color: #ffffff; +`; + +const ImageContainer = styled.div` + width: 192px; + height: 192px; + flex-shrink: 0; + border-radius: 3px; + background-color: #eaeaea; + position: relative; + margin-top: 14px; +`; + +const TextBox = styled.div` + width: 71px; + height: 23px; + flex-shrink: 0; + border-radius: 42px; + background: rgba(201, 8, 255, 0.2); + + position: absolute; + top: 162px; + right: 114px; + display: flex; + justify-content: center; + align-items: center; + + color: #c908ff; + text-align: center; + font-family: Pretendard; + font-size: 11px; + font-style: normal; + font-weight: 600; + line-height: 18px; /* 163.636% */ +`; + +const LikeBox = styled.div` + width: 21px; + height: 21px; + flex-shrink: 0; + + position: absolute; + top: 163px; + right: 9px; + + cursor: pointer; +`; + +const Layout = styled.div` + padding: 0 9px 5px 7px; +`; + +const TitleContainer = styled.div` + display: inline-flex; + height: 31px; + padding: 5px 0px 4px 0px; + justify-content: center; + align-items: center; + flex-shrink: 0; + + color: #000; + font-family: Pretendard; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 18px; +`; + +const InfoContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; +`; + +const TicketBox = styled.div` + display: flex; + gap: 8.61px; + + color: #000; + font-family: Pretendard; + font-size: 14px; + font-style: normal; + font-weight: 600; +`; + +const TimeBox = styled.div` + color: #8f8e94; + text-align: right; + font-family: Pretendard; + font-size: 11px; + font-style: normal; + font-weight: 400; +`; diff --git a/src/layout/RootLayout.tsx b/src/layout/RootLayout.tsx index 940453e..16e10da 100644 --- a/src/layout/RootLayout.tsx +++ b/src/layout/RootLayout.tsx @@ -1,24 +1,24 @@ -import styled from "styled-components"; -import Header from "../components/Header"; -import { Outlet } from "react-router-dom"; -import HeaderIconMenu from "../components/HeaderIconMenu"; +import styled from 'styled-components'; +import Header from '../components/Header'; +import { Outlet } from 'react-router-dom'; +import HeaderIconMenu from '../components/HeaderIconMenu'; const RootLayout = () => { - return ( - -
- {/* */} - - - ); + return ( + +
+ {/* */} + + + ); }; export default RootLayout; const Wrapper = styled.div` - // min-width: 1440px; - display: flex; - flex-direction: column; - align-items: center; - // justify-content: center; -` + // min-width: 1440px; + display: flex; + flex-direction: column; + align-items: center; + // justify-content: center; +`; diff --git a/src/mocks/RaffleData.tsx b/src/mocks/RaffleData.tsx new file mode 100644 index 0000000..5dc2d47 --- /dev/null +++ b/src/mocks/RaffleData.tsx @@ -0,0 +1,72 @@ +const getRaffleStatus = (closeTime: string): 'ongoing' | 'ended' => { + return new Date(closeTime).getTime() > Date.now() ? 'ongoing' : 'ended'; +}; + +export const raffleData = [ + { + id: 1, + marketId: 'm1', + images: ['1', '2', '3', '4'], // ✅ image -> images (리스트로 변경) + name: '한정판 스니커즈', + ticket: 3, + category: '신발', + openTime: '2025-01-29T10:00:00Z', + closeTime: '2025-01-30T22:00:00Z', + description: + '최고급 가죽과 세련된 디자인을 자랑하는 한정판 스니커즈입니다.', + participant: 7, + atLeastParticipant: 5, + view: 25, + like: 4, + raffleStatus: 'ended', + }, + { + id: 2, + marketId: 'm2', + images: ['1', '2', '3', '4'], // ✅ 변경 + name: '럭셔리 시계', + ticket: 5, + category: '악세서리', + openTime: '2025-02-03T09:00:00Z', + closeTime: '2025-02-07T23:59:59Z', + description: '고급스러운 디자인과 정교한 무브먼트가 특징인 시계입니다.', + participant: 9, + atLeastParticipant: 6, + view: 47, + like: 10, + raffleStatus: 'ongoing', + }, + { + id: 3, + marketId: 'm3', + images: ['1', '2', '3', '4'], // ✅ 변경 + name: '프리미엄 백팩', + ticket: 2, + category: '가방', + openTime: '2025-02-02T12:00:00Z', + closeTime: '2025-02-06T20:00:00Z', + description: + '편안한 착용감과 넉넉한 수납공간을 제공하는 프리미엄 백팩입니다.', + participant: 6, + atLeastParticipant: 4, + view: 53, + like: 2, + raffleStatus: 'unopen', + }, + { + id: 4, + marketId: 'm4', + images: ['1', '2', '3', '4'], // ✅ 변경 + name: '스마트폰', + ticket: 4, + category: '전자기기', + openTime: '2025-02-04T08:00:00Z', + closeTime: '2025-02-08T21:00:00Z', + description: '최신 기술이 적용된 고성능 스마트폰입니다.', + participant: 8, + atLeastParticipant: 7, + view: 92, + like: 15, + raffleStatus: getRaffleStatus('2025-02-08T21:00:00Z'), + }, +]; diff --git a/src/pages/homepage/components/AdBanner.tsx b/src/pages/homepage/components/AdBanner.tsx new file mode 100644 index 0000000..df13360 --- /dev/null +++ b/src/pages/homepage/components/AdBanner.tsx @@ -0,0 +1,91 @@ +import React from 'react'; +import styled from 'styled-components'; +import Slider from 'react-slick'; +import 'slick-carousel/slick/slick.css'; +import 'slick-carousel/slick/slick-theme.css'; + +function AdBanner() { + const settings = { + dots: true, + infinite: true, + slidesToShow: 3, + slidesToScroll: 1, + autoplay: true, + speed: 2000, + autoplaySpeed: 5000, + centerMode: true, + centerPadding: '46px', + cssEase: 'ease', + }; + + return ( + + +
+ 1 +
+
+ 2 +
+
+ 3 +
+
+
+ ); +} + +const Wrapper = styled.div` + width: 100%; + height: 396px; + margin: 39px auto 61px auto; + box-sizing: content-box; + overflow: hidden; + + /* .slick-initialized { + width: 100%; + } */ + + /* .slick-track { + display: flex; + align-items: center; + } */ + + .slick-slide { + display: flex; + justify-content: center; + align-items: center; + } + + .slick-dots { + bottom: -27px; + display: flex; + justify-content: center; + align-items: center; + font-size: 8px; + + .slick-active button::before { + color: #c908ff; /* 선택된 점의 색상 */ + font-size: 8px; + } + + button::before { + color: rgba(201, 8, 255, 0.2); /* 선택되지 않은 점의 색상 */ + font-size: 8px; + } + } +`; + +const AdBox = styled.a` + width: 825px; + height: 369px; + flex-shrink: 0; + margin: 0 23px; + border-radius: 31px; + background: #e7e7e7; + display: flex; + justify-content: center; + align-items: center; +`; + +export default AdBanner; diff --git a/src/pages/homepage/components/ImminentDeadline.tsx b/src/pages/homepage/components/ImminentDeadline.tsx new file mode 100644 index 0000000..e67a1a4 --- /dev/null +++ b/src/pages/homepage/components/ImminentDeadline.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import styled from 'styled-components'; +import clock from '../../../assets/homePage/clock.svg'; +import moreList from '../../../assets/homePage/moreList.svg'; +import SmallProductCard from '../../../components/SmallProductCard'; +import { useNavigate } from 'react-router-dom'; + +const ImminentDeadline = () => { + const navigate = useNavigate(); + return ( + + + + clock 마감임박 + + navigate('/')}> + 마감임박 상품 더보기 + moreList + + + + + + + + + + + + ); +}; + +const Wrapper = styled.div` + width: 100%; + height: 306px; + margin-bottom: 65px; +`; + +const HeaderContainer = styled.div` + display: flex; + height: 38px; + margin-bottom: 11px; + flex-direction: row; + justify-content: space-between; + flex-shrink: 0; + + align-items: center; +`; + +const TextBox = styled.div` + display: flex; + gap: 23px; + + color: #000; + font-family: Inter; + font-size: 22px; + font-style: normal; + font-weight: 600; + line-height: normal; +`; + +const MoreListBox = styled.a` + width: 220px; + height: 34px; + display: flex; + justify-content: flex-end; + align-items: center; + display: flex; + color: #8f8e94; + text-align: center; + font-family: Pretendard; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 36.832px; /* 230.199% */ + text-decoration-line: underline; + text-decoration-style: solid; + text-decoration-skip-ink: auto; + text-decoration-thickness: auto; + text-underline-offset: auto; + text-underline-position: from-font; + + img { + width: 10px; + height: 17px; + margin-left: 35px; + } + + cursor: pointer; +`; + +const ProductContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; +`; +export default ImminentDeadline; diff --git a/src/pages/homepage/components/MyFollow.tsx b/src/pages/homepage/components/MyFollow.tsx new file mode 100644 index 0000000..3e0b650 --- /dev/null +++ b/src/pages/homepage/components/MyFollow.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import styled from 'styled-components'; +import follow from '../../../assets/homePage/follow.svg'; +import moreList from '../../../assets/homePage/moreList.svg'; +import SmallProductCard from '../../../components/SmallProductCard'; +import { useNavigate } from 'react-router-dom'; + +const MyFollow = () => { + const navigate = useNavigate(); + return ( + + + + follow 팔로우한 상점의 래플 + + navigate('/')}> + 팔로우한 상점 래플 더보기 + moreList + + + + + + + + + + + + ); +}; + +const Wrapper = styled.div` + width: 100%; + height: 306px; + margin-bottom: 65px; +`; + +const HeaderContainer = styled.div` + display: flex; + height: 38px; + margin-bottom: 11px; + flex-direction: row; + justify-content: space-between; + flex-shrink: 0; + + align-items: center; +`; + +const TextBox = styled.div` + display: flex; + gap: 23px; + + color: #000; + font-family: Inter; + font-size: 22px; + font-style: normal; + font-weight: 600; + line-height: normal; +`; + +const MoreListBox = styled.a` + width: 250px; + height: 34px; + display: flex; + justify-content: flex-end; + align-items: center; + display: flex; + color: #8f8e94; + text-align: center; + font-family: Pretendard; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 36.832px; /* 230.199% */ + text-decoration-line: underline; + text-decoration-style: solid; + text-decoration-skip-ink: auto; + text-decoration-thickness: auto; + text-underline-offset: auto; + text-underline-position: from-font; + + img { + width: 10px; + height: 17px; + margin-left: 35px; + } + + cursor: pointer; +`; + +const ProductContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; +`; +export default MyFollow; diff --git a/src/pages/homepage/components/MyLike.tsx b/src/pages/homepage/components/MyLike.tsx new file mode 100644 index 0000000..41ee17b --- /dev/null +++ b/src/pages/homepage/components/MyLike.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import styled from 'styled-components'; +import like from '../../../assets/homePage/like.svg'; +import moreList from '../../../assets/homePage/moreList.svg'; +import SmallProductCard from '../../../components/SmallProductCard'; +import { useNavigate } from 'react-router-dom'; + +const Mylike = () => { + const navigate = useNavigate(); + return ( + + + + like 내가 찜한 래플 + + navigate('/')}> + 내가 찜한 래플 더보기 + moreList + + + + + + + + + + + + ); +}; + +const Wrapper = styled.div` + width: 100%; + height: 306px; + margin-bottom: 65px; +`; + +const HeaderContainer = styled.div` + display: flex; + height: 38px; + margin-bottom: 11px; + flex-direction: row; + justify-content: space-between; + flex-shrink: 0; + + align-items: center; +`; + +const TextBox = styled.div` + display: flex; + gap: 23px; + + color: #000; + font-family: Inter; + font-size: 22px; + font-style: normal; + font-weight: 600; + line-height: normal; +`; + +const MoreListBox = styled.a` + width: 220px; + height: 34px; + display: flex; + justify-content: flex-end; + align-items: center; + display: flex; + color: #8f8e94; + text-align: center; + font-family: Pretendard; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 36.832px; /* 230.199% */ + text-decoration-line: underline; + text-decoration-style: solid; + text-decoration-skip-ink: auto; + text-decoration-thickness: auto; + text-underline-offset: auto; + text-underline-position: from-font; + + img { + width: 10px; + height: 17px; + margin-left: 35px; + } + + cursor: pointer; +`; + +const ProductContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; +`; +export default Mylike; diff --git a/src/pages/homepage/homePage.tsx b/src/pages/homepage/homePage.tsx new file mode 100644 index 0000000..02a5990 --- /dev/null +++ b/src/pages/homepage/homePage.tsx @@ -0,0 +1,126 @@ +import React from 'react'; +import styled from 'styled-components'; +import AdBanner from './components/AdBanner'; +import ImminentDeadline from './components/ImminentDeadline'; +import MyLike from './components/MyLike'; +import MyFollow from './components/MyFollow'; +import moreList from '../../assets/homePage/moreList.svg'; +import ProductCard from '../../components/ProductCard'; +import { useNavigate } from 'react-router-dom'; + +const HomePage = () => { + const navigate = useNavigate(); + return ( + <> + + + + + + + 래플 둘러보기 + navigate('/')}> + 마감임박 상품 더보기 + moreList + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + width: 1080px; + margin: 0 auto; + align-items: center; +`; + +const LookAroundContainer = styled.div` + position: relative; + width: 100%; + display: flex; + align-items: center; +`; + +const LookAroundBox = styled.p` + position: absolute; + left: 50%; + transform: translateX(-50%); + color: #000; + text-align: center; + font-family: Pretendard; + font-size: 24px; + font-style: normal; + font-weight: 600; + line-height: normal; +`; + +const MoreListBox = styled.a` + width: 250px; + height: 34px; + display: flex; + justify-content: flex-end; + align-items: center; + display: flex; + color: #8f8e94; + text-align: center; + font-family: Pretendard; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 36.832px; /* 230.199% */ + text-decoration-line: underline; + text-decoration-style: solid; + text-decoration-skip-ink: auto; + text-decoration-thickness: auto; + text-underline-offset: auto; + text-underline-position: from-font; + + img { + width: 10px; + height: 17px; + margin-left: 35px; + } + + cursor: pointer; +`; + +const Horizon = styled.hr` + width: 100%; + border-top: 1px solid #8f8e94; + margin-top: 42px; + margin-bottom: 46px; +`; + +const ProductRow = styled.div` + width: 100%; + margin-bottom: 44px; + display: flex; + flex-direction: row; + justify-content: space-between; +`; + +export default HomePage; diff --git a/src/pages/raffleDetail/RaffleDetailPage.tsx b/src/pages/raffleDetail/RaffleDetailPage.tsx new file mode 100644 index 0000000..86d610d --- /dev/null +++ b/src/pages/raffleDetail/RaffleDetailPage.tsx @@ -0,0 +1,64 @@ +import React, { useEffect, useState } from 'react'; + +import styled from 'styled-components'; +import Item from './components/Item'; +import Market from './components/Market'; +import Probability from './components/Probability'; +import { raffleData } from '../../mocks/RaffleData'; + +type Role = 'p' | 'np' | 'h'; +type Winner = 'y' | 'n' | 'idk'; +type Result = 'success' | 'less' | 'failed'; + +const RaffleDetailPage = () => { + const [role, setRole] = useState('h'); // 기본값: 미참여자 + const [winner, setWinner] = useState('n'); + const [result, setResult] = useState('success'); + const raffle = raffleData[0]; // 0(ended) 1(ongoing) 2(unopen) + const [participant, setParticipant] = useState(raffle.participant); + const countParticipant = () => { + setParticipant((prev) => prev + 1); + }; + + return ( + + + + + + + + ); +}; + +export default RaffleDetailPage; + +const Wrapper = styled.div` + width: 1080px; + display: flex; + align-items: center; + flex-direction: column; + padding-top: 63px; + margin: 0 auto; +`; + +const MoreInfoLayout = styled.div` + display: flex; + align-items: center; + flex-direction: row; + gap: 157px; +`; diff --git a/src/pages/raffleDetail/components/DonutChart.tsx b/src/pages/raffleDetail/components/DonutChart.tsx new file mode 100644 index 0000000..c0bac53 --- /dev/null +++ b/src/pages/raffleDetail/components/DonutChart.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import styled from 'styled-components'; +import { Doughnut } from 'react-chartjs-2'; +import { + Chart as ChartJS, + ArcElement, + Tooltip, + Legend, + ChartOptions, + ChartData, +} from 'chart.js'; + +ChartJS.register(ArcElement, Tooltip, Legend); + +interface DonutChartProps { + participant: number; +} + +const DonutChart: React.FC = ({ participant }) => { + const winningProbability = (1 / participant) * 100; + + const data: ChartData<'doughnut'> = { + labels: [], + datasets: [ + { + data: [winningProbability, 100 - winningProbability], // 퍼센트 값 + backgroundColor: ['#C908FF', '#E4E4E4'], // 보라색 & 회색 + }, + ], + }; + + const options: ChartOptions<'doughnut'> = { + responsive: true, + maintainAspectRatio: false, + cutout: '75%', // 안쪽 원 크기 조절 + plugins: { + tooltip: { + callbacks: { + label: (tooltipItem) => { + const value = tooltipItem.raw as number; // 👈 `unknown`을 `number`로 변환 + return `${tooltipItem.label}: ${value.toFixed(2)}%`; + }, + }, + }, + }, + }; + + return ( + + + {winningProbability.toFixed(0)}% + + ); +}; + +const Wrapper = styled.div` + display: flex; + width: 167px; + height: 167px; + position: relative; +`; + +const PercentageBox = styled.div` + display: flex; + width: 122px; + height: 65px; + flex-direction: column; + justify-content: center; + + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + + color: #000; + text-align: center; + font-family: Pretendard; + font-size: 36px; + font-style: normal; + font-weight: 700; + line-height: 150%; /* 54px */ +`; + +export default DonutChart; diff --git a/src/pages/raffleDetail/components/DonutText.tsx b/src/pages/raffleDetail/components/DonutText.tsx new file mode 100644 index 0000000..e865197 --- /dev/null +++ b/src/pages/raffleDetail/components/DonutText.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import styled from 'styled-components'; + +type DonutTextProps = { + text: string; +}; + +const DonutText = ({ text }: DonutTextProps) => { + return ( + + + {/* 바깥 원 */} + + {/* 텍스트 */} + + {text} + + + + ); +}; + +export default DonutText; + +const Wrapper = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: 166.396px; + height: 166.396px; +`; + +// SVG 컨테이너 +const SVG = styled.svg` + width: 100%; + height: 100%; +`; + +// 원 스타일 +const Circle = styled.circle` + stroke: #e4e4e4; + stroke-width: 17; + fill: none; +`; + +// 텍스트 스타일 +const StyledText = styled.text` + fill: #8f8e94; + font-family: Pretendard; + font-size: 20px; + font-style: normal; + font-weight: 700; + line-height: 150%; + text-align: center; +`; diff --git a/src/pages/raffleDetail/components/ImgSlider.tsx b/src/pages/raffleDetail/components/ImgSlider.tsx new file mode 100644 index 0000000..d7d0153 --- /dev/null +++ b/src/pages/raffleDetail/components/ImgSlider.tsx @@ -0,0 +1,153 @@ +import React, { useState } from 'react'; +import { ReactNode } from 'react'; +import styled from 'styled-components'; +import Slider from 'react-slick'; +import 'slick-carousel/slick/slick.css'; +import 'slick-carousel/slick/slick-theme.css'; +// import { ReactComponent as Next } from "../../assets/next.svg"; +// import { ReactComponent as Prev } from "../../assets/prev.svg"; + +interface ItemProps { + images: string[]; + name: string; + children?: ReactNode; +} + +function ImgSlider({ images, name, children }: ItemProps) { + const [currentSlide, setCurrentSlide] = useState(0); + const totalSlides = images.length; + const lastSlide = totalSlides - 1; + + const getActiveDot = () => { + if (currentSlide === 0) return 0; // 첫 번째 dot 선택 + if (currentSlide === lastSlide) return 2; // 마지막 dot 선택 + return 1; // 중간 dot 선택 + }; + + const settings = { + rows: 1, + dots: true, + infinite: true, + speed: 500, + slidesToShow: 1, + slidesToScroll: 1, + arrows: true, + nextArrow:
, + prevArrow: , + beforeChange: (_: number, next: number) => setCurrentSlide(next), + appendDots: () => ( + + {[0, 1, 2].map((dotIndex) => ( +
  • + ))} + + ), + customPaging: () =>