├── .gitignore ├── client ├── assets │ └── images │ │ ├── default.png │ │ ├── stripeButton.png │ │ └── unicornbike.jpg ├── main.js ├── auth │ ├── PrivateRoute.js │ ├── api-auth.js │ ├── auth-helper.js │ └── Signin.js ├── theme.js ├── App.js ├── cart │ ├── Cart.js │ ├── AddToCart.js │ ├── cart-helper.js │ ├── PlaceOrder.js │ ├── Checkout.js │ └── CartItems.js ├── auction │ ├── OpenAuctions.js │ ├── Timer.js │ ├── DeleteAuction.js │ ├── MyAuctions.js │ ├── api-auction.js │ ├── Auctions.js │ ├── Bidding.js │ └── Auction.js ├── core │ ├── Home.js │ └── Menu.js ├── shop │ ├── DeleteShop.js │ ├── api-shop.js │ ├── Shops.js │ ├── Shop.js │ ├── MyShops.js │ ├── NewShop.js │ └── EditShop.js ├── order │ ├── MyOrders.js │ ├── api-order.js │ └── ShopOrders.js ├── product │ ├── DeleteProduct.js │ ├── Products.js │ ├── api-product.js │ ├── Categories.js │ ├── Search.js │ ├── MyProducts.js │ ├── Suggestions.js │ ├── Product.js │ ├── NewProduct.js │ └── EditProduct.js ├── user │ ├── DeleteUser.js │ ├── Users.js │ ├── api-user.js │ ├── StripeConnect.js │ ├── Signup.js │ ├── EditProfile.js │ └── Profile.js └── MainRouter.js ├── nodemon.json ├── .babelrc ├── server ├── routes │ ├── auth.routes.js │ ├── user.routes.js │ ├── shop.routes.js │ ├── auction.routes.js │ ├── product.routes.js │ └── order.routes.js ├── models │ ├── shop.model.js │ ├── product.model.js │ ├── auction.model.js │ ├── order.model.js │ └── user.model.js ├── devBundle.js ├── server.js ├── controllers │ ├── bidding.controller.js │ ├── auth.controller.js │ ├── order.controller.js │ ├── shop.controller.js │ ├── auction.controller.js │ ├── user.controller.js │ └── product.controller.js ├── helpers │ └── dbErrorHandler.js └── express.js ├── config └── config.js ├── .github └── stale.yml ├── webpack.config.client.production.js ├── template.js ├── webpack.config.server.js ├── LICENSE.md ├── webpack.config.client.js ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist/ 3 | /data/ 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /client/assets/images/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenBitSack/MERN_Marketplace/HEAD/client/assets/images/default.png -------------------------------------------------------------------------------- /client/assets/images/stripeButton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenBitSack/MERN_Marketplace/HEAD/client/assets/images/stripeButton.png -------------------------------------------------------------------------------- /client/assets/images/unicornbike.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenBitSack/MERN_Marketplace/HEAD/client/assets/images/unicornbike.jpg -------------------------------------------------------------------------------- /client/main.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { hydrate } from 'react-dom' 3 | import App from './App' 4 | 5 | hydrate(, document.getElementById('root')) 6 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": false, 3 | "watch": [ 4 | "./server" 5 | ], 6 | "exec": "webpack --mode=development --config webpack.config.server.js && node ./dist/server.generated.js" 7 | } 8 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", 4 | { 5 | "targets": { 6 | "node": "current" 7 | } 8 | } 9 | ], 10 | "@babel/preset-react" 11 | ], 12 | "plugins": [ 13 | "react-hot-loader/babel" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /server/routes/auth.routes.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import authCtrl from '../controllers/auth.controller' 3 | 4 | const router = express.Router() 5 | 6 | router.route('/auth/signin') 7 | .post(authCtrl.signin) 8 | router.route('/auth/signout') 9 | .get(authCtrl.signout) 10 | 11 | export default router 12 | -------------------------------------------------------------------------------- /client/auth/PrivateRoute.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Route, Redirect } from 'react-router-dom' 3 | import auth from './auth-helper' 4 | 5 | const PrivateRoute = ({ component: Component, ...rest }) => ( 6 | ( 7 | auth.isAuthenticated() ? ( 8 | 9 | ) : ( 10 | 14 | ) 15 | )}/> 16 | ) 17 | 18 | export default PrivateRoute 19 | -------------------------------------------------------------------------------- /server/models/shop.model.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | const ShopSchema = new mongoose.Schema({ 3 | name: { 4 | type: String, 5 | trim: true, 6 | required: 'Name is required' 7 | }, 8 | image: { 9 | data: Buffer, 10 | contentType: String 11 | }, 12 | description: { 13 | type: String, 14 | trim: true 15 | }, 16 | updated: Date, 17 | created: { 18 | type: Date, 19 | default: Date.now 20 | }, 21 | owner: {type: mongoose.Schema.ObjectId, ref: 'User'} 22 | }) 23 | 24 | export default mongoose.model('Shop', ShopSchema) 25 | -------------------------------------------------------------------------------- /config/config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | env: process.env.NODE_ENV || 'development', 3 | port: process.env.PORT || 3000, 4 | jwtSecret: process.env.JWT_SECRET || "YOUR_secret_key", 5 | mongoUri: process.env.MONGODB_URI || 6 | process.env.MONGO_HOST || 7 | 'mongodb://' + (process.env.IP || 'localhost') + ':' + 8 | (process.env.MONGO_PORT || '27017') + 9 | '/mernproject', 10 | stripe_connect_test_client_id: 'YOUR_stripe_connect_test_client', 11 | stripe_test_secret_key: 'YOUR_stripe_test_secret_key', 12 | stripe_test_api_key: 'YOUR_stripe_test_api_key' 13 | } 14 | 15 | export default config 16 | -------------------------------------------------------------------------------- /client/theme.js: -------------------------------------------------------------------------------- 1 | import { createMuiTheme } from '@material-ui/core/styles' 2 | import { blueGrey, lightGreen } from '@material-ui/core/colors' 3 | 4 | const theme = createMuiTheme({ 5 | palette: { 6 | primary: { 7 | light: '#8eacbb', 8 | main: '#607d8b', 9 | dark: '#34515e', 10 | contrastText: '#fff', 11 | }, 12 | secondary: { 13 | light: '#e7ff8c', 14 | main: '#b2ff59', 15 | dark: '#7ecb20', 16 | contrastText: '#000', 17 | }, 18 | openTitle: blueGrey['400'], 19 | protectedTitle: lightGreen['400'], 20 | type: 'light' 21 | } 22 | }) 23 | 24 | export default theme -------------------------------------------------------------------------------- /server/devBundle.js: -------------------------------------------------------------------------------- 1 | import config from './../config/config' 2 | import webpack from 'webpack' 3 | import webpackMiddleware from 'webpack-dev-middleware' 4 | import webpackHotMiddleware from 'webpack-hot-middleware' 5 | import webpackConfig from './../webpack.config.client.js' 6 | 7 | const compile = (app) => { 8 | if(config.env === "development"){ 9 | const compiler = webpack(webpackConfig) 10 | const middleware = webpackMiddleware(compiler, { 11 | publicPath: webpackConfig.output.publicPath 12 | }) 13 | app.use(middleware) 14 | app.use(webpackHotMiddleware(compiler)) 15 | } 16 | } 17 | 18 | export default { 19 | compile 20 | } 21 | -------------------------------------------------------------------------------- /client/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import MainRouter from './MainRouter' 3 | import {BrowserRouter} from 'react-router-dom' 4 | import { ThemeProvider } from '@material-ui/styles' 5 | import theme from './theme' 6 | import { hot } from 'react-hot-loader' 7 | 8 | const App = () => { 9 | React.useEffect(() => { 10 | const jssStyles = document.querySelector('#jss-server-side') 11 | if (jssStyles) { 12 | jssStyles.parentNode.removeChild(jssStyles) 13 | } 14 | }, []) 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | )} 22 | 23 | export default hot(module)(App) 24 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | import config from './../config/config' 2 | import app from './express' 3 | import mongoose from 'mongoose' 4 | import bidding from './controllers/bidding.controller' 5 | 6 | // Connection URL 7 | mongoose.Promise = global.Promise 8 | mongoose.connect(config.mongoUri, { useNewUrlParser: true, useCreateIndex: true, useUnifiedTopology: true, useFindAndModify: true }) 9 | mongoose.connection.on('error', () => { 10 | throw new Error(`unable to connect to database: ${config.mongoUri}`) 11 | }) 12 | 13 | const server = app.listen(config.port, (err) => { 14 | if (err) { 15 | console.log(err) 16 | } 17 | console.info('Server started on port %s.', config.port) 18 | }) 19 | 20 | bidding(server) 21 | -------------------------------------------------------------------------------- /client/auth/api-auth.js: -------------------------------------------------------------------------------- 1 | const signin = async (user) => { 2 | try { 3 | let response = await fetch('/auth/signin/', { 4 | method: 'POST', 5 | headers: { 6 | 'Accept': 'application/json', 7 | 'Content-Type': 'application/json' 8 | }, 9 | credentials: 'include', 10 | body: JSON.stringify(user) 11 | }) 12 | return await response.json() 13 | } catch(err) { 14 | console.log(err) 15 | } 16 | } 17 | 18 | const signout = async () => { 19 | try { 20 | let response = await fetch('/auth/signout/', { method: 'GET' }) 21 | return await response.json() 22 | } catch(err) { 23 | console.log(err) 24 | } 25 | } 26 | 27 | export { 28 | signin, 29 | signout 30 | } -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: inactive 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /server/routes/user.routes.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import userCtrl from '../controllers/user.controller' 3 | import authCtrl from '../controllers/auth.controller' 4 | 5 | const router = express.Router() 6 | 7 | router.route('/api/users') 8 | .get(userCtrl.list) 9 | .post(userCtrl.create) 10 | 11 | router.route('/api/users/:userId') 12 | .get(authCtrl.requireSignin, userCtrl.read) 13 | .put(authCtrl.requireSignin, authCtrl.hasAuthorization, userCtrl.update) 14 | .delete(authCtrl.requireSignin, authCtrl.hasAuthorization, userCtrl.remove) 15 | router.route('/api/stripe_auth/:userId') 16 | .put(authCtrl.requireSignin, authCtrl.hasAuthorization, userCtrl.stripe_auth, userCtrl.update) 17 | 18 | router.param('userId', userCtrl.userByID) 19 | 20 | export default router 21 | -------------------------------------------------------------------------------- /server/models/product.model.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | const ProductSchema = new mongoose.Schema({ 3 | name: { 4 | type: String, 5 | trim: true, 6 | required: 'Name is required' 7 | }, 8 | image: { 9 | data: Buffer, 10 | contentType: String 11 | }, 12 | description: { 13 | type: String, 14 | trim: true 15 | }, 16 | category: { 17 | type: String 18 | }, 19 | quantity: { 20 | type: Number, 21 | required: "Quantity is required" 22 | }, 23 | price: { 24 | type: Number, 25 | required: "Price is required" 26 | }, 27 | updated: Date, 28 | created: { 29 | type: Date, 30 | default: Date.now 31 | }, 32 | shop: {type: mongoose.Schema.ObjectId, ref: 'Shop'} 33 | }) 34 | 35 | export default mongoose.model('Product', ProductSchema) 36 | -------------------------------------------------------------------------------- /webpack.config.client.production.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const CURRENT_WORKING_DIR = process.cwd() 3 | 4 | const config = { 5 | mode: "production", 6 | entry: [ 7 | path.join(CURRENT_WORKING_DIR, 'client/main.js') 8 | ], 9 | output: { 10 | path: path.join(CURRENT_WORKING_DIR , '/dist'), 11 | filename: 'bundle.js', 12 | publicPath: "/dist/" 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.jsx?$/, 18 | exclude: /node_modules/, 19 | use: [ 20 | 'babel-loader' 21 | ] 22 | }, 23 | { 24 | test: /\.(ttf|eot|svg|gif|jpg|png)(\?[\s\S]+)?$/, 25 | use: 'file-loader' 26 | } 27 | ] 28 | } 29 | } 30 | 31 | module.exports = config 32 | -------------------------------------------------------------------------------- /template.js: -------------------------------------------------------------------------------- 1 | export default ({markup, css}) => { 2 | return ` 3 | 4 | 5 | 6 | MERN Marketplace 7 | 8 | 9 | 14 | 15 | 16 |
${markup}
17 | 18 | 19 | 20 | 21 | ` 22 | } 23 | -------------------------------------------------------------------------------- /webpack.config.server.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const nodeExternals = require('webpack-node-externals') 3 | const CURRENT_WORKING_DIR = process.cwd() 4 | 5 | const config = { 6 | name: "server", 7 | entry: [ path.join(CURRENT_WORKING_DIR , './server/server.js') ], 8 | target: "node", 9 | output: { 10 | path: path.join(CURRENT_WORKING_DIR , '/dist/'), 11 | filename: "server.generated.js", 12 | publicPath: '/dist/', 13 | libraryTarget: "commonjs2" 14 | }, 15 | externals: [nodeExternals()], 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.js$/, 20 | exclude: /node_modules/, 21 | use: [ 'babel-loader' ] 22 | }, 23 | { 24 | test: /\.(ttf|eot|svg|gif|jpg|png)(\?[\s\S]+)?$/, 25 | use: 'file-loader' 26 | } 27 | ] 28 | } 29 | } 30 | 31 | module.exports = config 32 | -------------------------------------------------------------------------------- /server/models/auction.model.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | const AuctionSchema = new mongoose.Schema({ 3 | itemName: { 4 | type: String, 5 | trim: true, 6 | required: 'Item name is required' 7 | }, 8 | description: { 9 | type: String, 10 | trim: true 11 | }, 12 | image: { 13 | data: Buffer, 14 | contentType: String 15 | }, 16 | updated: Date, 17 | created: { 18 | type: Date, 19 | default: Date.now 20 | }, 21 | bidStart: { 22 | type: Date, 23 | default: Date.now 24 | }, 25 | bidEnd: { 26 | type: Date, 27 | required: "Auction end time is required" 28 | }, 29 | seller: { 30 | type: mongoose.Schema.ObjectId, 31 | ref: 'User' 32 | }, 33 | startingBid: { type: Number, default: 0 }, 34 | bids: [{ 35 | bidder: {type: mongoose.Schema.ObjectId, ref: 'User'}, 36 | bid: Number, 37 | time: Date 38 | }] 39 | }) 40 | 41 | export default mongoose.model('Auction', AuctionSchema) 42 | -------------------------------------------------------------------------------- /server/routes/shop.routes.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import userCtrl from '../controllers/user.controller' 3 | import authCtrl from '../controllers/auth.controller' 4 | import shopCtrl from '../controllers/shop.controller' 5 | 6 | const router = express.Router() 7 | 8 | router.route('/api/shops') 9 | .get(shopCtrl.list) 10 | 11 | router.route('/api/shop/:shopId') 12 | .get(shopCtrl.read) 13 | 14 | router.route('/api/shops/by/:userId') 15 | .post(authCtrl.requireSignin, authCtrl.hasAuthorization, userCtrl.isSeller, shopCtrl.create) 16 | .get(authCtrl.requireSignin, authCtrl.hasAuthorization, shopCtrl.listByOwner) 17 | 18 | router.route('/api/shops/:shopId') 19 | .put(authCtrl.requireSignin, shopCtrl.isOwner, shopCtrl.update) 20 | .delete(authCtrl.requireSignin, shopCtrl.isOwner, shopCtrl.remove) 21 | 22 | router.route('/api/shops/logo/:shopId') 23 | .get(shopCtrl.photo, shopCtrl.defaultPhoto) 24 | 25 | router.route('/api/shops/defaultphoto') 26 | .get(shopCtrl.defaultPhoto) 27 | 28 | router.param('shopId', shopCtrl.shopByID) 29 | router.param('userId', userCtrl.userByID) 30 | 31 | export default router 32 | -------------------------------------------------------------------------------- /client/auth/auth-helper.js: -------------------------------------------------------------------------------- 1 | import { signout } from './api-auth.js' 2 | 3 | const auth = { 4 | isAuthenticated() { 5 | if (typeof window == "undefined") 6 | return false 7 | 8 | if (sessionStorage.getItem('jwt')) 9 | return JSON.parse(sessionStorage.getItem('jwt')) 10 | else 11 | return false 12 | }, 13 | authenticate(jwt, cb) { 14 | if (typeof window !== "undefined") 15 | sessionStorage.setItem('jwt', JSON.stringify(jwt)) 16 | cb() 17 | }, 18 | clearJWT(cb) { 19 | if (typeof window !== "undefined") 20 | sessionStorage.removeItem('jwt') 21 | cb() 22 | //optional 23 | signout().then((data) => { 24 | document.cookie = "t=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;" 25 | }) 26 | }, 27 | updateUser(user, cb) { 28 | if(typeof window !== "undefined"){ 29 | if(sessionStorage.getItem('jwt')){ 30 | let auth = JSON.parse(sessionStorage.getItem('jwt')) 31 | auth.user = user 32 | sessionStorage.setItem('jwt', JSON.stringify(auth)) 33 | cb() 34 | } 35 | } 36 | } 37 | } 38 | 39 | export default auth 40 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Shama Hoque 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /server/controllers/bidding.controller.js: -------------------------------------------------------------------------------- 1 | import Auction from '../models/auction.model' 2 | 3 | export default (server) => { 4 | const io = require('socket.io').listen(server) 5 | io.on('connection', function(socket){ 6 | socket.on('join auction room', data => { 7 | socket.join(data.room) 8 | }) 9 | socket.on('leave auction room', data => { 10 | socket.leave(data.room) 11 | }) 12 | socket.on('new bid', data => { 13 | bid(data.bidInfo, data.room) 14 | }) 15 | }) 16 | const bid = async (bid, auction) => { 17 | try { 18 | let result = await Auction.findOneAndUpdate({_id:auction, $or: [{'bids.0.bid':{$lt:bid.bid}},{bids:{$eq:[]}} ]}, {$push: {bids: {$each:[bid], $position: 0}}}, {new: true}) 19 | .populate('bids.bidder', '_id name') 20 | .populate('seller', '_id name') 21 | .exec() 22 | io 23 | .to(auction) 24 | .emit('new bid', result) 25 | } catch(err) { 26 | console.log(err) 27 | } 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /webpack.config.client.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const CURRENT_WORKING_DIR = process.cwd() 4 | 5 | const config = { 6 | name: "browser", 7 | mode: "development", 8 | devtool: 'eval-source-map', 9 | entry: [ 10 | 'react-hot-loader/patch', 11 | 'webpack-hot-middleware/client?reload=true', 12 | path.join(CURRENT_WORKING_DIR, 'client/main.js') 13 | ], 14 | output: { 15 | path: path.join(CURRENT_WORKING_DIR , '/dist'), 16 | filename: 'bundle.js', 17 | publicPath: '/dist/' 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.jsx?$/, 23 | exclude: /node_modules/, 24 | use: [ 25 | 'babel-loader' 26 | ] 27 | }, 28 | { 29 | test: /\.(ttf|eot|svg|gif|jpg|png)(\?[\s\S]+)?$/, 30 | use: 'file-loader' 31 | } 32 | ] 33 | }, plugins: [ 34 | new webpack.HotModuleReplacementPlugin(), 35 | new webpack.NoEmitOnErrorsPlugin() 36 | ] 37 | } 38 | 39 | module.exports = config 40 | -------------------------------------------------------------------------------- /server/helpers/dbErrorHandler.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Get unique error field name 5 | */ 6 | const getUniqueErrorMessage = (err) => { 7 | let output 8 | try { 9 | let fieldName = err.message.substring(err.message.lastIndexOf('.$') + 2, err.message.lastIndexOf('_1')) 10 | output = fieldName.charAt(0).toUpperCase() + fieldName.slice(1) + ' already exists' 11 | } catch (ex) { 12 | output = 'Unique field already exists' 13 | } 14 | 15 | return output 16 | } 17 | 18 | /** 19 | * Get the error message from error object 20 | */ 21 | const getErrorMessage = (err) => { 22 | let message = '' 23 | 24 | if (err.code) { 25 | switch (err.code) { 26 | case 11000: 27 | case 11001: 28 | message = getUniqueErrorMessage(err) 29 | break 30 | default: 31 | message = 'Something went wrong' 32 | } 33 | } else { 34 | for (let errName in err.errors) { 35 | if (err.errors[errName].message) message = err.errors[errName].message 36 | } 37 | } 38 | 39 | return message 40 | } 41 | 42 | export default {getErrorMessage} 43 | -------------------------------------------------------------------------------- /client/cart/Cart.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react' 2 | import Grid from '@material-ui/core/Grid' 3 | import {makeStyles} from '@material-ui/core/styles' 4 | import CartItems from './CartItems' 5 | import {StripeProvider} from 'react-stripe-elements' 6 | import config from './../../config/config' 7 | import Checkout from './Checkout' 8 | 9 | const useStyles = makeStyles(theme => ({ 10 | root: { 11 | flexGrow: 1, 12 | margin: 30, 13 | } 14 | })) 15 | 16 | export default function Cart () { 17 | const classes = useStyles() 18 | const [checkout, setCheckout] = useState(false) 19 | 20 | const showCheckout = val => { 21 | setCheckout(val) 22 | } 23 | 24 | return (
25 | 26 | 27 | 29 | 30 | {checkout && 31 | 32 | 33 | 34 | 35 | } 36 | 37 |
) 38 | } 39 | -------------------------------------------------------------------------------- /server/routes/auction.routes.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import userCtrl from '../controllers/user.controller' 3 | import authCtrl from '../controllers/auth.controller' 4 | import auctionCtrl from '../controllers/auction.controller' 5 | 6 | const router = express.Router() 7 | 8 | router.route('/api/auctions') 9 | .get(auctionCtrl.listOpen) 10 | 11 | router.route('/api/auctions/bid/:userId') 12 | .get(auctionCtrl.listByBidder) 13 | 14 | router.route('/api/auction/:auctionId') 15 | .get(auctionCtrl.read) 16 | 17 | router.route('/api/auctions/by/:userId') 18 | .post(authCtrl.requireSignin, authCtrl.hasAuthorization, userCtrl.isSeller, auctionCtrl.create) 19 | .get(authCtrl.requireSignin, authCtrl.hasAuthorization, auctionCtrl.listBySeller) 20 | 21 | router.route('/api/auctions/:auctionId') 22 | .put(authCtrl.requireSignin, auctionCtrl.isSeller, auctionCtrl.update) 23 | .delete(authCtrl.requireSignin, auctionCtrl.isSeller, auctionCtrl.remove) 24 | 25 | router.route('/api/auctions/image/:auctionId') 26 | .get(auctionCtrl.photo, auctionCtrl.defaultPhoto) 27 | 28 | router.route('/api/auctions/defaultphoto') 29 | .get(auctionCtrl.defaultPhoto) 30 | 31 | router.param('auctionId', auctionCtrl.auctionByID) 32 | router.param('userId', userCtrl.userByID) 33 | 34 | export default router 35 | -------------------------------------------------------------------------------- /server/routes/product.routes.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import productCtrl from '../controllers/product.controller' 3 | import authCtrl from '../controllers/auth.controller' 4 | import shopCtrl from '../controllers/shop.controller' 5 | 6 | const router = express.Router() 7 | 8 | router.route('/api/products/by/:shopId') 9 | .post(authCtrl.requireSignin, shopCtrl.isOwner, productCtrl.create) 10 | .get(productCtrl.listByShop) 11 | 12 | router.route('/api/products/latest') 13 | .get(productCtrl.listLatest) 14 | 15 | router.route('/api/products/related/:productId') 16 | .get(productCtrl.listRelated) 17 | 18 | router.route('/api/products/categories') 19 | .get(productCtrl.listCategories) 20 | 21 | router.route('/api/products') 22 | .get(productCtrl.list) 23 | 24 | router.route('/api/products/:productId') 25 | .get(productCtrl.read) 26 | 27 | router.route('/api/product/image/:productId') 28 | .get(productCtrl.photo, productCtrl.defaultPhoto) 29 | router.route('/api/product/defaultphoto') 30 | .get(productCtrl.defaultPhoto) 31 | 32 | router.route('/api/product/:shopId/:productId') 33 | .put(authCtrl.requireSignin, shopCtrl.isOwner, productCtrl.update) 34 | .delete(authCtrl.requireSignin, shopCtrl.isOwner, productCtrl.remove) 35 | 36 | router.param('shopId', shopCtrl.shopByID) 37 | router.param('productId', productCtrl.productByID) 38 | 39 | export default router 40 | -------------------------------------------------------------------------------- /server/models/order.model.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | const CartItemSchema = new mongoose.Schema({ 3 | product: {type: mongoose.Schema.ObjectId, ref: 'Product'}, 4 | quantity: Number, 5 | shop: {type: mongoose.Schema.ObjectId, ref: 'Shop'}, 6 | status: {type: String, 7 | default: 'Not processed', 8 | enum: ['Not processed' , 'Processing', 'Shipped', 'Delivered', 'Cancelled']} 9 | }) 10 | const CartItem = mongoose.model('CartItem', CartItemSchema) 11 | const OrderSchema = new mongoose.Schema({ 12 | products: [CartItemSchema], 13 | customer_name: { 14 | type: String, 15 | trim: true, 16 | required: 'Name is required' 17 | }, 18 | customer_email: { 19 | type: String, 20 | trim: true, 21 | match: [/.+\@.+\..+/, 'Please fill a valid email address'], 22 | required: 'Email is required' 23 | }, 24 | delivery_address: { 25 | street: {type: String, required: 'Street is required'}, 26 | city: {type: String, required: 'City is required'}, 27 | state: {type: String}, 28 | zipcode: {type: String, required: 'Zip Code is required'}, 29 | country: {type: String, required: 'Country is required'} 30 | }, 31 | payment_id: {}, 32 | updated: Date, 33 | created: { 34 | type: Date, 35 | default: Date.now 36 | }, 37 | user: {type: mongoose.Schema.ObjectId, ref: 'User'} 38 | }) 39 | 40 | const Order = mongoose.model('Order', OrderSchema) 41 | 42 | export {Order, CartItem} 43 | -------------------------------------------------------------------------------- /client/cart/AddToCart.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | import {makeStyles} from '@material-ui/core/styles' 3 | import PropTypes from 'prop-types' 4 | import IconButton from '@material-ui/core/IconButton' 5 | import AddCartIcon from '@material-ui/icons/AddShoppingCart' 6 | import DisabledCartIcon from '@material-ui/icons/RemoveShoppingCart' 7 | import cart from './cart-helper.js' 8 | import { Redirect } from 'react-router-dom' 9 | 10 | const useStyles = makeStyles(theme => ({ 11 | iconButton: { 12 | width: '28px', 13 | height: '28px' 14 | }, 15 | disabledIconButton: { 16 | color: '#7f7563', 17 | width: '28px', 18 | height: '28px' 19 | } 20 | })) 21 | 22 | export default function AddToCart(props) { 23 | const classes = useStyles() 24 | const [redirect, setRedirect] = useState(false) 25 | 26 | const addToCart = () => { 27 | cart.addItem(props.item, () => { 28 | setRedirect({redirect:true}) 29 | }) 30 | } 31 | if (redirect) { 32 | return () 33 | } 34 | return ( 35 | {props.item.quantity >= 0 ? 36 | 37 | 38 | : 39 | 40 | 41 | } 42 | ) 43 | } 44 | 45 | AddToCart.propTypes = { 46 | item: PropTypes.object.isRequired, 47 | cartStyle: PropTypes.string 48 | } -------------------------------------------------------------------------------- /server/routes/order.routes.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import orderCtrl from '../controllers/order.controller' 3 | import productCtrl from '../controllers/product.controller' 4 | import authCtrl from '../controllers/auth.controller' 5 | import shopCtrl from '../controllers/shop.controller' 6 | import userCtrl from '../controllers/user.controller' 7 | 8 | const router = express.Router() 9 | 10 | router.route('/api/orders/:userId') 11 | .post(authCtrl.requireSignin, userCtrl.stripeCustomer, productCtrl.decreaseQuantity, orderCtrl.create) 12 | 13 | router.route('/api/orders/shop/:shopId') 14 | .get(authCtrl.requireSignin, shopCtrl.isOwner, orderCtrl.listByShop) 15 | 16 | router.route('/api/orders/user/:userId') 17 | .get(authCtrl.requireSignin, orderCtrl.listByUser) 18 | 19 | router.route('/api/order/status_values') 20 | .get(orderCtrl.getStatusValues) 21 | 22 | router.route('/api/order/:shopId/cancel/:productId') 23 | .put(authCtrl.requireSignin, shopCtrl.isOwner, productCtrl.increaseQuantity, orderCtrl.update) 24 | 25 | router.route('/api/order/:orderId/charge/:userId/:shopId') 26 | .put(authCtrl.requireSignin, shopCtrl.isOwner, userCtrl.createCharge, orderCtrl.update) 27 | 28 | router.route('/api/order/status/:shopId') 29 | .put(authCtrl.requireSignin, shopCtrl.isOwner, orderCtrl.update) 30 | 31 | router.route('/api/order/:orderId') 32 | .get(orderCtrl.read) 33 | 34 | router.param('userId', userCtrl.userByID) 35 | router.param('shopId', shopCtrl.shopByID) 36 | router.param('productId', productCtrl.productByID) 37 | router.param('orderId', orderCtrl.orderByID) 38 | 39 | export default router 40 | -------------------------------------------------------------------------------- /server/controllers/auth.controller.js: -------------------------------------------------------------------------------- 1 | import User from '../models/user.model' 2 | import jwt from 'jsonwebtoken' 3 | import expressJwt from 'express-jwt' 4 | import config from './../../config/config' 5 | 6 | const signin = async (req, res) => { 7 | try { 8 | let user = await User.findOne({ 9 | "email": req.body.email 10 | }) 11 | 12 | if (!user) 13 | return res.status('401').json({ 14 | error: "User not found" 15 | }) 16 | 17 | if (!user.authenticate(req.body.password)) { 18 | return res.status('401').send({ 19 | error: "Email and password don't match." 20 | }) 21 | } 22 | 23 | const token = jwt.sign({ 24 | _id: user._id 25 | }, config.jwtSecret) 26 | 27 | res.cookie("t", token, { 28 | expire: new Date() + 9999 29 | }) 30 | 31 | return res.json({ 32 | token, 33 | user: {_id: user._id, name: user.name, email: user.email, seller: user.seller} 34 | }) 35 | } catch (err) { 36 | return res.status('401').json({ 37 | error: "Could not sign in" 38 | }) 39 | } 40 | } 41 | 42 | const signout = (req, res) => { 43 | res.clearCookie("t") 44 | return res.status('200').json({ 45 | message: "signed out" 46 | }) 47 | } 48 | 49 | const requireSignin = expressJwt({ 50 | secret: config.jwtSecret, 51 | userProperty: 'auth' 52 | }) 53 | 54 | const hasAuthorization = (req, res, next) => { 55 | const authorized = req.profile && req.auth && req.profile._id == req.auth._id 56 | if (!(authorized)) { 57 | return res.status('403').json({ 58 | error: "User is not authorized" 59 | }) 60 | } 61 | next() 62 | } 63 | 64 | export default { 65 | signin, 66 | signout, 67 | requireSignin, 68 | hasAuthorization 69 | } 70 | -------------------------------------------------------------------------------- /client/cart/cart-helper.js: -------------------------------------------------------------------------------- 1 | const cart = { 2 | itemTotal() { 3 | if (typeof window !== "undefined") { 4 | if (localStorage.getItem('cart')) { 5 | return JSON.parse(localStorage.getItem('cart')).length 6 | } 7 | } 8 | return 0 9 | }, 10 | addItem(item, cb) { 11 | let cart = [] 12 | if (typeof window !== "undefined") { 13 | if (localStorage.getItem('cart')) { 14 | cart = JSON.parse(localStorage.getItem('cart')) 15 | } 16 | cart.push({ 17 | product: item, 18 | quantity: 1, 19 | shop: item.shop._id 20 | }) 21 | localStorage.setItem('cart', JSON.stringify(cart)) 22 | cb() 23 | } 24 | }, 25 | updateCart(itemIndex, quantity) { 26 | let cart = [] 27 | if (typeof window !== "undefined") { 28 | if (localStorage.getItem('cart')) { 29 | cart = JSON.parse(localStorage.getItem('cart')) 30 | } 31 | cart[itemIndex].quantity = quantity 32 | localStorage.setItem('cart', JSON.stringify(cart)) 33 | } 34 | }, 35 | getCart() { 36 | if (typeof window !== "undefined") { 37 | if (localStorage.getItem('cart')) { 38 | return JSON.parse(localStorage.getItem('cart')) 39 | } 40 | } 41 | return [] 42 | }, 43 | removeItem(itemIndex) { 44 | let cart = [] 45 | if (typeof window !== "undefined") { 46 | if (localStorage.getItem('cart')) { 47 | cart = JSON.parse(localStorage.getItem('cart')) 48 | } 49 | cart.splice(itemIndex, 1) 50 | localStorage.setItem('cart', JSON.stringify(cart)) 51 | } 52 | return cart 53 | }, 54 | emptyCart(cb) { 55 | if (typeof window !== "undefined") { 56 | localStorage.removeItem('cart') 57 | cb() 58 | } 59 | } 60 | } 61 | 62 | export default cart 63 | -------------------------------------------------------------------------------- /client/auction/OpenAuctions.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react' 2 | import { makeStyles } from '@material-ui/core/styles' 3 | import Paper from '@material-ui/core/Paper' 4 | import Typography from '@material-ui/core/Typography' 5 | import {listOpen} from './api-auction.js' 6 | import Auctions from './Auctions' 7 | 8 | const useStyles = makeStyles(theme => ({ 9 | root: theme.mixins.gutters({ 10 | maxWidth: 600, 11 | margin: 'auto', 12 | padding: theme.spacing(3), 13 | marginTop: theme.spacing(5) 14 | }), 15 | title: { 16 | margin: `${theme.spacing(1)}px 0 4px ${theme.spacing(1)}px` , 17 | color: theme.palette.protectedTitle, 18 | fontSize: '1.2em' 19 | }, 20 | addButton:{ 21 | float:'right' 22 | }, 23 | leftIcon: { 24 | marginRight: "8px" 25 | } 26 | })) 27 | export default function OpenAuctions(){ 28 | const classes = useStyles() 29 | const [auctions, setAuctions] = useState([]) 30 | 31 | useEffect(() => { 32 | const abortController = new AbortController() 33 | const signal = abortController.signal 34 | listOpen(signal).then((data) => { 35 | if (data.error) { 36 | setRedirectToSignin(true) 37 | } else { 38 | setAuctions(data) 39 | } 40 | }) 41 | return function cleanup(){ 42 | abortController.abort() 43 | } 44 | }, []) 45 | 46 | const removeAuction = (auction) => { 47 | const updatedAuctions = [...auctions] 48 | const index = updatedAuctions.indexOf(auction) 49 | updatedAuctions.splice(index, 1) 50 | setAuctions(updatedAuctions) 51 | } 52 | return ( 53 |
54 | 55 | 56 | All Auctions 57 | 58 | 59 | 60 |
) 61 | } -------------------------------------------------------------------------------- /server/models/user.model.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | import crypto from 'crypto' 3 | const UserSchema = new mongoose.Schema({ 4 | name: { 5 | type: String, 6 | trim: true, 7 | required: 'Name is required' 8 | }, 9 | email: { 10 | type: String, 11 | trim: true, 12 | unique: 'Email already exists', 13 | match: [/.+\@.+\..+/, 'Please fill a valid email address'], 14 | required: 'Email is required' 15 | }, 16 | hashed_password: { 17 | type: String, 18 | required: "Password is required" 19 | }, 20 | salt: String, 21 | updated: Date, 22 | created: { 23 | type: Date, 24 | default: Date.now 25 | }, 26 | seller: { 27 | type: Boolean, 28 | default: false 29 | }, 30 | stripe_seller: {}, 31 | stripe_customer: {} 32 | }) 33 | 34 | UserSchema 35 | .virtual('password') 36 | .set(function(password) { 37 | this._password = password 38 | this.salt = this.makeSalt() 39 | this.hashed_password = this.encryptPassword(password) 40 | }) 41 | .get(function() { 42 | return this._password 43 | }) 44 | 45 | UserSchema.path('hashed_password').validate(function(v) { 46 | if (this._password && this._password.length < 6) { 47 | this.invalidate('password', 'Password must be at least 6 characters.') 48 | } 49 | if (this.isNew && !this._password) { 50 | this.invalidate('password', 'Password is required') 51 | } 52 | }, null) 53 | 54 | UserSchema.methods = { 55 | authenticate: function(plainText) { 56 | return this.encryptPassword(plainText) === this.hashed_password 57 | }, 58 | encryptPassword: function(password) { 59 | if (!password) return '' 60 | try { 61 | return crypto 62 | .createHmac('sha1', this.salt) 63 | .update(password) 64 | .digest('hex') 65 | } catch (err) { 66 | return '' 67 | } 68 | }, 69 | makeSalt: function() { 70 | return Math.round((new Date().valueOf() * Math.random())) + '' 71 | } 72 | } 73 | 74 | export default mongoose.model('User', UserSchema) 75 | -------------------------------------------------------------------------------- /client/core/Home.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react' 2 | import { makeStyles } from '@material-ui/core/styles' 3 | import Grid from '@material-ui/core/Grid' 4 | import Suggestions from './../product/Suggestions' 5 | import {listLatest, listCategories} from './../product/api-product.js' 6 | import Search from './../product/Search' 7 | import Categories from './../product/Categories' 8 | 9 | const useStyles = makeStyles(theme => ({ 10 | root: { 11 | flexGrow: 1, 12 | margin: 30, 13 | } 14 | })) 15 | 16 | 17 | export default function Home(){ 18 | const classes = useStyles() 19 | const [suggestionTitle, setSuggestionTitle] = useState("Latest Products") 20 | const [categories, setCategories] = useState([]) 21 | const [suggestions, setSuggestions] = useState([]) 22 | 23 | useEffect(() => { 24 | const abortController = new AbortController() 25 | const signal = abortController.signal 26 | listLatest(signal).then((data) => { 27 | if (data.error) { 28 | console.log(data.error) 29 | } else { 30 | setSuggestions(data) 31 | } 32 | }) 33 | return function cleanup(){ 34 | abortController.abort() 35 | } 36 | }, []) 37 | 38 | useEffect(() => { 39 | const abortController = new AbortController() 40 | const signal = abortController.signal 41 | listCategories(signal).then((data) => { 42 | if (data.error) { 43 | console.log(data.error) 44 | } else { 45 | setCategories(data) 46 | } 47 | }) 48 | return function cleanup(){ 49 | abortController.abort() 50 | } 51 | }, []) 52 | 53 | return ( 54 |
55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
65 | ) 66 | } 67 | 68 | 69 | -------------------------------------------------------------------------------- /client/shop/DeleteShop.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | import PropTypes from 'prop-types' 3 | import IconButton from '@material-ui/core/IconButton' 4 | import Button from '@material-ui/core/Button' 5 | import DeleteIcon from '@material-ui/icons/Delete' 6 | import Dialog from '@material-ui/core/Dialog' 7 | import DialogActions from '@material-ui/core/DialogActions' 8 | import DialogContent from '@material-ui/core/DialogContent' 9 | import DialogContentText from '@material-ui/core/DialogContentText' 10 | import DialogTitle from '@material-ui/core/DialogTitle' 11 | import auth from './../auth/auth-helper' 12 | import {remove} from './api-shop.js' 13 | 14 | export default function DeleteShop(props) { 15 | const [open, setOpen] = useState(false) 16 | 17 | const jwt = auth.isAuthenticated() 18 | const clickButton = () => { 19 | setOpen(true) 20 | } 21 | const deleteShop = () => { 22 | remove({ 23 | shopId: props.shop._id 24 | }, {t: jwt.token}).then((data) => { 25 | if (data.error) { 26 | console.log(data.error) 27 | } else { 28 | setOpen(false) 29 | props.onRemove(props.shop) 30 | } 31 | }) 32 | } 33 | const handleRequestClose = () => { 34 | setOpen(false) 35 | } 36 | return ( 37 | 38 | 39 | 40 | 41 | 42 | {"Delete "+props.shop.name} 43 | 44 | 45 | Confirm to delete your shop {props.shop.name}. 46 | 47 | 48 | 49 | 52 | 55 | 56 | 57 | ) 58 | } 59 | DeleteShop.propTypes = { 60 | shop: PropTypes.object.isRequired, 61 | onRemove: PropTypes.func.isRequired 62 | } -------------------------------------------------------------------------------- /client/auction/Timer.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react' 2 | import Typography from '@material-ui/core/Typography' 3 | import {makeStyles} from '@material-ui/core/styles' 4 | 5 | const useStyles = makeStyles(theme => ({ 6 | endTime: { 7 | fontSize: '0.75em', 8 | color: '#323232', 9 | fontWeight: 300 10 | }, 11 | subheading: { 12 | margin: '16px', 13 | color: theme.palette.openTitle 14 | }, 15 | })) 16 | 17 | const calculateTimeLeft = (date) => { 18 | const difference = date - new Date() 19 | let timeLeft = {} 20 | 21 | if (difference > 0) { 22 | timeLeft = { 23 | days: Math.floor(difference / (1000 * 60 * 60 * 24)), 24 | hours: Math.floor((difference / (1000 * 60 * 60)) % 24), 25 | minutes: Math.floor((difference / 1000 / 60) % 60), 26 | seconds: Math.floor((difference / 1000) % 60), 27 | timeEnd: false 28 | } 29 | } else { 30 | timeLeft = {timeEnd: true} 31 | } 32 | return timeLeft 33 | } 34 | export default function Timer (props) { 35 | const classes = useStyles() 36 | const [timeLeft, setTimeLeft] = useState(calculateTimeLeft(new Date(props.endTime))) 37 | 38 | useEffect(() => { 39 | let timer = null 40 | if(!timeLeft.timeEnd){ 41 | timer = setTimeout(() => { 42 | setTimeLeft(calculateTimeLeft(new Date(props.endTime))) 43 | }, 1000) 44 | }else{ 45 | props.update() 46 | } 47 | return () => { 48 | clearTimeout(timer) 49 | } 50 | }) 51 | return (
52 | {!timeLeft.timeEnd? 53 | {timeLeft.days != 0 && `${timeLeft.days} d `} 54 | {timeLeft.hours != 0 && `${timeLeft.hours} h `} 55 | {timeLeft.minutes != 0 && `${timeLeft.minutes} m `} 56 | {timeLeft.seconds != 0 && `${timeLeft.seconds} s`} left {`(ends at ${new Date(props.endTime).toLocaleString()})`} : 57 | Auction ended} 58 |
59 | ) 60 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mern-marketplace", 3 | "version": "2.0.0", 4 | "description": "A MERN stack based online marketplace application", 5 | "author": "Shama Hoque", 6 | "license": "MIT", 7 | "keywords": [ 8 | "react", 9 | "express", 10 | "mongodb", 11 | "node", 12 | "mern" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/shamahoque/mern-marketplace.git" 17 | }, 18 | "homepage": "https://github.com/shamahoque/mern-marketplace", 19 | "main": "./dist/server.generated.js", 20 | "scripts": { 21 | "development": "nodemon", 22 | "build": "webpack --config webpack.config.client.production.js && webpack --mode=production --config webpack.config.server.js", 23 | "start": "NODE_ENV=production node ./dist/server.generated.js" 24 | }, 25 | "engines": { 26 | "node": "13.12.0", 27 | "npm": "6.14.4" 28 | }, 29 | "devDependencies": { 30 | "@babel/core": "7.9.0", 31 | "@babel/preset-env": "7.9.0", 32 | "@babel/preset-react": "7.9.4", 33 | "babel-loader": "8.1.0", 34 | "file-loader": "6.0.0", 35 | "nodemon": "2.0.2", 36 | "webpack": "4.42.1", 37 | "webpack-cli": "3.3.11", 38 | "webpack-dev-middleware": "3.7.2", 39 | "webpack-hot-middleware": "2.25.0", 40 | "webpack-node-externals": "1.7.2" 41 | }, 42 | "dependencies": { 43 | "@hot-loader/react-dom": "16.13.0", 44 | "@material-ui/core": "4.9.8", 45 | "@material-ui/icons": "4.9.1", 46 | "body-parser": "1.19.0", 47 | "compression": "1.7.4", 48 | "cookie-parser": "1.4.5", 49 | "cors": "2.8.5", 50 | "express": "4.17.1", 51 | "express-jwt": "5.3.1", 52 | "formidable": "1.2.2", 53 | "helmet": "3.22.0", 54 | "jsonwebtoken": "8.5.1", 55 | "lodash": "4.17.15", 56 | "mongoose": "5.9.7", 57 | "query-string": "6.11.1", 58 | "react": "16.13.1", 59 | "react-dom": "16.13.1", 60 | "react-hot-loader": "4.12.20", 61 | "react-router": "5.1.2", 62 | "react-router-dom": "5.1.2", 63 | "react-stripe-elements": "6.1.1", 64 | "request": "2.88.2", 65 | "socket.io": "2.3.0", 66 | "socket.io-client": "2.3.0", 67 | "stripe": "8.38.0" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /client/auction/DeleteAuction.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | import PropTypes from 'prop-types' 3 | import IconButton from '@material-ui/core/IconButton' 4 | import Button from '@material-ui/core/Button' 5 | import DeleteIcon from '@material-ui/icons/Delete' 6 | import Dialog from '@material-ui/core/Dialog' 7 | import DialogActions from '@material-ui/core/DialogActions' 8 | import DialogContent from '@material-ui/core/DialogContent' 9 | import DialogContentText from '@material-ui/core/DialogContentText' 10 | import DialogTitle from '@material-ui/core/DialogTitle' 11 | import auth from '../auth/auth-helper' 12 | import {remove} from './api-auction.js' 13 | 14 | export default function DeleteAuction(props) { 15 | const [open, setOpen] = useState(false) 16 | 17 | const jwt = auth.isAuthenticated() 18 | const clickButton = () => { 19 | setOpen(true) 20 | } 21 | const deleteAuction = () => { 22 | remove({ 23 | auctionId: props.auction._id 24 | }, {t: jwt.token}).then((data) => { 25 | if (data.error) { 26 | console.log(data.error) 27 | } else { 28 | setOpen(false) 29 | props.onRemove(props.auction) 30 | } 31 | }) 32 | } 33 | const handleRequestClose = () => { 34 | setOpen(false) 35 | } 36 | return ( 37 | 38 | 39 | 40 | 41 | 42 | {"Delete "+props.auction.itemName} 43 | 44 | 45 | Confirm to delete your auction {props.auction.itemName}. 46 | 47 | 48 | 49 | 52 | 55 | 56 | 57 | ) 58 | } 59 | DeleteAuction.propTypes = { 60 | auction: PropTypes.object.isRequired, 61 | onRemove: PropTypes.func.isRequired 62 | } -------------------------------------------------------------------------------- /client/order/MyOrders.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react' 2 | import { makeStyles } from '@material-ui/core/styles' 3 | import Paper from '@material-ui/core/Paper' 4 | import List from '@material-ui/core/List' 5 | import ListItem from '@material-ui/core/ListItem' 6 | import ListItemText from '@material-ui/core/ListItemText' 7 | import Typography from '@material-ui/core/Typography' 8 | import Divider from '@material-ui/core/Divider' 9 | import auth from './../auth/auth-helper' 10 | import {listByUser} from './api-order.js' 11 | import {Link} from 'react-router-dom' 12 | 13 | const useStyles = makeStyles(theme => ({ 14 | root: theme.mixins.gutters({ 15 | maxWidth: 600, 16 | margin: '12px 24px', 17 | padding: theme.spacing(3), 18 | backgroundColor: '#3f3f3f0d' 19 | }), 20 | title: { 21 | margin: `${theme.spacing(2)}px 0 12px ${theme.spacing(1)}px`, 22 | color: theme.palette.openTitle 23 | } 24 | })) 25 | 26 | export default function MyOrders(){ 27 | const classes = useStyles() 28 | const [orders, setOrders] = useState([]) 29 | const jwt = auth.isAuthenticated() 30 | 31 | useEffect(() => { 32 | const abortController = new AbortController() 33 | const signal = abortController.signal 34 | listByUser({ 35 | userId: jwt.user._id 36 | }, {t: jwt.token}).then((data) => { 37 | if (data.error) { 38 | console.log(data.error) 39 | } else { 40 | setOrders(data) 41 | } 42 | }) 43 | return function cleanup(){ 44 | abortController.abort() 45 | } 46 | }, []) 47 | 48 | return ( 49 | 50 | 51 | Your Orders 52 | 53 | 54 | {orders.map((order, i) => { 55 | return 56 | 57 | 58 | {"Order # "+order._id}} secondary={(new Date(order.created)).toDateString()}/> 59 | 60 | 61 | 62 | })} 63 | 64 | 65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /client/product/DeleteProduct.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | import PropTypes from 'prop-types' 3 | import IconButton from '@material-ui/core/IconButton' 4 | import Button from '@material-ui/core/Button' 5 | import DeleteIcon from '@material-ui/icons/Delete' 6 | import Dialog from '@material-ui/core/Dialog' 7 | import DialogActions from '@material-ui/core/DialogActions' 8 | import DialogContent from '@material-ui/core/DialogContent' 9 | import DialogContentText from '@material-ui/core/DialogContentText' 10 | import DialogTitle from '@material-ui/core/DialogTitle' 11 | import auth from './../auth/auth-helper' 12 | import {remove} from './api-product.js' 13 | 14 | export default function DeleteProduct(props) { 15 | const [open, setOpen] = useState(false) 16 | 17 | const jwt = auth.isAuthenticated() 18 | const clickButton = () => { 19 | setOpen(true) 20 | } 21 | const deleteProduct = () => { 22 | remove({ 23 | shopId: props.shopId, 24 | productId: props.product._id 25 | }, {t: jwt.token}).then((data) => { 26 | if (data.error) { 27 | console.log(data.error) 28 | } else { 29 | setOpen(false) 30 | props.onRemove(props.product) 31 | } 32 | }) 33 | } 34 | const handleRequestClose = () => { 35 | setOpen(false) 36 | } 37 | return ( 38 | 39 | 40 | 41 | 42 | {"Delete "+props.product.name} 43 | 44 | 45 | Confirm to delete your product {props.product.name}. 46 | 47 | 48 | 49 | 52 | 55 | 56 | 57 | ) 58 | 59 | } 60 | DeleteProduct.propTypes = { 61 | shopId: PropTypes.string.isRequired, 62 | product: PropTypes.object.isRequired, 63 | onRemove: PropTypes.func.isRequired 64 | } 65 | 66 | -------------------------------------------------------------------------------- /client/user/DeleteUser.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | import PropTypes from 'prop-types' 3 | import IconButton from '@material-ui/core/IconButton' 4 | import Button from '@material-ui/core/Button' 5 | import DeleteIcon from '@material-ui/icons/Delete' 6 | import Dialog from '@material-ui/core/Dialog' 7 | import DialogActions from '@material-ui/core/DialogActions' 8 | import DialogContent from '@material-ui/core/DialogContent' 9 | import DialogContentText from '@material-ui/core/DialogContentText' 10 | import DialogTitle from '@material-ui/core/DialogTitle' 11 | import auth from './../auth/auth-helper' 12 | import {remove} from './api-user.js' 13 | import {Redirect} from 'react-router-dom' 14 | 15 | export default function DeleteUser(props) { 16 | const [open, setOpen] = useState(false) 17 | const [redirect, setRedirect] = useState(false) 18 | 19 | const jwt = auth.isAuthenticated() 20 | const clickButton = () => { 21 | setOpen(true) 22 | } 23 | const deleteAccount = () => { 24 | remove({ 25 | userId: props.userId 26 | }, {t: jwt.token}).then((data) => { 27 | if (data && data.error) { 28 | console.log(data.error) 29 | } else { 30 | auth.clearJWT(() => console.log('deleted')) 31 | setRedirect(true) 32 | } 33 | }) 34 | } 35 | const handleRequestClose = () => { 36 | setOpen(false) 37 | } 38 | 39 | if (redirect) { 40 | return 41 | } 42 | return ( 43 | 44 | 45 | 46 | 47 | 48 | {"Delete Account"} 49 | 50 | 51 | Confirm to delete your account. 52 | 53 | 54 | 55 | 58 | 61 | 62 | 63 | ) 64 | 65 | } 66 | DeleteUser.propTypes = { 67 | userId: PropTypes.string.isRequired 68 | } 69 | -------------------------------------------------------------------------------- /client/shop/api-shop.js: -------------------------------------------------------------------------------- 1 | const create = async (params, credentials, shop) => { 2 | try { 3 | let response = await fetch('/api/shops/by/'+ params.userId, { 4 | method: 'POST', 5 | headers: { 6 | 'Accept': 'application/json', 7 | 'Authorization': 'Bearer ' + credentials.t 8 | }, 9 | body: shop 10 | }) 11 | return response.json() 12 | } catch(err) { 13 | console.log(err) 14 | } 15 | } 16 | 17 | const list = async (signal) => { 18 | try { 19 | let response = await fetch('/api/shops', { 20 | method: 'GET', 21 | signal: signal 22 | }) 23 | return response.json() 24 | }catch(err) { 25 | console.log(err) 26 | } 27 | } 28 | 29 | const listByOwner = async (params, credentials, signal) => { 30 | try { 31 | let response = await fetch('/api/shops/by/'+params.userId, { 32 | method: 'GET', 33 | signal: signal, 34 | headers: { 35 | 'Accept': 'application/json', 36 | 'Authorization': 'Bearer ' + credentials.t 37 | } 38 | }) 39 | return response.json() 40 | }catch(err){ 41 | console.log(err) 42 | } 43 | } 44 | 45 | const read = async (params, signal) => { 46 | try { 47 | let response = await fetch('/api/shop/' + params.shopId, { 48 | method: 'GET', 49 | signal: signal, 50 | }) 51 | return response.json() 52 | }catch(err) { 53 | console.log(err) 54 | } 55 | } 56 | 57 | const update = async (params, credentials, shop) => { 58 | try { 59 | let response = await fetch('/api/shops/' + params.shopId, { 60 | method: 'PUT', 61 | headers: { 62 | 'Accept': 'application/json', 63 | 'Authorization': 'Bearer ' + credentials.t 64 | }, 65 | body: shop 66 | }) 67 | return response.json() 68 | } catch(err) { 69 | console.log(err) 70 | } 71 | } 72 | 73 | const remove = async (params, credentials) => { 74 | try { 75 | let response = await fetch('/api/shops/' + params.shopId, { 76 | method: 'DELETE', 77 | headers: { 78 | 'Accept': 'application/json', 79 | 'Content-Type': 'application/json', 80 | 'Authorization': 'Bearer ' + credentials.t 81 | } 82 | }) 83 | return response.json() 84 | } catch(err) { 85 | console.log(err) 86 | } 87 | } 88 | 89 | export { 90 | create, 91 | list, 92 | listByOwner, 93 | read, 94 | update, 95 | remove 96 | } 97 | -------------------------------------------------------------------------------- /server/controllers/order.controller.js: -------------------------------------------------------------------------------- 1 | import {Order, CartItem} from '../models/order.model' 2 | import errorHandler from './../helpers/dbErrorHandler' 3 | 4 | const create = async (req, res) => { 5 | try { 6 | req.body.order.user = req.profile 7 | let order = new Order(req.body.order) 8 | let result = await order.save() 9 | res.status(200).json(result) 10 | } catch (err){ 11 | return res.status(400).json({ 12 | error: errorHandler.getErrorMessage(err) 13 | }) 14 | } 15 | } 16 | 17 | const listByShop = async (req, res) => { 18 | try { 19 | let orders = await Order.find({"products.shop": req.shop._id}) 20 | .populate({path: 'products.product', select: '_id name price'}) 21 | .sort('-created') 22 | .exec() 23 | res.json(orders) 24 | } catch (err){ 25 | return res.status(400).json({ 26 | error: errorHandler.getErrorMessage(err) 27 | }) 28 | } 29 | } 30 | 31 | const update = async (req, res) => { 32 | try { 33 | let order = await Order.updateOne({'products._id':req.body.cartItemId}, { 34 | 'products.$.status': req.body.status 35 | }) 36 | res.json(order) 37 | } catch (err){ 38 | return res.status(400).json({ 39 | error: errorHandler.getErrorMessage(err) 40 | }) 41 | } 42 | } 43 | 44 | const getStatusValues = (req, res) => { 45 | res.json(CartItem.schema.path('status').enumValues) 46 | } 47 | 48 | const orderByID = async (req, res, next, id) => { 49 | try { 50 | let order = await Order.findById(id).populate('products.product', 'name price').populate('products.shop', 'name').exec() 51 | if (!order) 52 | return res.status('400').json({ 53 | error: "Order not found" 54 | }) 55 | req.order = order 56 | next() 57 | } catch (err){ 58 | return res.status(400).json({ 59 | error: errorHandler.getErrorMessage(err) 60 | }) 61 | } 62 | } 63 | 64 | const listByUser = async (req, res) => { 65 | try{ 66 | let orders = await Order.find({ "user": req.profile._id }) 67 | .sort('-created') 68 | .exec() 69 | res.json(orders) 70 | } catch (err){ 71 | return res.status(400).json({ 72 | error: errorHandler.getErrorMessage(err) 73 | }) 74 | } 75 | } 76 | 77 | const read = (req, res) => { 78 | return res.json(req.order) 79 | } 80 | 81 | export default { 82 | create, 83 | listByShop, 84 | update, 85 | getStatusValues, 86 | orderByID, 87 | listByUser, 88 | read 89 | } 90 | -------------------------------------------------------------------------------- /client/user/Users.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react' 2 | import { makeStyles } from '@material-ui/core/styles' 3 | import Paper from '@material-ui/core/Paper' 4 | import List from '@material-ui/core/List' 5 | import ListItem from '@material-ui/core/ListItem' 6 | import ListItemAvatar from '@material-ui/core/ListItemAvatar' 7 | import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction' 8 | import ListItemText from '@material-ui/core/ListItemText' 9 | import Avatar from '@material-ui/core/Avatar' 10 | import IconButton from '@material-ui/core/IconButton' 11 | import Typography from '@material-ui/core/Typography' 12 | import ArrowForward from '@material-ui/icons/ArrowForward' 13 | import Person from '@material-ui/icons/Person' 14 | import {Link} from 'react-router-dom' 15 | import {list} from './api-user.js' 16 | 17 | const useStyles = makeStyles(theme => ({ 18 | root: theme.mixins.gutters({ 19 | padding: theme.spacing(1), 20 | margin: theme.spacing(5) 21 | }), 22 | title: { 23 | margin: `${theme.spacing(4)}px 0 ${theme.spacing(2)}px`, 24 | color: theme.palette.openTitle 25 | } 26 | })) 27 | 28 | export default function Users() { 29 | const classes = useStyles() 30 | const [users, setUsers] = useState([]) 31 | 32 | useEffect(() => { 33 | const abortController = new AbortController() 34 | const signal = abortController.signal 35 | 36 | list(signal).then((data) => { 37 | if (data && data.error) { 38 | console.log(data.error) 39 | } else { 40 | setUsers(data) 41 | } 42 | }) 43 | 44 | return function cleanup(){ 45 | abortController.abort() 46 | } 47 | }, []) 48 | 49 | return ( 50 | 51 | 52 | All Users 53 | 54 | 55 | {users.map((item, i) => { 56 | return 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | }) 72 | } 73 | 74 | 75 | ) 76 | } 77 | -------------------------------------------------------------------------------- /client/auction/MyAuctions.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react' 2 | import { makeStyles } from '@material-ui/core/styles' 3 | import Paper from '@material-ui/core/Paper' 4 | import Icon from '@material-ui/core/Icon' 5 | import Button from '@material-ui/core/Button' 6 | import Typography from '@material-ui/core/Typography' 7 | import auth from '../auth/auth-helper' 8 | import {listBySeller} from './api-auction.js' 9 | import {Redirect, Link} from 'react-router-dom' 10 | import Auctions from './Auctions' 11 | 12 | const useStyles = makeStyles(theme => ({ 13 | root: theme.mixins.gutters({ 14 | maxWidth: 600, 15 | margin: 'auto', 16 | padding: theme.spacing(3), 17 | marginTop: theme.spacing(5) 18 | }), 19 | title: { 20 | margin: `${theme.spacing(3)}px 0 ${theme.spacing(3)}px ${theme.spacing(1)}px` , 21 | color: theme.palette.protectedTitle, 22 | fontSize: '1.2em' 23 | }, 24 | addButton:{ 25 | float:'right' 26 | }, 27 | leftIcon: { 28 | marginRight: "8px" 29 | } 30 | })) 31 | export default function MyAuctions(){ 32 | const classes = useStyles() 33 | const [auctions, setAuctions] = useState([]) 34 | const [redirectToSignin, setRedirectToSignin] = useState(false) 35 | const jwt = auth.isAuthenticated() 36 | 37 | useEffect(() => { 38 | const abortController = new AbortController() 39 | const signal = abortController.signal 40 | listBySeller({ 41 | userId: jwt.user._id 42 | }, {t: jwt.token}, signal).then((data) => { 43 | if (data.error) { 44 | setRedirectToSignin(true) 45 | } else { 46 | setAuctions(data) 47 | } 48 | }) 49 | return function cleanup(){ 50 | abortController.abort() 51 | } 52 | }, []) 53 | 54 | const removeAuction = (auction) => { 55 | const updatedAuctions = [...auctions] 56 | const index = updatedAuctions.indexOf(auction) 57 | updatedAuctions.splice(index, 1) 58 | setAuctions(updatedAuctions) 59 | } 60 | 61 | if (redirectToSignin) { 62 | return 63 | } 64 | return ( 65 |
66 | 67 | 68 | Your Auctions 69 | 70 | 71 | 74 | 75 | 76 | 77 | 78 | 79 |
) 80 | } -------------------------------------------------------------------------------- /client/product/Products.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import {makeStyles} from '@material-ui/core/styles' 4 | import Typography from '@material-ui/core/Typography' 5 | import GridList from '@material-ui/core/GridList' 6 | import GridListTile from '@material-ui/core/GridListTile' 7 | import GridListTileBar from '@material-ui/core/GridListTileBar' 8 | import {Link} from 'react-router-dom' 9 | import AddToCart from './../cart/AddToCart' 10 | 11 | const useStyles = makeStyles(theme => ({ 12 | root: { 13 | display: 'flex', 14 | flexWrap: 'wrap', 15 | justifyContent: 'space-around', 16 | overflow: 'hidden', 17 | background: theme.palette.background.paper, 18 | textAlign: 'left', 19 | padding: '0 8px' 20 | }, 21 | container: { 22 | minWidth: '100%', 23 | paddingBottom: '14px' 24 | }, 25 | gridList: { 26 | width: '100%', 27 | minHeight: 200, 28 | padding: '16px 0 10px' 29 | }, 30 | title: { 31 | padding:`${theme.spacing(3)}px ${theme.spacing(2.5)}px ${theme.spacing(2)}px`, 32 | color: theme.palette.openTitle, 33 | width: '100%' 34 | }, 35 | tile: { 36 | textAlign: 'center' 37 | }, 38 | image: { 39 | height: '100%' 40 | }, 41 | tileBar: { 42 | backgroundColor: 'rgba(0, 0, 0, 0.72)', 43 | textAlign: 'left' 44 | }, 45 | tileTitle: { 46 | fontSize:'1.1em', 47 | marginBottom:'5px', 48 | color:'rgb(189, 222, 219)', 49 | display:'block' 50 | } 51 | })) 52 | 53 | export default function Products(props){ 54 | const classes = useStyles() 55 | return ( 56 |
57 | {props.products.length > 0 ? 58 | (
59 | 60 | {props.products.map((product, i) => ( 61 | 62 | {product.name} 63 | {product.name}} 65 | subtitle={$ {product.price}} 66 | actionIcon={ 67 | 68 | } 69 | /> 70 | 71 | ))} 72 |
) : props.searched && (No products found! :()} 73 |
) 74 | } 75 | Products.propTypes = { 76 | products: PropTypes.array.isRequired, 77 | searched: PropTypes.bool.isRequired 78 | } 79 | -------------------------------------------------------------------------------- /server/express.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import path from 'path' 3 | import bodyParser from 'body-parser' 4 | import cookieParser from 'cookie-parser' 5 | import compress from 'compression' 6 | import cors from 'cors' 7 | import helmet from 'helmet' 8 | import Template from './../template' 9 | import userRoutes from './routes/user.routes' 10 | import authRoutes from './routes/auth.routes' 11 | import shopRoutes from './routes/shop.routes' 12 | import productRoutes from './routes/product.routes' 13 | import orderRoutes from './routes/order.routes' 14 | import auctionRoutes from './routes/auction.routes' 15 | 16 | // modules for server side rendering 17 | import React from 'react' 18 | import ReactDOMServer from 'react-dom/server' 19 | import MainRouter from './../client/MainRouter' 20 | import { StaticRouter } from 'react-router-dom' 21 | 22 | import { ServerStyleSheets, ThemeProvider } from '@material-ui/styles' 23 | import theme from './../client/theme' 24 | //end 25 | 26 | //comment out before building for production 27 | import devBundle from './devBundle' 28 | 29 | const CURRENT_WORKING_DIR = process.cwd() 30 | const app = express() 31 | 32 | //comment out before building for production 33 | devBundle.compile(app) 34 | 35 | // parse body params and attache them to req.body 36 | app.use(bodyParser.json()) 37 | app.use(bodyParser.urlencoded({ extended: true })) 38 | app.use(cookieParser()) 39 | app.use(compress()) 40 | // secure apps by setting various HTTP headers 41 | app.use(helmet()) 42 | // enable CORS - Cross Origin Resource Sharing 43 | app.use(cors()) 44 | 45 | app.use('/dist', express.static(path.join(CURRENT_WORKING_DIR, 'dist'))) 46 | 47 | // mount routes 48 | app.use('/', userRoutes) 49 | app.use('/', authRoutes) 50 | app.use('/', shopRoutes) 51 | app.use('/', productRoutes) 52 | app.use('/', orderRoutes) 53 | app.use('/', auctionRoutes) 54 | 55 | app.get('*', (req, res) => { 56 | const sheets = new ServerStyleSheets() 57 | const context = {} 58 | const markup = ReactDOMServer.renderToString( 59 | sheets.collect( 60 | 61 | 62 | 63 | 64 | 65 | ) 66 | ) 67 | if (context.url) { 68 | return res.redirect(303, context.url) 69 | } 70 | const css = sheets.toString() 71 | res.status(200).send(Template({ 72 | markup: markup, 73 | css: css 74 | })) 75 | }) 76 | 77 | // Catch unauthorised errors 78 | app.use((err, req, res, next) => { 79 | if (err.name === 'UnauthorizedError') { 80 | res.status(401).json({"error" : err.name + ": " + err.message}) 81 | }else if (err) { 82 | res.status(400).json({"error" : err.name + ": " + err.message}) 83 | console.log(err) 84 | } 85 | }) 86 | 87 | export default app 88 | -------------------------------------------------------------------------------- /client/user/api-user.js: -------------------------------------------------------------------------------- 1 | 2 | const create = async (user) => { 3 | try { 4 | let response = await fetch('/api/users/', { 5 | method: 'POST', 6 | headers: { 7 | 'Accept': 'application/json', 8 | 'Content-Type': 'application/json' 9 | }, 10 | body: JSON.stringify(user) 11 | }) 12 | return await response.json() 13 | } catch(err) { 14 | console.log(err) 15 | } 16 | } 17 | 18 | const list = async (signal) => { 19 | try { 20 | let response = await fetch('/api/users/', { 21 | method: 'GET', 22 | signal: signal, 23 | }) 24 | return await response.json() 25 | } catch(err) { 26 | console.log(err) 27 | } 28 | } 29 | 30 | const read = async (params, credentials, signal) => { 31 | try { 32 | let response = await fetch('/api/users/' + params.userId, { 33 | method: 'GET', 34 | signal: signal, 35 | headers: { 36 | 'Accept': 'application/json', 37 | 'Content-Type': 'application/json', 38 | 'Authorization': 'Bearer ' + credentials.t 39 | } 40 | }) 41 | return await response.json() 42 | } catch(err) { 43 | console.log(err) 44 | } 45 | } 46 | 47 | const update = async (params, credentials, user) => { 48 | try { 49 | let response = await fetch('/api/users/' + params.userId, { 50 | method: 'PUT', 51 | headers: { 52 | 'Accept': 'application/json', 53 | 'Content-Type': 'application/json', 54 | 'Authorization': 'Bearer ' + credentials.t 55 | }, 56 | body: JSON.stringify(user) 57 | }) 58 | return await response.json() 59 | } catch(err) { 60 | console.log(err) 61 | } 62 | } 63 | 64 | const remove = async (params, credentials) => { 65 | try { 66 | let response = await fetch('/api/users/' + params.userId, { 67 | method: 'DELETE', 68 | headers: { 69 | 'Accept': 'application/json', 70 | 'Content-Type': 'application/json', 71 | 'Authorization': 'Bearer ' + credentials.t 72 | } 73 | }) 74 | return await response.json() 75 | } catch(err) { 76 | console.log(err) 77 | } 78 | } 79 | 80 | const stripeUpdate = async (params, credentials, auth_code, signal) => { 81 | try { 82 | let response = await fetch ('/api/stripe_auth/'+params.userId, { 83 | method: 'PUT', 84 | signal: signal, 85 | headers: { 86 | 'Accept': 'application/json', 87 | 'Content-Type': 'application/json', 88 | 'Authorization': 'Bearer ' + credentials.t 89 | }, 90 | body: JSON.stringify({stripe: auth_code}) 91 | }) 92 | return await response.json() 93 | } catch(err) { 94 | console.log(err) 95 | } 96 | } 97 | 98 | export { 99 | create, 100 | list, 101 | read, 102 | update, 103 | remove, 104 | stripeUpdate 105 | } 106 | -------------------------------------------------------------------------------- /client/MainRouter.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Route, Switch} from 'react-router-dom' 3 | import Home from './core/Home' 4 | import Users from './user/Users' 5 | import Signup from './user/Signup' 6 | import Signin from './auth/Signin' 7 | import EditProfile from './user/EditProfile' 8 | import Profile from './user/Profile' 9 | import PrivateRoute from './auth/PrivateRoute' 10 | import Menu from './core/Menu' 11 | import NewShop from './shop/NewShop' 12 | import Shops from './shop/Shops' 13 | import MyShops from './shop/MyShops' 14 | import Shop from './shop/Shop' 15 | import EditShop from './shop/EditShop' 16 | import NewProduct from './product/NewProduct' 17 | import EditProduct from './product/EditProduct' 18 | import Product from './product/Product' 19 | import Cart from './cart/Cart' 20 | import StripeConnect from './user/StripeConnect' 21 | import ShopOrders from './order/ShopOrders' 22 | import Order from './order/Order' 23 | import MyAuctions from './auction/MyAuctions' 24 | import OpenAuctions from './auction/OpenAuctions' 25 | import NewAuction from './auction/NewAuction' 26 | import EditAuction from './auction/EditAuction' 27 | import Auction from './auction/Auction' 28 | 29 | const MainRouter = () => { 30 | return (
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
) 62 | } 63 | 64 | export default MainRouter 65 | -------------------------------------------------------------------------------- /client/user/StripeConnect.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react' 2 | import { makeStyles } from '@material-ui/core/styles' 3 | import Paper from '@material-ui/core/Paper' 4 | import Typography from '@material-ui/core/Typography' 5 | import queryString from 'query-string' 6 | import {stripeUpdate} from './api-user.js' 7 | import auth from './../auth/auth-helper' 8 | 9 | const useStyles = makeStyles(theme => ({ 10 | root: theme.mixins.gutters({ 11 | maxWidth: 600, 12 | margin: 'auto', 13 | padding: theme.spacing(3), 14 | marginTop: theme.spacing(5) 15 | }), 16 | title: { 17 | margin: `${theme.spacing(3)}px 0 ${theme.spacing(2)}px ${theme.spacing(2)}px`, 18 | color: theme.palette.protectedTitle, 19 | fontSize: '1.1em' 20 | }, 21 | subheading: { 22 | color: theme.palette.openTitle, 23 | marginLeft: "24px" 24 | } 25 | })) 26 | 27 | export default function StripeConnect(props){ 28 | const classes = useStyles() 29 | const [values, setValues] = useState({ 30 | error: false, 31 | connecting: false, 32 | connected: false 33 | }) 34 | const jwt = auth.isAuthenticated() 35 | useEffect(() => { 36 | const abortController = new AbortController() 37 | const signal = abortController.signal 38 | 39 | const parsed = queryString.parse(props.location.search) 40 | if(parsed.error){ 41 | setValues({...values, error: true}) 42 | } 43 | if(parsed.code){ 44 | setValues({...values, connecting: true, error: false}) 45 | //post call to stripe, get credentials and update user data 46 | stripeUpdate({ 47 | userId: jwt.user._id 48 | }, { 49 | t: jwt.token 50 | }, parsed.code, signal).then((data) => { 51 | if (data.error) { 52 | setValues({...values, error: true, connected: false, connecting: false}) 53 | } else { 54 | setValues({...values, connected: true, connecting: false, error: false}) 55 | } 56 | }) 57 | } 58 | return function cleanup(){ 59 | abortController.abort() 60 | } 61 | 62 | }, []) 63 | 64 | return ( 65 |
66 | 67 | 68 | Connect your Stripe Account 69 | 70 | {values.error && ( 71 | Could not connect your Stripe account. Try again later. 72 | )} 73 | {values.connecting && ( 74 | Connecting your Stripe account ... 75 | )} 76 | {values.connected && ( 77 | Your Stripe account successfully connected! 78 | )} 79 | 80 |
81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /client/shop/Shops.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import { makeStyles } from '@material-ui/core/styles' 3 | import Paper from '@material-ui/core/Paper' 4 | import List from '@material-ui/core/List' 5 | import ListItem from '@material-ui/core/ListItem' 6 | import ListItemAvatar from '@material-ui/core/ListItemAvatar' 7 | import Avatar from '@material-ui/core/Avatar' 8 | import Typography from '@material-ui/core/Typography' 9 | import Divider from '@material-ui/core/Divider' 10 | import {list} from './api-shop.js' 11 | import {Link} from 'react-router-dom' 12 | 13 | const useStyles = makeStyles(theme => ({ 14 | root: theme.mixins.gutters({ 15 | maxWidth: 600, 16 | margin: 'auto', 17 | padding: theme.spacing(3), 18 | marginTop: theme.spacing(5), 19 | marginBottom: theme.spacing(3) 20 | }), 21 | title: { 22 | margin: `${theme.spacing(3)}px 0 ${theme.spacing(2)}px`, 23 | color: theme.palette.protectedTitle, 24 | textAlign: 'center', 25 | fontSize: '1.2em' 26 | }, 27 | avatar:{ 28 | width: 100, 29 | height: 100 30 | }, 31 | subheading: { 32 | color: theme.palette.text.secondary 33 | }, 34 | shopTitle: { 35 | fontSize: '1.2em', 36 | marginBottom: '5px' 37 | }, 38 | details: { 39 | padding: '24px' 40 | } 41 | })) 42 | export default function Shops(){ 43 | const classes = useStyles() 44 | const [shops, setShops] = useState([]) 45 | 46 | useEffect(() => { 47 | const abortController = new AbortController() 48 | const signal = abortController.signal 49 | list(signal).then((data) => { 50 | if (data.error) { 51 | console.log(data.error) 52 | } else { 53 | setShops(data) 54 | } 55 | }) 56 | return function cleanup(){ 57 | abortController.abort() 58 | } 59 | 60 | }, []) 61 | 62 | return ( 63 |
64 | 65 | 66 | All Shops 67 | 68 | 69 | {shops.map((shop, i) => { 70 | return 71 | 72 | 73 | 74 | 75 | 76 |
77 | 78 | {shop.name} 79 | 80 | 81 | {shop.description} 82 | 83 |
84 |
85 | 86 | })} 87 |
88 |
89 |
) 90 | } 91 | -------------------------------------------------------------------------------- /client/auction/api-auction.js: -------------------------------------------------------------------------------- 1 | const create = async (params, credentials, auction) => { 2 | try { 3 | let response = await fetch('/api/auctions/by/'+ params.userId, { 4 | method: 'POST', 5 | headers: { 6 | 'Accept': 'application/json', 7 | 'Authorization': 'Bearer ' + credentials.t 8 | }, 9 | body: auction 10 | }) 11 | return response.json() 12 | } catch(err) { 13 | console.log(err) 14 | } 15 | } 16 | 17 | const listOpen = async (signal) => { 18 | try { 19 | let response = await fetch('/api/auctions', { 20 | method: 'GET', 21 | signal: signal 22 | }) 23 | return response.json() 24 | }catch(err) { 25 | console.log(err) 26 | } 27 | } 28 | 29 | const listBySeller = async (params, credentials, signal) => { 30 | try { 31 | let response = await fetch('/api/auctions/by/'+params.userId, { 32 | method: 'GET', 33 | signal: signal, 34 | headers: { 35 | 'Accept': 'application/json', 36 | 'Authorization': 'Bearer ' + credentials.t 37 | } 38 | }) 39 | return response.json() 40 | }catch(err){ 41 | console.log(err) 42 | } 43 | } 44 | 45 | const listByBidder = async (params, credentials, signal) => { 46 | try { 47 | let response = await fetch('/api/auctions/bid/'+params.userId, { 48 | method: 'GET', 49 | signal: signal, 50 | headers: { 51 | 'Accept': 'application/json', 52 | 'Authorization': 'Bearer ' + credentials.t 53 | } 54 | }) 55 | return response.json() 56 | }catch(err){ 57 | console.log(err) 58 | } 59 | } 60 | 61 | const read = async (params, signal) => { 62 | try { 63 | let response = await fetch('/api/auction/' + params.auctionId, { 64 | method: 'GET', 65 | signal: signal, 66 | }) 67 | return response.json() 68 | }catch(err) { 69 | console.log(err) 70 | } 71 | } 72 | 73 | const update = async (params, credentials, auction) => { 74 | try { 75 | let response = await fetch('/api/auctions/' + params.auctionId, { 76 | method: 'PUT', 77 | headers: { 78 | 'Accept': 'application/json', 79 | 'Authorization': 'Bearer ' + credentials.t 80 | }, 81 | body: auction 82 | }) 83 | return response.json() 84 | } catch(err) { 85 | console.log(err) 86 | } 87 | } 88 | 89 | const remove = async (params, credentials) => { 90 | try { 91 | let response = await fetch('/api/auctions/' + params.auctionId, { 92 | method: 'DELETE', 93 | headers: { 94 | 'Accept': 'application/json', 95 | 'Content-Type': 'application/json', 96 | 'Authorization': 'Bearer ' + credentials.t 97 | } 98 | }) 99 | return response.json() 100 | } catch(err) { 101 | console.log(err) 102 | } 103 | } 104 | 105 | export { 106 | create, 107 | listOpen, 108 | listBySeller, 109 | listByBidder, 110 | read, 111 | update, 112 | remove 113 | } 114 | -------------------------------------------------------------------------------- /client/core/Menu.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import AppBar from '@material-ui/core/AppBar' 3 | import Toolbar from '@material-ui/core/Toolbar' 4 | import Typography from '@material-ui/core/Typography' 5 | import IconButton from '@material-ui/core/IconButton' 6 | import HomeIcon from '@material-ui/icons/Home' 7 | import Button from '@material-ui/core/Button' 8 | import auth from './../auth/auth-helper' 9 | import {Link, withRouter} from 'react-router-dom' 10 | import CartIcon from '@material-ui/icons/ShoppingCart' 11 | import Badge from '@material-ui/core/Badge' 12 | import cart from './../cart/cart-helper' 13 | 14 | const isActive = (history, path) => { 15 | if (history.location.pathname == path) 16 | return {color: '#bef67a'} 17 | else 18 | return {color: '#ffffff'} 19 | } 20 | const isPartActive = (history, path) => { 21 | if (history.location.pathname.includes(path)) 22 | return {color: '#bef67a'} 23 | else 24 | return {color: '#ffffff'} 25 | } 26 | const Menu = withRouter(({history}) => ( 27 | 28 | 29 | 30 | MERN Marketplace 31 | 32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 51 | 52 |
53 |
54 | { 55 | !auth.isAuthenticated() && ( 56 | 57 | 59 | 60 | 61 | 63 | 64 | ) 65 | } 66 | { 67 | auth.isAuthenticated() && ( 68 | {auth.isAuthenticated().user.seller && (<> 69 | 70 | 71 | 72 | )} 73 | 74 | 75 | 76 | 79 | ) 80 | } 81 |
82 |
83 |
84 | )) 85 | 86 | export default Menu 87 | -------------------------------------------------------------------------------- /client/auth/Signin.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | import Card from '@material-ui/core/Card' 3 | import CardActions from '@material-ui/core/CardActions' 4 | import CardContent from '@material-ui/core/CardContent' 5 | import Button from '@material-ui/core/Button' 6 | import TextField from '@material-ui/core/TextField' 7 | import Typography from '@material-ui/core/Typography' 8 | import Icon from '@material-ui/core/Icon' 9 | import { makeStyles } from '@material-ui/core/styles' 10 | import auth from './../auth/auth-helper' 11 | import {Redirect} from 'react-router-dom' 12 | import {signin} from './api-auth.js' 13 | 14 | const useStyles = makeStyles(theme => ({ 15 | card: { 16 | maxWidth: 600, 17 | margin: 'auto', 18 | textAlign: 'center', 19 | marginTop: theme.spacing(5), 20 | paddingBottom: theme.spacing(2) 21 | }, 22 | error: { 23 | verticalAlign: 'middle' 24 | }, 25 | title: { 26 | marginTop: theme.spacing(2), 27 | color: theme.palette.openTitle 28 | }, 29 | textField: { 30 | marginLeft: theme.spacing(1), 31 | marginRight: theme.spacing(1), 32 | width: 300 33 | }, 34 | submit: { 35 | margin: 'auto', 36 | marginBottom: theme.spacing(2) 37 | } 38 | })) 39 | 40 | export default function Signin(props) { 41 | const classes = useStyles() 42 | const [values, setValues] = useState({ 43 | email: '', 44 | password: '', 45 | error: '', 46 | redirectToReferrer: false 47 | }) 48 | 49 | const clickSubmit = () => { 50 | const user = { 51 | email: values.email || undefined, 52 | password: values.password || undefined 53 | } 54 | 55 | signin(user).then((data) => { 56 | if (data.error) { 57 | setValues({ ...values, error: data.error}) 58 | } else { 59 | auth.authenticate(data, () => { 60 | setValues({ ...values, error: '',redirectToReferrer: true}) 61 | }) 62 | } 63 | }) 64 | } 65 | 66 | const handleChange = name => event => { 67 | setValues({ ...values, [name]: event.target.value }) 68 | } 69 | 70 | const {from} = props.location.state || { 71 | from: { 72 | pathname: '/' 73 | } 74 | } 75 | const {redirectToReferrer} = values 76 | if (redirectToReferrer) { 77 | return () 78 | } 79 | 80 | return ( 81 | 82 | 83 | 84 | Sign In 85 | 86 |
87 | 88 |
{ 89 | values.error && ( 90 | error 91 | {values.error} 92 | ) 93 | } 94 |
95 | 96 | 97 | 98 |
99 | ) 100 | } 101 | 102 | 103 | -------------------------------------------------------------------------------- /client/product/api-product.js: -------------------------------------------------------------------------------- 1 | import queryString from 'query-string' 2 | const create = async (params, credentials, product) => { 3 | try { 4 | let response = await fetch('/api/products/by/'+ params.shopId, { 5 | method: 'POST', 6 | headers: { 7 | 'Accept': 'application/json', 8 | 'Authorization': 'Bearer ' + credentials.t 9 | }, 10 | body: product 11 | }) 12 | return response.json() 13 | }catch(err) { 14 | console.log(err) 15 | } 16 | } 17 | 18 | const read = async (params, signal) => { 19 | try { 20 | let response = await fetch('/api/products/' + params.productId, { 21 | method: 'GET', 22 | signal: signal 23 | }) 24 | return response.json() 25 | } catch(err) { 26 | console.log(err) 27 | } 28 | } 29 | 30 | const update = async (params, credentials, product) => { 31 | try { 32 | let response = await fetch('/api/product/' + params.shopId +'/'+params.productId, { 33 | method: 'PUT', 34 | headers: { 35 | 'Accept': 'application/json', 36 | 'Authorization': 'Bearer ' + credentials.t 37 | }, 38 | body: product 39 | }) 40 | return response.json() 41 | } catch(err) { 42 | console.log(err) 43 | } 44 | } 45 | 46 | const remove = async (params, credentials) => { 47 | try { 48 | let response = await fetch('/api/product/' + params.shopId +'/'+params.productId, { 49 | method: 'DELETE', 50 | headers: { 51 | 'Accept': 'application/json', 52 | 'Content-Type': 'application/json', 53 | 'Authorization': 'Bearer ' + credentials.t 54 | } 55 | }) 56 | return response.json() 57 | } catch(err) { 58 | console.log(err) 59 | } 60 | } 61 | 62 | const listByShop = async (params, signal) => { 63 | try { 64 | let response = await fetch('/api/products/by/'+params.shopId, { 65 | method: 'GET', 66 | signal: signal 67 | }) 68 | return response.json() 69 | } catch(err) { 70 | console.log(err) 71 | } 72 | } 73 | 74 | const listLatest = async (signal) => { 75 | try { 76 | let response = await fetch('/api/products/latest', { 77 | method: 'GET', 78 | signal: signal 79 | }) 80 | return response.json() 81 | } catch(err) { 82 | console.log(err) 83 | } 84 | } 85 | 86 | const listRelated = async (params, signal) => { 87 | try { 88 | let response = await fetch('/api/products/related/'+params.productId, { 89 | method: 'GET', 90 | signal: signal 91 | }) 92 | return response.json() 93 | }catch(err) { 94 | console.log(err) 95 | } 96 | } 97 | 98 | const listCategories = async (signal) => { 99 | try { 100 | let response = await fetch('/api/products/categories', { 101 | method: 'GET', 102 | signal: signal 103 | }) 104 | return response.json() 105 | } catch(err) { 106 | console.log(err) 107 | } 108 | } 109 | 110 | const list = async (params, signal) => { 111 | const query = queryString.stringify(params) 112 | try { 113 | let response = await fetch('/api/products?'+query, { 114 | method: 'GET', 115 | signal: signal 116 | }) 117 | return response.json() 118 | }catch(err) { 119 | console.log(err) 120 | } 121 | } 122 | 123 | export { 124 | create, 125 | read, 126 | update, 127 | remove, 128 | listByShop, 129 | listLatest, 130 | listRelated, 131 | listCategories, 132 | list 133 | } 134 | -------------------------------------------------------------------------------- /client/product/Categories.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react' 2 | import PropTypes from 'prop-types' 3 | import {makeStyles} from '@material-ui/core/styles' 4 | import Card from '@material-ui/core/Card' 5 | import Typography from '@material-ui/core/Typography' 6 | import Divider from '@material-ui/core/Divider' 7 | import GridList from '@material-ui/core/GridList' 8 | import GridListTile from '@material-ui/core/GridListTile' 9 | import Icon from '@material-ui/core/Icon' 10 | import {list} from './api-product.js' 11 | import Products from './Products' 12 | 13 | const useStyles = makeStyles(theme => ({ 14 | root: { 15 | display: 'flex', 16 | flexWrap: 'wrap', 17 | justifyContent: 'space-around', 18 | overflow: 'hidden', 19 | background: theme.palette.background.paper, 20 | }, 21 | gridList: { 22 | flexWrap: 'nowrap', 23 | width:'100%', 24 | transform: 'translateZ(0)', 25 | }, 26 | tileTitle: { 27 | verticalAlign: 'middle', 28 | lineHeight: 2.5, 29 | textAlign: 'center', 30 | fontSize: '1.35em', 31 | margin: '0 4px 0 0', 32 | }, 33 | card: { 34 | margin: 'auto', 35 | marginTop: 20 36 | }, 37 | title: { 38 | padding:`${theme.spacing(3)}px ${theme.spacing(2.5)}px ${theme.spacing(2)}px`, 39 | color: theme.palette.openTitle, 40 | backgroundColor: '#80808024', 41 | fontSize: '1.1em' 42 | }, 43 | icon: { 44 | verticalAlign: 'sub', 45 | color: '#738272', 46 | fontSize: '0.9em' 47 | }, 48 | link: { 49 | color: '#4d6538', 50 | textShadow: '0px 2px 12px #ffffff', 51 | cursor:'pointer' 52 | } 53 | })) 54 | 55 | export default function Categories(props){ 56 | const classes = useStyles() 57 | const [products, setProducts] = useState([]) 58 | const [selected, setSelected] = useState(props.categories[0]) 59 | 60 | useEffect(() => { 61 | const abortController = new AbortController() 62 | const signal = abortController.signal 63 | 64 | list({ 65 | category: props.categories[0] 66 | }, signal).then((data) => { 67 | if (data.error) { 68 | console.log(data.error) 69 | } else { 70 | setProducts(data) 71 | } 72 | }) 73 | return function cleanup(){ 74 | abortController.abort() 75 | } 76 | }, [props.categories]) 77 | 78 | const listbyCategory = category => event => { 79 | setSelected(category) 80 | list({ 81 | category: category 82 | }).then((data) => { 83 | if (data.error) { 84 | console.log(data.error) 85 | } else { 86 | setProducts(data) 87 | } 88 | }) 89 | } 90 | 91 | return ( 92 |
93 | 94 | 95 | Explore by category 96 | 97 |
98 | 99 | {props.categories.map((tile, i) => ( 100 | 101 | {tile} {selected == tile && 'arrow_drop_down'} 102 | 103 | ))} 104 | 105 |
106 | 107 | 108 |
109 |
110 | ) 111 | } 112 | Categories.propTypes = { 113 | categories: PropTypes.array.isRequired 114 | } 115 | -------------------------------------------------------------------------------- /client/cart/PlaceOrder.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | import PropTypes from 'prop-types' 3 | import {makeStyles} from '@material-ui/core/styles' 4 | import Button from '@material-ui/core/Button' 5 | import Typography from '@material-ui/core/Typography' 6 | import Icon from '@material-ui/core/Icon' 7 | import auth from './../auth/auth-helper' 8 | import cart from './cart-helper.js' 9 | import {CardElement, injectStripe} from 'react-stripe-elements' 10 | import {create} from './../order/api-order.js' 11 | import {Redirect} from 'react-router-dom' 12 | 13 | const useStyles = makeStyles(theme => ({ 14 | subheading: { 15 | color: 'rgba(88, 114, 128, 0.87)', 16 | marginTop: "20px", 17 | }, 18 | checkout: { 19 | float: 'right', 20 | margin: '20px 30px' 21 | }, 22 | error: { 23 | display: 'inline', 24 | padding: "0px 10px" 25 | }, 26 | errorIcon: { 27 | verticalAlign: 'middle' 28 | }, 29 | StripeElement: { 30 | display: 'block', 31 | margin: '24px 0 10px 10px', 32 | maxWidth: '408px', 33 | padding: '10px 14px', 34 | boxShadow: 'rgba(50, 50, 93, 0.14902) 0px 1px 3px, rgba(0, 0, 0, 0.0196078) 0px 1px 0px', 35 | borderRadius: '4px', 36 | background: 'white' 37 | } 38 | })) 39 | 40 | const PlaceOrder = (props) => { 41 | const classes = useStyles() 42 | const [values, setValues] = useState({ 43 | order: {}, 44 | error: '', 45 | redirect: false, 46 | orderId: '' 47 | }) 48 | 49 | const placeOrder = ()=>{ 50 | props.stripe.createToken().then(payload => { 51 | if(payload.error){ 52 | setValues({...values, error: payload.error.message}) 53 | }else{ 54 | const jwt = auth.isAuthenticated() 55 | create({userId:jwt.user._id}, { 56 | t: jwt.token 57 | }, props.checkoutDetails, payload.token.id).then((data) => { 58 | if (data.error) { 59 | setValues({...values, error: data.error}) 60 | } else { 61 | cart.emptyCart(()=> { 62 | setValues({...values, 'orderId':data._id,'redirect': true}) 63 | }) 64 | } 65 | }) 66 | } 67 | }) 68 | } 69 | 70 | 71 | if (values.redirect) { 72 | return () 73 | } 74 | return ( 75 | 76 | 77 | Card details 78 | 79 | 96 |
97 | { values.error && 98 | ( 99 | error 100 | {values.error} 101 | ) 102 | } 103 | 104 |
105 |
) 106 | 107 | } 108 | PlaceOrder.propTypes = { 109 | checkoutDetails: PropTypes.object.isRequired 110 | } 111 | 112 | export default injectStripe(PlaceOrder) 113 | -------------------------------------------------------------------------------- /client/product/Search.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | import PropTypes from 'prop-types' 3 | import {makeStyles} from '@material-ui/core/styles' 4 | import Card from '@material-ui/core/Card' 5 | import Divider from '@material-ui/core/Divider' 6 | import MenuItem from '@material-ui/core/MenuItem' 7 | import TextField from '@material-ui/core/TextField' 8 | import Button from '@material-ui/core/Button' 9 | import SearchIcon from '@material-ui/icons/Search' 10 | import {list} from './api-product.js' 11 | import Products from './Products' 12 | 13 | const useStyles = makeStyles(theme => ({ 14 | card: { 15 | margin: 'auto', 16 | textAlign: 'center', 17 | paddingTop: 10, 18 | backgroundColor: '#80808024' 19 | }, 20 | menu: { 21 | width: 200, 22 | }, 23 | textField: { 24 | marginLeft: theme.spacing(1), 25 | marginRight: theme.spacing(1), 26 | width: 130, 27 | verticalAlign: 'bottom', 28 | marginBottom: '20px' 29 | }, 30 | searchField: { 31 | marginLeft: theme.spacing(1), 32 | marginRight: theme.spacing(1), 33 | width: 300, 34 | marginBottom: '20px' 35 | }, 36 | searchButton: { 37 | minWidth: '20px', 38 | height: '30px', 39 | padding: '0 8px', 40 | marginBottom: '20px' 41 | } 42 | })) 43 | 44 | export default function Search(props) { 45 | const classes = useStyles() 46 | const [values, setValues] = useState({ 47 | category: '', 48 | search: '', 49 | results: [], 50 | searched: false 51 | }) 52 | const handleChange = name => event => { 53 | setValues({ 54 | ...values, [name]: event.target.value, 55 | }) 56 | } 57 | const search = () => { 58 | if(values.search){ 59 | list({ 60 | search: values.search || undefined, category: values.category 61 | }).then((data) => { 62 | if (data.error) { 63 | console.log(data.error) 64 | } else { 65 | setValues({...values, results: data, searched:true}) 66 | } 67 | }) 68 | } 69 | } 70 | const enterKey = (event) => { 71 | if(event.keyCode == 13){ 72 | event.preventDefault() 73 | search() 74 | } 75 | } 76 | return ( 77 |
78 | 79 | 92 | 93 | All 94 | 95 | { props.categories.map(option => ( 96 | 97 | {option} 98 | 99 | ))} 100 | 101 | 110 | 113 | 114 | 115 | 116 |
117 | ) 118 | } 119 | Search.propTypes = { 120 | categories: PropTypes.array.isRequired 121 | } -------------------------------------------------------------------------------- /client/order/api-order.js: -------------------------------------------------------------------------------- 1 | const create = async (params, credentials, order, token) => { 2 | try { 3 | let response = await fetch('/api/orders/'+params.userId, { 4 | method: 'POST', 5 | headers: { 6 | 'Accept': 'application/json', 7 | 'Content-Type': 'application/json', 8 | 'Authorization': 'Bearer ' + credentials.t 9 | }, 10 | body: JSON.stringify({order: order, token:token}) 11 | }) 12 | return response.json() 13 | }catch(err) { 14 | console.log(err) 15 | } 16 | } 17 | 18 | const listByShop = async (params, credentials, signal) => { 19 | try { 20 | let response = await fetch('/api/orders/shop/'+params.shopId, { 21 | method: 'GET', 22 | signal: signal, 23 | headers: { 24 | 'Accept': 'application/json', 25 | 'Authorization': 'Bearer ' + credentials.t 26 | } 27 | }) 28 | return response.json() 29 | }catch(err){ 30 | console.log(err) 31 | } 32 | } 33 | 34 | const update = async (params, credentials, product) => { 35 | try { 36 | let response = await fetch('/api/order/status/' + params.shopId, { 37 | method: 'PUT', 38 | headers: { 39 | 'Accept': 'application/json', 40 | 'Content-Type': 'application/json', 41 | 'Authorization': 'Bearer ' + credentials.t 42 | }, 43 | body: JSON.stringify(product) 44 | }) 45 | return response.json() 46 | } catch(err){ 47 | console.log(err) 48 | } 49 | } 50 | 51 | const cancelProduct = async (params, credentials, product) => { 52 | try { 53 | let response = await fetch('/api/order/'+params.shopId+'/cancel/'+params.productId, { 54 | method: 'PUT', 55 | headers: { 56 | 'Accept': 'application/json', 57 | 'Content-Type': 'application/json', 58 | 'Authorization': 'Bearer ' + credentials.t 59 | }, 60 | body: JSON.stringify(product) 61 | }) 62 | return response.json() 63 | }catch(err){ 64 | console.log(err) 65 | } 66 | } 67 | 68 | const processCharge = async (params, credentials, product) => { 69 | try { 70 | let response = await fetch('/api/order/'+params.orderId+'/charge/'+params.userId+'/'+params.shopId, { 71 | method: 'PUT', 72 | headers: { 73 | 'Accept': 'application/json', 74 | 'Content-Type': 'application/json', 75 | 'Authorization': 'Bearer ' + credentials.t 76 | }, 77 | body: JSON.stringify(product) 78 | }) 79 | return response.json() 80 | } catch(err) { 81 | console.log(err) 82 | } 83 | } 84 | 85 | const getStatusValues = async (signal) => { 86 | try { 87 | let response = await fetch('/api/order/status_values', { 88 | method: 'GET', 89 | signal: signal 90 | }) 91 | return response.json() 92 | }catch(err) { 93 | console.log(err) 94 | } 95 | } 96 | 97 | const listByUser = async (params, credentials, signal) => { 98 | try { 99 | let response = await fetch('/api/orders/user/'+params.userId, { 100 | method: 'GET', 101 | signal: signal, 102 | headers: { 103 | 'Accept': 'application/json', 104 | 'Authorization': 'Bearer ' + credentials.t 105 | } 106 | }) 107 | return response.json() 108 | }catch(err) { 109 | console.log(err) 110 | } 111 | } 112 | 113 | const read = async (params, credentials, signal) => { 114 | try { 115 | let response = await fetch('/api/order/' + params.orderId, { 116 | method: 'GET', 117 | signal: signal 118 | }) 119 | return response.json() 120 | } catch(err) { 121 | console.log(err) 122 | } 123 | } 124 | 125 | export { 126 | create, 127 | listByShop, 128 | update, 129 | cancelProduct, 130 | processCharge, 131 | getStatusValues, 132 | listByUser, 133 | read 134 | } 135 | -------------------------------------------------------------------------------- /client/shop/Shop.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react' 2 | import { makeStyles } from '@material-ui/core/styles' 3 | import Card from '@material-ui/core/Card' 4 | import CardContent from '@material-ui/core/CardContent' 5 | import Typography from '@material-ui/core/Typography' 6 | import Avatar from '@material-ui/core/Avatar' 7 | import Grid from '@material-ui/core/Grid' 8 | import {read} from './api-shop.js' 9 | import Products from './../product/Products' 10 | import {listByShop} from './../product/api-product.js' 11 | 12 | const useStyles = makeStyles(theme => ({ 13 | root: { 14 | flexGrow: 1, 15 | margin: 30, 16 | }, 17 | card: { 18 | textAlign: 'center', 19 | paddingBottom: theme.spacing(2) 20 | }, 21 | title: { 22 | margin: theme.spacing(2), 23 | color: theme.palette.protectedTitle, 24 | fontSize: '1.2em' 25 | }, 26 | subheading: { 27 | marginTop: theme.spacing(1), 28 | color: theme.palette.openTitle 29 | }, 30 | bigAvatar: { 31 | width: 100, 32 | height: 100, 33 | margin: 'auto' 34 | }, 35 | productTitle: { 36 | padding:`${theme.spacing(3)}px ${theme.spacing(2.5)}px ${theme.spacing(1)}px ${theme.spacing(2)}px`, 37 | color: theme.palette.openTitle, 38 | width: '100%', 39 | fontSize: '1.2em' 40 | } 41 | })) 42 | 43 | export default function Shop({match}) { 44 | const classes = useStyles() 45 | const [shop, setShop] = useState('') 46 | const [products, setProducts] = useState([]) 47 | const [error, setError] = useState('') 48 | 49 | useEffect(() => { 50 | const abortController = new AbortController() 51 | const signal = abortController.signal 52 | 53 | listByShop({ 54 | shopId: match.params.shopId 55 | }, signal).then((data)=>{ 56 | if (data.error) { 57 | setError(data.error) 58 | } else { 59 | setProducts(data) 60 | } 61 | }) 62 | read({ 63 | shopId: match.params.shopId 64 | }, signal).then((data) => { 65 | if (data.error) { 66 | setError(data.error) 67 | } else { 68 | setShop(data) 69 | } 70 | }) 71 | 72 | return function cleanup(){ 73 | abortController.abort() 74 | } 75 | 76 | }, [match.params.shopId]) 77 | useEffect(() => { 78 | const abortController = new AbortController() 79 | const signal = abortController.signal 80 | 81 | listByShop({ 82 | shopId: match.params.shopId 83 | }, signal).then((data)=>{ 84 | if (data.error) { 85 | setError(data.error) 86 | } else { 87 | setProducts(data) 88 | } 89 | }) 90 | 91 | return function cleanup(){ 92 | abortController.abort() 93 | } 94 | 95 | }, [match.params.shopId]) 96 | 97 | const logoUrl = shop._id 98 | ? `/api/shops/logo/${shop._id}?${new Date().getTime()}` 99 | : '/api/shops/defaultphoto' 100 | return (
101 | 102 | 103 | 104 | 105 | 106 | {shop.name} 107 | 108 |
109 |
110 | 111 | {shop.description} 112 |
113 |
114 |
115 |
116 | 117 | 118 | Products 119 | 120 | 121 | 122 |
123 |
) 124 | } 125 | -------------------------------------------------------------------------------- /client/auction/Auctions.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import List from '@material-ui/core/List' 4 | import ListItem from '@material-ui/core/ListItem' 5 | import ListItemAvatar from '@material-ui/core/ListItemAvatar' 6 | import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction' 7 | import ListItemText from '@material-ui/core/ListItemText' 8 | import Avatar from '@material-ui/core/Avatar' 9 | import IconButton from '@material-ui/core/IconButton' 10 | import Edit from '@material-ui/icons/Edit' 11 | import ViewIcon from '@material-ui/icons/Visibility' 12 | import Divider from '@material-ui/core/Divider' 13 | import DeleteAuction from './DeleteAuction' 14 | import auth from '../auth/auth-helper' 15 | import {Link} from 'react-router-dom' 16 | 17 | const calculateTimeLeft = (date) => { 18 | const difference = date - new Date() 19 | let timeLeft = {} 20 | 21 | if (difference > 0) { 22 | timeLeft = { 23 | days: Math.floor(difference / (1000 * 60 * 60 * 24)), 24 | hours: Math.floor((difference / (1000 * 60 * 60)) % 24), 25 | minutes: Math.floor((difference / 1000 / 60) % 60), 26 | seconds: Math.floor((difference / 1000) % 60), 27 | timeEnd: false 28 | } 29 | } else { 30 | timeLeft = {timeEnd: true} 31 | } 32 | return timeLeft 33 | } 34 | 35 | export default function Auctions(props){ 36 | const currentDate = new Date() 37 | const showTimeLeft = (date) => { 38 | let timeLeft = calculateTimeLeft(date) 39 | return !timeLeft.timeEnd && 40 | {timeLeft.days != 0 && `${timeLeft.days} d `} 41 | {timeLeft.hours != 0 && `${timeLeft.hours} h `} 42 | {timeLeft.minutes != 0 && `${timeLeft.minutes} m `} 43 | {timeLeft.seconds != 0 && `${timeLeft.seconds} s`} left 44 | 45 | } 46 | const auctionState = (auction)=>{ 47 | return ( 48 | 49 | {currentDate < new Date(auction.bidStart) && `Auction Starts at ${new Date(auction.bidStart).toLocaleString()}`} 50 | {currentDate > new Date(auction.bidStart) && currentDate < new Date(auction.bidEnd) && <>{`Auction is live | ${auction.bids.length} bids |`} {showTimeLeft(new Date(auction.bidEnd))}} 51 | {currentDate > new Date(auction.bidEnd) && `Auction Ended | ${auction.bids.length} bids `} 52 | {currentDate > new Date(auction.bidStart) && auction.bids.length> 0 && ` | Last bid: $ ${auction.bids[0].bid}`} 53 | 54 | ) 55 | } 56 | return ( 57 | 58 | {props.auctions.map((auction, i) => { 59 | return 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | { auth.isAuthenticated().user && auth.isAuthenticated().user._id == auction.seller._id && 72 | <> 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | } 81 | 82 | 83 | 84 | })} 85 | 86 | ) 87 | } 88 | 89 | Auctions.propTypes = { 90 | auctions: PropTypes.array.isRequired, 91 | removeAuction: PropTypes.func.isRequired 92 | } 93 | 94 | -------------------------------------------------------------------------------- /client/user/Signup.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | import Card from '@material-ui/core/Card' 3 | import CardActions from '@material-ui/core/CardActions' 4 | import CardContent from '@material-ui/core/CardContent' 5 | import Button from '@material-ui/core/Button' 6 | import TextField from '@material-ui/core/TextField' 7 | import Typography from '@material-ui/core/Typography' 8 | import Icon from '@material-ui/core/Icon' 9 | import { makeStyles } from '@material-ui/core/styles' 10 | import {create} from './api-user.js' 11 | import Dialog from '@material-ui/core/Dialog' 12 | import DialogActions from '@material-ui/core/DialogActions' 13 | import DialogContent from '@material-ui/core/DialogContent' 14 | import DialogContentText from '@material-ui/core/DialogContentText' 15 | import DialogTitle from '@material-ui/core/DialogTitle' 16 | import {Link} from 'react-router-dom' 17 | 18 | const useStyles = makeStyles(theme => ({ 19 | card: { 20 | maxWidth: 600, 21 | margin: 'auto', 22 | textAlign: 'center', 23 | marginTop: theme.spacing(5), 24 | paddingBottom: theme.spacing(2) 25 | }, 26 | error: { 27 | verticalAlign: 'middle' 28 | }, 29 | title: { 30 | marginTop: theme.spacing(2), 31 | color: theme.palette.openTitle 32 | }, 33 | textField: { 34 | marginLeft: theme.spacing(1), 35 | marginRight: theme.spacing(1), 36 | width: 300 37 | }, 38 | submit: { 39 | margin: 'auto', 40 | marginBottom: theme.spacing(2) 41 | } 42 | })) 43 | 44 | export default function Signup() { 45 | const classes = useStyles() 46 | const [values, setValues] = useState({ 47 | name: '', 48 | password: '', 49 | email: '', 50 | open: false, 51 | error: '' 52 | }) 53 | 54 | const handleChange = name => event => { 55 | setValues({ ...values, [name]: event.target.value }) 56 | } 57 | 58 | const clickSubmit = () => { 59 | const user = { 60 | name: values.name || undefined, 61 | email: values.email || undefined, 62 | password: values.password || undefined 63 | } 64 | create(user).then((data) => { 65 | if (data.error) { 66 | setValues({ ...values, error: data.error}) 67 | } else { 68 | setValues({ ...values, error: '', open: true}) 69 | } 70 | }) 71 | } 72 | return (
73 | 74 | 75 | 76 | Sign Up 77 | 78 |
79 |
80 | 81 |
{ 82 | values.error && ( 83 | error 84 | {values.error}) 85 | } 86 |
87 | 88 | 89 | 90 |
91 | 92 | New Account 93 | 94 | 95 | New account successfully created. 96 | 97 | 98 | 99 | 100 | 103 | 104 | 105 | 106 |
107 | ) 108 | } -------------------------------------------------------------------------------- /server/controllers/shop.controller.js: -------------------------------------------------------------------------------- 1 | import Shop from '../models/shop.model' 2 | import extend from 'lodash/extend' 3 | import errorHandler from './../helpers/dbErrorHandler' 4 | import formidable from 'formidable' 5 | import fs from 'fs' 6 | import defaultImage from './../../client/assets/images/default.png' 7 | 8 | const create = (req, res) => { 9 | let form = new formidable.IncomingForm() 10 | form.keepExtensions = true 11 | form.parse(req, async (err, fields, files) => { 12 | if (err) { 13 | res.status(400).json({ 14 | message: "Image could not be uploaded" 15 | }) 16 | } 17 | let shop = new Shop(fields) 18 | shop.owner= req.profile 19 | if(files.image){ 20 | shop.image.data = fs.readFileSync(files.image.path) 21 | shop.image.contentType = files.image.type 22 | } 23 | try { 24 | let result = await shop.save() 25 | res.status(200).json(result) 26 | }catch (err){ 27 | return res.status(400).json({ 28 | error: errorHandler.getErrorMessage(err) 29 | }) 30 | } 31 | }) 32 | } 33 | 34 | const shopByID = async (req, res, next, id) => { 35 | try { 36 | let shop = await Shop.findById(id).populate('owner', '_id name').exec() 37 | if (!shop) 38 | return res.status('400').json({ 39 | error: "Shop not found" 40 | }) 41 | req.shop = shop 42 | next() 43 | } catch (err) { 44 | return res.status('400').json({ 45 | error: "Could not retrieve shop" 46 | }) 47 | } 48 | } 49 | 50 | const photo = (req, res, next) => { 51 | if(req.shop.image.data){ 52 | res.set("Content-Type", req.shop.image.contentType) 53 | return res.send(req.shop.image.data) 54 | } 55 | next() 56 | } 57 | const defaultPhoto = (req, res) => { 58 | return res.sendFile(process.cwd()+defaultImage) 59 | } 60 | 61 | const read = (req, res) => { 62 | req.shop.image = undefined 63 | return res.json(req.shop) 64 | } 65 | 66 | const update = (req, res) => { 67 | let form = new formidable.IncomingForm() 68 | form.keepExtensions = true 69 | form.parse(req, async (err, fields, files) => { 70 | if (err) { 71 | res.status(400).json({ 72 | message: "Photo could not be uploaded" 73 | }) 74 | } 75 | let shop = req.shop 76 | shop = extend(shop, fields) 77 | shop.updated = Date.now() 78 | if(files.image){ 79 | shop.image.data = fs.readFileSync(files.image.path) 80 | shop.image.contentType = files.image.type 81 | } 82 | try { 83 | let result = await shop.save() 84 | res.json(result) 85 | }catch (err){ 86 | return res.status(400).json({ 87 | error: errorHandler.getErrorMessage(err) 88 | }) 89 | } 90 | }) 91 | } 92 | 93 | const remove = async (req, res) => { 94 | try { 95 | let shop = req.shop 96 | let deletedShop = shop.remove() 97 | res.json(deletedShop) 98 | } catch (err) { 99 | return res.status(400).json({ 100 | error: errorHandler.getErrorMessage(err) 101 | }) 102 | } 103 | } 104 | 105 | const list = async (req, res) => { 106 | try { 107 | let shops = await Shop.find() 108 | res.json(shops) 109 | } catch (err){ 110 | return res.status(400).json({ 111 | error: errorHandler.getErrorMessage(err) 112 | }) 113 | } 114 | } 115 | 116 | const listByOwner = async (req, res) => { 117 | try { 118 | let shops = await Shop.find({owner: req.profile._id}).populate('owner', '_id name') 119 | res.json(shops) 120 | } catch (err){ 121 | return res.status(400).json({ 122 | error: errorHandler.getErrorMessage(err) 123 | }) 124 | } 125 | } 126 | 127 | const isOwner = (req, res, next) => { 128 | const isOwner = req.shop && req.auth && req.shop.owner._id == req.auth._id 129 | if(!isOwner){ 130 | return res.status('403').json({ 131 | error: "User is not authorized" 132 | }) 133 | } 134 | next() 135 | } 136 | 137 | export default { 138 | create, 139 | shopByID, 140 | photo, 141 | defaultPhoto, 142 | list, 143 | listByOwner, 144 | read, 145 | update, 146 | isOwner, 147 | remove 148 | } 149 | -------------------------------------------------------------------------------- /client/auction/Bidding.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react' 2 | import Button from '@material-ui/core/Button' 3 | import TextField from '@material-ui/core/TextField' 4 | import Typography from '@material-ui/core/Typography' 5 | import auth from '../auth/auth-helper' 6 | import Grid from '@material-ui/core/Grid' 7 | import {makeStyles} from '@material-ui/core/styles' 8 | 9 | const io = require('socket.io-client') 10 | const socket = io() 11 | 12 | const useStyles = makeStyles(theme => ({ 13 | bidHistory: { 14 | marginTop: '20px', 15 | backgroundColor: '#f3f3f3', 16 | padding: '16px' 17 | }, 18 | placeForm: { 19 | margin: '0px 16px 16px', 20 | backgroundColor: '#e7ede4', 21 | display: 'inline-block' 22 | }, 23 | marginInput: { 24 | margin: 16 25 | }, 26 | marginBtn: { 27 | margin: '8px 16px 16px' 28 | } 29 | })) 30 | export default function Bidding (props) { 31 | const classes = useStyles() 32 | const [bid, setBid] = useState('') 33 | 34 | const jwt = auth.isAuthenticated() 35 | 36 | useEffect(() => { 37 | socket.emit('join auction room', {room: props.auction._id}) 38 | return () => { 39 | socket.emit('leave auction room', { 40 | room: props.auction._id 41 | }) 42 | } 43 | }, []) 44 | 45 | useEffect(() => { 46 | socket.on('new bid', payload => { 47 | props.updateBids(payload) 48 | }) 49 | return () => { 50 | socket.off('new bid') 51 | } 52 | }) 53 | const handleChange = event => { 54 | setBid(event.target.value) 55 | } 56 | const placeBid = () => { 57 | let newBid = { 58 | bid: bid, 59 | time: new Date(), 60 | bidder: jwt.user 61 | } 62 | socket.emit('new bid', { 63 | room: props.auction._id, 64 | bidInfo: newBid 65 | }) 66 | setBid('') 67 | } 68 | const minBid = props.auction.bids && props.auction.bids.length> 0 ? props.auction.bids[0].bid : props.auction.startingBid 69 | return( 70 |
71 | {!props.justEnded && new Date() < new Date(props.auction.bidEnd) &&
72 |
77 |
78 |
} 79 |
80 | All bids
81 | 82 | 83 | Bid Amount 84 | 85 | 86 | Bid Time 87 | 88 | 89 | Bidder 90 | 91 | 92 | {props.auction.bids.map((item, index) => { 93 | return 94 | ${item.bid} 95 | {new Date(item.time).toLocaleString()} 96 | {item.bidder.name} 97 | 98 | })} 99 | 100 |
101 |
102 | ) 103 | } -------------------------------------------------------------------------------- /client/order/ShopOrders.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react' 2 | import { makeStyles } from '@material-ui/core/styles' 3 | import Paper from '@material-ui/core/Paper' 4 | import List from '@material-ui/core/List' 5 | import ListItem from '@material-ui/core/ListItem' 6 | import ListItemText from '@material-ui/core/ListItemText' 7 | import Typography from '@material-ui/core/Typography' 8 | import ExpandLess from '@material-ui/icons/ExpandLess' 9 | import ExpandMore from '@material-ui/icons/ExpandMore' 10 | import Collapse from '@material-ui/core/Collapse' 11 | import Divider from '@material-ui/core/Divider' 12 | import auth from './../auth/auth-helper' 13 | import {listByShop} from './api-order.js' 14 | import ProductOrderEdit from './ProductOrderEdit' 15 | 16 | const useStyles = makeStyles(theme => ({ 17 | root: theme.mixins.gutters({ 18 | maxWidth: 600, 19 | margin: 'auto', 20 | padding: theme.spacing(3), 21 | marginTop: theme.spacing(5) 22 | }), 23 | title: { 24 | margin: `${theme.spacing(3)}px 0 ${theme.spacing(3)}px ${theme.spacing(1)}px` , 25 | color: theme.palette.protectedTitle, 26 | fontSize: '1.2em' 27 | }, 28 | subheading: { 29 | marginTop: theme.spacing(1), 30 | color: '#434b4e', 31 | fontSize: '1.1em' 32 | }, 33 | customerDetails: { 34 | paddingLeft: '36px', 35 | paddingTop: '16px', 36 | backgroundColor:'#f8f8f8' 37 | } 38 | })) 39 | export default function ShopOrders({match}) { 40 | const classes = useStyles() 41 | const [orders, setOrders] = useState([]) 42 | const [open, setOpen] = useState(0) 43 | 44 | 45 | const jwt = auth.isAuthenticated() 46 | useEffect(() => { 47 | const abortController = new AbortController() 48 | const signal = abortController.signal 49 | listByShop({ 50 | shopId: match.params.shopId 51 | }, {t: jwt.token}, signal).then((data) => { 52 | if (data.error) { 53 | console.log(data) 54 | } else { 55 | setOrders(data) 56 | } 57 | }) 58 | return function cleanup(){ 59 | abortController.abort() 60 | } 61 | }, []) 62 | 63 | const handleClick = index => event => { 64 | setOpen(index) 65 | } 66 | 67 | const updateOrders = (index, updatedOrder) => { 68 | let updatedOrders = orders 69 | updatedOrders[index] = updatedOrder 70 | setOrders([...updatedOrders]) 71 | } 72 | 73 | return ( 74 |
75 | 76 | 77 | Orders in {match.params.shop} 78 | 79 | 80 | {orders.map((order, index) => { 81 | return 82 | 83 | 84 | {open == index ? : } 85 | 86 | 87 | 88 |
89 | 90 | Deliver to: 91 | 92 | {order.customer_name} ({order.customer_email}) 93 | {order.delivery_address.street} 94 | {order.delivery_address.city}, {order.delivery_address.state} {order.delivery_address.zipcode} 95 | {order.delivery_address.country}
96 |
97 |
98 | 99 |
})} 100 |
101 |
102 |
) 103 | } 104 | -------------------------------------------------------------------------------- /client/cart/Checkout.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react' 2 | import Card from '@material-ui/core/Card' 3 | import {makeStyles} from '@material-ui/core/styles' 4 | import TextField from '@material-ui/core/TextField' 5 | import Typography from '@material-ui/core/Typography' 6 | import Icon from '@material-ui/core/Icon' 7 | import auth from './../auth/auth-helper' 8 | import cart from './cart-helper.js' 9 | import PlaceOrder from './PlaceOrder' 10 | import {Elements} from 'react-stripe-elements' 11 | 12 | const useStyles = makeStyles(theme => ({ 13 | card: { 14 | margin: '24px 0px', 15 | padding: '16px 40px 90px 40px', 16 | backgroundColor: '#80808017' 17 | }, 18 | title: { 19 | margin: '24px 16px 8px 0px', 20 | color: theme.palette.openTitle 21 | }, 22 | subheading: { 23 | color: 'rgba(88, 114, 128, 0.87)', 24 | marginTop: "20px", 25 | }, 26 | addressField: { 27 | marginTop: "4px", 28 | marginLeft: theme.spacing(1), 29 | marginRight: theme.spacing(1), 30 | width: "45%" 31 | }, 32 | streetField: { 33 | marginTop: "4px", 34 | marginLeft: theme.spacing(1), 35 | marginRight: theme.spacing(1), 36 | width: "93%" 37 | }, 38 | textField: { 39 | marginLeft: theme.spacing(1), 40 | marginRight: theme.spacing(1), 41 | width: "90%" 42 | } 43 | })) 44 | 45 | export default function Checkout (){ 46 | const classes = useStyles() 47 | const user = auth.isAuthenticated().user 48 | const [values, setValues] = useState({ 49 | checkoutDetails: { 50 | products: cart.getCart(), 51 | customer_name: user.name, 52 | customer_email:user.email, 53 | delivery_address: { street: '', city: '', state: '', zipcode: '', country:''} 54 | }, 55 | error: '' 56 | }) 57 | 58 | const handleCustomerChange = name => event => { 59 | let checkoutDetails = values.checkoutDetails 60 | checkoutDetails[name] = event.target.value || undefined 61 | setValues({...values, checkoutDetails: checkoutDetails}) 62 | } 63 | 64 | const handleAddressChange = name => event => { 65 | let checkoutDetails = values.checkoutDetails 66 | checkoutDetails.delivery_address[name] = event.target.value || undefined 67 | setValues({...values, checkoutDetails: checkoutDetails}) 68 | } 69 | 70 | return ( 71 | 72 | 73 | Checkout 74 | 75 |
76 |
77 | 78 | Delivery Address 79 | 80 |
81 | 82 |
83 | 84 | 85 |
{ 86 | values.error && ( 87 | error 88 | {values.error}) 89 | } 90 |
91 | 92 | 93 | 94 |
95 |
) 96 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MERN Marketplace 2.0 2 | - *Looking for the first edition code? [Check here](https://github.com/shamahoque/mern-marketplace/tree/master)* 3 | 4 | An online marketplace application with seller accounts, product search and suggestions, shopping cart, order management, payment processing with Stripe, and live auction with Socket.io - developed using React, Node, Express and MongoDB. 5 | 6 | 7 | 8 | ### [Live Demo](http://marketplace2.mernbook.com/ "MERN Marketplace") 9 | 10 | #### What you need to run this code 11 | 1. Node (13.12.0) 12 | 2. NPM (6.14.4) or Yarn (1.22.4) 13 | 3. MongoDB (4.2.0) 14 | 4. Stripe account with test data 15 | 16 | #### How to run this code 17 | 1. Make sure MongoDB is running on your system 18 | 2. Clone this repository 19 | 3. Update config/config.js with your test values for Stripe API keys and Stripe Connect Client ID 20 | 4. Open command line in the cloned folder, 21 | - To install dependencies, run ``` npm install ``` or ``` yarn ``` 22 | - To run the application for development, run ``` npm run development ``` or ``` yarn development ``` 23 | 5. Open [localhost:3000](http://localhost:3000/) in the browser 24 | ---- 25 | ### More applications built using this stack 26 | 27 | * [MERN Skeleton](https://github.com/shamahoque/mern-social/tree/second-edition) 28 | * [MERN Social](https://github.com/shamahoque/mern-social/tree/second-edition) 29 | * [MERN Classroom](https://github.com/shamahoque/mern-classroom) 30 | * [MERN Expense Tracker](https://github.com/shamahoque/mern-expense-tracker) 31 | * [MERN Mediastream](https://github.com/shamahoque/mern-mediastream/tree/second-edition) 32 | * [MERN VR Game](https://github.com/shamahoque/mern-vrgame/tree/second-edition) 33 | 34 | Learn more at [mernbook.com](http://www.mernbook.com/) 35 | 36 | ---- 37 | ## Get the book 38 | #### [Full-Stack React Projects - Second Edition](https://www.packtpub.com/web-development/full-stack-react-projects-second-edition) 39 | *Learn MERN stack development by building modern web apps using MongoDB, Express, React, and Node.js* 40 | 41 | Full-Stack React Projects 42 | 43 | React combined with industry-tested, server-side technologies, such as Node, Express, and MongoDB, enables you to develop and deploy robust real-world full-stack web apps. This updated second edition focuses on the latest versions and conventions of the technologies in this stack, along with their new features such as Hooks in React and async/await in JavaScript. The book also explores advanced topics such as implementing real-time bidding, a web-based classroom app, and data visualization in an expense tracking app. 44 | 45 | Full-Stack React Projects will take you through the process of preparing the development environment for MERN stack-based web development, creating a basic skeleton app, and extending it to build six different web apps. You'll build apps for social media, classrooms, media streaming, online marketplaces with real-time bidding, and web-based games with virtual reality features. Throughout the book, you'll learn how MERN stack web development works, extend its capabilities for complex features, and gain actionable insights into creating MERN-based apps, along with exploring industry best practices to meet the ever-increasing demands of the real world. 46 | 47 | Things you'll learn in this book: 48 | 49 | - Extend a MERN-based application to build a variety of applications 50 | - Add real-time communication capabilities with Socket.IO 51 | - Implement data visualization features for React applications using Victory 52 | - Develop media streaming applications using MongoDB GridFS 53 | - Improve SEO for your MERN apps by implementing server-side rendering with data 54 | - Implement user authentication and authorization using JSON web tokens 55 | - Set up and use React 360 to develop user interfaces with VR capabilities 56 | - Make your MERN stack applications reliable and scalable with industry best practices 57 | 58 | If you feel this book is for you, get your [copy](https://www.amazon.com/dp/1839215410) today! 59 | 60 | --- 61 | -------------------------------------------------------------------------------- /client/shop/MyShops.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react' 2 | import { makeStyles } from '@material-ui/core/styles' 3 | import Paper from '@material-ui/core/Paper' 4 | import List from '@material-ui/core/List' 5 | import ListItem from '@material-ui/core/ListItem' 6 | import ListItemAvatar from '@material-ui/core/ListItemAvatar' 7 | import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction' 8 | import ListItemText from '@material-ui/core/ListItemText' 9 | import Avatar from '@material-ui/core/Avatar' 10 | import IconButton from '@material-ui/core/IconButton' 11 | import Icon from '@material-ui/core/Icon' 12 | import Button from '@material-ui/core/Button' 13 | import Typography from '@material-ui/core/Typography' 14 | import Edit from '@material-ui/icons/Edit' 15 | import Divider from '@material-ui/core/Divider' 16 | import auth from './../auth/auth-helper' 17 | import {listByOwner} from './api-shop.js' 18 | import {Redirect, Link} from 'react-router-dom' 19 | import DeleteShop from './DeleteShop' 20 | 21 | const useStyles = makeStyles(theme => ({ 22 | root: theme.mixins.gutters({ 23 | maxWidth: 600, 24 | margin: 'auto', 25 | padding: theme.spacing(3), 26 | marginTop: theme.spacing(5) 27 | }), 28 | title: { 29 | margin: `${theme.spacing(3)}px 0 ${theme.spacing(3)}px ${theme.spacing(1)}px` , 30 | color: theme.palette.protectedTitle, 31 | fontSize: '1.2em' 32 | }, 33 | addButton:{ 34 | float:'right' 35 | }, 36 | leftIcon: { 37 | marginRight: "8px" 38 | } 39 | })) 40 | 41 | export default function MyShops(){ 42 | const classes = useStyles() 43 | const [shops, setShops] = useState([]) 44 | const [redirectToSignin, setRedirectToSignin] = useState(false) 45 | const jwt = auth.isAuthenticated() 46 | 47 | useEffect(() => { 48 | const abortController = new AbortController() 49 | const signal = abortController.signal 50 | listByOwner({ 51 | userId: jwt.user._id 52 | }, {t: jwt.token}, signal).then((data) => { 53 | if (data.error) { 54 | setRedirectToSignin(true) 55 | } else { 56 | setShops(data) 57 | } 58 | }) 59 | return function cleanup(){ 60 | abortController.abort() 61 | } 62 | }, []) 63 | 64 | const removeShop = (shop) => { 65 | const updatedShops = [...shops] 66 | const index = updatedShops.indexOf(shop) 67 | updatedShops.splice(index, 1) 68 | setShops(updatedShops) 69 | } 70 | 71 | if (redirectToSignin) { 72 | return 73 | } 74 | return ( 75 |
76 | 77 | 78 | Your Shops 79 | 80 | 81 | 84 | 85 | 86 | 87 | 88 | {shops.map((shop, i) => { 89 | return 90 | 91 | 92 | 93 | 94 | 95 | { auth.isAuthenticated().user && auth.isAuthenticated().user._id == shop.owner._id && 96 | ( 97 | 98 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | ) 109 | } 110 | 111 | 112 | })} 113 | 114 | 115 |
) 116 | } -------------------------------------------------------------------------------- /client/shop/NewShop.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | import Card from '@material-ui/core/Card' 3 | import CardActions from '@material-ui/core/CardActions' 4 | import CardContent from '@material-ui/core/CardContent' 5 | import Button from '@material-ui/core/Button' 6 | import FileUpload from '@material-ui/icons/AddPhotoAlternate' 7 | import auth from './../auth/auth-helper' 8 | import TextField from '@material-ui/core/TextField' 9 | import Typography from '@material-ui/core/Typography' 10 | import Icon from '@material-ui/core/Icon' 11 | import { makeStyles } from '@material-ui/core/styles' 12 | import {create} from './api-shop.js' 13 | import {Link, Redirect} from 'react-router-dom' 14 | 15 | const useStyles = makeStyles(theme => ({ 16 | card: { 17 | maxWidth: 600, 18 | margin: 'auto', 19 | textAlign: 'center', 20 | marginTop: theme.spacing(5), 21 | paddingBottom: theme.spacing(2) 22 | }, 23 | error: { 24 | verticalAlign: 'middle' 25 | }, 26 | title: { 27 | marginTop: theme.spacing(2), 28 | color: theme.palette.openTitle, 29 | fontSize: '1em' 30 | }, 31 | textField: { 32 | marginLeft: theme.spacing(1), 33 | marginRight: theme.spacing(1), 34 | width: 300 35 | }, 36 | submit: { 37 | margin: 'auto', 38 | marginBottom: theme.spacing(2) 39 | }, 40 | input: { 41 | display: 'none' 42 | }, 43 | filename:{ 44 | marginLeft:'10px' 45 | } 46 | })) 47 | 48 | export default function NewShop() { 49 | const classes = useStyles() 50 | const [values, setValues] = useState({ 51 | name: '', 52 | description: '', 53 | image: '', 54 | redirect: false, 55 | error: '' 56 | }) 57 | const jwt = auth.isAuthenticated() 58 | 59 | const handleChange = name => event => { 60 | const value = name === 'image' 61 | ? event.target.files[0] 62 | : event.target.value 63 | setValues({...values, [name]: value }) 64 | } 65 | const clickSubmit = () => { 66 | let shopData = new FormData() 67 | values.name && shopData.append('name', values.name) 68 | values.description && shopData.append('description', values.description) 69 | values.image && shopData.append('image', values.image) 70 | create({ 71 | userId: jwt.user._id 72 | }, { 73 | t: jwt.token 74 | }, shopData).then((data) => { 75 | if (data.error) { 76 | setValues({...values, error: data.error}) 77 | } else { 78 | setValues({...values, error: '', redirect: true}) 79 | } 80 | }) 81 | } 82 | 83 | if (values.redirect) { 84 | return () 85 | } 86 | return (
87 | 88 | 89 | 90 | New Shop 91 | 92 |
93 | 94 | {values.image ? values.image.name : ''}
100 |
101 |
{ 111 | values.error && ( 112 | error 113 | {values.error}) 114 | } 115 |
116 | 117 | 118 | 119 | 120 |
121 |
) 122 | } 123 | -------------------------------------------------------------------------------- /client/product/MyProducts.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react' 2 | import PropTypes from 'prop-types' 3 | import { makeStyles } from '@material-ui/core/styles' 4 | import Button from '@material-ui/core/Button' 5 | import Card from '@material-ui/core/Card' 6 | import CardMedia from '@material-ui/core/CardMedia' 7 | import IconButton from '@material-ui/core/IconButton' 8 | import Icon from '@material-ui/core/Icon' 9 | import Edit from '@material-ui/icons/Edit' 10 | import List from '@material-ui/core/List' 11 | import ListItem from '@material-ui/core/ListItem' 12 | import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction' 13 | import Typography from '@material-ui/core/Typography' 14 | import {Link} from 'react-router-dom' 15 | import Divider from '@material-ui/core/Divider' 16 | import {listByShop} from './../product/api-product.js' 17 | import DeleteProduct from './../product/DeleteProduct' 18 | 19 | const useStyles = makeStyles(theme => ({ 20 | products: { 21 | padding: '24px' 22 | }, 23 | addButton:{ 24 | float:'right' 25 | }, 26 | leftIcon: { 27 | marginRight: "8px" 28 | }, 29 | title: { 30 | margin: theme.spacing(2), 31 | color: theme.palette.protectedTitle, 32 | fontSize: '1.2em' 33 | }, 34 | subheading: { 35 | marginTop: theme.spacing(2), 36 | color: theme.palette.openTitle 37 | }, 38 | cover: { 39 | width: 110, 40 | height: 100, 41 | margin: '8px' 42 | }, 43 | details: { 44 | padding: '10px' 45 | }, 46 | })) 47 | 48 | export default function MyProducts (props){ 49 | const classes = useStyles() 50 | const [products, setProducts] = useState([]) 51 | 52 | useEffect(() => { 53 | const abortController = new AbortController() 54 | const signal = abortController.signal 55 | 56 | listByShop({ 57 | shopId: props.shopId 58 | }, signal).then((data)=>{ 59 | if (data.error) { 60 | console.log(data.error) 61 | } else { 62 | setProducts(data) 63 | } 64 | }) 65 | return function cleanup(){ 66 | abortController.abort() 67 | } 68 | }, []) 69 | 70 | const removeProduct = (product) => { 71 | const updatedProducts = [...products] 72 | const index = updatedProducts.indexOf(product) 73 | updatedProducts.splice(index, 1) 74 | setProducts(updatedProducts) 75 | } 76 | 77 | return ( 78 | 79 | 80 | Products 81 | 82 | 83 | 86 | 87 | 88 | 89 | 90 | {products.map((product, i) => { 91 | return 92 | 93 | 98 |
99 | 100 | {product.name} 101 | 102 | 103 | Quantity: {product.quantity} | Price: ${product.price} 104 | 105 |
106 | 107 | 108 | 109 | 110 | 111 | 112 | 116 | 117 |
118 |
})} 119 |
120 |
) 121 | } 122 | MyProducts.propTypes = { 123 | shopId: PropTypes.string.isRequired 124 | } 125 | 126 | -------------------------------------------------------------------------------- /client/product/Suggestions.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import {makeStyles} from '@material-ui/core/styles' 4 | import Paper from '@material-ui/core/Paper' 5 | import Typography from '@material-ui/core/Typography' 6 | import IconButton from '@material-ui/core/IconButton' 7 | import {Link} from 'react-router-dom' 8 | import ViewIcon from '@material-ui/icons/Visibility' 9 | import Icon from '@material-ui/core/Icon' 10 | import Divider from '@material-ui/core/Divider' 11 | import Card from '@material-ui/core/Card' 12 | import CardContent from '@material-ui/core/CardContent' 13 | import CardMedia from '@material-ui/core/CardMedia' 14 | import AddToCart from './../cart/AddToCart' 15 | 16 | const useStyles = makeStyles(theme => ({ 17 | root: theme.mixins.gutters({ 18 | padding: theme.spacing(1), 19 | paddingBottom: 24, 20 | backgroundColor: '#80808024' 21 | }), 22 | title: { 23 | margin: `${theme.spacing(4)}px 0 ${theme.spacing(2)}px`, 24 | color: theme.palette.openTitle, 25 | fontSize: '1.1em' 26 | }, 27 | viewButton: { 28 | verticalAlign: 'middle' 29 | }, 30 | card: { 31 | width: '100%', 32 | display: 'inline-flex' 33 | }, 34 | details: { 35 | display: 'inline-block', 36 | width: "100%" 37 | }, 38 | content: { 39 | flex: '1 0 auto', 40 | padding: '16px 8px 0px' 41 | }, 42 | cover: { 43 | width: '65%', 44 | height: 130, 45 | margin: '8px' 46 | }, 47 | controls: { 48 | marginTop: '8px' 49 | }, 50 | date: { 51 | color: 'rgba(0, 0, 0, 0.4)' 52 | }, 53 | icon: { 54 | verticalAlign: 'sub' 55 | }, 56 | iconButton: { 57 | width: '28px', 58 | height: '28px' 59 | }, 60 | productTitle: { 61 | fontSize: '1.15em', 62 | marginBottom: '5px' 63 | }, 64 | subheading: { 65 | color: 'rgba(88, 114, 128, 0.67)' 66 | }, 67 | actions: { 68 | float: 'right', 69 | marginRight: '6px' 70 | }, 71 | price: { 72 | display: 'inline', 73 | lineHeight: '3', 74 | paddingLeft: '8px', 75 | color: theme.palette.text.secondary 76 | } 77 | })) 78 | 79 | export default function Suggestions (props) { 80 | const classes = useStyles() 81 | return (
82 | 83 | 84 | {props.title} 85 | 86 | {props.products.map((item, i) => { 87 | return 88 | 89 | 94 |
95 | 96 | {item.name} 97 | 98 | 99 | shopping_basket {item.shop.name} 100 | 101 | 102 | 103 | Added on {(new Date(item.created)).toDateString()} 104 | 105 | 106 |
107 | $ {item.price} 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 |
117 |
118 |
119 | 120 |
121 | }) 122 | } 123 |
124 |
) 125 | } 126 | 127 | Suggestions.propTypes = { 128 | products: PropTypes.array.isRequired, 129 | title: PropTypes.string.isRequired 130 | } 131 | -------------------------------------------------------------------------------- /client/product/Product.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react' 2 | import Card from '@material-ui/core/Card' 3 | import CardHeader from '@material-ui/core/CardHeader' 4 | import CardMedia from '@material-ui/core/CardMedia' 5 | import Typography from '@material-ui/core/Typography' 6 | import Icon from '@material-ui/core/Icon' 7 | import Grid from '@material-ui/core/Grid' 8 | import {makeStyles} from '@material-ui/core/styles' 9 | import {read, listRelated} from './api-product.js' 10 | import {Link} from 'react-router-dom' 11 | import Suggestions from './../product/Suggestions' 12 | import AddToCart from './../cart/AddToCart' 13 | 14 | const useStyles = makeStyles(theme => ({ 15 | root: { 16 | flexGrow: 1, 17 | margin: 30, 18 | }, 19 | flex:{ 20 | display:'flex' 21 | }, 22 | card: { 23 | padding:'24px 40px 40px' 24 | }, 25 | subheading: { 26 | margin: '24px', 27 | color: theme.palette.openTitle 28 | }, 29 | price: { 30 | padding: '16px', 31 | margin: '16px 0px', 32 | display: 'flex', 33 | backgroundColor: '#93c5ae3d', 34 | fontSize: '1.3em', 35 | color: '#375a53', 36 | }, 37 | media: { 38 | height: 200, 39 | display: 'inline-block', 40 | width: '50%', 41 | marginLeft: '24px' 42 | }, 43 | icon: { 44 | verticalAlign: 'sub' 45 | }, 46 | link:{ 47 | color: '#3e4c54b3', 48 | fontSize: '0.9em' 49 | }, 50 | addCart: { 51 | width: '35px', 52 | height: '35px', 53 | padding: '10px 12px', 54 | borderRadius: '0.25em', 55 | backgroundColor: '#5f7c8b' 56 | }, 57 | action: { 58 | margin: '8px 24px', 59 | display: 'inline-block' 60 | } 61 | })) 62 | 63 | export default function Product ({match}) { 64 | const classes = useStyles() 65 | const [product, setProduct] = useState({shop:{}}) 66 | const [suggestions, setSuggestions] = useState([]) 67 | const [error, setError] = useState('') 68 | useEffect(() => { 69 | const abortController = new AbortController() 70 | const signal = abortController.signal 71 | 72 | read({productId: match.params.productId}, signal).then((data) => { 73 | if (data.error) { 74 | setError(data.error) 75 | } else { 76 | setProduct(data) 77 | } 78 | }) 79 | return function cleanup(){ 80 | abortController.abort() 81 | } 82 | }, [match.params.productId]) 83 | 84 | useEffect(() => { 85 | const abortController = new AbortController() 86 | const signal = abortController.signal 87 | 88 | listRelated({ 89 | productId: match.params.productId}, signal).then((data) => { 90 | if (data.error) { 91 | setError(data.error) 92 | } else { 93 | setSuggestions(data) 94 | } 95 | }) 96 | return function cleanup(){ 97 | abortController.abort() 98 | } 99 | }, [match.params.productId]) 100 | 101 | const imageUrl = product._id 102 | ? `/api/product/image/${product._id}?${new Date().getTime()}` 103 | : '/api/product/defaultphoto' 104 | return ( 105 |
106 | 107 | 108 | 109 | 0? 'In Stock': 'Out of Stock'} 112 | action={ 113 | 114 | 115 | 116 | } 117 | /> 118 |
119 | 124 | 125 | {product.description}
126 | $ {product.price} 127 | 128 | 129 | shopping_basket {product.shop.name} 130 | 131 | 132 |
133 | 134 |
135 |
136 |
137 | {suggestions.length > 0 && 138 | ( 139 | 140 | )} 141 |
142 |
) 143 | } 144 | -------------------------------------------------------------------------------- /server/controllers/auction.controller.js: -------------------------------------------------------------------------------- 1 | import Auction from '../models/auction.model' 2 | import extend from 'lodash/extend' 3 | import errorHandler from '../helpers/dbErrorHandler' 4 | import formidable from 'formidable' 5 | import fs from 'fs' 6 | import defaultImage from './../../client/assets/images/default.png' 7 | 8 | const create = (req, res) => { 9 | let form = new formidable.IncomingForm() 10 | form.keepExtensions = true 11 | form.parse(req, async (err, fields, files) => { 12 | if (err) { 13 | res.status(400).json({ 14 | message: "Image could not be uploaded" 15 | }) 16 | } 17 | let auction = new Auction(fields) 18 | auction.seller= req.profile 19 | if(files.image){ 20 | auction.image.data = fs.readFileSync(files.image.path) 21 | auction.image.contentType = files.image.type 22 | } 23 | try { 24 | let result = await auction.save() 25 | res.status(200).json(result) 26 | }catch (err){ 27 | return res.status(400).json({ 28 | error: errorHandler.getErrorMessage(err) 29 | }) 30 | } 31 | }) 32 | } 33 | 34 | const auctionByID = async (req, res, next, id) => { 35 | try { 36 | let auction = await Auction.findById(id).populate('seller', '_id name').populate('bids.bidder', '_id name').exec() 37 | if (!auction) 38 | return res.status('400').json({ 39 | error: "Auction not found" 40 | }) 41 | req.auction = auction 42 | next() 43 | } catch (err) { 44 | return res.status('400').json({ 45 | error: "Could not retrieve auction" 46 | }) 47 | } 48 | } 49 | 50 | const photo = (req, res, next) => { 51 | if(req.auction.image.data){ 52 | res.set("Content-Type", req.auction.image.contentType) 53 | return res.send(req.auction.image.data) 54 | } 55 | next() 56 | } 57 | const defaultPhoto = (req, res) => { 58 | return res.sendFile(process.cwd()+defaultImage) 59 | } 60 | 61 | const read = (req, res) => { 62 | req.auction.image = undefined 63 | return res.json(req.auction) 64 | } 65 | 66 | const update = (req, res) => { 67 | let form = new formidable.IncomingForm() 68 | form.keepExtensions = true 69 | form.parse(req, async (err, fields, files) => { 70 | if (err) { 71 | res.status(400).json({ 72 | message: "Photo could not be uploaded" 73 | }) 74 | } 75 | let auction = req.auction 76 | auction = extend(auction, fields) 77 | auction.updated = Date.now() 78 | if(files.image){ 79 | auction.image.data = fs.readFileSync(files.image.path) 80 | auction.image.contentType = files.image.type 81 | } 82 | try { 83 | let result = await auction.save() 84 | res.json(result) 85 | }catch (err){ 86 | return res.status(400).json({ 87 | error: errorHandler.getErrorMessage(err) 88 | }) 89 | } 90 | }) 91 | } 92 | 93 | const remove = async (req, res) => { 94 | try { 95 | let auction = req.auction 96 | let deletedAuction = auction.remove() 97 | res.json(deletedAuction) 98 | } catch (err) { 99 | return res.status(400).json({ 100 | error: errorHandler.getErrorMessage(err) 101 | }) 102 | } 103 | } 104 | 105 | const listOpen = async (req, res) => { 106 | try { 107 | let auctions = await Auction.find({ 'bidEnd': { $gt: new Date() }}).sort('bidStart').populate('seller', '_id name').populate('bids.bidder', '_id name') 108 | res.json(auctions) 109 | } catch (err){ 110 | return res.status(400).json({ 111 | error: errorHandler.getErrorMessage(err) 112 | }) 113 | } 114 | } 115 | 116 | 117 | const listBySeller = async (req, res) => { 118 | try { 119 | let auctions = await Auction.find({seller: req.profile._id}).populate('seller', '_id name').populate('bids.bidder', '_id name') 120 | res.json(auctions) 121 | } catch (err){ 122 | return res.status(400).json({ 123 | error: errorHandler.getErrorMessage(err) 124 | }) 125 | } 126 | } 127 | const listByBidder = async (req, res) => { 128 | try { 129 | let auctions = await Auction.find({'bids.bidder': req.profile._id}).populate('seller', '_id name').populate('bids.bidder', '_id name') 130 | res.json(auctions) 131 | } catch (err){ 132 | return res.status(400).json({ 133 | error: errorHandler.getErrorMessage(err) 134 | }) 135 | } 136 | } 137 | 138 | const isSeller = (req, res, next) => { 139 | const isSeller = req.auction && req.auth && req.auction.seller._id == req.auth._id 140 | if(!isSeller){ 141 | return res.status('403').json({ 142 | error: "User is not authorized" 143 | }) 144 | } 145 | next() 146 | } 147 | 148 | export default { 149 | create, 150 | auctionByID, 151 | photo, 152 | defaultPhoto, 153 | listOpen, 154 | listBySeller, 155 | listByBidder, 156 | read, 157 | update, 158 | isSeller, 159 | remove 160 | } 161 | -------------------------------------------------------------------------------- /client/auction/Auction.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react' 2 | import Card from '@material-ui/core/Card' 3 | import CardHeader from '@material-ui/core/CardHeader' 4 | import CardMedia from '@material-ui/core/CardMedia' 5 | import Typography from '@material-ui/core/Typography' 6 | import Grid from '@material-ui/core/Grid' 7 | import {makeStyles} from '@material-ui/core/styles' 8 | import {read} from './api-auction.js' 9 | import {Link} from 'react-router-dom' 10 | import auth from '../auth/auth-helper' 11 | import Timer from './Timer' 12 | import Bidding from './Bidding' 13 | 14 | const useStyles = makeStyles(theme => ({ 15 | root: { 16 | flexGrow: 1, 17 | margin: 60, 18 | }, 19 | flex:{ 20 | display:'flex' 21 | }, 22 | card: { 23 | padding:'24px 40px 40px' 24 | }, 25 | subheading: { 26 | margin: '16px', 27 | color: theme.palette.openTitle 28 | }, 29 | description: { 30 | margin: '16px', 31 | fontSize: '0.9em', 32 | color: '#4f4f4f' 33 | }, 34 | price: { 35 | padding: '16px', 36 | margin: '16px 0px', 37 | display: 'flex', 38 | backgroundColor: '#93c5ae3d', 39 | fontSize: '1.3em', 40 | color: '#375a53', 41 | }, 42 | media: { 43 | height: 300, 44 | display: 'inline-block', 45 | width: '100%', 46 | }, 47 | icon: { 48 | verticalAlign: 'sub' 49 | }, 50 | link:{ 51 | color: '#3e4c54b3', 52 | fontSize: '0.9em' 53 | }, 54 | itemInfo:{ 55 | width: '35%', 56 | margin: '16px' 57 | }, 58 | bidSection: { 59 | margin: '20px', 60 | minWidth: '50%' 61 | }, 62 | lastBid: { 63 | color: '#303030', 64 | margin: '16px', 65 | } 66 | })) 67 | 68 | export default function Auction ({match}) { 69 | const classes = useStyles() 70 | const [auction, setAuction] = useState({}) 71 | const [error, setError] = useState('') 72 | const [justEnded, setJustEnded] = useState(false) 73 | 74 | useEffect(() => { 75 | const abortController = new AbortController() 76 | const signal = abortController.signal 77 | 78 | read({auctionId: match.params.auctionId}, signal).then((data) => { 79 | if (data.error) { 80 | setError(data.error) 81 | } else { 82 | setAuction(data) 83 | } 84 | }) 85 | return function cleanup(){ 86 | abortController.abort() 87 | } 88 | }, [match.params.auctionId]) 89 | const updateBids = (updatedAuction) => { 90 | setAuction(updatedAuction) 91 | } 92 | const update = () => { 93 | setJustEnded(true) 94 | } 95 | const imageUrl = auction._id 96 | ? `/api/auctions/image/${auction._id}?${new Date().getTime()}` 97 | : '/api/auctions/defaultphoto' 98 | const currentDate = new Date() 99 | return ( 100 |
101 | 102 | 105 | {currentDate < new Date(auction.bidStart) && 'Auction Not Started'} 106 | {currentDate > new Date(auction.bidStart) && currentDate < new Date(auction.bidEnd) && 'Auction Live'} 107 | {currentDate > new Date(auction.bidEnd) && 'Auction Ended'} 108 | } 109 | /> 110 | 111 | 112 | 117 | 118 | About Item 119 | 120 | {auction.description} 121 | 122 | 123 | 124 | {currentDate > new Date(auction.bidStart) 125 | ? (<> 126 | 127 | { auction.bids.length > 0 && 128 | 129 | {` Last bid: $ ${auction.bids[0].bid}`} 130 | 131 | } 132 | { !auth.isAuthenticated() && Please, sign in to place your bid. } 133 | { auth.isAuthenticated() && } 134 | ) 135 | : {`Auction Starts at ${new Date(auction.bidStart).toLocaleString()}`}} 136 | 137 | 138 | 139 | 140 | 141 | 142 |
) 143 | } 144 | -------------------------------------------------------------------------------- /client/user/EditProfile.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react' 2 | import Card from '@material-ui/core/Card' 3 | import CardActions from '@material-ui/core/CardActions' 4 | import CardContent from '@material-ui/core/CardContent' 5 | import Button from '@material-ui/core/Button' 6 | import TextField from '@material-ui/core/TextField' 7 | import Typography from '@material-ui/core/Typography' 8 | import Icon from '@material-ui/core/Icon' 9 | import FormControlLabel from '@material-ui/core/FormControlLabel' 10 | import Switch from '@material-ui/core/Switch' 11 | import { makeStyles } from '@material-ui/core/styles' 12 | import auth from './../auth/auth-helper' 13 | import {read, update} from './api-user.js' 14 | import {Redirect} from 'react-router-dom' 15 | 16 | const useStyles = makeStyles(theme => ({ 17 | card: { 18 | maxWidth: 600, 19 | margin: 'auto', 20 | textAlign: 'center', 21 | marginTop: theme.spacing(5), 22 | paddingBottom: theme.spacing(2) 23 | }, 24 | title: { 25 | margin: theme.spacing(2), 26 | color: theme.palette.protectedTitle 27 | }, 28 | error: { 29 | verticalAlign: 'middle' 30 | }, 31 | textField: { 32 | marginLeft: theme.spacing(1), 33 | marginRight: theme.spacing(1), 34 | width: 300 35 | }, 36 | submit: { 37 | margin: 'auto', 38 | marginBottom: theme.spacing(2) 39 | }, 40 | subheading: { 41 | marginTop: theme.spacing(2), 42 | color: theme.palette.openTitle 43 | } 44 | })) 45 | 46 | export default function EditProfile({ match }) { 47 | const classes = useStyles() 48 | const [values, setValues] = useState({ 49 | name: '', 50 | email: '', 51 | password: '', 52 | seller: false, 53 | redirectToProfile: false, 54 | error: '' 55 | }) 56 | const jwt = auth.isAuthenticated() 57 | useEffect(() => { 58 | const abortController = new AbortController() 59 | const signal = abortController.signal 60 | 61 | read({ 62 | userId: match.params.userId 63 | }, {t: jwt.token}, signal).then((data) => { 64 | if (data && data.error) { 65 | setValues({...values, error: data.error}) 66 | } else { 67 | setValues({...values, name: data.name, email: data.email, seller: data.seller}) 68 | } 69 | }) 70 | return function cleanup(){ 71 | abortController.abort() 72 | } 73 | 74 | }, [match.params.userId]) 75 | 76 | const clickSubmit = () => { 77 | const user = { 78 | name: values.name || undefined, 79 | email: values.email || undefined, 80 | password: values.password || undefined, 81 | seller: values.seller || undefined 82 | } 83 | update({ 84 | userId: match.params.userId 85 | }, { 86 | t: jwt.token 87 | }, user).then((data) => { 88 | if (data && data.error) { 89 | setValues({...values, error: data.error}) 90 | } else { 91 | auth.updateUser(data, ()=>{ 92 | setValues({...values, userId: data._id, redirectToProfile: true}) 93 | }) 94 | } 95 | }) 96 | } 97 | const handleChange = name => event => { 98 | setValues({...values, [name]: event.target.value}) 99 | } 100 | const handleCheck = (event, checked) => { 101 | setValues({...values, 'seller': checked}) 102 | } 103 | 104 | if (values.redirectToProfile) { 105 | return () 106 | } 107 | return ( 108 | 109 | 110 | 111 | Edit Profile 112 | 113 |
114 |
115 | 116 | 117 | Seller Account 118 | 119 | } 128 | label={values.seller? 'Active' : 'Inactive'} 129 | /> 130 |
{ 131 | values.error && ( 132 | error 133 | {values.error} 134 | ) 135 | } 136 |
137 | 138 | 139 | 140 |
141 | ) 142 | } 143 | -------------------------------------------------------------------------------- /client/product/NewProduct.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | import Card from '@material-ui/core/Card' 3 | import CardActions from '@material-ui/core/CardActions' 4 | import CardContent from '@material-ui/core/CardContent' 5 | import Button from '@material-ui/core/Button' 6 | import TextField from '@material-ui/core/TextField' 7 | import FileUpload from '@material-ui/icons/AddPhotoAlternate' 8 | import auth from './../auth/auth-helper' 9 | import Typography from '@material-ui/core/Typography' 10 | import Icon from '@material-ui/core/Icon' 11 | import { makeStyles } from '@material-ui/core/styles' 12 | import {create} from './api-product.js' 13 | import {Link, Redirect} from 'react-router-dom' 14 | 15 | const useStyles = makeStyles(theme => ({ 16 | card: { 17 | maxWidth: 600, 18 | margin: 'auto', 19 | textAlign: 'center', 20 | marginTop: theme.spacing(5), 21 | paddingBottom: theme.spacing(2) 22 | }, 23 | error: { 24 | verticalAlign: 'middle' 25 | }, 26 | title: { 27 | marginTop: theme.spacing(2), 28 | color: theme.palette.openTitle, 29 | fontSize: '1.2em' 30 | }, 31 | textField: { 32 | marginLeft: theme.spacing(1), 33 | marginRight: theme.spacing(1), 34 | width: 300 35 | }, 36 | submit: { 37 | margin: 'auto', 38 | marginBottom: theme.spacing(2) 39 | }, 40 | input: { 41 | display: 'none' 42 | }, 43 | filename:{ 44 | marginLeft:'10px' 45 | } 46 | })) 47 | 48 | export default function NewProduct({match}) { 49 | const classes = useStyles() 50 | const [values, setValues] = useState({ 51 | name: '', 52 | description: '', 53 | image: '', 54 | category: '', 55 | quantity: '', 56 | price: '', 57 | redirect: false, 58 | error: '' 59 | }) 60 | const jwt = auth.isAuthenticated() 61 | const handleChange = name => event => { 62 | const value = name === 'image' 63 | ? event.target.files[0] 64 | : event.target.value 65 | setValues({...values, [name]: value }) 66 | } 67 | const clickSubmit = () => { 68 | let productData = new FormData() 69 | values.name && productData.append('name', values.name) 70 | values.description && productData.append('description', values.description) 71 | values.image && productData.append('image', values.image) 72 | values.category && productData.append('category', values.category) 73 | values.quantity && productData.append('quantity', values.quantity) 74 | values.price && productData.append('price', values.price) 75 | 76 | create({ 77 | shopId: match.params.shopId 78 | }, { 79 | t: jwt.token 80 | }, productData).then((data) => { 81 | if (data.error) { 82 | setValues({...values, error: data.error}) 83 | } else { 84 | setValues({...values, error: '', redirect: true}) 85 | } 86 | }) 87 | } 88 | 89 | if (values.redirect) { 90 | return () 91 | } 92 | return (
93 | 94 | 95 | 96 | New Product 97 |
98 | 99 | {values.image ? values.image.name : ''}
105 |
106 |
116 |
117 |
118 |
119 | { 120 | values.error && ( 121 | error 122 | {values.error}) 123 | } 124 |
125 | 126 | 127 | 128 | 129 |
130 |
) 131 | } 132 | -------------------------------------------------------------------------------- /server/controllers/user.controller.js: -------------------------------------------------------------------------------- 1 | import User from '../models/user.model' 2 | import extend from 'lodash/extend' 3 | import errorHandler from './../helpers/dbErrorHandler' 4 | import request from 'request' 5 | import config from './../../config/config' 6 | import stripe from 'stripe' 7 | 8 | const myStripe = stripe(config.stripe_test_secret_key) 9 | 10 | const create = async (req, res) => { 11 | const user = new User(req.body) 12 | try { 13 | await user.save() 14 | return res.status(200).json({ 15 | message: "Successfully signed up!" 16 | }) 17 | } catch (err) { 18 | return res.status(400).json({ 19 | error: errorHandler.getErrorMessage(err) 20 | }) 21 | } 22 | } 23 | 24 | /** 25 | * Load user and append to req. 26 | */ 27 | const userByID = async (req, res, next, id) => { 28 | try { 29 | let user = await User.findById(id) 30 | if (!user) 31 | return res.status('400').json({ 32 | error: "User not found" 33 | }) 34 | req.profile = user 35 | next() 36 | } catch (err) { 37 | return res.status('400').json({ 38 | error: "Could not retrieve user" 39 | }) 40 | } 41 | } 42 | 43 | const read = (req, res) => { 44 | req.profile.hashed_password = undefined 45 | req.profile.salt = undefined 46 | return res.json(req.profile) 47 | } 48 | 49 | const list = async (req, res) => { 50 | try { 51 | let users = await User.find().select('name email updated created') 52 | res.json(users) 53 | } catch (err) { 54 | return res.status(400).json({ 55 | error: errorHandler.getErrorMessage(err) 56 | }) 57 | } 58 | } 59 | 60 | const update = async (req, res) => { 61 | try { 62 | let user = req.profile 63 | user = extend(user, req.body) 64 | user.updated = Date.now() 65 | await user.save() 66 | user.hashed_password = undefined 67 | user.salt = undefined 68 | res.json(user) 69 | } catch (err) { 70 | return res.status(400).json({ 71 | error: errorHandler.getErrorMessage(err) 72 | }) 73 | } 74 | } 75 | 76 | const remove = async (req, res) => { 77 | try { 78 | let user = req.profile 79 | let deletedUser = await user.remove() 80 | deletedUser.hashed_password = undefined 81 | deletedUser.salt = undefined 82 | res.json(deletedUser) 83 | } catch (err) { 84 | return res.status(400).json({ 85 | error: errorHandler.getErrorMessage(err) 86 | }) 87 | } 88 | } 89 | 90 | const isSeller = (req, res, next) => { 91 | const isSeller = req.profile && req.profile.seller 92 | if (!isSeller) { 93 | return res.status('403').json({ 94 | error: "User is not a seller" 95 | }) 96 | } 97 | next() 98 | } 99 | 100 | const stripe_auth = (req, res, next) => { 101 | request({ 102 | url: "https://connect.stripe.com/oauth/token", 103 | method: "POST", 104 | json: true, 105 | body: {client_secret:config.stripe_test_secret_key,code:req.body.stripe, grant_type:'authorization_code'} 106 | }, (error, response, body) => { 107 | //update user 108 | if(body.error){ 109 | return res.status('400').json({ 110 | error: body.error_description 111 | }) 112 | } 113 | req.body.stripe_seller = body 114 | next() 115 | }) 116 | } 117 | 118 | const stripeCustomer = (req, res, next) => { 119 | if(req.profile.stripe_customer){ 120 | //update stripe customer 121 | myStripe.customers.update(req.profile.stripe_customer, { 122 | source: req.body.token 123 | }, (err, customer) => { 124 | if(err){ 125 | return res.status(400).send({ 126 | error: "Could not update charge details" 127 | }) 128 | } 129 | req.body.order.payment_id = customer.id 130 | next() 131 | }) 132 | }else{ 133 | myStripe.customers.create({ 134 | email: req.profile.email, 135 | source: req.body.token 136 | }).then((customer) => { 137 | User.update({'_id':req.profile._id}, 138 | {'$set': { 'stripe_customer': customer.id }}, 139 | (err, order) => { 140 | if (err) { 141 | return res.status(400).send({ 142 | error: errorHandler.getErrorMessage(err) 143 | }) 144 | } 145 | req.body.order.payment_id = customer.id 146 | next() 147 | }) 148 | }) 149 | } 150 | } 151 | 152 | const createCharge = (req, res, next) => { 153 | if(!req.profile.stripe_seller){ 154 | return res.status('400').json({ 155 | error: "Please connect your Stripe account" 156 | }) 157 | } 158 | myStripe.tokens.create({ 159 | customer: req.order.payment_id, 160 | }, { 161 | stripeAccount: req.profile.stripe_seller.stripe_user_id, 162 | }).then((token) => { 163 | myStripe.charges.create({ 164 | amount: req.body.amount * 100, //amount in cents 165 | currency: "usd", 166 | source: token.id, 167 | }, { 168 | stripeAccount: req.profile.stripe_seller.stripe_user_id, 169 | }).then((charge) => { 170 | next() 171 | }) 172 | }) 173 | } 174 | 175 | export default { 176 | create, 177 | userByID, 178 | read, 179 | list, 180 | remove, 181 | update, 182 | isSeller, 183 | stripe_auth, 184 | stripeCustomer, 185 | createCharge 186 | } 187 | -------------------------------------------------------------------------------- /client/user/Profile.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import { makeStyles } from '@material-ui/core/styles' 3 | import Paper from '@material-ui/core/Paper' 4 | import List from '@material-ui/core/List' 5 | import ListItem from '@material-ui/core/ListItem' 6 | import ListItemAvatar from '@material-ui/core/ListItemAvatar' 7 | import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction' 8 | import ListItemText from '@material-ui/core/ListItemText' 9 | import Avatar from '@material-ui/core/Avatar' 10 | import IconButton from '@material-ui/core/IconButton' 11 | import Button from '@material-ui/core/Button' 12 | import Typography from '@material-ui/core/Typography' 13 | import Edit from '@material-ui/icons/Edit' 14 | import Person from '@material-ui/icons/Person' 15 | import Divider from '@material-ui/core/Divider' 16 | import DeleteUser from './DeleteUser' 17 | import auth from './../auth/auth-helper' 18 | import {read} from './api-user.js' 19 | import {Redirect, Link} from 'react-router-dom' 20 | import config from './../../config/config' 21 | import stripeButton from './../assets/images/stripeButton.png' 22 | import MyOrders from './../order/MyOrders' 23 | import Auctions from './../auction/Auctions' 24 | import {listByBidder} from './../auction/api-auction.js' 25 | 26 | const useStyles = makeStyles(theme => ({ 27 | root: theme.mixins.gutters({ 28 | maxWidth: 600, 29 | margin: 'auto', 30 | padding: theme.spacing(3), 31 | marginTop: theme.spacing(5) 32 | }), 33 | title: { 34 | margin: `${theme.spacing(3)}px 0 ${theme.spacing(2)}px`, 35 | color: theme.palette.protectedTitle 36 | }, 37 | stripe_connect: { 38 | marginRight: '10px', 39 | }, 40 | stripe_connected: { 41 | verticalAlign: 'super', 42 | marginRight: '10px' 43 | }, 44 | auctions: { 45 | maxWidth: 600, 46 | margin: '24px', 47 | padding: theme.spacing(3), 48 | backgroundColor: '#3f3f3f0d' 49 | } 50 | })) 51 | 52 | export default function Profile({ match }) { 53 | const classes = useStyles() 54 | const [user, setUser] = useState({}) 55 | const [redirectToSignin, setRedirectToSignin] = useState(false) 56 | const jwt = auth.isAuthenticated() 57 | 58 | const [auctions, setAuctions] = useState([]) 59 | 60 | useEffect(() => { 61 | const abortController = new AbortController() 62 | const signal = abortController.signal 63 | listByBidder({ 64 | userId: match.params.userId 65 | }, {t: jwt.token}, signal).then((data) => { 66 | if (data.error) { 67 | setRedirectToSignin(true) 68 | } else { 69 | setAuctions(data) 70 | } 71 | }) 72 | return function cleanup(){ 73 | abortController.abort() 74 | } 75 | }, []) 76 | 77 | const removeAuction = (auction) => { 78 | const updatedAuctions = [...auctions] 79 | const index = updatedAuctions.indexOf(auction) 80 | updatedAuctions.splice(index, 1) 81 | setAuctions(updatedAuctions) 82 | } 83 | 84 | useEffect(() => { 85 | const abortController = new AbortController() 86 | const signal = abortController.signal 87 | read({ 88 | userId: match.params.userId 89 | }, {t: jwt.token}, signal).then((data) => { 90 | if (data && data.error) { 91 | setRedirectToSignin(true) 92 | } else { 93 | setUser(data) 94 | } 95 | }) 96 | 97 | return function cleanup(){ 98 | abortController.abort() 99 | } 100 | 101 | }, [match.params.userId]) 102 | 103 | if (redirectToSignin) { 104 | return 105 | } 106 | return ( 107 | 108 | 109 | Profile 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | { 119 | auth.isAuthenticated().user && auth.isAuthenticated().user._id == user._id && 120 | ( 121 | {user.seller && 122 | (user.stripe_seller 123 | ? () 126 | : ( 127 | 128 | ) 129 | ) 130 | } 131 | 132 | 133 | 134 | 135 | 136 | 137 | ) 138 | } 139 | 140 | 141 | 142 | 144 | 145 | 146 | 147 | 148 | 149 | Auctions you bid in 150 | 151 | 152 | 153 | 154 | ) 155 | } 156 | -------------------------------------------------------------------------------- /client/shop/EditShop.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react' 2 | import Card from '@material-ui/core/Card' 3 | import CardActions from '@material-ui/core/CardActions' 4 | import CardContent from '@material-ui/core/CardContent' 5 | import Button from '@material-ui/core/Button' 6 | import TextField from '@material-ui/core/TextField' 7 | import Typography from '@material-ui/core/Typography' 8 | import Icon from '@material-ui/core/Icon' 9 | import Avatar from '@material-ui/core/Avatar' 10 | import auth from './../auth/auth-helper' 11 | import FileUpload from '@material-ui/icons/AddPhotoAlternate' 12 | import { makeStyles } from '@material-ui/core/styles' 13 | import {read, update} from './api-shop.js' 14 | import {Redirect} from 'react-router-dom' 15 | import Grid from '@material-ui/core/Grid' 16 | import MyProducts from './../product/MyProducts' 17 | 18 | const useStyles = makeStyles(theme => ({ 19 | root: { 20 | flexGrow: 1, 21 | margin: 30, 22 | }, 23 | card: { 24 | textAlign: 'center', 25 | paddingBottom: theme.spacing(2) 26 | }, 27 | title: { 28 | margin: theme.spacing(2), 29 | color: theme.palette.protectedTitle, 30 | fontSize: '1.2em' 31 | }, 32 | subheading: { 33 | marginTop: theme.spacing(2), 34 | color: theme.palette.openTitle 35 | }, 36 | error: { 37 | verticalAlign: 'middle' 38 | }, 39 | textField: { 40 | marginLeft: theme.spacing(1), 41 | marginRight: theme.spacing(1), 42 | width: 400 43 | }, 44 | submit: { 45 | margin: 'auto', 46 | marginBottom: theme.spacing(2) 47 | }, 48 | bigAvatar: { 49 | width: 60, 50 | height: 60, 51 | margin: 'auto' 52 | }, 53 | input: { 54 | display: 'none' 55 | }, 56 | filename:{ 57 | marginLeft:'10px' 58 | } 59 | })) 60 | 61 | export default function EditShop ({match}) { 62 | const classes = useStyles() 63 | const [values, setValues] = useState({ 64 | name: '', 65 | description: '', 66 | image: '', 67 | redirect: false, 68 | error: '', 69 | id: '' 70 | }) 71 | const jwt = auth.isAuthenticated() 72 | useEffect(() => { 73 | const abortController = new AbortController() 74 | const signal = abortController.signal 75 | read({ 76 | shopId: match.params.shopId 77 | }, signal).then((data) => { 78 | if (data.error) { 79 | setValues({...values, error: data.error}) 80 | } else { 81 | setValues({...values, id: data._id, name: data.name, description: data.description, owner: data.owner.name}) 82 | } 83 | }) 84 | return function cleanup(){ 85 | abortController.abort() 86 | } 87 | }, []) 88 | 89 | const clickSubmit = () => { 90 | let shopData = new FormData() 91 | values.name && shopData.append('name', values.name) 92 | values.description && shopData.append('description', values.description) 93 | values.image && shopData.append('image', values.image) 94 | update({ 95 | shopId: match.params.shopId 96 | }, { 97 | t: jwt.token 98 | }, shopData).then((data) => { 99 | if (data.error) { 100 | setValues({...values, error: data.error}) 101 | } else { 102 | setValues({...values, 'redirect': true}) 103 | } 104 | }) 105 | } 106 | const handleChange = name => event => { 107 | const value = name === 'image' 108 | ? event.target.files[0] 109 | : event.target.value 110 | setValues({...values, [name]: value }) 111 | } 112 | 113 | const logoUrl = values.id 114 | ? `/api/shops/logo/${values.id}?${new Date().getTime()}` 115 | : '/api/shops/defaultphoto' 116 | if (values.redirect) { 117 | return () 118 | } 119 | return (
120 | 121 | 122 | 123 | 124 | 125 | Edit Shop 126 | 127 |
128 |
129 | 130 | {values.image ? values.image.name : ''}
136 |
137 |
147 | 148 | Owner: {values.owner} 149 |
150 | { 151 | values.error && ( 152 | error 153 | {values.error} 154 | ) 155 | } 156 |
157 | 158 | 159 | 160 |
161 |
162 | 163 | 164 | 165 |
166 |
) 167 | } 168 | -------------------------------------------------------------------------------- /server/controllers/product.controller.js: -------------------------------------------------------------------------------- 1 | import Product from '../models/product.model' 2 | import extend from 'lodash/extend' 3 | import errorHandler from './../helpers/dbErrorHandler' 4 | import formidable from 'formidable' 5 | import fs from 'fs' 6 | import defaultImage from './../../client/assets/images/default.png' 7 | 8 | const create = (req, res, next) => { 9 | let form = new formidable.IncomingForm() 10 | form.keepExtensions = true 11 | form.parse(req, async (err, fields, files) => { 12 | if (err) { 13 | return res.status(400).json({ 14 | message: "Image could not be uploaded" 15 | }) 16 | } 17 | let product = new Product(fields) 18 | product.shop= req.shop 19 | if(files.image){ 20 | product.image.data = fs.readFileSync(files.image.path) 21 | product.image.contentType = files.image.type 22 | } 23 | try { 24 | let result = await product.save() 25 | res.json(result) 26 | } catch (err){ 27 | return res.status(400).json({ 28 | error: errorHandler.getErrorMessage(err) 29 | }) 30 | } 31 | }) 32 | } 33 | 34 | const productByID = async (req, res, next, id) => { 35 | try { 36 | let product = await Product.findById(id).populate('shop', '_id name').exec() 37 | if (!product) 38 | return res.status('400').json({ 39 | error: "Product not found" 40 | }) 41 | req.product = product 42 | next() 43 | } catch (err) { 44 | return res.status('400').json({ 45 | error: "Could not retrieve product" 46 | }) 47 | } 48 | } 49 | 50 | const photo = (req, res, next) => { 51 | if(req.product.image.data){ 52 | res.set("Content-Type", req.product.image.contentType) 53 | return res.send(req.product.image.data) 54 | } 55 | next() 56 | } 57 | const defaultPhoto = (req, res) => { 58 | return res.sendFile(process.cwd()+defaultImage) 59 | } 60 | 61 | const read = (req, res) => { 62 | req.product.image = undefined 63 | return res.json(req.product) 64 | } 65 | 66 | const update = (req, res) => { 67 | let form = new formidable.IncomingForm() 68 | form.keepExtensions = true 69 | form.parse(req, async (err, fields, files) => { 70 | if (err) { 71 | return res.status(400).json({ 72 | message: "Photo could not be uploaded" 73 | }) 74 | } 75 | let product = req.product 76 | product = extend(product, fields) 77 | product.updated = Date.now() 78 | if(files.image){ 79 | product.image.data = fs.readFileSync(files.image.path) 80 | product.image.contentType = files.image.type 81 | } 82 | try { 83 | let result = await product.save() 84 | res.json(result) 85 | }catch (err){ 86 | return res.status(400).json({ 87 | error: errorHandler.getErrorMessage(err) 88 | }) 89 | } 90 | }) 91 | } 92 | 93 | const remove = async (req, res) => { 94 | try{ 95 | let product = req.product 96 | let deletedProduct = await product.remove() 97 | res.json(deletedProduct) 98 | 99 | } catch (err) { 100 | return res.status(400).json({ 101 | error: errorHandler.getErrorMessage(err) 102 | }) 103 | } 104 | } 105 | 106 | const listByShop = async (req, res) => { 107 | try { 108 | let products = await Product.find({shop: req.shop._id}).populate('shop', '_id name').select('-image') 109 | res.json(products) 110 | } catch (err) { 111 | return res.status(400).json({ 112 | error: errorHandler.getErrorMessage(err) 113 | }) 114 | } 115 | } 116 | 117 | const listLatest = async (req, res) => { 118 | try { 119 | let products = await Product.find({}).sort('-created').limit(5).populate('shop', '_id name').exec() 120 | res.json(products) 121 | } catch (err){ 122 | return res.status(400).json({ 123 | error: errorHandler.getErrorMessage(err) 124 | }) 125 | } 126 | } 127 | 128 | const listRelated = async (req, res) => { 129 | try{ 130 | let products = await Product.find({ "_id": { "$ne": req.product }, "category": req.product.category}).limit(5).populate('shop', '_id name').exec() 131 | res.json(products) 132 | } catch (err){ 133 | return res.status(400).json({ 134 | error: errorHandler.getErrorMessage(err) 135 | }) 136 | } 137 | } 138 | 139 | const listCategories = async (req, res) => { 140 | try { 141 | let products = await Product.distinct('category',{}) 142 | res.json(products) 143 | } catch (err){ 144 | return res.status(400).json({ 145 | error: errorHandler.getErrorMessage(err) 146 | }) 147 | } 148 | } 149 | 150 | const list = async (req, res) => { 151 | const query = {} 152 | if(req.query.search) 153 | query.name = {'$regex': req.query.search, '$options': "i"} 154 | if(req.query.category && req.query.category != 'All') 155 | query.category = req.query.category 156 | try { 157 | let products = await Product.find(query).populate('shop', '_id name').select('-image').exec() 158 | res.json(products) 159 | } catch (err){ 160 | return res.status(400).json({ 161 | error: errorHandler.getErrorMessage(err) 162 | }) 163 | } 164 | } 165 | 166 | const decreaseQuantity = async (req, res, next) => { 167 | let bulkOps = req.body.order.products.map((item) => { 168 | return { 169 | "updateOne": { 170 | "filter": { "_id": item.product._id } , 171 | "update": { "$inc": {"quantity": -item.quantity} } 172 | } 173 | } 174 | }) 175 | try { 176 | await Product.bulkWrite(bulkOps, {}) 177 | next() 178 | } catch (err){ 179 | return res.status(400).json({ 180 | error: "Could not update product" 181 | }) 182 | } 183 | } 184 | 185 | const increaseQuantity = async (req, res, next) => { 186 | try { 187 | await Product.findByIdAndUpdate(req.product._id, {$inc: {"quantity": req.body.quantity}}, {new: true}) 188 | .exec() 189 | next() 190 | } catch (err){ 191 | return res.status(400).json({ 192 | error: errorHandler.getErrorMessage(err) 193 | }) 194 | } 195 | } 196 | 197 | export default { 198 | create, 199 | productByID, 200 | photo, 201 | defaultPhoto, 202 | read, 203 | update, 204 | remove, 205 | listByShop, 206 | listLatest, 207 | listRelated, 208 | listCategories, 209 | list, 210 | decreaseQuantity, 211 | increaseQuantity 212 | } 213 | -------------------------------------------------------------------------------- /client/product/EditProduct.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react' 2 | import Card from '@material-ui/core/Card' 3 | import CardActions from '@material-ui/core/CardActions' 4 | import CardContent from '@material-ui/core/CardContent' 5 | import Button from '@material-ui/core/Button' 6 | import TextField from '@material-ui/core/TextField' 7 | import Typography from '@material-ui/core/Typography' 8 | import Icon from '@material-ui/core/Icon' 9 | import Avatar from '@material-ui/core/Avatar' 10 | import auth from './../auth/auth-helper' 11 | import FileUpload from '@material-ui/icons/AddPhotoAlternate' 12 | import { makeStyles } from '@material-ui/core/styles' 13 | import {withStyles} from '@material-ui/core/styles' 14 | import {read, update} from './api-product.js' 15 | import {Link, Redirect} from 'react-router-dom' 16 | 17 | const useStyles = makeStyles(theme => ({ 18 | card: { 19 | margin: 'auto', 20 | textAlign: 'center', 21 | marginTop: theme.spacing(3), 22 | marginBottom: theme.spacing(2), 23 | maxWidth: 500, 24 | paddingBottom: theme.spacing(2) 25 | }, 26 | title: { 27 | margin: theme.spacing(2), 28 | color: theme.palette.protectedTitle, 29 | fontSize: '1.2em' 30 | }, 31 | error: { 32 | verticalAlign: 'middle' 33 | }, 34 | textField: { 35 | marginLeft: theme.spacing(1), 36 | marginRight: theme.spacing(1), 37 | width: 400 38 | }, 39 | submit: { 40 | margin: 'auto', 41 | marginBottom: theme.spacing(2) 42 | }, 43 | bigAvatar: { 44 | width: 60, 45 | height: 60, 46 | margin: 'auto' 47 | }, 48 | input: { 49 | display: 'none' 50 | }, 51 | filename:{ 52 | marginLeft:'10px' 53 | } 54 | })) 55 | 56 | export default function EditProduct ({match}) { 57 | const classes = useStyles() 58 | const [values, setValues] = useState({ 59 | name: '', 60 | description: '', 61 | image: '', 62 | category: '', 63 | quantity: '', 64 | price: '', 65 | redirect: false, 66 | error: '' 67 | }) 68 | 69 | const jwt = auth.isAuthenticated() 70 | useEffect(() => { 71 | const abortController = new AbortController() 72 | const signal = abortController.signal 73 | read({ 74 | productId: match.params.productId 75 | }, signal).then((data) => { 76 | if (data.error) { 77 | setValues({...values, error: data.error}) 78 | } else { 79 | setValues({...values, id: data._id, name: data.name, description: data.description, category: data.category, quantity:data.quantity, price: data.price}) 80 | } 81 | }) 82 | return function cleanup(){ 83 | abortController.abort() 84 | } 85 | }, []) 86 | const clickSubmit = () => { 87 | let productData = new FormData() 88 | values.name && productData.append('name', values.name) 89 | values.description && productData.append('description', values.description) 90 | values.image && productData.append('image', values.image) 91 | values.category && productData.append('category', values.category) 92 | values.quantity && productData.append('quantity', values.quantity) 93 | values.price && productData.append('price', values.price) 94 | 95 | update({ 96 | shopId: match.params.shopId, 97 | productId: match.params.productId 98 | }, { 99 | t: jwt.token 100 | }, productData).then((data) => { 101 | if (data.error) { 102 | setValues({...values, error: data.error}) 103 | } else { 104 | setValues({...values, 'redirect': true}) 105 | } 106 | }) 107 | } 108 | const handleChange = name => event => { 109 | const value = name === 'image' 110 | ? event.target.files[0] 111 | : event.target.value 112 | setValues({...values, [name]: value }) 113 | } 114 | const imageUrl = values.id 115 | ? `/api/product/image/${values.id}?${new Date().getTime()}` 116 | : '/api/product/defaultphoto' 117 | if (values.redirect) { 118 | return () 119 | } 120 | return (
121 | 122 | 123 | 124 | Edit Product 125 |
126 |
127 | 128 | {values.image ? values.image.name : ''}
134 |
135 |
145 |
146 |
147 |
148 | { 149 | values.error && ( 150 | error 151 | {values.error}) 152 | } 153 |
154 | 155 | 156 | 157 | 158 |
159 |
) 160 | } 161 | -------------------------------------------------------------------------------- /client/cart/CartItems.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | import auth from './../auth/auth-helper' 3 | import Card from '@material-ui/core/Card' 4 | import CardContent from '@material-ui/core/CardContent' 5 | import CardMedia from '@material-ui/core/CardMedia' 6 | import Button from '@material-ui/core/Button' 7 | import TextField from '@material-ui/core/TextField' 8 | import Typography from '@material-ui/core/Typography' 9 | import Divider from '@material-ui/core/Divider' 10 | import PropTypes from 'prop-types' 11 | import {makeStyles} from '@material-ui/core/styles' 12 | import cart from './cart-helper.js' 13 | import {Link} from 'react-router-dom' 14 | 15 | const useStyles = makeStyles(theme => ({ 16 | card: { 17 | margin: '24px 0px', 18 | padding: '16px 40px 60px 40px', 19 | backgroundColor: '#80808017' 20 | }, 21 | title: { 22 | margin: theme.spacing(2), 23 | color: theme.palette.openTitle, 24 | fontSize: '1.2em' 25 | }, 26 | price: { 27 | color: theme.palette.text.secondary, 28 | display: 'inline' 29 | }, 30 | textField: { 31 | marginLeft: theme.spacing(1), 32 | marginRight: theme.spacing(1), 33 | marginTop: 0, 34 | width: 50 35 | }, 36 | productTitle: { 37 | fontSize: '1.15em', 38 | marginBottom: '5px' 39 | }, 40 | subheading: { 41 | color: 'rgba(88, 114, 128, 0.67)', 42 | padding: '8px 10px 0', 43 | cursor: 'pointer', 44 | display: 'inline-block' 45 | }, 46 | cart: { 47 | width: '100%', 48 | display: 'inline-flex' 49 | }, 50 | details: { 51 | display: 'inline-block', 52 | width: "100%", 53 | padding: "4px" 54 | }, 55 | content: { 56 | flex: '1 0 auto', 57 | padding: '16px 8px 0px' 58 | }, 59 | cover: { 60 | width: 160, 61 | height: 125, 62 | margin: '8px' 63 | }, 64 | itemTotal: { 65 | float: 'right', 66 | marginRight: '40px', 67 | fontSize: '1.5em', 68 | color: 'rgb(72, 175, 148)' 69 | }, 70 | checkout: { 71 | float: 'right', 72 | margin: '24px' 73 | }, 74 | total: { 75 | fontSize: '1.2em', 76 | color: 'rgb(53, 97, 85)', 77 | marginRight: '16px', 78 | fontWeight: '600', 79 | verticalAlign: 'bottom' 80 | }, 81 | continueBtn: { 82 | marginLeft: '10px' 83 | }, 84 | itemShop: { 85 | display: 'block', 86 | fontSize: '0.90em', 87 | color: '#78948f' 88 | }, 89 | removeButton: { 90 | fontSize: '0.8em' 91 | } 92 | })) 93 | 94 | export default function CartItems (props) { 95 | const classes = useStyles() 96 | const [cartItems, setCartItems] = useState(cart.getCart()) 97 | 98 | const handleChange = index => event => { 99 | let updatedCartItems = cartItems 100 | if(event.target.value == 0){ 101 | updatedCartItems[index].quantity = 1 102 | }else{ 103 | updatedCartItems[index].quantity = event.target.value 104 | } 105 | setCartItems([...updatedCartItems]) 106 | cart.updateCart(index, event.target.value) 107 | } 108 | 109 | const getTotal = () => { 110 | return cartItems.reduce((a, b) => { 111 | return a + (b.quantity*b.product.price) 112 | }, 0) 113 | } 114 | 115 | const removeItem = index => event =>{ 116 | let updatedCartItems = cart.removeItem(index) 117 | if(updatedCartItems.length == 0){ 118 | props.setCheckout(false) 119 | } 120 | setCartItems(updatedCartItems) 121 | } 122 | 123 | const openCheckout = () => { 124 | props.setCheckout(true) 125 | } 126 | 127 | return ( 128 | 129 | Shopping Cart 130 | 131 | {cartItems.length>0 ? ( 132 | {cartItems.map((item, i) => { 133 | return 134 | 139 |
140 | 141 | {item.product.name} 142 |
143 | $ {item.product.price} 144 | ${item.product.price * item.quantity} 145 | Shop: {item.product.shop.name} 146 |
147 |
148 |
149 | Quantity: 161 | 162 |
163 |
164 |
165 | 166 |
}) 167 | } 168 |
169 | Total: ${getTotal()} 170 | {!props.checkout && (auth.isAuthenticated()? 171 | 172 | : 173 | 174 | 175 | )} 176 | 177 | 178 | 179 |
180 |
) : 181 | No items added to your cart. 182 | } 183 |
) 184 | } 185 | 186 | CartItems.propTypes = { 187 | checkout: PropTypes.bool.isRequired, 188 | setCheckout: PropTypes.func.isRequired 189 | } 190 | --------------------------------------------------------------------------------