├── .gitignore ├── screenshot.png ├── views ├── error.ejs ├── pages │ ├── careers.ejs │ ├── about-us.ejs │ ├── shipping-policy.ejs │ └── contact-us.ejs ├── partials │ ├── pagination.ejs │ ├── categories-navbar.ejs │ ├── footer.ejs │ └── header.ejs ├── user │ ├── signin.ejs │ ├── signup.ejs │ └── profile.ejs └── shop │ ├── product.ejs │ ├── checkout.ejs │ ├── shopping-cart.ejs │ ├── index.ejs │ └── home.ejs ├── public ├── images │ ├── dept0.jpg │ ├── dept1.jpg │ ├── dept2.jpg │ ├── slide1.jpg │ ├── slide2.jpg │ ├── slide3.jpg │ ├── shop-icon.png │ ├── banner-purses.jpg │ ├── banner-totes.jpg │ ├── banner-travel.jpg │ ├── banner-admin-page.jpg │ ├── banner-backpacks.jpg │ ├── banner-briefcases.jpg │ ├── banner-mini-bags.jpg │ ├── banner-all-products.jpg │ └── banner-large-handbags.jpg ├── javascripts │ ├── main.js │ ├── map.js │ └── checkout.js └── stylesheets │ └── style.css ├── models ├── category.js ├── user.js ├── product.js ├── cart.js └── order.js ├── middleware └── index.js ├── components ├── admin-imgPath-component.jsx ├── admin-dashboard-component.jsx └── admin-order-component.jsx ├── config ├── db.js ├── passport.js └── validator.js ├── seedDB ├── category-seed.js └── products-seed.js ├── package.json ├── routes ├── pages.js ├── products.js ├── user.js ├── admin.js └── index.js ├── app.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .env 3 | /.adminbro -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maryamaljanabi/bestbags-nodejs-ecommerce/HEAD/screenshot.png -------------------------------------------------------------------------------- /views/error.ejs: -------------------------------------------------------------------------------- 1 |

<%= message %>

2 |

<%= error.status %>

3 |
<%= error.stack %>
4 | -------------------------------------------------------------------------------- /public/images/dept0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maryamaljanabi/bestbags-nodejs-ecommerce/HEAD/public/images/dept0.jpg -------------------------------------------------------------------------------- /public/images/dept1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maryamaljanabi/bestbags-nodejs-ecommerce/HEAD/public/images/dept1.jpg -------------------------------------------------------------------------------- /public/images/dept2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maryamaljanabi/bestbags-nodejs-ecommerce/HEAD/public/images/dept2.jpg -------------------------------------------------------------------------------- /public/images/slide1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maryamaljanabi/bestbags-nodejs-ecommerce/HEAD/public/images/slide1.jpg -------------------------------------------------------------------------------- /public/images/slide2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maryamaljanabi/bestbags-nodejs-ecommerce/HEAD/public/images/slide2.jpg -------------------------------------------------------------------------------- /public/images/slide3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maryamaljanabi/bestbags-nodejs-ecommerce/HEAD/public/images/slide3.jpg -------------------------------------------------------------------------------- /public/images/shop-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maryamaljanabi/bestbags-nodejs-ecommerce/HEAD/public/images/shop-icon.png -------------------------------------------------------------------------------- /public/images/banner-purses.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maryamaljanabi/bestbags-nodejs-ecommerce/HEAD/public/images/banner-purses.jpg -------------------------------------------------------------------------------- /public/images/banner-totes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maryamaljanabi/bestbags-nodejs-ecommerce/HEAD/public/images/banner-totes.jpg -------------------------------------------------------------------------------- /public/images/banner-travel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maryamaljanabi/bestbags-nodejs-ecommerce/HEAD/public/images/banner-travel.jpg -------------------------------------------------------------------------------- /public/images/banner-admin-page.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maryamaljanabi/bestbags-nodejs-ecommerce/HEAD/public/images/banner-admin-page.jpg -------------------------------------------------------------------------------- /public/images/banner-backpacks.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maryamaljanabi/bestbags-nodejs-ecommerce/HEAD/public/images/banner-backpacks.jpg -------------------------------------------------------------------------------- /public/images/banner-briefcases.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maryamaljanabi/bestbags-nodejs-ecommerce/HEAD/public/images/banner-briefcases.jpg -------------------------------------------------------------------------------- /public/images/banner-mini-bags.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maryamaljanabi/bestbags-nodejs-ecommerce/HEAD/public/images/banner-mini-bags.jpg -------------------------------------------------------------------------------- /public/images/banner-all-products.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maryamaljanabi/bestbags-nodejs-ecommerce/HEAD/public/images/banner-all-products.jpg -------------------------------------------------------------------------------- /public/images/banner-large-handbags.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maryamaljanabi/bestbags-nodejs-ecommerce/HEAD/public/images/banner-large-handbags.jpg -------------------------------------------------------------------------------- /public/javascripts/main.js: -------------------------------------------------------------------------------- 1 | // fade out for flash messages 2 | setTimeout(function () { 3 | $("#flash-msg").fadeOut("slow"); 4 | }, 3000); 5 | 6 | setTimeout(function () { 7 | $("#success").fadeOut("slow"); 8 | }, 3000); 9 | 10 | setTimeout(function () { 11 | $("#error").fadeOut("slow"); 12 | }, 3000); 13 | -------------------------------------------------------------------------------- /public/javascripts/map.js: -------------------------------------------------------------------------------- 1 | mapboxgl.accessToken = 2 | "pk.eyJ1IjoibS0xMzVhIiwiYSI6ImNrOGsyb3ZqaDBkemkzcW10emc1eXoyNngifQ.NuSNrMKqrpdm-jxvPpx0_Q"; 3 | const lat = 48.8606; 4 | const lng = 2.3376; 5 | 6 | const map = new mapboxgl.Map({ 7 | container: "map", 8 | style: "mapbox://styles/mapbox/streets-v11", 9 | zoom: 8, 10 | center: [lng, lat], 11 | }); 12 | 13 | const marker = new mapboxgl.Marker().setLngLat([lng, lat]).addTo(map); 14 | -------------------------------------------------------------------------------- /models/category.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | const slug = require("mongoose-slug-updater"); 4 | 5 | mongoose.plugin(slug); 6 | 7 | const categorySchema = Schema({ 8 | title: { 9 | type: String, 10 | required: true, 11 | }, 12 | slug: { 13 | type: String, 14 | unique: true, 15 | slug: "title", 16 | }, 17 | }); 18 | 19 | module.exports = mongoose.model("Category", categorySchema); 20 | -------------------------------------------------------------------------------- /middleware/index.js: -------------------------------------------------------------------------------- 1 | let middlewareObject = {}; 2 | 3 | //a middleware to check if a user is logged in or not 4 | middlewareObject.isNotLoggedIn = (req, res, next) => { 5 | if (!req.isAuthenticated()) { 6 | return next(); 7 | } 8 | res.redirect("/"); 9 | }; 10 | 11 | middlewareObject.isLoggedIn = (req, res, next) => { 12 | if (req.isAuthenticated()) { 13 | return next(); 14 | } 15 | res.redirect("/user/signin"); 16 | }; 17 | 18 | module.exports = middlewareObject; 19 | -------------------------------------------------------------------------------- /components/admin-imgPath-component.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Label } from "admin-bro"; 3 | 4 | const imgStyle = { 5 | width: "30em", 6 | border: "2px solid gray", 7 | borderRadius: "11px", 8 | margin: "1em auto", 9 | }; 10 | 11 | const AdminShowImage = (props) => { 12 | const { record, property } = props; 13 | return ( 14 |
15 | 16 | Product Image 17 |
18 | ); 19 | }; 20 | 21 | export default AdminShowImage; 22 | -------------------------------------------------------------------------------- /config/db.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const connectDB = async () => { 4 | try { 5 | const uri = process.env.MONGO_URI || "mongodb://localhost/bags-ecommerce"; 6 | await mongoose 7 | .connect(uri, { 8 | useNewUrlParser: true, 9 | useCreateIndex: true, 10 | useUnifiedTopology: true, 11 | }) 12 | .catch((error) => console.log(error)); 13 | const connection = mongoose.connection; 14 | console.log("MONGODB CONNECTED SUCCESSFULLY!"); 15 | } catch (error) { 16 | console.log(error); 17 | return error; 18 | } 19 | }; 20 | 21 | module.exports = connectDB; 22 | -------------------------------------------------------------------------------- /models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const bcrypt = require("bcrypt-nodejs"); 3 | const Schema = mongoose.Schema; 4 | 5 | const userSchema = Schema({ 6 | username: { 7 | type: String, 8 | require: true, 9 | }, 10 | email: { 11 | type: String, 12 | require: true, 13 | }, 14 | password: { 15 | type: String, 16 | require: true, 17 | }, 18 | }); 19 | 20 | // encrypt the password before storing 21 | userSchema.methods.encryptPassword = (password) => { 22 | return bcrypt.hashSync(password, bcrypt.genSaltSync(5), null); 23 | }; 24 | 25 | userSchema.methods.validPassword = function (candidatePassword) { 26 | if (this.password != null) { 27 | return bcrypt.compareSync(candidatePassword, this.password); 28 | } else { 29 | return false; 30 | } 31 | }; 32 | 33 | module.exports = mongoose.model("User", userSchema); 34 | -------------------------------------------------------------------------------- /models/product.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const productSchema = Schema({ 5 | productCode: { 6 | type: String, 7 | required: true, 8 | unique: true, 9 | }, 10 | title: { 11 | type: String, 12 | required: true, 13 | }, 14 | imagePath: { 15 | type: String, 16 | required: true, 17 | }, 18 | description: { 19 | type: String, 20 | required: true, 21 | }, 22 | price: { 23 | type: Number, 24 | required: true, 25 | }, 26 | category: { 27 | type: mongoose.Schema.Types.ObjectId, 28 | ref: "Category", 29 | }, 30 | manufacturer: { 31 | type: String, 32 | }, 33 | available: { 34 | type: Boolean, 35 | required: true, 36 | }, 37 | createdAt: { 38 | type: Date, 39 | default: Date.now, 40 | }, 41 | }); 42 | 43 | module.exports = mongoose.model("Product", productSchema); 44 | -------------------------------------------------------------------------------- /seedDB/category-seed.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | require("dotenv").config({ path: path.join(__dirname, "../.env") }); 3 | const Category = require("../models/category"); 4 | const mongoose = require("mongoose"); 5 | const connectDB = require("./../config/db"); 6 | connectDB(); 7 | 8 | async function seedDB() { 9 | async function seedCateg(titleStr) { 10 | try { 11 | const categ = await new Category({ title: titleStr }); 12 | await categ.save(); 13 | } catch (error) { 14 | console.log(error); 15 | return error; 16 | } 17 | } 18 | 19 | async function closeDB() { 20 | console.log("CLOSING CONNECTION"); 21 | await mongoose.disconnect(); 22 | } 23 | await seedCateg("Backpacks"); 24 | await seedCateg("Briefcases"); 25 | await seedCateg("Mini Bags"); 26 | await seedCateg("Large Handbags"); 27 | await seedCateg("Travel"); 28 | await seedCateg("Totes"); 29 | await seedCateg("Purses"); 30 | await closeDB(); 31 | } 32 | 33 | seedDB(); 34 | -------------------------------------------------------------------------------- /models/cart.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const cartSchema = new mongoose.Schema({ 4 | items: [ 5 | { 6 | productId: { 7 | type: mongoose.Schema.Types.ObjectId, 8 | ref: "Product", 9 | }, 10 | qty: { 11 | type: Number, 12 | default: 0, 13 | }, 14 | price: { 15 | type: Number, 16 | default: 0, 17 | }, 18 | title: { 19 | type: String, 20 | }, 21 | productCode: { 22 | type: String, 23 | }, 24 | }, 25 | ], 26 | totalQty: { 27 | type: Number, 28 | default: 0, 29 | required: true, 30 | }, 31 | totalCost: { 32 | type: Number, 33 | default: 0, 34 | required: true, 35 | }, 36 | user: { 37 | type: mongoose.Schema.Types.ObjectId, 38 | ref: "User", 39 | required: false, 40 | }, 41 | createdAt: { 42 | type: Date, 43 | default: Date.now, 44 | }, 45 | }); 46 | 47 | module.exports = mongoose.model("Cart", cartSchema); 48 | -------------------------------------------------------------------------------- /views/pages/careers.ejs: -------------------------------------------------------------------------------- 1 | <%- include ../partials/header %> <%- include ../partials/categories-navbar %> 2 | 3 |
4 |
5 |
6 |

Work With Us

7 |

We are always looking to hire enthusiastic people!

8 |

9 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Asperiores nam 10 | voluptatem adipisci sed amet ipsa quod reiciendis eos. Magnam ad 11 | sapiente aut a libero aliquam veritatis incidunt molestiae tempora? 12 |

13 |

14 | If you are interested in working with us, please send your CV to the 15 | following email and we will get back to you.
16 | 17 | 18 | hr@email.com 19 | 20 |

