├── .gitignore ├── README.md ├── backend ├── .gitignore ├── jsconfig.json ├── package-lock.json ├── package.json └── src │ ├── config │ ├── cloudinary.config.js │ ├── database.config.js │ └── mail.config.js │ ├── constants │ ├── httpStatus.js │ └── orderStatus.js │ ├── data.js │ ├── helpers │ └── mail.helper.js │ ├── middleware │ ├── admin.mid.js │ └── auth.mid.js │ ├── models │ ├── food.model.js │ ├── order.model.js │ └── user.model.js │ ├── routers │ ├── food.router.js │ ├── order.router.js │ ├── upload.router.js │ └── user.router.js │ └── server.js ├── frontend ├── .gitignore ├── README.md ├── jsconfig.json ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── foods │ │ ├── food-1.jpg │ │ ├── food-2.jpg │ │ ├── food-3.jpg │ │ ├── food-4.jpg │ │ ├── food-5.jpg │ │ └── food-6.jpg │ ├── icons │ │ ├── foods.svg │ │ ├── orders.svg │ │ ├── profile.svg │ │ └── users.svg │ ├── index.html │ ├── layers-2x.png │ ├── layers.png │ ├── loading.svg │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ ├── marker-icon-2x.png │ ├── marker-icon.png │ ├── marker-shadow.png │ ├── robots.txt │ ├── star-empty.svg │ ├── star-full.svg │ └── star-half.svg └── src │ ├── App.js │ ├── AppRoutes.js │ ├── axiosConfig.js │ ├── components │ ├── AdminRoute │ │ └── AdminRoute.js │ ├── AuthRoute │ │ └── AuthRoute.js │ ├── Button │ │ ├── Button.js │ │ └── button.module.css │ ├── ChangePassword │ │ └── ChangePassword.js │ ├── DateTime │ │ └── DateTime.js │ ├── Header │ │ ├── Header.js │ │ └── header.module.css │ ├── Input │ │ ├── Input.js │ │ └── input.module.css │ ├── InputContainer │ │ ├── InputContainer.js │ │ └── inputContainer.module.css │ ├── Loading │ │ ├── Loading.js │ │ └── loading.module.css │ ├── Map │ │ ├── Map.js │ │ └── map.module.css │ ├── NotFound │ │ ├── NotFound.js │ │ └── notFound.module.css │ ├── OrderItemsList │ │ ├── OrderItemsList.js │ │ └── orderItemsList.module.css │ ├── PaypalButtons │ │ └── PaypalButtons.js │ ├── Price │ │ └── Price.js │ ├── Search │ │ ├── Search.js │ │ └── search.module.css │ ├── StarRating │ │ ├── StarRating.js │ │ └── starRating.module.css │ ├── Tags │ │ ├── Tags.js │ │ └── tags.module.css │ ├── Thumbnails │ │ ├── Thumbnails.js │ │ └── thumbnails.module.css │ └── Title │ │ ├── Title.js │ │ └── title.module.css │ ├── constants │ └── patterns.js │ ├── hooks │ ├── useAuth.js │ ├── useCart.js │ └── useLoading.js │ ├── index.css │ ├── index.js │ ├── interceptors │ ├── authInterceptor.js │ └── loadingInterceptor.js │ ├── pages │ ├── Cart │ │ ├── CartPage.js │ │ └── cartPage.module.css │ ├── Checkout │ │ ├── CheckoutPage.js │ │ └── checkoutPage.module.css │ ├── Dashboard │ │ ├── Dashboard.js │ │ └── dashboard.module.css │ ├── Food │ │ ├── FoodPage.js │ │ └── foodPage.module.css │ ├── FoodEdit │ │ ├── FoodEditPage.js │ │ └── foodEdit.module.css │ ├── FoodsAdmin │ │ ├── FoodsAdminPage.js │ │ └── foodsAdminPage.module.css │ ├── Home │ │ └── HomePage.js │ ├── Login │ │ ├── LoginPage.js │ │ └── loginPage.module.css │ ├── OrderTrack │ │ ├── OrderTrackPage.js │ │ └── orderTrackPage.module.css │ ├── Orders │ │ ├── OrdersPage.js │ │ └── ordersPage.module.css │ ├── Payment │ │ ├── PaymentPage.js │ │ └── paymentPage.module.css │ ├── Profile │ │ ├── ProfilePage.js │ │ └── profilePage.module.css │ ├── Register │ │ ├── RegisterPage.js │ │ └── registerPage.module.css │ ├── UserEdit │ │ ├── UserEditPage.js │ │ └── userEdit.module.css │ └── UsersPage │ │ ├── UsersPage.js │ │ └── usersPage.module.css │ ├── reportWebVitals.js │ └── services │ ├── foodService.js │ ├── orderService.js │ ├── uploadService.js │ └── userService.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | /.DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lessons: 2 | 3 | ## 1. Demo And Installation 4 | 5 | - [x] Install [NodeJs](https://nodejs.org/en) 6 | - [x] Install [Visual Studio Code](https://code.visualstudio.com) 7 | - [x] Install [Git](https://git-scm.com) 8 | 9 | ## 2. Creating React App 10 | 11 | - [x] Create React App 12 | - [x] Remove Unnecessary Codes 13 | 14 | ## 3. Adding Header 15 | 16 | - [x] Add Header.js 17 | - [x] Use Header in App.js 18 | - [x] Install react-router-dom in frontend 19 | - [x] Add header.module.css 20 | - [x] Use BrowserRouter inside index.js 21 | - [x] Update Header.js 22 | - [x] Update header.module.css 23 | 24 | ## 4. Adding Thumbnails 25 | 26 | - [x] Add HomePage component 27 | - [x] Add AppRoutes component 28 | - [x] Use AppRoutes in App.js 29 | - [x] Add data.js 30 | - [x] Add food Images 31 | - [x] Add foodService.js 32 | - [x] Update HomePage.js 33 | - [x] Add Reducer 34 | - [x] Load foods 35 | - [x] Add Thumbnails.js 36 | - [x] Add CSS File 37 | - [x] Add Image 38 | - [x] Add Title 39 | - [x] Add Favorite Icon 40 | - [x] Add StarRating.js 41 | - [x] Add Star Images 42 | - [x] Add CSS 43 | - [x] Add Origins 44 | - [x] Add Cook Time 45 | - [x] Add Price.js 46 | - [x] Update CSS File 47 | 48 | ## 5. Adding Search 49 | 50 | - [x] Add Search Route to AppRoutes.js 51 | - [x] Add Search function to foodService.js 52 | - [x] Use Search Inside HomePage.js 53 | - [x] Add Search Component 54 | - [x] Add CSS 55 | 56 | ## 6.Adding Tags Bar 57 | 58 | ### Showing The Tags: 59 | 60 | - [x] Add sample_tags to data.js 61 | - [x] Add getAllTags function to foodService.js 62 | - [x] Add Tags Component 63 | - [x] Add Css 64 | - [x] Use Tags Component in HomePage.js 65 | 66 | ### Showing Foods By Tag 67 | 68 | - [x] Add Tag route to AppRoutes.js 69 | - [x] Add getAllByTag function to foodService.js 70 | - [x] Use tag param in HomePage.js 71 | 72 | ## 7. Food Page 73 | 74 | - [x] Create FoodPage Component 75 | - [x] Add route to AppRoutes.js 76 | - [x] Add getById function to foodService.js 77 | - [x] Update FoodPage Component 78 | - [x] Load food 79 | - [x] Create Template 80 | - [x] Add Css 81 | 82 | ## 8. Cart Page 83 | 84 | - [x] Create Cart Page Component 85 | - [x] Create css 86 | - [x] Add cart route to the Routes 87 | - [x] Create useCart Hook 88 | - [x] Add CartProvider to index.js 89 | - [x] Initialize cart with sample foods 90 | - [x] Update Cart Page Compnent 91 | - [x] useCart hook 92 | - [x] Add Title Component 93 | - [x] Add JSX 94 | - [x] Add CSS 95 | - [x] Update useCart Hook 96 | - [x] Add to cart 97 | - [x] Remove from cart 98 | - [x] Change quantity 99 | - [x] Saving To LocalStorage 100 | - [x] In Food Page useCart for Add to cart buttons 101 | - [x] In Header useCart for cart total count 102 | 103 | ## 9.Not Found! 104 | 105 | - [x] Create NotFound Component 106 | - [x] Add CSS 107 | - [x] Add Not Found To: 108 | - [x] Home Page 109 | - [x] Food Page 110 | - [x] Cart Page 111 | - [x] Fixing Search Issue 112 | 113 | ## 10. Connect To Backend 114 | 115 | - [x] Create backend folder 116 | - [x] Initializing NPM Project 117 | - [x] Copy data.ts to backend/src 118 | - [x] npm install express cors 119 | - [x] Create .gitignore 120 | - [x] Create server.js 121 | - [x] Add & Config Express 122 | - [x] Add & Config Cors 123 | - [x] Add Food Router 124 | - [x] Add jsconfig.json 125 | - [x] Add Apis 126 | - [x] npm install nodemon 127 | - [x] Add dev Script into the package.json 128 | - [x] npm run dev 129 | - [x] Add axios package 130 | - [x] axiosConfig.js file 131 | - [x] Connect food service to the Apis 132 | 133 | ## 11. Login Page 134 | 135 | ### Backend 136 | 137 | - [x] Create User Router 138 | - [x] npm install jsonwebtoken 139 | - [x] Add Login Api 140 | - [x] Add sample_users to data.js 141 | - [x] Add httpStatus.js 142 | - [x] Add generateTokenResponse function 143 | - [x] Add User Router To server.js 144 | 145 | ### Frontend 146 | 147 | - [x] Create user service 148 | - [x] Add getUser function 149 | - [x] Add login function 150 | - [x] Add logout function 151 | - [x] npm install react-toastify 152 | - [x] Create useAuth hook 153 | - [x] Add user state 154 | - [x] Add Login function 155 | - [x] Add logout function 156 | - [x] Create LoginPage component 157 | - [x] Add to AppRoutes.js 158 | - [x] Create Custom Components 159 | - [x] Input Container 160 | - [x] CSS 161 | - [x] Input 162 | - [x] CSS 163 | - [x] Button 164 | - [x] CSS 165 | - [x] Add useAuth to the Header component 166 | 167 | ## 12. Connecting MongoDB 168 | 169 | ### Installation 170 | 171 | - [x] Install Mongo Db Community 172 | - [x] Windows 173 | - [x] Macos 174 | 175 | ### Coding 176 | 177 | - [x] Install mongoose 178 | - [x] Add User Model 179 | - [x] Add Food Model 180 | - [x] Add .env file 181 | - [x] Install dotenv 182 | - [x] Add MONGO_URI 183 | - [x] Add to .gitignore 184 | - [x] Add database.config.js 185 | - [x] Connect to mongodb 186 | - [x] Seed Users 187 | - [x] Install bcryptjs for password hashing 188 | - [x] Seed Foods 189 | - [x] Update user.router ( Using UserModel) 190 | - [x] Install express-async-handler 191 | - [x] Login Api 192 | - [x] generateTokenResponse 193 | - [x] Update food.router (Using FoodModel): 194 | - [x] Root Api ( Loading all foods ) 195 | - [x] Tags api 196 | - [x] Search Api 197 | - [x] FoodId api ( Finding food by id ) 198 | - [x] Fix Image url in: 199 | - [x] Thumnails compnent 200 | - [x] Food Page component 201 | - [x] Cart Page component 202 | 203 | ## 13. Register Page 204 | 205 | - [x] Add Register Page Component 206 | - [x] Add to AppRoutes 207 | - [x] Add Link to login page 208 | - [x] CSS 209 | - [x] Add '/register' api to user.router.js 210 | - [x] Add register function in userService 211 | - [x] Add register function in useAuth hook 212 | - [x] Add to Register page 213 | 214 | ## 14. Loading 215 | 216 | - [x] Create useLoading hook 217 | - [x] Add LoadingProvider to index.js 218 | - [x] Create Loading component 219 | - [x] Add to App.js 220 | - [x] Add Image 221 | - [x] CSS 222 | - [x] Create Loading Interceptor 223 | 224 | ## 15. Checkout Page 225 | 226 | - [x] Fixing Loading problem 227 | - [x] Create Checkout Page component 228 | - [x] Create AuthRoute 229 | - [x] Add to Routes 230 | - [x] Add css 231 | - [x] Create Order Items List 232 | - [x] Create Maps Component 233 | - [x] Install leaflet & react-leaflet 234 | - [x] Adding images to public folder 235 | - [x] Fixing header menu problem with map 236 | - [x] Create Order router 237 | 238 | - [x] Create auth middleware 239 | - [x] Add UNAUTHORIZED http statuss 240 | - [x] Add to Order router 241 | - [x] Create Order Model 242 | - [x] Create Order Status 243 | - [x] Add to server.js 244 | 245 | - [x] Connecting Frontend to Backend 246 | - [x] Create order service 247 | - [x] Add create function 248 | - [x] Create Auth interceptor 249 | - [x] Add to index.js 250 | 251 | ## 16. Payment Page 252 | 253 | - [x] Create PaymentPage component 254 | - [x] Add to Routes 255 | - [x] CSS 256 | - [x] Update Order Router 257 | - [x] Add newOrderForCurrentUser 258 | - [x] Add pay api 259 | - [x] Create PaypalButtons Component 260 | - [x] npm install @paypal/react-paypal-js 261 | - [x] Add clearCart to useCart 262 | - [x] Get clientId 263 | - [x] Create Sandbox user for testing 264 | 265 | ## 17. Order Track Page 266 | 267 | - [x] Create Order Track Page 268 | - [x] Add To Routes 269 | - [x] CSS 270 | - [x] Create DateTime Component 271 | - [x] Complete 272 | - [x] Map 273 | - [x] Fixing Marker Icon Issue 274 | - [x] Complete 275 | - [x] Order Router 276 | - [x] Add ‘track/:id’ api 277 | - [x] Add to orderService 278 | 279 | ## 18.Profile Page 280 | 281 | - [x] Create ProfilePage Component 282 | - [x] CSS 283 | - [x] Update Profile 284 | - [x] ChangePassword component 285 | - [x] Update useAuth hook 286 | - [x] Add updateProfile function 287 | - [x] Add changePassword function 288 | - [x] Update userService 289 | - [x] Add updateProfile funciton 290 | - [x] Add changePassword function 291 | - [x] Update User Router 292 | - [x] Add updateProfile api 293 | - [x] Add changePassword api 294 | 295 | ## 19. Orders Page 296 | 297 | - [x] Create Orders Page 298 | - [x] Add To Routes 299 | - [x] CSS 300 | - [x] Update Order Service 301 | - [x] Add getAll function 302 | - [x] Add getAllStatus function 303 | - [x] Update Order Router 304 | - [x] Add `/:status?` 305 | - [x] Add `/allStatus/:id` 306 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | public -------------------------------------------------------------------------------- /backend/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node" 4 | }, 5 | "include": ["src"] 6 | } 7 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node src/server.js", 9 | "dev": "nodemon src/server.js", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "bcryptjs": "^2.4.3", 17 | "cloudinary": "^1.41.0", 18 | "cors": "^2.8.5", 19 | "dotenv": "^16.1.4", 20 | "express": "^4.18.2", 21 | "express-async-handler": "^1.2.0", 22 | "form-data": "^4.0.0", 23 | "jsonwebtoken": "^9.0.0", 24 | "mailgun.js": "^9.4.1", 25 | "mongoose": "^7.3.0", 26 | "multer": "^1.4.5-lts.1" 27 | }, 28 | "devDependencies": { 29 | "nodemon": "^2.0.22" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /backend/src/config/cloudinary.config.js: -------------------------------------------------------------------------------- 1 | import { v2 as cloudinary } from 'cloudinary'; 2 | 3 | export const configCloudinary = () => { 4 | cloudinary.config({ 5 | cloud_name: process.env.CLOUDINARY_CLOUD_NAME, 6 | api_key: process.env.CLOUDINARY_API_KEY, 7 | api_secret: process.env.CLOUDINARY_API_SECRET, 8 | }); 9 | 10 | return cloudinary; 11 | }; 12 | -------------------------------------------------------------------------------- /backend/src/config/database.config.js: -------------------------------------------------------------------------------- 1 | import { connect, set } from 'mongoose'; 2 | import { UserModel } from '../models/user.model.js'; 3 | import { FoodModel } from '../models/food.model.js'; 4 | import { sample_users } from '../data.js'; 5 | import { sample_foods } from '../data.js'; 6 | import bcrypt from 'bcryptjs'; 7 | const PASSWORD_HASH_SALT_ROUNDS = 10; 8 | set('strictQuery', true); 9 | 10 | export const dbconnect = async () => { 11 | try { 12 | connect(process.env.MONGO_URI, { 13 | useNewUrlParser: true, 14 | useUnifiedTopology: true, 15 | }); 16 | await seedUsers(); 17 | await seedFoods(); 18 | console.log('connect successfully---'); 19 | } catch (error) { 20 | console.log(error); 21 | } 22 | }; 23 | 24 | async function seedUsers() { 25 | const usersCount = await UserModel.countDocuments(); 26 | if (usersCount > 0) { 27 | console.log('Users seed is already done!'); 28 | return; 29 | } 30 | 31 | for (let user of sample_users) { 32 | user.password = await bcrypt.hash(user.password, PASSWORD_HASH_SALT_ROUNDS); 33 | await UserModel.create(user); 34 | } 35 | 36 | console.log('Users seed is done!'); 37 | } 38 | 39 | async function seedFoods() { 40 | const foods = await FoodModel.countDocuments(); 41 | if (foods > 0) { 42 | console.log('Foods seed is already done!'); 43 | return; 44 | } 45 | 46 | for (const food of sample_foods) { 47 | food.imageUrl = `/foods/${food.imageUrl}`; 48 | await FoodModel.create(food); 49 | } 50 | 51 | console.log('Foods seed Is Done!'); 52 | } 53 | -------------------------------------------------------------------------------- /backend/src/config/mail.config.js: -------------------------------------------------------------------------------- 1 | import FormData from 'form-data'; 2 | import Mailgun from 'mailgun.js'; 3 | 4 | export function getClient() { 5 | const mailgun = new Mailgun(FormData); 6 | const client = mailgun.client({ 7 | username: 'api', 8 | key: process.env.MAILGUN_API_KEY, 9 | }); 10 | 11 | return client; 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/constants/httpStatus.js: -------------------------------------------------------------------------------- 1 | export const BAD_REQUEST = 400; 2 | export const UNAUTHORIZED = 401; 3 | -------------------------------------------------------------------------------- /backend/src/constants/orderStatus.js: -------------------------------------------------------------------------------- 1 | export const OrderStatus = { 2 | NEW: 'NEW', 3 | PAYED: 'PAYED', 4 | SHIPPED: 'SHIPPED', 5 | CANCELED: 'CANCELED', 6 | REFUNDED: 'REFUNDED', 7 | }; 8 | -------------------------------------------------------------------------------- /backend/src/data.js: -------------------------------------------------------------------------------- 1 | export const sample_foods = [ 2 | { 3 | id: '1', 4 | name: 'Pizza Pepperoni', 5 | cookTime: '10-20', 6 | price: 10, 7 | favorite: false, 8 | origins: ['italy'], 9 | stars: 4.5, 10 | imageUrl: 'food-1.jpg', 11 | tags: ['FastFood', 'Pizza', 'Lunch'], 12 | }, 13 | { 14 | id: '2', 15 | name: 'Meatball', 16 | price: 20, 17 | cookTime: '20-30', 18 | favorite: true, 19 | origins: ['persia', 'middle east', 'china'], 20 | stars: 5, 21 | imageUrl: 'food-2.jpg', 22 | tags: ['SlowFood', 'Lunch'], 23 | }, 24 | { 25 | id: '3', 26 | name: 'Hamburger', 27 | price: 5, 28 | cookTime: '10-15', 29 | favorite: false, 30 | origins: ['germany', 'us'], 31 | stars: 3.5, 32 | imageUrl: 'food-3.jpg', 33 | tags: ['FastFood', 'Hamburger'], 34 | }, 35 | { 36 | id: '4', 37 | name: 'Fried Potatoes', 38 | price: 2, 39 | cookTime: '15-20', 40 | favorite: true, 41 | origins: ['belgium', 'france'], 42 | stars: 3, 43 | imageUrl: 'food-4.jpg', 44 | tags: ['FastFood', 'Fry'], 45 | }, 46 | { 47 | id: '5', 48 | name: 'Chicken Soup', 49 | price: 11, 50 | cookTime: '40-50', 51 | favorite: false, 52 | origins: ['india', 'asia'], 53 | stars: 3.5, 54 | imageUrl: 'food-5.jpg', 55 | tags: ['SlowFood', 'Soup'], 56 | }, 57 | { 58 | id: '6', 59 | name: 'Vegetables Pizza', 60 | price: 9, 61 | cookTime: '40-50', 62 | favorite: false, 63 | origins: ['italy'], 64 | stars: 4.0, 65 | imageUrl: 'food-6.jpg', 66 | tags: ['FastFood', 'Pizza', 'Lunch'], 67 | }, 68 | ]; 69 | 70 | export const sample_tags = [ 71 | { name: 'All', count: 6 }, 72 | { name: 'FastFood', count: 4 }, 73 | { name: 'Pizza', count: 2 }, 74 | { name: 'Lunch', count: 3 }, 75 | { name: 'SlowFood', count: 2 }, 76 | { name: 'Hamburger', count: 1 }, 77 | { name: 'Fry', count: 1 }, 78 | { name: 'Soup', count: 1 }, 79 | ]; 80 | 81 | export const sample_users = [ 82 | { 83 | id: 1, 84 | name: 'John Doe', 85 | email: 'john@gmail.com', 86 | password: '12345', 87 | address: 'Toronto On', 88 | isAdmin: false, 89 | }, 90 | { 91 | id: 2, 92 | name: 'Jane Doe', 93 | email: 'jane@gmail.com', 94 | password: '12345', 95 | address: 'Shanghai', 96 | isAdmin: true, 97 | }, 98 | ]; 99 | -------------------------------------------------------------------------------- /backend/src/helpers/mail.helper.js: -------------------------------------------------------------------------------- 1 | import { getClient } from '../config/mail.config.js'; 2 | 3 | export const sendEmailReceipt = function (order) { 4 | const mailClient = getClient(); 5 | 6 | mailClient.messages 7 | .create('sandbox80bf0ab584cb42dbbf5cf0e9a249e188.mailgun.org', { 8 | from: 'orders@foodmine.com', 9 | to: order.user.email, 10 | subject: `Order ${order.id} is being processed`, 11 | html: getReceiptHtml(order), 12 | }) 13 | .then(msg => console.log(msg)) //success 14 | .catch(err => console.log(err)); //fail; 15 | }; 16 | 17 | const getReceiptHtml = function (order) { 18 | return ` 19 | 20 | 21 | 35 | 36 | 37 |

