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 (
-
-
-
-
-
{product.name}
-
- {Number(product.price)
- .toLocaleString("vi-VN", {
- style: "currency",
- currency: "VND",
- })
- .slice(0, -1)}
- VND
-
-
-
+
+
+
+
+
+
{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 = (
+
+
+
+
+
+
{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 (
+
+
+
+
+ );
+};
+
+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;