diff --git a/package-lock.json b/package-lock.json index 582421d..28bca40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "bootstrap": "^5.3.0", + "bootstrap-icons": "^1.10.5", "react": "^18.2.0", "react-bootstrap": "^2.8.0", "react-dom": "^18.2.0", @@ -5871,6 +5872,21 @@ "@popperjs/core": "^2.11.7" } }, + "node_modules/bootstrap-icons": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.10.5.tgz", + "integrity": "sha512-oSX26F37V7QV7NCE53PPEL45d7EGXmBgHG3pDpZvcRaKVzWMqIRL9wcqJUyEha1esFtM3NJzvmxFXDxjJYD0jQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ] + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -16798,16 +16814,16 @@ } }, "node_modules/typescript": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.3.tgz", - "integrity": "sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==", + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=14.17" + "node": ">=4.2.0" } }, "node_modules/unbox-primitive": { diff --git a/package.json b/package.json index 657f1fb..516bfbd 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "bootstrap": "^5.3.0", + "bootstrap-icons": "^1.10.5", "react": "^18.2.0", "react-bootstrap": "^2.8.0", "react-dom": "^18.2.0", diff --git a/src/App.js b/src/App.js index 4fff9a5..e81fe03 100644 --- a/src/App.js +++ b/src/App.js @@ -3,7 +3,7 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom"; import Root from "./components/Layout/Root"; import Error from "./pages/Error"; import HomePage, { loader as homeLoader } from "./pages/HomePage"; -import ShopPage from "./pages/ShopPage"; +import ShopPage, { loader as shopLoader } from "./pages/ShopPage"; import DetailPage from "./pages/DetailPage"; import CartPage from "./pages/CartPage"; import CheckoutPage from "./pages/CheckoutPage"; @@ -17,7 +17,7 @@ const router = createBrowserRouter([ errorElement: , children: [ { index: true, element: , loader: homeLoader }, - { path: "shop", element: }, + { path: "shop", element: , loader: shopLoader }, { path: "detail/:productId", element: }, { path: "cart", element: }, { path: "checkout", element: }, diff --git a/src/components/ListOfProducts/ListOfProducts.js b/src/components/ListOfProducts/ListOfProducts.js index 951da45..b156edf 100644 --- a/src/components/ListOfProducts/ListOfProducts.js +++ b/src/components/ListOfProducts/ListOfProducts.js @@ -1,26 +1,46 @@ -import React, { useState } from "react"; +import React, { Fragment, useState } from "react"; +import { useSelector, useDispatch } from "react-redux"; import Row from "react-bootstrap/Row"; import classes from "./ListOfProducts.module.scss"; import ProductItem from "./ProductItem"; +import ProductDetail from "../Products/ProductDetail"; const ListOfProducts = ({ data }) => { + const dispatch = useDispatch(); + const isClose = useSelector((state) => state.onClose); + const [detail, setDetail] = useState(null); + + if (detail) { + dispatch({ type: "SHOW_POPUP", payload: detail.info }); + setDetail(null); + } + return ( -
-
-
Made the hard way
-

- Top trending products -

-
-
- - {data.map((product) => ( - - ))} - + + {isClose && } +
+
+
+ Made the hard way +
+

+ Top trending products +

+
+
+ + {data.map((product) => ( + + ))} + +
-
+ ); }; diff --git a/src/components/ListOfProducts/ListOfProducts.module.scss b/src/components/ListOfProducts/ListOfProducts.module.scss index 4c78c68..9ffb052 100644 --- a/src/components/ListOfProducts/ListOfProducts.module.scss +++ b/src/components/ListOfProducts/ListOfProducts.module.scss @@ -29,6 +29,10 @@ color: gray; font-size: 0.9rem; } + a { + color: black; + text-decoration: none; + } } .product-content * { diff --git a/src/components/ListOfProducts/ProductItem.js b/src/components/ListOfProducts/ProductItem.js index 79cb5c6..889c243 100644 --- a/src/components/ListOfProducts/ProductItem.js +++ b/src/components/ListOfProducts/ProductItem.js @@ -1,27 +1,48 @@ import React from "react"; import Col from "react-bootstrap/Col"; import classes from "./ListOfProducts.module.scss"; +import { NavLink } from "react-router-dom"; + +const ProductItem = ({ product, setDetail, isLink }) => { + const price = `${Number(product.price) + .toLocaleString("vi-VN", { style: "currency", currency: "VND" }) + .slice(0, -1)} VND`; + function onClickHandler() { + const transformData = { + info: { + name: product.name, + price: price, + category: product.category, + img: product.img1, + long_desc: product.long_desc, + short_desc: product.short_desc, + _id: { + $oid: product._id.$oid, + }, + }, + onClose: true, + }; + setDetail(transformData); + } -const ProductItem = ({ product }) => { return ( - -
-
- img1 -
-
-
{product.name}
-

- {Number(product.price) - .toLocaleString("vi-VN", { - style: "currency", - currency: "VND", - }) - .slice(0, -1)} - VND -

-
-
+ + +
+
+ img1 +
+
+
{product.name}
+

{price}

+
+
+
); }; diff --git a/src/components/Products/Categories/Categories.js b/src/components/Products/Categories/Categories.js new file mode 100644 index 0000000..977b609 --- /dev/null +++ b/src/components/Products/Categories/Categories.js @@ -0,0 +1,120 @@ +import React, { useState } from "react"; +import { Link, useLoaderData, useSearchParams } from "react-router-dom"; +import Form from "react-bootstrap/Form"; +import InputGroup from "react-bootstrap/InputGroup"; + +import classes from "./Categories.module.scss"; +import ProductItem from "../../ListOfProducts/ProductItem"; +import Row from "react-bootstrap/esm/Row"; +import { useDispatch } from "react-redux"; + +const arrTitle = [ + "APPLE", + "All", + "IPHONE & MAC", + "iPhone", + "iPad", + "Macbook", + "WIRELESS", + "Airpod", + "Watch", + "OTHER", + "Mouse", + "Keyboard", + "Other", +]; + +const Categories = () => { + const data = useLoaderData(); + const [params] = useSearchParams(); + const sortId = params.get("sort"); + + const dispatch = useDispatch(); + const [detail, setDetail] = useState(null); + + if (detail) { + dispatch({ type: "SELECT", payload: detail.info }); + setDetail(null); + } + + let content = data; + if (sortId) { + switch (sortId.toLowerCase()) { + case "all": + content = data; + break; + + default: + content = data.filter( + (item) => item.category === sortId.toLowerCase() + ); + break; + } + } + + const Title = (item) => { + let linked; + if ( + item !== "APPLE" && + item !== "IPHONE & MAC" && + item !== "WIRELESS" && + item !== "OTHER" + ) { + linked = `?sort=${item}`; + } + return ( + + {item} + + ); + }; + + return ( +
+
+

Categories

+
+ {arrTitle.map((item) => Title(item))} +
+
+
+
+ + + + + + + + + +
+
+ + {content.length > 0 ? ( + content.map((product) => ( + + )) + ) : ( +

Not Found Product

+ )} +
+
+
+
+ ); +}; + +export default Categories; diff --git a/src/components/Products/Categories/Categories.module.scss b/src/components/Products/Categories/Categories.module.scss new file mode 100644 index 0000000..114df1c --- /dev/null +++ b/src/components/Products/Categories/Categories.module.scss @@ -0,0 +1,48 @@ +.showcase { + display: grid; + grid-template-columns: 2.5fr 7.5fr; + height: 100vh; + .arrTitle { + display: flex; + flex-direction: column; + font-size: smaller; + > * { + text-decoration: none; + color: black; + } + > *:hover { + background-color: #f8f8f8; + } + :nth-child(1) { + background-color: black; + color: white; + font-size: medium; + } + :nth-child(3) { + background-color: #e2e4e2; + color: black; + font-size: medium; + font-weight: 400 !important; + } + :nth-child(7) { + background-color: #e2e4e2; + color: black; + font-size: medium; + font-weight: 400 !important; + } + :nth-child(10) { + background-color: #e2e4e2; + color: black; + font-size: medium; + font-weight: 400 !important; + } + } +} + +.input { + max-width: 20rem; +} + +.select { + max-width: 10rem; +} diff --git a/src/components/Products/ProductDetail.js b/src/components/Products/ProductDetail.js new file mode 100644 index 0000000..2a29079 --- /dev/null +++ b/src/components/Products/ProductDetail.js @@ -0,0 +1,40 @@ +import React, { Fragment } from "react"; +import { Link } from "react-router-dom"; +import Modal from "../UI/Modal"; + +import classes from "./ProductDetail.module.scss"; +import { useDispatch, useSelector } from "react-redux"; + +const ProductDetail = () => { + const dispatch = useDispatch(); + const isInfo = useSelector((state) => state.info); + // show and hide about description detail + const hidePopUpHandler = () => dispatch({ type: "HIDE_POPUP" }); + + let content = ( +
+
+ img +
+
+

{isInfo.name}

+

{isInfo.price}

+

{isInfo.long_desc}

+ + + +
+
+ ); + + return ( + + {content} + + ); +}; + +export default ProductDetail; diff --git a/src/components/Products/ProductDetail.module.scss b/src/components/Products/ProductDetail.module.scss new file mode 100644 index 0000000..7c06ce4 --- /dev/null +++ b/src/components/Products/ProductDetail.module.scss @@ -0,0 +1,15 @@ +.showcase { + display: grid; + grid-template-columns: repeat(2, 1fr); + img { + width: 100%; + } + p { + color: gray; + font-size: 0.9rem; + } + h1 { + font-weight: bolder; + font-size: x-large; + } +} diff --git a/src/components/Products/ProductList.js b/src/components/Products/ProductList.js new file mode 100644 index 0000000..799e84f --- /dev/null +++ b/src/components/Products/ProductList.js @@ -0,0 +1,19 @@ +import React, { Fragment } from "react"; +import classes from "./ProductList.module.scss"; +import Categories from "./Categories/Categories"; + +const ProductList = () => { + return ( + +
+

Shop

+

+ Shop +

+
+ +
+ ); +}; + +export default ProductList; diff --git a/src/components/Products/ProductList.module.scss b/src/components/Products/ProductList.module.scss new file mode 100644 index 0000000..7404a23 --- /dev/null +++ b/src/components/Products/ProductList.module.scss @@ -0,0 +1,7 @@ +.banner { + width: 100%; + height: 25vh; + display: flex; + justify-content: space-between; + align-items: center; +} diff --git a/src/components/UI/Modal.module.scss b/src/components/UI/Modal.module.scss index 4041526..8ad6900 100644 --- a/src/components/UI/Modal.module.scss +++ b/src/components/UI/Modal.module.scss @@ -10,9 +10,9 @@ .modal { position: fixed; - top: 20vh; - left: 5%; - width: 90%; + top: 10vh; + left: 15%; + width: 70%; background-color: white; padding: 1rem; border-radius: 14px; @@ -21,13 +21,6 @@ animation: slide-down 300ms ease-out forwards; } -@media (min-width: 768px) { - .modal { - width: 40rem; - left: calc(50% - 20rem); - } -} - @keyframes slide-down { from { opacity: 0; diff --git a/src/context/categories-context.js b/src/context/categories-context.js new file mode 100644 index 0000000..6a634e0 --- /dev/null +++ b/src/context/categories-context.js @@ -0,0 +1,19 @@ +import React from "react"; + +export const CategoriesContext = React.createContext({ + categories: [], +}); + +const catetoryReducer = (state, action) => {}; + +const CategoriesProvider = (props) => { + const categoryContext = []; + + return ( + + {props.children} + + ); +}; + +export default CategoriesProvider; diff --git a/src/index.js b/src/index.js index 718f639..9161532 100644 --- a/src/index.js +++ b/src/index.js @@ -3,11 +3,18 @@ import ReactDOM from "react-dom/client"; import App from "./App"; import "bootstrap/dist/css/bootstrap.min.css"; +import "bootstrap-icons/font/bootstrap-icons.css"; import "./index.scss"; +// Redux +import { Provider } from "react-redux"; +import store from "./store/index"; + +// context + const root = ReactDOM.createRoot(document.getElementById("root")); root.render( - + - + ); diff --git a/src/pages/HomePage.js b/src/pages/HomePage.js index 8760b8e..195f081 100644 --- a/src/pages/HomePage.js +++ b/src/pages/HomePage.js @@ -8,7 +8,6 @@ import InfoAnother from "../components/InfoAnother/InfoAnother"; const HomePage = () => { const data = useLoaderData(); - console.log(data); return ( diff --git a/src/pages/ShopPage.js b/src/pages/ShopPage.js index e9b6e7e..eba890b 100644 --- a/src/pages/ShopPage.js +++ b/src/pages/ShopPage.js @@ -1,7 +1,29 @@ -import React from "react"; +import React, { Fragment } from "react"; +import ProductList from "../components/Products/ProductList"; +import { json } from "react-router-dom"; const ShopPage = () => { - return
ShopPage
; + return ( + + + + ); }; export default ShopPage; + +export async function loader() { + const response = await fetch( + "https://firebasestorage.googleapis.com/v0/b/funix-subtitle.appspot.com/o/Boutique_products.json?alt=media&token=dc67a5ea-e3e0-479e-9eaf-5e01bcd09c74" + ); + + if (!response.ok) { + throw json( + { message: "Could not fetch list of products." }, + { status: 500 } + ); + } else { + const resData = await response.json(); + return resData; + } +} diff --git a/src/store/index.js b/src/store/index.js index e69de29..d8cb74c 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -0,0 +1,62 @@ +import { createStore } from "redux"; + +const initialPopUpState = { + info: { + name: "", + price: "", + category: "", + img: "", + long_desc: "", + short_desc: "", + _id: { + $oid: "", + }, + }, + onClose: false, +}; + +const popupReducer = (state = initialPopUpState, action) => { + if (action.type === "SELECT") { + return { + info: { + name: action.payload.name, + price: action.payload.price, + category: action.payload.category, + img: action.payload.img, + long_desc: action.payload.long_desc, + short_desc: action.payload.short_desc, + _id: { + $oid: action.payload._id, + }, + }, + onClose: false, + }; + } + if (action.type === "SHOW_POPUP") { + return { + info: { + name: action.payload.name, + price: action.payload.price, + category: action.payload.category, + img: action.payload.img, + long_desc: action.payload.long_desc, + short_desc: action.payload.short_desc, + _id: { + $oid: action.payload._id, + }, + }, + onClose: true, + }; + } + if (action.type === "HIDE_POPUP") { + return { + info: state.info, + onClose: false, + }; + } + return state; +}; + +const store = createStore(popupReducer); + +export default store;