├── 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 |
8 |
9 |
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 |
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 |
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 | Buy Now
15 |
16 |
17 |
18 |
19 |
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 |
10 | Headphone Type
11 | Test
12 | Test
13 |
14 |
15 | {/* */}
16 |
17 |
18 | Price
19 | Test
20 | Test
21 |
22 |
23 | {/* */}
24 |
25 |
26 | Test
27 | Test
28 | Test
29 |
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 |
12 | +233 xxx xxx xxx
13 | location xx ,xxx
14 | Socials
15 |
16 |
17 |
18 |
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 |
14 |
15 | {/* */}
18 |
21 |
24 |
27 |
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 |
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 |
27 | Login
28 |
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 |
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 |
27 | Login
28 |
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 |
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 |
{
26 | store.reduceQuantity(product._id);
27 | }}
28 | >
29 | -
30 |
31 |
{product.quantity}
32 |
{
34 | store.addQuantity(product._id);
35 | }}
36 | >
37 | +
38 |
39 |
40 |
41 |
42 | {
44 | store.removeFromCart(product?._id);
45 | }}
46 | >
47 | Remove
48 |
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 |
27 |
33 |
34 |
35 |
36 |
37 | {/*
*/}
38 |
39 |
40 |
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 |
43 | Login
44 |
45 | ) : (
46 |
47 | Logout
48 |
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 |
30 |
31 | } />
32 | } />
33 | } />
34 | } />
35 |
36 |
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 |
47 | Reload Orders
48 |
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 |
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 | {
43 | if (store.state.cartQuantity > 10) {
44 | toast.warning("You can only add 10 items to cart");
45 | return;
46 | }
47 | store.addToCart(product?._id);
48 | }}
49 | >
50 | Add to Cart
51 |
52 | ) : (
53 | {
56 | store.removeFromCart(product?._id);
57 | }}
58 | >
59 | Remove from cart
60 |
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 |
47 | X
48 |
49 |
50 |
51 |
Are you sure you want to cancel your order?
52 |
53 |
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 | {
42 | setDelivery(item.target.value);
43 | }}
44 | >
45 |
46 | Standard
47 |
48 |
49 | Express
50 |
51 |
52 |
53 |
54 |
Promo Code
55 |
56 |
57 | 0 ? false : true}
60 | >
61 | Apply
62 |
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 | {
93 | if (phone.length > 0) {
94 | checkOut();
95 | toast.info("Your order is being processed");
96 | return;
97 | }
98 | toast.error("Please enter your phone number");
99 | }}
100 | disabled={store.state.cartQuantity > 0 ? false : true}
101 | >
102 | Checkout
103 |
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 |
65 | X
66 |
67 |
68 |
69 |
{header}
70 |
71 |
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 | {
129 | handleOpenCancelModal(order._id);
130 | }}
131 | >
132 | Cancel Order
133 |
134 | {
137 | // mailto link
138 | window.location.href = `mailto:www.minisylar3@gmail.com?subject=Reporting Order #${order._id.slice(
139 | 0,
140 | 6
141 | )}`;
142 | }}
143 | >
144 | Report Issue
145 |
146 |
147 |
148 | )}
149 |
150 |
151 |
152 |
153 |
154 | {expanded ? "Collapse" : "Expand"}
155 |
156 |
161 |
162 |
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 |
--------------------------------------------------------------------------------