├── .env.example ├── .gitignore ├── .prettierrc.yaml ├── backend ├── config │ └── db.js ├── controllers │ ├── orderController.js │ ├── productController.js │ └── userController.js ├── data │ ├── products.js │ └── users.js ├── middleware │ ├── asyncHandler.js │ ├── authMiddleware.js │ ├── checkObjectId.js │ └── errorMiddleware.js ├── models │ ├── orderModel.js │ ├── productModel.js │ └── userModel.js ├── routes │ ├── orderRoutes.js │ ├── productRoutes.js │ ├── uploadRoutes.js │ └── userRoutes.js ├── seeder.js ├── server.js └── utils │ ├── calcPrices.js │ ├── generateToken.js │ └── paypal.js ├── frontend ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── images │ │ ├── airpods.jpg │ │ ├── alexa.jpg │ │ ├── camera.jpg │ │ ├── mouse.jpg │ │ ├── phone.jpg │ │ ├── playstation.jpg │ │ ├── sample.jpg │ │ └── screens.png │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt └── src │ ├── App.js │ ├── assets │ ├── logo.png │ └── styles │ │ ├── bootstrap.custom.css │ │ └── index.css │ ├── components │ ├── AdminRoute.jsx │ ├── CheckoutSteps.jsx │ ├── Footer.jsx │ ├── FormContainer.jsx │ ├── Header.jsx │ ├── Loader.jsx │ ├── Message.jsx │ ├── Meta.jsx │ ├── Paginate.jsx │ ├── PrivateRoute.jsx │ ├── Product.jsx │ ├── ProductCarousel.jsx │ ├── Rating.jsx │ └── SearchBox.jsx │ ├── constants.js │ ├── index.js │ ├── reportWebVitals.js │ ├── screens │ ├── CartScreen.jsx │ ├── HomeScreen.jsx │ ├── LoginScreen.jsx │ ├── OrderScreen.jsx │ ├── PaymentScreen.jsx │ ├── PlaceOrderScreen.jsx │ ├── ProductScreen.jsx │ ├── ProfileScreen.jsx │ ├── RegisterScreen.jsx │ ├── ShippingScreen.jsx │ └── admin │ │ ├── OrderListScreen.jsx │ │ ├── ProductEditScreen.jsx │ │ ├── ProductListScreen.jsx │ │ ├── UserEditScreen.jsx │ │ └── UserListScreen.jsx │ ├── setupTests.js │ ├── slices │ ├── apiSlice.js │ ├── authSlice.js │ ├── cartSlice.js │ ├── ordersApiSlice.js │ ├── productsApiSlice.js │ └── usersApiSlice.js │ ├── store.js │ └── utils │ └── cartUtils.js ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── readme.md └── uploads └── .gitkeep /.env.example: -------------------------------------------------------------------------------- 1 | PORT=5000 2 | MONGO_URI= 3 | JWT_SECRET= 4 | PAYPAL_CLIENT_ID= 5 | PAYPAL_APP_SECRET= 6 | PAYPAL_API_URL=https://api-m.sandbox.paypal.com 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | frontend/build 27 | uploads/*png 28 | uploads/*jpg 29 | 30 | # API tests 31 | http 32 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | printWidth: 80 2 | tabWidth: 2 3 | useTabs: false 4 | semi: true 5 | singleQuote: true 6 | bracketSpacing: true 7 | jsxBracketSameLine: false 8 | jsxSingleQuote: true 9 | trailingComma: es5 10 | -------------------------------------------------------------------------------- /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 | console.log(`MongoDB Connected: ${conn.connection.host}`); 7 | } catch (error) { 8 | console.error(`Error: ${error.message}`); 9 | process.exit(1); 10 | } 11 | }; 12 | 13 | export default connectDB; 14 | -------------------------------------------------------------------------------- /backend/controllers/orderController.js: -------------------------------------------------------------------------------- 1 | import asyncHandler from '../middleware/asyncHandler.js'; 2 | import Order from '../models/orderModel.js'; 3 | import Product from '../models/productModel.js'; 4 | import { calcPrices } from '../utils/calcPrices.js'; 5 | import { verifyPayPalPayment, checkIfNewTransaction } from '../utils/paypal.js'; 6 | 7 | // @desc Create new order 8 | // @route POST /api/orders 9 | // @access Private 10 | const addOrderItems = asyncHandler(async (req, res) => { 11 | const { orderItems, shippingAddress, paymentMethod } = req.body; 12 | 13 | if (orderItems && orderItems.length === 0) { 14 | res.status(400); 15 | throw new Error('No order items'); 16 | } else { 17 | // NOTE: here we must assume that the prices from our client are incorrect. 18 | // We must only trust the price of the item as it exists in 19 | // our DB. This prevents a user paying whatever they want by hacking our client 20 | // side code - https://gist.github.com/bushblade/725780e6043eaf59415fbaf6ca7376ff 21 | 22 | // get the ordered items from our database 23 | const itemsFromDB = await Product.find({ 24 | _id: { $in: orderItems.map((x) => x._id) }, 25 | }); 26 | 27 | // map over the order items and use the price from our items from database 28 | const dbOrderItems = orderItems.map((itemFromClient) => { 29 | const matchingItemFromDB = itemsFromDB.find( 30 | (itemFromDB) => itemFromDB._id.toString() === itemFromClient._id 31 | ); 32 | return { 33 | ...itemFromClient, 34 | product: itemFromClient._id, 35 | price: matchingItemFromDB.price, 36 | _id: undefined, 37 | }; 38 | }); 39 | 40 | // calculate prices 41 | const { itemsPrice, taxPrice, shippingPrice, totalPrice } = 42 | calcPrices(dbOrderItems); 43 | 44 | const order = new Order({ 45 | orderItems: dbOrderItems, 46 | user: req.user._id, 47 | shippingAddress, 48 | paymentMethod, 49 | itemsPrice, 50 | taxPrice, 51 | shippingPrice, 52 | totalPrice, 53 | }); 54 | 55 | const createdOrder = await order.save(); 56 | 57 | res.status(201).json(createdOrder); 58 | } 59 | }); 60 | 61 | // @desc Get logged in user orders 62 | // @route GET /api/orders/myorders 63 | // @access Private 64 | const getMyOrders = asyncHandler(async (req, res) => { 65 | const orders = await Order.find({ user: req.user._id }); 66 | res.json(orders); 67 | }); 68 | 69 | // @desc Get order by ID 70 | // @route GET /api/orders/:id 71 | // @access Private 72 | const getOrderById = asyncHandler(async (req, res) => { 73 | const order = await Order.findById(req.params.id).populate( 74 | 'user', 75 | 'name email' 76 | ); 77 | 78 | if (order) { 79 | res.json(order); 80 | } else { 81 | res.status(404); 82 | throw new Error('Order not found'); 83 | } 84 | }); 85 | 86 | // @desc Update order to paid 87 | // @route PUT /api/orders/:id/pay 88 | // @access Private 89 | const updateOrderToPaid = asyncHandler(async (req, res) => { 90 | // NOTE: here we need to verify the payment was made to PayPal before marking 91 | // the order as paid 92 | const { verified, value } = await verifyPayPalPayment(req.body.id); 93 | if (!verified) throw new Error('Payment not verified'); 94 | 95 | // check if this transaction has been used before 96 | const isNewTransaction = await checkIfNewTransaction(Order, req.body.id); 97 | if (!isNewTransaction) throw new Error('Transaction has been used before'); 98 | 99 | const order = await Order.findById(req.params.id); 100 | 101 | if (order) { 102 | // check the correct amount was paid 103 | const paidCorrectAmount = order.totalPrice.toString() === value; 104 | if (!paidCorrectAmount) throw new Error('Incorrect amount paid'); 105 | 106 | order.isPaid = true; 107 | order.paidAt = Date.now(); 108 | order.paymentResult = { 109 | id: req.body.id, 110 | status: req.body.status, 111 | update_time: req.body.update_time, 112 | email_address: req.body.payer.email_address, 113 | }; 114 | 115 | const updatedOrder = await order.save(); 116 | 117 | res.json(updatedOrder); 118 | } else { 119 | res.status(404); 120 | throw new Error('Order not found'); 121 | } 122 | }); 123 | 124 | // @desc Update order to delivered 125 | // @route GET /api/orders/:id/deliver 126 | // @access Private/Admin 127 | const updateOrderToDelivered = asyncHandler(async (req, res) => { 128 | const order = await Order.findById(req.params.id); 129 | 130 | if (order) { 131 | order.isDelivered = true; 132 | order.deliveredAt = Date.now(); 133 | 134 | const updatedOrder = await order.save(); 135 | 136 | res.json(updatedOrder); 137 | } else { 138 | res.status(404); 139 | throw new Error('Order not found'); 140 | } 141 | }); 142 | 143 | // @desc Get all orders 144 | // @route GET /api/orders 145 | // @access Private/Admin 146 | const getOrders = asyncHandler(async (req, res) => { 147 | const orders = await Order.find({}).populate('user', 'id name'); 148 | res.json(orders); 149 | }); 150 | 151 | export { 152 | addOrderItems, 153 | getMyOrders, 154 | getOrderById, 155 | updateOrderToPaid, 156 | updateOrderToDelivered, 157 | getOrders, 158 | }; 159 | -------------------------------------------------------------------------------- /backend/controllers/productController.js: -------------------------------------------------------------------------------- 1 | import asyncHandler from '../middleware/asyncHandler.js'; 2 | import Product from '../models/productModel.js'; 3 | 4 | // @desc Fetch all products 5 | // @route GET /api/products 6 | // @access Public 7 | const getProducts = asyncHandler(async (req, res) => { 8 | const pageSize = process.env.PAGINATION_LIMIT; 9 | const page = Number(req.query.pageNumber) || 1; 10 | 11 | const keyword = req.query.keyword 12 | ? { 13 | name: { 14 | $regex: req.query.keyword, 15 | $options: 'i', 16 | }, 17 | } 18 | : {}; 19 | 20 | const count = await Product.countDocuments({ ...keyword }); 21 | const products = await Product.find({ ...keyword }) 22 | .limit(pageSize) 23 | .skip(pageSize * (page - 1)); 24 | 25 | res.json({ products, page, pages: Math.ceil(count / pageSize) }); 26 | }); 27 | 28 | // @desc Fetch single product 29 | // @route GET /api/products/:id 30 | // @access Public 31 | const getProductById = asyncHandler(async (req, res) => { 32 | // NOTE: checking for valid ObjectId to prevent CastError moved to separate 33 | // middleware. See README for more info. 34 | 35 | const product = await Product.findById(req.params.id); 36 | if (product) { 37 | return res.json(product); 38 | } else { 39 | // NOTE: this will run if a valid ObjectId but no product was found 40 | // i.e. product may be null 41 | res.status(404); 42 | throw new Error('Product not found'); 43 | } 44 | }); 45 | 46 | // @desc Create a product 47 | // @route POST /api/products 48 | // @access Private/Admin 49 | const createProduct = asyncHandler(async (req, res) => { 50 | const product = new Product({ 51 | name: 'Sample name', 52 | price: 0, 53 | user: req.user._id, 54 | image: '/images/sample.jpg', 55 | brand: 'Sample brand', 56 | category: 'Sample category', 57 | countInStock: 0, 58 | numReviews: 0, 59 | description: 'Sample description', 60 | }); 61 | 62 | const createdProduct = await product.save(); 63 | res.status(201).json(createdProduct); 64 | }); 65 | 66 | // @desc Update a product 67 | // @route PUT /api/products/:id 68 | // @access Private/Admin 69 | const updateProduct = asyncHandler(async (req, res) => { 70 | const { name, price, description, image, brand, category, countInStock } = 71 | req.body; 72 | 73 | const product = await Product.findById(req.params.id); 74 | 75 | if (product) { 76 | product.name = name; 77 | product.price = price; 78 | product.description = description; 79 | product.image = image; 80 | product.brand = brand; 81 | product.category = category; 82 | product.countInStock = countInStock; 83 | 84 | const updatedProduct = await product.save(); 85 | res.json(updatedProduct); 86 | } else { 87 | res.status(404); 88 | throw new Error('Product not found'); 89 | } 90 | }); 91 | 92 | // @desc Delete a product 93 | // @route DELETE /api/products/:id 94 | // @access Private/Admin 95 | const deleteProduct = asyncHandler(async (req, res) => { 96 | const product = await Product.findById(req.params.id); 97 | 98 | if (product) { 99 | await Product.deleteOne({ _id: product._id }); 100 | res.json({ message: 'Product removed' }); 101 | } else { 102 | res.status(404); 103 | throw new Error('Product not found'); 104 | } 105 | }); 106 | 107 | // @desc Create new review 108 | // @route POST /api/products/:id/reviews 109 | // @access Private 110 | const createProductReview = asyncHandler(async (req, res) => { 111 | const { rating, comment } = req.body; 112 | 113 | const product = await Product.findById(req.params.id); 114 | 115 | if (product) { 116 | const alreadyReviewed = product.reviews.find( 117 | (r) => r.user.toString() === req.user._id.toString() 118 | ); 119 | 120 | if (alreadyReviewed) { 121 | res.status(400); 122 | throw new Error('Product already reviewed'); 123 | } 124 | 125 | const review = { 126 | name: req.user.name, 127 | rating: Number(rating), 128 | comment, 129 | user: req.user._id, 130 | }; 131 | 132 | product.reviews.push(review); 133 | 134 | product.numReviews = product.reviews.length; 135 | 136 | product.rating = 137 | product.reviews.reduce((acc, item) => item.rating + acc, 0) / 138 | product.reviews.length; 139 | 140 | await product.save(); 141 | res.status(201).json({ message: 'Review added' }); 142 | } else { 143 | res.status(404); 144 | throw new Error('Product not found'); 145 | } 146 | }); 147 | 148 | // @desc Get top rated products 149 | // @route GET /api/products/top 150 | // @access Public 151 | const getTopProducts = asyncHandler(async (req, res) => { 152 | const products = await Product.find({}).sort({ rating: -1 }).limit(3); 153 | 154 | res.json(products); 155 | }); 156 | 157 | export { 158 | getProducts, 159 | getProductById, 160 | createProduct, 161 | updateProduct, 162 | deleteProduct, 163 | createProductReview, 164 | getTopProducts, 165 | }; 166 | -------------------------------------------------------------------------------- /backend/controllers/userController.js: -------------------------------------------------------------------------------- 1 | import asyncHandler from '../middleware/asyncHandler.js'; 2 | import generateToken from '../utils/generateToken.js'; 3 | import User from '../models/userModel.js'; 4 | 5 | // @desc Auth user & get token 6 | // @route POST /api/users/auth 7 | // @access Public 8 | const authUser = asyncHandler(async (req, res) => { 9 | const { email, password } = req.body; 10 | 11 | const user = await User.findOne({ email }); 12 | 13 | if (user && (await user.matchPassword(password))) { 14 | generateToken(res, user._id); 15 | 16 | res.json({ 17 | _id: user._id, 18 | name: user.name, 19 | email: user.email, 20 | isAdmin: user.isAdmin, 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 POST /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 | generateToken(res, user._id); 49 | 50 | res.status(201).json({ 51 | _id: user._id, 52 | name: user.name, 53 | email: user.email, 54 | isAdmin: user.isAdmin, 55 | }); 56 | } else { 57 | res.status(400); 58 | throw new Error('Invalid user data'); 59 | } 60 | }); 61 | 62 | // @desc Logout user / clear cookie 63 | // @route POST /api/users/logout 64 | // @access Public 65 | const logoutUser = (req, res) => { 66 | res.clearCookie('jwt'); 67 | res.status(200).json({ message: 'Logged out successfully' }); 68 | }; 69 | 70 | // @desc Get user profile 71 | // @route GET /api/users/profile 72 | // @access Private 73 | const getUserProfile = asyncHandler(async (req, res) => { 74 | const user = await User.findById(req.user._id); 75 | 76 | if (user) { 77 | res.json({ 78 | _id: user._id, 79 | name: user.name, 80 | email: user.email, 81 | isAdmin: user.isAdmin, 82 | }); 83 | } else { 84 | res.status(404); 85 | throw new Error('User not found'); 86 | } 87 | }); 88 | 89 | // @desc Update user profile 90 | // @route PUT /api/users/profile 91 | // @access Private 92 | const updateUserProfile = asyncHandler(async (req, res) => { 93 | const user = await User.findById(req.user._id); 94 | 95 | if (user) { 96 | user.name = req.body.name || user.name; 97 | user.email = req.body.email || user.email; 98 | 99 | if (req.body.password) { 100 | user.password = req.body.password; 101 | } 102 | 103 | const updatedUser = await user.save(); 104 | 105 | res.json({ 106 | _id: updatedUser._id, 107 | name: updatedUser.name, 108 | email: updatedUser.email, 109 | isAdmin: updatedUser.isAdmin, 110 | }); 111 | } else { 112 | res.status(404); 113 | throw new Error('User not found'); 114 | } 115 | }); 116 | 117 | // @desc Get all users 118 | // @route GET /api/users 119 | // @access Private/Admin 120 | const getUsers = asyncHandler(async (req, res) => { 121 | const users = await User.find({}); 122 | res.json(users); 123 | }); 124 | 125 | // @desc Delete user 126 | // @route DELETE /api/users/:id 127 | // @access Private/Admin 128 | const deleteUser = asyncHandler(async (req, res) => { 129 | const user = await User.findById(req.params.id); 130 | 131 | if (user) { 132 | if (user.isAdmin) { 133 | res.status(400); 134 | throw new Error('Can not delete admin user'); 135 | } 136 | await User.deleteOne({ _id: user._id }); 137 | res.json({ message: 'User removed' }); 138 | } else { 139 | res.status(404); 140 | throw new Error('User not found'); 141 | } 142 | }); 143 | 144 | // @desc Get user by ID 145 | // @route GET /api/users/:id 146 | // @access Private/Admin 147 | const getUserById = asyncHandler(async (req, res) => { 148 | const user = await User.findById(req.params.id).select('-password'); 149 | 150 | if (user) { 151 | res.json(user); 152 | } else { 153 | res.status(404); 154 | throw new Error('User not found'); 155 | } 156 | }); 157 | // @desc Update user 158 | // @route PUT /api/users/:id 159 | // @access Private/Admin 160 | const updateUser = asyncHandler(async (req, res) => { 161 | const user = await User.findById(req.params.id); 162 | 163 | if (user) { 164 | user.name = req.body.name || user.name; 165 | user.email = req.body.email || user.email; 166 | user.isAdmin = Boolean(req.body.isAdmin); 167 | 168 | const updatedUser = await user.save(); 169 | 170 | res.json({ 171 | _id: updatedUser._id, 172 | name: updatedUser.name, 173 | email: updatedUser.email, 174 | isAdmin: updatedUser.isAdmin, 175 | }); 176 | } else { 177 | res.status(404); 178 | throw new Error('User not found'); 179 | } 180 | }); 181 | 182 | export { 183 | authUser, 184 | registerUser, 185 | logoutUser, 186 | getUserProfile, 187 | updateUserProfile, 188 | getUsers, 189 | deleteUser, 190 | getUserById, 191 | updateUser, 192 | }; 193 | -------------------------------------------------------------------------------- /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 13 Pro 256GB Memory', 16 | image: '/images/phone.jpg', 17 | description: 18 | 'Introducing the iPhone 13 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 5', 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@email.com', 7 | password: bcrypt.hashSync('123456', 10), 8 | isAdmin: true, 9 | }, 10 | { 11 | name: 'John Doe', 12 | email: 'john@email.com', 13 | password: bcrypt.hashSync('123456', 10), 14 | }, 15 | { 16 | name: 'Jane Doe', 17 | email: 'jane@email.com', 18 | password: bcrypt.hashSync('123456', 10), 19 | }, 20 | ]; 21 | 22 | export default users; 23 | -------------------------------------------------------------------------------- /backend/middleware/asyncHandler.js: -------------------------------------------------------------------------------- 1 | const asyncHandler = (fn) => (req, res, next) => 2 | Promise.resolve(fn(req, res, next)).catch(next); 3 | 4 | export default asyncHandler; 5 | -------------------------------------------------------------------------------- /backend/middleware/authMiddleware.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import asyncHandler from './asyncHandler.js'; 3 | import User from '../models/userModel.js'; 4 | 5 | // User must be authenticated 6 | const protect = asyncHandler(async (req, res, next) => { 7 | let token; 8 | 9 | // Read JWT from the 'jwt' cookie 10 | token = req.cookies.jwt; 11 | 12 | if (token) { 13 | try { 14 | const decoded = jwt.verify(token, process.env.JWT_SECRET); 15 | 16 | req.user = await User.findById(decoded.userId).select('-password'); 17 | 18 | next(); 19 | } catch (error) { 20 | console.error(error); 21 | res.status(401); 22 | throw new Error('Not authorized, token failed'); 23 | } 24 | } else { 25 | res.status(401); 26 | throw new Error('Not authorized, no token'); 27 | } 28 | }); 29 | 30 | // User must be an admin 31 | const admin = (req, res, next) => { 32 | if (req.user && req.user.isAdmin) { 33 | next(); 34 | } else { 35 | res.status(401); 36 | throw new Error('Not authorized as an admin'); 37 | } 38 | }; 39 | 40 | export { protect, admin }; 41 | -------------------------------------------------------------------------------- /backend/middleware/checkObjectId.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { isValidObjectId } from 'mongoose'; 3 | 4 | /** 5 | * Checks if the req.params.id is a valid Mongoose ObjectId. 6 | * 7 | * @param {import('express').Request} req - The Express request object. 8 | * @param {import('express').Response} res - The Express response object. 9 | * @param {import('express').NextFunction} next - The Express next middleware function. 10 | * @throws {Error} Throws an error if the ObjectId is invalid. 11 | */ 12 | 13 | function checkObjectId(req, res, next) { 14 | if (!isValidObjectId(req.params.id)) { 15 | res.status(404); 16 | throw new Error(`Invalid ObjectId of: ${req.params.id}`); 17 | } 18 | next(); 19 | } 20 | 21 | export default checkObjectId; 22 | -------------------------------------------------------------------------------- /backend/middleware/errorMiddleware.js: -------------------------------------------------------------------------------- 1 | const notFound = (req, res, next) => { 2 | const error = new Error(`Not Found - ${req.originalUrl}`); 3 | res.status(404); 4 | next(error); 5 | }; 6 | 7 | const errorHandler = (err, req, res, next) => { 8 | let statusCode = res.statusCode === 200 ? 500 : res.statusCode; 9 | let message = err.message; 10 | 11 | // NOTE: checking for invalid ObjectId moved to it's own middleware 12 | // See README for further info. 13 | 14 | res.status(statusCode).json({ 15 | message: message, 16 | stack: process.env.NODE_ENV === 'production' ? null : err.stack, 17 | }); 18 | }; 19 | 20 | export { notFound, errorHandler }; 21 | -------------------------------------------------------------------------------- /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 | itemsPrice: { 40 | type: Number, 41 | required: true, 42 | default: 0.0, 43 | }, 44 | taxPrice: { 45 | type: Number, 46 | required: true, 47 | default: 0.0, 48 | }, 49 | shippingPrice: { 50 | type: Number, 51 | required: true, 52 | default: 0.0, 53 | }, 54 | totalPrice: { 55 | type: Number, 56 | required: true, 57 | default: 0.0, 58 | }, 59 | isPaid: { 60 | type: Boolean, 61 | required: true, 62 | default: false, 63 | }, 64 | paidAt: { 65 | type: Date, 66 | }, 67 | isDelivered: { 68 | type: Boolean, 69 | required: true, 70 | default: false, 71 | }, 72 | deliveredAt: { 73 | type: Date, 74 | }, 75 | }, 76 | { 77 | timestamps: true, 78 | } 79 | ); 80 | 81 | const Order = mongoose.model('Order', orderSchema); 82 | 83 | export default Order; 84 | -------------------------------------------------------------------------------- /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 | // Match user entered password to hashed password in database 31 | userSchema.methods.matchPassword = async function (enteredPassword) { 32 | return await bcrypt.compare(enteredPassword, this.password); 33 | }; 34 | 35 | // Encrypt password using bcrypt 36 | userSchema.pre('save', async function (next) { 37 | if (!this.isModified('password')) { 38 | next(); 39 | } 40 | 41 | const salt = await bcrypt.genSalt(10); 42 | this.password = await bcrypt.hash(this.password, salt); 43 | }); 44 | 45 | const User = mongoose.model('User', userSchema); 46 | 47 | export default User; 48 | -------------------------------------------------------------------------------- /backend/routes/orderRoutes.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | const router = express.Router(); 3 | import { 4 | addOrderItems, 5 | getMyOrders, 6 | getOrderById, 7 | updateOrderToPaid, 8 | updateOrderToDelivered, 9 | getOrders, 10 | } from '../controllers/orderController.js'; 11 | import { protect, admin } from '../middleware/authMiddleware.js'; 12 | 13 | router.route('/').post(protect, addOrderItems).get(protect, admin, getOrders); 14 | router.route('/mine').get(protect, getMyOrders); 15 | router.route('/:id').get(protect, getOrderById); 16 | router.route('/:id/pay').put(protect, updateOrderToPaid); 17 | router.route('/:id/deliver').put(protect, admin, updateOrderToDelivered); 18 | 19 | export default router; 20 | -------------------------------------------------------------------------------- /backend/routes/productRoutes.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | const router = express.Router(); 3 | import { 4 | getProducts, 5 | getProductById, 6 | createProduct, 7 | updateProduct, 8 | deleteProduct, 9 | createProductReview, 10 | getTopProducts, 11 | } from '../controllers/productController.js'; 12 | import { protect, admin } from '../middleware/authMiddleware.js'; 13 | import checkObjectId from '../middleware/checkObjectId.js'; 14 | 15 | router.route('/').get(getProducts).post(protect, admin, createProduct); 16 | router.route('/:id/reviews').post(protect, checkObjectId, createProductReview); 17 | router.get('/top', getTopProducts); 18 | router 19 | .route('/:id') 20 | .get(checkObjectId, getProductById) 21 | .put(protect, admin, checkObjectId, updateProduct) 22 | .delete(protect, admin, checkObjectId, deleteProduct); 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 fileFilter(req, file, cb) { 20 | const filetypes = /jpe?g|png|webp/; 21 | const mimetypes = /image\/jpe?g|image\/png|image\/webp/; 22 | 23 | const extname = filetypes.test(path.extname(file.originalname).toLowerCase()); 24 | const mimetype = mimetypes.test(file.mimetype); 25 | 26 | if (extname && mimetype) { 27 | cb(null, true); 28 | } else { 29 | cb(new Error('Images only!'), false); 30 | } 31 | } 32 | 33 | const upload = multer({ storage, fileFilter }); 34 | const uploadSingleImage = upload.single('image'); 35 | 36 | router.post('/', (req, res) => { 37 | uploadSingleImage(req, res, function (err) { 38 | if (err) { 39 | return res.status(400).send({ message: err.message }); 40 | } 41 | 42 | res.status(200).send({ 43 | message: 'Image uploaded successfully', 44 | image: `/${req.file.path}`, 45 | }); 46 | }); 47 | }); 48 | 49 | export default router; 50 | -------------------------------------------------------------------------------- /backend/routes/userRoutes.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { 3 | authUser, 4 | registerUser, 5 | logoutUser, 6 | getUserProfile, 7 | updateUserProfile, 8 | getUsers, 9 | deleteUser, 10 | getUserById, 11 | updateUser, 12 | } from '../controllers/userController.js'; 13 | import { protect, admin } from '../middleware/authMiddleware.js'; 14 | 15 | const router = express.Router(); 16 | 17 | router.route('/').post(registerUser).get(protect, admin, getUsers); 18 | router.post('/auth', authUser); 19 | router.post('/logout', logoutUser); 20 | router 21 | .route('/profile') 22 | .get(protect, getUserProfile) 23 | .put(protect, updateUserProfile); 24 | router 25 | .route('/:id') 26 | .delete(protect, admin, deleteUser) 27 | .get(protect, admin, getUserById) 28 | .put(protect, admin, updateUser); 29 | 30 | export default router; 31 | -------------------------------------------------------------------------------- /backend/seeder.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import dotenv from 'dotenv'; 3 | import colors from 'colors'; 4 | import users from './data/users.js'; 5 | import products from './data/products.js'; 6 | import User from './models/userModel.js'; 7 | import Product from './models/productModel.js'; 8 | import Order from './models/orderModel.js'; 9 | import connectDB from './config/db.js'; 10 | 11 | dotenv.config(); 12 | 13 | connectDB(); 14 | 15 | const importData = async () => { 16 | try { 17 | await Order.deleteMany(); 18 | await Product.deleteMany(); 19 | await User.deleteMany(); 20 | 21 | const createdUsers = await User.insertMany(users); 22 | 23 | const adminUser = createdUsers[0]._id; 24 | 25 | const sampleProducts = products.map((product) => { 26 | return { ...product, user: adminUser }; 27 | }); 28 | 29 | await Product.insertMany(sampleProducts); 30 | 31 | console.log('Data Imported!'.green.inverse); 32 | process.exit(); 33 | } catch (error) { 34 | console.error(`${error}`.red.inverse); 35 | process.exit(1); 36 | } 37 | }; 38 | 39 | const destroyData = async () => { 40 | try { 41 | await Order.deleteMany(); 42 | await Product.deleteMany(); 43 | await User.deleteMany(); 44 | 45 | console.log('Data Destroyed!'.red.inverse); 46 | process.exit(); 47 | } catch (error) { 48 | console.error(`${error}`.red.inverse); 49 | process.exit(1); 50 | } 51 | }; 52 | 53 | if (process.argv[2] === '-d') { 54 | destroyData(); 55 | } else { 56 | importData(); 57 | } 58 | -------------------------------------------------------------------------------- /backend/server.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import express from 'express'; 3 | import dotenv from 'dotenv'; 4 | import cookieParser from 'cookie-parser'; 5 | dotenv.config(); 6 | import connectDB from './config/db.js'; 7 | import productRoutes from './routes/productRoutes.js'; 8 | import userRoutes from './routes/userRoutes.js'; 9 | import orderRoutes from './routes/orderRoutes.js'; 10 | import uploadRoutes from './routes/uploadRoutes.js'; 11 | import { notFound, errorHandler } from './middleware/errorMiddleware.js'; 12 | 13 | const port = process.env.PORT || 5000; 14 | 15 | connectDB(); 16 | 17 | const app = express(); 18 | 19 | app.use(express.json()); 20 | app.use(express.urlencoded({ extended: true })); 21 | app.use(cookieParser()); 22 | 23 | app.use('/api/products', productRoutes); 24 | app.use('/api/users', userRoutes); 25 | app.use('/api/orders', orderRoutes); 26 | app.use('/api/upload', uploadRoutes); 27 | 28 | app.get('/api/config/paypal', (req, res) => 29 | res.send({ clientId: process.env.PAYPAL_CLIENT_ID }) 30 | ); 31 | 32 | if (process.env.NODE_ENV === 'production') { 33 | const __dirname = path.resolve(); 34 | app.use('/uploads', express.static('/var/data/uploads')); 35 | app.use(express.static(path.join(__dirname, '/frontend/build'))); 36 | 37 | app.get('*', (req, res) => 38 | res.sendFile(path.resolve(__dirname, 'frontend', 'build', 'index.html')) 39 | ); 40 | } else { 41 | const __dirname = path.resolve(); 42 | app.use('/uploads', express.static(path.join(__dirname, '/uploads'))); 43 | app.get('/', (req, res) => { 44 | res.send('API is running....'); 45 | }); 46 | } 47 | 48 | app.use(notFound); 49 | app.use(errorHandler); 50 | 51 | app.listen(port, () => 52 | console.log(`Server running in ${process.env.NODE_ENV} mode on port ${port}`) 53 | ); 54 | -------------------------------------------------------------------------------- /backend/utils/calcPrices.js: -------------------------------------------------------------------------------- 1 | function addDecimals(num) { 2 | return (Math.round(num * 100) / 100).toFixed(2); 3 | } 4 | 5 | // NOTE: the code below has been changed from the course code to fix an issue 6 | // with type coercion of strings to numbers. 7 | // Our addDecimals function expects a number and returns a string, so it is not 8 | // correct to call it passing a string as the argument. 9 | 10 | export function calcPrices(orderItems) { 11 | // Calculate the items price in whole number (pennies) to avoid issues with 12 | // floating point number calculations 13 | const itemsPrice = orderItems.reduce( 14 | (acc, item) => acc + (item.price * 100 * item.qty) / 100, 15 | 0 16 | ); 17 | 18 | // Calculate the shipping price 19 | const shippingPrice = itemsPrice > 100 ? 0 : 10; 20 | 21 | // Calculate the tax price 22 | const taxPrice = 0.15 * itemsPrice; 23 | 24 | // Calculate the total price 25 | const totalPrice = itemsPrice + shippingPrice + taxPrice; 26 | 27 | // return prices as strings fixed to 2 decimal places 28 | return { 29 | itemsPrice: addDecimals(itemsPrice), 30 | shippingPrice: addDecimals(shippingPrice), 31 | taxPrice: addDecimals(taxPrice), 32 | totalPrice: addDecimals(totalPrice), 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /backend/utils/generateToken.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | 3 | const generateToken = (res, userId) => { 4 | const token = jwt.sign({ userId }, process.env.JWT_SECRET, { 5 | expiresIn: '30d', 6 | }); 7 | 8 | // Set JWT as an HTTP-Only cookie 9 | res.cookie('jwt', token, { 10 | httpOnly: true, 11 | secure: process.env.NODE_ENV !== 'development', // Use secure cookies in production 12 | sameSite: 'strict', // Prevent CSRF attacks 13 | maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days 14 | }); 15 | }; 16 | 17 | export default generateToken; 18 | -------------------------------------------------------------------------------- /backend/utils/paypal.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | dotenv.config(); 3 | const { PAYPAL_CLIENT_ID, PAYPAL_APP_SECRET, PAYPAL_API_URL } = process.env; 4 | 5 | /** 6 | * Fetches an access token from the PayPal API. 7 | * @see {@link https://developer.paypal.com/reference/get-an-access-token/#link-getanaccesstoken} 8 | * 9 | * @returns {Promise} The access token if the request is successful. 10 | * @throws {Error} If the request is not successful. 11 | * 12 | */ 13 | async function getPayPalAccessToken() { 14 | // Authorization header requires base64 encoding 15 | const auth = Buffer.from(PAYPAL_CLIENT_ID + ':' + PAYPAL_APP_SECRET).toString( 16 | 'base64' 17 | ); 18 | 19 | const url = `${PAYPAL_API_URL}/v1/oauth2/token`; 20 | 21 | const headers = { 22 | Accept: 'application/json', 23 | 'Accept-Language': 'en_US', 24 | Authorization: `Basic ${auth}`, 25 | }; 26 | 27 | const body = 'grant_type=client_credentials'; 28 | const response = await fetch(url, { 29 | method: 'POST', 30 | headers, 31 | body, 32 | }); 33 | 34 | if (!response.ok) throw new Error('Failed to get access token'); 35 | 36 | const paypalData = await response.json(); 37 | 38 | return paypalData.access_token; 39 | } 40 | 41 | /** 42 | * Checks if a PayPal transaction is new by comparing the transaction ID with existing orders in the database. 43 | * 44 | * @param {Mongoose.Model} orderModel - The Mongoose model for the orders in the database. 45 | * @param {string} paypalTransactionId - The PayPal transaction ID to be checked. 46 | * @returns {Promise} Returns true if it is a new transaction (i.e., the transaction ID does not exist in the database), false otherwise. 47 | * @throws {Error} If there's an error in querying the database. 48 | * 49 | */ 50 | export async function checkIfNewTransaction(orderModel, paypalTransactionId) { 51 | try { 52 | // Find all documents where Order.paymentResult.id is the same as the id passed paypalTransactionId 53 | const orders = await orderModel.find({ 54 | 'paymentResult.id': paypalTransactionId, 55 | }); 56 | 57 | // If there are no such orders, then it's a new transaction. 58 | return orders.length === 0; 59 | } catch (err) { 60 | console.error(err); 61 | } 62 | } 63 | 64 | /** 65 | * Verifies a PayPal payment by making a request to the PayPal API. 66 | * @see {@link https://developer.paypal.com/docs/api/orders/v2/#orders_get} 67 | * 68 | * @param {string} paypalTransactionId - The PayPal transaction ID to be verified. 69 | * @returns {Promise} An object with properties 'verified' indicating if the payment is completed and 'value' indicating the payment amount. 70 | * @throws {Error} If the request is not successful. 71 | * 72 | */ 73 | export async function verifyPayPalPayment(paypalTransactionId) { 74 | const accessToken = await getPayPalAccessToken(); 75 | const paypalResponse = await fetch( 76 | `${PAYPAL_API_URL}/v2/checkout/orders/${paypalTransactionId}`, 77 | { 78 | headers: { 79 | 'Content-Type': 'application/json', 80 | Authorization: `Bearer ${accessToken}`, 81 | }, 82 | } 83 | ); 84 | if (!paypalResponse.ok) throw new Error('Failed to verify payment'); 85 | 86 | const paypalData = await paypalResponse.json(); 87 | return { 88 | verified: paypalData.status === 'COMPLETED', 89 | value: paypalData.purchase_units[0].amount.value, 90 | }; 91 | } 92 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "proxy": "http://localhost:5000", 6 | "dependencies": { 7 | "@paypal/react-paypal-js": "^7.8.2", 8 | "@reduxjs/toolkit": "^1.9.3", 9 | "@testing-library/jest-dom": "^5.16.5", 10 | "@testing-library/react": "^13.4.0", 11 | "@testing-library/user-event": "^13.5.0", 12 | "axios": "^1.3.4", 13 | "bootstrap": "^5.2.3", 14 | "react": "^18.2.0", 15 | "react-bootstrap": "^2.7.2", 16 | "react-dom": "^18.2.0", 17 | "react-helmet-async": "^1.3.0", 18 | "react-icons": "^4.8.0", 19 | "react-redux": "^8.0.5", 20 | "react-router-dom": "^6.8.2", 21 | "react-scripts": "5.0.1", 22 | "react-toastify": "^9.1.1", 23 | "web-vitals": "^2.1.4" 24 | }, 25 | "scripts": { 26 | "start": "react-scripts start", 27 | "build": "react-scripts build", 28 | "test": "react-scripts test", 29 | "eject": "react-scripts eject" 30 | }, 31 | "eslintConfig": { 32 | "extends": [ 33 | "react-app", 34 | "react-app/jest" 35 | ] 36 | }, 37 | "browserslist": { 38 | "production": [ 39 | ">0.2%", 40 | "not dead", 41 | "not op_mini all" 42 | ], 43 | "development": [ 44 | "last 1 chrome version", 45 | "last 1 firefox version", 46 | "last 1 safari version" 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradtraversy/proshop-v2/e23e3a897e5387f39da6c59fd72cdd12e46d0815/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/images/airpods.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradtraversy/proshop-v2/e23e3a897e5387f39da6c59fd72cdd12e46d0815/frontend/public/images/airpods.jpg -------------------------------------------------------------------------------- /frontend/public/images/alexa.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradtraversy/proshop-v2/e23e3a897e5387f39da6c59fd72cdd12e46d0815/frontend/public/images/alexa.jpg -------------------------------------------------------------------------------- /frontend/public/images/camera.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradtraversy/proshop-v2/e23e3a897e5387f39da6c59fd72cdd12e46d0815/frontend/public/images/camera.jpg -------------------------------------------------------------------------------- /frontend/public/images/mouse.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradtraversy/proshop-v2/e23e3a897e5387f39da6c59fd72cdd12e46d0815/frontend/public/images/mouse.jpg -------------------------------------------------------------------------------- /frontend/public/images/phone.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradtraversy/proshop-v2/e23e3a897e5387f39da6c59fd72cdd12e46d0815/frontend/public/images/phone.jpg -------------------------------------------------------------------------------- /frontend/public/images/playstation.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradtraversy/proshop-v2/e23e3a897e5387f39da6c59fd72cdd12e46d0815/frontend/public/images/playstation.jpg -------------------------------------------------------------------------------- /frontend/public/images/sample.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradtraversy/proshop-v2/e23e3a897e5387f39da6c59fd72cdd12e46d0815/frontend/public/images/sample.jpg -------------------------------------------------------------------------------- /frontend/public/images/screens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradtraversy/proshop-v2/e23e3a897e5387f39da6c59fd72cdd12e46d0815/frontend/public/images/screens.png -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Welcome To ProShop! 11 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradtraversy/proshop-v2/e23e3a897e5387f39da6c59fd72cdd12e46d0815/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradtraversy/proshop-v2/e23e3a897e5387f39da6c59fd72cdd12e46d0815/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 { useEffect } from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | import { Container } from 'react-bootstrap'; 4 | import { Outlet } from 'react-router-dom'; 5 | import Header from './components/Header'; 6 | import Footer from './components/Footer'; 7 | import { logout } from './slices/authSlice'; 8 | 9 | import { ToastContainer } from 'react-toastify'; 10 | import 'react-toastify/dist/ReactToastify.css'; 11 | 12 | const App = () => { 13 | const dispatch = useDispatch(); 14 | 15 | useEffect(() => { 16 | const expirationTime = localStorage.getItem('expirationTime'); 17 | if (expirationTime) { 18 | const currentTime = new Date().getTime(); 19 | 20 | if (currentTime > expirationTime) { 21 | dispatch(logout()); 22 | } 23 | } 24 | }, [dispatch]); 25 | 26 | return ( 27 | <> 28 | 29 |
30 |
31 | 32 | 33 | 34 |
35 |