21 |
22 |
23 |
24 | 25 | <%- include ../partials/footer %> 26 | -------------------------------------------------------------------------------- /components/admin-dashboard-component.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | const Title = styled.h1` 5 | font-size: 3em; 6 | color: #478ba2; 7 | font-weight: bold; 8 | line-height: 1em; 9 | `; 10 | 11 | const Lead = styled.p` 12 | font-size: 2em; 13 | color: black; 14 | font-weight: normal; 15 | line-height: 1em; 16 | `; 17 | 18 | const Wrapper = styled.section` 19 | padding: 4em; 20 | text-align: center; 21 | background: white; 22 | `; 23 | 24 | const imgStyle = { 25 | width: "100%", 26 | height: "30%", 27 | }; 28 | 29 | const Dashboard = () => { 30 | return ( 31 |
32 | dashboard-head 37 | 38 | Welcome to the Admin Panel 39 | 40 | Here you can manage your products and categories as well as view users 41 | and their orders. 42 | 43 | 44 |
45 | ); 46 | }; 47 | 48 | export default Dashboard; 49 | -------------------------------------------------------------------------------- /views/partials/pagination.ejs: -------------------------------------------------------------------------------- 1 | <% if (pages > 0) { %> 2 | 29 | <% } %> 30 | -------------------------------------------------------------------------------- /views/pages/about-us.ejs: -------------------------------------------------------------------------------- 1 | <%- include ../partials/header %> <%- include ../partials/categories-navbar %> 2 | 3 |
4 |
5 |
6 |

About Us

7 |

Welcome to our store

8 |

9 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Asperiores nam 10 | voluptatem adipisci sed amet ipsa quod reiciendis eos. Magnam ad 11 | sapiente aut a libero aliquam veritatis incidunt molestiae tempora? 12 |

13 |

14 | Maxime! Lorem ipsum dolor sit amet consectetur adipisicing elit. Labore 15 | autem assumenda vel laudantium quo ipsum omnis earum nostrum, excepturi 16 | quibusdam, dicta cupiditate voluptatibus aperiam molestiae? Facilis 17 | ipsum esse eligendi nobis. 18 |

19 |
20 |
21 |

Our location on map

22 |
23 |
24 |
25 |
26 | 27 | 28 | 29 | <%- include ../partials/footer %> 30 | -------------------------------------------------------------------------------- /views/partials/categories-navbar.ejs: -------------------------------------------------------------------------------- 1 | 39 | -------------------------------------------------------------------------------- /models/order.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const orderSchema = Schema({ 5 | user: { 6 | type: Schema.Types.ObjectId, 7 | ref: "User", 8 | }, 9 | cart: { 10 | totalQty: { 11 | type: Number, 12 | default: 0, 13 | required: true, 14 | }, 15 | totalCost: { 16 | type: Number, 17 | default: 0, 18 | required: true, 19 | }, 20 | items: [ 21 | { 22 | productId: { 23 | type: mongoose.Schema.Types.ObjectId, 24 | ref: "Product", 25 | }, 26 | qty: { 27 | type: Number, 28 | default: 0, 29 | }, 30 | price: { 31 | type: Number, 32 | default: 0, 33 | }, 34 | title: { 35 | type: String, 36 | }, 37 | productCode: { 38 | type: String, 39 | }, 40 | }, 41 | ], 42 | }, 43 | address: { 44 | type: String, 45 | required: true, 46 | }, 47 | paymentId: { 48 | type: String, 49 | required: true, 50 | }, 51 | createdAt: { 52 | type: Date, 53 | default: Date.now, 54 | }, 55 | Delivered: { 56 | type: Boolean, 57 | default: false, 58 | }, 59 | }); 60 | 61 | module.exports = mongoose.model("Order", orderSchema); 62 | -------------------------------------------------------------------------------- /views/user/signin.ejs: -------------------------------------------------------------------------------- 1 | <%- include ../partials/header %> <%- include ../partials/categories-navbar %> 2 | 3 |
4 |
5 |
6 |

Sign In Page

7 | 8 | <% if(errorMsg !=null && errorMsg.length>0) {%> 9 |
10 | <%=errorMsg %> 11 |
12 | <%}%> 13 |
14 |
15 | 16 | 17 |
18 |
19 | 20 | 26 |
27 | 28 | 31 |
32 |

33 | Don't have an account? Sign Up! 34 |

35 |
36 |
37 |
38 | 39 | <%- include ../partials/footer %> 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecommerce-nodejs-website", 3 | "version": "1.0.0", 4 | "main": "app.js", 5 | "description": "A virtual ecommerce website using Node js, Express js, and Mongoose.", 6 | "scripts": { 7 | "start": "node app.js", 8 | "dev": "nodemon app.js" 9 | }, 10 | "author": "Maryam Aljanabi", 11 | "license": "MIT", 12 | "dependencies": { 13 | "admin-bro": "^2.9.0", 14 | "admin-bro-expressjs": "^2.1.1", 15 | "admin-bro-mongoose": "^0.5.2", 16 | "bcrypt-nodejs": "0.0.3", 17 | "connect-flash": "^0.1.1", 18 | "connect-mongo": "^3.2.0", 19 | "cookie-parser": "^1.4.6", 20 | "csurf": "^1.11.0", 21 | "debug": "~2.6.9", 22 | "dotenv": "^8.6.0", 23 | "ejs": "~2.6.1", 24 | "ejs-lint": "^1.2.1", 25 | "express": "~4.16.1", 26 | "express-formidable": "^1.2.0", 27 | "express-session": "^1.17.2", 28 | "express-validator": "^6.14.0", 29 | "faker": "^4.1.0", 30 | "http-errors": "~1.6.3", 31 | "mapbox-gl": "^1.13.2", 32 | "moment": "^2.29.3", 33 | "mongoose": "^5.13.14", 34 | "mongoose-aggregate-paginate-v2": "^1.0.6", 35 | "mongoose-slug-updater": "^3.3.0", 36 | "morgan": "~1.9.1", 37 | "nodemailer": "^6.7.3", 38 | "nodemon": "^2.0.15", 39 | "passport": "^0.4.1", 40 | "passport-local": "^1.0.0", 41 | "react": "^16.14.0", 42 | "react-dom": "^16.14.0", 43 | "session": "^0.1.0", 44 | "slugify": "^1.6.5", 45 | "stripe": "^8.219.0", 46 | "styled-components": "^5.3.5" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /views/user/signup.ejs: -------------------------------------------------------------------------------- 1 | <%- include ../partials/header %> <%- include ../partials/categories-navbar %> 2 | 3 |
4 |
5 |
6 |

Sign Up Page

7 | <% if(errorMsg != null && errorMsg.length>0) {%> 8 |
9 | <%=errorMsg %> 10 |
11 | <%}%> 12 |
13 |
14 | 15 | 22 |
23 |
24 | 25 | 26 |
27 |
28 | 29 | 35 |
36 |
37 | 38 | 44 |
45 | 46 | 49 |
50 |
51 |
52 |
53 | 54 | <%- include ../partials/footer %> 55 | -------------------------------------------------------------------------------- /views/partials/footer.ejs: -------------------------------------------------------------------------------- 1 | 2 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /public/javascripts/checkout.js: -------------------------------------------------------------------------------- 1 | // Create a Stripe client. 2 | const stripe = Stripe("pk_test_Nw7zXh6zu9SXKrzk7KDxKUiV004Ly59ywq"); 3 | 4 | // Create an instance of Elements. 5 | const elements = stripe.elements(); 6 | 7 | const style = { 8 | base: { 9 | color: "#32325d", 10 | fontFamily: '"Helvetica Neue", Helvetica, sans-serif', 11 | fontSmoothing: "antialiased", 12 | fontSize: "16px", 13 | }, 14 | invalid: { 15 | color: "#fa755a", 16 | iconColor: "#fa755a", 17 | }, 18 | }; 19 | 20 | // Create an instance of the card Element. 21 | const card = elements.create("card", { style: style }); 22 | 23 | // Add an instance of the card Element into the `card-element`
. 24 | card.mount("#card-element"); 25 | 26 | // Handle real-time validation errors from the card Element. 27 | card.addEventListener("change", function (event) { 28 | const displayError = document.getElementById("card-errors"); 29 | if (event.error) { 30 | displayError.textContent = event.error.message; 31 | } else { 32 | displayError.textContent = ""; 33 | } 34 | }); 35 | 36 | // Handle form submission. 37 | const $form = $("#checkout-form"); 38 | 39 | $form.submit(function (event) { 40 | event.preventDefault(); 41 | $form.find("button").prop("disabled", true); 42 | 43 | const extraDetails = { 44 | name: $("#card-name").val(), 45 | }; 46 | 47 | stripe.createToken(card, extraDetails).then(function (result) { 48 | if (result.error) { 49 | $form.find("button").prop("disabled", false); // Re-enable submission 50 | } else { 51 | // Send the token to your server. 52 | stripeTokenHandler(result.token); 53 | } 54 | }); 55 | }); 56 | 57 | // Submit the form with the token ID. 58 | function stripeTokenHandler(token) { 59 | // Insert the token ID into the form so it gets submitted to the server 60 | $form.append($('').val(token.id)); 61 | // Submit the form 62 | $form.get(0).submit(); 63 | } 64 | -------------------------------------------------------------------------------- /views/user/profile.ejs: -------------------------------------------------------------------------------- 1 | <%- include ../partials/header %> 2 | <%- include ../partials/categories-navbar %> 3 | 4 |
5 | 6 |
7 |
8 | <% if (successMsg) { %> 9 |
10 | <%= successMsg %> 11 |
12 | <% } else { %> 13 |
14 | <% } %> 15 |
16 |
17 | <% if (errorMsg) { %> 18 |
19 | <%= errorMsg %> 20 |
21 | <% } else { %> 22 |
23 | <% } %> 24 |
25 |
26 | 27 |
28 |
29 | <% if (orders != null && orders.length > 0) { %> 30 |

My orders

31 | <% orders.forEach( order => { %> 32 |
33 |
34 |

35 |

    36 | <% order.cart.items.forEach( item => { %> 37 |
  • 38 | <%= item.title %> | <%= item.qty %> Units 39 | <%= item.price %> 40 |
  • 41 | <%}) %> 42 |
43 |

44 |
45 | 48 |
49 | <%}) %> 50 | <%} else { %> 51 |

You have not made any orders yet

52 | <%} %> 53 |
54 |
55 |
56 | 57 | <%- include ../partials/footer %> 58 | -------------------------------------------------------------------------------- /views/pages/shipping-policy.ejs: -------------------------------------------------------------------------------- 1 | <%- include ../partials/header %> <%- include ../partials/categories-navbar %> 2 | 3 |
4 |
5 |
6 |

Shipping Policy

7 |

8 | On this page you will find information about our shipping duration and 9 | costs. 10 |

11 | 12 |

Order Processing Duration

13 |

14 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Asperiores nam 15 | voluptatem adipisci sed amet ipsa quod reiciendis eos. Magnam ad 16 | sapiente aut a libero aliquam veritatis incidunt molestiae tempora? 17 |

18 | 19 |

Order Delivery

20 |

21 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Eum laborum 22 | dolor, totam quidem expedita impedit sequi natus molestias quae 23 | similique officiis? Eos, quas earum! Similique nobis enim unde amet 24 | suscipit. 25 |

26 | 27 |

Shipping Rates

28 |

29 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Expedita libero 30 | perspiciatis atque corrupti amet dignissimos reprehenderit excepturi aut 31 | ea distinctio mollitia nulla voluptatum deleniti, ipsum saepe soluta 32 | omnis debitis! Beatae. 33 |

34 | 35 |

Returning Products

36 |

37 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Vero, dicta. 38 | Consequatur praesentium esse aut minus ullam exercitationem nostrum! 39 | Provident minus dolores asperiores velit. Nulla, repellat magnam. Ab 40 | corrupti voluptatibus quos. 41 |

42 | 43 |

Do you have questions?

44 |

45 | Please do not hesitate to 46 | contact us through our emails or 47 | phone numbers 48 |

