├── .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 | Item |
50 | Unit Price |
51 | Quantity |
52 | Total Price |
53 |
54 |
55 |
56 | ${order.items
57 | .map(
58 | item =>
59 | `
60 |
61 | ${item.food.name} |
62 | $${item.food.price} |
63 | ${item.quantity} |
64 | $${item.price.toFixed(2)} |
65 |
66 | `
67 | )
68 | .join('\n')}
69 |
70 |
71 |
72 | Total: |
73 | $${order.totalPrice} |
74 |
75 |
76 |
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 |
57 |
--------------------------------------------------------------------------------
/frontend/public/icons/profile.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
50 |
--------------------------------------------------------------------------------
/frontend/public/icons/users.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
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 |
12 |
--------------------------------------------------------------------------------
/frontend/public/star-full.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/frontend/public/star-half.svg:
--------------------------------------------------------------------------------
1 |
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 |
59 |
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 |
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 |
24 |
34 | {error && {getErrorMessage()}
}
35 |
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 |
6 |
7 |
{children}
8 |
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 |
11 |
12 |

13 |
Loading...
14 |
15 |
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 |
17 |
29 |
30 |
35 |
36 |
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 |
81 | )}
82 |
83 | {position && (
84 | {
87 | setPosition(e.target.getLatLng());
88 | },
89 | }}
90 | position={position}
91 | draggable={!readonly}
92 | icon={markerIcon}
93 | >
94 | Shipping Location
95 |
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 |
7 | {message}
8 | {linkText}
9 |
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 |
9 |
10 |
11 |
12 | Order Items:
13 | |
14 |
15 | {order.items.map(item => (
16 |
17 |
18 |
19 |
20 |
21 | |
22 | {item.food.name} |
23 |
24 |
25 | |
26 | {item.quantity} |
27 |
28 |
29 | |
30 |
31 | ))}
32 |
33 |
34 | |
35 |
36 | Total :
37 | |
38 |
39 |
40 | |
41 |
42 |
43 |
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 |
21 |
22 |
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 |
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 {formatPrice()};
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 |
30 | setTerm(e.target.value)}
34 | onKeyUp={e => e.key === 'Enter' && search()}
35 | value={term}
36 | />
37 |
38 |
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 |
15 | ) : stars >= halfNumber ? (
16 |
17 | ) : (
18 |
19 | );
20 | }
21 |
22 | return (
23 |
24 | {[1, 2, 3, 4, 5].map(number => (
25 |
26 | ))}
27 |
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 |
13 | {tags.map(tag => (
14 |
15 | {tag.name}
16 | {!forFoodPage && `(${tag.count})`}
17 |
18 | ))}
19 |
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 |
9 | {foods.map(food => (
10 | -
11 |
12 |
17 |
18 |
19 |
{food.name}
20 |
25 | ❤
26 |
27 |
28 |
29 |
30 |
31 |
32 | {food.origins.map(origin => (
33 | {origin}
34 | ))}
35 |
36 |
37 | 🕒
38 | {food.cookTime}
39 |
40 |
41 |
44 |
45 |
46 |
47 | ))}
48 |
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 {title}
;
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 |
52 | {children}
53 |
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 |
88 | {children}
89 |
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 |
13 | {children}
14 |
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 |
18 |
19 |
20 |
21 |
22 |
23 |
35 |
36 |
37 |
38 |
39 |
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 |
13 |
14 | {cart.items.length === 0 ? (
15 |
16 | ) : (
17 |
18 |
19 | {cart.items.map(item => (
20 | -
21 |
22 |

23 |
24 |
25 | {item.food.name}
26 |
27 |
28 |
29 |
44 |
45 |
46 |
49 |
50 |
51 |
57 |
58 |
59 | ))}
60 |
61 |
62 |
63 |
64 |
{cart.totalCount}
65 |
68 |
69 |
70 |
Proceed To Checkout
71 |
72 |
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 |
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 |
11 |
12 | {allItems
13 | .filter(item => user.isAdmin || !item.forAdmin)
14 | .map(item => (
15 |
23 |

24 |
{item.title}
25 |
26 | ))}
27 |
28 |
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 |
28 | ) : (
29 |
30 |

35 |
36 |
37 |
38 | {food.name}
39 |
44 | ❤
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | {food.origins?.map(origin => (
53 | {origin}
54 | ))}
55 |
56 |
57 |
58 | {food.tags && (
59 | ({ name: tag }))}
61 | forFoodPage={true}
62 | />
63 | )}
64 |
65 |
66 |
67 |
68 | Time to cook about {food.cookTime} minutes
69 |
70 |
71 |
72 |
75 |
76 |
77 |
78 |
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 |
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 |
29 | ) : (
30 |
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 |
45 |
46 |
47 |
53 |
54 | Add Food +
55 |
56 |
57 | {foods &&
58 | foods.map(food => (
59 |
60 |

61 |
{food.name}
62 |
63 |
64 | Edit
65 | deleteFood(food)}>Delete
66 |
67 |
68 | ))}
69 |
70 |
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 |
47 |
48 | {foods.length === 0 && }
49 |
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 |
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 ;
24 |
25 | return (
26 | order && (
27 |
28 |
29 |
Order #{order.id}
30 |
31 |
32 | Date
33 |
34 |
35 |
36 | Name
37 | {order.name}
38 |
39 |
40 | Address
41 | {order.address}
42 |
43 |
44 | State
45 | {order.status}
46 |
47 | {order.paymentId && (
48 |
49 | Payment ID
50 | {order.paymentId}
51 |
52 | )}
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | {order.status === 'NEW' && (
64 |
65 | Go To Payment
66 |
67 | )}
68 |
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 |
39 |
40 |
41 | {allStatus && (
42 |
43 |
44 | All
45 |
46 | {allStatus.map(state => (
47 |
52 | {state}
53 |
54 | ))}
55 |
56 | )}
57 |
58 | {orders?.length === 0 && (
59 |
63 | )}
64 |
65 | {orders &&
66 | orders.map(order => (
67 |
68 |
69 | {order.id}
70 |
71 |
72 |
73 | {order.status}
74 |
75 |
76 | {order.items.map(item => (
77 |
78 |

79 |
80 | ))}
81 |
82 |
83 |
84 | Show Order
85 |
86 |
91 |
92 |
93 | ))}
94 |
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 |
21 |
22 |
23 |
24 |
25 |
Name:
26 | {order.name}
27 |
28 |
29 |
Address:
30 | {order.address}
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
46 |
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 |
25 |
26 |
27 |
51 |
52 |
53 |
54 |
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 |
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 |
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 |
33 |
34 |
35 |
41 |
42 |
Name
43 | Email
44 | Address
45 | Admin
46 | Actions
47 |
48 | {users &&
49 | users.map(user => (
50 |
51 | {user.name}
52 | {user.email}
53 | {user.address}
54 | {user.isAdmin ? '✅' : '❌'}
55 |
56 | Edit
57 | {auth.user.id !== user.id && (
58 | handleToggleBlock(user.id)}>
59 | {user.isBlocked ? 'Unblock' : 'Block'}
60 |
61 | )}
62 |
63 |
64 | ))}
65 |
66 |
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 |
--------------------------------------------------------------------------------