├── db ├── data │ └── .gitkeep ├── .gitignore ├── Dockerfile └── init-db.d │ └── seed.js ├── backend ├── .dockerignore ├── .gitignore ├── Dockerfile ├── .env ├── routes │ ├── catalogRoutes.js │ ├── orderRoutes.js │ ├── userRoutes.js │ ├── authRoutes.js │ └── cartRoutes.js ├── models │ ├── Cart.js │ ├── User.js │ └── Product.js ├── middleware │ └── checkToken.js ├── package.json ├── index.js ├── seeds │ └── products.js └── package-lock.json ├── frontend ├── .dockerignore ├── .gitignore ├── .env ├── src │ ├── envConfig.ts │ ├── components │ │ ├── Footer │ │ │ ├── index.ts │ │ │ └── Footer.tsx │ │ ├── Product │ │ │ ├── index.ts │ │ │ └── Product.tsx │ │ ├── Homepage │ │ │ ├── index.ts │ │ │ └── Homepage.tsx │ │ ├── NotFound │ │ │ ├── index.ts │ │ │ └── NotFound.tsx │ │ ├── LoginModal │ │ │ ├── index.ts │ │ │ └── LoginModal.tsx │ │ ├── AccountModal │ │ │ ├── index.ts │ │ │ └── AccountModal.tsx │ │ ├── ShoppingCart │ │ │ ├── index.ts │ │ │ └── ShoppingCart.tsx │ │ ├── RegisterModal │ │ │ ├── index.ts │ │ │ └── RegisterModal.tsx │ │ ├── OrderSuccessModal │ │ │ ├── index.ts │ │ │ └── OrderSuccessModal.tsx │ │ ├── Cart │ │ │ ├── index.ts │ │ │ └── Cart.tsx │ │ ├── Account │ │ │ ├── index.ts │ │ │ └── Account.tsx │ │ ├── FiltersList │ │ │ ├── index.ts │ │ │ └── FiltersList.tsx │ │ ├── ProductDetails │ │ │ ├── index.ts │ │ │ └── ProductDetails.tsx │ │ ├── CheckoutModal │ │ │ ├── index.ts │ │ │ └── CheckoutModal.tsx │ │ ├── Header │ │ │ ├── index.ts │ │ │ └── Header.tsx │ │ └── Products │ │ │ ├── index.ts │ │ │ └── Products.tsx │ ├── api │ │ ├── catalog.ts │ │ ├── order.ts │ │ ├── user.ts │ │ ├── auth.ts │ │ ├── cart.ts │ │ └── index.ts │ ├── typings │ │ ├── state │ │ │ ├── order.ts │ │ │ ├── sortBy.ts │ │ │ ├── cartProduct.ts │ │ │ ├── loggedUser.ts │ │ │ ├── catalog.ts │ │ │ ├── cart.ts │ │ │ ├── user.ts │ │ │ ├── index.ts │ │ │ ├── filters.ts │ │ │ ├── state.ts │ │ │ └── catalogProduct.ts │ │ ├── action.ts │ │ ├── modal.ts │ │ └── filters.ts │ ├── selectors │ │ ├── user.ts │ │ ├── cart.ts │ │ └── catalog.ts │ ├── styles │ │ ├── FiltersList.css │ │ ├── Footer.css │ │ ├── ShoppingCart.css │ │ ├── NotFound.css │ │ ├── Header.css │ │ ├── Homepage.css │ │ ├── AccountModal.css │ │ ├── OrderSuccessModal.css │ │ ├── LoginModal.css │ │ ├── RegisterModal.css │ │ ├── CheckoutModal.css │ │ ├── Account.css │ │ ├── Product.css │ │ ├── Products.css │ │ ├── Cart.css │ │ └── ProductDetails.css │ ├── reducers │ │ ├── sortReducer.ts │ │ ├── index.ts │ │ ├── catalogReducer.ts │ │ ├── cartReducer.ts │ │ ├── userReducer.ts │ │ └── filtersReducer.ts │ ├── index.tsx │ ├── constants │ │ └── index.ts │ ├── store │ │ └── configureStore.ts │ ├── sagas │ │ └── index.ts │ └── actions │ │ └── index.ts ├── public │ ├── img │ │ ├── logo.png │ │ ├── htc_u11.jpg │ │ ├── lg_g6.jpg │ │ ├── lg_v30.jpg │ │ ├── loader.gif │ │ ├── success.gif │ │ ├── huawei_p10.jpg │ │ ├── apple_iphone_x.jpg │ │ ├── samsung_galaxy_a3.JPG │ │ ├── samsung_galaxy_s8.jpg │ │ ├── apple_iphone_8_plus.jpg │ │ ├── huawei_mate_10_pro.jpg │ │ └── samsung_galaxy_note_8.jpg │ └── index.html ├── postcss.config.js ├── .babelrc ├── Dockerfile ├── tsconfig.json ├── package.json └── webpack.config.js ├── screenshots └── mobile_shop.jpg ├── docker-compose.yml └── README.md /db/data/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /db/.gitignore: -------------------------------------------------------------------------------- 1 | data/* 2 | !data/.gitkeep -------------------------------------------------------------------------------- /frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /frontend/.env: -------------------------------------------------------------------------------- 1 | # Development env variables 2 | 3 | API_URL=http://localhost:5000/api -------------------------------------------------------------------------------- /frontend/src/envConfig.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | apiUrl: process.env.API_URL, 3 | }; 4 | -------------------------------------------------------------------------------- /db/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mongo 2 | 3 | COPY ./init-db.d/seed.js /docker-entrypoint-initdb.d 4 | -------------------------------------------------------------------------------- /frontend/src/components/Footer/index.ts: -------------------------------------------------------------------------------- 1 | import Footer from './Footer'; 2 | 3 | export default Footer; 4 | -------------------------------------------------------------------------------- /frontend/src/components/Product/index.ts: -------------------------------------------------------------------------------- 1 | import Product from './Product'; 2 | 3 | export default Product; 4 | -------------------------------------------------------------------------------- /frontend/src/components/Homepage/index.ts: -------------------------------------------------------------------------------- 1 | import Homepage from './Homepage'; 2 | 3 | export default Homepage; 4 | -------------------------------------------------------------------------------- /frontend/src/components/NotFound/index.ts: -------------------------------------------------------------------------------- 1 | import NotFound from './NotFound'; 2 | 3 | export default NotFound; 4 | -------------------------------------------------------------------------------- /frontend/src/components/LoginModal/index.ts: -------------------------------------------------------------------------------- 1 | import LoginModal from './LoginModal'; 2 | 3 | export default LoginModal; 4 | -------------------------------------------------------------------------------- /frontend/public/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivan3123708/fullstack-shopping-cart/HEAD/frontend/public/img/logo.png -------------------------------------------------------------------------------- /screenshots/mobile_shop.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivan3123708/fullstack-shopping-cart/HEAD/screenshots/mobile_shop.jpg -------------------------------------------------------------------------------- /frontend/public/img/htc_u11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivan3123708/fullstack-shopping-cart/HEAD/frontend/public/img/htc_u11.jpg -------------------------------------------------------------------------------- /frontend/public/img/lg_g6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivan3123708/fullstack-shopping-cart/HEAD/frontend/public/img/lg_g6.jpg -------------------------------------------------------------------------------- /frontend/public/img/lg_v30.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivan3123708/fullstack-shopping-cart/HEAD/frontend/public/img/lg_v30.jpg -------------------------------------------------------------------------------- /frontend/public/img/loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivan3123708/fullstack-shopping-cart/HEAD/frontend/public/img/loader.gif -------------------------------------------------------------------------------- /frontend/public/img/success.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivan3123708/fullstack-shopping-cart/HEAD/frontend/public/img/success.gif -------------------------------------------------------------------------------- /frontend/src/components/AccountModal/index.ts: -------------------------------------------------------------------------------- 1 | import AccountModal from './AccountModal'; 2 | 3 | export default AccountModal; 4 | -------------------------------------------------------------------------------- /frontend/src/components/ShoppingCart/index.ts: -------------------------------------------------------------------------------- 1 | import ShoppingCart from './ShoppingCart'; 2 | 3 | export default ShoppingCart; 4 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | const autoprefixer = require('autoprefixer'); 2 | 3 | module.exports = { 4 | plugins: [autoprefixer] 5 | }; -------------------------------------------------------------------------------- /frontend/src/components/RegisterModal/index.ts: -------------------------------------------------------------------------------- 1 | import RegisterModal from './RegisterModal'; 2 | 3 | export default RegisterModal; 4 | -------------------------------------------------------------------------------- /frontend/public/img/huawei_p10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivan3123708/fullstack-shopping-cart/HEAD/frontend/public/img/huawei_p10.jpg -------------------------------------------------------------------------------- /frontend/public/img/apple_iphone_x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivan3123708/fullstack-shopping-cart/HEAD/frontend/public/img/apple_iphone_x.jpg -------------------------------------------------------------------------------- /frontend/public/img/samsung_galaxy_a3.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivan3123708/fullstack-shopping-cart/HEAD/frontend/public/img/samsung_galaxy_a3.JPG -------------------------------------------------------------------------------- /frontend/public/img/samsung_galaxy_s8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivan3123708/fullstack-shopping-cart/HEAD/frontend/public/img/samsung_galaxy_s8.jpg -------------------------------------------------------------------------------- /frontend/src/components/OrderSuccessModal/index.ts: -------------------------------------------------------------------------------- 1 | import OrderSuccessModal from './OrderSuccessModal'; 2 | 3 | export default OrderSuccessModal; 4 | -------------------------------------------------------------------------------- /frontend/public/img/apple_iphone_8_plus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivan3123708/fullstack-shopping-cart/HEAD/frontend/public/img/apple_iphone_8_plus.jpg -------------------------------------------------------------------------------- /frontend/public/img/huawei_mate_10_pro.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivan3123708/fullstack-shopping-cart/HEAD/frontend/public/img/huawei_mate_10_pro.jpg -------------------------------------------------------------------------------- /frontend/src/api/catalog.ts: -------------------------------------------------------------------------------- 1 | import http from './index'; 2 | 3 | const PREFIX = '/catalog'; 4 | 5 | export const getCatalog = () => http.get(PREFIX); 6 | -------------------------------------------------------------------------------- /frontend/public/img/samsung_galaxy_note_8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivan3123708/fullstack-shopping-cart/HEAD/frontend/public/img/samsung_galaxy_note_8.jpg -------------------------------------------------------------------------------- /frontend/src/typings/state/order.ts: -------------------------------------------------------------------------------- 1 | export interface IOrder { 2 | name: string; 3 | price: string; 4 | quantity: string; 5 | dateCreated: string; 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/typings/state/sortBy.ts: -------------------------------------------------------------------------------- 1 | export type TSortBy = 2 | 'Name: A-Z' | 3 | 'Name: Z-A' | 4 | 'Price: Low to High' | 5 | 'Price: High to Low'; 6 | -------------------------------------------------------------------------------- /frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "transform-class-properties" 4 | ], 5 | "presets": [ 6 | "@babel/env", 7 | "@babel/react" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/selectors/user.ts: -------------------------------------------------------------------------------- 1 | import { IState, IUser } from '@typings/state/index'; 2 | 3 | export const selectUser = (state: IState): IUser | null => state.loggedUser.user; 4 | -------------------------------------------------------------------------------- /frontend/src/api/order.ts: -------------------------------------------------------------------------------- 1 | import http from './index'; 2 | 3 | const PREFIX = '/order'; 4 | 5 | export const createOrder = (params: Record) => http.post(PREFIX, params); 6 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14-alpine 2 | 3 | WORKDIR /usr/local/share/backend 4 | 5 | COPY package*.json ./ 6 | RUN npm install 7 | COPY . . 8 | 9 | EXPOSE 5000 10 | 11 | CMD npm run start 12 | -------------------------------------------------------------------------------- /frontend/src/typings/state/cartProduct.ts: -------------------------------------------------------------------------------- 1 | import { ICatalogProduct } from './catalogProduct'; 2 | 3 | export interface ICartProduct { 4 | product: ICatalogProduct; 5 | _id: string; 6 | quantity?: number; 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/typings/state/loggedUser.ts: -------------------------------------------------------------------------------- 1 | import { IUser } from './user'; 2 | 3 | export interface ILoggedUser { 4 | isLoading: boolean; 5 | isLoaded: boolean; 6 | user: IUser | null; 7 | error: string | null; 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/api/user.ts: -------------------------------------------------------------------------------- 1 | import http from './index'; 2 | 3 | const PREFIX = '/user'; 4 | 5 | export const getUser = () => http.get(PREFIX); 6 | 7 | export const editUser = (data: Record) => http.put(PREFIX, data); 8 | -------------------------------------------------------------------------------- /frontend/src/typings/state/catalog.ts: -------------------------------------------------------------------------------- 1 | import { ICatalogProduct } from './catalogProduct'; 2 | 3 | export interface ICatalog { 4 | isLoading: boolean; 5 | isLoaded: boolean; 6 | items: ICatalogProduct[]; 7 | error: string | null; 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/styles/FiltersList.css: -------------------------------------------------------------------------------- 1 | .subheader { 2 | font-size: 16px !important; 3 | font-weight: bold !important; 4 | } 5 | 6 | .listItem { 7 | border-top: 1px solid #dcdcdc; 8 | } 9 | 10 | .checkbox { 11 | margin-left: 20px; 12 | } -------------------------------------------------------------------------------- /frontend/src/selectors/cart.ts: -------------------------------------------------------------------------------- 1 | import { IState, ICart, ICartProduct } from '@typings/state/index'; 2 | 3 | export const selectCart = (state: IState): ICart => state.cart; 4 | export const selectItems = (state: IState): ICartProduct[] => state.cart.items; 5 | -------------------------------------------------------------------------------- /frontend/src/styles/Footer.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | width: 100%; 3 | height: auto; 4 | /* margin-top: -30px; */ 5 | background: #00BCD4; 6 | color: #c3e3e7; 7 | text-align: center; 8 | } 9 | 10 | .footer p { 11 | margin: 0; 12 | padding: 3px; 13 | } -------------------------------------------------------------------------------- /frontend/src/typings/state/cart.ts: -------------------------------------------------------------------------------- 1 | import { ICartProduct } from './cartProduct'; 2 | 3 | export interface ICart { 4 | isLoading: boolean; 5 | isLoaded: boolean; 6 | _id: string | null; 7 | items: ICartProduct[]; 8 | error: string | null; 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/components/Footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '@styles/Footer.css'; 3 | 4 | const Footer = () => ( 5 |
6 |

MobileShop © 2018