49 |
50 |
51 |
52 | 53 | <%- include ../partials/footer %> 54 | -------------------------------------------------------------------------------- /config/passport.js: -------------------------------------------------------------------------------- 1 | const passport = require("passport"); 2 | const LocalStrategy = require("passport-local").Strategy; 3 | const User = require("../models/user"); 4 | 5 | passport.serializeUser((user, done) => { 6 | done(null, user.id); 7 | }); 8 | 9 | passport.deserializeUser((id, done) => { 10 | User.findById(id, (err, user) => { 11 | done(err, user); 12 | }); 13 | }); 14 | 15 | passport.use( 16 | "local.signup", 17 | new LocalStrategy( 18 | { 19 | usernameField: "email", 20 | passwordField: "password", 21 | passReqToCallback: true, 22 | }, 23 | async (req, email, password, done) => { 24 | try { 25 | const user = await User.findOne({ email: email }); 26 | if (user) { 27 | return done(null, false, { message: "Email already exists" }); 28 | } 29 | if (password != req.body.password2) { 30 | return done(null, false, { message: "Passwords must match" }); 31 | } 32 | const newUser = await new User(); 33 | newUser.email = email; 34 | newUser.password = newUser.encryptPassword(password); 35 | newUser.username = req.body.name; 36 | await newUser.save(); 37 | return done(null, newUser); 38 | } catch (error) { 39 | console.log(error); 40 | return done(error); 41 | } 42 | } 43 | ) 44 | ); 45 | 46 | passport.use( 47 | "local.signin", 48 | new LocalStrategy( 49 | { 50 | usernameField: "email", 51 | passwordField: "password", 52 | passReqToCallback: false, 53 | }, 54 | async (email, password, done) => { 55 | try { 56 | const user = await User.findOne({ email: email }); 57 | if (!user) { 58 | return done(null, false, { message: "User doesn't exist" }); 59 | } 60 | if (!user.validPassword(password)) { 61 | return done(null, false, { message: "Wrong password" }); 62 | } 63 | return done(null, user); 64 | } catch (error) { 65 | console.log(error); 66 | return done(error); 67 | } 68 | } 69 | ) 70 | ); 71 | -------------------------------------------------------------------------------- /views/shop/product.ejs: -------------------------------------------------------------------------------- 1 | <%- include ../partials/header %> <%- include ../partials/categories-navbar %> 2 |
3 | 4 |
5 |
6 | <% if (successMsg) { %> 7 |
8 | <%= successMsg %> 9 |
10 | <% } else { %> 11 |
12 | <% } %> 13 |
14 |
15 | <% if (errorMsg) { %> 16 |
17 | <%= errorMsg %> 18 |
19 | <% } else { %> 20 |
21 | <% } %> 22 |
23 |
24 | 25 |
26 |
27 | product image 32 |
33 |
34 |

<%=product.title%>

35 |
36 |

USD $<%=product.price%>

37 |

Description: 38 |

<%=product.description%>

39 |

40 | 41 |

Category: <%=product.category.title%>

42 |

Brand: <%=product.manufacturer%>

43 |

Product code: <%=product.productCode%>

44 |

Date added to the store: <%= moment(product.createdAt).format( 'MMMM Do YYYY')%>

45 | <% if(product.available) {%> 46 |

In stock

47 | 48 | Add to Shopping Cart 49 | 50 | <% } else { %> 51 |

Sold out

52 | <%} %> 53 |
54 |
55 |
56 | <%- include ../partials/footer %> 57 | -------------------------------------------------------------------------------- /views/shop/checkout.ejs: -------------------------------------------------------------------------------- 1 | <%- include ../partials/header %> <%- include ../partials/categories-navbar %> 2 |
3 | 4 |
5 |
6 | <% if (errorMsg) { %> 7 |
8 | <%= errorMsg %> 9 |
10 | <% } else { %> 11 |
12 | <% } %> 13 |
14 |
15 |
16 | 17 |
18 |
19 |
20 |
21 | 22 | 23 |
24 |
25 |
26 |
27 | 28 | 35 |
36 |
37 |
38 | 39 | 40 |
41 | 42 | 43 |
44 | 45 |
46 | 49 |
50 |
51 |
52 | 53 | 54 | 55 | 56 | <%- include ../partials/footer %> 57 | -------------------------------------------------------------------------------- /config/validator.js: -------------------------------------------------------------------------------- 1 | const { check, validationResult } = require("express-validator"); 2 | 3 | const userSignUpValidationRules = () => { 4 | return [ 5 | check("name", "Name is required").not().isEmpty(), 6 | check("email", "Invalid email").not().isEmpty().isEmail(), 7 | check("password", "Please enter a password with 4 or more characters") 8 | .not() 9 | .isEmpty() 10 | .isLength({ min: 4 }), 11 | ]; 12 | }; 13 | 14 | const userSignInValidationRules = () => { 15 | return [ 16 | check("email", "Invalid email").not().isEmpty().isEmail(), 17 | check("password", "Invalid password").not().isEmpty().isLength({ min: 4 }), 18 | ]; 19 | }; 20 | 21 | const userContactUsValidationRules = () => { 22 | return [ 23 | check("name", "Please enter a name").not().isEmpty(), 24 | check("email", "Please enter a valid email address") 25 | .not() 26 | .isEmpty() 27 | .isEmail(), 28 | check("message", "Please enter a message with at least 10 words") 29 | .not() 30 | .isEmpty() 31 | .isLength({ min: 10 }), 32 | ]; 33 | }; 34 | 35 | const validateSignup = (req, res, next) => { 36 | const errors = validationResult(req); 37 | if (!errors.isEmpty()) { 38 | var messages = []; 39 | errors.array().forEach((error) => { 40 | messages.push(error.msg); 41 | }); 42 | req.flash("error", messages); 43 | return res.redirect("/user/signup"); 44 | } 45 | next(); 46 | }; 47 | 48 | const validateSignin = (req, res, next) => { 49 | const errors = validationResult(req); 50 | if (!errors.isEmpty()) { 51 | var messages = []; 52 | errors.array().forEach((error) => { 53 | messages.push(error.msg); 54 | }); 55 | req.flash("error", messages); 56 | return res.redirect("/user/signin"); 57 | } 58 | next(); 59 | }; 60 | 61 | const validateContactUs = (req, res, next) => { 62 | const errors = validationResult(req); 63 | if (!errors.isEmpty()) { 64 | var messages = []; 65 | errors.array().forEach((error) => { 66 | messages.push(error.msg); 67 | }); 68 | console.log(messages); 69 | req.flash("error", messages); 70 | return res.redirect("/pages/contact-us"); 71 | } 72 | next(); 73 | }; 74 | 75 | module.exports = { 76 | userSignUpValidationRules, 77 | userSignInValidationRules, 78 | userContactUsValidationRules, 79 | validateSignup, 80 | validateSignin, 81 | validateContactUs, 82 | }; 83 | -------------------------------------------------------------------------------- /routes/pages.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const csrf = require("csurf"); 3 | const nodemailer = require("nodemailer"); 4 | const router = express.Router(); 5 | const { 6 | userContactUsValidationRules, 7 | validateContactUs, 8 | } = require("../config/validator"); 9 | const csrfProtection = csrf(); 10 | router.use(csrfProtection); 11 | 12 | //GET: display abous us page 13 | router.get("/about-us", (req, res) => { 14 | res.render("pages/about-us", { 15 | pageName: "About Us", 16 | }); 17 | }); 18 | 19 | //GET: display shipping policy page 20 | router.get("/shipping-policy", (req, res) => { 21 | res.render("pages/shipping-policy", { 22 | pageName: "Shipping Policy", 23 | }); 24 | }); 25 | 26 | //GET: display careers page 27 | router.get("/careers", (req, res) => { 28 | res.render("pages/careers", { 29 | pageName: "Careers", 30 | }); 31 | }); 32 | 33 | //GET: display contact us page and form with csrf tokens 34 | router.get("/contact-us", (req, res) => { 35 | const successMsg = req.flash("success")[0]; 36 | const errorMsg = req.flash("error"); 37 | res.render("pages/contact-us", { 38 | pageName: "Contact Us", 39 | csrfToken: req.csrfToken(), 40 | successMsg, 41 | errorMsg, 42 | }); 43 | }); 44 | 45 | //POST: handle contact us form logic using nodemailer 46 | router.post( 47 | "/contact-us", 48 | [userContactUsValidationRules(), validateContactUs], 49 | (req, res) => { 50 | // instantiate the SMTP server 51 | const smtpTrans = nodemailer.createTransport({ 52 | host: "smtp.gmail.com", 53 | port: 587, 54 | secure: false, 55 | auth: { 56 | // company's email and password 57 | user: process.env.GMAIL_EMAIL, 58 | pass: process.env.GMAIL_PASSWORD, 59 | }, 60 | tls: { 61 | rejectUnauthorized: false, 62 | }, 63 | }); 64 | 65 | // email options 66 | const mailOpts = { 67 | from: req.body.email, 68 | to: process.env.GMAIL_EMAIL, 69 | subject: `Enquiry from ${req.body.name}`, 70 | html: ` 71 |
72 |

Client's name: ${req.body.name}

73 |

Client's email: (${req.body.email})

74 |

75 |

Client's message:

76 |
77 | ${req.body.message} 78 |
79 | `, 80 | }; 81 | 82 | // send the email 83 | smtpTrans.sendMail(mailOpts, (error, response) => { 84 | if (error) { 85 | req.flash( 86 | "error", 87 | "An error occured... Please check your internet connection and try again later" 88 | ); 89 | return res.redirect("/pages/contact-us"); 90 | } else { 91 | req.flash( 92 | "success", 93 | "Email sent successfully! Thanks for your inquiry." 94 | ); 95 | return res.redirect("/pages/contact-us"); 96 | } 97 | }); 98 | } 99 | ); 100 | 101 | module.exports = router; 102 | -------------------------------------------------------------------------------- /views/shop/shopping-cart.ejs: -------------------------------------------------------------------------------- 1 | <%- include ../partials/header %> <%- include ../partials/categories-navbar %> 2 |
3 | <% if (cart != null && !cart.paid ) { %> 4 |
5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | <% products.forEach( product => { %> <% if(product != null) {%> 21 | 22 | 29 | 40 | 45 | 50 | 55 | 56 | 57 | 64 | 65 | <% } %> <% }) %> 66 | 67 |
Product  Quantity TotalSubtotal 
23 | 26 | image thumbnail 27 | 28 | 30 | 37 | 38 | 39 | 41 | 44 | 46 |

47 | <%= product.qty %> 48 |

49 |
51 | 54 | $<%= product.price %>$<%= product.totalPrice %> 58 | Remove all 63 |
68 |
69 |
70 |
71 |
72 |
73 | Total: <%= cart.totalCost %> 74 |
75 |
76 |
77 |
78 | Checkout 79 |
80 |
81 | <% } else { %> 82 |
83 |
84 |

No items in the cart

85 |
86 |
87 | 88 | <% } %> 89 |
90 | 91 | <%- include ../partials/footer %> 92 | -------------------------------------------------------------------------------- /components/admin-order-component.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Label } from "admin-bro"; 3 | import styled from "styled-components"; 4 | 5 | const imgStyle = { 6 | width: "30em", 7 | border: "2px solid gray", 8 | borderRadius: "11px", 9 | margin: "1em auto", 10 | }; 11 | 12 | const Container = styled.div` 13 | width: 80%; 14 | background-color: #f1f1f1; 15 | border-radius: 10px; 16 | display: flex; 17 | padding: 0.5em 1em; 18 | flex-direction: row; 19 | flex-wrap: wrap; 20 | justify-content: space-between; 21 | border: 2px solid #3f3f3f; 22 | `; 23 | 24 | const Col = styled.div` 25 | flex-direction: column; 26 | flex-wrap: wrap; 27 | margin: 1em; 28 | `; 29 | 30 | const ContainerBottom = styled.div` 31 | width: 80%; 32 | background-color: #3f3f3f; 33 | color: #f6f6f6; 34 | border-radius: 10px; 35 | padding: 0.2em 1em; 36 | margin: 0.5em 0; 37 | `; 38 | 39 | const AdminShowOrder = (props) => { 40 | const { record, property } = props; 41 | const polCarts = record.params; 42 | 43 | // filter the populated array to obtain certain fields 44 | const id_key = Object.entries(polCarts).filter(([key, value]) => { 45 | if (key.includes("productId")) { 46 | return value; 47 | } 48 | }); 49 | 50 | const code_key = Object.entries(polCarts).filter(([key, value]) => { 51 | if (key.includes("productCode")) { 52 | return value; 53 | } 54 | }); 55 | 56 | const titles_key = Object.entries(polCarts).filter(([key, value]) => { 57 | if (key.includes("title")) { 58 | return value; 59 | } 60 | }); 61 | 62 | const prices_key = Object.entries(polCarts).filter(([key, value]) => { 63 | if (key.includes("price")) { 64 | return value; 65 | } 66 | }); 67 | 68 | const qtys_key = Object.entries(polCarts).filter(([key, value]) => { 69 | if (key.includes("qty")) { 70 | return value; 71 | } 72 | }); 73 | 74 | // store the extracted data into variables to display them 75 | // the id_key array is used to reference a certain product and take the admin to that product's page 76 | let count = -1; 77 | const CodesData = code_key.map((code) => { 78 | count++; 79 | return ( 80 |

81 | 86 | {code[1]} 87 | 88 |

