├── .gitignore ├── 1.gif ├── 2.gif ├── README.md ├── backend ├── app.js ├── controllers │ ├── authController.js │ ├── orderController.js │ └── userController.js ├── database │ └── db.js ├── middleware │ └── validation.js ├── package-lock.json ├── package.json ├── routes │ ├── auth.js │ ├── index.js │ ├── orders.js │ ├── products.js │ └── users.js ├── services │ ├── authService.js │ ├── orderService.js │ └── userService.js └── sql_dump.sql └── client ├── .browserslistrc ├── .editorconfig ├── .gitignore ├── README.md ├── angular.json ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── auth │ │ ├── auth.module.ts │ │ └── components │ │ │ ├── login │ │ │ ├── login.component.html │ │ │ ├── login.component.scss │ │ │ ├── login.component.spec.ts │ │ │ └── login.component.ts │ │ │ └── register │ │ │ ├── register.component.html │ │ │ ├── register.component.scss │ │ │ ├── register.component.spec.ts │ │ │ └── register.component.ts │ ├── cart │ │ ├── cart.component.html │ │ ├── cart.component.scss │ │ ├── cart.component.spec.ts │ │ └── cart.component.ts │ ├── checkout │ │ ├── checkout.component.html │ │ ├── checkout.component.scss │ │ ├── checkout.component.spec.ts │ │ └── checkout.component.ts │ ├── footer │ │ ├── footer.component.html │ │ ├── footer.component.scss │ │ ├── footer.component.spec.ts │ │ └── footer.component.ts │ ├── guards │ │ ├── auth-guard.service.spec.ts │ │ └── auth-guard.service.ts │ ├── header │ │ ├── header.component.html │ │ ├── header.component.scss │ │ ├── header.component.spec.ts │ │ └── header.component.ts │ ├── home │ │ ├── home.component.html │ │ ├── home.component.scss │ │ ├── home.component.spec.ts │ │ └── home.component.ts │ ├── order-history │ │ ├── order-history.component.html │ │ ├── order-history.component.scss │ │ ├── order-history.component.spec.ts │ │ └── order-history.component.ts │ ├── product-card │ │ ├── product-card.component.html │ │ ├── product-card.component.scss │ │ ├── product-card.component.spec.ts │ │ └── product-card.component.ts │ ├── product │ │ ├── product.component.html │ │ ├── product.component.scss │ │ ├── product.component.spec.ts │ │ └── product.component.ts │ ├── profile │ │ ├── profile.component.html │ │ ├── profile.component.scss │ │ ├── profile.component.spec.ts │ │ └── profile.component.ts │ ├── services │ │ ├── api.service.spec.ts │ │ ├── api.service.ts │ │ ├── auth.service.spec.ts │ │ ├── auth.service.ts │ │ ├── cart.service.spec.ts │ │ ├── cart.service.ts │ │ ├── interceptor.service.spec.ts │ │ ├── interceptor.service.ts │ │ ├── product.service.spec.ts │ │ ├── product.service.ts │ │ ├── token-storage.service.spec.ts │ │ └── token-storage.service.ts │ └── shared │ │ ├── models │ │ └── product.model.ts │ │ └── shared.module.ts ├── assets │ ├── .gitkeep │ ├── 1.jpg │ ├── 2.jpg │ ├── 3.jpg │ └── 4.jpg ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.scss ├── test.ts └── theme.less ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | /backend/node_modules 2 | /backend/env -------------------------------------------------------------------------------- /1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelparkadze/angular-ecommerce-app/2ce1bfd45da4e4d8543e4af332828c20341e9f16/1.gif -------------------------------------------------------------------------------- /2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelparkadze/angular-ecommerce-app/2ce1bfd45da4e4d8543e4af332828c20341e9f16/2.gif -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular Ecommerce Web App 2 | 3 | I made this project to learn and understand the basics of both Angular and MySQL. It was not fun at first but once I got the hang of it it was quite pleasant. In addition to Angular and MySQL, this project is built using Ant Design for its UI and SwiperJS for its carousels. The backend is built using Node.js, Express.js, Joi for input validation, and JWT for authentication. I did want to provide a demo link for now there will be instructions on how to set up this project on your local machine. 4 | 5 |
6 | 7 | ![](1.gif) 8 | ![](2.gif) 9 | 10 | ## Functionality 11 | 12 | The application allows you to browse the home page for products, check out each of the details of the product and add them to your cart for a later checkout. 13 | 14 | Adding products to your cart will make a notification pop up and indicate that the product was added successfully. 15 | 16 | You are able to see a preview of the cart on the top bar or even navigate into a more detailed cart page. 17 | 18 | The checkout process is a multi-page form that at the end allows you to place an order that will later be shown on the order history page. 19 | 20 |
21 | 22 | ## Getting Started 23 | 24 | The app can be installed by cloning the git repository 25 | 26 | ``` 27 | git clone https://github.com/michaelparkadze/angular-ecommerce-app.git folder-name 28 | ``` 29 | 30 | Then cd into both directories and run npm install 31 | 32 | ``` 33 | cd folder-name 34 | cd backend 35 | npm run install 36 | cd.. // return to folder-name 37 | cd client 38 | npm run install 39 | ``` 40 | 41 |
42 | 43 | After the entire installation you need to run the server and the client by running this commands each in its own directory 44 | 45 | **backend** 46 | 47 | ``` 48 | npm run dev 49 | ``` 50 | 51 | **client** 52 | 53 | ``` 54 | ng serve 55 | ``` 56 | 57 |
58 | 59 | ## Prerequisites 60 | 61 | You will need to have node and npm installed. In addition you will need a MySQL server running in order to have full functionality of the application 62 | 63 |
64 | 65 | ## Environment Variables 66 | 67 | Create a folder called env in the root of the backend directory and create a development.env file. 68 | 69 | **Change DB variables to match your MySQL setup** 70 | 71 | ``` 72 | PORT=5000 73 | DB_HOST='localhost' 74 | DB_USER='root' 75 | DB_PASSWORD='' 76 | DB_NAME='myapp' 77 | ``` 78 | -------------------------------------------------------------------------------- /backend/app.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const dotenv = require("dotenv"); 3 | const path = require("path"); 4 | const logger = require("morgan"); 5 | const cors = require("cors"); 6 | 7 | // Config .env file 8 | dotenv.config({ 9 | path: path.join(__dirname, `env/${process.env.NODE_ENV}.env`), 10 | }); 11 | 12 | // Initialize express app 13 | const app = express(); 14 | 15 | // Middleware 16 | app.use(cors()); 17 | app.use(logger("dev")); 18 | app.use(express.json()); 19 | app.use(express.urlencoded({ extended: false })); 20 | 21 | // Router index 22 | const indexRouter = require("./routes/index"); 23 | app.use("/", indexRouter); 24 | 25 | // Health check 26 | app.get("/", (req, res) => { 27 | res.status(200).send("Health Check"); 28 | }); 29 | 30 | const PORT = process.env.PORT || 5000; 31 | const ENV = process.env.NODE_ENV || null; 32 | 33 | app.listen(PORT, () => { 34 | console.log(`Server is running on port ${PORT} using ${ENV} env.`); 35 | }); 36 | -------------------------------------------------------------------------------- /backend/controllers/authController.js: -------------------------------------------------------------------------------- 1 | const { registerUser, loginUser } = require("../services/authService"); 2 | 3 | exports.login_user = async (req, res, next) => { 4 | const { email, password } = req.body; 5 | 6 | loginUser({ email, password }) 7 | .then((result) => { 8 | console.log(result); 9 | const { statusCode = 200, message, data, token } = result; 10 | res.status(statusCode).send({ message, data, token }); 11 | }) 12 | .catch((err) => { 13 | const { statusCode = 400, message, data } = err; 14 | res.status(statusCode).send({ message, data }) && next(err); 15 | }); 16 | }; 17 | 18 | exports.register_user = async (req, res, next) => { 19 | const { fullName, email, password } = req.body; 20 | 21 | registerUser({ fullName, email, password }) 22 | .then((result) => { 23 | const { statusCode = 200, message, data, token } = result; 24 | res.status(statusCode).send({ message, data, token }); 25 | }) 26 | .catch((err) => { 27 | const { statusCode = 400, message, data } = err; 28 | res.status(statusCode).send({ message, data }) && next(err); 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /backend/controllers/orderController.js: -------------------------------------------------------------------------------- 1 | const { 2 | getOrders, 3 | getSingleOrder, 4 | createOrder, 5 | } = require("../services/orderService"); 6 | 7 | exports.create_order = async (req, res, next) => { 8 | const { userId, cart } = req.body; 9 | createOrder({ userId, cart }) 10 | .then((result) => { 11 | res.status(result.statusCode).send({ ...result }); 12 | }) 13 | .catch((err) => { 14 | const { statusCode = 400, message } = err; 15 | res.status(statusCode).send({ message }) && next(err); 16 | }); 17 | }; 18 | 19 | exports.get_single_order = async (req, res, next) => { 20 | const { orderId, userId } = req.query; 21 | getSingleOrder({ orderId, userId }) 22 | .then((result) => { 23 | const { message, data } = result; 24 | res.status(200).send({ message, data }); 25 | }) 26 | .catch((err) => { 27 | const { statusCode = 400, message } = err; 28 | res.status(statusCode).send({ message }) && next(err); 29 | }); 30 | }; 31 | 32 | exports.get_orders = async (req, res, next) => { 33 | const { userId } = req.query; 34 | getOrders({ userId }) 35 | .then((result) => { 36 | const { message, data } = result; 37 | res.status(200).send({ message, data }); 38 | }) 39 | .catch((err) => { 40 | const { statusCode = 400, message } = err; 41 | res.status(statusCode).send({ message }) && next(err); 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /backend/controllers/userController.js: -------------------------------------------------------------------------------- 1 | const { updateUser } = require("../services/userService"); 2 | 3 | exports.update_user = async (req, res, next) => { 4 | const { userId } = req.params; 5 | const { fullName, email, password } = req.body; 6 | 7 | updateUser({ userId, fullName, email, password }) 8 | .then((result) => { 9 | const { statusCode = 200, message, data } = result; 10 | res.status(statusCode).send({ message, data }); 11 | }) 12 | .catch((err) => { 13 | const { statusCode = 400, message, data } = err; 14 | res.status(statusCode).send({ message, data }) && next(err); 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /backend/database/db.js: -------------------------------------------------------------------------------- 1 | const mysql = require("mysql"); 2 | 3 | const connection = mysql.createConnection({ 4 | host: process.env.DB_HOST, 5 | user: process.env.DB_USER, 6 | password: process.env.DB_PASSWORD, 7 | database: process.env.DB_NAME, 8 | }); 9 | 10 | connection.connect((err) => { 11 | if (err) console.log(err); 12 | else console.log("MySQL is connected..."); 13 | }); 14 | 15 | module.exports = connection; 16 | -------------------------------------------------------------------------------- /backend/middleware/validation.js: -------------------------------------------------------------------------------- 1 | const Joi = require("joi"); 2 | 3 | var options = { 4 | errors: { 5 | wrap: { 6 | label: "", 7 | }, 8 | }, 9 | }; 10 | 11 | const registerValidation = (data) => { 12 | const schema = Joi.object({ 13 | fullName: Joi.string().required().strict(), 14 | email: Joi.string().email().required().strict(), 15 | password: Joi.string().min(6).required().strict(), 16 | }); 17 | 18 | return schema.validate(data, options); 19 | }; 20 | 21 | const loginValidation = (data) => { 22 | const schema = Joi.object({ 23 | email: Joi.string().email().required().strict(), 24 | password: Joi.string().min(6).required().strict(), 25 | }); 26 | 27 | return schema.validate(data, options); 28 | }; 29 | 30 | const updateUserValidation = (data) => { 31 | const schema = Joi.object({ 32 | userId: Joi.string().required().strict(), 33 | email: Joi.string().email().required().strict(), 34 | password: Joi.string().min(6).required().strict(), 35 | fullName: Joi.string().required().strict(), 36 | }); 37 | 38 | return schema.validate(data, options); 39 | }; 40 | 41 | module.exports = { 42 | registerValidation, 43 | loginValidation, 44 | updateUserValidation, 45 | }; 46 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "NODE_ENV=production node app", 8 | "dev": "NODE_ENV=development nodemon app" 9 | }, 10 | "author": "Michael Parkadze", 11 | "license": "ISC", 12 | "dependencies": { 13 | "cors": "^2.8.5", 14 | "dotenv": "^8.2.0", 15 | "express": "^4.17.1", 16 | "joi": "^17.4.0", 17 | "jsonwebtoken": "^8.5.1", 18 | "md5": "^2.3.0", 19 | "morgan": "^1.10.0", 20 | "mysql": "^2.18.1", 21 | "nodemon": "^2.0.7" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /backend/routes/auth.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const authController = require("../controllers/authController"); 4 | 5 | router.post("/register", authController.register_user); 6 | 7 | router.post("/login", authController.login_user); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /backend/routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | 4 | const authRoute = require("./auth"); 5 | const usersRoute = require("./users"); 6 | const productsRoute = require("./products"); 7 | const ordersRoute = require("./orders"); 8 | 9 | router.use("/api/v1/auth", authRoute); 10 | router.use("/api/v1/users", usersRoute); 11 | router.use("/api/v1/products", productsRoute); 12 | router.use("/api/v1/orders", ordersRoute); 13 | 14 | module.exports = router; 15 | -------------------------------------------------------------------------------- /backend/routes/orders.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const orderController = require("../controllers/orderController"); 4 | 5 | router.get("/", orderController.get_orders); 6 | 7 | router.get("/single", orderController.get_single_order); 8 | 9 | router.post("/create", orderController.create_order); 10 | 11 | module.exports = router; 12 | -------------------------------------------------------------------------------- /backend/routes/products.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const db = require("../database/db"); 4 | 5 | // GET ALL PRODUCTS 6 | router.get("/", async (req, res) => { 7 | const { page = 1, limit = 10 } = req.query; 8 | 9 | let startValue; 10 | let endValue; 11 | 12 | if (page > 0) { 13 | startValue = page * limit - limit; // 0,10,20,30 14 | endValue = page * limit; 15 | } else { 16 | startValue = 0; 17 | endValue = 10; 18 | } 19 | 20 | db.query( 21 | `SELECT p.id, p.title, p.image, p.price, p.short_desc, p.quantity, 22 | c.title as category FROM products p JOIN categories c ON 23 | c.id = p.cat_id LIMIT ${startValue}, ${limit}`, 24 | (err, results) => { 25 | if (err) console.log(err); 26 | else res.json(results); 27 | } 28 | ); 29 | }); 30 | 31 | // GET SINGLE PRODUCT BY ID 32 | router.get("/:productId", async (req, res) => { 33 | const { productId } = req.params; 34 | db.query( 35 | `SELECT p.id, p.title, p.image, p.images, p.description, p.price, p.quantity, p.short_desc, 36 | c.title as category FROM products p JOIN categories c ON 37 | c.id = p.cat_id WHERE p.id = ${productId}`, 38 | (err, results) => { 39 | if (err) console.log(err); 40 | else res.json(results[0]); 41 | } 42 | ); 43 | }); 44 | 45 | module.exports = router; 46 | -------------------------------------------------------------------------------- /backend/routes/users.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const db = require("../database/db"); 4 | const userController = require("../controllers/userController"); 5 | 6 | // Get all users 7 | router.get("/", (req, res) => { 8 | db.query("SELECT * FROM users", (err, results) => { 9 | if (err) console.log(err); 10 | else res.json(results); 11 | }); 12 | }); 13 | 14 | router.put("/:userId", userController.update_user); 15 | 16 | module.exports = router; 17 | -------------------------------------------------------------------------------- /backend/services/authService.js: -------------------------------------------------------------------------------- 1 | const { 2 | loginValidation, 3 | registerValidation, 4 | } = require("../middleware/validation"); 5 | const db = require("../database/db"); 6 | const jwt = require("jsonwebtoken"); 7 | const md5 = require("md5"); 8 | 9 | exports.loginUser = async (params) => { 10 | const { error } = loginValidation(params); 11 | if (error) throw { message: error.details[0].message, statusCode: 400 }; 12 | 13 | const { email, password } = params; 14 | const hashedPassword = md5(password.toString()); 15 | 16 | return new Promise((resolve, reject) => { 17 | db.query( 18 | "SELECT * FROM users WHERE email = ? AND password = ?", 19 | [email, hashedPassword], 20 | (err, result) => { 21 | if (err) { 22 | reject({ 23 | data: err, 24 | message: "Something went wrong, please try again", 25 | statusCode: 400, 26 | }); 27 | } 28 | 29 | if (result.length === 0) { 30 | reject({ 31 | message: "Wrong credentials, please try again", 32 | statusCode: 400, 33 | }); 34 | } 35 | 36 | if (result.length > 0) { 37 | const token = jwt.sign({ data: result }, "secret"); 38 | resolve({ 39 | message: "Logged in successfully", 40 | data: result, 41 | token, 42 | }); 43 | } 44 | } 45 | ); 46 | }); 47 | }; 48 | 49 | exports.registerUser = async (params) => { 50 | const { error } = registerValidation(params); 51 | if (error) throw { message: error.details[0].message, statusCode: 400 }; 52 | 53 | const { fullName, email, password } = params; 54 | const hashedPassword = md5(password.toString()); 55 | 56 | return new Promise((resolve, reject) => { 57 | db.query( 58 | `SELECT email FROM users WHERE email = ?`, 59 | [email], 60 | (err, result) => { 61 | if (result.length > 0) { 62 | reject({ 63 | message: "Email address is in use, please try a different one", 64 | statusCode: 400, 65 | }); 66 | } else if (result.length === 0) { 67 | db.query( 68 | `INSERT INTO users (fname, email, password) VALUES (?,?,?)`, 69 | [fullName, email, hashedPassword], 70 | (err, result) => { 71 | if (err) { 72 | reject({ 73 | message: "Something went wrong, please try again", 74 | statusCode: 400, 75 | data: err, 76 | }); 77 | } else { 78 | const token = jwt.sign({ data: result }, "secret"); 79 | resolve({ 80 | data: result, 81 | message: "You have successfully registered.", 82 | token: token, 83 | statusCode: 200, 84 | }); 85 | } 86 | } 87 | ); 88 | } 89 | } 90 | ); 91 | }); 92 | }; 93 | -------------------------------------------------------------------------------- /backend/services/orderService.js: -------------------------------------------------------------------------------- 1 | const db = require("../database/db"); 2 | 3 | exports.createOrder = async (params) => { 4 | const { userId, cart } = params; 5 | 6 | if (!cart) throw { message: "cart was not provided", statusCode: 400 }; 7 | if (!userId) throw { message: "userId was not provided", statusCode: 400 }; 8 | 9 | return new Promise((resolve, reject) => { 10 | db.query( 11 | `INSERT INTO orders (user_id) VALUES (?)`, 12 | [userId], 13 | (err, result) => { 14 | if (err) reject({ message: err, statusCode: 500 }); 15 | 16 | if (result) { 17 | let newOrderId = result.insertId; 18 | cart.products.forEach(async (prod) => { 19 | db.query( 20 | `SELECT p.quantity FROM products p WHERE p.id = ?`, 21 | [prod.id], 22 | (err, result) => { 23 | if (err) reject({ message: err, statusCode: 500 }); 24 | 25 | let productQuantity = result[0].quantity; // db product 26 | 27 | // deduct the quantity from products that were ordered in db 28 | let updatedQuantity = productQuantity - prod.quantity; 29 | if (updatedQuantity > 0) { 30 | productQuantity = updatedQuantity; 31 | } else productQuantity = 0; 32 | 33 | db.query( 34 | `INSERT INTO orders_details (order_id, product_id, quantity) VALUES (?,?,?)`, 35 | [newOrderId, prod.id, prod.quantity], 36 | (err, result) => { 37 | if (err) reject({ message: err, statusCode: 500 }); 38 | 39 | db.query( 40 | `UPDATE products SET quantity = ${productQuantity} WHERE id = ${prod.id}`, 41 | (err, result) => { 42 | if (err) reject({ message: err, statusCode: 500 }); 43 | console.log(result); 44 | } 45 | ); 46 | } 47 | ); 48 | } 49 | ); 50 | }); 51 | 52 | resolve({ 53 | message: `Order was successfully placed with order id ${newOrderId}`, 54 | orderId: newOrderId, 55 | products: cart.products, 56 | statusCode: 201, 57 | }); 58 | } else { 59 | reject({ 60 | message: "New order failed while adding order details", 61 | statusCode: 500, 62 | }); 63 | } 64 | } 65 | ); 66 | }); 67 | }; 68 | 69 | exports.getSingleOrder = async (params) => { 70 | const { orderId, userId } = params; 71 | 72 | if (!orderId) throw { message: "orderId was not provided", statusCode: 400 }; 73 | if (!userId) throw { message: "userId was not provided", statusCode: 400 }; 74 | 75 | return new Promise((resolve, reject) => { 76 | db.query( 77 | `SELECT * FROM orders INNER JOIN orders_details ON ( orders.id = orders_details.order_id ) WHERE orders.id = ? AND orders.user_id = ?`, 78 | [orderId, userId], 79 | (err, result) => { 80 | if (err) reject({ message: err, statusCode: 500 }); 81 | 82 | if (result.length === 0) 83 | reject({ message: "order was not found", statusCode: 400 }); 84 | 85 | resolve({ 86 | statusCode: 200, 87 | message: `Order was found`, 88 | data: result, 89 | }); 90 | } 91 | ); 92 | }); 93 | }; 94 | 95 | exports.getOrders = async (params) => { 96 | const { userId } = params; 97 | 98 | if (!userId) throw { message: "userId was not provided", statusCode: 400 }; 99 | 100 | return new Promise((resolve, reject) => { 101 | db.query( 102 | `SELECT * FROM orders INNER JOIN orders_details ON ( orders.id = orders_details.order_id ) WHERE user_id = ?`, 103 | [userId], 104 | (err, result) => { 105 | if (err) reject({ message: err, statusCode: 500 }); 106 | 107 | if (result.length === 0) 108 | reject({ message: "No order were found", statusCode: 400 }); 109 | 110 | resolve({ 111 | statusCode: 200, 112 | message: `${result.length} orders were found`, 113 | data: result, 114 | }); 115 | } 116 | ); 117 | }); 118 | }; 119 | -------------------------------------------------------------------------------- /backend/services/userService.js: -------------------------------------------------------------------------------- 1 | const { updateUserValidation } = require("../middleware/validation"); 2 | const db = require("../database/db"); 3 | const md5 = require("md5"); 4 | 5 | exports.updateUser = async (params) => { 6 | const { error } = updateUserValidation(params); 7 | if (error) throw { message: error.details[0].message, statusCode: 400 }; 8 | 9 | const { userId, fullName, email, password } = params; 10 | const hashedPassword = md5(password.toString()); 11 | 12 | return new Promise((resolve, reject) => { 13 | db.query( 14 | `SELECT * FROM users WHERE user_id = ? AND password = ?`, 15 | [userId, hashedPassword], 16 | (err, result) => { 17 | if (err) reject({ message: err, statusCode: 500 }); 18 | 19 | if (result.length === 0) { 20 | reject({ 21 | message: "Wrong credentials, please try again", 22 | statusCode: 400, 23 | }); 24 | } else { 25 | if (email === result[0].email && fullName === result[0].fname) { 26 | reject({ 27 | message: "No new data has been provided", 28 | statusCode: 400, 29 | }); 30 | } 31 | 32 | let query = ""; 33 | 34 | if (email !== result[0].email && fullName !== result[0].fname) { 35 | query = `fname = '${fullName}', email = '${email}'`; 36 | } else if (email !== result[0].email) { 37 | query = `email = '${email}'`; 38 | } else { 39 | query = `fname = '${fullName}'`; 40 | } 41 | 42 | db.query( 43 | `UPDATE users SET ${query} WHERE user_id = ?`, 44 | [userId], 45 | (err, result) => { 46 | if (err) throw { message: err, statusCode: 500 }; 47 | resolve({ 48 | message: "User details have been successfully updated", 49 | data: result, 50 | }); 51 | } 52 | ); 53 | } 54 | } 55 | ); 56 | }); 57 | }; 58 | -------------------------------------------------------------------------------- /backend/sql_dump.sql: -------------------------------------------------------------------------------- 1 | -- phpMyAdmin SQL Dump 2 | -- version 4.9.1 3 | -- https://www.phpmyadmin.net/ 4 | -- 5 | -- Host: 127.0.0.1 6 | -- Generation Time: Apr 20, 2020 at 09:26 PM 7 | -- Server version: 8.0.17 8 | -- PHP Version: 7.3.8 9 | 10 | SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; 11 | SET AUTOCOMMIT = 0; 12 | START TRANSACTION; 13 | SET time_zone = "+00:00"; 14 | 15 | 16 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 17 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 18 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 19 | /*!40101 SET NAMES utf8mb4 */; 20 | 21 | -- 22 | -- Database: `angular-ecommerce-app` 23 | -- 24 | 25 | -- -------------------------------------------------------- 26 | 27 | -- 28 | -- Table structure for table `addresses` 29 | -- 30 | 31 | CREATE TABLE `addresses` ( 32 | `id` int(11) NOT NULL, 33 | `line1` varchar(255) DEFAULT NULL, 34 | `line2` varchar(255) DEFAULT NULL, 35 | `city` varchar(45) DEFAULT NULL, 36 | `state` varchar(45) DEFAULT NULL, 37 | `street_name` varchar(45) DEFAULT NULL, 38 | `country` varchar(45) DEFAULT NULL, 39 | `phone` varchar(10) DEFAULT NULL, 40 | `pincode` int(6) DEFAULT NULL, 41 | `user_id` int(11) NOT NULL 42 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 43 | 44 | -- 45 | -- Dumping data for table `addresses` 46 | -- 47 | 48 | INSERT INTO `addresses` (`id`, `line1`, `line2`, `state`, `city`, `street_name`, `country`, `phone`, `pincode`, `user_id`) VALUES 49 | (1, 'Test Address', 'Test Address', 'Rheinland-Pfalz', 'Heiligenroth', 'Stuttgarter Platz 10', 'Germany', '9855698981', 110045, 1), 50 | (2, 'Test Address 2', 'Test Address 2', 'Rheinland-Pfalz', 'Heiligenroth', ' Stuttgarter Platz 3', 'Germany', '9855698981', 560100, 1); 51 | 52 | -- -------------------------------------------------------- 53 | 54 | -- 55 | -- Table structure for table `categories` 56 | -- 57 | 58 | CREATE TABLE `categories` ( 59 | `id` int(11) NOT NULL, 60 | `title` varchar(255) NOT NULL 61 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 62 | 63 | -- 64 | -- Dumping data for table `categories` 65 | -- 66 | 67 | INSERT INTO `categories` (`id`, `title`) VALUES 68 | (1, 'Shoes'), 69 | (2, 'Electronics'); 70 | 71 | -- -------------------------------------------------------- 72 | 73 | -- 74 | -- Table structure for table `orders` 75 | -- 76 | 77 | CREATE TABLE `orders` ( 78 | `id` int(10) NOT NULL, 79 | `user_id` int(11) NOT NULL 80 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 81 | 82 | -- 83 | -- Dumping data for table `orders` 84 | -- 85 | 86 | INSERT INTO `orders` (`id`, `user_id`) VALUES 87 | (7, 1), 88 | (8, 1), 89 | (13, 1), 90 | (14, 1), 91 | (15, 1), 92 | (16, 1), 93 | (17, 1), 94 | (18, 1), 95 | (19, 1), 96 | (20, 1), 97 | (21, 1), 98 | (22, 1), 99 | (23, 1), 100 | (24, 1), 101 | (25, 1), 102 | (26, 1), 103 | (27, 1), 104 | (28, 1), 105 | (29, 1), 106 | (30, 1), 107 | (31, 1), 108 | (32, 1), 109 | (33, 1), 110 | (34, 1), 111 | (35, 1), 112 | (36, 1), 113 | (37, 1), 114 | (38, 1), 115 | (39, 1), 116 | (40, 1), 117 | (41, 1), 118 | (52, 1), 119 | (53, 1), 120 | (54, 1), 121 | (55, 1), 122 | (56, 1), 123 | (57, 1), 124 | (58, 1), 125 | (59, 1), 126 | (60, 1), 127 | (61, 1), 128 | (62, 1), 129 | (64, 1), 130 | (65, 1), 131 | (66, 1), 132 | (67, 1), 133 | (68, 1), 134 | (69, 1), 135 | (70, 1), 136 | (71, 1), 137 | (72, 1), 138 | (73, 1), 139 | (74, 1), 140 | (75, 1), 141 | (76, 1), 142 | (77, 1), 143 | (78, 1), 144 | (79, 1), 145 | (80, 1), 146 | (81, 1), 147 | (82, 1), 148 | (83, 1), 149 | (84, 1), 150 | (85, 1), 151 | (86, 1), 152 | (87, 1), 153 | (88, 1), 154 | (89, 1), 155 | (90, 1), 156 | (91, 1), 157 | (93, 1), 158 | (94, 1), 159 | (95, 1), 160 | (96, 1), 161 | (97, 1), 162 | (98, 1), 163 | (99, 1), 164 | (100, 1), 165 | (101, 1), 166 | (102, 1), 167 | (103, 1), 168 | (6, 2), 169 | (42, 2), 170 | (43, 2), 171 | (44, 2), 172 | (63, 2), 173 | (104, 2), 174 | (105, 2), 175 | (106, 2), 176 | (107, 2), 177 | (108, 2), 178 | (109, 2), 179 | (110, 2), 180 | (111, 2), 181 | (112, 2), 182 | (113, 2), 183 | (114, 2), 184 | (115, 2), 185 | (116, 2), 186 | (117, 2), 187 | (118, 2), 188 | (119, 2); 189 | 190 | -- -------------------------------------------------------- 191 | 192 | -- 193 | -- Table structure for table `orders_details` 194 | -- 195 | 196 | CREATE TABLE `orders_details` ( 197 | `id` int(10) NOT NULL, 198 | `order_id` int(11) NOT NULL, 199 | `product_id` int(10) NOT NULL, 200 | `quantity` int(11) NOT NULL DEFAULT '1' 201 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 202 | 203 | -- 204 | -- Dumping data for table `orders_details` 205 | -- 206 | 207 | INSERT INTO `orders_details` (`id`, `order_id`, `product_id`, `quantity`) VALUES 208 | (1, 6, 27, 1), 209 | (2, 6, 26, 1), 210 | (3, 6, 49, 1), 211 | (4, 7, 4, 1), 212 | (5, 8, 38, 1), 213 | (6, 8, 5, 1), 214 | (7, 8, 53, 1), 215 | (9, 24, 26, 1), 216 | (10, 24, 49, 1), 217 | (11, 24, 38, 1), 218 | (12, 24, 27, 1), 219 | (45, 39, 1, 3), 220 | (46, 39, 13, 3), 221 | (47, 39, 18, 2), 222 | (49, 40, 1, 2), 223 | (50, 40, 18, 1), 224 | (51, 40, 38, 2), 225 | (52, 40, 13, 2), 226 | (53, 41, 31, 2), 227 | (54, 42, 1, 2), 228 | (55, 42, 18, 1), 229 | (56, 42, 38, 2), 230 | (57, 42, 13, 2), 231 | (58, 43, 13, 2), 232 | (59, 43, 1, 2), 233 | (60, 43, 18, 1), 234 | (61, 43, 38, 2), 235 | (62, 44, 1, 2), 236 | (63, 44, 32, 3), 237 | (64, 44, 13, 8), 238 | (65, 44, 18, 5), 239 | (66, 52, 4, 1), 240 | (67, 53, 7, 1), 241 | (68, 54, 1, 1), 242 | (69, 55, 3, 6), 243 | (70, 56, 1, 1), 244 | (71, 56, 4, 1), 245 | (72, 56, 7, 1), 246 | (73, 56, 6, 1), 247 | (74, 57, 2, 1), 248 | (75, 57, 3, 1), 249 | (76, 58, 3, 1), 250 | (77, 58, 2, 1), 251 | (78, 59, 2, 1), 252 | (79, 59, 3, 1), 253 | (80, 60, 2, 1), 254 | (81, 60, 3, 1), 255 | (82, 61, 2, 1), 256 | (83, 61, 3, 1), 257 | (84, 62, 1, 1), 258 | (85, 62, 4, 1), 259 | (86, 63, 1, 2), 260 | (87, 63, 18, 5), 261 | (88, 63, 32, 3), 262 | (89, 63, 13, 8), 263 | (90, 64, 1, 1), 264 | (91, 64, 2, 1), 265 | (92, 65, 2, 1), 266 | (93, 65, 3, 1), 267 | (94, 66, 2, 1), 268 | (95, 67, 3, 2), 269 | (96, 67, 2, 3), 270 | (97, 68, 8, 2), 271 | (98, 68, 7, 1), 272 | (99, 69, 5, 2), 273 | (100, 69, 6, 1), 274 | (101, 70, 2, 1), 275 | (102, 70, 3, 1), 276 | (103, 71, 2, 1), 277 | (104, 71, 1, 2), 278 | (105, 71, 4, 6), 279 | (106, 71, 3, 3), 280 | (107, 72, 1, 1), 281 | (108, 72, 3, 1), 282 | (109, 73, 6, 2), 283 | (110, 73, 4, 1), 284 | (111, 74, 5, 1), 285 | (112, 75, 6, 1), 286 | (113, 76, 2, 1), 287 | (114, 77, 2, 1), 288 | (115, 77, 3, 1), 289 | (116, 78, 1, 1), 290 | (117, 78, 2, 1), 291 | (118, 79, 6, 2), 292 | (119, 79, 8, 1), 293 | (120, 79, 7, 1), 294 | (121, 80, 1, 1), 295 | (122, 80, 2, 3), 296 | (123, 81, 4, 3), 297 | (124, 81, 2, 6), 298 | (125, 81, 3, 6), 299 | (126, 82, 2, 1), 300 | (127, 82, 3, 1), 301 | (128, 83, 5, 1), 302 | (129, 85, 3, 3), 303 | (130, 85, 6, 4), 304 | (131, 85, 4, 1), 305 | (132, 86, 2, 1), 306 | (133, 87, 4, 3), 307 | (134, 87, 6, 1), 308 | (135, 89, 7, 4), 309 | (136, 89, 4, 2), 310 | (137, 89, 5, 3), 311 | (138, 89, 6, 8), 312 | (139, 90, 4, 4), 313 | (140, 91, 2, 1), 314 | (141, 93, 2, 1), 315 | (142, 93, 3, 1), 316 | (143, 94, 2, 1), 317 | (144, 94, 3, 3), 318 | (145, 95, 5, 1), 319 | (146, 95, 3, 1), 320 | (147, 95, 2, 1), 321 | (148, 96, 2, 8), 322 | (149, 96, 4, 1), 323 | (150, 96, 3, 1), 324 | (151, 97, 6, 1), 325 | (152, 97, 5, 1), 326 | (153, 98, 3, 6), 327 | (154, 98, 4, 3), 328 | (155, 99, 2, 14), 329 | (156, 99, 5, 2), 330 | (157, 100, 2, 5), 331 | (158, 101, 3, 1), 332 | (159, 102, 3, 3), 333 | (160, 103, 2, 1), 334 | (161, 103, 4, 3), 335 | (162, 104, 1, 2), 336 | (163, 104, 32, 3), 337 | (164, 104, 13, 8), 338 | (165, 104, 18, 5), 339 | (166, 105, 1, 2), 340 | (167, 105, 32, 3), 341 | (168, 105, 18, 5), 342 | (169, 105, 13, 8), 343 | (170, 106, 13, 8), 344 | (171, 106, 1, 2), 345 | (172, 106, 32, 3), 346 | (173, 106, 18, 5), 347 | (174, 107, 1, 2), 348 | (175, 107, 18, 5), 349 | (176, 107, 32, 3), 350 | (177, 107, 13, 8), 351 | (178, 108, 2, 1), 352 | (179, 109, 2, 1), 353 | (180, 110, 5, 1), 354 | (181, 110, 4, 1), 355 | (182, 111, 6, 4), 356 | (183, 111, 7, 1), 357 | (184, 112, 5, 1), 358 | (185, 112, 2, 1), 359 | (186, 112, 3, 1), 360 | (187, 113, 2, 1), 361 | (188, 114, 3, 1), 362 | (189, 115, 2, 1), 363 | (190, 115, 3, 1), 364 | (191, 116, 3, 1), 365 | (192, 116, 5, 6), 366 | (193, 117, 4, 1), 367 | (194, 117, 5, 1), 368 | (195, 118, 2, 1), 369 | (196, 119, 3, 1); 370 | 371 | -- -------------------------------------------------------- 372 | 373 | -- 374 | -- Table structure for table `products` 375 | -- 376 | 377 | CREATE TABLE `products` ( 378 | `id` int(10) NOT NULL, 379 | `title` varchar(255) NOT NULL, 380 | `image` varchar(255) NOT NULL, 381 | `images` text CHARACTER SET utf8 COLLATE utf8_general_ci, 382 | `description` text NOT NULL, 383 | `price` float NOT NULL, 384 | `quantity` int(10) NOT NULL, 385 | `short_desc` varchar(255) NOT NULL, 386 | `cat_id` int(10) DEFAULT NULL 387 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 388 | 389 | -- 390 | -- Dumping data for table `products` 391 | -- 392 | 393 | INSERT INTO `products` (`id`, `title`, `image`, `images`, `description`, `price`, `quantity`, `short_desc`, `cat_id`) VALUES 394 | (1, 'PlayStation 4', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcSr-iFW5W8n3_jxNKiclAP_k71Fi9PGcojsMUC-vb8zbwJthbBd', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcSr-iFW5W8n3_jxNKiclAP_k71Fi9PGcojsMUC-vb8zbwJthbBd;https://static.toiimg.com/thumb/msid-56933980,width-640,resizemode-4,imgsize-85436/56933980.jpg;https://cdn.mos.cms.futurecdn.net/3328be45e8c7fe5194055b4c687fb769-1200-80.jpeg;https://img.etimg.com/thumb/width-640,height-480,imgsize-76492,resizemode-1,msid-52464286/46.jpg', 'With PS4, gaming becomes a lot more power packed. Ultra-fast processors, high-performance system, real-time game sharing, remote play and lots more makes it the ultimate companion device.', 240.99, 0, 'Gaming console', 2), 395 | (2, 'PEGASUS 33 Running Shoes For Men', 'https://i.pinimg.com/originals/43/40/8e/43408ee5a8d234752ecf80bbc3832e65.jpg', 'https://i.pinimg.com/originals/43/40/8e/43408ee5a8d234752ecf80bbc3832e65.jpg;https://i.ebayimg.com/images/g/eQgAAOSw2XdePfc0/s-l640.jpg;https://i.ebayimg.com/images/g/j~gAAOSwQ6FdG9Eh/s-l640.jpg;https://i.ebayimg.com/images/g/OesAAOSwDnpeJhWN/s-l640.jpg', 'The Nike Zoom Pegasus Turbo 2 is updated with a feather-light upper, while innovative foam brings revolutionary responsiveness to your long-distance training', 59.99, 51, 'SPORTS SHOES', 1), 396 | (3, 'MEN\'S ADIDAS RUNNING KALUS SHOES', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcSrEqFHfSbs6rUzcYnN_PcnS_D2JLXusKMVFk4Y8N_tn3hJgNIf', NULL, 'A well cushioned shoe with a fresher look that will appeal to young runners. Features Mesh upper for maximum ventilation, lightstrike IMEVA midsole with visible adiprene providing protection from harmful impact forces and durable Rubber outsole for long-lasting wear', 39.99, 69, 'SPORTS SHOES', 1), 397 | (4, 'Xbox One X Star Wars Jedi', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcQ8ufSADR9EyusxEfgMLErqISEcKVzQyjoD81zWcdpBvuEGBnYP', NULL, 'Own the Xbox One X Star Wars Jedi: Fallen Order™ Bundle and step into the role of a Jedi Padawan who narrowly escaped the purge of Order 66. This bundle includes a full-game download of Star Wars Jedi: Fallen Order™ Deluxe Edition, a 1-month trial of Xbox Game Pass for console and Xbox Live Gold, and 1-month of EA Access.***', 250, 78, 'Gaming console', 2), 398 | (5, 'PlayStation 4', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcSr-iFW5W8n3_jxNKiclAP_k71Fi9PGcojsMUC-vb8zbwJthbBd', NULL, 'With PS4, gaming becomes a lot more power packed. Ultra-fast processors, high-performance system, real-time game sharing, remote play and lots more makes it the ultimate companion device.', 240.99, 83, 'Gaming console', 2), 399 | (6, 'PEGASUS 33 Running Shoes For Men', 'https://i.pinimg.com/originals/43/40/8e/43408ee5a8d234752ecf80bbc3832e65.jpg', NULL, 'The Nike Zoom Pegasus Turbo 2 is updated with a feather-light upper, while innovative foam brings revolutionary responsiveness to your long-distance training', 59.99, 1, 'SPORTS SHOES', 1), 400 | (7, 'MEN\'S ADIDAS RUNNING KALUS SHOES', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcSrEqFHfSbs6rUzcYnN_PcnS_D2JLXusKMVFk4Y8N_tn3hJgNIf', NULL, 'A well cushioned shoe with a fresher look that will appeal to young runners. Features Mesh upper for maximum ventilation, lightstrike IMEVA midsole with visible adiprene providing protection from harmful impact forces and durable Rubber outsole for long-lasting wear', 39.99, 95, 'SPORTS SHOES', 1), 401 | (8, 'Xbox One X Star Wars Jedi', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcQ8ufSADR9EyusxEfgMLErqISEcKVzQyjoD81zWcdpBvuEGBnYP', NULL, 'Own the Xbox One X Star Wars Jedi: Fallen Order™ Bundle and step into the role of a Jedi Padawan who narrowly escaped the purge of Order 66. This bundle includes a full-game download of Star Wars Jedi: Fallen Order™ Deluxe Edition, a 1-month trial of Xbox Game Pass for console and Xbox Live Gold, and 1-month of EA Access.***', 250, 100, 'Gaming console', 2), 402 | (9, 'PlayStation 4', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcSr-iFW5W8n3_jxNKiclAP_k71Fi9PGcojsMUC-vb8zbwJthbBd', NULL, 'With PS4, gaming becomes a lot more power packed. Ultra-fast processors, high-performance system, real-time game sharing, remote play and lots more makes it the ultimate companion device.', 240.99, 100, 'Gaming console', 2), 403 | (10, 'PEGASUS 33 Running Shoes For Men', 'https://i.pinimg.com/originals/43/40/8e/43408ee5a8d234752ecf80bbc3832e65.jpg', NULL, 'The Nike Zoom Pegasus Turbo 2 is updated with a feather-light upper, while innovative foam brings revolutionary responsiveness to your long-distance training', 59.99, 100, 'SPORTS SHOES', 1), 404 | (11, 'MEN\'S ADIDAS RUNNING KALUS SHOES', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcSrEqFHfSbs6rUzcYnN_PcnS_D2JLXusKMVFk4Y8N_tn3hJgNIf', NULL, 'A well cushioned shoe with a fresher look that will appeal to young runners. Features Mesh upper for maximum ventilation, lightstrike IMEVA midsole with visible adiprene providing protection from harmful impact forces and durable Rubber outsole for long-lasting wear', 39.99, 100, 'SPORTS SHOES', 1), 405 | (12, 'Xbox One X Star Wars Jedi', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcQ8ufSADR9EyusxEfgMLErqISEcKVzQyjoD81zWcdpBvuEGBnYP', NULL, 'Own the Xbox One X Star Wars Jedi: Fallen Order™ Bundle and step into the role of a Jedi Padawan who narrowly escaped the purge of Order 66. This bundle includes a full-game download of Star Wars Jedi: Fallen Order™ Deluxe Edition, a 1-month trial of Xbox Game Pass for console and Xbox Live Gold, and 1-month of EA Access.***', 250, 100, 'Gaming console', 2), 406 | (13, 'PlayStation 4', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcSr-iFW5W8n3_jxNKiclAP_k71Fi9PGcojsMUC-vb8zbwJthbBd', NULL, 'With PS4, gaming becomes a lot more power packed. Ultra-fast processors, high-performance system, real-time game sharing, remote play and lots more makes it the ultimate companion device.', 240.99, 68, 'Gaming console', 2), 407 | (14, 'PEGASUS 33 Running Shoes For Men', 'https://i.pinimg.com/originals/43/40/8e/43408ee5a8d234752ecf80bbc3832e65.jpg', NULL, 'The Nike Zoom Pegasus Turbo 2 is updated with a feather-light upper, while innovative foam brings revolutionary responsiveness to your long-distance training', 59.99, 100, 'SPORTS SHOES', 1), 408 | (15, 'MEN\'S ADIDAS RUNNING KALUS SHOES', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcSrEqFHfSbs6rUzcYnN_PcnS_D2JLXusKMVFk4Y8N_tn3hJgNIf', NULL, 'A well cushioned shoe with a fresher look that will appeal to young runners. Features Mesh upper for maximum ventilation, lightstrike IMEVA midsole with visible adiprene providing protection from harmful impact forces and durable Rubber outsole for long-lasting wear', 39.99, 100, 'SPORTS SHOES', 1), 409 | (16, 'Xbox One X Star Wars Jedi', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcQ8ufSADR9EyusxEfgMLErqISEcKVzQyjoD81zWcdpBvuEGBnYP', NULL, 'Own the Xbox One X Star Wars Jedi: Fallen Order™ Bundle and step into the role of a Jedi Padawan who narrowly escaped the purge of Order 66. This bundle includes a full-game download of Star Wars Jedi: Fallen Order™ Deluxe Edition, a 1-month trial of Xbox Game Pass for console and Xbox Live Gold, and 1-month of EA Access.***', 250, 100, 'Gaming console', 2), 410 | (17, 'PlayStation 4', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcSr-iFW5W8n3_jxNKiclAP_k71Fi9PGcojsMUC-vb8zbwJthbBd', NULL, 'With PS4, gaming becomes a lot more power packed. Ultra-fast processors, high-performance system, real-time game sharing, remote play and lots more makes it the ultimate companion device.', 240.99, 100, 'Gaming console', 2), 411 | (18, 'PEGASUS 33 Running Shoes For Men', 'https://i.pinimg.com/originals/43/40/8e/43408ee5a8d234752ecf80bbc3832e65.jpg', NULL, 'The Nike Zoom Pegasus Turbo 2 is updated with a feather-light upper, while innovative foam brings revolutionary responsiveness to your long-distance training', 59.99, 80, 'SPORTS SHOES', 1), 412 | (19, 'MEN\'S ADIDAS RUNNING KALUS SHOES', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcSrEqFHfSbs6rUzcYnN_PcnS_D2JLXusKMVFk4Y8N_tn3hJgNIf', NULL, 'A well cushioned shoe with a fresher look that will appeal to young runners. Features Mesh upper for maximum ventilation, lightstrike IMEVA midsole with visible adiprene providing protection from harmful impact forces and durable Rubber outsole for long-lasting wear', 39.99, 100, 'SPORTS SHOES', 1), 413 | (20, 'Xbox One X Star Wars Jedi', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcQ8ufSADR9EyusxEfgMLErqISEcKVzQyjoD81zWcdpBvuEGBnYP', NULL, 'Own the Xbox One X Star Wars Jedi: Fallen Order™ Bundle and step into the role of a Jedi Padawan who narrowly escaped the purge of Order 66. This bundle includes a full-game download of Star Wars Jedi: Fallen Order™ Deluxe Edition, a 1-month trial of Xbox Game Pass for console and Xbox Live Gold, and 1-month of EA Access.***', 250, 100, 'Gaming console', 2), 414 | (21, 'PlayStation 4', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcSr-iFW5W8n3_jxNKiclAP_k71Fi9PGcojsMUC-vb8zbwJthbBd', NULL, 'With PS4, gaming becomes a lot more power packed. Ultra-fast processors, high-performance system, real-time game sharing, remote play and lots more makes it the ultimate companion device.', 240.99, 100, 'Gaming console', 2), 415 | (22, 'PEGASUS 33 Running Shoes For Men', 'https://i.pinimg.com/originals/43/40/8e/43408ee5a8d234752ecf80bbc3832e65.jpg', NULL, 'The Nike Zoom Pegasus Turbo 2 is updated with a feather-light upper, while innovative foam brings revolutionary responsiveness to your long-distance training', 59.99, 100, 'SPORTS SHOES', 1), 416 | (23, 'MEN\'S ADIDAS RUNNING KALUS SHOES', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcSrEqFHfSbs6rUzcYnN_PcnS_D2JLXusKMVFk4Y8N_tn3hJgNIf', NULL, 'A well cushioned shoe with a fresher look that will appeal to young runners. Features Mesh upper for maximum ventilation, lightstrike IMEVA midsole with visible adiprene providing protection from harmful impact forces and durable Rubber outsole for long-lasting wear', 39.99, 100, 'SPORTS SHOES', 1), 417 | (24, 'Xbox One X Star Wars Jedi', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcQ8ufSADR9EyusxEfgMLErqISEcKVzQyjoD81zWcdpBvuEGBnYP', NULL, 'Own the Xbox One X Star Wars Jedi: Fallen Order™ Bundle and step into the role of a Jedi Padawan who narrowly escaped the purge of Order 66. This bundle includes a full-game download of Star Wars Jedi: Fallen Order™ Deluxe Edition, a 1-month trial of Xbox Game Pass for console and Xbox Live Gold, and 1-month of EA Access.***', 250, 100, 'Gaming console', 2), 418 | (25, 'PlayStation 4', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcSr-iFW5W8n3_jxNKiclAP_k71Fi9PGcojsMUC-vb8zbwJthbBd', NULL, 'With PS4, gaming becomes a lot more power packed. Ultra-fast processors, high-performance system, real-time game sharing, remote play and lots more makes it the ultimate companion device.', 240.99, 100, 'Gaming console', 2), 419 | (26, 'PEGASUS 33 Running Shoes For Men', 'https://i.pinimg.com/originals/43/40/8e/43408ee5a8d234752ecf80bbc3832e65.jpg', NULL, 'The Nike Zoom Pegasus Turbo 2 is updated with a feather-light upper, while innovative foam brings revolutionary responsiveness to your long-distance training', 59.99, 100, 'SPORTS SHOES', 1), 420 | (27, 'MEN\'S ADIDAS RUNNING KALUS SHOES', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcSrEqFHfSbs6rUzcYnN_PcnS_D2JLXusKMVFk4Y8N_tn3hJgNIf', NULL, 'A well cushioned shoe with a fresher look that will appeal to young runners. Features Mesh upper for maximum ventilation, lightstrike IMEVA midsole with visible adiprene providing protection from harmful impact forces and durable Rubber outsole for long-lasting wear', 39.99, 100, 'SPORTS SHOES', 1), 421 | (28, 'Xbox One X Star Wars Jedi', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcQ8ufSADR9EyusxEfgMLErqISEcKVzQyjoD81zWcdpBvuEGBnYP', NULL, 'Own the Xbox One X Star Wars Jedi: Fallen Order™ Bundle and step into the role of a Jedi Padawan who narrowly escaped the purge of Order 66. This bundle includes a full-game download of Star Wars Jedi: Fallen Order™ Deluxe Edition, a 1-month trial of Xbox Game Pass for console and Xbox Live Gold, and 1-month of EA Access.***', 250, 100, 'Gaming console', 2), 422 | (29, 'PlayStation 4', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcSr-iFW5W8n3_jxNKiclAP_k71Fi9PGcojsMUC-vb8zbwJthbBd', NULL, 'With PS4, gaming becomes a lot more power packed. Ultra-fast processors, high-performance system, real-time game sharing, remote play and lots more makes it the ultimate companion device.', 240.99, 100, 'Gaming console', 2), 423 | (30, 'PEGASUS 33 Running Shoes For Men', 'https://i.pinimg.com/originals/43/40/8e/43408ee5a8d234752ecf80bbc3832e65.jpg', NULL, 'The Nike Zoom Pegasus Turbo 2 is updated with a feather-light upper, while innovative foam brings revolutionary responsiveness to your long-distance training', 59.99, 100, 'SPORTS SHOES', 1), 424 | (31, 'MEN\'S ADIDAS RUNNING KALUS SHOES', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcSrEqFHfSbs6rUzcYnN_PcnS_D2JLXusKMVFk4Y8N_tn3hJgNIf', NULL, 'A well cushioned shoe with a fresher look that will appeal to young runners. Features Mesh upper for maximum ventilation, lightstrike IMEVA midsole with visible adiprene providing protection from harmful impact forces and durable Rubber outsole for long-lasting wear', 39.99, 100, 'SPORTS SHOES', 1), 425 | (32, 'Xbox One X Star Wars Jedi', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcQ8ufSADR9EyusxEfgMLErqISEcKVzQyjoD81zWcdpBvuEGBnYP', NULL, 'Own the Xbox One X Star Wars Jedi: Fallen Order™ Bundle and step into the role of a Jedi Padawan who narrowly escaped the purge of Order 66. This bundle includes a full-game download of Star Wars Jedi: Fallen Order™ Deluxe Edition, a 1-month trial of Xbox Game Pass for console and Xbox Live Gold, and 1-month of EA Access.***', 250, 88, 'Gaming console', 2), 426 | (33, 'PEGASUS 33 Running Shoes For Men', 'https://i.pinimg.com/originals/43/40/8e/43408ee5a8d234752ecf80bbc3832e65.jpg', NULL, 'The Nike Zoom Pegasus Turbo 2 is updated with a feather-light upper, while innovative foam brings revolutionary responsiveness to your long-distance training', 59.99, 100, 'SPORTS SHOES', 1), 427 | (34, 'MEN\'S ADIDAS RUNNING KALUS SHOES', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcSrEqFHfSbs6rUzcYnN_PcnS_D2JLXusKMVFk4Y8N_tn3hJgNIf', NULL, 'A well cushioned shoe with a fresher look that will appeal to young runners. Features Mesh upper for maximum ventilation, lightstrike IMEVA midsole with visible adiprene providing protection from harmful impact forces and durable Rubber outsole for long-lasting wear', 39.99, 100, 'SPORTS SHOES', 1), 428 | (35, 'Xbox One X Star Wars Jedi', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcQ8ufSADR9EyusxEfgMLErqISEcKVzQyjoD81zWcdpBvuEGBnYP', NULL, 'Own the Xbox One X Star Wars Jedi: Fallen Order™ Bundle and step into the role of a Jedi Padawan who narrowly escaped the purge of Order 66. This bundle includes a full-game download of Star Wars Jedi: Fallen Order™ Deluxe Edition, a 1-month trial of Xbox Game Pass for console and Xbox Live Gold, and 1-month of EA Access.***', 250, 100, 'Gaming console', 2), 429 | (36, 'PlayStation 4', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcSr-iFW5W8n3_jxNKiclAP_k71Fi9PGcojsMUC-vb8zbwJthbBd', NULL, 'With PS4, gaming becomes a lot more power packed. Ultra-fast processors, high-performance system, real-time game sharing, remote play and lots more makes it the ultimate companion device.', 240.99, 100, 'Gaming console', 2), 430 | (37, 'PEGASUS 33 Running Shoes For Men', 'https://i.pinimg.com/originals/43/40/8e/43408ee5a8d234752ecf80bbc3832e65.jpg', NULL, 'The Nike Zoom Pegasus Turbo 2 is updated with a feather-light upper, while innovative foam brings revolutionary responsiveness to your long-distance training', 59.99, 100, 'SPORTS SHOES', 1), 431 | (38, 'MEN\'S ADIDAS RUNNING KALUS SHOES', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcSrEqFHfSbs6rUzcYnN_PcnS_D2JLXusKMVFk4Y8N_tn3hJgNIf', NULL, 'A well cushioned shoe with a fresher look that will appeal to young runners. Features Mesh upper for maximum ventilation, lightstrike IMEVA midsole with visible adiprene providing protection from harmful impact forces and durable Rubber outsole for long-lasting wear', 39.99, 100, 'SPORTS SHOES', 1), 432 | (39, 'Xbox One X Star Wars Jedi', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcQ8ufSADR9EyusxEfgMLErqISEcKVzQyjoD81zWcdpBvuEGBnYP', NULL, 'Own the Xbox One X Star Wars Jedi: Fallen Order™ Bundle and step into the role of a Jedi Padawan who narrowly escaped the purge of Order 66. This bundle includes a full-game download of Star Wars Jedi: Fallen Order™ Deluxe Edition, a 1-month trial of Xbox Game Pass for console and Xbox Live Gold, and 1-month of EA Access.***', 250, 100, 'Gaming console', 2), 433 | (40, 'PEGASUS 33 Running Shoes For Men', 'https://i.pinimg.com/originals/43/40/8e/43408ee5a8d234752ecf80bbc3832e65.jpg', NULL, 'The Nike Zoom Pegasus Turbo 2 is updated with a feather-light upper, while innovative foam brings revolutionary responsiveness to your long-distance training', 59.99, 100, 'SPORTS SHOES', 1), 434 | (41, 'MEN\'S ADIDAS RUNNING KALUS SHOES', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcSrEqFHfSbs6rUzcYnN_PcnS_D2JLXusKMVFk4Y8N_tn3hJgNIf', NULL, 'A well cushioned shoe with a fresher look that will appeal to young runners. Features Mesh upper for maximum ventilation, lightstrike IMEVA midsole with visible adiprene providing protection from harmful impact forces and durable Rubber outsole for long-lasting wear', 39.99, 100, 'SPORTS SHOES', 1), 435 | (42, 'Xbox One X Star Wars Jedi', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcQ8ufSADR9EyusxEfgMLErqISEcKVzQyjoD81zWcdpBvuEGBnYP', NULL, 'Own the Xbox One X Star Wars Jedi: Fallen Order™ Bundle and step into the role of a Jedi Padawan who narrowly escaped the purge of Order 66. This bundle includes a full-game download of Star Wars Jedi: Fallen Order™ Deluxe Edition, a 1-month trial of Xbox Game Pass for console and Xbox Live Gold, and 1-month of EA Access.***', 250, 100, 'Gaming console', 2), 436 | (43, 'PlayStation 4', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcSr-iFW5W8n3_jxNKiclAP_k71Fi9PGcojsMUC-vb8zbwJthbBd', NULL, 'With PS4, gaming becomes a lot more power packed. Ultra-fast processors, high-performance system, real-time game sharing, remote play and lots more makes it the ultimate companion device.', 240.99, 100, 'Gaming console', 2), 437 | (44, 'PEGASUS 33 Running Shoes For Men', 'https://i.pinimg.com/originals/43/40/8e/43408ee5a8d234752ecf80bbc3832e65.jpg', NULL, 'The Nike Zoom Pegasus Turbo 2 is updated with a feather-light upper, while innovative foam brings revolutionary responsiveness to your long-distance training', 59.99, 100, 'SPORTS SHOES', 1), 438 | (45, 'MEN\'S ADIDAS RUNNING KALUS SHOES', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcSrEqFHfSbs6rUzcYnN_PcnS_D2JLXusKMVFk4Y8N_tn3hJgNIf', NULL, 'A well cushioned shoe with a fresher look that will appeal to young runners. Features Mesh upper for maximum ventilation, lightstrike IMEVA midsole with visible adiprene providing protection from harmful impact forces and durable Rubber outsole for long-lasting wear', 39.99, 100, 'SPORTS SHOES', 1), 439 | (46, 'Xbox One X Star Wars Jedi', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcQ8ufSADR9EyusxEfgMLErqISEcKVzQyjoD81zWcdpBvuEGBnYP', NULL, 'Own the Xbox One X Star Wars Jedi: Fallen Order™ Bundle and step into the role of a Jedi Padawan who narrowly escaped the purge of Order 66. This bundle includes a full-game download of Star Wars Jedi: Fallen Order™ Deluxe Edition, a 1-month trial of Xbox Game Pass for console and Xbox Live Gold, and 1-month of EA Access.***', 250, 100, 'Gaming console', 2), 440 | (47, 'PEGASUS 33 Running Shoes For Men', 'https://i.pinimg.com/originals/43/40/8e/43408ee5a8d234752ecf80bbc3832e65.jpg', NULL, 'The Nike Zoom Pegasus Turbo 2 is updated with a feather-light upper, while innovative foam brings revolutionary responsiveness to your long-distance training', 59.99, 100, 'SPORTS SHOES', 1), 441 | (48, 'MEN\'S ADIDAS RUNNING KALUS SHOES', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcSrEqFHfSbs6rUzcYnN_PcnS_D2JLXusKMVFk4Y8N_tn3hJgNIf', NULL, 'A well cushioned shoe with a fresher look that will appeal to young runners. Features Mesh upper for maximum ventilation, lightstrike IMEVA midsole with visible adiprene providing protection from harmful impact forces and durable Rubber outsole for long-lasting wear', 39.99, 100, 'SPORTS SHOES', 1), 442 | (49, 'Xbox One X Star Wars Jedi', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcQ8ufSADR9EyusxEfgMLErqISEcKVzQyjoD81zWcdpBvuEGBnYP', NULL, 'Own the Xbox One X Star Wars Jedi: Fallen Order™ Bundle and step into the role of a Jedi Padawan who narrowly escaped the purge of Order 66. This bundle includes a full-game download of Star Wars Jedi: Fallen Order™ Deluxe Edition, a 1-month trial of Xbox Game Pass for console and Xbox Live Gold, and 1-month of EA Access.***', 250, 100, 'Gaming console', 2), 443 | (50, 'PlayStation 4', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcSr-iFW5W8n3_jxNKiclAP_k71Fi9PGcojsMUC-vb8zbwJthbBd', NULL, 'With PS4, gaming becomes a lot more power packed. Ultra-fast processors, high-performance system, real-time game sharing, remote play and lots more makes it the ultimate companion device.', 240.99, 100, 'Gaming console', 2), 444 | (51, 'PEGASUS 33 Running Shoes For Men', 'https://i.pinimg.com/originals/43/40/8e/43408ee5a8d234752ecf80bbc3832e65.jpg', NULL, 'The Nike Zoom Pegasus Turbo 2 is updated with a feather-light upper, while innovative foam brings revolutionary responsiveness to your long-distance training', 59.99, 100, 'SPORTS SHOES', 1), 445 | (52, 'MEN\'S ADIDAS RUNNING KALUS SHOES', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcSrEqFHfSbs6rUzcYnN_PcnS_D2JLXusKMVFk4Y8N_tn3hJgNIf', NULL, 'A well cushioned shoe with a fresher look that will appeal to young runners. Features Mesh upper for maximum ventilation, lightstrike IMEVA midsole with visible adiprene providing protection from harmful impact forces and durable Rubber outsole for long-lasting wear', 39.99, 100, 'SPORTS SHOES', 1), 446 | (53, 'Xbox One X Star Wars Jedi', 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcQ8ufSADR9EyusxEfgMLErqISEcKVzQyjoD81zWcdpBvuEGBnYP', NULL, 'Own the Xbox One X Star Wars Jedi: Fallen Order™ Bundle and step into the role of a Jedi Padawan who narrowly escaped the purge of Order 66. This bundle includes a full-game download of Star Wars Jedi: Fallen Order™ Deluxe Edition, a 1-month trial of Xbox Game Pass for console and Xbox Live Gold, and 1-month of EA Access.***', 250, 100, 'Gaming console', 2); 447 | 448 | -- -------------------------------------------------------- 449 | 450 | -- 451 | -- Table structure for table `users` 452 | -- 453 | 454 | CREATE TABLE `users` ( 455 | `id` int(11) NOT NULL, 456 | `username` varchar(255) NOT NULL, 457 | `password` varchar(255) NOT NULL, 458 | `email` varchar(255) NOT NULL, 459 | `fname` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT 'not set', 460 | `lname` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT 'not set', 461 | `age` int(10) DEFAULT '18', 462 | `role` int(10) DEFAULT '555', 463 | `photoUrl` text CHARACTER SET utf8 COLLATE utf8_general_ci, 464 | `type` varchar(255) NOT NULL DEFAULT 'local' 465 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 466 | 467 | -- 468 | -- Dumping data for table `users` 469 | -- 470 | 471 | INSERT INTO `users` (`id`, `username`, `password`, `email`, `fname`, `lname`, `age`, `role`, `photoUrl`, `type`) VALUES 472 | (1, 'john', '$2b$10$iLEz3tkVKeOCwuagcqhDUOV.Iswjc.I41unbnnFGiqK/gmstDCC3i', 'john@gmail.com', 'Indranil', 'Mukherjee', 31, 777, '', 'local'), 473 | (2, 'shane', '$2b$10$ewemZ.oLASXUIWfUrjkDeOTCZUCpnhSlR3/Act2eQnvGg6mnGLOtG', 'shane123@gmail.com', 'Navoneel', 'Mukherjee', 27, 555, '', 'local'), 474 | (11, 'mike', '$2b$10$vIS0W3LKhbx2tFh1GMYWhul7GWtIg4jnKU2C/NGux1pUG3QKMdNzO', 'mike-doe@excellent.com', 'Mike', 'Leming', 40, 555, 'https://i.pinimg.com/originals/dc/55/a0/dc55a0fec14d93d9cf6fa32c32f7c7f2.jpg', 'local'), 475 | (14, 'indramukh', '$2b$10$po91FRkYQIfPDN6G1BxG0uIV7Z54GbJRkJw2t9wRec9uqmRKillqa', 'indramukh@hotmail.com', 'Indranil', 'Mukherjee', NULL, 555, 'https://image.shutterstock.com/image-vector/person-gray-photo-placeholder-man-260nw-1259815156.jpg', 'local'), 476 | (19, 'test', '$2b$10$cSa3jm7cboNSJRTgNRFJg.GviAzr/pyfptMxwxmjdKP./CdxDtLlK', 'test@gmail.com', 'Indranil', 'Mukherjee', NULL, 555, 'https://image.shutterstock.com/image-vector/person-gray-photo-placeholder-man-260nw-1259815156.jpg', 'local'), 477 | (20, 'bhaikaju', '$2b$10$P9X8c/MC39.Zxr2k5SfxK.lwbcj6PNou2ueqod29CFZrmuDUCvlfe', 'bhaikaju@gmail.com', 'Programming\'s Fun', 'not set', NULL, 555, 'https://lh3.googleusercontent.com/a-/AOh14GiPx0OQHJOCy-fSfNmKr1vbnM-Rp7CgS_jx_6oY=s96-c', 'social'), 478 | (21, 'shreyamukherjee07', '$2b$10$SOw/LSErH0IbZ96h7pop.O6NJreOD3HjRGiZZmKnqiXiuUPCl8b8W', 'shreyamukherjee07@gmail.com', 'Shreya', 'Mukherjee', NULL, 555, 'https://lh3.googleusercontent.com/a-/AOh14Gg2Be7kKwqsUbQwyuCGToyZcCQ3ZDDWNkvSbQJVHA=s96-c', 'social'); 479 | 480 | -- 481 | -- Indexes for dumped tables 482 | -- 483 | 484 | -- 485 | -- Indexes for table `addresses` 486 | -- 487 | ALTER TABLE `addresses` 488 | ADD PRIMARY KEY (`id`), 489 | ADD KEY `fk_addresses_users1_idx` (`user_id`); 490 | 491 | -- 492 | -- Indexes for table `categories` 493 | -- 494 | ALTER TABLE `categories` 495 | ADD PRIMARY KEY (`id`); 496 | 497 | -- 498 | -- Indexes for table `orders` 499 | -- 500 | ALTER TABLE `orders` 501 | ADD PRIMARY KEY (`id`), 502 | ADD KEY `fk_orders_users1_idx` (`user_id`); 503 | 504 | -- 505 | -- Indexes for table `orders_details` 506 | -- 507 | ALTER TABLE `orders_details` 508 | ADD PRIMARY KEY (`id`), 509 | ADD KEY `fk_orders_has_products_products1_idx` (`product_id`), 510 | ADD KEY `fk_orders_has_products_orders1_idx` (`order_id`); 511 | 512 | -- 513 | -- Indexes for table `products` 514 | -- 515 | ALTER TABLE `products` 516 | ADD PRIMARY KEY (`id`), 517 | ADD KEY `products_ibfk_1` (`cat_id`); 518 | ALTER TABLE `products` ADD FULLTEXT KEY `description` (`description`); 519 | 520 | -- 521 | -- Indexes for table `users` 522 | -- 523 | ALTER TABLE `users` 524 | ADD PRIMARY KEY (`id`); 525 | 526 | -- 527 | -- AUTO_INCREMENT for dumped tables 528 | -- 529 | 530 | -- 531 | -- AUTO_INCREMENT for table `addresses` 532 | -- 533 | ALTER TABLE `addresses` 534 | MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3; 535 | 536 | -- 537 | -- AUTO_INCREMENT for table `categories` 538 | -- 539 | ALTER TABLE `categories` 540 | MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3; 541 | 542 | -- 543 | -- AUTO_INCREMENT for table `orders` 544 | -- 545 | ALTER TABLE `orders` 546 | MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=120; 547 | 548 | -- 549 | -- AUTO_INCREMENT for table `orders_details` 550 | -- 551 | ALTER TABLE `orders_details` 552 | MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=197; 553 | 554 | -- 555 | -- AUTO_INCREMENT for table `products` 556 | -- 557 | ALTER TABLE `products` 558 | MODIFY `id` int(10) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=54; 559 | 560 | -- 561 | -- AUTO_INCREMENT for table `users` 562 | -- 563 | ALTER TABLE `users` 564 | MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=22; 565 | 566 | -- 567 | -- Constraints for dumped tables 568 | -- 569 | 570 | -- 571 | -- Constraints for table `addresses` 572 | -- 573 | ALTER TABLE `addresses` 574 | ADD CONSTRAINT `fk_addresses_users1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; 575 | 576 | -- 577 | -- Constraints for table `orders` 578 | -- 579 | ALTER TABLE `orders` 580 | ADD CONSTRAINT `users` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; 581 | 582 | -- 583 | -- Constraints for table `orders_details` 584 | -- 585 | ALTER TABLE `orders_details` 586 | ADD CONSTRAINT `fk_orders_has_products_orders1` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, 587 | ADD CONSTRAINT `fk_orders_has_products_products1` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; 588 | 589 | -- 590 | -- Constraints for table `products` 591 | -- 592 | ALTER TABLE `products` 593 | ADD CONSTRAINT `products_ibfk_1` FOREIGN KEY (`cat_id`) REFERENCES `categories` (`id`) ON DELETE SET NULL ON UPDATE CASCADE; 594 | COMMIT; 595 | 596 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 597 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 598 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 599 | -------------------------------------------------------------------------------- /client/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. 18 | -------------------------------------------------------------------------------- /client/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # Client 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 11.2.6. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. 28 | -------------------------------------------------------------------------------- /client/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "client": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist/client", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "tsconfig.app.json", 25 | "aot": true, 26 | "assets": ["src/favicon.ico", "src/assets", {"glob":"**/*","input":"./node_modules/@ant-design/icons-angular/src/inline-svg/","output":"/assets/"}], 27 | "styles": [ 28 | "src/theme.less", 29 | "src/styles.scss" 30 | ], 31 | "scripts": [] 32 | }, 33 | "configurations": { 34 | "production": { 35 | "fileReplacements": [ 36 | { 37 | "replace": "src/environments/environment.ts", 38 | "with": "src/environments/environment.prod.ts" 39 | } 40 | ], 41 | "optimization": true, 42 | "outputHashing": "all", 43 | "sourceMap": false, 44 | "namedChunks": false, 45 | "extractLicenses": true, 46 | "vendorChunk": false, 47 | "buildOptimizer": true, 48 | "budgets": [ 49 | { 50 | "type": "initial", 51 | "maximumWarning": "2mb", 52 | "maximumError": "5mb" 53 | }, 54 | { 55 | "type": "anyComponentStyle", 56 | "maximumWarning": "6kb", 57 | "maximumError": "10kb" 58 | } 59 | ] 60 | } 61 | } 62 | }, 63 | "serve": { 64 | "builder": "@angular-devkit/build-angular:dev-server", 65 | "options": { 66 | "browserTarget": "client:build" 67 | }, 68 | "configurations": { 69 | "production": { 70 | "browserTarget": "client:build:production" 71 | } 72 | } 73 | }, 74 | "extract-i18n": { 75 | "builder": "@angular-devkit/build-angular:extract-i18n", 76 | "options": { 77 | "browserTarget": "client:build" 78 | } 79 | }, 80 | "test": { 81 | "builder": "@angular-devkit/build-angular:karma", 82 | "options": { 83 | "main": "src/test.ts", 84 | "polyfills": "src/polyfills.ts", 85 | "tsConfig": "tsconfig.spec.json", 86 | "karmaConfig": "karma.conf.js", 87 | "assets": ["src/favicon.ico", "src/assets"], 88 | "styles": ["src/styles.scss"], 89 | "scripts": [] 90 | } 91 | }, 92 | "lint": { 93 | "builder": "@angular-devkit/build-angular:tslint", 94 | "options": { 95 | "tsConfig": [ 96 | "tsconfig.app.json", 97 | "tsconfig.spec.json", 98 | "e2e/tsconfig.json" 99 | ], 100 | "exclude": ["**/node_modules/**"] 101 | } 102 | }, 103 | "e2e": { 104 | "builder": "@angular-devkit/build-angular:protractor", 105 | "options": { 106 | "protractorConfig": "e2e/protractor.conf.js", 107 | "devServerTarget": "client:serve" 108 | }, 109 | "configurations": { 110 | "production": { 111 | "devServerTarget": "client:serve:production" 112 | } 113 | } 114 | } 115 | } 116 | } 117 | }, 118 | "defaultProject": "client" 119 | } 120 | -------------------------------------------------------------------------------- /client/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | SELENIUM_PROMISE_MANAGER: false, 20 | baseUrl: 'http://localhost:4200/', 21 | framework: 'jasmine', 22 | jasmineNodeOpts: { 23 | showColors: true, 24 | defaultTimeoutInterval: 30000, 25 | print: function() {} 26 | }, 27 | onPrepare() { 28 | require('ts-node').register({ 29 | project: require('path').join(__dirname, './tsconfig.json') 30 | }); 31 | jasmine.getEnv().addReporter(new SpecReporter({ 32 | spec: { 33 | displayStacktrace: StacktraceOption.PRETTY 34 | } 35 | })); 36 | } 37 | }; -------------------------------------------------------------------------------- /client/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { browser, logging } from 'protractor'; 2 | import { AppPage } from './app.po'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', async () => { 12 | await page.navigateTo(); 13 | expect(await page.getTitleText()).toEqual('client app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /client/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | async navigateTo(): Promise { 5 | return browser.get(browser.baseUrl); 6 | } 7 | 8 | async getTitleText(): Promise { 9 | return element(by.css('app-root .content span')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /client/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../out-tsc/e2e", 6 | "module": "commonjs", 7 | "target": "es2018", 8 | "types": [ 9 | "jasmine", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, './coverage/client'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "~11.2.7", 15 | "@angular/common": "~11.2.7", 16 | "@angular/compiler": "~11.2.7", 17 | "@angular/core": "~11.2.7", 18 | "@angular/forms": "~11.2.7", 19 | "@angular/platform-browser": "~11.2.7", 20 | "@angular/platform-browser-dynamic": "~11.2.7", 21 | "@angular/router": "~11.2.7", 22 | "angular-responsive-carousel": "^2.0.2", 23 | "ng-zorro-antd": "^11.4.0", 24 | "rxjs": "~6.6.0", 25 | "swiper": "^6.5.6", 26 | "tslib": "^2.0.0", 27 | "zone.js": "~0.11.3" 28 | }, 29 | "devDependencies": { 30 | "@angular-devkit/build-angular": "~0.1102.6", 31 | "@angular/cli": "~11.2.6", 32 | "@angular/compiler-cli": "~11.2.7", 33 | "@types/jasmine": "~3.6.0", 34 | "@types/node": "^12.11.1", 35 | "codelyzer": "^6.0.0", 36 | "jasmine-core": "~3.6.0", 37 | "jasmine-spec-reporter": "~5.0.0", 38 | "karma": "~6.1.0", 39 | "karma-chrome-launcher": "~3.1.0", 40 | "karma-coverage": "~2.0.3", 41 | "karma-jasmine": "~4.0.0", 42 | "karma-jasmine-html-reporter": "^1.5.0", 43 | "protractor": "~7.0.0", 44 | "ts-node": "~8.3.0", 45 | "tslint": "~6.1.0", 46 | "typescript": "~4.1.5" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /client/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { CartComponent } from './cart/cart.component'; 4 | import { CheckoutComponent } from './checkout/checkout.component'; 5 | import { HomeComponent } from './home/home.component'; 6 | import { LoginComponent } from './auth/components/login/login.component'; 7 | import { OrderHistoryComponent } from './order-history/order-history.component'; 8 | import { ProductComponent } from './product/product.component'; 9 | import { ProfileComponent } from './profile/profile.component'; 10 | import { RegisterComponent } from './auth/components/register/register.component'; 11 | import { AuthGuardService } from './guards/auth-guard.service'; 12 | 13 | const routes: Routes = [ 14 | { path: '', component: HomeComponent }, 15 | { path: 'login', component: LoginComponent }, 16 | { path: 'register', component: RegisterComponent }, 17 | { 18 | path: 'profile', 19 | component: ProfileComponent, 20 | canActivate: [AuthGuardService], 21 | }, 22 | { path: 'product/:id', component: ProductComponent }, 23 | { path: 'cart', component: CartComponent }, 24 | { 25 | path: 'checkout', 26 | component: CheckoutComponent, 27 | canActivate: [AuthGuardService], 28 | }, 29 | { 30 | path: 'order-history', 31 | component: OrderHistoryComponent, 32 | canActivate: [AuthGuardService], 33 | }, 34 | ]; 35 | 36 | @NgModule({ 37 | imports: [RouterModule.forRoot(routes)], 38 | exports: [RouterModule], 39 | }) 40 | export class AppRoutingModule {} 41 | -------------------------------------------------------------------------------- /client/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /client/src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelparkadze/angular-ecommerce-app/2ce1bfd45da4e4d8543e4af332828c20341e9f16/client/src/app/app.component.scss -------------------------------------------------------------------------------- /client/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async () => { 7 | await TestBed.configureTestingModule({ 8 | imports: [ 9 | RouterTestingModule 10 | ], 11 | declarations: [ 12 | AppComponent 13 | ], 14 | }).compileComponents(); 15 | }); 16 | 17 | it('should create the app', () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.componentInstance; 20 | expect(app).toBeTruthy(); 21 | }); 22 | 23 | it(`should have as title 'client'`, () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | const app = fixture.componentInstance; 26 | expect(app.title).toEqual('client'); 27 | }); 28 | 29 | it('should render title', () => { 30 | const fixture = TestBed.createComponent(AppComponent); 31 | fixture.detectChanges(); 32 | const compiled = fixture.nativeElement; 33 | expect(compiled.querySelector('.content span').textContent).toContain('client app is running!'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /client/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'] 7 | }) 8 | export class AppComponent { 9 | title = 'client'; 10 | } 11 | -------------------------------------------------------------------------------- /client/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { AppRoutingModule } from './app-routing.module'; 4 | import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; 5 | import { IvyCarouselModule } from 'angular-responsive-carousel'; 6 | import { AppComponent } from './app.component'; 7 | import { HomeComponent } from './home/home.component'; 8 | import { LoginComponent } from './auth/components/login/login.component'; 9 | import { RegisterComponent } from './auth/components/register/register.component'; 10 | import { CartComponent } from './cart/cart.component'; 11 | import { OrderHistoryComponent } from './order-history/order-history.component'; 12 | import { ProductComponent } from './product/product.component'; 13 | import { CheckoutComponent } from './checkout/checkout.component'; 14 | import { ProfileComponent } from './profile/profile.component'; 15 | import { HeaderComponent } from './header/header.component'; 16 | import { FooterComponent } from './footer/footer.component'; 17 | import { ProductCardComponent } from './product-card/product-card.component'; 18 | import { AuthModule } from './auth/auth.module'; 19 | import { FormsModule } from '@angular/forms'; 20 | import { authInterceptorProviders } from './services/interceptor.service'; 21 | import { AuthGuardService } from './guards/auth-guard.service'; 22 | import { NZ_I18N } from 'ng-zorro-antd/i18n'; 23 | import { en_US } from 'ng-zorro-antd/i18n'; 24 | import { registerLocaleData } from '@angular/common'; 25 | import en from '@angular/common/locales/en'; 26 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 27 | import { NzButtonModule } from 'ng-zorro-antd/button'; 28 | import { NzDropDownModule } from 'ng-zorro-antd/dropdown'; 29 | import { NzIconModule } from 'ng-zorro-antd/icon'; 30 | import { NzInputModule } from 'ng-zorro-antd/input'; 31 | import { NzAlertModule } from 'ng-zorro-antd/alert'; 32 | import { NzInputNumberModule } from 'ng-zorro-antd/input-number'; 33 | import { SwiperModule } from 'swiper/angular'; 34 | import { NzSpinModule } from 'ng-zorro-antd/spin'; 35 | import { NzNotificationModule } from 'ng-zorro-antd/notification'; 36 | import { NzProgressModule } from 'ng-zorro-antd/progress'; 37 | import { NzTableModule } from 'ng-zorro-antd/table'; 38 | 39 | registerLocaleData(en); 40 | 41 | @NgModule({ 42 | declarations: [ 43 | AppComponent, 44 | HomeComponent, 45 | LoginComponent, 46 | RegisterComponent, 47 | CartComponent, 48 | OrderHistoryComponent, 49 | ProductComponent, 50 | CheckoutComponent, 51 | ProfileComponent, 52 | HeaderComponent, 53 | FooterComponent, 54 | ProductCardComponent, 55 | ], 56 | imports: [ 57 | BrowserModule, 58 | AppRoutingModule, 59 | IvyCarouselModule, 60 | HttpClientModule, 61 | AuthModule, 62 | FormsModule, 63 | BrowserAnimationsModule, 64 | NzButtonModule, 65 | NzDropDownModule, 66 | NzIconModule, 67 | NzInputModule, 68 | NzAlertModule, 69 | NzInputNumberModule, 70 | SwiperModule, 71 | NzSpinModule, 72 | NzNotificationModule, 73 | NzProgressModule, 74 | NzTableModule, 75 | ], 76 | providers: [ 77 | authInterceptorProviders, 78 | AuthGuardService, 79 | { provide: NZ_I18N, useValue: en_US }, 80 | ], 81 | bootstrap: [AppComponent], 82 | }) 83 | export class AppModule {} 84 | -------------------------------------------------------------------------------- /client/src/app/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | @NgModule({ 5 | declarations: [], 6 | imports: [CommonModule], 7 | }) 8 | export class AuthModule {} 9 | -------------------------------------------------------------------------------- /client/src/app/auth/components/login/login.component.html: -------------------------------------------------------------------------------- 1 | 50 | -------------------------------------------------------------------------------- /client/src/app/auth/components/login/login.component.scss: -------------------------------------------------------------------------------- 1 | .login-container { 2 | padding: 24px 16px; 3 | .form-container { 4 | max-width: 475px; 5 | padding: 24px 16px; 6 | margin: 0 auto; 7 | border-radius: 3px; 8 | background-color: #fff; 9 | 10 | > h2 { 11 | text-align: center; 12 | margin-bottom: 32px; 13 | } 14 | > .error-container { 15 | padding: 12px; 16 | margin: 0 0 16px; 17 | color: #fff; 18 | background-color: rgb(248, 51, 51); 19 | border-radius: 4px; 20 | } 21 | > form { 22 | > .cta-container { 23 | margin-top: 36px; 24 | margin-bottom: 60px; 25 | text-align: center; 26 | > button[type="submit"] { 27 | height: 52px; 28 | width: 100%; 29 | margin-bottom: 12px; 30 | } 31 | > div { 32 | text-align: left; 33 | } 34 | } 35 | .forgot-password { 36 | margin-bottom: 8px; 37 | text-align: center; 38 | font-size: 0.85rem; 39 | > a { 40 | color: #121212; 41 | } 42 | } 43 | } 44 | } 45 | } 46 | 47 | @media screen and (min-width: 768px) { 48 | .login-container { 49 | padding: 48px 16px 240px; 50 | // background-color: #f5f5f8; 51 | .form-container { 52 | padding: 36px; 53 | border: 1px solid #ccc; 54 | } 55 | } 56 | } 57 | 58 | .input-container { 59 | margin-bottom: 12px; 60 | > label { 61 | font-size: 0.875rem; 62 | display: inline-block; 63 | margin-bottom: 6px; 64 | } 65 | > input { 66 | padding: 10px 12px; 67 | width: 100%; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /client/src/app/auth/components/login/login.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LoginComponent } from './login.component'; 4 | 5 | describe('LoginComponent', () => { 6 | let component: LoginComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ LoginComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(LoginComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/src/app/auth/components/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { AuthService } from 'src/app/services/auth.service'; 4 | 5 | @Component({ 6 | selector: 'app-login', 7 | templateUrl: './login.component.html', 8 | styleUrls: ['./login.component.scss'], 9 | }) 10 | export class LoginComponent implements OnInit { 11 | email = ''; 12 | password = ''; 13 | error = ''; 14 | loading = false; 15 | 16 | constructor(private _auth: AuthService, private _router: Router) {} 17 | 18 | ngOnInit(): void {} 19 | 20 | onSubmit(): void { 21 | this.loading = true; 22 | this.error = ''; 23 | if (!this.email || !this.password) { 24 | this.error = 'Make sure to fill everything ;)'; 25 | } else { 26 | this._auth 27 | .login({ email: this.email, password: this.password }) 28 | .subscribe( 29 | (res) => { 30 | this.loading = false; 31 | this._router.navigate(['/']); 32 | }, 33 | (err) => { 34 | console.log(err); 35 | this.error = err.error.message; 36 | this.loading = false; 37 | } 38 | ); 39 | } 40 | } 41 | 42 | canSubmit(): boolean { 43 | return this.email.length > 0 && this.password.length > 0; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /client/src/app/auth/components/register/register.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Sign Up to Eccom