7 |
8 | ); 9 | 10 | export default Footer; 11 | -------------------------------------------------------------------------------- /frontend/src/styles/ShoppingCart.css: -------------------------------------------------------------------------------- 1 | button:focus { 2 | outline: 0; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | } 8 | 9 | .container { 10 | width: 100%; 11 | max-width: 1200px; 12 | margin: auto; 13 | font-family: 'Roboto'; 14 | color: #2e3a4edc; 15 | } -------------------------------------------------------------------------------- /frontend/src/typings/state/user.ts: -------------------------------------------------------------------------------- 1 | import { IOrder } from './order'; 2 | 3 | export interface IUser { 4 | orders: IOrder[]; 5 | username: string; 6 | email: string; 7 | address: string; 8 | phone: string; 9 | id: string; 10 | token: string; 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/api/auth.ts: -------------------------------------------------------------------------------- 1 | import http from './index'; 2 | 3 | const PREFIX = '/auth'; 4 | 5 | export const register = (data: Record) => http.post(`${PREFIX}/register`, data); 6 | 7 | export const login = (data: Record) => http.post(`${PREFIX}/login`, data); 8 | -------------------------------------------------------------------------------- /frontend/src/typings/action.ts: -------------------------------------------------------------------------------- 1 | export type actionTypes = 2 | 'GET_USER' | 'GET_USER_SUCCESS' | 'GET_USER_FAIL' | 'LOGOUT_SUCCESS' | 3 | 'INIT_CATALOG' | 'INIT_CATALOG_SUCCESS' | 'INIT_CATALOG_FAIL' | 4 | 'GET_CART' | 'GET_CART_SUCCESS' | 'GET_CART_FAIL' | 5 | 'SET_FILTER' | 'CLEAR_FILTERS' | 6 | 'SET_SORT_BY'; 7 | -------------------------------------------------------------------------------- /backend/.env: -------------------------------------------------------------------------------- 1 | # Development env variables 2 | 3 | # MONGODB_URI=mongodb://127.0.0.1:27017/mobileShop - uncomment if you don't want to run backend in docker container 4 | MONGODB_URI=mongodb://db:27017/mobileShop 5 | MONGODB_USER=root 6 | MONGODB_PASSWORD=password 7 | PRIVATE_KEY=jwt_private_key 8 | NPM_CONFIG_PRODUCTION=false 9 | -------------------------------------------------------------------------------- /backend/routes/catalogRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const Product = require('../models/Product'); 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/', (req, res) => { 7 | Product.find({}) 8 | .then((foundProduct) => { 9 | res.send(foundProduct); 10 | }); 11 | }); 12 | 13 | module.exports = router; -------------------------------------------------------------------------------- /frontend/src/typings/state/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cart'; 2 | export * from './cartProduct'; 3 | export * from './catalogProduct'; 4 | export * from './catalog'; 5 | export * from './filters'; 6 | export * from './loggedUser'; 7 | export * from './order'; 8 | export * from './state'; 9 | export * from './user'; 10 | export * from './sortBy'; 11 | -------------------------------------------------------------------------------- /frontend/src/components/NotFound/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '@styles/NotFound.css'; 3 | 4 | const NotFound = () => ( 5 |
6 |
7 |

404

8 |

Page Not Found

9 |
10 |
11 | ); 12 | 13 | export default NotFound; 14 | -------------------------------------------------------------------------------- /frontend/src/styles/NotFound.css: -------------------------------------------------------------------------------- 1 | .not-found-container { 2 | min-height: 100vh; 3 | margin-top: -72px; 4 | border: 1px solid #ffffff00; 5 | } 6 | 7 | .not-found { 8 | width: 50%; 9 | min-width: 330px; 10 | margin: auto; 11 | margin-top: 200px; 12 | } 13 | 14 | .not-found h1 { 15 | font-size: 60px; 16 | color: #00BCD4; 17 | } 18 | 19 | .not-found h3 { 20 | font-size: 45px; 21 | } -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14-alpine as build 2 | 3 | WORKDIR /usr/local/share/frontend 4 | 5 | COPY package*.json ./ 6 | RUN apk --update add libtool automake autoconf nasm gcc make g++ zlib-dev 7 | RUN npm install 8 | COPY . . 9 | RUN npm run build 10 | 11 | FROM danjellz/http-server:1.2 12 | 13 | ENV PORT=3000 14 | EXPOSE 3000 15 | 16 | COPY --from=build /usr/local/share/frontend/public . 17 | -------------------------------------------------------------------------------- /frontend/src/typings/state/filters.ts: -------------------------------------------------------------------------------- 1 | export interface IFilters { 2 | filters: { 3 | priceRange: string[]; 4 | brand: string[]; 5 | color: string[]; 6 | os: string[]; 7 | internalMemory: string[]; 8 | ram: string[]; 9 | displaySize: string[]; 10 | displayResolution: string[]; 11 | camera: string[]; 12 | cpu: string[]; 13 | } 14 | checked: string[]; 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/styles/Header.css: -------------------------------------------------------------------------------- 1 | .header > div { 2 | z-index: 0 !important; 3 | } 4 | 5 | .title h1 { 6 | font-family: 'Bungee Inline'; 7 | font-style: italic; 8 | } 9 | 10 | .menu { 11 | margin-top: 5px; 12 | } 13 | 14 | .icon-menu { 15 | display: none; 16 | } 17 | 18 | @media (max-width: 750px) { 19 | .menu { 20 | display: none; 21 | } 22 | 23 | .icon-menu { 24 | display: block; 25 | } 26 | } -------------------------------------------------------------------------------- /frontend/src/api/cart.ts: -------------------------------------------------------------------------------- 1 | import http from './index'; 2 | 3 | const PREFIX = '/cart'; 4 | 5 | export const getCart = () => http.get(PREFIX); 6 | 7 | export const createCart = (params: Record) => http.post(PREFIX, params); 8 | 9 | export const editCart = (params: Record) => http.put(PREFIX, params); 10 | 11 | export const deleteCart = (params: Record) => http.delete(PREFIX, { params }); 12 | -------------------------------------------------------------------------------- /frontend/src/components/Homepage/Homepage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FiltersList from '../FiltersList'; 3 | import Products from '../Products'; 4 | import '@styles/Homepage.css'; 5 | 6 | const Homepage = () => ( 7 |
8 |
9 | 10 |
11 | 12 |
13 | ); 14 | 15 | export default Homepage; 16 | -------------------------------------------------------------------------------- /backend/models/Cart.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const cartSchema = new mongoose.Schema({ 4 | user: { 5 | type: mongoose.Schema.Types.ObjectId, 6 | ref: 'User' 7 | }, 8 | items: [ 9 | { 10 | product: { 11 | type: mongoose.Schema.Types.ObjectId, 12 | ref: 'Product' 13 | }, 14 | quantity: Number 15 | } 16 | ] 17 | }); 18 | 19 | module.exports = mongoose.model('Cart', cartSchema); -------------------------------------------------------------------------------- /frontend/src/api/index.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance } from 'axios'; 2 | import env from '../envConfig'; 3 | 4 | const instance: AxiosInstance = axios.create({ baseURL: env.apiUrl }); 5 | 6 | instance.interceptors.request.use(config => { 7 | const token = localStorage.getItem('token'); 8 | 9 | if (token) { 10 | config.headers.common.Authorization = `Bearer ${token}`; 11 | } 12 | 13 | return config; 14 | }); 15 | 16 | export default instance; 17 | -------------------------------------------------------------------------------- /frontend/src/typings/state/state.ts: -------------------------------------------------------------------------------- 1 | import { RouterState} from 'connected-react-router'; 2 | import { ILoggedUser } from './loggedUser'; 3 | import { ICart } from './cart'; 4 | import { ICatalog } from './catalog'; 5 | import { IFilters } from './filters'; 6 | import { TSortBy } from './sortBy'; 7 | 8 | export interface IState { 9 | router: RouterState; 10 | loggedUser: ILoggedUser; 11 | cart: ICart; 12 | catalog: ICatalog; 13 | filters: IFilters; 14 | sortBy: TSortBy; 15 | } 16 | -------------------------------------------------------------------------------- /backend/routes/orderRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const User = require('../models/User'); 3 | const { checkToken } = require('../middleware/checkToken'); 4 | 5 | const router = express.Router(); 6 | 7 | router.post('/', checkToken, (req, res) => { 8 | User.findById(req.user.id) 9 | .then((foundUser) => { 10 | foundUser.orders = foundUser.orders.concat(req.body.order); 11 | foundUser.save(() => res.end()); 12 | }); 13 | }); 14 | 15 | module.exports = router; 16 | -------------------------------------------------------------------------------- /frontend/src/typings/modal.ts: -------------------------------------------------------------------------------- 1 | import { ICartProduct } from './state/index'; 2 | 3 | export type modal = 4 | null | 5 | 'snackbar' | 6 | 'checkout' | 7 | 'orderSuccess' | 8 | 'dialog' | 9 | 'login' | 10 | 'register'; 11 | 12 | export interface ModalProps { 13 | isOpen: boolean; 14 | setActiveModal: (modal: modal) => void; 15 | onRequestClose: (event?: any) => void; 16 | cart?: ICartProduct[]; 17 | makeOrder?: () => void; 18 | setUser?: (data: Record) => void; 19 | } 20 | -------------------------------------------------------------------------------- /backend/models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const userSchema = new mongoose.Schema({ 4 | username: String, 5 | password: String, 6 | email: String, 7 | address: String, 8 | phone: String, 9 | orders: [], 10 | token: String, 11 | }); 12 | 13 | userSchema.options.toJSON = { 14 | transform: (doc, ret) => { 15 | ret.id = ret._id; 16 | delete ret._id; 17 | delete ret.__v; 18 | return ret; 19 | } 20 | }; 21 | 22 | module.exports = mongoose.model('User', userSchema); -------------------------------------------------------------------------------- /frontend/src/styles/Homepage.css: -------------------------------------------------------------------------------- 1 | .homepage-container { 2 | display: flex; 3 | justify-content: space-between; 4 | min-height: 100vh; 5 | margin: 0 6px; 6 | margin-top: -72px; 7 | border: 1px solid #ffffff00; 8 | } 9 | 10 | .filtersList-desktop { 11 | width: 20%; 12 | height:100%; 13 | min-height: auto; 14 | margin-top: 90px; 15 | margin-bottom: 40px; 16 | background-color: #f5f5f5; 17 | } 18 | 19 | @media (max-width: 1115px) { 20 | .filtersList-desktop { 21 | display: none; 22 | } 23 | } -------------------------------------------------------------------------------- /frontend/src/components/Cart/index.ts: -------------------------------------------------------------------------------- 1 | import { compose } from 'recompose'; 2 | import { connect } from 'react-redux'; 3 | import { getCart } from '@actions/index'; 4 | import { selectCart } from '@selectors/cart'; 5 | import { IState } from '@typings/state/index'; 6 | import Cart, { Props } from './Cart'; 7 | 8 | const mapStateToProps = (state: IState) => ({ 9 | cart: selectCart(state) 10 | }); 11 | 12 | const actions = { getCart }; 13 | 14 | export default compose( 15 | connect(mapStateToProps, actions) 16 | )(Cart); 17 | -------------------------------------------------------------------------------- /frontend/src/components/Account/index.ts: -------------------------------------------------------------------------------- 1 | import { compose } from 'recompose'; 2 | import { connect } from 'react-redux'; 3 | import { selectUser } from '@selectors/user'; 4 | import { getUser } from '@actions/index'; 5 | import { IState } from '@typings/state/index'; 6 | import Account, { Props } from './Account'; 7 | 8 | const mapStateToProps = (state: IState) => ({ 9 | user: selectUser(state) 10 | }); 11 | 12 | const actions = { getUser }; 13 | 14 | export default compose( 15 | connect(mapStateToProps, actions) 16 | )(Account); 17 | -------------------------------------------------------------------------------- /frontend/src/styles/AccountModal.css: -------------------------------------------------------------------------------- 1 | .account-modal { 2 | width: 90%; 3 | max-width: 385px; 4 | height: 375px; 5 | margin: auto; 6 | margin-top: 100px; 7 | background: white; 8 | border: 1px solid white; 9 | border-radius: 5px; 10 | outline: none; 11 | } 12 | 13 | .account-modal .form { 14 | width: 70%; 15 | margin: auto; 16 | margin-top: 10px; 17 | text-align: center; 18 | font-family: 'Roboto'; 19 | } 20 | 21 | .account-modal .form h1 { 22 | color: #00BCD4; 23 | } 24 | 25 | .account-modal .form .btn { 26 | margin: 20px auto; 27 | } -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | MOBILE SHOP 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /frontend/src/reducers/sortReducer.ts: -------------------------------------------------------------------------------- 1 | import { SET_SORT_BY } from '../constants'; 2 | import { actionTypes } from '@typings/action'; 3 | import { TSortBy } from '@typings/state/sortBy'; 4 | 5 | interface IAction { 6 | type: actionTypes; 7 | payload: TSortBy; 8 | } 9 | 10 | const initState: TSortBy = 'Name: A-Z'; 11 | 12 | const sortReducer = (state = initState, action: IAction) => { 13 | switch (action.type) { 14 | case SET_SORT_BY: 15 | return action.payload; 16 | default: 17 | return state; 18 | } 19 | }; 20 | 21 | export default sortReducer; 22 | -------------------------------------------------------------------------------- /frontend/src/components/FiltersList/index.ts: -------------------------------------------------------------------------------- 1 | import { compose } from 'recompose'; 2 | import { connect } from 'react-redux'; 3 | import { selectFilters } from '@selectors/catalog'; 4 | import { setFilter } from '@actions/index'; 5 | import { IState } from '@typings/state/index'; 6 | import FiltersList, { Props } from './FiltersList'; 7 | 8 | const mapStateToProps = (state: IState) => ({ 9 | filters: selectFilters(state) 10 | }); 11 | 12 | const actions = { setFilter }; 13 | 14 | export default compose( 15 | connect(mapStateToProps, actions) 16 | )(FiltersList); 17 | -------------------------------------------------------------------------------- /frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { ConnectedRouter } from 'connected-react-router'; 5 | import configureStore, { history } from './store/configureStore'; 6 | import ShoppingCart from './components/ShoppingCart'; 7 | 8 | const store = configureStore(); 9 | 10 | ReactDOM.render( 11 | 12 | 13 | 14 | 15 | , 16 | document.getElementById('app_root') 17 | ); 18 | -------------------------------------------------------------------------------- /frontend/src/components/ProductDetails/index.ts: -------------------------------------------------------------------------------- 1 | import { compose } from 'recompose'; 2 | import { connect } from 'react-redux'; 3 | import { selectUser } from '@selectors/user'; 4 | import { selectProduct } from '@selectors/catalog'; 5 | import { IState } from '@typings/state/index' 6 | import ProductDetails, { Props } from './ProductDetails'; 7 | 8 | const mapStateToProps = (state: IState, ownProps: any) => ({ 9 | loggedUser: selectUser(state), 10 | product: selectProduct(state, ownProps) 11 | }); 12 | 13 | export default compose( 14 | connect(mapStateToProps) 15 | )(ProductDetails); 16 | -------------------------------------------------------------------------------- /frontend/src/components/CheckoutModal/index.ts: -------------------------------------------------------------------------------- 1 | import { compose } from 'recompose'; 2 | import { connect } from 'react-redux'; 3 | import { selectItems } from '@selectors/cart'; 4 | import { IState, ICartProduct, } from '@typings/state/index'; 5 | import { ModalProps } from '@typings/modal'; 6 | import CheckoutModal from './CheckoutModal'; 7 | 8 | interface EnhancedProps extends ModalProps { 9 | cart?: ICartProduct[]; 10 | } 11 | 12 | const mapStateToProps = (state: IState) => ({ 13 | cart: selectItems(state) 14 | }); 15 | 16 | export default compose( 17 | connect(mapStateToProps) 18 | )(CheckoutModal); 19 | -------------------------------------------------------------------------------- /frontend/src/styles/OrderSuccessModal.css: -------------------------------------------------------------------------------- 1 | .order-success-modal { 2 | width: 90%; 3 | max-width: 385px; 4 | height: 410px; 5 | margin: auto; 6 | margin-top: 100px; 7 | background: white; 8 | border: 1px solid white; 9 | border-radius: 5px; 10 | outline: none; 11 | } 12 | 13 | .order-success-modal .success { 14 | width: 70%; 15 | margin: auto; 16 | margin-top: 10px; 17 | text-align: center; 18 | font-family: 'Roboto'; 19 | } 20 | 21 | .order-success-modal h1 { 22 | color: #00BCD4; 23 | } 24 | 25 | .order-success-modal img { 26 | max-height: 100px; 27 | margin: 30px 0; 28 | } 29 | 30 | .order-success-modal .btn { 31 | margin-top: 20px; 32 | } -------------------------------------------------------------------------------- /frontend/src/components/Header/index.ts: -------------------------------------------------------------------------------- 1 | import { compose } from 'recompose'; 2 | import { connect } from 'react-redux'; 3 | import { getUser, getUserSuccess, logoutSuccess } from '@actions/index'; 4 | import { selectUser } from '@selectors/user'; 5 | import { selectCart } from '@selectors/cart'; 6 | import { IState } from '@typings/state/index'; 7 | import Header, { Props } from './Header'; 8 | 9 | const mapStateToProps = (state: IState) => ({ 10 | loggedUser: selectUser(state), 11 | cart: selectCart(state) 12 | }); 13 | 14 | const actions = { getUser, getUserSuccess, logoutSuccess }; 15 | 16 | export default compose( 17 | connect(mapStateToProps, actions) 18 | )(Header); 19 | -------------------------------------------------------------------------------- /frontend/src/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { connectRouter } from 'connected-react-router'; 3 | import { History } from 'history'; 4 | import userReducer from './userReducer'; 5 | import cartReducer from './cartReducer'; 6 | import catalogReducer from './catalogReducer'; 7 | import filtersReducer from './filtersReducer'; 8 | import sortReducer from './sortReducer'; 9 | 10 | const createRootReducer = (history: History) => combineReducers({ 11 | router: connectRouter(history), 12 | loggedUser: userReducer, 13 | cart: cartReducer, 14 | catalog: catalogReducer, 15 | filters: filtersReducer, 16 | sortBy: sortReducer 17 | }); 18 | 19 | export default createRootReducer; 20 | -------------------------------------------------------------------------------- /frontend/src/typings/filters.ts: -------------------------------------------------------------------------------- 1 | export type filterTypes = 2 | 'priceRange' | 3 | 'brand' | 4 | 'color' | 5 | 'os' | 6 | 'internalMemory' | 7 | 'ram' | 8 | 'displaySize' | 9 | 'displayResolution' | 10 | 'camera' | 11 | 'cpu'; 12 | 13 | export type filterValues = 14 | '<250' | '250-500' | '500-750' | '750>' | 15 | 'samsung' | 'apple' | 'huawei' | 'lg' | 'htc' | 16 | 'black' | 'white' | 'grey' | 17 | 'android' | 'ios' | 18 | '16' | '64' | '128' | '256' | 19 | '1' | '3' | '4' | '6' | 20 | '4.5' | '5.1' | '5.5' | '5.8' | '6.0' | '6.3' | 21 | '540x960' | '1080x1920' | '1125x2436' | '1440x2560' | '1440x2880' | '1440x2960' | 22 | '8' | '12' | '13' | '16' | 23 | 'quad_core' | 'hexa_core' | 'octa_core'; 24 | -------------------------------------------------------------------------------- /frontend/src/styles/LoginModal.css: -------------------------------------------------------------------------------- 1 | .login-modal { 2 | width: 90%; 3 | max-width: 385px; 4 | height: 360px; 5 | margin: auto; 6 | margin-top: 100px; 7 | background: white; 8 | color: #2e3a4e; 9 | border: 1px solid white; 10 | border-radius: 5px; 11 | outline: none; 12 | } 13 | 14 | .login-modal .form { 15 | width: 90%; 16 | max-width: 360px; 17 | margin: auto; 18 | margin-top: 10px; 19 | text-align: center; 20 | font-family: 'Roboto'; 21 | } 22 | 23 | .login-modal .form h1 { 24 | color: #00BCD4; 25 | } 26 | 27 | .login-modal .form .btn { 28 | margin: 20px auto; 29 | } 30 | 31 | .login-modal .form p { 32 | font-style: italic; 33 | } 34 | 35 | .login-modal .form a { 36 | color: #00BCD4; 37 | cursor: pointer; 38 | } -------------------------------------------------------------------------------- /backend/middleware/checkToken.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const User = require('../models/User'); 3 | 4 | const checkToken = (req, res, next) => { 5 | const header = req.headers['authorization']; 6 | 7 | if (typeof header !== 'undefined') { 8 | const bearer = header.split(' '); 9 | const token = bearer[1]; 10 | 11 | jwt.verify(token, process.env.PRIVATE_KEY, (err, data) => { 12 | if (err) { 13 | res.sendStatus(403); 14 | } else { 15 | User.findOne({ username: data }).exec((err, user) => { 16 | req.user = user; 17 | next(); 18 | }); 19 | } 20 | }) 21 | } else { 22 | res.sendStatus(403); 23 | } 24 | } 25 | 26 | module.exports = { 27 | checkToken, 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/typings/state/catalogProduct.ts: -------------------------------------------------------------------------------- 1 | export interface ICatalogProduct { 2 | info: { 3 | name: string; 4 | dimensions: string; 5 | weight: string; 6 | displayType: string; 7 | displaySize: string; 8 | displayResolution: string; 9 | os: string; 10 | cpu: string; 11 | internalMemory: string; 12 | ram: string; 13 | camera: string; 14 | batery: string; 15 | color: string; 16 | price: number; 17 | photo: string; 18 | }; 19 | tags: { 20 | priceRange: string; 21 | brand: string; 22 | color: string; 23 | os: string; 24 | internalMemory: string; 25 | ram: string; 26 | displaySize: string; 27 | displayResolution: string; 28 | camera: string; 29 | cpu: string; 30 | }; 31 | _id: string; 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/components/Products/index.ts: -------------------------------------------------------------------------------- 1 | import { compose } from 'recompose'; 2 | import { connect } from 'react-redux'; 3 | import { initCatalog, clearFilters, setSortBy } from '@actions/index'; 4 | import { isCatalogLoaded, sortProducts, filterProducts, selectSortBy } from '@selectors/catalog'; 5 | import { IState } from '@typings/state/index'; 6 | import Products, { Props } from './Products'; 7 | 8 | const mapStateToProps = (state: IState) => ({ 9 | catalogLoaded: isCatalogLoaded(state), 10 | catalog: sortProducts(filterProducts(state), state.sortBy), 11 | sortBy: selectSortBy(state) 12 | }); 13 | 14 | const actions = { 15 | initCatalog, 16 | clearFilters, 17 | setSortBy 18 | }; 19 | 20 | export default compose( 21 | connect(mapStateToProps, actions) 22 | )(Products); 23 | -------------------------------------------------------------------------------- /frontend/src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const GET_USER = 'GET_USER'; 2 | export const GET_USER_SUCCESS = 'GET_USER_SUCCESS'; 3 | export const GET_USER_FAIL = 'GET_USER_FAIL'; 4 | 5 | export const EDIT_USER = 'EDIT_USER'; 6 | export const EDIT_USER_SUCCESS = 'EDIT_USER_SUCCESS'; 7 | export const EDIT_USER_FAIL = 'EDIT_USER_FAIL'; 8 | 9 | export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS'; 10 | 11 | export const INIT_CATALOG = 'INIT_CATALOG'; 12 | export const INIT_CATALOG_SUCCESS = 'INIT_CATALOG_SUCCESS'; 13 | export const INIT_CATALOG_FAIL = 'INIT_CATALOG_FAIL'; 14 | 15 | export const GET_CART = 'GET_CART'; 16 | export const GET_CART_SUCCESS = 'GET_CART_SUCCESS'; 17 | export const GET_CART_FAIL = 'GET_CART_FAIL'; 18 | 19 | export const SET_FILTER = 'SET_FILTER'; 20 | export const CLEAR_FILTERS = 'CLEAR_FILTERS'; 21 | 22 | export const SET_SORT_BY = 'SET_SORT_BY'; 23 | -------------------------------------------------------------------------------- /backend/models/Product.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const productSchema = new mongoose.Schema({ 4 | info: { 5 | name: String, 6 | dimensions: String, 7 | weight: String, 8 | displayType: String, 9 | displaySize: String, 10 | displayResolution: String, 11 | os: String, 12 | cpu: String, 13 | internalMemory: String, 14 | ram: String, 15 | camera: String, 16 | batery: String, 17 | color: String, 18 | price: Number, 19 | photo: String 20 | }, 21 | tags: { 22 | priceRange: String, 23 | brand: String, 24 | color: String, 25 | os: String, 26 | internalMemory: String, 27 | ram: String, 28 | displaySize: String, 29 | displayResolution: String, 30 | camera: String, 31 | cpu: String 32 | } 33 | }); 34 | 35 | module.exports = mongoose.model('Product', productSchema); -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | 5 | db: 6 | build: ./db 7 | restart: always 8 | volumes: 9 | - ./db/data:/data/db 10 | environment: 11 | MONGO_INITDB_ROOT_USERNAME: root 12 | MONGO_INITDB_ROOT_PASSWORD: password 13 | MONGO_INITDB_DATABASE: mobileShop 14 | ports: 15 | - '27017:27017' 16 | healthcheck: 17 | test: echo 'db.runCommand("ping").ok' | mongo db:27017/test --quiet 18 | interval: 10s 19 | timeout: 10s 20 | retries: 3 21 | start_period: 40s 22 | 23 | backend: 24 | build: 25 | context: ./backend 26 | ports: 27 | - "5000:5000" 28 | links: 29 | - db 30 | depends_on: 31 | db: 32 | condition: service_healthy 33 | 34 | frontend: 35 | build: 36 | context: ./frontend 37 | ports: 38 | - "3000:3000" 39 | -------------------------------------------------------------------------------- /frontend/src/styles/RegisterModal.css: -------------------------------------------------------------------------------- 1 | .register-modal { 2 | width: 90%; 3 | max-width: 385px; 4 | height: 570px; 5 | margin: auto; 6 | margin-top: 100px; 7 | background: white; 8 | border: 1px solid white; 9 | border-radius: 5px; 10 | outline: none; 11 | } 12 | 13 | .register-modal .form { 14 | width: 90%; 15 | max-width: 360px; 16 | margin: auto; 17 | margin-top: 10px; 18 | text-align: center; 19 | font-family: 'Roboto'; 20 | } 21 | 22 | .register-modal .form h1 { 23 | color: #00BCD4; 24 | } 25 | 26 | .register-modal .form .btn { 27 | margin: 20px auto; 28 | } 29 | 30 | .register-modal .form p { 31 | font-style: italic; 32 | } 33 | 34 | .register-modal .form a { 35 | color: #00BCD4; 36 | cursor: pointer; 37 | } 38 | 39 | @media (max-height: 700px) { 40 | .register-modal { 41 | height: 500px; 42 | overflow: scroll; 43 | } 44 | } -------------------------------------------------------------------------------- /frontend/src/store/configureStore.ts: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import { routerMiddleware } from 'connected-react-router'; 3 | import createSagaMiddleware from 'redux-saga'; 4 | import { createBrowserHistory } from 'history'; 5 | import rootSaga from '../sagas'; 6 | import createRootReducer from '../reducers'; 7 | 8 | export const history = createBrowserHistory(); 9 | const composeEnhancers = (window).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 10 | const sagaMiddleware = createSagaMiddleware(); 11 | 12 | const configureStore = () => { 13 | const store = createStore( 14 | createRootReducer(history), 15 | composeEnhancers( 16 | applyMiddleware( 17 | routerMiddleware(history), 18 | sagaMiddleware 19 | ) 20 | ) 21 | ); 22 | 23 | sagaMiddleware.run(rootSaga); 24 | 25 | return store; 26 | }; 27 | 28 | export default configureStore; 29 | -------------------------------------------------------------------------------- /frontend/src/components/OrderSuccessModal/OrderSuccessModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Modal from 'react-modal'; 3 | import RaisedButton from 'material-ui/RaisedButton'; 4 | import { ModalProps } from '@typings/modal'; 5 | import '@styles/OrderSuccessModal.css'; 6 | 7 | const OrderSuccessModal = ({ isOpen, setActiveModal }: ModalProps) => ( 8 | setActiveModal(null)} 12 | > 13 |
14 |

Success!

15 | 16 |
17 |

18 | Your order has been received. The items you've ordered will be sent to your address. 19 |

20 | setActiveModal(null)} 22 | className="btn" 23 | label="OK" 24 | primary={true} 25 | /> 26 |
27 |
28 | ); 29 | 30 | export default OrderSuccessModal; 31 | -------------------------------------------------------------------------------- /backend/routes/userRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const _ = require('lodash'); 3 | const User = require('../models/User'); 4 | const { checkToken } = require('../middleware/checkToken'); 5 | 6 | const router = express.Router(); 7 | 8 | router.get('/', checkToken, (req, res) => { 9 | res.status(200).send(req.user); 10 | }); 11 | 12 | router.put('/', checkToken, async ({ body, user }, res) => { 13 | const { 14 | email, 15 | address, 16 | phone, 17 | } = body; 18 | 19 | try { 20 | const foundUser = await User.findById(user.id).exec(); 21 | 22 | await foundUser.replaceOne({ 23 | ..._.omit(foundUser.toJSON(), ['id']), 24 | email: email || foundUser.email, 25 | address: address || foundUser.address, 26 | phone: phone || foundUser.phone, 27 | }); 28 | 29 | return res.status(200).send({ message: 'User updated' }); 30 | } catch (err) { 31 | return res.status(500).send({ message: err }); 32 | } 33 | }); 34 | 35 | module.exports = router; -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fullstack-shopping-cart", 3 | "version": "1.0.0", 4 | "description": "Mobile phones shopping cart", 5 | "main": "index.js", 6 | "author": "Ivan Jakimovski", 7 | "license": "ISC", 8 | "engines": { 9 | "node": "8.10.0" 10 | }, 11 | "scripts": { 12 | "start:dev": "nodemon index.js", 13 | "start:client": "npm run start:dev --prefix client", 14 | "start:dev-full": "concurrently \"npm run start:dev\" \"npm run start:client\"", 15 | "install:client": "npm install --prefix client", 16 | "build": "npm run build --prefix client", 17 | "start": "node index.js", 18 | "heroku-postbuild": "npm run install:client && npm run build" 19 | }, 20 | "dependencies": { 21 | "bcrypt": "^5.0.0", 22 | "body-parser": "^1.18.2", 23 | "concurrently": "^3.5.1", 24 | "cors": "^2.8.5", 25 | "dotenv": "^8.2.0", 26 | "express": "^4.16.2", 27 | "jsonwebtoken": "^8.5.1", 28 | "lodash": "^4.17.20", 29 | "mongoose": "^5.11.9" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "outDir": "public/dist", 5 | "module": "esnext", 6 | "target": "es5", 7 | "lib": ["es2017","dom"], 8 | "sourceMap": true, 9 | "allowJs": true, 10 | "jsx": "react", 11 | "moduleResolution": "node", 12 | "rootDir": ".", 13 | "forceConsistentCasingInFileNames": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noImplicitAny": true, 17 | "strictNullChecks": true, 18 | "suppressImplicitAnyIndexErrors": true, 19 | "noUnusedLocals": true, 20 | "allowSyntheticDefaultImports": true, 21 | "paths": { 22 | "@api/*": ["src/api/*"], 23 | "@actions/*": ["src/actions/*"], 24 | "@selectors/*": ["src/selectors/*"], 25 | "@styles/*": ["src/styles/*"], 26 | "@typings/*": ["src/typings/*"], 27 | "@utils/*": ["src/utils/*"], 28 | } 29 | }, 30 | "exclude": [ 31 | "node_modules", 32 | "dist", 33 | "webpack", 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/reducers/catalogReducer.ts: -------------------------------------------------------------------------------- 1 | import { INIT_CATALOG, INIT_CATALOG_SUCCESS, INIT_CATALOG_FAIL } from '../constants'; 2 | import { ICatalog } from '@typings/state/index'; 3 | import { actionTypes } from '@typings/action'; 4 | 5 | interface IAction { 6 | type: actionTypes; 7 | payload: ICatalog; 8 | } 9 | 10 | const initState: ICatalog = { 11 | isLoading: false, 12 | isLoaded: false, 13 | items: [], 14 | error: null 15 | }; 16 | 17 | const catalogReducer = (state = initState, action: IAction) => { 18 | switch (action.type) { 19 | case INIT_CATALOG: 20 | return { 21 | ...state, 22 | isLoading: true 23 | } 24 | case INIT_CATALOG_SUCCESS: 25 | return { 26 | ...state, 27 | isLoading: false, 28 | isLoaded: true, 29 | items: action.payload 30 | } 31 | case INIT_CATALOG_FAIL: 32 | return { 33 | ...state, 34 | isLoaded: true, 35 | error: action.payload 36 | } 37 | default: 38 | return state 39 | } 40 | } 41 | 42 | export default catalogReducer; 43 | -------------------------------------------------------------------------------- /frontend/src/components/ShoppingCart/ShoppingCart.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter, Route, Switch } from 'react-router-dom'; 3 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 4 | import Header from '../Header'; 5 | import Account from '../Account'; 6 | import Cart from '../Cart'; 7 | import Homepage from '../Homepage'; 8 | import ProductDetails from '../ProductDetails'; 9 | import Footer from '../Footer'; 10 | import NotFound from '../NotFound'; 11 | import '@styles/ShoppingCart.css'; 12 | 13 | const ShoppingCart = () => ( 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 |
29 | ); 30 | 31 | export default ShoppingCart; 32 | -------------------------------------------------------------------------------- /frontend/src/reducers/cartReducer.ts: -------------------------------------------------------------------------------- 1 | import { GET_CART, GET_CART_SUCCESS, GET_CART_FAIL } from '../constants'; 2 | import { actionTypes } from '@typings/action'; 3 | import { ICart } from '@typings/state/cart'; 4 | 5 | interface IAction { 6 | type: actionTypes; 7 | payload?: ICart; 8 | } 9 | 10 | export const initState: ICart = { 11 | isLoading: false, 12 | isLoaded: false, 13 | _id: null, 14 | items: [], 15 | error: null 16 | }; 17 | 18 | const cartReducer = (state = initState, action: IAction) => { 19 | switch(action.type) { 20 | case GET_CART: 21 | return { 22 | ...state, 23 | isLoading: true 24 | } 25 | case GET_CART_SUCCESS: 26 | return { 27 | ...state, 28 | isLoading: false, 29 | isLoaded: true, 30 | _id: action.payload ? action.payload._id : null, 31 | items: action.payload ? action.payload.items : [], 32 | error: null 33 | } 34 | case GET_CART_FAIL: 35 | return { 36 | ...state, 37 | isLoaded: true, 38 | items: [], 39 | error: action.payload 40 | } 41 | default: 42 | return state; 43 | } 44 | }; 45 | 46 | export default cartReducer; 47 | -------------------------------------------------------------------------------- /frontend/src/sagas/index.ts: -------------------------------------------------------------------------------- 1 | import { put, all, takeLatest } from 'redux-saga/effects'; 2 | import { getUser as fetchUser } from '@api/user'; 3 | import { getCatalog } from '@api/catalog'; 4 | import { getCart as fetchCart } from '@api/cart'; 5 | import { 6 | INIT_CATALOG, 7 | GET_USER, 8 | GET_CART 9 | } from '../constants'; 10 | import * as actions from '../actions'; 11 | 12 | function* initCatalog() { 13 | try { 14 | const catalog = yield getCatalog(); 15 | 16 | yield put(actions.initCatalogSuccess(catalog.data)); 17 | } catch(e) { 18 | yield put(actions.initCatalogFail('COULD NOT GET CATALOG')); 19 | } 20 | } 21 | 22 | function* getUser() { 23 | try { 24 | const user = yield fetchUser(); 25 | 26 | yield put(actions.getUserSuccess(user.data)); 27 | } catch(e) { 28 | yield put(actions.getUserFail('COULD NOT GET USER')); 29 | } 30 | } 31 | 32 | function* getCart() { 33 | try { 34 | const cart = yield fetchCart(); 35 | 36 | yield put(actions.getCartSuccess(cart.data)); 37 | } catch(e) { 38 | yield put(actions.getCartFail('COULD NOT GET CART')); 39 | } 40 | } 41 | 42 | export default function*() { 43 | yield all([ 44 | takeLatest(INIT_CATALOG, initCatalog), 45 | takeLatest(GET_USER, getUser), 46 | takeLatest(GET_CART, getCart) 47 | ]); 48 | } 49 | -------------------------------------------------------------------------------- /frontend/src/styles/CheckoutModal.css: -------------------------------------------------------------------------------- 1 | .ReactModalPortal .ReactModal__Overlay--after-open { 2 | background-color: #00000073 !important; 3 | } 4 | 5 | .checkout-modal { 6 | width: 90%; 7 | max-width: 500px; 8 | height: auto; 9 | margin: auto; 10 | margin-top: 100px; 11 | background: white; 12 | border: 1px solid white; 13 | border-radius: 5px; 14 | outline: none; 15 | } 16 | 17 | .checkout-modal .order { 18 | width: 90%; 19 | max-width: 450px; 20 | margin: auto; 21 | margin-top: 10px; 22 | text-align: center; 23 | font-family: 'Roboto'; 24 | } 25 | 26 | .checkout-modal h1 { 27 | margin-bottom: 60px; 28 | color: #00BCD4; 29 | } 30 | 31 | .checkout-modal table { 32 | width: 100%; 33 | margin-top: 40px; 34 | border-spacing: 0; 35 | } 36 | 37 | .checkout-modal th { 38 | padding: 5px 5px; 39 | background-color: #00BCD4; 40 | color: white; 41 | text-align: left; 42 | } 43 | 44 | .checkout-modal td { 45 | padding: 12px 5px; 46 | text-align: left; 47 | } 48 | 49 | .checkout-modal .total { 50 | text-align: left; 51 | } 52 | 53 | .checkout-modal .total span { 54 | font-size: 26px; 55 | font-weight: bold; 56 | color: #64DD17; 57 | } 58 | 59 | .checkout-modal .btns { 60 | margin-top: 40px; 61 | margin-bottom: 20px; 62 | } 63 | 64 | .checkout-modal .btns .btn { 65 | margin: 0 20px; 66 | } 67 | 68 | @media (max-height: 700px) { 69 | .checkout-modal { 70 | height: 440px; 71 | min-height: auto; 72 | overflow: scroll; 73 | } 74 | } -------------------------------------------------------------------------------- /frontend/src/reducers/userReducer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GET_USER, 3 | GET_USER_SUCCESS, 4 | GET_USER_FAIL, 5 | LOGOUT_SUCCESS, 6 | } from '../constants'; 7 | import { IUser } from '@typings/state/user'; 8 | import { ILoggedUser } from '@typings/state/loggedUser'; 9 | import { actionTypes } from '@typings/action'; 10 | 11 | interface IAction { 12 | type: actionTypes; 13 | payload: IUser; 14 | } 15 | 16 | const initState: ILoggedUser = { 17 | isLoading: false, 18 | isLoaded: false, 19 | user: null, 20 | error: null 21 | } 22 | 23 | const userReducer = (state = initState, action: IAction) => { 24 | switch(action.type) { 25 | case GET_USER: 26 | return { 27 | ...state, 28 | isLoading: true 29 | } 30 | case GET_USER_SUCCESS: 31 | localStorage.setItem('token', action.payload.token); 32 | return { 33 | ...state, 34 | isLoading: false, 35 | isLoaded: true, 36 | user: action.payload, 37 | error: null 38 | } 39 | case GET_USER_FAIL: 40 | return { 41 | ...state, 42 | isLoading: false, 43 | isLoaded: true, 44 | user: null, 45 | error: action.payload 46 | } 47 | case LOGOUT_SUCCESS: 48 | localStorage.removeItem('token'); 49 | return { 50 | ...state, 51 | isLoading: false, 52 | isLoaded: true, 53 | user: null, 54 | error: null 55 | } 56 | default: 57 | return state; 58 | } 59 | } 60 | 61 | export default userReducer; 62 | -------------------------------------------------------------------------------- /frontend/src/styles/Account.css: -------------------------------------------------------------------------------- 1 | .account-container { 2 | min-height: 100vh; 3 | margin: 0 6px; 4 | margin-top: -72px; 5 | border: 1px solid #ffffff00; 6 | } 7 | 8 | .account-container .top { 9 | display: flex; 10 | justify-content: space-between; 11 | } 12 | 13 | .account-container .top h1 { 14 | margin-top: 100px; 15 | margin-bottom: 30px; 16 | } 17 | 18 | .loader { 19 | margin: 20px auto; 20 | color: #00BCD4; 21 | text-align: center; 22 | } 23 | 24 | .account { 25 | display: flex; 26 | flex-direction: row; 27 | } 28 | 29 | .account-info { 30 | width: 50%; 31 | margin-bottom: 30px; 32 | } 33 | 34 | .account-info .btn { 35 | margin-top: 20px; 36 | } 37 | 38 | .account-history { 39 | width: 50%; 40 | margin-bottom: 50px; 41 | } 42 | 43 | .account-history h1 { 44 | margin-top: 100px; 45 | text-align: center; 46 | } 47 | 48 | .account-history .orders table { 49 | width: 100%; 50 | margin: 10px auto; 51 | border-spacing: 0; 52 | } 53 | 54 | .orders th { 55 | padding: 5px 5px; 56 | background-color: #00BCD4; 57 | color: white; 58 | text-align: left; 59 | } 60 | 61 | .orders td { 62 | padding: 12px 5px; 63 | text-align: left; 64 | border-bottom: 1px solid #00BCD4; 65 | } 66 | 67 | .orders td:nth-child(4) { 68 | text-align: center; 69 | } 70 | 71 | @media (max-width: 1000px) { 72 | .account-container h1 { 73 | font-size: 24px; 74 | } 75 | 76 | .account { 77 | flex-direction: column; 78 | } 79 | 80 | .account-info { 81 | width: 100%; 82 | } 83 | 84 | .account-history { 85 | width: 100%; 86 | } 87 | } -------------------------------------------------------------------------------- /frontend/src/styles/Product.css: -------------------------------------------------------------------------------- 1 | .product { 2 | /* display: flex; 3 | flex-direction: row; */ 4 | width: 100%; 5 | /* height: 200px; 6 | max-height: 200px; */ 7 | height: auto; 8 | margin: 10px 0; 9 | box-shadow: 0 0 7px #d6d6d6; 10 | transition: .3s; 11 | } 12 | 13 | .product:hover { 14 | box-shadow: 0 0 7px #b3b3b3; 15 | } 16 | 17 | .content img { 18 | max-width: 170px; 19 | max-height: 170px; 20 | padding: 10px; 21 | } 22 | 23 | .content { 24 | display: flex; 25 | flex-direction: row; 26 | width: 100%; 27 | /* height: 190px; 28 | max-height: 190px; */ 29 | } 30 | 31 | .content-left { 32 | width: 80%; 33 | } 34 | 35 | .content-left h3 { 36 | margin: 10px 0 25px 0; 37 | } 38 | 39 | .content-left div { 40 | margin: 1.5px 0; 41 | } 42 | 43 | .content-right { 44 | width: 20%; 45 | } 46 | 47 | .content-right p { 48 | margin: 50px 0 0 0; 49 | } 50 | 51 | .content-right h2 { 52 | margin: 0 0 40px 0; 53 | font-size: 30px; 54 | color: #64DD17; 55 | } 56 | 57 | .content-info { 58 | width: 100% !important; 59 | text-align: left !important; 60 | } 61 | 62 | @media (max-width: 860px) { 63 | .content { 64 | flex-direction: column; 65 | } 66 | 67 | .content { 68 | height: auto; 69 | max-height: auto; 70 | } 71 | 72 | .content img { 73 | margin: auto; 74 | } 75 | 76 | .content-left, .content-right { 77 | width: 100%; 78 | margin: 10px 0; 79 | text-align: center; 80 | } 81 | 82 | .content-left h3, .content-left div { 83 | margin: 0; 84 | } 85 | 86 | .content-info { 87 | display: none; 88 | } 89 | } -------------------------------------------------------------------------------- /frontend/src/actions/index.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions'; 2 | import { 3 | INIT_CATALOG, 4 | INIT_CATALOG_SUCCESS, 5 | INIT_CATALOG_FAIL, 6 | GET_USER, 7 | GET_USER_SUCCESS, 8 | GET_USER_FAIL, 9 | EDIT_USER, 10 | EDIT_USER_SUCCESS, 11 | EDIT_USER_FAIL, 12 | LOGOUT_SUCCESS, 13 | GET_CART, 14 | GET_CART_SUCCESS, 15 | GET_CART_FAIL, 16 | SET_FILTER, 17 | CLEAR_FILTERS, 18 | SET_SORT_BY, 19 | } from '../constants'; 20 | import { filterTypes, filterValues } from '@typings/filters'; 21 | 22 | export const initCatalog = createAction(INIT_CATALOG); 23 | export const initCatalogSuccess = createAction(INIT_CATALOG_SUCCESS); 24 | export const initCatalogFail = createAction(INIT_CATALOG_FAIL); 25 | 26 | export const getUser = createAction(GET_USER); 27 | export const getUserSuccess = createAction(GET_USER_SUCCESS); 28 | export const getUserFail = createAction(GET_USER_FAIL); 29 | 30 | export const editUser = createAction(EDIT_USER); 31 | export const editUserSuccess = createAction(EDIT_USER_SUCCESS); 32 | export const editUserFail = createAction(EDIT_USER_FAIL); 33 | 34 | export const logoutSuccess = createAction(LOGOUT_SUCCESS); 35 | 36 | export const getCart = createAction(GET_CART); 37 | export const getCartSuccess = createAction(GET_CART_SUCCESS); 38 | export const getCartFail = createAction(GET_CART_FAIL); 39 | 40 | export const setFilter = createAction( 41 | SET_FILTER, 42 | (filterType: filterTypes, filterValue: filterValues) => ({ filterType, filterValue }) 43 | ); 44 | export const clearFilters = createAction(CLEAR_FILTERS); 45 | 46 | export const setSortBy = createAction(SET_SORT_BY); 47 | -------------------------------------------------------------------------------- /frontend/src/reducers/filtersReducer.ts: -------------------------------------------------------------------------------- 1 | import { SET_FILTER, CLEAR_FILTERS } from '../constants'; 2 | import { IFilters } from '@typings/state/index'; 3 | import { actionTypes } from '@typings/action'; 4 | import { filterTypes, filterValues } from '@typings/filters'; 5 | 6 | interface IAction { 7 | type: actionTypes; 8 | payload: { 9 | filterType: filterTypes; 10 | filterValue: filterValues; 11 | } 12 | } 13 | 14 | const initState: IFilters = { 15 | filters: { 16 | priceRange: [], 17 | brand: [], 18 | color: [], 19 | os: [], 20 | internalMemory: [], 21 | ram: [], 22 | displaySize: [], 23 | displayResolution: [], 24 | camera: [], 25 | cpu: [] 26 | }, 27 | checked: [] 28 | }; 29 | 30 | const filtersReducer = (state = initState, action: IAction) => { 31 | switch (action.type) { 32 | case SET_FILTER: 33 | const { filterType, filterValue } = action.payload; 34 | const newState = { ...state }; 35 | 36 | if (newState.filters[filterType].includes(filterValue)) { 37 | newState.filters[filterType] = newState.filters[filterType].filter((item: string) => item !== filterValue); 38 | newState.checked = newState.checked.filter((item) => item !== filterValue); 39 | } else { 40 | newState.filters[filterType].push(filterValue); 41 | newState.checked.push(filterValue); 42 | } 43 | 44 | return newState; 45 | case CLEAR_FILTERS: 46 | for (let key in initState.filters) { 47 | initState.filters[key] = []; 48 | } 49 | 50 | initState.checked = []; 51 | 52 | return initState; 53 | default: 54 | return state; 55 | } 56 | }; 57 | 58 | export default filtersReducer; 59 | -------------------------------------------------------------------------------- /frontend/src/components/LoginModal/LoginModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Modal from 'react-modal'; 3 | import TextField from 'material-ui/TextField'; 4 | import RaisedButton from 'material-ui/RaisedButton'; 5 | import { ModalProps } from '@typings/modal'; 6 | import { login } from '@api/auth'; 7 | import '@styles/LoginModal.css'; 8 | 9 | const LoginModal = ({ isOpen, onRequestClose, setActiveModal, setUser }: ModalProps): JSX.Element => { 10 | const [data, setData] = useState({}); 11 | 12 | const setFormField = (key: string, value: any) => { 13 | setData({ 14 | ...data, 15 | [key]: value, 16 | }); 17 | } 18 | 19 | const onSubmit = () => { 20 | login(data).then((res) => { 21 | setUser && setUser(res.data); 22 | onRequestClose(); 23 | }); 24 | } 25 | 26 | return ( 27 | 32 |
33 |

Log In

34 | setFormField('username', target.value)} 39 | />
40 | setFormField('password', target.value)} 45 | />
46 | 52 |

