├── .gitignore ├── .prettierrc ├── index.html ├── jsconfig.json ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── public └── vite.svg ├── src ├── App.jsx ├── apis │ ├── authService.js │ ├── axiosClient.js │ ├── cartService.js │ ├── orderService.js │ └── productsService.js ├── assets │ ├── icons │ │ ├── images │ │ │ └── Logo-retina.png │ │ └── svgs │ │ │ ├── boxIcon.svg │ │ │ ├── cartIcon.svg │ │ │ ├── chatIcon.svg │ │ │ ├── debitCardIcon.svg │ │ │ ├── fbIcon.svg │ │ │ ├── heart.svg │ │ │ ├── insIcon.svg │ │ │ ├── reloadIcon.svg │ │ │ ├── truckIcon.svg │ │ │ └── ytbIcon.svg │ ├── images │ │ └── Banner-Ecommerse.jpeg │ ├── react.svg │ └── styles │ │ ├── _global.module.scss │ │ ├── _mixin.module.scss │ │ ├── _variable.module.scss │ │ └── main.scss ├── components │ ├── AccordionMenu │ │ ├── index.jsx │ │ └── styles.module.scss │ ├── AdvanceHeadling │ │ ├── AdvanceHeadling.jsx │ │ └── styles.module.scss │ ├── Banner │ │ ├── Banner.jsx │ │ └── styles.module.scss │ ├── Blog │ │ └── Blog.jsx │ ├── Button │ │ ├── Button.jsx │ │ └── styles.module.scss │ ├── ContentSideBar │ │ ├── Cart │ │ │ ├── Cart.jsx │ │ │ └── styles.module.scss │ │ ├── Compare │ │ │ ├── Compare.jsx │ │ │ └── styles.module.scss │ │ ├── DetailProduct │ │ │ ├── DetailProduct.jsx │ │ │ └── styles.module.scss │ │ ├── Login │ │ │ ├── Login.jsx │ │ │ └── styles.module.scss │ │ ├── WishList │ │ │ ├── WishList.jsx │ │ │ └── styles.module.scss │ │ └── components │ │ │ ├── HeaderSidebar │ │ │ ├── HeaderSideBar.jsx │ │ │ └── styles.module.scss │ │ │ └── ItemProduct │ │ │ ├── ItemProduct.jsx │ │ │ └── styles.module.scss │ ├── CountdownBanner │ │ ├── CountdownBanner.jsx │ │ └── styles.module.scss │ ├── CountdownTimer │ │ ├── CountdownTimer.jsx │ │ └── styles.module.scss │ ├── Footer │ │ ├── Footer.jsx │ │ ├── constant.js │ │ └── styles.module.scss │ ├── Header │ │ ├── BoxIcon │ │ │ └── BoxIcon.jsx │ │ ├── Header.jsx │ │ ├── Menu │ │ │ └── Menu.jsx │ │ ├── constants.js │ │ └── styles.module.scss │ ├── HeadingListProduct │ │ ├── HeadingListProducts.jsx │ │ └── styles.module.scss │ ├── HomePage │ │ ├── HomePage.jsx │ │ └── styles.module.scss │ ├── Info │ │ ├── Info.jsx │ │ ├── InfoCard │ │ │ └── InfoCard.jsx │ │ ├── constants.js │ │ └── styles.module.scss │ ├── InputCommon │ │ ├── InputCommon.jsx │ │ └── styles.module.scss │ ├── InputCommon2 │ │ ├── Input.jsx │ │ └── styles.module.scss │ ├── Layout │ │ ├── Layout.jsx │ │ └── styles.module.scss │ ├── LoadingTextCommon │ │ ├── LoadingTextCommon.jsx │ │ └── styles.module.scss │ ├── MenuContent │ │ └── MenuContent.jsx │ ├── PaymentMethods │ │ ├── PaymentMethods.jsx │ │ └── styles.module.scss │ ├── PopularProduct │ │ ├── PopularProduct.jsx │ │ └── styles.module.scss │ ├── ProductItem │ │ ├── ProductItem.jsx │ │ ├── ProductItemLine.jsx │ │ └── styles.module.scss │ ├── SaleHomepage │ │ ├── SaleHomepage.jsx │ │ └── styles.module.scss │ ├── Sidebar │ │ ├── Sidebar.jsx │ │ └── styles.module.scss │ └── SliderCommon │ │ ├── SliderCommon.jsx │ │ └── styles.css ├── contexts │ ├── OurShopProvider.jsx │ ├── SideBarProvider.jsx │ ├── SteperProvider.jsx │ ├── ToastProvider.jsx │ └── storeProvider.jsx ├── hooks │ ├── useScrollHandling.js │ └── useTranslateXImage.js ├── main.jsx ├── pages │ ├── AboutUs │ │ ├── components │ │ │ └── Logos.jsx │ │ ├── index.jsx │ │ └── styles.module.scss │ ├── Cart │ │ ├── Cart.jsx │ │ ├── components │ │ │ ├── Checkout │ │ │ │ ├── Checkout.jsx │ │ │ │ ├── RightBody.jsx │ │ │ │ └── Styles.module.scss │ │ │ ├── ContentStep.jsx │ │ │ ├── Loading.jsx │ │ │ ├── contents │ │ │ │ ├── CartSummary.jsx │ │ │ │ ├── CartTable.jsx │ │ │ │ └── Contents.jsx │ │ │ └── steps │ │ │ │ ├── Stepper.jsx │ │ │ │ └── Steps.jsx │ │ └── styles.module.scss │ ├── DetailProduct │ │ ├── components │ │ │ ├── FormItem.jsx │ │ │ ├── Infomation.jsx │ │ │ └── Review.jsx │ │ ├── index.jsx │ │ └── styles.module.scss │ ├── Orders │ │ └── index.jsx │ └── OurShop │ │ ├── OurShop.jsx │ │ ├── components │ │ ├── Banner.jsx │ │ ├── Filter.jsx │ │ ├── ListProducts.jsx │ │ └── SelectBox.jsx │ │ └── styles.module.scss ├── routers │ └── routers.js └── utils │ └── helper.js ├── style.css ├── templateAPI.jsx └── vite.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "singleQuote": true, 4 | "jsxSingleQuote": true, 5 | "tabWidth": 4, 6 | "semi": true, 7 | "printWidth": 80, 8 | "trailingComma": "none" 9 | } 10 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@components/*": ["src/components/*"], 6 | "@assets/*": ["src/assets/*"] 7 | } 8 | }, 9 | "include": ["src/**/*"] 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecommerse", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "axios": "^1.7.7", 13 | "classnames": "^2.5.1", 14 | "formik": "^2.4.6", 15 | "js-cookie": "^3.0.5", 16 | "normalize.css": "^8.0.1", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0", 19 | "react-hook-form": "^7.56.4", 20 | "react-icons": "^5.3.0", 21 | "react-router-dom": "^6.26.2", 22 | "react-slick": "^0.30.2", 23 | "react-toastify": "^10.0.6", 24 | "simple-image-magnifier": "^1.0.10", 25 | "slick-carousel": "^1.8.1", 26 | "swiper": "^11.2.6", 27 | "yup": "^1.4.0" 28 | }, 29 | "devDependencies": { 30 | "@types/react": "^18.0.27", 31 | "@types/react-dom": "^18.0.10", 32 | "@vitejs/plugin-react": "^3.1.0", 33 | "sass": "^1.77.8", 34 | "vite": "^4.1.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter, Routes, Route } from 'react-router-dom'; 2 | import routers from '@/routers/routers'; 3 | import { Suspense } from 'react'; 4 | import { SidebarProvider } from '@/contexts/SideBarProvider'; 5 | import SideBar from '@components/Sidebar/Sidebar'; 6 | import { ToastProvider } from '@/contexts/ToastProvider'; 7 | import { StoreProvider } from '@/contexts/storeProvider'; 8 | 9 | function App() { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | Loading...}> 18 | 19 | {routers.map((item, index) => { 20 | return ( 21 | } 24 | key={index} 25 | /> 26 | ); 27 | })} 28 | 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | } 36 | 37 | export default App; 38 | -------------------------------------------------------------------------------- /src/apis/authService.js: -------------------------------------------------------------------------------- 1 | import axiosClient from './axiosClient'; 2 | 3 | const register = async (body) => { 4 | return await axiosClient.post('/register', body); 5 | }; 6 | 7 | const signIn = async (body) => { 8 | return await axiosClient.post('/login', body); 9 | }; 10 | 11 | const getInfo = async (userId) => { 12 | return await axiosClient.get(`/user/info/${userId}`); 13 | }; 14 | 15 | export { register, signIn, getInfo }; 16 | -------------------------------------------------------------------------------- /src/apis/axiosClient.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import Cookies from 'js-cookie'; 3 | 4 | const axiosClient = axios.create({ 5 | baseURL: 'https://be-project-reactjs.vercel.app/api/v1', 6 | timeout: 10000, 7 | headers: { 8 | 'Content-Type': 'application/json' 9 | } 10 | }); 11 | 12 | const handleRequestSuccess = async (config) => { 13 | const token = Cookies.get('token'); 14 | 15 | if (token) { 16 | config.headers.Authorization = `Bearer ${token}`; 17 | } 18 | 19 | return config; 20 | }; 21 | 22 | const handleRequestErr = (err) => { 23 | return Promise.reject(err); 24 | }; 25 | 26 | const handleResponseSuccess = (res) => { 27 | return res; 28 | }; 29 | 30 | const handleResponseErr = async (err) => { 31 | const originalRequest = err.config; 32 | 33 | if (err.response.status === 401 && !originalRequest._retry) { 34 | originalRequest._retry = true; 35 | const refreshToken = Cookies.get('refreshToken'); 36 | 37 | if (!refreshToken) return Promise.reject(err); 38 | 39 | try { 40 | const res = await axiosClient.post('/refresh-token', { 41 | token: refreshToken 42 | }); 43 | 44 | const newAccessToken = res.data.accessToken; 45 | Cookies.set('token', newAccessToken); 46 | originalRequest.headers.Authorization = `Bearer ${newAccessToken}`; 47 | 48 | return axiosClient(originalRequest); 49 | } catch (error) { 50 | Cookies.remove('token'); 51 | Cookies.remove('refreshToken'); 52 | Cookies.remove('userId'); 53 | 54 | return Promise.reject(error); 55 | } 56 | } 57 | }; 58 | 59 | axiosClient.interceptors.request.use( 60 | (config) => handleRequestSuccess(config), 61 | (err) => handleRequestErr(err) 62 | ); 63 | 64 | axiosClient.interceptors.response.use( 65 | (config) => handleResponseSuccess(config), 66 | (err) => handleResponseErr(err) 67 | ); 68 | 69 | export default axiosClient; 70 | -------------------------------------------------------------------------------- /src/apis/cartService.js: -------------------------------------------------------------------------------- 1 | import axiosClient from './axiosClient'; 2 | 3 | const addProductToCart = async (data) => { 4 | return await axiosClient.post('/cart', data); 5 | }; 6 | 7 | const getCart = async (userId) => { 8 | return await axiosClient.get(`/cart/${userId}`); 9 | }; 10 | 11 | const deleteItem = async (body) => { 12 | return await axiosClient.delete(`/cart/deleteItem`, { 13 | data: body 14 | }); 15 | }; 16 | 17 | const deleteCart = async (body) => { 18 | return await axiosClient.delete(`/cart/delete`, { 19 | data: body 20 | }); 21 | }; 22 | 23 | export { addProductToCart, getCart, deleteItem, deleteCart }; 24 | -------------------------------------------------------------------------------- /src/apis/orderService.js: -------------------------------------------------------------------------------- 1 | import axiosClient from './axiosClient'; 2 | 3 | const createOrder = async (data) => { 4 | return await axiosClient.post(`/orders`, data); 5 | }; 6 | 7 | const getDetailOrder = async (id) => { 8 | return await axiosClient.get(`/orders/${id}`); 9 | }; 10 | 11 | export { createOrder, getDetailOrder }; 12 | -------------------------------------------------------------------------------- /src/apis/productsService.js: -------------------------------------------------------------------------------- 1 | import axiosClient from './axiosClient'; 2 | 3 | const getProducts = async (query) => { 4 | const { sortType, page, limit } = query; 5 | 6 | const queryLimit = limit === 'all' ? '' : `limit=${limit}`; 7 | 8 | const res = await axiosClient.get( 9 | `/product?sortType=${sortType}&page=${page}&${queryLimit}` 10 | ); 11 | 12 | return res.data; 13 | }; 14 | 15 | const getDetailProduct = async (id) => { 16 | const res = await axiosClient.get(`/product/${id}`); 17 | return res.data; 18 | }; 19 | 20 | const getRelatedProduct = async (id) => { 21 | const res = await axiosClient.get(`/related-products/${id}`); 22 | return res.data.relatedProducts; 23 | }; 24 | 25 | export { getProducts, getDetailProduct, getRelatedProduct }; 26 | -------------------------------------------------------------------------------- /src/assets/icons/images/Logo-retina.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hltcd/ecommerse-reactjs/fb7f792037223d4da9a8eec65469f61a16dd9c7b/src/assets/icons/images/Logo-retina.png -------------------------------------------------------------------------------- /src/assets/icons/svgs/boxIcon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/cartIcon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/chatIcon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/debitCardIcon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/fbIcon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/heart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/insIcon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/reloadIcon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/truckIcon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/svgs/ytbIcon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/Banner-Ecommerse.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hltcd/ecommerse-reactjs/fb7f792037223d4da9a8eec65469f61a16dd9c7b/src/assets/images/Banner-Ecommerse.jpeg -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/styles/_global.module.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap'); 2 | 3 | body { 4 | font-family: 'Roboto Mono', monospace; 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/styles/_mixin.module.scss: -------------------------------------------------------------------------------- 1 | @mixin radius_common($radius: 10px) { 2 | border-radius: $radius; 3 | } 4 | 5 | @mixin flex_box_custom($x, $y, $gap: 10px) { 6 | display: flex; 7 | justify-content: $x; 8 | align-items: $y; 9 | gap: $gap; 10 | } 11 | 12 | @mixin btn($w, $h, $radius, $color: #fff) { 13 | outline: none; 14 | border: 1px solid #e1e1e1; 15 | width: $w; 16 | height: $h; 17 | background-color: $color; 18 | @include radius_common($radius); 19 | } 20 | -------------------------------------------------------------------------------- /src/assets/styles/_variable.module.scss: -------------------------------------------------------------------------------- 1 | $primary_color: #333; 2 | $seconddary_color: #888; 3 | $thr_color: #555; 4 | $four_color: #222; 5 | $white_color: #fff; 6 | $sub_color: #404040; 7 | -------------------------------------------------------------------------------- /src/assets/styles/main.scss: -------------------------------------------------------------------------------- 1 | @import 'normalize.css'; 2 | @import './variable.module.scss'; 3 | @import './mixin.module.scss'; 4 | @import './global.module.scss'; 5 | -------------------------------------------------------------------------------- /src/components/AccordionMenu/index.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import styles from './styles.module.scss'; 3 | import cls from 'classnames'; 4 | import { RiArrowDownWideLine } from 'react-icons/ri'; 5 | import { TfiLayoutLineSolid } from 'react-icons/tfi'; 6 | 7 | function AccordionMenu({ titleMenu, contentJsx, onClick, isSelected }) { 8 | const { 9 | container, 10 | title, 11 | activeTitle, 12 | contentMenu, 13 | isVisibility, 14 | borderBottom 15 | } = styles; 16 | 17 | const handleToggle = () => { 18 | onClick(); 19 | }; 20 | 21 | return ( 22 |
23 |
29 | {isSelected ? ( 30 | 31 | ) : ( 32 | 33 | )}{' '} 34 | {titleMenu} 35 |
36 | 37 |
42 | {contentJsx} 43 |
44 |
45 | ); 46 | } 47 | 48 | export default AccordionMenu; 49 | -------------------------------------------------------------------------------- /src/components/AccordionMenu/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@styles/variable.module.scss'; 2 | @import '@styles/mixin.module.scss'; 3 | 4 | .container { 5 | .title { 6 | @include flex_box_custom(flex-start, center, 10px); 7 | 8 | padding: 9px 15px; 9 | color: #404040; 10 | font-size: 14px; 11 | cursor: pointer; 12 | 13 | &:hover { 14 | color: #7c7c7c; 15 | } 16 | } 17 | 18 | .activeTitle { 19 | background-color: #f7f7f7; 20 | } 21 | 22 | .contentMenu { 23 | height: 0px; 24 | overflow: hidden; 25 | transition: all 0.3s ease; 26 | padding: 0px 20px; 27 | } 28 | 29 | .isVisibility { 30 | height: auto; 31 | padding: 20px; 32 | } 33 | 34 | .borderBottom { 35 | border-bottom: 1px solid #e1e1e1; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/components/AdvanceHeadling/AdvanceHeadling.jsx: -------------------------------------------------------------------------------- 1 | import MainLayout from '@components/Layout/Layout'; 2 | import styles from './styles.module.scss'; 3 | 4 | function AdvanceHeadling() { 5 | const { container, headline, containerMiddleBox, title, des } = styles; 6 | 7 | return ( 8 | 9 |
10 |
11 |
12 |

