├── demo.jpg
├── public
├── favicon.ico
└── index.html
├── src
├── assets
│ ├── about_img.jpg
│ └── hero_img.png
├── pages
│ ├── HomePage.js
│ ├── CheckoutPage.js
│ ├── ProductsPage.js
│ ├── ErrorPage.js
│ ├── AboutPage.js
│ ├── CartPage.js
│ └── SingleProductPage.js
├── components
│ ├── Error.js
│ ├── share
│ │ ├── Typography.js
│ │ ├── Button.js
│ │ └── Icons.js
│ ├── Stars.js
│ ├── Footer.js
│ ├── PopularProducts.js
│ ├── EmptyCart.js
│ ├── Loading.js
│ ├── Products.js
│ ├── Breadcrumb.js
│ ├── AddToCart.js
│ ├── AmountButtons.js
│ ├── index.js
│ ├── Services.js
│ ├── Hero.js
│ ├── GridProducts.js
│ ├── Contact.js
│ ├── CartButtons.js
│ ├── ListProducts.js
│ ├── CartTotals.js
│ ├── CartItem.js
│ ├── Navbar.js
│ ├── Sidebar.js
│ ├── Sort.js
│ └── Filters.js
├── utils
│ ├── helpers.js
│ ├── constants.js
│ └── actions.js
├── index.js
├── App.js
├── styles
│ ├── Screen.js
│ └── GlobalStyle.js
├── routes
│ └── ConfigRoutes.js
├── reducers
│ ├── products_reducer.js
│ ├── cart_reducer.js
│ └── filter_reducer.js
└── contexts
│ ├── cart_context.js
│ ├── products_context.js
│ └── filter_context.js
├── .gitignore
├── README.md
└── package.json
/demo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamalheydari/react-store-fake-api/HEAD/demo.jpg
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamalheydari/react-store-fake-api/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/assets/about_img.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamalheydari/react-store-fake-api/HEAD/src/assets/about_img.jpg
--------------------------------------------------------------------------------
/src/assets/hero_img.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamalheydari/react-store-fake-api/HEAD/src/assets/hero_img.png
--------------------------------------------------------------------------------
/src/pages/HomePage.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 |
4 | import { PopularProducts, Services, Hero, Contact } from "../components";
5 |
6 | const HomePage = () => {
7 |
8 |
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default HomePage;
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/src/components/Error.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 |
4 | import { Typography } from ".";
5 |
6 | const Error = () => {
7 | return (
8 |
9 | there was an error
10 |
11 | );
12 | };
13 |
14 | const Wrapper = styled.div`
15 | padding: 10rem;
16 | display: flex;
17 | align-items: center;
18 | justify-content: center;
19 | color: var(--red-color-1);
20 | `;
21 | export default Error;
22 |
--------------------------------------------------------------------------------
/src/utils/helpers.js:
--------------------------------------------------------------------------------
1 | export const truncate = (str = "", len) => {
2 | if (str.length > len && str.length > 0) {
3 | let new_str = str + " ";
4 | new_str = str.substr(0, len);
5 | new_str = str.substr(0, new_str.lastIndexOf(" "));
6 | new_str = new_str.length > 0 ? new_str : str.substr(0, len);
7 | return new_str + "...";
8 | }
9 | return str;
10 | };
11 |
12 | export const getUniqueValues = (data, type) => {
13 | let unique = data.map((item) => item[type]);
14 | return ["all", ...new Set(unique)];
15 | };
16 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import App from "./App";
4 |
5 | //? Contexts
6 | import { ProductsProvider } from "./contexts/products_context";
7 | import { FilterProvider } from "./contexts/filter_context";
8 | import { CartProvider } from "./contexts/cart_context";
9 |
10 |
11 | ReactDOM.render(
12 |
13 |
14 |
15 |
16 |
17 |
18 | ,
19 | document.getElementById("root")
20 | );
21 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { HashRouter as Router } from "react-router-dom";
3 |
4 | //? Components
5 | import { Navbar, Sidebar, Footer } from "./components";
6 | import ConfigRoutes from "./routes/ConfigRoutes";
7 |
8 | //? Global Style
9 | import GlobalStyle from "./styles/GlobalStyle";
10 |
11 | const App = () => {
12 | return (
13 | <>
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | >
22 | );
23 | };
24 |
25 | export default App;
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React e-commerce With Fake Store Api
2 |
3 | ## Using
4 |
5 | - react
6 | - styled-components
7 | - [FakeStoreApi](https://fakestoreapi.com/)
8 |
9 | ## Features:
10 | Search, filter, and sort functions
11 | - Enables users to easily find and sort products based on their preferences.
12 |
13 | Simple website structure
14 | - Consists of a home page, products page, single product page, about page, and a shopping cart.
15 |
16 | Using suspense for optimization
17 |
18 | ## Demo
19 | See demo on github pages
20 | [React Fake Store](https://kamalheydari.github.io/react-store-fake-api/#/)
21 |
22 | 
23 |
24 |
--------------------------------------------------------------------------------
/src/components/share/Typography.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | const H1 = styled.h1`
4 | font-size: var(--fs-700);
5 | color: var(--gray-color-1);
6 | `;
7 |
8 | const H2 = styled.h2`
9 | font-size: var(--fs-600);
10 | color: var(--gray-color-1);
11 | `;
12 |
13 | const H3 = styled.h3`
14 | font-size: var(--fs-500);
15 | color: var(--blue-color-4);
16 | `;
17 |
18 | const H4 = styled.h4`
19 | font-size: var(--fs-500);
20 | color: var(--blue-color-4);
21 | `;
22 |
23 | const P = styled.p`
24 | font-size: var(--fs-500);
25 | color: var(--gray-color-1);
26 | `;
27 |
28 | const Typography = { H1, H2, H3,H4, P };
29 |
30 | export default Typography;
31 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
15 | fake store
16 |
17 |
18 | You need to enable JavaScript to run this app.
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/pages/CheckoutPage.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from "react-router-dom";
3 | import styled from "styled-components";
4 |
5 | import { Typography, Button } from "../components";
6 |
7 | const CheckoutPage = () => {
8 | return (
9 |
10 |
11 | Checkout Successfully
12 |
13 | Back to shop
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | const Wrapper = styled.section`
21 | display: grid;
22 | place-items: center;
23 | text-align: center;
24 | h2 {
25 | margin-bottom: 2rem;
26 | }
27 | `;
28 | export default CheckoutPage;
29 |
--------------------------------------------------------------------------------
/src/pages/ProductsPage.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 |
4 | import { Breadcrumb, Filters, Products, Sort } from "../components";
5 |
6 | import Screen from "../styles/Screen";
7 |
8 | const ProductsPage = () => {
9 | return (
10 |
11 |
12 |
13 |
14 |
18 |
19 |
20 | );
21 | };
22 |
23 | const Wrapper = styled.section`
24 | padding: 1rem;
25 | ${Screen.lg`
26 | display: grid;
27 | gap: 3rem 2rem;
28 | grid-template-columns: 220px 1fr;
29 | `}
30 | `;
31 |
32 | export default ProductsPage;
33 |
--------------------------------------------------------------------------------
/src/pages/ErrorPage.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from "react-router-dom";
3 | import styled from "styled-components";
4 |
5 | import {Button,Typography} from '../components';
6 |
7 | const ErrorPage = () => {
8 | return (
9 |
10 | 404
11 |
12 |
13 | Back To Home
14 |
15 |
16 | );
17 | };
18 |
19 | const Wrapper = styled.section`
20 | display: flex;
21 | align-items: center;
22 | justify-content: center;
23 | flex-direction: column;
24 | gap: 4rem;
25 | h2 {
26 | font-size: 6rem;
27 | color: var(--red-color-1);
28 | }
29 |
30 | `;
31 |
32 | export default ErrorPage;
33 |
--------------------------------------------------------------------------------
/src/styles/Screen.js:
--------------------------------------------------------------------------------
1 | const Screen = {
2 | sm: (...args) => {
3 | const styles = args;
4 | return `@media (min-width:576px){
5 | ${styles}
6 | } `;
7 | },
8 | md: (...args) => {
9 | const styles = args;
10 | return `@media (min-width:768px){
11 | ${styles}
12 | } `;
13 | },
14 | lg: (...args) => {
15 | const styles = args;
16 | return `@media (min-width:992px){
17 | ${styles}
18 | } `;
19 | },
20 | xl: (...args) => {
21 | const styles = args;
22 | return `@media (min-width:1200px){
23 | ${styles}
24 | } `;
25 | },
26 | xxl: (...args) => {
27 | const styles = args;
28 | return `@media (min-width:1400px){
29 | ${styles}
30 | } `;
31 | },
32 | };
33 |
34 | export default Screen;
35 |
--------------------------------------------------------------------------------
/src/components/Stars.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 |
4 | import { BsStarFill, BsStarHalf, BsStar } from "react-icons/bs";
5 |
6 | const Stars = ({ stars = { rate: "" } }) => {
7 | const { rate } = stars;
8 |
9 | const tempStars = Array.from({ length: 5 }, (_, index) => {
10 | const number = index + 0.5;
11 | return (
12 |
13 | {rate >= index + 1 ? (
14 |
15 | ) : rate >= number ? (
16 |
17 | ) : (
18 |
19 | )}
20 |
21 | );
22 | });
23 |
24 | return {tempStars} ;
25 | };
26 | const Wrapper = styled.div`
27 | span {
28 | font-size: 1.4rem;
29 | margin-inline: 0.1rem;
30 | color: var(--yellow-color-2);
31 | }
32 | `;
33 | export default Stars;
34 |
--------------------------------------------------------------------------------
/src/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 |
4 | import { Typography } from ".";
5 |
6 | const Footer = () => {
7 | return (
8 |
9 |
10 | ©{new Date().getFullYear()} by
11 | kamal heydari , All rights
12 | reserved
13 |
14 |
15 | );
16 | };
17 |
18 | const FooterWrapper = styled.footer`
19 | background: var(--green-color-1);
20 | height: var(--footer-height);
21 | padding: 3rem;
22 | display: flex;
23 | align-items: center;
24 | justify-content: center;
25 |
26 | p {
27 | text-align: center;
28 | color: var(--white-color);
29 | }
30 |
31 | a {
32 | color: #fff;
33 | font-size: var(--fs-500);
34 | }
35 | `;
36 |
37 | export default Footer;
38 |
--------------------------------------------------------------------------------
/src/components/PopularProducts.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 |
4 | import { useProductsContext } from "../contexts/products_context";
5 |
6 | import { GridProducts, Loading, Error, Typography } from ".";
7 |
8 | const PopularProducts = () => {
9 | const {
10 | popular_products: products,
11 | products_loading: loading,
12 | products_error: error,
13 | } = useProductsContext();
14 |
15 | if (loading) {
16 | return ;
17 | }
18 |
19 | if (error) {
20 | return ;
21 | }
22 |
23 | return (
24 |
25 | Most popular products
26 |
27 |
28 | );
29 | };
30 |
31 | const Wrapper = styled.section`
32 | padding: 1rem;
33 | height: auto;
34 |
35 | h2 {
36 | margin-bottom: 3rem;
37 | }
38 | `;
39 |
40 | export default PopularProducts;
41 |
--------------------------------------------------------------------------------
/src/components/EmptyCart.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from "react-router-dom";
3 | import styled from "styled-components";
4 |
5 | import { Typography, Button } from ".";
6 |
7 | const EmptyCart = () => {
8 | return (
9 |
10 |
11 |
12 |
13 | Your cart is empty
14 |
15 |
16 | Shop now
17 |
18 |
19 |
20 |
21 | );
22 | };
23 | const Wrapper = styled.section`
24 | display: grid;
25 | place-items: center;
26 |
27 | .empty-cart {
28 | text-align: center;
29 | h2 {
30 | margin-bottom: 1.5rem;
31 | span {
32 | color: var(--red-color-1);
33 | }
34 | }
35 | }
36 | `;
37 | export default EmptyCart;
38 |
--------------------------------------------------------------------------------
/src/components/Loading.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 |
4 | const Loading = (props) => {
5 | return (
6 |
7 |
8 |
9 | );
10 | };
11 |
12 | const Wrapper = styled.div`
13 | padding: 10rem;
14 | display: flex;
15 | align-items: center;
16 | justify-content: center;
17 | min-height: ${(props) =>
18 | props.lazy ||
19 | "calc(100vh - (var(--header-height) + var(--footer-height)))"};
20 |
21 | margin-top: ${(props) => props.lazy || "var(--header-height)"};
22 |
23 | @keyframes spinner {
24 | to {
25 | transform: rotate(360deg);
26 | }
27 | }
28 |
29 | .loading {
30 | width: 6rem;
31 | height: 6rem;
32 | border-radius: 50%;
33 | border: 4px solid #ccc;
34 | border-top-color: var(--red-color-1);
35 | animation: spinner 0.6s linear infinite;
36 | }
37 | `;
38 |
39 | export default Loading;
40 |
--------------------------------------------------------------------------------
/src/components/Products.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { GridProducts, ListProducts } from ".";
4 |
5 | import { useProductsContext } from "../contexts/products_context";
6 | import { useFilterContext } from "../contexts/filter_context";
7 |
8 | import { Loading, Error, Typography } from ".";
9 | const Products = () => {
10 | const { filtered_products: products, grid_view } = useFilterContext();
11 | const {
12 | products_loading: loading,
13 | products_error: error,
14 | } = useProductsContext();
15 |
16 | if (error) {
17 | return ;
18 | }
19 |
20 | if (loading) {
21 | return ;
22 | }
23 |
24 | if (products.length < 1) {
25 | return (
26 | Sorry, no products matched your search...
27 | );
28 | }
29 |
30 | if (grid_view === false) {
31 | return ;
32 | }
33 | return ;
34 | };
35 |
36 | export default Products;
37 |
--------------------------------------------------------------------------------
/src/components/Breadcrumb.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from "react-router-dom";
3 | import styled from "styled-components";
4 | import { truncate } from "../utils/helpers";
5 |
6 | const Breadcrumb = ({ title, products }) => {
7 | return (
8 |
9 | Home /
10 | {products && prodcuts / }
11 | {truncate(title, 20)}
12 |
13 | );
14 | };
15 |
16 | const Wrapper = styled.div`
17 | height: var(--breadcrumb-height);
18 | max-width: var(--max-width);
19 | margin: 0 auto;
20 | padding: 1.3rem 1rem;
21 | width: 100%;
22 | display: flex;
23 | align-items: center;
24 | gap: 1rem;
25 |
26 | a {
27 | font-size: var(--fs-400);
28 | color: var(--blue-color-1);
29 | transition: var(--transition);
30 | &:hover {
31 | color: var(--green-color-1);
32 | }
33 | }
34 |
35 | span {
36 | font-size: var(--fs-400);
37 | color: var(--green-color-1);
38 |
39 | }
40 | `;
41 |
42 | export default Breadcrumb;
43 |
--------------------------------------------------------------------------------
/src/components/AddToCart.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Link } from "react-router-dom";
3 |
4 | import { AmountButtons } from ".";
5 |
6 | import { useCartContext } from "../contexts/cart_context";
7 |
8 | import { Button } from ".";
9 |
10 | const AddToCart = ({ product }) => {
11 | const { addToCart } = useCartContext();
12 |
13 | //? Local State
14 | const [amount, setAmount] = useState(1);
15 |
16 | //? Handlers
17 | const increase = () => {
18 | setAmount((oldAmount) => oldAmount + 1);
19 | };
20 | const decrease = () => {
21 | setAmount((oldAmount) => {
22 | let newAmount = oldAmount - 1;
23 | if (newAmount < 1) {
24 | newAmount = 1;
25 | }
26 | return newAmount;
27 | });
28 | };
29 |
30 | return (
31 | <>
32 |
33 |
34 | addToCart(product, amount)}>
35 | Add to cart
36 |
37 |
38 | >
39 | );
40 | };
41 |
42 | export default AddToCart;
43 |
--------------------------------------------------------------------------------
/src/utils/constants.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { GiReturnArrow, GiDeliveryDrone, GiPayMoney } from "react-icons/gi";
4 |
5 |
6 | export const links = [
7 | { id: 1, text: "Home", url: "/" },
8 | { id: 2, text: "Products", url: "/products" },
9 | { id: 3, text: "About", url: "/about" },
10 | ];
11 |
12 | export const services = [
13 | {
14 | id: 1,
15 | name: "Delivery",
16 | text: "Your order will be delivered within 7-12 business days following the order confirmation. Additional business days may be required for delivery during",
17 | icon: ,
18 | },
19 | {
20 | id: 2,
21 | name: "Payments",
22 | text: "Shop now, pay later. You'll only pay for the items you keep. Your payment will automatically be deducted from your card after 30 days, no additional charge",
23 | icon: ,
24 | },
25 | {
26 | id: 3,
27 | name: "Returns",
28 | text: "You are always welcome to return or exchange for free in any H&M store in the US, excluding Puerto Rico. You have 30 days to decide if an item is right for you",
29 | icon: ,
30 | },
31 | ];
32 |
--------------------------------------------------------------------------------
/src/components/AmountButtons.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 |
4 | import { Icons } from ".";
5 |
6 | const AmountButtons = ({ increase, decrease, amount }) => {
7 | return (
8 |
9 |
14 |
15 |
16 | {amount}
17 |
18 |
19 |
20 |
21 | );
22 | };
23 |
24 | const Wrapper = styled.div`
25 | width: max-content;
26 | display: flex;
27 | gap: 1rem;
28 |
29 | .opacity {
30 | opacity: 0.5;
31 | cursor: auto;
32 | }
33 |
34 | span {
35 | text-align: center;
36 | min-width: 2.6rem;
37 | font-size: var(--fs-700);
38 | color: var(--gray-color-1);
39 | }
40 |
41 | button {
42 | padding: 0.3rem;
43 | display: inline-block;
44 | transform: scale(1);
45 | }
46 |
47 | button:active {
48 | transform: scale(0.9);
49 | }
50 | `;
51 | export default AmountButtons;
52 |
--------------------------------------------------------------------------------
/src/utils/actions.js:
--------------------------------------------------------------------------------
1 | //? Products Context Actions
2 | export const OPEN_SIDEBAR = "OPEN_SIDEBAR";
3 | export const CLOSE_SIDEBAR = "CLOSE_SIDEBAR";
4 | export const GET_PRODUCTS_BEGIN = "GET_PRODUCTS_BEGIN";
5 | export const GET_PRODUCTS_SUCCESS = "GET_PRODUCTS_SUCCESS";
6 | export const GET_PRODUCTS_ERROR = "GET_PRODUCTS_ERROR";
7 | export const GET_SINGLE_PRODUCT_BEGIN = "GET_SINGLE_PRODUCT_BEGIN";
8 | export const GET_SINGLE_PRODUCT_SUCCESS = "GET_SINGLE_PRODUCT_SUCCESS";
9 | export const GET_SINGLE_PRODUCT_ERROR = "GET_SINGLE_PRODUCT_ERROR";
10 |
11 | //? Filter Context Actions
12 | export const LOAD_PRODUCTS = "LOAD_PRODUCTS";
13 | export const SET_GRID_VIEW = "SET_GRID_VIEW";
14 | export const SET_LIST_VIEW = "SET_LIST_VIEW";
15 | export const UPDATE_SORT = "UPDATE_SORT";
16 | export const SORT_PRODUCTS = "SORT_PRODUCTS";
17 | export const UPDATE_FILTERS = "UPDATE_FILTERS";
18 | export const FILTER_PRODUCTS = "FILTER_PRODUCTS";
19 | export const CLEAR_FILTERS = "CLEAR_FILTERS";
20 |
21 | //? Cart Context Actions
22 | export const ADD_TO_CART = "ADD_TO_CART";
23 | export const REMOVE_CART_ITEM = "REMOVE_CART_ITEM";
24 | export const TOGGLE_CART_ITEM = "TOGGLE_CART_ITEM";
25 | export const CLEAR_CART = "CLEAR_CART";
26 | export const COUNT_CART_TOTALS = "COUNT_CART_TOTALS";
27 | export const CHECKOUT = "CHECKOUT";
28 |
--------------------------------------------------------------------------------
/src/routes/ConfigRoutes.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Route, Routes } from "react-router-dom";
3 |
4 | import { Loading } from "../components";
5 |
6 | const HomePage = React.lazy(() => import("../pages/HomePage"));
7 | const ProductsPage = React.lazy(() => import("../pages/ProductsPage"));
8 | const AboutPage = React.lazy(() => import("../pages/AboutPage"));
9 | const ErrorPage = React.lazy(() => import("../pages/ErrorPage"));
10 | const SingleProductPage = React.lazy(() =>
11 | import("../pages/SingleProductPage")
12 | );
13 | const CartPage = React.lazy(() => import("../pages/CartPage"));
14 | const CheckoutPage = React.lazy(() => import("../pages/CheckoutPage"));
15 |
16 | const ConfigRoutes = () => {
17 | return (
18 | }>
19 |
20 | } />
21 | } />
22 | } />
23 | } />
24 | } />
25 | } />
26 | } />
27 |
28 |
29 | );
30 | };
31 |
32 | export default ConfigRoutes;
33 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "homepage": "https://kamalheydari.github.io/react-store-fake-api",
3 | "name": "react-fake-store",
4 | "version": "0.1.0",
5 | "private": true,
6 | "dependencies": {
7 | "@fontsource/poppins": "^4.5.8",
8 | "@testing-library/jest-dom": "^5.11.4",
9 | "@testing-library/react": "^11.1.0",
10 | "@testing-library/user-event": "^12.1.10",
11 | "react": "^17.0.2",
12 | "react-dom": "^17.0.2",
13 | "react-icons": "^4.3.1",
14 | "react-router-dom": "^6.2.1",
15 | "react-scripts": "4.0.3",
16 | "styled-components": "^5.3.3",
17 | "web-vitals": "^1.0.1"
18 | },
19 | "scripts": {
20 | "predeploy": "npm run build",
21 | "deploy": "gh-pages -d build",
22 | "start": "react-scripts start",
23 | "build": "react-scripts build",
24 | "test": "react-scripts test",
25 | "eject": "react-scripts eject"
26 | },
27 | "eslintConfig": {
28 | "extends": [
29 | "react-app",
30 | "react-app/jest"
31 | ]
32 | },
33 | "browserslist": {
34 | "production": [
35 | ">0.2%",
36 | "not dead",
37 | "not op_mini all"
38 | ],
39 | "development": [
40 | "last 1 chrome version",
41 | "last 1 firefox version",
42 | "last 1 safari version"
43 | ]
44 | },
45 | "devDependencies": {
46 | "gh-pages": "^3.2.3"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/share/Button.js:
--------------------------------------------------------------------------------
1 | import styled, { css } from "styled-components";
2 |
3 | const Button = styled.button`
4 | border-radius: var(--radius);
5 | transition: var(--transition);
6 | letter-spacing: 1px;
7 | font-size: var(--fs-400);
8 | font-weight: bold;
9 |
10 | a {
11 | display: inline-block;
12 | }
13 | ${(props) => {
14 | switch (props.variant) {
15 | case "primary":
16 | return css`
17 | border: 0.2rem solid var(--green-color-1);
18 | a {
19 | padding: 0.75rem 1.5rem;
20 | }
21 | &:hover {
22 | background: var(--green-color-1);
23 | a {
24 | color: var(--white-color);
25 | }
26 | }
27 | `;
28 |
29 | case "secondary":
30 | return css`
31 | border: 0.2rem solid var(--red-color-1);
32 | color: var(--gray-color-1);
33 | a {
34 | padding: 0.75rem 1.5rem;
35 | color: var(--gray-color-1);
36 | }
37 | &:hover {
38 | background: var(--red-color-1);
39 | color: var(--white-color);
40 | a {
41 | color: var(--white-color);
42 | }
43 | }
44 | `;
45 |
46 | default:
47 | break;
48 | }
49 | }}
50 | `;
51 | export default Button;
52 |
--------------------------------------------------------------------------------
/src/components/index.js:
--------------------------------------------------------------------------------
1 | import Navbar from "./Navbar";
2 | import CartButtons from "./CartButtons";
3 | import Sidebar from "./Sidebar";
4 | import Footer from "./Footer";
5 | import Breadcrumb from "./Breadcrumb";
6 | import Services from "./Services";
7 | import PopularProducts from "./PopularProducts";
8 | import Hero from "./Hero";
9 | import Contact from "./Contact";
10 | import GridProducts from "./GridProducts";
11 | import ListProducts from "./ListProducts";
12 | import Products from "./Products";
13 | import Filters from "./Filters";
14 | import Sort from "./Sort";
15 | import Loading from "./Loading";
16 | import Error from "./Error";
17 | import Stars from "./Stars";
18 | import AddToCart from "./AddToCart";
19 | import AmountButtons from "./AmountButtons";
20 | import CartItem from "./CartItem";
21 | import EmptyCart from "./EmptyCart";
22 | import CartTotals from "./CartTotals";
23 | import Typography from "./share/Typography";
24 | import Button from "./share/Button";
25 | import Icons from "./share/Icons";
26 |
27 | export {
28 | Navbar,
29 | CartButtons,
30 | Sidebar,
31 | Footer,
32 | Breadcrumb,
33 | Services,
34 | PopularProducts,
35 | Hero,
36 | Contact,
37 | GridProducts,
38 | ListProducts,
39 | Filters,
40 | Products,
41 | Sort,
42 | Loading,
43 | Error,
44 | Stars,
45 | AddToCart,
46 | AmountButtons,
47 | CartItem,
48 | EmptyCart,
49 | CartTotals,
50 | Typography,
51 | Button,
52 | Icons,
53 | };
54 |
--------------------------------------------------------------------------------
/src/components/Services.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 |
4 | import { services } from "../utils/constants";
5 |
6 | import Screen from "../styles/Screen";
7 |
8 | import { Typography } from ".";
9 |
10 | const Services = () => {
11 | return (
12 |
13 | {services.map(({ id, name, icon, text }) => (
14 |
15 |
16 | {icon}
17 | {name}
18 |
19 | {text}
20 |
21 | ))}
22 |
23 | );
24 | };
25 |
26 | const Wrapper = styled.section`
27 | padding: 2rem 1.5rem;
28 | display: grid;
29 | justify-items: center;
30 | gap: 1.5rem;
31 | ${Screen.md`
32 | grid-template-columns: 1fr 1fr 1fr;
33 | `}
34 |
35 | .service {
36 | padding: 1rem 1.5rem;
37 | border: 0.2rem solid var(--green-color-1);
38 | border-radius: var(--radius);
39 | transition: var(--transition);
40 | &:hover {
41 | box-shadow: 0 0 1rem 1rem var(--bg-color);
42 | }
43 | }
44 |
45 | .service__header {
46 | display: flex;
47 | align-items: center;
48 | margin-bottom: 1rem;
49 |
50 | svg {
51 | color: var(--green-color-1);
52 | font-size: 3rem;
53 | margin-right: 2rem;
54 | }
55 | }
56 |
57 | p {
58 | text-align: justify;
59 | }
60 | `;
61 |
62 | export default Services;
63 |
--------------------------------------------------------------------------------
/src/pages/AboutPage.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 |
4 | import { Breadcrumb,Typography } from "../components";
5 |
6 | import Screen from "../styles/Screen";
7 |
8 | import img from "../assets/about_img.jpg";
9 |
10 | const AboutPage = () => {
11 | return (
12 |
13 |
14 |
15 |
16 | About Us
17 |
18 | H&M Group is a family of brands and businesses, making it possible
19 | for customers around the world to express themselves through fashion
20 | and design, and to choose a more sustainable lifestyle. We create
21 | value for people and society in general by delivering our customer
22 | offering and by developing with a focus on sustainable and
23 | profitable growth.
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | const Wrapper = styled.section`
35 | display: grid;
36 | justify-items: center;
37 | align-items: center;
38 | padding: 1rem;
39 |
40 | ${Screen.lg`
41 | grid-template-columns: repeat(3,1fr);
42 | gap:2rem;
43 | `}
44 | .about__text {
45 | max-width: 600px;
46 | }
47 | .about__img {
48 | max-width: 700px;
49 | ${Screen.lg`
50 | grid-column: 2/4;
51 | `}
52 | }
53 |
54 | h1 {
55 | color: var(--green-color-1);
56 | margin-bottom: 2rem;
57 | }
58 | `;
59 |
60 | export default AboutPage;
61 |
--------------------------------------------------------------------------------
/src/reducers/products_reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | CLOSE_SIDEBAR,
3 | OPEN_SIDEBAR,
4 | GET_PRODUCTS_BEGIN,
5 | GET_PRODUCTS_ERROR,
6 | GET_PRODUCTS_SUCCESS,
7 | GET_SINGLE_PRODUCT_BEGIN,
8 | GET_SINGLE_PRODUCT_ERROR,
9 | GET_SINGLE_PRODUCT_SUCCESS,
10 | } from "../utils/actions";
11 |
12 | export const products_reducer = (state, action) => {
13 | let { type, payload } = action;
14 |
15 | if (type === CLOSE_SIDEBAR) {
16 | return { ...state, isSidebarOpen: false };
17 | }
18 |
19 | if (type === OPEN_SIDEBAR) {
20 | return { ...state, isSidebarOpen: true };
21 | }
22 |
23 | if (type === GET_PRODUCTS_BEGIN) {
24 | return { ...state, products_loading: true };
25 | }
26 |
27 | if (type === GET_PRODUCTS_SUCCESS) {
28 | const popular_products = payload.filter(
29 | (product) => product.rating.rate >= 4.5
30 | );
31 | return {
32 | ...state,
33 | products_loading: false,
34 | products: payload,
35 | popular_products,
36 | };
37 | }
38 |
39 | if (type === GET_PRODUCTS_ERROR) {
40 | return { ...state, products_loading: false, products_error: true };
41 | }
42 |
43 | if (type === GET_SINGLE_PRODUCT_BEGIN) {
44 | return { ...state, single_product_loading: true };
45 | }
46 |
47 | if (type === GET_SINGLE_PRODUCT_SUCCESS) {
48 | return {
49 | ...state,
50 | single_product_loading: false,
51 | single_product: payload,
52 | };
53 | }
54 |
55 | if (type === GET_SINGLE_PRODUCT_ERROR) {
56 | return {
57 | ...state,
58 | single_product_loading: false,
59 | single_product_error: true,
60 | };
61 | }
62 |
63 | throw new Error(`No Matching "${type}" - action type `);
64 | };
65 |
--------------------------------------------------------------------------------
/src/components/Hero.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from "react-router-dom";
3 | import styled from "styled-components";
4 |
5 | import heroImg from "../assets/hero_img.png";
6 | import { Typography, Button } from ".";
7 |
8 | const Hero = () => {
9 | return (
10 |
11 |
12 |
13 | Everything you need to be good looking
14 |
15 |
16 | Shop Now
17 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 | };
25 |
26 | const Wrapper = styled.section`
27 | overflow: hidden;
28 |
29 | .hero {
30 | padding: 0 1rem 0 2rem;
31 | display: flex;
32 | justify-content: space-between;
33 | flex-direction: column;
34 | gap: 1rem;
35 | }
36 |
37 | .hero__title {
38 | align-self: flex-start;
39 | margin-top: 10vh;
40 | position: relative;
41 | z-index: 1;
42 | span {
43 | color: var(--green-color-1);
44 | }
45 |
46 | &::before {
47 | content: "";
48 | position: absolute;
49 | top: -1rem;
50 | left: -5rem;
51 | width: 12rem;
52 | height: 12rem;
53 | border-radius: 50%;
54 | background: var(--yellow-color-1);
55 | z-index: -1;
56 | }
57 | }
58 |
59 | .header__link {
60 | width: max-content;
61 | z-index: 1;
62 | }
63 |
64 | .hero__img {
65 | width: min(90%, 480px);
66 | align-self: flex-end;
67 | }
68 | `;
69 |
70 | export default Hero;
71 |
--------------------------------------------------------------------------------
/src/components/share/Icons.js:
--------------------------------------------------------------------------------
1 | import styled, { css } from "styled-components";
2 |
3 | import {
4 | FaBars,
5 | FaTimes,
6 | FaStore,
7 | FaShoppingCart,
8 | FaPlus,
9 | FaMinus,
10 | FaTrash,
11 | } from "react-icons/fa";
12 |
13 | import { BsFillGridFill, BsList } from "react-icons/bs";
14 |
15 |
16 | //? base styles
17 | const bigIcon = css`
18 | font-size: 2.5rem;
19 | `;
20 |
21 | const mediumIcon = css`
22 | font-size: 2rem;
23 | `;
24 |
25 | //? styled icons
26 | const BsFillGridFillStyled = styled(BsFillGridFill)`
27 | color: var(--blue-color-1);
28 | ${mediumIcon}
29 | `;
30 |
31 | const BsListStyled = styled(BsList)`
32 | color: var(--blue-color-1);
33 | ${mediumIcon}
34 | `;
35 |
36 | const FaPlusStyled = styled(FaPlus)`
37 | ${mediumIcon}
38 | color: var(--green-color-1);
39 | `;
40 |
41 | const FaTrashStyled = styled(FaTrash)`
42 | ${mediumIcon}
43 | color: var(--red-color-1);
44 | `;
45 |
46 | const FaMinusStyled = styled(FaMinus)`
47 | ${mediumIcon}
48 | color: var(--red-color-1);
49 | `;
50 |
51 | const FaTimesStyled = styled(FaTimes)`
52 | ${bigIcon}
53 | color: var(--red-color-1);
54 | `;
55 |
56 | const FaBarsStyled = styled(FaBars)`
57 | ${bigIcon}
58 | color: var(--blue-color-1);
59 | `;
60 |
61 | const FaStoreStyled = styled(FaStore)`
62 | ${bigIcon}
63 | color: var(--blue-color-1);
64 | `;
65 |
66 | const FaShoppingCartStyled = styled(FaShoppingCart)`
67 | ${mediumIcon}
68 | color: var(--blue-color-1);
69 | `;
70 |
71 | const Icons = {
72 | FaBarsStyled,
73 | FaTimesStyled,
74 | FaStoreStyled,
75 | FaShoppingCartStyled,
76 | FaPlusStyled,
77 | FaMinusStyled,
78 | BsFillGridFillStyled,
79 | FaTrashStyled,
80 | BsListStyled,
81 | };
82 |
83 | export default Icons;
84 |
--------------------------------------------------------------------------------
/src/components/GridProducts.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import { Link } from "react-router-dom";
4 |
5 | import Screen from "../styles/Screen";
6 |
7 | import { truncate } from "../utils/helpers";
8 |
9 | import { Stars, Typography } from ".";
10 |
11 | const GridProducts = ({ products }) => {
12 | return (
13 |
14 | {products.map(({ title, id, image, price, rating }) => (
15 |
16 |
17 |
18 |
19 | {truncate(title, 25)}
20 |
24 |
25 | ))}
26 |
27 | );
28 | };
29 |
30 | const Wrapper = styled.div`
31 | display: grid;
32 | gap: 2rem 1.5rem;
33 | justify-content: center;
34 |
35 | ${Screen.md`
36 | grid-template-columns: 1fr 1fr;
37 | `}
38 | ${Screen.xl`
39 | grid-template-columns: 1fr 1fr 1fr ;
40 | `}
41 |
42 | article {
43 | padding: 1rem;
44 | background: var(--white-color);
45 | border-radius: var(--radius);
46 | transition: var(--transition);
47 | min-height: 35rem;
48 |
49 | &:hover {
50 | box-shadow: 0 0 1rem 1rem var(--gray-color-2);
51 | }
52 | h3 {
53 | margin: 1rem 0;
54 | min-height: 2.4rem;
55 | }
56 | img {
57 | max-height: 25rem;
58 | object-fit: contain;
59 | }
60 | div {
61 | display: flex;
62 | justify-content: space-between;
63 | align-items: center;
64 | }
65 |
66 | p {
67 | font-size: 1.8rem;
68 | color: var(--red-color-1);
69 | }
70 | }
71 | `;
72 |
73 | export default GridProducts;
74 |
--------------------------------------------------------------------------------
/src/components/Contact.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 |
4 | import { Typography } from ".";
5 |
6 | const Contact = () => {
7 | return (
8 |
9 |
10 | Join our newsletter and get 20% off
11 |
12 | Shop now, pay later. You'll only pay for the items you keep. Your
13 | payment will automatically be deducted from your card after 30 days,
14 | no additional charge
15 |
16 |
17 |
21 |
22 | );
23 | };
24 |
25 | const Wrapper = styled.section`
26 | display: flex;
27 | flex-direction: column;
28 | justify-content: center;
29 | align-items: center;
30 | gap: 2rem;
31 | padding: 4rem 1rem;
32 | max-width: 750px;
33 |
34 | .contact__text {
35 | h2 {
36 | margin-bottom: 2rem;
37 | }
38 | }
39 |
40 | .contact__form {
41 | border-radius: var(--radius);
42 | border: 0.3rem solid var(--green-color-1);
43 | padding: 0.5rem;
44 | width: min(100%, 400px);
45 | display: flex;
46 | gap: 1rem;
47 | justify-content: space-between;
48 |
49 | input,
50 | button {
51 | font-size: var(--fs-400);
52 | }
53 |
54 | button {
55 | background: var(--green-color-1);
56 | border-radius: var(--radius);
57 | padding: 0.5rem 1rem;
58 | transition: var(--transition);
59 | color: var(--blue-color-3);
60 | &:hover {
61 | color: var(--white-color);
62 | }
63 | }
64 | }
65 | `;
66 |
67 | export default Contact;
68 |
--------------------------------------------------------------------------------
/src/components/CartButtons.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { NavLink } from "react-router-dom";
3 | import styled from "styled-components";
4 |
5 | import { Icons } from ".";
6 |
7 | import { useProductsContext } from "../contexts/products_context";
8 | import { useCartContext } from "../contexts/cart_context";
9 |
10 | const CartButtons = () => {
11 | const { closeSidebar } = useProductsContext();
12 | const { total_items } = useCartContext();
13 |
14 | return (
15 |
16 |
20 | isActive ? "cart__container active" : "cart__container"
21 | }
22 | >
23 | Cart
24 |
25 |
26 | {total_items}
27 |
28 |
29 |
30 | );
31 | };
32 |
33 | const Wrapper = styled.div`
34 | display: flex;
35 | align-items: center;
36 |
37 | a {
38 | font-size: var(--fs-500);
39 | }
40 |
41 | .cart__container {
42 | display: flex;
43 | margin-right: 3rem;
44 | border-bottom: 0.3rem solid transparent;
45 | @media (max-width: 768px) {
46 | font-size: 2rem;
47 | }
48 | }
49 |
50 | .active {
51 | border-color: var(--red-color-1);
52 | }
53 |
54 | .cart-icon {
55 | position: relative;
56 | margin-left: 0.4rem;
57 |
58 | span {
59 | position: absolute;
60 | top: -12px;
61 | right: -18px;
62 | padding: 0.1rem;
63 | min-width: 2rem;
64 | background-color: var(--red-color-1);
65 | border-radius: 50%;
66 | color: var(--blue-color-3);
67 | font-size: 1.2rem;
68 | display: flex;
69 | align-items: center;
70 | justify-content: center;
71 | }
72 | }
73 | `;
74 |
75 | export default CartButtons;
76 |
--------------------------------------------------------------------------------
/src/contexts/cart_context.js:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, useEffect, useReducer } from "react";
2 | import { cart_reducer as reducer } from "../reducers/cart_reducer";
3 |
4 | import {
5 | ADD_TO_CART,
6 | REMOVE_CART_ITEM,
7 | TOGGLE_CART_ITEM,
8 | CLEAR_CART,
9 | COUNT_CART_TOTALS,
10 | CHECKOUT,
11 | } from "../utils/actions";
12 |
13 | const getLocalStorage = () => {
14 | let cart = localStorage.getItem("cart");
15 | if (cart) {
16 | return JSON.parse(cart);
17 | } else {
18 | return [];
19 | }
20 | };
21 |
22 | const initialState = {
23 | cart: getLocalStorage(),
24 | total_price: 0,
25 | total_items: 0,
26 | isCheckout: false,
27 | };
28 |
29 | const CartContext = createContext();
30 |
31 | export const CartProvider = ({ children }) => {
32 | const [state, dispatch] = useReducer(reducer, initialState);
33 |
34 | useEffect(() => {
35 | dispatch({ type: COUNT_CART_TOTALS });
36 | localStorage.setItem("cart", JSON.stringify(state.cart));
37 | }, [state.cart]);
38 |
39 | //? Handlers
40 | const addToCart = (product, amount) => {
41 | dispatch({ type: ADD_TO_CART, payload: { product, amount } });
42 | };
43 |
44 | const toggleAmount = (id, value) => {
45 | dispatch({ type: TOGGLE_CART_ITEM, payload: { id, value } });
46 | };
47 |
48 | const removeItem = (id) => {
49 | dispatch({ type: REMOVE_CART_ITEM, payload: id });
50 | };
51 |
52 | const clearCart = () => {
53 | dispatch({ type: CLEAR_CART });
54 | };
55 |
56 | const checkout = () => {
57 | dispatch({ type: CHECKOUT });
58 | };
59 |
60 | return (
61 |
71 | {children}
72 |
73 | );
74 | };
75 |
76 | export const useCartContext = () => {
77 | return useContext(CartContext);
78 | };
79 |
--------------------------------------------------------------------------------
/src/pages/CartPage.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from "react-router-dom";
3 | import styled from "styled-components";
4 |
5 | import Screen from "../styles/Screen";
6 |
7 | import {
8 | Breadcrumb,
9 | CartItem,
10 | CartTotals,
11 | EmptyCart,
12 | Button,
13 | } from "../components";
14 |
15 | import { useCartContext } from "../contexts/cart_context";
16 |
17 | const CartPage = () => {
18 | const { cart, clearCart } = useCartContext();
19 |
20 | if (cart.length < 1) {
21 | return ;
22 | }
23 | return (
24 |
25 |
26 |
27 |
28 |
29 | {cart.map((item) => (
30 |
31 | ))}
32 |
33 |
34 |
35 | Buy more
36 |
37 |
42 | Clear cart
43 |
44 |
45 |
46 |
47 |
48 |
49 | );
50 | };
51 |
52 | const Wrapper = styled.section`
53 | display: grid;
54 | place-items: center;
55 | ${Screen.lg`
56 | grid-template-columns: 1fr 1fr 1fr;
57 | `}
58 |
59 | .cart-content {
60 | display: grid;
61 | gap: 1rem;
62 | ${Screen.lg`
63 | grid-column: 1/3;
64 | `}
65 | }
66 |
67 | .cart__items {
68 | padding: 1rem;
69 | }
70 |
71 | .cart__links {
72 | display: flex;
73 | align-items: center;
74 | justify-content: space-between;
75 | padding: 0.5rem 1rem;
76 | }
77 |
78 | .clear-btn {
79 | padding: 0.75rem 1.5rem;
80 | }
81 | `;
82 | export default CartPage;
83 |
--------------------------------------------------------------------------------
/src/components/ListProducts.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import { Link } from "react-router-dom";
4 |
5 | import { truncate } from "../utils/helpers";
6 |
7 | import { Button, Typography } from ".";
8 |
9 | const ListProducts = ({ products }) => {
10 | return (
11 |
12 | {products.map(({ title, image, id, price, description }) => (
13 |
14 |
15 |
16 |
17 |
18 |
{title}
19 |
${price}
20 |
{truncate(description, 120)}
21 |
22 | Details
23 |
24 |
25 |
26 | ))}
27 |
28 | );
29 | };
30 |
31 | const Wrapper = styled.div`
32 | article {
33 | display: grid;
34 | grid-template-columns: auto 1fr;
35 | gap: 1.2rem;
36 | margin-bottom: 1.5rem;
37 | background: #fff;
38 | padding: 1rem;
39 | border-radius: var(--radius);
40 | transition: var(--transition);
41 | transform: scale(1);
42 | &:hover {
43 | box-shadow: 0 0 1rem 1rem var(--gray-color-2);
44 | }
45 |
46 | img {
47 | width: min(25vw, 220px);
48 | height: 20rem;
49 | object-fit: contain;
50 | }
51 |
52 | .product__info {
53 | display: flex;
54 | flex-direction: column;
55 | justify-content: center;
56 | gap: 1rem;
57 | }
58 |
59 | h3 {
60 | min-height: 2.2rem;
61 | color: var(--blue-color-4);
62 | }
63 |
64 | .price {
65 | font-size: 1.8rem;
66 | color: var(--red-color-1);
67 | }
68 |
69 | button {
70 | width: max-content;
71 | }
72 | }
73 | `;
74 |
75 | export default ListProducts;
76 |
--------------------------------------------------------------------------------
/src/components/CartTotals.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 |
4 | import Screen from "../styles/Screen";
5 |
6 | import { Typography } from "../components";
7 |
8 | import { useCartContext } from "../contexts/cart_context";
9 | import { Link } from "react-router-dom";
10 |
11 | const CartTotals = () => {
12 | const { total_items, total_price, checkout } = useCartContext();
13 | return (
14 |
15 |
16 |
17 | Total Items : {total_items}
18 |
19 |
20 |
21 | Total Price : ${total_price.toFixed(2)}
22 |
23 |
24 |
25 |
31 | Checkout
32 |
33 |
34 | );
35 | };
36 |
37 | const Wrapper = styled.article`
38 | border: 0.2rem solid var(--green-color-1);
39 | border-radius: var(--radius);
40 | padding: 1rem 1.5rem;
41 |
42 | ${Screen.lg`
43 | align-self: flex-start;
44 | `}
45 |
46 | .total__items {
47 | span {
48 | color: var(--blue-color-1);
49 | display: inline-block;
50 | text-align: center;
51 | width: 12rem;
52 | font-size: var(--fs-600);
53 | }
54 | }
55 |
56 | .total__price {
57 | span {
58 | color: var(--red-color-1);
59 | display: inline-block;
60 | text-align: center;
61 | font-size: var(--fs-600);
62 | width: 12rem;
63 | }
64 | }
65 |
66 | hr {
67 | border-bottom: 0.2rem solid var(--red-color-1);
68 | margin: 1.5rem 0;
69 | }
70 |
71 | .total__btn {
72 | background: var(--green-color-1);
73 | width: 100%;
74 | padding: 0.5rem;
75 | color: var(--white-color);
76 | font-size: 1.4rem;
77 | display: inline-block;
78 | text-align: center;
79 | letter-spacing: 0.2rem;
80 | border: 0.2rem solid var(--green-color-1);
81 | transition: var(--transition);
82 | &:hover {
83 | background: transparent;
84 | color: var(--red-color-1);
85 | }
86 | }
87 | `;
88 |
89 | export default CartTotals;
90 |
--------------------------------------------------------------------------------
/src/contexts/products_context.js:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, useEffect, useReducer } from "react";
2 |
3 | import {
4 | CLOSE_SIDEBAR,
5 | OPEN_SIDEBAR,
6 | GET_PRODUCTS_BEGIN,
7 | GET_PRODUCTS_ERROR,
8 | GET_PRODUCTS_SUCCESS,
9 | GET_SINGLE_PRODUCT_BEGIN,
10 | GET_SINGLE_PRODUCT_ERROR,
11 | GET_SINGLE_PRODUCT_SUCCESS,
12 | } from "../utils/actions";
13 |
14 | import { products_reducer as reducer } from "../reducers/products_reducer";
15 |
16 | const initialState = {
17 | isSidebarOpen: false,
18 | products_loading: false,
19 | products_error: false,
20 | products: [],
21 | popular_products: [],
22 | single_product_loading: false,
23 | single_product_error: false,
24 | single_product: {},
25 | };
26 |
27 | const ProductsContext = createContext();
28 |
29 | const API_ENDPOINT = "https://fakestoreapi.com/products";
30 |
31 | export const ProductsProvider = ({ children }) => {
32 | const [state, dispatch] = useReducer(reducer, initialState);
33 |
34 | const fetchProducts = async (url) => {
35 | dispatch({ type: GET_PRODUCTS_BEGIN });
36 | try {
37 | const response = await fetch(url);
38 | const products = await response.json();
39 | dispatch({ type: GET_PRODUCTS_SUCCESS, payload: products });
40 | } catch (error) {
41 | dispatch({ type: GET_PRODUCTS_ERROR });
42 | }
43 | };
44 |
45 | const fetchSingleProduct = async (params) => {
46 | dispatch({ type: GET_SINGLE_PRODUCT_BEGIN });
47 | try {
48 | const response = await fetch(`${API_ENDPOINT}/${params}`);
49 | const singleProduct = await response.json();
50 | dispatch({ type: GET_SINGLE_PRODUCT_SUCCESS, payload: singleProduct });
51 | } catch (error) {
52 | dispatch({ type: GET_SINGLE_PRODUCT_ERROR });
53 | }
54 | };
55 |
56 | useEffect(() => {
57 | fetchProducts(API_ENDPOINT);
58 | }, []);
59 |
60 | //? Handlers
61 | const openSidebar = () => {
62 | dispatch({ type: OPEN_SIDEBAR });
63 | };
64 | const closeSidebar = () => {
65 | dispatch({ type: CLOSE_SIDEBAR });
66 | };
67 | return (
68 |
71 | {children}
72 |
73 | );
74 | };
75 |
76 | export const useProductsContext = () => {
77 | return useContext(ProductsContext);
78 | };
79 |
--------------------------------------------------------------------------------
/src/contexts/filter_context.js:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, useEffect, useReducer } from "react";
2 |
3 | import {
4 | CLEAR_FILTERS,
5 | FILTER_PRODUCTS,
6 | UPDATE_SORT,
7 | LOAD_PRODUCTS,
8 | SET_GRID_VIEW,
9 | SET_LIST_VIEW,
10 | SORT_PRODUCTS,
11 | UPDATE_FILTERS,
12 | } from "../utils/actions";
13 | import { filter_reducer as reducer } from "../reducers/filter_reducer";
14 |
15 | import { useProductsContext } from "./products_context";
16 |
17 | const initialState = {
18 | filtered_products: [],
19 | all_products: [],
20 | grid_view: true,
21 | sort: "price-lowest",
22 | filters: {
23 | text: "",
24 | category: "all",
25 | min_price: 0,
26 | price: 0,
27 | max_price: 0,
28 | },
29 | };
30 |
31 | const FilterContext = createContext();
32 |
33 | export const FilterProvider = ({ children }) => {
34 | const { products } = useProductsContext();
35 | const [state, dispatch] = useReducer(reducer, initialState);
36 |
37 | useEffect(() => {
38 | dispatch({ type: LOAD_PRODUCTS, payload: products });
39 | }, [products]);
40 |
41 | useEffect(() => {
42 | dispatch({ type: FILTER_PRODUCTS });
43 | dispatch({ type: SORT_PRODUCTS });
44 | }, [products, state.sort, state.filters]);
45 |
46 | //? Handlers
47 | const setGridView = () => {
48 | dispatch({ type: SET_GRID_VIEW });
49 | };
50 | const setListView = () => {
51 | dispatch({ type: SET_LIST_VIEW });
52 | };
53 | const updateSort = (e) => {
54 | const value = e.target.value;
55 | dispatch({ type: UPDATE_SORT, payload: value });
56 | };
57 | const updateFilters = (e) => {
58 | let name = e.target.name;
59 | let value = e.target.value;
60 | console.log({ name, value });
61 | if (name === "category") {
62 | value = e.target.dataset.category;
63 | }
64 | if (name === "price") {
65 | value = Number(value);
66 | }
67 | dispatch({ type: UPDATE_FILTERS, payload: { name, value } });
68 | };
69 | const clearFilters = () => {
70 | dispatch({ type: CLEAR_FILTERS });
71 | };
72 | return (
73 |
83 | {children}
84 |
85 | );
86 | };
87 |
88 | export const useFilterContext = () => {
89 | return useContext(FilterContext);
90 | };
91 |
--------------------------------------------------------------------------------
/src/components/CartItem.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from "react-router-dom";
3 | import styled from "styled-components";
4 |
5 | import { truncate } from "../utils/helpers";
6 |
7 | import { AmountButtons, Typography, Icons } from ".";
8 |
9 | import Screen from "../styles/Screen";
10 |
11 | import { useCartContext } from "../contexts/cart_context";
12 |
13 | const CartItem = ({ title, price, amount, id, image }) => {
14 | const { toggleAmount, removeItem } = useCartContext();
15 | //? Handlers
16 | const increase = () => {
17 | toggleAmount(id, "inc");
18 | };
19 | const decrease = () => {
20 | toggleAmount(id, "dec");
21 | };
22 |
23 | return (
24 |
25 |
26 |
27 |
28 |
29 |
30 | {truncate(title, 20)}
31 |
32 |
33 | ${price} ☓ {amount} :
34 |
35 | ${(price * amount).toFixed(2)}
36 |
37 |
38 |
39 | removeItem(id)}
42 | >
43 |
44 |
45 |
46 | );
47 | };
48 |
49 | const Wrapper = styled.article`
50 | padding: 0.5rem;
51 | background: var(--white-color);
52 | display: flex;
53 | align-items: center;
54 | gap: 1rem;
55 | margin-bottom: 0.5rem;
56 |
57 | ${Screen.sm`
58 | gap: 2rem;
59 | `}
60 | ${Screen.md`
61 | gap: 3rem;
62 | `}
63 |
64 | .item__img {
65 | display: none;
66 | ${Screen.md`
67 | display: inline;
68 | `}
69 | img {
70 | max-width: 8rem;
71 | }
72 | }
73 |
74 | .item__info {
75 | display: flex;
76 | align-items: flex-start;
77 | justify-content: center;
78 | flex-direction: column;
79 | gap: 0.5rem;
80 | width: 100%;
81 |
82 | span {
83 | color: var(--red-color-1);
84 | }
85 |
86 | a {
87 | font-size: var(--fs-400);
88 | }
89 | }
90 |
91 | .item__delete {
92 | padding: 0.6rem;
93 | transition: var(--transition);
94 | transform: scale(1);
95 | }
96 |
97 | .active {
98 | transform: scale(1.3);
99 | }
100 | `;
101 |
102 | export default CartItem;
103 |
--------------------------------------------------------------------------------
/src/reducers/cart_reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | ADD_TO_CART,
3 | REMOVE_CART_ITEM,
4 | TOGGLE_CART_ITEM,
5 | CLEAR_CART,
6 | COUNT_CART_TOTALS,
7 | CHECKOUT,
8 | } from "../utils/actions";
9 |
10 | export const cart_reducer = (state, action) => {
11 | const { type, payload } = action;
12 |
13 | if (type === ADD_TO_CART) {
14 | const { product, amount } = payload;
15 | const tempItem = state.cart.find((item) => item.id === product.id);
16 | if (tempItem) {
17 | const tempCart = state.cart.map((item) => {
18 | if (item.id === tempItem.id) {
19 | let newAmount = item.amount + amount;
20 | return { ...item, amount: newAmount };
21 | } else {
22 | return item;
23 | }
24 | });
25 | return { ...state, cart: tempCart };
26 | } else {
27 | const { id, title, image, price } = product;
28 | const newItem = {
29 | id,
30 | title,
31 | image,
32 | price,
33 | amount,
34 | };
35 | return { ...state, cart: [...state.cart, newItem], isCheckout: false };
36 | }
37 | }
38 |
39 | if (type === TOGGLE_CART_ITEM) {
40 | const { id, value } = payload;
41 | const tempCart = state.cart.map((item) => {
42 | if (item.id === id) {
43 | if (value === "inc") {
44 | let newAmount = item.amount + 1;
45 | return { ...item, amount: newAmount };
46 | }
47 | if (value === "dec") {
48 | let newAmount = item.amount - 1;
49 | if (newAmount < 1) {
50 | newAmount = 1;
51 | }
52 | return { ...item, amount: newAmount };
53 | }
54 | }
55 | return item;
56 | });
57 | return { ...state, cart: tempCart };
58 | }
59 |
60 | if (type === REMOVE_CART_ITEM) {
61 | const tempCart = state.cart.filter((item) => item.id !== payload);
62 | return { ...state, cart: tempCart };
63 | }
64 |
65 | if (type === CLEAR_CART) {
66 | return { ...state, cart: [] };
67 | }
68 |
69 | if (type === COUNT_CART_TOTALS) {
70 | const { total_items, total_price } = state.cart.reduce(
71 | (total, cartItem) => {
72 | const { amount, price } = cartItem;
73 | total.total_items += amount;
74 | total.total_price += price * amount;
75 |
76 | return total;
77 | },
78 | {
79 | total_price: 0,
80 | total_items: 0,
81 | }
82 | );
83 | return { ...state, total_items, total_price };
84 | }
85 |
86 | if (type === CHECKOUT) {
87 | return {
88 | ...state,
89 | isCheckout: true,
90 | cart: [],
91 | };
92 | }
93 |
94 | throw new Error(`No Matching "${type}" - action type `);
95 | };
96 |
--------------------------------------------------------------------------------
/src/components/Navbar.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { NavLink, Link } from "react-router-dom";
3 | import styled from "styled-components";
4 |
5 | import Screen from "../styles/Screen";
6 |
7 | import { links } from "../utils/constants";
8 |
9 | import { CartButtons } from "./index";
10 |
11 | import { Icons } from ".";
12 |
13 | import { useProductsContext } from "../contexts/products_context";
14 |
15 | const Navbar = () => {
16 | const { openSidebar } = useProductsContext();
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | {links.map(({ id, url, text }) => (
26 |
27 | (isActive ? "active" : null)}
30 | >
31 | {text}
32 |
33 |
34 | ))}
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | );
46 | };
47 |
48 | const Header = styled.header`
49 | height: var(--header-height);
50 | display: flex;
51 | align-items: center;
52 | padding-inline: 2rem;
53 | box-shadow: 0 0 5rem 0.5rem var(--bg-color);
54 | position: fixed;
55 | background: var(--blue-color-2);
56 | top: 0;
57 | left: 0;
58 | width: 100%;
59 | z-index: 100;
60 |
61 | .header-center {
62 | width: min(100%, var(--max-width));
63 | margin-inline: auto;
64 | display: flex;
65 | align-items: center;
66 | justify-content: space-between;
67 | }
68 |
69 | .header__btn {
70 | ${Screen.md`
71 | display:none;
72 | `}
73 | }
74 |
75 | .header__nav {
76 | display: none;
77 | ${Screen.md`
78 | display:inline-block;
79 | `}
80 |
81 | ul {
82 | display: inline-flex;
83 | gap: 2.5rem;
84 |
85 | a {
86 | display: inline-block;
87 | font-size: var(--fs-500);
88 | padding: 0.2rem;
89 | border-bottom: 0.3rem solid transparent;
90 | }
91 |
92 | .active {
93 | border-bottom-color: var(--red-color-1);
94 | }
95 | }
96 | }
97 |
98 | .header__cart {
99 | display: none;
100 | ${Screen.md`
101 | display:inline-block;
102 | `}
103 | }
104 | `;
105 |
106 | export default Navbar;
107 |
--------------------------------------------------------------------------------
/src/styles/GlobalStyle.js:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from "styled-components";
2 | import Screen from "./Screen";
3 |
4 | //? fonts
5 | import "@fontsource/poppins";
6 |
7 | const GlobalStyle = createGlobalStyle`
8 | /* variables */
9 | :root {
10 | /** colors */
11 | --blue-color-1: #155e75;
12 | --blue-color-2: #edffec;
13 | --blue-color-3: #efeff0;
14 | --blue-color-4: #075985;
15 | --bg-color: #fff;
16 | --red-color-1: #e11d48;
17 | --green-color-1: #16a34a;
18 | --yellow-color-1: #ffff00;
19 | --yellow-color-2: #ffa200;
20 | --gray-color-1: #6b7280;
21 | --gray-color-2: #d6d7da41;
22 | --white-color: #fff;
23 |
24 | /** sizes */
25 | --max-width: 1300px;
26 | --header-height: 6rem;
27 | --footer-height: 5rem;
28 | --breadcrumb-height: 5rem;
29 |
30 | /** styles */
31 | --transition: 0.3s;
32 | --radius: 0.35rem;
33 |
34 |
35 | /* font-sizes */
36 | --fs-900: 9.375rem;
37 | --fs-800: 3rem;
38 | --fs-700: 2.5rem;
39 | --fs-600: 2rem;
40 | --fs-500: 1.62rem;
41 | --fs-400: 1.425rem;
42 | --fs-300: 1rem;
43 | --fs-200: 0.875rem;
44 | }
45 |
46 | /* resets */
47 | * {
48 | padding: 0;
49 | margin: 0;
50 | box-sizing: border-box;
51 | border: none;
52 | background: none;
53 | outline: none;
54 | }
55 |
56 | html {
57 | scroll-behavior: smooth;
58 | }
59 |
60 | ul {
61 | list-style-type: none;
62 | }
63 |
64 | a {
65 | text-decoration: none;
66 | }
67 |
68 | button {
69 | cursor: pointer;
70 | }
71 |
72 | /*? typographi */
73 | html {
74 | font-size: 0.525rem; //8px
75 |
76 | ${Screen.sm`
77 | font-size: 0.562rem; //9px
78 | `}
79 |
80 | ${Screen.md`
81 | font-size: 0.625rem; //10px
82 | `}
83 |
84 | ${Screen.lg`
85 | font-size: 0.75rem; //12px
86 | `}
87 | }
88 |
89 |
90 | /*? global styles */
91 |
92 | body {
93 | background: var(--bg-color);
94 | overflow-x: hidden;
95 | font-family: "Poppins", sans-serif;
96 | }
97 |
98 | img {
99 | width: 100%;
100 | height: 100%;
101 | }
102 |
103 | section {
104 | width: min(100%, var(--max-width));
105 | margin-inline: auto;
106 | }
107 | main {
108 | margin-top: var(--header-height);
109 | }
110 |
111 | a {
112 | color: var(--green-color-1);
113 | transition: var(--transition);
114 | }
115 | a:hover {
116 | color: var(--blue-color-1);
117 | }
118 | /*? global class */
119 |
120 | .page {
121 | min-height: calc(
122 | 100vh -
123 | (var(--header-height) + var(--footer-height) + var(--breadcrumb-height))
124 | );
125 | }
126 | .page-w-b {
127 | margin-top: var(--header-height);
128 | min-height: calc(100vh - (var(--header-height) + var(--footer-height)));
129 | }
130 |
131 |
132 | `;
133 |
134 | export default GlobalStyle;
135 |
--------------------------------------------------------------------------------
/src/components/Sidebar.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import { Link, NavLink } from "react-router-dom";
4 |
5 | import { links } from "../utils/constants";
6 |
7 | import { CartButtons, Icons } from ".";
8 |
9 | import { useProductsContext } from "../contexts/products_context";
10 |
11 | import Screen from "../styles/Screen";
12 |
13 | const Sidebar = () => {
14 | const { closeSidebar, isSidebarOpen } = useProductsContext();
15 |
16 | return (
17 |
18 |
46 |
47 | );
48 | };
49 |
50 | const Wrapper = styled.div`
51 | ${Screen.md`
52 | display:none;
53 | `}
54 | .sidebar {
55 | background: var(--bg-color);
56 | position: fixed;
57 | top: 0;
58 | left: 0;
59 | width: 100%;
60 | height: 100%;
61 | transition: var(--transition);
62 | transform: translate(-120%);
63 | z-index: -1;
64 | }
65 |
66 | .show-sidebar {
67 | transform: translate(0);
68 | z-index: 999;
69 | }
70 |
71 | .sidebar-header {
72 | height: var(--header-height);
73 | display: flex;
74 | align-items: center;
75 | justify-content: space-between;
76 | background: var(--blue-color-2);
77 | padding: 0 2rem;
78 | box-shadow: 0 0 1rem 1rem var(--bg-color);
79 | }
80 |
81 | .sidebar__nav {
82 | margin-bottom: 4rem;
83 | a {
84 | display: block;
85 | padding: 1rem;
86 | font-size: var(--fs-600);
87 | &:hover {
88 | padding-left: 1.5rem;
89 | }
90 | }
91 | .active {
92 | color: var(--red-color-1);
93 | }
94 | }
95 |
96 | .sidebar__cart {
97 | width: max-content;
98 | margin-inline: auto;
99 | }
100 | `;
101 |
102 | export default Sidebar;
103 |
--------------------------------------------------------------------------------
/src/reducers/filter_reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | CLEAR_FILTERS,
3 | FILTER_PRODUCTS,
4 | UPDATE_SORT,
5 | LOAD_PRODUCTS,
6 | SET_GRID_VIEW,
7 | SORT_PRODUCTS,
8 | UPDATE_FILTERS,
9 | SET_LIST_VIEW,
10 | } from "../utils/actions";
11 |
12 | export const filter_reducer = (state, action) => {
13 | const { type, payload } = action;
14 |
15 | if (type === LOAD_PRODUCTS) {
16 | let maxPrice = payload.map((product) => product.price);
17 | maxPrice = Math.max(...maxPrice);
18 |
19 | return {
20 | ...state,
21 | all_products: [...payload],
22 | filtered_products: [...payload],
23 | filters: { ...state.filters, max_price: maxPrice, price: maxPrice },
24 | };
25 | }
26 |
27 | if (type === SET_GRID_VIEW) {
28 | return { ...state, grid_view: true };
29 | }
30 | if (type === SET_LIST_VIEW) {
31 | return { ...state, grid_view: false };
32 | }
33 |
34 | if (type === UPDATE_SORT) {
35 | return { ...state, sort: payload };
36 | }
37 |
38 | if (type === SORT_PRODUCTS) {
39 | const { sort, filtered_products } = state;
40 | let tempProducts = [...filtered_products];
41 | if (sort === "price-lowest") {
42 | tempProducts = tempProducts.sort((a, b) => a.price - b.price);
43 | }
44 | if (sort === "price-highest") {
45 | tempProducts = tempProducts.sort((a, b) => b.price - a.price);
46 | }
47 | if (sort === "name-a") {
48 | tempProducts = tempProducts.sort((a, b) => {
49 | return a.title.localeCompare(b.title);
50 | });
51 | }
52 | if (sort === "name-z") {
53 | tempProducts = tempProducts.sort((a, b) => {
54 | return b.title.localeCompare(a.title);
55 | });
56 | }
57 |
58 | return { ...state, filtered_products: tempProducts };
59 | }
60 |
61 | if (type === UPDATE_FILTERS) {
62 | const { name, value } = payload;
63 | return { ...state, filters: { ...state.filters, [name]: value } };
64 | }
65 |
66 | if (type === FILTER_PRODUCTS) {
67 | const { all_products } = state;
68 | const { text, category, price } = state.filters;
69 | let tempProducts = [...all_products];
70 |
71 | if (text) {
72 | tempProducts = tempProducts.filter((product) =>
73 | product.title.toLowerCase().includes(text)
74 | );
75 | }
76 |
77 | if (category !== "all") {
78 | tempProducts = tempProducts.filter(
79 | (product) => product.category === category
80 | );
81 | }
82 |
83 | tempProducts = tempProducts.filter((product) => product.price <= price);
84 |
85 | return { ...state, filtered_products: tempProducts };
86 | }
87 |
88 | if (type === CLEAR_FILTERS) {
89 | return {
90 | ...state,
91 | filters: {
92 | ...state.filters,
93 | text: "",
94 | category: "all",
95 | price: state.filters.max_price,
96 | },
97 | };
98 | }
99 |
100 | throw new Error(`No Matching "${type}" - action type `);
101 | };
102 |
--------------------------------------------------------------------------------
/src/components/Sort.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 |
4 | import { useFilterContext } from "../contexts/filter_context";
5 |
6 | import { Typography, Icons } from ".";
7 |
8 | import Screen from "../styles/Screen";
9 |
10 | const Sort = () => {
11 | const {
12 | filtered_products: products,
13 | grid_view,
14 | setGridView,
15 | setListView,
16 | updateSort,
17 | sort,
18 | } = useFilterContext();
19 | return (
20 |
21 |
22 |
27 |
28 |
29 |
34 |
35 |
36 |
37 |
38 | {products.length} items found
39 |
40 |
41 |
56 |
57 | );
58 | };
59 |
60 | const Wrapper = styled.div`
61 | display: flex;
62 | align-items: center;
63 | margin-bottom: 2rem;
64 | ${Screen.sm`
65 | gap:0 1rem;
66 | `}
67 |
68 | .sort__btns {
69 | display: flex;
70 | align-items: center;
71 | justify-content: center;
72 | gap: 0.5rem;
73 |
74 | button {
75 | margin-right: 0.3rem;
76 | display: inline-block;
77 | padding: 0.2rem;
78 | display: flex;
79 | align-items: center;
80 | justify-content: center;
81 | ${Screen.md`
82 | padding: 0.5rem;
83 | `}
84 | }
85 | }
86 |
87 | .sort__items {
88 | font-size: 1.3rem;
89 | span {
90 | display: inline-block;
91 | min-width: 2rem;
92 | text-align: center;
93 | color: var(--blue-color-1);
94 | }
95 | }
96 |
97 | .sort__line {
98 | background-color: var(--red-color-1);
99 | height: 0.3rem;
100 | width: 30%;
101 | display: none;
102 | margin-inline: auto;
103 |
104 | ${Screen.lg`
105 | display: inline-block;
106 | `}
107 | ${Screen.xl`
108 | width:40%;
109 | `}
110 | }
111 |
112 | .sort__form {
113 | margin-left: auto;
114 | label {
115 | color: var(--blue-color-1);
116 | margin-right: 0.2rem;
117 | font-size: var(--fs-400);
118 | }
119 | option {
120 | font-size: var(--fs-600);
121 | padding: 1rem;
122 | background: var(--bg-color);
123 | }
124 | }
125 |
126 | .active {
127 | border: 0.2rem solid var(--green-color-1);
128 | border-radius: 0.3rem;
129 | }
130 | `;
131 |
132 | export default Sort;
133 |
--------------------------------------------------------------------------------
/src/pages/SingleProductPage.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { Link, useNavigate, useParams } from "react-router-dom";
3 | import styled from "styled-components";
4 |
5 | import { useProductsContext } from "../contexts/products_context";
6 |
7 | import {
8 | Breadcrumb,
9 | Error,
10 | Loading,
11 | Stars,
12 | AddToCart,
13 | Button,
14 | Typography,
15 | } from "../components";
16 |
17 | import Screen from "../styles/Screen";
18 |
19 | const SingleProductPage = () => {
20 | const navigate = useNavigate();
21 | const { id } = useParams();
22 |
23 | const {
24 | single_product: product,
25 | single_product_loading: loading,
26 | single_product_error: error,
27 | fetchSingleProduct,
28 | } = useProductsContext();
29 |
30 | const { title, price, image, category, description, rating } = product;
31 |
32 | useEffect(() => {
33 | fetchSingleProduct(id);
34 | }, [id]);
35 |
36 | useEffect(() => {
37 | if (error) {
38 | setTimeout(() => {
39 | navigate("/");
40 | }, 3000);
41 | }
42 | }, [error]);
43 |
44 | if (loading) {
45 | return (
46 |
47 |
48 |
49 | );
50 | }
51 |
52 | if (error) {
53 | return (
54 |
55 |
56 |
57 | );
58 | }
59 |
60 | return (
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | back to products
71 |
72 |
{title}
73 |
74 |
75 | Price : ${price}
76 |
77 |
78 | Category : {category}
79 |
80 |
{description}
81 |
82 |
83 |
84 |
85 |
86 | );
87 | };
88 |
89 | const Wrapper = styled.section`
90 | display: flex;
91 | align-items: center;
92 | justify-content: center;
93 |
94 | article {
95 | padding: 1.5rem 1rem;
96 | background: #fff;
97 | display: grid;
98 | justify-items: center;
99 | gap: 2rem;
100 | ${Screen.lg`
101 | grid-template-columns: 1fr 1fr 1fr;
102 | `}
103 |
104 | .product__img {
105 | img {
106 | max-width: 20rem;
107 | object-fit: contain;
108 | }
109 | }
110 |
111 | .product__info {
112 | display: grid;
113 | gap: 1rem;
114 | ${Screen.lg`
115 | grid-column: 2/4;
116 | `}
117 |
118 | button {
119 | width: max-content;
120 | }
121 |
122 | .info__price {
123 | color: var(--green-color-1);
124 | span {
125 | color: var(--red-color-1);
126 | }
127 | }
128 |
129 | .info__category {
130 | color: var(--green-color-1);
131 | span {
132 | color: var(--gray-color-1);
133 | }
134 | }
135 | }
136 | }
137 | `;
138 |
139 | export default SingleProductPage;
140 |
--------------------------------------------------------------------------------
/src/components/Filters.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 |
4 | import { useFilterContext } from "../contexts/filter_context";
5 |
6 | import { getUniqueValues } from "../utils/helpers";
7 |
8 | import { Typography, Button } from ".";
9 |
10 | const Filters = () => {
11 | const {
12 | filters: { text, category, min_price, max_price, price },
13 | updateFilters,
14 | all_products: products,
15 | clearFilters,
16 | } = useFilterContext();
17 |
18 | const categories = getUniqueValues(products, "category");
19 |
20 | if (products.length > 0) {
21 | return (
22 |
23 |
24 |
65 |
70 | Reset Filters
71 |
72 |
73 |
74 | );
75 | }
76 | return
;
77 | };
78 |
79 | const Wrapper = styled.div`
80 | margin-bottom: 2rem;
81 |
82 | .content {
83 | position: sticky;
84 | top: 7rem;
85 | }
86 |
87 | .filter__form {
88 | display: grid;
89 | gap: 2rem;
90 | }
91 |
92 | .search__input {
93 | background: var(--blue-color-3);
94 | border-radius: 0.5rem;
95 | padding: 1rem;
96 | width: min(100%, 200px);
97 | font-size: var(--fs-400);
98 | }
99 |
100 | .form__control > *:not(:first-child) {
101 | margin-top: 1rem;
102 | }
103 |
104 | .form__categories {
105 | display: grid;
106 | justify-items: flex-start;
107 | gap: 1rem;
108 |
109 | button {
110 | color: var(--blue-color-1);
111 | font-size: var(--fs-500);
112 | padding-right: 0.5rem;
113 | text-transform: capitalize;
114 | transition: var(--transition);
115 | border-bottom: 0.3rem solid transparent;
116 | &:hover {
117 | padding-left: 0.5rem;
118 | }
119 | }
120 |
121 | .active {
122 | border-bottom-color: var(--green-color-1);
123 | padding-left: 0.5rem;
124 | }
125 | }
126 |
127 | .price {
128 | color: var(--red-color-1);
129 | font-size: var(--fs-500);
130 | }
131 |
132 | .clear-btn {
133 | margin-block: 2rem;
134 | padding: 0.75rem 1.5rem;
135 | }
136 | `;
137 |
138 | export default Filters;
139 |
--------------------------------------------------------------------------------