Don't have an account yet? setActiveModal('register')}>Register here.

53 |
54 |
55 | ); 56 | } 57 | 58 | export default LoginModal; 59 | -------------------------------------------------------------------------------- /backend/index.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | const express = require('express'); 4 | const path = require('path'); 5 | const bodyParser = require('body-parser'); 6 | const cors = require('cors'); 7 | const mongoose = require('mongoose'); 8 | const authRoutes = require('./routes/authRoutes'); 9 | const catalogRoutes = require('./routes/catalogRoutes'); 10 | const userRoutes = require('./routes/userRoutes'); 11 | const cartRoutes = require('./routes/cartRoutes'); 12 | const orderRoutes = require('./routes/orderRoutes'); 13 | 14 | const publicPath = path.join(__dirname, '..', 'frontend', 'public'); 15 | const port = process.env.PORT || 5000; 16 | 17 | const app = express(); 18 | 19 | let connectionRetries = 0; 20 | 21 | const connectWithRetry = () => { 22 | console.log('CONNECTING TO DB...'); 23 | 24 | mongoose.connect(process.env.MONGODB_URI, { 25 | auth: { authSource: 'admin' }, 26 | user: process.env.MONGODB_USER, 27 | pass: process.env.MONGODB_PASSWORD, 28 | }).then(() => { 29 | console.log('CONNECTED TO DB!'); 30 | clearTimeout(connectWithRetry); 31 | }).catch((err) => { 32 | console.log(err); 33 | 34 | connectionRetries++; 35 | 36 | if (connectionRetries <= 4) { 37 | setTimeout(connectWithRetry, 5000); 38 | } else { 39 | clearTimeout(connectWithRetry); 40 | } 41 | }); 42 | }; 43 | 44 | connectWithRetry(); 45 | 46 | app.use(cors()); 47 | app.use(bodyParser.urlencoded({ extended: true })); 48 | app.use( 49 | bodyParser.json({ 50 | limit: '10MB', 51 | type: 'application/json', 52 | }), 53 | ); 54 | app.use(express.static(publicPath)); 55 | 56 | app.use('/api/auth', authRoutes); 57 | app.use('/api/catalog', catalogRoutes); 58 | app.use('/api/user', userRoutes); 59 | app.use('/api/cart', cartRoutes); 60 | app.use('/api/order', orderRoutes); 61 | 62 | app.listen(port, () => console.log(`SERVER NOW RUNNING ON PORT ${port}...`)); 63 | -------------------------------------------------------------------------------- /backend/routes/authRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const jwt = require('jsonwebtoken'); 3 | const bcrypt = require('bcrypt'); 4 | const User = require('../models/User'); 5 | 6 | const router = express.Router(); 7 | 8 | const generateToken = (data) => { 9 | return jwt.sign(data, process.env.PRIVATE_KEY); 10 | } 11 | 12 | router.post('/register', async ({ body }, res) => { 13 | const { 14 | username, 15 | password, 16 | email, 17 | address, 18 | phone, 19 | } = body; 20 | 21 | try { 22 | const user = await User.findOne({ username }).exec(); 23 | 24 | if (user) { 25 | return res.status(409).send({ message: 'User already exists' }); 26 | } 27 | 28 | const newUserData = { 29 | username, 30 | email, 31 | address, 32 | phone, 33 | orders: [] 34 | }; 35 | 36 | const salt = await bcrypt.genSalt(); 37 | const hash = await bcrypt.hash(password, salt); 38 | 39 | newUserData.password = hash; 40 | newUserData.token = generateToken(username); 41 | 42 | const newUser = new User(newUserData); 43 | const createdUser = await newUser.save(); 44 | 45 | res.status(201).send({ ...createdUser.toJSON() }); 46 | } catch (err) { 47 | res.status(500).send({ message: err }); 48 | } 49 | }); 50 | 51 | router.post('/login', async ({ body }, res) => { 52 | const { username, password } = body; 53 | 54 | try { 55 | const existingUser = await User.findOne({ username }).exec(); 56 | 57 | if (!existingUser) { 58 | return res.status(401).send({ message: 'No user found' }); 59 | } 60 | 61 | const correctPassword = await bcrypt.compare(password, existingUser.password); 62 | 63 | if (!correctPassword) { 64 | return res.status(401).send({ message: 'Invalid credentials' }); 65 | } 66 | 67 | return res.status(200).send({ ...existingUser.toJSON() }); 68 | } catch (err) { 69 | res.status(500).send({ message: err }); 70 | } 71 | }); 72 | 73 | module.exports = router; 74 | -------------------------------------------------------------------------------- /frontend/src/components/Product/Product.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import numeral from 'numeral'; 4 | import RaisedButton from 'material-ui/RaisedButton'; 5 | import NavigateNext from 'material-ui/svg-icons/image/navigate-next'; 6 | import { ICatalogProduct } from '@typings/state/index' 7 | import '@styles/Product.css'; 8 | 9 | interface Props { 10 | key: string; 11 | item: ICatalogProduct; 12 | } 13 | 14 | const Product = ({ item: {info, _id} }: Props) => { 15 | const { 16 | photo, 17 | name, 18 | displaySize, 19 | displayResolution, 20 | cpu, 21 | internalMemory, 22 | ram, 23 | camera, 24 | price 25 | } = info; 26 | 27 | return ( 28 |
29 |
30 | 31 |
32 |

{name}

33 |
34 |
Display size: {displaySize}
35 |
Display resolution: {displayResolution}
36 |
CPU: {cpu}
37 |
Internal memory: {internalMemory}
38 |
RAM: {ram}
39 |
Camera: {camera.length < 50 ? camera : camera.slice(0, 50) + '...'}
40 |
41 |
42 |
43 |
44 |

Price:

45 |

{numeral(price).format('$0,0.00')}

46 |
47 | } 49 | className="btn" 50 | label="See more" 51 | labelPosition="before" 52 | primary={true} 53 | icon={} 54 | /> 55 |
56 |
57 |
58 | ) 59 | }; 60 | 61 | export default Product; 62 | -------------------------------------------------------------------------------- /backend/routes/cartRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bodyParser = require('body-parser'); 3 | const Cart = require('../models/Cart'); 4 | const { checkToken } = require('../middleware/checkToken'); 5 | 6 | const router = express.Router(); 7 | const jsonParser = bodyParser.json(); 8 | 9 | router.post('/', checkToken, (req, res) => { 10 | const user = req.user; 11 | const item = { 12 | product: req.body.product, 13 | quantity: req.body.quantity 14 | }; 15 | 16 | Cart.findOne({ user: user }) 17 | .then((foundCart) => { 18 | if (foundCart) { 19 | let products = foundCart.items.map((item) => item.product + ''); 20 | if (products.includes(item.product)) { 21 | Cart.findOneAndUpdate({ 22 | user: user, 23 | items: { 24 | $elemMatch: { product: item.product } 25 | } 26 | }, 27 | { 28 | $inc: { 'items.$.quantity': item.quantity } 29 | }) 30 | .exec() 31 | .then(() => res.end()); 32 | } else { 33 | foundCart.items.push(item); 34 | foundCart.save().then(() => res.end()); 35 | } 36 | } else { 37 | Cart.create({ 38 | user: user, 39 | items: [item] 40 | }) 41 | .then(() => res.end()); 42 | } 43 | }); 44 | }); 45 | 46 | router.get('/', checkToken, (req, res) => { 47 | Cart.findOne({ user: req.user.id }) 48 | .populate('items.product') 49 | .exec((err, cart) => { 50 | if (!cart) { 51 | return res.send(null); 52 | } 53 | 54 | res.send(cart); 55 | }); 56 | }); 57 | 58 | router.put('/', checkToken, jsonParser, (req, res) => { 59 | Cart.findById(req.body.cartId) 60 | .then((foundCart) => { 61 | foundCart.items = foundCart.items.filter((item) => item._id != req.body.itemId); 62 | foundCart.save(() => res.end()); 63 | }); 64 | }); 65 | 66 | router.delete('/', checkToken, (req, res) => { 67 | Cart.findByIdAndRemove(req.query.id) 68 | .then(() => res.end()) 69 | .catch((err) => res.send(err)); 70 | }); 71 | 72 | module.exports = router; -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fullstack-shopping-cart", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "author": "", 7 | "license": "ISC", 8 | "scripts": { 9 | "start:dev": "webpack-dev-server", 10 | "build": "webpack -p" 11 | }, 12 | "dependencies": { 13 | "@material-ui/core": "^3.9.2", 14 | "@material-ui/icons": "^3.0.2", 15 | "axios": "^0.21.1", 16 | "connected-react-router": "^6.2.2", 17 | "dotenv": "^8.2.0", 18 | "history": "^4.7.2", 19 | "material-ui": "^0.20.2", 20 | "moment": "^2.20.1", 21 | "numeral": "^2.0.6", 22 | "react": "^17.0.1", 23 | "react-dom": "^17.0.1", 24 | "react-modal": "^3.1.12", 25 | "react-redux": "^6.0.0", 26 | "react-router-dom": "^5.2.0", 27 | "recompose": "^0.30.0", 28 | "redux": "^3.7.2", 29 | "redux-actions": "^2.6.4", 30 | "redux-persist": "^6.0.0", 31 | "redux-saga": "^1.0.0", 32 | "reselect": "^4.0.0", 33 | "terser": "^3.14.1" 34 | }, 35 | "devDependencies": { 36 | "@babel/cli": "^7.4.4", 37 | "@babel/core": "^7.4.5", 38 | "@babel/preset-env": "^7.4.5", 39 | "@babel/preset-react": "^7.0.0", 40 | "@types/material-ui": "^0.21.5", 41 | "@types/numeral": "0.0.25", 42 | "@types/react": "^16.8.1", 43 | "@types/react-dom": "^16.0.11", 44 | "@types/react-modal": "^3.8.0", 45 | "@types/react-redux": "^7.0.1", 46 | "@types/react-router-dom": "^4.3.1", 47 | "@types/recompose": "^0.30.3", 48 | "@types/redux-actions": "^2.3.1", 49 | "autoprefixer": "^9.1.5", 50 | "babel-loader": "^8.0.5", 51 | "babel-plugin-transform-class-properties": "^6.24.1", 52 | "babel-regenerator-runtime": "^6.5.0", 53 | "css-loader": "^0.28.9", 54 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 55 | "file-loader": "^1.1.6", 56 | "image-webpack-loader": "^4.6.0", 57 | "postcss-loader": "^3.0.0", 58 | "style-loader": "^0.20.1", 59 | "ts-loader": "^5.3.3", 60 | "tsconfig-paths-webpack-plugin": "^3.2.0", 61 | "typescript": "^3.3.1", 62 | "webpack": "^4.29.0", 63 | "webpack-cli": "^3.2.1", 64 | "webpack-dev-server": "^3.11.1" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MERN stack shopping cart 2 | 3 | 4 | 5 | ## Intro 6 | 7 | I started working on this app a while ago when I was still learning technologies used in this project, so please be aware that 8 | the codebase (and the whole app in general) isn't perfect and there is still room for improvement, even though I've updated the code multiple times over the past years. 9 | 10 | So, consider this as a learning example or just a showcase rather than a production-quality code. 11 | 12 | ## Description 13 | 14 | Shopping cart app build with MERN stack and using RESTful API design. Responsive front-end design done with Material-UI, uses 15 | Redux for state management, Node & Express for API, MongoDB as database. App runs in Docker containers but you can also run each sub-app separately, without Docker. 16 | 17 | You can get and view the list of all products from the API, register, add products to cart, remove specific product or empty entire cart, make order... 18 | 19 | ## Technologies & Tools 20 | 21 | ### Front-end: 22 | 23 | * React 24 | * Redux 25 | * Redux-Saga 26 | * Material-UI 27 | * Webpack 28 | * TypeScript 29 | 30 | ### Backend: 31 | 32 | * Node/Express 33 | * MongoDB/Mongoose 34 | 35 | ## Installation and Usage 36 | 37 | ### Requirements: 38 | 39 | * Docker 40 | 41 | In case you want to run it without Docker (requires additional setup): 42 | 43 | * Node.js installed 44 | * MongoDB connection 45 | 46 | ### Steps: 47 | 1. Clone repo on your local machine: 48 | ``` 49 | $ git clone https://github.com/ivan3123708/fullstack-shopping-cart.git 50 | ``` 51 | 2. Run `docker-compose` 52 | ``` 53 | $ cd fullstack-shopping-cart 54 | $ docker-compose up -d 55 | ``` 56 | This will pull images and build 3 containers for each part of the application: `frontent`, `backend` & `db`. 57 | 58 | 3. If everything went without problems, go to `localhost:3000`, you should see the running app. 59 | 60 | - `frontend` container (React app) runs on port `3000` 61 | - `backend` container (Node api) runs on port `5000` 62 | - `db` container (MongoDB server) runs on port `27017` 63 | 64 | Use `docker exec -it bash` to troubleshoot if there are any problems. 65 | -------------------------------------------------------------------------------- /frontend/src/components/CheckoutModal/CheckoutModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import numeral from 'numeral'; 3 | import Modal from 'react-modal'; 4 | import { ICartProduct } from '@typings/state/index'; 5 | import { ModalProps } from '@typings/modal'; 6 | import Button from '@material-ui/core/Button'; 7 | import '@styles/CheckoutModal.css'; 8 | 9 | const CheckoutModal = ({ cart, isOpen, setActiveModal, makeOrder }: ModalProps): JSX.Element => ( 10 | setActiveModal(null)} 14 | > 15 |
16 |

