├── src ├── components │ ├── Home │ │ ├── Filters │ │ │ ├── Filters.css │ │ │ ├── SortBy │ │ │ │ └── SortBy.jsx │ │ │ ├── Filters.jsx │ │ │ └── FilterBy │ │ │ │ ├── FilterBy.jsx │ │ │ │ └── FilterBy.css │ │ ├── Products │ │ │ ├── Products.css │ │ │ ├── Deals │ │ │ │ ├── Deals.css │ │ │ │ └── Deals.jsx │ │ │ ├── TopProducts │ │ │ │ ├── TopProducts.css │ │ │ │ └── TopProducts.jsx │ │ │ ├── Products.jsx │ │ │ └── Product │ │ │ │ ├── Product.css │ │ │ │ └── Product.jsx │ │ ├── Benefits │ │ │ ├── Benefits.css │ │ │ └── Benefits.jsx │ │ └── Banner │ │ │ ├── Banner.jsx │ │ │ └── Banner.css │ ├── NavBar │ │ ├── Account │ │ │ ├── Account.css │ │ │ └── Account.jsx │ │ ├── Logo │ │ │ └── Logo.jsx │ │ ├── Search │ │ │ └── Search.jsx │ │ ├── Links │ │ │ └── Links.jsx │ │ ├── NavBar.jsx │ │ └── NavBar.css │ ├── Delivery │ │ ├── DeliveryEmpty │ │ │ ├── DeliveryEmpty.css │ │ │ └── DeliveryEmpty.jsx │ │ └── DeliveryItem │ │ │ ├── DeliveryItem.jsx │ │ │ └── DeliveryItem.css │ ├── Cart │ │ ├── EmptyState │ │ │ ├── EmptyState.css │ │ │ └── EmptyState.jsx │ │ ├── Order.css │ │ ├── Order.jsx │ │ ├── OrderDetails │ │ │ ├── OrderDetails.css │ │ │ └── OrderDetails.jsx │ │ └── OrderSummary │ │ │ ├── OrderSummary.css │ │ │ └── OrderSummary.jsx │ ├── GlobalContext │ │ └── GlobalContext.jsx │ ├── Footer │ │ ├── ShopFooter.css │ │ └── ShopFooter.jsx │ ├── CookieBanner │ │ └── CookieBanner.jsx │ ├── DeliveryEmpty │ │ └── DeliveryEmpty.jsx │ └── Modals │ │ ├── Modal.css │ │ ├── CancelOrder.jsx │ │ └── Modal.jsx ├── assets │ ├── images │ │ ├── empty-cart.png │ │ ├── girl_headphones.png │ │ └── airpods_max_pink.jpg │ └── css │ │ ├── main.css │ │ └── base.css ├── views │ ├── CartView.jsx │ ├── ErrorView.jsx │ ├── HomeView.jsx │ └── DeliveryView.jsx ├── main.jsx ├── helpers │ └── checkExpiration.js ├── store │ ├── modal.js │ ├── orders.js │ ├── auth.js │ └── products.js └── App.jsx ├── .env.development ├── vercel.json ├── .env.production ├── jsconfig.json ├── vite.config.js ├── .gitignore ├── index.html ├── package.json ├── public └── vite.svg └── README.md /src/components/Home/Filters/Filters.css: -------------------------------------------------------------------------------- 1 | .filters-container { 2 | display: flex; 3 | } 4 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | VITE_API_URL="http://localhost:3000/api" # Use localhost URL for development 2 | -------------------------------------------------------------------------------- /src/components/NavBar/Account/Account.css: -------------------------------------------------------------------------------- 1 | .small-rounded{ 2 | padding: .3rem 1rem !important; 3 | } -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [{ "source": "/(.*)", "destination": "/" }], 3 | "cleanUrls": true 4 | } 5 | -------------------------------------------------------------------------------- /src/assets/images/empty-cart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mini-Sylar/react-e-commerce/HEAD/src/assets/images/empty-cart.png -------------------------------------------------------------------------------- /src/assets/images/girl_headphones.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mini-Sylar/react-e-commerce/HEAD/src/assets/images/girl_headphones.png -------------------------------------------------------------------------------- /src/components/Home/Filters/SortBy/SortBy.jsx: -------------------------------------------------------------------------------- 1 | const SortBy = () => { 2 | return
SortBy
; 3 | }; 4 | export default SortBy; 5 | -------------------------------------------------------------------------------- /src/assets/images/airpods_max_pink.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mini-Sylar/react-e-commerce/HEAD/src/assets/images/airpods_max_pink.jpg -------------------------------------------------------------------------------- /src/components/NavBar/Logo/Logo.jsx: -------------------------------------------------------------------------------- 1 | const Logo = () => { 2 | return
Logo
; 3 | }; 4 | export default Logo; 5 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # Your production environment variables go here 2 | VITE_API_URL="http://some_hosting_provider.com/api" # Use production URL when deployed 3 | -------------------------------------------------------------------------------- /src/components/Home/Products/Products.css: -------------------------------------------------------------------------------- 1 | .contains-product { 2 | display: grid; 3 | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); 4 | grid-gap: 2rem; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/Home/Products/Deals/Deals.css: -------------------------------------------------------------------------------- 1 | .contains-product { 2 | display: grid; 3 | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); 4 | grid-gap: 2rem; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/Home/Products/TopProducts/TopProducts.css: -------------------------------------------------------------------------------- 1 | .contains-product { 2 | display: grid; 3 | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); 4 | grid-gap: 2rem; 5 | } 6 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*"], 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@/*": ["src/*"] 7 | } 8 | }, 9 | "exclude": ["node_modules", "dist"] 10 | } 11 | -------------------------------------------------------------------------------- /src/views/CartView.jsx: -------------------------------------------------------------------------------- 1 | import Order from "../components/Cart/Order"; 2 | 3 | const CartView = () => { 4 | return ( 5 |
6 |
7 | 8 |
9 |
10 | ); 11 | }; 12 | export default CartView; 13 | -------------------------------------------------------------------------------- /src/views/ErrorView.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | const ErrorView = () => { 4 | return ( 5 |
6 |

404

7 |

Oops Page Not Found!

8 |
9 | ); 10 | }; 11 | export default ErrorView; 12 | -------------------------------------------------------------------------------- /src/components/Delivery/DeliveryEmpty/DeliveryEmpty.css: -------------------------------------------------------------------------------- 1 | .no-delivery-action-container { 2 | display: flex; 3 | justify-content: space-evenly; 4 | align-items: center; 5 | flex-wrap: wrap; 6 | } 7 | 8 | .no-delivery-container{ 9 | min-height: 600px; 10 | } 11 | 12 | .login-bg { 13 | background-color: #ff8b15; 14 | color: #fff; 15 | } 16 | -------------------------------------------------------------------------------- /src/components/NavBar/Search/Search.jsx: -------------------------------------------------------------------------------- 1 | import { MdSearch } from "react-icons/md"; 2 | 3 | const Search = () => { 4 | return ( 5 |
6 | 7 | 10 |
11 | ); 12 | }; 13 | export default Search; 14 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import { fileURLToPath, URL } from "node:url"; 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | resolve: { 8 | alias: { 9 | "@": fileURLToPath(new URL("./src", import.meta.url)), 10 | }, 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | Screens 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | E-Commerce-Sigma 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | import GlobalContext from "./components/GlobalContext/GlobalContext"; 5 | 6 | 7 | 8 | import "./assets/css/main.css"; 9 | ReactDOM.createRoot(document.getElementById("root")).render( 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /src/components/Home/Filters/Filters.jsx: -------------------------------------------------------------------------------- 1 | import FilterBy from "./FilterBy/FilterBy"; 2 | import SortBy from "./SortBy/SortBy"; 3 | 4 | import "./Filters.css"; 5 | 6 | const Filters = () => { 7 | return ( 8 |
9 |
10 | 11 | 12 |
13 |
14 | ); 15 | }; 16 | export default Filters; 17 | -------------------------------------------------------------------------------- /src/components/Cart/EmptyState/EmptyState.css: -------------------------------------------------------------------------------- 1 | .empty-cart-state { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | height: 100%; 7 | width: 100%; 8 | text-align: center; 9 | } 10 | 11 | .empty-cart-image img { 12 | width: 90%; 13 | height: 100%; 14 | object-fit: cover; 15 | } 16 | 17 | .empty-cart-text { 18 | font-size: 1.2rem; 19 | font-weight: 500; 20 | } 21 | 22 | .empty-cart-text h2 { 23 | margin-top: 0; 24 | margin-bottom: 10px; 25 | } 26 | 27 | .empty-cart-text p { 28 | margin-top: 0; 29 | margin-bottom: 25px; 30 | } 31 | 32 | .add-item { 33 | background-color: #ff8b15; 34 | padding: 1rem 2rem; 35 | border-radius: 0.5rem; 36 | } 37 | -------------------------------------------------------------------------------- /src/components/Cart/EmptyState/EmptyState.jsx: -------------------------------------------------------------------------------- 1 | import "./EmptyState.css"; 2 | import EmptyCart from "../../../assets/images/empty-cart.png"; 3 | import { Link } from "react-router-dom"; 4 | const EmptyState = () => { 5 | return ( 6 |
7 |
8 | Empty cart 9 |
10 |
11 |

Cart is empty

12 |

Looks like you haven't added anything to your cart yet.

13 | 14 | Add items to get started 15 | 16 |
17 |
18 | ); 19 | }; 20 | export default EmptyState; 21 | -------------------------------------------------------------------------------- /src/components/GlobalContext/GlobalContext.jsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from "react"; 2 | import useStore from "../../store/products"; 3 | import useAuth from "../../store/auth"; 4 | import useModal from "../../store/modal"; 5 | import useOrders from "../../store/orders"; 6 | 7 | const globalContext = createContext(); 8 | 9 | export const useGlobalContext = () => useContext(globalContext); 10 | 11 | const GlobalContext = ({ children }) => { 12 | const store = useStore(); 13 | const auth = useAuth(); 14 | const modal = useModal(); 15 | const orders = useOrders(); 16 | return ( 17 | 18 | {children} 19 | 20 | ); 21 | }; 22 | export default GlobalContext; 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-e-commerce", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "hamburger-react": "^2.5.1", 13 | "localforage": "^1.10.0", 14 | "match-sorter": "^6.3.4", 15 | "react": "^18.3.1", 16 | "react-dom": "^18.3.1", 17 | "react-icons": "^5.3.0", 18 | "react-loading-skeleton": "^3.5.0", 19 | "react-router-dom": "^6.26.2", 20 | "react-spinners": "^0.14.1", 21 | "react-toastify": "^10.0.5" 22 | }, 23 | "devDependencies": { 24 | "@types/react": "^18.3.9", 25 | "@types/react-dom": "^18.3.0", 26 | "@vitejs/plugin-react": "^4.3.1", 27 | "vite": "^5.4.8" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/Home/Benefits/Benefits.css: -------------------------------------------------------------------------------- 1 | .benefits { 2 | display: grid; 3 | grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); 4 | background-color: #ffffff; 5 | margin-top: -2.5rem; 6 | width: 60%; 7 | padding: 1rem; 8 | margin-bottom: 2rem; 9 | } 10 | 11 | .benefit-title { 12 | font-size: medium; 13 | } 14 | 15 | .benefit-body { 16 | font-size: small; 17 | } 18 | 19 | .benefit-icon { 20 | font-size: 2rem; 21 | color: #ff6b08; 22 | } 23 | 24 | .main-benefit { 25 | display: flex; 26 | justify-content: center; 27 | } 28 | 29 | .benefits-item { 30 | place-content: center; 31 | text-align: center; 32 | } 33 | 34 | 35 | @media screen and (max-width:500px) { 36 | .benefits{ 37 | grid-template-columns: repeat(auto-fill,minmax(300px,1fr)); 38 | width: 100%; 39 | } 40 | } -------------------------------------------------------------------------------- /src/helpers/checkExpiration.js: -------------------------------------------------------------------------------- 1 | const setExpirationDate = (days) => { 2 | const now = new Date(); 3 | const expirationDate = new Date(now.getTime() + days * 24 * 60 * 60 * 1000); 4 | return expirationDate.toISOString(); 5 | }; 6 | 7 | const isExpired = (expirationDate) => { 8 | const now = new Date(); 9 | return now.getTime() > expirationDate.getTime(); 10 | }; 11 | 12 | const getUserFromLocalStorage = () => { 13 | const userItem = localStorage.getItem("user"); 14 | if (!userItem) { 15 | return null; 16 | } 17 | const user = JSON.parse(userItem); 18 | const expirationDate = new Date(user.expirationDate); 19 | if (isExpired(expirationDate)) { 20 | localStorage.removeItem("user"); 21 | return null; 22 | } 23 | return user; 24 | }; 25 | 26 | export { setExpirationDate, getUserFromLocalStorage }; 27 | -------------------------------------------------------------------------------- /src/components/Home/Banner/Banner.jsx: -------------------------------------------------------------------------------- 1 | import girlHeadphones from "@/assets/images/girl_headphones.png"; 2 | import "./Banner.css"; 3 | 4 | const Banner = () => { 5 | return ( 6 |
7 |
8 |
9 |

10 | Grab upto 50% off on

selected Headphones 11 |

12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 | Girl Headphones 20 |
21 |
22 |
23 | ); 24 | }; 25 | export default Banner; 26 | -------------------------------------------------------------------------------- /src/components/Footer/ShopFooter.css: -------------------------------------------------------------------------------- 1 | .useful-links { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | align-items: center; 6 | } 7 | 8 | .logo-text { 9 | font-size: clamp(3rem, 5vw, 5rem); 10 | margin-bottom: 0; 11 | } 12 | 13 | .useful-details { 14 | text-align: center; 15 | margin: 0; 16 | padding: 0; 17 | font-size: clamp(0.5rem, 1.5vw, 1rem); 18 | } 19 | 20 | .bottom-section { 21 | display: flex; 22 | justify-content: space-between; 23 | align-items: center; 24 | padding: 1rem; 25 | background-color: #ff8b15; 26 | color: white; 27 | } 28 | 29 | .bottom-section-left ul { 30 | display: flex; 31 | gap: 1rem; 32 | 33 | } 34 | 35 | @media screen and (max-width:768px) { 36 | 37 | .bottom-section{ 38 | flex-direction: column; 39 | } 40 | 41 | .bottom-section-left ul{ 42 | width: 100%; 43 | padding: 0; 44 | } 45 | 46 | } 47 | 48 | -------------------------------------------------------------------------------- /src/components/Cart/Order.css: -------------------------------------------------------------------------------- 1 | .order-title { 2 | display: flex; 3 | justify-content: space-between; 4 | } 5 | 6 | .order-container { 7 | height: 75vh; 8 | overflow: auto; 9 | } 10 | 11 | .order-container::-webkit-scrollbar { 12 | width: 0.5rem; 13 | } 14 | 15 | .order-container::-webkit-scrollbar-thumb { 16 | background-color: #bdbdbd; 17 | border-radius: 10px; 18 | -webkit-border-radius: 10px; 19 | -moz-border-radius: 10px; 20 | -ms-border-radius: 10px; 21 | -o-border-radius: 10px; 22 | } 23 | 24 | .main-order-container { 25 | display: flex; 26 | gap: 2rem; 27 | width: min(100%, 1400px); 28 | margin: 0 auto; 29 | } 30 | 31 | .view-order { 32 | flex: 1; 33 | } 34 | 35 | .order-summary { 36 | flex: 0.4; 37 | } 38 | 39 | 40 | @media screen and (max-width:768px) { 41 | .main-order-container { 42 | flex-direction: column; 43 | } 44 | .order-summary { 45 | margin-top: 2rem; 46 | } 47 | 48 | 49 | } -------------------------------------------------------------------------------- /src/components/Home/Products/Deals/Deals.jsx: -------------------------------------------------------------------------------- 1 | import Product from "../Product/Product"; 2 | import "./Deals.css"; 3 | import { useGlobalContext } from "@/components/GlobalContext/GlobalContext"; 4 | import Skeleton from "react-loading-skeleton"; 5 | 6 | const Deals = () => { 7 | let {store} = useGlobalContext(); 8 | 9 | let cheapest = store.state.products.sort((a, b) => a.price - b.price); 10 | return ( 11 |
12 |

Deals Just For You!

13 | {store.state.products.length > 0 ? ( 14 |
15 | {cheapest.map((product) => { 16 | return ; 17 | })} 18 |
19 | ) : ( 20 |
21 | 22 |
23 | )} 24 |
25 | ); 26 | }; 27 | export default Deals; 28 | -------------------------------------------------------------------------------- /src/components/Home/Products/Products.jsx: -------------------------------------------------------------------------------- 1 | import Product from "./Product/Product"; 2 | 3 | import "./Products.css"; 4 | import { useGlobalContext } from "@/components/GlobalContext/GlobalContext"; 5 | import { memo } from "react"; 6 | import Skeleton from "react-loading-skeleton"; 7 | 8 | 9 | const Products = () => { 10 | let {store} = useGlobalContext(); 11 | let sortedProducts = store.state.products 12 | .slice() 13 | .sort((a, b) => a.name.localeCompare(b.name)); 14 | return ( 15 |
16 |

Headphones For You

17 | {store.state.products.length > 0 ? ( 18 |
19 | {sortedProducts.map((product) => ( 20 | 21 | ))} 22 |
23 | ) : ( 24 |
25 | 26 |
27 | )} 28 |
29 | ); 30 | }; 31 | export default memo(Products); 32 | -------------------------------------------------------------------------------- /src/assets/css/main.css: -------------------------------------------------------------------------------- 1 | @import "./base.css"; 2 | 3 | .sub-container { 4 | width: min(90%, 1200px); 5 | margin: 0 auto; 6 | } 7 | 8 | .btn-rounded { 9 | border-radius: 50px; 10 | background-color: #ffffff; 11 | color: #fd8a15; 12 | border: none; 13 | padding: 1rem 2rem; 14 | font-weight: 900; 15 | cursor: pointer; 16 | } 17 | 18 | .btn-submit { 19 | background-color: #fd8a15; 20 | color: #ffffff; 21 | border: none; 22 | padding: 1rem 2rem; 23 | font-weight: 900; 24 | } 25 | 26 | header, 27 | footer { 28 | background-color: #ff8b15; 29 | } 30 | 31 | .hero-section { 32 | background-color: #ff8b15; 33 | padding: 1rem; 34 | margin-bottom: 1rem; 35 | } 36 | 37 | .products-section { 38 | background-color: #f9f9f9; 39 | padding: 1rem; 40 | margin-bottom: 1rem; 41 | } 42 | 43 | footer { 44 | margin-top: 2rem; 45 | padding: 2rem; 46 | color: white; 47 | } 48 | 49 | ul { 50 | list-style: none; 51 | } 52 | 53 | a { 54 | text-decoration: none; 55 | color: white; 56 | } 57 | 58 | html { 59 | scroll-behavior: smooth; 60 | } 61 | -------------------------------------------------------------------------------- /src/components/NavBar/Links/Links.jsx: -------------------------------------------------------------------------------- 1 | import { Link, useLocation } from "react-router-dom"; 2 | const Links = () => { 3 | const location = useLocation(); 4 | const isHomePage = location.pathname === "/"; 5 | 6 | const scrollToProducts = () => { 7 | if (!isHomePage) return; 8 | const products = document.getElementById("products"); 9 | products.scrollIntoView({ behavior: "smooth" }); 10 | removeExpandedClass(); 11 | }; 12 | 13 | const removeExpandedClass = () => { 14 | let mobileExpandedMenu = document.querySelector(".mobile-expanded-menu"); 15 | mobileExpandedMenu.classList.remove("mobile-expanded"); 16 | }; 17 | 18 | return ( 19 |
20 | 21 | Deals 22 | 23 | 24 | What's New 25 | 26 | 27 | Delivery 28 | 29 |
30 | ); 31 | }; 32 | 33 | // replace with react router 34 | export default Links; 35 | -------------------------------------------------------------------------------- /src/components/Home/Filters/FilterBy/FilterBy.jsx: -------------------------------------------------------------------------------- 1 | import "./FilterBy.css"; 2 | 3 | import "./FilterBy.css"; 4 | 5 | const FilterBy = () => { 6 | return ( 7 |
8 |
9 | 14 |
15 | {/* */} 16 |
17 | 22 |
23 | {/* */} 24 |
25 | 30 |
31 |
32 | ); 33 | }; 34 | export default FilterBy; 35 | -------------------------------------------------------------------------------- /src/components/Footer/ShopFooter.jsx: -------------------------------------------------------------------------------- 1 | import "./ShopFooter.css"; 2 | import { Link } from "react-router-dom"; 3 | 4 | 5 | const ShopFooter = () => { 6 | const newYear = new Date().getFullYear(); 7 | return ( 8 |
9 |
10 |

LOGO

11 | 16 |
17 |
18 |
19 | 30 |
31 |
copyright © {newYear}
32 |
33 |
34 | ); 35 | }; 36 | export default ShopFooter; 37 | -------------------------------------------------------------------------------- /src/components/CookieBanner/CookieBanner.jsx: -------------------------------------------------------------------------------- 1 | import { toast } from "react-toastify"; 2 | 3 | const CookieBanner = () => { 4 | const handleIframeLoad = (event) => { 5 | const iframeDocument = event.target.contentDocument; 6 | 7 | if (iframeDocument && iframeDocument.requestStorageAccess) { 8 | iframeDocument 9 | .requestStorageAccess() 10 | .then(function (access) { 11 | document 12 | .hasStorageAccess() 13 | .then(() => { 14 | toast.success("Storage access granted!"); 15 | }) 16 | .catch(() => { 17 | toast.error("Storage access declined"); 18 | }); 19 | }) 20 | .catch(function (error) { 21 | toast.error("No Request API") 22 | console.log("Error requesting storage access:", error); 23 | }); 24 | } 25 | }; 26 | 27 | return ( 28 |
29 | 34 |
35 | ); 36 | }; 37 | export default CookieBanner; 38 | -------------------------------------------------------------------------------- /src/components/Home/Products/TopProducts/TopProducts.jsx: -------------------------------------------------------------------------------- 1 | import Product from "../Product/Product"; 2 | 3 | import "./TopProducts.css"; 4 | import { useGlobalContext } from "@/components/GlobalContext/GlobalContext"; 5 | import Skeleton from "react-loading-skeleton"; 6 | 7 | const TopProducts = () => { 8 | let {store} = useGlobalContext(); 9 | // return from highest to lowest using times_bought 10 | 11 | let topProducts = store.state.products.sort( 12 | (a, b) => b.times_bought - a.times_bought 13 | ); 14 | return ( 15 |
16 |

Top Sellers!

17 |
18 | {store.state.products.length > 0 ? ( 19 |
20 | {topProducts.map((product) => { 21 | return ; 22 | })} 23 |
24 | ) : ( 25 |
26 | 27 |
28 | )} 29 |
30 |
31 | ); 32 | }; 33 | export default TopProducts; 34 | -------------------------------------------------------------------------------- /src/views/HomeView.jsx: -------------------------------------------------------------------------------- 1 | import Banner from "@/components/Home/Banner/Banner"; 2 | import Products from "@/components/Home/Products/Products"; 3 | import Deals from "@/components/Home/Products/Deals/Deals"; 4 | import TopProducts from "@/components/Home/Products/TopProducts/TopProducts"; 5 | import Benefits from "@/components/Home/Benefits/Benefits"; 6 | 7 | function HomeView() { 8 | return ( 9 |
10 |
11 |
12 | 13 |
14 |
15 | {/*
16 | 17 |
*/} 18 |
19 | 20 |
21 |
22 | 23 |
24 |
25 | 26 |
27 |
28 | 29 |
30 |
31 |
32 | ); 33 | } 34 | 35 | export default HomeView; 36 | -------------------------------------------------------------------------------- /src/components/DeliveryEmpty/DeliveryEmpty.jsx: -------------------------------------------------------------------------------- 1 | import EmptyCart from "@/assets/images/empty-cart.png"; 2 | import { Link } from "react-router-dom"; 3 | import "./DeliveryEmpty.css"; 4 | import { useGlobalContext } from "../../GlobalContext/GlobalContext"; 5 | const DeliveryEmpty = () => { 6 | const { modal } = useGlobalContext(); 7 | const handleLogin = () => { 8 | modal.openModal(false); 9 | }; 10 | return ( 11 |
12 |
13 | Empty cart 14 |
15 |
16 |

Oops!

17 |

18 | Looks like you haven't placed any orders, place an order or sign in to 19 | view orders 20 |

21 |
22 | 23 | Place an order 24 | 25 | or 26 | 29 |
30 |
31 |
32 | ); 33 | }; 34 | export default DeliveryEmpty; 35 | -------------------------------------------------------------------------------- /src/components/Delivery/DeliveryEmpty/DeliveryEmpty.jsx: -------------------------------------------------------------------------------- 1 | import EmptyCart from "@/assets/images/empty-cart.png"; 2 | import { Link } from "react-router-dom"; 3 | import "./DeliveryEmpty.css"; 4 | import { useGlobalContext } from "../../GlobalContext/GlobalContext"; 5 | const DeliveryEmpty = () => { 6 | const { modal } = useGlobalContext(); 7 | const handleLogin = () => { 8 | modal.openModal(false); 9 | }; 10 | return ( 11 |
12 |
13 | Empty cart 14 |
15 |
16 |

Oops!

17 |

18 | Looks like you haven't placed any orders, place an order or sign in to 19 | view orders 20 |

21 |
22 | 23 | Place an order 24 | 25 | or 26 | 29 |
30 |
31 |
32 | ); 33 | }; 34 | export default DeliveryEmpty; 35 | -------------------------------------------------------------------------------- /src/components/Cart/Order.jsx: -------------------------------------------------------------------------------- 1 | import OrderDetails from "./OrderDetails/OrderDetails"; 2 | import OrderSummary from "./OrderSummary/OrderSummary"; 3 | import EmptyState from "./EmptyState/EmptyState"; 4 | import { useGlobalContext } from "@/components/GlobalContext/GlobalContext"; 5 | 6 | import "./Order.css"; 7 | 8 | const Order = () => { 9 | let { store } = useGlobalContext(); 10 | 11 | return ( 12 |
13 |
14 |
15 |

Order

16 |

{store.state.cartQuantity} Items

17 |
18 |
19 | {(store.state.cart.length > 0 && 20 | store.state.cart.map((product) => { 21 | return ( 22 | 26 | ); 27 | })) || } 28 |
29 |
30 |
31 |

Order Summary

32 | 33 |
34 |
35 | ); 36 | }; 37 | export default Order; 38 | -------------------------------------------------------------------------------- /src/components/Home/Filters/FilterBy/FilterBy.css: -------------------------------------------------------------------------------- 1 | .contains-select { 2 | display: flex; 3 | gap: 1rem; 4 | width: 100%; 5 | } 6 | 7 | .select-dropdown, 8 | .select-dropdown * { 9 | margin: 0; 10 | padding: 0.1rem; 11 | position: relative; 12 | box-sizing: border-box; 13 | } 14 | .select-dropdown { 15 | position: relative; 16 | background-color: #e6e6e6; 17 | border-radius: 25px; 18 | -webkit-border-radius: 25px; 19 | -moz-border-radius: 25px; 20 | -ms-border-radius: 25px; 21 | -o-border-radius: 25px; 22 | } 23 | .select-dropdown select { 24 | font-size: 1rem; 25 | font-weight: normal; 26 | max-width: 100%; 27 | padding: 8px 24px 8px 10px; 28 | border: none; 29 | background-color: transparent; 30 | -webkit-appearance: none; 31 | -moz-appearance: none; 32 | appearance: none; 33 | text-align: center; 34 | } 35 | .select-dropdown select:active, 36 | .select-dropdown select:focus { 37 | outline: none; 38 | box-shadow: none; 39 | } 40 | .select-dropdown:after { 41 | content: ""; 42 | position: absolute; 43 | top: 50%; 44 | right: 8px; 45 | width: 0; 46 | height: 0; 47 | margin-top: -2px; 48 | border-top: 5px solid #aaa; 49 | border-right: 5px solid transparent; 50 | border-left: 5px solid transparent; 51 | } 52 | -------------------------------------------------------------------------------- /src/components/Home/Benefits/Benefits.jsx: -------------------------------------------------------------------------------- 1 | import "./Benefits.css"; 2 | 3 | import { FaRocket, FaSmileWink } from "react-icons/fa"; 4 | import { AiFillSafetyCertificate } from "react-icons/ai"; 5 | 6 | const Benefits = () => { 7 | let benefits = [ 8 | { 9 | icon: , 10 | title: "FAST DELIVERY", 11 | text: "Deliveries in less than 2 days", 12 | id: 1, 13 | }, 14 | { 15 | icon: , 16 | title: "SAFE PAYMENT", 17 | text: "All payments are 100% secure", 18 | id: 2, 19 | }, 20 | { 21 | icon: , 22 | title: "FRIENDLY SERVICES", 23 | text: "Best customer care services", 24 | id: 3, 25 | }, 26 | ]; 27 | const allBenefits = benefits.map((benefit) => { 28 | return ( 29 |
30 |
{benefit.icon}
31 |
32 |

{benefit.title}

33 |

{benefit.text}

34 |
35 |
36 | ); 37 | }); 38 | return ( 39 |
40 |
{allBenefits}
41 |
42 | ); 43 | }; 44 | export default Benefits; 45 | -------------------------------------------------------------------------------- /src/components/Home/Banner/Banner.css: -------------------------------------------------------------------------------- 1 | .banner { 2 | display: flex; 3 | background-color: #f9efe6; 4 | padding-inline: min(5%, 5rem); 5 | margin-top: 1rem; 6 | margin-bottom: 2rem; 7 | border-radius: 10px; 8 | -webkit-border-radius: 10px; 9 | -moz-border-radius: 10px; 10 | -ms-border-radius: 10px; 11 | -o-border-radius: 10px; 12 | } 13 | 14 | .subject, 15 | .banner-text { 16 | width: 100%; 17 | align-self: center; 18 | } 19 | 20 | .banner-text h1 { 21 | font-size: clamp(2rem, 3vw, 3rem); 22 | font-weight: 500; 23 | } 24 | 25 | .subject { 26 | display: flex; 27 | justify-content: flex-end; 28 | } 29 | 30 | .subject img { 31 | width: min(60%, 400px); 32 | object-fit: contain; 33 | } 34 | 35 | @media screen and (max-width: 769px) { 36 | .buy-now { 37 | padding: 0.8rem 1rem !important; 38 | margin-bottom: 1rem !important; 39 | font-size: small !important; 40 | } 41 | } 42 | 43 | @media screen and (max-width: 500px) { 44 | .banner { 45 | flex-direction: column-reverse; 46 | padding: 1rem; 47 | } 48 | 49 | .subject { 50 | justify-content: center; 51 | } 52 | 53 | .is-buy-now { 54 | width: 100%; 55 | display: flex; 56 | justify-content: center; 57 | align-items: center; 58 | } 59 | 60 | .buy-now{ 61 | width: 100% ; 62 | padding: 1rem 2rem !important; 63 | } 64 | 65 | .banner-text h1{ 66 | text-align: center; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Cart/OrderDetails/OrderDetails.css: -------------------------------------------------------------------------------- 1 | .order-details { 2 | display: grid; 3 | grid-template-columns: 2fr 1.5fr 1.5fr 0.5fr; 4 | gap: 2rem; 5 | background-color: #fff; 6 | box-shadow: 0px 0px 10px #e8e8e8; 7 | padding: 1rem; 8 | margin-bottom: 1rem; 9 | } 10 | 11 | .left-side img { 12 | width: 5rem; 13 | height: 5rem; 14 | object-fit: contain; 15 | } 16 | 17 | .order-detail { 18 | display: flex; 19 | align-items: center; 20 | gap: 1rem; 21 | } 22 | 23 | .order-price { 24 | display: flex; 25 | align-items: center; 26 | } 27 | 28 | .remove { 29 | display: flex; 30 | align-items: center; 31 | } 32 | 33 | .remove button { 34 | background-color: transparent; 35 | border: none; 36 | cursor: pointer; 37 | color: #e0373d; 38 | transition: color 0.5s ease; 39 | -webkit-transition: color 0.5s ease; 40 | -moz-transition: color 0.5s ease; 41 | -ms-transition: color 0.5s ease; 42 | -o-transition: color 0.5s ease; 43 | } 44 | 45 | .remove button:hover { 46 | color: #ff3d43; 47 | } 48 | 49 | .quantity { 50 | width: min(5rem, 100%); 51 | } 52 | 53 | button:disabled { 54 | background-color: #aaa; 55 | cursor: not-allowed; 56 | } 57 | 58 | button:disabled:hover { 59 | background-color: #aaa; 60 | cursor: not-allowed; 61 | } 62 | 63 | 64 | @media screen and (max-width:500px) { 65 | .order-details{ 66 | grid-template-columns: 1fr; 67 | } 68 | 69 | .quantity { 70 | width: 100%; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/components/Cart/OrderDetails/OrderDetails.jsx: -------------------------------------------------------------------------------- 1 | import "./OrderDetails.css"; 2 | 3 | import { useGlobalContext } from "@/components/GlobalContext/GlobalContext"; 4 | 5 | const OrderDetails = ({ product }) => { 6 | const {store} = useGlobalContext(); 7 | return ( 8 |
9 |
10 |
11 | 12 |
13 |
14 |

{product.name}

15 |

{product.description}

16 |
17 |
18 |
19 |

${product.price}

20 |
21 |
22 |

Quantity

23 |
24 | 31 |

{product.quantity}

32 | 39 |
40 |
41 |
42 | 49 |
50 |
51 | ); 52 | }; 53 | export default OrderDetails; 54 | -------------------------------------------------------------------------------- /src/store/modal.js: -------------------------------------------------------------------------------- 1 | import { useReducer } from "react"; 2 | 3 | const initialState = { 4 | opened: false, 5 | isRegister: false, 6 | isCancelModal: false, 7 | }; 8 | 9 | const actions = Object.freeze({ 10 | OPEN_MODAL: "OPEN_MODAL", 11 | CLOSE_MODAL: "CLOSE_MODAL", 12 | OPEN_CANCEL_MODAL: "OPEN_CANCEL_MODAL", 13 | CLOSE_CANCEL_MODAL: "CLOSE_CANCEL_MODAL", 14 | }); 15 | 16 | const reducer = (state, action) => { 17 | if (action.type == actions.OPEN_MODAL) { 18 | return { ...state, opened: true }; 19 | } 20 | 21 | if (action.type == actions.CLOSE_MODAL) { 22 | return { ...state, opened: false }; 23 | } 24 | 25 | if (action.type == actions.OPEN_CANCEL_MODAL) { 26 | return { ...state, isCancelModal: true }; 27 | } 28 | 29 | if (action.type == actions.CLOSE_CANCEL_MODAL) { 30 | return { ...state, isCancelModal: false }; 31 | } 32 | 33 | return state; 34 | }; 35 | 36 | const useModal = () => { 37 | const [state, dispatch] = useReducer(reducer, initialState); 38 | 39 | const openModal = (isRegister = true) => { 40 | state.isRegister = isRegister; 41 | dispatch({ type: actions.OPEN_MODAL }); 42 | }; 43 | 44 | const closeModal = () => { 45 | dispatch({ type: actions.CLOSE_MODAL }); 46 | }; 47 | 48 | const openCancelModal = () => { 49 | dispatch({ type: actions.OPEN_CANCEL_MODAL }); 50 | }; 51 | 52 | const closeCancelModal = () => { 53 | dispatch({ type: actions.CLOSE_CANCEL_MODAL }); 54 | }; 55 | 56 | return { ...state, openModal, closeModal, openCancelModal, closeCancelModal }; 57 | }; 58 | 59 | export default useModal; 60 | -------------------------------------------------------------------------------- /src/components/NavBar/NavBar.jsx: -------------------------------------------------------------------------------- 1 | import Account from "./Account/Account"; 2 | import Links from "./Links/Links"; 3 | import Logo from "./Logo/Logo"; 4 | import Search from "./Search/Search"; 5 | import Hamburger from "hamburger-react"; 6 | import "./NavBar.css"; 7 | 8 | const NavBar = () => { 9 | const handleHamClick = () => { 10 | let mobileExpandedMenu = document.querySelector(".mobile-expanded-menu"); 11 | mobileExpandedMenu.classList.toggle("mobile-expanded"); 12 | }; 13 | const removeExpandedClass = () => { 14 | let mobileExpandedMenu = document.querySelector(".mobile-expanded-menu"); 15 | if (mobileExpandedMenu.classList.contains("mobile-expanded")) { 16 | mobileExpandedMenu.classList.remove("mobile-expanded"); 17 | return true; 18 | } 19 | 20 | return false; 21 | }; 22 | return ( 23 |
24 |
25 | 26 | 34 | 35 | 36 | 37 | {/* */} 38 | 39 |
40 |
41 |
42 | 43 | 44 |
45 |
46 |
47 | ); 48 | }; 49 | export default NavBar; 50 | -------------------------------------------------------------------------------- /src/components/NavBar/Account/Account.jsx: -------------------------------------------------------------------------------- 1 | import { FaShoppingCart } from "react-icons/fa"; 2 | 3 | import { useGlobalContext } from "@/components/GlobalContext/GlobalContext"; 4 | import { Link } from "react-router-dom"; 5 | import "./Account.css"; 6 | 7 | const Account = () => { 8 | // let { store } = useGlobalContext(); 9 | let { auth, store, modal } = useGlobalContext(); 10 | const cartTotal = store.state.cartQuantity; 11 | 12 | const handleShowModal = () => { 13 | modal.openModal(false); 14 | }; 15 | 16 | const handleLogout = () => { 17 | auth.logout(); 18 | }; 19 | 20 | return ( 21 |
22 |
23 | 24 | {auth.state.user == null ? ( 25 | Guest 26 | ) : ( 27 | 28 | {auth.state.user.username} 29 | 30 | )} 31 | 32 | 33 | {cartTotal} 34 | 35 | 36 |
37 |
38 | {auth.state.user == null ? ( 39 | 45 | ) : ( 46 | 49 | )} 50 |
51 |
52 | ); 53 | }; 54 | export default Account; 55 | -------------------------------------------------------------------------------- /src/components/Home/Products/Product/Product.css: -------------------------------------------------------------------------------- 1 | .product-container { 2 | max-height: 30rem; 3 | padding: 1rem; 4 | background: #ffffff; 5 | box-shadow: 0px 0px 10px #e8e8e8; 6 | transition: all 0.3s ease-in-out; 7 | -webkit-transition: all 0.3s ease-in-out; 8 | -moz-transition: all 0.3s ease-in-out; 9 | -ms-transition: all 0.3s ease-in-out; 10 | -o-transition: all 0.3s ease-in-out; 11 | } 12 | 13 | .product-container:hover { 14 | box-shadow: 13px 13px 26px #e8e8e8, -13px -13px 26px #ffffff; 15 | } 16 | 17 | .image img { 18 | width: 50%; 19 | height: 100%; 20 | object-fit: contain; 21 | } 22 | 23 | .star-rating { 24 | display: flex; 25 | gap: 3rem; 26 | margin-bottom: 1rem; 27 | } 28 | 29 | .star-rating * { 30 | color: gold; 31 | } 32 | 33 | .add-to-cart { 34 | /*roundedbutton*/ 35 | border-radius: 5px; 36 | /*backgroundcolor*/ 37 | background-color: transparent; 38 | /*border*/ 39 | border: 1px solid #ccc; 40 | /*bordercolor*/ 41 | -webkit-border-radius: 0px; 42 | -moz-border-radius: 5px; 43 | -ms-border-radius: 5px; 44 | -o-border-radius: 5px; 45 | padding: 10px; 46 | -webkit-border-radius: 5px; 47 | cursor: pointer; 48 | } 49 | 50 | .add-to-cart:hover { 51 | /*roundedbutton*/ 52 | border-radius: 5px; 53 | /*backgroundcolor*/ 54 | background-color: #ccc; 55 | /*border*/ 56 | border: 1px solid #ccc; 57 | /*bordercolor*/ 58 | -webkit-border-radius: 0px; 59 | -moz-border-radius: 5px; 60 | -ms-border-radius: 5px; 61 | -o-border-radius: 5px; 62 | padding: 10px; 63 | -webkit-border-radius: 5px; 64 | } 65 | .name-price-product { 66 | display: flex; 67 | justify-content: space-between; 68 | align-items: center; 69 | margin-bottom: 0; 70 | } 71 | 72 | .name-price-product * { 73 | margin: 0; 74 | margin-top: 1rem; 75 | font-weight: 900; 76 | } 77 | 78 | .name-price-product h4 { 79 | font-size: 1.2rem; 80 | } 81 | 82 | .actual-product-price { 83 | font-size: 1rem; 84 | color: #ff8b15; 85 | } 86 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import HomeView from "./views/HomeView"; 2 | import { BrowserRouter, Routes, Route } from "react-router-dom"; 3 | import NavBar from "@/components/NavBar/NavBar"; 4 | import ShopFooter from "@/components/Footer/ShopFooter"; 5 | import ErrorView from "./views/ErrorView"; 6 | import CartView from "./views/CartView"; 7 | import DeliveryView from "./views/DeliveryView"; 8 | import "react-loading-skeleton/dist/skeleton.css"; 9 | import { useEffect } from "react"; 10 | import { useGlobalContext } from "@/components/GlobalContext/GlobalContext"; 11 | import { ToastContainer, toast } from "react-toastify"; 12 | import Modal from "./components/Modals/Modal"; 13 | import CancelOrder from "./components/Modals/CancelOrder"; 14 | import "react-toastify/dist/ReactToastify.css"; 15 | import RequestCookie from "./components/CookieBanner/CookieBanner"; 16 | 17 | function App() { 18 | let { store } = useGlobalContext(); 19 | let { modal } = useGlobalContext(); 20 | useEffect(() => { 21 | if (store.state.products.length > 0) return; 22 | store.getProducts(); 23 | }, []); 24 | return ( 25 |
26 | 27 |
28 | 29 |
30 | 31 | } /> 32 | } /> 33 | } /> 34 | } /> 35 | 36 |
37 | 38 |
39 |
40 | {modal.opened && ( 41 | 47 | )} 48 | {modal.isCancelModal && } 49 | 50 | {/* */} 51 |
52 | ); 53 | } 54 | 55 | export default App; 56 | -------------------------------------------------------------------------------- /src/views/DeliveryView.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useGlobalContext } from "../components/GlobalContext/GlobalContext"; 3 | import { useEffect, useState } from "react"; 4 | import DeliveryEmpty from "../components/Delivery/DeliveryEmpty/DeliveryEmpty"; 5 | import DeliveryItem from "../components/Delivery/DeliveryItem/DeliveryItem"; 6 | import Skeleton from "react-loading-skeleton"; 7 | import { toast } from "react-toastify"; 8 | 9 | const DeliveryView = () => { 10 | const { orders, auth, modal } = useGlobalContext(); 11 | const [loadingOrders, setLoadingOrders] = useState(false); 12 | const [disabled, setDisabled] = useState(false); 13 | useEffect(() => { 14 | if (auth.state.user) { 15 | setLoadingOrders(true); 16 | if (orders.state.orders.length <= 0) { 17 | orders.getOrders(auth.state.user.id); 18 | } 19 | if (orders.state.orders.length > 0) { 20 | setLoadingOrders(false); 21 | } 22 | } else { 23 | modal.openModal(false); 24 | } 25 | }, [auth.state.user]); 26 | 27 | const reloadOrders = async () => { 28 | setDisabled(true); 29 | toast.info("Reloading orders..."); 30 | await orders.getOrders(auth.state.user.id); 31 | setDisabled(false); 32 | toast.success("Orders reloaded!"); 33 | }; 34 | 35 | return ( 36 |
37 | {auth.state.user == null ? ( 38 | 39 | ) : ( 40 |
41 |
42 | 49 |
50 | {(orders.state.orders.length > 0 && 51 | orders.state.orders.map((order) => { 52 | return ( 53 | 54 | ); 55 | })) || } 56 |
57 | )} 58 |
59 | ); 60 | }; 61 | 62 | export default DeliveryView; 63 | -------------------------------------------------------------------------------- /src/components/Modals/Modal.css: -------------------------------------------------------------------------------- 1 | .modal-container { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | /* background-color: rgba(0,0,0,0.5); */ 8 | background-color: rgba(0, 0, 0, 0.5); 9 | z-index: 1000; 10 | display: flex; 11 | justify-content: center; 12 | align-items: center; 13 | padding: 0 1rem; 14 | } 15 | 16 | .modal { 17 | background-color: white; 18 | width: 100%; 19 | max-width: 500px; 20 | padding: 1rem; 21 | border-radius: 5px; 22 | position: relative; 23 | } 24 | 25 | .modal-body { 26 | padding: 1rem; 27 | } 28 | 29 | .modal-header { 30 | display: flex; 31 | justify-content: space-between; 32 | align-items: center; 33 | padding: 1rem; 34 | } 35 | 36 | .modal-body form { 37 | display: flex; 38 | flex-direction: column; 39 | justify-content: center; 40 | align-items: center; 41 | } 42 | 43 | .modal-body form .form-group { 44 | width: 100%; 45 | margin-bottom: 1rem; 46 | } 47 | 48 | .form-control { 49 | width: 100%; 50 | padding: 0.5rem; 51 | border: 1px solid #ccc; 52 | border-radius: 5px; 53 | outline: none; 54 | } 55 | 56 | .modal-cancel { 57 | border: none; 58 | width: 100%; 59 | display: flex; 60 | justify-content: flex-end; 61 | font-weight: 900; 62 | } 63 | 64 | .modal-cancel button { 65 | background-color: transparent; 66 | border: none; 67 | text-decoration: none; 68 | color: #e14949; 69 | padding: 0.5rem 1rem; 70 | cursor: pointer; 71 | } 72 | 73 | .login-or-register { 74 | display: flex; 75 | justify-content: flex-end; 76 | align-items: center; 77 | width: 100%; 78 | } 79 | 80 | .cancel-modal-group { 81 | display: flex; 82 | justify-content: flex-end; 83 | align-items: center; 84 | width: 100%; 85 | gap: 1rem; 86 | } 87 | 88 | .btn-submit-small { 89 | padding: 0.5rem 1rem !important; 90 | } 91 | 92 | .btn-cancel { 93 | background-color: #e14949 !important; 94 | } 95 | 96 | @media screen and (max-width:500px) { 97 | .modal-container{ 98 | padding: 0 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/store/orders.js: -------------------------------------------------------------------------------- 1 | import { useReducer } from "react"; 2 | 3 | const initialState = { 4 | orders: [], 5 | order_to_be_canceled: null, 6 | }; 7 | 8 | const actions = Object.freeze({ 9 | GET_ORDERS: "GET_ORDERS", 10 | GET_ORDER_TO_BE_CANCELED: "GET_ORDER_TO_BE_CANCELED", 11 | }); 12 | 13 | const reducer = (state, action) => { 14 | if (action.type == actions.GET_ORDERS) { 15 | return { ...state, orders: action.orders }; 16 | } 17 | 18 | if (action.type == actions.GET_ORDER_TO_BE_CANCELED) { 19 | return { ...state, order_to_be_canceled: action.order_id }; 20 | } 21 | return state; 22 | }; 23 | 24 | const useOrders = () => { 25 | const [state, dispatch] = useReducer(reducer, initialState); 26 | 27 | const getOrders = async (user_id) => { 28 | const response = await fetch( 29 | `${import.meta.env.VITE_API_URL}/get-orders/${user_id}`, 30 | { 31 | method: "GET", 32 | headers: { 33 | "Content-Type": "application/json", 34 | }, 35 | mode: "cors", 36 | credentials: "include", 37 | } 38 | ); 39 | 40 | const data = await response.json(); 41 | if (data.error) { 42 | return data.error; 43 | } 44 | dispatch({ type: actions.GET_ORDERS, orders: data.orders }); 45 | return data.orders; 46 | }; 47 | 48 | const setOrderToBeCanceled = (order_id) => { 49 | dispatch({ type: actions.GET_ORDER_TO_BE_CANCELED, order_id }); 50 | }; 51 | 52 | const cancelOrder = async (order_id) => { 53 | const response = await fetch( 54 | `${import.meta.env.VITE_API_URL}/cancel-order`, 55 | { 56 | method: "POST", 57 | headers: { 58 | "Content-Type": "application/json", 59 | }, 60 | mode: "cors", 61 | credentials: "include", 62 | body: JSON.stringify({ order_id }), 63 | } 64 | ); 65 | 66 | const data = await response.json(); 67 | 68 | if (data.error) { 69 | return data.error; 70 | } 71 | 72 | dispatch({ type: actions.GET_ORDER_TO_BE_CANCELED, order_id: null }); 73 | getOrders(data.user_id); 74 | 75 | return data; 76 | }; 77 | 78 | return { state, getOrders, setOrderToBeCanceled, cancelOrder }; 79 | }; 80 | 81 | export default useOrders; 82 | -------------------------------------------------------------------------------- /src/components/Home/Products/Product/Product.jsx: -------------------------------------------------------------------------------- 1 | import "./Product.css"; 2 | import headphones_pink from "@/assets/images/airpods_max_pink.jpg"; 3 | import { FaStar } from "react-icons/fa"; 4 | import { useGlobalContext } from "@/components/GlobalContext/GlobalContext"; 5 | import { toast } from "react-toastify"; 6 | 7 | const Product = ({ product }) => { 8 | let {store} = useGlobalContext(); 9 | let stars = []; 10 | for (let i = 0; i < product?.rating; i++) { 11 | stars.push(); 12 | } 13 | const isInCart = product?.addedToCart; 14 | return ( 15 |
16 |
17 | Product Image 22 |
23 |
24 |
25 |
26 |
27 |

{product?.name}

28 |
29 | ${product?.price}.00 30 |
31 |
32 |
{product?.description}
33 |
34 |
{stars}
35 | ({parseInt(Math.random() * 100)} Reviews) 36 |
37 |
38 |
39 | {isInCart == false ? ( 40 | 52 | ) : ( 53 | 61 | )} 62 |
63 |
64 |
65 |
66 | ); 67 | }; 68 | export default Product; 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React-e-commerce 2 | This is a customizable e-commerce frontend designed to make it easy for ***student developers*** to build on top of.
Built using Vite, The template features a clean and modern design, with a clear and consistent folder structure that makes it easy to navigate the project. 3 |
4 | In addition to the frontend there's an even simpler ***backend-template*** to handle all your requests ([READ MORE](https://github.com/Mini-Sylar/react-e-commerce-backend)) 5 | 6 | # Getting Started 7 | - Clone this repo 8 | - run ```npm install``` 9 | - run ```npm run dev``` 10 | 11 | # Example Deployment 12 | Simple React-E-Commerce Frontend 13 | 14 | ## Advantages 15 | - **Responsive design** : Designed to be responsive on any device 16 | - **Customizable :** Easy to expand on and add your own features, no fancy libraries are used, everything is purely react and es6 functions 17 | - **Clean project structure** : Components are all seperated into their respective folders with their individual CSS files for easy styling 18 | - **Vite**: Get your server started in a matter of seconds using vite 19 | 20 | ## What you get 21 | - A cart system: A store built using only ***useReducer***, no redux or any other fancy state management system 22 | - Cached cart: Cart data is cached using localforage 23 | - Checkout: Adjust cart details and place orders here 24 | - Routing: Page routing has been set up to easily navigate between pages 25 | - Auth🔥: JWT authentication allowing the user to login and logout 26 | - Modals🔥: Easy and customzizable modals 27 | - Orders🔥: View orders, get details on when your order will be processed, cancel or report order 28 | 29 | ## What it doesn't have 30 | - SEO: This is primarily for educational purposes, if you need SEO, you would have to commit to that. 31 | 32 |
33 | 34 | **To see a stripped down version with no JWT see [NO JWT](https://github.com/Mini-Sylar/react-e-commerce/tree/main)** 35 | 36 | ## Things to note 37 | - When running on localhost vite uses ```.env.development``` to connect to your backend (which must be running, more info here 👉 [React E-Commerce-Backend With JWT](https://github.com/Mini-Sylar/react-e-commerce/tree/main-jwt)) . Update it when needed 38 | - In production mode, ```.env.production``` will be used instead. 39 | 40 | # Contributions 41 | Contributions are always welcome, 42 | 43 | # Licence 44 | This template is released under the MIT License. Feel free to use it for personal or commercial projects. 45 |
You may credit me as well 😊 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/components/Modals/CancelOrder.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { toast } from "react-toastify"; 3 | import "./Modal.css"; 4 | import { useGlobalContext } from "@/components/GlobalContext/GlobalContext"; 5 | import ClipLoader from "react-spinners/ClipLoader"; 6 | import { useState } from "react"; 7 | 8 | const CancelOrder = () => { 9 | const { modal, orders, auth } = useGlobalContext(); 10 | let [loading, setLoading] = useState(false); 11 | const handleClose = () => { 12 | modal.closeCancelModal(); 13 | }; 14 | const submitForm = (e) => { 15 | console.log("attempting to cancel order"); 16 | e.preventDefault(); 17 | setLoading(true); 18 | const order_to_be_cancelled = orders.state.order_to_be_canceled; 19 | orders 20 | .cancelOrder(order_to_be_cancelled) 21 | .then(() => { 22 | toast.success( 23 | `Order #${order_to_be_cancelled.slice(0, 6)} has been canceled` 24 | ); 25 | // get new orders 26 | orders.getOrders(auth.state.user.id); 27 | handleClose(); 28 | }) 29 | .catch(() => { 30 | toast.error("There was an issue cancelling your order"); 31 | }) 32 | .finally(() => { 33 | setLoading(false); 34 | }); 35 | // submit cancel order here 36 | // close modal here 37 | }; 38 | return ( 39 |
40 |
41 |
42 | 49 |
50 |
51 |

Are you sure you want to cancel your order?

52 |
53 |
54 |
55 |
56 | 70 | 79 |
80 |
81 |
82 |
83 |
84 | ); 85 | }; 86 | 87 | export default CancelOrder; 88 | -------------------------------------------------------------------------------- /src/components/Cart/OrderSummary/OrderSummary.css: -------------------------------------------------------------------------------- 1 | .is-order-summary { 2 | /* background-color: #f5f5f5; */ 3 | background: #ffffff; 4 | box-shadow: 0px 0px 10px #e8e8e8; 5 | padding: 20px; 6 | border-radius: 2px; 7 | -webkit-border-radius: 2px; 8 | -moz-border-radius: 2px; 9 | -ms-border-radius: 2px; 10 | -o-border-radius: 2px; 11 | } 12 | 13 | .select-dropdown, 14 | .select-dropdown * { 15 | margin: 0; 16 | padding: 0.5rem; 17 | position: relative; 18 | box-sizing: border-box; 19 | } 20 | .select-dropdown { 21 | width: 100%; 22 | position: relative; 23 | /* background-color: #e6e6e6; */ 24 | border-radius: 5px; 25 | -webkit-border-radius: 5px; 26 | -moz-border-radius: 5px; 27 | -ms-border-radius: 5px; 28 | -o-border-radius: 5px; 29 | } 30 | .select-dropdown select { 31 | font-size: 1rem; 32 | font-weight: normal; 33 | max-width: 100%; 34 | padding: 8px 24px 8px 10px; 35 | border: none; 36 | background-color: transparent; 37 | -webkit-appearance: none; 38 | -moz-appearance: none; 39 | appearance: none; 40 | text-align: center; 41 | } 42 | .select-dropdown select:active, 43 | .select-dropdown select:focus { 44 | outline: none; 45 | box-shadow: none; 46 | } 47 | .select-dropdown:after { 48 | content: ""; 49 | position: absolute; 50 | top: 50%; 51 | right: 8px; 52 | width: 0; 53 | height: 0; 54 | margin-top: -2px; 55 | border-top: 5px solid #aaa; 56 | border-right: 5px solid transparent; 57 | border-left: 5px solid transparent; 58 | } 59 | 60 | .total-cost, 61 | .final-cost { 62 | display: flex; 63 | justify-content: space-between; 64 | } 65 | 66 | .enter-promo * { 67 | display: block; 68 | margin: 1rem 0; 69 | } 70 | 71 | .flat-button { 72 | border: none; 73 | color: #fff; 74 | background-color: #ff8b15; 75 | padding: 0.5rem 1.5em; 76 | border-radius: 5px; 77 | -webkit-border-radius: 5px; 78 | -moz-border-radius: 5px; 79 | -ms-border-radius: 5px; 80 | -o-border-radius: 5px; 81 | cursor: pointer; 82 | transition: all 0.3s ease; 83 | } 84 | 85 | .flat-button:hover { 86 | background-color: #fc8005; 87 | } 88 | 89 | .apply-promo { 90 | background-color: #fa7474; 91 | } 92 | 93 | .apply-promo:hover { 94 | background-color: #ff3e3e; 95 | } 96 | 97 | .checkout { 98 | padding: 0.8rem 1.5rem; 99 | width: 100%; 100 | } 101 | 102 | .increase-quantity { 103 | display: flex; 104 | justify-content: space-evenly; 105 | gap: 3rem; 106 | } 107 | 108 | .increase-quantity button { 109 | border: none; 110 | background-color: #fff; 111 | padding: 0.5rem 1rem; 112 | border-radius: 5px; 113 | -webkit-border-radius: 5px; 114 | -moz-border-radius: 5px; 115 | -ms-border-radius: 5px; 116 | -o-border-radius: 5px; 117 | cursor: pointer; 118 | transition: all 0.3s ease; 119 | font-size: larger; 120 | } 121 | 122 | .increase-quantity button:nth-of-type(1):hover { 123 | color: white; 124 | background-color: #f75e5e; 125 | } 126 | 127 | .increase-quantity button:nth-of-type(2):hover { 128 | color: white; 129 | background-color: #5ef75e; 130 | } 131 | -------------------------------------------------------------------------------- /src/store/auth.js: -------------------------------------------------------------------------------- 1 | import { useReducer } from "react"; 2 | import { toast } from "react-toastify"; 3 | import { 4 | setExpirationDate, 5 | getUserFromLocalStorage, 6 | } from "../helpers/checkExpiration"; 7 | 8 | const initialState = { 9 | user: getUserFromLocalStorage() || null, 10 | }; 11 | const actions = Object.freeze({ 12 | SET_USER: "SET_USER", 13 | LOGOUT: "LOGOUT", 14 | }); 15 | 16 | const reducer = (state, action) => { 17 | if (action.type == actions.SET_USER) { 18 | return { ...state, user: action.user }; 19 | } 20 | if (action.type == actions.LOGOUT) { 21 | return { ...state, user: null }; 22 | } 23 | return state; 24 | }; 25 | 26 | const useAuth = () => { 27 | const [state, dispatch] = useReducer(reducer, initialState); 28 | 29 | const register = async (userInfo) => { 30 | try { 31 | const response = await fetch(`${import.meta.env.VITE_API_URL}/register`, { 32 | method: "POST", 33 | headers: { 34 | "Content-Type": "application/json", 35 | }, 36 | mode: "cors", 37 | credentials: "include", 38 | body: JSON.stringify(userInfo), 39 | }); 40 | 41 | const user = await response.json(); 42 | if (user.error) { 43 | toast.error(user.error); 44 | } 45 | if (user.user) { 46 | dispatch({ type: actions.SET_USER, user: user.user }); 47 | user.user.expirationDate = setExpirationDate(7); 48 | localStorage.setItem("user", JSON.stringify(user.user)); 49 | toast.success("Registration successful"); 50 | // login user 51 | } 52 | } catch (error) { 53 | toast.error("There was a problem registering, try again"); 54 | } 55 | }; 56 | 57 | const login = async (userInfo) => { 58 | try { 59 | const response = await fetch(`${import.meta.env.VITE_API_URL}/login`, { 60 | method: "POST", 61 | headers: { 62 | "Content-Type": "application/json", 63 | }, 64 | mode: "cors", 65 | credentials: "include", 66 | body: JSON.stringify(userInfo), 67 | }); 68 | const user = await response.json(); 69 | if (user.error) { 70 | toast.error(user.error); 71 | } 72 | if (user.user) { 73 | dispatch({ type: actions.SET_USER, user: user.user }); 74 | user.user.expirationDate = setExpirationDate(7); 75 | localStorage.setItem("user", JSON.stringify(user.user)); 76 | toast.success("Login successful"); 77 | } 78 | } catch (error) { 79 | toast.error("There was a problem logging in, try again"); 80 | } 81 | }; 82 | 83 | const logout = async () => { 84 | await fetch(`${import.meta.env.VITE_API_URL}/logout`, { 85 | method: "GET", 86 | headers: { 87 | "Content-Type": "application/json", 88 | }, 89 | mode: "cors", 90 | credentials: "include", 91 | }); 92 | localStorage.removeItem("user"); 93 | dispatch({ type: actions.LOGOUT }); 94 | }; 95 | 96 | return { state, register, login, logout }; 97 | }; 98 | 99 | export default useAuth; 100 | -------------------------------------------------------------------------------- /src/components/NavBar/NavBar.css: -------------------------------------------------------------------------------- 1 | .nav-main { 2 | position: relative; 3 | } 4 | 5 | .nav-container { 6 | display: flex; 7 | gap: 1rem; 8 | justify-content: space-evenly; 9 | width: 100%; 10 | align-items: center; 11 | min-height: 5rem; 12 | color: white; 13 | } 14 | 15 | .links, 16 | .account, 17 | .logo { 18 | width: 100%; 19 | } 20 | 21 | .links { 22 | display: flex; 23 | justify-content: space-around; 24 | text-align: center; 25 | } 26 | 27 | .desktop-links { 28 | display: flex; 29 | gap: 1rem; 30 | justify-content: space-around; 31 | width: 100%; 32 | } 33 | .links a { 34 | width: 100%; 35 | text-decoration: none; 36 | color: white; 37 | } 38 | 39 | .account { 40 | display: flex; 41 | gap: 1.5rem; 42 | justify-content: flex-end; 43 | align-items: center; 44 | } 45 | 46 | .account-details { 47 | position: relative; 48 | font-size: 1.4rem; 49 | } 50 | 51 | .contains-link-to-accounts { 52 | display: flex; 53 | align-items: center; 54 | gap: 10px; 55 | } 56 | .items-in-cart { 57 | position: absolute; 58 | content: ""; 59 | top: -10px; 60 | right: 0; 61 | left: 15px; 62 | background-color: #ff0000; 63 | color: white; 64 | border-radius: 50%; 65 | width: 1rem; 66 | height: 1rem; 67 | display: flex; 68 | justify-content: center; 69 | align-items: center; 70 | font-size: 12px !important; 71 | } 72 | 73 | .nav-mobile { 74 | display: none; 75 | } 76 | 77 | .hamburger { 78 | display: none; 79 | } 80 | 81 | @media screen and (max-width: 768px) { 82 | .delivery-link { 83 | display: none; 84 | } 85 | .links * { 86 | font-size: 0.8rem !important; 87 | } 88 | 89 | .account-user { 90 | font-size: 0.8rem; 91 | } 92 | 93 | .small-rounded { 94 | padding: 0.5rem 1rem !important; 95 | font-size: 0.8rem; 96 | } 97 | 98 | .account-details { 99 | font-size: 1rem; 100 | } 101 | 102 | .items-in-cart { 103 | width: 0.8rem !important; 104 | height: 0.8rem !important; 105 | font-size: 0.8rem !important; 106 | left: 10px; 107 | } 108 | } 109 | 110 | @media screen and (max-width: 500px) { 111 | .nav-mobile { 112 | display: block; 113 | } 114 | 115 | .desktop-links { 116 | display: none; 117 | } 118 | 119 | .login { 120 | display: none; 121 | } 122 | 123 | .mobile-expanded-menu .links { 124 | flex-direction: column; 125 | gap: 3rem; 126 | } 127 | 128 | .mobile-expanded-menu .account .login { 129 | margin: 3rem auto; 130 | display: block; 131 | } 132 | 133 | .mobile-expanded-menu .account { 134 | gap: 0; 135 | } 136 | 137 | .mobile-expanded-menu .account span { 138 | display: none; 139 | } 140 | 141 | .mobile-expanded-menu { 142 | position: absolute; 143 | width: 100%; 144 | background-color: #ff8a15; 145 | z-index: 3; 146 | max-height: 0; 147 | overflow: hidden; 148 | transition: all 0.3s ease-in-out; 149 | -webkit-transition: all 0.3s ease-in-out; 150 | -moz-transition: all 0.3s ease-in-out; 151 | -ms-transition: all 0.3s ease-in-out; 152 | -o-transition: all 0.3s ease-in-out; 153 | } 154 | 155 | .mobile-expanded { 156 | max-height: min(70rem, 100vh); 157 | overflow: hidden; 158 | transition: all 0.3s ease-in-out; 159 | -webkit-transition: all 0.3s ease-in-out; 160 | -moz-transition: all 0.3s ease-in-out; 161 | -ms-transition: all 0.3s ease-in-out; 162 | -o-transition: all 0.3s ease-in-out; 163 | } 164 | 165 | .logo { 166 | display: none; 167 | } 168 | 169 | .hamburger { 170 | display: block; 171 | background-color: transparent; 172 | border: none; 173 | } 174 | 175 | } 176 | -------------------------------------------------------------------------------- /src/components/Cart/OrderSummary/OrderSummary.jsx: -------------------------------------------------------------------------------- 1 | import "./OrderSummary.css"; 2 | import { useGlobalContext } from "@/components/GlobalContext/GlobalContext"; 3 | import { useState } from "react"; 4 | import { toast } from "react-toastify"; 5 | 6 | const OrderSummary = () => { 7 | const { store, modal, auth } = useGlobalContext(); 8 | const [deliveryType, setDeliveryType] = useState("Standard"); 9 | const [phone, setPhone] = useState(""); 10 | const setDelivery = (type) => { 11 | setDeliveryType(type); 12 | }; 13 | const checkOut = async () => { 14 | let payload = { 15 | DeliveryType: deliveryType, 16 | DeliveryTypeCost: deliveryType == "Standard" ? 5 : 10, 17 | costAfterDelieveryRate: 18 | store.state.cartTotal + (deliveryType == "Standard" ? 5 : 10), 19 | promoCode: "", 20 | phoneNumber: phone, 21 | user_id: auth.state.user?.id, 22 | }; 23 | 24 | const response = await store.confirmOrder(payload); 25 | if (response.showRegisterLogin) { 26 | modal.openModal(); 27 | } 28 | }; 29 | return ( 30 |
31 |
32 |
33 |
34 |

Total Items ({store.state.cartQuantity})

35 |

${store.state.cartTotal}

36 |
37 |
38 |

Shipping

39 | 52 |
53 |
54 |

Promo Code

55 |
56 | 57 | 63 |
64 |
65 |
66 |

Phone Number

67 | { 71 | setPhone(item.target.value); 72 | }} 73 | /> 74 | 75 | 76 | Your number would be called to verify the order placement 77 | 78 | 79 |
80 |
81 |

Total Cost

82 |

83 | ${" "} 84 | {store.state.cart.length > 0 85 | ? store.state.cartTotal + (deliveryType == "Standard" ? 5 : 10) 86 | : 0} 87 |

88 |
89 |
90 | 104 |
105 |
106 |
107 |
108 | ); 109 | }; 110 | export default OrderSummary; 111 | -------------------------------------------------------------------------------- /src/components/Modals/Modal.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { toast } from "react-toastify"; 3 | import "./Modal.css"; 4 | import { useGlobalContext } from "@/components/GlobalContext/GlobalContext"; 5 | import ClipLoader from "react-spinners/ClipLoader"; 6 | import { useState } from "react"; 7 | 8 | const Modal = ({ header, submitAction, buttonText, isRegister }) => { 9 | const { auth } = useGlobalContext(); 10 | const { modal } = useGlobalContext(); 11 | let [loading, setLoading] = useState(false); 12 | const handleClose = () => { 13 | modal.closeModal(); 14 | }; 15 | const handleSwitch = () => { 16 | modal.openModal(!isRegister); 17 | }; 18 | const submitForm = (e) => { 19 | e.preventDefault(); 20 | setLoading(true); 21 | const formData = new FormData(e.target); 22 | const data = Object.fromEntries(formData.entries()); 23 | // if empty fields 24 | if (Object.values(data).some((value) => value === "")) { 25 | toast.error("Please fill in all fields"); 26 | setLoading(false); 27 | return; 28 | } 29 | if (isRegister && data.password !== data.confirmPassword) { 30 | toast.error("Passwords do not match"); 31 | setLoading(false); 32 | return; 33 | } 34 | // register or login 35 | if (isRegister) { 36 | auth 37 | .register(data) 38 | .then(() => { 39 | // close modal 40 | modal.closeModal(); 41 | }) 42 | .finally(() => { 43 | setLoading(false); 44 | }); 45 | } else { 46 | auth 47 | .login(data) 48 | .then(() => { 49 | modal.closeModal(); 50 | }) 51 | .finally(() => { 52 | setLoading(false); 53 | }); 54 | } 55 | }; 56 | return ( 57 |
58 |
59 |
60 | 67 |
68 |
69 |

{header}

70 |
71 |
72 |
73 | {isRegister && ( 74 |
75 | 76 | 77 |
78 | )} 79 |
80 | 81 | 82 |
83 |
84 | 85 | 86 |
87 | {isRegister && ( 88 |
89 | 90 | 95 |
96 | )} 97 |
98 | {isRegister ? ( 99 | 100 | Already have an account? 101 | 110 | 111 | ) : ( 112 | 113 | Don't have an account? 114 | 123 | 124 | )} 125 |
126 |
127 | 138 |
139 |
140 |
141 |
142 |
143 | ); 144 | }; 145 | 146 | export default Modal; 147 | -------------------------------------------------------------------------------- /src/components/Delivery/DeliveryItem/DeliveryItem.jsx: -------------------------------------------------------------------------------- 1 | import "./DeliveryItem.css"; 2 | import { useState } from "react"; 3 | import { FaCaretUp } from "react-icons/fa"; 4 | import { useGlobalContext } from "../../GlobalContext/GlobalContext"; 5 | 6 | const DeliveryItem = ({ order }) => { 7 | const [expanded, setExpanded] = useState(false); 8 | const date = new Date(order.expected_delivery_date); 9 | const currentDate = new Date(); 10 | const formattedDate = date.toLocaleDateString("en-US", { 11 | month: "long", 12 | day: "numeric", 13 | year: "numeric", 14 | }); 15 | const numberOfDays = () => { 16 | if (currentDate.getTime() > date.getTime()) { 17 | return "0"; 18 | } 19 | return Math.ceil((date.getTime() - currentDate.getTime()) / (1000 * 3600 * 24)); 20 | 21 | } 22 | const toggleExpanded = () => { 23 | setExpanded(!expanded); 24 | }; 25 | 26 | const checkFlair = (percentage) => { 27 | if (percentage < 50) { 28 | return "flair danger-flair"; 29 | } else if (percentage < 90) { 30 | return "flair warning-flair"; 31 | } else { 32 | return "flair success-flair"; 33 | } 34 | }; 35 | 36 | const checkFlairText = (percentage) => { 37 | if (order.order_cancelled) { 38 | return "Order Cancelled"; 39 | } else if (percentage < 50) { 40 | return "Verification Pending"; 41 | } else if (percentage < 90) { 42 | return "Verified & In Delivery"; 43 | } else { 44 | return "Delivered"; 45 | } 46 | }; 47 | 48 | const { modal, orders } = useGlobalContext(); 49 | 50 | const handleOpenCancelModal = (order_id) => { 51 | modal.openCancelModal(); 52 | orders.setOrderToBeCanceled(order_id); 53 | }; 54 | 55 | return ( 56 |
57 |
58 |
59 |
60 |

61 | Order: # 62 | {order._id.slice(0, 6)} 63 |

64 |
65 |
Item Count: {order.totalItemCount}
66 |
Total Cost: ${order.cost_after_delivery_rate}
67 |
Delivery Type: {order.delivery_type}
68 |
Total cost includes delivery fee
69 |
70 |
71 |
72 |

Complete

73 |

74 | {order.percentage_complete}%{" "} 75 | 76 | {checkFlairText(order.percentage_complete)} 77 | 78 |

79 | 84 |
85 |
86 |

Expected Completion

87 | {(order.order_processed != true && 88 | order.order_cancelled != true &&

{formattedDate}

) || 89 | (order.order_processed == true && ( 90 |

Delivered

91 | )) || 92 | (order.order_cancelled == true && ( 93 |

Cancelled

94 | ))} 95 | 96 | {(order.order_processed != true && 97 | order.order_cancelled != true && ( 98 |

{numberOfDays()} day(s)

99 | )) || 100 | ""} 101 |
102 |
103 |
106 |
107 |

Products in Delivery

108 |
109 | {order.items.map((item) => { 110 | return ( 111 |
112 | 113 |
Product Name: {item.name}
114 |
Description: {item.description}
115 |
Price: ${item.price}
116 |
Quantity: {item.quantity}
117 |
118 | ); 119 | })} 120 |
121 |
122 | {order.order_processed != true && order.order_cancelled != true && ( 123 |
124 |

Danger Zone

125 |
126 | 134 | 146 |
147 |
148 | )} 149 |
150 |
151 | 152 |
153 | 163 |
164 |
165 | ); 166 | }; 167 | export default DeliveryItem; 168 | -------------------------------------------------------------------------------- /src/components/Delivery/DeliveryItem/DeliveryItem.css: -------------------------------------------------------------------------------- 1 | .delivery-summary { 2 | display: grid; 3 | grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 4 | } 5 | 6 | .expand-collapse-delivery { 7 | margin-block: 2rem; 8 | width: 100%; 9 | display: flex; 10 | justify-content: center; 11 | } 12 | 13 | .expand-collapse-delivery > button { 14 | background-color: transparent; 15 | border: none; 16 | display: flex; 17 | align-items: center; 18 | gap: 1rem; 19 | padding: 0.5rem; 20 | box-shadow: 0px 0px 10px #e8e8e8; 21 | } 22 | 23 | .expand-collapse-delivery > button > span { 24 | color: #3a3939; 25 | display: flex; 26 | align-items: center; 27 | } 28 | 29 | .caret-delivery { 30 | transition: all 0.5s ease-in-out; 31 | -webkit-transition: all 0.5s ease-in-out; 32 | -moz-transition: all 0.5s ease-in-out; 33 | -ms-transition: all 0.5s ease-in-out; 34 | -o-transition: all 0.5s ease-in-out; 35 | } 36 | 37 | .caret-expanded { 38 | transform: rotate(180deg); 39 | transition: all 0.5s ease-in-out; 40 | -webkit-transition: all 0.5s ease-in-out; 41 | -moz-transition: all 0.5s ease-in-out; 42 | -ms-transition: all 0.5s ease-in-out; 43 | -o-transition: all 0.5s ease-in-out; 44 | } 45 | 46 | .delivery-item-container, 47 | .delivery-products-item { 48 | margin-block: 3rem !important; 49 | padding: 1rem; 50 | background: #ffffff; 51 | box-shadow: 0px 0px 10px #e8e8e8; 52 | transition: all 0.3s ease-in-out; 53 | -webkit-transition: all 0.3s ease-in-out; 54 | -moz-transition: all 0.3s ease-in-out; 55 | -ms-transition: all 0.3s ease-in-out; 56 | -o-transition: all 0.3s ease-in-out; 57 | } 58 | 59 | .delivery-item-container:hover, 60 | .delivery-products-item:hover { 61 | box-shadow: 13px 13px 26px #e8e8e8, -13px -13px 26px #ffffff; 62 | } 63 | 64 | .fully-expanded { 65 | max-height: 0; 66 | margin-block: 2rem; 67 | overflow: hidden; 68 | transition: all 0.3s ease-in-out; 69 | -webkit-transition: all 0.3s ease-in-out; 70 | -moz-transition: all 0.3s ease-in-out; 71 | -ms-transition: all 0.3s ease-in-out; 72 | -o-transition: all 0.3s ease-in-out; 73 | } 74 | 75 | .isExpanded { 76 | max-height: min(85vh, 800px); 77 | } 78 | 79 | .delivery-date { 80 | text-align: right; 81 | } 82 | 83 | .delivery-progress { 84 | display: flex; 85 | flex-direction: column; 86 | align-items: flex-start; 87 | margin: 0 auto; 88 | } 89 | 90 | .delivery-progress > *:not(h3), 91 | .delivery-date > *:not(h3) { 92 | margin-block: 0.5rem; 93 | color: #3a3939; 94 | } 95 | 96 | progress[value] { 97 | /* Reset the default appearance */ 98 | -webkit-appearance: none; 99 | appearance: none; 100 | border: none; 101 | width: 250px; 102 | height: 10px; 103 | } 104 | 105 | progress[value]::-webkit-progress-bar { 106 | background-color: #eee; 107 | border-radius: 2px; 108 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.103) inset; 109 | } 110 | 111 | progress[value]::-webkit-progress-value { 112 | background-image: -webkit-linear-gradient( 113 | -45deg, 114 | transparent 33%, 115 | rgba(0, 0, 0, 0.1) 33%, 116 | rgba(0, 0, 0, 0.1) 66%, 117 | transparent 66% 118 | ), 119 | -webkit-linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.278)), 120 | -webkit-linear-gradient(right, #22cc00, #f44); 121 | border-radius: 2px; 122 | background-size: 35px 20px, 100% 100%, 100% 100%; 123 | -webkit-animation: animate-stripes 5s linear infinite; 124 | animation: animate-stripes 5s linear infinite; 125 | } 126 | 127 | progress[value]::-moz-progress-bar { 128 | background-image: -moz-linear-gradient( 129 | 135deg, 130 | transparent 33%, 131 | rgba(0, 0, 0, 0.1) 33%, 132 | rgba(0, 0, 0, 0.1) 66%, 133 | transparent 66% 134 | ), 135 | -moz-linear-gradient(top, rgba(255, 255, 255, 0.062), rgba(0, 0, 0, 0.062)), 136 | -moz-linear-gradient(right, #22cc00, #f44); 137 | 138 | border-radius: 2px; 139 | background-size: 35px 20px, 100% 100%, 100% 100%; 140 | } 141 | 142 | .delivery-products { 143 | display: grid; 144 | gap: 10px; 145 | padding: 1rem; 146 | grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 147 | } 148 | 149 | .delivery-products-item { 150 | margin-block: 0 !important; 151 | } 152 | 153 | .delivery-item-title { 154 | color: #212121 !important; 155 | } 156 | 157 | .order-main { 158 | color: #141414 !important; 159 | } 160 | 161 | .delivery-items h5 { 162 | font-size: 1rem !important; 163 | font-weight: 600; 164 | margin-block: 0; 165 | color: #3a3939; 166 | } 167 | 168 | .delivery-items h6 { 169 | margin-block-start: 0.5rem; 170 | color: #f44; 171 | } 172 | 173 | .flair { 174 | border: 1px solid transparent; 175 | background-color: #3a3939; 176 | color: #ffffff; 177 | border-radius: 20px; 178 | padding: 0.3rem; 179 | font-size: x-small; 180 | font-weight: 900; 181 | margin-inline: 0.5rem; 182 | -webkit-border-radius: 20px; 183 | -moz-border-radius: 20px; 184 | -ms-border-radius: 20px; 185 | -o-border-radius: 20px; 186 | } 187 | 188 | .danger-flair { 189 | background-color: #f44; 190 | } 191 | 192 | .warning-flair { 193 | background-color: #f4a; 194 | } 195 | 196 | .success-flair { 197 | background-color: #22cc00; 198 | } 199 | 200 | .is-delivered { 201 | color: #22cc00 !important; 202 | } 203 | 204 | .is-cancelled { 205 | color: #f44 !important; 206 | } 207 | 208 | .danger-zone-text { 209 | color: #f44; 210 | } 211 | 212 | .danger-zone-buttons { 213 | display: flex; 214 | gap: 1rem; 215 | } 216 | 217 | .danger-zone-button { 218 | color: #f44 !important; 219 | padding: 0.4rem 1rem !important; 220 | border: 1px solid #f44 !important; 221 | } 222 | 223 | .danger-zone-button:hover { 224 | background-color: #f44 !important; 225 | color: #ffffff !important; 226 | } 227 | 228 | .report-issue { 229 | color: #ffb300 !important; 230 | border-color: #ffb300 !important; 231 | } 232 | 233 | .report-issue:hover { 234 | background-color: #ffb300 !important; 235 | color: #ffffff !important; 236 | } 237 | 238 | .reload-orders { 239 | width: 100%; 240 | padding-top: 1rem; 241 | display: flex; 242 | justify-content: flex-end; 243 | } 244 | 245 | @media screen and (max-width: 500px) { 246 | .delivery-progress { 247 | width: 100%; 248 | } 249 | 250 | .delivery-date { 251 | text-align: left; 252 | } 253 | 254 | .delivery-products { 255 | max-height: 600px; 256 | overflow: scroll; 257 | } 258 | } 259 | 260 | @-webkit-keyframes animate-stripes { 261 | 100% { 262 | background-position: -100px 0px; 263 | } 264 | } 265 | 266 | @keyframes animate-stripes { 267 | 100% { 268 | background-position: -100px 0px; 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/store/products.js: -------------------------------------------------------------------------------- 1 | import { useReducer } from "react"; 2 | import localforage from "localforage"; 3 | import { toast } from "react-toastify"; 4 | 5 | const initialState = { 6 | products: [], 7 | cart: [], 8 | cartTotal: 0, 9 | cartQuantity: 0, 10 | order: [], 11 | }; 12 | 13 | const actions = Object.freeze({ 14 | ADD_TO_CART: "ADD_TO_CART", 15 | GET_PRODUCTS: "GET_PRODUCTS", 16 | REMOVE_FROM_CART: "REMOVE_FROM_CART", 17 | CLEAR_CART: "CLEAR_CART", 18 | ADD_QUANTITY: "ADD_QUANTITY", 19 | REDUCE_QUANTITY: "REDUCE_QUANTITY", 20 | PREFILL_CART: "PREFILL_CART", 21 | }); 22 | 23 | const reducer = (state, action) => { 24 | // GET PRODUCTS 25 | if (action.type == actions.GET_PRODUCTS) { 26 | if (action.backed_up_cart == []) { 27 | return { ...state, products: action.products }; 28 | } 29 | // prefil cart 30 | const cartTotal = action.backed_up_cart.reduce( 31 | (acc, item) => acc + item.price, 32 | 0 33 | ); 34 | const cartQuantity = action.backed_up_cart.reduce( 35 | (acc, item) => acc + item.quantity, 36 | 0 37 | ); 38 | 39 | const updatedProducts = action.products.map((product) => { 40 | const cartItem = action.backed_up_cart.find( 41 | (item) => item._id === product._id 42 | ); 43 | if (cartItem) { 44 | return { ...cartItem, addedToCart: true }; 45 | } else { 46 | return product; 47 | } 48 | }); 49 | return { 50 | ...state, 51 | products: updatedProducts, 52 | cart: action.backed_up_cart, 53 | cartQuantity, 54 | cartTotal, 55 | }; 56 | } 57 | // ADD TO CART 58 | if (action.type == actions.ADD_TO_CART) { 59 | const product = state.products.find( 60 | (product) => product._id == action.product 61 | ); 62 | product.addedToCart = true; 63 | product.quantity = 1; 64 | 65 | // backup with local Forage here 66 | localforage.setItem("cartItems", [...state.cart, product]); 67 | 68 | return { 69 | ...state, 70 | cart: [...state.cart, product], 71 | cartQuantity: state.cartQuantity + 1, 72 | cartTotal: state.cartTotal + product.price, 73 | }; 74 | } 75 | // Remove from cart 76 | if (action.type == actions.REMOVE_FROM_CART) { 77 | const product = state.products.find( 78 | (product) => product._id == action.product 79 | ); 80 | const newCart = state.cart.filter( 81 | (product) => product._id != action.product 82 | ); 83 | const updatedProduct = { ...product, addedToCart: false }; 84 | localforage.setItem("cartItems", newCart); 85 | 86 | // recalculate cart total 87 | let newCartTotal = 0; 88 | newCart.forEach((item) => { 89 | newCartTotal += item.price * item.quantity; 90 | }); 91 | return { 92 | ...state, 93 | products: state.products.map((p) => 94 | p._id === product._id ? updatedProduct : p 95 | ), 96 | cart: newCart, 97 | cartQuantity: state.cartQuantity - 1, 98 | cartTotal: newCartTotal, 99 | }; 100 | } 101 | 102 | // add quantity 103 | if (action.type == actions.ADD_QUANTITY) { 104 | const product = state.cart.find((product) => product._id == action.product); 105 | product.quantity = product.quantity + 1; 106 | 107 | return { 108 | ...state, 109 | cartTotal: state.cartTotal + product.price, 110 | }; 111 | } 112 | 113 | // reduce quantity 114 | if (action.type == actions.REDUCE_QUANTITY) { 115 | const product = state.cart.find((product) => product._id == action.product); 116 | if (product.quantity == 1) { 117 | return state; 118 | } 119 | product.quantity = product.quantity - 1; 120 | return { 121 | ...state, 122 | cartTotal: state.cartTotal - product.price, 123 | }; 124 | } 125 | 126 | // clear cart 127 | if (action.type == actions.CLEAR_CART) { 128 | localforage.setItem("cartItems", []); 129 | 130 | return { 131 | ...state, 132 | cart: [], 133 | order: [], 134 | cartTotal: 0, 135 | cartQuantity: 0, 136 | }; 137 | } 138 | 139 | return state; 140 | }; 141 | 142 | const useStore = () => { 143 | const [state, dispatch] = useReducer(reducer, initialState); 144 | 145 | const addToCart = (product) => { 146 | // TODO: Add logic here and remove modification from dispatch 147 | dispatch({ type: actions.ADD_TO_CART, product }); 148 | }; 149 | 150 | const removeFromCart = (product) => { 151 | // TODO: Add logic here and remove modification from dispatch 152 | dispatch({ type: actions.REMOVE_FROM_CART, product }); 153 | }; 154 | 155 | const clearCart = () => { 156 | dispatch({ type: actions.CLEAR_CART }); 157 | }; 158 | const getProducts = () => { 159 | fetch(`${import.meta.env.VITE_API_URL}/get-products`) 160 | .then(async (response) => { 161 | const data = await response.json(); 162 | let modifiedData = data.map((product) => { 163 | return { ...product, addedToCart: false }; 164 | }); 165 | let cart = (await localforage.getItem("cartItems")) || []; 166 | dispatch({ 167 | type: actions.GET_PRODUCTS, 168 | products: modifiedData, 169 | backed_up_cart: cart, 170 | }); 171 | }) 172 | .catch((err) => { 173 | toast.error( 174 | "There was a problem fetching products, check your internet connection and try again" 175 | ); 176 | return []; 177 | }); 178 | }; 179 | 180 | const addQuantity = (product) => { 181 | dispatch({ type: actions.ADD_QUANTITY, product }); 182 | }; 183 | 184 | const reduceQuantity = (product) => { 185 | dispatch({ type: actions.REDUCE_QUANTITY, product }); 186 | }; 187 | 188 | const confirmOrder = async (order) => { 189 | let payload = { 190 | items: state.cart, 191 | totalItemCount: state.cartQuantity, 192 | delivery_type: order.DeliveryType, 193 | delivery_type_cost: order.DeliveryTypeCost, 194 | cost_before_delivery_rate: state.cartTotal, 195 | cost_after_delivery_rate: order.costAfterDelieveryRate, 196 | promo_code: order.promo_code || "", 197 | contact_number: order.phoneNumber, 198 | user_id: order.user_id, 199 | }; 200 | const response = await fetch( 201 | `${import.meta.env.VITE_API_URL}/place-order`, 202 | { 203 | method: "POST", 204 | headers: { 205 | "Content-Type": "application/json", 206 | }, 207 | mode: "cors", 208 | credentials: "include", 209 | body: JSON.stringify(payload), 210 | } 211 | ); 212 | const data = await response.json(); 213 | if (data.error) { 214 | toast.error("You must be logged in to place an order"); 215 | return { showRegisterLogin: true }; 216 | } 217 | toast.success(data.message); 218 | clearCart(); 219 | return true; 220 | }; 221 | 222 | return { 223 | state, 224 | addToCart, 225 | removeFromCart, 226 | clearCart, 227 | getProducts, 228 | addQuantity, 229 | reduceQuantity, 230 | confirmOrder, 231 | }; 232 | }; 233 | 234 | export default useStore; 235 | -------------------------------------------------------------------------------- /src/assets/css/base.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | @import url("https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,400;0,700;1,400&display=swap"); 12 | 13 | html { 14 | line-height: 1.15; /* 1 */ 15 | -webkit-text-size-adjust: 100%; /* 2 */ 16 | } 17 | 18 | /* Sections 19 | ========================================================================== */ 20 | 21 | /** 22 | * Remove the margin in all browsers. 23 | */ 24 | 25 | body { 26 | margin: 0; 27 | font-family: "Lato", sans-serif; 28 | } 29 | 30 | /** 31 | * Render the `main` element consistently in IE. 32 | */ 33 | 34 | main { 35 | display: block; 36 | } 37 | 38 | /** 39 | * Correct the font size and margin on `h1` elements within `section` and 40 | * `article` contexts in Chrome, Firefox, and Safari. 41 | */ 42 | 43 | h1 { 44 | font-size: 2em; 45 | margin: 0.67em 0; 46 | } 47 | 48 | /* Grouping content 49 | ========================================================================== */ 50 | 51 | /** 52 | * 1. Add the correct box sizing in Firefox. 53 | * 2. Show the overflow in Edge and IE. 54 | */ 55 | 56 | hr { 57 | box-sizing: content-box; /* 1 */ 58 | height: 0; /* 1 */ 59 | overflow: visible; /* 2 */ 60 | } 61 | 62 | /** 63 | * 1. Correct the inheritance and scaling of font size in all browsers. 64 | * 2. Correct the odd `em` font sizing in all browsers. 65 | */ 66 | 67 | pre { 68 | font-family: monospace, monospace; /* 1 */ 69 | font-size: 1em; /* 2 */ 70 | } 71 | 72 | /* Text-level semantics 73 | ========================================================================== */ 74 | 75 | /** 76 | * Remove the gray background on active links in IE 10. 77 | */ 78 | 79 | a { 80 | background-color: transparent; 81 | } 82 | 83 | /** 84 | * 1. Remove the bottom border in Chrome 57- 85 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 86 | */ 87 | 88 | abbr[title] { 89 | border-bottom: none; /* 1 */ 90 | text-decoration: underline; /* 2 */ 91 | text-decoration: underline dotted; /* 2 */ 92 | } 93 | 94 | /** 95 | * Add the correct font weight in Chrome, Edge, and Safari. 96 | */ 97 | 98 | b, 99 | strong { 100 | font-weight: bolder; 101 | } 102 | 103 | /** 104 | * 1. Correct the inheritance and scaling of font size in all browsers. 105 | * 2. Correct the odd `em` font sizing in all browsers. 106 | */ 107 | 108 | code, 109 | kbd, 110 | samp { 111 | font-family: monospace, monospace; /* 1 */ 112 | font-size: 1em; /* 2 */ 113 | } 114 | 115 | /** 116 | * Add the correct font size in all browsers. 117 | */ 118 | 119 | small { 120 | font-size: 80%; 121 | } 122 | 123 | /** 124 | * Prevent `sub` and `sup` elements from affecting the line height in 125 | * all browsers. 126 | */ 127 | 128 | sub, 129 | sup { 130 | font-size: 75%; 131 | line-height: 0; 132 | position: relative; 133 | vertical-align: baseline; 134 | } 135 | 136 | sub { 137 | bottom: -0.25em; 138 | } 139 | 140 | sup { 141 | top: -0.5em; 142 | } 143 | 144 | /* Embedded content 145 | ========================================================================== */ 146 | 147 | /** 148 | * Remove the border on images inside links in IE 10. 149 | */ 150 | 151 | img { 152 | border-style: none; 153 | } 154 | 155 | /* Forms 156 | ========================================================================== */ 157 | 158 | /** 159 | * 1. Change the font styles in all browsers. 160 | * 2. Remove the margin in Firefox and Safari. 161 | */ 162 | 163 | button, 164 | input, 165 | optgroup, 166 | select, 167 | textarea { 168 | font-family: inherit; /* 1 */ 169 | font-size: 100%; /* 1 */ 170 | line-height: 1.15; /* 1 */ 171 | margin: 0; /* 2 */ 172 | } 173 | 174 | /** 175 | * Show the overflow in IE. 176 | * 1. Show the overflow in Edge. 177 | */ 178 | 179 | button, 180 | input { 181 | /* 1 */ 182 | overflow: visible; 183 | } 184 | 185 | /** 186 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 187 | * 1. Remove the inheritance of text transform in Firefox. 188 | */ 189 | 190 | button, 191 | select { 192 | /* 1 */ 193 | text-transform: none; 194 | } 195 | 196 | /** 197 | * Correct the inability to style clickable types in iOS and Safari. 198 | */ 199 | 200 | button, 201 | [type="button"], 202 | [type="reset"], 203 | [type="submit"] { 204 | -webkit-appearance: button; 205 | } 206 | 207 | /** 208 | * Remove the inner border and padding in Firefox. 209 | */ 210 | 211 | button::-moz-focus-inner, 212 | [type="button"]::-moz-focus-inner, 213 | [type="reset"]::-moz-focus-inner, 214 | [type="submit"]::-moz-focus-inner { 215 | border-style: none; 216 | padding: 0; 217 | } 218 | 219 | /** 220 | * Restore the focus styles unset by the previous rule. 221 | */ 222 | 223 | button:-moz-focusring, 224 | [type="button"]:-moz-focusring, 225 | [type="reset"]:-moz-focusring, 226 | [type="submit"]:-moz-focusring { 227 | outline: 1px dotted ButtonText; 228 | } 229 | 230 | /** 231 | * Correct the padding in Firefox. 232 | */ 233 | 234 | fieldset { 235 | padding: 0.35em 0.75em 0.625em; 236 | } 237 | 238 | /** 239 | * 1. Correct the text wrapping in Edge and IE. 240 | * 2. Correct the color inheritance from `fieldset` elements in IE. 241 | * 3. Remove the padding so developers are not caught out when they zero out 242 | * `fieldset` elements in all browsers. 243 | */ 244 | 245 | legend { 246 | box-sizing: border-box; /* 1 */ 247 | color: inherit; /* 2 */ 248 | display: table; /* 1 */ 249 | max-width: 100%; /* 1 */ 250 | padding: 0; /* 3 */ 251 | white-space: normal; /* 1 */ 252 | } 253 | 254 | /** 255 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 256 | */ 257 | 258 | progress { 259 | vertical-align: baseline; 260 | } 261 | 262 | /** 263 | * Remove the default vertical scrollbar in IE 10+. 264 | */ 265 | 266 | textarea { 267 | overflow: auto; 268 | } 269 | 270 | /** 271 | * 1. Add the correct box sizing in IE 10. 272 | * 2. Remove the padding in IE 10. 273 | */ 274 | 275 | [type="checkbox"], 276 | [type="radio"] { 277 | box-sizing: border-box; /* 1 */ 278 | padding: 0; /* 2 */ 279 | } 280 | 281 | /** 282 | * Correct the cursor style of increment and decrement buttons in Chrome. 283 | */ 284 | 285 | [type="number"]::-webkit-inner-spin-button, 286 | [type="number"]::-webkit-outer-spin-button { 287 | height: auto; 288 | } 289 | 290 | /** 291 | * 1. Correct the odd appearance in Chrome and Safari. 292 | * 2. Correct the outline style in Safari. 293 | */ 294 | 295 | [type="search"] { 296 | -webkit-appearance: textfield; /* 1 */ 297 | outline-offset: -2px; /* 2 */ 298 | } 299 | 300 | /** 301 | * Remove the inner padding in Chrome and Safari on macOS. 302 | */ 303 | 304 | [type="search"]::-webkit-search-decoration { 305 | -webkit-appearance: none; 306 | } 307 | 308 | /** 309 | * 1. Correct the inability to style clickable types in iOS and Safari. 310 | * 2. Change font properties to `inherit` in Safari. 311 | */ 312 | 313 | ::-webkit-file-upload-button { 314 | -webkit-appearance: button; /* 1 */ 315 | font: inherit; /* 2 */ 316 | } 317 | 318 | /* Interactive 319 | ========================================================================== */ 320 | 321 | /* 322 | * Add the correct display in Edge, IE 10+, and Firefox. 323 | */ 324 | 325 | details { 326 | display: block; 327 | } 328 | 329 | /* 330 | * Add the correct display in all browsers. 331 | */ 332 | 333 | summary { 334 | display: list-item; 335 | } 336 | 337 | /* Misc 338 | ========================================================================== */ 339 | 340 | /** 341 | * Add the correct display in IE 10+. 342 | */ 343 | 344 | template { 345 | display: none; 346 | } 347 | 348 | /** 349 | * Add the correct display in IE 10. 350 | */ 351 | 352 | [hidden] { 353 | display: none; 354 | } 355 | --------------------------------------------------------------------------------