don't miss super offers

13 |

Our best products

14 |
15 |
16 |
17 |
18 | ); 19 | } 20 | 21 | export default AdvanceHeadling; 22 | -------------------------------------------------------------------------------- /src/components/AdvanceHeadling/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@styles/variable.module.scss'; 2 | @import '@styles/mixin.module.scss'; 3 | 4 | .container { 5 | @include flex_box_custom(center, center, 0px); 6 | 7 | margin: 65px 0px 15px; 8 | } 9 | 10 | .headline { 11 | width: 100%; 12 | height: 2px; 13 | border-top: 1px solid #e1e1e1; 14 | border-bottom: 1px solid #e1e1e1; 15 | margin-top: 30px; 16 | } 17 | 18 | .containerMiddleBox { 19 | @include flex_box_custom(center, center, 0px); 20 | width: 60%; 21 | padding: 0px 50px; 22 | flex-direction: column; 23 | line-height: 0px; 24 | } 25 | 26 | .des { 27 | font-size: 14px; 28 | color: $thr_color; 29 | text-transform: uppercase; 30 | } 31 | 32 | .title { 33 | font-size: 24px; 34 | color: $primary_color; 35 | } 36 | -------------------------------------------------------------------------------- /src/components/Banner/Banner.jsx: -------------------------------------------------------------------------------- 1 | import Button from '../Button/Button'; 2 | import styles from './styles.module.scss'; 3 | 4 | function Banner() { 5 | const { container, content, title, des } = styles; 6 | return ( 7 |
8 |
9 |

XStore Marseille04 Demo

10 |
11 | Make yours celebrations even more special this years with 12 | beautiful. 13 |
14 | 15 |
20 |
22 |
23 |
24 | ); 25 | } 26 | 27 | export default Banner; 28 | -------------------------------------------------------------------------------- /src/components/Banner/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@styles/mixin.module.scss'; 2 | @import '@styles/variable.module.scss'; 3 | 4 | .container { 5 | @include flex_box_custom(center, center, 0px); 6 | 7 | background-image: url(../../assets/images/Banner-Ecommerse.jpeg); 8 | min-height: 750px; 9 | background-repeat: no-repeat; 10 | background-size: cover; 11 | background-position: center; 12 | } 13 | 14 | .content { 15 | @include flex_box_custom(center, center, 10px); 16 | 17 | flex-direction: column; 18 | } 19 | 20 | .title { 21 | font-size: 42px; 22 | color: $four_color; 23 | font-weight: 400; 24 | margin: 15px 0; 25 | } 26 | 27 | .des { 28 | color: $thr_color; 29 | font-weight: 300; 30 | margin-bottom: 25px; 31 | } 32 | -------------------------------------------------------------------------------- /src/components/Blog/Blog.jsx: -------------------------------------------------------------------------------- 1 | function Blog() { 2 | return
Blog
; 3 | } 4 | 5 | export default Blog; 6 | -------------------------------------------------------------------------------- /src/components/Button/Button.jsx: -------------------------------------------------------------------------------- 1 | import styles from './styles.module.scss'; 2 | import classNames from 'classnames'; 3 | 4 | function Button({ 5 | content, 6 | isPriamry = true, 7 | customClassname = false, 8 | ...props 9 | }) { 10 | const { btn, primaryBtn, secondaryBtn } = styles; 11 | return ( 12 | 22 | ); 23 | } 24 | 25 | export default Button; 26 | -------------------------------------------------------------------------------- /src/components/Button/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@styles/mixin.module.scss'; 2 | @import '@styles/variable.module.scss'; 3 | 4 | .btn { 5 | @include btn(100%, 42px, 3px, $primary_color); 6 | font-size: 14px; 7 | font-weight: 300; 8 | cursor: pointer; 9 | 10 | &:hover { 11 | transition: background-color 300ms; 12 | } 13 | } 14 | 15 | .primaryBtn { 16 | background-color: $primary_color; 17 | color: $white_color; 18 | 19 | &:hover { 20 | background-color: transparent; 21 | color: $primary_color; 22 | border: 1px solid $primary_color; 23 | } 24 | } 25 | 26 | .secondaryBtn { 27 | background-color: $white_color; 28 | color: $primary_color; 29 | border: 1px solid $primary_color; 30 | 31 | &:hover { 32 | background-color: $primary_color; 33 | color: $white_color; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/ContentSideBar/Cart/Cart.jsx: -------------------------------------------------------------------------------- 1 | import { SideBarContext } from '@/contexts/SideBarProvider'; 2 | import Button from '@components/Button/Button'; 3 | import HeaderSideBar from '@components/ContentSideBar/components/HeaderSidebar/HeaderSideBar'; 4 | import ItemProduct from '@components/ContentSideBar/components/ItemProduct/ItemProduct'; 5 | import LoadingTextCommon from '@components/LoadingTextCommon/LoadingTextCommon'; 6 | import cls from 'classnames'; 7 | import { useContext } from 'react'; 8 | import { PiShoppingCartLight } from 'react-icons/pi'; 9 | import { useNavigate } from 'react-router-dom'; 10 | import styles from './styles.module.scss'; 11 | 12 | function Cart() { 13 | const { 14 | container, 15 | total, 16 | boxBtn, 17 | price, 18 | containerListProductCart, 19 | overlayLoading, 20 | isEmpty, 21 | boxEmpty, 22 | boxBtnEmpty, 23 | containerListItem 24 | } = styles; 25 | const navigate = useNavigate(); 26 | 27 | const { listProductCart, isLoading, setIsOpen } = 28 | useContext(SideBarContext); 29 | 30 | const handleNavigateToShop = () => { 31 | navigate('/shop'); 32 | setIsOpen(false); 33 | }; 34 | 35 | const subTotal = listProductCart.reduce((acc, item) => { 36 | return acc + item.total; 37 | }, 0); 38 | 39 | const handleNavigateToCart = () => { 40 | navigate('/cart'); 41 | setIsOpen(false); 42 | }; 43 | 44 | return ( 45 |
50 | } 52 | title='CART' 53 | /> 54 | 55 | {listProductCart.length ? ( 56 |
57 |
63 | {isLoading ? ( 64 | 65 | ) : ( 66 | listProductCart.map((item, index) => { 67 | return ( 68 | 79 | ); 80 | }) 81 | )} 82 |
83 | 84 |
85 |
86 |

SUBTOTAL:

87 |

${subTotal.toFixed(2)}

88 |
89 | 90 |
91 |
97 |
98 |
99 | ) : ( 100 |
101 |
No products in the cart.
102 |
103 |
108 |
109 | )} 110 |
111 | ); 112 | } 113 | 114 | export default Cart; 115 | -------------------------------------------------------------------------------- /src/components/ContentSideBar/Cart/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@styles/mixin.module.scss'; 2 | @import '@styles/variable.module.scss'; 3 | 4 | .container { 5 | @include flex_box_custom(space-between, unset, 0px); 6 | 7 | padding: 20px 20px 0 30px; 8 | height: 97%; 9 | flex-direction: column; 10 | 11 | .total { 12 | @include flex_box_custom(space-between, center, 0px); 13 | 14 | font-size: 14px; 15 | color: $four_color; 16 | font-weight: 300; 17 | 18 | .price { 19 | font-size: 16px; 20 | font-weight: 400; 21 | } 22 | } 23 | 24 | .boxBtn { 25 | @include flex_box_custom(center, center, 10px); 26 | flex-direction: column; 27 | } 28 | 29 | .containerListProductCart { 30 | position: relative; 31 | .overlayLoading { 32 | @include flex_box_custom(center, center, 0px); 33 | 34 | position: absolute; 35 | top: 0; 36 | left: 0; 37 | width: 100%; 38 | height: 100%; 39 | background-color: rgba(255, 255, 255, 0.5); 40 | } 41 | } 42 | 43 | .boxEmpty { 44 | text-align: center; 45 | font-size: 14px; 46 | color: $four_color; 47 | margin-top: 30px; 48 | 49 | .boxBtnEmpty { 50 | width: 162px; 51 | margin: 20px auto; 52 | } 53 | } 54 | 55 | .containerListItem { 56 | @include flex_box_custom(space-between, unset, 0px); 57 | flex-direction: column; 58 | 59 | flex-grow: 1; 60 | } 61 | } 62 | 63 | .isEmpty { 64 | justify-content: unset; 65 | } 66 | -------------------------------------------------------------------------------- /src/components/ContentSideBar/Compare/Compare.jsx: -------------------------------------------------------------------------------- 1 | import HeaderSideBar from '@components/ContentSideBar/components/HeaderSidebar/HeaderSideBar'; 2 | import { TfiReload } from 'react-icons/tfi'; 3 | import styles from './styles.module.scss'; 4 | import ItemProduct from '@components/ContentSideBar/components/ItemProduct/ItemProduct'; 5 | import Button from '@components/Button/Button'; 6 | 7 | function Compare() { 8 | const { container, boxContent } = styles; 9 | return ( 10 |
11 |
12 | } 14 | title='COMPARE' 15 | /> 16 | 17 |
18 | 19 |
21 | ); 22 | } 23 | 24 | export default Compare; 25 | -------------------------------------------------------------------------------- /src/components/ContentSideBar/Compare/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@styles/mixin.module.scss'; 2 | 3 | .container { 4 | @include flex_box_custom(space-between, unset, 0px); 5 | 6 | flex-direction: column; 7 | padding: 20px 20px 0 20px; 8 | height: 95%; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/ContentSideBar/DetailProduct/DetailProduct.jsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { SideBarContext } from '@/contexts/SideBarProvider'; 3 | import styles from './styles.module.scss'; 4 | import SliderCommon from '@components/SliderCommon/SliderCommon'; 5 | import SelectBox from '@/pages/OurShop/components/SelectBox'; 6 | import Button from '@components/Button/Button'; 7 | import { PiShoppingCartThin } from 'react-icons/pi'; 8 | import { TfiReload } from 'react-icons/tfi'; 9 | import { CiHeart } from 'react-icons/ci'; 10 | import { FaXTwitter } from 'react-icons/fa6'; 11 | import { FaFacebookF } from 'react-icons/fa'; 12 | import { useState } from 'react'; 13 | import cls from 'classnames'; 14 | import { addProductToCart } from '@/apis/cartService'; 15 | 16 | function DetailProduct() { 17 | const { 18 | container, 19 | title, 20 | price, 21 | des, 22 | boxSize, 23 | size, 24 | label, 25 | boxAddToCart, 26 | boxOr, 27 | line, 28 | or, 29 | boxAddOther, 30 | boxFooter, 31 | isActive 32 | } = styles; 33 | 34 | const { 35 | detailProduct, 36 | userId, 37 | setType, 38 | handleGetListProductsCart, 39 | setIsLoading, 40 | setIsOpen 41 | } = useContext(SideBarContext); 42 | const [chooseSize, setChooseSize] = useState(''); 43 | const [quantity, setQuantity] = useState('1'); 44 | 45 | const showOptions = [ 46 | { label: '1', value: '1' }, 47 | { label: '2', value: '2' }, 48 | { label: '3', value: '3' }, 49 | { label: '4', value: '4' }, 50 | { label: '5', value: '5' }, 51 | { label: '6', value: '6' }, 52 | { label: '7', value: '7' } 53 | ]; 54 | 55 | const handleGetSize = (value) => { 56 | setChooseSize(value); 57 | }; 58 | 59 | const handleClearSize = () => { 60 | setChooseSize(''); 61 | }; 62 | 63 | const handleGetQuantity = (value) => { 64 | setQuantity(value); 65 | }; 66 | 67 | const handleAddToCart = () => { 68 | const data = { 69 | userId, 70 | productId: detailProduct._id, 71 | quantity, 72 | size: chooseSize, 73 | isMultiple: true 74 | }; 75 | 76 | setIsOpen(false); 77 | setIsLoading(true); 78 | addProductToCart(data) 79 | .then((res) => { 80 | setIsOpen(true); 81 | setType('cart'); 82 | handleGetListProductsCart(userId, 'cart'); 83 | }) 84 | .catch((err) => { 85 | console.log(err); 86 | }); 87 | }; 88 | 89 | return ( 90 |
91 | 92 | 93 |
{detailProduct.name}
94 |
${detailProduct.price}
95 |
{detailProduct.description}
96 | 97 |
Size {chooseSize}
98 |
99 | {detailProduct.size.map((item, index) => ( 100 |
handleGetSize(item.name)} 106 | > 107 | {item.name} 108 |
109 | ))} 110 |
111 | {chooseSize && ( 112 |
120 | clear 121 |
122 | )} 123 | 124 |
125 | 131 | 132 |
133 |
138 | } 139 | onClick={handleAddToCart} 140 | /> 141 |
142 |
143 | 144 |
145 |
146 |
OR
147 |
148 |
149 | 150 |
155 | } 156 | /> 157 | 158 |
159 | 160 |
Add to compare
161 |
162 | 163 |
164 | 165 |
Add to wishlist
166 |
167 | 168 |
169 | SKU: 12349 170 |
171 |
172 | Category: Pullovers 173 |
174 |
175 | Estimated delivery: 3 - 5 days 176 |
177 |
178 | Share:{' '} 179 | 180 | 181 | 182 | 183 |
184 |
185 | ); 186 | } 187 | 188 | export default DetailProduct; 189 | -------------------------------------------------------------------------------- /src/components/ContentSideBar/DetailProduct/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@styles/variable.module.scss'; 2 | @import '@styles/mixin.module.scss'; 3 | 4 | .container { 5 | padding: 20px; 6 | 7 | .title { 8 | font-size: 24px; 9 | color: $four_color; 10 | margin-top: 20px; 11 | } 12 | 13 | .price { 14 | font-size: 20px; 15 | color: $seconddary_color; 16 | margin: 10px 0; 17 | font-weight: 400; 18 | } 19 | 20 | .des { 21 | color: $thr_color; 22 | font-weight: 400; 23 | } 24 | 25 | .label { 26 | margin-top: 10px; 27 | font-size: 14px; 28 | color: $thr_color; 29 | margin-bottom: 5px; 30 | } 31 | 32 | .boxSize { 33 | @include flex_box_custom(flex-start, center, 10px); 34 | 35 | .size { 36 | padding: 9px 12px; 37 | border: 1px solid #e1e1e1; 38 | font-size: 12px; 39 | cursor: pointer; 40 | 41 | &:hover { 42 | border: 1px solid $primary_color; 43 | } 44 | } 45 | 46 | .isActive { 47 | border: 1px solid $primary_color; 48 | } 49 | } 50 | 51 | .boxAddToCart { 52 | @include flex_box_custom(flex-start, center, 10px); 53 | 54 | margin-top: 20px; 55 | 56 | select { 57 | width: 65px; 58 | height: 36px; 59 | } 60 | 61 | button { 62 | width: 190px; 63 | } 64 | } 65 | 66 | .boxOr { 67 | @include flex_box_custom(center, center, 10px); 68 | 69 | font-size: 12px; 70 | color: $thr_color; 71 | margin-top: 15px; 72 | margin-bottom: 15px; 73 | 74 | .line { 75 | width: 100px; 76 | height: 1px; 77 | background-color: #e1e1e1; 78 | flex-grow: 1; 79 | } 80 | } 81 | 82 | .boxAddOther { 83 | @include flex_box_custom(flex-start, center, 10px); 84 | 85 | margin-top: 12px; 86 | color: $four_color; 87 | cursor: pointer; 88 | } 89 | 90 | .boxFooter { 91 | margin-top: 12px; 92 | 93 | span { 94 | color: $seconddary_color; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/components/ContentSideBar/Login/Login.jsx: -------------------------------------------------------------------------------- 1 | import InputCommon from '@components/InputCommon/InputCommon'; 2 | import styles from './styles.module.scss'; 3 | import Button from '@components/Button/Button'; 4 | import { useFormik } from 'formik'; 5 | import * as Yup from 'yup'; 6 | import { useState } from 'react'; 7 | import { useContext } from 'react'; 8 | import { ToastContext } from '@/contexts/ToastProvider'; 9 | import { register, signIn, getInfo } from '@/apis/authService'; 10 | import Cookies from 'js-cookie'; 11 | import { SideBarContext } from '@/contexts/SideBarProvider'; 12 | import { StoreContext } from '@/contexts/storeProvider'; 13 | 14 | function Login() { 15 | const { container, title, boxRememberMe, lostPw } = styles; 16 | const [isRegister, setIsRegister] = useState(false); 17 | const [isLoading, setIsLoading] = useState(false); 18 | const { toast } = useContext(ToastContext); 19 | const { setIsOpen, handleGetListProductsCart } = useContext(SideBarContext); 20 | const { setUserId } = useContext(StoreContext); 21 | 22 | const formik = useFormik({ 23 | initialValues: { 24 | email: '', 25 | password: '' 26 | }, 27 | validationSchema: Yup.object({ 28 | email: Yup.string() 29 | .email('Invalid email') 30 | .required('Email is required'), 31 | password: Yup.string() 32 | .min(6, 'Password must be at least 6 characters') 33 | .required('Password is required'), 34 | cfmpassword: Yup.string().oneOf( 35 | [Yup.ref('password'), null], 36 | 'Passwords must match' 37 | ) 38 | }), 39 | 40 | onSubmit: async (values) => { 41 | if (isLoading) return; 42 | 43 | const { email: username, password } = values; 44 | 45 | setIsLoading(true); 46 | 47 | if (isRegister) { 48 | await register({ username, password }) 49 | .then((res) => { 50 | toast.success(res.data.message); 51 | setIsLoading(false); 52 | }) 53 | .catch((err) => { 54 | toast.error(err.response.data.message); 55 | setIsLoading(false); 56 | }); 57 | } 58 | 59 | if (!isRegister) { 60 | await signIn({ username, password }) 61 | .then((res) => { 62 | setIsLoading(false); 63 | const { id, token, refreshToken } = res.data; 64 | setUserId(id); 65 | Cookies.set('token', token); 66 | Cookies.set('refreshToken', refreshToken); 67 | Cookies.set('userId', id); 68 | toast.success('Sign in successfully!'); 69 | setIsOpen(false); 70 | handleGetListProductsCart(id, 'cart'); 71 | }) 72 | .catch((err) => { 73 | setIsLoading(false); 74 | toast.error('Sign in failed!'); 75 | }); 76 | } 77 | } 78 | }); 79 | 80 | const handleToggle = () => { 81 | setIsRegister(!isRegister); 82 | formik.resetForm(); 83 | }; 84 | 85 | return ( 86 |
87 |
{isRegister ? 'SIGN UP' : 'SIGN IN'}
88 | 89 |
90 | 97 | 98 | 105 | 106 | {isRegister && ( 107 | 114 | )} 115 | 116 | {!isRegister && ( 117 |
118 | 119 | Remember me 120 |
121 | )} 122 | 123 |
148 | ); 149 | } 150 | 151 | export default Login; 152 | -------------------------------------------------------------------------------- /src/components/ContentSideBar/Login/styles.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 30px 20px 0 20px; 3 | 4 | .title { 5 | font-size: 18px; 6 | text-align: center; 7 | margin-bottom: 30px; 8 | } 9 | 10 | .boxRememberMe { 11 | margin-bottom: 30px; 12 | 13 | input { 14 | margin-right: 10px; 15 | } 16 | 17 | span { 18 | font-size: 14px; 19 | } 20 | } 21 | 22 | .lostPw { 23 | margin-top: 20px; 24 | text-align: center; 25 | font-size: 14px; 26 | 27 | &:hover { 28 | cursor: pointer; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components/ContentSideBar/WishList/WishList.jsx: -------------------------------------------------------------------------------- 1 | import HeaderSideBar from '@components/ContentSideBar/components/HeaderSidebar/HeaderSideBar'; 2 | import { CiHeart } from 'react-icons/ci'; 3 | import styles from './styles.module.scss'; 4 | import ItemProduct from '@components/ContentSideBar/components/ItemProduct/ItemProduct'; 5 | import Button from '@components/Button/Button'; 6 | 7 | function WishList() { 8 | const { container, boxBtn } = styles; 9 | return ( 10 |
11 |
12 | 19 | } 20 | title='WISHLIST' 21 | /> 22 | 23 | 24 |
25 | 26 |
27 |
30 |
31 | ); 32 | } 33 | 34 | export default WishList; 35 | -------------------------------------------------------------------------------- /src/components/ContentSideBar/WishList/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@styles/mixin.module.scss'; 2 | 3 | .container { 4 | @include flex_box_custom(space-between, unset, 0px); 5 | 6 | padding: 20px 20px 0 20px; 7 | height: 95%; 8 | flex-direction: column; 9 | 10 | .boxBtn { 11 | @include flex_box_custom(space-between, unset, 10px); 12 | flex-direction: column; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/components/ContentSideBar/components/HeaderSidebar/HeaderSideBar.jsx: -------------------------------------------------------------------------------- 1 | import styles from './styles.module.scss'; 2 | 3 | function HeaderSideBar({ icon, title }) { 4 | const { container } = styles; 5 | return ( 6 |
7 | {icon} 8 |
{title}
9 |
10 | ); 11 | } 12 | 13 | export default HeaderSideBar; 14 | -------------------------------------------------------------------------------- /src/components/ContentSideBar/components/HeaderSidebar/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@styles/mixin.module.scss'; 2 | 3 | .container { 4 | @include flex_box_custom(center, center, 10px); 5 | flex-direction: column; 6 | 7 | div { 8 | font-size: 18px; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/components/ContentSideBar/components/ItemProduct/ItemProduct.jsx: -------------------------------------------------------------------------------- 1 | import { deleteItem } from '@/apis/cartService'; 2 | import styles from './styles.module.scss'; 3 | import { IoCloseOutline } from 'react-icons/io5'; 4 | import { useState } from 'react'; 5 | import { useContext } from 'react'; 6 | import { SideBarContext } from '@/contexts/SideBarProvider'; 7 | import LoadingTextCommon from '@components/LoadingTextCommon/LoadingTextCommon'; 8 | 9 | function ItemProduct({ 10 | src, 11 | nameProduct, 12 | priceProduct, 13 | skuProduct, 14 | sizeProduct, 15 | quantity, 16 | productId, 17 | userId 18 | }) { 19 | const { 20 | container, 21 | boxContent, 22 | title, 23 | price, 24 | boxClose, 25 | size, 26 | overlayLoading 27 | } = styles; 28 | const [isDelete, setIsDelete] = useState(false); 29 | const { handleGetListProductsCart } = useContext(SideBarContext); 30 | 31 | const handleRemoveItem = () => { 32 | setIsDelete(true); 33 | deleteItem({ 34 | productId, 35 | userId 36 | }) 37 | .then((res) => { 38 | setIsDelete(false); 39 | handleGetListProductsCart(userId, 'cart'); 40 | }) 41 | .catch((err) => { 42 | setIsDelete(false); 43 | }); 44 | }; 45 | 46 | return ( 47 |
48 | 49 | 50 |
51 | 57 |
58 | 59 |
60 |
{nameProduct}
61 |
Size: {sizeProduct}
62 |
63 | {' '} 64 | {quantity} x ${priceProduct} 65 |
66 |
SKU: {skuProduct}
67 |
68 | 69 | {isDelete && ( 70 |
71 | 72 |
73 | )} 74 |
75 | ); 76 | } 77 | 78 | export default ItemProduct; 79 | -------------------------------------------------------------------------------- /src/components/ContentSideBar/components/ItemProduct/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@styles/mixin.module.scss'; 2 | @import '@styles/variable.module.scss'; 3 | 4 | .container { 5 | @include flex_box_custom(unset, flex-start, 25px); 6 | 7 | margin-top: 20px; 8 | padding: 20px; 9 | transition: all 0.3s ease; 10 | position: relative; 11 | overflow: hidden; 12 | 13 | img { 14 | width: 70px; 15 | } 16 | 17 | .boxClose { 18 | position: absolute; 19 | top: 5px; 20 | right: 5px; 21 | cursor: pointer; 22 | transform: translateX(30px); 23 | transition: all 0.3s ease; 24 | } 25 | 26 | .boxContent { 27 | @include flex_box_custom(center, flex-start, 0px); 28 | flex-direction: column; 29 | 30 | .title { 31 | font-size: 16px; 32 | color: $primary_color; 33 | } 34 | 35 | .size { 36 | color: #9e9e9e; 37 | margin-top: 7px; 38 | margin-bottom: 10px; 39 | font-size: 14px; 40 | } 41 | 42 | .price { 43 | font-size: 14px; 44 | color: $four_color; 45 | margin-bottom: 7px; 46 | } 47 | } 48 | 49 | .overlayLoading { 50 | @include flex_box_custom(center, center, 0px); 51 | 52 | position: absolute; 53 | top: 0; 54 | left: 0; 55 | width: 100%; 56 | height: 100%; 57 | background-color: rgba(255, 255, 255, 0.5); 58 | } 59 | 60 | &:hover { 61 | background-color: #f7f7f7; 62 | .boxClose { 63 | transform: translateX(0); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/components/CountdownBanner/CountdownBanner.jsx: -------------------------------------------------------------------------------- 1 | import styles from './styles.module.scss'; 2 | import CountdownTimer from '@components/CountdownTimer/CountdownTimer'; 3 | import Button from '@components/Button/Button'; 4 | function CountdownBanner() { 5 | const { container, containerTimmer, title, boxBtn } = styles; 6 | const targetDate = '2025-12-31T00:00:00'; 7 | return ( 8 |
9 |
10 | 11 |
12 |

