├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── .prettierrc
├── Procfile
├── README.md
├── backend
├── config
│ └── db.js
├── controllers
│ ├── orderController.js
│ ├── productContoller.js
│ └── userController.js
├── data
│ ├── products.js
│ └── users.js
├── middleware
│ ├── authMiddleware.js
│ └── errorMiddleware.js
├── models
│ ├── orderModel.js
│ ├── productModel.js
│ └── userModel.js
├── routes
│ ├── orderRoutes.js
│ ├── productRoutes.js
│ ├── uploadRoutes.js
│ └── userRoutes.js
├── seeder.js
├── server.js
└── utils
│ └── generateToken.js
├── frontend
├── README.md
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── homepage.png
│ ├── images
│ │ ├── airpods.jpg
│ │ ├── alexa.jpg
│ │ ├── camera.jpg
│ │ ├── mouse.jpg
│ │ ├── phone.jpg
│ │ ├── playstation.jpg
│ │ └── sample.jpg
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
├── src
│ ├── App.js
│ ├── actions
│ │ ├── cartActions.js
│ │ ├── orderActions.js
│ │ ├── productActions.js
│ │ └── userActions.js
│ ├── assets
│ │ ├── cart.png
│ │ ├── homepage.png
│ │ ├── paymentPage.png
│ │ ├── placeOrder.png
│ │ ├── product.png
│ │ ├── shipping.png
│ │ └── signin.png
│ ├── bootstrap.min.css
│ ├── components
│ │ ├── CheckoutSteps.js
│ │ ├── Footer.js
│ │ ├── FormContainer.js
│ │ ├── Header.js
│ │ ├── Loader.js
│ │ ├── Message.js
│ │ ├── Meta.js
│ │ ├── Paginate.js
│ │ ├── Product.js
│ │ ├── ProductCarousel.js
│ │ ├── Rating.js
│ │ └── SearchBox.js
│ ├── constants
│ │ ├── cartConstants.js
│ │ ├── orderConstants.js
│ │ ├── productConstants.js
│ │ └── userConstants.js
│ ├── index.css
│ ├── index.js
│ ├── reducers
│ │ ├── cartReducers.js
│ │ ├── orderReducers.js
│ │ ├── productReducers.js
│ │ └── userReducers.js
│ ├── reportWebVitals.js
│ ├── screens
│ │ ├── CartScreen.js
│ │ ├── HomeScreen.js
│ │ ├── LoginScreen.js
│ │ ├── OrderListScreen.js
│ │ ├── OrderScreen.js
│ │ ├── PaymentScreen.js
│ │ ├── PlaceOrderScreen.js
│ │ ├── ProductEditScreen.js
│ │ ├── ProductListScreen.js
│ │ ├── ProductScreen.js
│ │ ├── ProfileScreen.js
│ │ ├── RegisterScreen.js
│ │ ├── ShippingScreen.js
│ │ ├── UserEditScreen.js
│ │ └── UserListScreen.js
│ └── store.js
└── yarn.lock
├── package-lock.json
├── package.json
├── renovate.json
└── uploads
├── file.txt
└── image-1607604569956.jpeg
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Build and Test
2 |
3 | on:
4 | push:
5 | branches: [master]
6 | pull_request:
7 | branches: [master]
8 |
9 | jobs:
10 | build_and_test:
11 | runs-on: "macOS-latest"
12 |
13 | strategy:
14 | matrix:
15 | node: [14]
16 |
17 | steps:
18 | - name: Checkout
19 | uses: actions/checkout@v5
20 | - name: Use Node.js ${{ matrix.node-version }}
21 | uses: actions/setup-node@v6
22 | with:
23 | node-version: ${{ matrix.node-version }}
24 | - name: Install project
25 | run: npm install
26 | - name: Build the project
27 | run: npm run build --if-present
28 | - name: Run tests
29 | run: npm test
30 |
--------------------------------------------------------------------------------
/.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 | node_modules/
7 | /.pnp
8 | .pnp.js
9 |
10 | # testing
11 | /coverage
12 |
13 | # production
14 | /frontend/build
15 |
16 | # misc
17 | .DS_Store
18 | .env
19 | .eslintcache
20 | .env.local
21 | .env.development.local
22 | .env.test.local
23 | .env.production.local
24 |
25 | npm-debug.log*
26 | yarn-debug.log*
27 | yarn-error.log*
28 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: node backend/server.js
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PROSHOP - MERN Stack eCommerce
2 |
3 | This is an eCommerce web application created with M(ongoDb)E(xpress)R(eact)N(ode) Stack. It has user authentication system, an admin user who can add new products in the app, check for order and payments and also choose which order has been delivered from the store. It is also connected to PayPal for payments. It is deployed in Heroku and below you can find the link to visit the app.
4 |
5 | 
6 | 
7 | 
8 | 
9 | 
10 | 
11 | 
12 |
13 | # Features
14 |
15 | - Full featured shopping cart
16 | - Product reviews and ratings
17 | - Top products carousel
18 | - Product pagination
19 | - Product search feature
20 | - User profile with orders
21 | - Admin product management
22 | - Admin user management
23 | - Admin Order details page
24 | - Mark orders as delivered option
25 | - Checkout process (shipping, payment method, etc)
26 | - PayPal / credit card integration
27 | - Database seeder (products & users)
28 |
29 | # Built with
30 |
31 | - NodeJs
32 | - Express
33 | - ReactJs
34 | - Redux
35 | - MongoDb
36 | - React Hooks
37 | - React Bootstrap
38 | - Axios
39 | - Bcrypt
40 | - JSON Web Tokens
41 | - Prettier
42 | - Eslint
43 | - React Helmet
44 |
45 | # Live Version
46 |
47 | https://proshopapp-by-mariosknl.herokuapp.com/
48 |
49 | # AUTHOR
50 |
51 | - Github: [@mariosknl](https://github.com/mariosknl)
52 | - Twitter: [@mariosknl](https://twitter.com/MariosKnl)
53 | - Linkedln: [marios-kanellopoulos](https://www.linkedin.com/in/marios-kanellopoulos)
54 | - Portfolio: [marios-kanellopoulos](https://marioskanellopoulos.com/)
55 |
56 | # Show your support
57 |
58 | Give ⭐️ if you like this project!
59 |
60 | # Acknowledgments
61 |
62 | - Github: [@bradtraversy](https://github.com/bradtraversy)
63 | - Website: [traversy-media](http://traversymedia.com/)
64 |
65 | ### 🤝 Contributing
66 |
67 | Contributions, issues, and feature requests are welcome!
68 | Feel free to check the issues page.
69 |
--------------------------------------------------------------------------------
/backend/config/db.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const connectDB = async () => {
4 | try {
5 | const conn = await mongoose.connect(process.env.MONGO_URI, {
6 | useUnifiedTopology: true,
7 | useNewUrlParser: true,
8 | useCreateIndex: true,
9 | });
10 |
11 | console.log(`MongoDB Connected: ${conn.connection.host}`.cyan.underline);
12 | } catch (error) {
13 | console.error(`Error: ${error.message}`.red.underline.bold);
14 | process.exit(1);
15 | }
16 | };
17 |
18 | export default connectDB;
19 |
--------------------------------------------------------------------------------
/backend/controllers/orderController.js:
--------------------------------------------------------------------------------
1 | import asyncHandler from "express-async-handler";
2 |
3 | import Order from "../models/orderModel.js";
4 |
5 | // @desc Create new order
6 | // @route POST /api/orders
7 | // @access Private
8 | const addOrderItems = asyncHandler(async (req, res) => {
9 | const {
10 | orderItems,
11 | shippingAddress,
12 | paymentMethod,
13 | itemsPrice,
14 | taxPrice,
15 | shippingPrice,
16 | totalPrice,
17 | } = req.body;
18 |
19 | if (orderItems && orderItems.length === 0) {
20 | res.status(400);
21 | throw new Error("No order items");
22 | return;
23 | } else {
24 | const order = new Order({
25 | orderItems,
26 | user: req.user._id,
27 | shippingAddress,
28 | paymentMethod,
29 | itemsPrice,
30 | taxPrice,
31 | shippingPrice,
32 | totalPrice,
33 | });
34 |
35 | const createdOrder = await order.save();
36 |
37 | res.status(201).json(createdOrder);
38 | }
39 | });
40 |
41 | // @desc Get order by ID
42 | // @route GET /api/orders/:id
43 | // @access Private
44 | const getOrderById = asyncHandler(async (req, res) => {
45 | const order = await Order.findById(req.params.id).populate(
46 | "user",
47 | "name email"
48 | );
49 |
50 | if (order) {
51 | res.json(order);
52 | } else {
53 | res.status(404);
54 | throw new Error("Order not found");
55 | }
56 | });
57 |
58 | // @desc Update order to paid
59 | // @route GET /api/orders/:id/pay
60 | // @access Private
61 | const updateOrderToPaid = asyncHandler(async (req, res) => {
62 | const order = await Order.findById(req.params.id);
63 |
64 | if (order) {
65 | order.isPaid = true;
66 | order.paidAt = Date.now();
67 | order.paymentResult = {
68 | id: req.body.id,
69 | status: req.body.status,
70 | update_time: req.body.update_time,
71 | email_address: req.body.payer.email_address,
72 | };
73 |
74 | const updatedOrder = await order.save();
75 |
76 | res.json(updatedOrder);
77 | } else {
78 | res.status(404);
79 | throw new Error("Order not found");
80 | }
81 | });
82 |
83 | // @desc Update order to delivered
84 | // @route GET /api/orders/:id/delivered
85 | // @access Private/Admin
86 | const updateOrderToDelivered = asyncHandler(async (req, res) => {
87 | const order = await Order.findById(req.params.id);
88 |
89 | if (order) {
90 | order.isDelivered = true;
91 | order.deliveredAt = Date.now();
92 |
93 | const updatedOrder = await order.save();
94 |
95 | res.json(updatedOrder);
96 | } else {
97 | res.status(404);
98 | throw new Error("Order not found");
99 | }
100 | });
101 |
102 | // @desc Get logged in user orders
103 | // @route GET /api/orders/myorders
104 | // @access Private
105 | const getMyOrders = asyncHandler(async (req, res) => {
106 | const orders = await Order.find({ user: req.user._id });
107 |
108 | res.json(orders);
109 | });
110 |
111 | // @desc Get all orders
112 | // @route GET /api/orders
113 | // @access Private/Admin
114 | const getOrders = asyncHandler(async (req, res) => {
115 | const orders = await Order.find({}).populate("user", "id name");
116 |
117 | res.json(orders);
118 | });
119 |
120 | export {
121 | addOrderItems,
122 | getOrderById,
123 | updateOrderToPaid,
124 | updateOrderToDelivered,
125 | getMyOrders,
126 | getOrders,
127 | };
128 |
--------------------------------------------------------------------------------
/backend/controllers/productContoller.js:
--------------------------------------------------------------------------------
1 | import asyncHandler from "express-async-handler";
2 |
3 | import Product from "../models/productModel.js";
4 |
5 | // @desc Fetch all Products
6 | // @route Get /api/products
7 | // @access Public
8 | const getProducts = asyncHandler(async (req, res) => {
9 | const pageSize = 3;
10 | const page = Number(req.query.pageNumber) || 1;
11 |
12 | const keyword = req.query.keyword
13 | ? {
14 | name: {
15 | $regex: req.query.keyword,
16 | $options: "i",
17 | },
18 | }
19 | : {};
20 |
21 | const count = await Product.countDocuments({ ...keyword });
22 | const products = await Product.find({ ...keyword })
23 | .limit(pageSize)
24 | .skip(pageSize * (page - 1));
25 |
26 | res.json({ products, page, pages: Math.ceil(count / pageSize) });
27 | });
28 |
29 | // @desc Fetch Single Product
30 | // @route Get /api/product/:id
31 | // @access Public
32 | const getProductById = asyncHandler(async (req, res) => {
33 | const product = await Product.findById(req.params.id);
34 |
35 | if (product) {
36 | res.json(product);
37 | } else {
38 | res.status(404);
39 | throw new Error("Product not Found!");
40 | }
41 | });
42 |
43 | // @desc Delete a product
44 | // @route DELETE /api/product/:id
45 | // @access Private/Admin
46 | const deleteProduct = asyncHandler(async (req, res) => {
47 | const product = await Product.findById(req.params.id);
48 |
49 | if (product) {
50 | await product.remove();
51 | res.json({ message: "Product removed" });
52 | } else {
53 | res.status(404);
54 | throw new Error("Product not Found!");
55 | }
56 | });
57 |
58 | // @desc Create a product
59 | // @route POST /api/products
60 | // @access Private/Admin
61 | const createProduct = asyncHandler(async (req, res) => {
62 | const product = new Product({
63 | name: "Sample name",
64 | price: 0,
65 | user: req.user._id,
66 | image: "/images/sample.jpg",
67 | brand: "Sample brand",
68 | category: "Sample Category",
69 | countInStock: 0,
70 | numReviews: 0,
71 | description: "Sample description",
72 | });
73 |
74 | const createdProduct = await product.save();
75 | res.status(201).json(createdProduct);
76 | });
77 |
78 | // @desc Update a product
79 | // @route PUT /api/products/:id
80 | // @access Private/Admin
81 | const updateProduct = asyncHandler(async (req, res) => {
82 | const {
83 | name,
84 | price,
85 | description,
86 | image,
87 | brand,
88 | category,
89 | countInStock,
90 | } = req.body;
91 |
92 | const product = await Product.findById(req.params.id);
93 |
94 | if (product) {
95 | product.name = name;
96 | product.price = price;
97 | product.description = description;
98 | product.image = image;
99 | product.brand = brand;
100 | product.category = category;
101 | product.countInStock = countInStock;
102 |
103 | const updatedProduct = await product.save();
104 | res.json(updatedProduct);
105 | } else {
106 | res.status(404);
107 | throw new Error("Product not Found");
108 | }
109 | });
110 |
111 | // @desc Create new review
112 | // @route POST /api/products/:id/reviews
113 | // @access Private
114 | const createProductReview = asyncHandler(async (req, res) => {
115 | const { rating, comment } = req.body;
116 |
117 | const product = await Product.findById(req.params.id);
118 |
119 | if (product) {
120 | const alreadyReviewed = product.reviews.find(
121 | (r) => r.user.toString() === req.user._id.toString()
122 | );
123 |
124 | if (alreadyReviewed) {
125 | res.status(400);
126 | throw new Error("Product already reviewed");
127 | }
128 |
129 | const review = {
130 | name: req.user.name,
131 | rating: Number(rating),
132 | comment,
133 | user: req.user._id,
134 | };
135 |
136 | product.reviews.push(review);
137 |
138 | product.numReviews = product.reviews.length;
139 |
140 | product.rating =
141 | product.reviews.reduce((acc, item) => item.rating + acc, 0) /
142 | product.reviews.length;
143 |
144 | await product.save();
145 | res.status(201).json({ message: "Review added" });
146 | } else {
147 | res.status(404);
148 | throw new Error("Product not Found");
149 | }
150 | });
151 |
152 | // @desc Get Top rated products
153 | // @route GET /api/products/top
154 | // @access Public
155 | const getTopProducts = asyncHandler(async (req, res) => {
156 | const products = await Product.find({}).sort({ rating: -1 }).limit(3);
157 |
158 | res.json(products);
159 | });
160 |
161 | export {
162 | getProducts,
163 | getProductById,
164 | createProduct,
165 | updateProduct,
166 | deleteProduct,
167 | createProductReview,
168 | getTopProducts,
169 | };
170 |
--------------------------------------------------------------------------------
/backend/controllers/userController.js:
--------------------------------------------------------------------------------
1 | import asyncHandler from "express-async-handler";
2 |
3 | import generateToken from "../utils/generateToken.js";
4 | import User from "../models/userModel.js";
5 |
6 | // @desc Auth user & get token
7 | // @route Get /api/users/login
8 | // @access Public
9 | const authUser = asyncHandler(async (req, res) => {
10 | const { email, password } = req.body;
11 |
12 | const user = await User.findOne({ email });
13 |
14 | if (user && (await user.matchPassword(password))) {
15 | res.json({
16 | _id: user._id,
17 | name: user.name,
18 | email: user.email,
19 | isAdmin: user.isAdmin,
20 | token: generateToken(user._id),
21 | });
22 | } else {
23 | res.status(401);
24 | throw new Error("Invalid email or password");
25 | }
26 | });
27 |
28 | // @desc Register a new user
29 | // @route Get /api/users
30 | // @access Public
31 | const registerUser = asyncHandler(async (req, res) => {
32 | const { name, email, password } = req.body;
33 |
34 | const userExists = await User.findOne({ email });
35 |
36 | if (userExists) {
37 | res.status(400);
38 | throw new Error("User already exists");
39 | }
40 |
41 | const user = await User.create({
42 | name,
43 | email,
44 | password,
45 | });
46 |
47 | if (user) {
48 | res.status(201).json({
49 | _id: user._id,
50 | name: user.name,
51 | email: user.email,
52 | isAdmin: user.isAdmin,
53 | token: generateToken(user._id),
54 | });
55 | } else {
56 | res.status(400);
57 | throw new Error("Invalid user data");
58 | }
59 | });
60 |
61 | // @desc Get user profile
62 | // @route GET /api/users/profile
63 | // @access Private
64 | const getUserProfile = asyncHandler(async (req, res) => {
65 | const user = await User.findById(req.user._id);
66 |
67 | if (user) {
68 | res.json({
69 | _id: user._id,
70 | name: user.name,
71 | email: user.email,
72 | isAdmin: user.isAdmin,
73 | });
74 | } else {
75 | res.status(404);
76 | throw new Error("User not Found");
77 | }
78 | });
79 |
80 | // @desc Update user profile
81 | // @route Put /api/users/profile
82 | // @access Private
83 | const updateUserProfile = asyncHandler(async (req, res) => {
84 | const user = await User.findById(req.user._id);
85 |
86 | if (user) {
87 | user.name = req.body.name || user.name;
88 | user.email = req.body.email || user.email;
89 | if (req.body.password) {
90 | user.password = req.body.password;
91 | }
92 |
93 | const updatedUser = await user.save();
94 |
95 | res.json({
96 | _id: updatedUser._id,
97 | name: updatedUser.name,
98 | email: updatedUser.email,
99 | isAdmin: updatedUser.isAdmin,
100 | token: generateToken(updatedUser._id),
101 | });
102 | } else {
103 | res.status(404);
104 | throw new Error("User not Found");
105 | }
106 | });
107 |
108 | // @desc Get all users
109 | // @route GET /api/users
110 | // @access Private/Admin
111 | const getUsers = asyncHandler(async (req, res) => {
112 | const users = await User.find({});
113 | res.json(users);
114 | });
115 |
116 | // @desc Delete user
117 | // @route DELETE /api/users/:id
118 | // @access Private/Admin
119 | const deleteUser = asyncHandler(async (req, res) => {
120 | const user = await User.findById(req.params.id);
121 |
122 | if (user) {
123 | await user.remove();
124 | res.json({ message: "User removed" });
125 | } else {
126 | res.status(404);
127 | throw new Error("User not found");
128 | }
129 | });
130 |
131 | // @desc Update user
132 | // @route Put /api/users/:id
133 | // @access Private/Admin
134 | const updateUser = asyncHandler(async (req, res) => {
135 | const user = await User.findById(req.params.id);
136 |
137 | if (user) {
138 | user.name = req.body.name || user.name;
139 | user.email = req.body.email || user.email;
140 | user.isAdmin = req.body.isAdmin;
141 |
142 | const updatedUser = await user.save();
143 |
144 | res.json({
145 | _id: updatedUser._id,
146 | name: updatedUser.name,
147 | email: updatedUser.email,
148 | isAdmin: updatedUser.isAdmin,
149 | });
150 | } else {
151 | res.status(404);
152 | throw new Error("User not Found");
153 | }
154 | });
155 |
156 | // @desc Get user by ID
157 | // @route GET /api/users/:id
158 | // @access Private/Admin
159 | const getUserById = asyncHandler(async (req, res) => {
160 | const user = await User.findById(req.params.id).select("-password");
161 | if (user) {
162 | res.json(user);
163 | } else {
164 | res.status(404);
165 | throw new Error("User not found");
166 | }
167 | });
168 |
169 | export {
170 | authUser,
171 | getUserProfile,
172 | registerUser,
173 | updateUserProfile,
174 | getUsers,
175 | deleteUser,
176 | getUserById,
177 | updateUser,
178 | };
179 |
--------------------------------------------------------------------------------
/backend/data/products.js:
--------------------------------------------------------------------------------
1 | const products = [
2 | {
3 | name: "Airpods Wireless Bluetooth Headphones",
4 | image: "/images/airpods.jpg",
5 | description:
6 | "Bluetooth technology lets you connect it with compatible devices wirelessly High-quality AAC audio offers immersive listening experience Built-in microphone allows you to take calls while working",
7 | brand: "Apple",
8 | category: "Electronics",
9 | price: 89.99,
10 | countInStock: 10,
11 | rating: 4.5,
12 | numReviews: 12,
13 | },
14 | {
15 | name: "iPhone 11 Pro 256GB Memory",
16 | image: "/images/phone.jpg",
17 | description:
18 | "Introducing the iPhone 11 Pro. A transformative triple-camera system that adds tons of capability without complexity. An unprecedented leap in battery life",
19 | brand: "Apple",
20 | category: "Electronics",
21 | price: 599.99,
22 | countInStock: 7,
23 | rating: 4.0,
24 | numReviews: 8,
25 | },
26 | {
27 | name: "Cannon EOS 80D DSLR Camera",
28 | image: "/images/camera.jpg",
29 | description:
30 | "Characterized by versatile imaging specs, the Canon EOS 80D further clarifies itself using a pair of robust focusing systems and an intuitive design",
31 | brand: "Cannon",
32 | category: "Electronics",
33 | price: 929.99,
34 | countInStock: 5,
35 | rating: 3,
36 | numReviews: 12,
37 | },
38 | {
39 | name: "Sony Playstation 4 Pro White Version",
40 | image: "/images/playstation.jpg",
41 | description:
42 | "The ultimate home entertainment center starts with PlayStation. Whether you are into gaming, HD movies, television, music",
43 | brand: "Sony",
44 | category: "Electronics",
45 | price: 399.99,
46 | countInStock: 11,
47 | rating: 5,
48 | numReviews: 12,
49 | },
50 | {
51 | name: "Logitech G-Series Gaming Mouse",
52 | image: "/images/mouse.jpg",
53 | description:
54 | "Get a better handle on your games with this Logitech LIGHTSYNC gaming mouse. The six programmable buttons allow customization for a smooth playing experience",
55 | brand: "Logitech",
56 | category: "Electronics",
57 | price: 49.99,
58 | countInStock: 7,
59 | rating: 3.5,
60 | numReviews: 10,
61 | },
62 | {
63 | name: "Amazon Echo Dot 3rd Generation",
64 | image: "/images/alexa.jpg",
65 | description:
66 | "Meet Echo Dot - Our most popular smart speaker with a fabric design. It is our most compact smart speaker that fits perfectly into small space",
67 | brand: "Amazon",
68 | category: "Electronics",
69 | price: 29.99,
70 | countInStock: 0,
71 | rating: 4,
72 | numReviews: 12,
73 | },
74 | ];
75 |
76 | export default products;
77 |
--------------------------------------------------------------------------------
/backend/data/users.js:
--------------------------------------------------------------------------------
1 | import bcrypt from "bcryptjs";
2 |
3 | const users = [
4 | {
5 | name: "Admin User",
6 | email: "admin@example.com",
7 | password: bcrypt.hashSync("123456", 10),
8 | isAdmin: true,
9 | },
10 | {
11 | name: "John Doe",
12 | email: "john@example.com",
13 | password: bcrypt.hashSync("123456", 10),
14 | },
15 | {
16 | name: "Jane Doe",
17 | email: "jane@example.com",
18 | password: bcrypt.hashSync("123456", 10),
19 | },
20 | ];
21 |
22 | export default users;
23 |
--------------------------------------------------------------------------------
/backend/middleware/authMiddleware.js:
--------------------------------------------------------------------------------
1 | import jwt from "jsonwebtoken";
2 | import asyncHandler from "express-async-handler";
3 | import User from "../models/userModel.js";
4 |
5 | const protect = asyncHandler(async (req, res, next) => {
6 | let token;
7 |
8 | if (
9 | req.headers.authorization &&
10 | req.headers.authorization.startsWith("Bearer")
11 | ) {
12 | try {
13 | token = req.headers.authorization.split(" ")[1];
14 |
15 | const decoded = jwt.verify(token, process.env.JWT_SECRET);
16 |
17 | req.user = await User.findById(decoded.id).select("-password");
18 |
19 | next();
20 | } catch (error) {
21 | console.error(error);
22 | res.status(401);
23 | throw new Error("Not authorized, token failed");
24 | }
25 | }
26 |
27 | if (!token) {
28 | res.status(401);
29 | throw new Error("Not authorized, no token");
30 | }
31 | });
32 |
33 | const admin = (req, res, next) => {
34 | if (req.user && req.user.isAdmin) {
35 | next();
36 | } else {
37 | res.status(401);
38 | throw new Error("Not authorized as an admin");
39 | }
40 | };
41 |
42 | export { protect, admin };
43 |
--------------------------------------------------------------------------------
/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(err);
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 | export { notFound, errorHandler };
17 |
--------------------------------------------------------------------------------
/backend/models/orderModel.js:
--------------------------------------------------------------------------------
1 | import mongoose from "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: Number, required: true },
14 | image: { type: String, required: true },
15 | price: { type: Number, 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 | city: { type: String, required: true },
26 | postalCode: { type: String, required: true },
27 | country: { type: String, required: true },
28 | },
29 | paymentMethod: {
30 | type: String,
31 | required: true,
32 | },
33 | paymentResult: {
34 | id: { type: String },
35 | status: { type: String },
36 | update_time: { type: String },
37 | email_address: { type: String },
38 | },
39 | taxPrice: {
40 | type: Number,
41 | required: true,
42 | default: 0.0,
43 | },
44 | shippingPrice: {
45 | type: Number,
46 | required: true,
47 | default: 0.0,
48 | },
49 | totalPrice: {
50 | type: Number,
51 | required: true,
52 | default: 0.0,
53 | },
54 | isPaid: {
55 | type: Boolean,
56 | required: true,
57 | default: false,
58 | },
59 | paidAt: {
60 | type: Date,
61 | },
62 | isDelivered: {
63 | type: Boolean,
64 | required: true,
65 | default: false,
66 | },
67 | deliveredAt: {
68 | type: Date,
69 | },
70 | },
71 | {
72 | timestamps: true,
73 | }
74 | );
75 |
76 | const Order = mongoose.model("Order", orderSchema);
77 |
78 | export default Order;
79 |
--------------------------------------------------------------------------------
/backend/models/productModel.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const reviewSchema = mongoose.Schema(
4 | {
5 | name: { type: String, required: true },
6 | rating: { type: Number, required: true },
7 | comment: { type: String, required: true },
8 | user: {
9 | type: mongoose.Schema.Types.ObjectId,
10 | required: true,
11 | ref: "User",
12 | },
13 | },
14 | {
15 | timestamps: true,
16 | }
17 | );
18 |
19 | const productSchema = mongoose.Schema(
20 | {
21 | user: {
22 | type: mongoose.Schema.Types.ObjectId,
23 | required: true,
24 | ref: "User",
25 | },
26 | name: {
27 | type: String,
28 | required: true,
29 | },
30 | image: {
31 | type: String,
32 | required: true,
33 | },
34 | brand: {
35 | type: String,
36 | required: true,
37 | },
38 | category: {
39 | type: String,
40 | required: true,
41 | },
42 | description: {
43 | type: String,
44 | required: true,
45 | },
46 | reviews: [reviewSchema],
47 | rating: {
48 | type: Number,
49 | required: true,
50 | default: 0,
51 | },
52 | numReviews: {
53 | type: Number,
54 | required: true,
55 | default: 0,
56 | },
57 | price: {
58 | type: Number,
59 | required: true,
60 | default: 0,
61 | },
62 | countInStock: {
63 | type: Number,
64 | required: true,
65 | default: 0,
66 | },
67 | },
68 | {
69 | timestamps: true,
70 | }
71 | );
72 |
73 | const Product = mongoose.model("Product", productSchema);
74 |
75 | export default Product;
76 |
--------------------------------------------------------------------------------
/backend/models/userModel.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import bcrypt from "bcryptjs";
3 |
4 | const userSchema = mongoose.Schema(
5 | {
6 | name: {
7 | type: String,
8 | required: true,
9 | },
10 | email: {
11 | type: String,
12 | required: true,
13 | unique: true,
14 | },
15 | password: {
16 | type: String,
17 | required: true,
18 | },
19 | isAdmin: {
20 | type: Boolean,
21 | required: true,
22 | default: false,
23 | },
24 | },
25 | {
26 | timestamps: true,
27 | }
28 | );
29 |
30 | userSchema.methods.matchPassword = async function (enteredPassword) {
31 | return await bcrypt.compare(enteredPassword, this.password);
32 | };
33 |
34 | userSchema.pre("save", async function (next) {
35 | if (!this.isModified("password")) {
36 | next();
37 | }
38 |
39 | const salt = await bcrypt.genSalt(10);
40 | this.password = await bcrypt.hash(this.password, salt);
41 | });
42 |
43 | const User = mongoose.model("User", userSchema);
44 |
45 | export default User;
46 |
--------------------------------------------------------------------------------
/backend/routes/orderRoutes.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | const router = express.Router();
3 |
4 | import {
5 | addOrderItems,
6 | getOrderById,
7 | updateOrderToPaid,
8 | updateOrderToDelivered,
9 | getMyOrders,
10 | getOrders,
11 | } from "../controllers/orderController.js";
12 | import { protect, admin } from "../middleware/authMiddleware.js";
13 |
14 | router.route("/").post(protect, addOrderItems).get(protect, admin, getOrders);
15 | router.route("/myorders").get(protect, getMyOrders);
16 | router.route("/:id").get(protect, getOrderById);
17 | router.route("/:id/pay").put(protect, updateOrderToPaid);
18 | router.route("/:id/deliver").put(protect, admin, updateOrderToDelivered);
19 |
20 | export default router;
21 |
--------------------------------------------------------------------------------
/backend/routes/productRoutes.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | const router = express.Router();
3 |
4 | import {
5 | getProductById,
6 | getProducts,
7 | deleteProduct,
8 | updateProduct,
9 | createProduct,
10 | createProductReview,
11 | getTopProducts,
12 | } from "../controllers/productContoller.js";
13 | import { protect, admin } from "../middleware/authMiddleware.js";
14 |
15 | router.route("/").get(getProducts).post(protect, admin, createProduct);
16 | router.route("/:id/reviews").post(protect, createProductReview);
17 | router.get("/top", getTopProducts);
18 | router
19 | .route("/:id")
20 | .get(getProductById)
21 | .delete(protect, admin, deleteProduct)
22 | .put(protect, admin, updateProduct);
23 |
24 | export default router;
25 |
--------------------------------------------------------------------------------
/backend/routes/uploadRoutes.js:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import express from "express";
3 | import multer from "multer";
4 |
5 | const router = express.Router();
6 |
7 | const storage = multer.diskStorage({
8 | destination(req, file, cb) {
9 | cb(null, "uploads/");
10 | },
11 | filename(req, file, cb) {
12 | cb(
13 | null,
14 | `${file.fieldname}-${Date.now()}${path.extname(file.originalname)}`
15 | );
16 | },
17 | });
18 |
19 | function checkFileType(file, cb) {
20 | const filetypes = /jpg|jpeg|png/;
21 | const extname = filetypes.test(path.extname(file.originalname).toLowerCase());
22 | const mimetype = filetypes.test(file.mimetype);
23 |
24 | if (extname && mimetype) {
25 | return cb(null, true);
26 | } else {
27 | cb("Images only");
28 | }
29 | }
30 |
31 | const upload = multer({
32 | storage,
33 | fileFilter: function (req, file, cb) {
34 | checkFileType(file, cb);
35 | },
36 | });
37 |
38 | router.post("/", upload.single("image"), (req, res) => {
39 | res.send(`/${req.file.path}`);
40 | });
41 |
42 | export default router;
43 |
--------------------------------------------------------------------------------
/backend/routes/userRoutes.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | const router = express.Router();
3 |
4 | import {
5 | authUser,
6 | getUserProfile,
7 | registerUser,
8 | updateUserProfile,
9 | getUsers,
10 | deleteUser,
11 | getUserById,
12 | updateUser,
13 | } from "../controllers/userController.js";
14 | import { protect, admin } from "../middleware/authMiddleware.js";
15 |
16 | router.route("/").post(registerUser).get(protect, admin, getUsers);
17 | router.post("/login", authUser);
18 | router
19 | .route("/profile")
20 | .get(protect, getUserProfile)
21 | .put(protect, updateUserProfile);
22 |
23 | router
24 | .route("/:id")
25 | .delete(protect, admin, deleteUser)
26 | .get(protect, admin, getUserById)
27 | .put(protect, admin, updateUser);
28 | export default router;
29 |
--------------------------------------------------------------------------------
/backend/seeder.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import dotenv from "dotenv";
3 | import colors from "colors";
4 |
5 | import users from "./data/users.js";
6 | import products from "./data/products.js";
7 | import User from "./models/userModel.js";
8 | import Product from "./models/productModel.js";
9 | import Order from "./models/orderModel.js";
10 | import connectDB from "./config/db.js";
11 |
12 | dotenv.config();
13 |
14 | connectDB();
15 |
16 | const importData = async () => {
17 | try {
18 | await Order.deleteMany();
19 | await Product.deleteMany();
20 | await User.deleteMany();
21 |
22 | const createdUsers = await User.insertMany(users);
23 |
24 | const adminUser = createdUsers[0]._id;
25 |
26 | const sampleProducts = products.map((product) => {
27 | return {
28 | ...product,
29 | user: adminUser,
30 | };
31 | });
32 |
33 | await Product.insertMany(sampleProducts);
34 |
35 | console.log("Data Imported!".green.inverse);
36 | process.exit();
37 | } catch (error) {
38 | console.error(`${error}`.red.inverse);
39 | process.exit(1);
40 | }
41 | };
42 |
43 | const destroyData = async () => {
44 | try {
45 | await Order.deleteMany();
46 | await Product.deleteMany();
47 | await User.deleteMany();
48 |
49 | console.log("Data Destroyed!".red.inverse);
50 | process.exit();
51 | } catch (error) {
52 | console.error(`${error}`.red.inverse);
53 | process.exit(1);
54 | }
55 | };
56 |
57 | if (process.argv[2] === "-d") {
58 | destroyData();
59 | } else {
60 | importData();
61 | }
62 |
--------------------------------------------------------------------------------
/backend/server.js:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import express from "express";
3 | import dotenv from "dotenv";
4 | import colors from "colors";
5 | import morgan from "morgan";
6 | import { notFound, errorHandler } from "./middleware/errorMiddleware.js";
7 |
8 | import connectDB from "./config/db.js";
9 | import productRoutes from "./routes/productRoutes.js";
10 | import userRoutes from "./routes/userRoutes.js";
11 | import orderRoutes from "./routes/orderRoutes.js";
12 | import uploadRoutes from "./routes/uploadRoutes.js";
13 |
14 | dotenv.config();
15 |
16 | connectDB();
17 |
18 | const app = express();
19 |
20 | if (process.env.NODE_ENV === "development") {
21 | app.use(morgan("dev"));
22 | }
23 |
24 | app.use(express.json());
25 |
26 | app.use("/api/products", productRoutes);
27 | app.use("/api/users", userRoutes);
28 | app.use("/api/orders", orderRoutes);
29 | app.use("/api/upload", uploadRoutes);
30 |
31 | app.get("/api/config/paypal", (req, res) =>
32 | res.send(process.env.PAYPAL_CLIENT_ID)
33 | );
34 |
35 | const __dirname = path.resolve();
36 | app.use("/uploads", express.static(path.join(__dirname, "/uploads")));
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(path.resolve(__dirname, "frontend", "build", "index.html"))
43 | );
44 | } else {
45 | app.get("/", (req, res) => {
46 | res.send("API is running...");
47 | });
48 | }
49 |
50 | app.use(notFound);
51 |
52 | app.use(errorHandler);
53 |
54 | const PORT = process.env.PORT || 5000;
55 |
56 | app.listen(
57 | PORT,
58 | console.log(
59 | `Server running in ${process.env.NODE_ENV} mode on port ${PORT}`.yellow.bold
60 | )
61 | );
62 |
--------------------------------------------------------------------------------
/backend/utils/generateToken.js:
--------------------------------------------------------------------------------
1 | import jwt from "jsonwebtoken";
2 |
3 | const generateToken = (id) => {
4 | return jwt.sign({ id }, process.env.JWT_SECRET, {
5 | expiresIn: "30d",
6 | });
7 | };
8 |
9 | export default generateToken;
10 |
--------------------------------------------------------------------------------
/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 | ### `yarn 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 | ### `yarn 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 | ### `yarn 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 | ### `yarn 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 | ### `yarn 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/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": "6.5.0",
8 | "@testing-library/react": "14.3.1",
9 | "react": "17.0.2",
10 | "react-bootstrap": "2.10.5",
11 | "react-dom": "17.0.2",
12 | "react-helmet": "6.1.0",
13 | "react-paypal-button-v2": "2.6.3",
14 | "react-redux": "9.0.4",
15 | "react-router-bootstrap": "0.26.3",
16 | "react-router-dom": "6.27.0",
17 | "react-scripts": "5.0.1",
18 | "redux": "5.0.1",
19 | "@redux-devtools/extension": "3.3.0",
20 | "redux-thunk": "3.1.0",
21 | "web-vitals": "3.5.1"
22 | },
23 | "scripts": {
24 | "start": "react-scripts start",
25 | "build": "react-scripts build",
26 | "test": "react-scripts test",
27 | "eject": "react-scripts eject"
28 | },
29 | "eslintConfig": {
30 | "extends": [
31 | "react-app",
32 | "react-app/jest"
33 | ]
34 | },
35 | "browserslist": {
36 | "production": [
37 | ">0.2%",
38 | "not dead",
39 | "not op_mini all"
40 | ],
41 | "development": [
42 | "last 1 chrome version",
43 | "last 1 firefox version",
44 | "last 1 safari version"
45 | ]
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mariosknl/ProShop/97c26df85e134c7beccac1e892bbd57b974f4d34/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/homepage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mariosknl/ProShop/97c26df85e134c7beccac1e892bbd57b974f4d34/frontend/public/homepage.png
--------------------------------------------------------------------------------
/frontend/public/images/airpods.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mariosknl/ProShop/97c26df85e134c7beccac1e892bbd57b974f4d34/frontend/public/images/airpods.jpg
--------------------------------------------------------------------------------
/frontend/public/images/alexa.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mariosknl/ProShop/97c26df85e134c7beccac1e892bbd57b974f4d34/frontend/public/images/alexa.jpg
--------------------------------------------------------------------------------
/frontend/public/images/camera.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mariosknl/ProShop/97c26df85e134c7beccac1e892bbd57b974f4d34/frontend/public/images/camera.jpg
--------------------------------------------------------------------------------
/frontend/public/images/mouse.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mariosknl/ProShop/97c26df85e134c7beccac1e892bbd57b974f4d34/frontend/public/images/mouse.jpg
--------------------------------------------------------------------------------
/frontend/public/images/phone.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mariosknl/ProShop/97c26df85e134c7beccac1e892bbd57b974f4d34/frontend/public/images/phone.jpg
--------------------------------------------------------------------------------
/frontend/public/images/playstation.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mariosknl/ProShop/97c26df85e134c7beccac1e892bbd57b974f4d34/frontend/public/images/playstation.jpg
--------------------------------------------------------------------------------
/frontend/public/images/sample.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mariosknl/ProShop/97c26df85e134c7beccac1e892bbd57b974f4d34/frontend/public/images/sample.jpg
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
19 |
20 | Welcome to ProShop
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/frontend/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mariosknl/ProShop/97c26df85e134c7beccac1e892bbd57b974f4d34/frontend/public/logo192.png
--------------------------------------------------------------------------------
/frontend/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mariosknl/ProShop/97c26df85e134c7beccac1e892bbd57b974f4d34/frontend/public/logo512.png
--------------------------------------------------------------------------------
/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/frontend/src/App.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { BrowserRouter as Router, Route } from "react-router-dom";
3 |
4 | import { Container } from "react-bootstrap";
5 | import Header from "./components/Header";
6 | import Footer from "./components/Footer";
7 | import HomeScreen from "./screens/HomeScreen";
8 | import ProductScreen from "./screens/ProductScreen";
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 PaymentScreen from "./screens/PaymentScreen";
15 | import PlaceOrderScreen from "./screens/PlaceOrderScreen";
16 | import OrderScreen from "./screens/OrderScreen";
17 | import UserListScreen from "./screens/UserListScreen";
18 | import UserEditScreen from "./screens/UserEditScreen";
19 | import ProductListScreen from "./screens/ProductListScreen";
20 | import ProductEditScreen from "./screens/ProductEditScreen";
21 | import OrderListScreen from "./screens/OrderListScreen";
22 |
23 | const App = () => {
24 | return (
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
46 |
51 |
52 |
53 |
54 |
59 |
60 |
61 |
62 |
63 |
64 | );
65 | };
66 |
67 | export default App;
68 |
--------------------------------------------------------------------------------
/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 |
12 | dispatch({
13 | type: CART_ADD_ITEM,
14 | payload: {
15 | product: data._id,
16 | name: data.name,
17 | image: data.image,
18 | price: data.price,
19 | countInStock: data.countInStock,
20 | qty,
21 | },
22 | });
23 |
24 | localStorage.setItem("cartItems", JSON.stringify(getState().cart.cartItems));
25 | };
26 |
27 | export const removeFromCart = (id) => (dispatch, getState) => {
28 | dispatch({
29 | type: CART_REMOVE_ITEM,
30 | payload: id,
31 | });
32 |
33 | localStorage.setItem("cartItems", JSON.stringify(getState().cart.cartItems));
34 | };
35 |
36 | export const saveShippingAddress = (data) => (dispatch) => {
37 | dispatch({
38 | type: CART_SAVE_SHIPPING_ADDRESS,
39 | payload: data,
40 | });
41 |
42 | localStorage.setItem("shippingAddress", JSON.stringify(data));
43 | };
44 |
45 | export const savePaymentMethod = (data) => (dispatch) => {
46 | dispatch({
47 | type: CART_SAVE_PAYMENT_METHOD,
48 | payload: data,
49 | });
50 |
51 | localStorage.setItem("paymentMethod", JSON.stringify(data));
52 | };
53 |
--------------------------------------------------------------------------------
/frontend/src/actions/orderActions.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import {
3 | ORDER_CREATE_REQUEST,
4 | ORDER_CREATE_SUCCESS,
5 | ORDER_CREATE_FAIL,
6 | ORDER_DETAILS_REQUEST,
7 | ORDER_DETAILS_SUCCESS,
8 | ORDER_DETAILS_FAIL,
9 | ORDER_PAY_REQUEST,
10 | ORDER_PAY_SUCCESS,
11 | ORDER_PAY_FAIL,
12 | ORDER_LIST_MY_REQUEST,
13 | ORDER_LIST_MY_SUCCESS,
14 | ORDER_LIST_MY_FAIL,
15 | ORDERS_LIST_REQUEST,
16 | ORDERS_LIST_SUCCESS,
17 | ORDERS_LIST_FAIL,
18 | ORDER_DELIVER_REQUEST,
19 | ORDER_DELIVER_SUCCESS,
20 | ORDER_DELIVER_FAIL,
21 | } from "../constants/orderConstants";
22 |
23 | export const createOrder = (order) => async (dispatch, getState) => {
24 | try {
25 | dispatch({
26 | type: ORDER_CREATE_REQUEST,
27 | });
28 |
29 | const {
30 | userLogin: { userInfo },
31 | } = getState();
32 |
33 | const config = {
34 | headers: {
35 | "Content-Type": "application/json",
36 | Authorization: `Bearer ${userInfo.token}`,
37 | },
38 | };
39 |
40 | const { data } = await axios.post(`/api/orders`, order, config);
41 |
42 | dispatch({
43 | type: ORDER_CREATE_SUCCESS,
44 | payload: data,
45 | });
46 | } catch (error) {
47 | dispatch({
48 | type: ORDER_CREATE_FAIL,
49 | payload:
50 | error.response && error.response.data.message
51 | ? error.response.data.message
52 | : error.message,
53 | });
54 | }
55 | };
56 |
57 | export const getOrderDetails = (id) => async (dispatch, getState) => {
58 | try {
59 | dispatch({
60 | type: ORDER_DETAILS_REQUEST,
61 | });
62 |
63 | const {
64 | userLogin: { userInfo },
65 | } = getState();
66 |
67 | const config = {
68 | headers: {
69 | Authorization: `Bearer ${userInfo.token}`,
70 | },
71 | };
72 |
73 | const { data } = await axios.get(`/api/orders/${id}`, config);
74 |
75 | dispatch({
76 | type: ORDER_DETAILS_SUCCESS,
77 | payload: data,
78 | });
79 | } catch (error) {
80 | dispatch({
81 | type: ORDER_DETAILS_FAIL,
82 | payload:
83 | error.response && error.response.data.message
84 | ? error.response.data.message
85 | : error.message,
86 | });
87 | }
88 | };
89 |
90 | export const payOrder = (orderId, paymentResult) => async (
91 | dispatch,
92 | getState
93 | ) => {
94 | try {
95 | dispatch({
96 | type: ORDER_PAY_REQUEST,
97 | });
98 |
99 | const {
100 | userLogin: { userInfo },
101 | } = getState();
102 |
103 | const config = {
104 | headers: {
105 | "Content-Type": "application/json",
106 | Authorization: `Bearer ${userInfo.token}`,
107 | },
108 | };
109 |
110 | const { data } = await axios.put(
111 | `/api/orders/${orderId}/pay`,
112 | paymentResult,
113 | config
114 | );
115 |
116 | dispatch({
117 | type: ORDER_PAY_SUCCESS,
118 | payload: data,
119 | });
120 | } catch (error) {
121 | dispatch({
122 | type: ORDER_PAY_FAIL,
123 | payload:
124 | error.response && error.response.data.message
125 | ? error.response.data.message
126 | : error.message,
127 | });
128 | }
129 | };
130 |
131 | export const deliverOrder = (orderId) => async (dispatch, getState) => {
132 | try {
133 | dispatch({
134 | type: ORDER_DELIVER_REQUEST,
135 | });
136 |
137 | const {
138 | userLogin: { userInfo },
139 | } = getState();
140 |
141 | const config = {
142 | headers: {
143 | Authorization: `Bearer ${userInfo.token}`,
144 | },
145 | };
146 |
147 | const { data } = await axios.put(
148 | `/api/orders/${orderId}/deliver`,
149 | {},
150 | config
151 | );
152 |
153 | dispatch({
154 | type: ORDER_DELIVER_SUCCESS,
155 | payload: data,
156 | });
157 | } catch (error) {
158 | dispatch({
159 | type: ORDER_DELIVER_FAIL,
160 | payload:
161 | error.response && error.response.data.message
162 | ? error.response.data.message
163 | : error.message,
164 | });
165 | }
166 | };
167 |
168 | export const listMyOrders = () => async (dispatch, getState) => {
169 | try {
170 | dispatch({
171 | type: ORDER_LIST_MY_REQUEST,
172 | });
173 |
174 | const {
175 | userLogin: { userInfo },
176 | } = getState();
177 |
178 | const config = {
179 | headers: {
180 | Authorization: `Bearer ${userInfo.token}`,
181 | },
182 | };
183 |
184 | const { data } = await axios.get(`/api/orders/myorders`, config);
185 |
186 | dispatch({
187 | type: ORDER_LIST_MY_SUCCESS,
188 | payload: data,
189 | });
190 | } catch (error) {
191 | dispatch({
192 | type: ORDER_LIST_MY_FAIL,
193 | payload:
194 | error.response && error.response.data.message
195 | ? error.response.data.message
196 | : error.message,
197 | });
198 | }
199 | };
200 |
201 | export const listOrders = () => async (dispatch, getState) => {
202 | try {
203 | dispatch({
204 | type: ORDERS_LIST_REQUEST,
205 | });
206 |
207 | const {
208 | userLogin: { userInfo },
209 | } = getState();
210 |
211 | const config = {
212 | headers: {
213 | Authorization: `Bearer ${userInfo.token}`,
214 | },
215 | };
216 |
217 | const { data } = await axios.get(`/api/orders`, config);
218 |
219 | dispatch({
220 | type: ORDERS_LIST_SUCCESS,
221 | payload: data,
222 | });
223 | } catch (error) {
224 | dispatch({
225 | type: ORDERS_LIST_FAIL,
226 | payload:
227 | error.response && error.response.data.message
228 | ? error.response.data.message
229 | : error.message,
230 | });
231 | }
232 | };
233 |
--------------------------------------------------------------------------------
/frontend/src/actions/productActions.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import {
3 | PRODUCT_LIST_REQUEST,
4 | PRODUCT_LIST_SUCCESS,
5 | PRODUCT_LIST_FAIL,
6 | PRODUCT_DETAILS_REQUEST,
7 | PRODUCT_DETAILS_SUCCESS,
8 | PRODUCT_DETAILS_FAIL,
9 | PRODUCT_DELETE_REQUEST,
10 | PRODUCT_DELETE_SUCCESS,
11 | PRODUCT_DELETE_FAIL,
12 | PRODUCT_CREATE_REQUEST,
13 | PRODUCT_CREATE_SUCCESS,
14 | PRODUCT_CREATE_FAIL,
15 | PRODUCT_UPDATE_REQUEST,
16 | PRODUCT_UPDATE_SUCCESS,
17 | PRODUCT_UPDATE_FAIL,
18 | PRODUCT_CREATE_REVIEW_REQUEST,
19 | PRODUCT_CREATE_REVIEW_SUCCESS,
20 | PRODUCT_CREATE_REVIEW_FAIL,
21 | PRODUCT_TOP_REQUEST,
22 | PRODUCT_TOP_SUCCESS,
23 | PRODUCT_TOP_FAIL,
24 | } from "../constants/productConstants";
25 |
26 | export const listProducts = (keyword = "", pageNumber = "") => async (
27 | dispatch
28 | ) => {
29 | try {
30 | dispatch({ type: PRODUCT_LIST_REQUEST });
31 |
32 | const { data } = await axios.get(
33 | `/api/products?keyword=${keyword}&pageNumber=${pageNumber}`
34 | );
35 |
36 | dispatch({
37 | type: PRODUCT_LIST_SUCCESS,
38 | payload: data,
39 | });
40 | } catch (error) {
41 | dispatch({
42 | type: PRODUCT_LIST_FAIL,
43 | payload:
44 | error.response && error.response.data.message
45 | ? error.response.data.message
46 | : error.message,
47 | });
48 | }
49 | };
50 |
51 | export const listProductDetails = (id) => async (dispatch) => {
52 | try {
53 | dispatch({ type: PRODUCT_DETAILS_REQUEST });
54 |
55 | const { data } = await axios.get(`/api/products/${id}`);
56 |
57 | dispatch({
58 | type: PRODUCT_DETAILS_SUCCESS,
59 | payload: data,
60 | });
61 | } catch (error) {
62 | dispatch({
63 | type: PRODUCT_DETAILS_FAIL,
64 | payload:
65 | error.response && error.response.data.message
66 | ? error.response.data.message
67 | : error.message,
68 | });
69 | }
70 | };
71 |
72 | export const deleteProduct = (id) => async (dispatch, getState) => {
73 | try {
74 | dispatch({
75 | type: PRODUCT_DELETE_REQUEST,
76 | });
77 |
78 | const {
79 | userLogin: { userInfo },
80 | } = getState();
81 |
82 | const config = {
83 | headers: {
84 | Authorization: `Bearer ${userInfo.token}`,
85 | },
86 | };
87 |
88 | await axios.delete(`/api/products/${id}`, config);
89 |
90 | dispatch({
91 | type: PRODUCT_DELETE_SUCCESS,
92 | });
93 | } catch (error) {
94 | dispatch({
95 | type: PRODUCT_DELETE_FAIL,
96 | payload:
97 | error.response && error.response.data.message
98 | ? error.response.data.message
99 | : error.message,
100 | });
101 | }
102 | };
103 |
104 | export const createProduct = () => async (dispatch, getState) => {
105 | try {
106 | dispatch({
107 | type: PRODUCT_CREATE_REQUEST,
108 | });
109 |
110 | const {
111 | userLogin: { userInfo },
112 | } = getState();
113 |
114 | const config = {
115 | headers: {
116 | Authorization: `Bearer ${userInfo.token}`,
117 | },
118 | };
119 |
120 | const { data } = await axios.post(`/api/products/`, {}, config);
121 |
122 | dispatch({
123 | type: PRODUCT_CREATE_SUCCESS,
124 | payload: data,
125 | });
126 | } catch (error) {
127 | dispatch({
128 | type: PRODUCT_CREATE_FAIL,
129 | payload:
130 | error.response && error.response.data.message
131 | ? error.response.data.message
132 | : error.message,
133 | });
134 | }
135 | };
136 |
137 | export const updateProduct = (product) => async (dispatch, getState) => {
138 | try {
139 | dispatch({
140 | type: PRODUCT_UPDATE_REQUEST,
141 | });
142 |
143 | const {
144 | userLogin: { userInfo },
145 | } = getState();
146 |
147 | const config = {
148 | headers: {
149 | "Content-Type": "application/json",
150 | Authorization: `Bearer ${userInfo.token}`,
151 | },
152 | };
153 |
154 | const { data } = await axios.put(
155 | `/api/products/${product._id}`,
156 | product,
157 | config
158 | );
159 |
160 | dispatch({
161 | type: PRODUCT_UPDATE_SUCCESS,
162 | payload: data,
163 | });
164 | } catch (error) {
165 | dispatch({
166 | type: PRODUCT_UPDATE_FAIL,
167 | payload:
168 | error.response && error.response.data.message
169 | ? error.response.data.message
170 | : error.message,
171 | });
172 | }
173 | };
174 |
175 | export const createProductReview = (productId, review) => async (
176 | dispatch,
177 | getState
178 | ) => {
179 | try {
180 | dispatch({
181 | type: PRODUCT_CREATE_REVIEW_REQUEST,
182 | });
183 |
184 | const {
185 | userLogin: { userInfo },
186 | } = getState();
187 |
188 | const config = {
189 | headers: {
190 | "Content-Type": "application/json",
191 | Authorization: `Bearer ${userInfo.token}`,
192 | },
193 | };
194 |
195 | await axios.post(`/api/products/${productId}/reviews`, review, config);
196 |
197 | dispatch({
198 | type: PRODUCT_CREATE_REVIEW_SUCCESS,
199 | });
200 | } catch (error) {
201 | dispatch({
202 | type: PRODUCT_CREATE_REVIEW_FAIL,
203 | payload:
204 | error.response && error.response.data.message
205 | ? error.response.data.message
206 | : error.message,
207 | });
208 | }
209 | };
210 |
211 | export const listTopProducts = () => async (dispatch) => {
212 | try {
213 | dispatch({ type: PRODUCT_TOP_REQUEST });
214 |
215 | const { data } = await axios.get(`/api/products/top`);
216 |
217 | dispatch({
218 | type: PRODUCT_TOP_SUCCESS,
219 | payload: data,
220 | });
221 | } catch (error) {
222 | dispatch({
223 | type: PRODUCT_TOP_FAIL,
224 | payload:
225 | error.response && error.response.data.message
226 | ? error.response.data.message
227 | : error.message,
228 | });
229 | }
230 | };
231 |
--------------------------------------------------------------------------------
/frontend/src/actions/userActions.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | import {
4 | USER_DETAILS_FAIL,
5 | USER_DETAILS_REQUEST,
6 | USER_DETAILS_SUCCESS,
7 | USER_LOGIN_FAIL,
8 | USER_LOGIN_REQUEST,
9 | USER_LOGIN_SUCCESS,
10 | USER_LOGOUT,
11 | USER_REGISTER_FAIL,
12 | USER_REGISTER_REQUEST,
13 | USER_REGISTER_SUCCESS,
14 | USER_UPDATE_PROFILE_FAIL,
15 | USER_UPDATE_PROFILE_REQUEST,
16 | USER_UPDATE_PROFILE_SUCCESS,
17 | USER_DETAILS_RESET,
18 | USER_LIST_REQUEST,
19 | USER_LIST_SUCCESS,
20 | USER_LIST_FAIL,
21 | USER_LIST_RESET,
22 | USER_DELETE_SUCCESS,
23 | USER_DELETE_FAIL,
24 | USER_DELETE_REQUEST,
25 | USER_UPDATE_FAIL,
26 | USER_UPDATE_SUCCESS,
27 | USER_UPDATE_REQUEST,
28 | } from "../constants/userConstants";
29 | import { ORDER_LIST_MY_RESET } from "../constants/orderConstants";
30 |
31 | export const login = (email, password) => async (dispatch) => {
32 | try {
33 | dispatch({
34 | type: USER_LOGIN_REQUEST,
35 | });
36 |
37 | const config = {
38 | headers: {
39 | "Content-Type": "application/json",
40 | },
41 | };
42 |
43 | const { data } = await axios.post(
44 | "/api/users/login",
45 | { email, password },
46 | config
47 | );
48 |
49 | dispatch({
50 | type: USER_LOGIN_SUCCESS,
51 | payload: data,
52 | });
53 |
54 | localStorage.setItem("userInfo", JSON.stringify(data));
55 | } catch (error) {
56 | dispatch({
57 | type: USER_LOGIN_FAIL,
58 | payload:
59 | error.response && error.response.data.message
60 | ? error.response.data.message
61 | : error.message,
62 | });
63 | }
64 | };
65 |
66 | export const logout = () => (dispatch) => {
67 | localStorage.removeItem("userInfo");
68 | localStorage.removeItem("cartItems");
69 | localStorage.removeItem("shippingAddress");
70 | localStorage.removeItem("paymentMethod");
71 | dispatch({ type: USER_LOGOUT });
72 | dispatch({ type: USER_DETAILS_RESET });
73 | dispatch({ type: ORDER_LIST_MY_RESET });
74 | dispatch({ type: USER_LIST_RESET });
75 | document.location.href = "/login";
76 | };
77 |
78 | export const register = (name, email, password) => async (dispatch) => {
79 | try {
80 | dispatch({
81 | type: USER_REGISTER_REQUEST,
82 | });
83 |
84 | const config = {
85 | headers: {
86 | "Content-Type": "application/json",
87 | },
88 | };
89 |
90 | const { data } = await axios.post(
91 | "/api/users",
92 | { name, email, password },
93 | config
94 | );
95 |
96 | dispatch({
97 | type: USER_REGISTER_SUCCESS,
98 | payload: data,
99 | });
100 |
101 | dispatch({
102 | type: USER_LOGIN_SUCCESS,
103 | payload: data,
104 | });
105 |
106 | localStorage.setItem("userInfo", JSON.stringify(data));
107 | } catch (error) {
108 | dispatch({
109 | type: USER_REGISTER_FAIL,
110 | payload:
111 | error.response && error.response.data.message
112 | ? error.response.data.message
113 | : error.message,
114 | });
115 | }
116 | };
117 |
118 | export const getUserDetails = (id) => async (dispatch, getState) => {
119 | try {
120 | dispatch({
121 | type: USER_DETAILS_REQUEST,
122 | });
123 |
124 | const {
125 | userLogin: { userInfo },
126 | } = getState();
127 |
128 | const config = {
129 | headers: {
130 | "Content-Type": "application/json",
131 | Authorization: `Bearer ${userInfo.token}`,
132 | },
133 | };
134 |
135 | const { data } = await axios.get(`/api/users/${id}`, config);
136 |
137 | dispatch({
138 | type: USER_DETAILS_SUCCESS,
139 | payload: data,
140 | });
141 | } catch (error) {
142 | dispatch({
143 | type: USER_DETAILS_FAIL,
144 | payload:
145 | error.response && error.response.data.message
146 | ? error.response.data.message
147 | : error.message,
148 | });
149 | }
150 | };
151 |
152 | export const updateUserProfile = (user) => async (dispatch, getState) => {
153 | try {
154 | dispatch({
155 | type: USER_UPDATE_PROFILE_REQUEST,
156 | });
157 |
158 | const {
159 | userLogin: { userInfo },
160 | } = getState();
161 |
162 | const config = {
163 | headers: {
164 | "Content-Type": "application/json",
165 | Authorization: `Bearer ${userInfo.token}`,
166 | },
167 | };
168 |
169 | const { data } = await axios.put(`/api/users/profile`, user, config);
170 |
171 | dispatch({
172 | type: USER_UPDATE_PROFILE_SUCCESS,
173 | payload: data,
174 | });
175 |
176 | dispatch({
177 | type: USER_LOGIN_SUCCESS,
178 | payload: data,
179 | });
180 |
181 | localStorage.setItem("userInfo", JSON.stringify(data));
182 | } catch (error) {
183 | dispatch({
184 | type: USER_UPDATE_PROFILE_FAIL,
185 | payload:
186 | error.response && error.response.data.message
187 | ? error.response.data.message
188 | : error.message,
189 | });
190 | }
191 | };
192 |
193 | export const listUsers = () => async (dispatch, getState) => {
194 | try {
195 | dispatch({
196 | type: USER_LIST_REQUEST,
197 | });
198 |
199 | const {
200 | userLogin: { userInfo },
201 | } = getState();
202 |
203 | const config = {
204 | headers: {
205 | Authorization: `Bearer ${userInfo.token}`,
206 | },
207 | };
208 |
209 | const { data } = await axios.get(`/api/users`, config);
210 |
211 | dispatch({
212 | type: USER_LIST_SUCCESS,
213 | payload: data,
214 | });
215 | } catch (error) {
216 | dispatch({
217 | type: USER_LIST_FAIL,
218 | payload:
219 | error.response && error.response.data.message
220 | ? error.response.data.message
221 | : error.message,
222 | });
223 | }
224 | };
225 |
226 | export const deleteUser = (id) => async (dispatch, getState) => {
227 | try {
228 | dispatch({
229 | type: USER_DELETE_REQUEST,
230 | });
231 |
232 | const {
233 | userLogin: { userInfo },
234 | } = getState();
235 |
236 | const config = {
237 | headers: {
238 | Authorization: `Bearer ${userInfo.token}`,
239 | },
240 | };
241 |
242 | await axios.delete(`/api/users/${id}`, config);
243 |
244 | dispatch({
245 | type: USER_DELETE_SUCCESS,
246 | });
247 | } catch (error) {
248 | dispatch({
249 | type: USER_DELETE_FAIL,
250 | payload:
251 | error.response && error.response.data.message
252 | ? error.response.data.message
253 | : error.message,
254 | });
255 | }
256 | };
257 |
258 | export const updateUser = (user) => async (dispatch, getState) => {
259 | try {
260 | dispatch({
261 | type: USER_UPDATE_REQUEST,
262 | });
263 |
264 | const {
265 | userLogin: { userInfo },
266 | } = getState();
267 |
268 | const config = {
269 | headers: {
270 | "Content-Type": "application/json",
271 | Authorization: `Bearer ${userInfo.token}`,
272 | },
273 | };
274 |
275 | const { data } = await axios.put(`/api/users/${user._id}/`, user, config);
276 |
277 | dispatch({
278 | type: USER_UPDATE_SUCCESS,
279 | });
280 | dispatch({
281 | type: USER_DETAILS_SUCCESS,
282 | payload: data,
283 | });
284 | } catch (error) {
285 | dispatch({
286 | type: USER_UPDATE_FAIL,
287 | payload:
288 | error.response && error.response.data.message
289 | ? error.response.data.message
290 | : error.message,
291 | });
292 | }
293 | };
294 |
--------------------------------------------------------------------------------
/frontend/src/assets/cart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mariosknl/ProShop/97c26df85e134c7beccac1e892bbd57b974f4d34/frontend/src/assets/cart.png
--------------------------------------------------------------------------------
/frontend/src/assets/homepage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mariosknl/ProShop/97c26df85e134c7beccac1e892bbd57b974f4d34/frontend/src/assets/homepage.png
--------------------------------------------------------------------------------
/frontend/src/assets/paymentPage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mariosknl/ProShop/97c26df85e134c7beccac1e892bbd57b974f4d34/frontend/src/assets/paymentPage.png
--------------------------------------------------------------------------------
/frontend/src/assets/placeOrder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mariosknl/ProShop/97c26df85e134c7beccac1e892bbd57b974f4d34/frontend/src/assets/placeOrder.png
--------------------------------------------------------------------------------
/frontend/src/assets/product.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mariosknl/ProShop/97c26df85e134c7beccac1e892bbd57b974f4d34/frontend/src/assets/product.png
--------------------------------------------------------------------------------
/frontend/src/assets/shipping.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mariosknl/ProShop/97c26df85e134c7beccac1e892bbd57b974f4d34/frontend/src/assets/shipping.png
--------------------------------------------------------------------------------
/frontend/src/assets/signin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mariosknl/ProShop/97c26df85e134c7beccac1e892bbd57b974f4d34/frontend/src/assets/signin.png
--------------------------------------------------------------------------------
/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 |
48 | );
49 | };
50 |
51 | export default CheckoutSteps;
52 |
--------------------------------------------------------------------------------
/frontend/src/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Container, Row, Col } from "react-bootstrap";
3 |
4 | const Footer = () => {
5 | return (
6 |
13 | );
14 | };
15 |
16 | export default Footer;
17 |
--------------------------------------------------------------------------------
/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/components/Header.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Route } from "react-router-dom";
3 | import { useDispatch, useSelector } from "react-redux";
4 | import { LinkContainer } from "react-router-bootstrap";
5 | import { Container, Nav, Navbar, NavDropdown } from "react-bootstrap";
6 |
7 | import SearchBox from "./SearchBox";
8 |
9 | import { logout } from "../actions/userActions";
10 |
11 | const Header = () => {
12 | const dispatch = useDispatch();
13 | const userLogin = useSelector((state) => state.userLogin);
14 |
15 | const { userInfo } = userLogin;
16 |
17 | const logoutHandler = () => {
18 | dispatch(logout());
19 | };
20 |
21 | return (
22 |
23 |
24 |
25 |
26 | ProShop
27 |
28 |
29 |
30 | } />
31 |
67 |
68 |
69 |
70 |
71 | );
72 | };
73 |
74 | export default Header;
75 |
--------------------------------------------------------------------------------
/frontend/src/components/Loader.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Spinner } from "react-bootstrap";
3 |
4 | const Loader = () => {
5 | return (
6 |
16 | Loading...
17 |
18 | );
19 | };
20 |
21 | export default Loader;
22 |
--------------------------------------------------------------------------------
/frontend/src/components/Message.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Alert } from "react-bootstrap";
3 |
4 | const Message = ({ variant, children }) => {
5 | return {children};
6 | };
7 |
8 | Message.defaultProps = {
9 | variant: "info",
10 | };
11 | export default Message;
12 |
--------------------------------------------------------------------------------
/frontend/src/components/Meta.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Helmet } from "react-helmet";
3 |
4 | const Meta = ({ title, description, keywords }) => {
5 | return (
6 |
7 | {title}
8 |
9 |
10 |
11 | );
12 | };
13 |
14 | Meta.defaultProps = {
15 | title: "Welcome to Proshop",
16 | description: "We sell the best products for cheap",
17 | keywords: "electronics, buy electronics, cheap electronics",
18 | };
19 |
20 | export default Meta;
21 |
--------------------------------------------------------------------------------
/frontend/src/components/Paginate.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Pagination } from "react-bootstrap";
3 | import { LinkContainer } from "react-router-bootstrap";
4 |
5 | const Paginate = ({ pages, page, isAdmin = false, keyword = "" }) => {
6 | return (
7 | pages > 1 && (
8 |
9 | {[...Array(pages).keys()].map((x) => (
10 |
20 | {x + 1}
21 |
22 | ))}
23 |
24 | )
25 | );
26 | };
27 |
28 | export default Paginate;
29 |
--------------------------------------------------------------------------------
/frontend/src/components/Product.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Card } from "react-bootstrap";
3 | import { Link } from "react-router-dom";
4 |
5 | import Rating from "./Rating";
6 |
7 | const Product = ({ product }) => {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | {product.name}
18 |
19 |
20 |
21 |
22 |
26 |
27 |
28 | ${product.price}
29 |
30 |
31 | );
32 | };
33 |
34 | export default Product;
35 |
--------------------------------------------------------------------------------
/frontend/src/components/ProductCarousel.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { Link } from "react-router-dom";
4 | import { Carousel, Image } from "react-bootstrap";
5 | import Loader from "./Loader";
6 | import Message from "./Message";
7 | import { listTopProducts } from "../actions/productActions";
8 |
9 | const ProductCarousel = () => {
10 | const dispatch = useDispatch();
11 |
12 | const productTopRated = useSelector((state) => state.productTopRated);
13 | const { loading, error, products } = productTopRated;
14 |
15 | useEffect(() => {
16 | dispatch(listTopProducts());
17 | }, [dispatch]);
18 |
19 | return loading ? (
20 |
21 | ) : error ? (
22 | {error}
23 | ) : (
24 |
25 | {products.map((product) => (
26 |
27 |
28 |
29 |
30 |
31 | {product.name} (${product.price})
32 |
33 |
34 |
35 |
36 | ))}
37 |
38 | );
39 | };
40 |
41 | export default ProductCarousel;
42 |
--------------------------------------------------------------------------------
/frontend/src/components/Rating.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | const Rating = ({ value, text, color }) => {
5 | return (
6 |
7 |
8 | = 1
12 | ? "fas fa-star"
13 | : value >= 0.5
14 | ? "fas fa-star-half-alt"
15 | : "far fa-star"
16 | }
17 | >
18 |
19 |
20 | = 2
24 | ? "fas fa-star"
25 | : value >= 1.5
26 | ? "fas fa-star-half-alt"
27 | : "far fa-star"
28 | }
29 | >
30 |
31 |
32 | = 3
36 | ? "fas fa-star"
37 | : value >= 2.5
38 | ? "fas fa-star-half-alt"
39 | : "far fa-star"
40 | }
41 | >
42 |
43 |
44 | = 4
48 | ? "fas fa-star"
49 | : value >= 3.5
50 | ? "fas fa-star-half-alt"
51 | : "far fa-star"
52 | }
53 | >
54 |
55 |
56 | = 5
60 | ? "fas fa-star"
61 | : value >= 4.5
62 | ? "fas fa-star-half-alt"
63 | : "far fa-star"
64 | }
65 | >
66 |
67 | {text && text}
68 |
69 | );
70 | };
71 |
72 | Rating.defaultProps = {
73 | color: "#f8e825",
74 | };
75 |
76 | Rating.propTypes = {
77 | value: PropTypes.number.isRequired,
78 | text: PropTypes.string.isRequired,
79 | color: PropTypes.string,
80 | };
81 |
82 | export default Rating;
83 |
--------------------------------------------------------------------------------
/frontend/src/components/SearchBox.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Form, Button } from "react-bootstrap";
3 |
4 | const SearchBox = ({ history }) => {
5 | const [keyword, setKeyword] = useState("");
6 |
7 | const submitHandler = (e) => {
8 | e.preventDefault();
9 | if (keyword.trim()) {
10 | history.push(`/search/${keyword}`);
11 | } else {
12 | history.push("/");
13 | }
14 | };
15 |
16 | return (
17 | setKeyword(e.target.value)}
22 | placeholder="Search Products..."
23 | className="mr-sm-2 ml-sm-5"
24 | >
25 |
28 |
29 | );
30 | };
31 |
32 | export default SearchBox;
33 |
--------------------------------------------------------------------------------
/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/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 ORDERS_LIST_REQUEST = "ORDER_LIST_REQUEST";
20 | export const ORDERS_LIST_SUCCESS = "ORDER_LIST_SUCCESS";
21 | export const ORDERS_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/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_DETAILS_REQUEST = "PRODUCT_DETAILS_REQUEST";
6 | export const PRODUCT_DETAILS_SUCCESS = "PRODUCT_DETAILS_SUCCESS";
7 | export const PRODUCT_DETAILS_FAIL = "PRODUCT_DETAILS_FAIL";
8 |
9 | export const PRODUCT_DELETE_REQUEST = "PRODUCT_DELETE_REQUEST";
10 | export const PRODUCT_DELETE_SUCCESS = "PRODUCT_DELETE_SUCCESS";
11 | export const PRODUCT_DELETE_FAIL = "PRODUCT_DELETE_FAIL";
12 |
13 | export const PRODUCT_CREATE_REQUEST = "PRODUCT_CREATE_REQUEST";
14 | export const PRODUCT_CREATE_SUCCESS = "PRODUCT_CREATE_SUCCESS";
15 | export const PRODUCT_CREATE_FAIL = "PRODUCT_CREATE_FAIL";
16 | export const PRODUCT_CREATE_RESET = "PRODUCT_CREATE_RESET";
17 |
18 | export const PRODUCT_UPDATE_REQUEST = "PRODUCT_UPDATE_REQUEST";
19 | export const PRODUCT_UPDATE_SUCCESS = "PRODUCT_UPDATE_SUCCESS";
20 | export const PRODUCT_UPDATE_FAIL = "PRODUCT_UPDATE_FAIL";
21 | export const PRODUCT_UPDATE_RESET = "PRODUCT_UPDATE_RESET";
22 |
23 | export const PRODUCT_CREATE_REVIEW_REQUEST = "PRODUCT_CREATE_REVIEW_REQUEST";
24 | export const PRODUCT_CREATE_REVIEW_SUCCESS = "PRODUCT_CREATE_REVIEW_SUCCESS";
25 | export const PRODUCT_CREATE_REVIEW_FAIL = "PRODUCT_CREATE_REVIEW_FAIL";
26 | export const PRODUCT_CREATE_REVIEW_RESET = "PRODUCT_CREATE_REVIEW_RESET";
27 |
28 | export const PRODUCT_TOP_REQUEST = "PRODUCT_TOP_REQUEST";
29 | export const PRODUCT_TOP_SUCCESS = "PRODUCT_TOP_SUCCESS";
30 | export const PRODUCT_TOP_FAIL = "PRODUCT_TOP_FAIL";
31 |
--------------------------------------------------------------------------------
/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_DETAILS_REQUEST = "USER_DETAILS_REQUEST";
11 | export const USER_DETAILS_SUCCESS = "USER_DETAILS_SUCCESS";
12 | export const USER_DETAILS_FAIL = "USER_DETAILS_FAIL";
13 | export const USER_DETAILS_RESET = "USER_DETAILS_RESET";
14 |
15 | export const USER_UPDATE_PROFILE_REQUEST = "USER_UPDATE_PROFILE_REQUEST";
16 | export const USER_UPDATE_PROFILE_SUCCESS = "USER_UPDATE_PROFILE_SUCCESS";
17 | export const USER_UPDATE_PROFILE_FAIL = "USER_UPDATE_PROFILE_FAIL";
18 | export const USER_UPDATE_PROFILE_RESET = "USER_UPDATE_PROFILE_RESET";
19 |
20 | export const USER_LIST_REQUEST = "USER_LIST_REQUEST";
21 | export const USER_LIST_SUCCESS = "USER_LIST_SUCCESS";
22 | export const USER_LIST_FAIL = "USER_LIST_FAIL";
23 | export const USER_LIST_RESET = "USER_LIST_RESET";
24 |
25 | export const USER_DELETE_REQUEST = "USER_DELETE_REQUEST";
26 | export const USER_DELETE_SUCCESS = "USER_DELETE_SUCCESS";
27 | export const USER_DELETE_FAIL = "USER_DELETE_FAIL";
28 |
29 | export const USER_UPDATE_REQUEST = "USER_UPDATE_REQUEST";
30 | export const USER_UPDATE_SUCCESS = "USER_UPDATE_SUCCESS";
31 | export const USER_UPDATE_FAIL = "USER_UPDATE_FAIL";
32 | export const USER_UPDATE_RESET = "USER_UPDATE_RESET";
33 |
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | main {
2 | min-height: 80vh;
3 | }
4 |
5 | h3 {
6 | padding: 1rem 0;
7 | }
8 |
9 | h1 {
10 | font-size: 1.8rem;
11 | padding: 1rem 0;
12 | }
13 |
14 | h2 {
15 | font-size: 1.4rem;
16 | padding: 0.5rem 0;
17 | }
18 |
19 | .rating span {
20 | margin: 0.1rem;
21 | }
22 |
23 | /* carousel */
24 | .carousel-item-next,
25 | .carousel-item-prev,
26 | .carousel-item.active {
27 | display: flex;
28 | }
29 | .carousel-caption {
30 | position: absolute;
31 | top: 0;
32 | }
33 |
34 | .carousel-caption h2 {
35 | color: #fff;
36 | }
37 |
38 | .carousel img {
39 | height: 300px;
40 | padding: 30px;
41 | margin: 40px;
42 | border-radius: 50%;
43 | margin-left: auto;
44 | margin-right: auto;
45 | }
46 | .carousel a {
47 | margin: 0 auto;
48 | }
49 | @media (max-width: 900px) {
50 | .carousel-caption h2 {
51 | font-size: 2.5vw;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/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 |
6 | import "./bootstrap.min.css";
7 | import "./index.css";
8 | import App from "./App";
9 | import reportWebVitals from "./reportWebVitals";
10 |
11 | ReactDOM.render(
12 |
13 |
14 | ,
15 | document.getElementById("root")
16 | );
17 |
18 | // If you want to start measuring performance in your app, pass a function
19 | // to log results (for example: reportWebVitals(console.log))
20 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
21 | reportWebVitals();
22 |
--------------------------------------------------------------------------------
/frontend/src/reducers/cartReducers.js:
--------------------------------------------------------------------------------
1 | import {
2 | CART_ADD_ITEM,
3 | CART_REMOVE_ITEM,
4 | CART_SAVE_SHIPPING_ADDRESS,
5 | CART_SAVE_PAYMENT_METHOD,
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 |
16 | const existItem = state.cartItems.find((x) => x.product === item.product);
17 |
18 | if (existItem) {
19 | return {
20 | ...state,
21 | cartItems: state.cartItems.map((x) =>
22 | x.product === existItem.product ? item : x
23 | ),
24 | };
25 | } else {
26 | return {
27 | ...state,
28 | cartItems: [...state.cartItems, item],
29 | };
30 | }
31 | case CART_REMOVE_ITEM:
32 | return {
33 | ...state,
34 | cartItems: state.cartItems.filter((x) => x.product !== action.payload),
35 | };
36 | case CART_SAVE_SHIPPING_ADDRESS:
37 | return {
38 | ...state,
39 | shippingAddress: action.payload,
40 | };
41 | case CART_SAVE_PAYMENT_METHOD:
42 | return {
43 | ...state,
44 | paymentMethod: action.payload,
45 | };
46 | default:
47 | return state;
48 | }
49 | };
50 |
--------------------------------------------------------------------------------
/frontend/src/reducers/orderReducers.js:
--------------------------------------------------------------------------------
1 | import {
2 | ORDER_CREATE_FAIL,
3 | ORDER_CREATE_REQUEST,
4 | ORDER_CREATE_SUCCESS,
5 | ORDER_DETAILS_REQUEST,
6 | ORDER_DETAILS_SUCCESS,
7 | ORDER_DETAILS_FAIL,
8 | ORDER_PAY_REQUEST,
9 | ORDER_PAY_SUCCESS,
10 | ORDER_PAY_FAIL,
11 | ORDER_PAY_RESET,
12 | ORDER_LIST_MY_REQUEST,
13 | ORDER_LIST_MY_SUCCESS,
14 | ORDER_LIST_MY_FAIL,
15 | ORDER_LIST_MY_RESET,
16 | ORDERS_LIST_REQUEST,
17 | ORDERS_LIST_SUCCESS,
18 | ORDERS_LIST_FAIL,
19 | ORDER_DELIVER_REQUEST,
20 | ORDER_DELIVER_SUCCESS,
21 | ORDER_DELIVER_FAIL,
22 | ORDER_DELIVER_RESET,
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 | default:
43 | return state;
44 | }
45 | };
46 |
47 | export const orderDetailsReducer = (
48 | state = { loading: true, orderItems: [], shippingAddress: {} },
49 | action
50 | ) => {
51 | switch (action.type) {
52 | case ORDER_DETAILS_REQUEST:
53 | return {
54 | ...state,
55 | loading: true,
56 | };
57 | case ORDER_DETAILS_SUCCESS:
58 | return {
59 | loading: false,
60 | order: action.payload,
61 | };
62 | case ORDER_DETAILS_FAIL:
63 | return {
64 | loading: false,
65 | error: action.payload,
66 | };
67 | default:
68 | return state;
69 | }
70 | };
71 |
72 | export const orderPayReducer = (state = {}, action) => {
73 | switch (action.type) {
74 | case ORDER_PAY_REQUEST:
75 | return {
76 | loading: true,
77 | };
78 | case ORDER_PAY_SUCCESS:
79 | return {
80 | loading: false,
81 | success: true,
82 | };
83 | case ORDER_PAY_FAIL:
84 | return {
85 | loading: false,
86 | error: action.payload,
87 | };
88 | case ORDER_PAY_RESET:
89 | return {};
90 | default:
91 | return state;
92 | }
93 | };
94 |
95 | export const orderDeliverReducer = (state = {}, action) => {
96 | switch (action.type) {
97 | case ORDER_DELIVER_REQUEST:
98 | return {
99 | loading: true,
100 | };
101 | case ORDER_DELIVER_SUCCESS:
102 | return {
103 | loading: false,
104 | success: true,
105 | };
106 | case ORDER_DELIVER_FAIL:
107 | return {
108 | loading: false,
109 | error: action.payload,
110 | };
111 | case ORDER_DELIVER_RESET:
112 | return {};
113 | default:
114 | return state;
115 | }
116 | };
117 |
118 | export const orderListMyReducer = (state = { orders: [] }, action) => {
119 | switch (action.type) {
120 | case ORDER_LIST_MY_REQUEST:
121 | return {
122 | loading: true,
123 | };
124 | case ORDER_LIST_MY_SUCCESS:
125 | return {
126 | loading: false,
127 | orders: action.payload,
128 | };
129 | case ORDER_LIST_MY_FAIL:
130 | return {
131 | loading: false,
132 | error: action.payload,
133 | };
134 | case ORDER_LIST_MY_RESET:
135 | return {
136 | orders: [],
137 | };
138 | default:
139 | return state;
140 | }
141 | };
142 |
143 | export const orderListReducer = (state = { orders: [] }, action) => {
144 | switch (action.type) {
145 | case ORDERS_LIST_REQUEST:
146 | return {
147 | loading: true,
148 | };
149 | case ORDERS_LIST_SUCCESS:
150 | return {
151 | loading: false,
152 | orders: action.payload,
153 | };
154 | case ORDERS_LIST_FAIL:
155 | return {
156 | loading: false,
157 | error: action.payload,
158 | };
159 | default:
160 | return state;
161 | }
162 | };
163 |
--------------------------------------------------------------------------------
/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_DELETE_REQUEST,
9 | PRODUCT_DELETE_SUCCESS,
10 | PRODUCT_DELETE_FAIL,
11 | PRODUCT_CREATE_RESET,
12 | PRODUCT_CREATE_REQUEST,
13 | PRODUCT_CREATE_SUCCESS,
14 | PRODUCT_CREATE_FAIL,
15 | PRODUCT_UPDATE_REQUEST,
16 | PRODUCT_UPDATE_SUCCESS,
17 | PRODUCT_UPDATE_FAIL,
18 | PRODUCT_UPDATE_RESET,
19 | PRODUCT_CREATE_REVIEW_REQUEST,
20 | PRODUCT_CREATE_REVIEW_SUCCESS,
21 | PRODUCT_CREATE_REVIEW_FAIL,
22 | PRODUCT_CREATE_REVIEW_RESET,
23 | PRODUCT_TOP_REQUEST,
24 | PRODUCT_TOP_SUCCESS,
25 | PRODUCT_TOP_FAIL,
26 | } from "../constants/productConstants";
27 |
28 | export const productListReducer = (state = { products: [] }, action) => {
29 | switch (action.type) {
30 | case PRODUCT_LIST_REQUEST:
31 | return { loading: true, products: [] };
32 | case PRODUCT_LIST_SUCCESS:
33 | return {
34 | loading: false,
35 | products: action.payload.products,
36 | pages: action.payload.pages,
37 | page: action.payload.page,
38 | };
39 | case PRODUCT_LIST_FAIL:
40 | return { loading: false, error: action.payload };
41 | default:
42 | return state;
43 | }
44 | };
45 |
46 | export const productDetailsReducer = (
47 | state = { product: { reviews: [] } },
48 | action
49 | ) => {
50 | switch (action.type) {
51 | case PRODUCT_DETAILS_REQUEST:
52 | return { loading: true, ...state };
53 | case PRODUCT_DETAILS_SUCCESS:
54 | return { loading: false, product: action.payload };
55 | case PRODUCT_DETAILS_FAIL:
56 | return { loading: false, error: action.payload };
57 | default:
58 | return state;
59 | }
60 | };
61 |
62 | export const productDeleteReducer = (state = { product: {} }, action) => {
63 | switch (action.type) {
64 | case PRODUCT_DELETE_REQUEST:
65 | return { loading: true };
66 | case PRODUCT_DELETE_SUCCESS:
67 | return { loading: false, success: true };
68 | case PRODUCT_DELETE_FAIL:
69 | return { loading: false, error: action.payload };
70 | default:
71 | return state;
72 | }
73 | };
74 |
75 | export const productCreateReducer = (state = {}, action) => {
76 | switch (action.type) {
77 | case PRODUCT_CREATE_REQUEST:
78 | return { loading: true };
79 | case PRODUCT_CREATE_SUCCESS:
80 | return { loading: false, success: true, product: action.payload };
81 | case PRODUCT_CREATE_FAIL:
82 | return { loading: false, error: action.payload };
83 | case PRODUCT_CREATE_RESET:
84 | return {};
85 | default:
86 | return state;
87 | }
88 | };
89 |
90 | export const productUpdateReducer = (state = { product: {} }, action) => {
91 | switch (action.type) {
92 | case PRODUCT_UPDATE_REQUEST:
93 | return { loading: true };
94 | case PRODUCT_UPDATE_SUCCESS:
95 | return { loading: false, success: true, product: action.payload };
96 | case PRODUCT_UPDATE_FAIL:
97 | return { loading: false, error: action.payload };
98 | case PRODUCT_UPDATE_RESET:
99 | return { product: {} };
100 | default:
101 | return state;
102 | }
103 | };
104 |
105 | export const productReviewCreateReducer = (state = {}, action) => {
106 | switch (action.type) {
107 | case PRODUCT_CREATE_REVIEW_REQUEST:
108 | return { loading: true };
109 | case PRODUCT_CREATE_REVIEW_SUCCESS:
110 | return { loading: false, success: true };
111 | case PRODUCT_CREATE_REVIEW_FAIL:
112 | return { loading: false, error: action.payload };
113 | case PRODUCT_CREATE_REVIEW_RESET:
114 | return {};
115 | default:
116 | return state;
117 | }
118 | };
119 |
120 | export const productTopRatedReducer = (state = { products: [] }, action) => {
121 | switch (action.type) {
122 | case PRODUCT_TOP_REQUEST:
123 | return { loading: true, products: [] };
124 | case PRODUCT_TOP_SUCCESS:
125 | return { loading: false, products: action.payload };
126 | case PRODUCT_TOP_FAIL:
127 | return { loading: false, error: action.payload };
128 | default:
129 | return state;
130 | }
131 | };
132 |
--------------------------------------------------------------------------------
/frontend/src/reducers/userReducers.js:
--------------------------------------------------------------------------------
1 | import {
2 | USER_DETAILS_FAIL,
3 | USER_DETAILS_REQUEST,
4 | USER_DETAILS_RESET,
5 | USER_DETAILS_SUCCESS,
6 | USER_LIST_FAIL,
7 | USER_LIST_REQUEST,
8 | USER_LIST_SUCCESS,
9 | USER_LIST_RESET,
10 | USER_LOGIN_FAIL,
11 | USER_LOGIN_REQUEST,
12 | USER_LOGIN_SUCCESS,
13 | USER_LOGOUT,
14 | USER_REGISTER_FAIL,
15 | USER_REGISTER_REQUEST,
16 | USER_REGISTER_SUCCESS,
17 | USER_UPDATE_PROFILE_FAIL,
18 | USER_UPDATE_PROFILE_REQUEST,
19 | USER_UPDATE_PROFILE_SUCCESS,
20 | USER_UPDATE_PROFILE_RESET,
21 | USER_DELETE_REQUEST,
22 | USER_DELETE_SUCCESS,
23 | USER_DELETE_FAIL,
24 | USER_UPDATE_RESET,
25 | USER_UPDATE_FAIL,
26 | USER_UPDATE_SUCCESS,
27 | USER_UPDATE_REQUEST,
28 | } from "../constants/userConstants";
29 |
30 | export const userLoginReducer = (state = {}, action) => {
31 | switch (action.type) {
32 | case USER_LOGIN_REQUEST:
33 | return { loading: true };
34 | case USER_LOGIN_SUCCESS:
35 | return { loading: false, userInfo: action.payload };
36 | case USER_LOGIN_FAIL:
37 | return { loading: false, error: action.payload };
38 | case USER_LOGOUT:
39 | return {};
40 | default:
41 | return state;
42 | }
43 | };
44 |
45 | export const userRegisterReducer = (state = {}, action) => {
46 | switch (action.type) {
47 | case USER_REGISTER_REQUEST:
48 | return { loading: true };
49 | case USER_REGISTER_SUCCESS:
50 | return { loading: false, userInfo: action.payload };
51 | case USER_REGISTER_FAIL:
52 | return { loading: false, error: action.payload };
53 | default:
54 | return state;
55 | }
56 | };
57 |
58 | export const userDetailsReducer = (state = { user: {} }, action) => {
59 | switch (action.type) {
60 | case USER_DETAILS_REQUEST:
61 | return { ...state, loading: true };
62 | case USER_DETAILS_SUCCESS:
63 | return { loading: false, user: action.payload };
64 | case USER_DETAILS_FAIL:
65 | return { loading: false, error: action.payload };
66 | case USER_DETAILS_RESET:
67 | return { user: {} };
68 | default:
69 | return state;
70 | }
71 | };
72 |
73 | export const userUpdateProfileReducer = (state = {}, action) => {
74 | switch (action.type) {
75 | case USER_UPDATE_PROFILE_REQUEST:
76 | return { loading: true };
77 | case USER_UPDATE_PROFILE_SUCCESS:
78 | return { loading: false, success: true, userInfo: action.payload };
79 | case USER_UPDATE_PROFILE_FAIL:
80 | return { loading: false, error: action.payload };
81 | case USER_UPDATE_PROFILE_RESET:
82 | return {};
83 | default:
84 | return state;
85 | }
86 | };
87 |
88 | export const userListReducer = (state = { users: [] }, action) => {
89 | switch (action.type) {
90 | case USER_LIST_REQUEST:
91 | return { loading: true };
92 | case USER_LIST_SUCCESS:
93 | return { loading: false, users: action.payload };
94 | case USER_LIST_FAIL:
95 | return { loading: false, error: action.payload };
96 | case USER_LIST_RESET:
97 | return { users: [] };
98 | default:
99 | return state;
100 | }
101 | };
102 |
103 | export const userDeleteReducer = (state = {}, action) => {
104 | switch (action.type) {
105 | case USER_DELETE_REQUEST:
106 | return { loading: true };
107 | case USER_DELETE_SUCCESS:
108 | return { loading: false, success: true };
109 | case USER_DELETE_FAIL:
110 | return { loading: false, error: action.payload };
111 | default:
112 | return state;
113 | }
114 | };
115 |
116 | export const userUpdateReducer = (state = { user: {} }, action) => {
117 | switch (action.type) {
118 | case USER_UPDATE_REQUEST:
119 | return { loading: true };
120 | case USER_UPDATE_SUCCESS:
121 | return { loading: false, success: true };
122 | case USER_UPDATE_FAIL:
123 | return { loading: false, error: action.payload };
124 | case USER_UPDATE_RESET:
125 | return {
126 | user: {},
127 | };
128 | default:
129 | return state;
130 | }
131 | };
132 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/frontend/src/screens/CartScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { Link } from "react-router-dom";
4 | import {
5 | Row,
6 | Col,
7 | ListGroup,
8 | Image,
9 | Form,
10 | Button,
11 | Card,
12 | } from "react-bootstrap";
13 | import { addToCart, removeFromCart } from "../actions/cartActions";
14 |
15 | import Message from "../components/Message";
16 |
17 | const CartScreen = ({ match, location, history }) => {
18 | const productId = match.params.id;
19 |
20 | const qty = location.search ? Number(location.search.split("=")[1]) : 1;
21 |
22 | const dispatch = useDispatch();
23 |
24 | const cart = useSelector((state) => state.cart);
25 | const { cartItems } = cart;
26 |
27 | useEffect(() => {
28 | if (productId) {
29 | dispatch(addToCart(productId, qty));
30 | }
31 | }, [dispatch, productId, qty]);
32 |
33 | const removeFromCartHandler = (id) => {
34 | dispatch(removeFromCart(id));
35 | };
36 |
37 | const checkoutHandler = () => {
38 | history.push("/login?redirect=shipping");
39 | };
40 |
41 | return (
42 |
43 |
44 | Shopping Cart
45 | {cartItems.length === 0 ? (
46 |
47 | Your cart is empty Go Back
48 |
49 | ) : (
50 |
51 | {cartItems.map((item) => (
52 |
53 |
54 |
55 |
56 |
57 |
58 | {item.name}
59 |
60 | ${item.price}
61 |
62 |
66 | dispatch(
67 | addToCart(item.product, Number(e.target.value))
68 | )
69 | }
70 | >
71 | {[...Array(item.countInStock).keys()].map((x) => (
72 |
75 | ))}
76 |
77 |
78 |
79 |
86 |
87 |
88 |
89 | ))}
90 |
91 | )}
92 |
93 |
94 |
95 |
96 |
97 |
98 | Subtotal ({cartItems.reduce((acc, item) => acc + item.qty, 0)})
99 | items{" "}
100 |
101 | $
102 | {cartItems
103 | .reduce((acc, item) => acc + item.qty * item.price, 0)
104 | .toFixed(2)}
105 |
106 |
107 |
115 |
116 |
117 |
118 |
119 |
120 | );
121 | };
122 |
123 | export default CartScreen;
124 |
--------------------------------------------------------------------------------
/frontend/src/screens/HomeScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { Link } from "react-router-dom";
3 | import { useDispatch, useSelector } from "react-redux";
4 | import { Col, Row } from "react-bootstrap";
5 |
6 | import Product from "../components/Product";
7 | import { listProducts } from "../actions/productActions";
8 | import Message from "../components/Message";
9 | import Loader from "../components/Loader";
10 | import Paginate from "../components/Paginate";
11 | import Meta from "../components/Meta";
12 | import ProductCarousel from "../components/ProductCarousel";
13 |
14 | const HomeScreen = ({ match }) => {
15 | const keyword = match.params.keyword;
16 |
17 | const pageNumber = match.params.pageNumber || 1;
18 |
19 | const dispatch = useDispatch();
20 |
21 | const productList = useSelector((state) => state.productList);
22 | const { loading, error, products, page, pages } = productList;
23 |
24 | useEffect(() => {
25 | dispatch(listProducts(keyword, pageNumber));
26 | }, [dispatch, keyword, pageNumber]);
27 |
28 | return (
29 | <>
30 |
31 | {!keyword ? (
32 |
33 | ) : (
34 |
35 | Go back
36 |
37 | )}
38 | Latest Products
39 | {loading ? (
40 |
41 | ) : error ? (
42 | {error}
43 | ) : (
44 | <>
45 |
46 | {products.map((product) => (
47 |
48 |
49 |
50 | ))}
51 |
52 |
57 | >
58 | )}
59 | >
60 | );
61 | };
62 |
63 | export default HomeScreen;
64 |
--------------------------------------------------------------------------------
/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 |
6 | import Message from "../components/Message";
7 | import Loader from "../components/Loader";
8 | import FormContainer from "../components/FormContainer";
9 | import { login } from "../actions/userActions";
10 |
11 | const LoginScreen = ({ location, history }) => {
12 | const [email, setEmail] = useState("");
13 | const [password, setPassword] = useState("");
14 |
15 | const dispatch = useDispatch();
16 |
17 | const userLogin = useSelector((state) => state.userLogin);
18 | const { loading, error, userInfo } = userLogin;
19 |
20 | const redirect = location.search ? location.search.split("=")[1] : "/";
21 |
22 | useEffect(() => {
23 | if (userInfo) {
24 | history.push(redirect);
25 | }
26 | }, [history, userInfo, redirect]);
27 |
28 | const submitHandler = (e) => {
29 | e.preventDefault();
30 | dispatch(login(email, password));
31 | };
32 |
33 | return (
34 |
35 | Sign In
36 | {error && {error}}
37 | {loading && }
38 |
40 | Email Address
41 | setEmail(e.target.value)}
46 | >
47 |
48 |
49 |
50 | Password
51 | setPassword(e.target.value)}
56 | >
57 |
58 |
59 |
62 |
63 |
64 |
65 |
66 | New Customer?{" "}
67 |
68 | Register
69 |
70 |
71 |
72 |
73 | );
74 | };
75 |
76 | export default LoginScreen;
77 |
--------------------------------------------------------------------------------
/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 |
6 | import Message from "../components/Message";
7 | import Loader from "../components/Loader";
8 | import { listOrders } from "../actions/orderActions";
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.pushState("/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 | ${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 |
73 |
74 | |
75 |
76 | ))}
77 |
78 |
79 | )}
80 | >
81 | );
82 | };
83 |
84 | export default OrderListScreen;
85 |
--------------------------------------------------------------------------------
/frontend/src/screens/OrderScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import axios from "axios";
3 | import { PayPalButton } from "react-paypal-button-v2";
4 | import { Row, Col, ListGroup, Image, Card, Button } from "react-bootstrap";
5 | import { useDispatch, useSelector } from "react-redux";
6 |
7 | import Message from "../components/Message";
8 | import Loader from "../components/Loader";
9 | import { Link } from "react-router-dom";
10 | import {
11 | getOrderDetails,
12 | payOrder,
13 | deliverOrder,
14 | } from "../actions/orderActions";
15 | import {
16 | ORDER_PAY_RESET,
17 | ORDER_DELIVER_RESET,
18 | } from "../constants/orderConstants";
19 |
20 | const OrderScreen = ({ match, history }) => {
21 | const orderId = match.params.id;
22 |
23 | const [sdkReady, setSdkReady] = useState(false);
24 |
25 | const dispatch = useDispatch();
26 |
27 | const orderDetails = useSelector((state) => state.orderDetails);
28 | const { order, loading, error } = orderDetails;
29 |
30 | const orderPay = useSelector((state) => state.orderPay);
31 | const { loading: loadingPay, success: successPay } = orderPay;
32 |
33 | const orderDeliver = useSelector((state) => state.orderDeliver);
34 | const { loading: loadingDeliver, success: successDeliver } = orderDeliver;
35 |
36 | const userLogin = useSelector((state) => state.userLogin);
37 | const { userInfo } = userLogin;
38 |
39 | if (!loading) {
40 | // Calculate prices
41 | const addDecimals = (num) => {
42 | return (Math.round(num * 100) / 100).toFixed(2);
43 | };
44 |
45 | order.itemsPrice = addDecimals(
46 | order.orderItems.reduce((acc, item) => acc + item.price * item.qty, 0)
47 | );
48 | }
49 |
50 | useEffect(() => {
51 | if (!userInfo) {
52 | history.push("/login");
53 | }
54 | const addPayPalScript = async () => {
55 | const { data: clientId } = await axios.get("/api/config/paypal");
56 | const script = document.createElement("script");
57 | script.type = "text/javascript";
58 | script.src = `https://www.paypal.com/sdk/js?client-id=${clientId}`;
59 | script.async = true;
60 | script.onload = () => {
61 | setSdkReady(true);
62 | };
63 | document.body.appendChild(script);
64 | };
65 |
66 | if (!order || successPay || successDeliver) {
67 | dispatch({ type: ORDER_PAY_RESET });
68 | dispatch({ type: ORDER_DELIVER_RESET });
69 | dispatch(getOrderDetails(orderId));
70 | } else if (!order.isPaid) {
71 | if (!window.paypal) {
72 | addPayPalScript();
73 | } else {
74 | setSdkReady(true);
75 | }
76 | }
77 | }, [dispatch, order, successPay, orderId, successDeliver]);
78 |
79 | const successPaymentHandler = (paymentResult) => {
80 | dispatch(payOrder(orderId, paymentResult));
81 | };
82 |
83 | const deliverHandler = () => {
84 | dispatch(deliverOrder(order));
85 | };
86 |
87 | return loading ? (
88 |
89 | ) : error ? (
90 | {error}
91 | ) : (
92 | <>
93 | Order {order._id}
94 |
95 |
96 |
97 |
98 | Shipping
99 |
100 | Name: {order.user.name}
101 |
102 |
103 | Email:
104 | {order.user.email}
105 |
106 |
107 | Address
108 | {order.shippingAddress.address},{order.shippingAddress.city},
109 | {order.shippingAddress.postalCode},
110 | {order.shippingAddress.country}
111 |
112 | {order.isDelivered ? (
113 |
114 | Delivered on {order.deliveredAt}
115 |
116 | ) : (
117 | Not Delivered
118 | )}
119 |
120 |
121 |
122 | Payment Method
123 |
124 | Method:
125 | {order.paymentMethod}
126 |
127 | {order.isPaid ? (
128 | Paid on {order.paidAt}
129 | ) : (
130 | Not Paid
131 | )}
132 |
133 |
134 |
135 | Order Items
136 | {order.orderItems.length === 0 ? (
137 | Order is empty
138 | ) : (
139 |
140 | {order.orderItems.map((item, index) => (
141 |
142 |
143 |
144 |
150 |
151 |
152 |
153 | {item.name}
154 |
155 |
156 |
157 | {item.qty} x ${item.price} = ${item.qty * item.price}
158 |
159 |
160 |
161 | ))}
162 |
163 | )}
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 | Order Summary
172 |
173 |
174 |
175 | Items
176 | ${order.itemsPrice}
177 |
178 |
179 |
180 |
181 |
182 | Shipping
183 | ${order.shippingPrice}
184 |
185 |
186 |
187 |
188 |
189 | Tax
190 | ${order.taxPrice}
191 |
192 |
193 |
194 |
195 |
196 | Total
197 | ${order.totalPrice}
198 |
199 |
200 |
201 | {!order.isPaid && (
202 |
203 | {loadingPay && }
204 | {!sdkReady ? (
205 |
206 | ) : (
207 |
211 | )}
212 |
213 | )}
214 | {loadingDeliver && }
215 | {userInfo &&
216 | userInfo.isAdmin &&
217 | order.isPaid &&
218 | !order.isDelivered && (
219 |
220 |
227 |
228 | )}
229 |
230 |
231 |
232 |
233 | >
234 | );
235 | };
236 |
237 | export default OrderScreen;
238 |
--------------------------------------------------------------------------------
/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 |
5 | import FormContainer from "../components/FormContainer";
6 | import CheckoutSteps from "../components/CheckoutSteps";
7 | import { savePaymentMethod } from "../actions/cartActions";
8 |
9 | const PaymentScreen = ({ history }) => {
10 | const cart = useSelector((state) => state.cart);
11 | const { shippingAddress } = cart;
12 |
13 | if (!shippingAddress) {
14 | history.push("/shipping");
15 | }
16 |
17 | const [paymentMethod, setpaymentMethod] = useState("PayPal");
18 |
19 | const dispatch = useDispatch();
20 |
21 | const submitHandler = (e) => {
22 | e.preventDefault();
23 | dispatch(savePaymentMethod(paymentMethod));
24 | history.push("/placeorder");
25 | };
26 |
27 | return (
28 |
29 |
30 | Payment Method
31 |
33 | Select Method
34 |
35 |
36 | setpaymentMethod(e.targe.value)}
44 | >
45 |
46 | {/* setpaymentMethod(e.targe.value)}
54 | > */}
55 |
56 |
57 |
58 |
61 |
62 |
63 | );
64 | };
65 |
66 | export default PaymentScreen;
67 |
--------------------------------------------------------------------------------
/frontend/src/screens/PlaceOrderScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { Button, Row, Col, ListGroup, Image, Card } from "react-bootstrap";
3 | import { useDispatch, useSelector } from "react-redux";
4 |
5 | import Message from "../components/Message";
6 | import CheckoutSteps from "../components/CheckoutSteps";
7 | import { Link } from "react-router-dom";
8 | import { createOrder } from "../actions/orderActions";
9 |
10 | const PlaceOrderScreen = ({ history }) => {
11 | const dispatch = useDispatch();
12 | const cart = useSelector((state) => state.cart);
13 |
14 | // Calculate prices
15 | const addDecimals = (num) => {
16 | return (Math.round(num * 100) / 100).toFixed(2);
17 | };
18 |
19 | cart.itemsPrice = addDecimals(
20 | cart.cartItems.reduce((acc, item) => acc + item.price * item.qty, 0)
21 | );
22 |
23 | cart.shippingPrice = addDecimals(cart.itemsPrice > 100 ? 0 : 100);
24 |
25 | cart.taxPrice = addDecimals(Number((0.15 * cart.itemsPrice).toFixed(2)));
26 |
27 | cart.totalPrice = (
28 | Number(cart.itemsPrice) +
29 | Number(cart.shippingPrice) +
30 | Number(cart.taxPrice)
31 | ).toFixed(2);
32 |
33 | const orderCreate = useSelector((state) => state.orderCreate);
34 | const { order, success, error } = orderCreate;
35 |
36 | useEffect(() => {
37 | if (success) {
38 | history.push(`/order/${order._id}`);
39 | }
40 | // eslint-disable-next-line
41 | }, [history, success]);
42 |
43 | const placeOrderHandler = () => {
44 | dispatch(
45 | createOrder({
46 | orderItems: cart.cartItems,
47 | shippingAddress: cart.shippingAddress,
48 | paymentMethod: cart.paymentMethod,
49 | itemsPrice: cart.itemsPrice,
50 | shippingPrice: cart.shippingPrice,
51 | taxPrice: cart.taxPrice,
52 | totalPrice: cart.totalPrice,
53 | })
54 | );
55 | };
56 | return (
57 | <>
58 |
59 |
60 |
61 |
62 |
63 | Shipping
64 |
65 | Address
66 | {cart.shippingAddress.address},{cart.shippingAddress.city},
67 | {cart.shippingAddress.postalCode},{cart.shippingAddress.country}
68 |
69 |
70 |
71 |
72 | Payment Method
73 | Method:
74 | {cart.paymentMethod}
75 |
76 |
77 |
78 | Order Items
79 | {cart.cartItems.length === 0 ? (
80 | Your cart is empty
81 | ) : (
82 |
83 | {cart.cartItems.map((item, index) => (
84 |
85 |
86 |
87 |
93 |
94 |
95 |
96 | {item.name}
97 |
98 |
99 |
100 | {item.qty} x ${item.price} = ${item.qty * item.price}
101 |
102 |
103 |
104 | ))}
105 |
106 | )}
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 | Order Summary
115 |
116 |
117 |
118 | Items
119 | ${cart.itemsPrice}
120 |
121 |
122 |
123 |
124 |
125 | Shipping
126 | ${cart.shippingPrice}
127 |
128 |
129 |
130 |
131 |
132 | Tax
133 | ${cart.taxPrice}
134 |
135 |
136 |
137 |
138 |
139 | Total
140 | ${cart.totalPrice}
141 |
142 |
143 |
144 |
145 | {error && {error}}
146 |
147 |
148 |
156 |
157 |
158 |
159 |
160 |
161 | >
162 | );
163 | };
164 |
165 | export default PlaceOrderScreen;
166 |
--------------------------------------------------------------------------------
/frontend/src/screens/ProductEditScreen.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import React, { useState, useEffect } from "react";
3 | import { Link } from "react-router-dom";
4 | import { Form, Button } from "react-bootstrap";
5 | import { useDispatch, useSelector } from "react-redux";
6 |
7 | import Message from "../components/Message";
8 | import Loader from "../components/Loader";
9 | import FormContainer from "../components/FormContainer";
10 | import { listProductDetails, updateProduct } from "../actions/productActions";
11 | import { PRODUCT_UPDATE_RESET } from "../constants/productConstants";
12 |
13 | const ProductEditScreen = ({ match, history }) => {
14 | const productId = match.params.id;
15 |
16 | const [name, setName] = useState("");
17 | const [price, setPrice] = useState(0);
18 | const [image, setImage] = useState("");
19 | const [brand, setBrand] = useState("");
20 | const [category, setCategory] = useState("");
21 | const [countInStock, setCountInStock] = useState(0);
22 | const [description, setDescription] = useState("");
23 | const [uploading, setUploading] = useState(false);
24 |
25 | const dispatch = useDispatch();
26 |
27 | const productDetails = useSelector((state) => state.productDetails);
28 | const { loading, error, product } = productDetails;
29 |
30 | const productUpdate = useSelector((state) => state.productUpdate);
31 | const {
32 | loading: loadingUpdate,
33 | error: errorUpdate,
34 | success: successUpdate,
35 | } = productUpdate;
36 |
37 | useEffect(() => {
38 | if (successUpdate) {
39 | dispatch({ type: PRODUCT_UPDATE_RESET });
40 | history.push("/admin/productlist");
41 | } else {
42 | if (!product.name || product._id !== productId) {
43 | dispatch(listProductDetails(productId));
44 | } else {
45 | setName(product.name);
46 | setPrice(product.price);
47 | setImage(product.image);
48 | setBrand(product.brand);
49 | setCategory(product.category);
50 | setCountInStock(product.countInStock);
51 | setDescription(product.description);
52 | }
53 | }
54 | }, [dispatch, history, product, productId, successUpdate]);
55 |
56 | const uploadFileHandler = async (e) => {
57 | const file = e.target.files[0];
58 | const formData = new FormData();
59 | formData.append("image", file);
60 | setUploading(true);
61 |
62 | try {
63 | const config = {
64 | headers: {
65 | "Content-Type": "multipart/form-data",
66 | },
67 | };
68 | const { data } = await axios.post("/api/upload", formData, config);
69 |
70 | setImage(data);
71 | setUploading(false);
72 | } catch (error) {
73 | console.error(error);
74 | setUploading(false);
75 | }
76 | };
77 |
78 | const submitHandler = (e) => {
79 | e.preventDefault();
80 | dispatch(
81 | updateProduct({
82 | _id: productId,
83 | name,
84 | price,
85 | image,
86 | brand,
87 | category,
88 | countInStock,
89 | description,
90 | })
91 | );
92 | };
93 |
94 | return (
95 | <>
96 |
97 | Go Back
98 |
99 |
100 |
101 | Edit Product
102 | {loadingUpdate && }
103 | {errorUpdate && {errorUpdate}}
104 | {loading ? (
105 |
106 | ) : error ? (
107 | {error}
108 | ) : (
109 |
111 | Name
112 | setName(e.target.value)}
117 | >
118 |
119 |
120 |
121 | Price
122 | setPrice(e.target.value)}
127 | >
128 |
129 |
130 |
131 | Image
132 | setImage(e.target.value)}
137 | >
138 |
144 | {uploading && }
145 |
146 |
147 |
148 | Brand
149 | setBrand(e.target.value)}
154 | >
155 |
156 |
157 |
158 | Count In Stock
159 | setCountInStock(e.target.value)}
164 | >
165 |
166 |
167 |
168 | Brand
169 | setCategory(e.target.value)}
174 | >
175 |
176 |
177 |
178 | Description
179 | setDescription(e.target.value)}
184 | >
185 |
186 |
187 |
190 |
191 | )}
192 |
193 | >
194 | );
195 | };
196 |
197 | export default ProductEditScreen;
198 |
--------------------------------------------------------------------------------
/frontend/src/screens/ProductListScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { LinkContainer } from "react-router-bootstrap";
3 | import { Table, Button, Row, Col } from "react-bootstrap";
4 | import { useDispatch, useSelector } from "react-redux";
5 |
6 | import Message from "../components/Message";
7 | import Loader from "../components/Loader";
8 | import Paginate from "../components/Paginate";
9 | import {
10 | listProducts,
11 | deleteProduct,
12 | createProduct,
13 | } from "../actions/productActions";
14 | import { PRODUCT_CREATE_RESET } from "../constants/productConstants";
15 |
16 | const ProductListScreen = ({ history, match }) => {
17 | const pageNumber = match.params.pageNumber || 1;
18 | const dispatch = useDispatch();
19 |
20 | const productList = useSelector((state) => state.productList);
21 | const { loading, error, products, pages, page } = productList;
22 |
23 | const productDelete = useSelector((state) => state.productDelete);
24 | const {
25 | loading: loadingDelete,
26 | error: errorDelete,
27 | success: successDelete,
28 | } = productDelete;
29 |
30 | const productCreate = useSelector((state) => state.productCreate);
31 | const {
32 | loading: loadingCreate,
33 | error: errorCreate,
34 | success: successCreate,
35 | product: createdProduct,
36 | } = productCreate;
37 |
38 | const userLogin = useSelector((state) => state.userLogin);
39 | const { userInfo } = userLogin;
40 |
41 | useEffect(() => {
42 | dispatch({ type: PRODUCT_CREATE_RESET });
43 |
44 | if (!userInfo.isAdmin) {
45 | history.push("/login");
46 | }
47 | if (successCreate) {
48 | history.push(`admin/product/${createdProduct._id}/edit`);
49 | } else {
50 | dispatch(listProducts("", pageNumber));
51 | }
52 | }, [
53 | dispatch,
54 | history,
55 | userInfo,
56 | successDelete,
57 | successCreate,
58 | createdProduct,
59 | pageNumber,
60 | ]);
61 |
62 | const deleteHandler = (id) => {
63 | if (window.confirm("Are you sure?")) {
64 | dispatch(deleteProduct(id));
65 | }
66 | };
67 |
68 | const createProductHandler = () => {
69 | dispatch(createProduct());
70 | };
71 |
72 | return (
73 | <>
74 |
75 |
76 | Products
77 |
78 |
79 |
82 |
83 |
84 | {loadingDelete && }
85 | {errorDelete && {errorDelete}}
86 | {loadingCreate && }
87 | {errorCreate && {errorCreate}}
88 | {loading ? (
89 |
90 | ) : error ? (
91 | {error}
92 | ) : (
93 | <>
94 |
95 |
96 |
97 | | ID |
98 | NAME |
99 | PRICE |
100 | CATEGORY |
101 | BRAND |
102 | |
103 |
104 |
105 |
106 | {products.map((product) => (
107 |
108 | | {product._id} |
109 | {product.name} |
110 | ${product.price} |
111 | {product.category} |
112 | {product.brand} |
113 |
114 |
115 |
118 |
119 |
126 | |
127 |
128 | ))}
129 |
130 |
131 |
132 | >
133 | )}
134 | >
135 | );
136 | };
137 |
138 | export default ProductListScreen;
139 |
--------------------------------------------------------------------------------
/frontend/src/screens/ProductScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { Link } from "react-router-dom";
4 | import {
5 | Row,
6 | Col,
7 | Image,
8 | ListGroup,
9 | Card,
10 | Button,
11 | Form,
12 | } from "react-bootstrap";
13 |
14 | import Rating from "../components/Rating";
15 | import {
16 | listProductDetails,
17 | createProductReview,
18 | } from "../actions/productActions";
19 | import { PRODUCT_CREATE_REVIEW_RESET } from "../constants/productConstants";
20 |
21 | import Loader from "../components/Loader";
22 | import Message from "../components/Message";
23 | import Meta from "../components/Meta";
24 |
25 | const ProductScreen = ({ history, match }) => {
26 | const [qty, setQty] = useState(1);
27 | const [rating, setRating] = useState(0);
28 | const [comment, setComment] = useState("");
29 |
30 | const dispatch = useDispatch();
31 |
32 | const productDetails = useSelector((state) => state.productDetails);
33 | const { loading, error, product } = productDetails;
34 |
35 | const productReviewCreate = useSelector((state) => state.productReviewCreate);
36 | const {
37 | error: errorProductReview,
38 | success: successProductReview,
39 | } = productReviewCreate;
40 |
41 | const userLogin = useSelector((state) => state.userLogin);
42 | const { userInfo } = userLogin;
43 |
44 | useEffect(() => {
45 | if (successProductReview) {
46 | alert("Review Submitted!");
47 | setRating(0);
48 | setComment("");
49 | dispatch({ type: PRODUCT_CREATE_REVIEW_RESET });
50 | }
51 | dispatch(listProductDetails(match.params.id));
52 | }, [dispatch, match, successProductReview]);
53 |
54 | const addToCartHandler = () => {
55 | history.push(`/cart/${match.params.id}?qty=${qty}`);
56 | };
57 |
58 | const submitHandler = (e) => {
59 | e.preventDefault();
60 | dispatch(
61 | createProductReview(match.params.id, {
62 | rating,
63 | comment,
64 | })
65 | );
66 | };
67 |
68 | return (
69 | <>
70 |
71 | Go Back
72 |
73 | {loading ? (
74 |
75 | ) : error ? (
76 | {error}
77 | ) : (
78 | <>
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | {product.name}
88 |
89 |
90 |
94 |
95 | Price: ${product.price}
96 |
97 | Description: ${product.description}
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | Price
107 |
108 | ${product.price}
109 |
110 |
111 |
112 |
113 |
114 |
115 | Status
116 |
117 | {product.countInStock > 0 ? "In Stock" : "Out of Stock"}
118 |
119 |
120 |
121 |
122 | {product.countInStock > 0 && (
123 |
124 |
125 | Qty
126 |
127 | setQty(e.target.value)}
131 | >
132 | {[...Array(product.countInStock).keys()].map(
133 | (x) => (
134 |
137 | )
138 | )}
139 |
140 |
141 |
142 |
143 | )}
144 |
145 |
146 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 | Reviews
162 | {product.reviews.length === 0 && No Reviews}
163 |
164 | {product.reviews.map((review) => (
165 |
166 | {review.name}
167 |
168 | {review.createdAt.substring(0, 10)}
169 | {review.comment}
170 |
171 | ))}
172 |
173 | Write a Customer Review
174 | {errorProductReview && (
175 | {errorProductReview}
176 | )}
177 | {userInfo ? (
178 |
180 | Rating
181 | setRating(e.target.value)}
185 | >
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 | Comment
196 | setComment(e.target.value)}
201 | >
202 |
203 |
206 |
207 | ) : (
208 |
209 | Please sign in to write a review
210 |
211 | )}
212 |
213 |
214 |
215 |
216 | >
217 | )}
218 | >
219 | );
220 | };
221 |
222 | export default ProductScreen;
223 |
--------------------------------------------------------------------------------
/frontend/src/screens/ProfileScreen.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { Form, Button, Row, Col, Table } from "react-bootstrap";
3 | import { LinkContainer } from "react-router-bootstrap";
4 | import { useDispatch, useSelector } from "react-redux";
5 |
6 | import Message from "../components/Message";
7 | import Loader from "../components/Loader";
8 |
9 | import { getUserDetails, updateUserProfile } from "../actions/userActions";
10 | import { listMyOrders } from "../actions/orderActions";
11 | import { USER_UPDATE_PROFILE_RESET } from "../constants/userConstants";
12 |
13 | const ProfileScreen = ({ location, history }) => {
14 | const [email, setEmail] = useState("");
15 | const [name, setName] = useState("");
16 | const [password, setPassword] = useState("");
17 | const [confirmPassword, setConfirmPassword] = useState("");
18 | const [message, setMessage] = useState(null);
19 |
20 | const dispatch = useDispatch();
21 |
22 | const userDetails = useSelector((state) => state.userDetails);
23 | const { loading, error, user } = userDetails;
24 |
25 | const userLogin = useSelector((state) => state.userLogin);
26 | const { userInfo } = userLogin;
27 |
28 | const userUpdateProfile = useSelector((state) => state.userUpdateProfile);
29 | const { success } = userUpdateProfile;
30 |
31 | const orderListMy = useSelector((state) => state.orderListMy);
32 | const { loading: loadingOrders, error: errorOrders, orders } = orderListMy;
33 |
34 | useEffect(() => {
35 | if (!userInfo) {
36 | history.push("/login");
37 | } else {
38 | if (!user.name || !user.name || success) {
39 | dispatch({ type: USER_UPDATE_PROFILE_RESET });
40 | dispatch(getUserDetails("profile"));
41 | dispatch(listMyOrders());
42 | } else {
43 | setName(user.name);
44 | setEmail(user.email);
45 | }
46 | }
47 | }, [dispatch, history, userInfo, user, success]);
48 |
49 | const submitHandler = (e) => {
50 | e.preventDefault();
51 | if (password !== confirmPassword) {
52 | setMessage("Password do not match");
53 | } else {
54 | dispatch(updateUserProfile({ id: user._id, name, email, password }));
55 | }
56 | };
57 |
58 | return (
59 |
60 |
61 | User Profile
62 | {message && {message}}
63 | {error && {error}}
64 | {success && Profile Update}
65 | {loading && }
66 |
68 | Name
69 | setName(e.target.value)}
74 | >
75 |
76 |
77 |
78 | Email Address
79 | setEmail(e.target.value)}
84 | >
85 |
86 |
87 |
88 | Password
89 | setPassword(e.target.value)}
94 | >
95 |
96 |
97 |
98 | Confirm Password
99 | setConfirmPassword(e.target.value)}
104 | >
105 |
106 |
107 |
110 |
111 |
112 |
113 | My Orders
114 | {loadingOrders ? (
115 |
116 | ) : errorOrders ? (
117 | {errorOrders}
118 | ) : (
119 |
120 |
121 |
122 | | 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 | {order.totalPrice} |
136 |
137 | {order.isPaid ? (
138 | order.paidAt.substring(0, 10)
139 | ) : (
140 |
141 | )}
142 | |
143 |
144 | {order.isDelivered ? (
145 | order.deliveredAt.substring(0, 10)
146 | ) : (
147 |
148 | )}
149 | |
150 |
151 |
152 |
153 |
156 |
157 | |
158 |
159 | ))}
160 |
161 |
162 | )}
163 |
164 |
165 | );
166 | };
167 |
168 | export default ProfileScreen;
169 |
--------------------------------------------------------------------------------
/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 |
6 | import Message from "../components/Message";
7 | import Loader from "../components/Loader";
8 | import FormContainer from "../components/FormContainer";
9 | import { register } from "../actions/userActions";
10 |
11 | const RegisterScreen = ({ location, history }) => {
12 | const [email, setEmail] = useState("");
13 | const [name, setName] = useState("");
14 | const [password, setPassword] = useState("");
15 | const [confirmPassword, setConfirmPassword] = useState("");
16 | const [message, setMessage] = useState(null);
17 |
18 | const dispatch = useDispatch();
19 |
20 | const userRegister = useSelector((state) => state.userRegister);
21 | const { loading, error, userInfo } = userRegister;
22 |
23 | const redirect = location.search ? location.search.split("=")[1] : "/";
24 |
25 | useEffect(() => {
26 | if (userInfo) {
27 | history.push(redirect);
28 | }
29 | }, [history, userInfo, redirect]);
30 |
31 | const submitHandler = (e) => {
32 | e.preventDefault();
33 | if (password !== confirmPassword) {
34 | setMessage("Password do not match");
35 | } else {
36 | dispatch(register(name, email, password));
37 | }
38 | };
39 |
40 | return (
41 |
42 | Sign Up
43 | {message && {message}}
44 | {error && {error}}
45 | {loading && }
46 |
48 | Name
49 | setName(e.target.value)}
54 | >
55 |
56 |
57 |
58 | Email Address
59 | setEmail(e.target.value)}
64 | >
65 |
66 |
67 |
68 | Password
69 | setPassword(e.target.value)}
74 | >
75 |
76 |
77 |
78 | Confirm Password
79 | setConfirmPassword(e.target.value)}
84 | >
85 |
86 |
87 |
90 |
91 |
92 |
93 |
94 | Have an account?{" "}
95 |
96 | Login
97 |
98 |
99 |
100 |
101 | );
102 | };
103 |
104 | export default RegisterScreen;
105 |
--------------------------------------------------------------------------------
/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 |
5 | import FormContainer from "../components/FormContainer";
6 | import CheckoutSteps from "../components/CheckoutSteps";
7 | import { saveShippingAddress } from "../actions/cartActions";
8 |
9 | const ShippingScreen = ({ history }) => {
10 | const cart = useSelector((state) => state.cart);
11 | const { shippingAddress } = cart;
12 |
13 | const [address, setAddress] = useState(shippingAddress.address);
14 | const [city, setCity] = useState(shippingAddress.city);
15 | const [postalCode, setPostalCode] = useState(shippingAddress.postalCode);
16 | const [country, setCountry] = useState(shippingAddress.country);
17 |
18 | const dispatch = useDispatch();
19 |
20 | const submitHandler = (e) => {
21 | e.preventDefault();
22 | dispatch(saveShippingAddress({ address, city, postalCode, country }));
23 | history.push("/payment");
24 | };
25 |
26 | return (
27 |
28 |
29 | Shipping
30 |
32 | Address
33 | setAddress(e.target.value)}
39 | >
40 |
41 |
42 |
43 | City
44 | setCity(e.target.value)}
50 | >
51 |
52 |
53 |
54 | Postal Code
55 | setPostalCode(e.target.value)}
61 | >
62 |
63 |
64 |
65 | Country
66 | setCountry(e.target.value)}
72 | >
73 |
74 |
75 |
78 |
79 |
80 | );
81 | };
82 |
83 | export default ShippingScreen;
84 |
--------------------------------------------------------------------------------
/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 |
6 | import Message from "../components/Message";
7 | import Loader from "../components/Loader";
8 | import FormContainer from "../components/FormContainer";
9 | import { getUserDetails, updateUser } from "../actions/userActions";
10 | import { USER_UPDATE_RESET } from "../constants/userConstants";
11 |
12 | const UserEditScreen = ({ match, history }) => {
13 | const userId = match.params.id;
14 |
15 | const [email, setEmail] = useState("");
16 | const [name, setName] = useState("");
17 | const [isAdmin, setIsAdmin] = useState(false);
18 |
19 | const dispatch = useDispatch();
20 |
21 | const userDetails = useSelector((state) => state.userDetails);
22 | const { loading, error, user } = userDetails;
23 |
24 | const userUpdate = useSelector((state) => state.userUpdate);
25 | const {
26 | loading: loadingUpdate,
27 | error: errorUpdate,
28 | success: successUpdate,
29 | } = userUpdate;
30 |
31 | useEffect(() => {
32 | if (successUpdate) {
33 | dispatch({ type: USER_UPDATE_RESET });
34 | history.push("/admin/userlist");
35 | } else {
36 | if (!user.name || user._id !== userId) {
37 | dispatch(getUserDetails(userId));
38 | } else {
39 | setName(user.name);
40 | setEmail(user.email);
41 | setIsAdmin(user.isAdmin);
42 | }
43 | }
44 | }, [dispatch, history, user, userId, successUpdate]);
45 |
46 | const submitHandler = (e) => {
47 | e.preventDefault();
48 | dispatch(updateUser({ _id: userId, name, email, isAdmin }));
49 | };
50 |
51 | return (
52 | <>
53 |
54 | Go Back
55 |
56 |
57 |
58 | Edit User
59 | {loadingUpdate && }
60 | {errorUpdate && {errorUpdate}}
61 | {loading ? (
62 |
63 | ) : error ? (
64 | {error}
65 | ) : (
66 |
68 | Name
69 | setName(e.target.value)}
74 | >
75 |
76 |
77 |
78 | Email Address
79 | setEmail(e.target.value)}
84 | >
85 |
86 |
87 |
88 | setIsAdmin(e.target.checked)}
93 | >
94 |
95 |
96 |
99 |
100 | )}
101 |
102 | >
103 | );
104 | };
105 |
106 | export default UserEditScreen;
107 |
--------------------------------------------------------------------------------
/frontend/src/screens/UserListScreen.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 |
6 | import Message from "../components/Message";
7 | import Loader from "../components/Loader";
8 | import { listUsers, deleteUser } from "../actions/userActions";
9 |
10 | const UserListScreen = ({ history }) => {
11 | const dispatch = useDispatch();
12 |
13 | const userList = useSelector((state) => state.userList);
14 | const { loading, error, users } = userList;
15 |
16 | const userLogin = useSelector((state) => state.userLogin);
17 | const { userInfo } = userLogin;
18 |
19 | const userDelete = useSelector((state) => state.userDelete);
20 | const { success: successDelete } = userDelete;
21 |
22 | useEffect(() => {
23 | if (userInfo && userInfo.isAdmin) {
24 | dispatch(listUsers());
25 | } else {
26 | history.pushState("/login");
27 | }
28 | }, [dispatch, history, userInfo, successDelete]);
29 |
30 | const deleteHandler = (id) => {
31 | if (window.confirm("Are you sure?")) {
32 | dispatch(deleteUser(id));
33 | }
34 | };
35 |
36 | return (
37 | <>
38 | Users
39 | {loading ? (
40 |
41 | ) : error ? (
42 | {error}
43 | ) : (
44 |
45 |
46 |
47 | | ID |
48 | NAME |
49 | EMAIL |
50 | ADMIN |
51 | |
52 |
53 |
54 |
55 | {users.map((user) => (
56 |
57 | | {user._id} |
58 | {user.name} |
59 |
60 | {user.email}
61 | |
62 |
63 | {user.isAdmin ? (
64 |
65 | ) : (
66 |
67 | )}
68 | |
69 |
70 |
71 |
74 |
75 |
82 | |
83 |
84 | ))}
85 |
86 |
87 | )}
88 | >
89 | );
90 | };
91 |
92 | export default UserListScreen;
93 |
--------------------------------------------------------------------------------
/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 |
5 | import {
6 | productListReducer,
7 | productDetailsReducer,
8 | productDeleteReducer,
9 | productCreateReducer,
10 | productUpdateReducer,
11 | productReviewCreateReducer,
12 | productTopRatedReducer,
13 | } from "./reducers/productReducers";
14 | import { cartReducer } from "./reducers/cartReducers";
15 | import {
16 | userDetailsReducer,
17 | userLoginReducer,
18 | userRegisterReducer,
19 | userUpdateProfileReducer,
20 | userListReducer,
21 | userDeleteReducer,
22 | userUpdateReducer,
23 | } from "./reducers/userReducers";
24 | import {
25 | orderCreateReducer,
26 | orderDetailsReducer,
27 | orderPayReducer,
28 | orderDeliverReducer,
29 | orderListMyReducer,
30 | orderListReducer,
31 | } from "./reducers/orderReducers";
32 |
33 | const reducer = combineReducers({
34 | productList: productListReducer,
35 | productDetails: productDetailsReducer,
36 | productDelete: productDeleteReducer,
37 | productCreate: productCreateReducer,
38 | productUpdate: productUpdateReducer,
39 | productReviewCreate: productReviewCreateReducer,
40 | productTopRated: productTopRatedReducer,
41 | cart: cartReducer,
42 | userLogin: userLoginReducer,
43 | userRegister: userRegisterReducer,
44 | userDetails: userDetailsReducer,
45 | userUpdateProfile: userUpdateProfileReducer,
46 | userList: userListReducer,
47 | userDelete: userDeleteReducer,
48 | userUpdate: userUpdateReducer,
49 | orderCreate: orderCreateReducer,
50 | orderDetails: orderDetailsReducer,
51 | orderPay: orderPayReducer,
52 | orderDeliver: orderDeliverReducer,
53 | orderListMy: orderListMyReducer,
54 | orderList: orderListReducer,
55 | });
56 |
57 | const cartItemsFromStorage = localStorage.getItem("cartItems")
58 | ? JSON.parse(localStorage.getItem("cartItems"))
59 | : [];
60 |
61 | const userInfoFromStorage = localStorage.getItem("userInfo")
62 | ? JSON.parse(localStorage.getItem("userInfo"))
63 | : null;
64 |
65 | const shippingAddressFromStorage = localStorage.getItem("shippingAddress")
66 | ? JSON.parse(localStorage.getItem("shippingAddress"))
67 | : {};
68 |
69 | const initialState = {
70 | cart: {
71 | cartItems: cartItemsFromStorage,
72 | shippingAddress: shippingAddressFromStorage,
73 | },
74 | userLogin: { userInfo: userInfoFromStorage },
75 | };
76 |
77 | const middleware = [thunk];
78 |
79 | const store = createStore(
80 | reducer,
81 | initialState,
82 | composeWithDevTools(applyMiddleware(...middleware))
83 | );
84 |
85 | export default store;
86 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "proshop",
3 | "version": "1.0.0",
4 | "description": "MERN shopping cart app",
5 | "main": "server.js",
6 | "type": "module",
7 | "scripts": {
8 | "start": "node backend/server",
9 | "server": "nodemon backend/server",
10 | "client": "npm start --prefix frontend",
11 | "dev": "concurrently \"npm run server\" \"npm run client\"",
12 | "data:import": "node backend/seeder",
13 | "data:destroy": "node backend/seeder -d",
14 | "heroku-postbuild": "NPM_CONFIG_PRODUCTION=false npm install --prefix frontend && npm run build --prefix frontend"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "git+https://github.com/mariosknl/ProShop.git"
19 | },
20 | "author": "Marios Kanellopoulos",
21 | "license": "MIT",
22 | "bugs": {
23 | "url": "https://github.com/mariosknl/ProShop/issues"
24 | },
25 | "homepage": "https://github.com/mariosknl/ProShop#readme",
26 | "dependencies": {
27 | "axios": "^1.0.0",
28 | "bcryptjs": "^3.0.0",
29 | "colors": "^1.4.0",
30 | "dotenv": "^17.0.0",
31 | "express": "^5.0.0",
32 | "express-async-handler": "^1.1.4",
33 | "jsonwebtoken": "^9.0.0",
34 | "mongoose": "^8.0.0",
35 | "morgan": "^1.10.0",
36 | "multer": "^2.0.0"
37 | },
38 | "devDependencies": {
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "config:base"
4 | ],
5 | "packageRules": [
6 | {
7 | "updateTypes": [
8 | "minor",
9 | "patch"
10 | ],
11 | "automerge": true
12 | }
13 | ],
14 | "timezone": "Europe/Athens",
15 | "schedule": [
16 | "after 10pm every weekday",
17 | "before 5am every weekday",
18 | "every weekend"
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/uploads/file.txt:
--------------------------------------------------------------------------------
1 | Add to git repo
--------------------------------------------------------------------------------
/uploads/image-1607604569956.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mariosknl/ProShop/97c26df85e134c7beccac1e892bbd57b974f4d34/uploads/image-1607604569956.jpeg
--------------------------------------------------------------------------------