├── front ├── .env ├── src │ ├── assets │ │ ├── star.png │ │ ├── emptyBasket.jpg │ │ └── shopping-basket.png │ ├── utils │ │ └── consts.js │ ├── http │ │ ├── index.js │ │ ├── userApi.js │ │ ├── ordersAPI.js │ │ └── deviceAPI.js │ ├── components │ │ ├── DeviceList.js │ │ ├── NavBar │ │ │ ├── preesent-components │ │ │ │ ├── falseAuth.js │ │ │ │ └── trueAuth.js │ │ │ ├── NavBar.js │ │ │ └── BasketNavBar.js │ │ ├── ratingStars.js │ │ ├── BrandBar.js │ │ ├── AppRouter.js │ │ ├── Pages.js │ │ ├── TypeBar.js │ │ ├── deviceItem.js │ │ ├── modals │ │ │ ├── CreateType.js │ │ │ ├── CreateBrand.js │ │ │ ├── DeleteBrandOrType.js │ │ │ └── CreateDevice.js │ │ ├── oneItemInBasket.js │ │ └── itemOneorderInAdmin.js │ ├── index.js │ ├── store │ │ ├── UserStore.js │ │ ├── DeviceStore.js │ │ └── BasketStore.js │ ├── pages │ │ ├── BasketCard.js │ │ ├── Ordering.js │ │ ├── Shop.js │ │ ├── OneOrder.js │ │ ├── Auth.js │ │ ├── DevicePage.js │ │ ├── Orders.js │ │ ├── Admin.js │ │ └── DevicePageEdit.js │ ├── routes.js │ └── App.js ├── public │ └── index.html └── package.json ├── .gitignore ├── DB-models-v1.jpg ├── DB-models-v2.jpg └── backend ├── functions └── getUserFromToken.js ├── db └── db.js ├── middleware ├── ErrorHandlingMiddleware.js ├── authMiddleware.js ├── checkDeleteDeviceFromBasket.js ├── checkRoleMiddleware.js └── checkAddRatingMiddleware.js ├── routes ├── typeRouter.js ├── userRouter.js ├── brandRouter.js ├── ratingRouter.js ├── basketRouter.js ├── ordersRouter.js ├── deviceRouter.js └── index.js ├── error └── apiError.js ├── package.json ├── controllers ├── typeController.js ├── brandController.js ├── userController.js ├── ratingController.js ├── basketController.js ├── ordersController.js └── deviceController.js ├── index.js └── models └── models.js /front/.env: -------------------------------------------------------------------------------- 1 | REACT_APP_API_URL='http://localhost:5000/' 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | backend/.env 4 | backend/static/* -------------------------------------------------------------------------------- /DB-models-v1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shkulipa/online-store/HEAD/DB-models-v1.jpg -------------------------------------------------------------------------------- /DB-models-v2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shkulipa/online-store/HEAD/DB-models-v2.jpg -------------------------------------------------------------------------------- /front/src/assets/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shkulipa/online-store/HEAD/front/src/assets/star.png -------------------------------------------------------------------------------- /front/src/assets/emptyBasket.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shkulipa/online-store/HEAD/front/src/assets/emptyBasket.jpg -------------------------------------------------------------------------------- /front/src/assets/shopping-basket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shkulipa/online-store/HEAD/front/src/assets/shopping-basket.png -------------------------------------------------------------------------------- /backend/functions/getUserFromToken.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | 3 | function getUserFromToken(headersAuth) { 4 | const token = headersAuth.split(' ')[1]; 5 | return jwt.verify(token, process.env.SECRET_KEY); 6 | } 7 | 8 | module.exports = getUserFromToken; 9 | 10 | -------------------------------------------------------------------------------- /backend/db/db.js: -------------------------------------------------------------------------------- 1 | const {Sequelize} = require('sequelize'); 2 | 3 | module.exports = new Sequelize( 4 | process.env.DATABASE_DB, 5 | process.env.USER_DB, 6 | process.env.PASSWORD_DB, 7 | { 8 | dialect: 'postgres', 9 | host: process.env.HOST_DB, 10 | port: process.env.PORT_DB, 11 | } 12 | ); -------------------------------------------------------------------------------- /backend/middleware/ErrorHandlingMiddleware.js: -------------------------------------------------------------------------------- 1 | const ApiError = require('./../error/apiError'); 2 | 3 | module.exports = function (err, req, res, next) { 4 | if(err instanceof ApiError) { 5 | return res.status(err.status).json({message: err.message}) 6 | } 7 | 8 | return res.status(500).json({message: "Unexpected error"}) 9 | } 10 | -------------------------------------------------------------------------------- /front/src/utils/consts.js: -------------------------------------------------------------------------------- 1 | export const ADMIN_ROUTE = '/admin'; 2 | export const LOGIN_ROUTE = '/login'; 3 | export const REGISTRATION_ROUTE = '/auth'; 4 | export const SHOP_ROUTE = '/'; 5 | export const BASKET_ROUTE = '/basket'; 6 | export const DEVICE_ROUTE = '/device'; 7 | export const ORDERS_ROUTE = '/orders'; 8 | export const DEVICE_EDIT_ROUTE = '/device/edit'; 9 | export const ORDERING_ROUTE = '/checkout'; 10 | 11 | 12 | -------------------------------------------------------------------------------- /backend/routes/typeRouter.js: -------------------------------------------------------------------------------- 1 | const Router = require('express'); 2 | const router = new Router(); 3 | const typeController = require('./../controllers/typeController'); 4 | const checkRole = require('../middleware/checkRoleMiddleware'); 5 | 6 | router 7 | .post('/', checkRole("ADMIN"), typeController.create) 8 | .get('/', typeController.getAll) 9 | .delete('/:id', checkRole("ADMIN"), typeController.delete); 10 | 11 | module.exports = router; 12 | -------------------------------------------------------------------------------- /backend/routes/userRouter.js: -------------------------------------------------------------------------------- 1 | const Router = require('express'); 2 | const router = new Router(); 3 | const userController = require('./../controllers/userController'); 4 | const authMiddleware = require('./../middleware/authMiddleware'); 5 | 6 | router 7 | .post('/registration', userController.registration) 8 | .post('/login', userController.login) 9 | .get('/auth', authMiddleware, userController.check); 10 | 11 | module.exports = router; 12 | -------------------------------------------------------------------------------- /backend/routes/brandRouter.js: -------------------------------------------------------------------------------- 1 | const Router = require('express'); 2 | const router = new Router(); 3 | const brandController = require('./../controllers/brandController'); 4 | const checkRole = require('../middleware/checkRoleMiddleware'); 5 | 6 | router 7 | .post('/', checkRole("ADMIN"), brandController.create) 8 | .get('/', brandController.getAll) 9 | .delete('/:id', checkRole("ADMIN"), brandController.delete); 10 | 11 | module.exports = router; 12 | -------------------------------------------------------------------------------- /backend/error/apiError.js: -------------------------------------------------------------------------------- 1 | class ApiError extends Error { 2 | constructor(status, message) { 3 | super(); 4 | this.status = status; 5 | this.message = message; 6 | } 7 | 8 | static badRequest(message) { 9 | return new ApiError(404, message); 10 | } 11 | 12 | static internal(message) { 13 | return new ApiError(500, message); 14 | } 15 | 16 | static forbidden(message) { 17 | return new ApiError(403, message); 18 | } 19 | } 20 | 21 | module.exports = ApiError; 22 | -------------------------------------------------------------------------------- /backend/routes/ratingRouter.js: -------------------------------------------------------------------------------- 1 | const Router = require('express'); 2 | const router = new Router(); 3 | const authMiddleware = require('./../middleware/authMiddleware'); 4 | const checkAddRatingMiddleware = require('./../middleware/checkAddRatingMiddleware'); 5 | const ratingController = require('./../controllers/ratingController') 6 | 7 | router 8 | .post('/', authMiddleware, checkAddRatingMiddleware, ratingController.addRating) 9 | .post('/check-rating', authMiddleware, ratingController.checkRating); 10 | 11 | module.exports = router; 12 | -------------------------------------------------------------------------------- /front/src/http/index.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const $host = axios.create({ 4 | baseURL: process.env.REACT_APP_API_URL, 5 | responseType: "json", 6 | }) 7 | 8 | const $authHost = axios.create({ 9 | baseURL: process.env.REACT_APP_API_URL, 10 | responseType: "json", 11 | }) 12 | 13 | const authInterceptor = config => { 14 | config.headers.authorization = `Bearer ${localStorage.getItem('token')}` 15 | return config 16 | } 17 | 18 | $authHost.interceptors.request.use(authInterceptor) 19 | 20 | export { 21 | $host, 22 | $authHost 23 | } 24 | -------------------------------------------------------------------------------- /backend/middleware/authMiddleware.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | 3 | module.exports = function (req, res, next) { 4 | if(req.method === "OPTIONS") { 5 | next() 6 | } 7 | try { 8 | const token = req.headers.authorization.split(' ')[1]; 9 | if(!token) { 10 | return res.status(401).json({message: "Not authorization"}); 11 | } 12 | req.user = jwt.verify(token, process.env.SECRET_KEY); 13 | next(); 14 | } catch (e) { 15 | res.status(401).json({message: "Not authorization"}); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /front/src/components/DeviceList.js: -------------------------------------------------------------------------------- 1 | import React, {useContext} from 'react'; 2 | import {observer} from "mobx-react-lite"; 3 | import {Context} from "../index"; 4 | import {Row} from "react-bootstrap"; 5 | import DeviceItem from "./deviceItem"; 6 | 7 | const DeviceList = observer(() => { 8 | const {device} = useContext(Context); 9 | 10 | return ( 11 | 12 | {device.devices.map(device => 13 | 14 | )} 15 | 16 | ); 17 | }); 18 | 19 | export default DeviceList; 20 | -------------------------------------------------------------------------------- /backend/routes/basketRouter.js: -------------------------------------------------------------------------------- 1 | const Router = require('express'); 2 | const router = new Router(); 3 | const BasketController = require('./../controllers/basketController'); 4 | const authMiddleware = require('./../middleware/authMiddleware'); 5 | const checkDeleteDeviceFromBasket = require('./../middleware/checkDeleteDeviceFromBasket'); 6 | 7 | router 8 | .post('/', authMiddleware, BasketController.addDevice) 9 | .get('/', authMiddleware, BasketController.getDevices) 10 | .delete('/:id', authMiddleware, checkDeleteDeviceFromBasket, BasketController.deleteDevice); 11 | 12 | module.exports = router; 13 | -------------------------------------------------------------------------------- /backend/routes/ordersRouter.js: -------------------------------------------------------------------------------- 1 | const Router = require('express'); 2 | const router = new Router(); 3 | 4 | const ordersController = require('./../controllers/ordersController'); 5 | const checkRole = require('./../middleware/checkRoleMiddleware'); 6 | 7 | router 8 | .post('/', ordersController.create) 9 | .get('/', checkRole("ADMIN"), ordersController.getAll) 10 | .get('/:id', checkRole("ADMIN"), ordersController.getOne) 11 | .put('/', checkRole("ADMIN"), ordersController.updateOrder) 12 | .delete('/', checkRole("ADMIN"), ordersController.deleteOrder); 13 | 14 | 15 | module.exports = router; 16 | -------------------------------------------------------------------------------- /backend/routes/deviceRouter.js: -------------------------------------------------------------------------------- 1 | const Router = require('express'); 2 | const router = new Router(); 3 | const deviceController = require('./../controllers/deviceController'); 4 | const checkRole = require('../middleware/checkRoleMiddleware'); 5 | 6 | router 7 | .post('/', deviceController.create) 8 | .get('/', deviceController.getAll) 9 | .get('/search', deviceController.getSearchAllDeviceByName) 10 | .get('/:id', deviceController.getOne) 11 | .delete('/:id', checkRole("ADMIN"), deviceController.delete) 12 | .put('/:id', checkRole("ADMIN"), deviceController.update) 13 | 14 | module.exports = router; 15 | -------------------------------------------------------------------------------- /front/src/components/NavBar/preesent-components/falseAuth.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import {Button, Nav} from "react-bootstrap"; 4 | import {LOGIN_ROUTE} from "../../../utils/consts"; 5 | import {NavLink} from "react-router-dom"; 6 | import BasketNavBar from "../BasketNavBar"; 7 | 8 | const FalseAuth = () => { 9 | return ( 10 | 16 | ); 17 | }; 18 | 19 | export default FalseAuth; 20 | -------------------------------------------------------------------------------- /front/src/index.js: -------------------------------------------------------------------------------- 1 | import React, {createContext} from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | import UserStore from "./store/UserStore"; 6 | import DeviceStore from "./store/DeviceStore"; 7 | import BasketStoreStore from "./store/BasketStore"; 8 | 9 | export const Context = createContext(null); 10 | 11 | ReactDOM.render( 12 | 19 | 20 | , 21 | document.getElementById('root') 22 | ); 23 | -------------------------------------------------------------------------------- /backend/middleware/checkDeleteDeviceFromBasket.js: -------------------------------------------------------------------------------- 1 | const {Basket, BasketDevice} = require('./../models/models'); 2 | const jwt = require('jsonwebtoken'); 3 | 4 | module.exports = async function (req, res, next) { 5 | try { 6 | const {id} = req.params; 7 | const user = req.user; 8 | const userBasket = await Basket.findOne({where: {userId: user.id}}); 9 | const deviceItem = await BasketDevice.findOne({where: {basketId: userBasket.id, deviceId: id}}); 10 | 11 | if(deviceItem) { 12 | return next(); 13 | } 14 | return res.json("Device didn't find in basket of user"); 15 | } catch (e) { 16 | res.json(e); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /front/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 16 | React App 17 | 18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "dev": "nodemon index.js" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "bcrypt": "^5.0.1", 15 | "cors": "^2.8.5", 16 | "dotenv": "^10.0.0", 17 | "express": "^4.17.1", 18 | "express-fileupload": "^1.2.1", 19 | "jsonwebtoken": "^8.5.1", 20 | "pg": "^8.6.0", 21 | "pg-hstore": "^2.3.4", 22 | "sequelize": "^6.6.2", 23 | "uuid": "^8.3.2" 24 | }, 25 | "devDependencies": { 26 | "nodemon": "^2.0.7" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend/routes/index.js: -------------------------------------------------------------------------------- 1 | const Router = require('express'); 2 | const router = new Router(); 3 | const deviceRouter = require('./deviceRouter'); 4 | const brandRouter = require('./brandRouter'); 5 | const typeRouter = require('./typeRouter'); 6 | const userRouter = require('./userRouter'); 7 | const basketRouter = require('./basketRouter'); 8 | const ratingRouter = require('./ratingRouter'); 9 | const ordersRouter = require('./ordersRouter'); 10 | 11 | router.use('/user', userRouter) 12 | router.use('/type', typeRouter) 13 | router.use('/brand', brandRouter) 14 | router.use('/device', deviceRouter) 15 | router.use('/basket', basketRouter) 16 | router.use('/rating', ratingRouter) 17 | router.use('/orders', ordersRouter) 18 | 19 | module.exports = router; 20 | -------------------------------------------------------------------------------- /front/src/http/userApi.js: -------------------------------------------------------------------------------- 1 | import {$authHost, $host} from "./index"; 2 | import jwt_decode from "jwt-decode"; 3 | 4 | export const registration = async (email, password) => { 5 | const {data} = await $host.post('api/user/registration', {email, password, role: 'ADMIN'}); 6 | localStorage.setItem('token', data.token); 7 | return jwt_decode(data.token); 8 | } 9 | 10 | export const login = async (email, password) => { 11 | const {data} = await $host.post('api/user/login', {email, password}); 12 | localStorage.setItem('token', data.token); 13 | return jwt_decode(data.token); 14 | } 15 | 16 | export const check = async () => { 17 | const {data} = await $authHost.get('api/user/auth'); 18 | localStorage.setItem('token', data.token); 19 | return jwt_decode(data.token); 20 | } 21 | -------------------------------------------------------------------------------- /front/src/components/ratingStars.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Rating } from '@material-ui/lab'; 3 | 4 | const RatingStars = ({ratingChanged, ratingVal, isAuth, isAccessRating}) => { 5 | if(isAuth && isAccessRating) { 6 | return ( 7 | { 10 | ratingChanged(newValue); 11 | }} 12 | precision={1} 13 | size="large" 14 | /> 15 | ) 16 | } else { 17 | return ( 18 | 24 | ) 25 | } 26 | }; 27 | 28 | export default RatingStars; 29 | -------------------------------------------------------------------------------- /backend/middleware/checkRoleMiddleware.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | 3 | module.exports = function(role) { 4 | return function (req, res, next) { 5 | if (req.method === "OPTIONS") { 6 | next() 7 | } 8 | try { 9 | const token = req.headers.authorization.split(' ')[1]; 10 | if (!token) { 11 | return res.status(401).json({message: "Not authorization"}); 12 | } 13 | const decoded = jwt.verify(token, process.env.SECRET_KEY); 14 | if(decoded.role !== role) { 15 | return res.status(403).json({message: "You haven't access"}); 16 | }; 17 | req.user = decoded; 18 | next(); 19 | } catch (e) { 20 | res.status(401).json({message: "Not authorization"}); 21 | } 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /front/src/components/NavBar/NavBar.js: -------------------------------------------------------------------------------- 1 | import React, {useContext} from 'react'; 2 | import {observer} from "mobx-react-lite"; 3 | import {NavLink} from "react-router-dom"; 4 | 5 | import {Context} from "../../index"; 6 | 7 | import {Container, Navbar} from "react-bootstrap"; 8 | import {SHOP_ROUTE} from "../../utils/consts"; 9 | import TrueAuth from "./preesent-components/trueAuth"; 10 | import FalseAuth from "./preesent-components/falseAuth"; 11 | 12 | const NavBar = observer(() => { 13 | const {user} = useContext(Context); 14 | 15 | return ( 16 | 17 | 18 | Магазин -70% 19 | {user.isAuth ? : } 20 | 21 | 22 | ); 23 | }); 24 | 25 | export default NavBar; 26 | -------------------------------------------------------------------------------- /front/src/components/BrandBar.js: -------------------------------------------------------------------------------- 1 | import React, {useContext} from 'react'; 2 | import {observer} from "mobx-react-lite"; 3 | import {Context} from "../index"; 4 | import {Card, Row} from "react-bootstrap"; 5 | 6 | const BrandBar = observer(() => { 7 | const {device} = useContext(Context); 8 | return ( 9 | 10 | {device.brands.map(brand => 11 | device.setSelectedBrand(brand)} 17 | > 18 | {brand.name} 19 | 20 | )} 21 | 22 | ); 23 | }); 24 | 25 | export default BrandBar; 26 | -------------------------------------------------------------------------------- /front/src/components/AppRouter.js: -------------------------------------------------------------------------------- 1 | import React, {useContext} from 'react'; 2 | import {Switch, Route, Redirect} from 'react-router-dom'; 3 | 4 | import {authRouters, publicRouters} from "../routes"; 5 | import {SHOP_ROUTE} from "../utils/consts"; 6 | import {Context} from "../index"; 7 | 8 | const AppRouter = () => { 9 | const {user} = useContext(Context); 10 | 11 | return ( 12 | 13 | {user.isAuth && user.User.role === "ADMIN" && authRouters.map( ({path, Component}) => { 14 | return 15 | })} 16 | {publicRouters.map( ({path, Component}) => { 17 | return 18 | })} 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default AppRouter; 25 | -------------------------------------------------------------------------------- /front/src/components/NavBar/BasketNavBar.js: -------------------------------------------------------------------------------- 1 | import React, {useContext} from 'react'; 2 | import {Image} from "react-bootstrap"; 3 | import shop_cart from "../../assets/shopping-basket.png"; 4 | import {NavLink} from "react-router-dom"; 5 | import {Context} from "../../index"; 6 | import {observer} from "mobx-react-lite"; 7 | import {BASKET_ROUTE} from "../../utils/consts"; 8 | 9 | const BasketNavBar = observer(() => { 10 | const {basket} = useContext(Context); 11 | 12 | return ( 13 |
14 | 15 | basket 16 |
{basket.Price} RUB
17 |
18 |
19 | ); 20 | }); 21 | export default BasketNavBar; 22 | 23 | 24 | -------------------------------------------------------------------------------- /front/src/components/Pages.js: -------------------------------------------------------------------------------- 1 | import React, {useContext} from 'react'; 2 | import {observer} from "mobx-react-lite"; 3 | import {Context} from "../index"; 4 | import {Pagination} from "react-bootstrap"; 5 | 6 | const Pages = observer(() => { 7 | const {device} = useContext(Context); 8 | const pageCount = Math.ceil(device.totalCount / device.limit); 9 | const pages = []; 10 | 11 | for (let i = 0; i < pageCount; i++) { 12 | pages.push(i + 1); 13 | } 14 | 15 | return ( 16 | 17 | {pages.map(page => 18 | device.setPage(page)} 22 | > 23 | {page} 24 | 25 | )} 26 | 27 | ); 28 | }); 29 | 30 | export default Pages; 31 | -------------------------------------------------------------------------------- /backend/middleware/checkAddRatingMiddleware.js: -------------------------------------------------------------------------------- 1 | const {Rating, Device} = require('./../models/models'); 2 | 3 | const jwt = require('jsonwebtoken'); 4 | 5 | module.exports = async function (req, res, next) { 6 | try { 7 | const {deviceId} = req.body; 8 | const token = req.headers.authorization.split(' ')[1]; 9 | const user = jwt.verify(token, process.env.SECRET_KEY); 10 | const checkRating = await Rating.findOne({where: {deviceId, userId: user.id}}); 11 | const checkDevices = await Device.findOne({where: {id: deviceId}}); 12 | 13 | if (!checkDevices) { 14 | return res.json("Product doesn't existing in data base"); 15 | } else if(checkRating && checkDevices) { 16 | return res.json("You have left a rating for this product"); 17 | } 18 | return next(); 19 | } catch (e) { 20 | return res.status(401).json("Something going wrong in checkAddRatingMiddleware.js"); 21 | } 22 | }; 23 | 24 | -------------------------------------------------------------------------------- /front/src/store/UserStore.js: -------------------------------------------------------------------------------- 1 | import {makeAutoObservable} from "mobx"; 2 | import jwt_decode from "jwt-decode"; 3 | 4 | export default class UserStore { 5 | constructor() { 6 | this._isAuth = false; 7 | this._user = {}; 8 | makeAutoObservable(this); 9 | this.checkValidToken = this.checkValidToken.bind(this); 10 | } 11 | 12 | checkValidToken() { 13 | let isExpired = false; 14 | const token = localStorage.getItem('token'); 15 | const decodedToken = jwt_decode(token); 16 | const dateNow = new Date(); 17 | 18 | if (decodedToken.exp < dateNow.getTime()){ 19 | isExpired = true; 20 | } 21 | 22 | return isExpired; 23 | } 24 | 25 | setIsAuth(bool) { 26 | this._isAuth = bool; 27 | } 28 | 29 | setUser(user) { 30 | this._user = user; 31 | } 32 | 33 | get isAuth() { 34 | return this._isAuth; 35 | } 36 | 37 | get User() { 38 | return this._user; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /backend/controllers/typeController.js: -------------------------------------------------------------------------------- 1 | const {Type} = require('./../models/models'); 2 | 3 | class TypeController { 4 | async create(req, res) { 5 | const {name} = req.body; 6 | const type = await Type.create({name}) 7 | return res.json(type) 8 | } 9 | 10 | async getAll(req, res) { 11 | const types = await Type.findAll() 12 | return res.json(types); 13 | } 14 | 15 | async delete(req, res) { 16 | try { 17 | const {id} = req.params; 18 | await Type.findOne({where:{id}}) 19 | .then( async data => { 20 | if(data) { 21 | await Type.destroy({where:{id}}).then(() => { 22 | return res.json("Type deleted"); 23 | }) 24 | } else { 25 | return res.json("This Type doesn't exist in DB"); 26 | } 27 | }) 28 | } catch (e) { 29 | return res.json(e); 30 | } 31 | } 32 | } 33 | 34 | module.exports = new TypeController(); 35 | -------------------------------------------------------------------------------- /backend/controllers/brandController.js: -------------------------------------------------------------------------------- 1 | const {Brand} = require('./../models/models'); 2 | 3 | class BranController { 4 | async create(req, res) { 5 | const {name} = req.body; 6 | 7 | const brand = await Brand.create({name}); 8 | return res.json(brand); 9 | } 10 | 11 | async getAll(req, res) { 12 | const types = await Brand.findAll(); 13 | return res.json(types); 14 | } 15 | 16 | async delete(req, res) { 17 | try { 18 | const {id} = req.params; 19 | 20 | await Brand.findOne({where:{id}}) 21 | .then( async data => { 22 | if(data) { 23 | await Brand.destroy({where:{id}}).then(() => { 24 | return res.json("Brand deleted"); 25 | }) 26 | } else { 27 | return res.json("This Brand doesn't exist in DB"); 28 | } 29 | }) 30 | } catch (e) { 31 | return res.json(e); 32 | } 33 | } 34 | } 35 | 36 | module.exports = new BranController(); 37 | -------------------------------------------------------------------------------- /backend/index.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | const host = process.env.HOST; 3 | const port = process.env.PORT; 4 | 5 | const express = require("express"); 6 | const app = express(); 7 | const cors = require("cors"); 8 | const fileUpload = require('express-fileupload'); 9 | const path = require('path'); 10 | 11 | const sequelize = require('./db/db'); 12 | const models = require("./models/models"); 13 | 14 | const router = require('./routes/index'); 15 | const errorHandler = require('./middleware/ErrorHandlingMiddleware'); 16 | 17 | //middleware 18 | app.use(cors()); 19 | app.use(express.json()); 20 | app.use(express.static(path.resolve(__dirname, 'static'))); 21 | app.use(fileUpload({})); 22 | app.use('/api', router); 23 | 24 | //Handler error, last middleware 25 | app.use(errorHandler); 26 | 27 | 28 | const start = async () => { 29 | try { 30 | await sequelize.authenticate(); 31 | await sequelize.sync(); 32 | 33 | app.listen(port, () => { 34 | console.log(`App listening at http://${host}:${port}`); 35 | }); 36 | } catch (e) { 37 | console.error(e); 38 | } 39 | } 40 | start(); 41 | 42 | 43 | -------------------------------------------------------------------------------- /front/src/http/ordersAPI.js: -------------------------------------------------------------------------------- 1 | import {$authHost, $host} from "./index"; 2 | 3 | export const sendOrder = async ({auth, mobile, basket}) => { 4 | if(auth) { 5 | const {data} = await $authHost({method:'POST', url: 'api/orders', data: {mobile, basket}}) 6 | return data; 7 | } else { 8 | const {data} = await $host({method:'POST', url: 'api/orders', data: {mobile, basket}}); 9 | return data; 10 | } 11 | } 12 | 13 | export const fetchOrders = async ({limit, page, complete}) => { 14 | const {data} = await $authHost.get(`api/orders?limit=${limit}&page=${page}&complete=${complete}`); 15 | return data; 16 | } 17 | 18 | export const fetchChangeStatusOrder = async ({complete, id}) => { 19 | const {data} = await $authHost.put('api/orders', {complete, id}); 20 | return data; 21 | } 22 | 23 | export const fetchDeleteOrder = async ({id}) => { 24 | const {data} = await $authHost({method:'DELETE', url: 'api/orders', data: {id}}); 25 | return data; 26 | } 27 | 28 | export const getOneOrderDevices = async (id) => { 29 | const {data} = await $authHost.get('api/orders/' + id); 30 | return data; 31 | } 32 | 33 | -------------------------------------------------------------------------------- /front/src/components/TypeBar.js: -------------------------------------------------------------------------------- 1 | import React, {useContext} from 'react'; 2 | import {observer} from "mobx-react-lite"; 3 | import {Context} from "../index"; 4 | import {ListGroup} from "react-bootstrap"; 5 | 6 | const TypeBar = observer(() => { 7 | const {device} = useContext(Context); 8 | 9 | const getAllDevices = () => { 10 | device.setSelectedType("all"); 11 | device.setSelectedBrand("all"); 12 | } 13 | 14 | return ( 15 | 16 | 21 | All 22 | 23 | {device.types.map(type => 24 | device.setSelectedType(type)} 29 | > 30 | {type.name} 31 | 32 | )} 33 | 34 | ); 35 | }); 36 | 37 | export default TypeBar; 38 | -------------------------------------------------------------------------------- /front/src/components/deviceItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Card, Col, Image} from "react-bootstrap"; 3 | 4 | import star from './../assets/star.png'; 5 | import {useHistory} from 'react-router-dom'; 6 | import {DEVICE_ROUTE} from "../utils/consts"; 7 | 8 | const DeviceItem = ({device}) => { 9 | const history = useHistory(); 10 | console.log(device); 11 | return ( 12 | history.push(DEVICE_ROUTE + '/' + device.id)}> 13 | 18 | 19 |
20 |
{device && device.brand.name}
21 |
22 |
{device.rating}
23 | 24 |
25 |
26 |
{device.name}
27 |
28 | 29 | ); 30 | }; 31 | 32 | export default DeviceItem; 33 | -------------------------------------------------------------------------------- /front/src/pages/BasketCard.js: -------------------------------------------------------------------------------- 1 | import React, {useContext} from 'react'; 2 | import {observer} from "mobx-react-lite"; 3 | 4 | 5 | import {Context} from "../index"; 6 | import {Button, Col, Image, Row} from "react-bootstrap"; 7 | import OneItemInBasket from "../components/oneItemInBasket"; 8 | 9 | import emptyBasket from "./../assets/emptyBasket.jpg"; 10 | import {ORDERING_ROUTE} from "../utils/consts"; 11 | import {NavLink} from "react-router-dom"; 12 | 13 | const BasketCard = observer(() => { 14 | const {basket} = useContext(Context); 15 | 16 | if(basket.Basket.length === 0) { 17 | return ( 18 |
19 | 20 |
Empty shopping basket
21 |
22 | ) 23 | } 24 | 25 | return ( 26 | <> 27 |
28 | 29 | 30 | 31 | 32 | 33 | {basket.Basket.map(device => )} 34 | 35 | 36 | 37 | ); 38 | }); 39 | 40 | export default BasketCard; 41 | -------------------------------------------------------------------------------- /front/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "front", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^4.11.4", 7 | "@material-ui/lab": "^4.0.0-alpha.58", 8 | "@testing-library/jest-dom": "^5.11.4", 9 | "@testing-library/react": "^11.1.0", 10 | "@testing-library/user-event": "^12.1.10", 11 | "axios": "^0.21.1", 12 | "bootstrap": "^4.6.0", 13 | "jwt-decode": "^3.1.2", 14 | "mobx": "^6.3.2", 15 | "mobx-react-lite": "^3.2.0", 16 | "node-sass": "^4.14.1", 17 | "react": "^17.0.2", 18 | "react-bootstrap": "^1.6.1", 19 | "react-dom": "^17.0.2", 20 | "react-rating-stars-component": "^2.2.0", 21 | "react-router-dom": "^5.2.0", 22 | "react-scripts": "4.0.3", 23 | "web-vitals": "^1.0.1" 24 | }, 25 | "scripts": { 26 | "start": "react-scripts start", 27 | "build": "react-scripts build", 28 | "test": "react-scripts test", 29 | "eject": "react-scripts eject" 30 | }, 31 | "eslintConfig": { 32 | "extends": [ 33 | "react-app", 34 | "react-app/jest" 35 | ] 36 | }, 37 | "browserslist": { 38 | "production": [ 39 | ">0.2%", 40 | "not dead", 41 | "not op_mini all" 42 | ], 43 | "development": [ 44 | "last 1 chrome version", 45 | "last 1 firefox version", 46 | "last 1 safari version" 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /front/src/components/modals/CreateType.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {Button, Form, Modal} from "react-bootstrap"; 3 | import {createType} from "../../http/deviceAPI"; 4 | 5 | const CreateType = ({show, onHide}) => { 6 | const [value, setValue] = useState(''); 7 | const addType = () => { 8 | createType({name: value}).then(() => { 9 | setValue('') 10 | onHide(); 11 | }); 12 | }; 13 | 14 | return ( 15 | 21 | 22 | 23 | Add new Type 24 | 25 | 26 | 27 |
28 | setValue(e.target.value)} 31 | value={value} 32 | /> 33 | 34 |
35 | 36 | 37 | 38 | 39 |
40 | ); 41 | }; 42 | 43 | export default CreateType; 44 | -------------------------------------------------------------------------------- /front/src/components/modals/CreateBrand.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {Button, Form, Modal} from "react-bootstrap"; 3 | import {createBrand} from "../../http/deviceAPI"; 4 | 5 | const CreateBrand = ({show, onHide}) => { 6 | const [value, setValue] = useState(''); 7 | const addBrand = () => { 8 | createBrand({name: value}).then(data => { 9 | setValue('') 10 | onHide(); 11 | }); 12 | }; 13 | 14 | return ( 15 | 21 | 22 | 23 | Add new Brand 24 | 25 | 26 | 27 |
28 | setValue(e.target.value)} 32 | /> 33 | 34 |
35 | 36 | 37 | 38 | 39 |
40 | ); 41 | }; 42 | 43 | export default CreateBrand; 44 | -------------------------------------------------------------------------------- /front/src/pages/Ordering.js: -------------------------------------------------------------------------------- 1 | import React, {useContext, useState} from 'react'; 2 | import {Button, Col, Form, Row} from "react-bootstrap"; 3 | import {Context} from "../index"; 4 | import {sendOrder} from "../http/ordersAPI"; 5 | import {useHistory} from "react-router-dom"; 6 | import {SHOP_ROUTE} from "../utils/consts"; 7 | 8 | const Ordering = () => { 9 | const {basket, user} = useContext(Context); 10 | const [phone, setPhone] = useState(null); 11 | const history = useHistory(); 12 | 13 | const buy = () => { 14 | let order = { 15 | mobile: phone, 16 | basket: basket.Basket 17 | } 18 | 19 | if(user.isAuth) { 20 | order.auth = true; 21 | } 22 | 23 | sendOrder(order).then(data => { 24 | console.log(data); 25 | basket.setDeleteAllDeviceFromBasket(); 26 | history.push(SHOP_ROUTE); 27 | }); 28 | } 29 | return ( 30 | <> 31 |
32 | setPhone(e.target.value)} 36 | /> 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | ); 45 | }; 46 | 47 | export default Ordering; 48 | -------------------------------------------------------------------------------- /front/src/components/NavBar/preesent-components/trueAuth.js: -------------------------------------------------------------------------------- 1 | import {Button, Nav} from "react-bootstrap"; 2 | import React, {useContext} from "react"; 3 | import {Context} from "../../../index"; 4 | import {useHistory} from "react-router-dom"; 5 | import {ADMIN_ROUTE, ORDERS_ROUTE} from "../../../utils/consts"; 6 | import BasketNavBar from "../BasketNavBar"; 7 | 8 | const TrueAuth = () => { 9 | const {user, basket} = useContext(Context); 10 | const history = useHistory(); 11 | 12 | const logOut = () => { 13 | user.setUser({}); 14 | user.setIsAuth(false); 15 | localStorage.removeItem('token'); 16 | basket.resetBasket(); 17 | } 18 | 19 | return ( 20 | 44 | ); 45 | }; 46 | 47 | export default TrueAuth; 48 | -------------------------------------------------------------------------------- /front/src/routes.js: -------------------------------------------------------------------------------- 1 | import { 2 | ADMIN_ROUTE, 3 | BASKET_ROUTE, DEVICE_EDIT_ROUTE, 4 | DEVICE_ROUTE, 5 | LOGIN_ROUTE, ORDERING_ROUTE, 6 | ORDERS_ROUTE, 7 | REGISTRATION_ROUTE, 8 | SHOP_ROUTE 9 | } from './utils/consts'; 10 | 11 | import Admin from "./pages/Admin"; 12 | import Orders from "./pages/Orders"; 13 | import Shop from "./pages/Shop"; 14 | import Auth from "./pages/Auth"; 15 | import DevicePage from "./pages/DevicePage"; 16 | import BasketCard from "./pages/BasketCard"; 17 | import OneOrder from "./pages/OneOrder"; 18 | import DevicePageEdit from "./pages/DevicePageEdit"; 19 | import Ordering from "./pages/Ordering"; 20 | 21 | 22 | export const authRouters = [ 23 | { 24 | path: ADMIN_ROUTE, 25 | Component: Admin 26 | }, 27 | { 28 | path: ORDERS_ROUTE, 29 | Component: Orders 30 | }, 31 | { 32 | path: ORDERS_ROUTE + '/:id', 33 | Component: OneOrder 34 | }, 35 | { 36 | path: DEVICE_EDIT_ROUTE + '/:id', 37 | Component: DevicePageEdit 38 | }, 39 | 40 | ]; 41 | 42 | export const publicRouters = [ 43 | { 44 | path: ORDERING_ROUTE, 45 | Component: Ordering 46 | }, 47 | { 48 | path: SHOP_ROUTE, 49 | Component: Shop 50 | }, 51 | { 52 | path: LOGIN_ROUTE, 53 | Component: Auth 54 | }, 55 | { 56 | path: REGISTRATION_ROUTE, 57 | Component: Auth 58 | }, 59 | { 60 | path: DEVICE_ROUTE + '/:id', 61 | Component: DevicePage 62 | }, 63 | { 64 | path: BASKET_ROUTE, 65 | Component: BasketCard 66 | }, 67 | ]; 68 | -------------------------------------------------------------------------------- /front/src/store/DeviceStore.js: -------------------------------------------------------------------------------- 1 | import {makeAutoObservable} from "mobx"; 2 | 3 | export default class DeviceStore { 4 | constructor() { 5 | this._types = []; 6 | this._brands = []; 7 | this._devices = []; 8 | this._selectedType = {}; 9 | this._selectedBrand = {}; 10 | this._page = 1; 11 | this._totalCount = 0; 12 | this._limit = 9; 13 | makeAutoObservable(this); 14 | } 15 | 16 | setSelectedType(selectedType) { 17 | this.setPage(1); 18 | this._selectedType = selectedType; 19 | } 20 | setSelectedBrand(selectedBrand) { 21 | this.setPage(1); 22 | this._selectedBrand = selectedBrand; 23 | } 24 | setTypes(types) { 25 | this._types = types; 26 | } 27 | setBrands(brands) { 28 | this._brands = brands; 29 | } 30 | setDevices(devices) { 31 | this._devices = devices; 32 | } 33 | setPage(page) { 34 | this._page = page; 35 | } 36 | setTotalCount(totalCount) { 37 | this._totalCount = totalCount; 38 | } 39 | 40 | get types() { 41 | return this._types; 42 | } 43 | get brands() { 44 | return this._brands; 45 | } 46 | get devices() { 47 | return this._devices; 48 | } 49 | get selectedType() { 50 | return this._selectedType; 51 | } 52 | get selectedBrand() { 53 | return this._selectedBrand; 54 | } 55 | get page() { 56 | return this._page; 57 | } 58 | get totalCount() { 59 | return this._totalCount; 60 | } 61 | get limit() { 62 | return this._limit; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /front/src/App.js: -------------------------------------------------------------------------------- 1 | import React, {useContext, useEffect, useState} from "react"; 2 | import {BrowserRouter} from "react-router-dom"; 3 | import {observer} from "mobx-react-lite"; 4 | 5 | import AppRouter from "./components/AppRouter"; 6 | import NavBar from "./components/NavBar/NavBar"; 7 | import {Container, Spinner} from "react-bootstrap"; 8 | import {Context} from "./index"; 9 | import {check} from "./http/userApi"; 10 | import {getDeviceFromBasket} from "./http/deviceAPI"; 11 | 12 | const App = observer(() => { 13 | const {user, basket} = useContext(Context); 14 | const [loading, setLoading] = useState(false); 15 | 16 | //check authorization 17 | useEffect(() => { 18 | if(localStorage.getItem('token')) { 19 | setLoading(true); 20 | check().then(data => { 21 | user.setUser(data); 22 | user.setIsAuth(true); 23 | }).finally(() => { 24 | setLoading(false); 25 | }) 26 | } 27 | }, [user]); 28 | 29 | 30 | //Loading Basket 31 | useEffect(() => { 32 | if(user.isAuth === false) { 33 | basket.setDeleteAllDeviceFromBasket(); 34 | const savedBasket = JSON.parse(localStorage.getItem("basket")); 35 | for (let key in savedBasket) { 36 | basket.setBasket(savedBasket[key]); 37 | } 38 | } else if(user.isAuth === true){ 39 | basket.setDeleteAllDeviceFromBasket(); 40 | getDeviceFromBasket().then(data => { 41 | for (let key in data) { 42 | basket.setBasket(data[key], true); 43 | } 44 | }) 45 | } 46 | }, [basket, user.isAuth]); 47 | 48 | if(loading) { 49 | return 50 | } 51 | 52 | return ( 53 | 54 | 55 | 56 | 57 | 58 | 59 | ); 60 | }); 61 | export default App; 62 | -------------------------------------------------------------------------------- /backend/controllers/userController.js: -------------------------------------------------------------------------------- 1 | const ApiError = require('./../error/apiError'); 2 | const bcrypt = require('bcrypt'); 3 | const {User, Basket} = require('./../models/models'); 4 | const jwt = require('jsonwebtoken'); 5 | 6 | const generateJwt = (id, email, role) => { 7 | return jwt.sign( 8 | {id, email, role}, 9 | process.env.SECRET_KEY, 10 | {expiresIn: '24h'} 11 | ); 12 | } 13 | 14 | class UserController { 15 | async registration(req, res, next) { 16 | const {email, password, role} = req.body; 17 | if (!email || !password) { 18 | return next(ApiError.badRequest('Not valid email or password')); 19 | } 20 | 21 | const candidate = await User.findOne({where: {email}}); 22 | if(candidate) { 23 | return next(ApiError.badRequest('User with this email already exists')); 24 | } 25 | 26 | const hashPassword = await bcrypt.hash(password, 5); 27 | const user = await User.create({email, role, password: hashPassword}); 28 | const basket = await Basket.create({userId: user.id}); 29 | const token = generateJwt(user.id, user.email, user.role); 30 | return res.json({token}); 31 | } 32 | 33 | 34 | async login(req, res, next) { 35 | const {email, password} = req.body; 36 | const user = await User.findOne({where: {email}}); 37 | if(!user) { 38 | return next(ApiError.internal('User with this name didn\'t find')); 39 | } 40 | let comparePassword = bcrypt.compareSync(password, user.password); 41 | if(!comparePassword) { 42 | return next(ApiError.internal('Not valid password')); 43 | } 44 | const token = generateJwt(user.id, user.email, user.role); 45 | return res.json({token}); 46 | } 47 | 48 | 49 | async check(req, res) { 50 | const token = generateJwt(req.user.id, req.user.email, req.user.role) 51 | return res.json({token}) 52 | }} 53 | 54 | 55 | module.exports = new UserController(); 56 | -------------------------------------------------------------------------------- /backend/controllers/ratingController.js: -------------------------------------------------------------------------------- 1 | const {Rating, Device} = require('./../models/models'); 2 | const jwt = require('jsonwebtoken'); 3 | 4 | class RatingController { 5 | async addRating(req, res) { 6 | try { 7 | const {rate, deviceId} = req.body; 8 | const token = req.headers.authorization.split(' ')[1]; 9 | const user = jwt.verify(token, process.env.SECRET_KEY); 10 | await Rating.create({rate, deviceId, userId: user.id}); 11 | 12 | let rating = await Rating.findAndCountAll({ 13 | where: { 14 | deviceId 15 | }, 16 | }); 17 | 18 | let allRating = 0; 19 | let middleRating; 20 | rating.rows.forEach(item => allRating += item.rate); 21 | middleRating = Number(allRating) / Number(rating.count); 22 | 23 | await Device.update( 24 | {rating: middleRating}, 25 | {where: {id: deviceId}} 26 | ); 27 | 28 | return res.json("Rating success added"); 29 | } catch (e) { 30 | console.error(e); 31 | } 32 | } 33 | 34 | async checkRating(req, res) { 35 | try { 36 | const {deviceId} = req.body; 37 | const token = req.headers.authorization.split(' ')[1]; 38 | const user = jwt.verify(token, process.env.SECRET_KEY); 39 | const checkRating = await Rating.findOne({where: {deviceId, userId: user.id}}); 40 | const checkDevices = await Device.findOne({where: {id: deviceId}}); 41 | if (!checkDevices) { 42 | return res.json({allow: false}); 43 | } else if(checkRating && checkDevices) { 44 | return res.json({allow: false}); 45 | } 46 | return res.json({allow: true}); 47 | } catch (e) { 48 | return res.status(401).json("Something going wrong in checkAddRatingMiddleware.js"); 49 | } 50 | } 51 | } 52 | 53 | module.exports = new RatingController(); 54 | -------------------------------------------------------------------------------- /front/src/pages/Shop.js: -------------------------------------------------------------------------------- 1 | import React, {useContext, useEffect} from 'react'; 2 | import {Col, Container, Row} from "react-bootstrap"; 3 | import TypeBar from "../components/TypeBar"; 4 | import BrandBar from "../components/BrandBar"; 5 | import DeviceList from "../components/DeviceList"; 6 | import {observer} from "mobx-react-lite"; 7 | import {Context} from "../index"; 8 | import {fetchBrands, fetchDevice, fetchTypes} from "../http/deviceAPI"; 9 | import Pages from "../components/Pages"; 10 | 11 | const Shop = observer(() => { 12 | const {device} = useContext(Context); 13 | 14 | useEffect(() => { 15 | fetchTypes().then(data => device.setTypes(data)); 16 | fetchBrands().then(data => device.setBrands(data)); 17 | fetchDevice(null, null, 1, 9).then(data => { 18 | device.setDevices(data.rows); 19 | device.setTotalCount(data.count); 20 | }); 21 | }, []); 22 | 23 | useEffect( 24 | () => { 25 | if(device.selectedType === "all") { 26 | fetchDevice(null, device.selectedBrand.id, device.page, 9).then(data => { 27 | device.setDevices(data.rows); 28 | device.setTotalCount(data.count); 29 | }); 30 | } else { 31 | fetchDevice(device.selectedType.id, device.selectedBrand.id, device.page, 9).then(data => { 32 | device.setDevices(data.rows); 33 | device.setTotalCount(data.count); 34 | }); 35 | } 36 | }, [device.page, device.selectedType, device.selectedBrand], 37 | ); 38 | 39 | return ( 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | ); 53 | }); 54 | 55 | export default Shop; 56 | -------------------------------------------------------------------------------- /front/src/pages/OneOrder.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import {Col, Container, Image, Row, Spinner} from "react-bootstrap"; 3 | import {useParams} from "react-router-dom"; 4 | import {getOneOrderDevices} from "../http/ordersAPI"; 5 | 6 | const OneOrder = () => { 7 | const {id} = useParams(); 8 | const [loading, setLoading] = useState(true); 9 | const [order, setOrder] = useState([]); 10 | 11 | useEffect(() => { 12 | getOneOrderDevices(id).then(data => { 13 | setOrder(data); 14 | setLoading(false); 15 | console.log(order); 16 | }) 17 | }, []); 18 | 19 | if(loading) { 20 | return 21 | } 22 | 23 | //Format date (createdAt) 24 | const formatDate = (propsDate) => { 25 | const date = new Date(Date.parse(propsDate)); 26 | const options = { 27 | weekday: "short", 28 | hour: 'numeric', 29 | minute: 'numeric', 30 | year: 'numeric', 31 | month: 'numeric', 32 | day: 'numeric', 33 | timezone: 'UTC' 34 | }; 35 | return date.toLocaleString("en-US", options); 36 | } 37 | 38 | return ( 39 | 40 | Order id: {id}
41 | Complete: {order?.descr.complete ? "Completed" : "Not complete"}
42 | User: {order?.descr.userId ? order.descr.userId : "User didn't register"}
43 | Created: {formatDate(order?.descr.createdAt)}
44 | {order?.descr.complete ? formatDate(order.descr.complete.updatedAt) : false } 45 | Mobile: {order?.descr.mobile} 46 |
47 | 48 | {order?.devices.map( ({count,descr}, i) => { 49 | return ( 50 | 51 | 52 | 53 | 54 | 55 | Brand: {descr.brand.name}
56 | Type: {descr.type.name}
57 | Name: {descr.name}
58 | Price: {descr.price} RUB
59 | Count: {count}
60 | Total price: {count * descr.price} RUB 61 | 62 |
63 | ) 64 | })} 65 | 66 |
67 | ); 68 | }; 69 | 70 | export default OneOrder; 71 | -------------------------------------------------------------------------------- /front/src/http/deviceAPI.js: -------------------------------------------------------------------------------- 1 | import {$authHost, $host} from "./index"; 2 | 3 | export const createType = async (type) => { 4 | const {data} = await $authHost.post('api/type', type); 5 | return data; 6 | } 7 | 8 | export const fetchTypes = async () => { 9 | const {data} = await $host.get('api/type'); 10 | return data; 11 | } 12 | 13 | export const deleteType = async (id) => { 14 | const {data} = await $authHost({method:'DELETE', url:'api/type/'+id}); 15 | return data; 16 | } 17 | 18 | export const createBrand = async (brand) => { 19 | const {data} = await $authHost.post('api/brand', brand); 20 | return data; 21 | } 22 | 23 | export const fetchBrands = async () => { 24 | const {data} = await $host.get('api/brand'); 25 | return data; 26 | } 27 | 28 | export const deleteBrand = async (id) => { 29 | const {data} = await $authHost({method:'DELETE', url:'api/brand/'+id}); 30 | return data; 31 | } 32 | 33 | export const createDevice = async (brand) => { 34 | const {data} = await $authHost.post('api/device', brand); 35 | return data; 36 | } 37 | 38 | export const fetchDevice = async (typeId, brandId, page, limit = 9) => { 39 | const {data} = await $host.get('api/device', {params: { 40 | typeId, brandId, page, limit 41 | }}); 42 | return data; 43 | } 44 | 45 | export const fetchOneDevice = async (id) => { 46 | const {data} = await $host.get(`api/device/${id}`); 47 | return data; 48 | } 49 | 50 | export const fetchDeleteDevice = async (id) => { 51 | const {data} = await $authHost({method:'DELETE', url:`api/device/${id}`}); 52 | return data; 53 | } 54 | 55 | export const updateDevices = async (id, body) => { 56 | const {data} = await $authHost({method:'PUT', url:`api/device/${id}`, data: body}); 57 | return data; 58 | } 59 | 60 | export const getAllDevicesInAdminPage = async (name, page = 1, filter = "All") => { 61 | const {data} = await $authHost({method:'GET', url:`api/device/search?page=${page}&name=${name}&filter=${filter}`}); 62 | return data; 63 | } 64 | 65 | export const addDeviceToBasket = async (device) => { 66 | const {data} = await $authHost.post('api/basket', device); 67 | return data; 68 | } 69 | 70 | export const getDeviceFromBasket = async () => { 71 | const {data} = await $authHost.get('api/basket'); 72 | return data; 73 | } 74 | 75 | export const deleteDeviceFromBasket = async (id) => { 76 | const {data} = await $authHost.delete(`api/basket/${id}`); 77 | return data; 78 | } 79 | 80 | export const addRating = async (body) => { 81 | const {data} = await $authHost.post('api/rating', body); 82 | return data; 83 | } 84 | 85 | export const checkRating = async (body) => { 86 | const {data} = await $authHost.post('api/rating/check-rating', body); 87 | return data; 88 | } 89 | -------------------------------------------------------------------------------- /backend/controllers/basketController.js: -------------------------------------------------------------------------------- 1 | const {Basket, BasketDevice, Device, DeviceInfo} = require('./../models/models'); 2 | const jwt = require('jsonwebtoken'); 3 | const { Op } = require("sequelize"); 4 | 5 | class BasketController { 6 | async addDevice(req, res) { 7 | try { 8 | const {id} = req.body; 9 | const token = req.headers.authorization.split(' ')[1]; 10 | const user = jwt.verify(token, process.env.SECRET_KEY); 11 | const basket = await Basket.findOne({where: {userId: user.id}}); 12 | await BasketDevice.create({basketId : basket.id, deviceId: id}); 13 | return res.json("Product added in card"); 14 | } catch (e) { 15 | console.error(e); 16 | } 17 | } 18 | 19 | async getDevices(req, res) { 20 | try { 21 | const token = req.headers.authorization.split(' ')[1]; 22 | const user = jwt.verify(token, process.env.SECRET_KEY); 23 | const {id} = await Basket.findOne({where: {userId: user.id}}); 24 | const basket = await BasketDevice.findAll({where: {basketId: id}}); 25 | 26 | const basketArr = []; 27 | for(let i = 0; i < basket.length; i++) { 28 | const basketDevice = await Device.findOne({ 29 | where: { 30 | id: basket[i].deviceId, 31 | 32 | }, 33 | include: { 34 | model: DeviceInfo, as: "info", 35 | where: { 36 | deviceId: basket[i].deviceId, 37 | [Op.or]: [ 38 | { 39 | deviceId: { 40 | [Op.not]: null 41 | } 42 | } 43 | ], 44 | }, 45 | required: false} 46 | }); 47 | basketArr.push(basketDevice); 48 | } 49 | 50 | return res.json(basketArr); 51 | } catch (e) { 52 | console.error(e); 53 | } 54 | } 55 | 56 | async deleteDevice(req, res) { 57 | try { 58 | const {id} = req.params; 59 | const user = req.user; 60 | 61 | await Basket.findOne({where: {userId: user.id}}).then(async userBasket => { 62 | if(userBasket.userId === user.id) { 63 | await BasketDevice.destroy({where: {basketId: userBasket.id, deviceId: id}}) 64 | } 65 | return res.json(`You haven't access for delete the device(${id}) from basket that didn't belong to you`); 66 | }); 67 | return res.json("Product deleted form your card"); 68 | } catch (e) { 69 | console.error(e); 70 | } 71 | } 72 | } 73 | 74 | module.exports = new BasketController(); 75 | -------------------------------------------------------------------------------- /front/src/store/BasketStore.js: -------------------------------------------------------------------------------- 1 | import {makeAutoObservable} from "mobx"; 2 | import {deleteDeviceFromBasket} from "../http/deviceAPI"; 3 | 4 | export default class BasketStoreStore { 5 | constructor() { 6 | this._totalPrice = 0; 7 | this._basket = []; 8 | makeAutoObservable(this); 9 | } 10 | 11 | async setDeleteItemBasket(device, isAuth = false) { 12 | if(isAuth) { 13 | await deleteDeviceFromBasket(device.id).then(() => { 14 | this._basket = this._basket.filter(item => item.id !== device.id); 15 | this._totalPrice -= device.price * device.count; 16 | }); 17 | } else { 18 | this._basket = this._basket.filter(item => item.id !== device.id); 19 | this._totalPrice -= device.price * device.count; 20 | 21 | localStorage.setItem("basket", JSON.stringify(this._basket)); 22 | } 23 | } 24 | 25 | setBasket(item, isAuth = false) { 26 | const checkDeviceInBasket = this._basket.findIndex(device => device.id === item.id); 27 | if(checkDeviceInBasket < 0) { 28 | this._basket = [...this._basket, { count: 1, ...item}]; 29 | let totalPrice = 0; 30 | this._basket.forEach(device => totalPrice += Number(device.price * device.count)); 31 | this._totalPrice = totalPrice; 32 | } 33 | 34 | if(!isAuth) { 35 | localStorage.setItem("basket", JSON.stringify(this._basket)); 36 | } 37 | } 38 | 39 | setDeleteAllDeviceFromBasket() { 40 | this._totalPrice = 0; 41 | return this._basket = []; 42 | } 43 | 44 | setCountDevice(deviceId, action, isAuth = false) { 45 | const itemInd = this._basket.findIndex(item => item.id === deviceId); 46 | const itemInState = this._basket.find(device => device.id === deviceId); 47 | if (action === "+") { 48 | const newItem = { 49 | ...itemInState, 50 | count: ++itemInState.count 51 | } 52 | this._basket = [...this._basket.slice(0, itemInd), newItem, ...this._basket.slice(itemInd + 1)] 53 | } else { 54 | const newItem = { 55 | ...itemInState, 56 | count: itemInState.count === 1 ? 1 : --itemInState.count 57 | } 58 | this._basket = [...this._basket.slice(0, itemInd), newItem, ...this._basket.slice(itemInd + 1)] 59 | } 60 | 61 | if(!isAuth) { 62 | localStorage.setItem("basket", JSON.stringify(this._basket)); 63 | } 64 | 65 | let totalPrice = 0; 66 | this._basket.forEach(device => totalPrice += Number(device.price * device.count)); 67 | this._totalPrice = totalPrice; 68 | } 69 | 70 | resetBasket() { 71 | this._basket = []; 72 | this._totalPrice = 0; 73 | localStorage.removeItem('basket'); 74 | } 75 | 76 | 77 | get Basket() { 78 | return this._basket; 79 | } 80 | 81 | get Price() { 82 | return this._totalPrice; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /front/src/pages/Auth.js: -------------------------------------------------------------------------------- 1 | import React, {useContext, useState} from 'react'; 2 | import {NavLink, useLocation, useHistory} from "react-router-dom"; 3 | 4 | import {Button, Card, Container, Form, Row} from "react-bootstrap"; 5 | import {LOGIN_ROUTE, REGISTRATION_ROUTE, SHOP_ROUTE} from "../utils/consts"; 6 | import {login, registration} from "../http/userApi"; 7 | import {observer} from "mobx-react-lite"; 8 | import {Context} from "../index"; 9 | 10 | const Auth = observer(() => { 11 | const {user} = useContext(Context); 12 | const history = useHistory(); 13 | const location = useLocation(); 14 | const isLogin = location.pathname === LOGIN_ROUTE; 15 | const [email, setEmail] = useState(''); 16 | const [password, setPassword] = useState(''); 17 | 18 | const click = async () => { 19 | try { 20 | let data; 21 | if(isLogin) { 22 | data = await login(email, password); 23 | } else { 24 | data = await registration(email, password); 25 | } 26 | user.setUser(data); 27 | user.setIsAuth(true); 28 | history.push(SHOP_ROUTE); 29 | } catch (e) { 30 | alert(e.response.data.message); 31 | } 32 | }; 33 | 34 | return ( 35 | 39 | 40 |

{isLogin ? "Authorization" : "Registration"}

41 |
42 | setEmail(e.target.value)} 47 | /> 48 | setPassword(e.target.value)} 53 | type="password" 54 | /> 55 | 56 | {isLogin ? 57 |
58 | Haven't account? Sign In 59 |
60 | : 61 |
62 | Has account? Login 63 |
64 | } 65 | 66 | 73 |
74 | 75 |
76 | 77 |
78 | ); 79 | }); 80 | 81 | export default Auth; 82 | -------------------------------------------------------------------------------- /backend/models/models.js: -------------------------------------------------------------------------------- 1 | const sequelize = require('./../db/db'); 2 | const {DataTypes} = require('sequelize'); 3 | 4 | const User = sequelize.define('user', { 5 | id: {type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true}, 6 | email: {type: DataTypes.STRING, unique: true}, 7 | password: {type: DataTypes.STRING}, 8 | role: {type: DataTypes.STRING, defaultValue: "USER"}, 9 | }); 10 | 11 | const Basket = sequelize.define('basket', { 12 | id: {type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true}, 13 | }); 14 | 15 | const BasketDevice = sequelize.define('basket_device', { 16 | id: {type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true}, 17 | deviceId: {type: DataTypes.INTEGER}, 18 | }); 19 | 20 | const Device = sequelize.define('device', { 21 | id: {type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true}, 22 | name: {type: DataTypes.STRING, unique: true, allowNull: false}, 23 | price: {type: DataTypes.INTEGER, allowNull: false}, 24 | rating: {type: DataTypes.INTEGER, defaultValue: 0}, 25 | img: {type: DataTypes.STRING, allowNull: false}, 26 | }); 27 | 28 | const Type = sequelize.define('type', { 29 | id: {type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true}, 30 | name: {type: DataTypes.STRING, unique: true, allowNull: false}, 31 | }); 32 | 33 | const Brand = sequelize.define('brand', { 34 | id: {type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true}, 35 | name: {type: DataTypes.STRING, unique: true, allowNull: false}, 36 | }); 37 | 38 | const Rating = sequelize.define('rating', { 39 | id: {type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true}, 40 | rate: {type: DataTypes.INTEGER, allowNull: false}, 41 | }); 42 | 43 | const DeviceInfo = sequelize.define('device_info', { 44 | id: {type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true}, 45 | title: {type: DataTypes.STRING, allowNull: false}, 46 | description: {type: DataTypes.STRING, allowNull: false}, 47 | }); 48 | 49 | const TypeBrand = sequelize.define('type_brand', { 50 | id: {type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true}, 51 | }) 52 | 53 | const Orders = sequelize.define('orders', { 54 | id: {type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true}, 55 | complete: {type: DataTypes.BOOLEAN, defaultValue: false}, 56 | mobile: {type: DataTypes.STRING(25), allowNull: false}, 57 | userId: {type: DataTypes.INTEGER, allowNull: true}, 58 | }) 59 | 60 | const OrderDevice = sequelize.define('order_device', { 61 | id: {type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true}, 62 | deviceId: {type: DataTypes.INTEGER, allowNull: false}, 63 | orderId: {type: DataTypes.INTEGER, allowNull: false}, 64 | count: {type: DataTypes.INTEGER, allowNull: false}, 65 | }) 66 | 67 | User.hasOne(Basket); 68 | Basket.belongsTo(User); 69 | 70 | User.hasMany(Rating); 71 | Rating.belongsTo(User); 72 | 73 | User.hasMany(Orders); 74 | Orders.belongsTo(User, 75 | { 76 | foreignKey: { name: 'userId' }, 77 | onDelete: 'CASCADE', 78 | } 79 | ); 80 | 81 | Orders.hasMany(OrderDevice); 82 | OrderDevice.belongsTo(Orders, 83 | { 84 | foreignKey: { name: 'orderId' }, 85 | onDelete: 'CASCADE', 86 | onUpdate: 'CASCADE', 87 | } 88 | ); 89 | 90 | Basket.hasMany(BasketDevice); 91 | BasketDevice.belongsTo(Basket); 92 | 93 | Type.hasMany(Device); 94 | Device.belongsTo(Type); 95 | 96 | Type.hasMany(Device); 97 | Device.belongsTo(Type); 98 | 99 | Brand.hasMany(Device); 100 | Device.belongsTo(Brand); 101 | 102 | Device.hasMany(Rating); 103 | Rating.belongsTo(Device); 104 | 105 | Device.hasMany(DeviceInfo, {as: 'info'}); 106 | DeviceInfo.belongsTo(Device); 107 | 108 | Type.belongsToMany(Brand, {through: TypeBrand}); 109 | Brand.belongsToMany(Type, {through: TypeBrand}); 110 | 111 | 112 | module.exports = { 113 | User, 114 | Basket, 115 | BasketDevice, 116 | Device, 117 | Type, 118 | Brand, 119 | Rating, 120 | TypeBrand, 121 | DeviceInfo, 122 | Orders, 123 | OrderDevice 124 | } 125 | 126 | -------------------------------------------------------------------------------- /front/src/pages/DevicePage.js: -------------------------------------------------------------------------------- 1 | import React, {useContext, useEffect, useState} from 'react'; 2 | import {Button, Card, Col, Container, Image, Row} from "react-bootstrap"; 3 | import bigStar from './../assets/star.png'; 4 | import {useParams} from 'react-router-dom'; 5 | import {addDeviceToBasket, addRating, checkRating, fetchOneDevice} from "../http/deviceAPI"; 6 | import {Context} from "../index"; 7 | import {observer} from "mobx-react-lite"; 8 | import RatingStars from "../components/ratingStars"; 9 | 10 | const DevicePage = observer(() => { 11 | const {user, basket} = useContext(Context); 12 | const [device, setDevice] = useState({info: []}); 13 | const [resRate, setResRate] = useState(""); 14 | const [isAccessRating, setSsAccessRating] = useState(false); 15 | const {id} = useParams(); 16 | 17 | 18 | useEffect( () => { 19 | fetchOneDevice(id).then(data => setDevice(data)); 20 | if(user.isAuth) { 21 | checkRating({deviceId: id}).then(res => setSsAccessRating(res.allow)); 22 | } 23 | },[id, resRate]); 24 | 25 | const isDeviceInBasket = () => { 26 | const findDevice = basket.Basket.findIndex(item => Number(item.id) === Number(device.id)); 27 | return findDevice < 0; 28 | } 29 | 30 | const addDeviceInBasket = (device) => { 31 | if(user.isAuth) { 32 | addDeviceToBasket(device).then(() => basket.setBasket(device, true)) 33 | } else { 34 | basket.setBasket(device); 35 | } 36 | } 37 | 38 | const ratingChanged = (rate) => { 39 | addRating({ 40 | rate, 41 | deviceId: id 42 | }).then(res => { 43 | setResRate(res); 44 | }); 45 | }; 46 | 47 | return ( 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |

{device.name}

56 |
60 | {device?.rating || 0} 61 |
62 | 68 | {resRate} 69 |
70 | 71 | 72 | 76 |

{device?.price || 0} RUB

77 | { isDeviceInBasket() ? 78 | 79 | : 80 | 81 | } 82 | 83 |
84 | 85 |
86 | 87 |

Characteristics

88 | {device.info.map( (info, index) => 89 | 90 | {info.title}: {info.description} 91 | 92 | )} 93 |
94 |
95 | ); 96 | }); 97 | 98 | export default DevicePage; 99 | 100 | 101 | /** 102 | * @param {{rating}} rating of device 103 | * @param {{price}} price of device 104 | */ 105 | -------------------------------------------------------------------------------- /front/src/pages/Orders.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import {Col, Container, Dropdown, ListGroup, Pagination, Row, Spinner} from "react-bootstrap"; 3 | import {fetchOrders} from "../http/ordersAPI"; 4 | import ItemOneOrderInAdmin from "../components/itemOneorderInAdmin"; 5 | 6 | const Orders = () => { 7 | const [loading, setLoading] = useState(false); 8 | const [orders, setOrders] = useState([]); 9 | const [currentPage, setCurrentPage] = useState(1); 10 | const [count, setCount] = useState(0); 11 | const [filter, setFilter] = useState("All"); 12 | const [rerender, setRerender] = useState(false); 13 | 14 | //pagination 15 | const limit = 5; 16 | const pageCount = Math.ceil(Number(count) / limit); 17 | const pages = []; 18 | 19 | useEffect(() => { 20 | fetchOrders({limit, page: 1}).then(data => { 21 | setOrders(data); 22 | setLoading(false); 23 | setCount(data.count); 24 | }) 25 | }, []); 26 | 27 | useEffect(() => { 28 | setLoading(true); 29 | fetchOrders({limit, page: currentPage}).then(data => { 30 | setOrders(data); 31 | setLoading(false); 32 | }) 33 | }, [currentPage]); 34 | 35 | useEffect(() => { 36 | setLoading(true); 37 | fetchOrders({limit, page: 1, complete: filter}).then(data => { 38 | setOrders(data); 39 | setLoading(false); 40 | setCount(data.count); 41 | setCurrentPage(1); 42 | }) 43 | }, [filter]); 44 | 45 | //re-render after change status, or delete some order 46 | useEffect(() => { 47 | setLoading(true); 48 | fetchOrders({limit, page: currentPage, complete: filter}).then(data => { 49 | setOrders(data); 50 | setLoading(false); 51 | setCount(data.count); 52 | setCurrentPage(1); 53 | }) 54 | }, [rerender]); 55 | 56 | const reRender = () => { 57 | setRerender(!rerender); 58 | } 59 | 60 | if(loading) { 61 | return 62 | } 63 | 64 | for (let number = 1; number < pageCount + 1; number++) { 65 | pages.push( 66 | setCurrentPage(number)}> 67 | {number} 68 | 69 | ); 70 | } 71 | 72 | return ( 73 | 74 | 75 | 76 |
Filter:
77 | 78 | 79 | {filter} 80 | 81 | 82 | 83 | {filter === "all" ? All : setFilter("all")}>All} 84 | {filter === "completed" ? Completed : setFilter("completed")}>Completed} 85 | {filter === "not-completed" ? Not Completed : setFilter("not-completed")}>Not Completed} 86 | 87 | 88 | 89 |
90 | 91 | {orders.rows?.map( ({id, complete, mobile, createdAt, updatedAt, userId}) => 92 | )} 101 | 102 | 103 | {pages} 104 | 105 |
106 | ); 107 | }; 108 | 109 | export default Orders; 110 | -------------------------------------------------------------------------------- /front/src/components/oneItemInBasket.js: -------------------------------------------------------------------------------- 1 | import React, {useContext} from 'react'; 2 | import {Button, Card, Col, Image, Row} from "react-bootstrap"; 3 | import {Context} from "../index"; 4 | import {NavLink} from "react-router-dom"; 5 | 6 | const OneItemInBasket = ({device}) => { 7 | const {basket, user} = useContext(Context); 8 | 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Title: {device.name} 20 | 21 | 22 |

23 | 24 | 25 | Description:

26 | {device.info && device.info.length !== 0? device.info.map((info, i) => { 27 | 28 | if(i % 2 === 0 ) { 29 | return ( 30 | 31 | 32 | {info.title} 33 | 34 | 35 | {info.description} 36 | 37 | 38 | ); 39 | } else { 40 | return ( 41 | 42 | 43 | {info.title} 44 | 45 | 46 | {info.description} 47 | 48 | 49 | ); 50 | } 51 | 52 | }) : "Description absent"} 53 | 54 |
55 | 56 | 57 | 58 | 59 | 60 | 61 | {user.isAuth ? 62 | : 63 | } 64 | 65 | 66 | 67 | 68 | Count: 69 | 70 | 71 | 72 | 73 | 74 | basket.setCountDevice(Number(e.target.value))} value={device.count}/> 75 | 76 | 77 | 78 | 79 | 80 | Price: {device.price * device.count} RUB 81 | 82 | 83 | 84 |
85 |
86 |
87 | )}; 88 | 89 | export default OneItemInBasket; 90 | -------------------------------------------------------------------------------- /front/src/components/modals/DeleteBrandOrType.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import {Button, Dropdown, Modal} from "react-bootstrap"; 3 | import {deleteBrand, deleteType, fetchBrands, fetchTypes} from "../../http/deviceAPI"; 4 | 5 | const DeleteBrandOrType = ({show, onHide, showSuccessMsgFunc}) => { 6 | const [brandOrType, setBrandOrType] = useState("Brand"); 7 | const [brands, setBrands] = useState([]); 8 | const [types, setTypes] = useState([]); 9 | const [selectBrand, setSelectBrand] = useState({name: "A Brand not selected"}); 10 | const [selectType, setSelectType] = useState({name: "A type not selected"}); 11 | const [showMsgErr, setShowMsgErr] = useState(false); 12 | const [msgErr, setMsgErr] = useState(''); 13 | 14 | useEffect(() => { 15 | fetchTypes().then(data => setTypes(data)); 16 | fetchBrands().then(data => setBrands(data)); 17 | }, []); 18 | 19 | const Delete = async () => { 20 | if(brandOrType === "Brand") { 21 | if(selectBrand.name !== "A Brand not selected") { 22 | await deleteBrand(selectBrand.id).then(data => { 23 | showSuccessMsgFunc(data); 24 | onHide(); 25 | setSelectBrand({name: "A Brand not selected"}); 26 | }); 27 | } else { 28 | setMsgErr("Please choose Brand"); 29 | setShowMsgErr(true); 30 | } 31 | } else { 32 | if(selectType.name !== "A Type not selected") { 33 | await deleteType(selectType.id).then(data => { 34 | showSuccessMsgFunc(data); 35 | onHide(); 36 | setSelectType({name: "A type not selected"}); 37 | }); 38 | } else { 39 | setMsgErr("Please choose Type"); 40 | setShowMsgErr(true); 41 | } 42 | } 43 | }; 44 | 45 | useEffect(() => setShowMsgErr(false), [selectType, selectBrand, brandOrType]) 46 | 47 | return ( 48 | 54 | 55 | 56 | Delete Type or Brand 57 | 58 | 59 | 60 | {showMsgErr && 61 | <> 62 |

{msgErr}

63 | 64 | } 65 | 66 | Choose Category: 67 | 68 | 69 | {brandOrType} 70 | 71 | 72 | 73 | {brandOrType === "Brand" ? Brand : setBrandOrType("Brand")}>Brand} 74 | {brandOrType === "Type" ? Type : setBrandOrType("Type")}>Type} 75 | 76 | 77 | 78 | Choose item of {brandOrType === "Brand" ? "Brand" : "Type"} 79 | 80 | 81 | {brandOrType === "Brand" ? selectBrand.name : selectType.name} 82 | 83 | 84 | 85 | {brandOrType === "Brand" ? 86 | brands.map(({id, name}) => 87 | selectBrand.name === name ? {name} : setSelectBrand({id, name})}>{name} 88 | ) 89 | : 90 | types.map(({id, name}) => 91 | selectType.name === name ? {name} : setSelectType({id, name})}>{name} 92 | ) 93 | } 94 | 95 | 96 | 97 | 98 | 99 |
100 | 101 | 102 | 103 | 104 |
105 | ); 106 | }; 107 | 108 | export default DeleteBrandOrType; 109 | -------------------------------------------------------------------------------- /backend/controllers/ordersController.js: -------------------------------------------------------------------------------- 1 | const {Orders, OrderDevice, Device, Brand, Type} = require('./../models/models'); 2 | const ApiError = require('../error/apiError'); 3 | const jwt = require('jsonwebtoken'); 4 | 5 | class OrdersController { 6 | async create(req, res) { 7 | const auth = req.headers.authorization || ""; 8 | const {mobile, basket} = req.body; 9 | 10 | try { 11 | let parseDevices = []; 12 | for (let key of basket) { 13 | parseDevices.push(key.id) 14 | } 15 | 16 | const isDeviceInDB = await Device.findAndCountAll({ 17 | where: {id: parseDevices}, 18 | attributes: ["id"] 19 | }); 20 | 21 | if(isDeviceInDB.count === parseDevices.length) { //if all devices was found in DB 22 | const row = {mobile}; 23 | if(auth) { 24 | const token = auth.split(' ')[1]; 25 | const {id} = jwt.verify(token, process.env.SECRET_KEY); 26 | row.userId = id; 27 | } 28 | 29 | await Orders.create(row).then(order => { 30 | const {id} = order.get(); 31 | parseDevices.forEach( async (deviceId, i) => { 32 | 33 | await OrderDevice.create({ 34 | orderId: id, 35 | deviceId, 36 | count: basket[i].count 37 | }); 38 | }); 39 | }); 40 | } else { //send msg about devices that didnt found in DB 41 | const notFoundIdDevices = []; 42 | const arrDevices = []; //found id 43 | isDeviceInDB.rows.forEach(item => arrDevices.push(item.id)); 44 | parseDevices.forEach(deviceId => { 45 | if(!arrDevices.includes(deviceId)) { 46 | notFoundIdDevices.push(deviceId); 47 | } 48 | }); 49 | return ApiError.badRequest(res.json(`Some Devices of id(${notFoundIdDevices.join(', ')}) not exist in DB`)); 50 | } 51 | 52 | return res.json("Thank you for you order! We will contact you shortly"); 53 | } catch (e) { 54 | return res.json(e); 55 | } 56 | } 57 | 58 | async updateOrder(req, res) { 59 | try { 60 | const { complete, id } = req.body; 61 | 62 | await Orders.findOne({where:{id}}) 63 | .then( async data => { 64 | if(data) { 65 | await Orders.update({complete}, {where:{id}} ).then(() => { 66 | return res.json("Order updated"); 67 | }) 68 | } else { 69 | return res.json("This order doesn't exist in DB"); 70 | } 71 | }) 72 | } catch (e) { 73 | return res.json("Updated didn't complete because was error: " + e); 74 | } 75 | 76 | } 77 | 78 | async deleteOrder(req, res) { 79 | try { 80 | const { id } = req.body; 81 | 82 | await Orders.findOne({where:{id}}) 83 | .then( async data => { 84 | if(data) { 85 | await Orders.destroy({where:{id}}).then(() => { 86 | return res.json("Order deleted"); 87 | }) 88 | } else { 89 | return res.json("This order doesn't exist in DB"); 90 | } 91 | }) 92 | } catch (e) { 93 | return res.json("Delete didn't complete because was error: " + e); 94 | } 95 | } 96 | 97 | async getAll(req, res) { 98 | let {limit, page, complete} = req.query; 99 | page = page || 1; 100 | limit = limit || 7; 101 | let offset = page * limit - limit; 102 | let devices; 103 | if(complete === "not-completed") { 104 | devices = await Orders.findAndCountAll({where:{complete: false}, limit, offset}); 105 | } else if(complete === "completed") { 106 | devices = await Orders.findAndCountAll({where:{complete: true}, limit, offset}); 107 | } else { 108 | devices = await Orders.findAndCountAll({limit, offset}); 109 | } 110 | 111 | return res.json(devices); 112 | } 113 | 114 | async getOne(req, res) { 115 | const {id} = req.params; 116 | const order = {}; 117 | try { 118 | let devices; 119 | let infoDevices = []; 120 | await Orders.findOne({where:{id}}).then(async data => { 121 | order.descr = data; 122 | devices = await OrderDevice.findAll({ 123 | attributes: ["deviceId", "count"], 124 | where:{orderId: data.id}, 125 | }); 126 | 127 | for (let device of devices) { 128 | await Device.findOne({ 129 | attributes: ["name", "img", "price"], 130 | where: {id: device.deviceId}, 131 | include: [ 132 | { 133 | attributes: ["name"], 134 | model: Type 135 | }, 136 | { 137 | attributes: ["name"], 138 | model: Brand 139 | }, 140 | ] 141 | }).then(async item => { 142 | let newObj = { 143 | descr: item, 144 | count: device.count 145 | } 146 | infoDevices.push(newObj); 147 | }); 148 | } 149 | order.devices = infoDevices; 150 | 151 | return res.json(order); 152 | }).catch(() => { 153 | return res.json("Order doesn't exist in data base"); 154 | }); 155 | 156 | } catch (e) { 157 | return res.json("Delete didn't complete because was error: " + e); 158 | } 159 | } 160 | } 161 | 162 | module.exports = new OrdersController(); 163 | -------------------------------------------------------------------------------- /front/src/components/modals/CreateDevice.js: -------------------------------------------------------------------------------- 1 | import React, {useContext, useEffect, useState} from 'react'; 2 | import {Context} from "../../index"; 3 | import {Button, Col, Dropdown, Form, Modal, Row} from "react-bootstrap"; 4 | import {createDevice, fetchBrands, fetchTypes} from "../../http/deviceAPI"; 5 | import {observer} from "mobx-react-lite"; 6 | 7 | const CreateDevice = observer(({show, onHide}) => { 8 | const {device} = useContext(Context); 9 | const [name, setName] = useState(''); 10 | const [price, setPrice] = useState(0); 11 | const [file, setFile] = useState(null); 12 | const [info, setInfo] = useState([]); 13 | 14 | useEffect(() => { 15 | fetchTypes().then(data => device.setTypes(data)); 16 | fetchBrands().then(data => device.setBrands(data)); 17 | }, []); 18 | 19 | const addInfo = () => { 20 | setInfo([...info, {title: '', description: '', number: Date.now()}]); 21 | }; 22 | 23 | const deleteInfo = (number) => { 24 | setInfo(info.filter(item => item.number !== number)); 25 | }; 26 | 27 | const changeInfo = (key, value, number) => { 28 | setInfo(info.map(i => i.number === number ? {...i, [key]: value} : i)); 29 | }; 30 | 31 | const selectFile = e => { 32 | setFile(e.target.files[0]); 33 | }; 34 | 35 | const addDevice = () => { 36 | const formData = new FormData(); 37 | formData.append('name', name); 38 | formData.append('price', `${price}`); 39 | formData.append('img', file); 40 | formData.append('brandId', device.selectedBrand.id); 41 | formData.append('typeId', device.selectedType.id); 42 | formData.append('info', JSON.stringify(info)); 43 | createDevice(formData).then(() => onHide()); 44 | } 45 | 46 | return ( 47 | 53 | 54 | 55 | Add new Device 56 | 57 | 58 | 59 |
60 | 61 | {device.selectedType.name || "Choose your Type"} 62 | 63 | {device.types.map(type => 64 | device.setSelectedType(type)} 67 | > 68 | {type.name} 69 | 70 | )} 71 | 72 | 73 | 74 | {device.selectedBrand.name || "Choose your Brand"} 75 | 76 | {device.brands.map(brand => 77 | device.setSelectedBrand(brand)} 80 | > 81 | {brand.name} 82 | 83 | )} 84 | 85 | 86 | setName(e.target.value)} 89 | className="mt-3" 90 | placeholder="Input name your device..." 91 | /> 92 | setPrice(Number(e.target.value))} 95 | className="mt-3" 96 | placeholder="Input name price for the device..." 97 | type="number" 98 | /> 99 | 104 |
105 | 111 | {info.map(item => 112 | 113 | 114 | changeInfo('title', e.target.value, item.number)} 118 | /> 119 | 120 | 121 | changeInfo('description', e.target.value, item.number)} 125 | /> 126 | 127 | 128 | 134 | 135 | 136 | )} 137 | 138 |
139 | 140 | 141 | 142 | 143 |
144 | ); 145 | }); 146 | 147 | export default CreateDevice; 148 | -------------------------------------------------------------------------------- /front/src/components/itemOneorderInAdmin.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {Button, Col, ListGroup, Modal, Row} from "react-bootstrap"; 3 | import {NavLink} from "react-router-dom"; 4 | import {fetchChangeStatusOrder, fetchDeleteOrder} from "../http/ordersAPI"; 5 | import {ORDERS_ROUTE} from "../utils/consts"; 6 | 7 | const ItemOneOrderInAdmin = ({id, complete, mobile, createdAt, updatedAt, userId, reRender}) => { 8 | const [modalDelete, setShowDelete] = useState(false); 9 | const [modalStatus, setShowStatus] = useState(false); 10 | 11 | //modal delete 12 | const handleCloseDelete = () => setShowDelete(false); 13 | const handleShowDelete = () => setShowDelete(true); 14 | const deleteOrder = () => { 15 | fetchDeleteOrder({id}).then(() => { 16 | setShowStatus(false); 17 | setTimeout(() => reRender(), 250); 18 | }) 19 | } 20 | 21 | //modal status 22 | const handleCloseStatus = () => setShowStatus(false); 23 | const handleShowStatus = () => setShowStatus(true); 24 | const changeStatusOrder = () => { 25 | fetchChangeStatusOrder({complete: !complete, id}).then(() => { 26 | setShowStatus(false); 27 | setTimeout(() => reRender(), 250); 28 | }) 29 | } 30 | 31 | //Format date (createdAt) 32 | const formatDate = (propsDate) => { 33 | const date = new Date(Date.parse(propsDate)); 34 | const options = { 35 | weekday: "short", 36 | hour: 'numeric', 37 | minute: 'numeric', 38 | year: 'numeric', 39 | month: 'numeric', 40 | day: 'numeric', 41 | timezone: 'UTC' 42 | }; 43 | return date.toLocaleString("en-US", options); 44 | } 45 | 46 | return ( 47 | <> 48 | 49 | 50 | 51 | 52 | 53 | id: {id} 54 | 55 | 56 | 57 | 58 | Phone: {mobile} 59 | 60 | 61 | 62 | 63 | Order created: {formatDate(createdAt)} 64 | 65 | 66 | {complete ? 67 | 68 | Order completed: {formatDate(updatedAt)} 69 | 70 | : false} 71 | 72 | 73 | {userId ? "Buyer: Registered" : "Buyer: Not registered"} 74 | 75 | 76 | 77 | 78 | Status: {complete ? "Completed" : "In progress"} 79 | 80 | 81 | 82 | 83 | 84 | 85 | {complete ? 86 | 87 | : 88 | } 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | {/*modal confirm change status*/} 99 | 100 | 101 | Please confirm 102 | 103 | 104 | Do you want changes status for this order(id: {id}), from {complete ? '\'Completed\'' : '\'In progress\''} to {complete ? '\'In progress\'' : '\'Completed\''}? 105 |

106 | Data: 107 |
    108 |
  • mobile: {mobile}
  • 109 |
  • Order CreatedAt: {formatDate(createdAt)}
  • 110 | {complete ? `Order completed: ${formatDate(updatedAt)}` : false} 111 |
  • Status: {complete ? 'Completed' : `In progress`}
  • 112 |
  • {userId ? 'Buyer registered' : `Buyer not registered`}
  • 113 |
114 |
115 | 116 | 119 | 122 | 123 |
124 | 125 | {/*modal confirm delete order*/} 126 | 127 | 128 | Please confirm 129 | 130 | 131 | Do you want delete this order(id: {id})? 132 |

133 | Data: 134 |
    135 |
  • mobile: {mobile}
  • 136 |
  • Order CreatedAt: {formatDate(createdAt)}
  • 137 | {complete ? `Order completed: ${formatDate(updatedAt)}` : false} 138 |
  • Status: {complete ? 'Completed' : `In progress`}
  • 139 |
  • {userId ? 'Buyer registered' : `Buyer not registered`}
  • 140 |
141 |
142 | 143 | 146 | 149 | 150 |
151 | 152 | ) 153 | }; 154 | 155 | export default ItemOneOrderInAdmin; 156 | -------------------------------------------------------------------------------- /front/src/pages/Admin.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import { 3 | Button, 4 | Col, 5 | Container, 6 | Dropdown, 7 | Form, 8 | Image, 9 | InputGroup, 10 | ListGroup, 11 | Pagination, 12 | Row 13 | } from "react-bootstrap"; 14 | 15 | import CreateDevice from "../components/modals/CreateDevice"; 16 | import CreateBrand from "../components/modals/CreateBrand"; 17 | import CreateType from "../components/modals/CreateType"; 18 | import {getAllDevicesInAdminPage} from "../http/deviceAPI"; 19 | import {NavLink} from "react-router-dom"; 20 | import {DEVICE_EDIT_ROUTE} from "../utils/consts"; 21 | import DeleteBrandOrType from "../components/modals/DeleteBrandOrType"; 22 | 23 | const Admin = () => { 24 | const [brandVisible, setBrandVisible] = useState(false); 25 | const [typeVisible, setTypeVisible] = useState(false); 26 | const [deviceVisible, setDeviceVisible] = useState(false); 27 | const [deleteBrandOrType, setDeleteBrandOrType] = useState(false); 28 | 29 | const [searchDevice, setSearchDevice] = useState(''); 30 | const [searchedDevice, setSearchedDevice] = useState([]); 31 | const [filter, setFilter] = useState("All"); 32 | const [currentPage, setCurrentPage] = useState(1); 33 | const [count, setCount] = useState(1); 34 | 35 | const [successMsg, setSuccessMsg] = useState(''); 36 | const [showSuccessMsg, setShowSuccessMsg] = useState(false); 37 | 38 | //pagination 39 | const limit = 5; 40 | const pageCount = Math.ceil(Number(count) / limit); 41 | const pages = []; 42 | for (let number = 1; number < pageCount + 1; number++) { 43 | pages.push( 44 | setCurrentPage(number)}> 45 | {number} 46 | 47 | ); 48 | } 49 | 50 | 51 | useEffect(() => { 52 | getAllDevicesInAdminPage(searchDevice, currentPage, filter).then(({count, rows}) => { 53 | setSearchedDevice(rows); 54 | setCount(count) 55 | }) 56 | }, [currentPage]) 57 | 58 | useEffect(() => { 59 | getAllDevicesInAdminPage(searchDevice, 1, filter).then(({count, rows}) => { 60 | setSearchedDevice(rows); 61 | setCount(count); 62 | setCurrentPage(1); 63 | }) 64 | }, [filter, successMsg]) 65 | 66 | 67 | const fetchDevice = () => { 68 | getAllDevicesInAdminPage(searchDevice, currentPage, filter).then(({count, rows}) => { 69 | setSearchedDevice(rows); 70 | setCount(count) 71 | }) 72 | }; 73 | 74 | const showSuccessMsgFunc = (msg) => { 75 | setSuccessMsg(msg); 76 | setShowSuccessMsg(true); 77 | setTimeout(() => setShowSuccessMsg(false), 5000); 78 | } 79 | 80 | return ( 81 | 82 | {showSuccessMsg &&

{successMsg}

} 83 | 90 | 97 | 104 | 111 | setDeviceVisible(false)}/> 112 | setBrandVisible(false)}/> 113 | setTypeVisible(false)}/> 114 | setDeleteBrandOrType(false)} showSuccessMsgFunc={showSuccessMsgFunc}/> 115 | 116 | 117 | 118 | {filter} 119 | 120 | 121 | 122 | {filter === "All" ? All : setFilter("All")}>All} 123 | {filter === "Without Brand or Type" ? Without Brand or Type : setFilter("Without Brand or Type")}>Without Brand or Type} 124 | 125 | 126 | 127 | 128 | setSearchDevice(e.target.value)} 133 | placeholder="Input device name..." 134 | /> 135 | 142 | 143 | 144 | 145 | {searchedDevice && searchedDevice.map( ({id, img, brand, type, price, name}) => { 146 | return ( 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | id: {id} 156 | 157 | 158 | 159 | 160 | Name: {name} 161 | 162 | 163 | 164 | 165 | Price: {price} 166 | 167 | 168 | 169 | 170 | Brand: {brand.name} 171 | 172 | 173 | 174 | 175 | Type: {type.name} 176 | 177 | 178 | 179 | 180 | Edit 181 | 182 | 183 | 184 | ) 185 | })} 186 | 187 | 188 | 189 | {searchedDevice && searchedDevice.length > 0 ? pages : false} 190 | 191 |
192 | ); 193 | }; 194 | 195 | export default Admin; 196 | -------------------------------------------------------------------------------- /backend/controllers/deviceController.js: -------------------------------------------------------------------------------- 1 | const { Op } = require("sequelize"); 2 | const uuid = require('uuid'); 3 | const path = require('path'); 4 | const {Device, DeviceInfo, Type, Brand, OrderDevice, BasketDevice} = require('../models/models'); 5 | const apiError = require('./../error/apiError'); 6 | 7 | class DeviceController { 8 | async create(req, res, next) { 9 | try { 10 | let {name, price, brandId, typeId, info} = req.body; 11 | const {img} = req.files; 12 | let fileName = uuid.v4() + ".jpg"; 13 | img.mv(path.resolve(__dirname, '..', 'static', fileName)); 14 | const device = await Device.create({ 15 | name, 16 | price, 17 | brandId, 18 | typeId, 19 | img: fileName 20 | }); 21 | 22 | if(info) { 23 | info = JSON.parse(info); 24 | info.forEach(i => { 25 | DeviceInfo.create({ 26 | title: i.title, 27 | description: i.description, 28 | deviceId: device.id 29 | }) 30 | }) 31 | } 32 | 33 | return res.json(device); 34 | } catch (e) { 35 | next(apiError.badRequest(e.message)); 36 | } 37 | 38 | } 39 | 40 | async getAll(req, res,next) { 41 | try { 42 | let {brandId, typeId, limit, page} = req.query; 43 | page = page || 1 44 | limit = limit || 9 45 | let offset = page * limit - limit 46 | let devices; 47 | if (!brandId && !typeId) { 48 | devices = await Device.findAndCountAll({ 49 | include: [ 50 | {model: Brand}, 51 | {model: Type}, 52 | ], 53 | limit, 54 | offset}) 55 | } 56 | if (brandId && !typeId) { 57 | devices = await Device.findAndCountAll({ 58 | where:{brandId}, 59 | include: [ 60 | {model: Brand}, 61 | {model: Type}, 62 | ], 63 | limit, 64 | offset 65 | })} 66 | if (!brandId && typeId) { 67 | devices = await Device.findAndCountAll({ 68 | where:{typeId}, 69 | include: [ 70 | {model: Brand}, 71 | {model: Type}, 72 | ], 73 | limit, 74 | offset 75 | })} 76 | if (brandId && typeId) { 77 | devices = await Device.findAndCountAll({ 78 | where:{typeId, brandId}, 79 | include: [ 80 | {model: Brand}, 81 | {model: Type}, 82 | ], 83 | limit, 84 | offset 85 | })} 86 | return res.json(devices) 87 | } catch (e) { 88 | next(apiError.badRequest(e.message)); 89 | } 90 | } 91 | 92 | async getSearchAllDeviceByName(req, res, next) { 93 | try { 94 | let {limit, page, name, filter} = req.query; 95 | 96 | page = page || 1; 97 | limit = limit || 7; 98 | let offset = page * limit - limit 99 | if(filter === "All") { 100 | const devices = await Device.findAndCountAll({ 101 | attributes: ["name", "price", "img", "id"], 102 | where: 103 | { 104 | name: { 105 | [Op.like]: `%${name}%` 106 | } 107 | }, 108 | include: [ 109 | { 110 | attributes: ["name"], 111 | model: Brand 112 | }, 113 | { 114 | attributes: ["name"], 115 | model: Type 116 | }, 117 | ], 118 | limit, 119 | offset, 120 | }) 121 | 122 | return res.json(devices); 123 | } else { 124 | const devices = await Device.findAndCountAll({ 125 | attributes: ["name", "price", "img", "id", "brandId", "typeId"], 126 | where: 127 | { 128 | name: { 129 | [Op.like]: `%${name}%` 130 | }, 131 | [Op.or]: [ 132 | { 133 | brandId: null, 134 | }, 135 | { 136 | typeId: null, 137 | }, 138 | ], 139 | }, 140 | include: [ 141 | { 142 | attributes: ["name"], 143 | model: Brand 144 | }, 145 | { 146 | attributes: ["name"], 147 | model: Type 148 | }, 149 | ], 150 | limit, 151 | offset, 152 | }) 153 | 154 | 155 | return res.json(devices); 156 | } 157 | } catch (e) { 158 | next(apiError.badRequest(e.message)); 159 | } 160 | } 161 | 162 | async getOne(req, res, next) { 163 | try { 164 | const {id} = req.params; 165 | let devices = await Device.findOne({ 166 | where: {id}, 167 | include: [ 168 | {model: DeviceInfo, as: 'info'}, 169 | {model: Type}, 170 | {model: Brand}, 171 | ] 172 | }); 173 | return res.json(devices); 174 | } catch (e) { 175 | next(apiError.badRequest(e.message)); 176 | } 177 | } 178 | 179 | async delete(req, res) { 180 | try { 181 | const {id} = req.params; 182 | await Device.findOne({where:{id}}) 183 | .then( async data => { 184 | if(data) { 185 | await Device.destroy({where:{id}}).then(() => { 186 | return res.json("Device deleted"); 187 | }) 188 | } else { 189 | return res.json("This Device doesn't exist in DB"); 190 | } 191 | 192 | await OrderDevice.destroy({where:{deviceId: id}}) 193 | await BasketDevice.destroy({where:{deviceId: id}}) 194 | }) 195 | } catch (e) { 196 | return res.json(e); 197 | } 198 | } 199 | 200 | async update(req, res) { 201 | try { 202 | const {id} = req.params; 203 | const {brandId, typeId, name, price, info} = req.body; 204 | 205 | await Device.findOne({where:{id}}) 206 | .then( async data => { 207 | if(data) { 208 | let newVal = {}; 209 | brandId ? newVal.brandId = brandId : false; 210 | typeId ? newVal.typeId = typeId : false; 211 | name ? newVal.name = name : false; 212 | price ? newVal.price = price : false; 213 | 214 | if(req.files) { 215 | const {img} = req.files; 216 | const type = img.mimetype.split('/')[1]; 217 | let fileName = uuid.v4() + `.${type}`; 218 | img.mv(path.resolve(__dirname, '..', 'static', fileName)); 219 | newVal.img = fileName; 220 | } 221 | 222 | if(info) { 223 | const parseInfo = JSON.parse(info); 224 | for (const item of parseInfo) { 225 | await DeviceInfo.findOne({where:{id: item.id}}).then( async data => { 226 | if(data) { 227 | await DeviceInfo.update({ 228 | title: item.title, 229 | description: item.description 230 | }, {where:{id: item.id}}) 231 | } else { 232 | await DeviceInfo.create({ 233 | title: item.title, 234 | description: item.description, 235 | deviceId: id 236 | }) 237 | } 238 | }) 239 | } 240 | } 241 | 242 | await Device.update({ 243 | ...newVal 244 | }, {where:{id}} ).then(() => { 245 | return res.json("Device updated"); 246 | }) 247 | } else { 248 | return res.json("This Device doesn't exist in DB"); 249 | } 250 | }) 251 | } catch (e) { 252 | return res.json(e); 253 | } 254 | } 255 | } 256 | 257 | module.exports = new DeviceController(); 258 | -------------------------------------------------------------------------------- /front/src/pages/DevicePageEdit.js: -------------------------------------------------------------------------------- 1 | import React, {useContext, useEffect, useState} from 'react'; 2 | import {Button, Col, Container, Dropdown, Form, Image, Modal, Row} from "react-bootstrap"; 3 | import {useParams, useHistory} from 'react-router-dom'; 4 | import {fetchDeleteDevice, fetchOneDevice, updateDevices} from "../http/deviceAPI"; 5 | import {Context} from "../index"; 6 | import {ADMIN_ROUTE} from "../utils/consts"; 7 | 8 | 9 | const DevicePageEdit = () => { 10 | const {device} = useContext(Context); 11 | const history = useHistory(); 12 | const {id} = useParams(); 13 | const [deviceCurr, setDeviceCurr] = useState({}); 14 | const [showMsg, setShowMsg] = useState(false); 15 | const [msg, setMsg] = useState(""); 16 | 17 | const [selectBrand, setSelectBrand] = useState({}); 18 | const [selectType, setSelectType] = useState({}); 19 | const [name, setName] = useState(""); 20 | const [price, setPrice] = useState(0); 21 | const [img, setImg] = useState(""); 22 | const [imgFile, setImgFile] = useState(null); 23 | const [info, setInfo] = useState([]); 24 | 25 | const [isDisabledPutBtn, setDisabledPutBtn] = useState(true); 26 | 27 | const deleteDevice = () => { 28 | fetchDeleteDevice(id).then(() => { 29 | history.push(ADMIN_ROUTE); 30 | }) 31 | } 32 | 33 | const [show, setShow] = useState(false); 34 | const handleClose = () => setShow(false); 35 | const handleShow = () => setShow(true); 36 | 37 | const imgHandler = e => { 38 | e.preventDefault(); 39 | 40 | const reader = new FileReader(); 41 | reader.onload = () => { 42 | setImg(reader.result); 43 | }; 44 | reader.readAsDataURL(e.target.files[0]); 45 | setImgFile(e.target.files[0]); 46 | } 47 | 48 | //info 49 | const addInfo = () => { 50 | setInfo([...info, {title: '', description: '', id: Date.now()}]); 51 | }; 52 | 53 | const deleteInfo = (number) => { 54 | setInfo(info.filter(item => item.number !== number)); 55 | }; 56 | 57 | const changeInfo = (key, value, number) => { 58 | setInfo(info.map(i => i.id === number ? {...i, [key]: value} : i)); 59 | }; 60 | 61 | const putDevice = () => { 62 | const formData = new FormData(); 63 | formData.append('name', name); 64 | formData.append('price', `${price}`); 65 | formData.append('img', imgFile); 66 | formData.append('brandId', selectBrand.id); 67 | formData.append('typeId', selectType.id); 68 | formData.append('info', JSON.stringify(info)); 69 | updateDevices(id, formData).then(data => { 70 | setShowMsg(true); 71 | setMsg(data); 72 | setTimeout(() => setShowMsg(true), 5000) 73 | }); 74 | } 75 | 76 | const checkInfo = () => { 77 | let isInfoEmpty = true; 78 | info.forEach(item => { 79 | for(let key in item) { 80 | if(key === "title" || key === "description") { 81 | if(!item[key]) { 82 | isInfoEmpty = false; 83 | } 84 | } 85 | } 86 | }); 87 | return isInfoEmpty; 88 | } 89 | 90 | useEffect(() => { 91 | let checkInfoVal = false; 92 | if(deviceCurr.info && deviceCurr.info.length !== info.length) { 93 | checkInfoVal = checkInfo(); 94 | } 95 | 96 | if(deviceCurr && deviceCurr.brand && deviceCurr.type) { 97 | if(deviceCurr.brand.name !== selectBrand.name || 98 | deviceCurr.type.name !== selectType.name || 99 | deviceCurr.name !== name || 100 | deviceCurr.price !== price || 101 | checkInfoVal || 102 | img 103 | ) { 104 | setDisabledPutBtn(false); 105 | } else { 106 | setDisabledPutBtn(true); 107 | } 108 | } 109 | }, [name, selectBrand, selectType, price, img, info]); 110 | 111 | useEffect(() => { 112 | fetchOneDevice(id).then(data => { 113 | setDeviceCurr(data); 114 | setSelectBrand(data.brand); 115 | setSelectType(data.type); 116 | setName(data.name); 117 | setPrice(data.price); 118 | setInfo(data.info) 119 | }); 120 | }, [id]); 121 | 122 | return ( 123 | 124 | {showMsg && 125 | {msg} 126 | } 127 | 128 | 129 | 130 | 131 | 132 | id: 133 | 134 | 135 | {deviceCurr.id} 136 | 137 | 138 | {/*Brand*/} 139 | 140 | 141 | Brand: 142 | 143 | 144 | 145 | {selectBrand.name || "Choose Brand"} 146 | 147 | {device.brands.map(brand => { 148 | return brand.name === selectBrand.name ? 149 | 153 | {brand.name} 154 | 155 | : 156 | setSelectBrand(brand)} 159 | > 160 | {brand.name} 161 | 162 | 163 | })} 164 | 165 | 166 | 167 | 168 | {/*Type*/} 169 | 170 | 171 | Types: 172 | 173 | 174 | 175 | {selectType.name || "Choose Type"} 176 | 177 | {device.types.map(type => { 178 | return type.name === selectType.name ? 179 | 183 | {type.name} 184 | 185 | : 186 | setSelectType(type)} 189 | > 190 | {type.name} 191 | 192 | 193 | })} 194 | 195 | 196 | 197 | 198 | {/*Name*/} 199 | 200 | 201 | Name: 202 | 203 | 204 | setName(e.target.value)} 208 | /> 209 | 210 | 211 | {name.length === 0 && Please input name of device} 212 | 213 | 214 | {/*Name*/} 215 | 216 | 217 | Price: 218 | 219 | 220 | setPrice(Number(e.target.value))} 224 | /> 225 | 226 | 227 | {price === 0 && Please input price of device} 228 | 229 | 230 | 231 | {/*Name*/} 232 | 233 | 234 | Current Img:
235 | 236 | 237 | {img && 238 | New Img:
239 | 240 | } 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 |
249 | 250 | {/*Characteristics*/} 251 | 252 |

Characteristics

253 | 259 | {info.map((item, index) => 260 | 261 | 262 | changeInfo('title', e.target.value, item.id)} 266 | /> 267 | {!info[index].title && Please input name} 268 | 269 | 270 | changeInfo('description', e.target.value, item.id)} 274 | /> 275 | {!info[index].description && Please input description} 276 | 277 | 278 | 284 | 285 | 286 | )} 287 |
288 | 289 | 290 | 291 | {isDisabledPutBtn ? : } 292 | 293 | 294 | 295 | 296 |
297 | 298 | 299 | 300 | Delete this device {deviceCurr.id}? 301 | 302 | 303 | 306 | 309 | 310 | 311 |
312 | ); 313 | }; 314 | 315 | export default DevicePageEdit; 316 | 317 | --------------------------------------------------------------------------------