The classics make a comeback

13 |
14 |
16 |
17 | ); 18 | } 19 | 20 | export default CountdownBanner; 21 | -------------------------------------------------------------------------------- /src/components/CountdownBanner/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@styles/variable.module.scss'; 2 | @import '@styles/mixin.module.scss'; 3 | 4 | .container { 5 | background-image: url('https://xstore.8theme.com/elementor2/marseille04/wp-content/uploads/sites/2/2022/12/photo-of-man-wearing-white-hoodie-5474310.jpeg'); 6 | height: 409px; 7 | width: calc(100% / 2); 8 | background-repeat: no-repeat; 9 | background-size: cover; 10 | } 11 | 12 | .containerTimmer { 13 | @include flex_box_custom(center, center, 10px); 14 | } 15 | 16 | .title { 17 | color: $four_color; 18 | font-size: 28px; 19 | margin-bottom: 5px; 20 | text-align: center; 21 | } 22 | 23 | .boxBtn { 24 | width: 172px; 25 | margin-top: 20px; 26 | margin: 20px auto; 27 | text-align: center; 28 | } 29 | -------------------------------------------------------------------------------- /src/components/CountdownTimer/CountdownTimer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import styles from './styles.module.scss'; 3 | 4 | const CountdownTimer = ({ targetDate }) => { 5 | const { box, title } = styles; 6 | const [timeLeft, setTimeLeft] = useState(calculateTimeLeft()); 7 | 8 | function calculateTimeLeft() { 9 | const difference = +new Date(targetDate) - +new Date(); 10 | let timeLeft = {}; 11 | 12 | if (difference > 0) { 13 | timeLeft = { 14 | Days: Math.floor(difference / (1000 * 60 * 60 * 24)), 15 | Hours: Math.floor((difference / (1000 * 60 * 60)) % 24), 16 | Mins: Math.floor((difference / 1000 / 60) % 60), 17 | Secs: Math.floor((difference / 1000) % 60) 18 | }; 19 | } 20 | 21 | return timeLeft; 22 | } 23 | 24 | useEffect(() => { 25 | const timer = setTimeout(() => { 26 | setTimeLeft(calculateTimeLeft()); 27 | }, 1000); 28 | 29 | return () => clearTimeout(timer); 30 | }); 31 | 32 | const formatNumber = (number) => { 33 | return String(number).padStart(2, '0'); 34 | }; 35 | 36 | const timerComponents = []; 37 | 38 | Object.keys(timeLeft).forEach((interval) => { 39 | if (timeLeft[interval] !== undefined) { 40 | timerComponents.push( 41 | 42 | {formatNumber(timeLeft[interval])}{' '} 43 | {interval}{' '} 44 | 45 | ); 46 | } 47 | }); 48 | 49 | return timerComponents; 50 | }; 51 | 52 | export default CountdownTimer; 53 | -------------------------------------------------------------------------------- /src/components/CountdownTimer/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@styles/variable.module.scss'; 2 | 3 | .box { 4 | background-color: white; 5 | padding: 10px; 6 | border-radius: 4px; 7 | margin-top: 150px; 8 | } 9 | 10 | .title { 11 | font-size: 18px; 12 | color: $seconddary_color; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/Footer/Footer.jsx: -------------------------------------------------------------------------------- 1 | import { dataMenu } from '@components/Footer/constant'; 2 | import styles from './styles.module.scss'; 3 | 4 | function MyFooter() { 5 | const { container, boxNav } = styles; 6 | return ( 7 |
8 |
9 | 15 |
16 | 17 |
18 | {dataMenu.map((item) => ( 19 |
{item.content}
20 | ))} 21 |
22 | 23 |
24 |

29 | Guaranteed safe checkout 30 |