Checkout Information

17 |

18 | Please read the list of items in your order and click "Confirm" to confirm your order. 19 |

20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {cart!.length && cart!.map((item: ICartProduct) => { 31 | return ( 32 | 33 | 34 | 35 | 36 | 37 | 38 | ) 39 | })} 40 | 41 |
Product NamePriceQuantityTotal
{item.product.info.name}{numeral(item.product.info.price).format('$0,0.00')}{item.quantity}{numeral(item.product.info.price * item.quantity!).format('$0,0.00')}
42 |

43 | TOTAL AMOUNT: 44 | {numeral(cart!.length && cart!.reduce((acc, item) => acc += item.product.info.price * item.quantity!, 0)).format('$0,0.00')} 45 |

46 |
47 | 52 | 57 |
58 |
59 |
60 | ); 61 | 62 | export default CheckoutModal; 63 | -------------------------------------------------------------------------------- /frontend/src/styles/Products.css: -------------------------------------------------------------------------------- 1 | .loader { 2 | margin-top: 150px; 3 | color: #00BCD4; 4 | text-align: center; 5 | } 6 | 7 | .no-products { 8 | margin-top: 150px; 9 | text-align: center; 10 | } 11 | 12 | .products { 13 | width: 78%; 14 | margin-top: 90px; 15 | margin-bottom: 10px; 16 | } 17 | 18 | .products-handle { 19 | display: flex; 20 | flex-direction: row; 21 | border-bottom: 1px solid #81D4FA; 22 | } 23 | 24 | .products-handle .products-found { 25 | width: 20%; 26 | margin-top: 16px; 27 | } 28 | 29 | .products-handle .filters { 30 | width: 20%; 31 | text-align: center; 32 | } 33 | 34 | .products-handle .filters .btn { 35 | width: 150px; 36 | margin: 5px; 37 | } 38 | 39 | .products-handle .set-filters { 40 | display: none; 41 | } 42 | 43 | .products-handle .products-sort { 44 | display: flex; 45 | justify-content: flex-end; 46 | width: 60%; 47 | } 48 | 49 | .products-handle .products-sort span { 50 | margin-top: 15px; 51 | margin-right: 5px; 52 | } 53 | 54 | .products-handle .products-sort .sort-field { 55 | margin-top: 0; 56 | } 57 | 58 | @media (max-width: 1115px) { 59 | .products { 60 | width: 100%; 61 | } 62 | 63 | .products-handle { 64 | padding-bottom: 10px; 65 | } 66 | 67 | .products-handle .products-found { 68 | margin-top: 42px; 69 | } 70 | 71 | .products-handle .set-filters { 72 | display: block; 73 | } 74 | 75 | .products-handle .products-sort span { 76 | margin-top: 41px; 77 | } 78 | 79 | .products-handle .products-sort .sort-field { 80 | margin-top: 26px; 81 | } 82 | } 83 | 84 | @media (max-width: 850px) { 85 | .products-handle { 86 | flex-direction: column; 87 | } 88 | 89 | .products-handle .products-found, .products-handle .filters, .products-handle .products-sort { 90 | width: 100%; 91 | margin: 5px 0; 92 | } 93 | 94 | .products-handle .products-sort { 95 | justify-content: center; 96 | } 97 | 98 | .products-handle .products-sort span { 99 | margin-top: 13px; 100 | } 101 | 102 | .products-handle .products-sort .sort-field { 103 | margin-top: 0; 104 | } 105 | } -------------------------------------------------------------------------------- /frontend/src/components/AccountModal/AccountModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Modal from 'react-modal'; 3 | import TextField from 'material-ui/TextField'; 4 | import RaisedButton from 'material-ui/RaisedButton'; 5 | import { getUser, editUser } from '@api/user'; 6 | import { IUser } from '@typings/state/index'; 7 | 8 | import '@styles/AccountModal.css'; 9 | 10 | interface Props { 11 | user: IUser; 12 | isOpen: boolean; 13 | onRequestClose: () => void; 14 | } 15 | 16 | interface State { 17 | email: string; 18 | address: string; 19 | phone: string; 20 | } 21 | 22 | const AccountModal = ({ user, isOpen, onRequestClose }: Props) => { 23 | const [userData, setUserData] = useState({ ...user }); 24 | 25 | const onInputChange = (key: string, value: any) => { 26 | setUserData({ 27 | ...userData, 28 | [key]: value, 29 | }); 30 | } 31 | 32 | const onEditUser = async () => { 33 | await editUser(userData); 34 | await getUser(); 35 | onRequestClose(); 36 | } 37 | 38 | return ( 39 | 44 |
45 |

Edit Account

46 | onInputChange('email', target.value)} 53 | />
54 | onInputChange('address', target.value)} 61 | />
62 | onInputChange('phone', target.value)} 69 | />
70 | 76 |
77 |
78 | ); 79 | } 80 | 81 | export default AccountModal; 82 | -------------------------------------------------------------------------------- /frontend/src/components/RegisterModal/RegisterModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Modal from 'react-modal'; 3 | import TextField from 'material-ui/TextField'; 4 | import RaisedButton from 'material-ui/RaisedButton'; 5 | import { ModalProps } from '@typings/modal'; 6 | import { register } from '@api/auth'; 7 | import '@styles/RegisterModal.css'; 8 | 9 | const RegisterModal = ({ isOpen, onRequestClose, setActiveModal, setUser }: ModalProps) => { 10 | const [data, setData] = useState({}); 11 | 12 | const setFormField = (key: string, value: any) => { 13 | setData({ 14 | ...data, 15 | [key]: value, 16 | }); 17 | } 18 | 19 | const onSubmit = () => { 20 | register(data).then((res) => { 21 | setUser && setUser(res.data); 22 | onRequestClose(); 23 | }); 24 | } 25 | 26 | return ( 27 | 32 |
33 |

Register

34 | setFormField('username', target.value)} 39 | />
40 | setFormField('password', target.value)} 45 | />
46 | setFormField('email', target.value)} 51 | />
52 | setFormField('address', target.value)} 56 | />
57 | setFormField('phone', target.value)} 61 | />
62 | 68 |

Already have an account? setActiveModal('login')}>Login here.