89 | ); 90 | }); 91 | 92 | const TitlesData = titles_key.map((title) => { 93 | return

{title[1]}

; 94 | }); 95 | 96 | const QtysData = qtys_key.map((qty) => { 97 | return

{qty[1]}

; 98 | }); 99 | 100 | const PricesData = prices_key.map((price) => { 101 | return

{price[1]}

; 102 | }); 103 | 104 | return ( 105 |
106 | 107 | 108 | 109 |

Product Code

110 | {CodesData} 111 | 112 | 113 |

Title

114 | {TitlesData} 115 | 116 | 117 |

Quantity

118 | {QtysData} 119 | 120 | 121 |

Price

122 | {PricesData} 123 | 124 |
125 | 126 |

Total Number of Items: {polCarts["cart.totalQty"]}

127 |

Total Cost: {polCarts["cart.totalCost"]}

128 |
129 |
130 | ); 131 | }; 132 | 133 | export default AdminShowOrder; 134 | -------------------------------------------------------------------------------- /views/shop/index.ejs: -------------------------------------------------------------------------------- 1 | <%- include ../partials/header %> <%- include ../partials/categories-navbar %> 2 | 3 |
4 | 5 | <% if(successMsg || errorMsg) {%> 6 |
7 |
8 | <% if (successMsg) { %> 9 |
10 | <%= successMsg %> 11 |
12 | <% } else { %> 13 |
14 | <% } %> 15 |
16 |
17 | <% if (errorMsg) { %> 18 |
19 | <%= errorMsg %> 20 |
21 | <% } else { %> 22 |
23 | <% } %> 24 |
25 |
26 | <% } %> 27 | 28 |
29 |
30 | <%if (breadcrumbs) { %> 31 | 40 | <%} %> 41 |
42 |
43 | 44 | 45 | <% if(pageName == 'All Products') { %> 46 |
47 | 51 |
52 |

<%=pageName%>

53 |
54 |
55 | <%} else {%> <%categories.forEach( category => {%> <%if(pageName == 56 | category.title){%> 57 |
58 | 62 |
63 |

<%=pageName%>

64 |
65 |
66 | <%}%> <%})%> <%}%> 67 | 68 |
69 | 70 |
71 | <% products.forEach((product) => { %> 72 |
73 |
74 | 78 | Bag 80 |
81 | 85 |
<%= product.title %>
87 |
$<%= product.price %>
88 |
89 | <% if(product.available) {%> 90 | 94 | Add to Shopping Cart 95 | 96 | <% } else { %> 97 | 101 | Sold out 102 | 103 | <%} %> 104 |
105 |
106 | <% }) %> 107 |
108 | 109 | <%- include ../partials/pagination %> 110 |
111 | <%- include ../partials/footer %> 112 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | const createError = require("http-errors"); 3 | const express = require("express"); 4 | const path = require("path"); 5 | const cookieParser = require("cookie-parser"); 6 | const logger = require("morgan"); 7 | const mongoose = require("mongoose"); 8 | const session = require("express-session"); 9 | const passport = require("passport"); 10 | const flash = require("connect-flash"); 11 | const Category = require("./models/category"); 12 | var MongoStore = require("connect-mongo")(session); 13 | const connectDB = require("./config/db"); 14 | 15 | const app = express(); 16 | require("./config/passport"); 17 | 18 | // mongodb configuration 19 | connectDB(); 20 | // view engine setup 21 | app.set("views", path.join(__dirname, "views")); 22 | app.set("view engine", "ejs"); 23 | 24 | // admin route 25 | const adminRouter = require("./routes/admin"); 26 | app.use("/admin", adminRouter); 27 | 28 | app.use(logger("dev")); 29 | app.use(express.json()); 30 | app.use(express.urlencoded({ extended: false })); 31 | app.use(cookieParser()); 32 | app.use(express.static(path.join(__dirname, "public"))); 33 | app.use( 34 | session({ 35 | secret: process.env.SESSION_SECRET, 36 | resave: false, 37 | saveUninitialized: false, 38 | store: new MongoStore({ 39 | mongooseConnection: mongoose.connection, 40 | }), 41 | //session expires after 3 hours 42 | cookie: { maxAge: 60 * 1000 * 60 * 3 }, 43 | }) 44 | ); 45 | app.use(flash()); 46 | app.use(passport.initialize()); 47 | app.use(passport.session()); 48 | 49 | // global variables across routes 50 | app.use(async (req, res, next) => { 51 | try { 52 | res.locals.login = req.isAuthenticated(); 53 | res.locals.session = req.session; 54 | res.locals.currentUser = req.user; 55 | const categories = await Category.find({}).sort({ title: 1 }).exec(); 56 | res.locals.categories = categories; 57 | next(); 58 | } catch (error) { 59 | console.log(error); 60 | res.redirect("/"); 61 | } 62 | }); 63 | 64 | // add breadcrumbs 65 | get_breadcrumbs = function (url) { 66 | var rtn = [{ name: "Home", url: "/" }], 67 | acc = "", // accumulative url 68 | arr = url.substring(1).split("/"); 69 | 70 | for (i = 0; i < arr.length; i++) { 71 | acc = i != arr.length - 1 ? acc + "/" + arr[i] : null; 72 | rtn[i + 1] = { 73 | name: arr[i].charAt(0).toUpperCase() + arr[i].slice(1), 74 | url: acc, 75 | }; 76 | } 77 | return rtn; 78 | }; 79 | app.use(function (req, res, next) { 80 | req.breadcrumbs = get_breadcrumbs(req.originalUrl); 81 | next(); 82 | }); 83 | 84 | //routes config 85 | const indexRouter = require("./routes/index"); 86 | const productsRouter = require("./routes/products"); 87 | const usersRouter = require("./routes/user"); 88 | const pagesRouter = require("./routes/pages"); 89 | app.use("/products", productsRouter); 90 | app.use("/user", usersRouter); 91 | app.use("/pages", pagesRouter); 92 | app.use("/", indexRouter); 93 | 94 | // catch 404 and forward to error handler 95 | app.use(function (req, res, next) { 96 | next(createError(404)); 97 | }); 98 | 99 | // error handler 100 | app.use(function (err, req, res, next) { 101 | // set locals, only providing error in development 102 | res.locals.message = err.message; 103 | res.locals.error = req.app.get("env") === "development" ? err : {}; 104 | 105 | // render the error page 106 | res.status(err.status || 500); 107 | res.render("error"); 108 | }); 109 | 110 | var port = process.env.PORT || 3000; 111 | app.set("port", port); 112 | app.listen(port, () => { 113 | console.log("Server running at port " + port); 114 | }); 115 | 116 | module.exports = app; 117 | -------------------------------------------------------------------------------- /routes/products.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const Product = require("../models/product"); 4 | const Category = require("../models/category"); 5 | var moment = require("moment"); 6 | 7 | // GET: display all products 8 | router.get("/", async (req, res) => { 9 | const successMsg = req.flash("success")[0]; 10 | const errorMsg = req.flash("error")[0]; 11 | const perPage = 8; 12 | let page = parseInt(req.query.page) || 1; 13 | try { 14 | const products = await Product.find({}) 15 | .sort("-createdAt") 16 | .skip(perPage * page - perPage) 17 | .limit(perPage) 18 | .populate("category"); 19 | 20 | const count = await Product.count(); 21 | 22 | res.render("shop/index", { 23 | pageName: "All Products", 24 | products, 25 | successMsg, 26 | errorMsg, 27 | current: page, 28 | breadcrumbs: null, 29 | home: "/products/?", 30 | pages: Math.ceil(count / perPage), 31 | }); 32 | } catch (error) { 33 | console.log(error); 34 | res.redirect("/"); 35 | } 36 | }); 37 | 38 | // GET: search box 39 | router.get("/search", async (req, res) => { 40 | const perPage = 8; 41 | let page = parseInt(req.query.page) || 1; 42 | const successMsg = req.flash("success")[0]; 43 | const errorMsg = req.flash("error")[0]; 44 | 45 | try { 46 | const products = await Product.find({ 47 | title: { $regex: req.query.search, $options: "i" }, 48 | }) 49 | .sort("-createdAt") 50 | .skip(perPage * page - perPage) 51 | .limit(perPage) 52 | .populate("category") 53 | .exec(); 54 | const count = await Product.count({ 55 | title: { $regex: req.query.search, $options: "i" }, 56 | }); 57 | res.render("shop/index", { 58 | pageName: "Search Results", 59 | products, 60 | successMsg, 61 | errorMsg, 62 | current: page, 63 | breadcrumbs: null, 64 | home: "/products/search?search=" + req.query.search + "&", 65 | pages: Math.ceil(count / perPage), 66 | }); 67 | } catch (error) { 68 | console.log(error); 69 | res.redirect("/"); 70 | } 71 | }); 72 | 73 | //GET: get a certain category by its slug (this is used for the categories navbar) 74 | router.get("/:slug", async (req, res) => { 75 | const successMsg = req.flash("success")[0]; 76 | const errorMsg = req.flash("error")[0]; 77 | const perPage = 8; 78 | let page = parseInt(req.query.page) || 1; 79 | try { 80 | const foundCategory = await Category.findOne({ slug: req.params.slug }); 81 | const allProducts = await Product.find({ category: foundCategory.id }) 82 | .sort("-createdAt") 83 | .skip(perPage * page - perPage) 84 | .limit(perPage) 85 | .populate("category"); 86 | 87 | const count = await Product.count({ category: foundCategory.id }); 88 | 89 | res.render("shop/index", { 90 | pageName: foundCategory.title, 91 | currentCategory: foundCategory, 92 | products: allProducts, 93 | successMsg, 94 | errorMsg, 95 | current: page, 96 | breadcrumbs: req.breadcrumbs, 97 | home: "/products/" + req.params.slug.toString() + "/?", 98 | pages: Math.ceil(count / perPage), 99 | }); 100 | } catch (error) { 101 | console.log(error); 102 | return res.redirect("/"); 103 | } 104 | }); 105 | 106 | // GET: display a certain product by its id 107 | router.get("/:slug/:id", async (req, res) => { 108 | const successMsg = req.flash("success")[0]; 109 | const errorMsg = req.flash("error")[0]; 110 | try { 111 | const product = await Product.findById(req.params.id).populate("category"); 112 | res.render("shop/product", { 113 | pageName: product.title, 114 | product, 115 | successMsg, 116 | errorMsg, 117 | moment: moment, 118 | }); 119 | } catch (error) { 120 | console.log(error); 121 | return res.redirect("/"); 122 | } 123 | }); 124 | 125 | module.exports = router; 126 | -------------------------------------------------------------------------------- /views/partials/header.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <% if (pageName) { %> 5 | BestBags | <%= pageName %> 6 | <%} else {%> 7 | Shopping Cart 8 | <% } %> 9 | 14 | 15 | 20 | 21 | 27 | 28 | 33 | 34 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 49 | 50 | 51 | 52 |
53 |
54 | 117 | 118 | -------------------------------------------------------------------------------- /views/pages/contact-us.ejs: -------------------------------------------------------------------------------- 1 | <%- include ../partials/header %> <%- include ../partials/categories-navbar %> 2 | 3 |
4 |
5 |
6 |

Contact Us

7 | 8 | <% if (successMsg != null && successMsg) { %> 9 |
10 | <%= successMsg %> 11 |
12 | <% } %> 13 |
14 |
15 | 16 |
17 |

You are always welcome to contact us at anytime! Feel free to email us or phone us using our contact details below
18 | You can also visit us or call us at anytime during our working hours from Sunday to Thursday: 9AM to 12PM.
19 | We reply to our emails even during the weekends. So please do not hesitate to contact us. 20 |

21 |
22 | 23 |
24 |

25 |

Contact us by email:
26 |

27 |

28 | For general inquiries: 29 | 30 | 31 | general@email.com 32 | 33 |

34 |

35 | For orders problems: 36 | 37 | 38 | orders@email.com 39 | 40 |

41 |

42 | For technical support: 43 | 44 | 45 | support@email.com 46 | 47 |

48 |
49 | 50 |
51 |

52 |

Contact us by phone:
53 |

54 |

55 | Call center phone 1: 56 | 57 | 58 | 555-555-5555 59 | 60 |

61 |

62 | Call center phone 2: 63 | 64 | 65 | 555-555-5555 66 | 67 |

68 |

69 | Whatsapp phone: 70 | 71 | 72 | 555-555-5555 73 | 74 |

75 |
76 |
77 | 78 | 79 |
80 |
81 |

Contact Us Form