31 | 35 |
36 | 37 |
38 | Copyright © 2024 HLTCD theme. Created by HLTCD 39 |
40 |
41 | ); 42 | } 43 | 44 | export default MyFooter; 45 | -------------------------------------------------------------------------------- /src/components/Footer/constant.js: -------------------------------------------------------------------------------- 1 | const dataMenu = [ 2 | { content: 'Home', href: '#' }, 3 | { content: 'Elements', href: '#' }, 4 | { content: 'Shop', href: '#' }, 5 | { content: 'Blog', href: '#' }, 6 | { content: 'About us', href: '#' }, 7 | { content: 'Contact us', href: '#' }, 8 | { content: 'Compare', href: '#' } 9 | ]; 10 | 11 | export { dataMenu }; 12 | -------------------------------------------------------------------------------- /src/components/Footer/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@styles/variable.module.scss'; 2 | @import '@styles/mixin.module.scss'; 3 | 4 | .container { 5 | @include flex_box_custom(center, center, 20px); 6 | 7 | flex-direction: column; 8 | background-color: #363636; 9 | margin-top: 80px; 10 | color: $white_color; 11 | padding: 50px 0; 12 | 13 | .boxNav { 14 | @include flex_box_custom(center, center, 50px); 15 | font-size: 15px; 16 | 17 | :hover { 18 | opacity: 0.7; 19 | cursor: pointer; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/Header/BoxIcon/BoxIcon.jsx: -------------------------------------------------------------------------------- 1 | import styles from '../styles.module.scss'; 2 | import fbIcon from '@icons/svgs/fbIcon.svg'; 3 | import insIcon from '@icons/svgs/insIcon.svg'; 4 | import ytbIcon from '@icons/svgs/ytbIcon.svg'; 5 | 6 | function BoxIcon({ type, href }) { 7 | const { boxIcon } = styles; 8 | 9 | const handleRenderIcon = (type) => { 10 | switch (type) { 11 | case 'fb': 12 | return fbIcon; 13 | case 'ins': 14 | return insIcon; 15 | case 'ytb': 16 | return ytbIcon; 17 | } 18 | }; 19 | 20 | return ( 21 |
22 | {type} 23 |
24 | ); 25 | } 26 | 27 | export default BoxIcon; 28 | -------------------------------------------------------------------------------- /src/components/Header/Header.jsx: -------------------------------------------------------------------------------- 1 | import BoxIcon from './BoxIcon/BoxIcon'; 2 | import Menu from './Menu/Menu'; 3 | import { dataBoxIcon, dataMenu } from './constants'; 4 | import styles from './styles.module.scss'; 5 | import Logo from '@icons/images/Logo-retina.png'; 6 | import { TfiReload } from 'react-icons/tfi'; 7 | import { BsHeart } from 'react-icons/bs'; 8 | import { PiShoppingCart } from 'react-icons/pi'; 9 | import useScrollHandling from '@/hooks/useScrollHandling'; 10 | import { useEffect } from 'react'; 11 | import { useState } from 'react'; 12 | import classNames from 'classnames'; 13 | import { useContext } from 'react'; 14 | import { SideBarContext } from '@/contexts/SideBarProvider'; 15 | import { StoreContext } from '@/contexts/storeProvider'; 16 | 17 | function MyHeader() { 18 | const { 19 | containerBoxIcon, 20 | containerMenu, 21 | containerHeader, 22 | containerBox, 23 | container, 24 | fixedHeader, 25 | topHeader, 26 | boxCart, 27 | quantity, 28 | } = styles; 29 | 30 | const { scrollPosition } = useScrollHandling(); 31 | const [fixedPosition, setFixedPosition] = useState(false); 32 | const { 33 | setIsOpen, 34 | setType, 35 | listProductCart, 36 | userId, 37 | handleGetListProductsCart, 38 | } = useContext(SideBarContext); 39 | const { userInfo } = useContext(StoreContext); 40 | 41 | const handleOpenSideBar = (type) => { 42 | setIsOpen(true); 43 | setType(type); 44 | }; 45 | 46 | const handleOpenCartSideBar = () => { 47 | handleGetListProductsCart(userId, 'cart'); 48 | handleOpenSideBar('cart'); 49 | }; 50 | 51 | const totalItemCart = listProductCart.length 52 | ? listProductCart.reduce((acc, item) => { 53 | return (acc += item.quantity); 54 | }, 0) 55 | : 0; 56 | 57 | useEffect(() => { 58 | setFixedPosition(scrollPosition > 80); 59 | }, [scrollPosition]); 60 | 61 | return ( 62 |
67 |
68 |
69 |
70 | {dataBoxIcon.map((item) => { 71 | return ; 72 | })} 73 |
74 |
75 | {dataMenu.slice(0, 3).map((item) => { 76 | return ; 77 | })} 78 |
79 |
80 |
81 | Logo 89 |
90 |
91 |
92 | {dataMenu.slice(3, dataMenu.length).map((item) => { 93 | return ; 94 | })} 95 |
96 | 97 |
98 | handleOpenSideBar('compare')} 103 | /> 104 | handleOpenSideBar('wishlist')} 109 | /> 110 |
111 | handleOpenCartSideBar()} 116 | /> 117 | 118 |
119 | {totalItemCart || userInfo?.amountCart || 0} 120 |
121 |
122 |
123 |
124 |
125 |
126 | ); 127 | } 128 | 129 | export default MyHeader; 130 | -------------------------------------------------------------------------------- /src/components/Header/Menu/Menu.jsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import styles from '../styles.module.scss'; 3 | import { SideBarContext } from '@/contexts/SideBarProvider'; 4 | import { StoreContext } from '@/contexts/storeProvider'; 5 | import { useState } from 'react'; 6 | import { useNavigate } from 'react-router-dom'; 7 | 8 | function Menu({ content, href }) { 9 | const { menu, subMenu } = styles; 10 | const { setIsOpen, setType } = useContext(SideBarContext); 11 | const { userInfo, handleLogOut } = useContext(StoreContext); 12 | const [isShowSubMenu, setIsShowSubMenu] = useState(false); 13 | const navigate = useNavigate(); 14 | 15 | const handleClickShowLogin = () => { 16 | if (content === 'Sign in' && !userInfo) { 17 | setIsOpen(true); 18 | setType('login'); 19 | 20 | return; 21 | } 22 | 23 | navigate(href); 24 | }; 25 | 26 | const handleRenderText = (content) => { 27 | if (content === 'Sign in' && userInfo) { 28 | return `Hello: ${userInfo?.username}`; 29 | } else { 30 | return content; 31 | } 32 | }; 33 | 34 | const handleHover = () => { 35 | if (content === 'Sign in' && userInfo) { 36 | setIsShowSubMenu(true); 37 | } 38 | }; 39 | 40 | return ( 41 |
46 | {handleRenderText(content)} 47 | 48 | {isShowSubMenu && ( 49 |
setIsShowSubMenu(false)} 51 | className={subMenu} 52 | onClick={handleLogOut} 53 | > 54 | LOG OUT 55 |
56 | )} 57 |
58 | ); 59 | } 60 | 61 | export default Menu; 62 | -------------------------------------------------------------------------------- /src/components/Header/constants.js: -------------------------------------------------------------------------------- 1 | const dataBoxIcon = [ 2 | { type: 'fb', href: '#' }, 3 | { type: 'ins', href: '#' }, 4 | { type: 'ytb', href: '#' }, 5 | ]; 6 | 7 | const dataMenu = [ 8 | { content: 'Elements', href: '#' }, 9 | { content: 'Our Shop', href: '/shop' }, 10 | { content: 'About us', href: '/about-us' }, 11 | { content: 'Contacts', href: '#' }, 12 | { content: 'Search', href: '#' }, 13 | { content: 'Sign in', href: '#' }, 14 | ]; 15 | 16 | export { dataBoxIcon, dataMenu }; 17 | -------------------------------------------------------------------------------- /src/components/Header/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@styles/mixin.module.scss'; 2 | @import '@styles/variable.module.scss'; 3 | 4 | .container { 5 | @include flex_box_custom(center, center, 0px); 6 | left: 0; 7 | right: 0; 8 | } 9 | 10 | .topHeader { 11 | position: absolute; 12 | top: 0; 13 | } 14 | 15 | .fixedHeader { 16 | position: fixed; 17 | top: -83px; 18 | background-color: #ffffffe6; 19 | z-index: 999; 20 | box-shadow: 2px 0px 12px 0px rgba(0, 0, 0, 0.15); 21 | transform: translateY(83px); 22 | backdrop-filter: blur(5px); 23 | transition: transform 0.7s ease; 24 | } 25 | 26 | .containerHeader { 27 | width: 1250px; 28 | display: flex; 29 | align-items: center; 30 | justify-content: space-between; 31 | height: 83px; 32 | } 33 | 34 | .containerBoxIcon { 35 | @include flex_box_custom(center, center, 20px); 36 | 37 | &:hover { 38 | cursor: pointer; 39 | } 40 | } 41 | 42 | .containerBox { 43 | @include flex_box_custom(center, center, 20px); 44 | } 45 | 46 | .containerMenu { 47 | @include flex_box_custom(center, center, 40px); 48 | } 49 | 50 | .boxIcon { 51 | @include radius_common(50%); 52 | @include flex_box_custom(center, center, 0px); 53 | 54 | background-color: $primary_color; 55 | width: 26px; 56 | height: 26px; 57 | } 58 | 59 | .menu { 60 | cursor: pointer; 61 | padding-top: 9px; 62 | font-size: 15px; 63 | color: $primary_color; 64 | position: relative; 65 | 66 | .subMenu { 67 | position: absolute; 68 | background-color: #fff; 69 | width: 100%; 70 | padding: 10px; 71 | top: 40px; 72 | } 73 | } 74 | 75 | .menu::after { 76 | content: ''; 77 | display: block; 78 | width: 100%; 79 | height: 3px; 80 | background-color: $primary_color; 81 | margin-top: 6px; 82 | 83 | transform-origin: right; 84 | transform: scale(0); 85 | opacity: 0; 86 | transition: transform 300ms, opacity 500ms; 87 | } 88 | 89 | .menu:hover::after { 90 | opacity: 1; 91 | transform: scale(1); 92 | } 93 | 94 | .boxCart { 95 | position: relative; 96 | 97 | .quantity { 98 | position: absolute; 99 | top: -5px; 100 | right: -10px; 101 | background-color: $primary_color; 102 | font-size: 10px; 103 | color: #fff; 104 | @include radius_common(50%); 105 | width: 15px; 106 | height: 15px; 107 | @include flex_box_custom(center, center, 0px); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/components/HeadingListProduct/HeadingListProducts.jsx: -------------------------------------------------------------------------------- 1 | import MainLayout from '@components/Layout/Layout'; 2 | import CountdownBanner from '@components/CountdownBanner/CountdownBanner'; 3 | import styles from './styles.module.scss'; 4 | import ProductItem from '@components/ProductItem/ProductItem'; 5 | 6 | function HeadingListProducts({ data }) { 7 | const { container, containerItem } = styles; 8 | 9 | return ( 10 | 11 |
12 | 13 |
14 | {data.map((item) => ( 15 | 23 | ))} 24 |
25 |
26 |
27 | ); 28 | } 29 | 30 | export default HeadingListProducts; 31 | -------------------------------------------------------------------------------- /src/components/HeadingListProduct/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@styles/variable.module.scss'; 2 | @import '@styles/mixin.module.scss'; 3 | 4 | .container { 5 | @include flex_box_custom(space-between, center, 10px); 6 | margin-top: 40px; 7 | } 8 | 9 | .containerItem { 10 | @include flex_box_custom(center, center, 10px); 11 | } 12 | -------------------------------------------------------------------------------- /src/components/HomePage/HomePage.jsx: -------------------------------------------------------------------------------- 1 | import { getProducts } from '@/apis/productsService'; 2 | import AdvanceHeadling from '@components/AdvanceHeadling/AdvanceHeadling'; 3 | import Banner from '@components/Banner/Banner'; 4 | import MyFooter from '@components/Footer/Footer'; 5 | import MyHeader from '@components/Header/Header'; 6 | import HeadingListProducts from '@components/HeadingListProduct/HeadingListProducts'; 7 | import Info from '@components/Info/Info'; 8 | import PopularProduct from '@components/PopularProduct/PopularProduct'; 9 | import SaleHomepage from '@components/SaleHomepage/SaleHomepage'; 10 | import { useEffect } from 'react'; 11 | import { useState } from 'react'; 12 | 13 | function HomePage() { 14 | const [listProducts, setListProducts] = useState([]); 15 | 16 | useEffect(() => { 17 | const query = { 18 | sortType: 0, 19 | page: 1, 20 | limit: 10 21 | }; 22 | 23 | getProducts(query).then((res) => { 24 | setListProducts(res.contents); 25 | }); 26 | }, []); 27 | 28 | return ( 29 | <> 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | } 41 | 42 | export default HomePage; 43 | -------------------------------------------------------------------------------- /src/components/HomePage/styles.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | z-index: -1; 4 | } 5 | -------------------------------------------------------------------------------- /src/components/Info/Info.jsx: -------------------------------------------------------------------------------- 1 | import MainLayout from '@components/Layout/Layout'; 2 | import { dataInfo } from './constants'; 3 | import InfoCard from './InfoCard/InfoCard'; 4 | import styles from './styles.module.scss'; 5 | 6 | function Info() { 7 | const { container } = styles; 8 | return ( 9 | 10 |
11 | {dataInfo.map((item) => { 12 | return ( 13 | 18 | ); 19 | })} 20 |
21 |
22 | ); 23 | } 24 | 25 | export default Info; 26 | -------------------------------------------------------------------------------- /src/components/Info/InfoCard/InfoCard.jsx: -------------------------------------------------------------------------------- 1 | import styles from '../styles.module.scss'; 2 | 3 | function InfoCard({ content, description, src }) { 4 | const { containerCard, containerContent, title, des } = styles; 5 | 6 | return ( 7 |
8 | TruckIcon 9 | 10 |
11 |
{content}
12 |
{description}
13 |
14 |
15 | ); 16 | } 17 | 18 | export default InfoCard; 19 | -------------------------------------------------------------------------------- /src/components/Info/constants.js: -------------------------------------------------------------------------------- 1 | import TruckIcon from '@icons/svgs/truckIcon.svg'; 2 | import DebitCardIcon from '@icons/svgs/debitCardIcon.svg'; 3 | import BoxIcon from '@icons/svgs/boxIcon.svg'; 4 | import ChatIcon from '@icons/svgs/chatIcon.svg'; 5 | 6 | export const dataInfo = [ 7 | { 8 | title: 'Fastest Shipping', 9 | description: 'Order at $39 order', 10 | src: TruckIcon 11 | }, 12 | { 13 | title: '100% Safe Payments', 14 | description: '9 month installments', 15 | src: DebitCardIcon 16 | }, 17 | { 18 | title: '14-Days Return', 19 | description: 'Shop with confidence', 20 | src: BoxIcon 21 | }, 22 | { 23 | title: '24/7 Online Support', 24 | description: 'Delivered to home', 25 | src: ChatIcon 26 | } 27 | ]; 28 | -------------------------------------------------------------------------------- /src/components/Info/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@styles/variable.module.scss'; 2 | @import '@styles/mixin.module.scss'; 3 | 4 | .container { 5 | // height: 144px; 6 | background-color: $primary_color; 7 | margin-top: -75px; 8 | padding: 20px 55px; 9 | 10 | @include flex_box_custom(space-between, center, 0px); 11 | } 12 | 13 | .containerCard { 14 | @include flex_box_custom(center, center, 18px); 15 | 16 | width: 280px; 17 | height: 104px; 18 | } 19 | 20 | .containerContent { 21 | @include flex_box_custom(center, flex-start, 13px); 22 | 23 | flex-direction: column; 24 | color: $white_color; 25 | font-weight: 300; 26 | 27 | .title { 28 | font-size: 17px; 29 | } 30 | 31 | .des { 32 | font-size: 16px; 33 | color: #ffffffc7; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/InputCommon/InputCommon.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import styles from './styles.module.scss'; 3 | import { FiEye } from 'react-icons/fi'; 4 | import { FiEyeOff } from 'react-icons/fi'; 5 | import cls from 'classnames'; 6 | 7 | function InputCommon({ label, type, isRequired = false, ...props }) { 8 | const { labelInput, boxInput, container, boxIcon, errMsg, isErrInput } = 9 | styles; 10 | const { formik, id } = props; 11 | const [showPassword, setShowPassword] = useState(false); 12 | 13 | const isPassword = type === 'password'; 14 | const isShowTextPassword = 15 | type === 'password' && showPassword ? 'text' : type; 16 | 17 | const handleShowPassword = () => { 18 | setShowPassword(!showPassword); 19 | }; 20 | 21 | const isErr = formik.touched[id] && formik.errors[id]; 22 | const messageErr = formik.errors[id]; 23 | 24 | return ( 25 |
26 |
31 | {label} {isRequired && *} 32 |
33 |
34 | 42 | {isPassword && ( 43 |
44 | {showPassword ? : } 45 |
46 | )} 47 | 48 | {isErr &&
{messageErr}
} 49 |
50 |
51 | ); 52 | } 53 | 54 | export default InputCommon; 55 | -------------------------------------------------------------------------------- /src/components/InputCommon/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@styles/variable.module.scss'; 2 | 3 | .container { 4 | margin-bottom: 20px; 5 | 6 | .labelInput { 7 | font-size: 14px; 8 | color: $four_color; 9 | margin-bottom: 5px; 10 | } 11 | 12 | .labelInput.errMsg { 13 | color: red; 14 | } 15 | 16 | .boxInput { 17 | width: 100%; 18 | position: relative; 19 | 20 | input { 21 | width: 100%; 22 | border: 1px solid #e1e1e1; 23 | outline: none; 24 | padding: 10px 0 10px 10px; 25 | } 26 | 27 | input.isErrInput { 28 | border: 1px solid red; 29 | } 30 | 31 | .boxIcon { 32 | position: absolute; 33 | top: 12px; 34 | right: 0; 35 | cursor: pointer; 36 | } 37 | 38 | .errMsg { 39 | font-size: 12px; 40 | color: red; 41 | margin-top: 3px; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/components/InputCommon2/Input.jsx: -------------------------------------------------------------------------------- 1 | import styles from './styles.module.scss'; 2 | 3 | function InputCustom({ 4 | label, 5 | type, 6 | dataOptions, 7 | isRequired = false, 8 | register, 9 | isShowlabel = true, 10 | isError = false, 11 | }) { 12 | const { container, labelCLS, error } = styles; 13 | 14 | const renderInput = () => { 15 | if (type === 'text') { 16 | return ( 17 | 23 | ); 24 | } else { 25 | return ( 26 | 36 | ); 37 | } 38 | }; 39 | 40 | return ( 41 |
42 | {isShowlabel && ( 43 | 46 | )} 47 | 48 | {renderInput()} 49 |
50 | ); 51 | } 52 | 53 | export default InputCustom; 54 | -------------------------------------------------------------------------------- /src/components/InputCommon2/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@styles/variable.module.scss'; 2 | 3 | .container { 4 | width: 100%; 5 | display: flex; 6 | flex-direction: column; 7 | 8 | .labelCLS { 9 | font-size: 14px; 10 | color: $thr_color; 11 | margin-bottom: 5px; 12 | } 13 | 14 | .error { 15 | border-color: red !important; 16 | } 17 | 18 | input { 19 | height: 37px; 20 | padding: 0px 15px; 21 | border: 1px solid #e1e1e1; 22 | 23 | &:focus { 24 | outline: #333; 25 | border: 1px solid #333; 26 | } 27 | } 28 | 29 | select { 30 | height: 37px; 31 | padding: 0px 15px; 32 | border: 1px solid #e1e1e1; 33 | 34 | &:focus { 35 | outline: #333; 36 | border: 1px solid #333; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/Layout/Layout.jsx: -------------------------------------------------------------------------------- 1 | import styles from './styles.module.scss'; 2 | 3 | function MainLayout({ children }) { 4 | const { wrapLayout, container } = styles; 5 | 6 | return ( 7 |
8 |
{children}
9 |
10 | ); 11 | } 12 | 13 | export default MainLayout; 14 | -------------------------------------------------------------------------------- /src/components/Layout/styles.module.scss: -------------------------------------------------------------------------------- 1 | .wrapLayout { 2 | display: flex; 3 | justify-content: center; 4 | } 5 | 6 | .container { 7 | width: 1250px; 8 | } 9 | -------------------------------------------------------------------------------- /src/components/LoadingTextCommon/LoadingTextCommon.jsx: -------------------------------------------------------------------------------- 1 | import styles from './styles.module.scss'; 2 | import { AiOutlineLoading3Quarters } from 'react-icons/ai'; 3 | 4 | function LoadingTextCommon() { 5 | const { rotate } = styles; 6 | return ; 7 | } 8 | 9 | export default LoadingTextCommon; 10 | -------------------------------------------------------------------------------- /src/components/LoadingTextCommon/styles.module.scss: -------------------------------------------------------------------------------- 1 | .rotate { 2 | animation: loading 1s linear infinite; 3 | @keyframes loading { 4 | 0% { 5 | transform: rotate(0); 6 | } 7 | 100% { 8 | transform: rotate(360deg); 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/MenuContent/MenuContent.jsx: -------------------------------------------------------------------------------- 1 | function MenuContent({ content }) { 2 | return
{content}
; 3 | } 4 | 5 | export default MenuContent; 6 | -------------------------------------------------------------------------------- /src/components/PaymentMethods/PaymentMethods.jsx: -------------------------------------------------------------------------------- 1 | import styles from './styles.module.scss'; 2 | 3 | function PaymentMethods() { 4 | const { 5 | containerMethods, 6 | titleMethods, 7 | boxImgMethods, 8 | imgMethods, 9 | textSecure 10 | } = styles; 11 | 12 | const srcMethods = [ 13 | 'https://xstore.8theme.com/elementor2/marseille04/wp-content/themes/xstore/images/woocommerce/payment-icons/visa.jpeg', 14 | 'https://xstore.8theme.com/elementor2/marseille04/wp-content/themes/xstore/images/woocommerce/payment-icons/master-card.jpeg', 15 | 'https://xstore.8theme.com/elementor2/marseille04/wp-content/themes/xstore/images/woocommerce/payment-icons/paypal.jpeg', 16 | 'https://xstore.8theme.com/elementor2/marseille04/wp-content/themes/xstore/images/woocommerce/payment-icons/american-express.jpeg', 17 | 'https://xstore.8theme.com/elementor2/marseille04/wp-content/themes/xstore/images/woocommerce/payment-icons/maestro.jpeg', 18 | 'https://xstore.8theme.com/elementor2/marseille04/wp-content/themes/xstore/images/woocommerce/payment-icons/bitcoin.jpeg' 19 | ]; 20 | 21 | return ( 22 | <> 23 |
24 |
25 | Guaranteed safe checkout 26 |
27 | 28 |
29 | {srcMethods.map((src, index) => { 30 | return ( 31 | {src} 37 | ); 38 | })} 39 |
40 |
41 | 42 |
Your Payment is 100% Secure
43 | 44 | ); 45 | } 46 | 47 | export default PaymentMethods; 48 | -------------------------------------------------------------------------------- /src/components/PaymentMethods/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@styles/variable.module.scss'; 2 | @import '@styles/mixin.module.scss'; 3 | 4 | .containerMethods { 5 | padding: 25px 0; 6 | position: relative; 7 | width: 100%; 8 | border: 1px solid #e1e1e1; 9 | margin-top: 45px; 10 | 11 | .titleMethods { 12 | position: absolute; 13 | top: -8px; 14 | transform: translateX(-50%); 15 | left: 50%; 16 | background-color: #fff; 17 | padding: 0px 10px; 18 | text-transform: uppercase; 19 | font-size: 14px; 20 | 21 | span { 22 | color: #2e7d32; 23 | } 24 | } 25 | 26 | .boxImgMethods { 27 | @include flex_box_custom(center, center, 10px); 28 | } 29 | 30 | .imgMethods { 31 | border: 1px solid #e1e1e1; 32 | max-width: 50%; 33 | width: 50px; 34 | height: 34px; 35 | border-radius: 5px; 36 | } 37 | } 38 | 39 | .textSecure { 40 | text-align: center; 41 | font-size: 14px; 42 | color: $thr_color; 43 | margin-top: 10px; 44 | } 45 | -------------------------------------------------------------------------------- /src/components/PopularProduct/PopularProduct.jsx: -------------------------------------------------------------------------------- 1 | import MainLayout from '@components/Layout/Layout'; 2 | import styles from './styles.module.scss'; 3 | import ProductItem from '@components/ProductItem/ProductItem'; 4 | 5 | function PopularProduct({ data }) { 6 | const { container } = styles; 7 | return ( 8 | <> 9 | 10 |
11 | {data.map((item) => ( 12 | 20 | ))} 21 |
22 |
23 | 24 | ); 25 | } 26 | 27 | export default PopularProduct; 28 | -------------------------------------------------------------------------------- /src/components/PopularProduct/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@styles/variable.module.scss'; 2 | @import '@styles/mixin.module.scss'; 3 | 4 | .container { 5 | @include flex_box_custom(space-between, center, 10px); 6 | 7 | margin-top: 10px; 8 | flex-wrap: wrap; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/ProductItem/ProductItem.jsx: -------------------------------------------------------------------------------- 1 | import { addProductToCart } from '@/apis/cartService'; 2 | import { OurShopContext } from '@/contexts/OurShopProvider'; 3 | import { SideBarContext } from '@/contexts/SideBarProvider'; 4 | import { ToastContext } from '@/contexts/ToastProvider'; 5 | import Button from '@components/Button/Button'; 6 | import LoadingTextCommon from '@components/LoadingTextCommon/LoadingTextCommon'; 7 | import cls from 'classnames'; 8 | import Cookies from 'js-cookie'; 9 | import { useContext, useEffect, useState } from 'react'; 10 | import { CiHeart } from 'react-icons/ci'; 11 | import { LiaEyeSolid, LiaShoppingBagSolid } from 'react-icons/lia'; 12 | import { TfiReload } from 'react-icons/tfi'; 13 | import styles from './styles.module.scss'; 14 | import { useNavigate } from 'react-router-dom'; 15 | import { handleAddProductToCartCommon } from '@/utils/helper'; 16 | 17 | function ProductItem({ 18 | src, 19 | prevSrc, 20 | name, 21 | price, 22 | details, 23 | isHomepage = true, 24 | slideItem = false 25 | }) { 26 | // const { isShowGrid } = useContext(OurShopContext); 27 | const [sizeChoose, setSizeChoose] = useState(''); 28 | const ourShopStore = useContext(OurShopContext); 29 | const [isShowGrid, setIsShowGrid] = useState(ourShopStore?.isShowGrid); 30 | const userId = Cookies.get('userId'); 31 | const { setIsOpen, setType, handleGetListProductsCart, setDetailProduct } = 32 | useContext(SideBarContext); 33 | const { toast } = useContext(ToastContext); 34 | const [isLoading, setIsLoading] = useState(false); 35 | const navigate = useNavigate(); 36 | 37 | const { 38 | boxImg, 39 | showImgWhenHover, 40 | showFncWhenHover, 41 | boxIcon, 42 | title, 43 | priceCl, 44 | boxSize, 45 | size, 46 | textCenter, 47 | boxBtn, 48 | content, 49 | containerItem, 50 | leftBtn, 51 | largImg, 52 | isActiveSize, 53 | btnClear 54 | } = styles; 55 | 56 | const handleChooseSize = (size) => { 57 | setSizeChoose(size); 58 | }; 59 | 60 | const handleClearSize = () => { 61 | setSizeChoose(''); 62 | }; 63 | 64 | const handleAddToCart = () => { 65 | handleAddProductToCartCommon( 66 | userId, 67 | setIsOpen, 68 | setType, 69 | toast, 70 | sizeChoose, 71 | details._id, 72 | 1, 73 | setIsLoading, 74 | handleGetListProductsCart 75 | ); 76 | }; 77 | 78 | const handleShowDetailProductSideBar = () => { 79 | setIsOpen(true); 80 | setType('detail'); 81 | setDetailProduct(details); 82 | }; 83 | 84 | const handleNavigateToDetail = () => { 85 | const path = `/product/${details._id}`; 86 | 87 | navigate(path); 88 | }; 89 | 90 | useEffect(() => { 91 | if (isHomepage) { 92 | setIsShowGrid(true); 93 | } else { 94 | setIsShowGrid(ourShopStore?.isShowGrid); 95 | } 96 | }, [isHomepage, ourShopStore?.isShowGrid]); 97 | 98 | useEffect(() => { 99 | if (slideItem) setIsShowGrid(true); 100 | }, [slideItem]); 101 | 102 | return ( 103 |
109 |
115 | 116 | 117 | 118 |
119 |
120 | 125 |
126 |
127 | 132 |
133 |
134 | 139 |
140 |
144 | 149 |
150 |
151 |
152 | 153 |
159 | {!isHomepage && ( 160 |
161 | {details.size.map((item, index) => { 162 | return ( 163 |
handleChooseSize(item.name)} 169 | > 170 | {item.name} 171 |
172 | ); 173 | })} 174 |
175 | )} 176 | 177 | {sizeChoose && ( 178 |
handleClearSize()}> 179 | clear 180 |
181 | )} 182 | 183 |
188 | {name} 189 |
190 | {!isHomepage && ( 191 |
197 | Brand 01 198 |
199 | )} 200 |
206 | ${price} 207 |
208 | 209 | {!isHomepage && ( 210 |
215 |
226 | )} 227 |
228 |
229 | ); 230 | } 231 | 232 | export default ProductItem; 233 | -------------------------------------------------------------------------------- /src/components/ProductItem/ProductItemLine.jsx: -------------------------------------------------------------------------------- 1 | import styles from './styles.module.scss'; 2 | import reLoadIcon from '@icons/svgs/reloadIcon.svg'; 3 | import heartIcon from '@icons/svgs/heart.svg'; 4 | import cartIcon from '@icons/svgs/cartIcon.svg'; 5 | import cls from 'classnames'; 6 | import Button from '@components/Button/Button'; 7 | 8 | function ProductItemLine({ details }) { 9 | const { 10 | boxImg, 11 | showImgWhenHover, 12 | showFncWhenHover, 13 | boxIcon, 14 | title, 15 | priceCl, 16 | boxSize, 17 | size, 18 | boxBtn, 19 | containerProductLine, 20 | content 21 | } = styles; 22 | 23 | const { size: sizes, images, name, price } = details; 24 | 25 | return ( 26 |
27 |
28 | 29 | 30 | 31 |
32 |
33 | 34 |
35 |
36 | 37 |
38 |
39 | 40 |
41 |
42 | 43 |
44 |
45 |
46 | 47 |
48 |
49 | {sizes.map((item, index) => { 50 | return ( 51 |
52 | {item.name} 53 |
54 | ); 55 | })} 56 |
57 | 58 |
{name}
59 |
Brand 01
60 |
${price}
61 |
62 |
64 |
65 |
66 | ); 67 | } 68 | 69 | export default ProductItemLine; 70 | -------------------------------------------------------------------------------- /src/components/ProductItem/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@styles/mixin.module.scss'; 2 | @import '@styles/variable.module.scss'; 3 | 4 | .boxImg { 5 | width: 300px; 6 | height: 100%; 7 | position: relative; 8 | 9 | img { 10 | width: 100%; 11 | height: 100%; 12 | object-fit: cover; 13 | } 14 | 15 | &:hover .showImgWhenHover { 16 | opacity: 1; 17 | transition: opacity 500ms; 18 | } 19 | 20 | .showImgWhenHover { 21 | opacity: 0; 22 | position: absolute; 23 | top: 0; 24 | left: 0; 25 | right: 0; 26 | bottom: 0; 27 | } 28 | 29 | &:hover .showFncWhenHover { 30 | opacity: 1; 31 | right: 20px; 32 | transition: opacity 500ms, right 300ms; 33 | } 34 | 35 | .showFncWhenHover { 36 | position: absolute; 37 | opacity: 0; 38 | bottom: 20px; 39 | right: 0px; 40 | background-color: white; 41 | } 42 | 43 | .boxIcon { 44 | width: 40px; 45 | height: 40px; 46 | @include flex_box_custom(center, center, 0px); 47 | 48 | cursor: pointer; 49 | 50 | img { 51 | width: 17px; 52 | height: 17px; 53 | } 54 | 55 | &:hover { 56 | background-color: rgb(222, 222, 222); 57 | transition: background-color 500ms; 58 | } 59 | } 60 | } 61 | 62 | .title { 63 | font-size: 16px; 64 | color: $four_color; 65 | margin: 6px 0px; 66 | } 67 | 68 | .textCenter { 69 | text-align: center; 70 | margin: 6px 0px; 71 | } 72 | 73 | .boxSize { 74 | @include flex_box_custom(center, center, 10px); 75 | 76 | .size { 77 | @include flex_box_custom(center, center, 0px); 78 | 79 | font-size: 10px; 80 | padding: 4px 6px; 81 | border: 1px solid #e1e1e1; 82 | cursor: pointer; 83 | } 84 | } 85 | 86 | .boxBtn { 87 | width: 142px; 88 | margin: 10px auto; 89 | } 90 | 91 | .priceCl { 92 | font-size: 14px; 93 | color: $thr_color; 94 | font-weight: 400; 95 | } 96 | 97 | .boxSize { 98 | @include flex_box_custom(center, center, 10px); 99 | 100 | .size { 101 | @include flex_box_custom(center, center, 0px); 102 | 103 | border: 1px solid #e1e1e1; 104 | padding: 4px 6px; 105 | font-size: 10px; 106 | cursor: pointer; 107 | 108 | &:hover { 109 | border: 1px solid $primary_color; 110 | transition: border 500ms; 111 | } 112 | } 113 | } 114 | 115 | .isActiveSize { 116 | border: 1px solid $primary_color !important; 117 | } 118 | 119 | .btnClear { 120 | font-size: 12px; 121 | text-align: center; 122 | cursor: pointer; 123 | margin-top: 10px; 124 | } 125 | 126 | .textCenter { 127 | text-align: center; 128 | } 129 | 130 | .boxBtn { 131 | width: 142px; 132 | margin: 10px auto; 133 | } 134 | 135 | .containerItem { 136 | @include flex_box_custom(flex-start, center, 20px); 137 | margin-top: 10px; 138 | 139 | .content { 140 | @include flex_box_custom(flex-start, flex-start, 10px); 141 | 142 | flex-direction: column; 143 | } 144 | 145 | .leftBtn { 146 | margin: 0px; 147 | } 148 | 149 | .largImg { 150 | width: 430px; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/components/SaleHomepage/SaleHomepage.jsx: -------------------------------------------------------------------------------- 1 | import Button from '@components/Button/Button'; 2 | import styles from './styles.module.scss'; 3 | import useTranslateXImage from '@/hooks/useTranslateXImage'; 4 | 5 | function SaleHomepage() { 6 | const { container, title, des, boxBtn, boxImage } = styles; 7 | const { translateXPosition } = useTranslateXImage(); 8 | 9 | return ( 10 |
11 |
18 | 22 |
23 |
24 |

