)
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 | Submit
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 | Place Order
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 |
111 |
112 |
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 | Submit
89 |
90 |
91 |
92 | New Account
93 |
94 |
95 | New account successfully created.
96 |
97 |
98 |
99 |
100 |
101 | Sign In
102 |
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 | Place Bid
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 |
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 |
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 |
82 | add_box New Shop
83 |
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 |
99 | View Orders
100 |
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 |
95 |
96 | Upload Logo
97 |
98 |
99 | {values.image ? values.image.name : ''}
100 |
101 | {
111 | values.error && (
112 | error
113 | {values.error} )
114 | }
115 |
116 |
117 | Submit
118 | Cancel
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 |
84 | add_box New Product
85 |
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 | Submit
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 |
100 |
101 | Upload Photo
102 |
103 |
104 | {values.image ? values.image.name : ''}
105 |
106 |
116 |
117 |
118 |
119 | {
120 | values.error && (
121 | error
122 | {values.error} )
123 | }
124 |
125 |
126 | Submit
127 | Cancel
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 | ? (
124 | Stripe connected
125 | )
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 ()
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 ()
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 | x Remove
162 |
163 |
164 |
165 |
166 | })
167 | }
168 |
169 | Total: ${getTotal()}
170 | {!props.checkout && (auth.isAuthenticated()?
171 | Checkout
172 | :
173 |
174 | Sign in to checkout
175 | )}
176 |
177 | Continue Shopping
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 |
--------------------------------------------------------------------------------