69 |
70 |
71 | ); 72 | } 73 | 74 | export default RegisterModal; 75 | -------------------------------------------------------------------------------- /frontend/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const dotenv = require('dotenv'); 4 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); 5 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 6 | 7 | module.exports = () => { 8 | const env = dotenv.config().parsed; 9 | 10 | const envKeys = Object.keys(env).reduce((prev, next) => { 11 | prev[`process.env.${next}`] = JSON.stringify(env[next]); 12 | return prev; 13 | }, {}); 14 | 15 | return { 16 | entry: ['babel-regenerator-runtime', './src/index.tsx'], 17 | output: { 18 | path: path.resolve(__dirname, 'public', 'dist'), 19 | filename: 'bundle.js' 20 | }, 21 | resolve: { 22 | extensions: ['.ts', '.tsx', '.js'], 23 | plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.json' })], 24 | alias: { 25 | '@api': path.resolve(__dirname, 'src/api'), 26 | '@actions': path.resolve(__dirname, 'src/actions'), 27 | '@selectors': path.resolve(__dirname, 'src/selectors'), 28 | '@styles': path.resolve(__dirname, 'src/styles'), 29 | '@typings': path.resolve(__dirname, 'src/typings'), 30 | '@utils': path.resolve(__dirname, 'src/utils') 31 | } 32 | }, 33 | module: { 34 | rules: [ 35 | { 36 | test: /\.tsx?$/, 37 | loader: 'ts-loader' 38 | }, 39 | { 40 | test: /\.js$/, 41 | exclude: /node_modules/, 42 | loader: 'babel-loader' 43 | }, 44 | { 45 | test: /\.css$/, 46 | use: ExtractTextPlugin.extract({ 47 | fallback: 'style-loader', 48 | use: ['css-loader', 'postcss-loader'] 49 | }) 50 | }, 51 | { 52 | test: /\.(gif|png|jpe?g|svg)$/i, 53 | use: [ 54 | 'file-loader', 55 | { 56 | loader: 'image-webpack-loader', 57 | options: { 58 | bypassOnDebug: true, 59 | } 60 | } 61 | ] 62 | } 63 | ] 64 | }, 65 | plugins: [ 66 | new webpack.DefinePlugin(envKeys), 67 | new ExtractTextPlugin('styles.css'), 68 | ], 69 | devtool: 'source-map', 70 | devServer: { 71 | contentBase: path.join(__dirname, 'public'), 72 | host: '0.0.0.0', 73 | port: 3000, 74 | historyApiFallback: true, 75 | publicPath: '/dist', 76 | watchOptions: { 77 | aggregateTimeout: 500, 78 | poll: 1000 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /frontend/src/styles/Cart.css: -------------------------------------------------------------------------------- 1 | .cart-container { 2 | min-height: 100vh; 3 | margin: 0 6px; 4 | margin-top: -72px; 5 | border: 1px solid #ffffff00; 6 | } 7 | 8 | .cart-container h1 { 9 | margin-top: 100px; 10 | margin-bottom: 40px; 11 | } 12 | 13 | .cart { 14 | display: flex; 15 | flex-direction: row; 16 | justify-content: space-between; 17 | } 18 | 19 | .cart-info { 20 | display: flex; 21 | flex-direction: column; 22 | width: 20%; 23 | height:100%; 24 | min-height: auto; 25 | background: #f5f5f5; 26 | } 27 | 28 | .cart-info p { 29 | padding: 0 10px; 30 | } 31 | 32 | .cart-info .total { 33 | font-size: 22px; 34 | color: #64DD17; 35 | } 36 | 37 | .cart-info .btns { 38 | text-align: center; 39 | } 40 | 41 | .cart-info .btn { 42 | width: 150px; 43 | margin: 10px auto; 44 | } 45 | 46 | .cart-items { 47 | width: 78%; 48 | } 49 | 50 | .cart-items img { 51 | max-width: 50px; 52 | } 53 | 54 | .cart-items table { 55 | width: 100%; 56 | border-spacing: 0; 57 | } 58 | 59 | .cart-items th { 60 | padding: 5px 5px; 61 | background-color: #00BCD4; 62 | color: white; 63 | text-align: left; 64 | } 65 | 66 | .cart-items th:first-child { 67 | width: 70px; 68 | } 69 | 70 | .cart-items th:last-child { 71 | width: 15px; 72 | } 73 | 74 | .cart-items td { 75 | padding: 12px 5px; 76 | text-align: left; 77 | border-bottom: 1px solid #00BCD4; 78 | } 79 | 80 | .cart-items td:nth-child(4) { 81 | text-align: center; 82 | } 83 | 84 | .cart-items a { 85 | color: inherit; 86 | text-decoration: none; 87 | } 88 | 89 | .cart-items button { 90 | padding: 1px 5px; 91 | background: none; 92 | color: #F44336; 93 | font-weight: bold; 94 | border: none; 95 | border-radius: 50%; 96 | cursor: pointer; 97 | } 98 | 99 | .cart-items button:hover { 100 | background: #F44336; 101 | color: white; 102 | } 103 | 104 | .cart-items h1 { 105 | text-align: center; 106 | margin-top: 100px; 107 | } 108 | 109 | @media (max-width: 1000px) { 110 | .cart-container h1 { 111 | margin-top: 100px; 112 | margin-bottom: 30px; 113 | font-size: 24px; 114 | } 115 | 116 | .cart { 117 | flex-direction: column; 118 | } 119 | 120 | .cart-info { 121 | flex-direction: row; 122 | width: 100%; 123 | } 124 | 125 | .cart-info .info { 126 | width: 50%; 127 | } 128 | 129 | .cart-info .btns { 130 | width: 50%; 131 | } 132 | 133 | .cart-info .btn { 134 | margin: 10px; 135 | } 136 | 137 | .cart-items { 138 | width: 100%; 139 | margin: 40px auto 60px auto; 140 | } 141 | } -------------------------------------------------------------------------------- /frontend/src/selectors/catalog.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import { IState, ICatalogProduct, TSortBy } from '@typings/state/index'; 3 | 4 | export const isCatalogLoaded = (state: IState) => state.catalog.isLoaded; 5 | export const selectFilters = (state: IState) => state.filters; 6 | export const selectSortBy = (state: IState) => state.sortBy; 7 | export const selectProducts = (state: IState) => state.catalog.items; 8 | export const selectProduct = (state: IState, props: any) => state.catalog.items.find((item) => item._id == props.match.params.id); 9 | 10 | export const sortProducts = (catalog: ICatalogProduct[], sortBy: TSortBy) => { 11 | switch (sortBy) { 12 | case 'Name: A-Z': 13 | return catalog.sort((a, b) => (a.info.name > b.info.name) ? 1 : ((b.info.name > a.info.name) ? -1 : 0)); 14 | case 'Name: Z-A': 15 | return catalog.sort((a, b) => (a.info.name < b.info.name) ? 1 : ((b.info.name < a.info.name) ? -1 : 0)); 16 | case 'Price: Low to High': 17 | return catalog.sort((a, b) => (a.info.price > b.info.price) ? 1 : ((b.info.price > a.info.price) ? -1 : 0)); 18 | case 'Price: High to Low': 19 | return catalog.sort((a, b) => (a.info.price < b.info.price) ? 1 : ((b.info.price < a.info.price) ? -1 : 0)); 20 | default: 21 | return catalog; 22 | } 23 | }; 24 | 25 | export const filterProducts = createSelector( 26 | [selectProducts, selectFilters], 27 | (catalog, { filters }) => { 28 | return catalog.filter((item) => { 29 | const priceRange = filters.priceRange.length ? filters.priceRange.includes(item.tags.priceRange) : item.tags.priceRange; 30 | const brand = filters.brand.length ? filters.brand.includes(item.tags.brand) : item.tags.brand; 31 | const color = filters.color.length ? filters.color.includes(item.tags.color) : item.tags.color; 32 | const os = filters.os.length ? filters.os.includes(item.tags.os) : item.tags.os; 33 | const internalMemory = filters.internalMemory.length ? filters.internalMemory.includes(item.tags.internalMemory) : item.tags.internalMemory; 34 | const ram = filters.ram.length ? filters.ram.includes(item.tags.ram) : item.tags.ram; 35 | const displaySize = filters.displaySize.length ? filters.displaySize.includes(item.tags.displaySize) : item.tags.displaySize; 36 | const displayResolution = filters.displayResolution.length ? filters.displayResolution.includes(item.tags.displayResolution) : item.tags.displayResolution; 37 | const camera = filters.camera.length ? filters.camera.includes(item.tags.camera) : item.tags.camera; 38 | const cpu = filters.cpu.length ? filters.cpu.includes(item.tags.cpu) : item.tags.cpu; 39 | 40 | return priceRange && brand && color && os && internalMemory && ram && displaySize && displayResolution && camera && cpu; 41 | }) 42 | } 43 | ); 44 | -------------------------------------------------------------------------------- /frontend/src/styles/ProductDetails.css: -------------------------------------------------------------------------------- 1 | .product-details-container { 2 | min-height: 100vh; 3 | margin: 0 6px; 4 | margin-top: -72px; 5 | border: 1px solid #ffffff00; 6 | } 7 | 8 | .product-details-container h1 { 9 | margin-top: 100px; 10 | margin-bottom: 20px; 11 | } 12 | 13 | .product-details { 14 | display: flex; 15 | flex-direction: row; 16 | } 17 | 18 | .product-image { 19 | width: 40%; 20 | } 21 | 22 | .product-image img { 23 | max-height: 364px; 24 | margin: 20px 0; 25 | } 26 | 27 | .product-info { 28 | width: 60%; 29 | text-align: center; 30 | } 31 | 32 | .product-info table { 33 | width: 100%; 34 | margin: 20px 0; 35 | } 36 | 37 | .product-info th { 38 | background-color: #00BCD4; 39 | color: white; 40 | } 41 | 42 | .product-info th, .product-info td { 43 | padding: 3px; 44 | text-align: left; 45 | border: 1px solid #80DEEA; 46 | } 47 | 48 | .price-text { 49 | font-size: 16px; 50 | font-weight: bold; 51 | } 52 | 53 | .price-num { 54 | font-size: 30px; 55 | font-weight: bold; 56 | color: #64DD17; 57 | } 58 | 59 | .product-handle { 60 | display: flex; 61 | flex-direction: row; 62 | margin: 10px 0 20px 0; 63 | } 64 | 65 | .product-handle .left { 66 | width: 40%; 67 | } 68 | 69 | .product-handle .left .btn { 70 | margin: 10px auto; 71 | } 72 | 73 | .product-handle .right { 74 | display: flex; 75 | flex-direction: row; 76 | justify-content: flex-start; 77 | width: 60%; 78 | } 79 | 80 | .right .price { 81 | width: 200px; 82 | margin: 5px 0; 83 | } 84 | 85 | .right .quantity { 86 | width: 130px; 87 | text-align: center; 88 | } 89 | 90 | .right .quantity input { 91 | width: 30px; 92 | height: 28px; 93 | margin: 10px; 94 | font-size: 17px; 95 | } 96 | 97 | .right .btn { 98 | width: 160px; 99 | margin: 10px 0; 100 | text-align: center; 101 | } 102 | 103 | @media (max-width: 1000px) { 104 | .product-details-container h1 { 105 | font-size: 24px; 106 | } 107 | 108 | .product-details { 109 | flex-direction: column; 110 | } 111 | 112 | .product-image { 113 | width: 100%; 114 | margin: 30px auto; 115 | text-align: center; 116 | } 117 | 118 | .product-image img { 119 | max-height: 300px; 120 | margin: auto; 121 | } 122 | 123 | .product-info { 124 | width: 100%; 125 | } 126 | } 127 | 128 | @media (max-width: 900px) { 129 | .product-handle { 130 | flex-direction: column; 131 | } 132 | 133 | .product-handle .left { 134 | width: 100%; 135 | order: 2; 136 | } 137 | 138 | .product-handle .right { 139 | width: 100%; 140 | order: 1; 141 | } 142 | } 143 | 144 | @media (max-width: 650px) { 145 | .product-handle .right { 146 | flex-direction: column; 147 | } 148 | 149 | .right .price, .right .quantity, .right .btn { 150 | width: 100%; 151 | } 152 | 153 | .right .price, .left { 154 | text-align: center; 155 | } 156 | } -------------------------------------------------------------------------------- /frontend/src/components/Products/Products.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import RaisedButton from 'material-ui/RaisedButton'; 3 | import Drawer from 'material-ui/Drawer'; 4 | import SelectField from 'material-ui/SelectField'; 5 | import MenuItem from 'material-ui/MenuItem'; 6 | import { ICatalogProduct } from '@typings/state/index'; 7 | import FiltersList from '../FiltersList'; 8 | import Product from '../Product'; 9 | import '@styles/Products.css'; 10 | 11 | export interface Props { 12 | catalogLoaded: boolean; 13 | catalog: ICatalogProduct[]; 14 | sortBy: string; 15 | initCatalog: () => void; 16 | clearFilters: () => void; 17 | setSortBy: (value: string) => void; 18 | } 19 | 20 | const Products = ({ sortBy, setSortBy, initCatalog, catalog, catalogLoaded, clearFilters }: Props) => { 21 | const [drawerOpen, setDrawerOpen] = useState(false); 22 | const [value, setValue] = useState(sortBy || 'Name: A-Z'); 23 | 24 | const handleChange = (e: React.ChangeEvent, index: number, value: string) => { 25 | setSortBy(value); 26 | setValue(value); 27 | } 28 | 29 | useEffect(() => { 30 | initCatalog(); 31 | }, []); 32 | 33 | if(!catalogLoaded) { 34 | return ( 35 |
36 | 37 |

LOADING PRODUCTS...

38 |
39 | ); 40 | } 41 | 42 | return ( 43 |
44 |
45 |
46 | Products found: {catalog.length} 47 |
48 |
49 |
50 | setDrawerOpen(!!drawerOpen)} 54 | primary={true} 55 | /> 56 |
57 | 63 |
64 |
65 | Sort By: 66 | 71 | 72 | 73 | 74 | 75 | 76 | setDrawerOpen(!!drawerOpen)} 81 | > 82 | 83 | 84 |
85 |
86 | {catalog.length ? 87 | catalog.map((item) => { 88 | return 89 | }) : 90 |

No products found.

} 91 |
92 | ); 93 | } 94 | 95 | export default Products; 96 | -------------------------------------------------------------------------------- /frontend/src/components/Account/Account.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import moment from 'moment'; 3 | import numeral from 'numeral'; 4 | import { IUser, IOrder } from '@typings/state/index'; 5 | import IconButton from '@material-ui/core/IconButton'; 6 | import EditIcon from '@material-ui/icons/Edit'; 7 | import Divider from '@material-ui/core/Divider'; 8 | import AccountModal from '../AccountModal'; 9 | import '@styles/Account.css'; 10 | 11 | export interface Props { 12 | user: IUser; 13 | getUser: () => void; 14 | editUser: (data: Record) => void; 15 | } 16 | 17 | const Account = ({ user, getUser }: Props) => { 18 | const [modalOpen, setModalOpen] = useState(false); 19 | 20 | useEffect(() => { 21 | getUser(); 22 | }, [modalOpen]); 23 | 24 | return ( 25 |
26 | {!user ? ( 27 |
28 | 29 |

LOADING ACCOUNT DATA...

30 |
31 | ) : ( 32 | <> 33 |

Your Account

34 |
35 |
36 |
37 |

Info

38 | setModalOpen(true)}> 42 | 43 | 44 |
45 | 46 |

Username: {user.username}

47 |

E-mail: {user.email}

48 |

Billing Address: {user.address}

49 |

Phone: {user.phone}

50 |
51 |
52 |

Order History

53 | 54 |
55 | {user.orders.length ? 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | {user.orders.map((order: IOrder) => ( 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | ))} 76 | 77 |
Date CreatedProduct NamePriceQtyTotal
{moment(order.dateCreated).format('ll')}{order.name}{numeral(order.price).format('$0,0.00')}{order.quantity}{numeral(parseInt(order.price) * parseInt(order.quantity)).format('$0,0.00')}
: 78 |

No order history.

79 | } 80 |
81 |
82 |
83 | setModalOpen(false)} 87 | /> 88 | 89 | )} 90 |
91 | ); 92 | }; 93 | 94 | export default Account; 95 | -------------------------------------------------------------------------------- /frontend/src/components/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import AppBar from 'material-ui/AppBar'; 4 | import FlatButton from 'material-ui/FlatButton'; 5 | import IconMenu from 'material-ui/IconMenu'; 6 | import IconButton from 'material-ui/IconButton'; 7 | import Person from 'material-ui/svg-icons/social/person'; 8 | import Menu from 'material-ui/svg-icons/navigation/menu'; 9 | import Logout from 'material-ui/svg-icons/navigation/subdirectory-arrow-left'; 10 | import ShoppingCart from '@material-ui/icons/ShoppingCart'; 11 | import Input from '@material-ui/icons/Input'; 12 | import { RouteComponentProps } from 'react-router-dom'; 13 | 14 | import { ILoggedUser } from '@typings/state/loggedUser'; 15 | import { ICart } from '@typings/state/cart'; 16 | import LoginModal from '../LoginModal'; 17 | import RegisterModal from '../RegisterModal'; 18 | 19 | import '@styles/Header.css'; 20 | 21 | export interface Props extends RouteComponentProps { 22 | loggedUser: ILoggedUser; 23 | cart: ICart; 24 | getUser: () => void; 25 | getUserSuccess: () => void; 26 | logoutSuccess: () => void; 27 | } 28 | 29 | const styles = { 30 | menuBtn: { 31 | color: '#fff' 32 | }, 33 | iconMenuBtn: { 34 | color: '#00BCD4', 35 | minWidth: '168px', 36 | textAlign: 'left' 37 | } 38 | } 39 | 40 | const Header = ({ history, loggedUser, getUser, getUserSuccess, logoutSuccess }: Props) => { 41 | const [activeModal, setActiveModal] = useState(null); 42 | 43 | useEffect(() => getUser(), []); 44 | 45 | const onLogout = () => { 46 | logoutSuccess(); 47 | history.push('/'); 48 | } 49 | 50 | return ( 51 |
52 | history.push('/')} 56 | showMenuIconButton={false} 57 | zDepth={0} 58 | iconElementRight={ 59 | loggedUser ? 60 |
61 |
62 | } 66 | containerElement={} 67 | /> 68 | } 72 | containerElement={} 73 | /> 74 | } 78 | containerElement={
} 79 | onClick={onLogout} 80 | /> 81 |
82 |
83 | } 85 | anchorOrigin={{ horizontal: 'right', vertical: 'top' }} 86 | targetOrigin={{ horizontal: 'right', vertical: 'top' }} 87 | iconStyle={{ color: '#fff' }} 88 | > 89 | } 92 | containerElement={} 93 | />
94 | } 97 | containerElement={} 98 | />
99 | } 102 | containerElement={
} 103 | onClick={onLogout} 104 | /> 105 | 106 |
107 |
: 108 | } 112 | onClick={() => setActiveModal('login')} 113 | /> 114 | } 115 | /> 116 | setActiveModal(null)} 119 | setActiveModal={setActiveModal} 120 | setUser={getUserSuccess} 121 | /> 122 | setActiveModal(null)} 125 | setActiveModal={setActiveModal} 126 | setUser={getUserSuccess} 127 | /> 128 |
129 | ); 130 | }; 131 | 132 | export default Header; 133 | -------------------------------------------------------------------------------- /frontend/src/components/ProductDetails/ProductDetails.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import numeral from 'numeral'; 4 | import RaisedButton from 'material-ui/RaisedButton'; 5 | import Snackbar from 'material-ui/Snackbar'; 6 | import AddShoppingCart from 'material-ui/svg-icons/action/add-shopping-cart'; 7 | import KeyboardArrowLeft from 'material-ui/svg-icons/hardware/keyboard-arrow-left'; 8 | import { IUser, ICatalogProduct } from '@typings/state/index'; 9 | import { createCart } from '@api/cart'; 10 | import '@styles/ProductDetails.css'; 11 | 12 | export interface Props { 13 | loggedUser: IUser; 14 | product: ICatalogProduct; 15 | } 16 | 17 | const ProductDetails = ({ loggedUser, product }: Props) => { 18 | const [quantity, setQuantity] = useState(1); 19 | const [snackbarOpen, setSnackbarOpen] = useState(false); 20 | 21 | const onQuantityChange = (e: React.ChangeEvent) => { 22 | let value = e.target.value; 23 | setQuantity(parseInt(value)); 24 | } 25 | 26 | const addToCart = async () => { 27 | loggedUser && await createCart({ 28 | user: loggedUser.id, 29 | product: product._id, 30 | quantity, 31 | }); 32 | 33 | setSnackbarOpen(true); 34 | } 35 | 36 | const { info } = product; 37 | 38 | return ( 39 |
40 |

{info.name}

41 |
42 |
43 | 44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 |
Model{info.name}
Dimensions{info.dimensions}
Weight{info.weight}
Display Type{info.displayType}
Display Size{info.displaySize}
Display Resolution{info.displayResolution}
OS{info.os}
CPU{info.cpu}
Internal Memory{info.internalMemory}
RAM{info.ram}
Camera{info.camera}
Batery{info.batery}
Color{info.color}
100 | 106 |
107 |
108 |
109 |
110 | } 112 | className="btn" 113 | label="Back to catalog" 114 | labelPosition="after" 115 | secondary={true} 116 | icon={} 117 | /> 118 |
119 |
120 |
121 | Price: 122 | {numeral(info.price).format('$0,0.00')} 123 |
124 |
125 | Quantity: 126 | 127 |
128 |
129 | } 135 | /> 136 |
137 |
138 |
139 |
140 | ); 141 | } 142 | 143 | export default ProductDetails; 144 | -------------------------------------------------------------------------------- /frontend/src/components/Cart/Cart.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import numeral from 'numeral'; 4 | import FlatButton from 'material-ui/FlatButton'; 5 | import RaisedButton from 'material-ui/RaisedButton'; 6 | import NavigateNext from 'material-ui/svg-icons/image/navigate-next'; 7 | import RemoveShoppingCart from '@material-ui/icons/RemoveShoppingCart'; 8 | import Dialog from 'material-ui/Dialog'; 9 | import Snackbar from 'material-ui/Snackbar'; 10 | import { editCart, deleteCart } from '@api/cart'; 11 | import { createOrder } from '@api/order'; 12 | import CheckoutModal from '../CheckoutModal'; 13 | import OrderSuccessModal from '../OrderSuccessModal'; 14 | import { ICart } from '@typings/state/index'; 15 | import { modal } from '@typings/modal'; 16 | import '@styles/Cart.css'; 17 | 18 | export interface Props { 19 | cart: ICart; 20 | getCart: () => ICart; 21 | } 22 | 23 | const Cart = ({ cart, getCart }: Props) => { 24 | const [activeModal, setActiveModal] = useState(null); 25 | 26 | const removeItem = async (itemId: string) => { 27 | await editCart({ 28 | cartId: cart._id, 29 | itemId: itemId 30 | }); 31 | 32 | getCart(); 33 | 34 | setActiveModal('snackbar'); 35 | setTimeout(() => { 36 | setActiveModal(null); 37 | }, 4000); 38 | } 39 | 40 | const emptyCart = async () => { 41 | await deleteCart({ id: cart._id }) 42 | await setActiveModal(null); 43 | await getCart(); 44 | } 45 | 46 | const makeOrder = async () => { 47 | const order = cart.items.map((item) => { 48 | let order = { 49 | name: item.product.info.name, 50 | price: item.product.info.price, 51 | quantity: item.quantity, 52 | dateCreated: Date.now() 53 | }; 54 | return order; 55 | }); 56 | 57 | await createOrder({ order }) 58 | await emptyCart(); 59 | 60 | setActiveModal('orderSuccess'); 61 | } 62 | 63 | useEffect(() => { 64 | getCart(); 65 | }, []); 66 | 67 | const cartExists = cart.isLoaded && !cart.error && cart.items.length; 68 | 69 | return ( 70 |
71 |

Your Cart

72 |
73 |
74 |
75 |

76 | Number of items: 77 | {cartExists ? cart.items.reduce((acc, item) => acc += item.quantity!, 0) : 0} 78 |

79 |

80 | Total amount: 81 | 82 | {cartExists ? numeral(cart.items.reduce((acc, item) => acc += item.product.info.price * item.quantity!, 0)).format('$0,0.00') : numeral(0).format('$0,0.00')} 83 | 84 |

85 |
86 |
87 | setActiveModal('checkout')} 89 | className="btn" 90 | label="Checkout" 91 | labelPosition="before" 92 | icon={} 93 | primary={true} 94 | disabled={!cartExists} 95 | /> 96 | setActiveModal('dialog')} 98 | className="btn" 99 | label="Empty cart" 100 | labelPosition="before" 101 | icon={} 102 | secondary={true} 103 | disabled={!cartExists} 104 | /> 105 |
106 | setActiveModal} 109 | setActiveModal={setActiveModal} 110 | makeOrder={makeOrder} 111 | /> 112 | {}} 116 | /> 117 | setActiveModal(null)} 124 | />, 125 | , 130 | ]} 131 | modal={true} 132 | open={activeModal === 'dialog'} 133 | > 134 | All items will be removed. 135 | 136 |
137 |
138 | {cartExists ? 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | {cart.items.map((item) => ( 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | ))} 161 | 162 |
Product NamePriceQtyTotal
{item.product.info.name}{numeral(item.product.info.price).format('$0,0.00')}{item.quantity}{numeral(item.product.info.price * item.quantity!).format('$0,0.00')}
: 163 |

No items in the cart.

164 | } 165 | 170 |
171 |
172 |
173 | ); 174 | }; 175 | 176 | export default Cart; 177 | -------------------------------------------------------------------------------- /frontend/src/components/FiltersList/FiltersList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { List, ListItem } from 'material-ui/List'; 3 | import Subheader from 'material-ui/Subheader'; 4 | import Checkbox from 'material-ui/Checkbox'; 5 | import { IFilters } from '@typings/state/index'; 6 | import '@styles/FiltersList.css'; 7 | 8 | export interface Props { 9 | filters: IFilters; 10 | setFilter: (name: string, value: string) => void; 11 | } 12 | 13 | const FiltersList = ({ filters, setFilter }: Props) => { 14 | const { checked } = filters; 15 | 16 | const handleCheck = (e: React.MouseEvent) => { 17 | setFilter(e.currentTarget.name, e.currentTarget.value); 18 | }; 19 | 20 | return ( 21 |
22 | 23 | Search by: 24 | , 31 | , 32 | , 33 | ')} onCheck={handleCheck} /> 34 | ]} 35 | /> 36 | , 43 | , 44 | , 45 | , 46 | , 47 | ]} 48 | /> 49 | , 56 | , 57 | 58 | ]} 59 | /> 60 | , 67 | 68 | ]} 69 | /> 70 | , 77 | , 78 | , 79 | 80 | ]} 81 | /> 82 | , 89 | , 90 | , 91 | 92 | ]} 93 | /> 94 | , 101 | , 102 | , 103 | , 104 | , 105 | 106 | ]} 107 | /> 108 | , 115 | , 116 | , 117 | , 118 | , 119 | 120 | ]} 121 | /> 122 | , 129 | , 130 | , 131 | 132 | ]} 133 | /> 134 | , 141 | , 142 | 143 | ]} 144 | /> 145 | 146 |
147 | ); 148 | }; 149 | 150 | export default FiltersList; 151 | -------------------------------------------------------------------------------- /db/init-db.d/seed.js: -------------------------------------------------------------------------------- 1 | db.createCollection('users'); 2 | db.createCollection('carts'); 3 | 4 | db.products.insertMany([ 5 | { 6 | info: { 7 | name: 'Apple iPhone 8 Plus', 8 | dimensions: '158.4 x 78.1 x 7.5 mm', 9 | weight: '202 g', 10 | displayType: 'LED-backlit IPS LCD, capacitive touchscreen, 16M colors', 11 | displaySize: '5.5"', 12 | displayResolution: '1080 x 1920 pixels', 13 | os: 'iOS 11', 14 | cpu: 'Hexa-core (2x Monsoon + 4x Mistral)', 15 | internalMemory: '256 GB', 16 | ram: '3 GB', 17 | camera: 'Dual: 12 MP (f/1.8, 28mm, OIS) + 12 MP (f/2.8, 57mm)', 18 | batery: 'Non-removable Li-Ion 2691 mAh battery (10.28 Wh)', 19 | color: 'White', 20 | price: 700, 21 | photo: '/img/apple_iphone_8_plus.jpg' 22 | }, 23 | tags: { 24 | priceRange: '500-750', 25 | brand: 'apple', 26 | color: 'white', 27 | os: 'ios', 28 | internalMemory: '256', 29 | ram: '3', 30 | displaySize: '5.5', 31 | displayResolution: '1080x1920', 32 | camera: '12', 33 | cpu: 'hexa_core' 34 | } 35 | }, 36 | { 37 | info: { 38 | name: 'Apple iPhone X', 39 | dimensions: '143.6 x 70.9 x 7.7 mm', 40 | weight: '174 g', 41 | displayType: 'Super AMOLED capacitive touchscreen, 16M colors', 42 | displaySize: '5.8"', 43 | displayResolution: '1125 x 2436 pixels', 44 | os: 'iOS 11.1.1', 45 | cpu: 'Hexa-core 2.39 GHz (2x Monsoon + 4x Mistral)', 46 | internalMemory: '256 GB', 47 | ram: '3 GB', 48 | camera: 'Dual: 12 MP (f/1.8, 28mm) + 12 MP (f/2.4, 52mm)', 49 | batery: 'Non-removable Li-Ion 2716 mAh battery (10.35 Wh)', 50 | color: 'Black', 51 | price: 950, 52 | photo: '/img/apple_iphone_x.jpg' 53 | }, 54 | tags: { 55 | priceRange: '750>', 56 | brand: 'apple', 57 | color: 'black', 58 | os: 'ios', 59 | internalMemory: '256', 60 | ram: '3', 61 | displaySize: '5.8', 62 | displayResolution: '1125x2436', 63 | camera: '12', 64 | cpu: 'hexa_core' 65 | } 66 | }, 67 | { 68 | info: { 69 | name: 'HTC U11', 70 | dimensions: '153.9 x 75.9 x 7.9 mm', 71 | weight: '169 g', 72 | displayType: 'Super LCD5 capacitive touchscreen, 16M colors', 73 | displaySize: '5.5"', 74 | displayResolution: '1440 x 2560 pixels', 75 | os: 'Android 7.1 (Nougat)', 76 | cpu: 'Octa-core (4x2.45 GHz Kryo & 4x1.9 GHz Kryo)', 77 | internalMemory: '128 GB', 78 | ram: '6 GB', 79 | camera: '12 MP (f/1.7, 1.4 µm, Dual Pixel PDAF, 5-axis OIS)', 80 | batery: 'Non-removable Li-Ion 3000 mAh battery', 81 | color: 'Ice White', 82 | price: 450, 83 | photo: '/img/htc_u11.jpg' 84 | }, 85 | tags: { 86 | priceRange: '250-500', 87 | brand: 'htc', 88 | color: 'white', 89 | os: 'android', 90 | internalMemory: '128', 91 | ram: '6', 92 | displaySize: '5.5', 93 | displayResolution: '1440x2560', 94 | camera: '12', 95 | cpu: 'octa_core' 96 | } 97 | }, 98 | { 99 | info: { 100 | name: 'Huawei Mate 10 Pro', 101 | dimensions: '154.2 x 74.5 x 7.9 mm', 102 | weight: '178 g', 103 | displayType: 'AMOLED capacitive touchscreen, 16M colors', 104 | displaySize: '6.0"', 105 | displayResolution: '1080 x 1920 pixels', 106 | os: 'Android 8.0 (Oreo)', 107 | cpu: 'Octa-core (4x2.4 GHz Cortex-A73 & 4x1.8 GHz Cortex-A53)', 108 | internalMemory: '128 GB', 109 | ram: '6 GB', 110 | camera: 'Dual: 12 MP (f/1.6, 27mm, OIS) +20 MP (f/1.6, 27mm)', 111 | batery: 'Non-removable Li-Po 4000 mAh battery', 112 | color: 'Titanium Gray', 113 | price: 800, 114 | photo: '/img/huawei_mate_10_pro.jpg' 115 | }, 116 | tags: { 117 | priceRange: '750>', 118 | brand: 'huawei', 119 | color: 'grey', 120 | os: 'android', 121 | internalMemory: '128', 122 | ram: '6', 123 | displaySize: '6.0', 124 | displayResolution: '1080x1920', 125 | camera: '12', 126 | cpu: 'octa_core' 127 | } 128 | }, 129 | { 130 | info: { 131 | name: 'Huawei P10', 132 | dimensions: '145.3 x 69.3 x 7 mm', 133 | weight: '145 g', 134 | displayType: 'IPS-NEO LCD capacitive touchscreen, 16M colors', 135 | displaySize: '5.1"', 136 | displayResolution: '1080 x 1920 pixels', 137 | os: 'Android 7.0 (Nougat)', 138 | cpu: 'Octa-core (4x2.4 GHz Cortex-A73 & 4x1.8 GHz Cortex-A53)', 139 | internalMemory: '64 GB', 140 | ram: '4 GB', 141 | camera: 'Dual: 12 MP (f/2.2, PDAF, OIS) + 20 MP', 142 | batery: 'Non-removable Li-Ion 3200 mAh battery', 143 | color: 'Mystic Silver', 144 | price: 680, 145 | photo: '/img/huawei_p10.jpg' 146 | }, 147 | tags: { 148 | priceRange: '500-750', 149 | brand: 'huawei', 150 | color: 'grey', 151 | os: 'android', 152 | internalMemory: '64', 153 | ram: '4', 154 | displaySize: '5.1', 155 | displayResolution: '1080x1920', 156 | camera: '12', 157 | cpu: 'octa_core' 158 | } 159 | }, 160 | { 161 | info: { 162 | name: 'LG G6', 163 | dimensions: '148.9 x 71.9 x 7.9 mm', 164 | weight: '163 g', 165 | displayType: 'IPS LCD capacitive touchscreen, 16M colors', 166 | displaySize: '5.8"', 167 | displayResolution: '1440 x 2880 pixels', 168 | os: 'Android 7.0 (Nougat)', 169 | cpu: 'Quad-core (2x2.35 GHz Kryo & 2x1.6 GHz Kryo)', 170 | internalMemory: '128 GB', 171 | ram: '4 GB', 172 | camera: 'Dual: 13 MP (f/1.8, 1/3", 1.12 µm, 3-axis OIS, PDAF) + 13 MP (f/2.4, no AF)', 173 | batery: 'Non-removable Li-Po 3300 mAh battery', 174 | color: 'Ice Platinum', 175 | price: 800, 176 | photo: '/img/lg_g6.jpg' 177 | }, 178 | tags: { 179 | priceRange: '750>', 180 | brand: 'lg', 181 | color: 'grey', 182 | os: 'android', 183 | internalMemory: '128', 184 | ram: '4', 185 | displaySize: '5.8', 186 | displayResolution: '1440x2880', 187 | camera: '13', 188 | cpu: 'quad_core' 189 | } 190 | }, 191 | { 192 | info: { 193 | name: 'LG V30', 194 | dimensions: '151.7 x 75.4 x 7.3 mm', 195 | weight: '158 g', 196 | displayType: 'P-OLED capacitive touchscreen, 16M colors', 197 | displaySize: '6.0"', 198 | displayResolution: '1440 x 2880 pixels', 199 | os: 'Android 7.1.2 (Nougat)', 200 | cpu: 'Octa-core (4x2.45 GHz Kryo & 4x1.9 GHz Kryo)', 201 | internalMemory: '64 GB', 202 | ram: '4 GB', 203 | camera: 'Dual: 16 MP (f/1.6, 1 µm, 3-axis OIS, PDAF) + 13 MP (f/1.9, no AF)', 204 | batery: 'Non-removable Li-Po 3300 mAh battery', 205 | color: 'Aurora Black', 206 | price: 800, 207 | photo: '/img/lg_v30.jpg' 208 | }, 209 | tags: { 210 | priceRange: '750>', 211 | brand: 'lg', 212 | color: 'black', 213 | os: 'android', 214 | internalMemory: '64', 215 | ram: '4', 216 | displaySize: '6.0', 217 | displayResolution: '1440x2880', 218 | camera: '16', 219 | cpu: 'octa_core' 220 | } 221 | }, 222 | { 223 | info: { 224 | name: 'Samsung Galaxy A3', 225 | dimensions: '130.1 x 65.5 x 6.9 mm', 226 | weight: '110.3 g', 227 | displayType: 'Super AMOLED capacitive touchscreen, 16M colors', 228 | displaySize: '4.5"', 229 | displayResolution: '540 x 960 pixels', 230 | os: 'Android 4.4.4 (KitKat)', 231 | cpu: 'Quad-core 1.2 GHz Cortex-A53', 232 | internalMemory: '16 GB', 233 | ram: '1 GB', 234 | camera: '8 MP (f/2.4, 31mm), autofocus, LED flash', 235 | batery: 'Non-removable Li-Ion 1900 mAh battery', 236 | color: 'Silver', 237 | price: 150, 238 | photo: '/img/samsung_galaxy_a3.JPG' 239 | }, 240 | tags: { 241 | priceRange: '<250', 242 | brand: 'samsung', 243 | color: 'grey', 244 | os: 'android', 245 | internalMemory: '16', 246 | ram: '1', 247 | displaySize: '4.5', 248 | displayResolution: '540x960', 249 | camera: '8', 250 | cpu: 'quad_core' 251 | } 252 | }, 253 | { 254 | info: { 255 | name: 'Samsung Galaxy Note 8', 256 | dimensions: '162.5 x 74.8 x 8.6 mm', 257 | weight: '195.3 g', 258 | displayType: 'Super AMOLED capacitive touchscreen, 16M colors', 259 | displaySize: '6.3"', 260 | displayResolution: '1440 x 2960 pixels', 261 | os: 'Android 7.1.1 (Nougat)', 262 | cpu: 'Octa-core (4x2.3 GHz & 4x1.7 GHz) - EMEA', 263 | internalMemory: '256 GB', 264 | ram: '6 GB', 265 | camera: 'Dual: 12 MP (f/1.7, 26mm, 1/2.5", 1.4 µm) + 12MP (f/2.4, 52mm, 1/3.6", 1 µm)', 266 | batery: 'Non-removable Li-Ion 3300 mAh battery', 267 | color: 'Midnight Black', 268 | price: 800, 269 | photo: '/img/samsung_galaxy_note_8.jpg' 270 | }, 271 | tags: { 272 | priceRange: '750>', 273 | brand: 'samsung', 274 | color: 'black', 275 | os: 'android', 276 | internalMemory: '256', 277 | ram: '6', 278 | displaySize: '6.3', 279 | displayResolution: '1440x2960', 280 | camera: '12', 281 | cpu: 'octa_core' 282 | } 283 | }, 284 | { 285 | info: { 286 | name: 'Samsung Galaxy S8', 287 | dimensions: '148.9 x 68.1 x 8 mm', 288 | weight: '155 g', 289 | displayType: 'Super AMOLED capacitive touchscreen, 16M colors', 290 | displaySize: '5.8"', 291 | displayResolution: '1440 x 2960 pixels', 292 | os: 'Android 7.0 (Nougat)', 293 | cpu: 'Octa-core (4x2.3 GHz & 4x1.7 GHz) - EMEA', 294 | internalMemory: '64 GB', 295 | ram: '4 GB', 296 | camera: '12 MP (f/1.7, 26mm, 1/2.5", 1.4 µm, Dual Pixel PDAF), phase detection autofocus, OIS', 297 | batery: 'Non-removable Li-Ion 3000 mAh battery', 298 | color: 'Midnight Black', 299 | price: 720, 300 | photo: '/img/samsung_galaxy_s8.jpg' 301 | }, 302 | tags: { 303 | priceRange: '500-750', 304 | brand: 'samsung', 305 | color: 'black', 306 | os: 'android', 307 | internalMemory: '64', 308 | ram: '4', 309 | displaySize: '5.8', 310 | displayResolution: '1440x2960', 311 | camera: '12', 312 | cpu: 'octa_core' 313 | } 314 | } 315 | ]); 316 | -------------------------------------------------------------------------------- /backend/seeds/products.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Product = require('../models/Product'); 3 | 4 | const products = [ 5 | { 6 | info: { 7 | name: 'Apple iPhone 8 Plus', 8 | dimensions: '158.4 x 78.1 x 7.5 mm', 9 | weight: '202 g', 10 | displayType: 'LED-backlit IPS LCD, capacitive touchscreen, 16M colors', 11 | displaySize: '5.5"', 12 | displayResolution: '1080 x 1920 pixels', 13 | os: 'iOS 11', 14 | cpu: 'Hexa-core (2x Monsoon + 4x Mistral)', 15 | internalMemory: '256 GB', 16 | ram: '3 GB', 17 | camera: 'Dual: 12 MP (f/1.8, 28mm, OIS) + 12 MP (f/2.8, 57mm)', 18 | batery: 'Non-removable Li-Ion 2691 mAh battery (10.28 Wh)', 19 | color: 'White', 20 | price: 700, 21 | photo: '/img/apple_iphone_8_plus.jpg' 22 | }, 23 | tags: { 24 | priceRange: '500-750', 25 | brand: 'apple', 26 | color: 'white', 27 | os: 'ios', 28 | internalMemory: '256', 29 | ram: '3', 30 | displaySize: '5.5', 31 | displayResolution: '1080x1920', 32 | camera: '12', 33 | cpu: 'hexa_core' 34 | } 35 | }, 36 | { 37 | info: { 38 | name: 'Apple iPhone X', 39 | dimensions: '143.6 x 70.9 x 7.7 mm', 40 | weight: '174 g', 41 | displayType: 'Super AMOLED capacitive touchscreen, 16M colors', 42 | displaySize: '5.8"', 43 | displayResolution: '1125 x 2436 pixels', 44 | os: 'iOS 11.1.1', 45 | cpu: 'Hexa-core 2.39 GHz (2x Monsoon + 4x Mistral)', 46 | internalMemory: '256 GB', 47 | ram: '3 GB', 48 | camera: 'Dual: 12 MP (f/1.8, 28mm) + 12 MP (f/2.4, 52mm)', 49 | batery: 'Non-removable Li-Ion 2716 mAh battery (10.35 Wh)', 50 | color: 'Black', 51 | price: 950, 52 | photo: '/img/apple_iphone_x.jpg' 53 | }, 54 | tags: { 55 | priceRange: '750>', 56 | brand: 'apple', 57 | color: 'black', 58 | os: 'ios', 59 | internalMemory: '256', 60 | ram: '3', 61 | displaySize: '5.8', 62 | displayResolution: '1125x2436', 63 | camera: '12', 64 | cpu: 'hexa_core' 65 | } 66 | }, 67 | { 68 | info: { 69 | name: 'HTC U11', 70 | dimensions: '153.9 x 75.9 x 7.9 mm', 71 | weight: '169 g', 72 | displayType: 'Super LCD5 capacitive touchscreen, 16M colors', 73 | displaySize: '5.5"', 74 | displayResolution: '1440 x 2560 pixels', 75 | os: 'Android 7.1 (Nougat)', 76 | cpu: 'Octa-core (4x2.45 GHz Kryo & 4x1.9 GHz Kryo)', 77 | internalMemory: '128 GB', 78 | ram: '6 GB', 79 | camera: '12 MP (f/1.7, 1.4 µm, Dual Pixel PDAF, 5-axis OIS)', 80 | batery: 'Non-removable Li-Ion 3000 mAh battery', 81 | color: 'Ice White', 82 | price: 450, 83 | photo: '/img/htc_u11.jpg' 84 | }, 85 | tags: { 86 | priceRange: '250-500', 87 | brand: 'htc', 88 | color: 'white', 89 | os: 'android', 90 | internalMemory: '128', 91 | ram: '6', 92 | displaySize: '5.5', 93 | displayResolution: '1440x2560', 94 | camera: '12', 95 | cpu: 'octa_core' 96 | } 97 | }, 98 | { 99 | info: { 100 | name: 'Huawei Mate 10 Pro', 101 | dimensions: '154.2 x 74.5 x 7.9 mm', 102 | weight: '178 g', 103 | displayType: 'AMOLED capacitive touchscreen, 16M colors', 104 | displaySize: '6.0"', 105 | displayResolution: '1080 x 1920 pixels', 106 | os: 'Android 8.0 (Oreo)', 107 | cpu: 'Octa-core (4x2.4 GHz Cortex-A73 & 4x1.8 GHz Cortex-A53)', 108 | internalMemory: '128 GB', 109 | ram: '6 GB', 110 | camera: 'Dual: 12 MP (f/1.6, 27mm, OIS) +20 MP (f/1.6, 27mm)', 111 | batery: 'Non-removable Li-Po 4000 mAh battery', 112 | color: 'Titanium Gray', 113 | price: 800, 114 | photo: '/img/huawei_mate_10_pro.jpg' 115 | }, 116 | tags: { 117 | priceRange: '750>', 118 | brand: 'huawei', 119 | color: 'grey', 120 | os: 'android', 121 | internalMemory: '128', 122 | ram: '6', 123 | displaySize: '6.0', 124 | displayResolution: '1080x1920', 125 | camera: '12', 126 | cpu: 'octa_core' 127 | } 128 | }, 129 | { 130 | info: { 131 | name: 'Huawei P10', 132 | dimensions: '145.3 x 69.3 x 7 mm', 133 | weight: '145 g', 134 | displayType: 'IPS-NEO LCD capacitive touchscreen, 16M colors', 135 | displaySize: '5.1"', 136 | displayResolution: '1080 x 1920 pixels', 137 | os: 'Android 7.0 (Nougat)', 138 | cpu: 'Octa-core (4x2.4 GHz Cortex-A73 & 4x1.8 GHz Cortex-A53)', 139 | internalMemory: '64 GB', 140 | ram: '4 GB', 141 | camera: 'Dual: 12 MP (f/2.2, PDAF, OIS) + 20 MP', 142 | batery: 'Non-removable Li-Ion 3200 mAh battery', 143 | color: 'Mystic Silver', 144 | price: 680, 145 | photo: '/img/huawei_p10.jpg' 146 | }, 147 | tags: { 148 | priceRange: '500-750', 149 | brand: 'huawei', 150 | color: 'grey', 151 | os: 'android', 152 | internalMemory: '64', 153 | ram: '4', 154 | displaySize: '5.1', 155 | displayResolution: '1080x1920', 156 | camera: '12', 157 | cpu: 'octa_core' 158 | } 159 | }, 160 | { 161 | info: { 162 | name: 'LG G6', 163 | dimensions: '148.9 x 71.9 x 7.9 mm', 164 | weight: '163 g', 165 | displayType: 'IPS LCD capacitive touchscreen, 16M colors', 166 | displaySize: '5.8"', 167 | displayResolution: '1440 x 2880 pixels', 168 | os: 'Android 7.0 (Nougat)', 169 | cpu: 'Quad-core (2x2.35 GHz Kryo & 2x1.6 GHz Kryo)', 170 | internalMemory: '128 GB', 171 | ram: '4 GB', 172 | camera: 'Dual: 13 MP (f/1.8, 1/3", 1.12 µm, 3-axis OIS, PDAF) + 13 MP (f/2.4, no AF)', 173 | batery: 'Non-removable Li-Po 3300 mAh battery', 174 | color: 'Ice Platinum', 175 | price: 800, 176 | photo: '/img/lg_g6.jpg' 177 | }, 178 | tags: { 179 | priceRange: '750>', 180 | brand: 'lg', 181 | color: 'grey', 182 | os: 'android', 183 | internalMemory: '128', 184 | ram: '4', 185 | displaySize: '5.8', 186 | displayResolution: '1440x2880', 187 | camera: '13', 188 | cpu: 'quad_core' 189 | } 190 | }, 191 | { 192 | info: { 193 | name: 'LG V30', 194 | dimensions: '151.7 x 75.4 x 7.3 mm', 195 | weight: '158 g', 196 | displayType: 'P-OLED capacitive touchscreen, 16M colors', 197 | displaySize: '6.0"', 198 | displayResolution: '1440 x 2880 pixels', 199 | os: 'Android 7.1.2 (Nougat)', 200 | cpu: 'Octa-core (4x2.45 GHz Kryo & 4x1.9 GHz Kryo)', 201 | internalMemory: '64 GB', 202 | ram: '4 GB', 203 | camera: 'Dual: 16 MP (f/1.6, 1 µm, 3-axis OIS, PDAF) + 13 MP (f/1.9, no AF)', 204 | batery: 'Non-removable Li-Po 3300 mAh battery', 205 | color: 'Aurora Black', 206 | price: 800, 207 | photo: '/img/lg_v30.jpg' 208 | }, 209 | tags: { 210 | priceRange: '750>', 211 | brand: 'lg', 212 | color: 'black', 213 | os: 'android', 214 | internalMemory: '64', 215 | ram: '4', 216 | displaySize: '6.0', 217 | displayResolution: '1440x2880', 218 | camera: '16', 219 | cpu: 'octa_core' 220 | } 221 | }, 222 | { 223 | info: { 224 | name: 'Samsung Galaxy A3', 225 | dimensions: '130.1 x 65.5 x 6.9 mm', 226 | weight: '110.3 g', 227 | displayType: 'Super AMOLED capacitive touchscreen, 16M colors', 228 | displaySize: '4.5"', 229 | displayResolution: '540 x 960 pixels', 230 | os: 'Android 4.4.4 (KitKat)', 231 | cpu: 'Quad-core 1.2 GHz Cortex-A53', 232 | internalMemory: '16 GB', 233 | ram: '1 GB', 234 | camera: '8 MP (f/2.4, 31mm), autofocus, LED flash', 235 | batery: 'Non-removable Li-Ion 1900 mAh battery', 236 | color: 'Silver', 237 | price: 150, 238 | photo: '/img/samsung_galaxy_a3.JPG' 239 | }, 240 | tags: { 241 | priceRange: '<250', 242 | brand: 'samsung', 243 | color: 'grey', 244 | os: 'android', 245 | internalMemory: '16', 246 | ram: '1', 247 | displaySize: '4.5', 248 | displayResolution: '540x960', 249 | camera: '8', 250 | cpu: 'quad_core' 251 | } 252 | }, 253 | { 254 | info: { 255 | name: 'Samsung Galaxy Note 8', 256 | dimensions: '162.5 x 74.8 x 8.6 mm', 257 | weight: '195.3 g', 258 | displayType: 'Super AMOLED capacitive touchscreen, 16M colors', 259 | displaySize: '6.3"', 260 | displayResolution: '1440 x 2960 pixels', 261 | os: 'Android 7.1.1 (Nougat)', 262 | cpu: 'Octa-core (4x2.3 GHz & 4x1.7 GHz) - EMEA', 263 | internalMemory: '256 GB', 264 | ram: '6 GB', 265 | camera: 'Dual: 12 MP (f/1.7, 26mm, 1/2.5", 1.4 µm) + 12MP (f/2.4, 52mm, 1/3.6", 1 µm)', 266 | batery: 'Non-removable Li-Ion 3300 mAh battery', 267 | color: 'Midnight Black', 268 | price: 800, 269 | photo: '/img/samsung_galaxy_note_8.jpg' 270 | }, 271 | tags: { 272 | priceRange: '750>', 273 | brand: 'samsung', 274 | color: 'black', 275 | os: 'android', 276 | internalMemory: '256', 277 | ram: '6', 278 | displaySize: '6.3', 279 | displayResolution: '1440x2960', 280 | camera: '12', 281 | cpu: 'octa_core' 282 | } 283 | }, 284 | { 285 | info: { 286 | name: 'Samsung Galaxy S8', 287 | dimensions: '148.9 x 68.1 x 8 mm', 288 | weight: '155 g', 289 | displayType: 'Super AMOLED capacitive touchscreen, 16M colors', 290 | displaySize: '5.8"', 291 | displayResolution: '1440 x 2960 pixels', 292 | os: 'Android 7.0 (Nougat)', 293 | cpu: 'Octa-core (4x2.3 GHz & 4x1.7 GHz) - EMEA', 294 | internalMemory: '64 GB', 295 | ram: '4 GB', 296 | camera: '12 MP (f/1.7, 26mm, 1/2.5", 1.4 µm, Dual Pixel PDAF), phase detection autofocus, OIS', 297 | batery: 'Non-removable Li-Ion 3000 mAh battery', 298 | color: 'Midnight Black', 299 | price: 720, 300 | photo: '/img/samsung_galaxy_s8.jpg' 301 | }, 302 | tags: { 303 | priceRange: '500-750', 304 | brand: 'samsung', 305 | color: 'black', 306 | os: 'android', 307 | internalMemory: '64', 308 | ram: '4', 309 | displaySize: '5.8', 310 | displayResolution: '1440x2960', 311 | camera: '12', 312 | cpu: 'octa_core' 313 | } 314 | } 315 | ]; 316 | 317 | const seedProducts = () => { 318 | Product.remove({}, (err) => { 319 | if(err) { 320 | console.log(err); 321 | } 322 | console.log('PRODUCTS REMOVED'); 323 | products.forEach((product) => { 324 | Product.create(product, (err, createdProduct) => { 325 | if(err) { 326 | console.log(err); 327 | } else { 328 | console.log('PRODUCT CREATED'); 329 | createdProduct.save(); 330 | } 331 | }) 332 | }) 333 | }) 334 | } 335 | 336 | module.exports = seedProducts; -------------------------------------------------------------------------------- /backend/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fullstack-shopping-cart", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/bson": { 8 | "version": "4.0.3", 9 | "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.3.tgz", 10 | "integrity": "sha512-mVRvYnTOZJz3ccpxhr3wgxVmSeiYinW+zlzQz3SXWaJmD1DuL05Jeq7nKw3SnbKmbleW5qrLG5vdyWe/A9sXhw==", 11 | "requires": { 12 | "@types/node": "*" 13 | } 14 | }, 15 | "@types/mongodb": { 16 | "version": "3.6.3", 17 | "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.6.3.tgz", 18 | "integrity": "sha512-6YNqGP1hk5bjUFaim+QoFFuI61WjHiHE1BNeB41TA00Xd2K7zG4lcWyLLq/XtIp36uMavvS5hoAUJ+1u/GcX2Q==", 19 | "requires": { 20 | "@types/bson": "*", 21 | "@types/node": "*" 22 | } 23 | }, 24 | "@types/node": { 25 | "version": "14.14.17", 26 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.17.tgz", 27 | "integrity": "sha512-G0lD1/7qD60TJ/mZmhog76k7NcpLWkPVGgzkRy3CTlnFu4LUQh5v2Wa661z6vnXmD8EQrnALUyf0VRtrACYztw==" 28 | }, 29 | "abbrev": { 30 | "version": "1.1.1", 31 | "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", 32 | "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" 33 | }, 34 | "accepts": { 35 | "version": "1.3.4", 36 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", 37 | "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=", 38 | "requires": { 39 | "mime-types": "~2.1.16", 40 | "negotiator": "0.6.1" 41 | } 42 | }, 43 | "ansi-regex": { 44 | "version": "0.2.1", 45 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", 46 | "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=" 47 | }, 48 | "ansi-styles": { 49 | "version": "1.1.0", 50 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", 51 | "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=" 52 | }, 53 | "aproba": { 54 | "version": "1.2.0", 55 | "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", 56 | "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" 57 | }, 58 | "are-we-there-yet": { 59 | "version": "1.1.5", 60 | "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", 61 | "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", 62 | "requires": { 63 | "delegates": "^1.0.0", 64 | "readable-stream": "^2.0.6" 65 | } 66 | }, 67 | "array-flatten": { 68 | "version": "1.1.1", 69 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 70 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 71 | }, 72 | "balanced-match": { 73 | "version": "1.0.0", 74 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 75 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" 76 | }, 77 | "bcrypt": { 78 | "version": "5.0.0", 79 | "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.0.tgz", 80 | "integrity": "sha512-jB0yCBl4W/kVHM2whjfyqnxTmOHkCX4kHEa5nYKSoGeYe8YrjTYTc87/6bwt1g8cmV0QrbhKriETg9jWtcREhg==", 81 | "requires": { 82 | "node-addon-api": "^3.0.0", 83 | "node-pre-gyp": "0.15.0" 84 | } 85 | }, 86 | "bl": { 87 | "version": "2.2.1", 88 | "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz", 89 | "integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==", 90 | "requires": { 91 | "readable-stream": "^2.3.5", 92 | "safe-buffer": "^5.1.1" 93 | } 94 | }, 95 | "bluebird": { 96 | "version": "3.5.1", 97 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", 98 | "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" 99 | }, 100 | "body-parser": { 101 | "version": "1.18.2", 102 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", 103 | "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", 104 | "requires": { 105 | "bytes": "3.0.0", 106 | "content-type": "~1.0.4", 107 | "debug": "2.6.9", 108 | "depd": "~1.1.1", 109 | "http-errors": "~1.6.2", 110 | "iconv-lite": "0.4.19", 111 | "on-finished": "~2.3.0", 112 | "qs": "6.5.1", 113 | "raw-body": "2.3.2", 114 | "type-is": "~1.6.15" 115 | } 116 | }, 117 | "brace-expansion": { 118 | "version": "1.1.11", 119 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 120 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 121 | "requires": { 122 | "balanced-match": "^1.0.0", 123 | "concat-map": "0.0.1" 124 | } 125 | }, 126 | "bson": { 127 | "version": "1.1.5", 128 | "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.5.tgz", 129 | "integrity": "sha512-kDuEzldR21lHciPQAIulLs1LZlCXdLziXI6Mb/TDkwXhb//UORJNPXgcRs2CuO4H0DcMkpfT3/ySsP3unoZjBg==" 130 | }, 131 | "buffer-equal-constant-time": { 132 | "version": "1.0.1", 133 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", 134 | "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" 135 | }, 136 | "bytes": { 137 | "version": "3.0.0", 138 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", 139 | "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" 140 | }, 141 | "chalk": { 142 | "version": "0.5.1", 143 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", 144 | "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=", 145 | "requires": { 146 | "ansi-styles": "^1.1.0", 147 | "escape-string-regexp": "^1.0.0", 148 | "has-ansi": "^0.1.0", 149 | "strip-ansi": "^0.3.0", 150 | "supports-color": "^0.2.0" 151 | }, 152 | "dependencies": { 153 | "supports-color": { 154 | "version": "0.2.0", 155 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", 156 | "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=" 157 | } 158 | } 159 | }, 160 | "chownr": { 161 | "version": "1.1.4", 162 | "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", 163 | "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" 164 | }, 165 | "code-point-at": { 166 | "version": "1.1.0", 167 | "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", 168 | "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" 169 | }, 170 | "commander": { 171 | "version": "2.6.0", 172 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.6.0.tgz", 173 | "integrity": "sha1-nfflL7Kgyw+4kFjugMMQQiXzfh0=" 174 | }, 175 | "concat-map": { 176 | "version": "0.0.1", 177 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 178 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 179 | }, 180 | "concurrently": { 181 | "version": "3.5.1", 182 | "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-3.5.1.tgz", 183 | "integrity": "sha512-689HrwGw8Rbk1xtV9C4dY6TPJAvIYZbRbnKSAtfJ7tHqICFGoZ0PCWYjxfmerRyxBG0o3sbG3pe7N8vqPwIHuQ==", 184 | "requires": { 185 | "chalk": "0.5.1", 186 | "commander": "2.6.0", 187 | "date-fns": "^1.23.0", 188 | "lodash": "^4.5.1", 189 | "rx": "2.3.24", 190 | "spawn-command": "^0.0.2-1", 191 | "supports-color": "^3.2.3", 192 | "tree-kill": "^1.1.0" 193 | } 194 | }, 195 | "console-control-strings": { 196 | "version": "1.1.0", 197 | "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", 198 | "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" 199 | }, 200 | "content-disposition": { 201 | "version": "0.5.2", 202 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", 203 | "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" 204 | }, 205 | "content-type": { 206 | "version": "1.0.4", 207 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 208 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 209 | }, 210 | "cookie": { 211 | "version": "0.3.1", 212 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", 213 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" 214 | }, 215 | "cookie-signature": { 216 | "version": "1.0.6", 217 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 218 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 219 | }, 220 | "core-util-is": { 221 | "version": "1.0.2", 222 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 223 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 224 | }, 225 | "cors": { 226 | "version": "2.8.5", 227 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 228 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 229 | "requires": { 230 | "object-assign": "^4", 231 | "vary": "^1" 232 | } 233 | }, 234 | "date-fns": { 235 | "version": "1.29.0", 236 | "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.29.0.tgz", 237 | "integrity": "sha512-lbTXWZ6M20cWH8N9S6afb0SBm6tMk+uUg6z3MqHPKE9atmsY3kJkTm8vKe93izJ2B2+q5MV990sM2CHgtAZaOw==" 238 | }, 239 | "debug": { 240 | "version": "2.6.9", 241 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 242 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 243 | "requires": { 244 | "ms": "2.0.0" 245 | } 246 | }, 247 | "deep-extend": { 248 | "version": "0.6.0", 249 | "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", 250 | "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" 251 | }, 252 | "delegates": { 253 | "version": "1.0.0", 254 | "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", 255 | "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" 256 | }, 257 | "denque": { 258 | "version": "1.4.1", 259 | "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz", 260 | "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==" 261 | }, 262 | "depd": { 263 | "version": "1.1.2", 264 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 265 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 266 | }, 267 | "destroy": { 268 | "version": "1.0.4", 269 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 270 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 271 | }, 272 | "detect-libc": { 273 | "version": "1.0.3", 274 | "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", 275 | "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" 276 | }, 277 | "dotenv": { 278 | "version": "8.2.0", 279 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", 280 | "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" 281 | }, 282 | "ecdsa-sig-formatter": { 283 | "version": "1.0.11", 284 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", 285 | "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", 286 | "requires": { 287 | "safe-buffer": "^5.0.1" 288 | } 289 | }, 290 | "ee-first": { 291 | "version": "1.1.1", 292 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 293 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 294 | }, 295 | "encodeurl": { 296 | "version": "1.0.2", 297 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 298 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 299 | }, 300 | "escape-html": { 301 | "version": "1.0.3", 302 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 303 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 304 | }, 305 | "escape-string-regexp": { 306 | "version": "1.0.5", 307 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 308 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" 309 | }, 310 | "etag": { 311 | "version": "1.8.1", 312 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 313 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 314 | }, 315 | "express": { 316 | "version": "4.16.2", 317 | "resolved": "https://registry.npmjs.org/express/-/express-4.16.2.tgz", 318 | "integrity": "sha1-41xt/i1kt9ygpc1PIXgb4ymeB2w=", 319 | "requires": { 320 | "accepts": "~1.3.4", 321 | "array-flatten": "1.1.1", 322 | "body-parser": "1.18.2", 323 | "content-disposition": "0.5.2", 324 | "content-type": "~1.0.4", 325 | "cookie": "0.3.1", 326 | "cookie-signature": "1.0.6", 327 | "debug": "2.6.9", 328 | "depd": "~1.1.1", 329 | "encodeurl": "~1.0.1", 330 | "escape-html": "~1.0.3", 331 | "etag": "~1.8.1", 332 | "finalhandler": "1.1.0", 333 | "fresh": "0.5.2", 334 | "merge-descriptors": "1.0.1", 335 | "methods": "~1.1.2", 336 | "on-finished": "~2.3.0", 337 | "parseurl": "~1.3.2", 338 | "path-to-regexp": "0.1.7", 339 | "proxy-addr": "~2.0.2", 340 | "qs": "6.5.1", 341 | "range-parser": "~1.2.0", 342 | "safe-buffer": "5.1.1", 343 | "send": "0.16.1", 344 | "serve-static": "1.13.1", 345 | "setprototypeof": "1.1.0", 346 | "statuses": "~1.3.1", 347 | "type-is": "~1.6.15", 348 | "utils-merge": "1.0.1", 349 | "vary": "~1.1.2" 350 | } 351 | }, 352 | "finalhandler": { 353 | "version": "1.1.0", 354 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", 355 | "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", 356 | "requires": { 357 | "debug": "2.6.9", 358 | "encodeurl": "~1.0.1", 359 | "escape-html": "~1.0.3", 360 | "on-finished": "~2.3.0", 361 | "parseurl": "~1.3.2", 362 | "statuses": "~1.3.1", 363 | "unpipe": "~1.0.0" 364 | } 365 | }, 366 | "forwarded": { 367 | "version": "0.1.2", 368 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 369 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 370 | }, 371 | "fresh": { 372 | "version": "0.5.2", 373 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 374 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 375 | }, 376 | "fs-minipass": { 377 | "version": "1.2.7", 378 | "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", 379 | "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", 380 | "requires": { 381 | "minipass": "^2.6.0" 382 | } 383 | }, 384 | "fs.realpath": { 385 | "version": "1.0.0", 386 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 387 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" 388 | }, 389 | "gauge": { 390 | "version": "2.7.4", 391 | "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", 392 | "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", 393 | "requires": { 394 | "aproba": "^1.0.3", 395 | "console-control-strings": "^1.0.0", 396 | "has-unicode": "^2.0.0", 397 | "object-assign": "^4.1.0", 398 | "signal-exit": "^3.0.0", 399 | "string-width": "^1.0.1", 400 | "strip-ansi": "^3.0.1", 401 | "wide-align": "^1.1.0" 402 | }, 403 | "dependencies": { 404 | "ansi-regex": { 405 | "version": "2.1.1", 406 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", 407 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" 408 | }, 409 | "strip-ansi": { 410 | "version": "3.0.1", 411 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", 412 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", 413 | "requires": { 414 | "ansi-regex": "^2.0.0" 415 | } 416 | } 417 | } 418 | }, 419 | "glob": { 420 | "version": "7.1.6", 421 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", 422 | "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", 423 | "requires": { 424 | "fs.realpath": "^1.0.0", 425 | "inflight": "^1.0.4", 426 | "inherits": "2", 427 | "minimatch": "^3.0.4", 428 | "once": "^1.3.0", 429 | "path-is-absolute": "^1.0.0" 430 | } 431 | }, 432 | "has-ansi": { 433 | "version": "0.1.0", 434 | "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", 435 | "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=", 436 | "requires": { 437 | "ansi-regex": "^0.2.0" 438 | } 439 | }, 440 | "has-flag": { 441 | "version": "1.0.0", 442 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", 443 | "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" 444 | }, 445 | "has-unicode": { 446 | "version": "2.0.1", 447 | "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", 448 | "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" 449 | }, 450 | "http-errors": { 451 | "version": "1.6.2", 452 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", 453 | "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", 454 | "requires": { 455 | "depd": "1.1.1", 456 | "inherits": "2.0.3", 457 | "setprototypeof": "1.0.3", 458 | "statuses": ">= 1.3.1 < 2" 459 | }, 460 | "dependencies": { 461 | "depd": { 462 | "version": "1.1.1", 463 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", 464 | "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" 465 | }, 466 | "setprototypeof": { 467 | "version": "1.0.3", 468 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", 469 | "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" 470 | } 471 | } 472 | }, 473 | "iconv-lite": { 474 | "version": "0.4.19", 475 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", 476 | "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" 477 | }, 478 | "ignore-walk": { 479 | "version": "3.0.3", 480 | "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", 481 | "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", 482 | "requires": { 483 | "minimatch": "^3.0.4" 484 | } 485 | }, 486 | "inflight": { 487 | "version": "1.0.6", 488 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 489 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 490 | "requires": { 491 | "once": "^1.3.0", 492 | "wrappy": "1" 493 | } 494 | }, 495 | "inherits": { 496 | "version": "2.0.3", 497 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 498 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 499 | }, 500 | "ini": { 501 | "version": "1.3.8", 502 | "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", 503 | "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" 504 | }, 505 | "ipaddr.js": { 506 | "version": "1.5.2", 507 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.5.2.tgz", 508 | "integrity": "sha1-1LUFvemUaYfM8PxY2QEP+WB+P6A=" 509 | }, 510 | "is-fullwidth-code-point": { 511 | "version": "1.0.0", 512 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", 513 | "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", 514 | "requires": { 515 | "number-is-nan": "^1.0.0" 516 | } 517 | }, 518 | "isarray": { 519 | "version": "1.0.0", 520 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 521 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 522 | }, 523 | "jsonwebtoken": { 524 | "version": "8.5.1", 525 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", 526 | "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", 527 | "requires": { 528 | "jws": "^3.2.2", 529 | "lodash.includes": "^4.3.0", 530 | "lodash.isboolean": "^3.0.3", 531 | "lodash.isinteger": "^4.0.4", 532 | "lodash.isnumber": "^3.0.3", 533 | "lodash.isplainobject": "^4.0.6", 534 | "lodash.isstring": "^4.0.1", 535 | "lodash.once": "^4.0.0", 536 | "ms": "^2.1.1", 537 | "semver": "^5.6.0" 538 | }, 539 | "dependencies": { 540 | "ms": { 541 | "version": "2.1.3", 542 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 543 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 544 | } 545 | } 546 | }, 547 | "jwa": { 548 | "version": "1.4.1", 549 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", 550 | "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", 551 | "requires": { 552 | "buffer-equal-constant-time": "1.0.1", 553 | "ecdsa-sig-formatter": "1.0.11", 554 | "safe-buffer": "^5.0.1" 555 | } 556 | }, 557 | "jws": { 558 | "version": "3.2.2", 559 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", 560 | "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", 561 | "requires": { 562 | "jwa": "^1.4.1", 563 | "safe-buffer": "^5.0.1" 564 | } 565 | }, 566 | "kareem": { 567 | "version": "2.3.2", 568 | "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.2.tgz", 569 | "integrity": "sha512-STHz9P7X2L4Kwn72fA4rGyqyXdmrMSdxqHx9IXon/FXluXieaFA6KJ2upcHAHxQPQ0LeM/OjLrhFxifHewOALQ==" 570 | }, 571 | "lodash": { 572 | "version": "4.17.20", 573 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", 574 | "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" 575 | }, 576 | "lodash.includes": { 577 | "version": "4.3.0", 578 | "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", 579 | "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" 580 | }, 581 | "lodash.isboolean": { 582 | "version": "3.0.3", 583 | "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", 584 | "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" 585 | }, 586 | "lodash.isinteger": { 587 | "version": "4.0.4", 588 | "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", 589 | "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" 590 | }, 591 | "lodash.isnumber": { 592 | "version": "3.0.3", 593 | "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", 594 | "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" 595 | }, 596 | "lodash.isplainobject": { 597 | "version": "4.0.6", 598 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", 599 | "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" 600 | }, 601 | "lodash.isstring": { 602 | "version": "4.0.1", 603 | "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", 604 | "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" 605 | }, 606 | "lodash.once": { 607 | "version": "4.1.1", 608 | "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", 609 | "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" 610 | }, 611 | "media-typer": { 612 | "version": "0.3.0", 613 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 614 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 615 | }, 616 | "memory-pager": { 617 | "version": "1.5.0", 618 | "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", 619 | "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", 620 | "optional": true 621 | }, 622 | "merge-descriptors": { 623 | "version": "1.0.1", 624 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 625 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 626 | }, 627 | "methods": { 628 | "version": "1.1.2", 629 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 630 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 631 | }, 632 | "mime": { 633 | "version": "1.4.1", 634 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", 635 | "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" 636 | }, 637 | "mime-db": { 638 | "version": "1.30.0", 639 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", 640 | "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" 641 | }, 642 | "mime-types": { 643 | "version": "2.1.17", 644 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", 645 | "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", 646 | "requires": { 647 | "mime-db": "~1.30.0" 648 | } 649 | }, 650 | "minimatch": { 651 | "version": "3.0.4", 652 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 653 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 654 | "requires": { 655 | "brace-expansion": "^1.1.7" 656 | } 657 | }, 658 | "minimist": { 659 | "version": "1.2.5", 660 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 661 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" 662 | }, 663 | "minipass": { 664 | "version": "2.9.0", 665 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", 666 | "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", 667 | "requires": { 668 | "safe-buffer": "^5.1.2", 669 | "yallist": "^3.0.0" 670 | }, 671 | "dependencies": { 672 | "safe-buffer": { 673 | "version": "5.2.1", 674 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 675 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 676 | } 677 | } 678 | }, 679 | "minizlib": { 680 | "version": "1.3.3", 681 | "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", 682 | "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", 683 | "requires": { 684 | "minipass": "^2.9.0" 685 | } 686 | }, 687 | "mkdirp": { 688 | "version": "0.5.5", 689 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", 690 | "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", 691 | "requires": { 692 | "minimist": "^1.2.5" 693 | } 694 | }, 695 | "mongodb": { 696 | "version": "3.6.3", 697 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.3.tgz", 698 | "integrity": "sha512-rOZuR0QkodZiM+UbQE5kDsJykBqWi0CL4Ec2i1nrGrUI3KO11r6Fbxskqmq3JK2NH7aW4dcccBuUujAP0ERl5w==", 699 | "requires": { 700 | "bl": "^2.2.1", 701 | "bson": "^1.1.4", 702 | "denque": "^1.4.1", 703 | "require_optional": "^1.0.1", 704 | "safe-buffer": "^5.1.2", 705 | "saslprep": "^1.0.0" 706 | }, 707 | "dependencies": { 708 | "safe-buffer": { 709 | "version": "5.2.1", 710 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 711 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 712 | } 713 | } 714 | }, 715 | "mongoose": { 716 | "version": "5.11.9", 717 | "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.11.9.tgz", 718 | "integrity": "sha512-lmG6R64jtGGxqtn88BkkY+v470LUfGgyTKUyjswQ5c01GNgQvxA0kQd8h+tm0hZb639hKNRxL9ZBQlLleUpuIQ==", 719 | "requires": { 720 | "@types/mongodb": "^3.5.27", 721 | "bson": "^1.1.4", 722 | "kareem": "2.3.2", 723 | "mongodb": "3.6.3", 724 | "mongoose-legacy-pluralize": "1.0.2", 725 | "mpath": "0.8.1", 726 | "mquery": "3.2.3", 727 | "ms": "2.1.2", 728 | "regexp-clone": "1.0.0", 729 | "safe-buffer": "5.2.1", 730 | "sift": "7.0.1", 731 | "sliced": "1.0.1" 732 | }, 733 | "dependencies": { 734 | "ms": { 735 | "version": "2.1.2", 736 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 737 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 738 | }, 739 | "safe-buffer": { 740 | "version": "5.2.1", 741 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 742 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 743 | } 744 | } 745 | }, 746 | "mongoose-legacy-pluralize": { 747 | "version": "1.0.2", 748 | "resolved": "https://registry.npmjs.org/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz", 749 | "integrity": "sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ==" 750 | }, 751 | "mpath": { 752 | "version": "0.8.1", 753 | "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.8.1.tgz", 754 | "integrity": "sha512-norEinle9aFc05McBawVPwqgFZ7npkts9yu17ztIVLwPwO9rq0OTp89kGVTqvv5rNLMz96E5iWHpVORjI411vA==" 755 | }, 756 | "mquery": { 757 | "version": "3.2.3", 758 | "resolved": "https://registry.npmjs.org/mquery/-/mquery-3.2.3.tgz", 759 | "integrity": "sha512-cIfbP4TyMYX+SkaQ2MntD+F2XbqaBHUYWk3j+kqdDztPWok3tgyssOZxMHMtzbV1w9DaSlvEea0Iocuro41A4g==", 760 | "requires": { 761 | "bluebird": "3.5.1", 762 | "debug": "3.1.0", 763 | "regexp-clone": "^1.0.0", 764 | "safe-buffer": "5.1.2", 765 | "sliced": "1.0.1" 766 | }, 767 | "dependencies": { 768 | "debug": { 769 | "version": "3.1.0", 770 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 771 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 772 | "requires": { 773 | "ms": "2.0.0" 774 | } 775 | }, 776 | "safe-buffer": { 777 | "version": "5.1.2", 778 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 779 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 780 | } 781 | } 782 | }, 783 | "ms": { 784 | "version": "2.0.0", 785 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 786 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 787 | }, 788 | "needle": { 789 | "version": "2.5.2", 790 | "resolved": "https://registry.npmjs.org/needle/-/needle-2.5.2.tgz", 791 | "integrity": "sha512-LbRIwS9BfkPvNwNHlsA41Q29kL2L/6VaOJ0qisM5lLWsTV3nP15abO5ITL6L81zqFhzjRKDAYjpcBcwM0AVvLQ==", 792 | "requires": { 793 | "debug": "^3.2.6", 794 | "iconv-lite": "^0.4.4", 795 | "sax": "^1.2.4" 796 | }, 797 | "dependencies": { 798 | "debug": { 799 | "version": "3.2.7", 800 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", 801 | "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", 802 | "requires": { 803 | "ms": "^2.1.1" 804 | } 805 | }, 806 | "ms": { 807 | "version": "2.1.3", 808 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 809 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 810 | } 811 | } 812 | }, 813 | "negotiator": { 814 | "version": "0.6.1", 815 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", 816 | "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" 817 | }, 818 | "node-addon-api": { 819 | "version": "3.1.0", 820 | "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.1.0.tgz", 821 | "integrity": "sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw==" 822 | }, 823 | "node-pre-gyp": { 824 | "version": "0.15.0", 825 | "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.15.0.tgz", 826 | "integrity": "sha512-7QcZa8/fpaU/BKenjcaeFF9hLz2+7S9AqyXFhlH/rilsQ/hPZKK32RtR5EQHJElgu+q5RfbJ34KriI79UWaorA==", 827 | "requires": { 828 | "detect-libc": "^1.0.2", 829 | "mkdirp": "^0.5.3", 830 | "needle": "^2.5.0", 831 | "nopt": "^4.0.1", 832 | "npm-packlist": "^1.1.6", 833 | "npmlog": "^4.0.2", 834 | "rc": "^1.2.7", 835 | "rimraf": "^2.6.1", 836 | "semver": "^5.3.0", 837 | "tar": "^4.4.2" 838 | } 839 | }, 840 | "nopt": { 841 | "version": "4.0.3", 842 | "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", 843 | "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", 844 | "requires": { 845 | "abbrev": "1", 846 | "osenv": "^0.1.4" 847 | } 848 | }, 849 | "npm-bundled": { 850 | "version": "1.1.1", 851 | "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", 852 | "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", 853 | "requires": { 854 | "npm-normalize-package-bin": "^1.0.1" 855 | } 856 | }, 857 | "npm-normalize-package-bin": { 858 | "version": "1.0.1", 859 | "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", 860 | "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" 861 | }, 862 | "npm-packlist": { 863 | "version": "1.4.8", 864 | "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", 865 | "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", 866 | "requires": { 867 | "ignore-walk": "^3.0.1", 868 | "npm-bundled": "^1.0.1", 869 | "npm-normalize-package-bin": "^1.0.1" 870 | } 871 | }, 872 | "npmlog": { 873 | "version": "4.1.2", 874 | "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", 875 | "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", 876 | "requires": { 877 | "are-we-there-yet": "~1.1.2", 878 | "console-control-strings": "~1.1.0", 879 | "gauge": "~2.7.3", 880 | "set-blocking": "~2.0.0" 881 | } 882 | }, 883 | "number-is-nan": { 884 | "version": "1.0.1", 885 | "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", 886 | "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" 887 | }, 888 | "object-assign": { 889 | "version": "4.1.1", 890 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 891 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 892 | }, 893 | "on-finished": { 894 | "version": "2.3.0", 895 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 896 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 897 | "requires": { 898 | "ee-first": "1.1.1" 899 | } 900 | }, 901 | "once": { 902 | "version": "1.4.0", 903 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 904 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 905 | "requires": { 906 | "wrappy": "1" 907 | } 908 | }, 909 | "os-homedir": { 910 | "version": "1.0.2", 911 | "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", 912 | "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" 913 | }, 914 | "os-tmpdir": { 915 | "version": "1.0.2", 916 | "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", 917 | "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" 918 | }, 919 | "osenv": { 920 | "version": "0.1.5", 921 | "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", 922 | "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", 923 | "requires": { 924 | "os-homedir": "^1.0.0", 925 | "os-tmpdir": "^1.0.0" 926 | } 927 | }, 928 | "parseurl": { 929 | "version": "1.3.2", 930 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", 931 | "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" 932 | }, 933 | "path-is-absolute": { 934 | "version": "1.0.1", 935 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 936 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" 937 | }, 938 | "path-to-regexp": { 939 | "version": "0.1.7", 940 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 941 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 942 | }, 943 | "process-nextick-args": { 944 | "version": "2.0.1", 945 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", 946 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" 947 | }, 948 | "proxy-addr": { 949 | "version": "2.0.2", 950 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.2.tgz", 951 | "integrity": "sha1-ZXFQT0e7mI7IGAJT+F3X4UlSvew=", 952 | "requires": { 953 | "forwarded": "~0.1.2", 954 | "ipaddr.js": "1.5.2" 955 | } 956 | }, 957 | "qs": { 958 | "version": "6.5.1", 959 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", 960 | "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" 961 | }, 962 | "range-parser": { 963 | "version": "1.2.0", 964 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", 965 | "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" 966 | }, 967 | "raw-body": { 968 | "version": "2.3.2", 969 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", 970 | "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", 971 | "requires": { 972 | "bytes": "3.0.0", 973 | "http-errors": "1.6.2", 974 | "iconv-lite": "0.4.19", 975 | "unpipe": "1.0.0" 976 | } 977 | }, 978 | "rc": { 979 | "version": "1.2.8", 980 | "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", 981 | "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", 982 | "requires": { 983 | "deep-extend": "^0.6.0", 984 | "ini": "~1.3.0", 985 | "minimist": "^1.2.0", 986 | "strip-json-comments": "~2.0.1" 987 | } 988 | }, 989 | "readable-stream": { 990 | "version": "2.3.7", 991 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", 992 | "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", 993 | "requires": { 994 | "core-util-is": "~1.0.0", 995 | "inherits": "~2.0.3", 996 | "isarray": "~1.0.0", 997 | "process-nextick-args": "~2.0.0", 998 | "safe-buffer": "~5.1.1", 999 | "string_decoder": "~1.1.1", 1000 | "util-deprecate": "~1.0.1" 1001 | } 1002 | }, 1003 | "regexp-clone": { 1004 | "version": "1.0.0", 1005 | "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-1.0.0.tgz", 1006 | "integrity": "sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw==" 1007 | }, 1008 | "require_optional": { 1009 | "version": "1.0.1", 1010 | "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", 1011 | "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", 1012 | "requires": { 1013 | "resolve-from": "^2.0.0", 1014 | "semver": "^5.1.0" 1015 | } 1016 | }, 1017 | "resolve-from": { 1018 | "version": "2.0.0", 1019 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", 1020 | "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" 1021 | }, 1022 | "rimraf": { 1023 | "version": "2.7.1", 1024 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", 1025 | "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", 1026 | "requires": { 1027 | "glob": "^7.1.3" 1028 | } 1029 | }, 1030 | "rx": { 1031 | "version": "2.3.24", 1032 | "resolved": "https://registry.npmjs.org/rx/-/rx-2.3.24.tgz", 1033 | "integrity": "sha1-FPlQpCF9fjXapxu8vljv9o6ksrc=" 1034 | }, 1035 | "safe-buffer": { 1036 | "version": "5.1.1", 1037 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", 1038 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" 1039 | }, 1040 | "saslprep": { 1041 | "version": "1.0.3", 1042 | "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz", 1043 | "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==", 1044 | "optional": true, 1045 | "requires": { 1046 | "sparse-bitfield": "^3.0.3" 1047 | } 1048 | }, 1049 | "sax": { 1050 | "version": "1.2.4", 1051 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", 1052 | "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" 1053 | }, 1054 | "semver": { 1055 | "version": "5.7.1", 1056 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 1057 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" 1058 | }, 1059 | "send": { 1060 | "version": "0.16.1", 1061 | "resolved": "https://registry.npmjs.org/send/-/send-0.16.1.tgz", 1062 | "integrity": "sha512-ElCLJdJIKPk6ux/Hocwhk7NFHpI3pVm/IZOYWqUmoxcgeyM+MpxHHKhb8QmlJDX1pU6WrgaHBkVNm73Sv7uc2A==", 1063 | "requires": { 1064 | "debug": "2.6.9", 1065 | "depd": "~1.1.1", 1066 | "destroy": "~1.0.4", 1067 | "encodeurl": "~1.0.1", 1068 | "escape-html": "~1.0.3", 1069 | "etag": "~1.8.1", 1070 | "fresh": "0.5.2", 1071 | "http-errors": "~1.6.2", 1072 | "mime": "1.4.1", 1073 | "ms": "2.0.0", 1074 | "on-finished": "~2.3.0", 1075 | "range-parser": "~1.2.0", 1076 | "statuses": "~1.3.1" 1077 | } 1078 | }, 1079 | "serve-static": { 1080 | "version": "1.13.1", 1081 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.1.tgz", 1082 | "integrity": "sha512-hSMUZrsPa/I09VYFJwa627JJkNs0NrfL1Uzuup+GqHfToR2KcsXFymXSV90hoyw3M+msjFuQly+YzIH/q0MGlQ==", 1083 | "requires": { 1084 | "encodeurl": "~1.0.1", 1085 | "escape-html": "~1.0.3", 1086 | "parseurl": "~1.3.2", 1087 | "send": "0.16.1" 1088 | } 1089 | }, 1090 | "set-blocking": { 1091 | "version": "2.0.0", 1092 | "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", 1093 | "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" 1094 | }, 1095 | "setprototypeof": { 1096 | "version": "1.1.0", 1097 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", 1098 | "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" 1099 | }, 1100 | "sift": { 1101 | "version": "7.0.1", 1102 | "resolved": "https://registry.npmjs.org/sift/-/sift-7.0.1.tgz", 1103 | "integrity": "sha512-oqD7PMJ+uO6jV9EQCl0LrRw1OwsiPsiFQR5AR30heR+4Dl7jBBbDLnNvWiak20tzZlSE1H7RB30SX/1j/YYT7g==" 1104 | }, 1105 | "signal-exit": { 1106 | "version": "3.0.3", 1107 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", 1108 | "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" 1109 | }, 1110 | "sliced": { 1111 | "version": "1.0.1", 1112 | "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", 1113 | "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" 1114 | }, 1115 | "sparse-bitfield": { 1116 | "version": "3.0.3", 1117 | "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", 1118 | "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=", 1119 | "optional": true, 1120 | "requires": { 1121 | "memory-pager": "^1.0.2" 1122 | } 1123 | }, 1124 | "spawn-command": { 1125 | "version": "0.0.2-1", 1126 | "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz", 1127 | "integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=" 1128 | }, 1129 | "statuses": { 1130 | "version": "1.3.1", 1131 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", 1132 | "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" 1133 | }, 1134 | "string-width": { 1135 | "version": "1.0.2", 1136 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", 1137 | "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", 1138 | "requires": { 1139 | "code-point-at": "^1.0.0", 1140 | "is-fullwidth-code-point": "^1.0.0", 1141 | "strip-ansi": "^3.0.0" 1142 | }, 1143 | "dependencies": { 1144 | "ansi-regex": { 1145 | "version": "2.1.1", 1146 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", 1147 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" 1148 | }, 1149 | "strip-ansi": { 1150 | "version": "3.0.1", 1151 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", 1152 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", 1153 | "requires": { 1154 | "ansi-regex": "^2.0.0" 1155 | } 1156 | } 1157 | } 1158 | }, 1159 | "string_decoder": { 1160 | "version": "1.1.1", 1161 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 1162 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 1163 | "requires": { 1164 | "safe-buffer": "~5.1.0" 1165 | } 1166 | }, 1167 | "strip-ansi": { 1168 | "version": "0.3.0", 1169 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", 1170 | "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=", 1171 | "requires": { 1172 | "ansi-regex": "^0.2.1" 1173 | } 1174 | }, 1175 | "strip-json-comments": { 1176 | "version": "2.0.1", 1177 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", 1178 | "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" 1179 | }, 1180 | "supports-color": { 1181 | "version": "3.2.3", 1182 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", 1183 | "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", 1184 | "requires": { 1185 | "has-flag": "^1.0.0" 1186 | } 1187 | }, 1188 | "tar": { 1189 | "version": "4.4.13", 1190 | "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", 1191 | "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", 1192 | "requires": { 1193 | "chownr": "^1.1.1", 1194 | "fs-minipass": "^1.2.5", 1195 | "minipass": "^2.8.6", 1196 | "minizlib": "^1.2.1", 1197 | "mkdirp": "^0.5.0", 1198 | "safe-buffer": "^5.1.2", 1199 | "yallist": "^3.0.3" 1200 | }, 1201 | "dependencies": { 1202 | "safe-buffer": { 1203 | "version": "5.2.1", 1204 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 1205 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 1206 | } 1207 | } 1208 | }, 1209 | "tree-kill": { 1210 | "version": "1.2.2", 1211 | "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", 1212 | "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==" 1213 | }, 1214 | "type-is": { 1215 | "version": "1.6.15", 1216 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", 1217 | "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", 1218 | "requires": { 1219 | "media-typer": "0.3.0", 1220 | "mime-types": "~2.1.15" 1221 | } 1222 | }, 1223 | "unpipe": { 1224 | "version": "1.0.0", 1225 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1226 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 1227 | }, 1228 | "util-deprecate": { 1229 | "version": "1.0.2", 1230 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1231 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 1232 | }, 1233 | "utils-merge": { 1234 | "version": "1.0.1", 1235 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1236 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 1237 | }, 1238 | "vary": { 1239 | "version": "1.1.2", 1240 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1241 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 1242 | }, 1243 | "wide-align": { 1244 | "version": "1.1.3", 1245 | "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", 1246 | "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", 1247 | "requires": { 1248 | "string-width": "^1.0.2 || 2" 1249 | } 1250 | }, 1251 | "wrappy": { 1252 | "version": "1.0.2", 1253 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1254 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 1255 | }, 1256 | "yallist": { 1257 | "version": "3.1.1", 1258 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", 1259 | "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" 1260 | } 1261 | } 1262 | } 1263 | --------------------------------------------------------------------------------