4 | 11 |
12 |
13 | 14 | 21 |
22 |
23 | 24 | 31 |
32 |
33 |
34 | 35 | 42 |
43 |
44 | 45 | 52 |
53 |
54 |
55 |
56 | 57 | I agree to receive instructional and promotional emails 58 |
59 |
60 | 61 | I agree to Eccom's Terms of Use & Privacy Policy 62 |
63 |
64 |
65 | 66 | 74 | 77 |
78 |
79 |
80 |
81 | -------------------------------------------------------------------------------- /client/src/app/auth/components/register/register.component.scss: -------------------------------------------------------------------------------- 1 | .register-container { 2 | padding: 24px 16px; 3 | .form-container { 4 | max-width: 475px; 5 | padding: 24px 16px; 6 | margin: 0 auto; 7 | border-radius: 3px; 8 | background-color: #fff; 9 | > h2 { 10 | text-align: center; 11 | margin-bottom: 32px; 12 | } 13 | > .error-container { 14 | padding: 12px; 15 | margin: 0 0 16px; 16 | color: #fff; 17 | background-color: rgb(248, 51, 51); 18 | border-radius: 4px; 19 | } 20 | > form { 21 | > .🤪 { 22 | display: grid; 23 | grid-column-gap: 12px; 24 | grid-template-columns: 1fr 1fr; 25 | margin-bottom: 24px; 26 | } 27 | > .✅ { 28 | margin-bottom: 32px; 29 | > .checkbox-container { 30 | display: flex; 31 | align-items: center; 32 | margin-bottom: 12px; 33 | > input { 34 | width: auto; 35 | margin-right: 12px; 36 | } 37 | > span { 38 | font-size: 0.75rem; 39 | } 40 | } 41 | } 42 | > .cta-container { 43 | text-align: center; 44 | > button[type="submit"] { 45 | height: 52px; 46 | width: 100%; 47 | padding: 0px 30px; 48 | margin-bottom: 12px; 49 | } 50 | > div { 51 | text-align: left; 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | @media screen and (min-width: 768px) { 59 | .register-container { 60 | padding: 48px 16px 240px; 61 | // background-color: #f5f5f8; 62 | .form-container { 63 | padding: 36px; 64 | border: 1px solid #ccc; 65 | } 66 | } 67 | } 68 | 69 | .input-container { 70 | margin-bottom: 12px; 71 | > label { 72 | font-size: 0.875rem; 73 | display: inline-block; 74 | margin-bottom: 6px; 75 | } 76 | > input { 77 | padding: 10px 12px; 78 | width: 100%; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /client/src/app/auth/components/register/register.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { RegisterComponent } from './register.component'; 4 | 5 | describe('RegisterComponent', () => { 6 | let component: RegisterComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ RegisterComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(RegisterComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/src/app/auth/components/register/register.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { AuthService } from 'src/app/services/auth.service'; 4 | import { ApiService } from '../../../services/api.service'; 5 | 6 | @Component({ 7 | selector: 'app-register', 8 | templateUrl: './register.component.html', 9 | styleUrls: ['./register.component.scss'], 10 | }) 11 | export class RegisterComponent implements OnInit { 12 | fullName = ''; 13 | email = ''; 14 | password = ''; 15 | confirmPassword = ''; 16 | errorMessage = ''; 17 | loading = false; 18 | constructor( 19 | private _api: ApiService, 20 | private _auth: AuthService, 21 | private _router: Router 22 | ) {} 23 | 24 | ngOnInit(): void {} 25 | 26 | onSubmit(): void { 27 | this.errorMessage = ''; 28 | if (this.fullName && this.password && this.email && this.confirmPassword) { 29 | if (this.password !== this.confirmPassword) { 30 | this.errorMessage = 'Passwords need to match'; 31 | } else { 32 | this.loading = true; 33 | this._auth 34 | .register({ 35 | fullName: this.fullName, 36 | email: this.email, 37 | password: this.password, 38 | }) 39 | .subscribe( 40 | (res) => { 41 | console.log(res); 42 | this.loading = false; 43 | this._router.navigate(['/login']); 44 | }, 45 | (err) => { 46 | this.errorMessage = err.error.message; 47 | this.loading = false; 48 | } 49 | ); 50 | } 51 | } else { 52 | this.errorMessage = 'Make sure to fill everything ;)'; 53 | } 54 | } 55 | 56 | canSubmit(): boolean { 57 | return this.fullName && this.email && this.password && this.confirmPassword 58 | ? true 59 | : false; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /client/src/app/cart/cart.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Your cart is empty. 4 |
5 |
6 |
7 |

Cart

8 |
9 |
14 |
15 | X 16 |
17 |
18 | 19 |
20 |
21 |
{{ product.title }}
22 |
{{ product.price | currency: "USD" }}
23 |
24 | 31 |
32 |
33 |
34 |
35 |
36 |
37 | 38 |

Total amount

39 |

{{ cartData.total | currency: "USD" }}

41 | 42 |

Total items

43 |

{{ cartData.products.length }}

44 |
45 |
46 |
47 | 55 | 56 |
57 |
58 |
59 | -------------------------------------------------------------------------------- /client/src/app/cart/cart.component.scss: -------------------------------------------------------------------------------- 1 | .cart-container { 2 | display: flex; 3 | flex-direction: column; 4 | &__empty { 5 | margin: 24px 16px 0; 6 | padding: 16px 24px; 7 | text-align: center; 8 | border: 1px solid #ccc; 9 | border-radius: 3px; 10 | font-size: 1rem; 11 | font-weight: 500; 12 | } 13 | &__list { 14 | max-height: 60vh; 15 | flex-grow: 1; 16 | padding: 16px; 17 | overflow-y: auto; 18 | border-bottom: 1px solid #ccc; 19 | .list-item { 20 | display: flex; 21 | align-items: center; 22 | margin-bottom: 24px; 23 | padding-bottom: 24px; 24 | border-bottom: 1px solid #ccc; 25 | position: relative; 26 | &__remove { 27 | border: 1px solid #ccc; 28 | height: 22px; 29 | width: 22px; 30 | display: flex; 31 | justify-content: center; 32 | align-items: center; 33 | cursor: pointer; 34 | position: absolute; 35 | right: 8px; 36 | top: 8px; 37 | > i { 38 | font-size: 1.25rem; 39 | } 40 | z-index: 1; 41 | } 42 | &__image { 43 | margin-right: 16px; 44 | min-height: 100px; 45 | min-width: 100px; 46 | max-height: 100px; 47 | max-width: 100px; 48 | > img { 49 | width: 100%; 50 | } 51 | } 52 | &__details { 53 | flex-grow: 1; 54 | position: relative; 55 | padding: 22px 0; 56 | padding-right: 18px; 57 | .title { 58 | margin-bottom: 8px; 59 | font-size: 1.125rem; 60 | font-weight: 500; 61 | } 62 | .price { 63 | margin-bottom: 12px; 64 | font-size: 1rem; 65 | } 66 | .cta { 67 | position: absolute; 68 | right: 0; 69 | bottom: -6px; 70 | } 71 | } 72 | } 73 | } 74 | &__total { 75 | padding: 24px 16px 16px; 76 | display: flex; 77 | flex-direction: column; 78 | .price { 79 | display: flex; 80 | flex-direction: column; 81 | margin-bottom: 16px; 82 | > span { 83 | flex-grow: 1; 84 | display: flex; 85 | align-items: center; 86 | > h3 { 87 | font-size: 1.25rem; 88 | flex-grow: 1; 89 | } 90 | } 91 | > span:nth-child(2) { 92 | > h3 { 93 | font-size: 1rem; 94 | color: #787878; 95 | font-weight: 400; 96 | } 97 | > h2 { 98 | font-size: 1.125rem; 99 | color: #787878; 100 | font-weight: 400; 101 | } 102 | } 103 | } 104 | .cta { 105 | display: flex; 106 | flex-direction: column; 107 | align-items: center; 108 | > button { 109 | height: 48px; 110 | width: 100%; 111 | margin-bottom: 12px; 112 | } 113 | } 114 | } 115 | } 116 | 117 | @media screen and (min-width: 1200px) { 118 | .cart-container { 119 | display: flex; 120 | flex-direction: row; 121 | max-width: 1024px; 122 | margin: 48px auto 0; 123 | border: 1px solid #ccc; 124 | border-radius: 3px; 125 | &__empty { 126 | margin: 0; 127 | padding-top: 40px; 128 | flex-grow: 1; 129 | border: none; 130 | } 131 | &__list { 132 | border: none; 133 | } 134 | &__total { 135 | width: 350px; 136 | padding: 0 16px; 137 | display: flex; 138 | justify-content: flex-start; 139 | align-items: flex-start; 140 | flex-direction: column; 141 | .price { 142 | width: 100%; 143 | display: flex; 144 | padding: 16px 0; 145 | > h3 { 146 | font-size: 1.25rem; 147 | } 148 | } 149 | .cta { 150 | width: 100%; 151 | padding: 0px; 152 | display: flex; 153 | flex-direction: column; 154 | align-items: center; 155 | > button { 156 | height: 48px; 157 | width: 100%; 158 | margin-bottom: 12px; 159 | } 160 | } 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /client/src/app/cart/cart.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CartComponent } from './cart.component'; 4 | 5 | describe('CartComponent', () => { 6 | let component: CartComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ CartComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(CartComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/src/app/cart/cart.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { CartService } from '../services/cart.service'; 3 | 4 | @Component({ 5 | selector: 'app-cart', 6 | templateUrl: './cart.component.html', 7 | styleUrls: ['./cart.component.scss'], 8 | }) 9 | export class CartComponent implements OnInit { 10 | cartData: any; 11 | 12 | constructor(private _cart: CartService) { 13 | this._cart.cartDataObs$.subscribe((cartData) => { 14 | this.cartData = cartData; 15 | console.log(cartData); 16 | }); 17 | } 18 | 19 | ngOnInit(): void {} 20 | 21 | updateCart(id: number, quantity: number): void { 22 | console.log({ id, quantity }); 23 | this._cart.updateCart(id, quantity); 24 | } 25 | 26 | removeCartItem(id: number): void { 27 | this._cart.removeProduct(id); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /client/src/app/checkout/checkout.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 8 |
9 | 10 | 11 |
12 |
13 |

Billing Address

14 |
15 |
19 | 20 | 28 |
29 |
30 | 37 |
38 |
39 | 40 |
41 |
42 |

Payment Details

43 |
44 |
45 | 46 | 54 |
55 |
56 | 57 | 65 |
66 |
67 |
68 | 69 | 77 |
78 |
79 | 80 | 88 |
89 |
90 |
91 | 98 |
99 |
100 | 101 |
102 |
103 |
104 |

Order Summary

105 |
106 |
Item(s) {{ cartData.products.length }}
107 |
Payment method Credit
108 |
109 | Order total {{ cartData.total | currency: "USD" }} 110 |
111 |
112 | 120 |
121 |
122 |

Order Items

123 |
124 |
125 |
126 | 127 |
128 |
129 | {{ item.title }} 130 |
131 |
132 | {{ item.price | currency: "USD" }} x {{ item.quantity }} = 133 | {{ item.quantity * item.price | currency: "USD" }} 134 |
135 |
136 |
137 |
138 |
139 |
140 | 141 |
142 |
143 |
144 |
145 |

Thank you!

146 |

Please check your email for the order confirmation

147 |
148 |
149 | Order ID: {{ orderId }} 150 |
151 |
152 | 155 |
156 |

Order Summary

157 |
158 |
159 |
160 | 161 |
162 |
163 | {{ item.title }} 164 |
165 |
166 | {{ item.price | currency: "USD" }} x {{ item.quantity }} = 167 | {{ item.quantity * item.price | currency: "USD" }} 168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 | -------------------------------------------------------------------------------- /client/src/app/checkout/checkout.component.scss: -------------------------------------------------------------------------------- 1 | .checkout-container { 2 | padding: 24px 16px; 3 | position: relative; 4 | // First step 5 | .progress-container { 6 | top: -12px; 7 | left: 0px; 8 | position: absolute; 9 | height: 20px; 10 | width: 100%; 11 | } 12 | .billing-container { 13 | > h2 { 14 | margin-bottom: 16px; 15 | } 16 | > form { 17 | margin-bottom: 24px; 18 | > .input-container { 19 | margin-bottom: 12px; 20 | > label { 21 | display: block; 22 | margin-bottom: 2px; 23 | } 24 | > input { 25 | height: 44px; 26 | } 27 | } 28 | } 29 | > button { 30 | width: 100%; 31 | height: 48px; 32 | } 33 | } 34 | // Second step 35 | .payment-container { 36 | > h2 { 37 | margin-bottom: 16px; 38 | } 39 | > form { 40 | margin-bottom: 24px; 41 | .input-container { 42 | margin-bottom: 12px; 43 | label { 44 | display: block; 45 | margin-bottom: 2px; 46 | } 47 | input { 48 | height: 44px; 49 | } 50 | } 51 | > .‼️ { 52 | display: grid; 53 | grid-template-columns: 1fr 1fr; 54 | column-gap: 12px; 55 | } 56 | } 57 | > button { 58 | width: 100%; 59 | height: 48px; 60 | } 61 | } 62 | // Third step 63 | .summary-container { 64 | &__order { 65 | > h2 { 66 | margin-bottom: 16px; 67 | } 68 | margin-bottom: 40px; 69 | .details { 70 | margin-bottom: 12px; 71 | > div { 72 | display: flex; 73 | margin-bottom: 3px; 74 | > span { 75 | margin-right: auto; 76 | } 77 | } 78 | } 79 | > button { 80 | height: 44px; 81 | width: 100%; 82 | } 83 | } 84 | &__items { 85 | > h2 { 86 | margin-bottom: 16px; 87 | } 88 | .item-list { 89 | max-height: 300px; 90 | overflow-y: auto; 91 | .item-container { 92 | display: flex; 93 | align-items: center; 94 | font-weight: 500; 95 | margin-bottom: 24px; 96 | padding: 8px 0; 97 | .item-image { 98 | margin-right: 16px; 99 | > img { 100 | width: 80px; 101 | } 102 | } 103 | .item-title { 104 | flex-grow: 1; 105 | } 106 | .item-total { 107 | text-align: right; 108 | padding-left: 16px; 109 | } 110 | } 111 | } 112 | } 113 | } 114 | .confirmation-container { 115 | text-align: center; 116 | > header { 117 | padding-bottom: 40px; 118 | text-align: center; 119 | border-bottom: 1px solid #ccc; 120 | .title { 121 | > h1 { 122 | font-size: 2.75rem; 123 | } 124 | } 125 | .order-id { 126 | > span { 127 | font-weight: 500; 128 | } 129 | } 130 | } 131 | .cta-button { 132 | width: 100%; 133 | max-width: 250px; 134 | height: 44px; 135 | margin: 24px auto auto; 136 | } 137 | .products-container { 138 | text-align: left; 139 | padding-top: 40px; 140 | > h2 { 141 | margin-bottom: 16px; 142 | } 143 | &__list { 144 | .list-item { 145 | display: flex; 146 | align-items: center; 147 | font-weight: 500; 148 | margin-bottom: 24px; 149 | padding: 8px 0; 150 | .item-image { 151 | margin-right: 16px; 152 | > img { 153 | width: 80px; 154 | } 155 | } 156 | .item-title { 157 | flex-grow: 1; 158 | } 159 | .item-total { 160 | text-align: right; 161 | padding-left: 16px; 162 | } 163 | } 164 | } 165 | } 166 | } 167 | } 168 | 169 | @media screen and (min-width: 1200px) { 170 | .checkout-container { 171 | padding: 0; 172 | padding-bottom: 60px; 173 | .progress-container { 174 | top: -60px; 175 | left: 0px; 176 | position: absolute; 177 | height: 20px; 178 | width: 100%; 179 | } 180 | .billing-container { 181 | padding: 24px 16px; 182 | max-width: 600px; 183 | margin: 48px auto 0; 184 | border: 1px solid #ccc; 185 | border-radius: 3px; 186 | } 187 | .payment-container { 188 | padding: 24px 16px; 189 | max-width: 600px; 190 | margin: 48px auto 0; 191 | border: 1px solid #ccc; 192 | border-radius: 3px; 193 | } 194 | .summary-container { 195 | max-width: 1024px; 196 | margin: 48px auto 0; 197 | border: 1px solid #ccc; 198 | border-radius: 3px; 199 | display: flex; 200 | flex-direction: row-reverse; 201 | &__order { 202 | width: 350px; 203 | padding: 16px; 204 | } 205 | &__items { 206 | flex-grow: 1; 207 | > h2 { 208 | padding: 16px; 209 | margin: 0; 210 | } 211 | .item-list { 212 | padding: 16px; 213 | } 214 | } 215 | } 216 | .confirmation-container { 217 | max-width: 700px; 218 | margin: 48px auto 0; 219 | padding: 24px; 220 | border: 1px solid #ccc; 221 | border-radius: 3px; 222 | } 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /client/src/app/checkout/checkout.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CheckoutComponent } from './checkout.component'; 4 | 5 | describe('CheckoutComponent', () => { 6 | let component: CheckoutComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ CheckoutComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(CheckoutComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/src/app/checkout/checkout.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { AuthService } from '../services/auth.service'; 3 | import { CartService } from '../services/cart.service'; 4 | 5 | @Component({ 6 | selector: 'app-checkout', 7 | templateUrl: './checkout.component.html', 8 | styleUrls: ['./checkout.component.scss'], 9 | }) 10 | export class CheckoutComponent implements OnInit { 11 | currentUser: any; 12 | currentStep = 1; 13 | cardNumber: string; 14 | cardName: string; 15 | cardExpiry: string; 16 | cardCode: string; 17 | cartData: any; 18 | products: any; 19 | loading = false; 20 | successMessage = ''; 21 | orderId; 22 | 23 | constructor(private _auth: AuthService, private _cart: CartService) { 24 | this._auth.user.subscribe((user) => { 25 | if (user) { 26 | this.currentUser = user; 27 | this.billingAddress[0].value = user.fname; 28 | this.billingAddress[1].value = user.email; 29 | } 30 | }); 31 | 32 | this._cart.cartDataObs$.subscribe((cartData) => { 33 | this.cartData = cartData; 34 | }); 35 | } 36 | 37 | ngOnInit(): void {} 38 | 39 | submitCheckout() { 40 | this.loading = true; 41 | setTimeout(() => { 42 | this._cart 43 | .submitCheckout(this.currentUser.user_id, this.cartData) 44 | .subscribe( 45 | (res: any) => { 46 | console.log(res); 47 | this.loading = false; 48 | this.orderId = res.orderId; 49 | this.products = res.products; 50 | this.currentStep = 4; 51 | this._cart.clearCart(); 52 | }, 53 | (err) => { 54 | console.log(err); 55 | this.loading = false; 56 | } 57 | ); 58 | }, 750); 59 | } 60 | 61 | getProgressPrecent() { 62 | return (this.currentStep / 4) * 100; 63 | } 64 | 65 | submitBilling(): void { 66 | this.nextStep(); 67 | } 68 | 69 | canBillingSubmit(): boolean { 70 | return this.billingAddress.filter((field) => field.value.length > 0) 71 | .length !== 7 72 | ? true 73 | : false; 74 | } 75 | 76 | submitPayment(): void { 77 | this.nextStep(); 78 | } 79 | 80 | canPaymentSubmit(): boolean { 81 | return this.cardNumber && this.cardName && this.cardExpiry && this.cardCode 82 | ? true 83 | : false; 84 | } 85 | 86 | nextStep(): void { 87 | this.currentStep += 1; 88 | localStorage.setItem('checkoutStep', this.currentStep.toString()); 89 | } 90 | 91 | prevStep(): void { 92 | if (this.currentStep > 1) { 93 | this.currentStep -= 1; 94 | localStorage.setItem('checkoutStep', this.currentStep.toString()); 95 | } 96 | } 97 | 98 | billingAddress = [ 99 | { 100 | name: 'Full name', 101 | placeholder: 'Enter your full name', 102 | type: 'text', 103 | value: '', 104 | }, 105 | { 106 | name: 'Email', 107 | placeholder: 'Enter your email address', 108 | type: 'email', 109 | value: '', 110 | }, 111 | { 112 | name: 'Address', 113 | placeholder: 'Enter your address', 114 | type: 'text', 115 | value: '', 116 | }, 117 | { 118 | name: 'City', 119 | placeholder: 'Enter your city', 120 | type: 'text', 121 | value: '', 122 | }, 123 | { 124 | name: 'Country', 125 | placeholder: 'Enter your country', 126 | type: 'text', 127 | value: '', 128 | }, 129 | { 130 | name: 'ZIP', 131 | placeholder: 'Enter your zip code', 132 | type: 'text', 133 | value: '', 134 | }, 135 | { 136 | name: 'Telephone', 137 | placeholder: 'Enter your telephone number', 138 | type: 'text', 139 | value: '', 140 | }, 141 | ]; 142 | } 143 | -------------------------------------------------------------------------------- /client/src/app/footer/footer.component.html: -------------------------------------------------------------------------------- 1 |

footer works!

2 | -------------------------------------------------------------------------------- /client/src/app/footer/footer.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelparkadze/angular-ecommerce-app/2ce1bfd45da4e4d8543e4af332828c20341e9f16/client/src/app/footer/footer.component.scss -------------------------------------------------------------------------------- /client/src/app/footer/footer.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { FooterComponent } from './footer.component'; 4 | 5 | describe('FooterComponent', () => { 6 | let component: FooterComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ FooterComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(FooterComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/src/app/footer/footer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-footer', 5 | templateUrl: './footer.component.html', 6 | styleUrls: ['./footer.component.scss'] 7 | }) 8 | export class FooterComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit(): void { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /client/src/app/guards/auth-guard.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AuthGuardService } from './auth-guard.service'; 4 | 5 | describe('AuthGuardService', () => { 6 | let service: AuthGuardService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(AuthGuardService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /client/src/app/guards/auth-guard.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | ActivatedRouteSnapshot, 4 | CanActivate, 5 | Router, 6 | RouterStateSnapshot, 7 | UrlTree, 8 | } from '@angular/router'; 9 | import { Observable } from 'rxjs'; 10 | import { TokenStorageService } from '../services/token-storage.service'; 11 | 12 | @Injectable({ 13 | providedIn: 'root', 14 | }) 15 | export class AuthGuardService implements CanActivate { 16 | constructor(private _route: Router, private _token: TokenStorageService) {} 17 | canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): any { 18 | const currentUser = this._token.getUser(); 19 | if (currentUser) { 20 | return true; 21 | } 22 | this._route.navigate(['/login']); 23 | return false; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /client/src/app/header/header.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 17 |
18 |
19 |

Eccom

20 |
21 |
22 | 23 |
{{ cartData.products.length }}
24 | 42 | 43 |
44 |
48 | Your cart is empty. 49 |
50 |
54 |
55 |
63 |
64 |
68 | X 69 |
70 |
71 | 72 |
73 |
74 |
{{ product.title }}
75 |
76 | {{ product.quantity }} x 77 | {{ product.price | currency: "USD" }} 78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | {{ cartData.products.length }} Item(s) selected 86 |
87 |
88 | Total: {{ cartData.total | currency: "USD" }} 89 |
90 |
91 |
92 | 105 | 119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 | 127 | 141 | 142 |
143 | 144 |
145 |

Eccom

146 | 156 |
157 | 165 | 173 | 176 |
177 |
178 |
179 | 180 |
184 | -------------------------------------------------------------------------------- /client/src/app/header/header.component.scss: -------------------------------------------------------------------------------- 1 | .header-container { 2 | height: 60px; 3 | padding: 0 16px; 4 | display: flex; 5 | align-items: center; 6 | border-bottom: 1px solid #ccc; 7 | > div { 8 | height: 100%; 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | } 13 | &__left { 14 | } 15 | &__middle { 16 | flex-grow: 1; 17 | text-align: center; 18 | > h3 { 19 | } 20 | } 21 | &__right { 22 | position: relative; 23 | .cart-counter { 24 | color: #fff; 25 | background-color: red; 26 | width: 17px; 27 | height: 17px; 28 | font-size: 11px; 29 | display: flex; 30 | justify-content: center; 31 | align-items: center; 32 | border-radius: 50px; 33 | position: absolute; 34 | right: 0px; 35 | top: 12px; 36 | z-index: 2; 37 | } 38 | } 39 | } 40 | 41 | @media screen and (min-width: 768px) { 42 | .header-container { 43 | padding: 0 24px; 44 | } 45 | } 46 | 47 | // Cart Dropdown 48 | .cart-dropdown { 49 | max-width: 280px; 50 | min-width: 280px; 51 | background-color: #fff; 52 | border: 1px solid #ccc; 53 | &__empty { 54 | padding: 12px; 55 | text-align: center; 56 | font-weight: 500; 57 | } 58 | &__content { 59 | padding-top: 12px; 60 | .product-list { 61 | padding: 0 12px; 62 | max-height: 180px; 63 | overflow-y: auto; 64 | border-bottom: 1px solid #ccc; 65 | &__item { 66 | padding: 12px 0; 67 | margin-bottom: 12px; 68 | cursor: default; 69 | .product-item { 70 | display: flex; 71 | position: relative; 72 | align-items: center; 73 | &__remove { 74 | top: 4px; 75 | right: 0px; 76 | position: absolute; 77 | border: 1px solid #ccc; 78 | height: 22px; 79 | width: 22px; 80 | display: flex; 81 | justify-content: center; 82 | align-items: center; 83 | cursor: pointer; 84 | } 85 | &__image { 86 | > img { 87 | height: 72px; 88 | width: 72px; 89 | object-fit: contain; 90 | } 91 | } 92 | &__description { 93 | margin-left: 12px; 94 | padding-right: 24px; 95 | .product-name { 96 | font-size: 1.125rem; 97 | } 98 | .product-amount { 99 | > span { 100 | font-weight: 500; 101 | } 102 | } 103 | } 104 | } 105 | } 106 | } 107 | .cart-info { 108 | padding: 16px; 109 | .selected-amount { 110 | font-size: 0.875rem; 111 | } 112 | .total-price { 113 | font-size: 1.25rem; 114 | font-weight: 500; 115 | } 116 | } 117 | .cart-cta { 118 | display: flex; 119 | flex-direction: row; 120 | > button { 121 | width: 100%; 122 | } 123 | } 124 | } 125 | } 126 | 127 | .nav-container { 128 | padding: 16px; 129 | background-color: #eee; 130 | border-bottom: 1px solid #ccc; 131 | > nav { 132 | > ul { 133 | display: flex; 134 | > li { 135 | cursor: pointer; 136 | margin-right: 16px; 137 | } 138 | } 139 | } 140 | } 141 | 142 | // Side Menu Styles 143 | 144 | .side-menu { 145 | width: 285px; 146 | height: 100vh; 147 | position: fixed; 148 | top: 0px; 149 | padding: 56px 0px 24px; 150 | transform: translateX(-285px); 151 | background-color: #fff; 152 | border-right: 1px solid #ccc; 153 | transition: all 0.35s ease; 154 | z-index: 10002; 155 | > i { 156 | top: 24px; 157 | right: 24px; 158 | position: absolute; 159 | font-size: 1.25rem; 160 | cursor: pointer; 161 | } 162 | &__content { 163 | height: 100%; 164 | position: relative; 165 | > h2 { 166 | padding-left: 24px; 167 | margin-bottom: 24px; 168 | outline: none; 169 | cursor: pointer; 170 | transition: transform 0.2s; 171 | &:hover { 172 | transform: translateX(16px); 173 | } 174 | } 175 | > nav { 176 | > ul { 177 | > li { 178 | outline: none; 179 | padding: 8px 0 8px 24px; 180 | margin-bottom: 12px; 181 | cursor: pointer; 182 | transition: background-color 0.2s; 183 | &:hover { 184 | background-color: rgb(236, 236, 236); 185 | } 186 | } 187 | } 188 | } 189 | .🍑 { 190 | position: absolute; 191 | bottom: 0; 192 | left: 24px; 193 | display: flex; 194 | flex-direction: column; 195 | justify-content: center; 196 | align-items: center; 197 | > button { 198 | margin-bottom: 8px; 199 | height: 44px; 200 | width: 236px; 201 | flex-grow: 1; 202 | border: 1px solid #ccc; 203 | background: transparent; 204 | } 205 | } 206 | } 207 | } 208 | 209 | .side-menu.show { 210 | transform: translateX(0); 211 | } 212 | 213 | .darken-background { 214 | height: 100vh; 215 | width: 100vw; 216 | position: fixed; 217 | top: 0; 218 | left: 0; 219 | background-color: transparent; 220 | transition: background-color 0.35s ease; 221 | z-index: 10001; 222 | pointer-events: none; 223 | } 224 | 225 | .darken-background.enable { 226 | background-color: rgba(0, 0, 0, 0.5); 227 | pointer-events: auto; 228 | } 229 | -------------------------------------------------------------------------------- /client/src/app/header/header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HeaderComponent } from './header.component'; 4 | 5 | describe('HeaderComponent', () => { 6 | let component: HeaderComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ HeaderComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(HeaderComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/src/app/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { HostListener } from '@angular/core'; 3 | import { AuthService } from '../services/auth.service'; 4 | import { CartService } from '../services/cart.service'; 5 | import { TokenStorageService } from '../services/token-storage.service'; 6 | 7 | @Component({ 8 | selector: 'app-header', 9 | templateUrl: './header.component.html', 10 | styleUrls: ['./header.component.scss'], 11 | }) 12 | export class HeaderComponent implements OnInit { 13 | screenHeight: any; 14 | screenWidth: any; 15 | isMenuOpen = false; 16 | isMobile = false; 17 | isLoggedIn = false; 18 | dropdownVisible = false; 19 | cartData: any; 20 | 21 | @HostListener('window:resize', ['$event']) 22 | getScreenSize(event?) { 23 | this.screenHeight = window.innerHeight; 24 | this.screenWidth = window.innerWidth; 25 | 26 | if (this.screenWidth > 768) this.isMobile = false; 27 | else this.isMobile = true; 28 | } 29 | 30 | constructor( 31 | private _token: TokenStorageService, 32 | private _auth: AuthService, 33 | private _cart: CartService 34 | ) { 35 | this.getScreenSize(); 36 | this._auth.user.subscribe((user) => { 37 | if (user) this.isLoggedIn = true; 38 | else this.isLoggedIn = false; 39 | }); 40 | this._cart.cartDataObs$.subscribe((cartData) => { 41 | this.cartData = cartData; 42 | }); 43 | } 44 | 45 | ngOnInit(): void { 46 | if (this._token.getUser()) this.isLoggedIn = true; 47 | else this.isLoggedIn = false; 48 | } 49 | 50 | toggleMenu() { 51 | this.isMenuOpen = !this.isMenuOpen; 52 | } 53 | 54 | toggleDropdown() { 55 | this.dropdownVisible = !this.dropdownVisible; 56 | } 57 | 58 | removeProductFromCart(id: number) { 59 | this._cart.removeProduct(id); 60 | } 61 | 62 | logout() { 63 | this._auth.logout(); 64 | this.isMenuOpen = false; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /client/src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Categories

5 |
6 |
7 | 14 | 15 |
{{ c.name }}
16 |
17 |
18 |
19 |
20 |
21 |
22 |

New Products

23 |
24 |
25 | 26 | 31 | 32 | 33 |
34 |
35 | 46 | 47 |
48 |
49 |
50 | 57 |
58 |
59 | -------------------------------------------------------------------------------- /client/src/app/home/home.component.scss: -------------------------------------------------------------------------------- 1 | .home-container { 2 | padding-top: 24px; 3 | .categories { 4 | margin: 0 auto; 5 | padding: 24px 16px; 6 | max-width: calc(350px + 32px); // width of cards + all padding + gap 7 | &__header { 8 | margin-bottom: 16px; 9 | > h2 { 10 | font-size: 1.75rem; 11 | } 12 | } 13 | &__list { 14 | // just for categories 15 | .swiper-pagination-bullet-active { 16 | background-color: #fff; 17 | } 18 | .category-card { 19 | height: 150px; 20 | display: flex; 21 | justify-content: center; 22 | align-items: center; 23 | border-radius: 3px; 24 | font-size: 1.375rem; 25 | letter-spacing: 5px; 26 | font-weight: 500; 27 | text-transform: uppercase; 28 | color: #fff; 29 | background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); 30 | background-size: 400% 400%; 31 | animation: gradient 15s ease infinite; 32 | cursor: pointer; 33 | } 34 | } 35 | } 36 | .products { 37 | margin: 0 auto; 38 | padding: 24px 16px; 39 | max-width: calc(350px + 32px); // width of cards + all padding + gap 40 | &__loading { 41 | text-align: center; 42 | } 43 | &__header { 44 | margin-bottom: 16px; 45 | > h2 { 46 | font-size: 1.75rem; 47 | } 48 | } 49 | &__list { 50 | display: grid; 51 | grid-template-columns: 1fr; 52 | row-gap: 24px; 53 | .product-card { 54 | height: 100%; 55 | max-width: 350px; 56 | margin: 0 auto; 57 | &__image { 58 | cursor: pointer; 59 | } 60 | } 61 | } 62 | } 63 | .load-products { 64 | text-align: center; 65 | padding: 16px 16px 32px; 66 | > button { 67 | max-width: 341px; 68 | height: 44px; 69 | width: 100%; 70 | } 71 | } 72 | } 73 | 74 | @media screen and (min-width: 768px) { 75 | .home-container { 76 | .categories { 77 | max-width: calc(700px + 72px); // width of cards + all padding + gap 78 | padding: 36px 24px; 79 | &__list { 80 | .category-card { 81 | height: 200px; 82 | } 83 | } 84 | } 85 | .products { 86 | max-width: calc(700px + 72px); // width of cards + all padding + gap 87 | padding: 36px 24px; 88 | &__header { 89 | margin-bottom: 16px; 90 | } 91 | &__list { 92 | display: grid; 93 | grid-template-columns: 1fr 1fr; 94 | column-gap: 24px; 95 | row-gap: 24px; 96 | .product-card { 97 | } 98 | } 99 | } 100 | } 101 | } 102 | 103 | @media screen and (min-width: 1200px) { 104 | .home-container { 105 | .categories { 106 | max-width: calc(1050px + 96px); // width of cards + all padding + gap 107 | padding: 36px 24px; 108 | &__list { 109 | .category-card { 110 | width: 348px; 111 | } 112 | } 113 | } 114 | .products { 115 | max-width: calc(1050px + 96px); // width of cards + all padding + gap 116 | padding: 36px 24px; 117 | &__header { 118 | margin-bottom: 16px; 119 | } 120 | &__list { 121 | display: grid; 122 | grid-template-columns: 1fr 1fr 1fr; 123 | .product-card { 124 | } 125 | } 126 | } 127 | } 128 | } 129 | 130 | @keyframes gradient { 131 | 0% { 132 | background-position: 0% 50%; 133 | } 134 | 50% { 135 | background-position: 100% 50%; 136 | } 137 | 100% { 138 | background-position: 0% 50%; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /client/src/app/home/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HomeComponent } from './home.component'; 4 | 5 | describe('HomeComponent', () => { 6 | let component: HomeComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ HomeComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(HomeComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | OnInit, 4 | ViewEncapsulation, 5 | HostListener, 6 | } from '@angular/core'; 7 | import { CartService } from '../services/cart.service'; 8 | import { ProductService } from '../services/product.service'; 9 | import { Products, Product } from '../shared/models/product.model'; 10 | 11 | @Component({ 12 | selector: 'app-home', 13 | templateUrl: './home.component.html', 14 | styleUrls: ['./home.component.scss'], 15 | encapsulation: ViewEncapsulation.None, 16 | }) 17 | export class HomeComponent implements OnInit { 18 | products: Product[] = []; 19 | categories: any[] = [ 20 | { 21 | name: 'Laptops', 22 | }, 23 | { 24 | name: 'Accessories', 25 | }, 26 | { 27 | name: 'Cameras', 28 | }, 29 | ]; 30 | loading = false; 31 | productPageCounter = 1; 32 | additionalLoading = false; 33 | 34 | constructor( 35 | private productService: ProductService, 36 | private cartService: CartService 37 | ) {} 38 | 39 | public screenWidth: any; 40 | public screenHeight: any; 41 | 42 | @HostListener('window:resize', ['$event']) 43 | onResize(event) { 44 | this.screenWidth = window.innerWidth; 45 | this.screenHeight = window.innerHeight; 46 | } 47 | 48 | ngOnInit(): void { 49 | this.screenWidth = window.innerWidth; 50 | this.screenHeight = window.innerHeight; 51 | this.loading = true; 52 | setTimeout(() => { 53 | this.productService.getAllProducts(9, this.productPageCounter).subscribe( 54 | (res: any) => { 55 | console.log(res); 56 | this.products = res; 57 | this.loading = false; 58 | }, 59 | (err) => { 60 | console.log(err); 61 | this.loading = false; 62 | } 63 | ); 64 | }, 500); 65 | } 66 | 67 | showMoreProducts(): void { 68 | this.additionalLoading = true; 69 | this.productPageCounter = this.productPageCounter + 1; 70 | setTimeout(() => { 71 | this.productService.getAllProducts(9, this.productPageCounter).subscribe( 72 | (res: any) => { 73 | console.log(res); 74 | this.products = [...this.products, ...res]; 75 | this.additionalLoading = false; 76 | }, 77 | (err) => { 78 | console.log(err); 79 | this.additionalLoading = false; 80 | } 81 | ); 82 | }, 500); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /client/src/app/order-history/order-history.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Order History

3 | {{ error }} 4 | 5 | 6 | 7 | Name 8 | Quantity 9 | OrderID 10 | 11 | 12 | 13 | 14 | {{ item.title }} 15 | {{ item.quantity }} 16 | {{ item.order_id }} 17 | 18 | 19 | 20 |
21 | -------------------------------------------------------------------------------- /client/src/app/order-history/order-history.component.scss: -------------------------------------------------------------------------------- 1 | .order-history-container { 2 | padding: 24px 16px; 3 | max-width: 1024px; 4 | margin: 0 auto; 5 | > h2 { 6 | margin-bottom: 16px; 7 | } 8 | .order-list { 9 | .order-container { 10 | .order-image { 11 | > img { 12 | width: 100%; 13 | } 14 | } 15 | .order-title { 16 | } 17 | .order-total { 18 | } 19 | } 20 | } 21 | } 22 | 23 | @media screen and (min-width: 1200px) { 24 | .order-history-container { 25 | margin-top: 48px; 26 | padding: 24px; 27 | border: 1px solid #ccc; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /client/src/app/order-history/order-history.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { OrderHistoryComponent } from './order-history.component'; 4 | 5 | describe('OrderHistoryComponent', () => { 6 | let component: OrderHistoryComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ OrderHistoryComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(OrderHistoryComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/src/app/order-history/order-history.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ApiService } from '../services/api.service'; 3 | import { AuthService } from '../services/auth.service'; 4 | import { ProductService } from '../services/product.service'; 5 | 6 | @Component({ 7 | selector: 'app-order-history', 8 | templateUrl: './order-history.component.html', 9 | styleUrls: ['./order-history.component.scss'], 10 | }) 11 | export class OrderHistoryComponent implements OnInit { 12 | listOfData: any[] = [ 13 | { 14 | key: '1', 15 | name: 'John Brown', 16 | age: 32, 17 | address: 'New York No. 1 Lake Park', 18 | }, 19 | { 20 | key: '2', 21 | name: 'Jim Green', 22 | age: 42, 23 | address: 'London No. 1 Lake Park', 24 | }, 25 | { 26 | key: '3', 27 | name: 'Joe Black', 28 | age: 32, 29 | address: 'Sidney No. 1 Lake Park', 30 | }, 31 | ]; 32 | user: any; 33 | orders: any[] = []; 34 | error = ''; 35 | constructor( 36 | private _api: ApiService, 37 | private _auth: AuthService, 38 | private _product: ProductService 39 | ) { 40 | this.user = this._auth.getUser(); 41 | } 42 | 43 | ngOnInit(): void { 44 | this._api.getTypeRequest(`orders/?userId=${this.user.user_id}`).subscribe( 45 | (res: any) => { 46 | console.log(res); 47 | res.data.forEach((item) => { 48 | this._product 49 | .getSingleProduct(item.product_id) 50 | .subscribe((product) => { 51 | console.log(product); 52 | this.orders.push({ ...product, ...item }); 53 | }); 54 | }); 55 | // let uniqueProductsArray = Array.from( 56 | // new Set(res.data.map((p) => p.product_id)) 57 | // ); 58 | }, 59 | (err) => { 60 | this.error = err.error.message; 61 | } 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /client/src/app/product-card/product-card.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |
7 |
8 |
{{ title }}
9 |
{{ short_desc }}
10 |
11 |
{{ price | currency: "USD" }}
12 |
13 |
14 |
15 | 30 |
31 |
32 | -------------------------------------------------------------------------------- /client/src/app/product-card/product-card.component.scss: -------------------------------------------------------------------------------- 1 | .product-card { 2 | display: flex; 3 | flex-direction: column; 4 | border: 1px solid #ccc; 5 | border-radius: 3px; 6 | transition: all 0.2s; 7 | overflow: hidden; 8 | &__image { 9 | > img { 10 | width: 100%; 11 | object-fit: cover; 12 | border-bottom: 1px solid #ccc; 13 | } 14 | } 15 | &__info { 16 | flex-grow: 1; 17 | padding: 16px 16px 32px; 18 | > header { 19 | display: flex; 20 | align-items: center; 21 | .title { 22 | flex-grow: 1; 23 | .product-name { 24 | padding-right: 12px; 25 | font-size: 1.125rem; 26 | font-weight: 500; 27 | text-transform: uppercase; 28 | margin-bottom: 4px; 29 | } 30 | .product-short-desc { 31 | color: #777; 32 | } 33 | } 34 | .product-price { 35 | font-size: 1.375rem; 36 | font-weight: 600; 37 | } 38 | } 39 | } 40 | &__cta { 41 | > button { 42 | height: 48px; 43 | width: 100%; 44 | border: none; 45 | background-color: #fff; 46 | border-top: 1px solid #ccc; 47 | cursor: pointer; 48 | transition: background-color 0.2s; 49 | } 50 | } 51 | } 52 | 53 | @media screen and (min-width: 1024px) { 54 | .product-card { 55 | &:hover { 56 | transform: scale(1.0125); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /client/src/app/product-card/product-card.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProductCardComponent } from './product-card.component'; 4 | 5 | describe('ProductCardComponent', () => { 6 | let component: ProductCardComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ProductCardComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ProductCardComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/src/app/product-card/product-card.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-product-card', 5 | templateUrl: './product-card.component.html', 6 | styleUrls: ['./product-card.component.scss'], 7 | }) 8 | export class ProductCardComponent implements OnInit { 9 | @Input() title: string; 10 | @Input() image: string; 11 | @Input() short_desc: string; 12 | @Input() category: string; 13 | @Input() quantity: number; 14 | @Input() price: string; 15 | @Input() id: number; 16 | @Input() onAdd: any; 17 | 18 | constructor() {} 19 | 20 | ngOnInit(): void {} 21 | } 22 | -------------------------------------------------------------------------------- /client/src/app/product/product.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 12 | 13 | `Product image` 14 | 15 | 16 | 17 | 24 | 29 | `Product image` 30 | 31 | 32 |
33 |
34 |

{{ product.title }}

35 |
36 |
37 | 38 | 39 | 40 | 41 | 42 |
43 | 10 Review(s) | 45 | Add your review 47 |
48 |
49 |

{{ product.price | currency: "USD" }}

50 | {{ quantity === 0 ? "Out of stock" : "In stock" }} 55 |
56 |
{{ product.description }}
57 |
58 |
59 | QTY: 60 | 66 | 74 |
75 |
76 | Category: {{ product.category }} 77 |
78 | 85 |
86 |
87 |
88 |
89 |
90 | -------------------------------------------------------------------------------- /client/src/app/product/product.component.scss: -------------------------------------------------------------------------------- 1 | .product-container { 2 | padding-bottom: 60px; 3 | &__image { 4 | margin-bottom: 24px; 5 | text-align: center; 6 | } 7 | &__details { 8 | margin: 0 auto; 9 | max-width: 1024px; 10 | padding: 16px; 11 | .title { 12 | font-size: 1.5rem; 13 | line-height: 1.25; 14 | margin-bottom: 6px; 15 | } 16 | .reviews { 17 | display: flex; 18 | align-items: center; 19 | margin-bottom: 4px; 20 | .rating { 21 | margin-right: 12px; 22 | > i { 23 | margin-right: 2px; 24 | font-size: 1rem; 25 | color: #aaa; 26 | cursor: pointer; 27 | &:nth-child(n + 1):nth-child(-n + 4) { 28 | color: #f9d71c; 29 | } 30 | } 31 | } 32 | .review-link { 33 | color: #525252; 34 | font-size: 0.75rem; 35 | } 36 | } 37 | .price-container { 38 | display: flex; 39 | align-items: center; 40 | margin-bottom: 12px; 41 | .price { 42 | font-size: 1.75rem; 43 | margin-right: 12px; 44 | } 45 | .availability { 46 | font-size: 1rem; 47 | text-transform: uppercase; 48 | } 49 | } 50 | .description { 51 | margin-bottom: 20px; 52 | } 53 | .cta { 54 | .add-to-cart { 55 | display: flex; 56 | align-items: center; 57 | margin-bottom: 40px; 58 | > span { 59 | margin-right: 4px; 60 | } 61 | > button { 62 | height: 46px; 63 | margin-left: 12px; 64 | border-radius: 50px; 65 | } 66 | } 67 | .category { 68 | text-transform: uppercase; 69 | margin-bottom: 16px; 70 | > span { 71 | margin-right: 6px; 72 | } 73 | } 74 | .social-links { 75 | text-transform: uppercase; 76 | > span { 77 | margin-right: 6px; 78 | } 79 | > i { 80 | font-size: 1rem; 81 | margin: 0 8px; 82 | cursor: pointer; 83 | transition: color 0.2s; 84 | &:hover { 85 | color: #1890ff; 86 | } 87 | } 88 | } 89 | } 90 | } 91 | } 92 | 93 | // @media screen and (min-width: 1200px) { 94 | // .product-container { 95 | // padding-bottom: 60px; 96 | // &__image { 97 | // margin-bottom: 24px; 98 | // text-align: center; 99 | // } 100 | // &__details { 101 | // } 102 | // } 103 | // } 104 | -------------------------------------------------------------------------------- /client/src/app/product/product.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProductComponent } from './product.component'; 4 | 5 | describe('ProductComponent', () => { 6 | let component: ProductComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ProductComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ProductComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/src/app/product/product.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { ProductService } from '../services/product.service'; 4 | import { map } from 'rxjs/operators'; 5 | 6 | // import Swiper core and required components 7 | import SwiperCore, { 8 | Navigation, 9 | Pagination, 10 | Scrollbar, 11 | A11y, 12 | Virtual, 13 | Zoom, 14 | Autoplay, 15 | Thumbs, 16 | Controller, 17 | } from 'swiper/core'; 18 | import { CartService } from '../services/cart.service'; 19 | 20 | // install Swiper components 21 | SwiperCore.use([ 22 | Navigation, 23 | Pagination, 24 | Scrollbar, 25 | A11y, 26 | Virtual, 27 | Zoom, 28 | Autoplay, 29 | Thumbs, 30 | Controller, 31 | ]); 32 | 33 | @Component({ 34 | selector: 'app-product', 35 | templateUrl: './product.component.html', 36 | styleUrls: ['./product.component.scss'], 37 | }) 38 | export class ProductComponent implements OnInit { 39 | id: number; 40 | product: any; 41 | quantity: number; 42 | showcaseImages: any[] = []; 43 | loading = false; 44 | 45 | constructor( 46 | private _route: ActivatedRoute, 47 | private _product: ProductService, 48 | private _cart: CartService 49 | ) {} 50 | 51 | ngOnInit(): void { 52 | this.loading = true; 53 | this._route.paramMap 54 | .pipe( 55 | map((param: any) => { 56 | return param.params.id; 57 | }) 58 | ) 59 | .subscribe((productId) => { 60 | // returns string so convert it to number 61 | this.id = parseInt(productId); 62 | this._product.getSingleProduct(productId).subscribe((product) => { 63 | console.log(product); 64 | this.product = product; 65 | if (product.quantity === 0) this.quantity = 0; 66 | else this.quantity = 1; 67 | 68 | if (product.images) { 69 | this.showcaseImages = product.images.split(';'); 70 | } 71 | this.loading = false; 72 | }); 73 | }); 74 | } 75 | 76 | addToCart(): void { 77 | this._cart.addProduct({ 78 | id: this.id, 79 | price: this.product.price, 80 | quantity: this.quantity, 81 | image: this.product.image, 82 | title: this.product.title, 83 | maxQuantity: this.product.quantity, 84 | }); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /client/src/app/profile/profile.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Profile

4 | 11 |
12 |
13 | 14 | 21 |
22 |
23 | 31 |
32 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /client/src/app/profile/profile.component.scss: -------------------------------------------------------------------------------- 1 | .profile-container { 2 | padding: 24px 16px; 3 | .form-container { 4 | max-width: 475px; 5 | padding: 24px 16px; 6 | margin: 0 auto; 7 | > h2 { 8 | text-align: center; 9 | margin-bottom: 24px; 10 | } 11 | > form { 12 | .input-container { 13 | margin-bottom: 12px; 14 | > label { 15 | font-size: 0.875rem; 16 | display: inline-block; 17 | margin-bottom: 6px; 18 | } 19 | > input { 20 | padding: 10px 12px; 21 | width: 100%; 22 | } 23 | } 24 | .cta-container { 25 | margin-top: 24px; 26 | > button { 27 | height: 52px; 28 | width: 100%; 29 | } 30 | } 31 | } 32 | } 33 | } 34 | 35 | @media screen and (min-width: 768px) { 36 | .profile-container { 37 | padding: 48px 16px 240px; 38 | .form-container { 39 | padding: 36px; 40 | border: 1px solid #ccc; 41 | border-radius: 12px; 42 | // > h2 { 43 | // text-align: center; 44 | // } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /client/src/app/profile/profile.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProfileComponent } from './profile.component'; 4 | 5 | describe('ProfileComponent', () => { 6 | let component: ProfileComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ProfileComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ProfileComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/src/app/profile/profile.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { ApiService } from '../services/api.service'; 4 | import { TokenStorageService } from '../services/token-storage.service'; 5 | 6 | @Component({ 7 | selector: 'app-profile', 8 | templateUrl: './profile.component.html', 9 | styleUrls: ['./profile.component.scss'], 10 | }) 11 | export class ProfileComponent implements OnInit { 12 | user = [ 13 | { 14 | key: 'fullName', 15 | label: 'Full name', 16 | value: '', 17 | type: 'text', 18 | }, 19 | { 20 | key: 'email', 21 | label: 'Email address', 22 | value: '', 23 | type: 'email', 24 | }, 25 | { 26 | key: 'password', 27 | label: 'Password', 28 | value: '', 29 | type: 'password', 30 | }, 31 | { 32 | key: 'confirmPassword', 33 | label: 'Confirm password', 34 | value: '', 35 | type: 'password', 36 | }, 37 | ]; 38 | userId = null; 39 | alertMessage = ''; 40 | alertType = ''; 41 | alertVisible = false; 42 | loading = false; 43 | 44 | constructor( 45 | private _api: ApiService, 46 | private _token: TokenStorageService, 47 | private _router: Router 48 | ) {} 49 | 50 | // Update user fields with current details 51 | ngOnInit(): void { 52 | const { user_id, fname, email } = this._token.getUser(); 53 | this.userId = user_id; 54 | this.user[0].value = fname; 55 | this.user[1].value = email; 56 | console.log(this.user); 57 | } 58 | 59 | canUpdate(): boolean { 60 | return this.user.filter((field) => field.value.length > 0).length !== 4 61 | ? true 62 | : false; 63 | } 64 | 65 | // Submit data to be updated 66 | onSubmit(): void { 67 | this.alertVisible = false; 68 | if (this.user[2].value !== this.user[3].value) { 69 | this.alertType = 'error'; 70 | this.alertMessage = 'Passwords do not match'; 71 | this.alertVisible = true; 72 | } else { 73 | this.loading = true; 74 | this._api 75 | .putTypeRequest(`users/${this.userId}`, { 76 | fullName: this.user[0].value, 77 | email: this.user[1].value, 78 | password: this.user[2].value, 79 | }) 80 | .subscribe( 81 | (res: any) => { 82 | console.log(res); 83 | this.alertMessage = res.message; 84 | this.alertType = 'success'; 85 | this.alertVisible = true; 86 | this.loading = false; 87 | const oldDetails = this._token.getUser(); 88 | this._token.setUser({ 89 | ...oldDetails, 90 | fname: this.user[0].value, 91 | email: this.user[1].value, 92 | }); 93 | this.user[2].value = ''; 94 | this.user[3].value = ''; 95 | // window.location.reload(); 96 | }, 97 | (err: any) => { 98 | console.log(err); 99 | this.alertMessage = err.error.message; 100 | this.alertVisible = true; 101 | this.alertType = 'error'; 102 | this.loading = false; 103 | } 104 | ); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /client/src/app/services/api.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ApiService } from './api.service'; 4 | 5 | describe('ApiService', () => { 6 | let service: ApiService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(ApiService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /client/src/app/services/api.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { environment } from '../../environments/environment'; 3 | import { HttpClient } from '@angular/common/http'; 4 | import { map } from 'rxjs/operators'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class ApiService { 10 | private baseUrl = environment.apiUrl; 11 | 12 | constructor(private _http: HttpClient) {} 13 | 14 | getTypeRequest(url: string) { 15 | return this._http.get(`${this.baseUrl}${url}`).pipe( 16 | map((res) => { 17 | return res; 18 | }) 19 | ); 20 | } 21 | postTypeRequest(url: string, payload: any) { 22 | return this._http.post(`${this.baseUrl}${url}`, payload).pipe( 23 | map((res) => { 24 | return res; 25 | }) 26 | ); 27 | } 28 | putTypeRequest(url: string, payload: any) { 29 | return this._http.put(`${this.baseUrl}${url}`, payload).pipe( 30 | map((res) => { 31 | return res; 32 | }) 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /client/src/app/services/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AuthService } from './auth.service'; 4 | 5 | describe('AuthService', () => { 6 | let service: AuthService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(AuthService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /client/src/app/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { map } from 'rxjs/operators'; 3 | import { BehaviorSubject, Observable } from 'rxjs'; 4 | import { ApiService } from './api.service'; 5 | import { TokenStorageService } from './token-storage.service'; 6 | 7 | @Injectable({ 8 | providedIn: 'root', 9 | }) 10 | export class AuthService { 11 | private userSubject: BehaviorSubject; 12 | public user: Observable; 13 | 14 | constructor(private _api: ApiService, private _token: TokenStorageService) { 15 | this.userSubject = new BehaviorSubject(this._token.getUser()); 16 | this.user = this.userSubject.asObservable(); 17 | } 18 | 19 | getUser() { 20 | console.log(this.userSubject); 21 | console.log(this.userSubject.value); 22 | return this.userSubject.value; 23 | } 24 | 25 | login(credentials: any): Observable { 26 | return this._api 27 | .postTypeRequest('auth/login', { 28 | email: credentials.email, 29 | password: credentials.password, 30 | }) 31 | .pipe( 32 | map((res: any) => { 33 | let user = { 34 | email: credentials.email, 35 | token: res.token, 36 | }; 37 | this._token.setToken(res.token); 38 | this._token.setUser(res.data[0]); 39 | console.log(res); 40 | this.userSubject.next(user); 41 | return user; 42 | }) 43 | ); 44 | } 45 | 46 | register(user: any): Observable { 47 | return this._api.postTypeRequest('auth/register', { 48 | fullName: user.fullName, 49 | email: user.email, 50 | password: user.password, 51 | }); 52 | } 53 | 54 | logout() { 55 | this._token.clearStorage(); 56 | this.userSubject.next(null); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /client/src/app/services/cart.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { CartService } from './cart.service'; 4 | 5 | describe('CartService', () => { 6 | let service: CartService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(CartService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /client/src/app/services/cart.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | import { NzNotificationService } from 'ng-zorro-antd/notification'; 4 | import { ApiService } from './api.service'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class CartService { 10 | cartData = { 11 | products: [], 12 | total: 0, 13 | }; 14 | 15 | cartDataObs$ = new BehaviorSubject(this.cartData); 16 | 17 | constructor( 18 | private _notification: NzNotificationService, 19 | private _api: ApiService 20 | ) { 21 | let localCartData = JSON.parse(localStorage.getItem('cart')); 22 | if (localCartData) this.cartData = localCartData; 23 | 24 | this.cartDataObs$.next(this.cartData); 25 | } 26 | 27 | submitCheckout(userId, cart) { 28 | return this._api.postTypeRequest('orders/create', { 29 | userId: userId, 30 | cart: cart, 31 | }); 32 | } 33 | 34 | addProduct(params): void { 35 | const { id, price, quantity, image, title, maxQuantity } = params; 36 | const product = { id, price, quantity, image, title, maxQuantity }; 37 | 38 | if (!this.isProductInCart(id)) { 39 | if (quantity) this.cartData.products.push(product); 40 | else this.cartData.products.push({ ...product, quantity: 1 }); 41 | } else { 42 | // copy array, find item index and update 43 | let updatedProducts = [...this.cartData.products]; 44 | let productIndex = updatedProducts.findIndex((prod) => prod.id == id); 45 | let product = updatedProducts[productIndex]; 46 | 47 | // if no quantity, increment 48 | if (quantity) { 49 | updatedProducts[productIndex] = { 50 | ...product, 51 | quantity: quantity, 52 | }; 53 | } else { 54 | updatedProducts[productIndex] = { 55 | ...product, 56 | quantity: product.quantity + 1, 57 | }; 58 | } 59 | 60 | console.log(updatedProducts); 61 | this.cartData.products = updatedProducts; 62 | } 63 | 64 | this.cartData.total = this.getCartTotal(); 65 | this._notification.create( 66 | 'success', 67 | 'Product added to cart', 68 | `${title} was successfully added to the cart` 69 | ); 70 | this.cartDataObs$.next({ ...this.cartData }); 71 | localStorage.setItem('cart', JSON.stringify(this.cartData)); 72 | } 73 | 74 | updateCart(id: number, quantity: number): void { 75 | // copy array, find item index and update 76 | let updatedProducts = [...this.cartData.products]; 77 | let productIndex = updatedProducts.findIndex((prod) => prod.id == id); 78 | 79 | updatedProducts[productIndex] = { 80 | ...updatedProducts[productIndex], 81 | quantity: quantity, 82 | }; 83 | 84 | this.cartData.products = updatedProducts; 85 | this.cartData.total = this.getCartTotal(); 86 | this.cartDataObs$.next({ ...this.cartData }); 87 | console.log(this.cartData.products); 88 | localStorage.setItem('cart', JSON.stringify(this.cartData)); 89 | } 90 | 91 | removeProduct(id: number): void { 92 | let updatedProducts = this.cartData.products.filter( 93 | (prod) => prod.id !== id 94 | ); 95 | this.cartData.products = updatedProducts; 96 | this.cartData.total = this.getCartTotal(); 97 | this.cartDataObs$.next({ ...this.cartData }); 98 | localStorage.setItem('cart', JSON.stringify(this.cartData)); 99 | 100 | this._notification.create( 101 | 'success', 102 | 'Removed successfully', 103 | 'The selected item was removed from the cart successfully' 104 | ); 105 | } 106 | 107 | clearCart(): void { 108 | this.cartData = { 109 | products: [], 110 | total: 0, 111 | }; 112 | this.cartDataObs$.next({ ...this.cartData }); 113 | localStorage.setItem('cart', JSON.stringify(this.cartData)); 114 | } 115 | 116 | getCartTotal(): number { 117 | let totalSum = 0; 118 | this.cartData.products.forEach( 119 | (prod) => (totalSum += prod.price * prod.quantity) 120 | ); 121 | 122 | return totalSum; 123 | } 124 | 125 | isProductInCart(id: number): boolean { 126 | return this.cartData.products.findIndex((prod) => prod.id === id) !== -1; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /client/src/app/services/interceptor.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { InterceptorService } from './interceptor.service'; 4 | 5 | describe('InterceptorService', () => { 6 | let service: InterceptorService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(InterceptorService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /client/src/app/services/interceptor.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | HttpInterceptor, 4 | HttpEvent, 5 | HttpResponse, 6 | HttpRequest, 7 | HttpHandler, 8 | HTTP_INTERCEPTORS, 9 | } from '@angular/common/http'; 10 | import { TokenStorageService } from '../services/token-storage.service'; 11 | import { Observable } from 'rxjs'; 12 | 13 | const TOKEN_HEADER_KEY = 'x-access-token'; 14 | 15 | @Injectable() 16 | export class MyInterceptor implements HttpInterceptor { 17 | constructor(private _token: TokenStorageService) {} 18 | 19 | intercept( 20 | req: HttpRequest, 21 | next: HttpHandler 22 | ): Observable> { 23 | let authReq = req; 24 | let token = this._token.getToken(); 25 | if (token != null) { 26 | authReq = req.clone({ 27 | headers: req.headers.set(TOKEN_HEADER_KEY, token), 28 | }); 29 | } 30 | return next.handle(authReq); 31 | } 32 | } 33 | 34 | export const authInterceptorProviders = [ 35 | { provide: HTTP_INTERCEPTORS, useClass: MyInterceptor, multi: true }, 36 | ]; 37 | -------------------------------------------------------------------------------- /client/src/app/services/product.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ProductService } from './product.service'; 4 | 5 | describe('ProductService', () => { 6 | let service: ProductService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(ProductService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /client/src/app/services/product.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { Products, Product } from '../shared/models/product.model'; 5 | import { environment } from '../../environments/environment'; 6 | import { ApiService } from './api.service'; 7 | 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | export class ProductService { 12 | private url = environment.apiUrl; 13 | 14 | constructor(private http: HttpClient, private _api: ApiService) {} 15 | 16 | getAllProducts(limitOfResults = 9, page): Observable { 17 | return this.http.get(this.url + 'products', { 18 | params: { 19 | limit: limitOfResults.toString(), 20 | page: page, 21 | }, 22 | }); 23 | } 24 | 25 | getSingleProduct(id: Number): Observable { 26 | console.log(id); 27 | return this._api.getTypeRequest('products/' + id); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /client/src/app/services/token-storage.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { TokenStorageService } from './token-storage.service'; 4 | 5 | describe('TokenStorageService', () => { 6 | let service: TokenStorageService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(TokenStorageService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /client/src/app/services/token-storage.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root', 5 | }) 6 | export class TokenStorageService { 7 | TOKEN_KEY = 'auth-token'; 8 | USER_KEY = 'auth-user'; 9 | 10 | constructor() {} 11 | 12 | public getToken(): string { 13 | return sessionStorage.getItem(this.TOKEN_KEY); 14 | } 15 | 16 | setToken(token: string): void { 17 | sessionStorage.removeItem(this.TOKEN_KEY); 18 | sessionStorage.setItem(this.TOKEN_KEY, token); 19 | } 20 | 21 | getUser(): any { 22 | return JSON.parse(sessionStorage.getItem(this.USER_KEY)); 23 | } 24 | 25 | setUser(user): void { 26 | sessionStorage.removeItem(this.USER_KEY); 27 | sessionStorage.setItem(this.USER_KEY, JSON.stringify(user)); 28 | } 29 | 30 | clearStorage(): void { 31 | sessionStorage.clear(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /client/src/app/shared/models/product.model.ts: -------------------------------------------------------------------------------- 1 | export interface Products { 2 | count: number; 3 | products: Product[]; 4 | } 5 | 6 | export interface Product { 7 | id: Number; 8 | name: String; 9 | category: String; 10 | description: String; 11 | image: String; 12 | price: Number; 13 | quantity: Number; 14 | images: String; 15 | } 16 | -------------------------------------------------------------------------------- /client/src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | 5 | 6 | @NgModule({ 7 | declarations: [], 8 | imports: [ 9 | CommonModule 10 | ] 11 | }) 12 | export class SharedModule { } 13 | -------------------------------------------------------------------------------- /client/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelparkadze/angular-ecommerce-app/2ce1bfd45da4e4d8543e4af332828c20341e9f16/client/src/assets/.gitkeep -------------------------------------------------------------------------------- /client/src/assets/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelparkadze/angular-ecommerce-app/2ce1bfd45da4e4d8543e4af332828c20341e9f16/client/src/assets/1.jpg -------------------------------------------------------------------------------- /client/src/assets/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelparkadze/angular-ecommerce-app/2ce1bfd45da4e4d8543e4af332828c20341e9f16/client/src/assets/2.jpg -------------------------------------------------------------------------------- /client/src/assets/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelparkadze/angular-ecommerce-app/2ce1bfd45da4e4d8543e4af332828c20341e9f16/client/src/assets/3.jpg -------------------------------------------------------------------------------- /client/src/assets/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelparkadze/angular-ecommerce-app/2ce1bfd45da4e4d8543e4af332828c20341e9f16/client/src/assets/4.jpg -------------------------------------------------------------------------------- /client/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /client/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | apiUrl: 'http://localhost:5000/api/v1/', 8 | }; 9 | 10 | /* 11 | * For easier debugging in development mode, you can import the following file 12 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 13 | * 14 | * This import should be commented out in production mode because it will have a negative impact 15 | * on performance if an error is thrown. 16 | */ 17 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 18 | -------------------------------------------------------------------------------- /client/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelparkadze/angular-ecommerce-app/2ce1bfd45da4e4d8543e4af332828c20341e9f16/client/src/favicon.ico -------------------------------------------------------------------------------- /client/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Client 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /client/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /client/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * IE11 requires the following for NgClass support on SVG elements 23 | */ 24 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 25 | 26 | /** 27 | * Web Animations `@angular/platform-browser/animations` 28 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 29 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 30 | */ 31 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 32 | 33 | /** 34 | * By default, zone.js will patch all possible macroTask and DomEvents 35 | * user can disable parts of macroTask/DomEvents patch by setting following flags 36 | * because those flags need to be set before `zone.js` being loaded, and webpack 37 | * will put import in the top of bundle, so user need to create a separate file 38 | * in this directory (for example: zone-flags.ts), and put the following flags 39 | * into that file, and then add the following code before importing zone.js. 40 | * import './zone-flags'; 41 | * 42 | * The flags allowed in zone-flags.ts are listed here. 43 | * 44 | * The following flags will work for all browsers. 45 | * 46 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 47 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 48 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 49 | * 50 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 51 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 52 | * 53 | * (window as any).__Zone_enable_cross_context_check = true; 54 | * 55 | */ 56 | 57 | /*************************************************************************************************** 58 | * Zone JS is required by default for Angular itself. 59 | */ 60 | import 'zone.js/dist/zone'; // Included with Angular CLI. 61 | 62 | 63 | /*************************************************************************************************** 64 | * APPLICATION IMPORTS 65 | */ 66 | -------------------------------------------------------------------------------- /client/src/styles.scss: -------------------------------------------------------------------------------- 1 | @import "~ng-zorro-antd/ng-zorro-antd.min.css"; 2 | @import "~swiper/swiper-bundle"; 3 | 4 | * { 5 | box-sizing: border-box; 6 | } 7 | 8 | html, 9 | body { 10 | margin: 0; 11 | padding: 0; 12 | } 13 | 14 | h1, 15 | h2, 16 | h3, 17 | h4, 18 | h5, 19 | h6 { 20 | margin: 0; 21 | } 22 | 23 | ul { 24 | margin: 0; 25 | padding: 0; 26 | list-style: none; 27 | } 28 | 29 | .ant-input-number-input { 30 | height: 44px; 31 | } 32 | 33 | .swiper-pagination-bullet { 34 | width: 6px; 35 | height: 6px; 36 | } 37 | 38 | .swiper-pagination-bullet-active { 39 | background-color: #555; 40 | } 41 | 42 | .swiper-pagination-fraction, 43 | .swiper-pagination-custom, 44 | .swiper-container-horizontal > .swiper-pagination-bullets { 45 | bottom: 4px; 46 | } 47 | -------------------------------------------------------------------------------- /client/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | keys(): string[]; 13 | (id: string): T; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting() 21 | ); 22 | // Then we find all the tests. 23 | const context = require.context('./', true, /\.spec\.ts$/); 24 | // And load the modules. 25 | context.keys().map(context); 26 | -------------------------------------------------------------------------------- /client/src/theme.less: -------------------------------------------------------------------------------- 1 | // Custom Theming for NG-ZORRO 2 | // For more information: https://ng.ant.design/docs/customize-theme/en 3 | @import "../node_modules/ng-zorro-antd/ng-zorro-antd.less"; 4 | @import "~ng-zorro-antd/ng-zorro-antd.less"; 5 | 6 | // Override less variables to here 7 | // View all variables: https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/components/style/themes/default.less 8 | 9 | // @primary-color: #1890ff; 10 | -------------------------------------------------------------------------------- /client/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "downlevelIteration": true, 10 | "experimentalDecorators": true, 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "module": "es2020", 15 | "lib": [ 16 | "es2018", 17 | "dom" 18 | ] 19 | }, 20 | "angularCompilerOptions": { 21 | "enableI18nLegacyMessageIdFormat": false 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /client/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /client/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [ 4 | "codelyzer" 5 | ], 6 | "rules": { 7 | "align": { 8 | "options": [ 9 | "parameters", 10 | "statements" 11 | ] 12 | }, 13 | "array-type": false, 14 | "arrow-return-shorthand": true, 15 | "curly": true, 16 | "deprecation": { 17 | "severity": "warning" 18 | }, 19 | "eofline": true, 20 | "import-blacklist": [ 21 | true, 22 | "rxjs/Rx" 23 | ], 24 | "import-spacing": true, 25 | "indent": { 26 | "options": [ 27 | "spaces" 28 | ] 29 | }, 30 | "max-classes-per-file": false, 31 | "max-line-length": [ 32 | true, 33 | 140 34 | ], 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-console": [ 47 | true, 48 | "debug", 49 | "info", 50 | "time", 51 | "timeEnd", 52 | "trace" 53 | ], 54 | "no-empty": false, 55 | "no-inferrable-types": [ 56 | true, 57 | "ignore-params" 58 | ], 59 | "no-non-null-assertion": true, 60 | "no-redundant-jsdoc": true, 61 | "no-switch-case-fall-through": true, 62 | "no-var-requires": false, 63 | "object-literal-key-quotes": [ 64 | true, 65 | "as-needed" 66 | ], 67 | "quotemark": [ 68 | true, 69 | "single" 70 | ], 71 | "semicolon": { 72 | "options": [ 73 | "always" 74 | ] 75 | }, 76 | "space-before-function-paren": { 77 | "options": { 78 | "anonymous": "never", 79 | "asyncArrow": "always", 80 | "constructor": "never", 81 | "method": "never", 82 | "named": "never" 83 | } 84 | }, 85 | "typedef": [ 86 | true, 87 | "call-signature" 88 | ], 89 | "typedef-whitespace": { 90 | "options": [ 91 | { 92 | "call-signature": "nospace", 93 | "index-signature": "nospace", 94 | "parameter": "nospace", 95 | "property-declaration": "nospace", 96 | "variable-declaration": "nospace" 97 | }, 98 | { 99 | "call-signature": "onespace", 100 | "index-signature": "onespace", 101 | "parameter": "onespace", 102 | "property-declaration": "onespace", 103 | "variable-declaration": "onespace" 104 | } 105 | ] 106 | }, 107 | "variable-name": { 108 | "options": [ 109 | "ban-keywords", 110 | "check-format", 111 | "allow-pascal-case" 112 | ] 113 | }, 114 | "whitespace": { 115 | "options": [ 116 | "check-branch", 117 | "check-decl", 118 | "check-operator", 119 | "check-separator", 120 | "check-type", 121 | "check-typecast" 122 | ] 123 | }, 124 | "component-class-suffix": true, 125 | "contextual-lifecycle": true, 126 | "directive-class-suffix": true, 127 | "no-conflicting-lifecycle": true, 128 | "no-host-metadata-property": true, 129 | "no-input-rename": true, 130 | "no-inputs-metadata-property": true, 131 | "no-output-native": true, 132 | "no-output-on-prefix": true, 133 | "no-output-rename": true, 134 | "no-outputs-metadata-property": true, 135 | "template-banana-in-box": true, 136 | "template-no-negated-async": true, 137 | "use-lifecycle-interface": true, 138 | "use-pipe-transform-interface": true, 139 | "directive-selector": [ 140 | true, 141 | "attribute", 142 | "app", 143 | "camelCase" 144 | ], 145 | "component-selector": [ 146 | true, 147 | "element", 148 | "app", 149 | "kebab-case" 150 | ] 151 | } 152 | } 153 | --------------------------------------------------------------------------------