82 | 83 | <% if(errorMsg!=null && errorMsg.length>0) {%> 84 |
85 | <% errorMsg.forEach((message) => { %> 86 |

87 | <%=message %> 88 |

89 | <%}) %> 90 |
91 | <%}%> 92 |
93 |
94 | 95 | 96 |
97 |
98 | 99 | 100 |
101 |
102 | 103 | 104 |
105 | 106 | 109 |
110 |
111 |
112 |
113 |
114 | 115 | <%- include ../partials/footer %> 116 | -------------------------------------------------------------------------------- /routes/user.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const csrf = require("csurf"); 4 | var passport = require("passport"); 5 | var LocalStrategy = require("passport-local").Strategy; 6 | const Product = require("../models/product"); 7 | const Order = require("../models/order"); 8 | const Cart = require("../models/cart"); 9 | const middleware = require("../middleware"); 10 | const { 11 | userSignUpValidationRules, 12 | userSignInValidationRules, 13 | validateSignup, 14 | validateSignin, 15 | } = require("../config/validator"); 16 | const csrfProtection = csrf(); 17 | router.use(csrfProtection); 18 | 19 | // GET: display the signup form with csrf token 20 | router.get("/signup", middleware.isNotLoggedIn, (req, res) => { 21 | var errorMsg = req.flash("error")[0]; 22 | res.render("user/signup", { 23 | csrfToken: req.csrfToken(), 24 | errorMsg, 25 | pageName: "Sign Up", 26 | }); 27 | }); 28 | // POST: handle the signup logic 29 | router.post( 30 | "/signup", 31 | [ 32 | middleware.isNotLoggedIn, 33 | userSignUpValidationRules(), 34 | validateSignup, 35 | passport.authenticate("local.signup", { 36 | successRedirect: "/user/profile", 37 | failureRedirect: "/user/signup", 38 | failureFlash: true, 39 | }), 40 | ], 41 | async (req, res) => { 42 | try { 43 | //if there is cart session, save it to the user's cart in db 44 | if (req.session.cart) { 45 | const cart = await new Cart(req.session.cart); 46 | cart.user = req.user._id; 47 | await cart.save(); 48 | } 49 | // redirect to the previous URL 50 | if (req.session.oldUrl) { 51 | var oldUrl = req.session.oldUrl; 52 | req.session.oldUrl = null; 53 | res.redirect(oldUrl); 54 | } else { 55 | res.redirect("/user/profile"); 56 | } 57 | } catch (err) { 58 | console.log(err); 59 | req.flash("error", err.message); 60 | return res.redirect("/"); 61 | } 62 | } 63 | ); 64 | 65 | // GET: display the signin form with csrf token 66 | router.get("/signin", middleware.isNotLoggedIn, async (req, res) => { 67 | var errorMsg = req.flash("error")[0]; 68 | res.render("user/signin", { 69 | csrfToken: req.csrfToken(), 70 | errorMsg, 71 | pageName: "Sign In", 72 | }); 73 | }); 74 | 75 | // POST: handle the signin logic 76 | router.post( 77 | "/signin", 78 | [ 79 | middleware.isNotLoggedIn, 80 | userSignInValidationRules(), 81 | validateSignin, 82 | passport.authenticate("local.signin", { 83 | failureRedirect: "/user/signin", 84 | failureFlash: true, 85 | }), 86 | ], 87 | async (req, res) => { 88 | try { 89 | // cart logic when the user logs in 90 | let cart = await Cart.findOne({ user: req.user._id }); 91 | // if there is a cart session and user has no cart, save it to the user's cart in db 92 | if (req.session.cart && !cart) { 93 | const cart = await new Cart(req.session.cart); 94 | cart.user = req.user._id; 95 | await cart.save(); 96 | } 97 | // if user has a cart in db, load it to session 98 | if (cart) { 99 | req.session.cart = cart; 100 | } 101 | // redirect to old URL before signing in 102 | if (req.session.oldUrl) { 103 | var oldUrl = req.session.oldUrl; 104 | req.session.oldUrl = null; 105 | res.redirect(oldUrl); 106 | } else { 107 | res.redirect("/user/profile"); 108 | } 109 | } catch (err) { 110 | console.log(err); 111 | req.flash("error", err.message); 112 | return res.redirect("/"); 113 | } 114 | } 115 | ); 116 | 117 | // GET: display user's profile 118 | router.get("/profile", middleware.isLoggedIn, async (req, res) => { 119 | const successMsg = req.flash("success")[0]; 120 | const errorMsg = req.flash("error")[0]; 121 | try { 122 | // find all orders of this user 123 | allOrders = await Order.find({ user: req.user }); 124 | res.render("user/profile", { 125 | orders: allOrders, 126 | errorMsg, 127 | successMsg, 128 | pageName: "User Profile", 129 | }); 130 | } catch (err) { 131 | console.log(err); 132 | return res.redirect("/"); 133 | } 134 | }); 135 | 136 | // GET: logout 137 | router.get("/logout", middleware.isLoggedIn, (req, res) => { 138 | req.logout(); 139 | req.session.cart = null; 140 | res.redirect("/"); 141 | }); 142 | module.exports = router; 143 | -------------------------------------------------------------------------------- /routes/admin.js: -------------------------------------------------------------------------------- 1 | const AdminBro = require("admin-bro"); 2 | const AdminBroExpress = require("admin-bro-expressjs"); 3 | const AdminBroMongoose = require("admin-bro-mongoose"); 4 | const mongoose = require("mongoose"); 5 | const Product = require("../models/product"); 6 | const User = require("../models/user"); 7 | const Order = require("../models/order"); 8 | const Category = require("../models/category"); 9 | AdminBro.registerAdapter(AdminBroMongoose); 10 | 11 | const express = require("express"); 12 | const app = express(); 13 | 14 | const adminBro = new AdminBro({ 15 | databases: [mongoose], 16 | rootPath: "/admin", 17 | branding: { 18 | companyName: "BestBags", 19 | logo: "/images/shop-icon.png", 20 | softwareBrothers: false, 21 | }, 22 | resources: [ 23 | { 24 | resource: Product, 25 | options: { 26 | parent: { 27 | name: "Admin Content", 28 | icon: "InventoryManagement", 29 | }, 30 | properties: { 31 | description: { 32 | type: "richtext", 33 | isVisible: { list: false, filter: true, show: true, edit: true }, 34 | }, 35 | _id: { 36 | isVisible: { list: false, filter: true, show: true, edit: false }, 37 | }, 38 | title: { 39 | isTitle: true, 40 | }, 41 | price: { 42 | type: "number", 43 | }, 44 | imagePath: { 45 | isVisible: { list: false, filter: false, show: true, edit: true }, 46 | components: { 47 | show: AdminBro.bundle( 48 | "../components/admin-imgPath-component.jsx" 49 | ), 50 | }, 51 | }, 52 | }, 53 | }, 54 | }, 55 | { 56 | resource: User, 57 | options: { 58 | parent: { 59 | name: "User Content", 60 | icon: "User", 61 | }, 62 | properties: { 63 | _id: { 64 | isVisible: { list: false, filter: true, show: true, edit: false }, 65 | }, 66 | username: { 67 | isTitle: true, 68 | }, 69 | }, 70 | }, 71 | }, 72 | { 73 | resource: Order, 74 | options: { 75 | parent: { 76 | name: "User Content", 77 | icon: "User", 78 | }, 79 | properties: { 80 | user: { 81 | isTitle: true, 82 | }, 83 | _id: { 84 | isVisible: { list: false, filter: true, show: true, edit: false }, 85 | }, 86 | paymentId: { 87 | isVisible: { list: false, filter: true, show: true, edit: false }, 88 | }, 89 | address: { 90 | isVisible: { list: false, filter: true, show: true, edit: false }, 91 | }, 92 | createdAt: { 93 | isVisible: { list: true, filter: true, show: true, edit: false }, 94 | }, 95 | cart: { 96 | isVisible: { list: false, filter: false, show: true, edit: false }, 97 | components: { 98 | show: AdminBro.bundle("../components/admin-order-component.jsx"), 99 | }, 100 | }, 101 | "cart.items": { 102 | isVisible: { 103 | list: false, 104 | filter: false, 105 | show: false, 106 | edit: false, 107 | }, 108 | }, 109 | "cart.totalQty": { 110 | isVisible: { 111 | list: false, 112 | filter: false, 113 | show: false, 114 | edit: false, 115 | }, 116 | }, 117 | "cart.totalCost": { 118 | isVisible: { 119 | list: false, 120 | filter: false, 121 | show: false, 122 | edit: false, 123 | }, 124 | }, 125 | }, 126 | }, 127 | }, 128 | { 129 | resource: Category, 130 | options: { 131 | parent: { 132 | name: "Admin Content", 133 | icon: "User", 134 | }, 135 | properties: { 136 | _id: { 137 | isVisible: { list: false, filter: true, show: true, edit: false }, 138 | }, 139 | slug: { 140 | isVisible: { list: false, filter: false, show: false, edit: false }, 141 | }, 142 | title: { 143 | isTitle: true, 144 | }, 145 | }, 146 | }, 147 | }, 148 | ], 149 | locale: { 150 | translations: { 151 | labels: { 152 | loginWelcome: "Admin Panel Login", 153 | }, 154 | messages: { 155 | loginWelcome: 156 | "Please enter your credentials to log in and manage your website contents", 157 | }, 158 | }, 159 | }, 160 | dashboard: { 161 | component: AdminBro.bundle("../components/admin-dashboard-component.jsx"), 162 | }, 163 | }); 164 | 165 | const ADMIN = { 166 | email: process.env.ADMIN_EMAIL, 167 | password: process.env.ADMIN_PASSWORD, 168 | }; 169 | 170 | const router = AdminBroExpress.buildAuthenticatedRouter(adminBro, { 171 | authenticate: async (email, password) => { 172 | if (ADMIN.password === password && ADMIN.email === email) { 173 | return ADMIN; 174 | } 175 | return null; 176 | }, 177 | cookieName: process.env.ADMIN_COOKIE_NAME, 178 | cookiePassword: process.env.ADMIN_COOKIE_PASSWORD, 179 | }); 180 | 181 | module.exports = router; 182 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BestBags 2 | 3 | ## Table of contents 4 | 5 | - [Introduction](#introduction) 6 | - [Demo](#demo) 7 | - [Run](#run) 8 | - [Technology](#technology) 9 | - [Features](#features) 10 | - [Database Models](#database) 11 | - [Color Palette](#colors) 12 | - [License](#license) 13 | 14 | ## Introduction 15 | 16 | A virtual ecommerce website using Node js, Express js, and Mongoose. 17 | 18 | NOTE: Please read the RUN section before opening an issue. 19 | 20 | ## Demo 21 | 22 | ![screenshot](screenshot.png) 23 | 24 | The application is deployed to Heroku and can be accessed through the following link: 25 | 26 | [BestBags on Heroku](https://best-bags.herokuapp.com/) 27 | 28 | The website resembles a real store and you can add products to your cart and pay for them. If you want to try the checkout process, you can use the dummy card number provided by stripe for testing which is 4242 4242 4242 4242 with any expiration date, CVC, and zip codes. Please DO NOT provide real card number and data. 29 | 30 | In order to access the admin panel on "/admin" you need to provide the admin email and password. 31 | 32 | ## Run 33 | 34 | To run this application, you have to set your own environmental variables. For security reasons, some variables have been hidden from view and used as environmental variables with the help of dotenv package. Below are the variables that you need to set in order to run the application: 35 | 36 | - MONGO_URI: this is the connection string of your MongoDB Atlas database. 37 | 38 | - SESSION_SECRET: a secret message for the session. You can use any string here. 39 | 40 | - STRIPE_PRIVATE_KEY: the stripe package is used to process payment in the checkout route. To get this, you should set up a stripe account and put your private API key here. 41 | 42 | - GMAIL_EMAIL, GMAIL_PASSWORD: the email and password given to nodemailer to send/receive the email. Please put a real email and password here because you will receive the messages sent from the contact us form on this email. 43 | 44 | - ADMIN_EMAIL, ADMIN_PASSWORD: the email and password used to log into the admin panel using AdminBro. You can put any email and password here. 45 | 46 | - ADMIN_COOKIE_NAME, ADMIN_COOKIE_PASSWORD: the cookie name and password used in the AdminBro authentication method. You can put any strings here. 47 | 48 | After you've set these environmental variables in the .env file at the root of the project, you need to navigate to the "seedDB" folder and run "node category-seed.js" and "node products-seed.js" to fill your empty MongoDB Atlas database. 49 | 50 | Now you can run "npm start" in the terminal and the application should work. 51 | 52 | ## Technology 53 | 54 | The application is built with: 55 | 56 | - Node.js version 12.16.3 57 | - MongoDB version 4.2.0 58 | - Express version 4.16.1 59 | - Bootstrap version 4.4.1 60 | - FontAwesome version 5.13.0 61 | - Stripe API v3: used for payment in the checkout page 62 | - Mapbox API: used to show the map in the about us page 63 | - AdminBro: used and customized to implement the admin panel 64 | - Nodemailer: used to send emails from the contact us form 65 | - Passport: used for authentication 66 | - Express Validator: used for form validation 67 | 68 | ## Features 69 | 70 | The application displays a virtual bags store that contains virtual products and contact information. 71 | 72 | Users can do the following: 73 | 74 | - Create an account, login or logout 75 | - Browse available products added by the admin 76 | - Add products to the shopping cart 77 | - Delete products from the shopping cart 78 | - Display the shopping cart 79 | - To checkout, a user must be logged in 80 | - Checkout information is processed using stripe and the payment is send to the admin 81 | - The profile contains all the orders a user has made 82 | 83 | Admins can do the following: 84 | 85 | - Login or logout to the admin panel 86 | - View all the information stored in the database. They can view/add/edit/delete orders, users, products and categories. The cart model cannot be modified by an admin because a cart is either modified by the logged in user before the purchase or deleted after the purchase. 87 | 88 | ## Database 89 | 90 | All the models can be found in the models directory created using mongoose. 91 | 92 | ### User Schema: 93 | 94 | - username (String) 95 | - email (String) 96 | - password (String) 97 | 98 | ### Category Schema: 99 | 100 | - title (String) 101 | - slug (String) 102 | 103 | ### Product Schema: 104 | 105 | - productCode (String) 106 | - title (String) 107 | - imagePath (String) 108 | - description (String) 109 | - price (Number) 110 | - category (ObjectId - a reference to the category schema) 111 | - manufacturer (String) 112 | - available (Boolean) 113 | - createdAt (Date) 114 | 115 | ### Cart Schema: 116 | 117 | - items: an array of objects, each object contains:
118 | ~ productId (ObjectId - a reference to the product schema)
119 | ~ qty (Number)
120 | ~ price (Number)
121 | ~ title (String)
122 | ~ productCode (Number)
123 | - totalQty (Number) 124 | - totalCost (Number) 125 | - user (ObjectId - a reference to the user schema) 126 | - createdAt 127 |