Sale Of The Year

25 |

26 | Libero sed faucibus facilisis fermentum. Est nibh sed massa 27 | sodales. 28 |

29 | 30 |
31 |
33 |
34 |
41 | 45 |
46 |
47 | ); 48 | } 49 | 50 | export default SaleHomepage; 51 | -------------------------------------------------------------------------------- /src/components/SaleHomepage/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@styles/variable.module.scss'; 2 | @import '@styles/mixin.module.scss'; 3 | 4 | .container { 5 | @include flex_box_custom(space-between, center, 0px); 6 | 7 | margin-top: 100px; 8 | 9 | .title { 10 | font-size: 35px; 11 | color: $four_color; 12 | font-weight: 400; 13 | text-align: center; 14 | } 15 | 16 | .des { 17 | font-size: 16px; 18 | color: $thr_color; 19 | font-weight: 300; 20 | text-align: center; 21 | width: 460px; 22 | line-height: 25px; 23 | } 24 | 25 | .boxBtn { 26 | @include flex_box_custom(center, center, 0px); 27 | width: 172px; 28 | margin: 30px auto; 29 | } 30 | 31 | .boxImage { 32 | @include flex_box_custom(center, center, 0px); 33 | 34 | width: 100%; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/components/Sidebar/Sidebar.jsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import styles from './styles.module.scss'; 3 | import { SideBarContext } from '@/contexts/SideBarProvider'; 4 | import classNames from 'classnames'; 5 | import { TfiClose } from 'react-icons/tfi'; 6 | import Login from '@components/ContentSideBar/Login/Login'; 7 | import Compare from '@components/ContentSideBar/Compare/Compare'; 8 | import WishList from '@components/ContentSideBar/WishList/WishList'; 9 | import Cart from '@components/ContentSideBar/Cart/Cart'; 10 | import DetailProduct from '@components/ContentSideBar/DetailProduct/DetailProduct'; 11 | 12 | function SideBar() { 13 | const { container, overlay, sideBar, slideSideBar, boxIcon } = styles; 14 | const { isOpen, setIsOpen, type } = useContext(SideBarContext); 15 | 16 | const handleToggle = () => { 17 | setIsOpen(!isOpen); 18 | }; 19 | 20 | const handleRenderContent = () => { 21 | switch (type) { 22 | case 'login': 23 | return ; 24 | case 'compare': 25 | return ; 26 | case 'wishlist': 27 | return ; 28 | case 'cart': 29 | return ; 30 | case 'detail': 31 | return ; 32 | 33 | default: 34 | return ; 35 | } 36 | }; 37 | 38 | return ( 39 |
40 |
46 |
51 | {isOpen && ( 52 |
53 | 54 |
55 | )} 56 | 57 | {handleRenderContent()} 58 |
59 |
60 | ); 61 | } 62 | 63 | export default SideBar; 64 | -------------------------------------------------------------------------------- /src/components/Sidebar/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@styles/mixin.module.scss'; 2 | 3 | .container { 4 | position: relative; 5 | 6 | .overlay { 7 | position: fixed; 8 | top: 0; 9 | left: 0; 10 | right: 0; 11 | bottom: 0; 12 | background-color: #0000004d; 13 | z-index: 1000; 14 | 15 | transition: all 0.3s ease; 16 | } 17 | 18 | .sideBar { 19 | position: fixed; 20 | top: 0; 21 | right: 0; 22 | width: 350px; 23 | background-color: #fff; 24 | z-index: 1001; 25 | height: 100%; 26 | 27 | transform: translateX(370px); 28 | transition: all 0.3s ease; 29 | } 30 | 31 | .slideSideBar { 32 | transform: translateX(0); 33 | } 34 | 35 | .boxIcon { 36 | @include flex_box_custom(center, center, 0px); 37 | 38 | position: absolute; 39 | top: 25px; 40 | left: -60px; 41 | width: 35px; 42 | height: 35px; 43 | background-color: #fff; 44 | border-radius: 50%; 45 | transition: all 0.3s ease; 46 | cursor: pointer; 47 | 48 | &:hover { 49 | background-color: #ccc; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/components/SliderCommon/SliderCommon.jsx: -------------------------------------------------------------------------------- 1 | import Slider from 'react-slick'; 2 | import 'slick-carousel/slick/slick.css'; 3 | import 'slick-carousel/slick/slick-theme.css'; 4 | import { MdOutlineArrowBackIosNew } from 'react-icons/md'; 5 | import { MdArrowForwardIos } from 'react-icons/md'; 6 | import './styles.css'; 7 | import ProductItem from '@components/ProductItem/ProductItem'; 8 | 9 | function SliderCommon({ data, isProductItem = false, showItem = 1 }) { 10 | var settings = { 11 | dots: false, 12 | infinite: true, 13 | speed: 500, 14 | slidesToShow: showItem, 15 | slidesToScroll: 1, 16 | nextArrow: , 17 | prevArrow: 18 | }; 19 | 20 | return ( 21 | 22 | {data.map((item, index) => { 23 | const src = item?.images ? item?.images[0] : item.image; 24 | 25 | return ( 26 | <> 27 | {isProductItem ? ( 28 | 37 | ) : ( 38 | test 39 | )} 40 | 41 | ); 42 | })} 43 | 44 | ); 45 | } 46 | 47 | export default SliderCommon; 48 | -------------------------------------------------------------------------------- /src/components/SliderCommon/styles.css: -------------------------------------------------------------------------------- 1 | .slick-slider:hover .slick-arrow { 2 | color: #333; 3 | width: 20px; 4 | height: 20px; 5 | opacity: 1; 6 | transition: all 0.3s ease; 7 | } 8 | 9 | .slick-slider:hover .slick-prev { 10 | left: 10px; 11 | } 12 | 13 | .slick-slider:hover .slick-next { 14 | right: 10px; 15 | } 16 | 17 | .slick-prev { 18 | left: 0px; 19 | z-index: 9999; 20 | } 21 | 22 | .slick-next { 23 | right: 0; 24 | z-index: 9999; 25 | } 26 | -------------------------------------------------------------------------------- /src/contexts/OurShopProvider.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useState } from 'react'; 3 | import { createContext } from 'react'; 4 | import { getProducts } from '@/apis/productsService'; 5 | 6 | export const OurShopContext = createContext(); 7 | 8 | export const OurShopProvider = ({ children }) => { 9 | const sortOptions = [ 10 | { label: 'Default sorting', value: '0' }, 11 | { label: 'Sort by popularity', value: '1' }, 12 | { label: 'Sort by average rating', value: '2' }, 13 | { label: 'Sort by latest', value: '3' }, 14 | { label: 'Sort by price: low to high', value: '4' }, 15 | { label: 'Sort by price: high to low', value: '5' } 16 | ]; 17 | 18 | const showOptions = [ 19 | { label: '8', value: '8' }, 20 | { label: '12', value: '12' }, 21 | { label: 'All', value: 'all' } 22 | ]; 23 | 24 | const [sortId, setSortId] = useState('0'); 25 | const [showId, setShowId] = useState('8'); 26 | const [isShowGrid, setIsShowGrid] = useState(true); 27 | const [products, setProducts] = useState([]); 28 | const [isLoading, setIsLoading] = useState(false); 29 | const [isLoadMore, setIsLoadMore] = useState(false); 30 | const [page, setPage] = useState(1); 31 | const [total, setTotal] = useState(0); 32 | 33 | const handleLoadMore = () => { 34 | const query = { 35 | sortType: sortId, 36 | page: page + 1, 37 | limit: showId 38 | }; 39 | 40 | setIsLoadMore(true); 41 | 42 | getProducts(query) 43 | .then((res) => { 44 | setProducts((prev) => { 45 | return [...prev, ...res.contents]; 46 | }); 47 | setPage(+res.page); 48 | setTotal(res.total); 49 | setIsLoadMore(false); 50 | }) 51 | .catch((err) => { 52 | console.log(err); 53 | setIsLoadMore(false); 54 | }); 55 | }; 56 | 57 | const values = { 58 | sortOptions, 59 | showOptions, 60 | setSortId, 61 | setShowId, 62 | setIsShowGrid, 63 | products, 64 | isShowGrid, 65 | isLoading, 66 | handleLoadMore, 67 | total, 68 | isLoadMore 69 | }; 70 | 71 | useEffect(() => { 72 | const query = { 73 | sortType: sortId, 74 | page: 1, 75 | limit: showId 76 | }; 77 | setIsLoading(true); 78 | getProducts(query) 79 | .then((res) => { 80 | setProducts(res.contents); 81 | setTotal(res.total); 82 | setIsLoading(false); 83 | }) 84 | .catch((err) => { 85 | console.log(err); 86 | setIsLoading(false); 87 | }); 88 | }, [sortId, showId]); 89 | 90 | return ( 91 | 92 | {children} 93 | 94 | ); 95 | }; 96 | -------------------------------------------------------------------------------- /src/contexts/SideBarProvider.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { createContext } from 'react'; 3 | import { getCart } from '@/apis/cartService'; 4 | import { useEffect } from 'react'; 5 | import Cookies from 'js-cookie'; 6 | import { useParams } from 'react-router-dom'; 7 | import { useLocation } from 'react-router-dom'; 8 | 9 | export const SideBarContext = createContext(); 10 | 11 | export const SidebarProvider = ({ children }) => { 12 | const [isOpen, setIsOpen] = useState(false); 13 | const [type, setType] = useState(''); 14 | const [listProductCart, setListProductCart] = useState([]); 15 | const [isLoading, setIsLoading] = useState(false); 16 | const [detailProduct, setDetailProduct] = useState(null); 17 | const userId = Cookies.get('userId'); 18 | 19 | const handleGetListProductsCart = (userId, type) => { 20 | if (userId && type === 'cart') { 21 | setIsLoading(true); 22 | getCart(userId) 23 | .then((res) => { 24 | setListProductCart(res.data.data); 25 | setIsLoading(false); 26 | }) 27 | .catch((err) => { 28 | setListProductCart([]); 29 | setIsLoading(false); 30 | }); 31 | } 32 | }; 33 | 34 | const value = { 35 | isOpen, 36 | setIsOpen, 37 | type, 38 | setType, 39 | handleGetListProductsCart, 40 | listProductCart, 41 | isLoading, 42 | setIsLoading, 43 | userId, 44 | detailProduct, 45 | setDetailProduct, 46 | setListProductCart 47 | }; 48 | 49 | return ( 50 | 51 | {children} 52 | 53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /src/contexts/SteperProvider.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { createContext } from 'react'; 3 | 4 | export const StepperContext = createContext(); 5 | 6 | export const StepperProvider = ({ children }) => { 7 | const [currentStep, setCurrentStep] = useState(1); 8 | 9 | const value = { 10 | currentStep, 11 | setCurrentStep, 12 | }; 13 | 14 | return ( 15 | {children} 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/contexts/ToastProvider.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { createContext } from 'react'; 3 | import { ToastContainer, toast } from 'react-toastify'; 4 | 5 | import 'react-toastify/dist/ReactToastify.css'; 6 | 7 | export const ToastContext = createContext(); 8 | 9 | export const ToastProvider = ({ children }) => { 10 | const value = { 11 | toast 12 | }; 13 | 14 | return ( 15 | 16 | {children} 17 | 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/contexts/storeProvider.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useState } from 'react'; 3 | import { createContext } from 'react'; 4 | import Cookies from 'js-cookie'; 5 | import { getInfo } from '@/apis/authService'; 6 | 7 | export const StoreContext = createContext(); 8 | 9 | export const StoreProvider = ({ children }) => { 10 | const [userInfo, setUserInfo] = useState(null); 11 | const [userId, setUserId] = useState(Cookies.get('userId')); 12 | 13 | const handleLogOut = () => { 14 | Cookies.remove('token'); 15 | Cookies.remove('refreshToken'); 16 | Cookies.remove('userId'); 17 | setUserInfo(null); 18 | window.location.reload(); 19 | }; 20 | 21 | useEffect(() => { 22 | // call api info 23 | if (userId) { 24 | getInfo(userId) 25 | .then((res) => { 26 | setUserInfo(res.data.data); 27 | }) 28 | .catch((err) => { 29 | console.log(err); 30 | }); 31 | } 32 | }, [userId]); 33 | 34 | return ( 35 | 36 | {children} 37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/hooks/useScrollHandling.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useRef } from 'react'; 3 | import { useState } from 'react'; 4 | 5 | const useScrollHandling = () => { 6 | const [scrollDriction, setScrollDrection] = useState(null); 7 | const previousScrollPosition = useRef(0); 8 | const [scrollPosition, setScrollPosition] = useState(0); 9 | 10 | const scrollTracking = () => { 11 | const currentScrollPosition = window.pageYOffset; 12 | 13 | if (currentScrollPosition > previousScrollPosition.current) { 14 | setScrollDrection('down'); 15 | } else if (currentScrollPosition < previousScrollPosition.current) { 16 | setScrollDrection('up'); 17 | } 18 | 19 | previousScrollPosition.current = 20 | currentScrollPosition <= 0 ? 0 : currentScrollPosition; 21 | 22 | setScrollPosition(currentScrollPosition); 23 | }; 24 | 25 | useEffect(() => { 26 | window.addEventListener('scroll', scrollTracking); 27 | 28 | return () => window.removeEventListener('scroll', scrollTracking); 29 | }, []); 30 | 31 | return { 32 | scrollDriction, 33 | scrollPosition 34 | }; 35 | }; 36 | 37 | export default useScrollHandling; 38 | -------------------------------------------------------------------------------- /src/hooks/useTranslateXImage.js: -------------------------------------------------------------------------------- 1 | import useScrollHandling from '@/hooks/useScrollHandling'; 2 | import { useEffect } from 'react'; 3 | import { useState } from 'react'; 4 | 5 | const useTranslateXImage = () => { 6 | const { scrollPosition, scrollDriction } = useScrollHandling(); 7 | const [translateXPosition, setTranslateXPosition] = useState(80); 8 | 9 | const handleTranslateX = () => { 10 | if (scrollDriction === 'down' && scrollPosition >= 1500) { 11 | setTranslateXPosition( 12 | translateXPosition <= 0 ? 0 : translateXPosition - 1 13 | ); 14 | } else if (scrollDriction === 'up') { 15 | setTranslateXPosition( 16 | translateXPosition >= 80 ? 80 : translateXPosition + 1 17 | ); 18 | } 19 | }; 20 | 21 | useEffect(() => { 22 | handleTranslateX(); 23 | }, [scrollPosition]); 24 | 25 | return { 26 | translateXPosition 27 | }; 28 | }; 29 | 30 | export default useTranslateXImage; 31 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | import '@styles/main.scss'; 5 | 6 | ReactDOM.createRoot(document.getElementById('root')).render(); 7 | -------------------------------------------------------------------------------- /src/pages/AboutUs/components/Logos.jsx: -------------------------------------------------------------------------------- 1 | // Import Swiper React components 2 | import { Swiper, SwiperSlide } from 'swiper/react'; 3 | import { Navigation } from 'swiper/modules'; 4 | import { useRef } from 'react'; 5 | 6 | // Import Swiper styles 7 | import 'swiper/css'; 8 | import 'swiper/css/navigation'; 9 | 10 | // Custom styles for the component 11 | const styles = { 12 | container: { 13 | position: 'relative', 14 | marginTop: '80px', 15 | overflow: 'visible', // Đảm bảo nút điều hướng không bị cắt 16 | }, 17 | }; 18 | 19 | // Add custom CSS for hover effect 20 | const customStyles = ` 21 | .logos-slider .swiper-button-next, 22 | .logos-slider .swiper-button-prev { 23 | opacity: 0; 24 | transition: opacity 0.3s ease; 25 | color: black; 26 | width: 44px; 27 | height: 44px; 28 | margin-top: -22px; 29 | } 30 | 31 | .logos-slider:hover .swiper-button-next, 32 | .logos-slider:hover .swiper-button-prev { 33 | opacity: 1; 34 | } 35 | 36 | .logos-slider .swiper-button-next { 37 | right: -20px; /* Đặt nút next xa hơn về bên phải */ 38 | } 39 | 40 | .logos-slider .swiper-button-prev { 41 | left: -20px; /* Đặt nút prev xa hơn về bên trái */ 42 | } 43 | 44 | .logos-slider .swiper-button-next::after, 45 | .logos-slider .swiper-button-prev::after { 46 | font-size: 24px; 47 | } 48 | `; 49 | 50 | function Logos() { 51 | const dataLogos = [ 52 | { 53 | id: '1', 54 | img: 'https://xstore.b-cdn.net/elementor2/marseille04/wp-content/uploads/sites/2/2024/04/brand-01-min.png', 55 | }, 56 | { 57 | id: '2', 58 | img: 'https://xstore.b-cdn.net/elementor2/marseille04/wp-content/uploads/sites/2/2024/04/brand-01-min.png', 59 | }, 60 | { 61 | id: '3', 62 | img: 'https://xstore.b-cdn.net/elementor2/marseille04/wp-content/uploads/sites/2/2024/04/brand-01-min.png', 63 | }, 64 | { 65 | id: '4', 66 | img: 'https://xstore.b-cdn.net/elementor2/marseille04/wp-content/uploads/sites/2/2024/04/brand-01-min.png', 67 | }, 68 | { 69 | id: '5', 70 | img: 'https://xstore.b-cdn.net/elementor2/marseille04/wp-content/uploads/sites/2/2024/04/brand-01-min.png', 71 | }, 72 | { 73 | id: '6', 74 | img: 'https://xstore.b-cdn.net/elementor2/marseille04/wp-content/uploads/sites/2/2024/04/brand-01-min.png', 75 | }, 76 | { 77 | id: '7', 78 | img: 'https://xstore.b-cdn.net/elementor2/marseille04/wp-content/uploads/sites/2/2024/04/brand-01-min.png', 79 | }, 80 | { 81 | id: '8', 82 | img: 'https://xstore.b-cdn.net/elementor2/marseille04/wp-content/uploads/sites/2/2024/04/brand-01-min.png', 83 | }, 84 | ]; 85 | 86 | return ( 87 |
88 | {/* Add style tag for custom CSS */} 89 | 90 | 91 | console.log('slide change')} 95 | onSwiper={(swiper) => console.log(swiper)} 96 | navigation={true} 97 | modules={[Navigation]} 98 | className="logos-slider" // Add class for targeting in CSS 99 | > 100 | {dataLogos.map((item) => ( 101 | 109 | 117 | 118 | ))} 119 | 120 |
121 | ); 122 | } 123 | 124 | export default Logos; 125 | -------------------------------------------------------------------------------- /src/pages/AboutUs/index.jsx: -------------------------------------------------------------------------------- 1 | import MyFooter from '@components/Footer/Footer'; 2 | import MyHeader from '@components/Header/Header'; 3 | import MainLayout from '@components/Layout/Layout'; 4 | import styles from './styles.module.scss'; 5 | import Logos from '@/pages/AboutUs/components/Logos'; 6 | 7 | function AboutUs() { 8 | const { 9 | container, 10 | functionBox, 11 | specialText, 12 | btnBack, 13 | containerTitle, 14 | line, 15 | title, 16 | textS, 17 | textL, 18 | containerContent, 19 | des, 20 | } = styles; 21 | 22 | const dataContents = [ 23 | { 24 | id: '1', 25 | url: 'https://xstore.b-cdn.net/elementor2/marseille04/wp-content/uploads/sites/2/2022/12/Image-copy-min.jpg', 26 | des: 'Ac eget cras augue nisi neque lacinia in aliquam. Odio pellentesque sed ultrices dolor amet nunc habitasse proin consec. tur feugiat egestas eget.', 27 | }, 28 | { 29 | id: '2', 30 | url: 'https://xstore.b-cdn.net/elementor2/marseille04/wp-content/uploads/sites/2/2022/12/Image-copy-2-min.jpg', 31 | des: 'Ac eget cras augue nisi neque lacinia in aliquam. Odio pellentesque sed ultrices dolor amet nunc habitasse proin consec. tur feugiat egestas eget.', 32 | }, 33 | { 34 | id: '3', 35 | url: 'http://xstore.b-cdn.net/elementor2/marseille04/wp-content/uploads/sites/2/2022/12/Image-min.jpg', 36 | des: 'Ac eget cras augue nisi neque lacinia in aliquam. Odio pellentesque sed ultrices dolor amet nunc habitasse proin consec. tur feugiat egestas eget.', 37 | }, 38 | ]; 39 | 40 | return ( 41 | <> 42 | 43 | 44 | 45 |
46 |
47 |
48 | Home > About us 49 |
50 |
handleBackPreviousPage()}> 51 | < Return to previous page 52 |
53 |
54 | 55 |
56 |
57 |
58 |
we try our best for you
59 |
Welcome to the Marseille04 Shop
60 |
61 |
62 |
63 | 64 |
65 | {dataContents.map((item) => ( 66 |
67 | 68 |
{item.des}
69 |
70 | ))} 71 |
72 | 73 | 74 |
75 |
76 | 77 | 78 | 79 | ); 80 | } 81 | 82 | export default AboutUs; 83 | -------------------------------------------------------------------------------- /src/pages/AboutUs/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@styles/variable.module.scss'; 2 | @import '@styles/mixin.module.scss'; 3 | 4 | .container { 5 | padding-top: 83px; 6 | height: 100vh; 7 | 8 | .functionBox { 9 | @include flex_box_custom(space-between, center, 0px); 10 | color: $seconddary_color; 11 | font-size: 14px; 12 | font-weight: 300; 13 | 14 | .specialText { 15 | color: $primary_color; 16 | font-weight: 400; 17 | } 18 | 19 | .btnBack { 20 | cursor: pointer; 21 | } 22 | } 23 | 24 | .containerTitle { 25 | margin-top: 66px; 26 | 27 | .line { 28 | width: 100%; 29 | height: 2px; 30 | border-top: 1px solid #e1e1e1; 31 | border-bottom: 1px solid #e1e1e1; 32 | position: relative; 33 | } 34 | 35 | .title { 36 | position: absolute; 37 | top: -30px; 38 | left: 50%; 39 | transform: translateX(-50%); 40 | text-align: center; 41 | background-color: white; 42 | padding: 0px 60px; 43 | 44 | .textS { 45 | font-size: 14px; 46 | color: $thr_color; 47 | text-transform: uppercase; 48 | } 49 | 50 | .textL { 51 | font-size: 24px; 52 | color: $primary_color; 53 | } 54 | } 55 | } 56 | 57 | .containerContent { 58 | display: flex; 59 | gap: 30px; 60 | margin-top: 70px; 61 | 62 | img { 63 | width: 100%; 64 | height: 390px; 65 | } 66 | 67 | .des { 68 | font-size: 16px; 69 | color: $thr_color; 70 | margin-top: 25px; 71 | font-weight: 300; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/pages/Cart/Cart.jsx: -------------------------------------------------------------------------------- 1 | import { StepperProvider } from '@/contexts/SteperProvider'; 2 | import Steps from '@/pages/Cart/components/steps/Steps'; 3 | import MyFooter from '@components/Footer/Footer'; 4 | import MyHeader from '@components/Header/Header'; 5 | import MainLayout from '@components/Layout/Layout'; 6 | import ContentStep from '@/pages/Cart/components/ContentStep'; 7 | import styles from './styles.module.scss'; 8 | 9 | function Cart() { 10 | const { container } = styles; 11 | 12 | return ( 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 |
21 | 22 |
23 | ); 24 | } 25 | 26 | export default Cart; 27 | -------------------------------------------------------------------------------- /src/pages/Cart/components/Checkout/Checkout.jsx: -------------------------------------------------------------------------------- 1 | import { createOrder } from '@/apis/orderService'; 2 | import RightBody from '@/pages/Cart/components/Checkout/RightBody'; 3 | import InputCustom from '@components/InputCommon2/Input'; 4 | import axios from 'axios'; 5 | import cls from 'classnames'; 6 | import { useEffect, useRef, useState } from 'react'; 7 | import { useForm } from 'react-hook-form'; 8 | import styles from './Styles.module.scss'; 9 | import { useNavigate } from 'react-router-dom'; 10 | 11 | const CN_BASE = 'https://countriesnow.space/api/v0.1'; 12 | 13 | function Checkout() { 14 | const dataOptions = [ 15 | { value: '1', label: 'Option 1' }, 16 | { value: '2', label: 'Option 2' }, 17 | { value: '3', label: 'Option 3' }, 18 | ]; 19 | 20 | const { container, title, coupon, leftBody, rightBody, row, row2Column } = 21 | styles; 22 | 23 | const [countries, setCountries] = useState([]); 24 | const [cities, setCities] = useState([]); 25 | const [states, setStates] = useState([]); 26 | const navigate = useNavigate(); 27 | 28 | const { 29 | register, 30 | handleSubmit, 31 | watch, 32 | formState: { errors }, 33 | } = useForm(); 34 | const formRef = useRef(); 35 | 36 | const handleExternalSubmit = () => { 37 | formRef.current.requestSubmit(); // hoặc formRef.current.dispatchEvent(new Event('submit')) 38 | }; 39 | 40 | const onSubmit = async (data) => { 41 | try { 42 | const res = await createOrder(data); 43 | navigate( 44 | `/order?id=${res.data.data._id}&totalAmount=${res.data.data.totalAmount}` 45 | ); 46 | } catch (error) { 47 | console.log(error); 48 | } 49 | }; 50 | 51 | useEffect(() => { 52 | axios.get(`${CN_BASE}/countries/iso`).then((res) => 53 | setCountries( 54 | res.data.data.map((c) => ({ 55 | value: c.name, 56 | label: c.name, 57 | })) 58 | ) 59 | ); 60 | }, []); 61 | 62 | useEffect(() => { 63 | if (!watch('country')) return; 64 | 65 | if (watch('country') === 'Vietnam' && !localStorage.getItem('listCities')) { 66 | axios.get('https://provinces.open-api.vn/api/?depth=2').then((res) => { 67 | localStorage.setItem('listCities', JSON.stringify(res.data)); 68 | 69 | setCities( 70 | res.data.data.map((item) => ({ 71 | label: item.name, 72 | value: item.codename, 73 | })) 74 | ); 75 | }); 76 | 77 | return; 78 | } 79 | 80 | if (localStorage.getItem('listCities')) { 81 | const data = JSON.parse(localStorage.getItem('listCities')); 82 | setCities( 83 | data.map((item) => ({ 84 | label: item.name, 85 | value: item.codename, 86 | })) 87 | ); 88 | } 89 | }, [watch('country')]); 90 | 91 | useEffect(() => { 92 | if (!watch('cities')) return; 93 | 94 | if (localStorage.getItem('listCities')) { 95 | const data = JSON.parse(localStorage.getItem('listCities')); 96 | const statesCustom = data 97 | .find((item) => item.codename === watch('cities')) 98 | .districts.map((item) => ({ 99 | label: item.name, 100 | value: item.codename, 101 | })); 102 | 103 | setStates(statesCustom); 104 | } 105 | }, [watch('cities')]); 106 | 107 | return ( 108 |
109 |
110 |

111 | Have a coupon? Click here to enter 112 |

113 | 114 |

BILLING DETAILS

115 | 116 |
117 |
118 | 128 | 138 |
139 | 140 |
141 | 147 |
148 | 149 |
150 | 159 |
160 | 161 |
162 | 171 |
172 | 173 |
174 | 180 |
181 | 182 |
183 | 192 |
193 | 194 |
195 | 204 |
205 | 206 |
207 | 216 |
217 | 218 |
219 | 228 |
229 | 230 |
231 | 240 |
241 | 242 | {/* */} 243 |
244 |
245 | 246 | 247 |
248 | ); 249 | } 250 | 251 | export default Checkout; 252 | -------------------------------------------------------------------------------- /src/pages/Cart/components/Checkout/RightBody.jsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import styles from './Styles.module.scss'; 3 | import { SideBarContext } from '@/contexts/SideBarProvider'; 4 | import Button from '@components/Button/Button'; 5 | import PaymentMethods from '@components/PaymentMethods/PaymentMethods'; 6 | import { handleTotalPrice } from '@/utils/helper'; 7 | 8 | function RightBody({ handleExternalSubmit }) { 9 | const { rightBody, title, items, item, total, subTotal, payment, btn } = 10 | styles; 11 | 12 | const { listProductCart } = useContext(SideBarContext); 13 | 14 | return ( 15 |
16 |

YOUR ORDER

17 | 18 |
19 | {listProductCart.map((product) => ( 20 |
21 | 22 | 23 |
24 |

{product.name}

25 |

Price: {product.price}

26 |

Size: {product.size}

27 |
28 |
29 | ))} 30 |
31 | 32 |
33 |

Subtotal

34 |

${handleTotalPrice(listProductCart).toFixed(2)}

35 |
36 | 37 |
38 |

TOTAL

39 |

${handleTotalPrice(listProductCart).toFixed(2)}

40 |
41 | 42 |
43 |  {' '} 44 | 45 |
46 | 47 |
48 |  {' '} 49 | 50 |
51 | 52 |
53 |
55 | 56 | 57 |
58 | ); 59 | } 60 | 61 | export default RightBody; 62 | -------------------------------------------------------------------------------- /src/pages/Cart/components/Checkout/Styles.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | width: 100%; 4 | gap: 20px; 5 | 6 | .leftBody { 7 | flex-grow: 1; 8 | 9 | .coupon { 10 | font-size: 14px; 11 | color: #222; 12 | 13 | span { 14 | text-decoration: underline; 15 | cursor: pointer; 16 | } 17 | } 18 | 19 | .title { 20 | font-size: 14px; 21 | color: #333; 22 | } 23 | 24 | .row { 25 | margin-bottom: 20px; 26 | } 27 | 28 | .row2Column { 29 | display: flex; 30 | gap: 20px; 31 | } 32 | } 33 | 34 | .rightBody { 35 | max-width: 500px; 36 | width: 100%; 37 | height: 600px; 38 | border: 2px solid #333; 39 | padding: 15px 25px; 40 | 41 | .items { 42 | height: 85px; 43 | overflow: scroll; 44 | border-bottom: 1px solid #ccc; 45 | border-top: 1px solid #ccc; 46 | padding: 20px 0; 47 | 48 | .item { 49 | display: flex; 50 | gap: 10px; 51 | align-items: flex-start; 52 | margin-bottom: 10px; 53 | 54 | img { 55 | width: 70px; 56 | height: 80px; 57 | } 58 | 59 | p { 60 | margin: 0 0 10px 0; 61 | color: #555; 62 | } 63 | } 64 | } 65 | 66 | .title { 67 | font-size: 16px; 68 | color: #333; 69 | 70 | padding-bottom: 8px; 71 | margin-bottom: 8px; 72 | } 73 | 74 | .subTotal { 75 | display: flex; 76 | justify-content: space-between; 77 | color: #888; 78 | margin-top: 8px; 79 | 80 | p:nth-child(2) { 81 | font-size: 14px; 82 | } 83 | } 84 | 85 | .total { 86 | display: flex; 87 | justify-content: space-between; 88 | color: #222; 89 | font-size: 20px; 90 | } 91 | 92 | .payment { 93 | border-top: 1px solid #ccc; 94 | padding-top: 25px; 95 | margin-bottom: 10px; 96 | 97 | label { 98 | color: #222; 99 | } 100 | } 101 | 102 | .btn { 103 | margin-top: 30px; 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/pages/Cart/components/ContentStep.jsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { StepperContext } from '@/contexts/SteperProvider'; 3 | import Contents from '@/pages/Cart/components/contents/Contents'; 4 | import Checkout from '@/pages/Cart/components/Checkout/Checkout'; 5 | 6 | function ContentStep() { 7 | const { currentStep } = useContext(StepperContext); 8 | 9 | const handleRenderContent = () => { 10 | switch (currentStep) { 11 | case 1: 12 | return ; 13 | case 2: 14 | return ( 15 | <> 16 | 17 | 18 | ); 19 | case 3: 20 | return <>step 3; 21 | } 22 | }; 23 | return <>{handleRenderContent()}; 24 | } 25 | 26 | export default ContentStep; 27 | -------------------------------------------------------------------------------- /src/pages/Cart/components/Loading.jsx: -------------------------------------------------------------------------------- 1 | import LoadingTextCommon from '@components/LoadingTextCommon/LoadingTextCommon'; 2 | import styles from '../styles.module.scss'; 3 | 4 | function LoadingCart() { 5 | const { loadingCart } = styles; 6 | return ( 7 |
8 | 9 |
10 | ); 11 | } 12 | 13 | export default LoadingCart; 14 | -------------------------------------------------------------------------------- /src/pages/Cart/components/contents/CartSummary.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from '../../styles.module.scss'; 3 | import Button from '@components/Button/Button'; 4 | import cls from 'classnames'; 5 | import { useContext } from 'react'; 6 | import { SideBarContext } from '@/contexts/SideBarProvider'; 7 | import LoadingCart from '@/pages/Cart/components/Loading'; 8 | import PaymentMethods from '@components/PaymentMethods/PaymentMethods'; 9 | import { StepperContext } from '@/contexts/SteperProvider'; 10 | import { handleTotalPrice } from '@/utils/helper'; 11 | 12 | const CartSummary = () => { 13 | const { 14 | containerSummary, 15 | title, 16 | boxTotal, 17 | price, 18 | subTotal, 19 | totals, 20 | space, 21 | containerRight, 22 | } = styles; 23 | const { listProductCart, isLoading } = useContext(SideBarContext); 24 | const { setCurrentStep } = useContext(StepperContext); 25 | 26 | const handleProcessCheckout = () => { 27 | setCurrentStep(2); 28 | }; 29 | 30 | return ( 31 |
32 |
33 |
CART TOTALS
34 | 35 |
36 |
Subtotal
37 |
38 | ${handleTotalPrice(listProductCart).toFixed(2)} 39 |
40 |
41 | 42 |
43 |
TOTAL
44 |
${handleTotalPrice(listProductCart).toFixed(2)}
45 |
46 | 47 |
56 | 57 | 58 |
59 | ); 60 | }; 61 | 62 | export default CartSummary; 63 | -------------------------------------------------------------------------------- /src/pages/Cart/components/contents/CartTable.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from '../../styles.module.scss'; 3 | import SelectBox from '@/pages/OurShop/components/SelectBox'; 4 | import LoadingCart from '@/pages/Cart/components/Loading'; 5 | 6 | const CartTable = ({ listProductCart, getData, isLoading, getDataDelete }) => { 7 | const { cartTable } = styles; 8 | 9 | const handleQuantityChange = (id, newQuantity) => { 10 | console.log('Update item:', id, 'to quantity:', newQuantity); 11 | }; 12 | 13 | const showOptions = [ 14 | { label: '1', value: '1' }, 15 | { label: '2', value: '2' }, 16 | { label: '3', value: '3' }, 17 | { label: '4', value: '4' }, 18 | { label: '5', value: '5' }, 19 | { label: '6', value: '6' }, 20 | { label: '7', value: '7' } 21 | ]; 22 | 23 | const getValueSelect = (userId, productId, quantity, size) => { 24 | const data = { 25 | userId, 26 | productId, 27 | quantity, 28 | size, 29 | isMultiple: true 30 | }; 31 | 32 | getData(data); 33 | }; 34 | 35 | return ( 36 |
37 | 38 | 39 | 40 | 41 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | {listProductCart.map((item) => ( 50 | 51 | 58 | 73 | 74 | 75 | 90 | 91 | 92 | ))} 93 | 94 |
PRODUCT 42 | PRICESKUQUANTITYSUBTOTAL
52 | {item.name} 53 |
54 |

{item.name}

55 |

Size: {item.size}

56 |
57 |
59 |
61 | getDataDelete({ 62 | userId: item.userId, 63 | productId: item.productId 64 | }) 65 | } 66 | style={{ 67 | cursor: 'pointer' 68 | }} 69 | > 70 | 🗑 71 |
72 |
${item.price.toFixed(2)}{item.sku} 76 | 79 | getValueSelect( 80 | item.userId, 81 | item.productId, 82 | e, 83 | item.size 84 | ) 85 | } 86 | type='show' 87 | defaultValue={item.quantity} 88 | /> 89 | ${(item.price * item.quantity).toFixed(2)}
95 | 96 | {isLoading && } 97 |
98 | ); 99 | }; 100 | 101 | export default CartTable; 102 | -------------------------------------------------------------------------------- /src/pages/Cart/components/contents/Contents.jsx: -------------------------------------------------------------------------------- 1 | import CartTable from '@/pages/Cart/components/contents/CartTable'; 2 | import styles from '../../styles.module.scss'; 3 | import CartSummary from '@/pages/Cart/components/contents/CartSummary'; 4 | import Button from '@components/Button/Button'; 5 | import { useContext } from 'react'; 6 | import { SideBarContext } from '@/contexts/SideBarProvider'; 7 | import { addProductToCart, deleteItem, deleteCart } from '@/apis/cartService'; 8 | import { PiShoppingCartLight } from 'react-icons/pi'; 9 | import { useNavigate } from 'react-router-dom'; 10 | import { useEffect } from 'react'; 11 | import { getCart } from '@/apis/cartService'; 12 | 13 | function Contents() { 14 | const { 15 | containerContents, 16 | boxFooter, 17 | boxBtnDelete, 18 | boxCoupon, 19 | boxEmptyCart, 20 | titleEmpty, 21 | boxBtnEmpty 22 | } = styles; 23 | const { 24 | listProductCart, 25 | handleGetListProductsCart, 26 | isLoading, 27 | setIsLoading, 28 | userId, 29 | setListProductCart 30 | } = useContext(SideBarContext); 31 | const navigate = useNavigate(); 32 | 33 | const handleReplaceQuantity = (data) => { 34 | setIsLoading(true); 35 | addProductToCart(data) 36 | .then((res) => { 37 | handleGetListProductsCart(data.userId, 'cart'); 38 | }) 39 | .catch((err) => { 40 | setIsLoading(false); 41 | console.log(err); 42 | }); 43 | }; 44 | 45 | const handleDeleteItemCart = (data) => { 46 | setIsLoading(true); 47 | deleteItem(data) 48 | .then((res) => { 49 | handleGetListProductsCart(data.userId, 'cart'); 50 | }) 51 | .catch((err) => { 52 | setIsLoading(false); 53 | console.log(err); 54 | }); 55 | }; 56 | 57 | const handleDeleteCart = () => { 58 | setIsLoading(true); 59 | deleteCart({ userId }) 60 | .then((res) => { 61 | handleGetListProductsCart(userId, 'cart'); 62 | }) 63 | .catch((err) => { 64 | console.log(err); 65 | }); 66 | }; 67 | 68 | const handleNavigateToShop = () => { 69 | navigate('/shop'); 70 | }; 71 | 72 | useEffect(() => { 73 | if (userId) { 74 | getCart(userId) 75 | .then((res) => { 76 | setListProductCart(res.data.data); 77 | setIsLoading(false); 78 | }) 79 | .catch((err) => { 80 | setListProductCart([]); 81 | setIsLoading(false); 82 | }); 83 | } 84 | }, []); 85 | 86 | return ( 87 | <> 88 | {listProductCart.length > 0 && userId ? ( 89 |
90 |
95 | 101 | 102 |
103 |
104 | 105 |
107 | 108 |
109 |
112 | } 113 | isPriamry={false} 114 | onClick={handleDeleteCart} 115 | /> 116 |
117 |
118 |
119 | 120 | 121 |
122 | ) : ( 123 |
124 | 129 |
130 | YOUR SHOPPING CART IS EMPTY 131 |
132 |
133 | We invite you to get acquainted with an assortment of 134 | our shop. Surely you can find something for yourself! 135 |
136 |
137 |
142 |
143 | )} 144 | 145 | ); 146 | } 147 | 148 | export default Contents; 149 | -------------------------------------------------------------------------------- /src/pages/Cart/components/steps/Stepper.jsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import styles from '../../styles.module.scss'; 3 | import cls from 'classnames'; 4 | import { StepperContext } from '@/contexts/SteperProvider'; 5 | 6 | function Stepper({ number, content, isDisabled }) { 7 | const { stepper, numberStep, textStep, isDisableNumber, isDisableText } = 8 | styles; 9 | 10 | const { setCurrentStep } = useContext(StepperContext); 11 | 12 | return ( 13 |
setCurrentStep(number)}> 14 |
15 | {number} 16 |
17 |
18 | {content} 19 |
20 |
21 | ); 22 | } 23 | 24 | export default Stepper; 25 | -------------------------------------------------------------------------------- /src/pages/Cart/components/steps/Steps.jsx: -------------------------------------------------------------------------------- 1 | import Stepper from '@/pages/Cart/components/steps/Stepper'; 2 | import styles from '../../styles.module.scss'; 3 | import { useContext } from 'react'; 4 | import { StepperContext } from '@/contexts/SteperProvider'; 5 | 6 | function Steps() { 7 | const { containerSteps, steps, line, textNoti } = styles; 8 | const { currentStep } = useContext(StepperContext); 9 | 10 | const dataSteps = [ 11 | { number: 1, content: 'Shopping cart' }, 12 | { number: 2, content: 'Checkout' }, 13 | { number: 3, content: 'Order status' }, 14 | ]; 15 | 16 | return ( 17 |
18 |
19 | {dataSteps.map((item, index) => { 20 | return ( 21 | <> 22 | = currentStep} 27 | /> 28 | {index !== dataSteps.length - 1 &&
} 29 | 30 | ); 31 | })} 32 |
33 | 34 |
35 | You are out of time! Checkout now to avoid losing your order! 36 |
37 |
38 | ); 39 | } 40 | 41 | export default Steps; 42 | -------------------------------------------------------------------------------- /src/pages/Cart/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@styles/variable.module.scss'; 2 | @import '@styles/mixin.module.scss'; 3 | 4 | .container { 5 | padding-top: 83px; 6 | } 7 | 8 | .containerSteps { 9 | @include flex_box_custom(center, center, 0px); 10 | 11 | background-color: #fafafa; 12 | padding: 2.3vw 0; 13 | flex-direction: column; 14 | margin-bottom: 2.3vw; 15 | 16 | .steps { 17 | @include flex_box_custom(center, center, 30px); 18 | 19 | .line { 20 | min-width: 120px; 21 | height: 1px; 22 | background-color: #e1e1e1; 23 | } 24 | } 25 | 26 | .textNoti { 27 | font-size: 16px; 28 | color: $four_color; 29 | font-weight: 400; 30 | padding-top: 35px; 31 | } 32 | 33 | .stepper { 34 | @include flex_box_custom(center, center, 10px); 35 | text-transform: uppercase; 36 | 37 | .numberStep { 38 | @include flex_box_custom(center, center, 0px); 39 | background-color: #333; 40 | width: 28px; 41 | height: 28px; 42 | border-radius: 50%; 43 | color: $white_color; 44 | } 45 | 46 | .isDisableNumber { 47 | background-color: transparent; 48 | border: 1px solid #e1e1e1; 49 | color: #888; 50 | } 51 | 52 | .textStep { 53 | color: $four_color; 54 | font-size: 21px; 55 | font-weight: 300; 56 | } 57 | 58 | .isDisableText { 59 | color: #9a9a9a; 60 | } 61 | } 62 | } 63 | 64 | .containerContents { 65 | @include flex_box_custom(space-between, flex-start, 30px); 66 | 67 | .cartTable { 68 | // width: 100%; 69 | margin: 0 auto; 70 | padding-bottom: 15px; 71 | border-bottom: 1px solid #e1e1e1; 72 | 73 | position: relative; 74 | 75 | table { 76 | width: 100%; 77 | border-collapse: collapse; 78 | text-align: left; 79 | 80 | thead { 81 | tr { 82 | th { 83 | padding: 10px 15px; 84 | border-bottom: 1px solid #ccc; 85 | font-size: 14px; 86 | font-weight: 300; 87 | text-align: center; 88 | } 89 | 90 | th:first-child { 91 | padding-left: 0; 92 | text-align: left; 93 | } 94 | 95 | th:last-child { 96 | padding-right: 0; 97 | text-align: right; 98 | } 99 | } 100 | } 101 | 102 | tbody { 103 | tr { 104 | td { 105 | padding: 15px; 106 | vertical-align: top; 107 | color: $seconddary_color; 108 | font-size: 14px; 109 | font-weight: 300; 110 | 111 | &:nth-child(1) { 112 | display: flex; 113 | padding-left: 0; 114 | color: $four_color; 115 | font-size: 16px; 116 | 117 | img { 118 | width: calc(100px - 0.71em); 119 | margin-right: 10px; 120 | border-radius: 2px; 121 | } 122 | 123 | div { 124 | flex-grow: 1; 125 | 126 | p:first-child { 127 | margin: 0; 128 | } 129 | 130 | p:last-child { 131 | font-size: 0.9em; 132 | color: #777; 133 | } 134 | } 135 | } 136 | &:nth-child(4) { 137 | color: $thr_color; 138 | } 139 | 140 | &:nth-child(5) { 141 | select { 142 | width: 100%; 143 | padding: 8px; 144 | border: 1px solid #ccc; 145 | border-radius: 1px; 146 | color: $seconddary_color; 147 | } 148 | } 149 | 150 | &:last-child { 151 | text-align: right; 152 | padding-right: 0; 153 | } 154 | } 155 | } 156 | } 157 | } 158 | } 159 | 160 | .containerRight { 161 | // flex-grow: 1; 162 | width: 40%; 163 | } 164 | 165 | .containerSummary { 166 | padding: 25px; 167 | border: 2px solid #333; 168 | position: relative; 169 | 170 | .title { 171 | font-size: 14px; 172 | padding-bottom: 15px; 173 | border-bottom: 1px solid #e1e1e1; 174 | color: $primary_color; 175 | } 176 | 177 | .boxTotal { 178 | @include flex_box_custom(space-between, center, 0px); 179 | margin: 25px 0; 180 | } 181 | 182 | .subTotal { 183 | color: $thr_color; 184 | 185 | .price { 186 | color: $seconddary_color; 187 | font-weight: 300; 188 | font-size: 14px; 189 | } 190 | } 191 | 192 | .totals { 193 | color: $four_color; 194 | font-size: 20px; 195 | } 196 | 197 | .space { 198 | height: 10px; 199 | } 200 | } 201 | 202 | .boxFooter { 203 | @include flex_box_custom(space-between, center, 0px); 204 | margin-top: 15px; 205 | 206 | .boxCoupon { 207 | @include flex_box_custom(flex-start, center, 0px); 208 | 209 | input { 210 | border: 1px solid #e1e1e1; 211 | padding: 8px 15px; 212 | width: 300px; 213 | outline: none; 214 | } 215 | 216 | button { 217 | height: 36px; 218 | border-radius: 0px; 219 | padding: 0px 10px; 220 | font-size: 12px; 221 | width: 36px; 222 | } 223 | } 224 | 225 | .boxBtnDelete { 226 | button { 227 | height: 36px; 228 | font-size: 12px; 229 | padding: 0px 30px; 230 | } 231 | } 232 | } 233 | } 234 | 235 | .boxEmptyCart { 236 | @include flex_box_custom(center, center, 20px); 237 | flex-direction: column; 238 | 239 | width: 100%; 240 | font-size: 14px; 241 | color: $thr_color; 242 | 243 | .titleEmpty { 244 | font-size: 24px; 245 | color: $primary_color; 246 | } 247 | 248 | .boxBtnEmpty { 249 | button { 250 | padding: 0 20px; 251 | font-size: 12px; 252 | } 253 | } 254 | } 255 | 256 | .loadingCart { 257 | @include flex_box_custom(center, center, 30px); 258 | 259 | position: absolute; 260 | top: 0; 261 | left: 0; 262 | right: 0; 263 | bottom: 0; 264 | background-color: rgba(255, 255, 255, 0.5); 265 | } 266 | -------------------------------------------------------------------------------- /src/pages/DetailProduct/components/FormItem.jsx: -------------------------------------------------------------------------------- 1 | import { GoStarFill } from 'react-icons/go'; 2 | import styles from '../styles.module.scss'; 3 | 4 | function FormItem({ label, isRequired, typeChildren }) { 5 | const { formItem, boxItemStar } = styles; 6 | 7 | const renderStar = (length) => { 8 | return Array.from({ length }, (_, index) => ( 9 | 15 | )); 16 | }; 17 | 18 | const renderChildren = () => { 19 | switch (typeChildren) { 20 | case 'rating': 21 | return ( 22 |
23 |
{renderStar(1)}
24 |
{renderStar(2)}
25 |
{renderStar(3)}
26 |
{renderStar(4)}
27 |
{renderStar(5)}
28 |
29 | ); 30 | case 'input': 31 | return ; 32 | case 'textarea': 33 | return