Order Payment Confirmation

38 |

Dear ${order.name},

39 |

Thank you for choosing us! Your order has been successfully paid and is now being processed.

40 |

Tracking ID: ${order.id}

41 |

Order Date: ${order.createdAt 42 | .toISOString() 43 | .replace('T', ' ') 44 | .substr(0, 16)}

45 |

Order Details

46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | ${order.items 57 | .map( 58 | item => 59 | ` 60 | 61 | 62 | 63 | 64 | 65 | 66 | ` 67 | ) 68 | .join('\n')} 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 |
ItemUnit PriceQuantityTotal Price
${item.food.name}$${item.food.price}${item.quantity}$${item.price.toFixed(2)}
Total:$${order.totalPrice}
77 |

Shipping Address: ${order.address}

78 | 79 | 80 | 81 | `; 82 | }; 83 | -------------------------------------------------------------------------------- /backend/src/middleware/admin.mid.js: -------------------------------------------------------------------------------- 1 | import { UNAUTHORIZED } from '../constants/httpStatus.js'; 2 | import authMid from './auth.mid.js'; 3 | const adminMid = (req, res, next) => { 4 | if (!req.user.isAdmin) res.status(UNAUTHORIZED).send(); 5 | 6 | return next(); 7 | }; 8 | 9 | export default [authMid, adminMid]; 10 | -------------------------------------------------------------------------------- /backend/src/middleware/auth.mid.js: -------------------------------------------------------------------------------- 1 | import { verify } from 'jsonwebtoken'; 2 | import { UNAUTHORIZED } from '../constants/httpStatus.js'; 3 | 4 | export default (req, res, next) => { 5 | const token = req.headers.access_token; 6 | if (!token) return res.status(UNAUTHORIZED).send(); 7 | 8 | try { 9 | const decoded = verify(token, process.env.JWT_SECRET); 10 | req.user = decoded; 11 | } catch (error) { 12 | res.status(UNAUTHORIZED).send(); 13 | } 14 | 15 | return next(); 16 | }; 17 | -------------------------------------------------------------------------------- /backend/src/models/food.model.js: -------------------------------------------------------------------------------- 1 | import { model, Schema } from 'mongoose'; 2 | 3 | export const FoodSchema = new Schema( 4 | { 5 | name: { type: String, required: true }, 6 | price: { type: Number, required: true }, 7 | tags: { type: [String] }, 8 | favorite: { type: Boolean, default: false }, 9 | stars: { type: Number, default: 3 }, 10 | imageUrl: { type: String, required: true }, 11 | origins: { type: [String], required: true }, 12 | cookTime: { type: String, required: true }, 13 | }, 14 | { 15 | toJSON: { 16 | virtuals: true, 17 | }, 18 | toObject: { 19 | virtuals: true, 20 | }, 21 | timestamps: true, 22 | } 23 | ); 24 | 25 | export const FoodModel = model('food', FoodSchema); 26 | -------------------------------------------------------------------------------- /backend/src/models/order.model.js: -------------------------------------------------------------------------------- 1 | import { model, Schema } from 'mongoose'; 2 | import { OrderStatus } from '../constants/orderStatus.js'; 3 | import { FoodModel } from './food.model.js'; 4 | 5 | export const LatLngSchema = new Schema( 6 | { 7 | lat: { type: String, required: true }, 8 | lng: { type: String, required: true }, 9 | }, 10 | { 11 | _id: false, 12 | } 13 | ); 14 | 15 | export const OrderItemSchema = new Schema( 16 | { 17 | food: { type: FoodModel.schema, required: true }, 18 | price: { type: Number, required: true }, 19 | quantity: { type: Number, required: true }, 20 | }, 21 | { 22 | _id: false, 23 | } 24 | ); 25 | 26 | OrderItemSchema.pre('validate', function (next) { 27 | this.price = this.food.price * this.quantity; 28 | next(); 29 | }); 30 | 31 | const orderSchema = new Schema( 32 | { 33 | name: { type: String, required: true }, 34 | address: { type: String, required: true }, 35 | addressLatLng: { type: LatLngSchema, required: true }, 36 | paymentId: { type: String }, 37 | totalPrice: { type: Number, required: true }, 38 | items: { type: [OrderItemSchema], required: true }, 39 | status: { type: String, default: OrderStatus.NEW }, 40 | user: { type: Schema.Types.ObjectId, required: true, ref: 'user' }, 41 | }, 42 | { 43 | timestamps: true, 44 | toJSON: { 45 | virtuals: true, 46 | }, 47 | toObject: { 48 | virtuals: true, 49 | }, 50 | } 51 | ); 52 | 53 | export const OrderModel = model('order', orderSchema); 54 | -------------------------------------------------------------------------------- /backend/src/models/user.model.js: -------------------------------------------------------------------------------- 1 | import { model, Schema } from 'mongoose'; 2 | 3 | export const UserSchema = new Schema( 4 | { 5 | name: { type: String, required: true }, 6 | email: { type: String, required: true, unique: true }, 7 | password: { type: String, required: true }, 8 | address: { type: String, required: true }, 9 | isAdmin: { type: Boolean, default: false }, 10 | isBlocked: { type: Boolean, default: false }, 11 | }, 12 | { 13 | timestamps: true, 14 | toJSON: { 15 | virtuals: true, 16 | }, 17 | toObject: { 18 | virtuals: true, 19 | }, 20 | } 21 | ); 22 | 23 | export const UserModel = model('user', UserSchema); 24 | -------------------------------------------------------------------------------- /backend/src/routers/food.router.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { FoodModel } from '../models/food.model.js'; 3 | import handler from 'express-async-handler'; 4 | import admin from '../middleware/admin.mid.js'; 5 | 6 | const router = Router(); 7 | 8 | router.get( 9 | '/', 10 | handler(async (req, res) => { 11 | const foods = await FoodModel.find({}); 12 | res.send(foods); 13 | }) 14 | ); 15 | 16 | router.post( 17 | '/', 18 | admin, 19 | handler(async (req, res) => { 20 | const { name, price, tags, favorite, imageUrl, origins, cookTime } = 21 | req.body; 22 | 23 | const food = new FoodModel({ 24 | name, 25 | price, 26 | tags: tags.split ? tags.split(',') : tags, 27 | favorite, 28 | imageUrl, 29 | origins: origins.split ? origins.split(',') : origins, 30 | cookTime, 31 | }); 32 | 33 | await food.save(); 34 | 35 | res.send(food); 36 | }) 37 | ); 38 | 39 | router.put( 40 | '/', 41 | admin, 42 | handler(async (req, res) => { 43 | const { id, name, price, tags, favorite, imageUrl, origins, cookTime } = 44 | req.body; 45 | 46 | await FoodModel.updateOne( 47 | { _id: id }, 48 | { 49 | name, 50 | price, 51 | tags: tags.split ? tags.split(',') : tags, 52 | favorite, 53 | imageUrl, 54 | origins: origins.split ? origins.split(',') : origins, 55 | cookTime, 56 | } 57 | ); 58 | 59 | res.send(); 60 | }) 61 | ); 62 | 63 | router.delete( 64 | '/:foodId', 65 | admin, 66 | handler(async (req, res) => { 67 | const { foodId } = req.params; 68 | await FoodModel.deleteOne({ _id: foodId }); 69 | res.send(); 70 | }) 71 | ); 72 | 73 | router.get( 74 | '/tags', 75 | handler(async (req, res) => { 76 | const tags = await FoodModel.aggregate([ 77 | { 78 | $unwind: '$tags', 79 | }, 80 | { 81 | $group: { 82 | _id: '$tags', 83 | count: { $sum: 1 }, 84 | }, 85 | }, 86 | { 87 | $project: { 88 | _id: 0, 89 | name: '$_id', 90 | count: '$count', 91 | }, 92 | }, 93 | ]).sort({ count: -1 }); 94 | 95 | const all = { 96 | name: 'All', 97 | count: await FoodModel.countDocuments(), 98 | }; 99 | 100 | tags.unshift(all); 101 | 102 | res.send(tags); 103 | }) 104 | ); 105 | 106 | router.get( 107 | '/search/:searchTerm', 108 | handler(async (req, res) => { 109 | const { searchTerm } = req.params; 110 | const searchRegex = new RegExp(searchTerm, 'i'); 111 | 112 | const foods = await FoodModel.find({ name: { $regex: searchRegex } }); 113 | res.send(foods); 114 | }) 115 | ); 116 | 117 | router.get( 118 | '/tag/:tag', 119 | handler(async (req, res) => { 120 | const { tag } = req.params; 121 | const foods = await FoodModel.find({ tags: tag }); 122 | res.send(foods); 123 | }) 124 | ); 125 | 126 | router.get( 127 | '/:foodId', 128 | handler(async (req, res) => { 129 | const { foodId } = req.params; 130 | const food = await FoodModel.findById(foodId); 131 | res.send(food); 132 | }) 133 | ); 134 | 135 | export default router; 136 | -------------------------------------------------------------------------------- /backend/src/routers/order.router.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import handler from 'express-async-handler'; 3 | import auth from '../middleware/auth.mid.js'; 4 | import { BAD_REQUEST } from '../constants/httpStatus.js'; 5 | import { OrderModel } from '../models/order.model.js'; 6 | import { OrderStatus } from '../constants/orderStatus.js'; 7 | import { UserModel } from '../models/user.model.js'; 8 | import { sendEmailReceipt } from '../helpers/mail.helper.js'; 9 | 10 | const router = Router(); 11 | router.use(auth); 12 | 13 | router.post( 14 | '/create', 15 | handler(async (req, res) => { 16 | const order = req.body; 17 | 18 | if (order.items.length <= 0) res.status(BAD_REQUEST).send('Cart Is Empty!'); 19 | 20 | await OrderModel.deleteOne({ 21 | user: req.user.id, 22 | status: OrderStatus.NEW, 23 | }); 24 | 25 | const newOrder = new OrderModel({ ...order, user: req.user.id }); 26 | await newOrder.save(); 27 | res.send(newOrder); 28 | }) 29 | ); 30 | 31 | router.put( 32 | '/pay', 33 | handler(async (req, res) => { 34 | const { paymentId } = req.body; 35 | const order = await getNewOrderForCurrentUser(req); 36 | if (!order) { 37 | res.status(BAD_REQUEST).send('Order Not Found!'); 38 | return; 39 | } 40 | 41 | order.paymentId = paymentId; 42 | order.status = OrderStatus.PAYED; 43 | await order.save(); 44 | 45 | sendEmailReceipt(order); 46 | 47 | res.send(order._id); 48 | }) 49 | ); 50 | 51 | router.get( 52 | '/track/:orderId', 53 | handler(async (req, res) => { 54 | const { orderId } = req.params; 55 | const user = await UserModel.findById(req.user.id); 56 | 57 | const filter = { 58 | _id: orderId, 59 | }; 60 | 61 | if (!user.isAdmin) { 62 | filter.user = user._id; 63 | } 64 | 65 | const order = await OrderModel.findOne(filter); 66 | 67 | if (!order) return res.send(UNAUTHORIZED); 68 | 69 | return res.send(order); 70 | }) 71 | ); 72 | 73 | router.get( 74 | '/newOrderForCurrentUser', 75 | handler(async (req, res) => { 76 | const order = await getNewOrderForCurrentUser(req); 77 | if (order) res.send(order); 78 | else res.status(BAD_REQUEST).send(); 79 | }) 80 | ); 81 | 82 | router.get('/allstatus', (req, res) => { 83 | const allStatus = Object.values(OrderStatus); 84 | res.send(allStatus); 85 | }); 86 | 87 | router.get( 88 | '/:status?', 89 | handler(async (req, res) => { 90 | const status = req.params.status; 91 | const user = await UserModel.findById(req.user.id); 92 | const filter = {}; 93 | 94 | if (!user.isAdmin) filter.user = user._id; 95 | if (status) filter.status = status; 96 | 97 | const orders = await OrderModel.find(filter).sort('-createdAt'); 98 | res.send(orders); 99 | }) 100 | ); 101 | 102 | const getNewOrderForCurrentUser = async req => 103 | await OrderModel.findOne({ 104 | user: req.user.id, 105 | status: OrderStatus.NEW, 106 | }).populate('user'); 107 | export default router; 108 | -------------------------------------------------------------------------------- /backend/src/routers/upload.router.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import admin from '../middleware/admin.mid.js'; 3 | import multer from 'multer'; 4 | import handler from 'express-async-handler'; 5 | import { BAD_REQUEST } from '../constants/httpStatus.js'; 6 | import { configCloudinary } from '../config/cloudinary.config.js'; 7 | 8 | const router = Router(); 9 | const upload = multer(); 10 | 11 | router.post( 12 | '/', 13 | admin, 14 | upload.single('image'), 15 | handler(async (req, res) => { 16 | const file = req.file; 17 | if (!file) { 18 | res.status(BAD_REQUEST).send(); 19 | return; 20 | } 21 | 22 | const imageUrl = await uploadImageToCloudinary(req.file?.buffer); 23 | res.send({ imageUrl }); 24 | }) 25 | ); 26 | 27 | const uploadImageToCloudinary = imageBuffer => { 28 | const cloudinary = configCloudinary(); 29 | 30 | return new Promise((resolve, reject) => { 31 | if (!imageBuffer) reject(null); 32 | 33 | cloudinary.uploader 34 | .upload_stream((error, result) => { 35 | if (error || !result) reject(error); 36 | else resolve(result.url); 37 | }) 38 | .end(imageBuffer); 39 | }); 40 | }; 41 | 42 | export default router; 43 | -------------------------------------------------------------------------------- /backend/src/routers/user.router.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import jwt from 'jsonwebtoken'; 3 | const router = Router(); 4 | import { BAD_REQUEST } from '../constants/httpStatus.js'; 5 | import handler from 'express-async-handler'; 6 | import { UserModel } from '../models/user.model.js'; 7 | import bcrypt from 'bcryptjs'; 8 | import auth from '../middleware/auth.mid.js'; 9 | import admin from '../middleware/admin.mid.js'; 10 | const PASSWORD_HASH_SALT_ROUNDS = 10; 11 | 12 | router.post( 13 | '/login', 14 | handler(async (req, res) => { 15 | const { email, password } = req.body; 16 | const user = await UserModel.findOne({ email }); 17 | 18 | if (user && (await bcrypt.compare(password, user.password))) { 19 | res.send(generateTokenResponse(user)); 20 | return; 21 | } 22 | 23 | res.status(BAD_REQUEST).send('Username or password is invalid'); 24 | }) 25 | ); 26 | 27 | router.post( 28 | '/register', 29 | handler(async (req, res) => { 30 | const { name, email, password, address } = req.body; 31 | 32 | const user = await UserModel.findOne({ email }); 33 | 34 | if (user) { 35 | res.status(BAD_REQUEST).send('User already exists, please login!'); 36 | return; 37 | } 38 | 39 | const hashedPassword = await bcrypt.hash( 40 | password, 41 | PASSWORD_HASH_SALT_ROUNDS 42 | ); 43 | 44 | const newUser = { 45 | name, 46 | email: email.toLowerCase(), 47 | password: hashedPassword, 48 | address, 49 | }; 50 | 51 | const result = await UserModel.create(newUser); 52 | res.send(generateTokenResponse(result)); 53 | }) 54 | ); 55 | 56 | router.put( 57 | '/updateProfile', 58 | auth, 59 | handler(async (req, res) => { 60 | const { name, address } = req.body; 61 | const user = await UserModel.findByIdAndUpdate( 62 | req.user.id, 63 | { name, address }, 64 | { new: true } 65 | ); 66 | 67 | res.send(generateTokenResponse(user)); 68 | }) 69 | ); 70 | 71 | router.put( 72 | '/changePassword', 73 | auth, 74 | handler(async (req, res) => { 75 | const { currentPassword, newPassword } = req.body; 76 | const user = await UserModel.findById(req.user.id); 77 | 78 | if (!user) { 79 | res.status(BAD_REQUEST).send('Change Password Failed!'); 80 | return; 81 | } 82 | 83 | const equal = await bcrypt.compare(currentPassword, user.password); 84 | 85 | if (!equal) { 86 | res.status(BAD_REQUEST).send('Current Password Is Not Correct!'); 87 | return; 88 | } 89 | 90 | user.password = await bcrypt.hash(newPassword, PASSWORD_HASH_SALT_ROUNDS); 91 | await user.save(); 92 | 93 | res.send(); 94 | }) 95 | ); 96 | 97 | router.get( 98 | '/getall/:searchTerm?', 99 | admin, 100 | handler(async (req, res) => { 101 | const { searchTerm } = req.params; 102 | 103 | const filter = searchTerm 104 | ? { name: { $regex: new RegExp(searchTerm, 'i') } } 105 | : {}; 106 | 107 | const users = await UserModel.find(filter, { password: 0 }); 108 | res.send(users); 109 | }) 110 | ); 111 | 112 | router.put( 113 | '/toggleBlock/:userId', 114 | admin, 115 | handler(async (req, res) => { 116 | const { userId } = req.params; 117 | 118 | if (userId === req.user.id) { 119 | res.status(BAD_REQUEST).send("Can't block yourself!"); 120 | return; 121 | } 122 | 123 | const user = await UserModel.findById(userId); 124 | user.isBlocked = !user.isBlocked; 125 | user.save(); 126 | 127 | res.send(user.isBlocked); 128 | }) 129 | ); 130 | 131 | router.get( 132 | '/getById/:userId', 133 | admin, 134 | handler(async (req, res) => { 135 | const { userId } = req.params; 136 | const user = await UserModel.findById(userId, { password: 0 }); 137 | res.send(user); 138 | }) 139 | ); 140 | 141 | router.put( 142 | '/update', 143 | admin, 144 | handler(async (req, res) => { 145 | const { id, name, email, address, isAdmin } = req.body; 146 | await UserModel.findByIdAndUpdate(id, { 147 | name, 148 | email, 149 | address, 150 | isAdmin, 151 | }); 152 | 153 | res.send(); 154 | }) 155 | ); 156 | 157 | const generateTokenResponse = user => { 158 | const token = jwt.sign( 159 | { 160 | id: user.id, 161 | email: user.email, 162 | isAdmin: user.isAdmin, 163 | }, 164 | process.env.JWT_SECRET, 165 | { 166 | expiresIn: '30d', 167 | } 168 | ); 169 | 170 | return { 171 | id: user.id, 172 | email: user.email, 173 | name: user.name, 174 | address: user.address, 175 | isAdmin: user.isAdmin, 176 | token, 177 | }; 178 | }; 179 | 180 | export default router; 181 | -------------------------------------------------------------------------------- /backend/src/server.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | dotenv.config(); 3 | import { fileURLToPath } from 'url'; 4 | import express from 'express'; 5 | import cors from 'cors'; 6 | import foodRouter from './routers/food.router.js'; 7 | import userRouter from './routers/user.router.js'; 8 | import orderRouter from './routers/order.router.js'; 9 | import uploadRouter from './routers/upload.router.js'; 10 | 11 | import { dbconnect } from './config/database.config.js'; 12 | import path, { dirname } from 'path'; 13 | dbconnect(); 14 | 15 | const __filename = fileURLToPath(import.meta.url); 16 | const __dirname = dirname(__filename); 17 | 18 | const app = express(); 19 | app.use(express.json()); 20 | app.use( 21 | cors({ 22 | credentials: true, 23 | origin: ['http://localhost:3000'], 24 | }) 25 | ); 26 | 27 | app.use('/api/foods', foodRouter); 28 | app.use('/api/users', userRouter); 29 | app.use('/api/orders', orderRouter); 30 | app.use('/api/upload', uploadRouter); 31 | 32 | const publicFolder = path.join(__dirname, 'public'); 33 | app.use(express.static(publicFolder)); 34 | 35 | app.get('*', (req, res) => { 36 | const indexFilePath = path.join(publicFolder, 'index.html'); 37 | res.sendFile(indexFilePath); 38 | }); 39 | 40 | const PORT = process.env.PORT || 5000; 41 | app.listen(PORT, () => { 42 | console.log('listening on port ' + PORT); 43 | }); 44 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /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/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node" 4 | }, 5 | "include": ["src"] 6 | } 7 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@paypal/react-paypal-js": "^8.1.1", 7 | "@testing-library/jest-dom": "^5.16.5", 8 | "@testing-library/react": "^13.4.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "axios": "^1.4.0", 11 | "leaflet": "^1.9.4", 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0", 14 | "react-hook-form": "^7.43.9", 15 | "react-leaflet": "^4.2.1", 16 | "react-router-dom": "^6.8.1", 17 | "react-scripts": "5.0.1", 18 | "react-toastify": "^9.1.2", 19 | "web-vitals": "^2.1.4" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject" 26 | }, 27 | "eslintConfig": { 28 | "extends": [ 29 | "react-app", 30 | "react-app/jest" 31 | ] 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.2%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nasirjd/foodmine-react-js/306e82c72b3dd728eb5fdc9e91d7944219dece47/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/foods/food-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nasirjd/foodmine-react-js/306e82c72b3dd728eb5fdc9e91d7944219dece47/frontend/public/foods/food-1.jpg -------------------------------------------------------------------------------- /frontend/public/foods/food-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nasirjd/foodmine-react-js/306e82c72b3dd728eb5fdc9e91d7944219dece47/frontend/public/foods/food-2.jpg -------------------------------------------------------------------------------- /frontend/public/foods/food-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nasirjd/foodmine-react-js/306e82c72b3dd728eb5fdc9e91d7944219dece47/frontend/public/foods/food-3.jpg -------------------------------------------------------------------------------- /frontend/public/foods/food-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nasirjd/foodmine-react-js/306e82c72b3dd728eb5fdc9e91d7944219dece47/frontend/public/foods/food-4.jpg -------------------------------------------------------------------------------- /frontend/public/foods/food-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nasirjd/foodmine-react-js/306e82c72b3dd728eb5fdc9e91d7944219dece47/frontend/public/foods/food-5.jpg -------------------------------------------------------------------------------- /frontend/public/foods/food-6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nasirjd/foodmine-react-js/306e82c72b3dd728eb5fdc9e91d7944219dece47/frontend/public/foods/food-6.jpg -------------------------------------------------------------------------------- /frontend/public/icons/foods.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/icons/orders.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /frontend/public/icons/profile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /frontend/public/icons/users.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | 35 | 38 | 39 | 40 | 41 | 42 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /frontend/public/layers-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nasirjd/foodmine-react-js/306e82c72b3dd728eb5fdc9e91d7944219dece47/frontend/public/layers-2x.png -------------------------------------------------------------------------------- /frontend/public/layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nasirjd/foodmine-react-js/306e82c72b3dd728eb5fdc9e91d7944219dece47/frontend/public/layers.png -------------------------------------------------------------------------------- /frontend/public/loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nasirjd/foodmine-react-js/306e82c72b3dd728eb5fdc9e91d7944219dece47/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nasirjd/foodmine-react-js/306e82c72b3dd728eb5fdc9e91d7944219dece47/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/marker-icon-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nasirjd/foodmine-react-js/306e82c72b3dd728eb5fdc9e91d7944219dece47/frontend/public/marker-icon-2x.png -------------------------------------------------------------------------------- /frontend/public/marker-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nasirjd/foodmine-react-js/306e82c72b3dd728eb5fdc9e91d7944219dece47/frontend/public/marker-icon.png -------------------------------------------------------------------------------- /frontend/public/marker-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nasirjd/foodmine-react-js/306e82c72b3dd728eb5fdc9e91d7944219dece47/frontend/public/marker-shadow.png -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/public/star-empty.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /frontend/public/star-full.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /frontend/public/star-half.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import AppRoutes from './AppRoutes'; 2 | import Header from './components/Header/Header'; 3 | import Loading from './components/Loading/Loading'; 4 | import { useLoading } from './hooks/useLoading'; 5 | import { setLoadingInterceptor } from './interceptors/loadingInterceptor'; 6 | import { useEffect } from 'react'; 7 | 8 | function App() { 9 | const { showLoading, hideLoading } = useLoading(); 10 | 11 | useEffect(() => { 12 | setLoadingInterceptor({ showLoading, hideLoading }); 13 | }, []); 14 | 15 | return ( 16 | <> 17 | 18 |
19 | 20 | 21 | ); 22 | } 23 | 24 | export default App; 25 | -------------------------------------------------------------------------------- /frontend/src/AppRoutes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Routes } from 'react-router-dom'; 3 | import CartPage from './pages/Cart/CartPage'; 4 | import FoodPage from './pages/Food/FoodPage'; 5 | import HomePage from './pages/Home/HomePage'; 6 | import LoginPage from './pages/Login/LoginPage'; 7 | import RegisterPage from './pages/Register/RegisterPage'; 8 | import AuthRoute from './components/AuthRoute/AuthRoute'; 9 | import CheckoutPage from './pages/Checkout/CheckoutPage'; 10 | import PaymentPage from './pages/Payment/PaymentPage'; 11 | import OrderTrackPage from './pages/OrderTrack/OrderTrackPage'; 12 | import ProfilePage from './pages/Profile/ProfilePage'; 13 | import OrdersPage from './pages/Orders/OrdersPage'; 14 | import Dashboard from './pages/Dashboard/Dashboard'; 15 | import AdminRoute from './components/AdminRoute/AdminRoute'; 16 | import FoodsAdminPage from './pages/FoodsAdmin/FoodsAdminPage'; 17 | import FoodEditPage from './pages/FoodEdit/FoodEditPage'; 18 | import UsersPage from './pages/UsersPage/UsersPage'; 19 | import UserEditPage from './pages/UserEdit/UserEditPage'; 20 | 21 | export default function AppRoutes() { 22 | return ( 23 | 24 | } /> 25 | } /> 26 | } /> 27 | } /> 28 | } /> 29 | } /> 30 | } /> 31 | 35 | 36 | 37 | } 38 | /> 39 | 43 | 44 | 45 | } 46 | /> 47 | 51 | 52 | 53 | } 54 | /> 55 | 59 | 60 | 61 | } 62 | /> 63 | 67 | 68 | 69 | } 70 | /> 71 | 75 | 76 | 77 | } 78 | /> 79 | 83 | 84 | 85 | } 86 | /> 87 | 88 | 92 | 93 | 94 | } 95 | /> 96 | 100 | 101 | 102 | } 103 | /> 104 | 108 | 109 | 110 | } 111 | /> 112 | 113 | 117 | 118 | 119 | } 120 | /> 121 | 122 | ); 123 | } 124 | -------------------------------------------------------------------------------- /frontend/src/axiosConfig.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | axios.defaults.baseURL = 4 | process.env.NODE_ENV !== 'production' ? 'http://localhost:5000' : '/'; 5 | -------------------------------------------------------------------------------- /frontend/src/components/AdminRoute/AdminRoute.js: -------------------------------------------------------------------------------- 1 | import React, { Children } from 'react'; 2 | import { useAuth } from '../../hooks/useAuth'; 3 | import NotFound from '../NotFound/NotFound'; 4 | import AuthRoute from '../AuthRoute/AuthRoute'; 5 | 6 | function AdminRoute({ children }) { 7 | const { user } = useAuth(); 8 | return user.isAdmin ? ( 9 | children 10 | ) : ( 11 | 16 | ); 17 | } 18 | 19 | const AdminRouteExport = ({ children }) => ( 20 | 21 | {children} 22 | 23 | ); 24 | 25 | export default AdminRouteExport; 26 | -------------------------------------------------------------------------------- /frontend/src/components/AuthRoute/AuthRoute.js: -------------------------------------------------------------------------------- 1 | import { Navigate, useLocation } from 'react-router-dom'; 2 | import { useAuth } from '../../hooks/useAuth'; 3 | 4 | export default function AuthRoute({ children }) { 5 | const location = useLocation(); 6 | const { user } = useAuth(); 7 | return user ? ( 8 | children 9 | ) : ( 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/components/Button/Button.js: -------------------------------------------------------------------------------- 1 | import classes from './button.module.css'; 2 | 3 | export default function Button({ 4 | type, 5 | text, 6 | onClick, 7 | color, 8 | backgroundColor, 9 | fontSize, 10 | width, 11 | height, 12 | }) { 13 | return ( 14 |
15 | 28 |
29 | ); 30 | } 31 | 32 | Button.defaultProps = { 33 | type: 'button', 34 | text: 'Submit', 35 | backgroundColor: '#e72929', 36 | color: 'white', 37 | fontSize: '1.3rem', 38 | width: '12rem', 39 | height: '3.5rem', 40 | }; 41 | -------------------------------------------------------------------------------- /frontend/src/components/Button/button.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | justify-content: center; 4 | width: 100%; 5 | } 6 | 7 | .container button { 8 | border: none; 9 | outline: none; 10 | font-weight: 100; 11 | border-radius: 0.8rem; 12 | width: 100%; 13 | margin: 1rem auto; 14 | opacity: 0.8; 15 | } 16 | 17 | .container button:hover { 18 | cursor: pointer; 19 | opacity: 1; 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/components/ChangePassword/ChangePassword.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useForm } from 'react-hook-form'; 3 | import Title from '../Title/Title'; 4 | import Input from '../Input/Input'; 5 | import Button from '../Button/Button'; 6 | import { useAuth } from '../../hooks/useAuth'; 7 | 8 | export default function ChangePassword() { 9 | const { 10 | handleSubmit, 11 | register, 12 | getValues, 13 | formState: { errors }, 14 | } = useForm(); 15 | 16 | const { changePassword } = useAuth(); 17 | const submit = passwords => { 18 | changePassword(passwords); 19 | }; 20 | 21 | return ( 22 |
23 | 24 | <form onSubmit={handleSubmit(submit)}> 25 | <Input 26 | type="password" 27 | label="Current Password" 28 | {...register('currentPassword', { 29 | required: true, 30 | })} 31 | error={errors.currentPassword} 32 | /> 33 | 34 | <Input 35 | type="password" 36 | label="New Password" 37 | {...register('newPassword', { 38 | required: true, 39 | minLength: 5, 40 | })} 41 | error={errors.newPassword} 42 | /> 43 | 44 | <Input 45 | type="password" 46 | label="Confirm Password" 47 | {...register('confirmNewPassword', { 48 | required: true, 49 | validate: value => 50 | value != getValues('newPassword') 51 | ? 'Passwords Do No Match' 52 | : true, 53 | })} 54 | error={errors.confirmNewPassword} 55 | /> 56 | 57 | <Button type="submit" text="Change" /> 58 | </form> 59 | </div> 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /frontend/src/components/DateTime/DateTime.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | DateTime.defaultProps = { 4 | options: { 5 | weekday: 'short', 6 | year: 'numeric', 7 | month: 'long', 8 | day: 'numeric', 9 | hour: 'numeric', 10 | minute: 'numeric', 11 | second: 'numeric', 12 | }, 13 | }; 14 | 15 | export default function DateTime({ 16 | date, 17 | options: { weekday, year, month, day, hour, minute, second }, 18 | }) { 19 | var currentLocale = new Intl.DateTimeFormat().resolvedOptions().locale; 20 | 21 | const getDate = () => 22 | new Intl.DateTimeFormat(currentLocale, { 23 | year, 24 | month, 25 | weekday, 26 | day, 27 | hour, 28 | minute, 29 | second, 30 | }).format(Date.parse(date)); 31 | return <>{getDate()}</>; 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/components/Header/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { useCart } from '../../hooks/useCart'; 4 | import classes from './header.module.css'; 5 | import { useAuth } from '../../hooks/useAuth'; 6 | 7 | export default function Header() { 8 | const { user, logout } = useAuth(); 9 | 10 | const { cart } = useCart(); 11 | 12 | return ( 13 | <header className={classes.header}> 14 | <div className={classes.container}> 15 | <Link to="/" className={classes.logo}> 16 | Food Mine! 17 | </Link> 18 | <nav> 19 | <ul> 20 | {user ? ( 21 | <li className={classes.menu_container}> 22 | <Link to="/dashboard">{user.name}</Link> 23 | <div className={classes.menu}> 24 | <Link to="/profile">Profile</Link> 25 | <Link to="/orders">Orders</Link> 26 | <a onClick={logout}>Logout</a> 27 | </div> 28 | </li> 29 | ) : ( 30 | <Link to="/login">Login</Link> 31 | )} 32 | 33 | <li> 34 | <Link to="/cart"> 35 | Cart 36 | {cart.totalCount > 0 && ( 37 | <span className={classes.cart_count}>{cart.totalCount}</span> 38 | )} 39 | </Link> 40 | </li> 41 | </ul> 42 | </nav> 43 | </div> 44 | </header> 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /frontend/src/components/Header/header.module.css: -------------------------------------------------------------------------------- 1 | .header { 2 | background: white; 3 | padding: 0; 4 | border-bottom: 1px solid #e72929; 5 | } 6 | 7 | .header a { 8 | color: #af1313; 9 | } 10 | 11 | .header a:hover { 12 | background: #e72929; 13 | color: white; 14 | cursor: pointer; 15 | } 16 | 17 | .container { 18 | margin: 0 auto; 19 | display: flex; 20 | justify-content: space-between; 21 | } 22 | 23 | .logo { 24 | font-weight: bold; 25 | padding: 1rem; 26 | } 27 | 28 | .header ul { 29 | display: flex; 30 | list-style-type: none; 31 | margin: 0; 32 | } 33 | 34 | .header ul a { 35 | padding: 1rem; 36 | display: inline-block; 37 | } 38 | 39 | .menu_container { 40 | position: relative; 41 | } 42 | 43 | .menu { 44 | position: absolute; 45 | z-index: 1001; 46 | background-color: whitesmoke; 47 | display: none; 48 | } 49 | 50 | .menu_container:hover .menu { 51 | display: block; 52 | } 53 | 54 | .menu a { 55 | width: 100%; 56 | min-width: 8rem; 57 | } 58 | 59 | .cart_count { 60 | background-color: #ff4d4d; 61 | color: white; 62 | padding: 0.1rem 0.45rem; 63 | border-radius: 100rem; 64 | font-size: 0.9rem; 65 | } 66 | -------------------------------------------------------------------------------- /frontend/src/components/Input/Input.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import InputContainer from '../InputContainer/InputContainer'; 3 | import classes from './input.module.css'; 4 | function Input( 5 | { label, type, defaultValue, onChange, onBlur, name, error }, 6 | ref 7 | ) { 8 | const getErrorMessage = () => { 9 | if (!error) return; 10 | if (error.message) return error.message; 11 | //defaults 12 | switch (error.type) { 13 | case 'required': 14 | return 'This Field Is Required'; 15 | case 'minLength': 16 | return 'Field Is Too Short'; 17 | default: 18 | return '*'; 19 | } 20 | }; 21 | 22 | return ( 23 | <InputContainer label={label}> 24 | <input 25 | defaultValue={defaultValue} 26 | className={classes.input} 27 | type={type} 28 | placeholder={label} 29 | ref={ref} 30 | name={name} 31 | onChange={onChange} 32 | onBlur={onBlur} 33 | /> 34 | {error && <div className={classes.error}>{getErrorMessage()}</div>} 35 | </InputContainer> 36 | ); 37 | } 38 | 39 | export default React.forwardRef(Input); 40 | -------------------------------------------------------------------------------- /frontend/src/components/Input/input.module.css: -------------------------------------------------------------------------------- 1 | .input { 2 | width: 100%; 3 | height: 100%; 4 | border: none; 5 | border-bottom: 0 solid grey; 6 | transition: border-width 0.15s ease-out; 7 | background-color: white; 8 | font-size: 1.1rem; 9 | outline: none; 10 | } 11 | 12 | .input::placeholder { 13 | color: #dfdfdf; 14 | font-size: 0.95rem; 15 | } 16 | 17 | .input:focus { 18 | border-width: 2.9px; 19 | } 20 | 21 | .error { 22 | display: flex; 23 | justify-content: center; 24 | align-items: center; 25 | position: absolute; 26 | top: 0; 27 | right: 1rem; 28 | height: 100%; 29 | width: 12rem; 30 | color: red; 31 | text-align: center; 32 | font-size: 0.95rem; 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/components/InputContainer/InputContainer.js: -------------------------------------------------------------------------------- 1 | import classes from './inputContainer.module.css'; 2 | 3 | export default function InputContainer({ label, bgColor, children }) { 4 | return ( 5 | <div className={classes.container} style={{ backgroundColor: bgColor }}> 6 | <label className={classes.label}>{label}</label> 7 | <div className={classes.content}>{children}</div> 8 | </div> 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/components/InputContainer/inputContainer.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | margin-bottom: 0.5rem; 4 | border-radius: 0.7rem; 5 | padding-top: 0.3rem; 6 | border: 1px solid #e0e0e0; 7 | } 8 | 9 | .label { 10 | display: inline-block; 11 | margin-left: 0.5rem; 12 | color: #5f5f5f; 13 | font-size: 1rem; 14 | } 15 | 16 | .content { 17 | padding: 0 0.5rem; 18 | height: 2.7rem; 19 | display: flex; 20 | align-items: center; 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/components/Loading/Loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useLoading } from '../../hooks/useLoading'; 3 | import classes from './loading.module.css'; 4 | 5 | export default function Loading() { 6 | const { isLoading } = useLoading(); 7 | if (!isLoading) return; 8 | 9 | return ( 10 | <div className={classes.container}> 11 | <div className={classes.items}> 12 | <img src="/loading.svg" alt="Loading!" /> 13 | <h1>Loading...</h1> 14 | </div> 15 | </div> 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/components/Loading/loading.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | position: fixed; 3 | width: 100%; 4 | height: 100%; 5 | background-color: #ffffffea; 6 | z-index: 10000; 7 | top: 0; 8 | left: 0; 9 | } 10 | 11 | .items { 12 | display: flex; 13 | flex-direction: column; 14 | justify-content: center; 15 | align-items: center; 16 | height: 80%; 17 | width: 100%; 18 | } 19 | 20 | .container img { 21 | border-bottom: 10px solid brown; 22 | } 23 | 24 | .container h1 { 25 | color: brown; 26 | text-transform: lowercase; 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/components/Map/Map.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import classes from './map.module.css'; 3 | import 'leaflet/dist/leaflet.css'; 4 | import { 5 | MapContainer, 6 | TileLayer, 7 | Marker, 8 | Popup, 9 | useMapEvents, 10 | } from 'react-leaflet'; 11 | import { toast } from 'react-toastify'; 12 | import * as L from 'leaflet'; 13 | 14 | export default function Map({ readonly, location, onChange }) { 15 | return ( 16 | <div className={classes.container}> 17 | <MapContainer 18 | className={classes.map} 19 | center={[0, 0]} 20 | zoom={1} 21 | dragging={!readonly} 22 | touchZoom={!readonly} 23 | doubleClickZoom={!readonly} 24 | scrollWheelZoom={!readonly} 25 | boxZoom={!readonly} 26 | keyboard={!readonly} 27 | attributionControl={false} 28 | > 29 | <TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" /> 30 | <FindButtonAndMarker 31 | readonly={readonly} 32 | location={location} 33 | onChange={onChange} 34 | /> 35 | </MapContainer> 36 | </div> 37 | ); 38 | } 39 | 40 | function FindButtonAndMarker({ readonly, location, onChange }) { 41 | const [position, setPosition] = useState(location); 42 | 43 | useEffect(() => { 44 | if (readonly) { 45 | map.setView(position, 13); 46 | return; 47 | } 48 | if (position) onChange(position); 49 | }, [position]); 50 | 51 | const map = useMapEvents({ 52 | click(e) { 53 | !readonly && setPosition(e.latlng); 54 | }, 55 | locationfound(e) { 56 | setPosition(e.latlng); 57 | map.flyTo(e.latlng, 13); 58 | }, 59 | locationerror(e) { 60 | toast.error(e.message); 61 | }, 62 | }); 63 | 64 | const markerIcon = new L.Icon({ 65 | iconUrl: '/marker-icon-2x.png', 66 | iconSize: [25, 41], 67 | iconAnchor: [12.5, 41], 68 | popupAnchor: [0, -41], 69 | }); 70 | 71 | return ( 72 | <> 73 | {!readonly && ( 74 | <button 75 | type="button" 76 | className={classes.find_location} 77 | onClick={() => map.locate()} 78 | > 79 | Find My Location 80 | </button> 81 | )} 82 | 83 | {position && ( 84 | <Marker 85 | eventHandlers={{ 86 | dragend: e => { 87 | setPosition(e.target.getLatLng()); 88 | }, 89 | }} 90 | position={position} 91 | draggable={!readonly} 92 | icon={markerIcon} 93 | > 94 | <Popup>Shipping Location</Popup> 95 | </Marker> 96 | )} 97 | </> 98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /frontend/src/components/Map/map.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | width: 35rem; 4 | height: 22rem; 5 | text-align: center; 6 | } 7 | 8 | .map { 9 | width: 100% !important; 10 | height: 100% !important; 11 | } 12 | 13 | .find_location { 14 | position: absolute; 15 | margin: auto; 16 | width: 12rem; 17 | min-height: 2.5rem; 18 | font-size: 1rem; 19 | top: 0; 20 | left: 0; 21 | right: 0; 22 | z-index: 1000; 23 | background-color: white; 24 | cursor: pointer; 25 | border-radius: 0 0 1rem 1rem; 26 | border: none; 27 | border-top: 1px solid lightgrey; 28 | } 29 | 30 | .find_location:hover { 31 | background-color: whitesmoke; 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/components/NotFound/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classes from './notFound.module.css'; 3 | import { Link } from 'react-router-dom'; 4 | export default function NotFound({ message, linkRoute, linkText }) { 5 | return ( 6 | <div className={classes.container}> 7 | {message} 8 | <Link to={linkRoute}>{linkText}</Link> 9 | </div> 10 | ); 11 | } 12 | 13 | NotFound.defaultProps = { 14 | message: 'Nothing Found!', 15 | linkRoute: '/', 16 | linkText: 'Go To Home Page', 17 | }; 18 | -------------------------------------------------------------------------------- /frontend/src/components/NotFound/notFound.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | align-items: center; 6 | font-size: 1.5rem; 7 | font-weight: 100; 8 | height: 15rem; 9 | } 10 | 11 | .container a { 12 | font-size: 1rem; 13 | background-color: #e72929; 14 | color: white; 15 | border-radius: 10rem; 16 | padding: 0.7rem 1rem; 17 | margin: 1rem; 18 | opacity: 0.8; 19 | } 20 | 21 | .container a:hover { 22 | opacity: 1; 23 | cursor: pointer; 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/components/OrderItemsList/OrderItemsList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import Price from '../Price/Price'; 4 | import classes from './orderItemsList.module.css'; 5 | 6 | export default function OrderItemsList({ order }) { 7 | return ( 8 | <table className={classes.table}> 9 | <tbody> 10 | <tr> 11 | <td colSpan="5"> 12 | <h3>Order Items:</h3> 13 | </td> 14 | </tr> 15 | {order.items.map(item => ( 16 | <tr key={item.food.id}> 17 | <td> 18 | <Link to={`/food/${item.food.id}`}> 19 | <img src={item.food.imageUrl} /> 20 | </Link> 21 | </td> 22 | <td>{item.food.name}</td> 23 | <td> 24 | <Price price={item.food.price} /> 25 | </td> 26 | <td>{item.quantity}</td> 27 | <td> 28 | <Price price={item.price} /> 29 | </td> 30 | </tr> 31 | ))} 32 | 33 | <tr> 34 | <td colSpan="3"></td> 35 | <td> 36 | <strong>Total :</strong> 37 | </td> 38 | <td> 39 | <Price price={order.totalPrice} /> 40 | </td> 41 | </tr> 42 | </tbody> 43 | </table> 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /frontend/src/components/OrderItemsList/orderItemsList.module.css: -------------------------------------------------------------------------------- 1 | .table { 2 | width: 100%; 3 | border: 1px solid #eeeeee; 4 | margin-top: 1rem; 5 | border-radius: 5px; 6 | padding: 0.5rem 1rem; 7 | } 8 | 9 | .table td { 10 | height: 3rem; 11 | } 12 | .table td img { 13 | width: 3rem; 14 | height: 3rem; 15 | object-fit: cover; 16 | border-radius: 100rem; 17 | } 18 | 19 | .table h3 { 20 | margin-top: 0.5rem; 21 | margin-left: 0.3rem; 22 | color: grey; 23 | } 24 | .table a:hover { 25 | opacity: 0.9; 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/components/PaypalButtons/PaypalButtons.js: -------------------------------------------------------------------------------- 1 | import { 2 | PayPalButtons, 3 | PayPalScriptProvider, 4 | usePayPalScriptReducer, 5 | } from '@paypal/react-paypal-js'; 6 | import React, { useEffect } from 'react'; 7 | import { useLoading } from '../../hooks/useLoading'; 8 | import { pay } from '../../services/orderService'; 9 | import { useCart } from '../../hooks/useCart'; 10 | import { toast } from 'react-toastify'; 11 | import { useNavigate } from 'react-router-dom'; 12 | 13 | export default function PaypalButtons({ order }) { 14 | return ( 15 | <PayPalScriptProvider 16 | options={{ 17 | clientId: 18 | 'AUWcnaHjOUoXVI3IjLpMkM0Kk0Sigq1CUAWP-finHI950yQD2Qni8XPkRbs76Q-_JIT8hJFhKD8YVy3u', 19 | }} 20 | > 21 | <Buttons order={order} /> 22 | </PayPalScriptProvider> 23 | ); 24 | } 25 | 26 | function Buttons({ order }) { 27 | const { clearCart } = useCart(); 28 | const navigate = useNavigate(); 29 | const [{ isPending }] = usePayPalScriptReducer(); 30 | const { showLoading, hideLoading } = useLoading(); 31 | useEffect(() => { 32 | isPending ? showLoading() : hideLoading(); 33 | }); 34 | 35 | const createOrder = (data, actions) => { 36 | return actions.order.create({ 37 | purchase_units: [ 38 | { 39 | amount: { 40 | currency_code: 'USD', 41 | value: order.totalPrice, 42 | }, 43 | }, 44 | ], 45 | }); 46 | }; 47 | 48 | const onApprove = async (data, actions) => { 49 | try { 50 | const payment = await actions.order.capture(); 51 | const orderId = await pay(payment.id); 52 | clearCart(); 53 | toast.success('Payment Saved Successfully', 'Success'); 54 | navigate('/track/' + orderId); 55 | } catch (error) { 56 | toast.error('Payment Save Failed', 'Error'); 57 | } 58 | }; 59 | 60 | const onError = err => { 61 | toast.error('Payment Failed', 'Error'); 62 | }; 63 | 64 | return ( 65 | <PayPalButtons 66 | createOrder={createOrder} 67 | onApprove={onApprove} 68 | onError={onError} 69 | /> 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /frontend/src/components/Price/Price.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Price({ price, locale, currency }) { 4 | const formatPrice = () => 5 | new Intl.NumberFormat(locale, { 6 | style: 'currency', 7 | currency, 8 | }).format(price); 9 | 10 | return <span>{formatPrice()}</span>; 11 | } 12 | 13 | Price.defaultProps = { 14 | locale: 'en-US', 15 | currency: 'USD', 16 | }; 17 | -------------------------------------------------------------------------------- /frontend/src/components/Search/Search.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { useNavigate, useParams } from 'react-router-dom'; 3 | import classes from './search.module.css'; 4 | 5 | Search.defaultProps = { 6 | searchRoute: '/search/', 7 | defaultRoute: '/', 8 | placeholder: 'Search Food Mine!', 9 | }; 10 | 11 | export default function Search({ 12 | searchRoute, 13 | defaultRoute, 14 | margin, 15 | placeholder, 16 | }) { 17 | const [term, setTerm] = useState(''); 18 | const navigate = useNavigate(); 19 | const { searchTerm } = useParams(); 20 | 21 | useEffect(() => { 22 | setTerm(searchTerm ?? ''); 23 | }, [searchTerm]); 24 | 25 | const search = async () => { 26 | term ? navigate(searchRoute + term) : navigate(defaultRoute); 27 | }; 28 | return ( 29 | <div className={classes.container} style={{ margin }}> 30 | <input 31 | type="text" 32 | placeholder={placeholder} 33 | onChange={e => setTerm(e.target.value)} 34 | onKeyUp={e => e.key === 'Enter' && search()} 35 | value={term} 36 | /> 37 | <button onClick={search}>Search</button> 38 | </div> 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /frontend/src/components/Search/search.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | justify-content: center; 4 | margin-top: 3rem; 5 | margin-bottom: 1.5rem; 6 | } 7 | 8 | .container input { 9 | border-radius: 10rem 0 0 10rem; 10 | border: none; 11 | height: 3rem; 12 | width: 20rem; 13 | background-color: #f1f1f1; 14 | padding: 1.2rem; 15 | font-size: 1rem; 16 | font-weight: 500; 17 | outline: none; 18 | } 19 | 20 | .container button { 21 | color: grey; 22 | height: 3rem; 23 | width: 5rem; 24 | font-size: 1rem; 25 | border-radius: 0 10rem 10rem 0; 26 | border: none; 27 | background-color: #e72929; 28 | color: white; 29 | opacity: 0.8; 30 | outline: none; 31 | } 32 | 33 | .container button:hover { 34 | opacity: 1; 35 | cursor: pointer; 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/components/StarRating/StarRating.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classes from './starRating.module.css'; 3 | export default function StarRating({ stars, size }) { 4 | const styles = { 5 | width: size + 'px', 6 | height: size + 'px', 7 | marginRight: size / 6 + 'px', 8 | }; 9 | 10 | function Star({ number }) { 11 | const halfNumber = number - 0.5; 12 | 13 | return stars >= number ? ( 14 | <img src="/star-full.svg" style={styles} alt={number} /> 15 | ) : stars >= halfNumber ? ( 16 | <img src="/star-half.svg" style={styles} alt={number} /> 17 | ) : ( 18 | <img src="/star-empty.svg" style={styles} alt={number} /> 19 | ); 20 | } 21 | 22 | return ( 23 | <div className={classes.rating}> 24 | {[1, 2, 3, 4, 5].map(number => ( 25 | <Star key={number} number={number} /> 26 | ))} 27 | </div> 28 | ); 29 | } 30 | 31 | StarRating.defaultProps = { 32 | size: 18, 33 | }; 34 | -------------------------------------------------------------------------------- /frontend/src/components/StarRating/starRating.module.css: -------------------------------------------------------------------------------- 1 | .rating { 2 | display: flex; 3 | flex-wrap: nowrap; 4 | } 5 | -------------------------------------------------------------------------------- /frontend/src/components/Tags/Tags.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import classes from './tags.module.css'; 4 | 5 | export default function Tags({ tags, forFoodPage }) { 6 | return ( 7 | <div 8 | className={classes.container} 9 | style={{ 10 | justifyContent: forFoodPage ? 'start' : 'center', 11 | }} 12 | > 13 | {tags.map(tag => ( 14 | <Link key={tag.name} to={`/tag/${tag.name}`}> 15 | {tag.name} 16 | {!forFoodPage && `(${tag.count})`} 17 | </Link> 18 | ))} 19 | </div> 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/components/Tags/tags.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-wrap: wrap; 4 | } 5 | 6 | .container a { 7 | background-color: #f0f0f0; 8 | padding: 0.3rem 1rem; 9 | margin: 0.2rem 0.15rem; 10 | border-radius: 10rem; 11 | font-weight: 600; 12 | color: blue; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/components/Thumbnails/Thumbnails.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import Price from '../Price/Price'; 4 | import StarRating from '../StarRating/StarRating'; 5 | import classes from './thumbnails.module.css'; 6 | export default function Thumbnails({ foods }) { 7 | return ( 8 | <ul className={classes.list}> 9 | {foods.map(food => ( 10 | <li key={food.id}> 11 | <Link to={`/food/${food.id}`}> 12 | <img 13 | className={classes.image} 14 | src={`${food.imageUrl}`} 15 | alt={food.name} 16 | /> 17 | 18 | <div className={classes.content}> 19 | <div className={classes.name}>{food.name}</div> 20 | <span 21 | className={`${classes.favorite} ${ 22 | food.favorite ? '' : classes.not 23 | }`} 24 | > 25 | ❤ 26 | </span> 27 | <div className={classes.stars}> 28 | <StarRating stars={food.stars} /> 29 | </div> 30 | <div className={classes.product_item_footer}> 31 | <div className={classes.origins}> 32 | {food.origins.map(origin => ( 33 | <span key={origin}>{origin}</span> 34 | ))} 35 | </div> 36 | <div className={classes.cook_time}> 37 | <span>🕒</span> 38 | {food.cookTime} 39 | </div> 40 | </div> 41 | <div className={classes.price}> 42 | <Price price={food.price} /> 43 | </div> 44 | </div> 45 | </Link> 46 | </li> 47 | ))} 48 | </ul> 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /frontend/src/components/Thumbnails/thumbnails.module.css: -------------------------------------------------------------------------------- 1 | .list { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | flex-wrap: wrap; 6 | list-style-type: none; 7 | padding: 0; 8 | } 9 | 10 | .list li a { 11 | height: 22.5rem; 12 | width: 20rem; 13 | border: 1px solid whitesmoke; 14 | border-radius: 1rem; 15 | margin: 0.5rem; 16 | display: flex; 17 | flex-direction: column; 18 | overflow: hidden; 19 | color: #e72929; 20 | } 21 | 22 | .image { 23 | object-fit: cover; 24 | height: 14.5rem; 25 | } 26 | 27 | .content { 28 | margin-top: 0.3rem; 29 | padding: 0.5rem 1rem; 30 | position: relative; 31 | height: 7rem; 32 | } 33 | 34 | .favorite { 35 | position: absolute; 36 | top: 0.5rem; 37 | right: 1rem; 38 | font-size: 1.2rem; 39 | } 40 | 41 | .favorite.not { 42 | color: grey; 43 | } 44 | 45 | .stars { 46 | margin: 0.5rem 0; 47 | } 48 | 49 | .product_item_footer { 50 | display: flex; 51 | justify-content: space-between; 52 | align-items: flex-start; 53 | } 54 | 55 | .origins span { 56 | border-radius: 2rem; 57 | background-color: whitesmoke; 58 | display: inline-block; 59 | font-size: 0.8rem; 60 | color: grey; 61 | margin-right: 0.3rem; 62 | padding: 0 0.5rem; 63 | margin-top: 0.2rem; 64 | } 65 | 66 | .origins { 67 | flex: 9; 68 | } 69 | 70 | .cook_time { 71 | font-size: 0.9rem; 72 | flex: 3; 73 | text-align: right; 74 | } 75 | 76 | .price { 77 | position: absolute; 78 | bottom: 0; 79 | left: 0.9rem; 80 | background-color: white; 81 | padding: 0.3rem 100% 0 0; 82 | color: #414141; 83 | margin-top: 0.7rem; 84 | font-size: 1.1rem; 85 | } 86 | -------------------------------------------------------------------------------- /frontend/src/components/Title/Title.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Title({ title, fontSize, margin }) { 4 | return <h1 style={{ fontSize, margin, color: '#616161' }}>{title}</h1>; 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/components/Title/title.module.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nasirjd/foodmine-react-js/306e82c72b3dd728eb5fdc9e91d7944219dece47/frontend/src/components/Title/title.module.css -------------------------------------------------------------------------------- /frontend/src/constants/patterns.js: -------------------------------------------------------------------------------- 1 | export const EMAIL = { 2 | value: /^[\w-.]+@([\w-]+\.)+[\w-]{2,63}$/i, 3 | message: 'Email Is Not Valid', 4 | }; 5 | -------------------------------------------------------------------------------- /frontend/src/hooks/useAuth.js: -------------------------------------------------------------------------------- 1 | import { useState, createContext, useContext } from 'react'; 2 | import * as userService from '../services/userService'; 3 | import { toast } from 'react-toastify'; 4 | 5 | const AuthContext = createContext(null); 6 | 7 | export const AuthProvider = ({ children }) => { 8 | const [user, setUser] = useState(userService.getUser()); 9 | 10 | const login = async (email, password) => { 11 | try { 12 | const user = await userService.login(email, password); 13 | setUser(user); 14 | toast.success('Login Successful'); 15 | } catch (err) { 16 | toast.error(err.response.data); 17 | } 18 | }; 19 | 20 | const register = async data => { 21 | try { 22 | const user = await userService.register(data); 23 | setUser(user); 24 | toast.success('Register Successful'); 25 | } catch (err) { 26 | toast.error(err.response.data); 27 | } 28 | }; 29 | 30 | const logout = () => { 31 | userService.logout(); 32 | setUser(null); 33 | toast.success('Logout Successful'); 34 | }; 35 | 36 | const updateProfile = async user => { 37 | const updatedUser = await userService.updateProfile(user); 38 | toast.success('Profile Update Was Successful'); 39 | if (updatedUser) setUser(updatedUser); 40 | }; 41 | 42 | const changePassword = async passwords => { 43 | await userService.changePassword(passwords); 44 | logout(); 45 | toast.success('Password Changed Successfully, Please Login Again!'); 46 | }; 47 | 48 | return ( 49 | <AuthContext.Provider 50 | value={{ user, login, logout, register, updateProfile, changePassword }} 51 | > 52 | {children} 53 | </AuthContext.Provider> 54 | ); 55 | }; 56 | 57 | export const useAuth = () => useContext(AuthContext); 58 | -------------------------------------------------------------------------------- /frontend/src/hooks/useCart.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useEffect, useState } from 'react'; 2 | 3 | const CartContext = createContext(null); 4 | const CART_KEY = 'cart'; 5 | const EMPTY_CART = { 6 | items: [], 7 | totalPrice: 0, 8 | totalCount: 0, 9 | }; 10 | 11 | export default function CartProvider({ children }) { 12 | const initCart = getCartFromLocalStorage(); 13 | const [cartItems, setCartItems] = useState(initCart.items); 14 | const [totalPrice, setTotalPrice] = useState(initCart.totalPrice); 15 | const [totalCount, setTotalCount] = useState(initCart.totalCount); 16 | 17 | useEffect(() => { 18 | const totalPrice = sum(cartItems.map(item => item.price)); 19 | const totalCount = sum(cartItems.map(item => item.quantity)); 20 | setTotalPrice(totalPrice); 21 | setTotalCount(totalCount); 22 | 23 | localStorage.setItem( 24 | CART_KEY, 25 | JSON.stringify({ 26 | items: cartItems, 27 | totalPrice, 28 | totalCount, 29 | }) 30 | ); 31 | }, [cartItems]); 32 | 33 | function getCartFromLocalStorage() { 34 | const storedCart = localStorage.getItem(CART_KEY); 35 | return storedCart ? JSON.parse(storedCart) : EMPTY_CART; 36 | } 37 | 38 | const sum = items => { 39 | return items.reduce((prevValue, curValue) => prevValue + curValue, 0); 40 | }; 41 | 42 | const removeFromCart = foodId => { 43 | const filteredCartItems = cartItems.filter(item => item.food.id !== foodId); 44 | setCartItems(filteredCartItems); 45 | }; 46 | 47 | const changeQuantity = (cartItem, newQauntity) => { 48 | const { food } = cartItem; 49 | 50 | const changedCartItem = { 51 | ...cartItem, 52 | quantity: newQauntity, 53 | price: food.price * newQauntity, 54 | }; 55 | 56 | setCartItems( 57 | cartItems.map(item => (item.food.id === food.id ? changedCartItem : item)) 58 | ); 59 | }; 60 | 61 | const addToCart = food => { 62 | const cartItem = cartItems.find(item => item.food.id === food.id); 63 | if (cartItem) { 64 | changeQuantity(cartItem, cartItem.quantity + 1); 65 | } else { 66 | setCartItems([...cartItems, { food, quantity: 1, price: food.price }]); 67 | } 68 | }; 69 | 70 | const clearCart = () => { 71 | localStorage.removeItem(CART_KEY); 72 | const { items, totalPrice, totalCount } = EMPTY_CART; 73 | setCartItems(items); 74 | setTotalPrice(totalPrice); 75 | setTotalCount(totalCount); 76 | }; 77 | 78 | return ( 79 | <CartContext.Provider 80 | value={{ 81 | cart: { items: cartItems, totalPrice, totalCount }, 82 | removeFromCart, 83 | changeQuantity, 84 | addToCart, 85 | clearCart, 86 | }} 87 | > 88 | {children} 89 | </CartContext.Provider> 90 | ); 91 | } 92 | 93 | export const useCart = () => useContext(CartContext); 94 | -------------------------------------------------------------------------------- /frontend/src/hooks/useLoading.js: -------------------------------------------------------------------------------- 1 | import { useState, createContext, useContext } from 'react'; 2 | 3 | const LoadingContext = createContext({}); 4 | 5 | export const LoadingProvider = ({ children }) => { 6 | const [isLoading, setIsLoading] = useState(false); 7 | 8 | const showLoading = () => setIsLoading(true); 9 | const hideLoading = () => setIsLoading(false); 10 | 11 | return ( 12 | <LoadingContext.Provider value={{ isLoading, showLoading, hideLoading }}> 13 | {children} 14 | </LoadingContext.Provider> 15 | ); 16 | }; 17 | 18 | export const useLoading = () => useContext(LoadingContext); 19 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Quicksand&display=swap'); 2 | 3 | * { 4 | box-sizing: border-box; 5 | } 6 | 7 | html { 8 | font-size: 18px; 9 | } 10 | 11 | body { 12 | margin: 0; 13 | font-family: Quicksand, sans-serif; 14 | } 15 | 16 | a { 17 | text-decoration: none; 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | import { BrowserRouter } from 'react-router-dom'; 7 | import CartProvider from './hooks/useCart'; 8 | import './axiosConfig'; 9 | import { AuthProvider } from './hooks/useAuth'; 10 | import { ToastContainer } from 'react-toastify'; 11 | import 'react-toastify/dist/ReactToastify.css'; 12 | import { LoadingProvider } from './hooks/useLoading'; 13 | import './interceptors/authInterceptor'; 14 | 15 | const root = ReactDOM.createRoot(document.getElementById('root')); 16 | root.render( 17 | <React.StrictMode> 18 | <BrowserRouter> 19 | <LoadingProvider> 20 | <AuthProvider> 21 | <CartProvider> 22 | <App /> 23 | <ToastContainer 24 | position="bottom-right" 25 | autoClose={5000} 26 | hideProgressBar={false} 27 | newestOnTop={false} 28 | closeOnClick 29 | rtl={false} 30 | pauseOnFocusLoss 31 | draggable 32 | pauseOnHover 33 | theme="light" 34 | /> 35 | </CartProvider> 36 | </AuthProvider> 37 | </LoadingProvider> 38 | </BrowserRouter> 39 | </React.StrictMode> 40 | ); 41 | 42 | // If you want to start measuring performance in your app, pass a function 43 | // to log results (for example: reportWebVitals(console.log)) 44 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 45 | reportWebVitals(); 46 | -------------------------------------------------------------------------------- /frontend/src/interceptors/authInterceptor.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | axios.interceptors.request.use( 4 | req => { 5 | const user = localStorage.getItem('user'); 6 | const token = user && JSON.parse(user).token; 7 | if (token) { 8 | req.headers['access_token'] = token; 9 | } 10 | return req; 11 | }, 12 | error => { 13 | return Promise.reject(error); 14 | } 15 | ); 16 | -------------------------------------------------------------------------------- /frontend/src/interceptors/loadingInterceptor.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export const setLoadingInterceptor = ({ showLoading, hideLoading }) => { 4 | axios.interceptors.request.use( 5 | req => { 6 | if (!(req.data instanceof FormData)) showLoading(); 7 | return req; 8 | }, 9 | error => { 10 | hideLoading(); 11 | return Promise.reject(error); 12 | } 13 | ); 14 | 15 | axios.interceptors.response.use( 16 | res => { 17 | hideLoading(); 18 | return res; 19 | }, 20 | error => { 21 | hideLoading(); 22 | return Promise.reject(error); 23 | } 24 | ); 25 | }; 26 | 27 | export default setLoadingInterceptor; 28 | -------------------------------------------------------------------------------- /frontend/src/pages/Cart/CartPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import Price from '../../components/Price/Price'; 4 | import Title from '../../components/Title/Title'; 5 | import { useCart } from '../../hooks/useCart'; 6 | import classes from './cartPage.module.css'; 7 | import NotFound from '../../components/NotFound/NotFound'; 8 | export default function CartPage() { 9 | const { cart, removeFromCart, changeQuantity } = useCart(); 10 | return ( 11 | <> 12 | <Title title="Cart Page" margin="1.5rem 0 0 2.5rem" /> 13 | 14 | {cart.items.length === 0 ? ( 15 | <NotFound message="Cart Page Is Empty!" /> 16 | ) : ( 17 | <div className={classes.container}> 18 | <ul className={classes.list}> 19 | {cart.items.map(item => ( 20 | <li key={item.food.id}> 21 | <div> 22 | <img src={`${item.food.imageUrl}`} alt={item.food.name} /> 23 | </div> 24 | <div> 25 | <Link to={`/food/${item.food.id}`}>{item.food.name}</Link> 26 | </div> 27 | 28 | <div> 29 | <select 30 | value={item.quantity} 31 | onChange={e => changeQuantity(item, Number(e.target.value))} 32 | > 33 | <option>1</option> 34 | <option>2</option> 35 | <option>3</option> 36 | <option>4</option> 37 | <option>5</option> 38 | <option>6</option> 39 | <option>7</option> 40 | <option>8</option> 41 | <option>9</option> 42 | <option>10</option> 43 | </select> 44 | </div> 45 | 46 | <div> 47 | <Price price={item.price} /> 48 | </div> 49 | 50 | <div> 51 | <button 52 | className={classes.remove_button} 53 | onClick={() => removeFromCart(item.food.id)} 54 | > 55 | Remove 56 | </button> 57 | </div> 58 | </li> 59 | ))} 60 | </ul> 61 | 62 | <div className={classes.checkout}> 63 | <div> 64 | <div className={classes.foods_count}>{cart.totalCount}</div> 65 | <div className={classes.total_price}> 66 | <Price price={cart.totalPrice} /> 67 | </div> 68 | </div> 69 | 70 | <Link to="/checkout">Proceed To Checkout</Link> 71 | </div> 72 | </div> 73 | )} 74 | </> 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /frontend/src/pages/Cart/cartPage.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-wrap: wrap; 4 | align-items: flex-start; 5 | margin: 1.5rem; 6 | margin-top: 0.5rem; 7 | } 8 | 9 | .list { 10 | display: flex; 11 | flex-direction: column; 12 | flex: 3 0; 13 | justify-content: space-evenly; 14 | border: 1px solid #ffbbbb; 15 | border-radius: 1rem; 16 | list-style-type: none; 17 | margin: 0.5rem; 18 | padding: 0; 19 | } 20 | 21 | .list li { 22 | display: flex; 23 | justify-content: space-around; 24 | align-items: center; 25 | flex-wrap: wrap; 26 | border-bottom: 1px solid #e4e4e4; 27 | } 28 | 29 | .list li:last-child { 30 | border: none; 31 | } 32 | 33 | .list li img { 34 | width: 5rem; 35 | height: 5rem; 36 | border-radius: 100rem; 37 | object-fit: cover; 38 | } 39 | 40 | .list li div { 41 | padding: 1rem; 42 | } 43 | 44 | .list li div:not(:first-child) { 45 | flex-basis: 18%; 46 | } 47 | 48 | .list li select { 49 | width: 3rem; 50 | outline: none; 51 | border: none; 52 | border-bottom: 1px solid lightgrey; 53 | font-size: 1.1rem; 54 | font-weight: 100; 55 | } 56 | 57 | .list .remove_button { 58 | border-radius: 1rem; 59 | border: none; 60 | padding: 0.5rem; 61 | color: #e72929; 62 | opacity: 0.7; 63 | outline: none; 64 | } 65 | 66 | .list .remove_button:hover { 67 | opacity: 1; 68 | cursor: pointer; 69 | } 70 | 71 | .checkout { 72 | display: flex; 73 | flex-direction: column; 74 | justify-content: space-between; 75 | align-items: center; 76 | flex: 1 3; 77 | height: 20rem; 78 | border: 1px solid #ffbbbb; 79 | border-radius: 1rem; 80 | padding: 0.5rem; 81 | margin: 0.5rem; 82 | } 83 | 84 | .checkout > div { 85 | font-size: 1.4rem; 86 | margin: 1rem; 87 | flex: 3; 88 | display: flex; 89 | flex-direction: column; 90 | justify-content: center; 91 | align-items: flex-start; 92 | } 93 | 94 | .checkout .foods_count { 95 | margin-bottom: 1.5rem; 96 | } 97 | 98 | .checkout .foods_count::before { 99 | content: 'Count: '; 100 | color: grey; 101 | } 102 | 103 | .checkout .total_price::before { 104 | content: 'Price: '; 105 | color: grey; 106 | } 107 | 108 | .checkout a { 109 | padding: 1rem; 110 | color: white; 111 | background: #e72929; 112 | display: block; 113 | width: 99%; 114 | border-radius: 1rem; 115 | text-align: center; 116 | justify-self: center; 117 | } 118 | 119 | .checkout a:hover { 120 | opacity: 0.8; 121 | cursor: pointer; 122 | } 123 | -------------------------------------------------------------------------------- /frontend/src/pages/Checkout/CheckoutPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useCart } from '../../hooks/useCart'; 3 | import { useAuth } from '../../hooks/useAuth'; 4 | import { useNavigate } from 'react-router-dom'; 5 | import { useState } from 'react'; 6 | import { useForm } from 'react-hook-form'; 7 | import { toast } from 'react-toastify'; 8 | import { createOrder } from '../../services/orderService'; 9 | import classes from './checkoutPage.module.css'; 10 | import Title from '../../components/Title/Title'; 11 | import Input from '../../components/Input/Input'; 12 | import Button from '../../components/Button/Button'; 13 | import OrderItemsList from '../../components/OrderItemsList/OrderItemsList'; 14 | import Map from '../../components/Map/Map'; 15 | export default function CheckoutPage() { 16 | const { cart } = useCart(); 17 | const { user } = useAuth(); 18 | const navigate = useNavigate(); 19 | const [order, setOrder] = useState({ ...cart }); 20 | 21 | const { 22 | register, 23 | formState: { errors }, 24 | handleSubmit, 25 | } = useForm(); 26 | 27 | const submit = async data => { 28 | if (!order.addressLatLng) { 29 | toast.warning('Please select your location on the map'); 30 | return; 31 | } 32 | 33 | await createOrder({ ...order, name: data.name, address: data.address }); 34 | navigate('/payment'); 35 | }; 36 | 37 | return ( 38 | <> 39 | <form onSubmit={handleSubmit(submit)} className={classes.container}> 40 | <div className={classes.content}> 41 | <Title title="Order Form" fontSize="1.6rem" /> 42 | <div className={classes.inputs}> 43 | <Input 44 | defaultValue={user.name} 45 | label="Name" 46 | {...register('name')} 47 | error={errors.name} 48 | /> 49 | <Input 50 | defaultValue={user.address} 51 | label="Address" 52 | {...register('address')} 53 | error={errors.address} 54 | /> 55 | </div> 56 | <OrderItemsList order={order} /> 57 | </div> 58 | <div> 59 | <Title title="Choose Your Location" fontSize="1.6rem" /> 60 | <Map 61 | location={order.addressLatLng} 62 | onChange={latlng => { 63 | console.log(latlng); 64 | setOrder({ ...order, addressLatLng: latlng }); 65 | }} 66 | /> 67 | </div> 68 | 69 | <div className={classes.buttons_container}> 70 | <div className={classes.buttons}> 71 | <Button 72 | type="submit" 73 | text="Go To Payment" 74 | width="100%" 75 | height="3rem" 76 | /> 77 | </div> 78 | </div> 79 | </form> 80 | </> 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /frontend/src/pages/Checkout/checkoutPage.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | margin-top: 1rem; 3 | display: flex; 4 | margin: 0 0 6rem 2rem; 5 | flex-wrap: wrap; 6 | justify-content: center; 7 | } 8 | 9 | .content { 10 | width: 35rem; 11 | margin: 0 3rem 0 0; 12 | } 13 | 14 | .content, 15 | .inputs { 16 | display: flex; 17 | flex-direction: column; 18 | } 19 | 20 | .inputs { 21 | width: 100%; 22 | } 23 | 24 | .buttons_container { 25 | flex-basis: 100%; 26 | display: flex; 27 | justify-content: center; 28 | } 29 | .buttons { 30 | margin: 2rem; 31 | width: 35rem; 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/pages/Dashboard/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useAuth } from '../../hooks/useAuth'; 3 | import classes from './dashboard.module.css'; 4 | import { Link } from 'react-router-dom'; 5 | 6 | export default function Dashboard() { 7 | const { user } = useAuth(); 8 | 9 | return ( 10 | <div className={classes.container}> 11 | <div className={classes.menu}> 12 | {allItems 13 | .filter(item => user.isAdmin || !item.forAdmin) 14 | .map(item => ( 15 | <Link 16 | key={item.title} 17 | to={item.url} 18 | style={{ 19 | backgroundColor: item.bgColor, 20 | color: item.color, 21 | }} 22 | > 23 | <img src={item.imageUrl} alt={item.title} /> 24 | <h2>{item.title}</h2> 25 | </Link> 26 | ))} 27 | </div> 28 | </div> 29 | ); 30 | } 31 | 32 | const allItems = [ 33 | { 34 | title: 'Orders', 35 | imageUrl: '/icons/orders.svg', 36 | url: '/orders', 37 | bgColor: '#ec407a', 38 | color: 'white', 39 | }, 40 | { 41 | title: 'Profile', 42 | imageUrl: '/icons/profile.svg', 43 | url: '/profile', 44 | bgColor: '#1565c0', 45 | color: 'white', 46 | }, 47 | { 48 | title: 'Users', 49 | imageUrl: '/icons/users.svg', 50 | url: '/admin/users', 51 | forAdmin: true, 52 | bgColor: '#00bfa5', 53 | color: 'white', 54 | }, 55 | { 56 | title: 'Foods', 57 | imageUrl: '/icons/foods.svg', 58 | url: '/admin/foods', 59 | forAdmin: true, 60 | bgColor: '#e040fb', 61 | color: 'white', 62 | }, 63 | ]; 64 | -------------------------------------------------------------------------------- /frontend/src/pages/Dashboard/dashboard.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | margin: auto; 3 | margin-top: 1rem; 4 | max-width: 70rem; 5 | width: 100%; 6 | } 7 | 8 | .menu { 9 | display: flex; 10 | justify-content: center; 11 | flex-wrap: wrap; 12 | } 13 | 14 | .menu a { 15 | width: 22rem; 16 | height: 11rem; 17 | margin: 1.5rem 2rem; 18 | background-color: whitesmoke; 19 | box-shadow: 0 0 5px 1px; 20 | border-radius: 1rem; 21 | display: flex; 22 | align-items: center; 23 | color: #444444; 24 | justify-content: space-around; 25 | } 26 | 27 | .menu img { 28 | width: 10rem; 29 | height: 10rem; 30 | margin: 1rem; 31 | } 32 | 33 | .menu a:hover { 34 | box-shadow: 2px 2px 10px 1px grey; 35 | } 36 | 37 | .menu h2 { 38 | font-size: 2.5rem; 39 | margin-right: 2rem; 40 | } 41 | -------------------------------------------------------------------------------- /frontend/src/pages/Food/FoodPage.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { useNavigate, useParams } from 'react-router-dom'; 3 | import Price from '../../components/Price/Price'; 4 | import StarRating from '../../components/StarRating/StarRating'; 5 | import Tags from '../../components/Tags/Tags'; 6 | import { useCart } from '../../hooks/useCart'; 7 | import { getById } from '../../services/foodService'; 8 | import classes from './foodPage.module.css'; 9 | import NotFound from '../../components/NotFound/NotFound'; 10 | export default function FoodPage() { 11 | const [food, setFood] = useState({}); 12 | const { id } = useParams(); 13 | const { addToCart } = useCart(); 14 | const navigate = useNavigate(); 15 | 16 | const handleAddToCart = () => { 17 | addToCart(food); 18 | navigate('/cart'); 19 | }; 20 | 21 | useEffect(() => { 22 | getById(id).then(setFood); 23 | }, [id]); 24 | return ( 25 | <> 26 | {!food ? ( 27 | <NotFound message="Food Not Found!" linkText="Back To Homepage" /> 28 | ) : ( 29 | <div className={classes.container}> 30 | <img 31 | className={classes.image} 32 | src={`${food.imageUrl}`} 33 | alt={food.name} 34 | /> 35 | 36 | <div className={classes.details}> 37 | <div className={classes.header}> 38 | <span className={classes.name}>{food.name}</span> 39 | <span 40 | className={`${classes.favorite} ${ 41 | food.favorite ? '' : classes.not 42 | }`} 43 | > 44 | ❤ 45 | </span> 46 | </div> 47 | <div className={classes.rating}> 48 | <StarRating stars={food.stars} size={25} /> 49 | </div> 50 | 51 | <div className={classes.origins}> 52 | {food.origins?.map(origin => ( 53 | <span key={origin}>{origin}</span> 54 | ))} 55 | </div> 56 | 57 | <div className={classes.tags}> 58 | {food.tags && ( 59 | <Tags 60 | tags={food.tags.map(tag => ({ name: tag }))} 61 | forFoodPage={true} 62 | /> 63 | )} 64 | </div> 65 | 66 | <div className={classes.cook_time}> 67 | <span> 68 | Time to cook about <strong>{food.cookTime}</strong> minutes 69 | </span> 70 | </div> 71 | 72 | <div className={classes.price}> 73 | <Price price={food.price} /> 74 | </div> 75 | 76 | <button onClick={handleAddToCart}>Add To Cart</button> 77 | </div> 78 | </div> 79 | )} 80 | </> 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /frontend/src/pages/Food/foodPage.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | flex-wrap: wrap; 6 | margin: 3rem; 7 | } 8 | 9 | .container > * { 10 | min-width: 25rem; 11 | max-width: 40rem; 12 | } 13 | 14 | .image { 15 | border-radius: 3rem; 16 | flex: 1 0; 17 | object-fit: cover; 18 | height: 35rem; 19 | margin: 1rem; 20 | } 21 | 22 | .details { 23 | width: 100%; 24 | display: flex; 25 | flex-direction: column; 26 | flex: 1 0; 27 | border-radius: 3rem; 28 | padding: 2rem; 29 | color: black; 30 | margin-left: 1rem; 31 | } 32 | 33 | .header { 34 | display: flex; 35 | justify-content: space-between; 36 | } 37 | 38 | .name { 39 | font-size: 2rem; 40 | font-weight: bold; 41 | } 42 | 43 | .favorite { 44 | color: #e72929; 45 | font-size: 2.5rem; 46 | } 47 | 48 | .favorite.not { 49 | color: grey; 50 | } 51 | 52 | .origins { 53 | display: flex; 54 | flex-wrap: wrap; 55 | margin: 0.7rem 0; 56 | } 57 | 58 | .origins > span { 59 | padding: 0.5rem; 60 | font-size: 1.2rem; 61 | margin: 0.5rem 0.5rem 0 0; 62 | border-radius: 2rem; 63 | background-color: aliceblue; 64 | } 65 | 66 | .cook_time { 67 | margin-top: 1rem; 68 | } 69 | 70 | .cook_time span { 71 | padding: 0.6rem 2rem 0.6rem 0; 72 | border-radius: 10rem; 73 | font-size: 1.3rem; 74 | } 75 | 76 | .price { 77 | font-size: 1.8rem; 78 | margin: 2rem 2rem 2rem 0; 79 | color: green; 80 | } 81 | 82 | .price::before { 83 | content: 'Price: '; 84 | color: darkgrey; 85 | } 86 | 87 | .container button { 88 | color: white; 89 | background-color: #e72929; 90 | border: none; 91 | font-size: 1.2rem; 92 | padding: 1rem; 93 | border-radius: 10rem; 94 | outline: none; 95 | } 96 | 97 | .container button:hover { 98 | opacity: 0.9; 99 | cursor: pointer; 100 | } 101 | -------------------------------------------------------------------------------- /frontend/src/pages/FoodEdit/FoodEditPage.js: -------------------------------------------------------------------------------- 1 | import { useParams } from 'react-router-dom'; 2 | import classes from './foodEdit.module.css'; 3 | import { useForm } from 'react-hook-form'; 4 | import { useEffect, useState } from 'react'; 5 | import { add, getById, update } from '../../services/foodService'; 6 | import Title from '../../components/Title/Title'; 7 | import InputContainer from '../../components/InputContainer/InputContainer'; 8 | import Input from '../../components/Input/Input'; 9 | import Button from '../../components/Button/Button'; 10 | import { uploadImage } from '../../services/uploadService'; 11 | import { toast } from 'react-toastify'; 12 | import { useNavigate } from 'react-router-dom'; 13 | 14 | export default function FoodEditPage() { 15 | const { foodId } = useParams(); 16 | const [imageUrl, setImageUrl] = useState(); 17 | const isEditMode = !!foodId; 18 | 19 | const navigate = useNavigate(); 20 | 21 | const { 22 | handleSubmit, 23 | register, 24 | formState: { errors }, 25 | reset, 26 | } = useForm(); 27 | 28 | useEffect(() => { 29 | if (!isEditMode) return; 30 | 31 | getById(foodId).then(food => { 32 | if (!food) return; 33 | reset(food); 34 | setImageUrl(food.imageUrl); 35 | }); 36 | }, [foodId]); 37 | 38 | const submit = async foodData => { 39 | const food = { ...foodData, imageUrl }; 40 | 41 | if (isEditMode) { 42 | await update(food); 43 | toast.success(`Food "${food.name}" updated successfully!`); 44 | return; 45 | } 46 | 47 | const newFood = await add(food); 48 | toast.success(`Food "${food.name}" added successfully!`); 49 | navigate('/admin/editFood/' + newFood.id, { replace: true }); 50 | }; 51 | 52 | const upload = async event => { 53 | setImageUrl(null); 54 | const imageUrl = await uploadImage(event); 55 | setImageUrl(imageUrl); 56 | }; 57 | 58 | return ( 59 | <div className={classes.container}> 60 | <div className={classes.content}> 61 | <Title title={isEditMode ? 'Edit Food' : 'Add Food'} /> 62 | <form 63 | className={classes.form} 64 | onSubmit={handleSubmit(submit)} 65 | noValidate 66 | > 67 | <InputContainer label="Select Image"> 68 | <input type="file" onChange={upload} accept="image/jpeg" /> 69 | </InputContainer> 70 | 71 | {imageUrl && ( 72 | <a href={imageUrl} className={classes.image_link} target="blank"> 73 | <img src={imageUrl} alt="Uploaded" /> 74 | </a> 75 | )} 76 | 77 | <Input 78 | type="text" 79 | label="Name" 80 | {...register('name', { required: true, minLength: 5 })} 81 | error={errors.name} 82 | /> 83 | 84 | <Input 85 | type="number" 86 | label="Price" 87 | {...register('price', { required: true })} 88 | error={errors.price} 89 | /> 90 | 91 | <Input 92 | type="text" 93 | label="Tags" 94 | {...register('tags')} 95 | error={errors.tags} 96 | /> 97 | 98 | <Input 99 | type="text" 100 | label="Origins" 101 | {...register('origins', { required: true })} 102 | error={errors.origins} 103 | /> 104 | 105 | <Input 106 | type="text" 107 | label="Cook Time" 108 | {...register('cookTime', { required: true })} 109 | error={errors.cookTime} 110 | /> 111 | 112 | <Button type="submit" text={isEditMode ? 'Update' : 'Create'} /> 113 | </form> 114 | </div> 115 | </div> 116 | ); 117 | } 118 | -------------------------------------------------------------------------------- /frontend/src/pages/FoodEdit/foodEdit.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | justify-content: center; 4 | margin-bottom: 10rem; 5 | } 6 | 7 | .content, 8 | .form { 9 | display: flex; 10 | flex-direction: column; 11 | max-width: 25rem; 12 | width: 100%; 13 | } 14 | 15 | .image_link { 16 | display: block; 17 | width: 100%; 18 | height: 20rem; 19 | border-radius: 1rem; 20 | border: 1px solid #e0e0e0; 21 | margin: 0.5rem 0 1rem 0; 22 | } 23 | 24 | .image_link img { 25 | object-fit: cover; 26 | width: 100%; 27 | height: 100%; 28 | border-radius: 1rem; 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/pages/FoodsAdmin/FoodsAdminPage.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import classes from './foodsAdminPage.module.css'; 3 | import { Link, useParams } from 'react-router-dom'; 4 | import { deleteById, getAll, search } from '../../services/foodService'; 5 | import NotFound from '../../components/NotFound/NotFound'; 6 | import Title from '../../components/Title/Title'; 7 | import Search from '../../components/Search/Search'; 8 | import Price from '../../components/Price/Price'; 9 | import { toast } from 'react-toastify'; 10 | 11 | export default function FoodsAdminPage() { 12 | const [foods, setFoods] = useState(); 13 | const { searchTerm } = useParams(); 14 | 15 | useEffect(() => { 16 | loadFoods(); 17 | }, [searchTerm]); 18 | 19 | const loadFoods = async () => { 20 | const foods = searchTerm ? await search(searchTerm) : await getAll(); 21 | setFoods(foods); 22 | }; 23 | 24 | const FoodsNotFound = () => { 25 | if (foods && foods.length > 0) return; 26 | 27 | return searchTerm ? ( 28 | <NotFound linkRoute="/admin/foods" linkText="Show All" /> 29 | ) : ( 30 | <NotFound linkRoute="/dashboard" linkText="Back to dashboard!" /> 31 | ); 32 | }; 33 | 34 | const deleteFood = async food => { 35 | const confirmed = window.confirm(`Delete Food ${food.name}?`); 36 | if (!confirmed) return; 37 | 38 | await deleteById(food.id); 39 | toast.success(`"${food.name}" Has Been Removed!`); 40 | setFoods(foods.filter(f => f.id !== food.id)); 41 | }; 42 | 43 | return ( 44 | <div className={classes.container}> 45 | <div className={classes.list}> 46 | <Title title="Manage Foods" margin="1rem auto" /> 47 | <Search 48 | searchRoute="/admin/foods/" 49 | defaultRoute="/admin/foods" 50 | margin="1rem 0" 51 | placeholder="Search Foods" 52 | /> 53 | <Link to="/admin/addFood" className={classes.add_food}> 54 | Add Food + 55 | </Link> 56 | <FoodsNotFound /> 57 | {foods && 58 | foods.map(food => ( 59 | <div key={food.id} className={classes.list_item}> 60 | <img src={food.imageUrl} alt={food.name} /> 61 | <Link to={'/food/' + food.id}>{food.name}</Link> 62 | <Price price={food.price} /> 63 | <div className={classes.actions}> 64 | <Link to={'/admin/editFood/' + food.id}>Edit</Link> 65 | <Link onClick={() => deleteFood(food)}>Delete</Link> 66 | </div> 67 | </div> 68 | ))} 69 | </div> 70 | </div> 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /frontend/src/pages/FoodsAdmin/foodsAdminPage.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | justify-content: center; 4 | margin-top: 1rem; 5 | margin-bottom: 10rem; 6 | } 7 | 8 | .list { 9 | margin: 1rem; 10 | max-width: 70rem; 11 | } 12 | 13 | a.add_food { 14 | display: inline-block; 15 | background: darkred; 16 | color: white; 17 | padding: 0.7rem; 18 | margin: 0.8rem 0; 19 | border-radius: 1.5rem; 20 | } 21 | 22 | a.add_food:hover { 23 | opacity: 0.9; 24 | } 25 | 26 | .list_item { 27 | display: grid; 28 | grid-template-columns: 29 | minmax(5rem, 7rem) 30 | minmax(10rem, 20rem) 31 | minmax(5rem, 10rem) 32 | minmax(5rem, 10rem); 33 | font-size: 1.2rem; 34 | height: 5rem; 35 | align-items: center; 36 | justify-content: center; 37 | } 38 | 39 | .list_item img { 40 | width: 4rem; 41 | height: 4rem; 42 | border-radius: 100rem; 43 | object-fit: cover; 44 | } 45 | 46 | .actions > a { 47 | margin-right: 1rem; 48 | color: darkblue; 49 | cursor: pointer; 50 | } 51 | -------------------------------------------------------------------------------- /frontend/src/pages/Home/HomePage.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useReducer } from 'react'; 2 | import { useParams } from 'react-router-dom'; 3 | import Search from '../../components/Search/Search'; 4 | import Tags from '../../components/Tags/Tags'; 5 | import Thumbnails from '../../components/Thumbnails/Thumbnails'; 6 | import { 7 | getAll, 8 | getAllByTag, 9 | getAllTags, 10 | search, 11 | } from '../../services/foodService'; 12 | import NotFound from '../../components/NotFound/NotFound'; 13 | 14 | const initialState = { foods: [], tags: [] }; 15 | 16 | const reducer = (state, action) => { 17 | switch (action.type) { 18 | case 'FOODS_LOADED': 19 | return { ...state, foods: action.payload }; 20 | case 'TAGS_LOADED': 21 | return { ...state, tags: action.payload }; 22 | default: 23 | return state; 24 | } 25 | }; 26 | 27 | export default function HomePage() { 28 | const [state, dispatch] = useReducer(reducer, initialState); 29 | const { foods, tags } = state; 30 | const { searchTerm, tag } = useParams(); 31 | 32 | useEffect(() => { 33 | getAllTags().then(tags => dispatch({ type: 'TAGS_LOADED', payload: tags })); 34 | 35 | const loadFoods = tag 36 | ? getAllByTag(tag) 37 | : searchTerm 38 | ? search(searchTerm) 39 | : getAll(); 40 | 41 | loadFoods.then(foods => dispatch({ type: 'FOODS_LOADED', payload: foods })); 42 | }, [searchTerm, tag]); 43 | 44 | return ( 45 | <> 46 | <Search /> 47 | <Tags tags={tags} /> 48 | {foods.length === 0 && <NotFound linkText="Reset Search" />} 49 | <Thumbnails foods={foods} /> 50 | </> 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /frontend/src/pages/Login/LoginPage.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useForm } from 'react-hook-form'; 3 | import { useNavigate, useSearchParams, Link } from 'react-router-dom'; 4 | import { useAuth } from '../../hooks/useAuth'; 5 | import classes from './loginPage.module.css'; 6 | import Title from '../../components/Title/Title'; 7 | import Input from '../../components/Input/Input'; 8 | import Button from '../../components/Button/Button'; 9 | import { EMAIL } from '../../constants/patterns'; 10 | export default function LoginPage() { 11 | const { 12 | handleSubmit, 13 | register, 14 | formState: { errors }, 15 | } = useForm(); 16 | 17 | const navigate = useNavigate(); 18 | const { user, login } = useAuth(); 19 | const [params] = useSearchParams(); 20 | const returnUrl = params.get('returnUrl'); 21 | 22 | useEffect(() => { 23 | if (!user) return; 24 | 25 | returnUrl ? navigate(returnUrl) : navigate('/'); 26 | }, [user]); 27 | 28 | const submit = async ({ email, password }) => { 29 | await login(email, password); 30 | }; 31 | 32 | return ( 33 | <div className={classes.container}> 34 | <div className={classes.details}> 35 | <Title title="Login" /> 36 | <form onSubmit={handleSubmit(submit)} noValidate> 37 | <Input 38 | type="email" 39 | label="Email" 40 | {...register('email', { 41 | required: true, 42 | pattern: EMAIL, 43 | })} 44 | error={errors.email} 45 | /> 46 | 47 | <Input 48 | type="password" 49 | label="Password" 50 | {...register('password', { 51 | required: true, 52 | })} 53 | error={errors.password} 54 | /> 55 | 56 | <Button type="submit" text="Login" /> 57 | 58 | <div className={classes.register}> 59 | New user?   60 | <Link to={`/register${returnUrl ? '?returnUrl=' + returnUrl : ''}`}> 61 | Register here 62 | </Link> 63 | </div> 64 | </form> 65 | </div> 66 | </div> 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /frontend/src/pages/Login/loginPage.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | height: 100%; 6 | margin-top: 3rem; 7 | } 8 | 9 | .details { 10 | width: 28rem; 11 | } 12 | 13 | .details form { 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: center; 17 | } 18 | 19 | .register { 20 | display: flex; 21 | justify-content: center; 22 | align-items: center; 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/pages/OrderTrack/OrderTrackPage.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Link, useParams } from 'react-router-dom'; 3 | import { trackOrderById } from '../../services/orderService'; 4 | import NotFound from '../../components/NotFound/NotFound'; 5 | import classes from './orderTrackPage.module.css'; 6 | import DateTime from '../../components/DateTime/DateTime'; 7 | import OrderItemsList from '../../components/OrderItemsList/OrderItemsList'; 8 | import Title from '../../components/Title/Title'; 9 | import Map from '../../components/Map/Map'; 10 | 11 | export default function OrderTrackPage() { 12 | const { orderId } = useParams(); 13 | const [order, setOrder] = useState(); 14 | 15 | useEffect(() => { 16 | orderId && 17 | trackOrderById(orderId).then(order => { 18 | setOrder(order); 19 | }); 20 | }, []); 21 | 22 | if (!orderId) 23 | return <NotFound message="Order Not Found" linkText="Go To Home Page" />; 24 | 25 | return ( 26 | order && ( 27 | <div className={classes.container}> 28 | <div className={classes.content}> 29 | <h1>Order #{order.id}</h1> 30 | <div className={classes.header}> 31 | <div> 32 | <strong>Date</strong> 33 | <DateTime date={order.createdAt} /> 34 | </div> 35 | <div> 36 | <strong>Name</strong> 37 | {order.name} 38 | </div> 39 | <div> 40 | <strong>Address</strong> 41 | {order.address} 42 | </div> 43 | <div> 44 | <strong>State</strong> 45 | {order.status} 46 | </div> 47 | {order.paymentId && ( 48 | <div> 49 | <strong>Payment ID</strong> 50 | {order.paymentId} 51 | </div> 52 | )} 53 | </div> 54 | 55 | <OrderItemsList order={order} /> 56 | </div> 57 | 58 | <div> 59 | <Title title="Your Location" fontSize="1.6rem" /> 60 | <Map location={order.addressLatLng} readonly={true} /> 61 | </div> 62 | 63 | {order.status === 'NEW' && ( 64 | <div className={classes.payment}> 65 | <Link to="/payment">Go To Payment</Link> 66 | </div> 67 | )} 68 | </div> 69 | ) 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /frontend/src/pages/OrderTrack/orderTrackPage.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | margin-top: 1rem; 3 | display: flex; 4 | justify-content: center; 5 | flex-wrap: wrap; 6 | } 7 | 8 | .content { 9 | display: flex; 10 | flex-direction: column; 11 | width: 100%; 12 | max-width: 35rem; 13 | margin-right: 1rem; 14 | } 15 | 16 | .header { 17 | margin-bottom: 1rem; 18 | } 19 | 20 | .header > div { 21 | margin-top: 1rem; 22 | font-size: 1.1rem; 23 | } 24 | 25 | .header strong { 26 | display: inline-block; 27 | width: 20%; 28 | } 29 | 30 | .payment { 31 | width: 100%; 32 | text-align: center; 33 | margin-top: 2rem; 34 | margin-bottom: 5rem; 35 | } 36 | 37 | .payment a { 38 | background-color: #ff4d4d; 39 | color: white; 40 | padding: 0.8rem 5rem; 41 | border-radius: 1rem; 42 | } 43 | -------------------------------------------------------------------------------- /frontend/src/pages/Orders/OrdersPage.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useReducer } from 'react'; 2 | import { Link, useParams } from 'react-router-dom'; 3 | import { getAll, getAllStatus } from '../../services/orderService'; 4 | import classes from './ordersPage.module.css'; 5 | import Title from '../../components/Title/Title'; 6 | import DateTime from '../../components/DateTime/DateTime'; 7 | import Price from '../../components/Price/Price'; 8 | import NotFound from '../../components/NotFound/NotFound'; 9 | 10 | const initialState = {}; 11 | const reducer = (state, action) => { 12 | const { type, payload } = action; 13 | switch (type) { 14 | case 'ALL_STATUS_FETCHED': 15 | return { ...state, allStatus: payload }; 16 | case 'ORDERS_FETCHED': 17 | return { ...state, orders: payload }; 18 | default: 19 | return state; 20 | } 21 | }; 22 | 23 | export default function OrdersPage() { 24 | const [{ allStatus, orders }, dispatch] = useReducer(reducer, initialState); 25 | 26 | const { filter } = useParams(); 27 | 28 | useEffect(() => { 29 | getAllStatus().then(status => { 30 | dispatch({ type: 'ALL_STATUS_FETCHED', payload: status }); 31 | }); 32 | getAll(filter).then(orders => { 33 | dispatch({ type: 'ORDERS_FETCHED', payload: orders }); 34 | }); 35 | }, [filter]); 36 | 37 | return ( 38 | <div className={classes.container}> 39 | <Title title="Orders" margin="1.5rem 0 0 .2rem" fontSize="1.9rem" /> 40 | 41 | {allStatus && ( 42 | <div className={classes.all_status}> 43 | <Link to="/orders" className={!filter ? classes.selected : ''}> 44 | All 45 | </Link> 46 | {allStatus.map(state => ( 47 | <Link 48 | key={state} 49 | className={state == filter ? classes.selected : ''} 50 | to={`/orders/${state}`} 51 | > 52 | {state} 53 | </Link> 54 | ))} 55 | </div> 56 | )} 57 | 58 | {orders?.length === 0 && ( 59 | <NotFound 60 | linkRoute={filter ? '/orders' : '/'} 61 | linkText={filter ? 'Show All' : 'Go To Home Page'} 62 | /> 63 | )} 64 | 65 | {orders && 66 | orders.map(order => ( 67 | <div key={order.id} className={classes.order_summary}> 68 | <div className={classes.header}> 69 | <span>{order.id}</span> 70 | <span> 71 | <DateTime date={order.createdAt} /> 72 | </span> 73 | <span>{order.status}</span> 74 | </div> 75 | <div className={classes.items}> 76 | {order.items.map(item => ( 77 | <Link key={item.food.id} to={`/food/${item.food.id}`}> 78 | <img src={item.food.imageUrl} alt={item.food.name} /> 79 | </Link> 80 | ))} 81 | </div> 82 | <div className={classes.footer}> 83 | <div> 84 | <Link to={`/track/${order.id}`}>Show Order</Link> 85 | </div> 86 | <div> 87 | <span className={classes.price}> 88 | <Price price={order.totalPrice} /> 89 | </span> 90 | </div> 91 | </div> 92 | </div> 93 | ))} 94 | </div> 95 | ); 96 | } 97 | -------------------------------------------------------------------------------- /frontend/src/pages/Orders/ordersPage.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | flex-direction: column; 6 | } 7 | 8 | .all_status, 9 | .order_summary { 10 | max-width: 45rem; 11 | width: 100%; 12 | } 13 | 14 | .all_status { 15 | display: flex; 16 | justify-content: space-around; 17 | align-items: center; 18 | height: 3rem; 19 | border: 1px solid whitesmoke; 20 | font-size: 1.1rem; 21 | margin-top: 1rem; 22 | border-radius: 1rem; 23 | } 24 | 25 | .all_status a { 26 | color: grey; 27 | width: 100%; 28 | height: 100%; 29 | border-radius: 1rem; 30 | display: flex; 31 | align-items: center; 32 | justify-content: center; 33 | } 34 | 35 | .all_status a.selected, 36 | .all_status a:hover { 37 | color: white; 38 | background-color: black; 39 | opacity: 0.8; 40 | } 41 | 42 | .order_summary { 43 | display: flex; 44 | flex-direction: column; 45 | border: 1px solid #d4d4d4; 46 | border-radius: 1rem; 47 | margin-top: 2rem; 48 | } 49 | 50 | .order_summary > * { 51 | padding: 0.8rem; 52 | } 53 | 54 | .header { 55 | display: flex; 56 | justify-content: space-between; 57 | border-bottom: 1px solid whitesmoke; 58 | border-radius: 1rem 1rem 0 0; 59 | color: grey; 60 | } 61 | 62 | .items { 63 | display: flex; 64 | flex-wrap: nowrap; 65 | overflow-x: auto; 66 | margin: 0.5rem 0; 67 | } 68 | 69 | .items img { 70 | width: 5rem; 71 | height: 5rem; 72 | border-radius: 2rem; 73 | margin-right: 1rem; 74 | object-fit: cover; 75 | } 76 | 77 | .items img:hover { 78 | opacity: 0.8; 79 | } 80 | 81 | .footer { 82 | display: flex; 83 | justify-content: space-between; 84 | border-top: 1px solid whitesmoke; 85 | border-radius: 0 0 1rem 1rem; 86 | } 87 | .price { 88 | color: green; 89 | } 90 | -------------------------------------------------------------------------------- /frontend/src/pages/Payment/PaymentPage.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import classes from './paymentPage.module.css'; 3 | import { getNewOrderForCurrentUser } from '../../services/orderService'; 4 | import Title from '../../components/Title/Title'; 5 | import OrderItemsList from '../../components/OrderItemsList/OrderItemsList'; 6 | import Map from '../../components/Map/Map'; 7 | import PaypalButtons from '../../components/PaypalButtons/PaypalButtons'; 8 | 9 | export default function PaymentPage() { 10 | const [order, setOrder] = useState(); 11 | 12 | useEffect(() => { 13 | getNewOrderForCurrentUser().then(data => setOrder(data)); 14 | }, []); 15 | 16 | if (!order) return; 17 | 18 | return ( 19 | <> 20 | <div className={classes.container}> 21 | <div className={classes.content}> 22 | <Title title="Order Form" fontSize="1.6rem" /> 23 | <div className={classes.summary}> 24 | <div> 25 | <h3>Name:</h3> 26 | <span>{order.name}</span> 27 | </div> 28 | <div> 29 | <h3>Address:</h3> 30 | <span>{order.address}</span> 31 | </div> 32 | </div> 33 | <OrderItemsList order={order} /> 34 | </div> 35 | 36 | <div className={classes.map}> 37 | <Title title="Your Location" fontSize="1.6rem" /> 38 | <Map readonly={true} location={order.addressLatLng} /> 39 | </div> 40 | 41 | <div className={classes.buttons_container}> 42 | <div className={classes.buttons}> 43 | <PaypalButtons order={order} /> 44 | </div> 45 | </div> 46 | </div> 47 | </> 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /frontend/src/pages/Payment/paymentPage.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | margin: 0 2rem 6rem 2rem; 4 | flex-wrap: wrap; 5 | justify-content: center; 6 | } 7 | 8 | .content { 9 | width: 35rem; 10 | margin: 0 3rem 0 0; 11 | display: flex; 12 | flex-direction: column; 13 | } 14 | 15 | .summary > div { 16 | display: flex; 17 | margin-top: 0.7rem; 18 | } 19 | 20 | .summary span { 21 | font-size: 1.2rem; 22 | } 23 | .summary h3 { 24 | margin: 0 1rem 0 0; 25 | flex-basis: 5rem; 26 | } 27 | 28 | .buttons_container { 29 | flex-basis: 100%; 30 | display: flex; 31 | justify-content: center; 32 | } 33 | 34 | .buttons { 35 | margin: 2rem; 36 | width: 35rem; 37 | } 38 | -------------------------------------------------------------------------------- /frontend/src/pages/Profile/ProfilePage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useForm } from 'react-hook-form'; 3 | import { useAuth } from '../../hooks/useAuth'; 4 | import classes from './profilePage.module.css'; 5 | import Title from '../../components/Title/Title'; 6 | import Input from '../../components/Input/Input'; 7 | import Button from '../../components/Button/Button'; 8 | import ChangePassword from '../../components/ChangePassword/ChangePassword'; 9 | 10 | export default function ProfilePage() { 11 | const { 12 | handleSubmit, 13 | register, 14 | formState: { errors }, 15 | } = useForm(); 16 | 17 | const { user, updateProfile } = useAuth(); 18 | 19 | const submit = user => { 20 | updateProfile(user); 21 | }; 22 | 23 | return ( 24 | <div className={classes.container}> 25 | <div className={classes.details}> 26 | <Title title="Update Profile" /> 27 | <form onSubmit={handleSubmit(submit)}> 28 | <Input 29 | defaultValue={user.name} 30 | type="text" 31 | label="Name" 32 | {...register('name', { 33 | required: true, 34 | minLength: 5, 35 | })} 36 | error={errors.name} 37 | /> 38 | <Input 39 | defaultValue={user.address} 40 | type="text" 41 | label="Address" 42 | {...register('address', { 43 | required: true, 44 | minLength: 10, 45 | })} 46 | error={errors.address} 47 | /> 48 | 49 | <Button type="submit" text="Update" backgroundColor="#009e84" /> 50 | </form> 51 | 52 | <ChangePassword /> 53 | </div> 54 | </div> 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /frontend/src/pages/Profile/profilePage.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | height: 100%; 6 | margin-top: 3rem; 7 | } 8 | 9 | .details { 10 | width: 28rem; 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/pages/Register/RegisterPage.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useForm } from 'react-hook-form'; 3 | import Input from '../../components/Input/Input'; 4 | import Title from '../../components/Title/Title'; 5 | import classes from './registerPage.module.css'; 6 | import Button from '../../components/Button/Button'; 7 | import { Link } from 'react-router-dom'; 8 | import { useSearchParams, useNavigate } from 'react-router-dom'; 9 | import { useAuth } from '../../hooks/useAuth'; 10 | import { EMAIL } from '../../constants/patterns'; 11 | 12 | export default function RegisterPage() { 13 | const auth = useAuth(); 14 | const { user } = auth; 15 | const navigate = useNavigate(); 16 | const [params] = useSearchParams(); 17 | const returnUrl = params.get('returnUrl'); 18 | 19 | useEffect(() => { 20 | if (!user) return; 21 | returnUrl ? navigate(returnUrl) : navigate('/'); 22 | }, [user]); 23 | 24 | const { 25 | handleSubmit, 26 | register, 27 | getValues, 28 | formState: { errors }, 29 | } = useForm(); 30 | 31 | const submit = async data => { 32 | await auth.register(data); 33 | }; 34 | 35 | return ( 36 | <div className={classes.container}> 37 | <div className={classes.details}> 38 | <Title title="Register" /> 39 | <form onSubmit={handleSubmit(submit)} noValidate> 40 | <Input 41 | type="text" 42 | label="Name" 43 | {...register('name', { 44 | required: true, 45 | minLength: 5, 46 | })} 47 | error={errors.name} 48 | /> 49 | 50 | <Input 51 | type="email" 52 | label="Email" 53 | {...register('email', { 54 | required: true, 55 | pattern: EMAIL, 56 | })} 57 | error={errors.email} 58 | /> 59 | 60 | <Input 61 | type="password" 62 | label="Password" 63 | {...register('password', { 64 | required: true, 65 | minLength: 5, 66 | })} 67 | error={errors.password} 68 | /> 69 | 70 | <Input 71 | type="password" 72 | label="Confirm Password" 73 | {...register('confirmPassword', { 74 | required: true, 75 | validate: value => 76 | value !== getValues('password') 77 | ? 'Passwords Do No Match' 78 | : true, 79 | })} 80 | error={errors.confirmPassword} 81 | /> 82 | 83 | <Input 84 | type="text" 85 | label="Address" 86 | {...register('address', { 87 | required: true, 88 | minLength: 10, 89 | })} 90 | error={errors.address} 91 | /> 92 | 93 | <Button type="submit" text="Register" /> 94 | 95 | <div className={classes.login}> 96 | Already a user?   97 | <Link to={`/login${returnUrl ? '?returnUrl=' + returnUrl : ''}`}> 98 | Login here 99 | </Link> 100 | </div> 101 | </form> 102 | </div> 103 | </div> 104 | ); 105 | } 106 | -------------------------------------------------------------------------------- /frontend/src/pages/Register/registerPage.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | height: 100%; 6 | margin-top: 3rem; 7 | } 8 | 9 | .details { 10 | width: 25rem; 11 | } 12 | 13 | .details form { 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: center; 17 | } 18 | 19 | .login { 20 | display: flex; 21 | justify-content: center; 22 | align-items: center; 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/pages/UserEdit/UserEditPage.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useForm } from 'react-hook-form'; 3 | import { getById, register, updateUser } from '../../services/userService'; 4 | import { useParams } from 'react-router-dom'; 5 | import classes from './userEdit.module.css'; 6 | import Title from '../../components/Title/Title'; 7 | import Input from '../../components/Input/Input'; 8 | import { EMAIL } from '../../constants/patterns'; 9 | import Button from '../../components/Button/Button'; 10 | 11 | export default function UserEditPage() { 12 | const { 13 | register, 14 | reset, 15 | handleSubmit, 16 | formState: { errors }, 17 | } = useForm(); 18 | 19 | const { userId } = useParams(); 20 | const isEditMode = userId; 21 | 22 | useEffect(() => { 23 | if (isEditMode) loadUser(); 24 | }, [userId]); 25 | 26 | const loadUser = async () => { 27 | const user = await getById(userId); 28 | reset(user); 29 | }; 30 | 31 | const submit = userData => { 32 | updateUser(userData); 33 | }; 34 | 35 | return ( 36 | <div className={classes.container}> 37 | <div className={classes.content}> 38 | <Title title={isEditMode ? 'Edit User' : 'Add User'} /> 39 | <form onSubmit={handleSubmit(submit)} noValidate> 40 | <Input 41 | label="Name" 42 | {...register('name', { required: true, minLength: 3 })} 43 | error={errors.name} 44 | /> 45 | <Input 46 | label="Email" 47 | {...register('email', { required: true, pattern: EMAIL })} 48 | error={errors.email} 49 | /> 50 | <Input 51 | label="Address" 52 | {...register('address', { required: true, minLength: 5 })} 53 | error={errors.address} 54 | /> 55 | 56 | <Input label="Is Admin" type="checkbox" {...register('isAdmin')} /> 57 | <Button type="submit" /> 58 | </form> 59 | </div> 60 | </div> 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /frontend/src/pages/UserEdit/userEdit.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | margin-top: 1rem; 3 | margin-bottom: 10rem; 4 | display: flex; 5 | justify-content: center; 6 | } 7 | 8 | .content { 9 | margin: 1rem; 10 | max-width: 25rem; 11 | width: 100%; 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/pages/UsersPage/UsersPage.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Link, useParams } from 'react-router-dom'; 3 | import { useAuth } from '../../hooks/useAuth'; 4 | import { getAll, toggleBlock } from '../../services/userService'; 5 | import classes from './usersPage.module.css'; 6 | import Title from '../../components/Title/Title'; 7 | import Search from '../../components/Search/Search'; 8 | 9 | export default function UsersPage() { 10 | const [users, setUsers] = useState(); 11 | const { searchTerm } = useParams(); 12 | const auth = useAuth(); 13 | 14 | useEffect(() => { 15 | loadUsers(); 16 | }, [searchTerm]); 17 | 18 | const loadUsers = async () => { 19 | const users = await getAll(searchTerm); 20 | setUsers(users); 21 | }; 22 | 23 | const handleToggleBlock = async userId => { 24 | const isBlocked = await toggleBlock(userId); 25 | 26 | setUsers(oldUsers => 27 | oldUsers.map(user => (user.id === userId ? { ...user, isBlocked } : user)) 28 | ); 29 | }; 30 | 31 | return ( 32 | <div className={classes.container}> 33 | <div className={classes.list}> 34 | <Title title="Manage Users" /> 35 | <Search 36 | searchRoute="/admin/users/" 37 | defaultRoute="/admin/users" 38 | placeholder="Search Users" 39 | margin="1rem 0" 40 | /> 41 | <div className={classes.list_item}> 42 | <h3>Name</h3> 43 | <h3>Email</h3> 44 | <h3>Address</h3> 45 | <h3>Admin</h3> 46 | <h3>Actions</h3> 47 | </div> 48 | {users && 49 | users.map(user => ( 50 | <div key={user.id} className={classes.list_item}> 51 | <span>{user.name}</span> 52 | <span>{user.email}</span> 53 | <span>{user.address}</span> 54 | <span>{user.isAdmin ? '✅' : '❌'}</span> 55 | <span className={classes.actions}> 56 | <Link to={'/admin/editUser/' + user.id}>Edit</Link> 57 | {auth.user.id !== user.id && ( 58 | <Link onClick={() => handleToggleBlock(user.id)}> 59 | {user.isBlocked ? 'Unblock' : 'Block'} 60 | </Link> 61 | )} 62 | </span> 63 | </div> 64 | ))} 65 | </div> 66 | </div> 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /frontend/src/pages/UsersPage/usersPage.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | justify-content: center; 4 | margin-top: 2rem; 5 | font-size: 1.2rem; 6 | } 7 | 8 | .list { 9 | width: 100%; 10 | max-width: 50rem; 11 | } 12 | 13 | .list_item { 14 | display: grid; 15 | grid-template-columns: 20% 35% 20% 10% 15%; 16 | gap: 1%; 17 | align-items: center; 18 | border-bottom: 1px solid whitesmoke; 19 | min-height: 3rem; 20 | } 21 | 22 | .list_item > * { 23 | word-wrap: break-word; 24 | } 25 | 26 | h3 { 27 | margin-left: 0; 28 | } 29 | 30 | .actions { 31 | display: flex; 32 | gap: 0.7rem; 33 | } 34 | -------------------------------------------------------------------------------- /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/services/foodService.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export const getAll = async () => { 4 | const { data } = await axios.get('/api/foods'); 5 | return data; 6 | }; 7 | 8 | export const search = async searchTerm => { 9 | const { data } = await axios.get('/api/foods/search/' + searchTerm); 10 | return data; 11 | }; 12 | 13 | export const getAllTags = async () => { 14 | const { data } = await axios.get('/api/foods/tags'); 15 | return data; 16 | }; 17 | 18 | export const getAllByTag = async tag => { 19 | if (tag === 'All') return getAll(); 20 | const { data } = await axios.get('/api/foods/tag/' + tag); 21 | return data; 22 | }; 23 | 24 | export const getById = async foodId => { 25 | const { data } = await axios.get('/api/foods/' + foodId); 26 | return data; 27 | }; 28 | 29 | export async function deleteById(foodId) { 30 | await axios.delete('/api/foods/' + foodId); 31 | } 32 | 33 | export async function update(food) { 34 | await axios.put('/api/foods', food); 35 | } 36 | 37 | export async function add(food) { 38 | const { data } = await axios.post('/api/foods', food); 39 | return data; 40 | } 41 | -------------------------------------------------------------------------------- /frontend/src/services/orderService.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export const createOrder = async order => { 4 | try { 5 | const { data } = axios.post('/api/orders/create', order); 6 | return data; 7 | } catch (error) {} 8 | }; 9 | 10 | export const getNewOrderForCurrentUser = async () => { 11 | const { data } = await axios.get('/api/orders/newOrderForCurrentUser'); 12 | return data; 13 | }; 14 | 15 | export const pay = async paymentId => { 16 | try { 17 | const { data } = await axios.put('/api/orders/pay', { paymentId }); 18 | return data; 19 | } catch (error) {} 20 | }; 21 | 22 | export const trackOrderById = async orderId => { 23 | const { data } = await axios.get('/api/orders/track/' + orderId); 24 | return data; 25 | }; 26 | 27 | export const getAll = async state => { 28 | const { data } = await axios.get(`/api/orders/${state ?? ''}`); 29 | return data; 30 | }; 31 | 32 | export const getAllStatus = async () => { 33 | const { data } = await axios.get(`/api/orders/allstatus`); 34 | return data; 35 | }; 36 | -------------------------------------------------------------------------------- /frontend/src/services/uploadService.js: -------------------------------------------------------------------------------- 1 | import { toast } from 'react-toastify'; 2 | import axios from 'axios'; 3 | 4 | export const uploadImage = async event => { 5 | let toastId = null; 6 | 7 | const image = await getImage(event); 8 | if (!image) return null; 9 | 10 | const formData = new FormData(); 11 | formData.append('image', image, image.name); 12 | const response = await axios.post('api/upload', formData, { 13 | onUploadProgress: ({ progress }) => { 14 | if (toastId) toast.update(toastId, { progress }); 15 | else toastId = toast.success('Uploading...', { progress }); 16 | }, 17 | }); 18 | toast.dismiss(toastId); 19 | return response.data.imageUrl; 20 | }; 21 | 22 | const getImage = async event => { 23 | const files = event.target.files; 24 | 25 | if (!files || files.length <= 0) { 26 | toast.warning('Upload file is nott selected!', 'File Upload'); 27 | return null; 28 | } 29 | 30 | const file = files[0]; 31 | 32 | if (file.type !== 'image/jpeg') { 33 | toast.error('Only JPG type is allowed', 'File Type Error'); 34 | return null; 35 | } 36 | 37 | return file; 38 | }; 39 | -------------------------------------------------------------------------------- /frontend/src/services/userService.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export const getUser = () => 4 | localStorage.getItem('user') 5 | ? JSON.parse(localStorage.getItem('user')) 6 | : null; 7 | 8 | export const login = async (email, password) => { 9 | const { data } = await axios.post('api/users/login', { email, password }); 10 | localStorage.setItem('user', JSON.stringify(data)); 11 | return data; 12 | }; 13 | 14 | export const register = async registerData => { 15 | const { data } = await axios.post('api/users/register', registerData); 16 | localStorage.setItem('user', JSON.stringify(data)); 17 | return data; 18 | }; 19 | 20 | export const logout = () => { 21 | localStorage.removeItem('user'); 22 | }; 23 | 24 | export const updateProfile = async user => { 25 | const { data } = await axios.put('/api/users/updateProfile', user); 26 | localStorage.setItem('user', JSON.stringify(data)); 27 | return data; 28 | }; 29 | 30 | export const changePassword = async passwords => { 31 | await axios.put('/api/users/changePassword', passwords); 32 | }; 33 | 34 | export const getAll = async searchTerm => { 35 | const { data } = await axios.get('/api/users/getAll/' + (searchTerm ?? '')); 36 | return data; 37 | }; 38 | 39 | export const toggleBlock = async userId => { 40 | const { data } = await axios.put('/api/users/toggleBlock/' + userId); 41 | return data; 42 | }; 43 | 44 | export const getById = async userId => { 45 | const { data } = await axios.get('/api/users/getById/' + userId); 46 | return data; 47 | }; 48 | 49 | export const updateUser = async userData => { 50 | const { data } = await axios.put('/api/users/update', userData); 51 | return data; 52 | }; 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foodmine-react", 3 | "version": "1.0.0", 4 | "description": "## 1. Demo And Installation", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "cd backend && npm start", 8 | "prebuild": "cd backend && npm install & cd frontend && npm install", 9 | "build": "cd frontend && npm run build", 10 | "postbuild": "mv -f frontend/build backend/src/public" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "engines": { 16 | "node": ">=18 <19" 17 | } 18 | } 19 | --------------------------------------------------------------------------------