128 | \*\*The reason for including the title, price, and productCode again in the items object is AdminBro. If we are to write our own admin interface, we can remove them and instead populate a product field using the product id. However, AdminBro doesn't populate deep levels, so we had to repeat these fields in the items array in order to display them in the admin panel. 129 | 130 | ### Order Schema: 131 | 132 | - user (ObjectId - a reference to the user schema) 133 | - cart (instead of a reference, we had to structure an object identical to the cart schema because of AdminBro, so we can display the cart's contents in the admin interface under each order) 134 | - address (String) 135 | - paymentId (String) 136 | - createdAt (Date) 137 | - Delivered (Boolean) 138 | 139 | ## Colors 140 | 141 | Below is the color palette used in this application: 142 | 143 | - ![#478ba2](https://via.placeholder.com/15/478ba2/000000?text=+) `#478ba2` 144 | - ![#b9d4db](https://via.placeholder.com/15/b9d4db/000000?text=+) `#b9d4db` 145 | - ![#e9765b](https://via.placeholder.com/15/e9765b/000000?text=+) `#e9765b` 146 | - ![#f2a490](https://via.placeholder.com/15/f2a490/000000?text=+) `#f2a490` 147 | - ![#de5b6d](https://via.placeholder.com/15/de5b6d/000000?text=+) `#de5b6d` 148 | - ![#18a558](https://via.placeholder.com/15/18a558/000000?text=+) `#18a558` 149 | - ![#f9f7f4](https://via.placeholder.com/15/f9f7f4/000000?text=+) `#f9f7f4` 150 | - ![#202020](https://via.placeholder.com/15/202020/000000?text=+) `#202020` 151 | - ![#474747](https://via.placeholder.com/15/474747/000000?text=+) `#474747` 152 | 153 | ## License 154 | 155 | [![License](https://img.shields.io/:License-MIT-blue.svg?style=flat-square)](http://badges.mit-license.org) 156 | 157 | - MIT License 158 | - Copyright 2020 © [Maryam Aljanabi](https://github.com/maryamaljanabi) 159 | -------------------------------------------------------------------------------- /views/shop/home.ejs: -------------------------------------------------------------------------------- 1 | <%- include ../partials/header %> <%- include ../partials/categories-navbar%> 2 | 3 | 4 | 44 | 45 | 46 |
47 |

Store's Categories

