├── .firebase └── hosting.YnVpbGQ.cache ├── .firebaserc ├── .gitignore ├── README.md ├── firebase.json ├── functions ├── .eslintrc.json ├── .gitignore ├── index.js ├── package-lock.json └── package.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.js ├── App.test.js ├── app.scss ├── assets │ ├── add_shopping_cart.svg │ ├── auth-logo.png │ ├── cart.svg │ ├── header-logo.png │ ├── home-bg-alexa.jpg │ ├── home-bg-computer.jpg │ ├── home-bg-gift.jpg │ ├── home-bg-nz.jpg │ ├── home-bg-prime.jpg │ ├── home-bg-ship.jpg │ ├── menu.svg │ ├── place.svg │ ├── search-icon.svg │ ├── star.svg │ ├── star_border.svg │ └── start_half.svg ├── axios.js ├── components │ ├── card │ │ ├── Card.jsx │ │ ├── Card.scss │ │ ├── CheckoutCard.jsx │ │ ├── CheckoutCard.scss │ │ ├── ProductCard.jsx │ │ └── ProductCard.scss │ ├── carousel │ │ ├── Carousel.jsx │ │ └── Carousel.scss │ ├── footer │ │ ├── Footer.jsx │ │ └── Footer.scss │ ├── header │ │ ├── header.jsx │ │ └── header.scss │ ├── home │ │ ├── BestSellers.jsx │ │ ├── BestSellers.scss │ │ ├── PopularSection.jsx │ │ ├── PopularSection.scss │ │ ├── RecommendSection.jsx │ │ ├── RecommendSection.scss │ │ ├── RelatedSection.jsx │ │ ├── ShopBySection.jsx │ │ └── ShopBySection.scss │ ├── link │ │ ├── RouterLink.jsx │ │ └── RouterLink.scss │ ├── order │ │ ├── Order.jsx │ │ └── Order.scss │ └── shop │ │ ├── ShopPage.jsx │ │ └── ShopPage.scss ├── firebase.utils.js ├── index.js ├── layouts │ ├── AuthLayout │ │ ├── AuthLayout.scss │ │ └── index.js │ └── MainLayout │ │ ├── MainLayout.scss │ │ └── index.js ├── mockup.js ├── redux │ ├── cartSelector.js │ ├── cartSlice.js │ ├── productSelector.js │ ├── productsSlice.js │ ├── store.js │ └── userSlice.js ├── routes.js ├── scss │ ├── _basic.scss │ ├── _mixins.scss │ └── _variables.scss ├── serviceWorker.js ├── setupTests.js └── views │ ├── auth │ ├── Login.jsx │ ├── Login.scss │ ├── Register.jsx │ └── Register.scss │ ├── checkout │ ├── Checkout.jsx │ └── Checkout.scss │ ├── errors │ └── NotFoundView.js │ ├── home │ ├── home.jsx │ └── home.scss │ ├── orders │ ├── Orders.jsx │ └── Orders.scss │ ├── payment │ ├── Payment.jsx │ ├── Payment.scss │ └── Success.jsx │ ├── product │ ├── Product.jsx │ └── Product.scss │ └── shop │ ├── shop.jsx │ └── shop.scss └── yarn.lock /.firebase/hosting.YnVpbGQ.cache: -------------------------------------------------------------------------------- 1 | asset-manifest.json,1606266629988,2d9a7020a38c1ae8715b72c0ec82f5f9a9ebdae99c37232e18610a0519ec61e1 2 | index.html,1606266629986,1387f9c095dfea6f2982471129158ab4e9a0dd90ff5de7ce13d2ee7256491445 3 | favicon.ico,1605145252532,d96ddbc4933b04e12c738ab39f469573143949ca2c39eda0a49d16f83d40c319 4 | manifest.json,1605145252532,5c997de1364b8be939319fa9209abd77f2caf7f8844999a9e2e9173f844e7840 5 | logo192.png,1605145252532,3ee59515172ee198f3be375979df15ac5345183e656720a381b8872b2a39dc8b 6 | robots.txt,1605145252532,b2090cf9761ef60aa06e4fab97679bd43dfa5e5df073701ead5879d7c68f1ec5 7 | precache-manifest.1cc78e8194080b1723486cec36e4d137.js,1606266629987,37cdbb6d2c0bd5aa16f80390d40f756169e7ef5e6455e988547659692ed1bf70 8 | service-worker.js,1606266629987,48f8044014129ea1cc64900c781efbb6e5ae1b4581ff8dd8ddcf63ab6c8281fd 9 | static/css/main.5a749ae4.chunk.css,1606266629961,e35b69e1d12c37b59ef8c6e65caae53151e1778d4d876fe798a284df8a687316 10 | logo512.png,1605145252532,ee7e2f3fdb8209c4b6fd7bef6ba50d1b9dba30a25bb5c3126df057e1cb6f5331 11 | static/css/main.5a749ae4.chunk.css.map,1606266629986,fb38a6a115da870602c1a6bd252bdcc90df4dbe4432fa94524b79aa6cab65873 12 | static/js/2.e4f6a30c.chunk.js.LICENSE.txt,1606266629985,f4c4cfb51c4344d8e4b70e60f0d080d512d460172db8050964fb5013d7d8d183 13 | static/js/runtime-main.7006a95e.js,1606266629985,b1a9c4519d1da00d405553d197bf302fa160921ff5bf4fd921d0770ef0a355af 14 | static/js/runtime-main.7006a95e.js.map,1606266629985,454af535b516dd224aec3a3d183da3395ee6ef75b88fd0f3718882175aaea9df 15 | static/media/add_shopping_cart.bba0d047.svg,1606266629984,d384167b7e4a88dab543f877c65782b3fde9328c273c6dadf888fee38394ceef 16 | static/media/cart.84c5fa25.svg,1606266629963,70e034dd9a801aa90bbde39b2dff52538141850e59c472248246f9493146e489 17 | static/media/menu.55f68494.svg,1606266629953,fdddffcdd481562b185b38d79157793f03b55e3f7468a23fe0333bce23c02e20 18 | static/media/place.09425351.svg,1606266629984,cb99358d05dfb9e6208029a25ff8d748cccba09c4b2264f49038065546ddaa3a 19 | static/media/search-icon.db5d8122.svg,1606266629956,77dfa9bbdf380baa793b4664b2178b0aa706cfe36ea77154f924777ba91c1744 20 | static/media/slick.b7c9e1e4.woff,1606266629950,029a727c9e1081d2f9e1015f02f18da88f268512ea4f58589a6f17f532928530 21 | static/media/slick.f97e3bbf.svg,1606266629952,d00477d40ed40fa1dd3bdee27d9286804121b15fea83bae7d5415d902b055de0 22 | static/media/slick.d41f55a7.ttf,1606266629952,6aad751007cf9a89350af287f824ab64f096c472c7c9c679fb3d55b6174fc004 23 | static/media/star.555f59ec.svg,1606266629953,a294f099e4233acead5b1c7021c647f95b8a7d3da5dd3a6554a189f04f47509f 24 | static/media/start_half.71db7c4e.svg,1606266629953,756e87211bf273d2fb7a8dbee001ca4686536ed36d976b00140a39f464052f72 25 | static/media/star_border.e8c07bf4.svg,1606266629953,998a24820b96a893de008b62c1904840a843e273ebff83ac4439b80ed8c5fb76 26 | static/media/auth-logo.f07041c4.png,1606266629953,e8be3194bc336bc5ee9ff7cd8cdc2be1a150c8915dc981a83d20bbd8b4b08210 27 | static/media/home-bg-nz.38761ed4.jpg,1606266629953,81ea1a68dcf8eee951db3690e6e121a6dfec01120e7162b4eb28a8e1deaf2ed8 28 | static/media/slick.ced611da.eot,1606266629952,52b9a169969e053d43df6bd21c4dc789c2518d99afd8590a17ef50cd24c77cf6 29 | static/js/main.2a595da6.chunk.js,1606266629961,f7f64b1f554eb25b3655a0f295f1e7ea526d3dae6b9d7b6ee53e1632c44fa3b5 30 | static/media/home-bg-ship.1b10c93f.jpg,1606266629953,9c87cdf43341a95d83d28f7920cc04b1e9f425f13f38b5ad83704df15ce4988e 31 | static/media/home-bg-computer.6aafad0c.jpg,1606266629953,c4ee3bb97162a4e21269ab791108a2d665c3b6d94e7225ff3afde6c9cf551942 32 | static/media/header-logo.7451f65b.png,1606266629953,9c2857d0056bec3cfb4e789b19eb576703f565589ebc39a6dd56afc494c090b8 33 | static/media/home-bg-prime.80469a24.jpg,1606266629952,b6b0f92d90ff7db059c0d6379b7963c554b2775eeb026d0c1270b549e25c9acb 34 | static/media/home-bg-alexa.87b0e88b.jpg,1606266629953,8d3d277fad4370cae47ed0733b0998ac83cbd51e56c41a26858cc6c256abbb42 35 | static/js/main.2a595da6.chunk.js.map,1606266629985,7dfe7c6a78c4c4568c98ff564564c352e8b5e83234fc6b0fd109184e2dc275c8 36 | static/css/2.8c03b463.chunk.css,1606266629985,cc61bdf46d88906ada16ec92f758038cdb696a30b6a002a14b718a32d2ff452c 37 | static/js/2.e4f6a30c.chunk.js,1606266629985,d150163fde6b9438ad28228b26869b0b6f84a751f0f7af570e49a3470f8995d1 38 | static/css/2.8c03b463.chunk.css.map,1606266629986,e804e652bcf479b1e341d8ce81afd437c1b38bf2249e1fa5849a798ddcacc387 39 | static/js/2.e4f6a30c.chunk.js.map,1606266629987,a7d8816b4fd752aeafc0c2b76b42d8d4c588a1c4eae3a8c59c65108058ea2107 40 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "github-c5c88" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | 107 | build/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## [Amazon Clone](https://github-c5c88.firebaseapp.com/) 2 | 3 | - Views are on: https://github-c5c88.firebaseapp.com
4 | 5 | [![Amazon Clone Index](https://firebasestorage.googleapis.com/v0/b/github-c5c88.appspot.com/o/appScreenshot%2Famazon-index.jpg?alt=media&token=79eb6701-a406-46f7-9216-e32e9b30f2a5)](https://github-c5c88.firebaseapp.com/) 6 | > This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
7 | It is a fully functional E-Commerical Amazon clone including user register, user login, user authentication, shop cart management, order management, and payment using STRIPE, etc. 8 | ## Technical Skills used in this project 9 | 10 | | Front End | Back End | 11 | | ------------------------ | :----------------------------------------------------------- | 12 | | ✔ react hooks | ✔ NodeJS 13 | | ✔ react redux |✔ Express / cors 14 | | ✔ react router |✔ Authtentication with Firebase 15 | | ✔ redux toolkit |✔ Firebase Cloud Function 16 | | ✔ redux-devtools-extension | ✔ Firebase Hosting 17 | | ✔ immer | ✔ Realtime Database / Cloud Firestore 18 | | ✔ reselect | ✔ Stripe-js 19 | | ✔ redux-thunk | - 20 | | ✔ redux-persist | - 21 | | ✔ formik | - 22 | | ✔ SASS | - 23 | | ✔ react bootstrap / AntDesign | - 24 | 25 | ## Demo 26 | 27 | - [Home Page](https://github-c5c88.firebaseapp.com/) 28 | - [Register Page](https://github-c5c88.firebaseapp.com/auth/register) 29 | - [Login Page](https://github-c5c88.firebaseapp.com/auth) 30 | - [Product Page](https://github-c5c88.firebaseapp.com/product/hDZ4xItnMf0l3YT17uY5) 31 | - [Cart Page](https://github-c5c88.firebaseapp.com/checkout) 32 | - [Checkout Page](https://github-c5c88.firebaseapp.com/checkout) 33 | - [Payment Page](https://github-c5c88.firebaseapp.com/payment) 34 | - [Order Page](https://github-c5c88.firebaseapp.com/orders) 35 | - [Payment API Endpoint](https://us-central1-github-c5c88.cloudfunctions.net/amazonStripeApi) 36 | - [Stripe Payments Show](https://firebasestorage.googleapis.com/v0/b/github-c5c88.appspot.com/o/appScreenshot%2Famazon-payment.png?alt=media&token=ea6245b6-9a90-4709-a10d-8023aeac72b7) 37 | [![Stripe Payment](https://firebasestorage.googleapis.com/v0/b/github-c5c88.appspot.com/o/appScreenshot%2Famazon-payment.png?alt=media&token=ea6245b6-9a90-4709-a10d-8023aeac72b7)](https://firebasestorage.googleapis.com/v0/b/github-c5c88.appspot.com/o/appScreenshot%2Famazon-payment.png?alt=media&token=ea6245b6-9a90-4709-a10d-8023aeac72b7) 38 | 39 | ## Quick start 40 | 41 | - [Download from Github](https://github.com/aaroncodehub/amazon-clone/archive/master.zip) or clone the repo: `git clone https://github.com/aaroncodehub/amazon-clone.git` 42 | 43 | - Make sure your NodeJS and npm versions are up to date for `16.13.1` 44 | 45 | - Install dependencies: `npm install` or `yarn` 46 | 47 | - Start the server: `npm run start` or `yarn start` 48 | 49 | - Views are on: `localhost:3000` 50 | 51 | - Payment API Endpoint: `https://us-central1-github-c5c88.cloudfunctions.net/amazonStripeApi` or
Get a local endpoint: `firebase emulators:start` 52 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": { 3 | "predeploy": [ 4 | "npm --prefix \"$RESOURCE_DIR\" run lint" 5 | ], 6 | "source": "functions" 7 | }, 8 | "hosting": { 9 | "public": "build", 10 | "ignore": [ 11 | "firebase.json", 12 | "**/.*", 13 | "**/node_modules/**" 14 | ], 15 | "rewrites": [ 16 | { 17 | "source": "**", 18 | "destination": "/index.html" 19 | } 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /functions/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | // Required for certain syntax usages 4 | "ecmaVersion": 2017 5 | }, 6 | "plugins": [ 7 | "promise" 8 | ], 9 | "extends": "eslint:recommended", 10 | "rules": { 11 | // Removed rule "disallow the use of console" from recommended eslint rules 12 | "no-console": "off", 13 | 14 | // Removed rule "disallow multiple spaces in regular expressions" from recommended eslint rules 15 | "no-regex-spaces": "off", 16 | 17 | // Removed rule "disallow the use of debugger" from recommended eslint rules 18 | "no-debugger": "off", 19 | 20 | // Removed rule "disallow unused variables" from recommended eslint rules 21 | "no-unused-vars": "off", 22 | 23 | // Removed rule "disallow mixed spaces and tabs for indentation" from recommended eslint rules 24 | "no-mixed-spaces-and-tabs": "off", 25 | 26 | // Removed rule "disallow the use of undeclared variables unless mentioned in /*global */ comments" from recommended eslint rules 27 | "no-undef": "off", 28 | 29 | // Warn against template literal placeholder syntax in regular strings 30 | "no-template-curly-in-string": 1, 31 | 32 | // Warn if return statements do not either always or never specify values 33 | "consistent-return": 1, 34 | 35 | // Warn if no return statements in callbacks of array methods 36 | "array-callback-return": 1, 37 | 38 | // Require the use of === and !== 39 | "eqeqeq": 2, 40 | 41 | // Disallow the use of alert, confirm, and prompt 42 | "no-alert": 2, 43 | 44 | // Disallow the use of arguments.caller or arguments.callee 45 | "no-caller": 2, 46 | 47 | // Disallow null comparisons without type-checking operators 48 | "no-eq-null": 2, 49 | 50 | // Disallow the use of eval() 51 | "no-eval": 2, 52 | 53 | // Warn against extending native types 54 | "no-extend-native": 1, 55 | 56 | // Warn against unnecessary calls to .bind() 57 | "no-extra-bind": 1, 58 | 59 | // Warn against unnecessary labels 60 | "no-extra-label": 1, 61 | 62 | // Disallow leading or trailing decimal points in numeric literals 63 | "no-floating-decimal": 2, 64 | 65 | // Warn against shorthand type conversions 66 | "no-implicit-coercion": 1, 67 | 68 | // Warn against function declarations and expressions inside loop statements 69 | "no-loop-func": 1, 70 | 71 | // Disallow new operators with the Function object 72 | "no-new-func": 2, 73 | 74 | // Warn against new operators with the String, Number, and Boolean objects 75 | "no-new-wrappers": 1, 76 | 77 | // Disallow throwing literals as exceptions 78 | "no-throw-literal": 2, 79 | 80 | // Require using Error objects as Promise rejection reasons 81 | "prefer-promise-reject-errors": 2, 82 | 83 | // Enforce “for” loop update clause moving the counter in the right direction 84 | "for-direction": 2, 85 | 86 | // Enforce return statements in getters 87 | "getter-return": 2, 88 | 89 | // Disallow await inside of loops 90 | "no-await-in-loop": 2, 91 | 92 | // Disallow comparing against -0 93 | "no-compare-neg-zero": 2, 94 | 95 | // Warn against catch clause parameters from shadowing variables in the outer scope 96 | "no-catch-shadow": 1, 97 | 98 | // Disallow identifiers from shadowing restricted names 99 | "no-shadow-restricted-names": 2, 100 | 101 | // Enforce return statements in callbacks of array methods 102 | "callback-return": 2, 103 | 104 | // Require error handling in callbacks 105 | "handle-callback-err": 2, 106 | 107 | // Warn against string concatenation with __dirname and __filename 108 | "no-path-concat": 1, 109 | 110 | // Prefer using arrow functions for callbacks 111 | "prefer-arrow-callback": 1, 112 | 113 | // Return inside each then() to create readable and reusable Promise chains. 114 | // Forces developers to return console logs and http calls in promises. 115 | "promise/always-return": 2, 116 | 117 | //Enforces the use of catch() on un-returned promises 118 | "promise/catch-or-return": 2, 119 | 120 | // Warn against nested then() or catch() statements 121 | "promise/no-nesting": 1 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /functions/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.log -------------------------------------------------------------------------------- /functions/index.js: -------------------------------------------------------------------------------- 1 | const functions = require("firebase-functions"); 2 | 3 | // // Create and Deploy Your First Cloud Functions 4 | // // https://firebase.google.com/docs/functions/write-firebase-functions 5 | // 6 | // exports.helloWorld = functions.https.onRequest((request, response) => { 7 | // response.send("Hello from Firebase!"); 8 | // }); 9 | 10 | // set env vars: gcloud functions deploy FUNCTION_NAME --set-env-vars FOO=bar , get env vars : process.env.FOO 11 | 12 | const express = require("express"); 13 | const cors = require("cors"); 14 | const stripe = require("stripe")( 15 | process.env.STRIPE_KEY 16 | ); 17 | 18 | // - App Config 19 | const app = express(); 20 | 21 | // - Middlrwares 22 | app.use(cors()); 23 | app.use(express.json()); 24 | 25 | // - API Routes 26 | app.get("/", (req, res) => res.status(200).send("welcome")); 27 | 28 | app.post("/payments/create/:total", async (req, res) => { 29 | 30 | if (req.params.total > 0) { 31 | // only accept integer 32 | const total = parseFloat(req.params.total).toFixed(0); 33 | console.log("Payment request received. Amount ->>>", total); 34 | 35 | const paymentIntent = await stripe.paymentIntents.create({ 36 | amount: total, 37 | currency: "usd", 38 | }); 39 | res.status(201).send({ 40 | clientSecret: paymentIntent.client_secret, 41 | }); 42 | } else { 43 | res.status(201).send({ 44 | clientSecret: 'no session id', 45 | }) 46 | } 47 | }); 48 | 49 | // - Listen Command 50 | exports.amazonStripeApi = functions.https.onRequest(app); 51 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "scripts": { 5 | "lint": "eslint .", 6 | "serve": "firebase emulators:start --only functions", 7 | "shell": "firebase functions:shell", 8 | "start": "npm run shell", 9 | "deploy": "firebase deploy --only functions", 10 | "logs": "firebase functions:log" 11 | }, 12 | "engines": { 13 | "node": "12" 14 | }, 15 | "dependencies": { 16 | "cors": "^2.8.5", 17 | "express": "^4.17.1", 18 | "firebase-admin": "^8.10.0", 19 | "firebase-functions": "^3.6.1", 20 | "stripe": "^8.115.0" 21 | }, 22 | "devDependencies": { 23 | "eslint": "^5.12.0", 24 | "eslint-plugin-promise": "^4.0.1", 25 | "firebase-functions-test": "^0.2.0" 26 | }, 27 | "private": true 28 | } 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "amazon-cl", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@ant-design/icons": "^4.3.0", 7 | "@brainhubeu/react-carousel": "2.0.1", 8 | "@reduxjs/toolkit": "^1.4.0", 9 | "@stripe/react-stripe-js": "^1.1.2", 10 | "@stripe/stripe-js": "^1.10.0", 11 | "@testing-library/jest-dom": "^4.2.4", 12 | "@testing-library/react": "^9.3.2", 13 | "@testing-library/user-event": "^7.1.2", 14 | "antd": "^4.8.4", 15 | "axios": "^0.20.0", 16 | "bootstrap": "^4.5.2", 17 | "firebase": "^7.23.0", 18 | "formik": "^2.2.0", 19 | "history": "^5.0.0", 20 | "moment": "^2.29.1", 21 | "node-sass": "^4.14.1", 22 | "react": "^16.13.1", 23 | "react-bootstrap": "^1.3.0", 24 | "react-currency-format": "^1.0.0", 25 | "react-dom": "^16.13.1", 26 | "react-image-gallery": "^1.0.8", 27 | "react-redux": "^7.2.1", 28 | "react-router-dom": "^6.0.0-beta.0", 29 | "react-scripts": "3.4.3", 30 | "react-slick": "^0.27.13", 31 | "redux-persist": "^6.0.0", 32 | "slick-carousel": "^1.8.1", 33 | "uuid": "^8.3.1", 34 | "yup": "^0.29.3" 35 | }, 36 | "scripts": { 37 | "start": "react-scripts start", 38 | "build": "react-scripts build", 39 | "test": "react-scripts test", 40 | "eject": "react-scripts eject" 41 | }, 42 | "eslintConfig": { 43 | "extends": "react-app" 44 | }, 45 | "browserslist": { 46 | "production": [ 47 | ">0.2%", 48 | "not dead", 49 | "not op_mini all" 50 | ], 51 | "development": [ 52 | "last 1 chrome version", 53 | "last 1 firefox version", 54 | "last 1 safari version" 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaroncodehub/amazon-clone-react-redux/3e5c8952334f10cab16e724a6a10932976d12d61/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Amazon Clone 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaroncodehub/amazon-clone-react-redux/3e5c8952334f10cab16e724a6a10932976d12d61/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaroncodehub/amazon-clone-react-redux/3e5c8952334f10cab16e724a6a10932976d12d61/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useRoutes } from "react-router-dom"; 3 | import { useDispatch } from "react-redux"; 4 | import { auth } from "./firebase.utils"; 5 | import routes from "./routes"; 6 | import "./app.scss"; 7 | import { setUser, fetchUser } from "./redux/userSlice"; 8 | 9 | function App() { 10 | const dispatch = useDispatch(); 11 | 12 | useEffect(() => { 13 | const unsubscribe = auth.onAuthStateChanged((authUser) => { 14 | if (authUser) { 15 | dispatch(fetchUser(authUser.uid)); 16 | } else { 17 | dispatch(setUser(null)); 18 | } 19 | }); 20 | return () => { 21 | unsubscribe(); 22 | }; 23 | }, [dispatch]); 24 | 25 | const routing = useRoutes(routes); 26 | return
{routing}
; 27 | } 28 | 29 | export default App; 30 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/app.scss: -------------------------------------------------------------------------------- 1 | @import "./scss/basic"; 2 | @import "./scss/variables"; 3 | @import "./scss/mixins"; 4 | @import "./views/home/home.scss"; 5 | @import "./views/shop/shop.scss"; 6 | @import "./components/header/header.scss"; 7 | @import "./components/card/Card.scss"; 8 | @import "./components/card/ProductCard.scss"; 9 | @import "./components/home/ShopBySection.scss"; 10 | @import "./components/carousel/Carousel.scss"; 11 | @import "./components/shop/ShopPage.scss"; 12 | @import './components/footer/Footer.scss'; 13 | @import './views/auth/Register.scss'; 14 | @import './views/auth/Login.scss'; 15 | @import './layouts/MainLayout/MainLayout.scss'; 16 | @import './layouts/AuthLayout/AuthLayout.scss'; 17 | @import './views/checkout/Checkout.scss'; 18 | @import './components/card/CheckoutCard.scss'; 19 | @import './views/product/Product.scss'; 20 | @import './views/payment/Payment.scss'; 21 | @import './components/order/Order.scss'; 22 | @import './views/orders/Orders.scss'; 23 | @import './views/payment/Payment.scss'; 24 | @import './components/carousel/Carousel.scss'; 25 | -------------------------------------------------------------------------------- /src/assets/add_shopping_cart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/auth-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaroncodehub/amazon-clone-react-redux/3e5c8952334f10cab16e724a6a10932976d12d61/src/assets/auth-logo.png -------------------------------------------------------------------------------- /src/assets/cart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 11 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/assets/header-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaroncodehub/amazon-clone-react-redux/3e5c8952334f10cab16e724a6a10932976d12d61/src/assets/header-logo.png -------------------------------------------------------------------------------- /src/assets/home-bg-alexa.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaroncodehub/amazon-clone-react-redux/3e5c8952334f10cab16e724a6a10932976d12d61/src/assets/home-bg-alexa.jpg -------------------------------------------------------------------------------- /src/assets/home-bg-computer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaroncodehub/amazon-clone-react-redux/3e5c8952334f10cab16e724a6a10932976d12d61/src/assets/home-bg-computer.jpg -------------------------------------------------------------------------------- /src/assets/home-bg-gift.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaroncodehub/amazon-clone-react-redux/3e5c8952334f10cab16e724a6a10932976d12d61/src/assets/home-bg-gift.jpg -------------------------------------------------------------------------------- /src/assets/home-bg-nz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaroncodehub/amazon-clone-react-redux/3e5c8952334f10cab16e724a6a10932976d12d61/src/assets/home-bg-nz.jpg -------------------------------------------------------------------------------- /src/assets/home-bg-prime.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaroncodehub/amazon-clone-react-redux/3e5c8952334f10cab16e724a6a10932976d12d61/src/assets/home-bg-prime.jpg -------------------------------------------------------------------------------- /src/assets/home-bg-ship.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaroncodehub/amazon-clone-react-redux/3e5c8952334f10cab16e724a6a10932976d12d61/src/assets/home-bg-ship.jpg -------------------------------------------------------------------------------- /src/assets/menu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/place.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/search-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/star.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/star_border.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/start_half.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/axios.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const instance = axios.create({ 4 | baseURL:'https://us-central1-github-c5c88.cloudfunctions.net/amazonStripeApi' 5 | }) 6 | 7 | export const source = axios.CancelToken.source(); 8 | 9 | export default instance; -------------------------------------------------------------------------------- /src/components/card/Card.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Card, Button } from "react-bootstrap"; 3 | import RouterLink from "../link/RouterLink"; 4 | 5 | const SharedCard = ({ title, imgUrl, linkInfo }) => { 6 | return ( 7 |
8 | 9 | 10 | 11 | {title} 12 | 13 | 14 | 15 | 16 | 17 |
18 | ); 19 | }; 20 | 21 | export default SharedCard; 22 | -------------------------------------------------------------------------------- /src/components/card/Card.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | min-width: 320px !important; 3 | width: 320px; 4 | } -------------------------------------------------------------------------------- /src/components/card/CheckoutCard.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Form } from "react-bootstrap"; 3 | import { useDispatch } from "react-redux"; 4 | import { updateItemQuantity, removeItem } from "../../redux/cartSlice"; 5 | import RouterLink from "../link/RouterLink"; 6 | 7 | const CheckoutCard = ({ 8 | imgUrl, 9 | price, 10 | productId, 11 | active, 12 | title, 13 | quantity, 14 | hiddenAction 15 | }) => { 16 | const dispatch = useDispatch(); 17 | const formatedPrice = price.toLocaleString("en-US", { 18 | style: "currency", 19 | currency: "USD", 20 | }); 21 | 22 | const link = `/product/${productId}` 23 | 24 | const updateQuantity = (e) => { 25 | const itemQuantity = e.target.value; 26 | dispatch( 27 | updateItemQuantity({ productId, quantity: parseInt(itemQuantity) }) 28 | ); 29 | }; 30 | 31 | const removeCartItem = () => { 32 | dispatch(removeItem(productId)); 33 | }; 34 | 35 | return ( 36 |
37 | Amazon 38 |
39 |
40 |
41 | 42 | {title} 43 | 44 |
45 |
46 | {active ? In stock : Out of stock} 47 |
48 |
49 | Enable for FREE Delivery 50 |
51 | { 52 | !hiddenAction &&
53 |
54 |
55 | 56 | 62 | 63 |
64 |
65 |
66 | | 67 | Delete 68 |
69 |
70 | } 71 |
72 |
73 | {formatedPrice} 74 |
75 |
76 |
77 | ); 78 | }; 79 | 80 | export default CheckoutCard; 81 | -------------------------------------------------------------------------------- /src/components/card/CheckoutCard.scss: -------------------------------------------------------------------------------- 1 | .checkout-card { 2 | display: flex; 3 | 4 | &__img { 5 | @include breakpoint(l) { 6 | width: 8rem; 7 | margin: 1rem; 8 | } 9 | width: 8rem; 10 | object-fit: contain; 11 | margin: 1rem; 12 | } 13 | &__body { 14 | display: flex; 15 | padding-top: 1rem; 16 | flex-grow: 1; 17 | } 18 | &__title { 19 | font-size: 1.1rem; 20 | font-weight: 600; 21 | line-height: 1.2; 22 | 23 | color: $text-color-blue; 24 | } 25 | &__status { 26 | font-size: 0.8rem; 27 | color: green; 28 | font-weight: 500; 29 | } 30 | &__delivery { 31 | color: $text-color-grey; 32 | font-size: 0.8rem; 33 | font-weight: 600; 34 | } 35 | &__action { 36 | display: flex; 37 | align-items: center; 38 | &__qty { 39 | width: 4rem; 40 | margin-top: 0.5rem; 41 | } 42 | &__delete span:first-child { 43 | color: $line-color; 44 | padding-left: 1rem; 45 | } 46 | &__delete span:last-child { 47 | cursor: pointer; 48 | color: $text-color-blue; 49 | padding-left: 1rem; 50 | font-size: 0.8rem; 51 | font-weight: 600; 52 | } 53 | } 54 | &__price { 55 | font-weight: 600; 56 | @include breakpoint(l) { 57 | padding-left: 6rem; 58 | } 59 | padding-left: .6rem; 60 | } 61 | &__content { 62 | flex-grow: 1; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/components/card/ProductCard.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ReactComponent as Star } from "../../assets/star.svg"; 3 | import { ReactComponent as StarBorder } from "../../assets/star_border.svg"; 4 | import { ReactComponent as StarHalf } from "../../assets/start_half.svg"; 5 | import RouterLink from '../link/RouterLink' 6 | 7 | 8 | const ProductCard = ({ title, price, count, imgUrl, rate, productId}) => { 9 | 10 | 11 | const formatedPrice = price.toLocaleString('en-US', { style: 'currency', currency: 'USD' }); 12 | const splitPrice = formatedPrice.split('.') 13 | 14 | const link = `/product/${productId}` 15 | 16 | const renderStar = (rate) => { 17 | switch (rate) { 18 | case 1: 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | case 1.5: 29 | return ( 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | ); 38 | case 2: 39 | return ( 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | ); 48 | case 2.5: 49 | return ( 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | ); 58 | case 3: 59 | return ( 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | ); 68 | case 3.5: 69 | return ( 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | ); 78 | case 4: 79 | return ( 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | ); 88 | case 4.5: 89 | return ( 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | ); 98 | default: 99 | return ( 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | ); 108 | } 109 | }; 110 | 111 | return ( 112 |
113 | 114 | alex 115 |
116 |
{title}
117 |
118 | {splitPrice[0]} 119 | {splitPrice[1]} 120 |
121 | 122 | {renderStar(rate)} 123 | {`(${count})`} 124 | 125 |
126 |
127 |
128 |
129 | ); 130 | }; 131 | 132 | export default ProductCard; 133 | -------------------------------------------------------------------------------- /src/components/card/ProductCard.scss: -------------------------------------------------------------------------------- 1 | .productCard { 2 | min-width: 12rem; 3 | max-width: 12rem; 4 | margin: 1rem; 5 | &__img { 6 | min-width: 10rem; 7 | max-width: 10rem; 8 | min-height: 10rem; 9 | max-height: 10rem; 10 | object-fit: contain; 11 | } 12 | &__body { 13 | 14 | &__title { 15 | font-weight: 600; 16 | font-size: smaller; 17 | height: 3rem; 18 | 19 | } 20 | 21 | &__text span:first-child { 22 | font-weight: 600; 23 | font-size: 1.3rem; 24 | 25 | } 26 | 27 | &__price{ 28 | display: inline-block; 29 | transform: translateY(-.4rem); 30 | font-size: .8rem; 31 | padding-left: .1rem; 32 | } 33 | &__button { 34 | 35 | background-color: $button-color; 36 | border: 1px solid $button-border; 37 | color: $primary-color-light; 38 | padding: .1rem .5rem; 39 | margin: .5rem auto; 40 | border-radius: .2rem; 41 | width: 100%; 42 | } 43 | &__star { 44 | fill: $star-color; 45 | width: 1.2rem; 46 | & + span { 47 | margin-left: .5rem; 48 | } 49 | } 50 | &__count { 51 | padding-left: .5rem; 52 | } 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /src/components/carousel/Carousel.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Slider from "react-slick"; 3 | import "slick-carousel/slick/slick.css"; 4 | import "slick-carousel/slick/slick-theme.css"; 5 | import { Card, Button } from "react-bootstrap"; 6 | import "./Carousel.scss"; 7 | import RouterLink from "./../link/RouterLink"; 8 | 9 | const SharedCarousel = ({ title, imgs, alt, linkInfo,id }) => { 10 | 11 | let slidesToShow 12 | const width = 13 | window.innerWidth || 14 | document.documentElement.clientWidth || 15 | document.body.clientWidth; 16 | if (width > 768) { 17 | slidesToShow = 5 18 | } else if(width ===768) { 19 | slidesToShow = 4 20 | } else { 21 | slidesToShow =1 22 | } 23 | 24 | const settings = { 25 | infinite: true, 26 | speed: 500, 27 | slidesToShow, 28 | slidesToScroll: 1, 29 | }; 30 | 31 | 32 | return ( 33 | 34 | 35 |
36 | {title} 37 | 38 | 41 | 42 |
43 | 44 | {imgs.map((el, index) => ( 45 |
46 | 47 | {alt} 52 | 53 |
54 | ))} 55 |
56 |
57 |
58 | ); 59 | }; 60 | 61 | export default SharedCarousel; 62 | -------------------------------------------------------------------------------- /src/components/carousel/Carousel.scss: -------------------------------------------------------------------------------- 1 | .carousel { 2 | width: 100%; 3 | margin: 1rem auto; 4 | &__item { 5 | display: flex !important; 6 | justify-content: space-around; 7 | flex-wrap: nowrap; 8 | &__img { 9 | margin: 0 1rem; 10 | height: 200px; 11 | } 12 | } 13 | 14 | &__title { 15 | display: flex; 16 | align-items: center; 17 | &__link { 18 | transform: translateY(-0.3rem); 19 | } 20 | } 21 | } 22 | 23 | .slick-slide img { 24 | display: block; 25 | margin: auto; 26 | } 27 | 28 | .slick-arrow { 29 | background-color: #eaeded !important; 30 | width: 2rem !important; 31 | height: 5rem !important; 32 | border-radius: 2px; 33 | box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.2); 34 | opacity: 0.8; 35 | } 36 | 37 | .slick-arrow:hover { 38 | background: #f1c964 !important; 39 | transition: background 0.6s; 40 | } 41 | .slick-prev { 42 | z-index: 100; 43 | left: 20px !important; 44 | } 45 | .slick-next { 46 | z-index: 100; 47 | right: 20px !important; 48 | } 49 | -------------------------------------------------------------------------------- /src/components/footer/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Footer = () => { 4 | return ( 5 |
6 |
7 |
8 |

Get to Know Us

9 |

Careers

10 |

Blog

11 |

About Amazon

12 |

Investor Relations

13 |

Amazon Devices

14 |

Amazon Tours

15 |
16 |
17 |

Make Money with Us

18 |

Sell products on Amazon

19 |

Become an Affiliate

20 |

Advertise Your Products

21 |

Self-publish with US

22 |

Host an Amazon Hub

23 |
24 |
25 |

Amazon Payment Products

26 |

Amazon Business Card

27 |

Shop with Points

28 |

Reload Your Balance

29 |

Amazon Currency Converter

30 |
31 | {/*
32 |

Let Us Help You

33 |

Amazon and Covid-19

34 |

Your Account

35 |

Your Orders

36 |

Shipping Rates & Policies

37 |

Returns & Rplacement

38 |

Manage Your Content and Devices

39 |

Amazon Assistant

40 |

Help

41 |
*/} 42 |
43 |
44 |
45 | Conditions of Use 46 | Privacy Notice 47 | Interest-Based Ads 48 | © 2020, Amazon Clone By Aaron. 49 |
50 |
51 |
52 | ); 53 | }; 54 | 55 | export default Footer; 56 | -------------------------------------------------------------------------------- /src/components/footer/Footer.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | background-color: $primary-color-light; 3 | &__container { 4 | max-width: 1500px; 5 | color: $text-color; 6 | display: flex; 7 | justify-content: space-around; 8 | margin: 0 auto; 9 | padding: 2rem; 10 | gap: 1rem; 11 | } 12 | 13 | &__title { 14 | font-weight: 800; 15 | & ~ p { 16 | font-size: .9rem; 17 | } 18 | } 19 | 20 | &__copyright { 21 | color: $text-color; 22 | background-color: $primary-color-light; 23 | padding: 2rem; 24 | position: relative; 25 | &__info { 26 | transform: translate(-50%, -50%); 27 | margin: 0; 28 | position: absolute; 29 | top: 50%; 30 | left: 50%; 31 | & span { 32 | padding-right: 1rem; 33 | font-size: small; 34 | &:last-child { 35 | color: $text-color-grey; 36 | } 37 | } 38 | } 39 | 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/header/header.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { selectUser } from "../../redux/userSlice"; 3 | import { useSelector } from "react-redux"; 4 | import RouterLink from "../link/RouterLink"; 5 | import { ReactComponent as Menu } from "../../assets/menu.svg"; 6 | import { ReactComponent as SearchIcon } from "../../assets/search-icon.svg"; 7 | import { ReactComponent as Cart } from "../../assets/cart.svg"; 8 | import { ReactComponent as Place } from "../../assets/place.svg"; 9 | import logo from "../../assets/header-logo.png"; 10 | import { InputGroup, FormControl, Form } from "react-bootstrap"; 11 | import { auth } from "../../firebase.utils"; 12 | import { selectCartItemsCount } from "../../redux/cartSelector"; 13 | import { useNavigate } from "react-router-dom"; 14 | import { Drawer,Divider} from "antd"; 15 | 16 | 17 | 18 | const Header = () => { 19 | const [showMenu, setShowMenu] = useState(false); 20 | const user = useSelector(selectUser); 21 | const cartCount = useSelector((state) => selectCartItemsCount(state)); 22 | 23 | 24 | const navigate = useNavigate(); 25 | 26 | 27 | const signOut = () => { 28 | if (user) auth.signOut(); 29 | }; 30 | 31 | const handleSearch = (e) => { 32 | e.preventDefault(); 33 | navigate("/shop"); 34 | }; 35 | return ( 36 | <> 37 | setShowMenu(false)} 43 | visible={showMenu} 44 | footer={ 45 |
46 |

Help & Settings

47 |

Account

48 |

Customer Service

49 |

Sign Out

50 |
51 | } 52 | > 53 | 54 | 55 | 56 |
57 |

Treading

58 |

Best Sellers

59 |

New Releases

60 |

Movers and Shakers

61 | 62 | 63 |

Digital Content And Devices

64 |

Amazon Fire TV

65 |

Amazon Prime Video

66 |

Amazon Music

67 | 68 | 69 | 70 |

Shop By Department

71 |

Books

72 |

Music, Movies & Games

73 |

Computers

74 |

Electronics

75 |

Home

76 |

Baby

77 | 78 |
79 |
80 |
81 | 82 |
83 |
84 | setShowMenu(true)} 86 | className="header__menu__icon" 87 | /> 88 |
89 | 90 |
91 | logo 92 |
93 |
94 |
95 |
96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 113 |
114 |
115 | 116 |
117 | 118 |
119 | Hello {user ? user.name : "Guest"} 120 | {user ? "Sign Out" : "Sign In"} 121 |
122 |
123 | 124 |
125 | returns 126 | 127 | 128 | &orders 129 |
130 |
131 | 132 |
133 | {cartCount} 134 | 135 |
136 |
137 |
138 |
139 | 140 |
141 |
142 | 143 |
144 | Deliver to 145 | New Zealand 146 |
147 |
148 |
149 | Today's Deals 150 | Customer Service 151 | 152 | 153 | Gift Cards 154 | Registry 155 | Sell 156 |
157 |
158 | Amazon's response to COVID-19 159 |
160 |
161 |
162 | 163 | ); 164 | }; 165 | 166 | export default Header; 167 | -------------------------------------------------------------------------------- /src/components/header/header.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | display: flex; 3 | padding: 0.5rem 1rem; 4 | background-color: $primary-color-main; 5 | align-items: center; 6 | &__menu { 7 | height: 3rem; 8 | &__icon { 9 | width: 2.5rem; 10 | height: 3rem; 11 | fill: $text-color; 12 | border: 0.1rem solid grey; 13 | border-radius: 0.2rem; 14 | transition: border 0.2s ease-in-out; 15 | &:hover { 16 | border: 0.1rem solid $text-color; 17 | } 18 | } 19 | } 20 | 21 | &__logo > img { 22 | width: 6rem; 23 | @include breakpoint(l) { 24 | margin: 0.5rem 2rem 0 0.5rem; 25 | } 26 | margin: 0 0.5rem; 27 | } 28 | 29 | &__search { 30 | display: none; 31 | flex-grow: 1; 32 | background-color: $secondary-color-main; 33 | border-radius: 0.2rem; 34 | @include breakpoint(m) { 35 | display: inline; 36 | } 37 | 38 | & button { 39 | display: none; 40 | } 41 | 42 | &__category { 43 | background-color: $background-color !important; 44 | width: 6rem !important; 45 | } 46 | 47 | &__icon { 48 | width: 2.3rem; 49 | height: 2.3rem; 50 | fill: $primary-color-light; 51 | padding: 0.5rem; 52 | } 53 | } 54 | 55 | &__nav { 56 | display: flex; 57 | margin: auto 1rem; 58 | &__option { 59 | @include breakpoint(m) { 60 | display: flex; 61 | } 62 | color: $text-color; 63 | display: none; 64 | flex-direction: column; 65 | margin: 0 0.5rem; 66 | transform: translateY(0.5rem); 67 | & span:first-child { 68 | font-size: 0.7rem; 69 | } 70 | & span:last-child { 71 | font-size: 0.8rem; 72 | font-weight: 800; 73 | } 74 | } 75 | &__cart { 76 | color: $secondary-color-dark; 77 | &__icon { 78 | width: 3.2rem; 79 | fill: $text-color; 80 | position: absolute; 81 | top: 0.5rem; 82 | right: 2rem; 83 | @include breakpoint(m) { 84 | position: relative; 85 | top: 0rem; 86 | right: 0rem; 87 | } 88 | } 89 | & > span { 90 | position: absolute; 91 | top: 1.05rem; 92 | right: 3.5rem; 93 | font-weight: 500; 94 | } 95 | } 96 | } 97 | } 98 | 99 | .sub-header { 100 | @include breakpoint(l) { 101 | display: flex; 102 | } 103 | background-color: $primary-color-light; 104 | display: none; 105 | align-items: center; 106 | padding: 0.2rem 1rem; 107 | &__deliver { 108 | display: flex; 109 | &__icon { 110 | fill: white; 111 | width: 2.5rem; 112 | height: 2.5rem; 113 | } 114 | &__info { 115 | @extend .header__nav__option; 116 | display: flex; 117 | } 118 | } 119 | &__link { 120 | flex-grow: 1; 121 | padding-left: 2rem; 122 | color: $text-color; 123 | & span { 124 | padding-left: 1.5rem; 125 | } 126 | } 127 | 128 | &__notice { 129 | color: $text-color; 130 | font-weight: 600; 131 | } 132 | } 133 | .menuFooter { 134 | margin-left: 1rem; 135 | font-weight: 500; 136 | 137 | & p:first-child { 138 | font-size: 1.2rem; 139 | font-weight: bold; 140 | } 141 | } 142 | 143 | .menu__body__title { 144 | 145 | font-size: 1.2rem; 146 | font-weight: bold; 147 | & ~ p { 148 | font-weight: 500; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/components/home/BestSellers.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import SharedCarousel from "../carousel/Carousel"; 3 | import { v4 as uuid } from "uuid"; 4 | import { selectProducts } from "../../redux/productsSlice"; 5 | import { useSelector } from "react-redux"; 6 | 7 | 8 | const BestSellersSection = () => { 9 | const products = useSelector(selectProducts); 10 | const imgs = products 11 | .slice(13, 19) 12 | .map((img) => ({ id: img.id, alt: img.title, imgUrl: img.imgUrl })); 13 | const topSellers = [{ 14 | id: uuid(), 15 | title: "Most Popular Sellers", 16 | linkInfo: "Shop now", 17 | imgs, 18 | }]; 19 | 20 | return ( 21 |
22 | {topSellers?.map(({ id, ...props }) => ( 23 | 24 | ))} 25 |
26 | ); 27 | }; 28 | 29 | export default BestSellersSection; 30 | -------------------------------------------------------------------------------- /src/components/home/BestSellers.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaroncodehub/amazon-clone-react-redux/3e5c8952334f10cab16e724a6a10932976d12d61/src/components/home/BestSellers.scss -------------------------------------------------------------------------------- /src/components/home/PopularSection.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import SharedCarousel from "../carousel/Carousel"; 3 | import { v4 as uuid } from "uuid"; 4 | import { selectProducts } from "../../redux/productsSlice"; 5 | import { useSelector } from "react-redux"; 6 | 7 | 8 | const PopularSection = () => { 9 | const products = useSelector(selectProducts); 10 | const imgs = products 11 | .slice(8, 16) 12 | .map((img) => ({ id: img.id, alt: img.title, imgUrl: img.imgUrl })); 13 | const topSellers = [{ 14 | id: uuid(), 15 | title: "Amazon Top Sellers", 16 | linkInfo: "Shop now", 17 | imgs, 18 | }]; 19 | 20 | return ( 21 |
22 | {topSellers?.map(({ id, ...props }) => ( 23 | 24 | ))} 25 |
26 | ); 27 | }; 28 | 29 | export default PopularSection; 30 | -------------------------------------------------------------------------------- /src/components/home/PopularSection.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaroncodehub/amazon-clone-react-redux/3e5c8952334f10cab16e724a6a10932976d12d61/src/components/home/PopularSection.scss -------------------------------------------------------------------------------- /src/components/home/RecommendSection.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import SharedCard from "../card/Card"; 3 | import { v4 as uuid } from 'uuid'; 4 | 5 | const recommend = [ 6 | {id:uuid(), 7 | title: "Find unique gifts", 8 | imgUrl: 9 | "https://images-na.ssl-images-amazon.com/images/G/01/US-hq/2019/img/Social_Shopping/B00YBC17FK_260._CB442485249_SY260_.jpg", 10 | linkInfo: "See more gift ideas", 11 | }, 12 | {id:uuid(), 13 | title: "Inexpensive finds", 14 | imgUrl: 15 | "https://images-na.ssl-images-amazon.com/images/G/01/xba/BTF_US_All_Sep2019_1x._CB440956261_SY260_.jpg", 16 | linkInfo: "See more", 17 | }, 18 | {id:uuid(), 19 | title: "New gift card designs", 20 | imgUrl: 21 | "https://images-na.ssl-images-amazon.com/images/G/01/gift-certificates/consumer/2019/NewGCs/New/NewGCs_catcard_260x260_amazon_egc_1._CB438911828_SY260_.jpg", 22 | linkInfo: "Shop the collection", 23 | }, 24 | {id:uuid(), 25 | title: "Renewed electronics", 26 | imgUrl: 27 | "https://images-na.ssl-images-amazon.com/images/G/01/US-hq/2019/img/Certified_Refurbished/XCM_Manual_1170890_desktop_tiles_260x260_Certified_Refurbished_XCM_Manual_1170890_us_certified_refurbished_desktop_tiles_260x260_2_1557752691_jpg._CB464246644_SY260_.jpg", 28 | linkInfo: "See more from Renewed", 29 | }, 30 | ]; 31 | 32 | const RecommendSection = () => { 33 | return ( 34 |
35 | {recommend.map(({ id, ...props }) => ( 36 | 37 | ))} 38 |
39 | ); 40 | }; 41 | 42 | export default RecommendSection; 43 | -------------------------------------------------------------------------------- /src/components/home/RecommendSection.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaroncodehub/amazon-clone-react-redux/3e5c8952334f10cab16e724a6a10932976d12d61/src/components/home/RecommendSection.scss -------------------------------------------------------------------------------- /src/components/home/RelatedSection.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import SharedCarousel from "../carousel/Carousel"; 3 | import { v4 as uuid } from "uuid"; 4 | import { selectProducts } from "../../redux/productsSlice"; 5 | import { useSelector } from "react-redux"; 6 | 7 | 8 | const RelatedSection = () => { 9 | const products = useSelector(selectProducts); 10 | const imgs = products 11 | .slice(1, 7) 12 | .map((img) => ({ id: img.id, alt: img.title, imgUrl: img.imgUrl })); 13 | const topSellers = [{ 14 | id: uuid(), 15 | title: "Related to Items You've Viewed", 16 | linkInfo: "See more", 17 | imgs, 18 | }]; 19 | 20 | return ( 21 |
22 | {topSellers?.map(({ id, ...props }) => ( 23 | 24 | ))} 25 |
26 | ); 27 | }; 28 | 29 | export default RelatedSection; 30 | -------------------------------------------------------------------------------- /src/components/home/ShopBySection.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Card, Button } from "react-bootstrap"; 3 | import { selectUser } from "../../redux/userSlice.js"; 4 | import { useSelector } from "react-redux"; 5 | import { auth } from "../../firebase.utils"; 6 | import { useNavigate } from "react-router-dom"; 7 | import RouterLink from "./../link/RouterLink"; 8 | 9 | const ShopBySection = () => { 10 | const user = useSelector(selectUser); 11 | const navigate = useNavigate(); 12 | 13 | const signOut = (e) => { 14 | e.preventDefault(); 15 | if (user) auth.signOut(); 16 | }; 17 | const signIn = (e) => { 18 | e.preventDefault(); 19 | navigate("/auth"); 20 | }; 21 | return ( 22 |
23 | 24 | 25 | 26 | AmazonBasics 27 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | Shop by Category 39 |
40 |
41 | Computers & Accessories 45 |

Computers & Accessories

46 |
47 |
48 | Video Games 52 |

Video Games

53 |
54 |
55 |
56 |
57 | Baby 61 |

Baby

62 |
63 |
64 | Fashion 68 |

Fashion

69 |
70 |
71 | 72 |
73 |
74 |
75 | 76 | 77 | 78 | 79 | Get US $10 off 80 | 81 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | {user ? ( 92 |
93 | Hi, {user?.name} 94 | 101 |
102 | ) : ( 103 |
104 | Sign in for the best experience 105 | 112 |
113 | )} 114 |
115 |
116 |
117 | ); 118 | }; 119 | 120 | export default ShopBySection; 121 | -------------------------------------------------------------------------------- /src/components/home/ShopBySection.scss: -------------------------------------------------------------------------------- 1 | .shop-by-section { 2 | display: flex; 3 | justify-content: space-between; 4 | flex-wrap: wrap; 5 | gap: 1rem; 6 | 7 | 8 | // &:after { 9 | // content: ""; 10 | // flex: auto; 11 | // } 12 | 13 | &__card { 14 | 15 | height: 350px !important; 16 | overflow: hidden; 17 | } 18 | &__user { 19 | display: none !important; 20 | min-width: 320px !important; 21 | height: 350px !important; 22 | &__button { 23 | background-color: $button-color !important; 24 | border: 1px solid $button-border !important; 25 | color: $primary-color-light !important; 26 | } 27 | 28 | @include breakpoint(xxl) { 29 | display: inline !important; 30 | } 31 | } 32 | 33 | &__category { 34 | display: flex; 35 | justify-content: space-around; 36 | 37 | & p { 38 | font-size: smaller; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/link/RouterLink.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | const RouterLink = ({ children, ...props }) => { 5 | return {children}; 6 | }; 7 | 8 | export default RouterLink; 9 | -------------------------------------------------------------------------------- /src/components/link/RouterLink.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaroncodehub/amazon-clone-react-redux/3e5c8952334f10cab16e724a6a10932976d12d61/src/components/link/RouterLink.scss -------------------------------------------------------------------------------- /src/components/order/Order.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import moment from "moment"; 3 | import CheckoutCard from "./../card/CheckoutCard"; 4 | import CurrencyFormat from "react-currency-format"; 5 | 6 | const Order = ({ order }) => { 7 | 8 | return ( 9 |
10 |

Order

11 |

{moment.unix(order.data.created).format("MMMM Do YYYY,h:mma")}

12 |

13 | Order ID: {order.id} 14 |

15 | {order.data.cartItems && 16 | order.data.cartItems.map(({ productId, ...props }) => ( 17 |
18 | 19 |
20 |
21 | ))} 22 | ( 29 |
30 |

Order Total : {value}

31 |
32 | )} 33 | /> 34 |
35 | ); 36 | }; 37 | 38 | export default Order; 39 | -------------------------------------------------------------------------------- /src/components/order/Order.scss: -------------------------------------------------------------------------------- 1 | .order { 2 | @include breakpoint(m){ 3 | 4 | padding: 40px; 5 | margin: 20px 0; 6 | border: 1px solid lightgray; 7 | margin: 5px 0; 8 | padding: 10px; 9 | } 10 | margin-left: -3rem; 11 | position: relative; 12 | background-color: white; 13 | 14 | &__id { 15 | @include breakpoint(l){ 16 | position: absolute; 17 | right: 20px; 18 | top: 40px; 19 | } 20 | } 21 | &__total { 22 | font-weight: 500; 23 | text-align: right; 24 | } 25 | } -------------------------------------------------------------------------------- /src/components/shop/ShopPage.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Form } from "react-bootstrap"; 3 | import { ReactComponent as Star } from "../../assets/star.svg"; 4 | import { ReactComponent as StarBorder } from "../../assets/star_border.svg"; 5 | import ProductCard from "../card/ProductCard"; 6 | 7 | const ShopPage = ({ products, departments }) => { 8 | // Merge the products from different department 9 | // const allProducts = shopData 10 | // .map((el) => el.products) 11 | // .reduce((all, item) => [...all, ...item]); 12 | 13 | // remove duplated brand and seller 14 | const brands = products.map((el) => el.brand); 15 | const featuredBrands = [...new Set(brands)]; 16 | 17 | const sellers = products.map((el) => el.seller); 18 | const featuredSellers = [...new Set(sellers)]; 19 | 20 | return ( 21 |
22 |
23 |
24 |
25 |
26 | Department 27 |
28 |
29 | {departments && 30 | departments.map(({departmentName, id}) => ( 31 |
32 | 36 | 37 | 38 |
39 | ))} 40 |
41 |
42 | 43 |
44 |
45 | From Our Brands 46 |
47 |
48 |
49 | 53 | 54 | 55 |
56 |
57 |
58 |
59 |
60 | Featured Brands 61 |
62 |
63 | {featuredBrands && 64 | featuredBrands.map((brand, id) => ( 65 |
66 | 70 | 71 | 72 |
73 | ))} 74 |
75 |
76 |
77 |
78 | Avg. Customer Review 79 |
80 |
81 | 82 | 83 | 84 | 85 | 86 | & Up 87 |
88 |
89 | 90 | 91 | 92 | 93 | 94 | & Up 95 |
96 |
97 | 98 | 99 | 100 | 101 | 102 | & Up 103 |
104 |
105 | 106 | 107 | 108 | 109 | 110 | & Up 111 |
112 |
113 |
114 |
115 | Price 116 |
117 |
118 | under $25
119 | $25 to $50
120 | $50 to $100
121 | $100 to $200
122 | $200 and above
123 |
124 |
125 | 126 |
127 |
128 | Seler 129 |
130 |
131 | {featuredSellers && 132 | featuredSellers.map((seller, id) => ( 133 |
134 | 138 | 139 | 140 |
141 | ))} 142 |
143 |
144 | 145 |
146 |
147 | Availability 148 |
149 |
150 |
151 | 155 | 156 | 157 |
158 |
159 | 163 | 164 | 165 |
166 |
167 | 171 | 172 | 173 |
174 |
175 |
176 |
177 |
178 |
179 | {products.map(({ id, ...props }) => ( 180 | 181 | ))} 182 |
183 |
184 | ); 185 | }; 186 | 187 | export default ShopPage; 188 | -------------------------------------------------------------------------------- /src/components/shop/ShopPage.scss: -------------------------------------------------------------------------------- 1 | .shopPage { 2 | padding: 0.5rem 1rem; 3 | display: flex; 4 | justify-content: center; 5 | font-size: small; 6 | font-weight: 600; 7 | position: relative; 8 | background-color: $background-color-light; 9 | &__sideBar { 10 | display: none; 11 | @include breakpoint(m){ 12 | display: inline-block; 13 | &::after { 14 | content: ""; 15 | width: 0.1rem; 16 | height: 96%; 17 | background: $line-color; 18 | left: 16rem; 19 | top: 0.5rem; 20 | position: absolute; 21 | } 22 | } 23 | &__refinedBy__title { 24 | font-weight: 800; 25 | padding: 0.5rem 0; 26 | // make sure the space is fully occupied by content 27 | width: 16rem; 28 | } 29 | 30 | &__refinedBy__content { 31 | & a { 32 | display: block; 33 | } 34 | &__star { 35 | fill: $star-color; 36 | width: 1.3rem; 37 | } 38 | 39 | &--form { 40 | margin: 0 !important; 41 | } 42 | } 43 | } 44 | 45 | &__list { 46 | @include breakpoint(m){ 47 | display: flex; 48 | flex-wrap: wrap; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/firebase.utils.js: -------------------------------------------------------------------------------- 1 | import firebase from 'firebase/app'; 2 | import 'firebase/firestore'; 3 | import 'firebase/auth'; 4 | 5 | 6 | 7 | const firebaseConfig = { 8 | apiKey: "AIzaSyAmoYVi2rCFrFeJh4Vr9h1MMlOYgPt9VZI", 9 | authDomain: "github-c5c88.firebaseapp.com", 10 | databaseURL: "https://github-c5c88.firebaseio.com", 11 | projectId: "github-c5c88", 12 | storageBucket: "github-c5c88.appspot.com", 13 | messagingSenderId: "389956365199", 14 | appId: "1:389956365199:web:4bf736df934794516179fa", 15 | measurementId: "G-MNFMWVVQ26", 16 | }; 17 | // Initialize Firebase 18 | firebase.initializeApp(firebaseConfig); 19 | 20 | export const db = firebase.firestore(); 21 | export const auth = firebase.auth(); 22 | 23 | export const createUserProfileDocument = async (userAuth, additionalData) => { 24 | if (!userAuth) return; 25 | 26 | const userRef = db.doc(`amazonUsers/${userAuth.user.uid}`); 27 | const { displayName, email } = userAuth.user; 28 | 29 | 30 | const snapShot = await userRef.get(); 31 | if (snapShot.exists) return; 32 | 33 | try { 34 | await userRef.set({ 35 | displayName, 36 | email, 37 | timestamp:firebase.firestore.FieldValue.serverTimestamp(), 38 | ...additionalData, 39 | }); 40 | } catch (err) { 41 | console.log("error creating user", err.message); 42 | } 43 | // return userRef; 44 | }; 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { Provider } from "react-redux"; 4 | import { PersistGate } from "redux-persist/integration/react"; 5 | import { BrowserRouter } from "react-router-dom"; 6 | import App from "./App"; 7 | import {store, persistor} from "./redux/store"; 8 | import "bootstrap/dist/css/bootstrap.min.css"; 9 | import 'antd/dist/antd.css'; 10 | import * as serviceWorker from "./serviceWorker"; 11 | 12 | ReactDOM.render( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | , 22 | document.getElementById("root") 23 | ); 24 | 25 | // If you want your app to work offline and load faster, you can change 26 | // unregister() to register() below. Note this comes with some pitfalls. 27 | // Learn more about service workers: https://bit.ly/CRA-PWA 28 | serviceWorker.unregister(); 29 | -------------------------------------------------------------------------------- /src/layouts/AuthLayout/AuthLayout.scss: -------------------------------------------------------------------------------- 1 | .auth-layout{ 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | background-color: white; 6 | height: 100vh; 7 | &__img{ 8 | width: 12rem; 9 | margin: 1rem auto -1rem auto; 10 | } 11 | } -------------------------------------------------------------------------------- /src/layouts/AuthLayout/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Outlet } from "react-router-dom"; 3 | import logo from '../../assets/auth-logo.png' 4 | 5 | 6 | const AuthLayout = () => { 7 | return ( 8 |
9 | Amazon 10 | 11 |
12 | ) 13 | } 14 | 15 | export default AuthLayout 16 | -------------------------------------------------------------------------------- /src/layouts/MainLayout/MainLayout.scss: -------------------------------------------------------------------------------- 1 | .main-layout__spainner { 2 | position: absolute; 3 | top:50%; 4 | right: 50%; 5 | } -------------------------------------------------------------------------------- /src/layouts/MainLayout/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import Spinner from "react-bootstrap/Spinner"; 3 | import { Outlet } from "react-router-dom"; 4 | import Header from "../../components/header/header"; 5 | import Footer from "../../components/footer/Footer"; 6 | import { useDispatch, useSelector } from "react-redux"; 7 | import { 8 | fetchProducts, 9 | fetchDepartment, 10 | selectProducts, 11 | } from "../../redux/productsSlice"; 12 | 13 | const MainLayout = () => { 14 | const dispatch = useDispatch(); 15 | const products = useSelector(selectProducts); 16 | useEffect(() => { 17 | dispatch(fetchDepartment()); 18 | dispatch(fetchProducts()); 19 | }, [dispatch]); 20 | return ( 21 |
22 |
23 | {products ? ( 24 |
25 | 26 |
27 |
28 | ) : ( 29 | 30 | Loading... 31 | 32 | )} 33 |
34 | ); 35 | }; 36 | 37 | export default MainLayout; 38 | -------------------------------------------------------------------------------- /src/mockup.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { db } from "./firebase.utils"; 3 | 4 | // use each doc.id of firestore as its unique ID after fetching data from firebase 5 | 6 | const MOCKUP_DATA = [ 7 | { 8 | departmentName: "Computers and Accessories", 9 | routeName: "computers-and-accessories", 10 | products: [ 11 | { 12 | title: "New Apple iPad Pro (12.9-inch, Wi-Fi, 256GB) - Space Gray", 13 | price: 760.35, 14 | seller: "Amazon", 15 | brand: "Apple", 16 | rate: 5, 17 | count: 100, 18 | imgUrl: 19 | "https://m.media-amazon.com/images/I/81SGb5l+lZL._AC_UY218_.jpg", 20 | active: true, 21 | upcoming: false, 22 | missed: false, 23 | featured: true, 24 | gallery: [ 25 | { 26 | original: 27 | "https://images-na.ssl-images-amazon.com/images/I/81SGb5l%2BlZL._AC_SL1500_.jpg", 28 | thumbnail: 29 | "https://images-na.ssl-images-amazon.com/images/I/41PhQzleq-L._AC_SR38,50_AA50_.jpg", 30 | }, 31 | { 32 | original: 33 | "https://images-na.ssl-images-amazon.com/images/I/91zyHoys2jL._AC_SL1500_.jpg", 34 | thumbnail: 35 | "https://images-na.ssl-images-amazon.com/images/I/41Amb7qeEWL._AC_SR38,50_AA50_.jpg", 36 | }, 37 | { 38 | original: 39 | "https://images-na.ssl-images-amazon.com/images/I/71oxyKvOTbL._AC_SL1500_.jpg", 40 | thumbnail: 41 | "https://images-na.ssl-images-amazon.com/images/I/41UqtjhGdLL._AC_SR38,50_AA50_.jpg", 42 | }, 43 | { 44 | original: 45 | "https://images-na.ssl-images-amazon.com/images/I/81gyPqvQiDL._AC_SL1500_.jpg", 46 | thumbnail: 47 | "https://images-na.ssl-images-amazon.com/images/I/41Ybicv3XuL._AC_SR38,50_AA50_.jpg", 48 | }, 49 | ], 50 | description: 51 | " 12.9-inch edge-to-edge Liquid Retina display with ProMotion, True Tone, and P3 wide color; A12Z Bionic chip with Neural Engine;12MP Wide camera, 10MP Ultra Wide camera, and LiDAR Scanner;7MP TrueDepth front camera;Face ID for secure authentication and Apple Pay;Four speaker audio and five studio- quality microphones; 802.11ax Wi- Fi 6;Up to 10 hours of battery life;USB - C connector for charging and accessories; Support for Magic Keyboard, Smart Keyboard Folio, and Apple Pencil", 52 | }, 53 | { 54 | title: 55 | "Apple MacBook Air (13-inch Retina Display, 8GB RAM, 256GB SSD Storage)", 56 | price: 839.99, 57 | seller: "Amazon", 58 | brand: "Apple", 59 | rate: 4, 60 | count: 280, 61 | imgUrl: 62 | "https://m.media-amazon.com/images/I/71thf1SYnGL._AC_UY218_.jpg", 63 | active: false, 64 | upcoming: true, 65 | missed: false, 66 | featured: true, 67 | gallery: [ 68 | { 69 | original: 70 | "https://images-na.ssl-images-amazon.com/images/I/71thf1SYnGL._AC_SL1500_.jpg", 71 | thumbnail: 72 | "https://images-na.ssl-images-amazon.com/images/I/41Kp8-ILkYL._AC_SR38,50_AA50_.jpg", 73 | }, 74 | { 75 | original: 76 | "https://images-na.ssl-images-amazon.com/images/I/81e1YAr8YlL._AC_SL1500_.jpg", 77 | thumbnail: 78 | "https://images-na.ssl-images-amazon.com/images/I/41mb7IKbevL._AC_SR38,50_AA50_.jpg", 79 | }, 80 | { 81 | original: 82 | "https://images-na.ssl-images-amazon.com/images/I/71p1u5qhgmL._AC_SL1500_.jpg", 83 | thumbnail: 84 | "https://images-na.ssl-images-amazon.com/images/I/31VGv0DOdbL._AC_SR38,50_AA50_.jpg", 85 | }, 86 | ], 87 | description: 88 | " Stunning 13.3-inch Retina display with True Tone technology; Backlit Magic Keyboard and Touch ID; Tenth-generation Intel Core i3 processor; Intel Iris Plus Graphics; Fast SSD storage; 8GB of memory; Stereo speakers with wider stereo sound; Two Thunderbolt 3 (USB-C) ports; Up to 11 hours of battery life; Force Touch trackpad", 89 | }, 90 | { 91 | title: 92 | "Apple MacBook Pro with Intel Processor (13-inch, 8GB RAM, 256GB SSD Storage) - Space Gray", 93 | price: 1109.99, 94 | seller: "Apple", 95 | brand: "Apple", 96 | rate: 4.5, 97 | count: 30, 98 | imgUrl: 99 | "https://m.media-amazon.com/images/I/71HI3U9ZwZL._AC_UY218_.jpg", 100 | active: true, 101 | upcoming: false, 102 | missed: false, 103 | featured: true, 104 | gallery: [ 105 | { 106 | original: 107 | "https://images-na.ssl-images-amazon.com/images/I/71HI3U9ZwZL._AC_SL1500_.jpg", 108 | thumbnail: 109 | "https://images-na.ssl-images-amazon.com/images/I/41J5KJVM3FL._AC_SR38,50_AA50_.jpg", 110 | }, 111 | { 112 | original: 113 | "https://images-na.ssl-images-amazon.com/images/I/71Wl6Bv6o0L._AC_SL1500_.jpg", 114 | thumbnail: 115 | "https://images-na.ssl-images-amazon.com/images/I/31os7oouNEL._AC_SR38,50_AA50_.jpg", 116 | }, 117 | { 118 | original: 119 | "https://images-na.ssl-images-amazon.com/images/I/71LY-aeAEnL._AC_SL1500_.jpg", 120 | thumbnail: 121 | "https://images-na.ssl-images-amazon.com/images/I/31Xk3NpUCfL._AC_SR38,50_AA50_.jpg", 122 | }, 123 | ], 124 | description: 125 | "Eighth-generation quad-core Intel Core i5 processor; Brilliant Retina display with True Tone technology; Backlit Magic Keyboard; Touch Bar and Touch ID; Intel Iris Plus Graphics 645; Ultrafast SSD; Two Thunderbolt 3 (USB-C) ports; Up to 10 hours of battery life; 802.11ac Wi-Fi; Force Touch trackpad", 126 | }, 127 | { 128 | title: 129 | "Fire TV Stick 4K streaming device with Alexa Voice Remote | Dolby Vision | 2018 release", 130 | price: 29.99, 131 | seller: "Amazon", 132 | brand: "Amazon", 133 | rate: 3, 134 | count: 602, 135 | imgUrl: 136 | "https://m.media-amazon.com/images/I/51CgKGfMelL._AC_UY218_.jpg", 137 | active: false, 138 | upcoming: true, 139 | missed: false, 140 | featured: true, 141 | gallery: [ 142 | { 143 | original: 144 | "https://images-na.ssl-images-amazon.com/images/I/61b-Vw7i5LL._AC_SL1000_.jpg", 145 | thumbnail: 146 | "https://images-na.ssl-images-amazon.com/images/I/51-LstfxkML._AC_US40_AA50_.jpg", 147 | }, 148 | { 149 | original: 150 | "https://images-na.ssl-images-amazon.com/images/I/51ZdmnHKukL._AC_SL1000_.jpg", 151 | thumbnail: 152 | "https://images-na.ssl-images-amazon.com/images/I/41SR4yCI7aL._AC_US40_AA50_.jpg", 153 | }, 154 | { 155 | original: 156 | "https://images-na.ssl-images-amazon.com/images/I/51FvcXWGgHL._AC_SL1000_.jpg", 157 | thumbnail: 158 | "https://images-na.ssl-images-amazon.com/images/I/417gof%2BW2cL._AC_US40_AA50_.jpg", 159 | }, 160 | ], 161 | description: 162 | "A Certified Refurbished Fire TV Stick with all-new Alexa Voice Remote is refurbished, tested, and certified to look and work like new.; Fire TV Stick, the #1 best-selling streaming media player, with Alexa Voice Remote (2nd Gen). Use the dedicated power, volume, and mute buttons to control your TV, soundbar, and receiver.; Launch and control content with the Alexa Voice Remote. Watch favorites from Netflix, YouTube, Prime Video, STARZ, SHOWTIME, or CBS All Access, plus stream for free with Pluto TV, IMDb TV, and others.; Fire TV Stick devices have more storage for apps and games than any other streaming media stick.; Experience tens of thousands of channels, apps, and Alexa skills, plus browse millions of websites like Facebook and Reddit using Firefox or Silk.; Alexa on Fire TV provides the most comprehensive voice experience of any streaming media player—view live camera feeds, check the weather, dim the lights, and stream music.; Amazon Prime members get unlimited access to thousands of movies and TV episodes, plus ad-free listening to millions of songs with Prime Music.", 163 | }, 164 | { 165 | title: 166 | "HP 15-dy1036nr 10th Gen Intel Core i5-1035G1, 15-Inch FHD Laptop, Natural Silver", 167 | price: 889.99, 168 | seller: "Amazon", 169 | brand: "HP", 170 | rate: 3.5, 171 | count: 69, 172 | imgUrl: 173 | "https://m.media-amazon.com/images/I/71WSt8D7KFL._AC_UY218_.jpg", 174 | active: false, 175 | upcoming: true, 176 | missed: false, 177 | featured: true, 178 | gallery: [ 179 | { 180 | original: 181 | "https://images-na.ssl-images-amazon.com/images/I/71WSt8D7KFL._AC_SX569_.jpg", 182 | thumbnail: 183 | "https://images-na.ssl-images-amazon.com/images/I/41RZjMPGN0L._AC_US40_.jpg", 184 | }, 185 | { 186 | original: 187 | "https://images-na.ssl-images-amazon.com/images/I/81rSbRZ1l0L._AC_SX569_.jpg", 188 | thumbnail: 189 | "https://images-na.ssl-images-amazon.com/images/I/51GMRgAxZgL._AC_US40_.jpg", 190 | }, 191 | { 192 | original: 193 | "https://images-na.ssl-images-amazon.com/images/I/71hStpySRmL._AC_SX569_.jpg", 194 | thumbnail: 195 | "https://images-na.ssl-images-amazon.com/images/I/41HyPHy4hML._AC_US40_.jpg", 196 | }, 197 | ], 198 | description: 199 | "10th Gen Intel Core i7-10510U (1.8 GHz base frequency, up to 4.9 GHz base with Intel Turbo Boost Technology, 8 MB cache, 4 cores) ; diagonal HD SVA BrightView micro-edge WLED-backlit (1366 x 768) Display; Intel UHD Graphics; 8GB DDR4-2666MHz SDRAM Memory for full-power multitasking; 128GB SSD; Ideal for Home, Student, Professionals, Small Business, School Education, and Commercial Enterprise, Online Class, Google Classroom, Remote Learning, Zoom Ready.; 802.11b/g/n (1x1) Wi-Fi and Bluetooth 4.2 combo; 1 USB 3.1 Gen 1 Type-C (Data Transfer Only, 5 Gb/s signaling rate); 2 USB 3.1 Gen 1 Type-A (Data Transfer Only); 1 RJ-45; 1 AC smart pin; 1 HDMI 1.4b; 1 headphone/microphone combo; 1 multi-format SD media card reader; ▌Authorized BROAGE Bundle ▌Bundled with BROAGE MousePad, Authorized Sellers ONLY. Windows 10 Home 64-Bit, Natural silver", 200 | }, 201 | ], 202 | }, 203 | { 204 | departmentName: "Camera & Photo", 205 | routeName: "camera-photo", 206 | products: [ 207 | { 208 | title: 209 | "Panasonic LUMIX DC-FZ80GN-K 4K Bridge Hi-Zoom Point and Shoot Travel Camera, Black", 210 | price: 455.0, 211 | seller: "Amazon", 212 | brand: "Panasonic", 213 | rate: 5, 214 | count: 11, 215 | imgUrl: 216 | "https://images-fe.ssl-images-amazon.com/images/I/41jwbCD0khL._AC_AA160_.jpg", 217 | active: false, 218 | upcoming: false, 219 | missed: true, 220 | featured: true, 221 | gallery: [ 222 | { 223 | original: 224 | "https://images-na.ssl-images-amazon.com/images/I/917LEZ%2Bit3L._AC_SL1500_.jpg", 225 | thumbnail: 226 | "https://images-na.ssl-images-amazon.com/images/I/51VWc0XqprL._AC_US40_AA50_.jpg", 227 | }, 228 | { 229 | original: 230 | "https://images-na.ssl-images-amazon.com/images/I/61D1PKdrRqL._AC_.jpg", 231 | thumbnail: 232 | "https://images-na.ssl-images-amazon.com/images/I/61D1PKdrRqL._AC_US40_AA50_.jpg", 233 | }, 234 | { 235 | original: 236 | "https://images-na.ssl-images-amazon.com/images/I/6150C4%2BFrmL._AC_.jpg", 237 | thumbnail: 238 | "https://images-na.ssl-images-amazon.com/images/I/6150C4%2BFrmL._AC_US40_AA50_.jpg", 239 | }, 240 | ], 241 | description: 242 | "Superb DSLM image quality without the bulk and weight of traditional DSLRs; Never miss a photo with three unique 4K ultra HD video pause and save 4K photo modes; Fast and precise auto focusing tracks the subject; Focus mode AFS (single) / AFF (flexible) / AFC (continuous) / MF, AF mode face/eye detection / tracking / 49 area / custom Multi / 1 area / pinpoint; Class leading, ultra compact, interchangeable lens and accessory option; HDMI: microHDMI TypeD / VIERA Link,video: Auto / 4K / 1080p / 1080i / 720p / 480p, Audio: stereo; Unwire your creativity with integrated Wi Fi sharing", 243 | }, 244 | { 245 | title: 246 | "Nikon D3500 KIT + AF-P 18-55 VR + AF-P 70-300 VR Twin Lens Kit, Black", 247 | price: 898.2, 248 | seller: "Nikon", 249 | brand: "Nikon", 250 | rate: 4, 251 | count: 27, 252 | imgUrl: 253 | "https://images-fe.ssl-images-amazon.com/images/I/413eVsQoYeL._AC_AA160_.jpg", 254 | active: true, 255 | upcoming: false, 256 | missed: false, 257 | featured: true, 258 | gallery: [ 259 | { 260 | original: 261 | "https://images-na.ssl-images-amazon.com/images/I/61A4tg5IFhL._AC_SL1000_.jpg", 262 | thumbnail: 263 | "https://images-na.ssl-images-amazon.com/images/I/51RHexdJkUL._AC_US40_AA50_.jpg", 264 | }, 265 | { 266 | original: 267 | "https://images-na.ssl-images-amazon.com/images/I/61MtcYSMoLL._AC_SL1000_.jpg", 268 | thumbnail: 269 | "https://images-na.ssl-images-amazon.com/images/I/51KNi4NJLzL._AC_US40_AA50_.jpg", 270 | }, 271 | { 272 | original: 273 | "https://images-na.ssl-images-amazon.com/images/I/61a1bF0a3rL._AC_SL1000_.jpg", 274 | thumbnail: 275 | "https://images-na.ssl-images-amazon.com/images/I/51x4UIVB2gL._AC_US40_AA50_.jpg", 276 | }, 277 | ], 278 | description: 279 | "This Grace Photo Camera Bundle Comes Complete With Manufacturer Supplied Accessories(U.S. Compatible) and a 1 Year Seller Provided Warranty; Package Includes: Nikon D3500 DSLR Camera (International Version) 24.2MP DX-Format CMOS Sensor; EXPEED 4 Image Processor; No Optical Low-Pass Filter; Native ISO 100-25600; 5 fps Shooting; 3.0 921k-Dot LCD Monitor; Full HD 1080p Video Recording at 60 fps; Multi-CAM 1000 11-Point AF System; SnapBridge Bluetooth Connectivity Nikon AF-P DX NIKKOR 18-55mm f/3.5-5.6G VR Lens - F-Mount Lens/DX Format , 27-82.5mm (35mm Equivalent) , Super Integrated Coating , Pulse Stepping Motor AF System , Nikon VR Image Stabilization , Access Lens Settings in Camera Menu , Ro", 280 | }, 281 | { 282 | title: 283 | "G7X Mark II + Selphy Portable Printer + 40 Sheets of Photo Paper", 284 | price: 1064.8, 285 | seller: "Canon", 286 | brand: "Canon", 287 | rate: 4.5, 288 | count: 3, 289 | imgUrl: 290 | "https://images-fe.ssl-images-amazon.com/images/I/41KLEZXgg0L._AC_AA160_.jpg", 291 | active: true, 292 | upcoming: false, 293 | missed: false, 294 | featured: true, 295 | gallery: [ 296 | { 297 | original: 298 | "https://images-na.ssl-images-amazon.com/images/I/416b8xNfDJL._AC_.jpg", 299 | thumbnail: 300 | "https://images-na.ssl-images-amazon.com/images/I/416b8xNfDJL._AC_US40_AA50_.jpg", 301 | }, 302 | { 303 | original: 304 | "https://images-na.ssl-images-amazon.com/images/I/61Srr4ACqJL._AC_SL1500_.jpg", 305 | thumbnail: 306 | "https://images-na.ssl-images-amazon.com/images/I/416IPK%2BEVDL._AC_US40_AA50_.jpg", 307 | }, 308 | { 309 | original: 310 | "https://images-na.ssl-images-amazon.com/images/I/71ef4iykr5L._AC_SL1500_.jpg", 311 | thumbnail: 312 | "https://images-na.ssl-images-amazon.com/images/I/41vjntfdIcL._AC_US40_AA50_.jpg", 313 | }, 314 | ], 315 | description: 316 | "The advanced video capabilities of the Power Shot G7 X Mark II camera can capture moments in the quality they deserve ; Features a large 1.0 inch, 20.1 Megapixel CMOS sensor that helps capture high quality images and videos with a wide dynamic range. Autofocus system features ttl autofocus and manual focus. Operating temperature is 0 to 40 degree celsius. Note charging time varies considerably depending on the remaining battery power; An aperture value of f/1.8 at the wide angle and f/2.8 when fully zoomed to a factor of 4.2x (24 100 millimeter), this lens equipped to capture a variety of situations with precision ; High resolution, 3.0 inches LCD monitor that tilts up 180 degrees and down 45 degrees is ideal for self portraits and capturing pictures at high and low angles with ease Built in Wi Fi for on the go convenience and the ability to easily post your images to select social networking and media sites ; With the ability to shoot continuously at up to 8 fps, it can help you capture incredible images with nearly no time wasted", 317 | }, 318 | { 319 | title: "Ricoh Theta V 360 Spherical Camera", 320 | price: 599.99, 321 | seller: "Amazon", 322 | brand: "Ricoh", 323 | rate: 3.5, 324 | count: 110, 325 | imgUrl: 326 | "https://images-fe.ssl-images-amazon.com/images/I/31jNXNVKclL._AC_AA160_.jpg", 327 | active: false, 328 | upcoming: true, 329 | missed: false, 330 | featured: true, 331 | gallery: [ 332 | { 333 | original: 334 | "https://images-na.ssl-images-amazon.com/images/I/61%2BGE4CgqrL._AC_SL1231_.jpg", 335 | thumbnail: 336 | "https://images-na.ssl-images-amazon.com/images/I/41ficUhE82L._AC_US40_AA50_.jpg", 337 | }, 338 | { 339 | original: 340 | "https://images-na.ssl-images-amazon.com/images/I/71Y3gcvXl%2BL._AC_SL1000_.jpg", 341 | thumbnail: 342 | "https://images-na.ssl-images-amazon.com/images/I/61oTk5H9apL._AC_US40_AA50_.jpg", 343 | }, 344 | { 345 | original: 346 | "https://images-na.ssl-images-amazon.com/images/I/61lUvafK6IL._AC_SL1000_.jpg", 347 | thumbnail: 348 | "https://images-na.ssl-images-amazon.com/images/I/51OWRVBrZIL._AC_US40_AA50_.jpg", 349 | }, 350 | ], 351 | description: 352 | "Theta V shoots hi-def smooth 360 Degree video at 30 fps @ 3840 x 1920 pixels or 4K. It supports the H.264 file format for video recording ; Remote release: CA-3-compatible ; Hi-res 360 Degree spherical stills & video with improvements to the image quality. New Qualcomm Snapdragon processor has completely enhanced the exposure accuracy and white balance algorithm. Object distance- Approx. 10cm - ∞ (from front of lens).Lens configuration:7 elements in 6 groups Theta has a 4-channel microphone that supports 360 Degree spatial audio recording built in Omnidirectional audio is recorded not just in the horizontal direction but also in the vertical direction. Storage temperature range - -20°C - 60°C The world’s-first Remote Playback capable fully spherical camera allows users to wirelessly playback 360 Degree images and videos on a large-screen display Using a compatible wireless display adapter ; Wireless LAN transfer speed (WLAN, MAX): 2.4GHz, 3mm: 20Mbps; 2.4GHz, 10mm: 10Mbps; 5GHz, 3mm: 50Mbps; 5GHz, 10mm: 10Mbps. The camera can always be connected to a smartphone using Bluetooth low energy (BLE) This function provides improvements in usability and power consumption", 353 | }, 354 | { 355 | title: 356 | "Olympus OM-D E-M5 Mark III Camera - Kit with 14-150mm Lens (Silver)", 357 | price: 1895.0, 358 | seller: "Olympus", 359 | brand: "Olympus", 360 | rate: 3, 361 | count: 7, 362 | imgUrl: 363 | "https://m.media-amazon.com/images/I/81mSUtJFdZL._AC_UL320_.jpg", 364 | active: true, 365 | upcoming: false, 366 | missed: false, 367 | featured: true, 368 | gallery: [ 369 | { 370 | original: 371 | "https://images-na.ssl-images-amazon.com/images/I/71jT8sUbriL._AC_SL1500_.jpg", 372 | thumbnail: 373 | "https://images-na.ssl-images-amazon.com/images/I/41j9B76ly6L._AC_US40_.jpg", 374 | }, 375 | { 376 | original: 377 | "https://images-na.ssl-images-amazon.com/images/I/81xViZ0Gm1L._AC_SL1500_.jpg", 378 | thumbnail: 379 | "https://images-na.ssl-images-amazon.com/images/I/41FoCm5aq-L._AC_US40_.jpg", 380 | }, 381 | { 382 | original: 383 | "https://images-na.ssl-images-amazon.com/images/I/71BVeMB9ZqL._AC_SL1500_.jpg", 384 | thumbnail: 385 | "https://images-na.ssl-images-amazon.com/images/I/41zrlbdP%2B4L._AC_US40_.jpg", 386 | }, 387 | ], 388 | description: 389 | "20MP live MOS sensor ; Portable, weather sealed design; 121-point all-cross-type on-chip phase detection AF; Compact, in-body 5-axis image stabilization (up to approx. 5.5 EV steps of compensation); 50MP tripod high-res shot", 390 | }, 391 | ], 392 | }, 393 | { 394 | departmentName: "Mobile Phones & Communication", 395 | routeName: "mobile-phones-communication", 396 | products: [ 397 | { 398 | title: 399 | "Wireless Headphones Bluetooth Earbuds with Charging Case Noise Cancelling 3D Stereo Headphones", 400 | price: 220.0, 401 | seller: "Apple", 402 | brand: "Apple", 403 | rate: 5, 404 | count: 110, 405 | imgUrl: 406 | "https://m.media-amazon.com/images/I/51uxlKNiGAL._AC_UY218_.jpg", 407 | active: true, 408 | upcoming: false, 409 | missed: false, 410 | featured: true, 411 | gallery: [ 412 | { 413 | original: 414 | "https://images-na.ssl-images-amazon.com/images/I/71FlmfMetKL._AC_SX522_.jpg", 415 | thumbnail: 416 | "https://images-na.ssl-images-amazon.com/images/I/51vDudmSP5L._AC_US40_.jpg", 417 | }, 418 | { 419 | original: 420 | "https://images-na.ssl-images-amazon.com/images/I/61M5T%2BhSUpL._AC_SX522_.jpg", 421 | thumbnail: 422 | "https://images-na.ssl-images-amazon.com/images/I/41C5XBgAuPL._AC_US40_.jpg", 423 | }, 424 | { 425 | original: 426 | "https://images-na.ssl-images-amazon.com/images/I/610E2mym0zL._AC_SX522_.jpg", 427 | thumbnail: 428 | "https://images-na.ssl-images-amazon.com/images/I/51Z4fUS0%2BIL._AC_US40_.jpg", 429 | }, 430 | ], 431 | description: 432 | "♫【Bluetooth upgrade 5.0 technology】:Wireless earbuds adopt professional chipset and equipped with better than ordinary 5.0 chip to ensure excellent audio performance with fast, stable and efficient transmission.Offers a truly natural, authentic sound and powerful bass performance with 14mm large size speaker driver.; ♫【Dual HD microphones】:Built-in microphones support phone calls and voice assistant with either or both earbuds. With brilliant acoustic design, earphones deliver a clean response with no distortions.CVC8.0 noise cancelling technology which can filters out external noise & enhances the sound of your voice for clear hands-free calls, allow you hear in louder environments.;; ♫【Quality Battery Life 24 Hours】:Support fast charging, charging for 15 minutes, using for two hours.The charging case has 4 charging contacts for safer charging. The earplugs can be used for 5-6 hours in just 1 hour of charging time. The 650mAh portable magnetic charging case can charge the headset 4 times and extend the playback time for up to 20 hours.; ♫【Comfortable & Lightweight Design】:Get rid of the shackles and the real liberation. After layer testing, After testing, we chose an ergonomically designed headset (65°), comfortable to wear, not easy to drop, and the headphones are very lightweight (3.5g) to ensure ear stability and comfort.", 433 | }, 434 | { 435 | title: "Sony Noise Cancelling Headphones WH1000XM3", 436 | price: 324.8, 437 | seller: "Sony", 438 | brand: "Sony", 439 | rate: 4.5, 440 | count: 507, 441 | imgUrl: 442 | "https://m.media-amazon.com/images/I/71IIenbgEcL._AC_UY218_.jpg", 443 | active: true, 444 | upcoming: false, 445 | missed: false, 446 | featured: true, 447 | gallery: [ 448 | { 449 | original: 450 | "https://images-na.ssl-images-amazon.com/images/I/71IIenbgEcL._AC_SX522_.jpg", 451 | thumbnail: 452 | "https://images-na.ssl-images-amazon.com/images/I/41xa1cPWH3L._AC_US40_.jpg", 453 | }, 454 | { 455 | original: 456 | "https://images-na.ssl-images-amazon.com/images/I/518ISPl1BZL._AC_SX522_.jpg", 457 | thumbnail: 458 | "https://images-na.ssl-images-amazon.com/images/I/31Y8K%2ByUXkL._AC_US40_.jpg", 459 | }, 460 | { 461 | original: 462 | "https://images-na.ssl-images-amazon.com/images/I/51CjGcEXyIL._AC_SX522_.jpg", 463 | thumbnail: 464 | "https://images-na.ssl-images-amazon.com/images/I/41n3w7DNInL._AC_US40_.jpg", 465 | }, 466 | ], 467 | description: 468 | "BUNDLE INCLUDES: Sony WH-1000XM3 Wireless Noise-Canceling Over-Ear Headphones (Black) with Sony SRS-XB10 Portable Wireless Bluetooth Speaker ; VIRTUAL SOUND: Smart Listening by Adaptive Sound Control / SENSE ENGINE for virtual surround sound; POWERFUL DRIVERS: Hi-Res audio LCP drivers for up to 40 kHz frequency; CONNECTIVITY: Bluetooth 4.1 LDAC connection / Connect to the Sony Headphones Connect App; SONY AUTHORIZED DEALER: Includes USA Manufacturer Guarantee", 469 | }, 470 | { 471 | title: 472 | "Nokia 5.3 Android One Smartphone (Official Australian Version 2020)", 473 | price: 364.8, 474 | seller: "Nokia", 475 | brand: "Nokia", 476 | rate: 4, 477 | count: 30, 478 | imgUrl: 479 | "https://m.media-amazon.com/images/I/71M8ocFNBgL._AC_UL320_.jpg", 480 | active: true, 481 | upcoming: false, 482 | missed: false, 483 | featured: true, 484 | gallery: [ 485 | { 486 | original: 487 | "https://images-na.ssl-images-amazon.com/images/I/71xFT1Yi11L._AC_SX425_.jpg", 488 | thumbnail: 489 | "https://images-na.ssl-images-amazon.com/images/I/61A1XGiyH1L._AC_US40_.jpg", 490 | }, 491 | { 492 | original: 493 | "https://images-na.ssl-images-amazon.com/images/I/61HxZaQlYQL._AC_SX425_.jpg", 494 | thumbnail: 495 | "https://images-na.ssl-images-amazon.com/images/I/4102YfWF8LL._AC_US40_.jpg", 496 | }, 497 | { 498 | original: 499 | "https://images-na.ssl-images-amazon.com/images/I/31CGbfSTdeL._AC_SX425_.jpg", 500 | thumbnail: 501 | "https://images-na.ssl-images-amazon.com/images/I/31CGbfSTdeL._AC_US40_.jpg", 502 | }, 503 | ], 504 | description: 505 | "Go big with the Nokia 5.3 smartphone's epic-sized 6. 55-inch HD+ display and incredible quad rear camera that captures stunning pictures even in low light, helps you select your best shot, and creates professional-looking portraits.; Capture great detail with the quad camera's 13 MP main lens, get everything in the frame with the ultra-wide, 118-degree 5 MP lens, focus in on the tiniest details with the 2 MP macro lens, and create blurred bokeh images with the 2 MP depth sensor.; Stay in the game for longer thanks to the dynamic Qualcomm Snapdragon 665 processor's silky smooth graphics and AI-assisted Adaptive Battery that saves energy for the apps you use the most and stays charged for up to 2 days.; The Nokia 5.3 and Google Assistant utilize AI-powered machine learning to learn and adapt to you with every use, and the dedicated Google Assistant button makes it easy to ask questions, update calendars, check traffic, and more.", 506 | }, 507 | { 508 | title: 509 | "Samsung Galaxy A11 Smartphone with 2 Year Manufacturer Warranty,Black", 510 | price: 233.89, 511 | seller: "Amazon", 512 | brand: "Samsung", 513 | rate: 3, 514 | count: 20, 515 | imgUrl: 516 | "https://m.media-amazon.com/images/I/61aoEfQb63L._AC_UL320_.jpg", 517 | active: true, 518 | upcoming: false, 519 | missed: false, 520 | featured: true, 521 | gallery: [ 522 | { 523 | original: 524 | "https://images-na.ssl-images-amazon.com/images/I/81qOdpVypzL._AC_SY550_.jpg", 525 | thumbnail: 526 | "https://images-na.ssl-images-amazon.com/images/I/4187bqrxiTL._AC_SR38,50_.jpg", 527 | }, 528 | { 529 | original: 530 | "https://images-na.ssl-images-amazon.com/images/I/817ADBC6U%2BL._AC_SX466_.jpg", 531 | thumbnail: 532 | "https://images-na.ssl-images-amazon.com/images/I/41xWhSJaPQL._AC_SR38,50_.jpg", 533 | }, 534 | { 535 | original: 536 | "https://images-na.ssl-images-amazon.com/images/I/712HPEtUe8L._AC_SY550_.jpg", 537 | thumbnail: 538 | "https://images-na.ssl-images-amazon.com/images/I/21z6rj-fCEL._AC_SR38,50_.jpg", 539 | }, 540 | ], 541 | description: 542 | "CHARGE UP. POWER THROUGH: You need a phone you can rely on. Thanks to the long-lasting battery(Based on average battery life under typical usage conditions. Average expected performance based on typical use. Actual battery life depends on factors such as network, features selected, frequency of calls, and voice, data, and other application usage patterns. Results may vary.) of the Galaxy A11, your phone will have enough power to do almost anything. You can spend more time scrolling, texting and sharing, and less time looking for an outlet to recharge.; TAKE YOUR ENTERTAINMENT TO THE EDGE: A 6.4” Infinity-O Display Measured diagonally, the screen size is 6.4;with accounting for the rounded corners. Actual viewable area is less due to the rounded corners and camera lens. gives you plenty of room to scroll through your feeds, see the latest news and keep up with friends and family. Colors and details look bright and bold on its HD+ screen.; THREE LENSES. ENDLESS POTENTIAL: Getting a great photo and video is now easier with this versatile camera. In addition to portraits and videos, the A11's depth lens helps you take beautiful portraits too.; STORE MORE: Whether you’re snapping, storing or downloading, this device can keep it all. If the built-in 32GB of memory enough, you can expand the base storage up to an amazing 512GB with a microSD card ", 543 | }, 544 | { 545 | title: 546 | "Apple iPhone 11 White 128GB Physical Dual Sim [AU Seller] MWN82CH/A", 547 | price: 1299.0, 548 | seller: "Apple", 549 | brand: "Apple", 550 | rate: 5, 551 | count: 10, 552 | imgUrl: 553 | "https://m.media-amazon.com/images/I/61U+Xz90BAL._AC_UL320_.jpg", 554 | active: true, 555 | upcoming: false, 556 | missed: false, 557 | featured: true, 558 | gallery: [ 559 | { 560 | original: 561 | "https://m.media-amazon.com/images/I/81j2j9x-KWL._FMwebp__.jpg", 562 | thumbnail: 563 | "https://m.media-amazon.com/images/I/41YQjtzk7UL._SS40_FMwebp__.jpg", 564 | }, 565 | { 566 | original: 567 | "https://m.media-amazon.com/images/I/71dcUoPEBsL._FMwebp__.jpg", 568 | thumbnail: 569 | "https://m.media-amazon.com/images/I/31P1p9qLXcL._SS40_FMwebp__.jpg", 570 | }, 571 | { 572 | original: 573 | "https://m.media-amazon.com/images/I/815peUj+oYL._FMwebp__.jpg", 574 | thumbnail: 575 | "https://m.media-amazon.com/images/I/41AisChzPDL._SS40_FMwebp__.jpg", 576 | }, 577 | ], 578 | description: 579 | "OFFER INCLUDES: An Apple iPhone and a wireless plan with unlimited data/talk/text; WIRELESS PLAN: Unlimited talk, text, and data with mobile hotspot, nationwide coverage, and international reach. No long-term contract required.; PROGRAM DETAILS: When you add this offer to cart, it will reflect 3 items: the iPhone, SIM kit, and carrier subscription; 6.5-inch Super Retina XDR OLED display, water and dust resistant, with Face ID; Triple-camera system with 12MP Ultra Wide camera, 12MP TrueDepth front camera with Portrait mode. Fast-charge and wireless charging capable; Note: The box label simply says “iPhone” but the phone is the iPhone 11 Pro Max", 580 | }, 581 | ], 582 | }, 583 | { 584 | departmentName: "Clothing, Shoes & Accessories", 585 | routeName: "clothing-shoes-accessories", 586 | products: [ 587 | { 588 | title: "Mens 21861 Extreme Motion Swope Cargo Short Cargo", 589 | price: 89.99, 590 | seller: "Perfect Clothing", 591 | brand: "Lee", 592 | rate: 5, 593 | count: 1038, 594 | imgUrl: 595 | "https://m.media-amazon.com/images/I/81RHhaW4SOL._AC_UL320_.jpg", 596 | active: true, 597 | upcoming: false, 598 | missed: false, 599 | featured: true, 600 | gallery: [ 601 | { 602 | original: 603 | "https://images-na.ssl-images-amazon.com/images/I/81RHhaW4SOL._AC_UY500_.jpg", 604 | thumbnail: 605 | "https://images-na.ssl-images-amazon.com/images/I/415WG5oPk9L._AC_SR38,50_.jpg", 606 | }, 607 | { 608 | original: 609 | "https://images-na.ssl-images-amazon.com/images/I/81XnXFCp8pL._AC_UY500_.jpg", 610 | thumbnail: 611 | "https://images-na.ssl-images-amazon.com/images/I/41XtmnSUy0L._AC_SR38,50_.jpg", 612 | }, 613 | { 614 | original: 615 | "https://images-na.ssl-images-amazon.com/images/I/A1nAhaaj5KL._AC_UX466_.jpg", 616 | thumbnail: 617 | "https://images-na.ssl-images-amazon.com/images/I/51gSOYk2ptL._AC_SR38,50_.jpg", 618 | }, 619 | ], 620 | description: 621 | "98% Cotton, 2% Spandex; Imported; Zipper closure; Machine Wash; Relaxed fit; hits at the knee; Featuring an elastic waistband that moves with you; Side cargo pockets and cell phone pocket", 622 | }, 623 | { 624 | title: "Authentic Originals Sueded Fleece Pullover", 625 | price: 68.8, 626 | seller: "Amazon", 627 | brand: "Champion", 628 | rate: 3.5, 629 | count: 9, 630 | imgUrl: 631 | "https://m.media-amazon.com/images/I/91Sx6JsV9CL._AC_UL320_.jpg", 632 | active: true, 633 | upcoming: false, 634 | missed: false, 635 | featured: true, 636 | gallery: [ 637 | { 638 | original: 639 | "https://images-na.ssl-images-amazon.com/images/I/91Sx6JsV9CL._AC_UX425_.jpg", 640 | thumbnail: 641 | "https://images-na.ssl-images-amazon.com/images/I/41uOGZMbBGL._AC_SR38,50_.jpg", 642 | }, 643 | { 644 | original: 645 | "https://m.media-amazon.com/images/I/A1nqkyzZIPL._AC_SX425._SX._UX._SY._UY_.jpg", 646 | thumbnail: 647 | "https://m.media-amazon.com/images/I/51YC-ctGalL._AC_SR38,50_.jpg", 648 | }, 649 | ], 650 | description: 651 | "80% Cotton, 20% Polyester; Imported; Machine Wash; Fleece is sueded outside, incredibly soft inside; Hood has contrasting jersey lining; Flat woven draw cord with turned and tacked ends; Ribbed cuffs and waistband; Forward shoulders; relaxed waistband; pouch pocket", 652 | }, 653 | { 654 | title: "Half Dome Hoodie, TNF", 655 | price: 39.8, 656 | seller: "Amazon", 657 | brand: "The North Face", 658 | rate: 4.5, 659 | count: 1020, 660 | imgUrl: 661 | "https://m.media-amazon.com/images/I/81blGg02EJL._AC_UL320_.jpg", 662 | active: true, 663 | upcoming: false, 664 | missed: false, 665 | featured: true, 666 | gallery: [ 667 | { 668 | original: 669 | "https://m.media-amazon.com/images/I/81xUYaah6pL._AC_SX385._SX._UX._SY._UY_.jpg", 670 | thumbnail: 671 | "https://m.media-amazon.com/images/I/418Pf6T0yOL._AC_SR38,50_.jpg", 672 | }, 673 | { 674 | original: 675 | "https://m.media-amazon.com/images/I/815Bd5SgD2L._AC_SX385._SX._UX._SY._UY_.jpg", 676 | thumbnail: 677 | "https://m.media-amazon.com/images/I/31CIoFe3lRL._AC_SR38,50_.jpg", 678 | }, 679 | { 680 | original: 681 | "https://m.media-amazon.com/images/I/81IPGpibsbL._AC_SX385._SX._UX._SY._UY_.jpg", 682 | thumbnail: 683 | "https://m.media-amazon.com/images/I/31gd9FQHmlL._AC_SR38,50_.jpg", 684 | }, 685 | ], 686 | description: 687 | "SIGNATURE HOODIE. This standard fit sweatshirt is made with a recycled fabric blend. With enough room for layering, this hoodie is great for getting outside and enjoying cooler weather.; HOODED COVERAGE. An adjustable drawstring hood helps keep you covered and bundled up as the temperature cools off.;; CLASSIC STAPLE. For an extra layer to keep the chill away, turn to your favorite hoodie. Rib cuffs and hem enhance durability for lasting wear.; FRONT-POCKET STORAGE. A kangaroo pocket hold small items and help keep hands warm.; THE NORTH FACE. The North Face screen print adds a signature look to your sweatshirt", 688 | }, 689 | { 690 | title: "Modern Series Extreme Motion Straight Fit Tapered Leg Jean", 691 | price: 28.09, 692 | seller: "Lee", 693 | brand: "Lee", 694 | rate: 4.5, 695 | count: 3441, 696 | imgUrl: 697 | "https://m.media-amazon.com/images/I/71ZsLtQOADL._AC_UL320_.jpg", 698 | active: true, 699 | upcoming: false, 700 | missed: false, 701 | featured: true, 702 | gallery: [ 703 | { 704 | original: 705 | "https://images-na.ssl-images-amazon.com/images/I/71ZsLtQOADL._AC_UX466_.jpg", 706 | thumbnail: 707 | "https://images-na.ssl-images-amazon.com/images/I/31CKI8sFYzL._AC_SR38,50_.jpg", 708 | }, 709 | { 710 | original: 711 | "https://images-na.ssl-images-amazon.com/images/I/71tMzRETIBL._AC_UX466_.jpg", 712 | thumbnail: 713 | "https://images-na.ssl-images-amazon.com/images/I/41cPHoa2%2BHL._AC_SR38,50_.jpg", 714 | }, 715 | { 716 | original: 717 | "https://images-na.ssl-images-amazon.com/images/I/61HW9n3VP7L._AC_UY500_.jpg", 718 | thumbnail: 719 | "https://images-na.ssl-images-amazon.com/images/I/51-xJ00LMdL._AC_SR38,50_.jpg", 720 | }, 721 | ], 722 | description: 723 | "69% Cotton, 20% Polyester, 8% Rayon, 3% Spandex; Imported; Button closure; Machine Wash; STRAIGHT FIT.With a straight fit through the hip and thigh, these jeans sit just below the waist with an Extreme Flex waistband for natural comfort.; CLASSIC 5-POCKET STYLING.A classic fit jean designed with an authentic five- pocket style.Our bestselling straight fit jean is designed with a timeless five- pocket style, perfect for most occasions.; JEANS FOR ALL. This jean is available in a midweight denim with a wide variety of washes, finishes, and fits. Add a classic denim jean to your everyday rotation or try out a new alternative wash -- there's a pair of Lee jeans to fit every style", 724 | }, 725 | { 726 | title: "Graphic Powerblend Fleece Crew", 727 | price: 35.0, 728 | seller: "Champion", 729 | brand: "Champion", 730 | rate: 4.5, 731 | count: 4848, 732 | imgUrl: 733 | "https://m.media-amazon.com/images/I/91MPi4sVKLL._AC_UL320_.jpg", 734 | active: true, 735 | upcoming: false, 736 | missed: false, 737 | featured: true, 738 | gallery: [ 739 | { 740 | original: 741 | "https://m.media-amazon.com/images/I/91MPi4sVKLL._AC_SY500._SX._UX._SY._UY_.jpg", 742 | thumbnail: 743 | "https://m.media-amazon.com/images/I/41-htzTiXoL._AC_SR38,50_.jpg", 744 | }, 745 | { 746 | original: 747 | "https://m.media-amazon.com/images/I/9158gOHEJRL._AC_SY500._SX._UX._SY._UY_.jpg", 748 | thumbnail: 749 | "https://m.media-amazon.com/images/I/41c6wQaWm+L._AC_SR38,50_.jpg", 750 | }, 751 | ], 752 | description: 753 | "50% Cotton, 50% Polyester; Imported; Pull On closure; Machine Wash; Better fit. Wider rib cuffs and hem; Soft comfortable fabric. A percentage of recycled fibers used in making the fabric", 754 | }, 755 | ], 756 | }, 757 | ]; 758 | 759 | const Mockup = () => { 760 | const mockup = () => { 761 | MOCKUP_DATA.forEach((el) => { 762 | db.doc(`amazon/${el.routeName}`) 763 | .set({ 764 | departmentName: el.departmentName, 765 | routeName: el.routeName, 766 | }) 767 | .then(() => { 768 | el.products.forEach((product) => { 769 | db.collection("amazon") 770 | .doc(el.routeName) 771 | .collection("products") 772 | .add(product); 773 | }); 774 | }); 775 | }); 776 | }; 777 | 778 | return ( 779 |
780 | 781 |
782 | ); 783 | }; 784 | 785 | export default Mockup; 786 | -------------------------------------------------------------------------------- /src/redux/cartSelector.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from "@reduxjs/toolkit"; 2 | 3 | const selectCart = state => state.cart.cart 4 | const selectTaxPercent = (state) => state.cart.taxPercent; 5 | 6 | export const selectCartItems = createSelector( 7 | selectCart, 8 | items => { 9 | return items 10 | } 11 | ); 12 | 13 | export const selectCartItemsCount = createSelector( 14 | selectCartItems, 15 | cartItems => { 16 | if (cartItems) 17 | { 18 | return cartItems.reduce((acc, item) => acc + item.quantity, 0) 19 | } 20 | return 0 21 | } 22 | ) 23 | 24 | export const selectSubtotal = createSelector( 25 | selectCartItems, 26 | cartItems => cartItems.reduce((acc, item) => acc + item.quantity * item.price,0 ) 27 | ) 28 | 29 | export const selectTax = createSelector( 30 | selectSubtotal, 31 | selectTaxPercent, 32 | (subtotal, taxPercent) => subtotal * (taxPercent/100) 33 | ) 34 | 35 | export const selectTotal = createSelector( 36 | selectSubtotal, 37 | selectTax, 38 | (subtotal, tax) => subtotal + tax 39 | ) 40 | 41 | -------------------------------------------------------------------------------- /src/redux/cartSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | export const cartSlice = createSlice({ 4 | name: "cart", 5 | initialState: { 6 | cart: [], 7 | taxPercent: 15, 8 | }, 9 | reducers: { 10 | addItem: { 11 | reducer(state, action) { 12 | const existItemIndex = state.cart.findIndex( 13 | (item) => item.productId === action.payload.productId 14 | ); 15 | 16 | //could use (JSON.stringify(existItem, null, 2)) to console.log result of state.cart.find 17 | 18 | if (existItemIndex >= 0) { 19 | state.cart[existItemIndex] = {...action.payload, quantity: state.cart[existItemIndex].quantity + 1} 20 | } else { 21 | state.cart.push(action.payload); 22 | } 23 | }, 24 | prepare(item) { 25 | return { payload: { ...item, quantity: 1 } }; 26 | }, 27 | }, 28 | updateItemQuantity: (state, action) => { 29 | 30 | const itemIndex = state.cart.findIndex((item) => item.productId === action.payload.productId) 31 | if (action.payload.quantity > 0) { 32 | 33 | if (itemIndex >= 0) { 34 | state.cart[itemIndex] = {...state.cart[itemIndex], quantity: action.payload.quantity} 35 | } 36 | } 37 | 38 | }, 39 | removeItem: (state, action) => { 40 | state.cart = state.cart.filter((item) => item.productId !== action.payload); 41 | }, 42 | emptyCart: (state) => { 43 | state.cart=[] 44 | } 45 | }, 46 | }); 47 | 48 | export const { addItem, removeItem , updateItemQuantity, emptyCart} = cartSlice.actions; 49 | 50 | export default cartSlice.reducer; 51 | -------------------------------------------------------------------------------- /src/redux/productSelector.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from "@reduxjs/toolkit"; 2 | 3 | export const selectProduct = createSelector( 4 | (state) => state.products.products, 5 | (_, productId) => productId, 6 | (products, productId) => products.find((product) => product.id === productId) 7 | ); 8 | -------------------------------------------------------------------------------- /src/redux/productsSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | import { db } from "../firebase.utils"; 3 | 4 | export const productsSlice = createSlice({ 5 | name: "products", 6 | initialState: { 7 | products: null, 8 | departments:null 9 | }, 10 | reducers: { 11 | setProducts: (state, action) => { 12 | state.products = action.payload; 13 | }, 14 | setDepartment: (state, action) => { 15 | state.departments = action.payload 16 | } 17 | }, 18 | }); 19 | 20 | export const fetchProducts = () => { 21 | return (dispatch) => { 22 | const productsRef = db.collectionGroup("products"); 23 | let products = []; 24 | productsRef 25 | // .limit(1) 26 | .get() 27 | .then((querySnapShot) => { 28 | querySnapShot.forEach((doc) => { 29 | products.push({ ...doc.data(), id: doc.id }); 30 | }); 31 | dispatch(setProducts(products)); 32 | }); 33 | }; 34 | }; 35 | 36 | export const fetchDepartment = () => { 37 | return (dispatch) => { 38 | const productsRef = db.collection("amazon"); 39 | let departments = []; 40 | productsRef 41 | // .limit(1) 42 | .get() 43 | .then((querySnapShot) => { 44 | querySnapShot.forEach((doc) => { 45 | departments.push({ ...doc.data(), id: doc.id }); 46 | }); 47 | dispatch(setDepartment(departments)); 48 | }); 49 | }; 50 | }; 51 | 52 | 53 | export const { setProducts, setDepartment } = productsSlice.actions; 54 | 55 | export const selectProducts = (state) => state.products.products; 56 | export const selectDepartments = (state) => state.products.departments; 57 | 58 | export default productsSlice.reducer; 59 | -------------------------------------------------------------------------------- /src/redux/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore, getDefaultMiddleware } from "@reduxjs/toolkit"; 2 | import { persistReducer, persistStore } from "redux-persist"; 3 | import { combineReducers } from "redux"; 4 | import storage from "redux-persist/lib/storage"; 5 | import userReducer from "./userSlice"; 6 | import productsReducer from "./productsSlice"; 7 | import cartReducer from "./cartSlice"; 8 | 9 | const rootReducer = combineReducers({ 10 | user: userReducer, 11 | products: productsReducer, 12 | cart: cartReducer, 13 | }); 14 | 15 | const persistConfig = { 16 | key: "root", 17 | storage, 18 | whitelist: ["cart", "product"], 19 | }; 20 | 21 | 22 | const persistedReducer = persistReducer(persistConfig, rootReducer) 23 | 24 | const store = configureStore({ 25 | reducer: persistedReducer, 26 | middleware: getDefaultMiddleware({ 27 | serializableCheck: { 28 | ignoredActions:["persist/PERSIST"] 29 | } 30 | }) 31 | }); 32 | 33 | const persistor = persistStore(store) 34 | 35 | export { store, persistor }; -------------------------------------------------------------------------------- /src/redux/userSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | import { db } from "../firebase.utils"; 3 | 4 | // "ducks" pattern which suggests that you should put all your action creators and reducers in one file, do named exports of the action creators, and a default export of the reducer function. 5 | 6 | export const userSlice = createSlice({ 7 | name: "user", 8 | initialState: { 9 | user: null, 10 | }, 11 | reducers: { 12 | setUser: (state, action) => { 13 | state.user = action.payload; 14 | }, 15 | }, 16 | }); 17 | 18 | export const fetchUser = (uid) => { 19 | return (dispatch) => { 20 | db.collection('amazonUsers').doc(uid) 21 | .get() 22 | .then((doc) => { 23 | if (doc.exists) { 24 | dispatch(setUser({name: doc.data().name, email: doc.data().email, uid: doc.id})); 25 | } 26 | }) 27 | .catch((error) => console.log(error.message)); 28 | }; 29 | }; 30 | 31 | export const { setUser } = userSlice.actions; 32 | 33 | // The function below is called a thunk and allows us to perform async logic. It 34 | // can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This 35 | // will call the thunk with the `dispatch` function as the first argument. Async 36 | // code can then be executed and other actions can be dispatched 37 | // export const incrementAsync = amount => dispatch => { 38 | // setTimeout(() => { 39 | // dispatch(incrementByAmount(amount)); 40 | // }, 1000); 41 | // }; 42 | 43 | // The function below is called a selector and allows us to select a value from 44 | // the state. Selectors can also be defined inline where they're used instead of 45 | // in the slice file. For example: `useSelector((state) => state.user.value)` 46 | export const selectUser = (state) => state.user.user; 47 | 48 | export default userSlice.reducer; 49 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Navigate } from "react-router-dom"; 3 | import Home from "./views/home/home"; 4 | import Shop from "./views/shop/shop"; 5 | import NotFoundView from "./views/errors/NotFoundView"; 6 | import MainLayout from "./layouts/MainLayout"; 7 | import Register from "./views/auth/Register"; 8 | import Login from "./views/auth/Login"; 9 | import AuthLayout from "./layouts/AuthLayout"; 10 | import Checkout from "./views/checkout/Checkout"; 11 | import Product from "./views/product/Product"; 12 | import Payment from "./views/payment/Payment"; 13 | import { loadStripe } from "@stripe/stripe-js"; 14 | import { Elements } from "@stripe/react-stripe-js"; 15 | import Orders from "./views/orders/Orders"; 16 | import Mockup from "./mockup"; 17 | import Success from "./views/payment/Success"; 18 | 19 | const promise = loadStripe( 20 | "pk_test_51HeTtQImEgmfO9dx3rV5DTtE8sIiZOUG8KZbdXdfOEAzOG54ej7Xivl2kXmgMrHGV8dLRSlLioJ56v4KbB0bkMnP00UWd0uuAL" 21 | ); 22 | 23 | 24 | const routes = [ 25 | { 26 | path: "/", 27 | element: , 28 | children: [ 29 | { 30 | path: "shop", 31 | element: , 32 | }, 33 | { 34 | path:'checkout', 35 | element: 36 | }, 37 | { 38 | path: 'payment', 39 | element: 40 | }, 41 | { 42 | path: 'orders', 43 | element: 44 | }, 45 | { 46 | path: 'success/:orderId', 47 | element: 48 | }, 49 | { 50 | path: 'mockup', 51 | element: 52 | }, 53 | { 54 | path: 'product/:productId', 55 | element: 56 | }, 57 | { 58 | path: "404", 59 | element: , 60 | }, 61 | { 62 | path: "/", 63 | element: , 64 | }, 65 | { 66 | path: "*", 67 | element: , 68 | }, 69 | ], 70 | }, 71 | { 72 | path: "auth", 73 | element: , 74 | children: [ 75 | { 76 | path: "register", 77 | element: , 78 | }, 79 | { 80 | path: "/", 81 | element: , 82 | }, 83 | ], 84 | }, 85 | ]; 86 | 87 | export default routes; 88 | -------------------------------------------------------------------------------- /src/scss/_basic.scss: -------------------------------------------------------------------------------- 1 | @import "./variables"; 2 | 3 | *, 4 | *::before, 5 | *::after { 6 | margin: 0; 7 | padding: 0; 8 | box-sizing: border-box; 9 | } 10 | 11 | body { 12 | min-height: 100%; 13 | font-family: -apple-system, BlinkMacSystemFont, "Amazon Ember", "Segoe UI", 14 | "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", 15 | "Helvetica Neue", sans-serif; 16 | -webkit-font-smoothing: antialiased; 17 | -moz-osx-font-smoothing: grayscale; 18 | // background-color: $background-color !important; 19 | } 20 | 21 | code { 22 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 23 | monospace; 24 | } 25 | 26 | .ant-btn-primary { 27 | background-color: $button-color !important; 28 | border: 1px solid $button-border !important; 29 | color: $primary-color-light !important; 30 | } 31 | 32 | .ant-btn:hover { 33 | border: 1px solid $button-border !important; 34 | color: $primary-color-light !important; 35 | } 36 | 37 | .anticon-close { 38 | color: white !important; 39 | } 40 | 41 | .ant-drawer-title { 42 | color: white !important; 43 | font-weight: bold !important; 44 | font-size: 1.2rem !important; 45 | } 46 | 47 | .image-gallery-thumbnail-image { 48 | width: 60% !important; 49 | } 50 | 51 | .image-gallery-image { 52 | width: 80% !important; 53 | } -------------------------------------------------------------------------------- /src/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | /* Breakpoints */ 2 | @mixin breakpoint($point) { 3 | @if $point == xs { 4 | @media (min-width: 30rem) { @content; } 5 | } 6 | @else if $point == s { 7 | @media (min-width: 40rem) { @content; } 8 | } 9 | @else if $point == m { 10 | @media (min-width: 48rem) { @content; } 11 | } 12 | @else if $point == l { 13 | @media (min-width: 60rem) { @content; } 14 | } 15 | @else if $point == xl { 16 | @media (min-width: 64rem) { @content; } 17 | } 18 | @else if $point == xxl { 19 | @media (min-width: 80rem) { @content; } 20 | } 21 | } 22 | 23 | /** clear fix */ 24 | 25 | 26 | @mixin clearfix() { 27 | &::after { 28 | display: block; 29 | clear: both; 30 | content: ""; 31 | } 32 | } -------------------------------------------------------------------------------- /src/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | $primary-color-main:#131A22; 2 | $primary-color-light:#232F3E; 3 | 4 | $secondary-color-main:#F1C55A; 5 | $secondary-color-light:#F0C459; 6 | $secondary-color-dark:#FF794A; 7 | 8 | $background-color:#EAEDED; 9 | $background-color-light:#fff; 10 | 11 | $text-color:#fff; 12 | $text-color-grey:#918382; 13 | $text-color-blue:#0167C0; 14 | $text-color-red:#B12705; 15 | 16 | $button-color:#F1C964; 17 | $button-border:#A88733; 18 | 19 | $star-color:#e67e22; 20 | 21 | $line-color:#dddddd; -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/views/auth/Login.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | import { Form, Button, Spinner } from "react-bootstrap"; 4 | import RouterLink from "../../components/link/RouterLink"; 5 | import { Formik } from "formik"; 6 | import * as Yup from "yup"; 7 | import { auth } from "../../firebase.utils"; 8 | import { message } from "antd"; 9 | import { useDispatch } from "react-redux"; 10 | import { fetchUser } from "../../redux/userSlice"; 11 | 12 | 13 | const Login = () => { 14 | const navigate = useNavigate(); 15 | const [loading, setLoading] = useState(false); 16 | const dispatch = useDispatch(); 17 | 18 | return ( 19 |
20 | { 36 | setLoading(true); 37 | setSubmitting(true); 38 | auth 39 | .signInWithEmailAndPassword(values.email, values.password) 40 | .then((user) => { 41 | dispatch(fetchUser(user.user.uid)) 42 | setLoading(false); 43 | navigate("/shop"); 44 | }) 45 | .catch((err) => { 46 | message.error(err.message); 47 | setLoading(false); 48 | }); 49 | 50 | // resetForm(); 51 | setSubmitting(false); 52 | }} 53 | > 54 | {({ 55 | values, 56 | errors, 57 | touched, 58 | handleChange, 59 | handleBlur, 60 | handleSubmit, 61 | isSubmitting, 62 | }) => ( 63 |
64 |

Sign-In

65 | 66 | 67 | Email 68 | 76 | {touched.email && errors.email && ( 77 | {errors.email} 78 | )} 79 | 80 | 81 | 82 | Password 83 | 92 | {touched.password && errors.password && ( 93 | {errors.password} 94 | )} 95 | 96 | 110 |
111 |

112 | By continuing, you agree to Amazon's Conditions of Use and 113 | Privacy Notice. 114 |

115 |

116 | New to Amazon ? 117 |

118 |
119 | 120 | 123 | 124 |
125 | )} 126 |
127 |
128 | ); 129 | }; 130 | 131 | export default Login; 132 | -------------------------------------------------------------------------------- /src/views/auth/Login.scss: -------------------------------------------------------------------------------- 1 | .login { 2 | padding: 2rem; 3 | background-color: #fff; 4 | 5 | 6 | &__form { 7 | border: 1px solid #dddddd; 8 | border-radius: 0.2rem; 9 | width: 30%; 10 | min-width: 25rem; 11 | margin: 0 auto; 12 | padding: 1rem; 13 | background-color: #fff; 14 | 15 | &--title { 16 | font-size: 1.5rem; 17 | font-weight: 600; 18 | } 19 | 20 | &--label { 21 | font-weight: bold; 22 | font-size: medium; 23 | } 24 | &--button { 25 | background-color: $button-color !important; 26 | border: 1px solid $button-border !important; 27 | color: $primary-color-light !important; 28 | } 29 | &--link { 30 | background-color: $background-color !important; 31 | border: 1px solid $text-color-grey !important; 32 | color: $primary-color-light !important; 33 | } 34 | &--footer{ 35 | font-size: smaller; 36 | margin: 1rem auto; 37 | 38 | & span { 39 | color: $text-color-grey; 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/views/auth/Register.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | import { Form, Button, Spinner, Alert } from "react-bootstrap"; 4 | import { Formik } from "formik"; 5 | import * as Yup from "yup"; 6 | import RouterLink from "./../../components/link/RouterLink"; 7 | import { auth, createUserProfileDocument } from "../../firebase.utils"; 8 | 9 | const Register = () => { 10 | const [loading, setLoading] = useState(false); 11 | const [show, setShow] = useState(false); 12 | const [error, setError] = useState(""); 13 | const navigate = useNavigate(); 14 | 15 | return ( 16 |
17 | setShow(false)} 21 | dismissible 22 | > 23 | Oops! You got an error! 24 |

{error}

25 |
26 | { 55 | setLoading(true); 56 | setSubmitting(true); 57 | 58 | auth 59 | .createUserWithEmailAndPassword(values.email, values.password) 60 | .then((user) => { 61 | createUserProfileDocument(user, { name: values.name }); 62 | }) 63 | .then(() => { 64 | setLoading(false) 65 | navigate("/auth"); 66 | }) 67 | .catch((err) => { 68 | setError(err.message); 69 | setLoading(false); 70 | setShow(true); 71 | }); 72 | 73 | resetForm(); 74 | setSubmitting(false); 75 | }} 76 | > 77 | {({ 78 | values, 79 | errors, 80 | touched, 81 | handleChange, 82 | handleBlur, 83 | handleSubmit, 84 | isSubmitting, 85 | }) => ( 86 |
87 |

Create account

88 | 89 | 90 | Your name 91 | 92 | 100 | {touched.name && errors.name && ( 101 | {errors.name} 102 | )} 103 | 104 | 105 | Email 106 | 114 | {touched.email && errors.email && ( 115 | {errors.email} 116 | )} 117 | 118 | 119 | 120 | 121 | Password 122 | 123 | 133 | {touched.password && errors.password && ( 134 | {errors.password} 135 | )} 136 | 137 | 138 | 139 | Re-enter password 140 | 141 | 151 | {touched.passwordConfirmation && errors.passwordConfirmation && ( 152 | 153 | {errors.passwordConfirmation} 154 | 155 | )} 156 | 157 | 171 |
172 |

173 | By creating an account, you agree to Amazon's Conditions of Use 174 | and Privacy Notice. 175 |

176 | 177 |

178 | Already have an account ? Sign-In 179 |

180 |
181 |
182 |
183 | )} 184 |
185 |
186 | ); 187 | }; 188 | 189 | export default Register; 190 | -------------------------------------------------------------------------------- /src/views/auth/Register.scss: -------------------------------------------------------------------------------- 1 | .register { 2 | padding: 2rem; 3 | background-color: #fff; 4 | 5 | &__form { 6 | border: 1px solid #dddddd; 7 | border-radius: 0.2rem; 8 | width: 30%; 9 | min-width: 25rem; 10 | margin: 0 auto; 11 | padding: 1rem; 12 | background-color: #fff; 13 | 14 | &--title { 15 | font-size: 1.5rem; 16 | font-weight: 600; 17 | } 18 | 19 | &--label { 20 | font-weight: bold; 21 | font-size: medium; 22 | } 23 | &--button { 24 | background-color: $button-color !important; 25 | border: 1px solid $button-border !important; 26 | color: $primary-color-light !important; 27 | } 28 | &--footer{ 29 | font-size: smaller; 30 | margin: 1rem auto; 31 | 32 | & span { 33 | font-weight: bold; 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/views/checkout/Checkout.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import CheckoutCard from "../../components/card/CheckoutCard"; 3 | import { Button } from "react-bootstrap"; 4 | import { selectCartItems } from "../../redux/cartSelector"; 5 | import { useSelector } from "react-redux"; 6 | import { selectCartItemsCount, selectSubtotal } from "../../redux/cartSelector"; 7 | import RouterLink from "../../components/link/RouterLink"; 8 | import {useNavigate} from 'react-router-dom'; 9 | 10 | const Checkout = () => { 11 | const cartItems = useSelector((state) => selectCartItems(state)); 12 | const cartCount = useSelector((state) => selectCartItemsCount(state)); 13 | const subtotal = useSelector((state) => selectSubtotal(state)); 14 | 15 | const navigate = useNavigate(); 16 | const formatedSubtotal = subtotal.toLocaleString("en-US", { 17 | style: "currency", 18 | currency: "USD", 19 | }); 20 | 21 | return ( 22 |
23 |
24 | 29 |
30 | Shopping Cart 31 | Price 32 |
33 | {cartItems.length > 0 ? ( 34 | cartItems.map(({ productId, ...props }) => ( 35 |
36 | 37 |
38 |
39 | )) 40 | ) : ( 41 |
42 |

Your Amazon Cart is empty.

43 | 44 | Check your Saved for later items below or{" "} 45 | 46 | continue shopping 47 | 48 | . 49 | 50 |
51 | )} 52 | {cartItems.length > 0 && ( 53 |
54 | Subtotal({cartCount} items): 55 | {formatedSubtotal} 56 |
57 | )} 58 |
59 |
60 |
61 | Subtotal({cartCount} items): 62 | {formatedSubtotal} 63 |
64 |
65 | 68 |
69 |
70 |
71 | ); 72 | }; 73 | 74 | export default Checkout; 75 | -------------------------------------------------------------------------------- /src/views/checkout/Checkout.scss: -------------------------------------------------------------------------------- 1 | .checkout { 2 | @include breakpoint(l){ 3 | display: flex; 4 | } 5 | padding: 1rem; 6 | 7 | &__cart { 8 | flex-grow: 1; 9 | &__ad { 10 | width: 70vw; 11 | padding-top: 0.2rem; 12 | } 13 | &__title { 14 | display: flex; 15 | justify-content: space-between; 16 | margin-left: 0.5rem; 17 | margin-top: 1rem; 18 | border-bottom: 1px solid $line-color; 19 | & span:first-child { 20 | font-size: 1.6rem; 21 | margin-bottom: 0.5rem; 22 | } 23 | & span:last-child { 24 | align-self: flex-end; 25 | } 26 | } 27 | &__message { 28 | margin: 1rem; 29 | & > p { 30 | font-weight: 600; 31 | font-size: 1.8rem; 32 | } 33 | } 34 | &__subtotal { 35 | text-align: right; 36 | margin: 1rem auto; 37 | & span:first-child { 38 | font-weight: 600; 39 | } 40 | & span:last-child { 41 | font-weight: 800; 42 | } 43 | } 44 | &__card { 45 | content: ""; 46 | height: 1px; 47 | background: $line-color; 48 | width: 100%; 49 | margin-left: 0.5rem; 50 | } 51 | } 52 | &__subtotal { 53 | border: 1px solid $line-color; 54 | border-radius: 0.2rem; 55 | background-color: $background-color; 56 | width: 18rem; 57 | min-width: 18rem; 58 | height: max-content; 59 | margin: 1rem; 60 | padding: 1rem; 61 | 62 | &__title { 63 | margin-bottom: 1rem; 64 | & span:first-child { 65 | margin-bottom: 1rem; 66 | font-weight: 600; 67 | } 68 | & span:last-child { 69 | margin-bottom: 1rem; 70 | font-weight: 800; 71 | } 72 | } 73 | &__button { 74 | background-color: $button-color !important; 75 | border: 1px solid $button-border !important; 76 | color: $primary-color-light !important; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/views/errors/NotFoundView.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Result } from 'antd'; 3 | 4 | const NotFoundView = () => { 5 | return ( 6 | 12 | ) 13 | } 14 | 15 | export default NotFoundView 16 | -------------------------------------------------------------------------------- /src/views/home/home.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import homeHeroShip from "../../assets/home-bg-ship.jpg"; 3 | import homeHeroPrime from "../../assets/home-bg-prime.jpg"; 4 | import homeHeroComputer from "../../assets/home-bg-computer.jpg"; 5 | import homeHeroAlexa from "../../assets/home-bg-alexa.jpg"; 6 | import homeHeroNZ from '../../assets/home-bg-nz.jpg' 7 | import { Carousel } from "react-bootstrap"; 8 | import ShopBySection from "../../components/home/ShopBySection"; 9 | import PopularSection from "../../components/home/PopularSection"; 10 | import RecommendSection from "../../components/home/RecommendSection"; 11 | import BestSellers from './../../components/home/BestSellers'; 12 | import RelatedSection from "../../components/home/RelatedSection"; 13 | 14 | const Home = () => { 15 | return ( 16 |
17 |
18 | 19 | 20 | Amazon NZ 25 | 26 | 27 | Amazon Ship 32 | 33 | 34 | Amazon Prime 39 | 40 | 41 | Amazon Alexa 46 | 47 | 48 | Amazon Computer 53 | 54 | 55 |
56 |
57 |
58 | 59 |
60 |
61 | 62 |
63 |
64 | 65 |
66 |
67 | 68 |
69 |
70 | 71 |
72 |
73 |
74 | ); 75 | }; 76 | 77 | export default Home; 78 | -------------------------------------------------------------------------------- /src/views/home/home.scss: -------------------------------------------------------------------------------- 1 | .home { 2 | max-width: 1500px; 3 | margin: 0 auto; 4 | &__hero { 5 | width: 100%; 6 | mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1), rgba(0, 0, 0, 0)); 7 | margin: -1rem auto -3rem auto; 8 | @include breakpoint(m) { 9 | margin: -1rem auto -16rem auto; 10 | } 11 | } 12 | 13 | &__content { 14 | margin: 0 1rem; 15 | } 16 | } 17 | 18 | 19 | .carousel-control-next-icon, .carousel-control-prev-icon { 20 | position: absolute; 21 | top:7rem; 22 | width: 32px !important; 23 | height: 32px !important; 24 | } 25 | -------------------------------------------------------------------------------- /src/views/orders/Orders.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { db } from "../../firebase.utils"; 3 | import { selectUser } from "../../redux/userSlice"; 4 | import { useSelector } from "react-redux"; 5 | import Order from "./../../components/order/Order"; 6 | import Spinner from "react-bootstrap/Spinner"; 7 | import { useNavigate } from "react-router-dom"; 8 | import { Result } from "antd"; 9 | 10 | const Orders = () => { 11 | const [orders, setOrders] = useState([]); 12 | const user = useSelector(selectUser); 13 | const navigate = useNavigate(); 14 | 15 | useEffect(() => { 16 | let mounted = true; 17 | if (user && mounted) { 18 | db.collection("amazonUsers") 19 | .doc(user?.uid) 20 | .collection("orders") 21 | .orderBy("created", "desc") 22 | .onSnapshot((snapshot) => { 23 | if (mounted) { 24 | setOrders( 25 | snapshot.docs.map((doc) => ({ 26 | id: doc.id, 27 | data: doc.data(), 28 | })) 29 | ); 30 | } 31 | }); 32 | } else { 33 | setOrders([]); 34 | navigate("/auth"); 35 | } 36 | return () => (mounted = false); 37 | }, [user, navigate]); 38 | 39 | return ( 40 |
41 | {orders.length > 0 ? ( 42 |

Your Orders

43 | ) : ( 44 | 49 | )} 50 | 51 |
52 | {orders ? ( 53 | orders.map((order) => ) 54 | ) : ( 55 | 60 | Loading... 61 | 62 | )} 63 |
64 |
65 | ); 66 | }; 67 | 68 | export default Orders; 69 | -------------------------------------------------------------------------------- /src/views/orders/Orders.scss: -------------------------------------------------------------------------------- 1 | .orders{ 2 | padding: 20px 80px; 3 | 4 | & > h1 { 5 | padding: 30px 0; 6 | } 7 | } -------------------------------------------------------------------------------- /src/views/payment/Payment.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | import { useSelector, useDispatch } from "react-redux"; 4 | import { selectUser } from "../../redux/userSlice"; 5 | import { emptyCart } from "../../redux/cartSlice"; 6 | import CheckoutCard from "../../components/card/CheckoutCard"; 7 | import Spinner from "react-bootstrap/Spinner"; 8 | import { 9 | selectCartItemsCount, 10 | selectSubtotal, 11 | selectCartItems, 12 | } from "../../redux/cartSelector"; 13 | import { Link } from "react-router-dom"; 14 | import { useStripe, useElements, CardElement } from "@stripe/react-stripe-js"; 15 | import CurrencyFormat from "react-currency-format"; 16 | import axios from "../../axios"; 17 | import { db } from "../../firebase.utils"; 18 | 19 | const Payment = () => { 20 | const navigate = useNavigate(); 21 | const dispatch = useDispatch(); 22 | 23 | const user = useSelector(selectUser); 24 | const cartItems = useSelector((state) => selectCartItems(state)); 25 | const cartCount = useSelector((state) => selectCartItemsCount(state)); 26 | const subtotal = useSelector((state) => selectSubtotal(state)); 27 | 28 | const [error, setError] = useState(null); 29 | const [disabled, setDisabled] = useState(true); 30 | const [processing, setProcessing] = useState(""); 31 | const [succeeded, setSucceeded] = useState(false); 32 | const [clientSecret, setClientSecret] = useState(true); 33 | 34 | const stripe = useStripe(); 35 | const elements = useElements(); 36 | 37 | useEffect(() => { 38 | let unsubscribe = false; 39 | 40 | const getClientSecret = async () => { 41 | try { 42 | const response = await axios({ 43 | method: "post", 44 | // Stripe expects the total in a currencies subunits 45 | url: `/payments/create/${subtotal * 100}`, 46 | }); 47 | if (!unsubscribe) setClientSecret(response.data.clientSecret); 48 | } catch (error) { 49 | console.log(error); 50 | } 51 | }; 52 | getClientSecret(); 53 | return () => { 54 | unsubscribe = true; 55 | }; 56 | }, [subtotal]); 57 | 58 | const handleSubmit = async (event) => { 59 | event.preventDefault(); 60 | setProcessing(true); 61 | 62 | await stripe 63 | .confirmCardPayment(clientSecret, { 64 | payment_method: { 65 | card: elements.getElement(CardElement), 66 | }, 67 | }) 68 | .then(({ paymentIntent }) => { 69 | // paymentIntent = payment confirmation 70 | db.collection("amazonUsers") 71 | .doc(user?.uid) 72 | .collection("orders") 73 | .doc(paymentIntent.id) 74 | .set({ 75 | cartItems, 76 | amount: paymentIntent.amount, 77 | created: paymentIntent.created, 78 | }); 79 | setSucceeded(true); 80 | setError(null); 81 | setProcessing(false); 82 | dispatch(emptyCart()); 83 | navigate(`/success/${paymentIntent.id}`); 84 | }); 85 | }; 86 | 87 | const handleChange = (event) => { 88 | setDisabled(event.empty); 89 | setError(event.error ? event.error.message : ""); 90 | }; 91 | 92 | return ( 93 |
94 | 95 |
96 |

97 | Checkout ({cartCount} items) 98 |

99 |
100 |
101 |

Delivery Address

102 |
103 |
104 |

{user?.name}

105 |

18 Great South Rd

106 |

Auckland , New Zealand

107 |
108 |
109 |
110 |
111 |

112 | Review items
113 | and delivery 114 |

115 |
116 |
117 | {cartItems.length > 0 && 118 | cartItems.map(({ productId, ...props }) => ( 119 |
120 | 121 |
122 |
123 | ))} 124 |
125 |
126 |
127 |
128 |

Payment Method

129 |
130 |
131 |
132 | 133 |
134 |

Enter 4242 4242 4242 4242 as the card number.

135 | ( 142 |
143 |

Order Total : {value}

144 |
145 | )} 146 | /> 147 | 162 |
163 | {error &&
{error}
} 164 | 165 |
166 |
167 |
168 |
169 | ); 170 | }; 171 | 172 | export default Payment; 173 | -------------------------------------------------------------------------------- /src/views/payment/Payment.scss: -------------------------------------------------------------------------------- 1 | .payment { 2 | background-color: white; 3 | &__container { 4 | & > h1 { 5 | text-align: center; 6 | padding: 10px; 7 | font-weight: 400; 8 | background-color: rgb(234,237,237); 9 | border-bottom: 1px solid lightgray; 10 | 11 | & a{ 12 | text-decoration: none; 13 | } 14 | } 15 | } 16 | 17 | &__section { 18 | display: flex; 19 | padding: 20px; 20 | margin: 0 20px; 21 | border-bottom: 1px solid lightgray; 22 | } 23 | 24 | &__title { 25 | flex: 0.2; 26 | } 27 | 28 | &__address, &__items,&__details { 29 | flex: 0.8; 30 | } 31 | 32 | &__address p { 33 | font-size: 1.2rem; 34 | } 35 | 36 | &__details > form { 37 | max-width: 400px; 38 | } 39 | 40 | &__details > h3 { 41 | padding-bottom: 20px; 42 | } 43 | &__details > form > div > button{ 44 | background: #f0c14b; 45 | border-radius: 2px; 46 | width: 100%; 47 | height: 30px; 48 | border: 1px solid; 49 | font-weight: bold; 50 | margin-top: 10px; 51 | border-color: #a88724 #9c7e31 #846a29; 52 | color: #111; 53 | } 54 | 55 | &__priceContainer { 56 | margin-top: 2rem; 57 | 58 | & > p { 59 | color: lightgray; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/views/payment/Success.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Result, Button } from "antd"; 3 | import { useParams, useNavigate } from 'react-router-dom' 4 | 5 | 6 | const Success = () => { 7 | const { orderId } = useParams() 8 | const navigate = useNavigate() 9 | 10 | const handleOrders = () => { 11 | navigate('/orders') 12 | } 13 | const handleShopping = () => { 14 | navigate('/shop') 15 | } 16 | return ( 17 | 23 | See all orders 24 | , 25 | , 26 | ]} 27 | /> 28 | ); 29 | }; 30 | 31 | export default Success; 32 | -------------------------------------------------------------------------------- /src/views/product/Product.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useDispatch } from "react-redux"; 3 | import { Button } from "react-bootstrap"; 4 | import { useParams, useNavigate } from "react-router-dom"; 5 | import { useSelector } from "react-redux"; 6 | import { selectProduct } from "../../redux/productSelector"; 7 | import { ReactComponent as AddToCart } from "../../assets/add_shopping_cart.svg"; 8 | import { addItem } from "../../redux/cartSlice"; 9 | import ImageGallery from "react-image-gallery"; 10 | import "react-image-gallery/styles/css/image-gallery.css"; 11 | import { Modal } from "antd"; 12 | import { ExclamationCircleOutlined } from "@ant-design/icons"; 13 | import { selectProducts } from "../../redux/productsSlice"; 14 | import ProductCard from "../../components/card/ProductCard"; 15 | 16 | import Slider from "react-slick"; 17 | import "slick-carousel/slick/slick.css"; 18 | import "slick-carousel/slick/slick-theme.css"; 19 | 20 | 21 | 22 | const Product = () => { 23 | //related slider 24 | let slidesToShow; 25 | const width = 26 | window.innerWidth || 27 | document.documentElement.clientWidth || 28 | document.body.clientWidth; 29 | if (width > 1024) { 30 | slidesToShow = 7; 31 | } else if (width > 768) { 32 | slidesToShow = 5; 33 | } else if(width === 768) { 34 | slidesToShow = 4; 35 | } else { 36 | slidesToShow = 1; 37 | } 38 | 39 | const settings = { 40 | infinite: true, 41 | speed: 500, 42 | slidesToShow, 43 | slidesToScroll: 1, 44 | }; 45 | 46 | const dispatch = useDispatch(); 47 | const navigate = useNavigate(); 48 | const { productId } = useParams(); 49 | const { 50 | imgUrl, 51 | brand, 52 | title, 53 | price, 54 | count, 55 | rate, 56 | active, 57 | description, 58 | gallery, 59 | } = useSelector((state) => selectProduct(state, productId)); 60 | 61 | const products = useSelector(selectProducts); 62 | 63 | const formatedPrice = price.toLocaleString("en-US", { 64 | style: "currency", 65 | currency: "USD", 66 | }); 67 | const descriptions = description.split(";"); 68 | const handleShopCart = () => { 69 | dispatch( 70 | addItem({ 71 | title, 72 | price, 73 | count, 74 | imgUrl, 75 | rate, 76 | productId, 77 | active, 78 | }) 79 | ); 80 | Modal.confirm({ 81 | title: "Product added to your cart", 82 | icon: , 83 | content: `You've added the ${title} to your cart`, 84 | okText: "Continue Shopping", 85 | cancelText: "Proceed to Checkout", 86 | onOk: () => navigate("/shop"), 87 | onCancel: () => navigate("/checkout"), 88 | }); 89 | }; 90 | 91 | return ( 92 |
93 |
94 |
95 | 101 |
102 |
103 |
{title}
104 |
105 | Brand: 106 | {brand} 107 |
108 |
109 | Price: {formatedPrice} 110 |
111 |
112 |
113 |
    114 | {descriptions.map((el, index) => ( 115 |
  • {el}
  • 116 | ))} 117 |
118 |
119 |
120 |
121 |
{formatedPrice}
122 |
123 | 130 |
131 |
132 |
133 |
134 |

Inspired by your recent shopping trends

135 | 136 | {products.map(({ id, ...props }) => ( 137 | 138 | ))} 139 | 140 |
141 |
142 | ); 143 | }; 144 | 145 | export default Product; 146 | -------------------------------------------------------------------------------- /src/views/product/Product.scss: -------------------------------------------------------------------------------- 1 | .product { 2 | @include breakpoint(l) { 3 | display: flex; 4 | } 5 | justify-content: space-between; 6 | margin: 2rem 1rem; 7 | &__gallery { 8 | flex-basis: 50%; 9 | margin-right: 3rem; 10 | width: fit-content; 11 | overflow: hidden; 12 | } 13 | &__description { 14 | display: flex; 15 | flex-direction: column; 16 | flex-basis: 40%; 17 | &__title { 18 | font-weight: 600; 19 | font-size: 1.1rem; 20 | line-height: 1.5; 21 | } 22 | &__brand { 23 | font-weight: 600; 24 | color: $text-color-blue; 25 | font-size: 0.8rem; 26 | margin: 0.8rem 0; 27 | } 28 | &__price span:first-child { 29 | color: $text-color-grey; 30 | font-weight: 600; 31 | } 32 | &__price span:last-child { 33 | color: $text-color-red; 34 | font-weight: 600; 35 | } 36 | 37 | &__line { 38 | content: ""; 39 | height: 1px; 40 | background: $line-color; 41 | width: 100%; 42 | margin-left: 0.5rem; 43 | margin: 1.5rem 0; 44 | } 45 | 46 | &__content li { 47 | padding-bottom: 1rem; 48 | line-height: 1.5; 49 | } 50 | 51 | &__content { 52 | padding-left: 1rem; 53 | } 54 | } 55 | &__cart { 56 | flex-basis: 10%; 57 | border: 1px solid $line-color; 58 | border-radius: 0.2rem; 59 | background-color: $background-color; 60 | width: 18rem; 61 | min-width: 18rem; 62 | height: max-content; 63 | margin: 1rem; 64 | padding: 1rem; 65 | &__price { 66 | margin-bottom: 1rem; 67 | color: $text-color-red; 68 | font-weight: 600; 69 | } 70 | } 71 | 72 | &__related { 73 | margin: 2rem; 74 | @include breakpoint(l) { 75 | margin: 4rem; 76 | } 77 | 78 | & > h3 { 79 | color: $secondary-color-dark; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/views/shop/shop.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ShopPage from "../../components/shop/ShopPage"; 3 | import { useSelector } from "react-redux"; 4 | import { 5 | selectProducts, 6 | selectDepartments, 7 | } from "../../redux/productsSlice.js"; 8 | 9 | const Shop = () => { 10 | const products = useSelector(selectProducts); 11 | const departments = useSelector(selectDepartments); 12 | return ( 13 |
14 | 15 |
16 | ); 17 | }; 18 | 19 | export default Shop; 20 | -------------------------------------------------------------------------------- /src/views/shop/shop.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aaroncodehub/amazon-clone-react-redux/3e5c8952334f10cab16e724a6a10932976d12d61/src/views/shop/shop.scss --------------------------------------------------------------------------------