├── .nvmrc
├── mocharc.json
├── frontend
├── public
│ ├── robots.txt
│ ├── favicon.ico
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── apple-touch-icon.png
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── about.txt
│ ├── manifest.json
│ └── index.html
├── src
│ ├── constants
│ │ ├── deliveryChargeConstants.js
│ │ ├── categoryConstants.js
│ │ ├── cartConstants.js
│ │ ├── carouselConstants.js
│ │ ├── orderConstants.js
│ │ ├── productConstants.js
│ │ └── userConstants.js
│ ├── utils
│ │ ├── capitalizeFirstLetter.js
│ │ ├── currencyFormatter.js
│ │ └── calculateDiscount.js
│ ├── setupTests.js
│ ├── App.test.js
│ ├── screens
│ │ ├── CategoryListScreen.js
│ │ ├── Error404Screen.js
│ │ ├── CategoryScreen.js
│ │ ├── SearchScreen.js
│ │ ├── HomeScreen.js
│ │ ├── ForgotPasswordScreen.js
│ │ ├── PaymentScreen.js
│ │ ├── CreateCarouselScreen.js
│ │ ├── ResetPasswordScreen.js
│ │ ├── LoginScreen.js
│ │ ├── CarouselListScreen.js
│ │ ├── OrderListScreen.js
│ │ ├── UsersListScreen.js
│ │ ├── CarouselEditScreen.js
│ │ ├── RegisterScreen.js
│ │ ├── UserEditScreen.js
│ │ ├── ProductListScreen.js
│ │ ├── ShippingScreen.js
│ │ ├── ProductScreen.js
│ │ ├── CartScreen.js
│ │ └── ProfileScreen.js
│ ├── components
│ │ ├── FormContainer.js
│ │ ├── Loader.js
│ │ ├── Message.js
│ │ ├── Category.js
│ │ ├── SearchBox.js
│ │ ├── Slider.js
│ │ ├── Footer.js
│ │ ├── Product.js
│ │ ├── CheckoutSteps.js
│ │ └── Header.js
│ ├── reportWebVitals.js
│ ├── pages
│ │ ├── Privacy.js
│ │ ├── Terms.js
│ │ ├── Returns.js
│ │ └── Help.js
│ ├── reducers
│ │ ├── categoryReducers.js
│ │ ├── cartReducers.js
│ │ ├── carouselReducers.js
│ │ ├── productReducers.js
│ │ ├── orderReducers.js
│ │ └── userReducers.js
│ ├── actions
│ │ ├── categoryActions.js
│ │ ├── cartActions.js
│ │ ├── carouselActions.js
│ │ ├── productActions.js
│ │ └── orderActions.js
│ ├── index.js
│ ├── store.js
│ ├── App.js
│ └── index.css
├── package.json
└── README.md
├── .prettierrc.json
├── backend
├── utils
│ ├── generateToken.js
│ └── email.js
├── data
│ ├── carousels.js
│ ├── users.js
│ ├── categories.js
│ └── products.js
├── middleware
│ ├── errorMiddleware.js
│ └── authMiddleware.js
├── routes
│ ├── carouselRoutes.js
│ ├── productRoutes.js
│ ├── orderRoutes.js
│ ├── userRoutes.js
│ ├── uploadRoutes.js
│ └── categoryRoutes.js
├── models
│ ├── carouselModel.js
│ ├── categoryModel.js
│ ├── userModel.js
│ ├── productModel.js
│ └── orderModel.js
├── config
│ └── db.js
├── controllers
│ ├── carouselControllers.js
│ ├── productController.js
│ └── orderControllers.js
├── seeder.js
└── server.js
├── .gitignore
├── .github
└── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
├── LICENSE
├── test
├── orderControllers.test.js
├── productController.test.js
└── userController.test.js
├── package.json
├── CONTRIBUTING.md
├── CODE_OF_CONDUCT.md
└── README.md
/.nvmrc:
--------------------------------------------------------------------------------
1 | v14
--------------------------------------------------------------------------------
/mocharc.json:
--------------------------------------------------------------------------------
1 | {
2 | "timeout": 10000,
3 | "exit": true
4 | }
--------------------------------------------------------------------------------
/frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/roopeshsn/freshcomm/HEAD/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/roopeshsn/freshcomm/HEAD/frontend/public/favicon-16x16.png
--------------------------------------------------------------------------------
/frontend/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/roopeshsn/freshcomm/HEAD/frontend/public/favicon-32x32.png
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "all",
3 | "tabWidth": 2,
4 | "semi": false,
5 | "singleQuote": true
6 | }
7 |
--------------------------------------------------------------------------------
/frontend/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/roopeshsn/freshcomm/HEAD/frontend/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/frontend/src/constants/deliveryChargeConstants.js:
--------------------------------------------------------------------------------
1 | export const freeDeliveryCutoff = 400
2 | export const deliveryCharge = 40
3 |
--------------------------------------------------------------------------------
/frontend/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/roopeshsn/freshcomm/HEAD/frontend/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/frontend/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/roopeshsn/freshcomm/HEAD/frontend/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/frontend/src/utils/capitalizeFirstLetter.js:
--------------------------------------------------------------------------------
1 | export default function capitalizeFirstLetter(s) {
2 | return (s && s[0].toUpperCase() + s.slice(1)) || s
3 | }
4 |
--------------------------------------------------------------------------------
/backend/utils/generateToken.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken')
2 |
3 | const generateToken = (id) => {
4 | return jwt.sign({ id }, process.env.JWT_SECRET, { expiresIn: '30d' })
5 | }
6 |
7 | module.exports = generateToken
8 |
--------------------------------------------------------------------------------
/frontend/src/constants/categoryConstants.js:
--------------------------------------------------------------------------------
1 | export const CATEGORY_LIST_REQUEST = 'CATEGORY_LIST_REQUEST'
2 | export const CATEGORY_LIST_SUCCESS = 'CATEGORY_LIST_SUCCESS'
3 | export const CATEGORY_LIST_FAIL = 'CATEGORY_LIST_FAIL'
4 |
--------------------------------------------------------------------------------
/frontend/src/utils/currencyFormatter.js:
--------------------------------------------------------------------------------
1 | export default function formatter(value) {
2 | const options = {
3 | style: 'currency',
4 | currency: 'INR',
5 | minimumFractionDigits: 2,
6 | }
7 | return new Intl.NumberFormat('en-IN', options).format(value)
8 | }
9 |
--------------------------------------------------------------------------------
/frontend/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom'
6 |
--------------------------------------------------------------------------------
/frontend/src/constants/cartConstants.js:
--------------------------------------------------------------------------------
1 | export const CART_ADD_ITEM = 'CART_ADD_ITEM'
2 | export const CART_REMOVE_ITEM = 'CART_REMOVE_ITEM'
3 | export const CART_SAVE_SHIPPING_ADDRESS = 'CART_SAVE_SHIPPING_ADDRESS'
4 | export const CART_SAVE_PAYMENT_METHOD = 'CART_SAVE_PAYMENT_METHOD'
5 |
--------------------------------------------------------------------------------
/frontend/src/App.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react'
2 | import App from './App'
3 |
4 | test('renders learn react link', () => {
5 | render( )
6 | const linkElement = screen.getByText(/learn react/i)
7 | expect(linkElement).toBeInTheDocument()
8 | })
9 |
--------------------------------------------------------------------------------
/frontend/src/utils/calculateDiscount.js:
--------------------------------------------------------------------------------
1 | function calculateDiscount(originalPrice, sellingPrice) {
2 | let discountPrice = originalPrice - sellingPrice
3 | let discountPercentage = Math.floor((discountPrice / originalPrice) * 100)
4 | return [discountPrice, discountPercentage]
5 | }
6 |
7 | export default calculateDiscount
8 |
--------------------------------------------------------------------------------
/frontend/src/screens/CategoryListScreen.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Col, Row } from 'react-bootstrap'
3 |
4 | export const CategoryListScreen = () => {
5 | return (
6 | <>
7 |
8 |
9 | Category
10 |
11 |
12 |
13 | Page is under construction!
14 |
15 | >
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/backend/data/carousels.js:
--------------------------------------------------------------------------------
1 | const carousels = [
2 | {
3 | imageSrc: 'https://i.ibb.co/VxN2j1t/slide-1.jpg',
4 | imageAlt: 'slide-1',
5 | },
6 | {
7 | imageSrc: 'https://i.ibb.co/J7rQchw/slide-2.jpg',
8 | imageAlt: 'slide-2',
9 | },
10 |
11 | {
12 | imageSrc: 'https://i.ibb.co/pJpN4Fx/slide-3.jpg',
13 | imageAlt: 'slide-3',
14 | },
15 | ]
16 |
17 | module.exports = carousels
18 |
--------------------------------------------------------------------------------
/frontend/public/about.txt:
--------------------------------------------------------------------------------
1 | This favicon was generated using the following graphics from Twitter Twemoji:
2 |
3 | - Graphics Title: 1f6cd.svg
4 | - Graphics Author: Copyright 2020 Twitter, Inc and other contributors (https://github.com/twitter/twemoji)
5 | - Graphics Source: https://github.com/twitter/twemoji/blob/master/assets/svg/1f6cd.svg
6 | - Graphics License: CC-BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
7 |
--------------------------------------------------------------------------------
/backend/data/users.js:
--------------------------------------------------------------------------------
1 | const bcrypt = require('bcryptjs')
2 |
3 | const users = [
4 | {
5 | name: 'Roopesh',
6 | email: 'roopeshsaravanan.dev@gmail.com',
7 | password: bcrypt.hashSync('123456', 10),
8 | isAdmin: true,
9 | },
10 | {
11 | name: 'Admin',
12 | email: 'admin@admin.com',
13 | password: bcrypt.hashSync('123456', 10),
14 | isAdmin: true,
15 | },
16 | ]
17 |
18 | module.exports = users
19 |
--------------------------------------------------------------------------------
/frontend/src/components/FormContainer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Container, Row, Col } from 'react-bootstrap'
3 |
4 | const FormContainer = ({ children }) => {
5 | return (
6 |
7 |
8 |
9 | {children}
10 |
11 |
12 |
13 | )
14 | }
15 |
16 | export default FormContainer
17 |
--------------------------------------------------------------------------------
/frontend/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = (onPerfEntry) => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry)
5 | getFID(onPerfEntry)
6 | getFCP(onPerfEntry)
7 | getLCP(onPerfEntry)
8 | getTTFB(onPerfEntry)
9 | })
10 | }
11 | }
12 |
13 | export default reportWebVitals
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | node_modules/
6 | /.pnp
7 | .pnp.js
8 |
9 | # testing
10 | /coverage
11 |
12 | # production
13 | /frontend/build
14 |
15 | # misc
16 | .DS_Store
17 | .env
18 | .env.local
19 | .env.development.local
20 | .env.test.local
21 | .env.production.local
22 |
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 |
--------------------------------------------------------------------------------
/frontend/src/components/Loader.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Spinner } from 'react-bootstrap'
3 |
4 | const Loader = () => {
5 | return (
6 |
13 | Loading...
14 |
15 | )
16 | }
17 |
18 | export default Loader
19 |
--------------------------------------------------------------------------------
/frontend/src/components/Message.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Alert } from 'react-bootstrap'
3 |
4 | const Message = ({ className, variant, children }) => {
5 | return (
6 |
11 | {children}
12 |
13 | )
14 | }
15 |
16 | Message.defaultProps = {
17 | variant: 'info',
18 | className: 'my-2',
19 | }
20 |
21 | export default Message
22 |
--------------------------------------------------------------------------------
/backend/middleware/errorMiddleware.js:
--------------------------------------------------------------------------------
1 | const notFound = (req, res, next) => {
2 | const error = new Error(`Not found - ${req.originalUrl}`)
3 | res.status(404)
4 | next(error)
5 | }
6 |
7 | const errorHandler = (err, req, res, next) => {
8 | const statusCode = res.statusCode === 200 ? 500 : res.statusCode
9 | res.status(statusCode)
10 | res.json({
11 | message: err.message,
12 | stack: process.env.NODE_ENV === 'production' ? null : err.stack,
13 | })
14 | }
15 |
16 | module.exports = { notFound, errorHandler }
17 |
--------------------------------------------------------------------------------
/backend/routes/carouselRoutes.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const {
3 | getCarousels,
4 | updateCarousel,
5 | getCarouselById,
6 | createCarousel,
7 | } = require('../controllers/carouselControllers')
8 | const { protect, admin } = require('../middleware/authMiddleware')
9 | const router = express.Router()
10 |
11 | router.route('/').get(getCarousels).post(protect, admin, createCarousel)
12 |
13 | router.route('/:id').get(getCarouselById).put(protect, admin, updateCarousel)
14 |
15 | module.exports = router
16 |
--------------------------------------------------------------------------------
/backend/models/carouselModel.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 |
3 | const carouselSchema = mongoose.Schema(
4 | {
5 | user: {
6 | type: mongoose.Schema.Types.ObjectId,
7 | required: true,
8 | ref: 'User',
9 | },
10 | imageSrc: {
11 | type: String,
12 | required: true,
13 | },
14 | imageAlt: {
15 | type: String,
16 | required: true,
17 | },
18 | },
19 | {
20 | timestamps: true,
21 | },
22 | )
23 |
24 | const Carousel = mongoose.model('Carousel', carouselSchema)
25 |
26 | module.exports = Carousel
27 |
--------------------------------------------------------------------------------
/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Large Green",
3 | "name": "Large Green - Daily Grocery App",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | { "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
11 | { "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
12 | ],
13 | "start_url": ".",
14 | "display": "standalone",
15 | "theme_color": "#000000",
16 | "background_color": "#ffffff"
17 | }
18 |
--------------------------------------------------------------------------------
/backend/routes/productRoutes.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const router = express.Router()
3 | const {
4 | getProducts,
5 | getProductById,
6 | deleteProduct,
7 | updateProduct,
8 | createProduct,
9 | } = require('../controllers/productController')
10 | const { protect, admin } = require('../middleware/authMiddleware')
11 |
12 | router.route('/').get(getProducts).post(protect, admin, createProduct)
13 | router
14 | .route('/:id')
15 | .get(getProductById)
16 | .delete(protect, admin, deleteProduct)
17 | .put(protect, admin, updateProduct)
18 |
19 | module.exports = router
20 |
--------------------------------------------------------------------------------
/frontend/src/pages/Privacy.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Privacy = () => {
4 | return (
5 |
6 |
Privacy Policy
7 |
8 | We value the trust you place in us and recognize the importance of
9 | secure transactions and information privacy. This Privacy Policy
10 | describes how Freshbey and its affiliates (collectively “Freshbey, we,
11 | our, us”) collect, use, share or otherwise process your personal
12 | information through Freshbey website.
13 |
14 |
15 | )
16 | }
17 |
18 | export default Privacy
19 |
--------------------------------------------------------------------------------
/frontend/src/components/Category.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Row, Card } from 'react-bootstrap'
3 | import { Link } from 'react-router-dom'
4 |
5 | const Category = ({ category }) => {
6 | return (
7 |
8 |
9 |
10 |
16 |
17 |
18 |
19 | )
20 | }
21 |
22 | export default Category
23 |
--------------------------------------------------------------------------------
/frontend/src/reducers/categoryReducers.js:
--------------------------------------------------------------------------------
1 | import {
2 | CATEGORY_LIST_REQUEST,
3 | CATEGORY_LIST_SUCCESS,
4 | CATEGORY_LIST_FAIL,
5 | } from '../constants/categoryConstants'
6 |
7 | export const categoryListReducer = (state = { categories: [] }, action) => {
8 | switch (action.type) {
9 | case CATEGORY_LIST_REQUEST:
10 | return { loading: true, categories: [] }
11 | case CATEGORY_LIST_SUCCESS:
12 | return { loading: false, categories: action.payload }
13 | case CATEGORY_LIST_FAIL:
14 | return { loading: false, error: action.payload }
15 | default:
16 | return state
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/backend/utils/email.js:
--------------------------------------------------------------------------------
1 | const nodemailer = require('nodemailer')
2 |
3 | const sendEmail = async (options) => {
4 | const transporter = nodemailer.createTransport({
5 | host: process.env.EMAIL_HOST,
6 | port: process.env.EMAIL_PORT,
7 | auth: {
8 | user: process.env.EMAIL_USERNAME,
9 | pass: process.env.EMAIL_PASSWORD,
10 | },
11 | })
12 |
13 | const mailOptions = {
14 | from: 'Roopesh Saravanan {
4 | return (
5 |
6 |
Terms
7 |
8 | This document is an electronic record in terms of Information Technology
9 | Act, 2000 and rules there under as applicable and the amended provisions
10 | pertaining to electronic records in various statutes as amended by the
11 | Information Technology Act, 2000. This electronic record is generated by
12 | a computer system and does not require any physical or digital
13 | signatures.
14 |
15 |
16 | )
17 | }
18 |
19 | export default Terms
20 |
--------------------------------------------------------------------------------
/frontend/src/pages/Returns.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Returns = () => {
4 | return (
5 |
6 |
Returns
7 |
8 | Returns is a scheme provided by respective sellers directly under this
9 | policy in terms of which the option of exchange, replacement and/ or
10 | refund is offered by the respective sellers to you. All products listed
11 | under a particular category may not have the same returns policy. For
12 | all products, the returns/replacement policy provided on the product
13 | page shall prevail over the general returns policy.
14 |
15 |
16 | )
17 | }
18 |
19 | export default Returns
20 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/frontend/src/actions/categoryActions.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | import {
4 | CATEGORY_LIST_REQUEST,
5 | CATEGORY_LIST_SUCCESS,
6 | CATEGORY_LIST_FAIL,
7 | } from '../constants/categoryConstants'
8 |
9 | export const listCategories = () => async (dispatch) => {
10 | try {
11 | dispatch({ type: CATEGORY_LIST_REQUEST })
12 | const { data } = await axios.get(`/api/categories`)
13 | dispatch({ type: CATEGORY_LIST_SUCCESS, payload: data })
14 | } catch (error) {
15 | dispatch({
16 | type: CATEGORY_LIST_FAIL,
17 | payload:
18 | error.response && error.response.data.message
19 | ? error.response.data.message
20 | : error.message,
21 | })
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/backend/routes/orderRoutes.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const router = express.Router()
3 | const {
4 | addOrderItems,
5 | getOrderById,
6 | updateOrderToPaid,
7 | getMyOrders,
8 | getOrders,
9 | updateOrderToDelivered,
10 | } = require('../controllers/orderControllers')
11 | const { protect, admin } = require('../middleware/authMiddleware.js')
12 |
13 | router.route('/').post(protect, addOrderItems).get(protect, admin, getOrders)
14 | router.route('/myorders').get(protect, getMyOrders)
15 | router.route('/:id').get(protect, getOrderById)
16 | router.route('/:id/pay').put(protect, updateOrderToPaid)
17 | router.route('/:id/deliver').put(protect, admin, updateOrderToDelivered)
18 |
19 | module.exports = router
20 |
--------------------------------------------------------------------------------
/backend/models/categoryModel.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 |
3 | const categorySchema = mongoose.Schema(
4 | {
5 | user: {
6 | type: mongoose.Schema.Types.ObjectId,
7 | required: true,
8 | ref: 'User',
9 | },
10 | name: {
11 | type: String,
12 | required: true,
13 | },
14 | imageSrc: {
15 | type: String,
16 | required: true,
17 | },
18 | imageAlt: {
19 | type: String,
20 | required: true,
21 | },
22 | href: {
23 | type: String,
24 | required: true,
25 | },
26 | },
27 | {
28 | timestamps: true,
29 | },
30 | )
31 |
32 | const Category = mongoose.model('Category', categorySchema)
33 |
34 | module.exports = Category
35 |
--------------------------------------------------------------------------------
/frontend/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import { Provider } from 'react-redux'
4 | import store from './store'
5 | import './bootstrap.min.css'
6 | import './index.css'
7 | import App from './App'
8 | import reportWebVitals from './reportWebVitals'
9 |
10 | import axios from 'axios'
11 | axios.defaults.withCredentials = true
12 | ReactDOM.render(
13 |
14 |
15 | ,
16 | document.getElementById('root'),
17 | )
18 |
19 | // If you want to start measuring performance in your app, pass a function
20 | // to log results (for example: reportWebVitals(console.log))
21 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
22 | reportWebVitals()
23 |
--------------------------------------------------------------------------------
/backend/config/db.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const cloudinary = require('cloudinary').v2
3 |
4 | const connectDB = async () => {
5 | try {
6 | const conn = await mongoose.connect(process.env.MONGODB_URI, {
7 | useNewUrlParser: true,
8 | useUnifiedTopology: true,
9 | })
10 | console.log(`MongoDB connected: ${conn.connection.host}`)
11 | } catch (error) {
12 | console.log(`Error: ${error.message}`)
13 | process.exit(1)
14 | }
15 | }
16 |
17 | //cloudinary config details which will be used further to upload images
18 | cloudinary.config({
19 | cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
20 | api_key: process.env.CLOUDINARY_API_KEY,
21 | api_secret: process.env.CLOUDINARY_API_SECRET,
22 | secure: true,
23 | })
24 |
25 | module.exports = {
26 | connectDB,
27 | cloudinary,
28 | }
29 |
--------------------------------------------------------------------------------
/frontend/src/screens/Error404Screen.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Row, Col, Button } from 'react-bootstrap'
3 | import { LinkContainer } from 'react-router-bootstrap'
4 |
5 | function Error404Screen() {
6 | return (
7 | <>
8 |
9 |
10 |
11 |
12 | 404
13 |
14 |
15 |
16 | Page not found
17 |
18 |
19 |
20 |
21 | Take me home
22 |
23 |
24 | >
25 | )
26 | }
27 |
28 | export default Error404Screen
29 |
--------------------------------------------------------------------------------
/backend/data/categories.js:
--------------------------------------------------------------------------------
1 | const categories = [
2 | {
3 | name: 'vegetables',
4 | imageSrc: 'https://i.ibb.co/8D0nnzc/fresh-vegetables.jpg',
5 | imageAlt: 'vegetables image',
6 | href: '#',
7 | },
8 | {
9 | name: 'fruits',
10 | imageSrc: 'https://i.ibb.co/zmRpGsD/fresh-fruits.jpg',
11 | imageAlt: 'fruits image',
12 | href: '#',
13 | },
14 |
15 | {
16 | name: 'exotic',
17 | imageSrc: 'https://i.ibb.co/Jx1rPLJ/exotic-corner.jpg',
18 | imageAlt: 'exotic image',
19 | href: '#',
20 | },
21 | {
22 | name: 'seasonal',
23 | imageSrc: 'https://i.ibb.co/sbjKQdv/seasonal-veggies.jpg',
24 | imageAlt: 'seasonal image',
25 | href: '#',
26 | },
27 |
28 | {
29 | name: 'sprouts',
30 | imageSrc: 'https://i.ibb.co/7x3y5nT/cuts-and-sprouts.jpg',
31 | imageAlt: 'sprouts image',
32 | href: '#',
33 | },
34 | ]
35 |
36 | module.exports = categories
37 |
--------------------------------------------------------------------------------
/backend/routes/userRoutes.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const {
3 | authUser,
4 | getUserProfile,
5 | registerUser,
6 | updateUserProfile,
7 | forgotPassword,
8 | resetPassword,
9 | getUsers,
10 | deleteUser,
11 | getUserById,
12 | updateUser,
13 | logoutUser,
14 | } = require('../controllers/userController')
15 | const { protect, admin } = require('../middleware/authMiddleware')
16 | const router = express.Router()
17 |
18 | router.route('/').post(registerUser).get(protect, admin, getUsers)
19 | router.post('/login', authUser)
20 | router
21 | .route('/profile')
22 | .get(protect, getUserProfile)
23 | .put(protect, updateUserProfile)
24 | router.post('/forgotpassword', forgotPassword)
25 | router.patch('/resetpassword/:token', resetPassword)
26 | router.get('/logout', logoutUser)
27 | router
28 | .route('/:id')
29 | .delete(protect, admin, deleteUser)
30 | .get(protect, admin, getUserById)
31 | .put(protect, admin, updateUser)
32 |
33 | module.exports = router
34 |
--------------------------------------------------------------------------------
/backend/middleware/authMiddleware.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken')
2 | const asyncHandler = require('express-async-handler')
3 | const User = require('../models/userModel')
4 |
5 | const protect = asyncHandler(async (req, res, next) => {
6 | const { accessToken: token } = req.cookies
7 | if (token) {
8 | try {
9 | const decoded = jwt.verify(token, process.env.JWT_SECRET)
10 | req.user = await User.findById(decoded.id).select('-password')
11 | next()
12 | } catch (error) {
13 | console.error(error)
14 | res.status(401)
15 | throw new Error('Not authorized, token failed')
16 | }
17 | }
18 |
19 | if (!token) {
20 | res.status(401)
21 | throw new Error('Not authorized, no token')
22 | }
23 | })
24 |
25 | const admin = (req, res, next) => {
26 | if (req.user && req.user.isAdmin) {
27 | next()
28 | } else {
29 | res.status(401)
30 | throw new Error('Not authorized as an admin')
31 | }
32 | }
33 |
34 | module.exports = { protect, admin }
35 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/frontend/src/constants/carouselConstants.js:
--------------------------------------------------------------------------------
1 | export const CAROUSEL_LIST_REQUEST = 'CAROUSEL_LIST_REQUEST'
2 | export const CAROUSEL_LIST_SUCCESS = 'CAROUSEL_LIST_SUCCESS'
3 | export const CAROUSEL_LIST_FAIL = 'CAROUSEL_LIST_FAIL'
4 |
5 | export const CAROUSEL_DETAILS_REQUEST = 'CAROUSEL_DETAILS_REQUEST'
6 | export const CAROUSEL_DETAILS_SUCCESS = 'CAROUSEL_DETAILS_SUCCESS'
7 | export const CAROUSEL_DETAILS_FAIL = 'CAROUSEL_DETAILS_FAIL'
8 | export const CAROUSEL_DETAILS_RESET = 'CAROUSEL_DETAILS_RESET'
9 |
10 | export const CAROUSEL_CREATE_REQUEST = 'CAROUSEL_CREATE_REQUEST'
11 | export const CAROUSEL_CREATE_SUCCESS = 'CAROUSEL_CREATE_SUCCESS'
12 | export const CAROUSEL_CREATE_FAIL = 'CAROUSEL_CREATE_FAIL'
13 | export const CAROUSEL_CREATE_RESET = 'CAROUSEL_CREATE_RESET'
14 |
15 | export const CAROUSEL_UPDATE_REQUEST = 'CAROUSEL_UPDATE_REQUEST'
16 | export const CAROUSEL_UPDATE_SUCCESS = 'CAROUSEL_UPDATE_SUCCESS'
17 | export const CAROUSEL_UPDATE_FAIL = 'CAROUSEL_UPDATE_FAIL'
18 | export const CAROUSEL_UPDATE_RESET = 'CAROUSEL_UPDATE_RESET'
19 |
--------------------------------------------------------------------------------
/frontend/src/components/SearchBox.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { Form } from 'react-bootstrap'
3 | import FormContainer from './FormContainer'
4 |
5 | const SearchBox = ({ history }) => {
6 | const [keyword, setKeyword] = useState('')
7 |
8 | const submitHandler = (e) => {
9 | e.preventDefault()
10 | setKeyword(e.target.value)
11 | if (keyword.trim()) {
12 | history.push(`/search/${keyword}`)
13 | } else {
14 | history.push('/')
15 | }
16 | }
17 |
18 | return (
19 |
20 | submitHandler(e)}
25 | placeholder="Search Freshbey"
26 | className="me-2"
27 | >
28 |
29 | {/*
30 | Search
31 | */}
32 |
33 |
34 | )
35 | }
36 |
37 | export default SearchBox
38 |
--------------------------------------------------------------------------------
/backend/routes/uploadRoutes.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const express = require('express')
3 | const multer = require('multer')
4 | const { cloudinary } = require('../config/db')
5 | const { CloudinaryStorage } = require('multer-storage-cloudinary')
6 |
7 | const router = express.Router()
8 |
9 | const storage = new CloudinaryStorage({
10 | cloudinary,
11 | params: {
12 | folder: 'Products',
13 | },
14 | })
15 |
16 | function checkFileType(file, cb) {
17 | const filetypes = /jpg|jpeg|png/
18 | const extname = filetypes.test(path.extname(file.originalname).toLowerCase())
19 | const mimetype = filetypes.test(file.mimetype)
20 |
21 | if (extname && mimetype) {
22 | return cb(null, true)
23 | } else {
24 | return cb(new Error('Upload Images(.jpg, .jpeg, .png) only!'))
25 | }
26 | }
27 |
28 | const upload = multer({
29 | storage,
30 | fileFilter: function (req, file, cb) {
31 | checkFileType(file, cb)
32 | },
33 | })
34 |
35 | router.post('/', upload.single('image'), (req, res) => {
36 | res.send(`${req.file.path}`)
37 | })
38 |
39 | module.exports = router
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Roopesh Saravanan
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
16 |
17 |
18 |
19 |
20 | Freshbey
21 |
22 |
23 | You need to enable JavaScript to run this app.
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/backend/routes/categoryRoutes.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const asyncHandler = require('express-async-handler')
3 | const router = express.Router()
4 | const Product = require('../models/productModel')
5 | const Category = require('../models/categoryModel')
6 |
7 | // @desc Fetch all categories
8 | // @route GET /api/categories
9 | // @access Public
10 |
11 | router.get(
12 | '/',
13 | asyncHandler(async (req, res) => {
14 | const categories = await Category.find({})
15 | res.json(categories)
16 | }),
17 | )
18 |
19 | // @desc Fetch products based on category
20 | // @route GET /api/categories/:category
21 | // @access Public
22 |
23 | router.get(
24 | '/:category',
25 | asyncHandler(async (req, res) => {
26 | const categorywiseProducts = await Product.find({
27 | category: req.params.category,
28 | })
29 | // const categorywiseProducts = products.filter((p) => p.category === req.params.category)
30 | if (categorywiseProducts) {
31 | res.json(categorywiseProducts)
32 | } else {
33 | res.status(404)
34 | throw new Error('Products not found')
35 | }
36 | }),
37 | )
38 |
39 | module.exports = router
40 |
--------------------------------------------------------------------------------
/frontend/src/pages/Help.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Help = () => {
4 | return (
5 |
6 |
Freshbey Help Centre
7 |
8 | The Freshbey Help Centre page lists out various types of issues that you
9 | may have encountered so that there can be quick resolution and you can
10 | go back to shopping online. For example, you can get more information
11 | regarding order tracking, delivery date changes, help with returns (and
12 | refunds), and much more. You can get the Freshbey Help Centre number or
13 | even access Freshbey Help Centre support if you need professional help
14 | regarding various topics. The support executive will ensure speedy
15 | assistance so that your shopping experience is positive and enjoyable.
16 | You can even inform your loved ones of the support page so that they can
17 | properly get their grievances addressed as well. Once you have all your
18 | queries addressed, you can pull out your shopping list and shop for all
19 | your essentials in one place. You can shop during festive sales to get
20 | your hands on some unbelievable deals online.
21 |
22 |
23 | )
24 | }
25 |
26 | export default Help
27 |
--------------------------------------------------------------------------------
/frontend/src/screens/CategoryScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { Col, Row } from 'react-bootstrap'
3 | import { useDispatch, useSelector } from 'react-redux'
4 | import Product from '../components/Product'
5 | import { listProductsByCategory } from '../actions/productActions'
6 | import Loader from '../components/Loader'
7 | import Message from '../components/Message'
8 |
9 | const CategoryScreen = ({ match, history }) => {
10 | const dispatch = useDispatch()
11 | const productList = useSelector((state) => state.productListByCategory)
12 | const { loading, error, products } = productList
13 | useEffect(() => {
14 | dispatch(listProductsByCategory(match.params.category))
15 | }, [dispatch, match])
16 | return (
17 | <>
18 | Latest Stocks on demand
19 | {loading ? (
20 |
21 | ) : error ? (
22 | {error}
23 | ) : (
24 |
25 | {products.map((product) => (
26 |
27 |
28 |
29 | ))}
30 |
31 | )}
32 | >
33 | )
34 | }
35 |
36 | export default CategoryScreen
37 |
--------------------------------------------------------------------------------
/frontend/src/constants/orderConstants.js:
--------------------------------------------------------------------------------
1 | export const ORDER_CREATE_REQUEST = 'ORDER_CREATE_REQUEST'
2 | export const ORDER_CREATE_SUCCESS = 'ORDER_CREATE_SUCCESS'
3 | export const ORDER_CREATE_FAIL = 'ORDER_CREATE_FAIL'
4 |
5 | export const ORDER_DETAILS_REQUEST = 'ORDER_DETAILS_REQUEST'
6 | export const ORDER_DETAILS_SUCCESS = 'ORDER_DETAILS_SUCCESS'
7 | export const ORDER_DETAILS_FAIL = 'ORDER_DETAILS_FAIL'
8 |
9 | export const ORDER_PAY_REQUEST = 'ORDER_PAY_REQUEST'
10 | export const ORDER_PAY_SUCCESS = 'ORDER_PAY_SUCCESS'
11 | export const ORDER_PAY_FAIL = 'ORDER_PAY_FAIL'
12 | export const ORDER_PAY_RESET = 'ORDER_PAY_RESET'
13 |
14 | export const ORDER_LIST_MY_REQUEST = 'ORDER_LIST_MY_REQUEST'
15 | export const ORDER_LIST_MY_SUCCESS = 'ORDER_LIST_MY_SUCCESS'
16 | export const ORDER_LIST_MY_FAIL = 'ORDER_LIST_MY_FAIL'
17 | export const ORDER_LIST_MY_RESET = 'ORDER_LIST_MY_RESET'
18 |
19 | export const ORDER_LIST_REQUEST = 'ORDER_LIST_REQUEST'
20 | export const ORDER_LIST_SUCCESS = 'ORDER_LIST_SUCCESS'
21 | export const ORDER_LIST_FAIL = 'ORDER_LIST_FAIL'
22 |
23 | export const ORDER_DELIVER_REQUEST = 'ORDER_DELIVER_REQUEST'
24 | export const ORDER_DELIVER_SUCCESS = 'ORDER_DELIVER_SUCCESS'
25 | export const ORDER_DELIVER_FAIL = 'ORDER_DELIVER_FAIL'
26 | export const ORDER_DELIVER_RESET = 'ORDER_DELIVER_RESET'
27 |
--------------------------------------------------------------------------------
/frontend/src/screens/SearchScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { Col, Row } from 'react-bootstrap'
3 | import { useDispatch, useSelector } from 'react-redux'
4 | import Product from '../components/Product'
5 | import { listProducts } from '../actions/productActions'
6 | import Loader from '../components/Loader'
7 | import Message from '../components/Message'
8 |
9 | const SearchScreen = ({ match, history }) => {
10 | const keyword = match.params.keyword
11 | const dispatch = useDispatch()
12 | const productList = useSelector((state) => state.productListBySearch)
13 | const { loading, error, products } = productList
14 | useEffect(() => {
15 | dispatch(listProducts(keyword))
16 | }, [dispatch, match, keyword])
17 | return (
18 | <>
19 | Results based on your search
20 | {loading ? (
21 |
22 | ) : error ? (
23 | {error}
24 | ) : (
25 |
26 | {/* {!products && No products found based on your search } */}
27 | {products.map((product) => (
28 |
29 |
30 |
31 | ))}
32 |
33 | )}
34 | >
35 | )
36 | }
37 |
38 | export default SearchScreen
39 |
--------------------------------------------------------------------------------
/frontend/src/reducers/cartReducers.js:
--------------------------------------------------------------------------------
1 | import {
2 | CART_ADD_ITEM,
3 | CART_REMOVE_ITEM,
4 | CART_SAVE_PAYMENT_METHOD,
5 | CART_SAVE_SHIPPING_ADDRESS,
6 | } from '../constants/cartConstants'
7 |
8 | export const cartReducer = (
9 | state = { cartItems: [], shippingAddress: {} },
10 | action,
11 | ) => {
12 | switch (action.type) {
13 | case CART_ADD_ITEM:
14 | const item = action.payload
15 | const existItem = state.cartItems.find((x) => x.product === item.product)
16 | if (existItem) {
17 | return {
18 | ...state,
19 | cartItems: state.cartItems.map((x) =>
20 | x.product === existItem.product ? item : x,
21 | ),
22 | }
23 | } else {
24 | return {
25 | ...state,
26 | cartItems: [...state.cartItems, item],
27 | }
28 | }
29 |
30 | case CART_REMOVE_ITEM:
31 | return {
32 | ...state,
33 | cartItems: state.cartItems.filter(
34 | (item) => item.product !== action.payload,
35 | ),
36 | }
37 |
38 | case CART_SAVE_SHIPPING_ADDRESS:
39 | return {
40 | ...state,
41 | shippingAddress: action.payload,
42 | }
43 |
44 | case CART_SAVE_PAYMENT_METHOD:
45 | return {
46 | ...state,
47 | paymentMethod: action.payload,
48 | }
49 |
50 | default:
51 | return state
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/test/orderControllers.test.js:
--------------------------------------------------------------------------------
1 | const Order = require('../backend/models/orderModel')
2 | const app = require('../backend/server')
3 | const Product = require('../backend/models/productModel')
4 | const chai = require('chai')
5 | const chaiHttp = require('chai-http')
6 | const expect = chai.expect
7 | chai.use(chaiHttp)
8 | chai.should()
9 | describe('ORDER TESTS', () => {
10 | beforeEach((done) => {
11 | Order.deleteMany({}, (err) => {
12 | done()
13 | })
14 | })
15 | describe('/POST /api/order', () => {
16 | it('should create order', (done) => {
17 | let order = new Order({
18 | orderItems: [
19 | {
20 | name: 'onions',
21 | qty: '1',
22 | imageSrc: 'hjldehde',
23 | price: '20',
24 | product: 1,
25 | },
26 | ],
27 | user: 1,
28 | shippingAddress: {
29 | address: 'test',
30 | pinCode: 2,
31 | city: 'Nairobi',
32 | state: 'Narok',
33 | country: 'Kenya',
34 | },
35 | paymentMethod: 'wtf',
36 | itemsPrice: 2,
37 | shippingPrice: 4,
38 | totalPrice: 23,
39 | })
40 | chai
41 | .request(app)
42 | .post('/api/orders')
43 | .send(order)
44 | .end((err, res) => {
45 | res.should.have.a('object')
46 | done()
47 | })
48 | })
49 | })
50 | })
51 |
--------------------------------------------------------------------------------
/frontend/src/actions/cartActions.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import {
3 | CART_ADD_ITEM,
4 | CART_REMOVE_ITEM,
5 | CART_SAVE_PAYMENT_METHOD,
6 | CART_SAVE_SHIPPING_ADDRESS,
7 | } from '../constants/cartConstants'
8 |
9 | export const addToCart = (id, qty) => async (dispatch, getState) => {
10 | const { data } = await axios.get(`/api/products/${id}`)
11 | dispatch({
12 | type: CART_ADD_ITEM,
13 | payload: {
14 | product: data._id,
15 | name: data.name,
16 | imageSrc: data.imageSrc,
17 | price: data.price,
18 | countInStock: data.countInStock,
19 | qty,
20 | },
21 | })
22 |
23 | localStorage.setItem('cartItems', JSON.stringify(getState().cart.cartItems))
24 | }
25 |
26 | export const removeFromCart = (id) => (dispatch, getState) => {
27 | dispatch({
28 | type: CART_REMOVE_ITEM,
29 | payload: id,
30 | })
31 | localStorage.setItem('cartItems', JSON.stringify(getState().cart.cartItems))
32 | }
33 |
34 | export const saveShippingAddress = (data) => (dispatch) => {
35 | dispatch({
36 | type: CART_SAVE_SHIPPING_ADDRESS,
37 | payload: data,
38 | })
39 |
40 | localStorage.setItem('shippingAddress', JSON.stringify(data))
41 | }
42 |
43 | export const savePaymentMethod = (data) => (dispatch) => {
44 | dispatch({
45 | type: CART_SAVE_PAYMENT_METHOD,
46 | payload: data,
47 | })
48 |
49 | localStorage.setItem('paymentMethod', JSON.stringify(data))
50 | }
51 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "proxy": "http://127.0.0.1:5000",
4 | "version": "0.1.0",
5 | "private": true,
6 | "dependencies": {
7 | "@testing-library/jest-dom": "^5.14.1",
8 | "@testing-library/react": "^11.2.7",
9 | "@testing-library/user-event": "^12.8.3",
10 | "axios": "^0.22.0",
11 | "react": "^17.0.2",
12 | "react-bootstrap": "^1.6.3",
13 | "react-dom": "^17.0.2",
14 | "react-helmet": "^6.1.0",
15 | "react-icons": "^4.3.1",
16 | "react-redux": "^7.2.5",
17 | "react-router-bootstrap": "^0.25.0",
18 | "react-router-dom": "^5.3.0",
19 | "react-scripts": "5.0.1",
20 | "redux": "^4.1.1",
21 | "redux-devtools-extension": "^2.13.9",
22 | "redux-thunk": "^2.3.0",
23 | "swiper": "^7.0.7",
24 | "web-vitals": "^1.1.2"
25 | },
26 | "scripts": {
27 | "start": "react-scripts start",
28 | "build": "react-scripts build",
29 | "test": "react-scripts test",
30 | "eject": "react-scripts eject"
31 | },
32 | "eslintConfig": {
33 | "extends": [
34 | "react-app",
35 | "react-app/jest"
36 | ]
37 | },
38 | "browserslist": {
39 | "production": [
40 | ">0.2%",
41 | "not dead",
42 | "not op_mini all"
43 | ],
44 | "development": [
45 | "last 1 chrome version",
46 | "last 1 firefox version",
47 | "last 1 safari version"
48 | ]
49 | },
50 | "devDependencies": {
51 | "purgecss": "^4.0.3"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/frontend/src/screens/HomeScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { useDispatch, useSelector } from 'react-redux'
3 | import { Col, Row } from 'react-bootstrap'
4 | import Category from '../components/Category'
5 | import { listCategories } from '../actions/categoryActions'
6 | import Loader from '../components/Loader'
7 | import Message from '../components/Message'
8 | import Slider from '../components/Slider'
9 |
10 | const HomeScreen = () => {
11 | const dispatch = useDispatch()
12 | const categoryList = useSelector((state) => state.categoryList)
13 | const { loading, error, categories } = categoryList
14 | useEffect(() => {
15 | dispatch(listCategories())
16 | }, [dispatch])
17 | return (
18 | <>
19 |
20 |
21 |
Shop by category
22 | {loading ? (
23 |
24 | ) : error ? (
25 | {error}
26 | ) : (
27 |
28 | {categories.map((category) => (
29 |
37 |
38 |
39 | ))}
40 |
41 | )}
42 |
43 | >
44 | )
45 | }
46 |
47 | export default HomeScreen
48 |
--------------------------------------------------------------------------------
/frontend/src/components/Slider.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { Carousel, Image } from 'react-bootstrap'
3 | import { useDispatch, useSelector } from 'react-redux'
4 | import { listCarousels } from '../actions/carouselActions'
5 | import Loader from '../components/Loader'
6 | import Message from '../components/Message'
7 |
8 | const Slider = () => {
9 | const dispatch = useDispatch()
10 | const carouselList = useSelector((state) => state.carouselList)
11 | const { loading, carousels, error } = carouselList
12 | useEffect(() => {
13 | dispatch(listCarousels())
14 | }, [dispatch])
15 | return (
16 | <>
17 | {loading ? (
18 |
19 | ) : error ? (
20 | {error}
21 | ) : (
22 |
31 | {carousels.map((carousel) => (
32 |
37 |
43 |
44 | ))}
45 |
46 | )}
47 | >
48 | )
49 | }
50 |
51 | export default Slider
52 |
--------------------------------------------------------------------------------
/backend/models/userModel.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const bcrypt = require('bcryptjs')
3 | const crypto = require('crypto')
4 |
5 | const userSchema = mongoose.Schema(
6 | {
7 | name: {
8 | type: String,
9 | required: true,
10 | },
11 | email: {
12 | type: String,
13 | unique: true,
14 | required: true,
15 | },
16 | password: {
17 | type: String,
18 | required: true,
19 | },
20 | isAdmin: {
21 | type: Boolean,
22 | required: true,
23 | default: false,
24 | },
25 | passwordResetToken: {
26 | type: String,
27 | },
28 | passwordResetExpires: {
29 | type: Date,
30 | },
31 | },
32 | {
33 | timestamps: true,
34 | },
35 | )
36 |
37 | userSchema.methods.matchPassword = async function (enteredPassword) {
38 | return await bcrypt.compare(enteredPassword, this.password)
39 | }
40 |
41 | userSchema.methods.createPasswordResetToken = function () {
42 | const resetToken = crypto.randomBytes(32).toString('hex')
43 | this.passwordResetToken = crypto
44 | .createHash('sha256')
45 | .update(resetToken)
46 | .digest('hex')
47 | this.passwordResetExpires = Date.now() + 10 * 60 * 1000
48 | return resetToken
49 | }
50 |
51 | userSchema.pre('save', async function (next) {
52 | if (!this.isModified('password')) {
53 | next()
54 | }
55 |
56 | const salt = await bcrypt.genSalt(10)
57 | this.password = await bcrypt.hash(this.password, salt)
58 | })
59 |
60 | const User = mongoose.model('User', userSchema)
61 |
62 | module.exports = User
63 |
--------------------------------------------------------------------------------
/backend/models/productModel.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 |
3 | const productSchema = mongoose.Schema(
4 | {
5 | user: {
6 | type: mongoose.Schema.Types.ObjectId,
7 | required: true,
8 | ref: 'User',
9 | },
10 | name: {
11 | type: String,
12 | required: [true, 'Please Enter Name of the Product'],
13 | },
14 | imageSrc: {
15 | type: String,
16 | required: [true, 'Please Upload an Image File'],
17 | },
18 | imageAlt: {
19 | type: String,
20 | required: [true, 'Please Enter Image Alt'],
21 | },
22 | category: {
23 | type: String,
24 | required: [true, 'Please Enter Category of the Product'],
25 | },
26 | description: {
27 | type: String,
28 | required: [true, 'Please Enter Description of the Product'],
29 | },
30 | price: {
31 | type: Number,
32 | required: [true, 'Please Enter Price of Product'],
33 | default: 0,
34 | min: [0, 'Price should be greater than or equal to 0'],
35 | },
36 | mrp: {
37 | type: Number,
38 | required: [true, 'Please Enter MRP of the Product'],
39 | default: 0,
40 | min: [0, 'MRP should be greater than or equal to 0'],
41 | },
42 | countInStock: {
43 | type: Number,
44 | required: [true, 'Please Enter Stock of the Product'],
45 | default: 0,
46 | min: [0, 'Count In Stock value should be greater than or equal to 0'],
47 | },
48 | },
49 | {
50 | timestamps: true,
51 | },
52 | )
53 |
54 | const Product = mongoose.model('Product', productSchema)
55 |
56 | module.exports = Product
57 |
--------------------------------------------------------------------------------
/frontend/src/constants/productConstants.js:
--------------------------------------------------------------------------------
1 | export const PRODUCT_LIST_REQUEST = 'PRODUCT_LIST_REQUEST'
2 | export const PRODUCT_LIST_SUCCESS = 'PRODUCT_LIST_SUCCESS'
3 | export const PRODUCT_LIST_FAIL = 'PRODUCT_LIST_FAIL'
4 |
5 | export const PRODUCT_LIST_BY_CATEGORY_REQUEST =
6 | 'PRODUCT_LIST_BY_CATEGORY_REQUEST'
7 | export const PRODUCT_LIST_BY_CATEGORY_SUCCESS =
8 | 'PRODUCT_LIST_BY_CATEGORY_SUCCESS'
9 | export const PRODUCT_LIST_BY_CATEGORY_FAIL = 'PRODUCT_LIST_BY_CATEGORY_FAIL'
10 |
11 | export const PRODUCT_LIST_BY_SEARCH_REQUEST = 'PRODUCT_LIST_BY_SEARCH_REQUEST'
12 | export const PRODUCT_LIST_BY_SEARCH_SUCCESS = 'PRODUCT_LIST_BY_SEARCH_SUCCESS'
13 | export const PRODUCT_LIST_BY_SEARCH_FAIL = 'PRODUCT_LIST_BY_SEARCH_FAIL'
14 |
15 | export const PRODUCT_DETAILS_REQUEST = 'PRODUCT_DETAILS_REQUEST'
16 | export const PRODUCT_DETAILS_SUCCESS = 'PRODUCT_DETAILS_SUCCESS'
17 | export const PRODUCT_DETAILS_FAIL = 'PRODUCT_DETAILS_FAIL'
18 | export const PRODUCT_DETAILS_RESET = 'PRODUCT_DETAILS_RESET'
19 |
20 | export const PRODUCT_DELETE_REQUEST = 'PRODUCT_DELETE_REQUEST'
21 | export const PRODUCT_DELETE_SUCCESS = 'PRODUCT_DELETE_SUCCESS'
22 | export const PRODUCT_DELETE_FAIL = 'PRODUCT_DELETE_FAIL'
23 |
24 | export const PRODUCT_CREATE_REQUEST = 'PRODUCT_CREATE_REQUEST'
25 | export const PRODUCT_CREATE_SUCCESS = 'PRODUCT_CREATE_SUCCESS'
26 | export const PRODUCT_CREATE_FAIL = 'PRODUCT_CREATE_FAIL'
27 | export const PRODUCT_CREATE_RESET = 'PRODUCT_CREATE_RESET'
28 |
29 | export const PRODUCT_UPDATE_REQUEST = 'PRODUCT_UPDATE_REQUEST'
30 | export const PRODUCT_UPDATE_SUCCESS = 'PRODUCT_UPDATE_SUCCESS'
31 | export const PRODUCT_UPDATE_FAIL = 'PRODUCT_UPDATE_FAIL'
32 | export const PRODUCT_UPDATE_RESET = 'PRODUCT_UPDATE_RESET'
33 |
--------------------------------------------------------------------------------
/frontend/src/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Container, Row, Col, Nav } from 'react-bootstrap'
3 | import { Link } from 'react-router-dom'
4 |
5 | const Footer = () => {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | Returns
15 |
16 |
17 |
18 |
19 | Terms
20 |
21 |
22 |
23 |
24 | Privacy
25 |
26 |
27 |
28 |
29 | Help
30 |
31 |
32 |
33 |
38 | GitHub
39 |
40 |
41 |
42 |
43 |
44 | Copyright © 2022 Freshbey
45 |
46 |
47 |
48 |
49 | )
50 | }
51 |
52 | export default Footer
53 |
--------------------------------------------------------------------------------
/frontend/src/screens/ForgotPasswordScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import { Button, Form } from 'react-bootstrap'
3 | import { useDispatch, useSelector } from 'react-redux'
4 | import { forgotPassword } from '../actions/userActions'
5 | import FormContainer from '../components/FormContainer'
6 | import Message from '../components/Message'
7 | import Loader from '../components/Loader'
8 |
9 | const ForgotPasswordScreen = ({ match }) => {
10 | const [email, setEmail] = useState('')
11 | const dispatch = useDispatch()
12 |
13 | const { loading, message, error } = useSelector(
14 | (state) => state.userForgotPassword,
15 | )
16 |
17 | useEffect(() => {
18 | if (match.params.email) {
19 | setEmail(match.params.email)
20 | }
21 | }, [match])
22 |
23 | const submitHandler = (e) => {
24 | e.preventDefault()
25 | dispatch(forgotPassword(email))
26 | }
27 |
28 | return (
29 |
30 | Password assistance
31 | Enter the email address associated with your account.
32 | {error && {error} }
33 | {message && {message} }
34 | {loading && }
35 |
36 |
38 | Email Address
39 | setEmail(e.target.value)}
45 | >
46 |
47 |
48 | Continue
49 |
50 |
51 |
52 | )
53 | }
54 |
55 | export default ForgotPasswordScreen
56 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "freshbey",
3 | "version": "0.1.0",
4 | "description": "An online grocery ecommerce store",
5 | "main": "server.js",
6 | "engines": {
7 | "node": "14.16.1",
8 | "npm": "7.24.1"
9 | },
10 | "scripts": {
11 | "test": "mocha ***/*.test.js --timeout 50000",
12 | "test-watch": "nodemon --exec \"npm test\"",
13 | "prettier": "prettier --write **/*.js",
14 | "start": "node backend/server",
15 | "server": "nodemon backend/server",
16 | "client": "npm start --prefix frontend",
17 | "dev": "concurrently npm start \"npm run server\" \"npm run client\"",
18 | "data:import": "node backend/seeder",
19 | "data:destroy": "node backend/seeder -d",
20 | "heroku-postbuild": "NPM_CONFIG_PRODUCTION=false npm install --prefix frontend && npm run build --prefix frontend"
21 | },
22 | "repository": {
23 | "type": "git",
24 | "url": "git+https://github.com/roopeshsn/freshbey.git"
25 | },
26 | "author": "Roopesh Saravanan",
27 | "license": "MIT",
28 | "bugs": {
29 | "url": "https://github.com/roopeshsn/freshbey/issues"
30 | },
31 | "homepage": "https://github.com/roopeshsn/freshbey#readme",
32 | "dependencies": {
33 | "bcryptjs": "^2.4.3",
34 | "cloudinary": "^1.32.0",
35 | "compression": "^1.7.4",
36 | "cookie-parser": "^1.4.6",
37 | "cors": "^2.8.5",
38 | "dotenv": "^10.0.0",
39 | "express": "^4.17.3",
40 | "express-async-handler": "^1.1.4",
41 | "jsonwebtoken": "^9.0.0",
42 | "mocha-junit-reporter": "^2.0.2",
43 | "mongoose": "^6.4.6",
44 | "multer": "^1.4.3",
45 | "multer-storage-cloudinary": "^4.0.0",
46 | "nodemailer": "^6.6.5"
47 | },
48 | "devDependencies": {
49 | "chai": "^4.3.6",
50 | "chai-http": "^4.3.0",
51 | "concurrently": "^6.3.0",
52 | "mocha": "^9.2.2",
53 | "nodemon": "^2.0.20",
54 | "prettier": "2.7.1"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/backend/controllers/carouselControllers.js:
--------------------------------------------------------------------------------
1 | const asyncHandler = require('express-async-handler')
2 | const Carousel = require('../models/carouselModel')
3 |
4 | // @desc Fetch all carousels
5 | // @route GET /api/carousels
6 | // @access Public
7 | const getCarousels = asyncHandler(async (req, res) => {
8 | const carousels = await Carousel.find({})
9 | if (carousels) {
10 | res.json(carousels)
11 | } else {
12 | res.status(404)
13 | throw new Error('No carousels found')
14 | }
15 | })
16 |
17 | // @desc Fetch single carousel
18 | // @route GET /api/carousels/:id
19 | // @access Public
20 | const getCarouselById = asyncHandler(async (req, res) => {
21 | const carousel = await Carousel.findById(req.params.id)
22 | if (carousel) {
23 | res.json(carousel)
24 | } else {
25 | res.status(404)
26 | throw new Error('Carousel not found')
27 | }
28 | })
29 |
30 | // @desc Create a carousel
31 | // @route POST /api/carousels
32 | // @access Private/Admin
33 | const createCarousel = asyncHandler(async (req, res) => {
34 | const { imageSrc, imageAlt } = req.body
35 | const carousel = new Carousel({
36 | user: req.user._id,
37 | imageSrc,
38 | imageAlt,
39 | })
40 |
41 | const createdCarousel = await carousel.save()
42 | res.status(201).json(createdCarousel)
43 | })
44 |
45 | // @desc Update a carousel
46 | // @route PUT /api/carousels/:id
47 | // @access Private/Admin
48 | const updateCarousel = asyncHandler(async (req, res) => {
49 | const { imageSrc, imageAlt } = req.body
50 |
51 | const carousel = await Carousel.findById(req.params.id)
52 |
53 | if (carousel) {
54 | carousel.imageSrc = imageSrc
55 | carousel.imageAlt = imageAlt
56 |
57 | const updatedCarousel = await carousel.save()
58 | res.json(updatedCarousel)
59 | } else {
60 | res.status(404)
61 | throw new Error('Carousel not found')
62 | }
63 | })
64 |
65 | module.exports = {
66 | getCarousels,
67 | getCarouselById,
68 | createCarousel,
69 | updateCarousel,
70 | }
71 |
--------------------------------------------------------------------------------
/backend/seeder.js:
--------------------------------------------------------------------------------
1 | const dotenv = require('dotenv')
2 | const users = require('./data/users')
3 | const products = require('./data/products')
4 | const categories = require('./data/categories')
5 | const carousels = require('./data/carousels')
6 | const User = require('./models/userModel')
7 | const Carousel = require('./models/carouselModel')
8 | const Category = require('./models/categoryModel')
9 | const Product = require('./models/productModel')
10 | const Order = require('./models/orderModel')
11 | const { connectDB } = require('./config/db')
12 |
13 | dotenv.config()
14 |
15 | connectDB()
16 |
17 | const importData = async () => {
18 | try {
19 | await Order.deleteMany()
20 | await Product.deleteMany()
21 | await Category.deleteMany()
22 | await Carousel.deleteMany()
23 | await User.deleteMany()
24 | const createdUsers = await User.insertMany(users)
25 | const adminUser = createdUsers[0]._id
26 | const sampleCategories = categories.map((category) => {
27 | return { ...category, user: adminUser }
28 | })
29 | const sampleCarousels = carousels.map((carousel) => {
30 | return { ...carousel, user: adminUser }
31 | })
32 | const sampleProducts = products.map((product) => {
33 | return { ...product, user: adminUser }
34 | })
35 | await Carousel.insertMany(sampleCarousels)
36 | await Category.insertMany(sampleCategories)
37 | await Product.insertMany(sampleProducts)
38 | console.log('Data Imported!')
39 | process.exit()
40 | } catch (error) {
41 | console.error(error)
42 | process.exit(1)
43 | }
44 | }
45 |
46 | const destroyData = async () => {
47 | try {
48 | await Order.deleteMany()
49 | await Product.deleteMany()
50 | await Category.deleteMany()
51 | await Carousel.deleteMany()
52 | await User.deleteMany()
53 | console.log('Data Destroyed!')
54 | process.exit()
55 | } catch (error) {
56 | console.error(error)
57 | process.exit(1)
58 | }
59 | }
60 |
61 | if (process.argv[2] === '-d') {
62 | destroyData()
63 | } else {
64 | importData()
65 | }
66 |
--------------------------------------------------------------------------------
/frontend/src/constants/userConstants.js:
--------------------------------------------------------------------------------
1 | export const USER_LOGIN_REQUEST = 'USER_LOGIN_REQUEST'
2 | export const USER_LOGIN_SUCCESS = 'USER_LOGIN_SUCCESS'
3 | export const USER_LOGIN_FAIL = 'USER_LOGIN_FAIL'
4 | export const USER_LOGOUT = 'USER_LOGOUT'
5 |
6 | export const USER_REGISTER_REQUEST = 'USER_REGISTER_REQUEST'
7 | export const USER_REGISTER_SUCCESS = 'USER_REGISTER_SUCCESS'
8 | export const USER_REGISTER_FAIL = 'USER_REGISTER_FAIL'
9 |
10 | export const USER_FORGOT_PASSWORD_REQUEST = 'USER_FORGOT_PASSWORD_REQUEST'
11 | export const USER_FORGOT_PASSWORD_SUCCESS = 'USER_FORGOT_PASSWORD_SUCCESS'
12 | export const USER_FORGOT_PASSWORD_FAIL = 'USER_FORGOT_PASSWORD_FAIL'
13 |
14 | export const USER_RESET_PASSWORD_REQUEST = 'USER_RESET_PASSWORD_REQUEST'
15 | export const USER_RESET_PASSWORD_SUCCESS = 'USER_RESET_PASSWORD_SUCCESS'
16 | export const USER_RESET_PASSWORD_FAIL = 'USER_RESET_PASSWORD_FAIL'
17 |
18 | export const USER_DETAILS_REQUEST = 'USER_DETAILS_REQUEST'
19 | export const USER_DETAILS_SUCCESS = 'USER_DETAILS_SUCCESS'
20 | export const USER_DETAILS_FAIL = 'USER_DETAILS_FAIL'
21 | export const USER_DETAILS_RESET = 'USER_DETAILS_RESET'
22 |
23 | export const USER_UPDATE_PROFILE_REQUEST = 'USER_UPDATE_PROFILE_REQUEST'
24 | export const USER_UPDATE_PROFILE_SUCCESS = 'USER_UPDATE_PROFILE_SUCCESS'
25 | export const USER_UPDATE_PROFILE_FAIL = 'USER_UPDATE_PROFILE_FAIL'
26 | export const USER_UPDATE_PROFILE_RESET = 'USER_UPDATE_PROFILE_RESET'
27 |
28 | export const USERS_LIST_REQUEST = 'USERS_LIST_REQUEST'
29 | export const USERS_LIST_SUCCESS = 'USERS_LIST_SUCCESS'
30 | export const USERS_LIST_FAIL = 'USERS_LIST_FAIL'
31 | export const USERS_LIST_RESET = 'USERS_LIST_RESET'
32 |
33 | export const USER_DELETE_REQUEST = 'USER_DELETE_REQUEST'
34 | export const USER_DELETE_SUCCESS = 'USER_DELETE_SUCCESS'
35 | export const USER_DELETE_FAIL = 'USER_DELETE_FAIL'
36 |
37 | export const USER_UPDATE_REQUEST = 'USER_UPDATE_REQUEST'
38 | export const USER_UPDATE_SUCCESS = 'USER_UPDATE_SUCCESS'
39 | export const USER_UPDATE_FAIL = 'USER_UPDATE_FAIL'
40 | export const USER_UPDATE_RESET = 'USER_UPDATE_RESET'
41 |
--------------------------------------------------------------------------------
/backend/server.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const express = require('express')
3 | const cors = require('cors')
4 | require('dotenv').config()
5 | const cookieParser = require('cookie-parser')
6 | const { connectDB } = require('./config/db')
7 | const productRoutes = require('./routes/productRoutes')
8 | const userRoutes = require('./routes/userRoutes')
9 | const orderRoutes = require('./routes/orderRoutes')
10 | const uploadRoutes = require('./routes/uploadRoutes')
11 | const categoryRoutes = require('./routes/categoryRoutes')
12 | const carouselRoutes = require('./routes/carouselRoutes')
13 | const { notFound, errorHandler } = require('./middleware/errorMiddleware')
14 | const compression = require('compression')
15 |
16 | const app = express()
17 | app.use(cookieParser())
18 | app.use(
19 | cors({
20 | origin: ['http://localhost:3000', 'https://freshbey.herokuapp.com'],
21 | credentials: true,
22 | }),
23 | )
24 | const PORT = process.env.PORT || 5000
25 |
26 | // DB connection
27 | connectDB()
28 |
29 | app.use(compression())
30 | app.use(express.json())
31 | app.use('/api/carousels', carouselRoutes)
32 | app.use('/api/categories', categoryRoutes)
33 | app.use('/api/products', productRoutes)
34 | app.use('/api/users', userRoutes)
35 | app.use('/api/orders', orderRoutes)
36 | app.use('/api/upload', uploadRoutes)
37 |
38 | if (process.env.NODE_ENV === 'production') {
39 | app.use(express.static(path.join(__dirname, '../frontend/build')))
40 |
41 | app.get('*', (req, res) =>
42 | res.sendFile(
43 | path.resolve(__dirname, '..', 'frontend', 'build', 'index.html'),
44 | ),
45 | )
46 | } else {
47 | app.get(`/`, (req, res) => {
48 | let greetings = {
49 | message: 'Welcome to Freshbey Backend',
50 | version: process.env.VERSION,
51 | license: 'MIT',
52 | }
53 | res.send(greetings).status(200)
54 | console.log(greetings)
55 | })
56 | }
57 |
58 | app.use(notFound)
59 | app.use(errorHandler)
60 |
61 | app.listen(
62 | PORT,
63 | console.log(`${process.env.NODE_ENV} Server Started on PORT: ${PORT}`),
64 | )
65 |
66 | module.exports = app
67 |
--------------------------------------------------------------------------------
/backend/models/orderModel.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 |
3 | const orderSchema = mongoose.Schema(
4 | {
5 | user: {
6 | type: mongoose.Schema.Types.ObjectId,
7 | required: true,
8 | ref: 'User',
9 | },
10 | orderItems: [
11 | {
12 | name: { type: String, required: true },
13 | qty: { type: String, required: true },
14 | imageSrc: { type: String, required: true },
15 | price: { type: String, required: true },
16 | product: {
17 | type: mongoose.Schema.Types.ObjectId,
18 | required: true,
19 | ref: 'Product',
20 | },
21 | },
22 | ],
23 | shippingAddress: {
24 | address: { type: String, required: true },
25 | pinCode: { type: String, required: true }, // PinCode type is String (because: Different Countries uses PinCode as AlphaNumeric or Numeric as well - anujverma-eng)
26 | city: { type: String, required: true },
27 | state: { type: String, required: true },
28 | country: { type: String, required: true },
29 | },
30 | paymentMethod: {
31 | type: String,
32 | required: true,
33 | },
34 | paymentResult: {
35 | id: { type: String },
36 | status: { type: String },
37 | update_time: { type: String },
38 | email_address: { type: String },
39 | },
40 | itemsPrice: {
41 | type: Number,
42 | required: true,
43 | default: 0.0,
44 | },
45 | shippingPrice: {
46 | type: Number,
47 | required: true,
48 | default: 0.0,
49 | },
50 | totalPrice: {
51 | type: Number,
52 | required: true,
53 | default: 0.0,
54 | },
55 | isPaid: {
56 | type: Boolean,
57 | required: true,
58 | default: false,
59 | },
60 | paidAt: {
61 | type: Date,
62 | },
63 | isDelivered: {
64 | type: Boolean,
65 | required: true,
66 | default: false,
67 | },
68 | deliveredAt: {
69 | type: Date,
70 | },
71 | },
72 | {
73 | timestamps: true,
74 | },
75 | )
76 |
77 | const Order = mongoose.model('Order', orderSchema)
78 |
79 | module.exports = Order
80 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | 👍🎉 First off, thanks for taking the time to contribute! 🎉👍
4 |
5 | The following is a set of guidelines for contributing to Conception and its packages, which are hosted on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
6 |
7 | To begin, please check the Issues tab to see where you can contribute.
8 |
9 | ## Issue Label/Tagging Breakdown
10 |
11 | Checking the open issues is a great place to see what's currently being worked on and also what's available to work on. Labels are used to group different issues by status, type, and difficulty. The main labels you should be aware of are:
12 |
13 | - react
14 | - bug
15 | - enhancement
16 | - good first issue
17 | - code level easy
18 | - code level medium
19 | - code level hard
20 | - in progress
21 | - ready to work on
22 | - discussion
23 |
24 | **To claim an issue to work on please write a comment on the issue letting me know you'd like to take it on. Once the issue has been claimed the 'in progress' label will be added.**
25 |
26 | If something doesn't make sense please ask for clarification or assistance by writing a comment on the issue.
27 |
28 | ## Pull Requests
29 |
30 | Before submitting a PR that relates directly to an issue, please write a comment on the relevant issue letting me know you'd like to take it on. Once the issue has been claimed the 'in progress' label will be added. After long periods of inactivity, I'll remove the 'in progress' label so someone else can work on it.
31 |
32 | If you've never submitted a pull request before then I would recommend you check out this guide: [Step-by-step guide to contributing on GitHub](https://www.dataschool.io/how-to-contribute-on-github/)
33 |
34 | Don't panic if you feel as though your code isn't quite up-to-scratch. This is an opportunity for you to learn and to receive feedback. If your code needs some work then please see this as a positive experience and opportunity to improve.
35 |
36 | Pull requests big or small will be accepted, however, if you are submitting a major feature then please make sure you create an issue for discussion before attempting this work, likewise if you spot a bug please raise a new issue before creating a pull request.
37 |
--------------------------------------------------------------------------------
/frontend/src/screens/PaymentScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { Form, Button, Col } from 'react-bootstrap'
3 | import { useDispatch, useSelector } from 'react-redux'
4 | import FormContainer from '../components/FormContainer'
5 | import CheckoutSteps from '../components/CheckoutSteps'
6 | import { savePaymentMethod } from '../actions/cartActions'
7 |
8 | const PaymentScreen = ({ history }) => {
9 | const cart = useSelector((state) => state.cart)
10 | const { shippingAddress } = cart
11 |
12 | if (!shippingAddress.address) {
13 | history.push('/shipping')
14 | }
15 |
16 | const [paymentMethod, setPaymentMethod] = useState('COD')
17 |
18 | const dispatch = useDispatch()
19 |
20 | const submitHandler = (e) => {
21 | e.preventDefault()
22 | dispatch(savePaymentMethod(paymentMethod))
23 | history.push('/placeorder')
24 | }
25 |
26 | return (
27 |
28 |
29 | Payment Method
30 |
32 | Select Method
33 |
34 | setPaymentMethod(e.target.value)}
42 | >
43 | setPaymentMethod(e.target.value)}
51 | >
52 | setPaymentMethod(e.target.value)}
59 | >
60 |
61 |
62 |
63 |
64 | Continue
65 |
66 |
67 |
68 | )
69 | }
70 |
71 | export default PaymentScreen
72 |
--------------------------------------------------------------------------------
/frontend/src/screens/CreateCarouselScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import { Button, Form } from 'react-bootstrap'
3 | import { useDispatch, useSelector } from 'react-redux'
4 | import { Link } from 'react-router-dom'
5 | import { createCarousel } from '../actions/carouselActions'
6 | import FormContainer from '../components/FormContainer'
7 | import Loader from '../components/Loader'
8 | import Message from '../components/Message'
9 |
10 | const CreateCarouselScreen = ({ history }) => {
11 | const [imageSrc, setImageSrc] = useState('')
12 | const [imageAlt, setImageAlt] = useState('')
13 |
14 | const dispatch = useDispatch()
15 |
16 | const carouselCreate = useSelector((state) => state.carouselCreate)
17 | const { loading, error, success } = carouselCreate
18 |
19 | useEffect(() => {
20 | if (success) {
21 | history.push('/admin/carousellist')
22 | }
23 | }, [success, history])
24 |
25 | const submitHandler = (e) => {
26 | e.preventDefault()
27 | dispatch(
28 | createCarousel({
29 | imageSrc,
30 | imageAlt,
31 | }),
32 | )
33 | }
34 |
35 | return (
36 | <>
37 |
38 | Create Carousel
39 | {loading && }
40 | {error && {error} }
41 |
42 |
44 | Image Src
45 | setImageSrc(e.target.value)}
50 | >
51 |
52 |
53 |
54 | Image Alt
55 | setImageAlt(e.target.value)}
60 | >
61 |
62 |
63 |
64 | Create
65 |
66 |
67 | Go Back
68 |
69 |
70 |
71 | >
72 | )
73 | }
74 |
75 | export default CreateCarouselScreen
76 |
--------------------------------------------------------------------------------
/frontend/src/reducers/carouselReducers.js:
--------------------------------------------------------------------------------
1 | import {
2 | CAROUSEL_LIST_REQUEST,
3 | CAROUSEL_LIST_SUCCESS,
4 | CAROUSEL_LIST_FAIL,
5 | CAROUSEL_DETAILS_REQUEST,
6 | CAROUSEL_DETAILS_SUCCESS,
7 | CAROUSEL_DETAILS_FAIL,
8 | CAROUSEL_DETAILS_RESET,
9 | CAROUSEL_UPDATE_REQUEST,
10 | CAROUSEL_UPDATE_SUCCESS,
11 | CAROUSEL_UPDATE_FAIL,
12 | CAROUSEL_UPDATE_RESET,
13 | CAROUSEL_CREATE_REQUEST,
14 | CAROUSEL_CREATE_SUCCESS,
15 | CAROUSEL_CREATE_FAIL,
16 | CAROUSEL_CREATE_RESET,
17 | } from '../constants/carouselConstants'
18 |
19 | export const carouselListReducer = (state = { carousels: [] }, action) => {
20 | switch (action.type) {
21 | case CAROUSEL_LIST_REQUEST:
22 | return { loading: true, carousels: [] }
23 | case CAROUSEL_LIST_SUCCESS:
24 | return { loading: false, carousels: action.payload }
25 | case CAROUSEL_LIST_FAIL:
26 | return { loading: false, error: action.payload }
27 | default:
28 | return state
29 | }
30 | }
31 |
32 | export const carouselDetailsReducer = (state = { carousel: {} }, action) => {
33 | switch (action.type) {
34 | case CAROUSEL_DETAILS_REQUEST:
35 | return { loading: true, ...state }
36 | case CAROUSEL_DETAILS_SUCCESS:
37 | return { loading: false, carousel: action.payload }
38 | case CAROUSEL_DETAILS_FAIL:
39 | return { loading: false, error: action.payload }
40 | case CAROUSEL_DETAILS_RESET:
41 | return {}
42 | default:
43 | return state
44 | }
45 | }
46 |
47 | export const carouselCreateReducer = (state = { carousel: {} }, action) => {
48 | switch (action.type) {
49 | case CAROUSEL_CREATE_REQUEST:
50 | return { loading: true }
51 | case CAROUSEL_CREATE_SUCCESS:
52 | return { loading: false, success: true, carousel: action.payload }
53 | case CAROUSEL_CREATE_FAIL:
54 | return { loading: false, error: action.payload }
55 | case CAROUSEL_CREATE_RESET:
56 | return {}
57 | default:
58 | return state
59 | }
60 | }
61 |
62 | export const carouselUpdateReducer = (state = { carousel: {} }, action) => {
63 | switch (action.type) {
64 | case CAROUSEL_UPDATE_REQUEST:
65 | return { loading: true }
66 | case CAROUSEL_UPDATE_SUCCESS:
67 | return { loading: false, success: true, carousel: action.payload }
68 | case CAROUSEL_UPDATE_FAIL:
69 | return { loading: false, error: action.payload }
70 | case CAROUSEL_UPDATE_RESET:
71 | return { carousel: {} }
72 | default:
73 | return state
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/frontend/src/screens/ResetPasswordScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { Button, Form } from 'react-bootstrap'
3 | import { useDispatch, useSelector } from 'react-redux'
4 | import { Link } from 'react-router-dom'
5 | import { resetPassword } from '../actions/userActions'
6 | import FormContainer from '../components/FormContainer'
7 | import Loader from '../components/Loader'
8 | import Message from '../components/Message'
9 |
10 | const ResetPasswordScreen = ({ match }) => {
11 | const [password, setPassword] = useState('')
12 | const [confirmPassword, setConfirmPassword] = useState('')
13 | const [passwordErrorMessage, setPasswordErrorMessage] = useState(null)
14 |
15 | const dispatch = useDispatch()
16 |
17 | const submitHandler = (e) => {
18 | e.preventDefault()
19 | const token = match.params.token
20 | if (password !== confirmPassword) {
21 | setPasswordErrorMessage('Passwords do not match')
22 | } else {
23 | setPasswordErrorMessage(null)
24 | dispatch(resetPassword(token, password))
25 | }
26 | }
27 |
28 | const { loading, message, error } = useSelector(
29 | (state) => state.userResetPassword,
30 | )
31 |
32 | return (
33 |
34 | Reset Password
35 | {passwordErrorMessage && (
36 | {passwordErrorMessage}
37 | )}
38 | {message && {message} }
39 | {message && Click here to login}
40 | {error && error }
41 | {loading && }
42 |
44 | New password
45 | setPassword(e.target.value)}
49 | >
50 |
51 |
52 | Confirm new password
53 | setConfirmPassword(e.target.value)}
57 | >
58 |
59 |
60 | Continue
61 |
62 |
63 |
64 | )
65 | }
66 |
67 | export default ResetPasswordScreen
68 |
--------------------------------------------------------------------------------
/frontend/src/screens/LoginScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { Link } from 'react-router-dom'
3 | import { Form, Button, Row, Col } from 'react-bootstrap'
4 | import { useDispatch, useSelector } from 'react-redux'
5 | import Message from '../components/Message'
6 | import Loader from '../components/Loader'
7 | import FormContainer from '../components/FormContainer'
8 | import { login } from '../actions/userActions'
9 |
10 | const LoginScreen = ({ location, history }) => {
11 | const [email, setEmail] = useState('')
12 | const [password, setPassword] = useState('')
13 |
14 | const dispatch = useDispatch()
15 |
16 | const userLogin = useSelector((state) => state.userLogin)
17 | const { loading, error, userInfo } = userLogin
18 |
19 | const redirect = location.search ? location.search.split('=')[1] : '/'
20 |
21 | useEffect(() => {
22 | if (userInfo) {
23 | history.push(redirect)
24 | }
25 | }, [history, userInfo, redirect])
26 |
27 | const submitHandler = (e) => {
28 | e.preventDefault()
29 | dispatch(login(email, password))
30 | }
31 |
32 | return (
33 |
34 | Sign In
35 | {error && {error} }
36 | {loading && }
37 |
39 | Email Address
40 | setEmail(e.target.value)}
45 | >
46 |
47 |
48 |
49 | Password
50 | setPassword(e.target.value)}
55 | >
56 |
57 |
58 |
59 | Sign In
60 |
61 |
62 |
63 |
64 |
65 | Need help?{' '}
66 |
67 | Forgot Password
68 |
69 |
70 |
71 | New Customer?{' '}
72 |
73 | Register
74 |
75 |
76 |
77 |
78 | )
79 | }
80 |
81 | export default LoginScreen
82 |
--------------------------------------------------------------------------------
/test/productController.test.js:
--------------------------------------------------------------------------------
1 | const app = require('../backend/server')
2 | const Product = require('../backend/models/productModel')
3 | const chai = require('chai')
4 | const chaiHttp = require('chai-http')
5 | const expect = chai.expect
6 | chai.use(chaiHttp)
7 | chai.should()
8 |
9 | describe('PRODUCT TESTS', () => {
10 | beforeEach((done) => {
11 | Product.deleteMany({}, (err) => {
12 | done()
13 | })
14 | })
15 | describe('/GET /api/products', () => {
16 | it('Should return all products', (done) => {
17 | chai
18 | .request(app)
19 | .get('/api/products')
20 | .end((err, res) => {
21 | res.should.have.a('object')
22 | done()
23 | })
24 | })
25 | })
26 | describe('/ GET /api/products/:id', () => {
27 | it('should return product by id', (done) => {
28 | const id = 1
29 | chai
30 | .request(app)
31 | .get(`/api/products${id}`)
32 | .end((err, res) => {
33 | res.should.have.a('object')
34 | done()
35 | })
36 | })
37 | })
38 | describe('/ DELETE /api/products/:id', () => {
39 | it('should return product by id', (done) => {
40 | const id = 1
41 | chai
42 | .request(app)
43 | .delete(`/api/products${id}`)
44 | .end((err, res) => {
45 | res.should.have.a('object')
46 | done()
47 | })
48 | })
49 | })
50 | describe('/ POST /api/products/', () => {
51 | it('should create a product', (done) => {
52 | let products = new Product({
53 | name: 'Banana',
54 | price: 20,
55 | mrp: 1,
56 | user: 1,
57 | imageSrc: 'jkdekde',
58 | imageAlt: 'kdieduied',
59 | category: 'kdidldil',
60 | countInStock: 2,
61 | description: 'Test sweet',
62 | })
63 | chai
64 | .request(app)
65 | .post('/api/products')
66 | .send(products)
67 | .end((err, res) => {
68 | res.should.have.a('object')
69 | done()
70 | })
71 | })
72 | })
73 | describe('/ PUT /api/products/:id', () => {
74 | it('should update a product', (done) => {
75 | let id = 1
76 | let products = new Product({
77 | name: 'orange',
78 | price: 27,
79 | mrp: 2,
80 | user: 1,
81 | imageSrc: 'jkdekdess',
82 | imageAlt: 'kdieduiedcsxdsx',
83 | category: 'kdidldilxs',
84 | countInStock: 3,
85 | description: 'Test sweeter',
86 | })
87 | chai
88 | .request(app)
89 | .put(`/api/products/${id}`)
90 | .send(products)
91 | .end((err, res) => {
92 | res.should.have.a('object')
93 | done()
94 | })
95 | })
96 | })
97 | })
98 |
--------------------------------------------------------------------------------
/frontend/src/screens/CarouselListScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { Button, Col, Row, Table } from 'react-bootstrap'
3 | import { useDispatch, useSelector } from 'react-redux'
4 | import { LinkContainer } from 'react-router-bootstrap'
5 | import { listCarousels } from '../actions/carouselActions'
6 | import Loader from '../components/Loader'
7 | import Message from '../components/Message'
8 |
9 | const CarouselListScreen = ({ history }) => {
10 | const dispatch = useDispatch()
11 |
12 | const carouselList = useSelector((state) => state.carouselList)
13 | const { loading, error, carousels } = carouselList
14 |
15 | const userLogin = useSelector((state) => state.userLogin)
16 | const { userInfo } = userLogin
17 |
18 | useEffect(() => {
19 | if (userInfo && userInfo.isAdmin) {
20 | dispatch(listCarousels())
21 | } else {
22 | history.push('/login')
23 | }
24 | }, [dispatch, history, userInfo])
25 |
26 | const createCarouselHandler = () => {
27 | history.push('/admin/createcarousel')
28 | }
29 |
30 | return (
31 | <>
32 |
33 |
34 | Carousels
35 |
36 |
37 |
38 | Create Carousel
39 |
40 |
41 |
42 | {loading ? (
43 |
44 | ) : error ? (
45 | {error}
46 | ) : (
47 |
48 |
49 |
50 | ID
51 | DATE
52 | IMAGE SRC
53 | IMAGE ALT
54 | OPTIONS
55 |
56 |
57 |
58 | {carousels.map((carousel) => (
59 |
60 | {carousel._id}
61 | {carousel.createdAt.substring(0, 10)}
62 |
63 |
64 | {carousel.imageSrc}
65 |
66 |
67 | {carousel.imageAlt}
68 |
69 |
70 |
71 | Edit
72 |
73 |
74 |
75 |
76 | ))}
77 |
78 |
79 | )}
80 | >
81 | )
82 | }
83 |
84 | export default CarouselListScreen
85 |
--------------------------------------------------------------------------------
/frontend/src/components/Product.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { useDispatch } from 'react-redux'
3 | import { Link } from 'react-router-dom'
4 | import { Card, Col, Row, Button, Form } from 'react-bootstrap'
5 | import calculateDiscount from '../utils/calculateDiscount'
6 | import formatter from '../utils/currencyFormatter'
7 | import { addToCart } from '../actions/cartActions'
8 |
9 | const Product = ({ product, history }) => {
10 | const [qty, setQty] = useState(1)
11 | const dispatch = useDispatch()
12 | const [discountPercentage] = calculateDiscount(product.mrp, product.price)
13 |
14 | const addToCartHandler = () => {
15 | dispatch(addToCart(product._id, qty))
16 | history.push('/cart')
17 | }
18 |
19 | return (
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | {product.name}
32 |
33 |
34 |
35 |
{formatter(product.price)}
36 |
37 | M.R.P:
38 |
39 | {formatter(product.mrp)}
40 |
41 | ({discountPercentage}% off)
42 |
43 | {product.countInStock > 0 && (
44 |
45 |
46 | setQty(e.target.value)}
51 | >
52 | {[...Array(product.countInStock).keys()].map((x) => (
53 |
54 | {x + 1} Kg
55 |
56 | ))}
57 |
58 |
59 |
60 |
65 | Add
66 |
67 |
68 |
69 | )}
70 |
71 |
72 |
73 |
74 |
75 | )
76 | }
77 |
78 | export default Product
79 |
--------------------------------------------------------------------------------
/frontend/src/screens/OrderListScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { LinkContainer } from 'react-router-bootstrap'
3 | import { Table, Button } from 'react-bootstrap'
4 | import { useDispatch, useSelector } from 'react-redux'
5 | import Message from '../components/Message'
6 | import Loader from '../components/Loader'
7 | import { listOrders } from '../actions/orderActions'
8 | import formatter from '../utils/currencyFormatter'
9 |
10 | const OrderListScreen = ({ history }) => {
11 | const dispatch = useDispatch()
12 |
13 | const orderList = useSelector((state) => state.orderList)
14 | const { loading, error, orders } = orderList
15 |
16 | const userLogin = useSelector((state) => state.userLogin)
17 | const { userInfo } = userLogin
18 |
19 | useEffect(() => {
20 | if (userInfo && userInfo.isAdmin) {
21 | dispatch(listOrders())
22 | } else {
23 | history.push('/login')
24 | }
25 | }, [dispatch, history, userInfo])
26 |
27 | return (
28 | <>
29 | Orders
30 | {loading ? (
31 |
32 | ) : error ? (
33 | {error}
34 | ) : (
35 |
36 |
37 |
38 | ID
39 | USER
40 | DATE
41 | TOTAL
42 | PAID
43 | DELIVERED
44 |
45 |
46 |
47 |
48 | {orders.map((order) => (
49 |
50 | {order._id}
51 | {order.user && order.user.name}
52 | {order.createdAt.substring(0, 10)}
53 | {formatter(order.totalPrice)}
54 |
55 | {order.isPaid ? (
56 | order.paidAt.substring(0, 10)
57 | ) : (
58 |
59 | )}
60 |
61 |
62 | {order.isDelivered ? (
63 | order.deliveredAt.substring(0, 10)
64 | ) : (
65 |
66 | )}
67 |
68 |
69 |
70 |
71 | Details
72 |
73 |
74 |
75 |
76 | ))}
77 |
78 |
79 | )}
80 | >
81 | )
82 | }
83 |
84 | export default OrderListScreen
85 |
--------------------------------------------------------------------------------
/frontend/src/components/CheckoutSteps.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Nav } from 'react-bootstrap'
3 | import { LinkContainer } from 'react-router-bootstrap'
4 |
5 | const CheckoutSteps = ({ step1, step2, step3, step4 }) => {
6 | return (
7 |
8 |
9 | {step1 ? (
10 |
11 | Sign In
12 |
13 | ) : (
14 | Sign In
15 | )}
16 |
17 |
18 |
19 | {step2 ? (
20 |
21 | Shipping
22 |
23 | ) : (
24 | Shipping
25 | )}
26 |
27 |
28 |
29 | {step3 ? (
30 |
31 | Payment
32 |
33 | ) : (
34 | Payment
35 | )}
36 |
37 |
38 |
39 | {step4 ? (
40 |
41 | Place Order
42 |
43 | ) : (
44 | Place Order
45 | )}
46 |
47 |
48 | //
49 | //
50 | // {step1 ? (
51 | //
52 | // Sign In
53 | //
54 | // ) : (
55 | // Sign In
56 | // )}
57 | //
58 | //
59 | // {step2 ? (
60 | //
61 | // Shipping
62 | //
63 | // ) : (
64 | // Shipping
65 | // )}
66 | //
67 | //
68 | // {' '}
69 | // {step3 ? (
70 | //
71 | // Payment
72 | //
73 | // ) : (
74 | // Payment
75 | // )}
76 | //
77 | //
78 | // {step4 ? (
79 | //
80 | // Place Order
81 | //
82 | // ) : (
83 | // Place Order
84 | // )}
85 | //
86 | //
87 | )
88 | }
89 |
90 | export default CheckoutSteps
91 |
--------------------------------------------------------------------------------
/frontend/src/screens/UsersListScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { LinkContainer } from 'react-router-bootstrap'
3 | import { Table, Button, ButtonGroup } from 'react-bootstrap'
4 | import { useDispatch, useSelector } from 'react-redux'
5 | import Message from '../components/Message'
6 | import Loader from '../components/Loader'
7 | import { deleteUser, listUsers } from '../actions/userActions'
8 |
9 | const UsersListScreen = ({ history }) => {
10 | const dispatch = useDispatch()
11 |
12 | const usersList = useSelector((state) => state.usersList)
13 | const { loading, error, users } = usersList
14 |
15 | const userLogin = useSelector((state) => state.userLogin)
16 | const { userInfo } = userLogin
17 |
18 | const userDelete = useSelector((state) => state.userDelete)
19 | const { success: successDelete } = userDelete
20 |
21 | useEffect(() => {
22 | if (userInfo && userInfo.isAdmin) {
23 | dispatch(listUsers())
24 | } else {
25 | history.push('/login')
26 | }
27 | }, [dispatch, history, userInfo, successDelete])
28 |
29 | const deleteHandler = (id) => {
30 | if (window.confirm('Are you sure?')) {
31 | dispatch(deleteUser(id))
32 | }
33 | }
34 |
35 | return (
36 | <>
37 | Users
38 | {loading ? (
39 |
40 | ) : error ? (
41 | {error}
42 | ) : (
43 |
44 |
45 |
46 | USER ID
47 | NAME
48 | EMAIL
49 | ADMIN
50 | OPTIONS
51 |
52 |
53 |
54 | {users.map((user) => (
55 |
56 | {user._id}
57 | {user.name}
58 |
59 | {user.email}
60 |
61 |
62 | {user.isAdmin ? (
63 |
64 | ) : (
65 |
66 | )}
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | deleteHandler(user._id)}
79 | >
80 |
81 |
82 |
83 |
84 |
85 | ))}
86 |
87 |
88 | )}
89 | >
90 | )
91 | }
92 |
93 | export default UsersListScreen
94 |
--------------------------------------------------------------------------------
/frontend/src/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, combineReducers, applyMiddleware } from 'redux'
2 | import thunk from 'redux-thunk'
3 | import { composeWithDevTools } from 'redux-devtools-extension'
4 | import {
5 | carouselCreateReducer,
6 | carouselDetailsReducer,
7 | carouselListReducer,
8 | carouselUpdateReducer,
9 | } from './reducers/carouselReducers'
10 | import { categoryListReducer } from './reducers/categoryReducers'
11 | import {
12 | productListReducer,
13 | productDetailsReducer,
14 | productListByCategoryReducer,
15 | productDeleteReducer,
16 | productCreateReducer,
17 | productUpdateReducer,
18 | productListBySearchReducer,
19 | } from './reducers/productReducers'
20 | import { cartReducer } from './reducers/cartReducers'
21 | import {
22 | userDeleteReducer,
23 | userDetailsReducer,
24 | userForgotPasswordReducer,
25 | userLoginReducer,
26 | userRegisterReducer,
27 | userResetPasswordReducer,
28 | usersListReducer,
29 | userUpdateProfileReducer,
30 | userUpdateReducer,
31 | } from './reducers/userReducers'
32 | import {
33 | orderCreateReducer,
34 | orderDeliverReducer,
35 | orderDetailsReducer,
36 | orderListMyReducer,
37 | orderListReducer,
38 | orderPayReducer,
39 | } from './reducers/orderReducers'
40 |
41 | const reducer = combineReducers({
42 | carouselList: carouselListReducer,
43 | carouselDetails: carouselDetailsReducer,
44 | carouselCreate: carouselCreateReducer,
45 | carouselUpdate: carouselUpdateReducer,
46 | categoryList: categoryListReducer,
47 | productList: productListReducer,
48 | productListBySearch: productListBySearchReducer,
49 | productListByCategory: productListByCategoryReducer,
50 | productDetails: productDetailsReducer,
51 | productDelete: productDeleteReducer,
52 | productCreate: productCreateReducer,
53 | productUpdate: productUpdateReducer,
54 | cart: cartReducer,
55 | userLogin: userLoginReducer,
56 | userRegister: userRegisterReducer,
57 | userForgotPassword: userForgotPasswordReducer,
58 | userResetPassword: userResetPasswordReducer,
59 | userDetails: userDetailsReducer,
60 | userUpdateProfile: userUpdateProfileReducer,
61 | usersList: usersListReducer,
62 | userDelete: userDeleteReducer,
63 | userUpdate: userUpdateReducer,
64 | orderCreate: orderCreateReducer,
65 | orderDetails: orderDetailsReducer,
66 | orderListMy: orderListMyReducer,
67 | orderList: orderListReducer,
68 | orderPay: orderPayReducer,
69 | orderDeliver: orderDeliverReducer,
70 | })
71 |
72 | const cartItemsFromStorage = localStorage.getItem('cartItems')
73 | ? JSON.parse(localStorage.getItem('cartItems'))
74 | : []
75 |
76 | const userInfoFromStorage = localStorage.getItem('userInfo')
77 | ? JSON.parse(localStorage.getItem('userInfo'))
78 | : null
79 |
80 | const shippingAddressFromStorage = localStorage.getItem('shippingAddress')
81 | ? JSON.parse(localStorage.getItem('shippingAddress'))
82 | : {}
83 |
84 | const initialState = {
85 | cart: {
86 | cartItems: cartItemsFromStorage,
87 | shippingAddress: shippingAddressFromStorage,
88 | },
89 | userLogin: { userInfo: userInfoFromStorage },
90 | }
91 |
92 | const middleware = [thunk]
93 |
94 | const store = createStore(
95 | reducer,
96 | initialState,
97 | composeWithDevTools(applyMiddleware(...middleware)),
98 | )
99 |
100 | export default store
101 |
--------------------------------------------------------------------------------
/frontend/src/actions/carouselActions.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import {
3 | CAROUSEL_LIST_REQUEST,
4 | CAROUSEL_LIST_SUCCESS,
5 | CAROUSEL_LIST_FAIL,
6 | CAROUSEL_UPDATE_REQUEST,
7 | CAROUSEL_UPDATE_SUCCESS,
8 | CAROUSEL_UPDATE_FAIL,
9 | CAROUSEL_DETAILS_SUCCESS,
10 | CAROUSEL_DETAILS_REQUEST,
11 | CAROUSEL_DETAILS_FAIL,
12 | CAROUSEL_CREATE_REQUEST,
13 | CAROUSEL_CREATE_SUCCESS,
14 | CAROUSEL_CREATE_FAIL,
15 | } from '../constants/carouselConstants'
16 | import { logout } from './userActions'
17 |
18 | export const listCarousels = () => async (dispatch) => {
19 | try {
20 | dispatch({ type: CAROUSEL_LIST_REQUEST })
21 | const { data } = await axios.get(`/api/carousels`)
22 | dispatch({ type: CAROUSEL_LIST_SUCCESS, payload: data })
23 | } catch (error) {
24 | dispatch({
25 | type: CAROUSEL_LIST_FAIL,
26 | payload:
27 | error.response && error.response.data.message
28 | ? error.response.data.message
29 | : error.message,
30 | })
31 | }
32 | }
33 |
34 | export const listCarouselDetails = (id) => async (dispatch) => {
35 | try {
36 | dispatch({ type: CAROUSEL_DETAILS_REQUEST })
37 | const { data } = await axios.get(`/api/carousels/${id}`)
38 | dispatch({ type: CAROUSEL_DETAILS_SUCCESS, payload: data })
39 | } catch (error) {
40 | dispatch({
41 | type: CAROUSEL_DETAILS_FAIL,
42 | payload:
43 | error.response && error.response.data.message
44 | ? error.response.data.message
45 | : error.message,
46 | })
47 | }
48 | }
49 |
50 | export const createCarousel = (carousel) => async (dispatch, getState) => {
51 | try {
52 | dispatch({
53 | type: CAROUSEL_CREATE_REQUEST,
54 | })
55 |
56 | const config = {
57 | headers: {
58 | 'Content-Type': 'application/json',
59 | },
60 | }
61 |
62 | const { data } = await axios.post(`/api/carousels`, carousel, config)
63 |
64 | dispatch({
65 | type: CAROUSEL_CREATE_SUCCESS,
66 | payload: data,
67 | })
68 | } catch (error) {
69 | const message =
70 | error.response && error.response.data.message
71 | ? error.response.data.message
72 | : error.message
73 | if (message === 'Not authorized, token failed') {
74 | dispatch(logout())
75 | }
76 | dispatch({
77 | type: CAROUSEL_CREATE_FAIL,
78 | payload: message,
79 | })
80 | }
81 | }
82 |
83 | export const updateCarousel = (carousel) => async (dispatch, getState) => {
84 | try {
85 | dispatch({
86 | type: CAROUSEL_UPDATE_REQUEST,
87 | })
88 |
89 | const config = {
90 | headers: {
91 | 'Content-Type': 'application/json',
92 | },
93 | }
94 |
95 | const { data } = await axios.put(
96 | `/api/carousels/${carousel._id}`,
97 | carousel,
98 | config,
99 | )
100 |
101 | dispatch({
102 | type: CAROUSEL_UPDATE_SUCCESS,
103 | payload: data,
104 | })
105 | dispatch({ type: CAROUSEL_DETAILS_SUCCESS, payload: data })
106 | } catch (error) {
107 | const message =
108 | error.response && error.response.data.message
109 | ? error.response.data.message
110 | : error.message
111 | if (message === 'Not authorized, token failed') {
112 | dispatch(logout())
113 | }
114 | dispatch({
115 | type: CAROUSEL_UPDATE_FAIL,
116 | payload: message,
117 | })
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Create React App
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `npm start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
13 |
14 | The page will reload if you make edits.\
15 | You will also see any lint errors in the console.
16 |
17 | ### `npm test`
18 |
19 | Launches the test runner in the interactive watch mode.\
20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21 |
22 | ### `npm run build`
23 |
24 | Builds the app for production to the `build` folder.\
25 | It correctly bundles React in production mode and optimizes the build for the best performance.
26 |
27 | The build is minified and the filenames include the hashes.\
28 | Your app is ready to be deployed!
29 |
30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31 |
32 | ### `npm run eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
35 |
36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
39 |
40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
41 |
42 | ## Learn More
43 |
44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45 |
46 | To learn React, check out the [React documentation](https://reactjs.org/).
47 |
48 | ### Code Splitting
49 |
50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
51 |
52 | ### Analyzing the Bundle Size
53 |
54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
55 |
56 | ### Making a Progressive Web App
57 |
58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
59 |
60 | ### Advanced Configuration
61 |
62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
63 |
64 | ### Deployment
65 |
66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
67 |
68 | ### `npm run build` fails to minify
69 |
70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
71 |
--------------------------------------------------------------------------------
/frontend/src/screens/CarouselEditScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import { Button, Form } from 'react-bootstrap'
3 | import { useDispatch, useSelector } from 'react-redux'
4 | import { Link } from 'react-router-dom'
5 | import { listCarouselDetails, updateCarousel } from '../actions/carouselActions'
6 | import FormContainer from '../components/FormContainer'
7 | import Loader from '../components/Loader'
8 | import Message from '../components/Message'
9 | import {
10 | CAROUSEL_DETAILS_RESET,
11 | CAROUSEL_UPDATE_RESET,
12 | } from '../constants/carouselConstants'
13 |
14 | const CarouselEditScreen = ({ match, history }) => {
15 | const carouselId = match.params.id
16 |
17 | const [imageSrc, setImageSrc] = useState('')
18 | const [imageAlt, setImageAlt] = useState('')
19 |
20 | const dispatch = useDispatch()
21 |
22 | const carouselDetails = useSelector((state) => state.carouselDetails)
23 | const { loading, error, carousel } = carouselDetails
24 |
25 | const carouselUpdate = useSelector((state) => state.carouselUpdate)
26 | const {
27 | loading: loadingUpdate,
28 | error: errorUpdate,
29 | success: successUpdate,
30 | } = carouselUpdate
31 |
32 | useEffect(() => {
33 | if (successUpdate) {
34 | dispatch({ type: CAROUSEL_UPDATE_RESET })
35 | dispatch({ type: CAROUSEL_DETAILS_RESET })
36 | history.push('/admin/carousellist')
37 | } else {
38 | if (carousel._id !== carouselId) {
39 | dispatch(listCarouselDetails(carouselId))
40 | } else {
41 | setImageSrc(carousel.imageSrc)
42 | setImageAlt(carousel.imageAlt)
43 | }
44 | }
45 | }, [dispatch, history, carousel, carouselId, successUpdate])
46 |
47 | const submitHandler = (e) => {
48 | e.preventDefault()
49 | dispatch(
50 | updateCarousel({
51 | _id: carouselId,
52 | imageSrc,
53 | imageAlt,
54 | }),
55 | )
56 | }
57 |
58 | return (
59 | <>
60 |
61 | Edit Carousel
62 | {loadingUpdate && }
63 | {errorUpdate && {errorUpdate} }
64 | {loading ? (
65 |
66 | ) : error ? (
67 | {error}
68 | ) : (
69 |
71 | Image Src
72 | setImageSrc(e.target.value)}
77 | >
78 |
79 |
80 |
81 | Image Alt
82 | setImageAlt(e.target.value)}
87 | >
88 |
89 |
90 |
91 | Update
92 |
93 |
94 | Go Back
95 |
96 |
97 | )}
98 |
99 | >
100 | )
101 | }
102 |
103 | export default CarouselEditScreen
104 |
--------------------------------------------------------------------------------
/frontend/src/screens/RegisterScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { Link } from 'react-router-dom'
3 | import { Form, Button, Row, Col } from 'react-bootstrap'
4 | import { useDispatch, useSelector } from 'react-redux'
5 | import Message from '../components/Message'
6 | import Loader from '../components/Loader'
7 | import FormContainer from '../components/FormContainer'
8 | import { register } from '../actions/userActions'
9 |
10 | const RegisterScreen = ({ location, history }) => {
11 | const [name, setName] = useState('')
12 | const [email, setEmail] = useState('')
13 | const [password, setPassword] = useState('')
14 | const [confirmPassword, setConfirmPassword] = useState('')
15 | const [message, setMessage] = useState(null)
16 |
17 | const dispatch = useDispatch()
18 |
19 | const userRegister = useSelector((state) => state.userRegister)
20 | const { loading, error, userInfo } = userRegister
21 |
22 | const redirect = location.search ? location.search.split('=')[1] : '/'
23 |
24 | useEffect(() => {
25 | if (userInfo) {
26 | history.push(redirect)
27 | }
28 | }, [history, userInfo, redirect])
29 |
30 | const submitHandler = (e) => {
31 | e.preventDefault()
32 | if (password !== confirmPassword) {
33 | setMessage('Passwords do not match')
34 | } else {
35 | dispatch(register(name, email, password))
36 | }
37 | }
38 |
39 | return (
40 |
41 | Sign Up
42 | {message && {message} }
43 | {error && {error} }
44 | {loading && }
45 |
47 | Name
48 | setName(e.target.value)}
53 | >
54 |
55 |
56 |
57 | Email Address
58 | setEmail(e.target.value)}
63 | >
64 |
65 |
66 |
67 | Password
68 | setPassword(e.target.value)}
73 | >
74 |
75 |
76 |
77 | Confirm Password
78 | setConfirmPassword(e.target.value)}
83 | >
84 |
85 |
86 |
87 | Register
88 |
89 |
90 |
91 |
92 |
93 | Have an Account?{' '}
94 |
95 | Login
96 |
97 |
98 |
99 |
100 | )
101 | }
102 |
103 | export default RegisterScreen
104 |
--------------------------------------------------------------------------------
/frontend/src/reducers/productReducers.js:
--------------------------------------------------------------------------------
1 | import {
2 | PRODUCT_LIST_REQUEST,
3 | PRODUCT_LIST_SUCCESS,
4 | PRODUCT_LIST_FAIL,
5 | PRODUCT_DETAILS_REQUEST,
6 | PRODUCT_DETAILS_SUCCESS,
7 | PRODUCT_DETAILS_FAIL,
8 | PRODUCT_LIST_BY_CATEGORY_REQUEST,
9 | PRODUCT_LIST_BY_CATEGORY_SUCCESS,
10 | PRODUCT_LIST_BY_CATEGORY_FAIL,
11 | PRODUCT_DELETE_REQUEST,
12 | PRODUCT_DELETE_SUCCESS,
13 | PRODUCT_DELETE_FAIL,
14 | PRODUCT_CREATE_REQUEST,
15 | PRODUCT_CREATE_SUCCESS,
16 | PRODUCT_CREATE_FAIL,
17 | PRODUCT_CREATE_RESET,
18 | PRODUCT_UPDATE_REQUEST,
19 | PRODUCT_UPDATE_SUCCESS,
20 | PRODUCT_UPDATE_FAIL,
21 | PRODUCT_UPDATE_RESET,
22 | PRODUCT_DETAILS_RESET,
23 | } from '../constants/productConstants'
24 |
25 | export const productListReducer = (state = { products: [] }, action) => {
26 | switch (action.type) {
27 | case PRODUCT_LIST_REQUEST:
28 | return { loading: true, products: [] }
29 | case PRODUCT_LIST_SUCCESS:
30 | return { loading: false, products: action.payload }
31 | case PRODUCT_LIST_FAIL:
32 | return { loading: false, error: action.payload }
33 | default:
34 | return state
35 | }
36 | }
37 |
38 | export const productListBySearchReducer = (
39 | state = { products: [] },
40 | action,
41 | ) => {
42 | switch (action.type) {
43 | case PRODUCT_LIST_REQUEST:
44 | return { loading: true, products: [] }
45 | case PRODUCT_LIST_SUCCESS:
46 | return { loading: false, products: action.payload }
47 | case PRODUCT_LIST_FAIL:
48 | return { loading: false, error: action.payload }
49 | default:
50 | return state
51 | }
52 | }
53 |
54 | export const productListByCategoryReducer = (
55 | state = { products: [] },
56 | action,
57 | ) => {
58 | switch (action.type) {
59 | case PRODUCT_LIST_BY_CATEGORY_REQUEST:
60 | return { loading: true, products: [] }
61 | case PRODUCT_LIST_BY_CATEGORY_SUCCESS:
62 | return { loading: false, products: action.payload }
63 | case PRODUCT_LIST_BY_CATEGORY_FAIL:
64 | return { loading: false, error: action.payload }
65 | default:
66 | return state
67 | }
68 | }
69 |
70 | export const productDetailsReducer = (state = { product: {} }, action) => {
71 | switch (action.type) {
72 | case PRODUCT_DETAILS_REQUEST:
73 | return { loading: true, ...state }
74 | case PRODUCT_DETAILS_SUCCESS:
75 | return { loading: false, product: action.payload }
76 | case PRODUCT_DETAILS_FAIL:
77 | return { loading: false, error: action.payload }
78 | case PRODUCT_DETAILS_RESET:
79 | return { product: {} }
80 | default:
81 | return state
82 | }
83 | }
84 |
85 | export const productDeleteReducer = (state = {}, action) => {
86 | switch (action.type) {
87 | case PRODUCT_DELETE_REQUEST:
88 | return { loading: true }
89 | case PRODUCT_DELETE_SUCCESS:
90 | return { loading: false, success: true }
91 | case PRODUCT_DELETE_FAIL:
92 | return { loading: false, error: action.payload }
93 | default:
94 | return state
95 | }
96 | }
97 |
98 | export const productCreateReducer = (state = { product: {} }, action) => {
99 | switch (action.type) {
100 | case PRODUCT_CREATE_REQUEST:
101 | return { loading: true }
102 | case PRODUCT_CREATE_SUCCESS:
103 | return { loading: false, success: true, product: action.payload }
104 | case PRODUCT_CREATE_FAIL:
105 | return { loading: false, error: action.payload }
106 | case PRODUCT_CREATE_RESET:
107 | return {}
108 | default:
109 | return state
110 | }
111 | }
112 |
113 | export const productUpdateReducer = (state = { product: {} }, action) => {
114 | switch (action.type) {
115 | case PRODUCT_UPDATE_REQUEST:
116 | return { loading: true }
117 | case PRODUCT_UPDATE_SUCCESS:
118 | return { loading: false, success: true, product: action.payload }
119 | case PRODUCT_UPDATE_FAIL:
120 | return { loading: false, error: action.payload }
121 | case PRODUCT_UPDATE_RESET:
122 | return { product: {} }
123 | default:
124 | return state
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/frontend/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useDispatch, useSelector } from 'react-redux'
3 | import { LinkContainer } from 'react-router-bootstrap'
4 | import { Nav, Navbar, Container, NavDropdown } from 'react-bootstrap'
5 | import { logout } from '../actions/userActions'
6 | import { BsWhatsapp, BsCart } from 'react-icons/bs'
7 | import { FiPhoneCall, FiUser } from 'react-icons/fi'
8 | import { FaCircle } from 'react-icons/fa'
9 | import SearchBox from './SearchBox'
10 | import { Route } from 'react-router'
11 | import { useLocation } from 'react-router'
12 |
13 | const Header = () => {
14 | const dispatch = useDispatch()
15 | const userLogin = useSelector((state) => state.userLogin)
16 | const cart = useSelector((state) => state.cart)
17 | const { cartItems } = cart
18 | const { userInfo } = userLogin
19 | const logoutHandler = () => {
20 | dispatch(logout())
21 | }
22 | const location = useLocation()
23 |
24 | return (
25 |
26 |
27 |
28 |
29 | Freshbey
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | {cartItems.length > 0 && }
44 |
45 |
46 |
47 |
48 |
49 |
50 | {userInfo ? (
51 |
52 |
53 | Profile
54 |
55 |
56 | Logout
57 |
58 |
59 | ) : (
60 |
61 |
62 | Login / Sign Up
63 |
64 |
65 | )}
66 | {userInfo && userInfo.isAdmin && (
67 |
84 | )}
85 |
86 |
87 |
88 |
89 |
90 | {location.pathname === '/login' ||
91 | location.pathname === '/register' ||
92 | location.pathname === '/forgotpassword' ||
93 | location.pathname === '/forgotpassword/:email' ? (
94 | ''
95 | ) : (
96 | } />
97 | )}
98 |
99 | )
100 | }
101 |
102 | export default Header
103 |
--------------------------------------------------------------------------------
/frontend/src/screens/UserEditScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { Link } from 'react-router-dom'
3 | import { Form, Button } from 'react-bootstrap'
4 | import { useDispatch, useSelector } from 'react-redux'
5 | import Message from '../components/Message'
6 | import Loader from '../components/Loader'
7 | import FormContainer from '../components/FormContainer'
8 | import { getUserDetails, updateUser } from '../actions/userActions'
9 | // import { USER_UPDATE_RESET } from '../constants/userConstants'
10 |
11 | const UserEditScreen = ({ match, history }) => {
12 | const userId = match.params.id
13 |
14 | const [name, setName] = useState('')
15 | const [email, setEmail] = useState('')
16 | const [isAdmin, setIsAdmin] = useState(false)
17 |
18 | const dispatch = useDispatch()
19 |
20 | const userDetails = useSelector((state) => state.userDetails)
21 | const { loading, error, user } = userDetails
22 |
23 | const userUpdate = useSelector((state) => state.userUpdate)
24 | const {
25 | loading: loadingUpdate,
26 | error: errorUpdate,
27 | success: successUpdate,
28 | } = userUpdate
29 |
30 | const userLogin = useSelector((state) => state.userLogin)
31 | const { userInfo } = userLogin
32 |
33 | // useEffect(() => {
34 | // if (successUpdate) {
35 | // dispatch({ type: USER_UPDATE_RESET })
36 | // history.push('/admin/users')
37 | // } else {
38 | // if (!user.name || user._id !== userId) {
39 | // dispatch(getUserDetails(userId))
40 | // } else {
41 | // setName(user.name)
42 | // setEmail(user.email)
43 | // setIsAdmin(user.isAdmin)
44 | // }
45 | // }
46 | // }, [dispatch, history, userId, user, successUpdate])
47 |
48 | useEffect(() => {
49 | if (userInfo && userInfo.isAdmin) {
50 | if (!user || user._id !== userId) {
51 | dispatch(getUserDetails(userId))
52 | } else {
53 | setName(user.name)
54 | setEmail(user.email)
55 | setIsAdmin(user.isAdmin)
56 | }
57 | } else {
58 | history.push('/login')
59 | }
60 | }, [userId, dispatch, user, history, userInfo, successUpdate])
61 |
62 | const submitHandler = (e) => {
63 | e.preventDefault()
64 | dispatch(updateUser({ _id: userId, name, email, isAdmin }))
65 | }
66 |
67 | return (
68 | <>
69 |
70 | Edit User
71 | {loadingUpdate && }
72 | {errorUpdate && {errorUpdate} }
73 | {loading ? (
74 |
75 | ) : error ? (
76 | {error}
77 | ) : (
78 |
80 | Name
81 | setName(e.target.value)}
86 | >
87 |
88 |
89 |
90 | Email Address
91 | setEmail(e.target.value)}
96 | >
97 |
98 |
99 |
100 | setIsAdmin(e.target.checked)}
105 | >
106 |
107 |
108 |
109 | Update
110 |
111 |
112 | Go Back
113 |
114 |
115 | )}
116 |
117 | >
118 | )
119 | }
120 |
121 | export default UserEditScreen
122 |
--------------------------------------------------------------------------------
/frontend/src/screens/ProductListScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { LinkContainer } from 'react-router-bootstrap'
3 | import { Table, Button, Row, Col, ButtonGroup } from 'react-bootstrap'
4 | import { useDispatch, useSelector } from 'react-redux'
5 | import Message from '../components/Message'
6 | import Loader from '../components/Loader'
7 | import { deleteProduct, listProducts } from '../actions/productActions'
8 | import formatter from '../utils/currencyFormatter'
9 | import { PRODUCT_CREATE_RESET } from '../constants/productConstants'
10 |
11 | const ProductListScreen = ({ history, match }) => {
12 | const dispatch = useDispatch()
13 |
14 | const productList = useSelector((state) => state.productList)
15 | const { loading, error, products } = productList
16 |
17 | const productDelete = useSelector((state) => state.productDelete)
18 | const {
19 | loading: loadingDelete,
20 | error: errorDelete,
21 | success: successDelete,
22 | } = productDelete
23 |
24 | // const productCreate = useSelector((state) => state.productCreate)
25 | // const {
26 | // loading: loadingCreate,
27 | // error: errorCreate,
28 | // success: successCreate,
29 | // product: createdProduct,
30 | // } = productCreate
31 |
32 | const userLogin = useSelector((state) => state.userLogin)
33 | const { userInfo } = userLogin
34 |
35 | useEffect(() => {
36 | dispatch({ type: PRODUCT_CREATE_RESET })
37 |
38 | if (!userInfo || !userInfo.isAdmin) {
39 | history.push('/login')
40 | } else {
41 | dispatch(listProducts())
42 | }
43 |
44 | // if (successCreate) {
45 | // history.push(`/admin/product/${createdProduct._id}/edit`)
46 | // } else {
47 | // dispatch(listProducts())
48 | // }
49 | }, [dispatch, history, userInfo, successDelete])
50 |
51 | const deleteHandler = (id) => {
52 | if (window.confirm('Are you sure')) {
53 | dispatch(deleteProduct(id))
54 | }
55 | }
56 |
57 | const createProductHandler = () => {
58 | history.push('/admin/createproduct')
59 | }
60 |
61 | return (
62 | <>
63 |
64 |
65 | Products
66 |
67 |
68 |
69 | Create Product
70 |
71 |
72 |
73 | {loadingDelete && }
74 | {errorDelete && {errorDelete} }
75 | {/* {loadingCreate && }
76 | {errorCreate && {errorCreate} } */}
77 | {loading ? (
78 |
79 | ) : error ? (
80 | {error}
81 | ) : (
82 |
83 |
84 |
85 | PRODUCT ID
86 | NAME
87 | PRICE
88 | CATEGORY
89 | OPTIONS
90 |
91 |
92 |
93 | {products.map((product) => (
94 |
95 | {product._id}
96 | {product.name}
97 | {formatter(product.price)}
98 | {product.category}
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | deleteHandler(product._id)}
110 | >
111 |
112 |
113 |
114 |
115 |
116 | ))}
117 |
118 |
119 | )}
120 | >
121 | )
122 | }
123 |
124 | export default ProductListScreen
125 |
--------------------------------------------------------------------------------
/test/userController.test.js:
--------------------------------------------------------------------------------
1 | const app = require('../backend/server')
2 | const mongoose = require('mongoose')
3 | const User = require('../backend/models/userModel')
4 | const chai = require('chai')
5 | const chaiHttp = require('chai-http')
6 | const expect = chai.expect
7 | chai.use(chaiHttp)
8 | chai.should()
9 |
10 | describe('USERS', () => {
11 | beforeEach((done) => {
12 | User.deleteMany({}, (err) => {
13 | done()
14 | })
15 | })
16 | describe('/POST user', () => {
17 | it('it should register user', (done) => {
18 | let user = {
19 | name: 'Felex onyango',
20 | email: 'felex@gmail.com',
21 | password: '@Felex2020',
22 | }
23 | chai
24 | .request(app)
25 | .post('/api/users')
26 | .send(user)
27 | .end((err, res) => {
28 | res.should.have.status(201)
29 | res.should.have.a('object')
30 | res.body.should.have.property('name')
31 | res.body.should.have.property('email')
32 | done()
33 | })
34 | })
35 | })
36 | describe('/POST login user', () => {
37 | it('should login user', (done) => {
38 | let user = {
39 | email: 'felex@gmail.com',
40 | password: '@Felex2020',
41 | }
42 | chai
43 | .request(app)
44 | .post('/api/users/login')
45 | .send(user)
46 | .end((err, res) => {
47 | res.should.have.a('object')
48 | done()
49 | })
50 | })
51 | })
52 |
53 | describe('/GET profile ', () => {
54 | it('it should return profile of user user by id', (done) => {
55 | let user = new User({ name: 'Felex onyango', email: 'felex@gmail.com' })
56 | user.save((err, user) => {
57 | chai
58 | .request(app)
59 | .get('/api/users/profile')
60 | .send(user)
61 | .end((err, res) => {
62 | res.should.have.a('object')
63 | done()
64 | })
65 | })
66 | })
67 | })
68 | describe('PUT /api/users/profile', () => {
69 | it('should update user profile', (done) => {
70 | let user = new User({
71 | name: 'Felex onyango',
72 | email: 'felexonyango@gmail.com',
73 | })
74 | user.save((err, user) => {
75 | chai
76 | .request(app)
77 | .put('/api/users/profile')
78 | .send(user)
79 | .end((err, res) => {
80 | res.should.have.a('object')
81 | done()
82 | })
83 | })
84 | })
85 | })
86 | describe('/api/users/forgotpassword', () => {
87 | it('should forget password of a user', (done) => {
88 | let user = {
89 | email: 'felexonyango@gmail.com',
90 | }
91 | chai
92 | .request(app)
93 | .post('/api/users/forgotpassword')
94 | .send(user)
95 | .end((err, res) => {
96 | res.should.have.a('object')
97 | done()
98 | })
99 | })
100 | })
101 | describe('/GET users ', () => {
102 | it('it should return all users', (done) => {
103 | chai
104 | .request(app)
105 | .get('/api/users')
106 | .end((err, res) => {
107 | res.should.have.a('object')
108 | done()
109 | })
110 | })
111 | })
112 | describe('/DELETE UserId ', () => {
113 | it('it should delete the user by id', (done) => {
114 | const id = 1
115 | chai
116 | .request(app)
117 | .delete(`/api/users/${id}`)
118 | .end((err, res) => {
119 | res.should.have.a('object')
120 | done()
121 | })
122 | })
123 | })
124 | describe('/GET UserId ', () => {
125 | it('it should get the user by id', (done) => {
126 | const id = 2
127 | chai
128 | .request(app)
129 | .delete(`/api/users/${id}`)
130 | .end((err, res) => {
131 | res.should.have.a('object')
132 | done()
133 | })
134 | })
135 | })
136 | describe('PUT /api/users/:id', () => {
137 | it('it should update the user by id', (done) => {
138 | const id = 2
139 | let user = {
140 | name: 'Felex tabu',
141 | email: 'felex@gmail.com',
142 | }
143 | chai
144 | .request(app)
145 | .patch(`/api/users/${id}`)
146 | .send(user)
147 | .end((err, res) => {
148 | res.should.have.a('object')
149 | done()
150 | })
151 | })
152 | })
153 | })
154 |
--------------------------------------------------------------------------------
/backend/controllers/productController.js:
--------------------------------------------------------------------------------
1 | const asyncHandler = require('express-async-handler')
2 | const Product = require('../models/productModel')
3 |
4 | // @desc Fetch all products
5 | // @route GET /api/products
6 | // @access Public
7 | const getProducts = asyncHandler(async (req, res) => {
8 | const keyword = req.query.keyword
9 | ? {
10 | name: {
11 | $regex: req.query.keyword,
12 | $options: 'i',
13 | },
14 | }
15 | : {}
16 |
17 | const products = await Product.find({ ...keyword })
18 | if (products) {
19 | res.json(products)
20 | } else {
21 | res.status(404)
22 | throw new Error('No product found based on your search')
23 | }
24 | })
25 |
26 | // @desc Fetch single product
27 | // @route GET /api/products/:id
28 | // @access Public
29 | const getProductById = asyncHandler(async (req, res) => {
30 | const product = await Product.findById(req.params.id)
31 | if (product) {
32 | res.json(product)
33 | } else {
34 | res.status(404)
35 | throw new Error('Product not found')
36 | }
37 | })
38 |
39 | // @desc Delete a product
40 | // @route DELETE /api/products/:id
41 | // @access Private/Admin
42 | const deleteProduct = asyncHandler(async (req, res) => {
43 | const product = await Product.findById(req.params.id)
44 |
45 | if (product) {
46 | await product.remove()
47 | res.json({ message: 'Product removed' })
48 | } else {
49 | res.status(404)
50 | throw new Error('Product not found')
51 | }
52 | })
53 |
54 | // @desc Create a product
55 | // @route POST /api/products
56 | // @access Private/Admin
57 | const createProduct = asyncHandler(async (req, res) => {
58 | const {
59 | name,
60 | price,
61 | mrp,
62 | description,
63 | imageSrc,
64 | imageAlt,
65 | category,
66 | countInStock,
67 | } = req.body
68 | const product = new Product({
69 | name,
70 | price,
71 | mrp,
72 | user: req.user._id,
73 | imageSrc,
74 | imageAlt,
75 | category,
76 | countInStock,
77 | description,
78 | })
79 |
80 | try {
81 | const createdProduct = await product.save()
82 | res.status(201).json(createdProduct)
83 | } catch (error) {
84 | if (error.name === 'ValidationError') {
85 | let errors = ''
86 | Object.keys(error.errors).forEach((key) => {
87 | errors += error.errors[key].message + '.\n'
88 | })
89 | res.status(500).json(errors)
90 | }
91 | }
92 | })
93 |
94 | // @desc Update a product
95 | // @route PUT /api/products/:id
96 | // @access Private/Admin
97 | const updateProduct = asyncHandler(async (req, res) => {
98 | const {
99 | name,
100 | price,
101 | mrp,
102 | description,
103 | imageSrc,
104 | imageAlt,
105 | category,
106 | countInStock,
107 | } = req.body
108 |
109 | const product = await Product.findById(req.params.id)
110 |
111 | if (product) {
112 | product.name = name
113 | product.price = price
114 | product.mrp = mrp
115 | product.description = description
116 | product.imageSrc = imageSrc
117 | product.imageAlt = imageAlt
118 | product.category = category
119 | product.countInStock = countInStock
120 | try {
121 | const updatedProduct = await product.save()
122 | res.json(updatedProduct)
123 | } catch (error) {
124 | if (error.name === 'ValidationError') {
125 | let errors = ''
126 | Object.keys(error.errors).forEach((key) => {
127 | errors += error.errors[key].message + '.\n'
128 | })
129 | res.status(500).json(errors)
130 | }
131 | }
132 | } else {
133 | res.status(404)
134 | throw new Error('Product not found')
135 | }
136 | })
137 |
138 | const updateStockCount = asyncHandler(async (req, res, id, qty) => {
139 | try {
140 | var product = await Product.findById(id)
141 | } catch (err) {
142 | throw new Error(err.message)
143 | }
144 |
145 | if (product.countInStock < qty) {
146 | throw new Error(
147 | 'We are really sorry. We have not ' +
148 | qty +
149 | ' amount of ' +
150 | product.name +
151 | ' currently on stock. Either you wait for stock to fullfilled or you can decrease your ordered quantity of ' +
152 | product.name,
153 | )
154 | }
155 | product.countInStock -= qty
156 | const updatedProduct = await product.save()
157 | return updateProduct
158 | })
159 |
160 | module.exports = {
161 | getProducts,
162 | getProductById,
163 | deleteProduct,
164 | createProduct,
165 | updateProduct,
166 | updateStockCount,
167 | }
168 |
--------------------------------------------------------------------------------
/frontend/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
3 | import { Container } from 'react-bootstrap'
4 | import Header from './components/Header'
5 | import Footer from './components/Footer'
6 | import HomeScreen from './screens/HomeScreen'
7 | import ProductScreen from './screens/ProductScreen'
8 | import CategoryScreen from './screens/CategoryScreen'
9 | import CartScreen from './screens/CartScreen'
10 | import LoginScreen from './screens/LoginScreen'
11 | import RegisterScreen from './screens/RegisterScreen'
12 | import ProfileScreen from './screens/ProfileScreen'
13 | import ShippingScreen from './screens/ShippingScreen'
14 | import ForgotPasswordScreen from './screens/ForgotPasswordScreen'
15 | import ResetPasswordScreen from './screens/ResetPasswordScreen'
16 | import PaymentScreen from './screens/PaymentScreen'
17 | import PlaceOrderScreen from './screens/PlaceOrderScreen'
18 | import OrderScreen from './screens/OrderScreen'
19 | import UsersListScreen from './screens/UsersListScreen'
20 | import UserEditScreen from './screens/UserEditScreen'
21 | import ProductListScreen from './screens/ProductListScreen'
22 | import CreateProductScreen from './screens/CreateProductScreen'
23 | import ProductEditScreen from './screens/ProductEditScreen'
24 | import OrderListScreen from './screens/OrderListScreen'
25 | import SearchScreen from './screens/SearchScreen'
26 | import CarouselListScreen from './screens/CarouselListScreen'
27 | import CarouselEditScreen from './screens/CarouselEditScreen'
28 | import CreateCarouselScreen from './screens/CreateCarouselScreen'
29 | import { CategoryListScreen } from './screens/CategoryListScreen'
30 | import Error404Screen from './screens/Error404Screen'
31 | import Terms from './pages/Terms'
32 | import Returns from './pages/Returns'
33 | import Privacy from './pages/Privacy'
34 | import Help from './pages/Help'
35 |
36 | function App() {
37 | return (
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
53 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
70 |
74 |
75 |
76 |
80 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | )
96 | }
97 |
98 | export default App
99 |
--------------------------------------------------------------------------------
/frontend/src/actions/productActions.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | import {
4 | PRODUCT_LIST_REQUEST,
5 | PRODUCT_LIST_SUCCESS,
6 | PRODUCT_LIST_FAIL,
7 | PRODUCT_DETAILS_REQUEST,
8 | PRODUCT_DETAILS_SUCCESS,
9 | PRODUCT_DETAILS_FAIL,
10 | PRODUCT_LIST_BY_CATEGORY_REQUEST,
11 | PRODUCT_LIST_BY_CATEGORY_SUCCESS,
12 | PRODUCT_LIST_BY_CATEGORY_FAIL,
13 | PRODUCT_DELETE_REQUEST,
14 | PRODUCT_DELETE_SUCCESS,
15 | PRODUCT_DELETE_FAIL,
16 | PRODUCT_CREATE_REQUEST,
17 | PRODUCT_CREATE_SUCCESS,
18 | PRODUCT_CREATE_FAIL,
19 | PRODUCT_UPDATE_REQUEST,
20 | PRODUCT_UPDATE_SUCCESS,
21 | PRODUCT_UPDATE_FAIL,
22 | } from '../constants/productConstants'
23 |
24 | import { logout } from './userActions'
25 |
26 | export const listProducts =
27 | (keyword = '') =>
28 | async (dispatch) => {
29 | try {
30 | dispatch({ type: PRODUCT_LIST_REQUEST })
31 | const { data } = await axios.get(`/api/products?keyword=${keyword}`)
32 | dispatch({ type: PRODUCT_LIST_SUCCESS, payload: data })
33 | } catch (error) {
34 | dispatch({
35 | type: PRODUCT_LIST_FAIL,
36 | payload:
37 | error.response && error.response.data.message
38 | ? error.response.data.message
39 | : error.message,
40 | })
41 | }
42 | }
43 |
44 | export const listProductsByCategory = (category) => async (dispatch) => {
45 | try {
46 | dispatch({ type: PRODUCT_LIST_BY_CATEGORY_REQUEST })
47 | const { data } = await axios.get(`/api/categories/${category}`)
48 | dispatch({ type: PRODUCT_LIST_BY_CATEGORY_SUCCESS, payload: data })
49 | } catch (error) {
50 | dispatch({
51 | type: PRODUCT_LIST_BY_CATEGORY_FAIL,
52 | payload:
53 | error.response && error.response.data.message
54 | ? error.response.data.message
55 | : error.message,
56 | })
57 | }
58 | }
59 |
60 | export const listProductDetails = (id) => async (dispatch) => {
61 | try {
62 | dispatch({ type: PRODUCT_DETAILS_REQUEST })
63 | const { data } = await axios.get(`/api/products/${id}`)
64 | dispatch({ type: PRODUCT_DETAILS_SUCCESS, payload: data })
65 | } catch (error) {
66 | dispatch({
67 | type: PRODUCT_DETAILS_FAIL,
68 | payload:
69 | error.response && error.response.data.message
70 | ? error.response.data.message
71 | : error.message,
72 | })
73 | }
74 | }
75 |
76 | export const deleteProduct = (id) => async (dispatch, getState) => {
77 | try {
78 | dispatch({
79 | type: PRODUCT_DELETE_REQUEST,
80 | })
81 |
82 | await axios.delete(`/api/products/${id}`)
83 |
84 | dispatch({
85 | type: PRODUCT_DELETE_SUCCESS,
86 | })
87 | } catch (error) {
88 | const message =
89 | error.response && error.response.data.message
90 | ? error.response.data.message
91 | : error.message
92 | if (message === 'Not authorized, token failed') {
93 | dispatch(logout())
94 | }
95 | dispatch({
96 | type: PRODUCT_DELETE_FAIL,
97 | payload: message,
98 | })
99 | }
100 | }
101 |
102 | export const createProduct = (product) => async (dispatch, getState) => {
103 | try {
104 | dispatch({
105 | type: PRODUCT_CREATE_REQUEST,
106 | })
107 |
108 | const config = {
109 | headers: {
110 | 'Content-Type': 'application/json',
111 | },
112 | }
113 |
114 | const { data } = await axios.post(`/api/products`, product, config)
115 |
116 | dispatch({
117 | type: PRODUCT_CREATE_SUCCESS,
118 | payload: data,
119 | })
120 | } catch (error) {
121 | const message =
122 | error.response && error.response.data
123 | ? error.response.data
124 | : error.message
125 | if (message === 'Not authorized, token failed') {
126 | dispatch(logout())
127 | }
128 | dispatch({
129 | type: PRODUCT_CREATE_FAIL,
130 | payload: message,
131 | })
132 | }
133 | }
134 |
135 | export const updateProduct = (product) => async (dispatch, getState) => {
136 | try {
137 | dispatch({
138 | type: PRODUCT_UPDATE_REQUEST,
139 | })
140 |
141 | const config = {
142 | headers: {
143 | 'Content-Type': 'application/json',
144 | },
145 | }
146 |
147 | const { data } = await axios.put(
148 | `/api/products/${product._id}`,
149 | product,
150 | config,
151 | )
152 |
153 | dispatch({
154 | type: PRODUCT_UPDATE_SUCCESS,
155 | payload: data,
156 | })
157 | dispatch({ type: PRODUCT_DETAILS_SUCCESS, payload: data })
158 | } catch (error) {
159 | const message =
160 | error.response && error.response.data
161 | ? error.response.data
162 | : error.message
163 | if (message === 'Not authorized, token failed') {
164 | dispatch(logout())
165 | }
166 | dispatch({
167 | type: PRODUCT_UPDATE_FAIL,
168 | payload: message,
169 | })
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/frontend/src/reducers/orderReducers.js:
--------------------------------------------------------------------------------
1 | import {
2 | ORDER_CREATE_FAIL,
3 | ORDER_CREATE_REQUEST,
4 | ORDER_CREATE_SUCCESS,
5 | ORDER_DELIVER_FAIL,
6 | ORDER_DELIVER_REQUEST,
7 | ORDER_DELIVER_RESET,
8 | ORDER_DELIVER_SUCCESS,
9 | ORDER_DETAILS_FAIL,
10 | ORDER_DETAILS_REQUEST,
11 | ORDER_DETAILS_SUCCESS,
12 | ORDER_LIST_FAIL,
13 | ORDER_LIST_MY_FAIL,
14 | ORDER_LIST_MY_REQUEST,
15 | ORDER_LIST_MY_RESET,
16 | ORDER_LIST_MY_SUCCESS,
17 | ORDER_LIST_REQUEST,
18 | ORDER_LIST_SUCCESS,
19 | ORDER_PAY_FAIL,
20 | ORDER_PAY_REQUEST,
21 | ORDER_PAY_RESET,
22 | ORDER_PAY_SUCCESS,
23 | } from '../constants/orderConstants'
24 |
25 | export const orderCreateReducer = (state = {}, action) => {
26 | switch (action.type) {
27 | case ORDER_CREATE_REQUEST:
28 | return {
29 | loading: true,
30 | }
31 | case ORDER_CREATE_SUCCESS:
32 | return {
33 | loading: false,
34 | success: true,
35 | order: action.payload,
36 | }
37 | case ORDER_CREATE_FAIL:
38 | return {
39 | loading: false,
40 | error: action.payload,
41 | }
42 | // case ORDER_CREATE_RESET:
43 | // return {}
44 | default:
45 | return state
46 | }
47 | }
48 |
49 | export const orderDetailsReducer = (
50 | state = { loading: true, orderItems: [], shippingAddress: {} },
51 | action,
52 | ) => {
53 | switch (action.type) {
54 | case ORDER_DETAILS_REQUEST:
55 | return {
56 | ...state,
57 | loading: true,
58 | }
59 | case ORDER_DETAILS_SUCCESS:
60 | case ORDER_PAY_SUCCESS:
61 | case ORDER_DELIVER_SUCCESS:
62 | return {
63 | loading: false,
64 | order: action.payload,
65 | }
66 | case ORDER_DETAILS_FAIL:
67 | return {
68 | loading: false,
69 | error: action.payload,
70 | }
71 | default:
72 | return state
73 | }
74 | }
75 |
76 | export const orderPayReducer = (state = {}, action) => {
77 | switch (action.type) {
78 | case ORDER_PAY_REQUEST:
79 | return {
80 | loading: true,
81 | }
82 | case ORDER_PAY_SUCCESS:
83 | return {
84 | loading: false,
85 | success: true,
86 | }
87 | case ORDER_PAY_FAIL:
88 | return {
89 | loading: false,
90 | error: action.payload,
91 | }
92 | case ORDER_PAY_RESET:
93 | return {}
94 | default:
95 | return state
96 | }
97 | }
98 |
99 | // export const payOrder = (orderId, paymentResult) => async (dispatch, getState) => {
100 | // try {
101 | // dispatch({
102 | // type: ORDER_PAY_REQUEST,
103 | // })
104 | // const config = {
105 | // headers: {
106 | // 'Content-Type': 'application/json',
107 | // },
108 | // }
109 |
110 | // const { data } = await axios.put(`/api/orders/${orderId}/pay`, paymentResult, config)
111 |
112 | // dispatch({
113 | // type: ORDER_PAY_SUCCESS,
114 | // payload: data,
115 | // })
116 | // } catch (error) {
117 | // const message =
118 | // error.response && error.response.data.message ? error.response.data.message : error.message
119 | // if (message === 'Not authorized, token failed') {
120 | // dispatch(logout())
121 | // }
122 | // dispatch({
123 | // type: ORDER_PAY_FAIL,
124 | // payload: message,
125 | // })
126 | // }
127 | // }
128 |
129 | export const orderListMyReducer = (state = { orders: [] }, action) => {
130 | switch (action.type) {
131 | case ORDER_LIST_MY_REQUEST:
132 | return {
133 | loading: true,
134 | }
135 | case ORDER_LIST_MY_SUCCESS:
136 | return {
137 | loading: false,
138 | orders: action.payload,
139 | }
140 | case ORDER_LIST_MY_FAIL:
141 | return {
142 | loading: false,
143 | error: action.payload,
144 | }
145 | case ORDER_LIST_MY_RESET:
146 | return { orders: [] }
147 | default:
148 | return state
149 | }
150 | }
151 |
152 | export const orderListReducer = (state = { orders: [] }, action) => {
153 | switch (action.type) {
154 | case ORDER_LIST_REQUEST:
155 | return {
156 | loading: true,
157 | }
158 | case ORDER_LIST_SUCCESS:
159 | return {
160 | loading: false,
161 | orders: action.payload,
162 | }
163 | case ORDER_LIST_FAIL:
164 | return {
165 | loading: false,
166 | error: action.payload,
167 | }
168 | default:
169 | return state
170 | }
171 | }
172 |
173 | export const orderDeliverReducer = (state = {}, action) => {
174 | switch (action.type) {
175 | case ORDER_DELIVER_REQUEST:
176 | return {
177 | loading: true,
178 | }
179 | case ORDER_DELIVER_SUCCESS:
180 | return {
181 | loading: false,
182 | success: true,
183 | }
184 | case ORDER_DELIVER_FAIL:
185 | return {
186 | loading: false,
187 | error: action.payload,
188 | }
189 | case ORDER_DELIVER_RESET:
190 | return {}
191 | default:
192 | return state
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/backend/controllers/orderControllers.js:
--------------------------------------------------------------------------------
1 | const asyncHandler = require('express-async-handler')
2 | const Order = require('../models/orderModel')
3 | const Product = require('../models/productModel')
4 | const productController = require('./../controllers/productController')
5 |
6 | // @desc Create new order
7 | // @route POST /api/orders
8 | // @access Private
9 | const addOrderItems = asyncHandler(async (req, res) => {
10 | const {
11 | orderItems,
12 | shippingAddress,
13 | paymentMethod,
14 | itemsPrice,
15 | shippingPrice,
16 | totalPrice,
17 | } = req.body
18 | // Product price validation
19 | orderItems.forEach(async (item) => {
20 | let lookupItem = await Product.findById(item.product)
21 | if (parseFloat(item.price) !== lookupItem.price) {
22 | res.status(400)
23 | throw new Error(
24 | 'There is a discrepancy between the prices of the items, and whats in the Database, please try again!',
25 | )
26 | }
27 | })
28 |
29 | if (orderItems && orderItems.length === 0) {
30 | res.status(400)
31 | throw new Error('No order items')
32 | } else {
33 | const order = new Order({
34 | orderItems,
35 | user: req.user._id,
36 | shippingAddress,
37 | paymentMethod,
38 | itemsPrice,
39 | shippingPrice,
40 | totalPrice,
41 | })
42 |
43 | const createdOrder = await order.save()
44 |
45 | res.status(201).json(createdOrder)
46 | }
47 | })
48 |
49 | // @desc Get order by ID
50 | // @route GET /api/orders/:id
51 | // @access Private
52 | const getOrderById = asyncHandler(async (req, res) => {
53 | const order = await Order.findById(req.params.id).populate(
54 | 'user',
55 | 'name email',
56 | )
57 |
58 | if (order && (req.user.isAdmin || order.user._id.equals(req.user._id))) {
59 | res.json(order)
60 | } else {
61 | res.status(404)
62 | throw new Error('Order not found')
63 | }
64 | })
65 |
66 | // @desc Update order to paid
67 | // @route GET /api/orders/:id/pay
68 | // @access Private
69 | // const updateOrderToPaid = asyncHandler(async (req, res) => {
70 | // const order = await Order.findById(req.params.id)
71 |
72 | // if (order) {
73 | // order.isPaid = true
74 | // order.paidAt = Date.now()
75 | // order.paymentResult = {
76 | // id: req.body.id,
77 | // status: req.body.status,
78 | // update_time: req.body.update_time,
79 | // email_address: req.body.payer.email_address,
80 | // }
81 |
82 | // const updatedOrder = await order.save()
83 |
84 | // res.json(updatedOrder)
85 | // } else {
86 | // res.status(404)
87 | // throw new Error('Order not found')
88 | // }
89 | // })
90 |
91 | const updateOrderToPaid = asyncHandler(async (req, res) => {
92 | const order = await Order.findById(req.params.id)
93 |
94 | if (order) {
95 | //below .map() function return an array of promises
96 | const updatePromises = order.orderItems.map(async (item) => {
97 | await productController.updateStockCount(
98 | req,
99 | res,
100 | item.product,
101 | parseInt(item.qty),
102 | )
103 | })
104 | //below function to resolve the array of promises
105 | try {
106 | await Promise.all(updatePromises)
107 | } catch (error) {
108 | return res.status(404).json({
109 | status: 'fail',
110 | message: error.message,
111 | })
112 | }
113 |
114 | order.isPaid = true
115 | order.paidAt = Date.now()
116 |
117 | const updatedOrder = await order.save()
118 | await updatedOrder.populate(
119 | 'user',
120 | 'name email',
121 | )
122 | res.json(updatedOrder)
123 | } else {
124 | res.status(404)
125 | throw new Error('Order not found')
126 | }
127 | })
128 |
129 | // @desc Get logged in user orders
130 | // @route GET /api/orders/myorders
131 | // @access Private
132 | const getMyOrders = asyncHandler(async (req, res) => {
133 | const orders = await Order.find({ user: req.user._id })
134 | res.json(orders)
135 | })
136 |
137 | // @desc Get all orders
138 | // @route GET /api/orders
139 | // @access Private/Admin
140 | const getOrders = asyncHandler(async (req, res) => {
141 | const orders = await Order.find({}).populate('user', 'id name')
142 | res.json(orders)
143 | })
144 |
145 | // @desc Update order to delivered
146 | // @route GET /api/orders/:id/deliver
147 | // @access Private/Admin
148 | const updateOrderToDelivered = asyncHandler(async (req, res) => {
149 | const order = await Order.findById(req.params.id)
150 |
151 | if (order) {
152 | order.isDelivered = true
153 | order.deliveredAt = Date.now()
154 |
155 | const updatedOrder = await order.save()
156 | await updatedOrder.populate(
157 | 'user',
158 | 'name email',
159 | )
160 | res.json(updatedOrder)
161 | } else {
162 | res.status(404)
163 | throw new Error('Order not found')
164 | }
165 | })
166 |
167 | module.exports = {
168 | addOrderItems,
169 | getOrderById,
170 | updateOrderToPaid,
171 | getMyOrders,
172 | getOrders,
173 | updateOrderToDelivered,
174 | }
175 |
--------------------------------------------------------------------------------
/frontend/src/reducers/userReducers.js:
--------------------------------------------------------------------------------
1 | import {
2 | USERS_LIST_FAIL,
3 | USERS_LIST_REQUEST,
4 | USERS_LIST_RESET,
5 | USERS_LIST_SUCCESS,
6 | USER_DELETE_FAIL,
7 | USER_DELETE_REQUEST,
8 | USER_DELETE_SUCCESS,
9 | USER_DETAILS_FAIL,
10 | USER_DETAILS_REQUEST,
11 | USER_DETAILS_RESET,
12 | USER_DETAILS_SUCCESS,
13 | USER_FORGOT_PASSWORD_FAIL,
14 | USER_FORGOT_PASSWORD_REQUEST,
15 | USER_FORGOT_PASSWORD_SUCCESS,
16 | USER_LOGIN_FAIL,
17 | USER_LOGIN_REQUEST,
18 | USER_LOGIN_SUCCESS,
19 | USER_LOGOUT,
20 | USER_REGISTER_FAIL,
21 | USER_REGISTER_REQUEST,
22 | USER_REGISTER_SUCCESS,
23 | USER_RESET_PASSWORD_FAIL,
24 | USER_RESET_PASSWORD_REQUEST,
25 | USER_RESET_PASSWORD_SUCCESS,
26 | USER_UPDATE_FAIL,
27 | USER_UPDATE_PROFILE_FAIL,
28 | USER_UPDATE_PROFILE_REQUEST,
29 | USER_UPDATE_PROFILE_RESET,
30 | USER_UPDATE_PROFILE_SUCCESS,
31 | USER_UPDATE_REQUEST,
32 | USER_UPDATE_RESET,
33 | USER_UPDATE_SUCCESS,
34 | } from '../constants/userConstants'
35 |
36 | export const userLoginReducer = (state = {}, action) => {
37 | switch (action.type) {
38 | case USER_LOGIN_REQUEST:
39 | return { loading: true }
40 | case USER_LOGIN_SUCCESS:
41 | return { loading: false, userInfo: action.payload }
42 | case USER_LOGIN_FAIL:
43 | return { loading: false, error: action.payload }
44 | case USER_LOGOUT:
45 | return {}
46 | default:
47 | return state
48 | }
49 | }
50 |
51 | export const userRegisterReducer = (state = {}, action) => {
52 | switch (action.type) {
53 | case USER_REGISTER_REQUEST:
54 | return { loading: true }
55 | case USER_REGISTER_SUCCESS:
56 | return { loading: false, userInfo: action.payload }
57 | case USER_REGISTER_FAIL:
58 | return { loading: false, error: action.payload }
59 | default:
60 | return state
61 | }
62 | }
63 |
64 | export const userForgotPasswordReducer = (state = {}, action) => {
65 | switch (action.type) {
66 | case USER_FORGOT_PASSWORD_REQUEST:
67 | return { loading: true }
68 | case USER_FORGOT_PASSWORD_SUCCESS:
69 | return { loading: false, message: action.payload.message }
70 | case USER_FORGOT_PASSWORD_FAIL:
71 | return { loading: false, error: action.payload }
72 | default:
73 | return state
74 | }
75 | }
76 |
77 | export const userResetPasswordReducer = (state = {}, action) => {
78 | switch (action.type) {
79 | case USER_RESET_PASSWORD_REQUEST:
80 | return { loading: true }
81 | case USER_RESET_PASSWORD_SUCCESS:
82 | return { loading: false, message: action.payload.message }
83 | case USER_RESET_PASSWORD_FAIL:
84 | return { loading: false, error: action.payload }
85 | default:
86 | return state
87 | }
88 | }
89 |
90 | export const userDetailsReducer = (state = { user: {} }, action) => {
91 | switch (action.type) {
92 | case USER_DETAILS_REQUEST:
93 | return { ...state, loading: true }
94 | case USER_DETAILS_SUCCESS:
95 | return { loading: false, user: action.payload }
96 | case USER_DETAILS_FAIL:
97 | return { loading: false, error: action.payload }
98 | case USER_DETAILS_RESET:
99 | return { user: {} }
100 | default:
101 | return state
102 | }
103 | }
104 |
105 | export const userUpdateProfileReducer = (state = {}, action) => {
106 | switch (action.type) {
107 | case USER_UPDATE_PROFILE_REQUEST:
108 | return { loading: true }
109 | case USER_UPDATE_PROFILE_SUCCESS:
110 | return { loading: false, success: true, userInfo: action.payload }
111 | case USER_UPDATE_PROFILE_FAIL:
112 | return { loading: false, error: action.payload }
113 | case USER_UPDATE_PROFILE_RESET:
114 | return {}
115 | default:
116 | return state
117 | }
118 | }
119 | // admin only
120 |
121 | export const usersListReducer = (state = { users: [] }, action) => {
122 | switch (action.type) {
123 | case USERS_LIST_REQUEST:
124 | return { loading: true }
125 | case USERS_LIST_SUCCESS:
126 | return { loading: false, users: action.payload }
127 | case USERS_LIST_FAIL:
128 | return { loading: false, error: action.payload }
129 | case USERS_LIST_RESET:
130 | return { users: [] }
131 | default:
132 | return state
133 | }
134 | }
135 |
136 | export const userDeleteReducer = (state = {}, action) => {
137 | switch (action.type) {
138 | case USER_DELETE_REQUEST:
139 | return { loading: true }
140 | case USER_DELETE_SUCCESS:
141 | return { loading: false, success: true }
142 | case USER_DELETE_FAIL:
143 | return { loading: false, error: action.payload }
144 | default:
145 | return state
146 | }
147 | }
148 |
149 | export const userUpdateReducer = (state = { user: {} }, action) => {
150 | switch (action.type) {
151 | case USER_UPDATE_REQUEST:
152 | return { loading: true }
153 | case USER_UPDATE_SUCCESS:
154 | return { loading: false, success: true }
155 | case USER_UPDATE_FAIL:
156 | return { loading: false, error: action.payload }
157 | case USER_UPDATE_RESET:
158 | return {
159 | user: {},
160 | }
161 | default:
162 | return state
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/frontend/src/actions/orderActions.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import {
3 | ORDER_CREATE_FAIL,
4 | ORDER_CREATE_REQUEST,
5 | ORDER_CREATE_SUCCESS,
6 | ORDER_DELIVER_FAIL,
7 | ORDER_DELIVER_REQUEST,
8 | ORDER_DELIVER_SUCCESS,
9 | ORDER_DETAILS_FAIL,
10 | ORDER_DETAILS_REQUEST,
11 | ORDER_DETAILS_SUCCESS,
12 | ORDER_LIST_FAIL,
13 | ORDER_LIST_MY_FAIL,
14 | ORDER_LIST_MY_REQUEST,
15 | ORDER_LIST_MY_SUCCESS,
16 | ORDER_LIST_REQUEST,
17 | ORDER_LIST_SUCCESS,
18 | ORDER_PAY_FAIL,
19 | ORDER_PAY_REQUEST,
20 | ORDER_PAY_SUCCESS,
21 | } from '../constants/orderConstants'
22 | import { logout } from './userActions'
23 |
24 | export const createOrder = (order) => async (dispatch, getState) => {
25 | try {
26 | dispatch({
27 | type: ORDER_CREATE_REQUEST,
28 | })
29 |
30 | const config = {
31 | headers: {
32 | 'Content-Type': 'application/json',
33 | },
34 | }
35 |
36 | const { data } = await axios.post(`/api/orders`, order, config)
37 |
38 | dispatch({
39 | type: ORDER_CREATE_SUCCESS,
40 | payload: data,
41 | })
42 | // dispatch({
43 | // type: CART_CLEAR_ITEMS,
44 | // payload: data,
45 | // })
46 | localStorage.removeItem('cartItems')
47 | } catch (error) {
48 | const message =
49 | error.response && error.response.data.message
50 | ? error.response.data.message
51 | : error.message
52 | if (message === 'Not authorized, token failed') {
53 | dispatch(logout())
54 | }
55 | dispatch({
56 | type: ORDER_CREATE_FAIL,
57 | payload: message,
58 | })
59 | }
60 | }
61 |
62 | export const getOrderDetails = (id) => async (dispatch, getState) => {
63 | try {
64 | dispatch({
65 | type: ORDER_DETAILS_REQUEST,
66 | })
67 |
68 | const { data } = await axios.get(`/api/orders/${id}`)
69 |
70 | dispatch({
71 | type: ORDER_DETAILS_SUCCESS,
72 | payload: data,
73 | })
74 | } catch (error) {
75 | const message =
76 | error.response && error.response.data.message
77 | ? error.response.data.message
78 | : error.message
79 | if (message === 'Not authorized, token failed') {
80 | dispatch(logout())
81 | }
82 | dispatch({
83 | type: ORDER_DETAILS_FAIL,
84 | payload: message,
85 | })
86 | }
87 | }
88 |
89 | export const listMyOrders = () => async (dispatch, getState) => {
90 | try {
91 | dispatch({
92 | type: ORDER_LIST_MY_REQUEST,
93 | })
94 |
95 | const { data } = await axios.get(`/api/orders/myorders`)
96 |
97 | dispatch({
98 | type: ORDER_LIST_MY_SUCCESS,
99 | payload: data,
100 | })
101 | } catch (error) {
102 | const message =
103 | error.response && error.response.data.message
104 | ? error.response.data.message
105 | : error.message
106 | if (message === 'Not authorized, token failed') {
107 | dispatch(logout())
108 | }
109 | dispatch({
110 | type: ORDER_LIST_MY_FAIL,
111 | payload: message,
112 | })
113 | }
114 | }
115 |
116 | export const listOrders = () => async (dispatch, getState) => {
117 | try {
118 | dispatch({
119 | type: ORDER_LIST_REQUEST,
120 | })
121 |
122 | const { data } = await axios.get(`/api/orders`)
123 |
124 | dispatch({
125 | type: ORDER_LIST_SUCCESS,
126 | payload: data,
127 | })
128 | } catch (error) {
129 | const message =
130 | error.response && error.response.data.message
131 | ? error.response.data.message
132 | : error.message
133 | if (message === 'Not authorized, token failed') {
134 | dispatch(logout())
135 | }
136 | dispatch({
137 | type: ORDER_LIST_FAIL,
138 | payload: message,
139 | })
140 | }
141 | }
142 |
143 | export const deliverOrder = (order) => async (dispatch, getState) => {
144 | try {
145 | dispatch({
146 | type: ORDER_DELIVER_REQUEST,
147 | })
148 |
149 | const { data } = await axios.put(`/api/orders/${order._id}/deliver`, {})
150 |
151 | dispatch({
152 | type: ORDER_DELIVER_SUCCESS,
153 | payload: data,
154 | })
155 | } catch (error) {
156 | const message =
157 | error.response && error.response.data.message
158 | ? error.response.data.message
159 | : error.message
160 | if (message === 'Not authorized, token failed') {
161 | dispatch(logout())
162 | }
163 | dispatch({
164 | type: ORDER_DELIVER_FAIL,
165 | payload: message,
166 | })
167 | }
168 | }
169 |
170 | export const payOrder = (order) => async (dispatch, getState) => {
171 | try {
172 | dispatch({
173 | type: ORDER_PAY_REQUEST,
174 | })
175 |
176 | const { data } = await axios.put(`/api/orders/${order._id}/pay`, {})
177 |
178 | dispatch({
179 | type: ORDER_PAY_SUCCESS,
180 | payload: data,
181 | })
182 | } catch (error) {
183 | const message =
184 | error.response && error.response.data.message
185 | ? error.response.data.message
186 | : error.message
187 | if (message === 'Not authorized, token failed') {
188 | dispatch(logout())
189 | }
190 | dispatch({
191 | type: ORDER_PAY_FAIL,
192 | payload: message,
193 | })
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=League+Spartan:wght@600&display=swap');
2 |
3 | :root {
4 | --bs-blue: #3459e6;
5 | --bs-indigo: #6610f2;
6 | --bs-purple: #6f42c1;
7 | --bs-pink: #d63384;
8 | --bs-red: #da292e;
9 | --bs-orange: #f8765f;
10 | --bs-yellow: #f4bd61;
11 | --bs-green-hover: #53b175;
12 | --bs-green: #34a853;
13 | --bs-teal: #20c997;
14 | --bs-cyan: #287bb5;
15 | --bs-white: #fff;
16 | --bs-gray: #6c757d;
17 | --bs-gray-dark: #343a40;
18 | --bs-gray-100: #f8f9fa;
19 | --bs-gray-200: #e9ecef;
20 | --bs-gray-300: #dee2e6;
21 | --bs-gray-400: #ced4da;
22 | --bs-gray-500: #adb5bd;
23 | --bs-gray-600: #6c757d;
24 | --bs-gray-700: #495057;
25 | --bs-gray-800: #343a40;
26 | --bs-gray-900: #212529;
27 | --bs-primary: #3459e6;
28 | --bs-secondary: #fff;
29 | --bs-success: #2fb380;
30 | --bs-info: #287bb5;
31 | --bs-warning: #f4bd61;
32 | --bs-danger: #da292e;
33 | --bs-light: #f8f9fa;
34 | --bs-dark: #212529;
35 | --bs-primary-rgb: 52, 89, 230;
36 | --bs-secondary-rgb: 255, 255, 255;
37 | --bs-success-rgb: 47, 179, 128;
38 | --bs-info-rgb: 40, 123, 181;
39 | --bs-warning-rgb: 244, 189, 97;
40 | --bs-danger-rgb: 218, 41, 46;
41 | --bs-light-rgb: 248, 249, 250;
42 | --bs-dark-rgb: 33, 37, 41;
43 | --bs-white-rgb: 255, 255, 255;
44 | --bs-black-rgb: 0, 0, 0;
45 | --bs-body-color-rgb: 73, 80, 87;
46 | --bs-body-bg-rgb: 255, 255, 255;
47 | --bs-font-sans-serif: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI',
48 | Roboto, 'Helvetica Neue', Arial, sans-serif, 'Apple Color Emoji',
49 | 'Segoe UI Emoji', 'Segoe UI Symbol';
50 | --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas,
51 | 'Liberation Mono', 'Courier New', monospace;
52 | --bs-gradient: linear-gradient(
53 | 180deg,
54 | rgba(255, 255, 255, 0.15),
55 | rgba(255, 255, 255, 0)
56 | );
57 | --bs-body-font-family: var(--bs-font-sans-serif);
58 | --bs-body-font-size: 1rem;
59 | --bs-body-font-weight: 400;
60 | --bs-body-line-height: 1.5;
61 | --bs-body-color: #495057;
62 | --bs-body-bg: #fff;
63 | }
64 |
65 | body {
66 | overflow-y: overlay;
67 | }
68 |
69 | main {
70 | min-height: 80vh;
71 | }
72 |
73 | .small {
74 | font-size: 0.825rem;
75 | }
76 |
77 | a {
78 | color: var(--bs-gray-700);
79 | }
80 |
81 | a:hover {
82 | color: var(--bs-green);
83 | }
84 |
85 | .text-link {
86 | color: var(--bs-gray-700);
87 | text-decoration: underline !important;
88 | }
89 |
90 | .text-link:hover {
91 | color: var(--bs-green);
92 | text-decoration: underline !important;
93 | }
94 |
95 | .btn-primary {
96 | background-color: var(--bs-green);
97 | border-color: var(--bs-green);
98 | }
99 |
100 | .btn-primary:hover {
101 | color: var(--bs-secondary);
102 | background-color: var(--bs-green-hover);
103 | border-color: var(--bs-green-hover);
104 | }
105 |
106 | .btn-primary.disabled,
107 | .btn-primary:disabled {
108 | color: var(--bs-secondary);
109 | background-color: var(--bs-green);
110 | border-color: var(--bs-green);
111 | }
112 |
113 | .btn-check:active + .btn-primary,
114 | .btn-check:checked + .btn-primary,
115 | .btn-primary.active,
116 | .btn-primary:active,
117 | .show > .btn-primary.dropdown-toggle {
118 | color: #fff;
119 | background-color: var(--bs-green);
120 | border-color: var(--bs-green);
121 | }
122 |
123 | .form-check-input:checked {
124 | background-color: var(--bs-green);
125 | border-color: var(--bs-green);
126 | }
127 |
128 | .form-control:focus {
129 | border-color: var(--bs-green);
130 | box-shadow: 0 0 0 0.2rem rgba(52, 168, 83, 0.25);
131 | }
132 |
133 | .btn-check:focus + .btn-primary,
134 | .btn-primary:focus {
135 | color: var(--bs-secondary);
136 | background-color: var(--bs-green);
137 | border-color: var(--bs-green);
138 | }
139 | .btn-check:focus + .btn,
140 | .btn:focus {
141 | outline: 0;
142 | box-shadow: 0 1px 2px rgb(0 0 0 / 5%);
143 | }
144 |
145 | .text-primary {
146 | --bs-text-opacity: 1;
147 | color: rgba(var(--bs-success-rgb), var(--bs-text-opacity)) !important;
148 | }
149 |
150 | .dropdown-item.active,
151 | .dropdown-item:active,
152 | .dropdown-item:focus,
153 | .dropdown-item:hover {
154 | color: var(--bs-secondary);
155 | background-color: var(--bs-green);
156 | }
157 |
158 | h3 {
159 | padding: 1rem 0;
160 | }
161 |
162 | h1 {
163 | font-size: 1.5rem;
164 | padding: 1rem 0;
165 | }
166 |
167 | h2 {
168 | font-size: 1rem;
169 | padding: 0.5rem 0;
170 | }
171 |
172 | .nav-right {
173 | display: flex;
174 | flex-direction: row;
175 | gap: 1rem;
176 | margin-left: 1rem;
177 | }
178 |
179 | .cart-circle {
180 | position: relative;
181 | font-size: 0.375rem;
182 | color: red;
183 | top: -0.5rem;
184 | left: -0.2rem;
185 | }
186 |
187 | #logo-text {
188 | color: var(--bs-green);
189 | font-size: x-large;
190 | font-family: 'League Spartan', sans-serif;
191 | }
192 |
193 | ul {
194 | margin-bottom: 0.25rem;
195 | }
196 |
197 | .li-separator > li:not(:last-child)::after {
198 | content: '|';
199 | margin-left: 0.25rem;
200 | }
201 |
202 | @media only screen and (min-width: 768px) and (max-width: 1200px) {
203 | .product-page-section {
204 | min-width: 350px;
205 | }
206 |
207 | .product-page-section:nth-child(3) {
208 | margin: 20px 0 0 50%;
209 | }
210 | }
211 |
212 | @media (max-width: 900px) {
213 | .carousel-caption h2 {
214 | font-size: 2.5vw;
215 | }
216 | }
217 |
--------------------------------------------------------------------------------
/frontend/src/screens/ShippingScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { Form, Button } from 'react-bootstrap'
3 | import { useDispatch, useSelector } from 'react-redux'
4 | import FormContainer from '../components/FormContainer'
5 | import { saveShippingAddress } from '../actions/cartActions'
6 | import CheckoutSteps from '../components/CheckoutSteps'
7 | import { locations } from '../data/LocationsAvailable' // Locations is an Array of objects, which includes a hierarchy of countries > States > cities
8 |
9 | const ShippingScreen = ({ history }) => {
10 | const cart = useSelector((state) => state.cart)
11 | const { shippingAddress } = cart
12 |
13 | // the Ternary operator in useState Hook is used because Firs time the User came and it does not have any Cart Address available, So to Prevent this Error !! A component is changing an uncontrolled input to be controlled. !! we uses the Ternary
14 | const [address, setAddress] = useState(
15 | shippingAddress.address && shippingAddress.address
16 | ? shippingAddress.address
17 | : '',
18 | )
19 | const [pinCode, setPinCode] = useState(
20 | shippingAddress.pinCode && shippingAddress.pinCode
21 | ? shippingAddress.pinCode
22 | : '',
23 | )
24 | const [city, setCity] = useState(
25 | shippingAddress.city && shippingAddress.city ? shippingAddress.city : '',
26 | )
27 | const [state, setState] = useState(
28 | shippingAddress.state && shippingAddress.state ? shippingAddress.state : '',
29 | )
30 | const [country, setCountry] = useState(
31 | shippingAddress.country && shippingAddress.country
32 | ? shippingAddress.country
33 | : '',
34 | )
35 |
36 | // Once we Receive the Country we will find the State from our Country array in Locations Object and find the the States Available in it.
37 | const statesAvl = locations.countries.find(
38 | ({ countryName }) => countryName === country,
39 | )
40 | // Once we Receive the StatesAvl we will find the Cities from our statesAvl object and then iterate through the Cities in it.
41 | const citiesAvl = statesAvl?.states.find(
42 | ({ stateName }) => stateName === state,
43 | )
44 |
45 | const dispatch = useDispatch()
46 |
47 | const submitHandler = (e) => {
48 | e.preventDefault()
49 | dispatch(saveShippingAddress({ address, city, pinCode, state, country }))
50 | history.push('/payment')
51 | }
52 |
53 | return (
54 |
55 | Shipping
56 |
57 |
59 | Address
60 | setAddress(e.target.value)}
66 | >
67 |
68 |
69 |
70 | Country
71 | setCountry(e.target.value)}
77 | >
78 | {'Select Country...'}
79 | {locations.countries.map((value, key) => {
80 | return (
81 |
82 | {value.countryName}
83 |
84 | )
85 | })}
86 |
87 |
88 |
89 |
90 | State
91 | setState(e.target.value)}
97 | >
98 | {'Select State...'}
99 | {statesAvl?.states.map((value, key) => {
100 | return (
101 |
102 | {value.stateName}
103 |
104 | )
105 | })}
106 |
107 |
108 |
109 |
110 | City
111 | setCity(e.target.value)}
117 | >
118 | {'Select City...'}
119 | {citiesAvl?.cities.map((value, key) => {
120 | return (
121 |
122 | {value}
123 |
124 | )
125 | })}
126 |
127 |
128 |
129 |
130 | Pin Code
131 | setPinCode(e.target.value)}
137 | >
138 |
139 |
140 |
141 | Continue
142 |
143 |
144 |
145 | )
146 | }
147 |
148 | export default ShippingScreen
149 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | roopeshsaravanan.dev@gmail.com.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Freshcomm
2 |
3 | An open-source and self-hostable ecommerce application with a focus on developer productivity and ease of customization.
4 |
5 | 
6 |
7 | ## Project Purpose!
8 |
9 | I am a huge enthusiast of open source. Contributing to open source is a fantastic way to learn and grow. Making open source contributions will require you to have been exposed to a few important concepts, practices, and transferable skills:
10 |
11 | - Version control
12 | - Working with tickets & issues
13 | - Working with other developers
14 | - Creating pull requests
15 | - Experiencing a code review process
16 | - Setting up a local development environment
17 | - Contributing code to a pre-existing codebase
18 |
19 | ## General Idea
20 |
21 | Freshcomm is an open-source and self-hostable ecommerce application with a focus on developer productivity and ease of customization.
22 |
23 | ## Current State
24 |
25 | As of now, Freshcomm is built using MERN stack. Additionally, [React Bootstrap](https://react-bootstrap.github.io/) along with [Bootswatch Zephyr](https://bootswatch.com/zephyr/) theme is used for styling and [React Redux](https://react-redux.js.org/) is used for state management.
26 |
27 | ## Getting Started
28 |
29 | ### Setting up the repository
30 |
31 | Fork the repository at - [roopeshsn/freshcomm](https://github.com/roopeshsn/freshcomm) to your GitHub account.
32 |
33 | Then clone the forked repository, by typing the following line in your local terminal/powershell. Remember to replace `` with your actual GitHub username.
34 |
35 | ```bash
36 | git clone https://github.com//freshcomm.git
37 | ```
38 |
39 | Navigate to the cloned repository on your local system
40 |
41 | ```bash
42 | cd freshcomm
43 | ```
44 |
45 | Add remotes to the parent repository. This will help you fetch the code from the
46 | parent repo to avoid any merge conflicts later.
47 |
48 | ```bash
49 | git remote add upstream https://github.com/roopeshsn/freshcomm.git
50 | ```
51 |
52 | To verify, use the command `git remote -v` to check if you have two remotes - origin and upstream set up.
53 |
54 | Finally, fetch the upstream's latest code from the main branch.
55 |
56 | ```bash
57 | git fetch upstream master
58 | ```
59 |
60 | ### Env Variables
61 |
62 | Create a .env file in then root and add the following
63 |
64 | ```
65 | NODE_ENV = development
66 | PORT = 5000
67 | MONGODB_URI =
68 | JWT_SECRET =
69 | EMAIL_USERNAME =
70 | EMAIL_PASSWORD =
71 | EMAIL_HOST =
72 | EMAIL_PORT =
73 | CLOUDINARY_CLOUD_NAME =
74 | CLOUDINARY_API_KEY =
75 | CLOUDINARY_API_SECRET =
76 | ```
77 |
78 | ### Add data in .env file
79 |
80 | The backend folder in this repository serves as the codebase for the API server which connects to a MongoDB instance to store and retrieve user, products, orders data.
81 |
82 | #### MONGODB_URI
83 |
84 | MongoDB version = 5.0.12. You can either spin up a local/Docker instance or can use MongoDB Atlas (Recommended).
85 |
86 | Here's the similar URI which can be used to connect with the cluster
87 | `mongodb+srv://admin:@cluster0.p2u97.mongodb.net/freshcomm?retryWrites=true&w=majority`
88 |
89 | Replace with the password for the admin user.
90 |
91 | #### JWT_SECRET
92 |
93 | Add any string like YOUR_NAME_ANY_SIGN (eg. chrismathew7) is used to create a private key which will authenticate.
94 |
95 | #### EMAIL_USERNAME, EMAIL_PASSWORD, EMAIL_HOST, EMAIL_PORT
96 |
97 | To get this credentials you need a [Mailtrap](https://mailtrap.io) account. Mailtrap is an Email testing tool. We can simulate this for the forget and reset password functions.
98 |
99 | Select Nodemailer from integrations. You will get credentials similar to this,
100 |
101 | ```
102 | var transport = nodemailer.createTransport({
103 | host: "smtp.mailtrap.io",
104 | port: 2525,
105 | auth: {
106 | user: "99195eec06f5",
107 | pass: "d26fe4c7d762"
108 | }
109 | });
110 | ```
111 |
112 | #### CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, CLOUDINARY_API_SECRET
113 | Product images are hosted in Cloudinary. Create an account in Cloudinary and add appopriate credentials in `.env` file.
114 |
115 | ### Install Dependencies (frontend & backend)
116 |
117 | ```
118 | npm install
119 | cd frontend
120 | npm install
121 | ```
122 |
123 | ### Run
124 |
125 | ```
126 | # Run frontend (:3000) & backend (:5000)
127 | npm run dev
128 |
129 | # Run backend only
130 | npm run server
131 | ```
132 |
133 | Server should be running on `PORT` specified in env file (or `5000` by default)
134 |
135 | To test, type `localhost:PORT/` in your browser and following output should appear.
136 |
137 | ```
138 | {
139 | message: 'Welcome to Freshcomm Backend',
140 | version: process.env.VERSION,
141 | license: 'MIT',
142 | }
143 | ```
144 |
145 | ## Mail Service
146 |
147 | [Mailtrap](https://www.example.com) (A Email Sandbox Service) is used as an email inbox to reset passwords.
148 |
149 | ## Image Service
150 |
151 | The Images for product, slide, category are manually hosted in [Cloudinary](https://cloudinary.com/)
152 |
153 | ## Build & Deploy
154 |
155 | The project is deployed on Heroku
156 |
157 | ```
158 | git push heroku master
159 | ```
160 |
161 | The above command is used to deploy the project on Heroku
162 |
163 | ## Seed Database
164 |
165 | You can use the following commands to seed the database with some sample users and products as well as destroy all data:
166 |
167 | ```
168 | # Import data
169 | npm run data:import
170 |
171 | # Destroy data
172 | npm run data:destroy
173 | ```
174 |
175 | ## Contributing
176 |
177 | Please check out [CONTRIBUTING.md](CONTRIBUTING.md) for more information regarding how to contribute.
178 |
179 | ## License
180 |
181 | MIT, see [LICENSE](LICENSE)
182 |
--------------------------------------------------------------------------------
/frontend/src/screens/ProductScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { useDispatch, useSelector } from 'react-redux'
3 | import { LinkContainer } from 'react-router-bootstrap'
4 | import {
5 | Row,
6 | Col,
7 | Image,
8 | ListGroup,
9 | Card,
10 | Button,
11 | Breadcrumb,
12 | Form,
13 | } from 'react-bootstrap'
14 | import calculateDiscount from '../utils/calculateDiscount'
15 | import { listProductDetails } from '../actions/productActions'
16 | import Loader from '../components/Loader'
17 | import Message from '../components/Message'
18 | import formatter from '../utils/currencyFormatter'
19 | import { addToCart } from '../actions/cartActions'
20 | import capitalizeFirstLetter from '../utils/capitalizeFirstLetter'
21 |
22 | const ProductScreen = ({ match, history }) => {
23 | const dispatch = useDispatch()
24 | useEffect(() => {
25 | dispatch(listProductDetails(match.params.id))
26 | // const fetchProduct = async () => {
27 | // const { data } = await axios.get(`/api/products/${match.params.id}`)
28 | // setProduct(data)
29 | // }
30 | // fetchProduct()
31 | }, [dispatch, match])
32 |
33 | const [qty, setQty] = useState(1)
34 | const productDetails = useSelector((state) => state.productDetails)
35 | const { loading, product, error } = productDetails
36 | const categoryBreadcrumb = capitalizeFirstLetter(product.category)
37 |
38 | const addToCartHandler = () => {
39 | // history.push(`/cart/${match.params.id}?qty=${qty}`)
40 | dispatch(addToCart(product._id, parseInt(qty)))
41 | history.push('/cart')
42 | }
43 |
44 | const [discountPrice, discountPercentage] = calculateDiscount(
45 | product.mrp,
46 | product.price,
47 | )
48 |
49 | return (
50 | <>
51 |
52 |
53 | Home
54 |
55 |
56 | {categoryBreadcrumb}
57 |
58 | {product.name}
59 |
60 | {loading ? (
61 |
62 | ) : error ? (
63 | {error}
64 | ) : (
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | {product.name}
73 |
74 | Price: {formatter(product.price)}
75 |
76 | Description: {product.description}
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | Price:{' '}
87 | {formatter(product.price)} {' '}
88 |
89 | M.R.P:
90 |
91 | {formatter(product.mrp)}
92 |
93 |
94 |
95 | You save: {formatter(discountPrice)}
96 |
97 | ({discountPercentage}% off)
98 |
99 |
100 |
101 |
102 |
103 |
104 | Status:
105 | {product.countInStock > 0 ? (
106 |
107 | In Stock
108 |
109 | ) : (
110 |
111 | Out of Stock
112 |
113 | )}
114 |
115 |
116 |
117 |
118 | {product.countInStock > 0 && (
119 |
120 |
121 | Qty
122 |
123 | setQty(parseInt(e.target.value))}
127 | >
128 | {[...Array(product.countInStock).keys()].map((x) => (
129 |
130 | {x + 1} Kg
131 |
132 | ))}
133 |
134 |
135 |
136 |
137 | )}
138 |
139 |
140 |
141 |
147 | Add to Cart
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 | )}
156 | >
157 | )
158 | }
159 |
160 | export default ProductScreen
161 |
--------------------------------------------------------------------------------
/frontend/src/screens/CartScreen.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link } from 'react-router-dom'
3 | import { useDispatch, useSelector } from 'react-redux'
4 | import { Row, Col, ListGroup, Form, Button, Card } from 'react-bootstrap'
5 | import Message from '../components/Message'
6 | import { addToCart, removeFromCart } from '../actions/cartActions'
7 | import formatter from '../utils/currencyFormatter'
8 | import { TiTick } from 'react-icons/ti'
9 | import { AiFillInfoCircle } from 'react-icons/ai'
10 | import {
11 | freeDeliveryCutoff,
12 | deliveryCharge,
13 | } from '../constants/deliveryChargeConstants'
14 |
15 | const CartScreen = ({ match, location, history }) => {
16 | // const productId = match.params.id
17 |
18 | // const qty = location.search ? Number(location.search.split('=')[1]) : 1
19 |
20 | const dispatch = useDispatch()
21 |
22 | const cart = useSelector((state) => state.cart)
23 | const { cartItems } = cart
24 |
25 | const cartItemsPrice = cartItems.reduce(
26 | (acc, item) => acc + parseInt(item.qty) * parseInt(item.price),
27 | 0,
28 | )
29 | const shippingPrice =
30 | cartItemsPrice >= freeDeliveryCutoff ? 0 : deliveryCharge
31 |
32 | // useEffect(() => {
33 | // if (productId) {
34 | // dispatch(addToCart(productId, qty))
35 | // }
36 | // }, [dispatch, productId, qty])
37 |
38 | const removeFromCartHandler = (id) => {
39 | dispatch(removeFromCart(id))
40 | history.push('/cart')
41 | }
42 |
43 | const checkoutHandler = () => {
44 | history.push('/login?redirect=shipping')
45 | }
46 |
47 | return (
48 |
49 | Shopping Cart
50 | {cartItems.length === 0 ? (
51 |
52 |
53 | Your cart is empty Go Back
54 |
55 |
56 | ) : (
57 |
58 | {cartItems.map((item) => (
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | {item.name}
71 |
72 |
73 |
74 | {formatter(item.price)}
75 |
76 |
77 |
78 |
79 |
84 | dispatch(
85 | addToCart(item.product, Number(e.target.value)),
86 | )
87 | }
88 | >
89 | {[...Array(item.countInStock).keys()].map((x) => (
90 |
91 | {x + 1} Kg
92 |
93 | ))}
94 |
95 |
96 |
97 | removeFromCartHandler(item.product)}
102 | >
103 | Delete
104 |
105 |
106 |
107 |
108 |
109 |
110 | ))}
111 |
112 | )}
113 |
114 |
115 |
116 |
117 |
118 |
119 | Subtotal (
120 | {cartItems.reduce((acc, item) => acc + parseInt(item.qty), 0)})
121 | items
122 |
123 | {formatter(
124 | cartItems.reduce((acc, item) => acc + item.qty * item.price, 0),
125 | )}
126 |
127 | {cartItems.length !== 0 && (
128 | <>
129 |
130 |
131 | Shipping
132 | {formatter(shippingPrice)}
133 |
134 |
135 |
136 | {cartItemsPrice < freeDeliveryCutoff ? (
137 | <>
138 |
139 |
140 | Add {formatter(freeDeliveryCutoff - cartItemsPrice)} of
141 | eligible items to your order for FREE delivery.
142 |
143 | >
144 | ) : (
145 | <>
146 |
147 |
148 | You are eligible for free delivery!
149 |
150 | >
151 | )}
152 |
153 | >
154 | )}
155 |
156 |
162 | Proceed To Checkout
163 |
164 |
165 |
166 |
167 |
168 |
169 | )
170 | }
171 |
172 | export default CartScreen
173 |
--------------------------------------------------------------------------------
/backend/data/products.js:
--------------------------------------------------------------------------------
1 | const products = [
2 | {
3 | name: 'Onion',
4 | imageSrc: 'https://i.ibb.co/QHwhBDK/onion.jpg',
5 | description:
6 | 'Onions are known to be rich in biotin. Most of the flavonoids which are known as anti-oxidants are concentrated more in the outer layers, so when you peel off the layers, you should remove as little as possible. Onion can fill your kitchen with a thick spicy aroma. It is a common base vegetable in most Indian dishes, thanks to the wonderful flavor that it adds to any dish. Onions are high in sulphur, vitamin B6 and B9. It has high quantities of water and naturally low in fat. It is high in phytochemical compounds.',
7 | imageAlt: 'onion image',
8 | category: 'vegetables',
9 | price: 99,
10 | mrp: 150,
11 | countInStock: 10,
12 | },
13 | {
14 | name: 'Tomato - Local',
15 | imageSrc: 'https://i.ibb.co/NrCsfm2/tomato-local.jpg',
16 | description:
17 | 'Local tomatoes are partly sour and partly sweet and contain many seeds inside which are edible. The red colour present in tomatoes is due to lycopene, an anti-oxidant. Tomatoes contain Vitamin C,K. lycopene, an antioxidant that reduces the risk of cancer and heart-diseases. They protect the eyes from light induced damage. Essential for pregnant women as these tomatoes protect infants against neural tube defects.',
18 | imageAlt: 'tomato local image',
19 | category: 'vegetables',
20 | price: 39,
21 | mrp: 52,
22 | countInStock: 7,
23 | },
24 | {
25 | name: 'Tomato - Hybrid',
26 | imageSrc: 'https://i.ibb.co/bFYxc86/tomato-naveen.jpg',
27 | description:
28 | 'Tomato Hybrids are high-quality fruits compared to desi, local tomatoes. They contain numerous edible seeds and are red in colour due to lycopene, an anti-oxidant. Tomatoes contain Vitamin C,K. lycopene, an antioxidant that reduces the risk of cancer and heart-diseases. They protect the eyes from light induced damage. Essential for pregnant women as these tomatoes protect infants against neural tube defects.',
29 | imageAlt: 'tomato image',
30 | category: 'vegetables',
31 | price: 37,
32 | mrp: 61,
33 | countInStock: 7,
34 | },
35 | {
36 | name: 'Carrot',
37 | imageSrc: 'https://i.ibb.co/Dg7yts3/carrot.jpg',
38 | description:
39 | 'A popular sweet-tasting root vegetable, Carrots are narrow and cone shaped. They have thick, fleshy, deeply colored root, which grows underground, and feathery green leaves that emerge above the ground. FreshBey brings you the flavour and richness of the finest crispy and juicy carrots that are locally grown and the best of the region.',
40 | imageAlt: 'carrot image',
41 | category: 'vegetables',
42 | price: 39,
43 | mrp: 52,
44 | countInStock: 5,
45 | },
46 | {
47 | name: 'Apple',
48 | imageSrc: 'https://i.ibb.co/8KHq7cC/apple.jpg',
49 | description:
50 | 'Considered as most commonly grown apples in India, Shimla apples have a light red skin, juicy and crunchy flesh. We source the best apples with residue and wax free peel from the trusted growers. Apples are one of the healthiest fruits. Rich in vitamin C and dietary fiber which keep our digestive and immune system healthy. Protects from Alzheimers, type 2 diabetes and cancer. It is a natural teeth whitener and prevent bad breath. Eating apple peel lowers the risk of obesity. Apple mask is an excellent cure for wrinkles.',
51 | imageAlt: 'apple image',
52 | category: 'fruits',
53 | price: 72,
54 | mrp: 135,
55 | countInStock: 11,
56 | },
57 | {
58 | name: 'Banana - Yelaki',
59 | imageSrc: 'https://i.ibb.co/x1pHbfW/banana-yelaki.jpg',
60 | description:
61 | 'Get a better handle on your games with this Logitech LIGHTSYNC gaming mouse. The six programmable buttons allow customization for a smooth playing experience',
62 | imageAlt: 'mango image',
63 | category: 'seasonal',
64 | price: 66,
65 | mrp: 82,
66 | countInStock: 7,
67 | },
68 | {
69 | name: 'Pomegranate',
70 | imageSrc: 'https://i.ibb.co/LkjG3HZ/pomegranate.jpg',
71 | description:
72 | 'With ruby color and an intense floral, sweet-tart flavor, the pomegranate delivers both taste and beauty. You can remove the skin and the membranes to get at the delicious fruit with nutty seeds. FreshBey Pomegranates are finely sorted and graded to deliver the best tasting pomegranates to you.',
73 | imageAlt: 'pomegranate image',
74 | category: 'fruits',
75 | price: 156,
76 | mrp: 196,
77 | countInStock: 5,
78 | },
79 | {
80 | name: 'Guava',
81 | imageSrc: 'https://i.ibb.co/hd2P4bC/guava.jpg',
82 | description:
83 | 'Savour the green guavas along with hard, pale yellow edible seeds. The off-white flesh is crunchy and mildly sweet with very good fragrance. Guavas reduce the risk of diabetes and regulate blood pressure levels. They are rich in vitamin A, C, folate, fiber, lycopene and other essential minerals. Help in improving eye health.',
84 | imageAlt: 'Guava image',
85 | category: 'fruits',
86 | price: 48,
87 | mrp: 60,
88 | countInStock: 5,
89 | },
90 | {
91 | name: 'Potato',
92 | imageSrc: 'https://i.ibb.co/LnTkwPL/potato.jpg',
93 | description:
94 | 'FreshBey Potatoes are nutrient-dense, non-fattening and have reasonable amount of calories. Include them in your regular meals so that the body receives a good supply of carbohydrates, dietary fibers and essential minerals such as copper, magnesium, and iron. In India, potatoes are probably the second-most consumed vegetables after onions. Consumption of potatoes helps to maintain the blood glucose level and keeps the brain alert and active.',
95 | imageAlt: 'potato image',
96 | category: 'vegetables',
97 | price: 26,
98 | mrp: 35,
99 | countInStock: 5,
100 | },
101 | {
102 | name: 'Mushroom',
103 | imageSrc: 'https://i.ibb.co/h1LGnH0/mushroom-freshbey.webp',
104 | description:
105 | 'Buttom mushrooms are very small sized mushrooms with smooth round caps and short stems. They have a mild flavour with a good texture that becomes more fragrant and meaty when cooked.',
106 | imageAlt: 'mushroom image',
107 | category: 'exotic',
108 | price: 49,
109 | mrp: 99,
110 | countInStock: 21,
111 | },
112 | {
113 | name: 'Moong Green',
114 | imageSrc: 'https://i.ibb.co/6HXts8S/sprouts-freshbey.webp',
115 | description:
116 | 'Bean sprouts are the young shoots of the mung bean with petite pale yellow to pale green leaves attached on a silvery bright white shoot.',
117 | imageAlt: 'moong green image',
118 | category: 'sprouts',
119 | price: 24,
120 | mrp: 50,
121 | countInStock: 11,
122 | },
123 | ]
124 |
125 | module.exports = products
126 |
--------------------------------------------------------------------------------
/frontend/src/screens/ProfileScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { useDispatch, useSelector } from 'react-redux'
3 | import { LinkContainer } from 'react-router-bootstrap'
4 | import Message from '../components/Message'
5 | import Loader from '../components/Loader'
6 | import { getUserDetails, updateUserProfile } from '../actions/userActions'
7 | import { Row, Col, Form, Button, Table } from 'react-bootstrap'
8 | import { listMyOrders } from '../actions/orderActions'
9 | import formatter from '../utils/currencyFormatter'
10 | import { USER_UPDATE_PROFILE_RESET } from '../constants/userConstants'
11 |
12 | const ProfileScreen = ({ location, history }) => {
13 | const [name, setName] = useState('')
14 | const [email, setEmail] = useState('')
15 | const [password, setPassword] = useState('')
16 | const [confirmPassword, setConfirmPassword] = useState('')
17 | const [message, setMessage] = useState(null)
18 |
19 | const dispatch = useDispatch()
20 |
21 | const userDetails = useSelector((state) => state.userDetails)
22 | const { loading, error, user } = userDetails
23 |
24 | const userLogin = useSelector((state) => state.userLogin)
25 | const { userInfo } = userLogin
26 |
27 | const userUpdateProfile = useSelector((state) => state.userUpdateProfile)
28 | const { success } = userUpdateProfile
29 |
30 | const orderListMy = useSelector((state) => state.orderListMy)
31 | const { loading: loadingOrders, error: errorOrders, orders } = orderListMy
32 |
33 | useEffect(() => {
34 | if (!userInfo) {
35 | history.push('/login')
36 | } else {
37 | if (!user || !user.name || success) {
38 | dispatch({ type: USER_UPDATE_PROFILE_RESET })
39 | dispatch(getUserDetails('profile'))
40 | dispatch(listMyOrders())
41 | } else {
42 | setName(user.name)
43 | setEmail(user.email)
44 | }
45 | }
46 | }, [dispatch, history, userInfo, user, success])
47 |
48 | const submitHandler = (e) => {
49 | e.preventDefault()
50 | if (password !== confirmPassword) {
51 | setMessage('Passwords do not match')
52 | } else {
53 | dispatch(updateUserProfile({ id: user._id, name, email, password }))
54 | }
55 | }
56 |
57 | return (
58 |
59 |
60 | User Profile
61 | {message && {message} }
62 | {error && {error} }
63 | {success && Profile Updated }
64 | {loading && }
65 |
67 | Name
68 | setName(e.target.value)}
73 | >
74 |
75 |
76 |
77 | Email Address
78 | setEmail(e.target.value)}
83 | >
84 |
85 |
86 |
87 | Password
88 | setPassword(e.target.value)}
93 | >
94 |
95 |
96 |
97 | Confirm Password
98 | setConfirmPassword(e.target.value)}
103 | >
104 |
105 |
106 |
107 | Update
108 |
109 |
110 |
111 |
112 | My Orders
113 | {loadingOrders ? (
114 |
115 | ) : errorOrders ? (
116 | {errorOrders}
117 | ) : (
118 | <>
119 |
120 |
121 |
122 | ORDER ID
123 | DATE
124 | TOTAL
125 | PAID
126 | DELIVERED
127 |
128 |
129 |
130 |
131 | {orders.map((order) => (
132 |
133 | {order._id}
134 | {order.createdAt.substring(0, 10)}
135 | {formatter(order.totalPrice)}
136 |
137 | {order.isPaid ? (
138 | order.paidAt.substring(0, 10)
139 | ) : (
140 |
144 | )}
145 |
146 |
147 | {order.isDelivered ? (
148 | order.deliveredAt.substring(0, 10)
149 | ) : (
150 |
154 | )}
155 |
156 |
157 |
158 |
159 | Details
160 |
161 |
162 |
163 |
164 | ))}
165 |
166 |
167 | {orders.length === 0 && (
168 |
169 | You haven't placed any order yet,{' '}
170 | Start Shopping!
171 |
172 | )}
173 | >
174 | )}
175 |
176 |
177 | )
178 | }
179 |
180 | export default ProfileScreen
181 |
--------------------------------------------------------------------------------