48 | <% if (categories) { %> 49 |
50 | <% for(let i=0; i<3; i++){ %> 51 | 57 | <%} %> 58 |
59 | 60 | 61 | 62 |
63 |
64 | <%}%> 65 |
66 | 67 |
68 | 69 | 70 |
71 | 72 | 243 | 244 |
245 | 246 | <%- include ../partials/footer %> 247 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const csrf = require("csurf"); 3 | const stripe = require("stripe")(process.env.STRIPE_PRIVATE_KEY); 4 | const Product = require("../models/product"); 5 | const Category = require("../models/category"); 6 | const Cart = require("../models/cart"); 7 | const Order = require("../models/order"); 8 | const middleware = require("../middleware"); 9 | const router = express.Router(); 10 | 11 | const csrfProtection = csrf(); 12 | router.use(csrfProtection); 13 | 14 | // GET: home page 15 | router.get("/", async (req, res) => { 16 | try { 17 | const products = await Product.find({}) 18 | .sort("-createdAt") 19 | .populate("category"); 20 | res.render("shop/home", { pageName: "Home", products }); 21 | } catch (error) { 22 | console.log(error); 23 | res.redirect("/"); 24 | } 25 | }); 26 | 27 | // GET: add a product to the shopping cart when "Add to cart" button is pressed 28 | router.get("/add-to-cart/:id", async (req, res) => { 29 | const productId = req.params.id; 30 | try { 31 | // get the correct cart, either from the db, session, or an empty cart. 32 | let user_cart; 33 | if (req.user) { 34 | user_cart = await Cart.findOne({ user: req.user._id }); 35 | } 36 | let cart; 37 | if ( 38 | (req.user && !user_cart && req.session.cart) || 39 | (!req.user && req.session.cart) 40 | ) { 41 | cart = await new Cart(req.session.cart); 42 | } else if (!req.user || !user_cart) { 43 | cart = new Cart({}); 44 | } else { 45 | cart = user_cart; 46 | } 47 | 48 | // add the product to the cart 49 | const product = await Product.findById(productId); 50 | const itemIndex = cart.items.findIndex((p) => p.productId == productId); 51 | if (itemIndex > -1) { 52 | // if product exists in the cart, update the quantity 53 | cart.items[itemIndex].qty++; 54 | cart.items[itemIndex].price = cart.items[itemIndex].qty * product.price; 55 | cart.totalQty++; 56 | cart.totalCost += product.price; 57 | } else { 58 | // if product does not exists in cart, find it in the db to retrieve its price and add new item 59 | cart.items.push({ 60 | productId: productId, 61 | qty: 1, 62 | price: product.price, 63 | title: product.title, 64 | productCode: product.productCode, 65 | }); 66 | cart.totalQty++; 67 | cart.totalCost += product.price; 68 | } 69 | 70 | // if the user is logged in, store the user's id and save cart to the db 71 | if (req.user) { 72 | cart.user = req.user._id; 73 | await cart.save(); 74 | } 75 | req.session.cart = cart; 76 | req.flash("success", "Item added to the shopping cart"); 77 | res.redirect(req.headers.referer); 78 | } catch (err) { 79 | console.log(err.message); 80 | res.redirect("/"); 81 | } 82 | }); 83 | 84 | // GET: view shopping cart contents 85 | router.get("/shopping-cart", async (req, res) => { 86 | try { 87 | // find the cart, whether in session or in db based on the user state 88 | let cart_user; 89 | if (req.user) { 90 | cart_user = await Cart.findOne({ user: req.user._id }); 91 | } 92 | // if user is signed in and has cart, load user's cart from the db 93 | if (req.user && cart_user) { 94 | req.session.cart = cart_user; 95 | return res.render("shop/shopping-cart", { 96 | cart: cart_user, 97 | pageName: "Shopping Cart", 98 | products: await productsFromCart(cart_user), 99 | }); 100 | } 101 | // if there is no cart in session and user is not logged in, cart is empty 102 | if (!req.session.cart) { 103 | return res.render("shop/shopping-cart", { 104 | cart: null, 105 | pageName: "Shopping Cart", 106 | products: null, 107 | }); 108 | } 109 | // otherwise, load the session's cart 110 | return res.render("shop/shopping-cart", { 111 | cart: req.session.cart, 112 | pageName: "Shopping Cart", 113 | products: await productsFromCart(req.session.cart), 114 | }); 115 | } catch (err) { 116 | console.log(err.message); 117 | res.redirect("/"); 118 | } 119 | }); 120 | 121 | // GET: reduce one from an item in the shopping cart 122 | router.get("/reduce/:id", async function (req, res, next) { 123 | // if a user is logged in, reduce from the user's cart and save 124 | // else reduce from the session's cart 125 | const productId = req.params.id; 126 | let cart; 127 | try { 128 | if (req.user) { 129 | cart = await Cart.findOne({ user: req.user._id }); 130 | } else if (req.session.cart) { 131 | cart = await new Cart(req.session.cart); 132 | } 133 | 134 | // find the item with productId 135 | let itemIndex = cart.items.findIndex((p) => p.productId == productId); 136 | if (itemIndex > -1) { 137 | // find the product to find its price 138 | const product = await Product.findById(productId); 139 | // if product is found, reduce its qty 140 | cart.items[itemIndex].qty--; 141 | cart.items[itemIndex].price -= product.price; 142 | cart.totalQty--; 143 | cart.totalCost -= product.price; 144 | // if the item's qty reaches 0, remove it from the cart 145 | if (cart.items[itemIndex].qty <= 0) { 146 | await cart.items.remove({ _id: cart.items[itemIndex]._id }); 147 | } 148 | req.session.cart = cart; 149 | //save the cart it only if user is logged in 150 | if (req.user) { 151 | await cart.save(); 152 | } 153 | //delete cart if qty is 0 154 | if (cart.totalQty <= 0) { 155 | req.session.cart = null; 156 | await Cart.findByIdAndRemove(cart._id); 157 | } 158 | } 159 | res.redirect(req.headers.referer); 160 | } catch (err) { 161 | console.log(err.message); 162 | res.redirect("/"); 163 | } 164 | }); 165 | 166 | // GET: remove all instances of a single product from the cart 167 | router.get("/removeAll/:id", async function (req, res, next) { 168 | const productId = req.params.id; 169 | let cart; 170 | try { 171 | if (req.user) { 172 | cart = await Cart.findOne({ user: req.user._id }); 173 | } else if (req.session.cart) { 174 | cart = await new Cart(req.session.cart); 175 | } 176 | //fnd the item with productId 177 | let itemIndex = cart.items.findIndex((p) => p.productId == productId); 178 | if (itemIndex > -1) { 179 | //find the product to find its price 180 | cart.totalQty -= cart.items[itemIndex].qty; 181 | cart.totalCost -= cart.items[itemIndex].price; 182 | await cart.items.remove({ _id: cart.items[itemIndex]._id }); 183 | } 184 | req.session.cart = cart; 185 | //save the cart it only if user is logged in 186 | if (req.user) { 187 | await cart.save(); 188 | } 189 | //delete cart if qty is 0 190 | if (cart.totalQty <= 0) { 191 | req.session.cart = null; 192 | await Cart.findByIdAndRemove(cart._id); 193 | } 194 | res.redirect(req.headers.referer); 195 | } catch (err) { 196 | console.log(err.message); 197 | res.redirect("/"); 198 | } 199 | }); 200 | 201 | // GET: checkout form with csrf token 202 | router.get("/checkout", middleware.isLoggedIn, async (req, res) => { 203 | const errorMsg = req.flash("error")[0]; 204 | 205 | if (!req.session.cart) { 206 | return res.redirect("/shopping-cart"); 207 | } 208 | //load the cart with the session's cart's id from the db 209 | cart = await Cart.findById(req.session.cart._id); 210 | 211 | const errMsg = req.flash("error")[0]; 212 | res.render("shop/checkout", { 213 | total: cart.totalCost, 214 | csrfToken: req.csrfToken(), 215 | errorMsg, 216 | pageName: "Checkout", 217 | }); 218 | }); 219 | 220 | // POST: handle checkout logic and payment using Stripe 221 | router.post("/checkout", middleware.isLoggedIn, async (req, res) => { 222 | if (!req.session.cart) { 223 | return res.redirect("/shopping-cart"); 224 | } 225 | const cart = await Cart.findById(req.session.cart._id); 226 | stripe.charges.create( 227 | { 228 | amount: cart.totalCost * 100, 229 | currency: "usd", 230 | source: req.body.stripeToken, 231 | description: "Test charge", 232 | }, 233 | function (err, charge) { 234 | if (err) { 235 | req.flash("error", err.message); 236 | console.log(err); 237 | return res.redirect("/checkout"); 238 | } 239 | const order = new Order({ 240 | user: req.user, 241 | cart: { 242 | totalQty: cart.totalQty, 243 | totalCost: cart.totalCost, 244 | items: cart.items, 245 | }, 246 | address: req.body.address, 247 | paymentId: charge.id, 248 | }); 249 | order.save(async (err, newOrder) => { 250 | if (err) { 251 | console.log(err); 252 | return res.redirect("/checkout"); 253 | } 254 | await cart.save(); 255 | await Cart.findByIdAndDelete(cart._id); 256 | req.flash("success", "Successfully purchased"); 257 | req.session.cart = null; 258 | res.redirect("/user/profile"); 259 | }); 260 | } 261 | ); 262 | }); 263 | 264 | // create products array to store the info of each product in the cart 265 | async function productsFromCart(cart) { 266 | let products = []; // array of objects 267 | for (const item of cart.items) { 268 | let foundProduct = ( 269 | await Product.findById(item.productId).populate("category") 270 | ).toObject(); 271 | foundProduct["qty"] = item.qty; 272 | foundProduct["totalPrice"] = item.price; 273 | products.push(foundProduct); 274 | } 275 | return products; 276 | } 277 | 278 | module.exports = router; 279 | -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --dark-blue: #478ba2; 3 | --light-blue: #b9d4db; 4 | --dark-orange: #e9765b; 5 | --light-orange: #f2a490; 6 | --dark-pink: #de5b6d; 7 | --green: #18a558; 8 | --ivory: #f9f7f4; 9 | --black: #202020; 10 | --dark-gray: #474747; 11 | } 12 | * { 13 | margin: 0px; 14 | padding: 0px; 15 | box-sizing: border-box; 16 | } 17 | 18 | body { 19 | font-family: 9px "Poppins", sans-serif; 20 | } 21 | 22 | html { 23 | scroll-behavior: smooth; 24 | } 25 | 26 | /* NAVBAR STYLE */ 27 | a, 28 | .logo-container a, 29 | .right-nav a { 30 | color: var(--black); 31 | text-decoration: none !important; 32 | } 33 | 34 | a:hover, 35 | .logo-container a:hover, 36 | .right-nav a:hover { 37 | color: var(--dark-gray) !important; 38 | text-decoration: none; 39 | } 40 | 41 | .nav-container { 42 | margin: auto; 43 | padding: 0.5em 1em; 44 | display: flex; 45 | justify-content: center; 46 | color: var(--black); 47 | } 48 | 49 | .search-container { 50 | width: 70%; 51 | } 52 | .right-nav { 53 | font-size: 16px; 54 | display: flex; 55 | flex-direction: row; 56 | justify-content: flex-end; 57 | align-items: center; 58 | margin-left: auto !important; 59 | padding-left: auto !important; 60 | } 61 | 62 | .right-nav ul li { 63 | display: inline; 64 | padding: 0.3em; 65 | } 66 | 67 | .logo-container { 68 | display: flex; 69 | flex-direction: row; 70 | justify-content: center; 71 | transform: translateX(3em); 72 | } 73 | .logo-container a { 74 | display: flex; 75 | flex-direction: row; 76 | justify-content: center; 77 | } 78 | 79 | .logo-title { 80 | font-weight: 600; 81 | margin: 4px 1px; 82 | } 83 | 84 | .logo-img { 85 | max-height: 3em; 86 | } 87 | 88 | /* CATEGORIES NAVBAR */ 89 | 90 | .navbar-custom { 91 | background-color: var(--dark-blue); 92 | } 93 | 94 | .navbar-custom .navbar-brand, 95 | .navbar-custom .navbar-text, 96 | .navbar-custom .navbar-nav .nav-link { 97 | color: var(--ivory); 98 | } 99 | 100 | .navbar-custom .nav-item .nav-link:hover, 101 | .navbar-custom .nav-item.active .nav-link { 102 | color: var(--light-blue) !important; 103 | } 104 | 105 | /* SEARCH BOX */ 106 | 107 | .search-title { 108 | font-size: 22px; 109 | font-weight: 900; 110 | text-align: center; 111 | color: #ff8b88; 112 | } 113 | 114 | .search-input { 115 | width: 100%; 116 | padding: 12px 24px; 117 | background-color: transparent; 118 | transition: transform 250ms ease-in-out; 119 | font-size: 14px; 120 | line-height: 18px; 121 | color: #575756; 122 | background-color: transparent; 123 | /* background-image: url(http://mihaeltomic.com/codepen/input-search/ic_search_black_24px.svg); */ 124 | 125 | background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E"); 126 | background-repeat: no-repeat; 127 | background-size: 18px 18px; 128 | background-position: 95% center; 129 | border-radius: 50px; 130 | border: 1px solid #575756; 131 | transition: all 250ms ease-in-out; 132 | backface-visibility: hidden; 133 | transform-style: preserve-3d; 134 | } 135 | 136 | .search-input { 137 | color: color(#575756 a(0.8)); 138 | letter-spacing: 1.5px; 139 | } 140 | 141 | .search-input:hover, 142 | .search-input:focus { 143 | padding: 12px 0; 144 | outline: 0; 145 | border: 1px solid transparent; 146 | border-bottom: 1px solid #575756; 147 | border-radius: 0; 148 | background-position: 100% center; 149 | } 150 | 151 | /* HOME PAGE */ 152 | 153 | .circle-img { 154 | object-fit: cover; 155 | width: 200px; 156 | height: 200px; 157 | border-radius: 50%; 158 | cursor: pointer; 159 | transition: all 0.2s linear; 160 | -webkit-transition: all 0.2s linear; 161 | } 162 | .circle-img:hover { 163 | transform: scale(1.1); 164 | -ms-transform: scale(1.1); 165 | -webkit-transform: scale(1.1); 166 | box-shadow: 5px 5px 5px var(--dark-gray); 167 | } 168 | 169 | .fa-chevron-circle-right { 170 | font-size: 50px; 171 | color: var(--dark-blue); 172 | margin-top: 1.5em; 173 | border-radius: 55%; 174 | box-shadow: 5px 5px 5px var(--dark-gray); 175 | transition: all 0.2s linear; 176 | } 177 | 178 | .fa-chevron-circle-right:hover { 179 | transform: scale(1.4); 180 | } 181 | 182 | hr.gradient-style { 183 | border: 0; 184 | height: 1px; 185 | background-image: linear-gradient( 186 | to right, 187 | rgba(0, 0, 0, 0), 188 | rgba(0, 0, 0, 0.75), 189 | rgba(0, 0, 0, 0) 190 | ); 191 | margin: 1em auto; 192 | } 193 | 194 | /* PRODUCTS INDEX PAGE */ 195 | 196 | .title-link { 197 | color: var(--black); 198 | text-decoration: none; 199 | } 200 | 201 | .title-link:hover { 202 | color: var(--dark-gray); 203 | text-decoration: none; 204 | } 205 | 206 | .button-style { 207 | background-color: var(--dark-blue); 208 | color: var(--ivory) !important; 209 | } 210 | 211 | .button-style:hover { 212 | background-color: var(--light-blue); 213 | color: var(--black) !important; 214 | } 215 | 216 | .button-style-danger { 217 | background-color: var(--dark-orange); 218 | color: var(--ivory) !important; 219 | } 220 | 221 | .button-style-danger:hover { 222 | background-color: var(--light-orange); 223 | color: var(--black) !important; 224 | } 225 | 226 | .price { 227 | color: var(--dark-pink); 228 | font-weight: bold; 229 | font-size: 20px; 230 | margin: 1px 0px 6px 0px; 231 | } 232 | 233 | .product-index-box { 234 | padding: 0.5em; 235 | margin-bottom: 1em; 236 | text-align: center; 237 | } 238 | 239 | .product-index-box .btn { 240 | width: 90%; 241 | margin: 0.3em auto; 242 | } 243 | 244 | .product-index-box img { 245 | width: 90%; 246 | height: 20em; 247 | object-fit: cover; 248 | border: 1px solid #c2c2c2; 249 | border-radius: 5px; 250 | } 251 | 252 | /* BANNER IMAGE */ 253 | .hero-container { 254 | display: grid; 255 | overflow: hidden; 256 | } 257 | 258 | .hero-image, 259 | .hero-text { 260 | grid-column: 1; 261 | grid-row: 1; 262 | } 263 | 264 | .hero-text { 265 | color: white; 266 | text-align: center; 267 | padding-top: 10%; 268 | background-image: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)); 269 | } 270 | 271 | /* SINGLE PRODUCT PAGE */ 272 | 273 | .product-img { 274 | border: 1px solid var(--dark-gray); 275 | border-radius: 7px; 276 | } 277 | 278 | .is-not-available { 279 | background-color: var(--dark-orange); 280 | width: 8em; 281 | color: var(--ivory); 282 | font-weight: bold; 283 | font-size: 12px; 284 | padding: 0.3em; 285 | text-transform: uppercase; 286 | } 287 | 288 | .is-available { 289 | background-color: var(--green); 290 | width: 8em; 291 | color: var(--ivory); 292 | font-weight: bold; 293 | font-size: 12px; 294 | padding: 0.3em; 295 | text-transform: uppercase; 296 | } 297 | 298 | /* PAGINATION STYLE */ 299 | .pagination .active .page-link { 300 | color: var(--ivory); 301 | background-color: var(--dark-blue); 302 | } 303 | 304 | .pagination .page-link { 305 | color: var(--dark-blue); 306 | } 307 | 308 | /* BREADCRUMBS STYLE */ 309 | .breadcrumb-item a:hover { 310 | color: var(--dark-blue) !important; 311 | } 312 | 313 | /* SHOPPING CART STYLES */ 314 | .container.cart { 315 | margin: 1em auto; 316 | } 317 | 318 | .img-small { 319 | height: 5%; 320 | margin: 2px; 321 | } 322 | 323 | .qty-display { 324 | padding: 3px; 325 | border: 1px solid rgb(54, 54, 54); 326 | border-radius: 5px; 327 | } 328 | 329 | .fa-plus-square { 330 | color: var(--green); 331 | font-size: 25px; 332 | margin: 2px; 333 | } 334 | 335 | .fa-minus-square { 336 | color: var(--dark-orange); 337 | font-size: 25px; 338 | margin: 2px; 339 | } 340 | 341 | .fa-shopping-cart { 342 | font-size: 22px; 343 | padding-top: 0.3em; 344 | } 345 | 346 | /* FOOTER */ 347 | 348 | ul { 349 | list-style: none; 350 | } 351 | 352 | .white-links { 353 | margin-left: 2.5em; 354 | } 355 | .white-links a { 356 | font-size: 14px; 357 | color: var(--ivory); 358 | margin-right: 1em; 359 | } 360 | .white-links a:hover { 361 | color: var(--light-blue) !important; 362 | } 363 | .footer-title { 364 | font-weight: 300; 365 | color: whitesmoke; 366 | text-transform: uppercase; 367 | margin-bottom: 0.3em; 368 | } 369 | #contact { 370 | text-align: left; 371 | vertical-align: middle; 372 | align-items: center; 373 | color: var(--ivory); 374 | background-color: var(--black); 375 | padding: 1em 1em 0.2em 1em; 376 | border-bottom: 5px var(--dark-blue) solid; 377 | } 378 | .bottom-footer { 379 | font-size: 14px; 380 | padding: 1em 1.5em; 381 | color: var(--ivory); 382 | background-color: #1a1a1a; 383 | } 384 | 385 | #page-container { 386 | position: relative; 387 | min-height: 100vh; 388 | } 389 | 390 | #content-wrap { 391 | padding-bottom: 2.5rem; 392 | } 393 | 394 | footer { 395 | position: absolute; 396 | bottom: 0; 397 | width: 100%; 398 | height: 2.5rem; 399 | } 400 | 401 | /* STRIPE ELEMENTS FOR CARD PAYMENT */ 402 | .StripeElement { 403 | box-sizing: border-box; 404 | width: 100%; 405 | height: 40px; 406 | margin-bottom: 6px; 407 | padding: 10px 12px; 408 | 409 | border: 1px solid rgb(207, 207, 207); 410 | border-radius: 4px; 411 | background-color: white; 412 | 413 | box-shadow: 0 1px 3px 0 #e6ebf1; 414 | -webkit-transition: box-shadow 150ms ease; 415 | transition: box-shadow 150ms ease; 416 | } 417 | 418 | .StripeElement--focus { 419 | box-shadow: 0 1px 3px 0 #cfd7df; 420 | } 421 | 422 | .StripeElement--invalid { 423 | border-color: #fa755a; 424 | } 425 | 426 | .StripeElement--webkit-autofill { 427 | background-color: #fefde5 !important; 428 | } 429 | 430 | /* MEDIA QUERY */ 431 | /* XS SCREENS */ 432 | @media only screen and (max-width: 576px) { 433 | .card img { 434 | height: 17em; 435 | } 436 | .controls-top h3 { 437 | font-size: 20px; 438 | } 439 | .controls-top .fa { 440 | font-size: 12px; 441 | } 442 | .hero-text h1 { 443 | font-size: 25px; 444 | } 445 | } 446 | 447 | /* SM SCREENS */ 448 | @media only screen and (max-width: 768px) { 449 | .right-nav { 450 | justify-content: center; 451 | } 452 | .search-container { 453 | width: 80%; 454 | margin: auto; 455 | } 456 | .logo-container { 457 | transform: translateX(0em); 458 | margin: 0.5em auto; 459 | } 460 | .white-links { 461 | margin-left: 0px; 462 | } 463 | .hero-text { 464 | padding-top: 8%; 465 | } 466 | } 467 | 468 | /* MD SCREENS */ 469 | @media only screen and (min-width: 768px) and (max-width: 992px) { 470 | .col-md-3 { 471 | margin: 10px; 472 | } 473 | .circle-img { 474 | width: 150px; 475 | height: 150px; 476 | } 477 | } 478 | -------------------------------------------------------------------------------- /seedDB/products-seed.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | require("dotenv").config({ path: path.join(__dirname, "../.env") }); 3 | const Product = require("../models/product"); 4 | const Category = require("../models/category"); 5 | const mongoose = require("mongoose"); 6 | const faker = require("faker"); 7 | const connectDB = require("./../config/db"); 8 | connectDB(); 9 | 10 | async function seedDB() { 11 | faker.seed(0); 12 | 13 | //----------------------Backpacks 14 | const backpacks_titles = [ 15 | "Classic Blue Backpack", 16 | "Black Fjallraven Backpack", 17 | "Brown and Green Leather Backpack", 18 | "Grey Stylish Backpack", 19 | "Elegant Black Backpack", 20 | "Practical Blue Backpack With Leather Straps", 21 | "Soft Classic Biege Backpack", 22 | "Practical Durable Backpack", 23 | "Comfortable Laptop Backpack", 24 | "Extra Large Grey Backpack", 25 | ]; 26 | const backpacks_imgs = [ 27 | "https://images.unsplash.com/photo-1553062407-98eeb64c6a62?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=334&q=80", 28 | "https://images.unsplash.com/photo-1562546106-b9cb3a76a206?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1534&q=80", 29 | "https://images.unsplash.com/photo-1577733966973-d680bffd2e80?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1500&q=80", 30 | "https://images.unsplash.com/photo-1546938576-6e6a64f317cc?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1400&q=80", 31 | "https://images.unsplash.com/photo-1585916420730-d7f95e942d43?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1534&q=80", 32 | "https://images.pexels.com/photos/2905238/pexels-photo-2905238.jpeg?auto=compress&cs=tinysrgb&dpr=3&h=750&w=1260", 33 | "https://images.pexels.com/photos/2422476/pexels-photo-2422476.png?auto=compress&cs=tinysrgb&dpr=3&h=750&w=1260", 34 | "https://images.pexels.com/photos/1545998/pexels-photo-1545998.jpeg?auto=compress&cs=tinysrgb&dpr=3&h=750&w=1260", 35 | "https://live.staticflickr.com/3428/3361015646_303a2d0571_b.jpg", 36 | "https://storage.needpix.com/rsynced_images/backpack-2634622_1280.jpg", 37 | ]; 38 | 39 | //--------------------Travel Bags 40 | const travel_titles = [ 41 | "Stylish Pastel Pink Travel Bag", 42 | "A Fahionable Set of Two Pink Travel Bags", 43 | "White and Black Hard Luggage", 44 | "Rainbow Dotted Duffle Bag Luggage", 45 | "Blue and Gray Classic Suitcase", 46 | "A Set of Three Hard Durable Suitcases", 47 | "Light Blue Hard Luggage", 48 | "Black Leather Vintage Suitcase", 49 | "A Set of Three Large Travel Bags", 50 | "Two Stylish Light Green Travel Bags With Different Sizes", 51 | "Simple Blue Luggage with Many Compartments", 52 | ]; 53 | 54 | const travel_imgs = [ 55 | "https://p1.pxfuel.com/preview/899/786/420/travel-bag-hard-and-bag.jpg", 56 | "https://p1.pxfuel.com/preview/479/120/981/luggage-metallic-luguagge-case.jpg", 57 | "https://images.unsplash.com/photo-1565026057447-bc90a3dceb87?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1534&q=80", 58 | "https://cdn.pixabay.com/photo/2019/06/20/16/10/duffle-bag-4287485_960_720.png", 59 | "https://p0.pikrepo.com/preview/74/133/blue-and-gray-suede-rolling-luggage-thumbnail.jpg", 60 | "https://cdn.pixabay.com/photo/2019/01/22/15/53/suitcases-3948389_960_720.png", 61 | "https://cdn.pixabay.com/photo/2019/07/09/11/52/travel-bag-4326738_960_720.jpg", 62 | "https://p0.pxfuel.com/preview/942/496/984/various-bag-bags-luggage.jpg", 63 | "https://p0.pxfuel.com/preview/273/580/962/travelvarious-bag-bags-holiday.jpg", 64 | "https://p1.pxfuel.com/preview/926/897/247/travel-bag-hard-and-bag.jpg", 65 | "https://p0.pxfuel.com/preview/963/699/697/bag-blue-handbag-white.jpg", 66 | ]; 67 | 68 | //--------------------Briefcases 69 | const briefcases_titles = [ 70 | "Aluminium Metal Suitcase", 71 | "Black Leather Durable Suitcase", 72 | ]; 73 | 74 | const briefcases_imgs = [ 75 | "https://upload.wikimedia.org/wikipedia/commons/6/6d/Aluminium_Briefcase.jpg", 76 | "http://res.freestockphotos.biz/pictures/1/1751-black-leather-briefcase-on-a-white-background-pv.jpg", 77 | ]; 78 | 79 | //--------------------Mini Bags 80 | const miniBags_titles = [ 81 | "Pink Leather Crossbody Bag", 82 | "Stylish Pink Crossbody Bag", 83 | "Mini Black Carra Shoulder Bag", 84 | "White Leather Mini Bag with Crossbody Strap", 85 | "Blue Jeans Mini Bag", 86 | "Biege Be Dior Mini Bag with Crossbody Strap", 87 | "Red Be Dior Mini Bag with Crossbody Strap", 88 | "Light Blue Mini Bag with Golden Strap", 89 | "Light Green Mini Bag with Golden Strap", 90 | "Pastel Pink Mini Bag with Golden Strap", 91 | "Biege Leather Crossbody Bag", 92 | "White Leather Crossbody Bag", 93 | "Elegant White Mini Bag with Silver Strap", 94 | "Simple Red Mini Bag", 95 | ]; 96 | const miniBags_imgs = [ 97 | "https://images.unsplash.com/photo-1566150905458-1bf1fc113f0d?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1502&q=80", 98 | "https://upload.wikimedia.org/wikipedia/commons/b/bc/DKNY_Mini_Flap_Crossbody_W_-_SS_Crossbody_R1513004_Kalbsleder_beige_%281%29_%2816080518124%29.jpg", 99 | "https://p1.pxfuel.com/preview/177/215/691/handbag-bag-today-the-postwoman-fashion-style-skin.jpg", 100 | "https://p2.piqsels.com/preview/392/1016/905/handbags-white-fashion-bag-shoulder-bag.jpg", 101 | "https://c.pxhere.com/photos/37/cb/camera_bag_scene_package_fashion-900156.jpg!d", 102 | "https://c.pxhere.com/photos/94/0e/bag_dior_x_n-867928.jpg!d", 103 | "https://c.pxhere.com/photos/92/ad/bag_dior_u-867943.jpg!d", 104 | "https://c.pxhere.com/photos/5b/ea/bag_fashion_style-518819.jpg!d", 105 | "https://c.pxhere.com/photos/19/aa/bag_fashion_style-518820.jpg!d", 106 | "https://c.pxhere.com/photos/41/9e/bag_fashion_style-518821.jpg!d", 107 | "https://c.pxhere.com/photos/24/f9/bag_fashion_style-518803.jpg!d", 108 | "https://c.pxhere.com/photos/16/e8/bag_fashion_style-518804.jpg!d", 109 | "https://images.unsplash.com/photo-1564422167509-4f8763ff046e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1534&q=80", 110 | "https://c.pxhere.com/photos/87/f0/bag_crimson_product_photos_padlock_bag_women_bags_dot_white-1000331.jpg!d", 111 | ]; 112 | 113 | //--------------------Large Handags 114 | 115 | const largeHandbags_titles = [ 116 | "Elegant Shiny Brown Leather Handbag", 117 | "Black Leather Handbag with Golden Chains", 118 | "Elegant Black Leather Handbag", 119 | "Stylish Blue Handbag with its Purse", 120 | "A set of Two Elegant Handbags", 121 | "Practical Blue Leather Handbag with its Purse", 122 | "Simple Black Leather Handbag", 123 | "Golden Leather Handbag", 124 | "Shiny Black Leather Handbag", 125 | "Gray and Yellow Flowery Shoulder Bag", 126 | "Blue and Brown Leather Handbag with Shoulder Strap", 127 | ]; 128 | const largeHandbags_imgs = [ 129 | "https://c.pxhere.com/photos/a8/b7/handbag_purse_fashion_bag_female_style_women_elegance-703150.jpg!d", 130 | "https://c.pxhere.com/photos/b6/5c/handbag_purse_fashion_bag_female_women_accessory_modern-703145.jpg!d", 131 | "https://c.pxhere.com/photos/4b/82/handbag_purse_fashion_bag_female_style_women_lady-703156.jpg!d", 132 | "https://images.unsplash.com/photo-1564422170194-896b89110ef8?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1534&q=80", 133 | "https://images.unsplash.com/photo-1564222256577-45e728f2c611?ixlib=rb-1.2.1&auto=format&fit=crop&w=1500&q=80", 134 | "https://p1.pxfuel.com/preview/680/478/429/online-shopping-lisaswardrobe-handbags-shopping.jpg", 135 | "https://p1.pxfuel.com/preview/762/878/334/handbag-black-gold.jpg", 136 | "https://p1.pxfuel.com/preview/550/178/484/bag-handbag-haberdashery.jpg", 137 | "https://p1.pxfuel.com/preview/5/396/904/package-briefcase-leather-bags.jpg", 138 | "https://p1.pxfuel.com/preview/843/210/542/vera-bradley-purse-handbag-shoulder-bag.jpg", 139 | "https://p1.pxfuel.com/preview/57/634/392/purse-bag-handbag-fashion.jpg", 140 | ]; 141 | 142 | //-----------------------Purses 143 | const purses_titles = [ 144 | "Hot Pink Leather Purse", 145 | "Glittery Black Purse with Golden Strap", 146 | "Practical Black Leather Purse", 147 | "Red Leather Pouche with Free Earrings", 148 | "Lavender Leather Purse", 149 | "White and Black Snakeskin Purse", 150 | "Dark Brown Simple Purse", 151 | "Red Kipling Pouche", 152 | "Biege Kipling Pouche", 153 | ]; 154 | const purses_imgs = [ 155 | "https://c.pxhere.com/photos/c2/fc/bag_fashion_style-518806.jpg!d", 156 | "https://images.unsplash.com/photo-1564222195116-8a74a96b2c8c?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1534&q=80", 157 | "https://c.pxhere.com/photos/cb/9e/wallet_black_clutch_purse_leather_fashion_style_accessory-952715.jpg!d", 158 | "https://c.pxhere.com/photos/63/90/purse_handbag_fashion_bag_style_design_leather_accessory-780266.jpg!d", 159 | "https://c.pxhere.com/photos/2d/da/wallet_purple_wallet_purple_money_purse_billfold_lavender_fashion-863005.jpg!d", 160 | "https://images.unsplash.com/photo-1563904092230-7ec217b65fe2?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1534&q=80", 161 | "https://www.publicdomainpictures.net/pictures/60000/velka/leather-purse-isolated-background.jpg", 162 | "https://c.pxhere.com/photos/94/29/bag_handbag_purse_pink_red_fashion_glamour_accessory-952105.jpg!d", 163 | "https://c.pxhere.com/photos/9b/57/bag_purse_handbag_fashion_style_accessory_white-1336949.jpg!d", 164 | ]; 165 | 166 | //-----------------Totes 167 | 168 | const totes_titles = [ 169 | "Plain White Cotton Tote", 170 | "Elegant Red Leather Tote", 171 | "Handmade Embroided White Tote with Red Roses", 172 | "Multicolored White Tote", 173 | "Owl White Cotton Tote", 174 | "Simple Grey Zipped Tote", 175 | "Earth Positive Tote Bag", 176 | "Deep Purple Handstamped Tote", 177 | "White Cotton Tote with Drawings", 178 | "Grey Wolf Tote", 179 | "Yellow and Green Bold Tote", 180 | ]; 181 | const totes_imgs = [ 182 | "https://p1.pxfuel.com/preview/1021/986/529/bag-cotton-cotton-bag-textile-wall-white.jpg", 183 | "https://p1.pxfuel.com/preview/741/996/910/handbag-fashion-fashionable-woman.jpg", 184 | "https://p1.pxfuel.com/preview/58/205/88/shop-bag-bags-sale.jpg", 185 | "https://p1.pxfuel.com/preview/367/279/652/bag-bag-elephant-cloth-bag.jpg", 186 | "https://p0.pikrepo.com/preview/627/393/white-blue-and-red-owl-print-tote-bag.jpg", 187 | "https://farm5.staticflickr.com/4022/4714518639_8d9e06be13_b.jpg", 188 | "https://live.staticflickr.com/3538/3674472019_727d8c4669.jpg", 189 | "https://live.staticflickr.com/5161/5342130557_7fa8cc5935_b.jpg", 190 | "https://p1.pxfuel.com/preview/368/540/34/bag-cotton-natural-cotton-bag-advertising-royalty-free-thumbnail.jpg", 191 | "https://p1.pxfuel.com/preview/726/975/813/bag-handbag-womans-bag-sport-bag.jpg", 192 | "https://p1.pxfuel.com/preview/844/198/547/bag-burlap-advertising.jpg", 193 | ]; 194 | 195 | async function seedProducts(titlesArr, imgsArr, categStr) { 196 | try { 197 | const categ = await Category.findOne({ title: categStr }); 198 | for (let i = 0; i < titlesArr.length; i++) { 199 | let prod = new Product({ 200 | productCode: faker.helpers.replaceSymbolWithNumber("####-##########"), 201 | title: titlesArr[i], 202 | imagePath: imgsArr[i], 203 | description: faker.lorem.paragraph(), 204 | price: faker.random.number({ min: 10, max: 50 }), 205 | manufacturer: faker.company.companyName(0), 206 | available: true, 207 | category: categ._id, 208 | }); 209 | await prod.save(); 210 | } 211 | } catch (error) { 212 | console.log(error); 213 | return error; 214 | } 215 | } 216 | 217 | async function closeDB() { 218 | console.log("CLOSING CONNECTION"); 219 | await mongoose.disconnect(); 220 | } 221 | 222 | await seedProducts(backpacks_titles, backpacks_imgs, "Backpacks"); 223 | await seedProducts(briefcases_titles, briefcases_imgs, "Briefcases"); 224 | await seedProducts(travel_titles, travel_imgs, "Travel"); 225 | await seedProducts(miniBags_titles, miniBags_imgs, "Mini Bags"); 226 | await seedProducts( 227 | largeHandbags_titles, 228 | largeHandbags_imgs, 229 | "Large Handbags" 230 | ); 231 | await seedProducts(purses_titles, purses_imgs, "Purses"); 232 | await seedProducts(totes_titles, totes_imgs, "Totes"); 233 | 234 | await closeDB(); 235 | } 236 | 237 | seedDB(); 238 | --------------------------------------------------------------------------------