Spaces:
Sleeping
Sleeping
Feature: frontend add update user
Browse files- frontend/.gitignore +4 -0
- frontend/package-lock.json +153 -0
- frontend/package.json +4 -0
- frontend/public/default_avatar.jpg +0 -0
- frontend/src/index.js +12 -0
- frontend/src/molecules/AdminNavBar.js +0 -0
- frontend/src/molecules/Navbar.js +14 -12
- frontend/src/organisms/DataStorage.js +91 -0
- frontend/src/organisms/MenuSection.js +4 -3
- frontend/src/organisms/jwtDecoder.js +22 -0
- frontend/src/pages/AdminHomePage.js +0 -0
- frontend/src/pages/CartPage.js +72 -0
- frontend/src/pages/LoginPage.js +39 -7
- frontend/src/pages/MenuPage.js +99 -18
- frontend/src/pages/RegisterPage.js +1 -1
- frontend/src/pages/UserInfoPage.js +257 -0
frontend/.gitignore
CHANGED
@@ -21,3 +21,7 @@
|
|
21 |
npm-debug.log*
|
22 |
yarn-debug.log*
|
23 |
yarn-error.log*
|
|
|
|
|
|
|
|
|
|
21 |
npm-debug.log*
|
22 |
yarn-debug.log*
|
23 |
yarn-error.log*
|
24 |
+
|
25 |
+
.env
|
26 |
+
note_dev.txt
|
27 |
+
object.json
|
frontend/package-lock.json
CHANGED
@@ -11,10 +11,14 @@
|
|
11 |
"@testing-library/jest-dom": "^5.17.0",
|
12 |
"@testing-library/react": "^13.4.0",
|
13 |
"@testing-library/user-event": "^13.5.0",
|
|
|
14 |
"bootstrap": "^5.3.3",
|
|
|
|
|
15 |
"react": "^18.3.1",
|
16 |
"react-bootstrap": "^2.10.5",
|
17 |
"react-dom": "^18.3.1",
|
|
|
18 |
"react-router-dom": "^6.27.0",
|
19 |
"react-scripts": "5.0.1",
|
20 |
"validator": "^13.12.0",
|
@@ -5870,6 +5874,31 @@
|
|
5870 |
"node": ">=4"
|
5871 |
}
|
5872 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5873 |
"node_modules/axobject-query": {
|
5874 |
"version": "4.1.0",
|
5875 |
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
@@ -6402,6 +6431,12 @@
|
|
6402 |
"node-int64": "^0.4.0"
|
6403 |
}
|
6404 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
6405 |
"node_modules/buffer-from": {
|
6406 |
"version": "1.1.2",
|
6407 |
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
@@ -7812,6 +7847,15 @@
|
|
7812 |
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
7813 |
"license": "MIT"
|
7814 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7815 |
"node_modules/ee-first": {
|
7816 |
"version": "1.1.1",
|
7817 |
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
@@ -13187,6 +13231,15 @@
|
|
13187 |
"jiti": "bin/jiti.js"
|
13188 |
}
|
13189 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
13190 |
"node_modules/js-tokens": {
|
13191 |
"version": "4.0.0",
|
13192 |
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
@@ -13350,6 +13403,28 @@
|
|
13350 |
"node": ">=0.10.0"
|
13351 |
}
|
13352 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
13353 |
"node_modules/jsx-ast-utils": {
|
13354 |
"version": "3.3.5",
|
13355 |
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
|
@@ -13365,6 +13440,27 @@
|
|
13365 |
"node": ">=4.0"
|
13366 |
}
|
13367 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
13368 |
"node_modules/keyv": {
|
13369 |
"version": "4.5.4",
|
13370 |
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
@@ -13513,6 +13609,42 @@
|
|
13513 |
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
|
13514 |
"license": "MIT"
|
13515 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
13516 |
"node_modules/lodash.memoize": {
|
13517 |
"version": "4.1.2",
|
13518 |
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
@@ -13525,6 +13657,12 @@
|
|
13525 |
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
|
13526 |
"license": "MIT"
|
13527 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
13528 |
"node_modules/lodash.sortby": {
|
13529 |
"version": "4.7.0",
|
13530 |
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
|
@@ -15955,6 +16093,12 @@
|
|
15955 |
"node": ">= 0.10"
|
15956 |
}
|
15957 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
15958 |
"node_modules/psl": {
|
15959 |
"version": "1.9.0",
|
15960 |
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
|
@@ -16356,6 +16500,15 @@
|
|
16356 |
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==",
|
16357 |
"license": "MIT"
|
16358 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
16359 |
"node_modules/react-is": {
|
16360 |
"version": "17.0.2",
|
16361 |
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
|
|
11 |
"@testing-library/jest-dom": "^5.17.0",
|
12 |
"@testing-library/react": "^13.4.0",
|
13 |
"@testing-library/user-event": "^13.5.0",
|
14 |
+
"axios": "^1.7.7",
|
15 |
"bootstrap": "^5.3.3",
|
16 |
+
"js-cookie": "^3.0.5",
|
17 |
+
"jsonwebtoken": "^9.0.2",
|
18 |
"react": "^18.3.1",
|
19 |
"react-bootstrap": "^2.10.5",
|
20 |
"react-dom": "^18.3.1",
|
21 |
+
"react-image-crop": "^11.0.7",
|
22 |
"react-router-dom": "^6.27.0",
|
23 |
"react-scripts": "5.0.1",
|
24 |
"validator": "^13.12.0",
|
|
|
5874 |
"node": ">=4"
|
5875 |
}
|
5876 |
},
|
5877 |
+
"node_modules/axios": {
|
5878 |
+
"version": "1.7.7",
|
5879 |
+
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
|
5880 |
+
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
|
5881 |
+
"license": "MIT",
|
5882 |
+
"dependencies": {
|
5883 |
+
"follow-redirects": "^1.15.6",
|
5884 |
+
"form-data": "^4.0.0",
|
5885 |
+
"proxy-from-env": "^1.1.0"
|
5886 |
+
}
|
5887 |
+
},
|
5888 |
+
"node_modules/axios/node_modules/form-data": {
|
5889 |
+
"version": "4.0.1",
|
5890 |
+
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
|
5891 |
+
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
|
5892 |
+
"license": "MIT",
|
5893 |
+
"dependencies": {
|
5894 |
+
"asynckit": "^0.4.0",
|
5895 |
+
"combined-stream": "^1.0.8",
|
5896 |
+
"mime-types": "^2.1.12"
|
5897 |
+
},
|
5898 |
+
"engines": {
|
5899 |
+
"node": ">= 6"
|
5900 |
+
}
|
5901 |
+
},
|
5902 |
"node_modules/axobject-query": {
|
5903 |
"version": "4.1.0",
|
5904 |
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
|
|
6431 |
"node-int64": "^0.4.0"
|
6432 |
}
|
6433 |
},
|
6434 |
+
"node_modules/buffer-equal-constant-time": {
|
6435 |
+
"version": "1.0.1",
|
6436 |
+
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
6437 |
+
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
6438 |
+
"license": "BSD-3-Clause"
|
6439 |
+
},
|
6440 |
"node_modules/buffer-from": {
|
6441 |
"version": "1.1.2",
|
6442 |
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
|
|
7847 |
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
7848 |
"license": "MIT"
|
7849 |
},
|
7850 |
+
"node_modules/ecdsa-sig-formatter": {
|
7851 |
+
"version": "1.0.11",
|
7852 |
+
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
7853 |
+
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
7854 |
+
"license": "Apache-2.0",
|
7855 |
+
"dependencies": {
|
7856 |
+
"safe-buffer": "^5.0.1"
|
7857 |
+
}
|
7858 |
+
},
|
7859 |
"node_modules/ee-first": {
|
7860 |
"version": "1.1.1",
|
7861 |
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
|
|
13231 |
"jiti": "bin/jiti.js"
|
13232 |
}
|
13233 |
},
|
13234 |
+
"node_modules/js-cookie": {
|
13235 |
+
"version": "3.0.5",
|
13236 |
+
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
|
13237 |
+
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
|
13238 |
+
"license": "MIT",
|
13239 |
+
"engines": {
|
13240 |
+
"node": ">=14"
|
13241 |
+
}
|
13242 |
+
},
|
13243 |
"node_modules/js-tokens": {
|
13244 |
"version": "4.0.0",
|
13245 |
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
|
|
13403 |
"node": ">=0.10.0"
|
13404 |
}
|
13405 |
},
|
13406 |
+
"node_modules/jsonwebtoken": {
|
13407 |
+
"version": "9.0.2",
|
13408 |
+
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
|
13409 |
+
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
|
13410 |
+
"license": "MIT",
|
13411 |
+
"dependencies": {
|
13412 |
+
"jws": "^3.2.2",
|
13413 |
+
"lodash.includes": "^4.3.0",
|
13414 |
+
"lodash.isboolean": "^3.0.3",
|
13415 |
+
"lodash.isinteger": "^4.0.4",
|
13416 |
+
"lodash.isnumber": "^3.0.3",
|
13417 |
+
"lodash.isplainobject": "^4.0.6",
|
13418 |
+
"lodash.isstring": "^4.0.1",
|
13419 |
+
"lodash.once": "^4.0.0",
|
13420 |
+
"ms": "^2.1.1",
|
13421 |
+
"semver": "^7.5.4"
|
13422 |
+
},
|
13423 |
+
"engines": {
|
13424 |
+
"node": ">=12",
|
13425 |
+
"npm": ">=6"
|
13426 |
+
}
|
13427 |
+
},
|
13428 |
"node_modules/jsx-ast-utils": {
|
13429 |
"version": "3.3.5",
|
13430 |
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
|
|
|
13440 |
"node": ">=4.0"
|
13441 |
}
|
13442 |
},
|
13443 |
+
"node_modules/jwa": {
|
13444 |
+
"version": "1.4.1",
|
13445 |
+
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
|
13446 |
+
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
|
13447 |
+
"license": "MIT",
|
13448 |
+
"dependencies": {
|
13449 |
+
"buffer-equal-constant-time": "1.0.1",
|
13450 |
+
"ecdsa-sig-formatter": "1.0.11",
|
13451 |
+
"safe-buffer": "^5.0.1"
|
13452 |
+
}
|
13453 |
+
},
|
13454 |
+
"node_modules/jws": {
|
13455 |
+
"version": "3.2.2",
|
13456 |
+
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
|
13457 |
+
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
|
13458 |
+
"license": "MIT",
|
13459 |
+
"dependencies": {
|
13460 |
+
"jwa": "^1.4.1",
|
13461 |
+
"safe-buffer": "^5.0.1"
|
13462 |
+
}
|
13463 |
+
},
|
13464 |
"node_modules/keyv": {
|
13465 |
"version": "4.5.4",
|
13466 |
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
|
|
13609 |
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
|
13610 |
"license": "MIT"
|
13611 |
},
|
13612 |
+
"node_modules/lodash.includes": {
|
13613 |
+
"version": "4.3.0",
|
13614 |
+
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
13615 |
+
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
|
13616 |
+
"license": "MIT"
|
13617 |
+
},
|
13618 |
+
"node_modules/lodash.isboolean": {
|
13619 |
+
"version": "3.0.3",
|
13620 |
+
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
13621 |
+
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
13622 |
+
"license": "MIT"
|
13623 |
+
},
|
13624 |
+
"node_modules/lodash.isinteger": {
|
13625 |
+
"version": "4.0.4",
|
13626 |
+
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
13627 |
+
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
|
13628 |
+
"license": "MIT"
|
13629 |
+
},
|
13630 |
+
"node_modules/lodash.isnumber": {
|
13631 |
+
"version": "3.0.3",
|
13632 |
+
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
13633 |
+
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
|
13634 |
+
"license": "MIT"
|
13635 |
+
},
|
13636 |
+
"node_modules/lodash.isplainobject": {
|
13637 |
+
"version": "4.0.6",
|
13638 |
+
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
13639 |
+
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
13640 |
+
"license": "MIT"
|
13641 |
+
},
|
13642 |
+
"node_modules/lodash.isstring": {
|
13643 |
+
"version": "4.0.1",
|
13644 |
+
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
13645 |
+
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
|
13646 |
+
"license": "MIT"
|
13647 |
+
},
|
13648 |
"node_modules/lodash.memoize": {
|
13649 |
"version": "4.1.2",
|
13650 |
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
|
|
13657 |
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
|
13658 |
"license": "MIT"
|
13659 |
},
|
13660 |
+
"node_modules/lodash.once": {
|
13661 |
+
"version": "4.1.1",
|
13662 |
+
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
13663 |
+
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
13664 |
+
"license": "MIT"
|
13665 |
+
},
|
13666 |
"node_modules/lodash.sortby": {
|
13667 |
"version": "4.7.0",
|
13668 |
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
|
|
|
16093 |
"node": ">= 0.10"
|
16094 |
}
|
16095 |
},
|
16096 |
+
"node_modules/proxy-from-env": {
|
16097 |
+
"version": "1.1.0",
|
16098 |
+
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
16099 |
+
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
16100 |
+
"license": "MIT"
|
16101 |
+
},
|
16102 |
"node_modules/psl": {
|
16103 |
"version": "1.9.0",
|
16104 |
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
|
|
|
16500 |
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==",
|
16501 |
"license": "MIT"
|
16502 |
},
|
16503 |
+
"node_modules/react-image-crop": {
|
16504 |
+
"version": "11.0.7",
|
16505 |
+
"resolved": "https://registry.npmjs.org/react-image-crop/-/react-image-crop-11.0.7.tgz",
|
16506 |
+
"integrity": "sha512-ZciKWHDYzmm366JDL18CbrVyjnjH0ojufGDmScfS4ZUqLHg4nm6ATY+K62C75W4ZRNt4Ii+tX0bSjNk9LQ2xzQ==",
|
16507 |
+
"license": "ISC",
|
16508 |
+
"peerDependencies": {
|
16509 |
+
"react": ">=16.13.1"
|
16510 |
+
}
|
16511 |
+
},
|
16512 |
"node_modules/react-is": {
|
16513 |
"version": "17.0.2",
|
16514 |
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
frontend/package.json
CHANGED
@@ -6,10 +6,14 @@
|
|
6 |
"@testing-library/jest-dom": "^5.17.0",
|
7 |
"@testing-library/react": "^13.4.0",
|
8 |
"@testing-library/user-event": "^13.5.0",
|
|
|
9 |
"bootstrap": "^5.3.3",
|
|
|
|
|
10 |
"react": "^18.3.1",
|
11 |
"react-bootstrap": "^2.10.5",
|
12 |
"react-dom": "^18.3.1",
|
|
|
13 |
"react-router-dom": "^6.27.0",
|
14 |
"react-scripts": "5.0.1",
|
15 |
"validator": "^13.12.0",
|
|
|
6 |
"@testing-library/jest-dom": "^5.17.0",
|
7 |
"@testing-library/react": "^13.4.0",
|
8 |
"@testing-library/user-event": "^13.5.0",
|
9 |
+
"axios": "^1.7.7",
|
10 |
"bootstrap": "^5.3.3",
|
11 |
+
"js-cookie": "^3.0.5",
|
12 |
+
"jsonwebtoken": "^9.0.2",
|
13 |
"react": "^18.3.1",
|
14 |
"react-bootstrap": "^2.10.5",
|
15 |
"react-dom": "^18.3.1",
|
16 |
+
"react-image-crop": "^11.0.7",
|
17 |
"react-router-dom": "^6.27.0",
|
18 |
"react-scripts": "5.0.1",
|
19 |
"validator": "^13.12.0",
|
frontend/public/default_avatar.jpg
ADDED
![]() |
frontend/src/index.js
CHANGED
@@ -10,6 +10,8 @@ import LoginPage from './pages/LoginPage';
|
|
10 |
import RegisterPage from './pages/RegisterPage';
|
11 |
import NewsPage from './pages/NewsPage';
|
12 |
import MenuPage from './pages/MenuPage';
|
|
|
|
|
13 |
|
14 |
const router = createBrowserRouter([
|
15 |
{
|
@@ -35,6 +37,16 @@ const router = createBrowserRouter([
|
|
35 |
{
|
36 |
path: "/menu",
|
37 |
element: <MenuPage/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
38 |
}
|
39 |
]);
|
40 |
|
|
|
10 |
import RegisterPage from './pages/RegisterPage';
|
11 |
import NewsPage from './pages/NewsPage';
|
12 |
import MenuPage from './pages/MenuPage';
|
13 |
+
import CartPage from './pages/CartPage';
|
14 |
+
import UserInfoPage from './pages/UserInfoPage';
|
15 |
|
16 |
const router = createBrowserRouter([
|
17 |
{
|
|
|
37 |
{
|
38 |
path: "/menu",
|
39 |
element: <MenuPage/>
|
40 |
+
},
|
41 |
+
{
|
42 |
+
path: "/cart",
|
43 |
+
element: <CartPage/>,
|
44 |
+
errorElement: <ErrorPage/>
|
45 |
+
},
|
46 |
+
{
|
47 |
+
path: "/userinfo",
|
48 |
+
element: <UserInfoPage/>,
|
49 |
+
errorElement: <ErrorPage/>
|
50 |
}
|
51 |
]);
|
52 |
|
frontend/src/molecules/AdminNavBar.js
ADDED
File without changes
|
frontend/src/molecules/Navbar.js
CHANGED
@@ -1,23 +1,23 @@
|
|
1 |
-
import Container from 'react-bootstrap
|
2 |
-
import Nav from 'react-bootstrap/Nav';
|
3 |
-
import Navbar from 'react-bootstrap/Navbar';
|
4 |
-
import Button from 'react-bootstrap/Button';
|
5 |
-
import { Stack } from 'react-bootstrap';
|
6 |
import { useNavigate } from 'react-router-dom';
|
|
|
7 |
|
8 |
export default function ANavbar() {
|
9 |
|
10 |
const navigate = useNavigate();
|
11 |
-
|
12 |
function handleLogout() {
|
13 |
-
|
14 |
-
|
15 |
-
|
|
|
|
|
|
|
16 |
navigate('/');
|
17 |
}
|
18 |
|
19 |
-
let username =
|
20 |
-
let isLoggedIn =
|
21 |
|
22 |
let userContent;
|
23 |
if (isLoggedIn === 'true') {
|
@@ -26,7 +26,9 @@ export default function ANavbar() {
|
|
26 |
<Button href="/userinfo" variant='primary'>
|
27 |
Xin chào, {username}
|
28 |
</Button>
|
29 |
-
|
|
|
|
|
30 |
<Button onClick={handleLogout} variant='outline-primary'>
|
31 |
Đăng xuất
|
32 |
</Button>
|
|
|
1 |
+
import {Container, Nav, Navbar, Button, Stack} from 'react-bootstrap'
|
|
|
|
|
|
|
|
|
2 |
import { useNavigate } from 'react-router-dom';
|
3 |
+
import DataStorage from '../organisms/DataStorage';
|
4 |
|
5 |
export default function ANavbar() {
|
6 |
|
7 |
const navigate = useNavigate();
|
8 |
+
|
9 |
function handleLogout() {
|
10 |
+
DataStorage.set('isLoggedIn','false');
|
11 |
+
DataStorage.remove('accessToken');
|
12 |
+
DataStorage.remove('role');
|
13 |
+
DataStorage.remove('username');
|
14 |
+
DataStorage.remove('cart');
|
15 |
+
DataStorage.remove('expiryDate');
|
16 |
navigate('/');
|
17 |
}
|
18 |
|
19 |
+
let username = DataStorage.get('username');
|
20 |
+
let isLoggedIn = DataStorage.get('isLoggedIn');
|
21 |
|
22 |
let userContent;
|
23 |
if (isLoggedIn === 'true') {
|
|
|
26 |
<Button href="/userinfo" variant='primary'>
|
27 |
Xin chào, {username}
|
28 |
</Button>
|
29 |
+
<Button href="/cart" variant='outline-primary'>
|
30 |
+
Giỏ hàng
|
31 |
+
</Button>
|
32 |
<Button onClick={handleLogout} variant='outline-primary'>
|
33 |
Đăng xuất
|
34 |
</Button>
|
frontend/src/organisms/DataStorage.js
ADDED
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import Cookies from 'js-cookie';
|
2 |
+
|
3 |
+
export default class DataStorage {
|
4 |
+
static storageMethod = process.env.REACT_APP_STORAGE_METHOD;
|
5 |
+
|
6 |
+
// Get data
|
7 |
+
static get(key) {
|
8 |
+
if (this.storageMethod === 'session') {
|
9 |
+
return sessionStorage.getItem(key);
|
10 |
+
} else if (this.storageMethod === 'cookie') {
|
11 |
+
return Cookies.get(key);
|
12 |
+
}
|
13 |
+
return null;
|
14 |
+
}
|
15 |
+
|
16 |
+
// Set data
|
17 |
+
static set(key, value, param = { expiryDate: null }) {
|
18 |
+
if (this.storageMethod === 'session') {
|
19 |
+
sessionStorage.setItem(key, value);
|
20 |
+
} else if (this.storageMethod === 'cookie') {
|
21 |
+
if (param.expiryDate) {
|
22 |
+
const expiryDate = new Date(param.expiryDate * 1000);
|
23 |
+
Cookies.set(key, value, { expires: expiryDate })
|
24 |
+
} else if (Cookies.get('expiryDate')) {
|
25 |
+
const expiryDate = new Date(Cookies.get('expiryDate') * 1000);
|
26 |
+
Cookies.set(key, value, { expires: expiryDate });
|
27 |
+
} else Cookies.set(key, value, { expires: 7 })// Expires in 7 by default
|
28 |
+
}
|
29 |
+
}
|
30 |
+
|
31 |
+
// Remove data
|
32 |
+
static remove(key) {
|
33 |
+
if (this.storageMethod === 'session') {
|
34 |
+
sessionStorage.removeItem(key);
|
35 |
+
} else if (this.storageMethod === 'cookie') {
|
36 |
+
Cookies.remove(key);
|
37 |
+
}
|
38 |
+
}
|
39 |
+
|
40 |
+
// Get all data
|
41 |
+
static getAll() {
|
42 |
+
const data = {};
|
43 |
+
if (this.storageMethod === 'session') {
|
44 |
+
for (let i = 0; i < sessionStorage.length; i++) {
|
45 |
+
const key = sessionStorage.key(i);
|
46 |
+
data[key] = sessionStorage.getItem(key);
|
47 |
+
}
|
48 |
+
} else if (this.storageMethod === 'cookie') {
|
49 |
+
const cookies = Cookies.get();
|
50 |
+
Object.keys(cookies).forEach(key => {
|
51 |
+
data[key] = cookies[key];
|
52 |
+
});
|
53 |
+
}
|
54 |
+
return data;
|
55 |
+
}
|
56 |
+
|
57 |
+
// Clear all data
|
58 |
+
static clearAll() {
|
59 |
+
if (this.storageMethod === 'session') {
|
60 |
+
sessionStorage.clear();
|
61 |
+
} else if (this.storageMethod === 'cookie') {
|
62 |
+
const cookies = Cookies.get();
|
63 |
+
Object.keys(cookies).forEach(key => {
|
64 |
+
Cookies.remove(key);
|
65 |
+
});
|
66 |
+
}
|
67 |
+
}
|
68 |
+
|
69 |
+
// Check if a key exists
|
70 |
+
static hasKey(key) {
|
71 |
+
if (this.storageMethod === 'session') {
|
72 |
+
return sessionStorage.getItem(key) !== null;
|
73 |
+
} else if (this.storageMethod === 'cookie') {
|
74 |
+
return Cookies.get(key) !== undefined;
|
75 |
+
}
|
76 |
+
return false;
|
77 |
+
}
|
78 |
+
|
79 |
+
// Get all keys
|
80 |
+
static getKeys() {
|
81 |
+
const keys = [];
|
82 |
+
if (this.storageMethod === 'session') {
|
83 |
+
for (let i = 0; i < sessionStorage.length; i++) {
|
84 |
+
keys.push(sessionStorage.key(i));
|
85 |
+
}
|
86 |
+
} else if (this.storageMethod === 'cookie') {
|
87 |
+
keys.push(...Object.keys(Cookies.get()));
|
88 |
+
}
|
89 |
+
return keys;
|
90 |
+
}
|
91 |
+
}
|
frontend/src/organisms/MenuSection.js
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
import { useState } from 'react';
|
2 |
-
import { Container, Carousel, Row, Col } from 'react-bootstrap';
|
3 |
import MenuItem from '../molecules/MenuItem';
|
4 |
|
5 |
function MenuSection() {
|
@@ -20,7 +20,7 @@ function MenuSection() {
|
|
20 |
return (
|
21 |
<Container id="menu" className="text-center justify-content-center align-items-center my-5">
|
22 |
<h1 className='mb-5'>Menu</h1>
|
23 |
-
<Carousel
|
24 |
{Array.from({ length: countCarouselSlides }).map((_, slideIndex) => (
|
25 |
<Carousel.Item key={slideIndex}>
|
26 |
<div className="d-flex justify-content-center align-items-center" style={{ width: '100%' }}>
|
@@ -33,7 +33,7 @@ function MenuSection() {
|
|
33 |
</div>
|
34 |
|
35 |
<Carousel.Caption>
|
36 |
-
<div >
|
37 |
<Row md={3} className="g-4">
|
38 |
{menuItems.map((item, idx) => (
|
39 |
<Col key={idx}>
|
@@ -45,6 +45,7 @@ function MenuSection() {
|
|
45 |
</Col>
|
46 |
))}
|
47 |
</Row>
|
|
|
48 |
</div>
|
49 |
</Carousel.Caption>
|
50 |
</Carousel.Item>
|
|
|
1 |
import { useState } from 'react';
|
2 |
+
import { Container, Carousel, Row, Col, Button } from 'react-bootstrap';
|
3 |
import MenuItem from '../molecules/MenuItem';
|
4 |
|
5 |
function MenuSection() {
|
|
|
20 |
return (
|
21 |
<Container id="menu" className="text-center justify-content-center align-items-center my-5">
|
22 |
<h1 className='mb-5'>Menu</h1>
|
23 |
+
<Carousel activeIndex={index} onSelect={handleSelect} data-bs-theme="dark">
|
24 |
{Array.from({ length: countCarouselSlides }).map((_, slideIndex) => (
|
25 |
<Carousel.Item key={slideIndex}>
|
26 |
<div className="d-flex justify-content-center align-items-center" style={{ width: '100%' }}>
|
|
|
33 |
</div>
|
34 |
|
35 |
<Carousel.Caption>
|
36 |
+
<div className='my-5'>
|
37 |
<Row md={3} className="g-4">
|
38 |
{menuItems.map((item, idx) => (
|
39 |
<Col key={idx}>
|
|
|
45 |
</Col>
|
46 |
))}
|
47 |
</Row>
|
48 |
+
<Button as='a' href='/menu' className='mt-5'> Xem thêm các món </Button>
|
49 |
</div>
|
50 |
</Carousel.Caption>
|
51 |
</Carousel.Item>
|
frontend/src/organisms/jwtDecoder.js
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
function decodeBase64Url(base64Url) {
|
2 |
+
// Thay các ký tự theo chuẩn Base64 URL thành chuẩn Base64 thông thường
|
3 |
+
let base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
4 |
+
// Thêm padding nếu thiếu
|
5 |
+
base64 += '='.repeat((4 - base64.length % 4) % 4);
|
6 |
+
// Giải mã từ Base64 sang chuỗi JSON
|
7 |
+
return JSON.parse(atob(base64));
|
8 |
+
}
|
9 |
+
|
10 |
+
export default function jwtDecoder(jwtToken) {
|
11 |
+
const [header, payload, _] = jwtToken.split('.');
|
12 |
+
|
13 |
+
// Giải mã Header và Payload
|
14 |
+
const decodedHeader = decodeBase64Url(header);
|
15 |
+
const decodedPayload = decodeBase64Url(payload);
|
16 |
+
console.log("Signature:", _);
|
17 |
+
|
18 |
+
console.log("Header:", decodedHeader);
|
19 |
+
console.log("Payload:", decodedPayload);
|
20 |
+
return {"header": decodedHeader, "payload": decodedPayload}
|
21 |
+
}
|
22 |
+
|
frontend/src/pages/AdminHomePage.js
ADDED
File without changes
|
frontend/src/pages/CartPage.js
ADDED
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import BasicTemplate from "../templates/BasicTemplate";
|
2 |
+
import { Container, Row, Col, Card, Button } from "react-bootstrap";
|
3 |
+
import { useState, useEffect } from "react";
|
4 |
+
import DataStorage from "../organisms/DataStorage";
|
5 |
+
|
6 |
+
export default function CartPage() {
|
7 |
+
|
8 |
+
const [cartItems, setCartItems] = useState([]);
|
9 |
+
|
10 |
+
useEffect(() => {
|
11 |
+
// Lấy giỏ hàng từ sessionStorage
|
12 |
+
const cart = JSON.parse(DataStorage.get('cart')) || {};
|
13 |
+
|
14 |
+
// Chuyển cart thành mảng chứa các món có số lượng > 0
|
15 |
+
const items = Object.entries(cart)
|
16 |
+
.filter(([name, amount]) => amount > 0)
|
17 |
+
.map(([name, amount]) => ({
|
18 |
+
name,
|
19 |
+
imageSrc: '/placeholder3.jpg',
|
20 |
+
price: 100, // Thêm đơn giá của món ăn (giả sử ở đây là 100 cho mỗi món)
|
21 |
+
amount
|
22 |
+
}));
|
23 |
+
|
24 |
+
setCartItems(items);
|
25 |
+
}, []);
|
26 |
+
|
27 |
+
|
28 |
+
return (
|
29 |
+
<BasicTemplate content={
|
30 |
+
(
|
31 |
+
<Container className="d-flex align-items-center justify-content-center my-5" style={{ 'min-height': '70vh' }}>
|
32 |
+
{cartItems.length > 0 ? (
|
33 |
+
<div className="text-center">
|
34 |
+
<h2 className="text-center mb-4">Giỏ hàng của bạn</h2>
|
35 |
+
<Row className="g-3">
|
36 |
+
{cartItems.map((item, idx) => (
|
37 |
+
<Col md={12} key={idx} className="my-3">
|
38 |
+
<Card className="shadow-sm" style={{ display: 'flex', flexDirection: 'row' }}>
|
39 |
+
<Card.Img
|
40 |
+
variant="left"
|
41 |
+
src={item.imageSrc}
|
42 |
+
style={{ width: '150px', objectFit: 'cover' }}
|
43 |
+
/>
|
44 |
+
<Card.Body>
|
45 |
+
<Row xs={4} className="align-items-center justify-content-center">
|
46 |
+
<Card.Title as='Col'>{item.name}</Card.Title>
|
47 |
+
<Card.Text as='Col'>Đơn giá: {item.price} VND</Card.Text>
|
48 |
+
<Card.Text as='Col'>Số lượng: {item.amount}</Card.Text>
|
49 |
+
<Card.Text as='Col'>
|
50 |
+
Tổng cộng: {item.price * item.amount} VND
|
51 |
+
</Card.Text>
|
52 |
+
</Row>
|
53 |
+
</Card.Body>
|
54 |
+
</Card>
|
55 |
+
</Col>
|
56 |
+
))}
|
57 |
+
</Row>
|
58 |
+
<Button as='a' href='/payment' className='my-3'>
|
59 |
+
Thanh toán
|
60 |
+
</Button>
|
61 |
+
</div>
|
62 |
+
) : (
|
63 |
+
<div className="text-center">
|
64 |
+
<p className="text-center my-3">Giỏ hàng của bạn hiện đang trống.</p>
|
65 |
+
<Button as='a' href='/menu'>Xem menu</Button>
|
66 |
+
</div>
|
67 |
+
)}
|
68 |
+
</Container>
|
69 |
+
)
|
70 |
+
} />
|
71 |
+
)
|
72 |
+
}
|
frontend/src/pages/LoginPage.js
CHANGED
@@ -2,9 +2,14 @@ import { useState } from "react";
|
|
2 |
import { Alert, Container, Form, Row, Col, Button, Card } from "react-bootstrap";
|
3 |
import BasicTemplate from "../templates/BasicTemplate";
|
4 |
import { useNavigate } from "react-router-dom";
|
|
|
|
|
|
|
5 |
|
6 |
export default function LoginPage() {
|
7 |
|
|
|
|
|
8 |
const [username, setUsername] = useState('');
|
9 |
const [password, setPassword] = useState('');
|
10 |
const [error, setError] = useState('');
|
@@ -16,16 +21,43 @@ export default function LoginPage() {
|
|
16 |
// Validate password and confirm password match
|
17 |
if (password.length === 0) {
|
18 |
setError('Hãy nhập mật khẩu');
|
19 |
-
} else if (username.length === 0) {
|
20 |
setError('Tên đăng nhập không thể trống')
|
21 |
} else {
|
22 |
setError('');
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
29 |
}
|
30 |
};
|
31 |
|
|
|
2 |
import { Alert, Container, Form, Row, Col, Button, Card } from "react-bootstrap";
|
3 |
import BasicTemplate from "../templates/BasicTemplate";
|
4 |
import { useNavigate } from "react-router-dom";
|
5 |
+
import axios from 'axios';
|
6 |
+
import jwtDecoder from "../organisms/jwtDecoder";
|
7 |
+
import DataStorage from "../organisms/DataStorage";
|
8 |
|
9 |
export default function LoginPage() {
|
10 |
|
11 |
+
const domain = process.env.REACT_APP_API_URL;
|
12 |
+
|
13 |
const [username, setUsername] = useState('');
|
14 |
const [password, setPassword] = useState('');
|
15 |
const [error, setError] = useState('');
|
|
|
21 |
// Validate password and confirm password match
|
22 |
if (password.length === 0) {
|
23 |
setError('Hãy nhập mật khẩu');
|
24 |
+
} else if (username.trim().length === 0) {
|
25 |
setError('Tên đăng nhập không thể trống')
|
26 |
} else {
|
27 |
setError('');
|
28 |
+
|
29 |
+
let data = {
|
30 |
+
'username': username,
|
31 |
+
'password': password
|
32 |
+
}
|
33 |
+
|
34 |
+
axios.post(domain + '/authentication/login', data)
|
35 |
+
.then((response) => {
|
36 |
+
if (response.status === 200) {
|
37 |
+
// setError(JSON.stringify(jwtDecoder(response.data.access_token)));
|
38 |
+
const decodedToken = jwtDecoder(response.data.access_token);
|
39 |
+
const full_name = decodedToken.payload.username;
|
40 |
+
const role = decodedToken.payload.roles;
|
41 |
+
const expiryTime = decodedToken.payload.exp;
|
42 |
+
DataStorage.set('expiryDate', expiryTime, {expiryDate:expiryTime});
|
43 |
+
DataStorage.set('username', full_name);
|
44 |
+
DataStorage.set('role', role);
|
45 |
+
DataStorage.set('isLoggedIn', 'true');
|
46 |
+
DataStorage.set('accessToken', response.data.access_token);
|
47 |
+
DataStorage.set('cart', '{}');
|
48 |
+
navigate('/');
|
49 |
+
} else {
|
50 |
+
setError(JSON.stringify(response));
|
51 |
+
}
|
52 |
+
})
|
53 |
+
.catch((error) => {
|
54 |
+
if (error.status === 400) {
|
55 |
+
setError('Lỗi đăng nhập, vui lòng kiểm tra lại tài khoản và mật khẩu');
|
56 |
+
} else if (error.status === 500) {
|
57 |
+
setError('Server đang tạm gặp vấn đề');
|
58 |
+
}
|
59 |
+
})
|
60 |
+
|
61 |
}
|
62 |
};
|
63 |
|
frontend/src/pages/MenuPage.js
CHANGED
@@ -1,16 +1,55 @@
|
|
1 |
import { useState } from 'react';
|
2 |
-
import { Modal, Button, Container, Row, Col, Tab, Tabs } from 'react-bootstrap';
|
3 |
import MenuItem from '../molecules/MenuItem';
|
4 |
import BasicTemplate from '../templates/BasicTemplate';
|
|
|
|
|
5 |
function MenuPage() {
|
6 |
const [key, setKey] = useState('cat1');
|
7 |
const [selectedDish, setSelectedDish] = useState(null);
|
8 |
const [show, setShow] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
|
10 |
-
const handleClose = () => setShow(false);
|
11 |
function handleShow(dish) {
|
12 |
-
|
13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
}
|
15 |
|
16 |
const menuItems1 = [
|
@@ -35,23 +74,65 @@ function MenuPage() {
|
|
35 |
{ name: 'Món 6 thể loại 3', description: 'Mô tả món 3', imageSrc: '/placeholder3.jpg' }
|
36 |
];
|
37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
38 |
return <BasicTemplate content={(
|
39 |
<Container fluid className='my-5'>
|
40 |
<>
|
41 |
-
|
42 |
-
<Modal.Header closeButton>
|
43 |
-
<Modal.Title>{selectedDish?.name}</Modal.Title> {/* Dish name in the title */}
|
44 |
-
</Modal.Header>
|
45 |
-
<Modal.Body>
|
46 |
-
<p>{selectedDish?.description}</p> {/* Dish description */}
|
47 |
-
<img src={selectedDish?.imageSrc} alt={selectedDish?.name} style={{ width: '100%' }} /> {/* Dish image */}
|
48 |
-
</Modal.Body>
|
49 |
-
<Modal.Footer>
|
50 |
-
<Button variant="secondary" onClick={handleClose}>
|
51 |
-
Close
|
52 |
-
</Button>
|
53 |
-
</Modal.Footer>
|
54 |
-
</Modal>
|
55 |
</>
|
56 |
<h1 className='text-center mb-5'>Thực đơn</h1>
|
57 |
<Row>
|
|
|
1 |
import { useState } from 'react';
|
2 |
+
import { Modal, Button, Container, Row, Col, Tab, Tabs, Form, InputGroup } from 'react-bootstrap';
|
3 |
import MenuItem from '../molecules/MenuItem';
|
4 |
import BasicTemplate from '../templates/BasicTemplate';
|
5 |
+
import DataStorage from '../organisms/DataStorage';
|
6 |
+
|
7 |
function MenuPage() {
|
8 |
const [key, setKey] = useState('cat1');
|
9 |
const [selectedDish, setSelectedDish] = useState(null);
|
10 |
const [show, setShow] = useState(false);
|
11 |
+
const [cartAmount, setCartAmount] = useState(0);
|
12 |
+
|
13 |
+
function handleClose() {
|
14 |
+
const cart = JSON.parse(DataStorage.get('cart')) || {}; // Đảm bảo cart không null
|
15 |
+
cart[selectedDish.name] = cartAmount;
|
16 |
+
const filteredCart = Object.fromEntries(
|
17 |
+
Object.entries(cart).filter(([key, value]) => value > 0)
|
18 |
+
);
|
19 |
+
DataStorage.set('cart', JSON.stringify(filteredCart));
|
20 |
+
// console.log(JSON.stringify(filteredCart));
|
21 |
+
setShow(false);
|
22 |
+
}
|
23 |
+
|
24 |
+
function notLoggedInClose() {
|
25 |
+
setShow(false);
|
26 |
+
}
|
27 |
|
|
|
28 |
function handleShow(dish) {
|
29 |
+
if (DataStorage.get('isLoggedIn') === null) {
|
30 |
+
setShow(true);
|
31 |
+
return;
|
32 |
+
} else {
|
33 |
+
setSelectedDish(dish); // Đặt selectedDish ngay lập tức
|
34 |
+
|
35 |
+
// Sau khi cập nhật selectedDish, lấy dữ liệu từ cart theo tên món ăn mới
|
36 |
+
const cart = JSON.parse(DataStorage.get('cart')) || {};
|
37 |
+
|
38 |
+
// Kiểm tra số lượng món ăn trong cart
|
39 |
+
const amount = cart[dish.name] !== undefined ? cart[dish.name] : 0;
|
40 |
+
setCartAmount(amount); // Cập nhật số lượng
|
41 |
+
setShow(true); // Hiển thị modal
|
42 |
+
}
|
43 |
+
}
|
44 |
+
|
45 |
+
function setIncrease() {
|
46 |
+
setCartAmount(cartAmount + 1);
|
47 |
+
}
|
48 |
+
|
49 |
+
function setDecrease() {
|
50 |
+
if (cartAmount > 0) {
|
51 |
+
setCartAmount(cartAmount - 1);
|
52 |
+
}
|
53 |
}
|
54 |
|
55 |
const menuItems1 = [
|
|
|
74 |
{ name: 'Món 6 thể loại 3', description: 'Mô tả món 3', imageSrc: '/placeholder3.jpg' }
|
75 |
];
|
76 |
|
77 |
+
let modalContent;
|
78 |
+
|
79 |
+
if (DataStorage.get('isLoggedIn') === null) {
|
80 |
+
modalContent = (
|
81 |
+
<Modal show={show} onHide={notLoggedInClose} className='text-center align-items-center'>
|
82 |
+
<Modal.Header closeButton className='text-center'>
|
83 |
+
<Modal.Title>Bạn chưa đăng nhập</Modal.Title>
|
84 |
+
</Modal.Header>
|
85 |
+
<Modal.Body>
|
86 |
+
Vui lòng đăng nhập để xem chi tiết món và đặt hàng
|
87 |
+
</Modal.Body>
|
88 |
+
<Modal.Footer>
|
89 |
+
<Button variant="primary" as='a' href='/login'>
|
90 |
+
Đăng nhập
|
91 |
+
</Button>
|
92 |
+
<Button variant="secondary" onClick={notLoggedInClose}>
|
93 |
+
Đóng
|
94 |
+
</Button>
|
95 |
+
</Modal.Footer>
|
96 |
+
</Modal>
|
97 |
+
)
|
98 |
+
}
|
99 |
+
else {
|
100 |
+
modalContent = (<Modal show={show} onHide={handleClose} className='text-center'>
|
101 |
+
<Modal.Header closeButton className='text-center'>
|
102 |
+
<Modal.Title >{selectedDish?.name}</Modal.Title> {/* Dish name in the title */}
|
103 |
+
</Modal.Header>
|
104 |
+
<Modal.Body>
|
105 |
+
<img src={selectedDish?.imageSrc} alt={selectedDish?.name} style={{ width: '100%' }} /> {/* Dish image */}
|
106 |
+
<p>{selectedDish?.description}</p> {/* Dish description */}
|
107 |
+
|
108 |
+
<Row className='mb-5'>
|
109 |
+
<Col md={2}></Col>
|
110 |
+
<Col md={8}>
|
111 |
+
<Form.Label>Số lượng</Form.Label>
|
112 |
+
<InputGroup as='row' mb={4} className='mb-3'>
|
113 |
+
<InputGroup.Text as='button' onClick={setDecrease}>-</InputGroup.Text>
|
114 |
+
<Form.Control
|
115 |
+
value={cartAmount}
|
116 |
+
aria-label="Amount"
|
117 |
+
onChange={(e) => setCartAmount(e.target.value)} />
|
118 |
+
<InputGroup.Text as='button' onClick={setIncrease}>+</InputGroup.Text>
|
119 |
+
</InputGroup></Col>
|
120 |
+
<Col md={2}></Col>
|
121 |
+
</Row>
|
122 |
+
|
123 |
+
</Modal.Body>
|
124 |
+
<Modal.Footer>
|
125 |
+
<Button variant="secondary" onClick={handleClose}>
|
126 |
+
Đóng
|
127 |
+
</Button>
|
128 |
+
</Modal.Footer>
|
129 |
+
</Modal>);
|
130 |
+
}
|
131 |
+
|
132 |
return <BasicTemplate content={(
|
133 |
<Container fluid className='my-5'>
|
134 |
<>
|
135 |
+
{modalContent}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
136 |
</>
|
137 |
<h1 className='text-center mb-5'>Thực đơn</h1>
|
138 |
<Row>
|
frontend/src/pages/RegisterPage.js
CHANGED
@@ -19,7 +19,7 @@ const RegisterPage = () => {
|
|
19 |
// Validate password and confirm password match
|
20 |
if (full_name.length === 0) {
|
21 |
setError('Họ và tên không thể để trống');
|
22 |
-
} else if (!validator.isMobilePhone(phone_number)) {
|
23 |
setError('Số điện thoại không hợp lệ');
|
24 |
} else if (!validator.isEmail(email)) {
|
25 |
setError('Email không hợp lệ');
|
|
|
19 |
// Validate password and confirm password match
|
20 |
if (full_name.length === 0) {
|
21 |
setError('Họ và tên không thể để trống');
|
22 |
+
} else if (!validator.isMobilePhone(phone_number, 'vi-VN')) {
|
23 |
setError('Số điện thoại không hợp lệ');
|
24 |
} else if (!validator.isEmail(email)) {
|
25 |
setError('Email không hợp lệ');
|
frontend/src/pages/UserInfoPage.js
ADDED
@@ -0,0 +1,257 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import BasicTemplate from "../templates/BasicTemplate";
|
2 |
+
import { Container, Form, Row, Col, Card, Alert, Button, Image } from "react-bootstrap";
|
3 |
+
import React, { useState, useEffect } from "react";
|
4 |
+
import validator from "validator";
|
5 |
+
import axios from "axios";
|
6 |
+
import DataStorage from "../organisms/DataStorage";
|
7 |
+
|
8 |
+
export default function UserInfoPage() {
|
9 |
+
|
10 |
+
// fetch data:
|
11 |
+
|
12 |
+
const [error, setErrors] = useState("");
|
13 |
+
const [initialData, setInitialData] = useState(null);
|
14 |
+
const [formData, setFormData] = useState({
|
15 |
+
avatar: "/default_avatar.jpg",
|
16 |
+
full_name: "",
|
17 |
+
phone_number: "",
|
18 |
+
address: "",
|
19 |
+
email: "",
|
20 |
+
password: "",
|
21 |
+
confirmPassword: "",
|
22 |
+
});
|
23 |
+
|
24 |
+
useEffect(() => {
|
25 |
+
axios
|
26 |
+
.get(process.env.REACT_APP_API_URL + '/authentication/profile', {
|
27 |
+
headers: {
|
28 |
+
Authorization: `Bearer ${DataStorage.get('accessToken')}`,
|
29 |
+
},
|
30 |
+
})
|
31 |
+
.then((response) => {
|
32 |
+
if (response.status === 401) {
|
33 |
+
// phiên hết hạn
|
34 |
+
setErrors(JSON.stringify(response));
|
35 |
+
} else {
|
36 |
+
const { avatar, full_name, address, phone_number, email } = response.data;
|
37 |
+
const fetchedData = {
|
38 |
+
avatar: avatar || "/default_avatar.jpg",
|
39 |
+
full_name: full_name || "",
|
40 |
+
phone_number: phone_number || "",
|
41 |
+
address: address || "",
|
42 |
+
email: email || "",
|
43 |
+
password: "",
|
44 |
+
confirmPassword: "",
|
45 |
+
};
|
46 |
+
|
47 |
+
if (fetchedData.full_name !== DataStorage.get('username')) DataStorage.set('username', fetchedData.full_name)
|
48 |
+
setInitialData(fetchedData);
|
49 |
+
setFormData(fetchedData);
|
50 |
+
}
|
51 |
+
})
|
52 |
+
.catch((error) => {
|
53 |
+
setErrors(JSON.stringify(error));
|
54 |
+
});
|
55 |
+
}, []);
|
56 |
+
|
57 |
+
// console.log('formdata', formData);
|
58 |
+
// console.log('initialdata', initialData);
|
59 |
+
// State để hiển thị lỗi khi validation
|
60 |
+
|
61 |
+
const [isChanged, setChanged] = useState(false);
|
62 |
+
|
63 |
+
const handleChange = (e) => {
|
64 |
+
const { name, value } = e.target;
|
65 |
+
setFormData({ ...formData, [name]: value });
|
66 |
+
};
|
67 |
+
|
68 |
+
// Hàm xử lý thay đổi avatar
|
69 |
+
const handleAvatarChange = (e) => {
|
70 |
+
const file = e.target.files[0];
|
71 |
+
if (file) {
|
72 |
+
const reader = new FileReader();
|
73 |
+
reader.onloadend = () => {
|
74 |
+
setFormData((prevData) => ({
|
75 |
+
...prevData,
|
76 |
+
avatar: reader.result // Cập nhật ảnh đại diện
|
77 |
+
}));
|
78 |
+
};
|
79 |
+
reader.readAsDataURL(file);
|
80 |
+
setChanged(true);
|
81 |
+
}
|
82 |
+
};
|
83 |
+
|
84 |
+
useEffect(() => {
|
85 |
+
const hasChanges = () => {
|
86 |
+
if (initialData) {
|
87 |
+
for (let key in formData) {
|
88 |
+
if (formData[key] !== initialData[key]) {
|
89 |
+
return true;
|
90 |
+
}
|
91 |
+
}
|
92 |
+
return false;
|
93 |
+
} else {
|
94 |
+
return false;
|
95 |
+
}
|
96 |
+
};
|
97 |
+
setChanged(hasChanges());
|
98 |
+
}, [formData, initialData]);
|
99 |
+
|
100 |
+
const handleSubmit = async (e) => {
|
101 |
+
e.preventDefault();
|
102 |
+
setChanged(false); // prevent another click
|
103 |
+
if (formData.phone_number.trim() !== "" && !validator.isMobilePhone(formData.phone_number, 'vi-VN')) setErrors("Số điện thoại không hợp lệ");
|
104 |
+
else if (formData.email.trim() !== "" && !validator.isEmail(formData.email)) setErrors("Email không hợp lệ");
|
105 |
+
else if (formData.password !== "" && formData.confirmPassword !== formData.password) setErrors("Mật khẩu không khớp");
|
106 |
+
else {
|
107 |
+
|
108 |
+
// let avatarBase64 = null;
|
109 |
+
|
110 |
+
// if (formData.avatar) {
|
111 |
+
// // Chuyển ảnh đại diện thành chuỗi Base64
|
112 |
+
// avatarBase64 = await new Promise((resolve, reject) => {
|
113 |
+
// const reader = new FileReader();
|
114 |
+
// reader.onloadend = () => resolve(reader.result.split(",")[1]); // Chỉ lấy phần Base64
|
115 |
+
// reader.onerror = (error) => reject(error);
|
116 |
+
// reader.readAsDataURL(formData.avatar);
|
117 |
+
// });
|
118 |
+
// }
|
119 |
+
|
120 |
+
let data = {};
|
121 |
+
|
122 |
+
// if (avatarBase64) data.avatar = avatarBase64;
|
123 |
+
if (formData.full_name.trim() !== "" && formData.full_name.trim() !== initialData.full_name) data.full_name = formData.full_name.trim();
|
124 |
+
if (formData.phone_number.trim() !== "" && formData.phone_number.trim() !== initialData.phone_number) data.phone_number = formData.phone_number.trim();
|
125 |
+
if (formData.address.trim() !== "" && formData.address.trim() !== initialData.address) data.address = formData.address.trim();
|
126 |
+
if (formData.email.trim() !== "" && formData.email.trim() !== initialData.email) data.email = formData.email.trim();
|
127 |
+
if (formData.password.trim() !== "") data.hash_password = formData.password;
|
128 |
+
|
129 |
+
// gửi request ở đây
|
130 |
+
axios.post(process.env.REACT_APP_API_URL + '/users/updateUser', data, {
|
131 |
+
headers: {
|
132 |
+
Authorization: `Bearer ${DataStorage.get('accessToken')}`,
|
133 |
+
}
|
134 |
+
}).then((response) => {
|
135 |
+
if (response.status === 200 || response.status === 201) {
|
136 |
+
window.location.reload(); // cập nhật lại thông tin
|
137 |
+
} else {
|
138 |
+
setErrors(JSON.stringify(response));
|
139 |
+
}
|
140 |
+
}).catch((error) => setErrors(JSON.stringify(error)));
|
141 |
+
}
|
142 |
+
};
|
143 |
+
|
144 |
+
return <BasicTemplate content={
|
145 |
+
(
|
146 |
+
<Container fluid className="d-flex align-items-center justify-content-center mt-5">
|
147 |
+
<Row>
|
148 |
+
<Col xs={1} md={2}></Col>
|
149 |
+
<Col xs={10} md={8}>
|
150 |
+
<Card style={{ width: '30rem' }} className='justify-content-center'>
|
151 |
+
<Card.Header>
|
152 |
+
<Card.Title className='mt-1 text-center'>Thông tin khách hàng</Card.Title>
|
153 |
+
</Card.Header>
|
154 |
+
<Card.Body>
|
155 |
+
|
156 |
+
<Form onSubmit={handleSubmit}>
|
157 |
+
{error && <Alert variant="danger">{error}</Alert>}
|
158 |
+
<Row className="mb-3">
|
159 |
+
<Col xs={12} md={6} className="text-center">
|
160 |
+
{/*Image*/}
|
161 |
+
<Image
|
162 |
+
src={formData.avatar} // Hiển thị ảnh đại diện từ formData
|
163 |
+
alt="User Avatar"
|
164 |
+
roundedCircle
|
165 |
+
width={120}
|
166 |
+
height={120}
|
167 |
+
/>
|
168 |
+
<Form.Group controlId="formAvatar">
|
169 |
+
<Form.Label>Ảnh đại diện</Form.Label>
|
170 |
+
<Form.Control type="file" accept="image/*" onChange={handleAvatarChange} />
|
171 |
+
</Form.Group>
|
172 |
+
</Col>
|
173 |
+
<Col xs={12} md={6}>
|
174 |
+
<Form.Group controlId="formUsername">
|
175 |
+
<Form.Label>Họ tên người dùng</Form.Label>
|
176 |
+
<Form.Control
|
177 |
+
type="text"
|
178 |
+
name="full_name"
|
179 |
+
placeholder="Họ tên người dùng"
|
180 |
+
value={formData.full_name}
|
181 |
+
onChange={handleChange}
|
182 |
+
/>
|
183 |
+
</Form.Group>
|
184 |
+
</Col>
|
185 |
+
</Row>
|
186 |
+
|
187 |
+
<Form.Group controlId="formPhone" className="mb-3">
|
188 |
+
<Form.Label>Số điện thoại</Form.Label>
|
189 |
+
<Form.Control
|
190 |
+
type="text"
|
191 |
+
name="phone_number"
|
192 |
+
placeholder="Số điện thoại"
|
193 |
+
value={formData.phone_number}
|
194 |
+
onChange={handleChange}
|
195 |
+
/>
|
196 |
+
</Form.Group>
|
197 |
+
|
198 |
+
<Form.Group controlId="formAddress" className="mb-3">
|
199 |
+
<Form.Label>Địa chỉ giao hàng mặc định</Form.Label>
|
200 |
+
<Form.Control
|
201 |
+
type="text"
|
202 |
+
name="address"
|
203 |
+
placeholder="Địa chỉ giao hàng"
|
204 |
+
value={formData.address}
|
205 |
+
onChange={handleChange}
|
206 |
+
/>
|
207 |
+
</Form.Group>
|
208 |
+
|
209 |
+
<Form.Group controlId="formEmail" className="mb-3">
|
210 |
+
<Form.Label>Email</Form.Label>
|
211 |
+
<Form.Control
|
212 |
+
type="text"
|
213 |
+
name="email"
|
214 |
+
placeholder="Email"
|
215 |
+
value={formData.email}
|
216 |
+
onChange={handleChange}
|
217 |
+
/>
|
218 |
+
</Form.Group>
|
219 |
+
|
220 |
+
<Form.Group controlId="formPassword" className="mb-3">
|
221 |
+
<Form.Label>Password</Form.Label>
|
222 |
+
<Form.Control
|
223 |
+
type="password"
|
224 |
+
name="password"
|
225 |
+
placeholder="********"
|
226 |
+
value={formData.password}
|
227 |
+
onChange={handleChange}
|
228 |
+
/>
|
229 |
+
</Form.Group>
|
230 |
+
|
231 |
+
<Form.Group controlId="formConfirmPassword" className="mb-3">
|
232 |
+
<Form.Label>Nhập lại Password</Form.Label>
|
233 |
+
<Form.Control
|
234 |
+
type="password"
|
235 |
+
name="confirmPassword"
|
236 |
+
placeholder="********"
|
237 |
+
value={formData.confirmPassword}
|
238 |
+
onChange={handleChange}
|
239 |
+
/>
|
240 |
+
</Form.Group>
|
241 |
+
|
242 |
+
<Button variant="primary" type="submit" disabled={!isChanged}>
|
243 |
+
Cập nhật
|
244 |
+
</Button>
|
245 |
+
</Form>
|
246 |
+
</Card.Body>
|
247 |
+
</Card>
|
248 |
+
|
249 |
+
</Col>
|
250 |
+
<Col xs={1} md={2}></Col>
|
251 |
+
</Row>
|
252 |
+
|
253 |
+
|
254 |
+
</Container>
|
255 |
+
)
|
256 |
+
} />
|
257 |
+
}
|