Auth redirects, language switch, basic profile
This commit is contained in:
parent
2091bdb4e3
commit
36a4659915
1
exam/dist/assets/index-BF6aZoB4.js
vendored
Normal file
1
exam/dist/assets/index-BF6aZoB4.js
vendored
Normal file
File diff suppressed because one or more lines are too long
193
exam/dist/assets/index-BJXidBC9.js
vendored
193
exam/dist/assets/index-BJXidBC9.js
vendored
File diff suppressed because one or more lines are too long
102
exam/dist/assets/vendor_mui-DqRwP0O9.js
vendored
Normal file
102
exam/dist/assets/vendor_mui-DqRwP0O9.js
vendored
Normal file
File diff suppressed because one or more lines are too long
72
exam/dist/assets/vendor_react-BnzSvkeI.js
vendored
Normal file
72
exam/dist/assets/vendor_react-BnzSvkeI.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
exam/dist/assets/vendor_tanstack-BjOH7Cun.js
vendored
Normal file
1
exam/dist/assets/vendor_tanstack-BjOH7Cun.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
exam/dist/index.html
vendored
7
exam/dist/index.html
vendored
@ -5,8 +5,11 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/phpCourse/exam/dist/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/phpCourse/exam/dist/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite + React + TS</title>
|
<title>Vite + React + TS</title>
|
||||||
<script type="module" crossorigin src="/phpCourse/exam/dist/assets/index-BJXidBC9.js"></script>
|
<script type="module" crossorigin src="/phpCourse/exam/dist/assets/index-BF6aZoB4.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/phpCourse/exam/dist/assets/index-D83Ey19k.css">
|
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/vendor_mui-DqRwP0O9.js">
|
||||||
|
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/vendor_react-BnzSvkeI.js">
|
||||||
|
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/vendor_tanstack-BjOH7Cun.js">
|
||||||
|
<link rel="stylesheet" crossorigin href="/phpCourse/exam/dist/assets/vendor_react-D83Ey19k.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
8
exam/dist/locales/de/translation.json
vendored
8
exam/dist/locales/de/translation.json
vendored
@ -12,5 +12,11 @@
|
|||||||
|
|
||||||
"Log in": "Anmelden",
|
"Log in": "Anmelden",
|
||||||
"Log out": "Abmelden",
|
"Log out": "Abmelden",
|
||||||
"Profile": "Profil"
|
"Profile": "Profil",
|
||||||
|
|
||||||
|
"Updating": "Aktualisiert",
|
||||||
|
|
||||||
|
"Username": "Benutzername",
|
||||||
|
"Member since": "Mitglied seit",
|
||||||
|
"Post count": "Anzahl Posts"
|
||||||
}
|
}
|
||||||
|
|||||||
8
exam/dist/locales/en/translation.json
vendored
8
exam/dist/locales/en/translation.json
vendored
@ -12,5 +12,11 @@
|
|||||||
|
|
||||||
"Log in": "Log in",
|
"Log in": "Log in",
|
||||||
"Log out": "Log out",
|
"Log out": "Log out",
|
||||||
"Profile": "Profile"
|
"Profile": "Profile",
|
||||||
|
|
||||||
|
"Updating": "Updating",
|
||||||
|
|
||||||
|
"Username": "Username",
|
||||||
|
"Member since": "Member since",
|
||||||
|
"Post count": "Post count"
|
||||||
}
|
}
|
||||||
|
|||||||
4842
exam/dist/stats.html
vendored
Normal file
4842
exam/dist/stats.html
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -43,6 +43,7 @@
|
|||||||
"eslint-plugin-react-refresh": "^0.4.7",
|
"eslint-plugin-react-refresh": "^0.4.7",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"prettier-plugin-organize-imports": "^3.2.4",
|
"prettier-plugin-organize-imports": "^3.2.4",
|
||||||
|
"rollup-plugin-visualizer": "^5.0.0",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^5.3.4"
|
"vite": "^5.3.4"
|
||||||
}
|
}
|
||||||
|
|||||||
132
exam/react/pnpm-lock.yaml
generated
132
exam/react/pnpm-lock.yaml
generated
@ -102,6 +102,9 @@ importers:
|
|||||||
prettier-plugin-organize-imports:
|
prettier-plugin-organize-imports:
|
||||||
specifier: ^3.2.4
|
specifier: ^3.2.4
|
||||||
version: 3.2.4(prettier@3.3.3)(typescript@5.5.3)
|
version: 3.2.4(prettier@3.3.3)(typescript@5.5.3)
|
||||||
|
rollup-plugin-visualizer:
|
||||||
|
specifier: ^5.0.0
|
||||||
|
version: 5.12.0(rollup@4.18.1)
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.2.2
|
specifier: ^5.2.2
|
||||||
version: 5.5.3
|
version: 5.5.3
|
||||||
@ -1020,6 +1023,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||||
engines: {node: '>= 8.10.0'}
|
engines: {node: '>= 8.10.0'}
|
||||||
|
|
||||||
|
cliui@8.0.1:
|
||||||
|
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
clsx@2.1.1:
|
clsx@2.1.1:
|
||||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@ -1091,6 +1098,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
|
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
define-lazy-prop@2.0.0:
|
||||||
|
resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
dir-glob@3.0.1:
|
dir-glob@3.0.1:
|
||||||
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
|
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -1105,6 +1116,9 @@ packages:
|
|||||||
electron-to-chromium@1.4.830:
|
electron-to-chromium@1.4.830:
|
||||||
resolution: {integrity: sha512-TrPKKH20HeN0J1LHzsYLs2qwXrp8TF4nHdu4sq61ozGbzMpWhI7iIOPYPPkxeq1azMT9PZ8enPFcftbs/Npcjg==}
|
resolution: {integrity: sha512-TrPKKH20HeN0J1LHzsYLs2qwXrp8TF4nHdu4sq61ozGbzMpWhI7iIOPYPPkxeq1azMT9PZ8enPFcftbs/Npcjg==}
|
||||||
|
|
||||||
|
emoji-regex@8.0.0:
|
||||||
|
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||||
|
|
||||||
error-ex@1.3.2:
|
error-ex@1.3.2:
|
||||||
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
|
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
|
||||||
|
|
||||||
@ -1237,6 +1251,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
get-caller-file@2.0.5:
|
||||||
|
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||||
|
engines: {node: 6.* || 8.* || >= 10.*}
|
||||||
|
|
||||||
get-intrinsic@1.2.4:
|
get-intrinsic@1.2.4:
|
||||||
resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==}
|
resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@ -1359,10 +1377,19 @@ packages:
|
|||||||
resolution: {integrity: sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==}
|
resolution: {integrity: sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
is-docker@2.2.1:
|
||||||
|
resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
is-extglob@2.1.1:
|
is-extglob@2.1.1:
|
||||||
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
is-fullwidth-code-point@3.0.0:
|
||||||
|
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
is-generator-function@1.0.10:
|
is-generator-function@1.0.10:
|
||||||
resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==}
|
resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@ -1383,6 +1410,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==}
|
resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
is-wsl@2.2.0:
|
||||||
|
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
isexe@2.0.0:
|
isexe@2.0.0:
|
||||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||||
|
|
||||||
@ -1492,6 +1523,10 @@ packages:
|
|||||||
once@1.4.0:
|
once@1.4.0:
|
||||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||||
|
|
||||||
|
open@8.4.2:
|
||||||
|
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
optionator@0.9.4:
|
optionator@0.9.4:
|
||||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@ -1626,6 +1661,10 @@ packages:
|
|||||||
regenerator-runtime@0.14.1:
|
regenerator-runtime@0.14.1:
|
||||||
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
|
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
|
||||||
|
|
||||||
|
require-directory@2.1.1:
|
||||||
|
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
resolve-from@4.0.0:
|
resolve-from@4.0.0:
|
||||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@ -1643,6 +1682,16 @@ packages:
|
|||||||
deprecated: Rimraf versions prior to v4 are no longer supported
|
deprecated: Rimraf versions prior to v4 are no longer supported
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
rollup-plugin-visualizer@5.12.0:
|
||||||
|
resolution: {integrity: sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
rollup: 2.x || 3.x || 4.x
|
||||||
|
peerDependenciesMeta:
|
||||||
|
rollup:
|
||||||
|
optional: true
|
||||||
|
|
||||||
rollup@4.18.1:
|
rollup@4.18.1:
|
||||||
resolution: {integrity: sha512-Elx2UT8lzxxOXMpy5HWQGZqkrQOtrVDDa/bm9l10+U4rQnVzbL/LgZ4NOM1MPIDyHk69W4InuYDF5dzRh4Kw1A==}
|
resolution: {integrity: sha512-Elx2UT8lzxxOXMpy5HWQGZqkrQOtrVDDa/bm9l10+U4rQnVzbL/LgZ4NOM1MPIDyHk69W4InuYDF5dzRh4Kw1A==}
|
||||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||||
@ -1704,6 +1753,10 @@ packages:
|
|||||||
stream-slice@0.1.2:
|
stream-slice@0.1.2:
|
||||||
resolution: {integrity: sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==}
|
resolution: {integrity: sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==}
|
||||||
|
|
||||||
|
string-width@4.2.3:
|
||||||
|
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
strip-ansi@6.0.1:
|
strip-ansi@6.0.1:
|
||||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -1867,9 +1920,17 @@ packages:
|
|||||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
wrap-ansi@7.0.0:
|
||||||
|
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
wrappy@1.0.2:
|
wrappy@1.0.2:
|
||||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||||
|
|
||||||
|
y18n@5.0.8:
|
||||||
|
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
yallist@3.1.1:
|
yallist@3.1.1:
|
||||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||||
|
|
||||||
@ -1877,6 +1938,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
|
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
yargs-parser@21.1.1:
|
||||||
|
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
yargs@17.7.2:
|
||||||
|
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
yocto-queue@0.1.0:
|
yocto-queue@0.1.0:
|
||||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -2871,6 +2940,12 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
cliui@8.0.1:
|
||||||
|
dependencies:
|
||||||
|
string-width: 4.2.3
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
wrap-ansi: 7.0.0
|
||||||
|
|
||||||
clsx@2.1.1: {}
|
clsx@2.1.1: {}
|
||||||
|
|
||||||
color-convert@1.9.3:
|
color-convert@1.9.3:
|
||||||
@ -2933,6 +3008,8 @@ snapshots:
|
|||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
gopd: 1.0.1
|
gopd: 1.0.1
|
||||||
|
|
||||||
|
define-lazy-prop@2.0.0: {}
|
||||||
|
|
||||||
dir-glob@3.0.1:
|
dir-glob@3.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-type: 4.0.0
|
path-type: 4.0.0
|
||||||
@ -2948,6 +3025,8 @@ snapshots:
|
|||||||
|
|
||||||
electron-to-chromium@1.4.830: {}
|
electron-to-chromium@1.4.830: {}
|
||||||
|
|
||||||
|
emoji-regex@8.0.0: {}
|
||||||
|
|
||||||
error-ex@1.3.2:
|
error-ex@1.3.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-arrayish: 0.2.1
|
is-arrayish: 0.2.1
|
||||||
@ -3122,6 +3201,8 @@ snapshots:
|
|||||||
|
|
||||||
gensync@1.0.0-beta.2: {}
|
gensync@1.0.0-beta.2: {}
|
||||||
|
|
||||||
|
get-caller-file@2.0.5: {}
|
||||||
|
|
||||||
get-intrinsic@1.2.4:
|
get-intrinsic@1.2.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
@ -3249,8 +3330,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
|
|
||||||
|
is-docker@2.2.1: {}
|
||||||
|
|
||||||
is-extglob@2.1.1: {}
|
is-extglob@2.1.1: {}
|
||||||
|
|
||||||
|
is-fullwidth-code-point@3.0.0: {}
|
||||||
|
|
||||||
is-generator-function@1.0.10:
|
is-generator-function@1.0.10:
|
||||||
dependencies:
|
dependencies:
|
||||||
has-tostringtag: 1.0.2
|
has-tostringtag: 1.0.2
|
||||||
@ -3267,6 +3352,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
which-typed-array: 1.1.15
|
which-typed-array: 1.1.15
|
||||||
|
|
||||||
|
is-wsl@2.2.0:
|
||||||
|
dependencies:
|
||||||
|
is-docker: 2.2.1
|
||||||
|
|
||||||
isexe@2.0.0: {}
|
isexe@2.0.0: {}
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
@ -3349,6 +3438,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
wrappy: 1.0.2
|
wrappy: 1.0.2
|
||||||
|
|
||||||
|
open@8.4.2:
|
||||||
|
dependencies:
|
||||||
|
define-lazy-prop: 2.0.0
|
||||||
|
is-docker: 2.2.1
|
||||||
|
is-wsl: 2.2.0
|
||||||
|
|
||||||
optionator@0.9.4:
|
optionator@0.9.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
deep-is: 0.1.4
|
deep-is: 0.1.4
|
||||||
@ -3460,6 +3555,8 @@ snapshots:
|
|||||||
|
|
||||||
regenerator-runtime@0.14.1: {}
|
regenerator-runtime@0.14.1: {}
|
||||||
|
|
||||||
|
require-directory@2.1.1: {}
|
||||||
|
|
||||||
resolve-from@4.0.0: {}
|
resolve-from@4.0.0: {}
|
||||||
|
|
||||||
resolve@1.22.8:
|
resolve@1.22.8:
|
||||||
@ -3474,6 +3571,15 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
glob: 7.2.3
|
glob: 7.2.3
|
||||||
|
|
||||||
|
rollup-plugin-visualizer@5.12.0(rollup@4.18.1):
|
||||||
|
dependencies:
|
||||||
|
open: 8.4.2
|
||||||
|
picomatch: 2.3.1
|
||||||
|
source-map: 0.7.4
|
||||||
|
yargs: 17.7.2
|
||||||
|
optionalDependencies:
|
||||||
|
rollup: 4.18.1
|
||||||
|
|
||||||
rollup@4.18.1:
|
rollup@4.18.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.5
|
'@types/estree': 1.0.5
|
||||||
@ -3542,6 +3648,12 @@ snapshots:
|
|||||||
|
|
||||||
stream-slice@0.1.2: {}
|
stream-slice@0.1.2: {}
|
||||||
|
|
||||||
|
string-width@4.2.3:
|
||||||
|
dependencies:
|
||||||
|
emoji-regex: 8.0.0
|
||||||
|
is-fullwidth-code-point: 3.0.0
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
|
||||||
strip-ansi@6.0.1:
|
strip-ansi@6.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
ansi-regex: 5.0.1
|
ansi-regex: 5.0.1
|
||||||
@ -3669,12 +3781,32 @@ snapshots:
|
|||||||
|
|
||||||
word-wrap@1.2.5: {}
|
word-wrap@1.2.5: {}
|
||||||
|
|
||||||
|
wrap-ansi@7.0.0:
|
||||||
|
dependencies:
|
||||||
|
ansi-styles: 4.3.0
|
||||||
|
string-width: 4.2.3
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
|
||||||
wrappy@1.0.2: {}
|
wrappy@1.0.2: {}
|
||||||
|
|
||||||
|
y18n@5.0.8: {}
|
||||||
|
|
||||||
yallist@3.1.1: {}
|
yallist@3.1.1: {}
|
||||||
|
|
||||||
yaml@1.10.2: {}
|
yaml@1.10.2: {}
|
||||||
|
|
||||||
|
yargs-parser@21.1.1: {}
|
||||||
|
|
||||||
|
yargs@17.7.2:
|
||||||
|
dependencies:
|
||||||
|
cliui: 8.0.1
|
||||||
|
escalade: 3.1.2
|
||||||
|
get-caller-file: 2.0.5
|
||||||
|
require-directory: 2.1.1
|
||||||
|
string-width: 4.2.3
|
||||||
|
y18n: 5.0.8
|
||||||
|
yargs-parser: 21.1.1
|
||||||
|
|
||||||
yocto-queue@0.1.0: {}
|
yocto-queue@0.1.0: {}
|
||||||
|
|
||||||
zod@3.23.8: {}
|
zod@3.23.8: {}
|
||||||
|
|||||||
@ -12,5 +12,11 @@
|
|||||||
|
|
||||||
"Log in": "Anmelden",
|
"Log in": "Anmelden",
|
||||||
"Log out": "Abmelden",
|
"Log out": "Abmelden",
|
||||||
"Profile": "Profil"
|
"Profile": "Profil",
|
||||||
|
|
||||||
|
"Updating": "Aktualisiert",
|
||||||
|
|
||||||
|
"Username": "Benutzername",
|
||||||
|
"Member since": "Mitglied seit",
|
||||||
|
"Post count": "Anzahl Posts"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,5 +12,11 @@
|
|||||||
|
|
||||||
"Log in": "Log in",
|
"Log in": "Log in",
|
||||||
"Log out": "Log out",
|
"Log out": "Log out",
|
||||||
"Profile": "Profile"
|
"Profile": "Profile",
|
||||||
|
|
||||||
|
"Updating": "Updating",
|
||||||
|
|
||||||
|
"Username": "Username",
|
||||||
|
"Member since": "Member since",
|
||||||
|
"Post count": "Post count"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,8 +6,9 @@ const BASE = 'https://khofmann.userpage.fu-berlin.de/phpCourse/exam/api/';
|
|||||||
let instance: ApiImpl;
|
let instance: ApiImpl;
|
||||||
|
|
||||||
class ApiImpl {
|
class ApiImpl {
|
||||||
//FIXME: PRIVATE when reauth token exists
|
private token?: string;
|
||||||
public token?: string;
|
private refreshToken?: string;
|
||||||
|
private self?: User;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
if (instance) {
|
if (instance) {
|
||||||
@ -19,18 +20,26 @@ class ApiImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public hasAuth = () => this.token !== undefined;
|
public hasAuth = () => this.token !== undefined;
|
||||||
|
public isAdmin = () => this.hasAuth() && this.self?.isAdmin;
|
||||||
|
public getAuthenticatedUser = () => this.self;
|
||||||
|
public getCurrentSession = () => [this.token, this.refreshToken];
|
||||||
|
|
||||||
//FIXME: Currently returns Auth token, switch to reauth token
|
public logIn = async (email: string, password: string): Promise<void> => {
|
||||||
public logIn = async (email: string, password: string): Promise<[User, string]> => {
|
|
||||||
const { user, token } = await (await this.post('login', { email, password })).json();
|
const { user, token } = await (await this.post('login', { email, password })).json();
|
||||||
|
this.self = user;
|
||||||
|
this.isAdmin = user.isAdmin;
|
||||||
this.token = token;
|
this.token = token;
|
||||||
|
|
||||||
return [user, token];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public logOut = async (): Promise<boolean> => {
|
public logOut = async (): Promise<boolean> => {
|
||||||
this.token = undefined;
|
try {
|
||||||
return await (await this.postAuth('logout')).json();
|
return await (await this.postAuth('logout')).json();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
this.self = undefined;
|
||||||
|
this.token = undefined;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public posts = async (page?: number): Promise<PostListNonAuth | PostListAuth> => {
|
public posts = async (page?: number): Promise<PostListNonAuth | PostListAuth> => {
|
||||||
@ -41,6 +50,10 @@ class ApiImpl {
|
|||||||
return await (await this.get(url)).json();
|
return await (await this.get(url)).json();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public user = async (id?: number): Promise<User> => {
|
||||||
|
return await (await this.getAuth(`users/${id ?? this.self?.id}`)).json();
|
||||||
|
};
|
||||||
|
|
||||||
private post = async (
|
private post = async (
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
body: Record<string, unknown> | undefined = undefined,
|
body: Record<string, unknown> | undefined = undefined,
|
||||||
|
|||||||
@ -4,13 +4,14 @@ import { useRouter } from '@tanstack/react-router';
|
|||||||
import { FC, useState } from 'react';
|
import { FC, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import Api from '../../../api/Api';
|
import Api from '../../../api/Api';
|
||||||
import useGuestBookStore from '../../../store/store';
|
|
||||||
import handleError from '../../../utils/errors';
|
import handleError from '../../../utils/errors';
|
||||||
|
|
||||||
const Login: FC = () => {
|
interface Props {
|
||||||
const [error, setError] = useState();
|
handleClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
const setUser = useGuestBookStore((state) => state.setUser);
|
const LoginForm: FC<Props> = ({ handleClose }) => {
|
||||||
|
const [error, setError] = useState();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -22,9 +23,9 @@ const Login: FC = () => {
|
|||||||
},
|
},
|
||||||
onSubmit: async ({ value }) => {
|
onSubmit: async ({ value }) => {
|
||||||
try {
|
try {
|
||||||
const [user, token] = await Api.logIn(value.email, value.password);
|
await Api.logIn(value.email, value.password);
|
||||||
setUser(user, token);
|
|
||||||
router.invalidate();
|
router.invalidate();
|
||||||
|
handleClose();
|
||||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setError(error);
|
setError(error);
|
||||||
@ -126,4 +127,4 @@ const Login: FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Login;
|
export default LoginForm;
|
||||||
@ -1,22 +1,20 @@
|
|||||||
import { AccountCircle } from '@mui/icons-material';
|
import { AccountCircle, Translate } from '@mui/icons-material';
|
||||||
import {
|
import {
|
||||||
AppBar,
|
AppBar,
|
||||||
Avatar,
|
Avatar,
|
||||||
Box,
|
Box,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
IconButton,
|
IconButton,
|
||||||
Menu,
|
|
||||||
MenuItem,
|
|
||||||
Link as MUILink,
|
Link as MUILink,
|
||||||
Toolbar,
|
Toolbar,
|
||||||
useScrollTrigger,
|
useScrollTrigger,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Link, useRouter, useRouterState } from '@tanstack/react-router';
|
import { Link, useRouterState } from '@tanstack/react-router';
|
||||||
import { cloneElement, FC, ReactElement, useState } from 'react';
|
import { cloneElement, FC, ReactElement, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import Api from '../../api/Api';
|
import Api from '../../api/Api';
|
||||||
import useGuestBookStore from '../../store/store';
|
import LanguageMenu from '../Menus/Language/LanguageMenu';
|
||||||
import Login from '../Forms/Login/Login';
|
import UserMenu from '../Menus/User/UserMenu';
|
||||||
|
|
||||||
const ElevationScroll = ({ children }: { children: ReactElement }) => {
|
const ElevationScroll = ({ children }: { children: ReactElement }) => {
|
||||||
const trigger = useScrollTrigger({
|
const trigger = useScrollTrigger({
|
||||||
@ -30,21 +28,17 @@ const ElevationScroll = ({ children }: { children: ReactElement }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Header: FC = () => {
|
const Header: FC = () => {
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
const [anchorUserMenu, setAnchorUserMenu] = useState<null | HTMLElement>(null);
|
||||||
|
const [anchorLanguageMenu, setAnchorLanguageMenu] = useState<null | HTMLElement>(null);
|
||||||
const user = useGuestBookStore((state) => state.user);
|
|
||||||
const setUser = useGuestBookStore((state) => state.setUser);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
|
||||||
const isLoading = useRouterState({ select: (s) => s.status === 'pending' });
|
const isLoading = useRouterState({ select: (s) => s.status === 'pending' });
|
||||||
|
|
||||||
const handleMenu = (event: React.MouseEvent<HTMLElement>) => {
|
const user = Api.getAuthenticatedUser();
|
||||||
setAnchorEl(event.currentTarget);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setAnchorEl(null);
|
setAnchorLanguageMenu(null);
|
||||||
|
setAnchorUserMenu(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -58,55 +52,20 @@ const Header: FC = () => {
|
|||||||
</MUILink>
|
</MUILink>
|
||||||
{isLoading && <CircularProgress size={16} thickness={10} sx={{ color: 'white' }} />}
|
{isLoading && <CircularProgress size={16} thickness={10} sx={{ color: 'white' }} />}
|
||||||
</Box>
|
</Box>
|
||||||
|
<IconButton size="large" onClick={(event) => setAnchorLanguageMenu(event.currentTarget)}>
|
||||||
|
<Translate sx={{ color: 'white' }} />
|
||||||
|
</IconButton>
|
||||||
{user ? (
|
{user ? (
|
||||||
<IconButton onClick={handleMenu} sx={{ p: 0 }}>
|
<IconButton onClick={(event) => setAnchorUserMenu(event.currentTarget)} sx={{ p: 0 }}>
|
||||||
<Avatar alt={user.username} src={`storage/${user.image}`} />
|
<Avatar alt={user.username} src={`storage/${user.image}`} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
) : (
|
) : (
|
||||||
<IconButton size="large" onClick={handleMenu} color="inherit">
|
<IconButton size="large" onClick={(event) => setAnchorUserMenu(event.currentTarget)} color="inherit">
|
||||||
<AccountCircle />
|
<AccountCircle />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
<Menu
|
<LanguageMenu anchorEl={anchorLanguageMenu} handleClose={handleClose} />
|
||||||
id="menu-appbar"
|
<UserMenu anchorEl={anchorUserMenu} handleClose={handleClose} />
|
||||||
anchorEl={anchorEl}
|
|
||||||
anchorOrigin={{
|
|
||||||
vertical: 'bottom',
|
|
||||||
horizontal: 'right',
|
|
||||||
}}
|
|
||||||
keepMounted
|
|
||||||
transformOrigin={{
|
|
||||||
vertical: 'top',
|
|
||||||
horizontal: 'right',
|
|
||||||
}}
|
|
||||||
open={Boolean(anchorEl)}
|
|
||||||
onClose={handleClose}
|
|
||||||
sx={{
|
|
||||||
'& .MuiMenu-paper': {
|
|
||||||
minWidth: '240px',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{user ? (
|
|
||||||
[
|
|
||||||
<MenuItem key="profile" onClick={handleClose}>
|
|
||||||
{t('Profile')}
|
|
||||||
</MenuItem>,
|
|
||||||
<MenuItem
|
|
||||||
key="logout"
|
|
||||||
onClick={() => {
|
|
||||||
Api.logOut();
|
|
||||||
setUser();
|
|
||||||
router.invalidate();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('Log out')}
|
|
||||||
</MenuItem>,
|
|
||||||
]
|
|
||||||
) : (
|
|
||||||
<Login />
|
|
||||||
)}
|
|
||||||
</Menu>
|
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
<Toolbar />
|
<Toolbar />
|
||||||
|
|||||||
55
exam/react/src/components/Menus/Language/LanguageMenu.tsx
Normal file
55
exam/react/src/components/Menus/Language/LanguageMenu.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { Menu, MenuItem } from '@mui/material';
|
||||||
|
import { FC } from 'react';
|
||||||
|
import i18n from '../../../i18n';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
anchorEl: HTMLElement | null;
|
||||||
|
handleClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LanguageMenu: FC<Props> = ({ anchorEl, handleClose }) => {
|
||||||
|
return (
|
||||||
|
<Menu
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: 'right',
|
||||||
|
}}
|
||||||
|
keepMounted
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'right',
|
||||||
|
}}
|
||||||
|
open={Boolean(anchorEl)}
|
||||||
|
onClose={handleClose}
|
||||||
|
sx={{
|
||||||
|
'& .MuiMenu-paper': {
|
||||||
|
minWidth: '240px',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
key="de"
|
||||||
|
selected={i18n.language === 'en'}
|
||||||
|
onClick={() => {
|
||||||
|
i18n.changeLanguage('en');
|
||||||
|
handleClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
English
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
key="en"
|
||||||
|
selected={i18n.language === 'de'}
|
||||||
|
onClick={() => {
|
||||||
|
i18n.changeLanguage('de');
|
||||||
|
handleClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Deutsch
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LanguageMenu;
|
||||||
69
exam/react/src/components/Menus/User/UserMenu.tsx
Normal file
69
exam/react/src/components/Menus/User/UserMenu.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { Menu, MenuItem } from '@mui/material';
|
||||||
|
import { useNavigate, useRouter } from '@tanstack/react-router';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
import { FC } from 'react';
|
||||||
|
import Api from '../../../api/Api';
|
||||||
|
import { ROUTES } from '../../../types/Routes';
|
||||||
|
import LoginForm from '../../Forms/Login/LoginForm';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
anchorEl: HTMLElement | null;
|
||||||
|
handleClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserMenu: FC<Props> = ({ anchorEl, handleClose }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const user = Api.getAuthenticatedUser();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: 'right',
|
||||||
|
}}
|
||||||
|
keepMounted
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'right',
|
||||||
|
}}
|
||||||
|
open={Boolean(anchorEl)}
|
||||||
|
onClose={handleClose}
|
||||||
|
sx={{
|
||||||
|
'& .MuiMenu-paper': {
|
||||||
|
minWidth: '240px',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{user ? (
|
||||||
|
[
|
||||||
|
<MenuItem
|
||||||
|
key="profile"
|
||||||
|
onClick={() => {
|
||||||
|
navigate({ to: ROUTES.PROFILE });
|
||||||
|
handleClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Profile')}
|
||||||
|
</MenuItem>,
|
||||||
|
<MenuItem
|
||||||
|
key="logout"
|
||||||
|
onClick={() => {
|
||||||
|
Api.logOut();
|
||||||
|
router.invalidate();
|
||||||
|
handleClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Log out')}
|
||||||
|
</MenuItem>,
|
||||||
|
]
|
||||||
|
) : (
|
||||||
|
<LoginForm handleClose={handleClose} />
|
||||||
|
)}
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserMenu;
|
||||||
@ -1,7 +1,9 @@
|
|||||||
import { Avatar, Card, CardContent, CardHeader, Link as MUILink, Typography } from '@mui/material';
|
import { Avatar, Card, CardContent, CardHeader, Link as MUILink, Typography } from '@mui/material';
|
||||||
import { Link } from '@tanstack/react-router';
|
import { Link } from '@tanstack/react-router';
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
|
import Api from '../../api/Api';
|
||||||
import { PostAuth, PostNonAuth } from '../../types/Post';
|
import { PostAuth, PostNonAuth } from '../../types/Post';
|
||||||
|
import convertDate from '../../utils/date';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
post: PostNonAuth | PostAuth;
|
post: PostNonAuth | PostAuth;
|
||||||
@ -12,21 +14,37 @@ const Post: FC<Props> = ({ post }) => {
|
|||||||
<Card sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1 }}>
|
<Card sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1 }}>
|
||||||
<CardHeader
|
<CardHeader
|
||||||
avatar={
|
avatar={
|
||||||
<MUILink component={Link} to="/" color="#FFF" variant="h6" underline="none">
|
'id' in post.user ? (
|
||||||
|
<MUILink
|
||||||
|
component={Link}
|
||||||
|
to="/profile/$id"
|
||||||
|
params={{ id: post.user.id }}
|
||||||
|
color="#FFF"
|
||||||
|
variant="h6"
|
||||||
|
underline="none"
|
||||||
|
>
|
||||||
|
<Avatar alt={post.user.username} src={`storage/${post.user.image}`} />
|
||||||
|
</MUILink>
|
||||||
|
) : (
|
||||||
<Avatar alt={post.user.username} src={`storage/${post.user.image}`} />
|
<Avatar alt={post.user.username} src={`storage/${post.user.image}`} />
|
||||||
</MUILink>
|
)
|
||||||
}
|
}
|
||||||
title={post.user.username}
|
title={
|
||||||
subheader={new Date(post.postedAt.date).toLocaleString(navigator.languages[0] ?? 'de-DE', {
|
'id' in post.user ? (
|
||||||
timeZone: post.postedAt.timezone,
|
post.user.id !== Api.getAuthenticatedUser()?.id ? (
|
||||||
weekday: 'short',
|
<MUILink component={Link} to="/profile/$id" params={{ id: post.user.id }}>
|
||||||
day: '2-digit',
|
{post.user.username}
|
||||||
month: '2-digit',
|
</MUILink>
|
||||||
year: 'numeric',
|
) : (
|
||||||
hour: '2-digit',
|
<MUILink component={Link} to="/profile">
|
||||||
hour12: false,
|
{post.user.username}
|
||||||
minute: '2-digit',
|
</MUILink>
|
||||||
})}
|
)
|
||||||
|
) : (
|
||||||
|
post.user.username
|
||||||
|
)
|
||||||
|
}
|
||||||
|
subheader={convertDate(post.postedAt)}
|
||||||
/>
|
/>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Typography>{post.content}</Typography>
|
<Typography>{post.content}</Typography>
|
||||||
|
|||||||
34
exam/react/src/components/Profile/Profile.tsx
Normal file
34
exam/react/src/components/Profile/Profile.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { Avatar, Box, Grid, Typography } from '@mui/material';
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { User } from '../../types/User';
|
||||||
|
import convertDate from '../../utils/date';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Profile: FC<Props> = ({ user }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid item sx={{ display: 'grid', gridTemplateColumns: 'fit-content(100%) 1fr', columnGap: 2, rowGap: 1 }}>
|
||||||
|
<Box sx={{ gridColumn: '1/3', display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<Avatar alt={user.username} src={`storage/${user.image}`} sx={{ width: 100, height: 100 }} />
|
||||||
|
</Box>
|
||||||
|
<Typography fontWeight="bold">{t('Username')}:</Typography>
|
||||||
|
<Typography>{user.username}</Typography>
|
||||||
|
<Typography fontWeight="bold">{t('Member since')}:</Typography>
|
||||||
|
<Typography>{convertDate(user.memberSince)}</Typography>
|
||||||
|
<Typography fontWeight="bold">{t('Post count')}:</Typography>
|
||||||
|
<Typography>{user.postCount}</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item></Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Profile;
|
||||||
@ -12,6 +12,8 @@
|
|||||||
|
|
||||||
import { Route as rootRoute } from './routes/__root'
|
import { Route as rootRoute } from './routes/__root'
|
||||||
import { Route as IndexImport } from './routes/index'
|
import { Route as IndexImport } from './routes/index'
|
||||||
|
import { Route as ProfileIndexImport } from './routes/profile/index'
|
||||||
|
import { Route as ProfileIdImport } from './routes/profile/$id'
|
||||||
|
|
||||||
// Create/Update Routes
|
// Create/Update Routes
|
||||||
|
|
||||||
@ -20,6 +22,16 @@ const IndexRoute = IndexImport.update({
|
|||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
|
const ProfileIndexRoute = ProfileIndexImport.update({
|
||||||
|
path: '/profile/',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
const ProfileIdRoute = ProfileIdImport.update({
|
||||||
|
path: '/profile/$id',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
// Populate the FileRoutesByPath interface
|
// Populate the FileRoutesByPath interface
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
@ -31,12 +43,30 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof IndexImport
|
preLoaderRoute: typeof IndexImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
|
'/profile/$id': {
|
||||||
|
id: '/profile/$id'
|
||||||
|
path: '/profile/$id'
|
||||||
|
fullPath: '/profile/$id'
|
||||||
|
preLoaderRoute: typeof ProfileIdImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
|
'/profile/': {
|
||||||
|
id: '/profile/'
|
||||||
|
path: '/profile'
|
||||||
|
fullPath: '/profile'
|
||||||
|
preLoaderRoute: typeof ProfileIndexImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create and export the route tree
|
// Create and export the route tree
|
||||||
|
|
||||||
export const routeTree = rootRoute.addChildren({ IndexRoute })
|
export const routeTree = rootRoute.addChildren({
|
||||||
|
IndexRoute,
|
||||||
|
ProfileIdRoute,
|
||||||
|
ProfileIndexRoute,
|
||||||
|
})
|
||||||
|
|
||||||
/* prettier-ignore-end */
|
/* prettier-ignore-end */
|
||||||
|
|
||||||
@ -46,11 +76,19 @@ export const routeTree = rootRoute.addChildren({ IndexRoute })
|
|||||||
"__root__": {
|
"__root__": {
|
||||||
"filePath": "__root.tsx",
|
"filePath": "__root.tsx",
|
||||||
"children": [
|
"children": [
|
||||||
"/"
|
"/",
|
||||||
|
"/profile/$id",
|
||||||
|
"/profile/"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"/": {
|
"/": {
|
||||||
"filePath": "index.tsx"
|
"filePath": "index.tsx"
|
||||||
|
},
|
||||||
|
"/profile/$id": {
|
||||||
|
"filePath": "profile/$id.tsx"
|
||||||
|
},
|
||||||
|
"/profile/": {
|
||||||
|
"filePath": "profile/index.tsx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,26 +1,53 @@
|
|||||||
import { Toolbar } from '@mui/material';
|
import { QueryClient, useQueryErrorResetBoundary } from '@tanstack/react-query';
|
||||||
import { QueryClient } from '@tanstack/react-query';
|
import { createRootRouteWithContext, ErrorRouteComponent, Outlet, redirect, useRouter } from '@tanstack/react-router';
|
||||||
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router';
|
|
||||||
import { TanStackRouterDevtools } from '@tanstack/router-devtools';
|
import { TanStackRouterDevtools } from '@tanstack/router-devtools';
|
||||||
|
import { useEffect } from 'react';
|
||||||
import Api from '../api/Api';
|
import Api from '../api/Api';
|
||||||
import Header from '../components/Header/Header';
|
import Header from '../components/Header/Header';
|
||||||
import useGuestBookStore from '../store/store';
|
import { ROUTES } from '../types/Routes';
|
||||||
|
import { ERRORS } from '../utils/errors';
|
||||||
|
|
||||||
const Root = () => {
|
const Root = () => {
|
||||||
//FIXME: REAUTH HERE
|
//TODO: REAUTH HERE
|
||||||
const token = useGuestBookStore((state) => state.token);
|
|
||||||
Api.token = token;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header />
|
<Header />
|
||||||
<Toolbar />
|
|
||||||
<Outlet />
|
<Outlet />
|
||||||
{process.env.NODE_ENV === 'development' && <TanStackRouterDevtools />}
|
{process.env.NODE_ENV === 'development' && <TanStackRouterDevtools />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//TODO: Make nice
|
||||||
|
const Error: ErrorRouteComponent = ({ error }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const queryErrorResetBoundary = useQueryErrorResetBoundary();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Reset the query error boundary
|
||||||
|
queryErrorResetBoundary.reset();
|
||||||
|
}, [queryErrorResetBoundary]);
|
||||||
|
|
||||||
|
if ('code' in error && error.code === ERRORS.UNAUTHORIZED) {
|
||||||
|
Api.logOut();
|
||||||
|
redirect({ to: ROUTES.INDEX });
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{error.message}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
router.invalidate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({
|
export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({
|
||||||
component: Root,
|
component: Root,
|
||||||
|
errorComponent: Error,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import { Grid, Pagination, PaginationItem, Snackbar } from '@mui/material';
|
import { Grid, Pagination, PaginationItem, Snackbar } from '@mui/material';
|
||||||
import { queryOptions, useSuspenseQuery } from '@tanstack/react-query';
|
import { queryOptions, useSuspenseQuery } from '@tanstack/react-query';
|
||||||
import { createFileRoute, Link } from '@tanstack/react-router';
|
import { createFileRoute, Link } from '@tanstack/react-router';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import Api from '../api/Api';
|
import Api from '../api/Api';
|
||||||
import Post from '../components/Post/Post';
|
import Post from '../components/Post/Post';
|
||||||
|
import { ROUTES } from '../types/Routes';
|
||||||
|
|
||||||
const postsQueryOptions = (page?: number) =>
|
const postsQueryOptions = (page?: number) =>
|
||||||
queryOptions({
|
queryOptions({
|
||||||
@ -10,28 +12,18 @@ const postsQueryOptions = (page?: number) =>
|
|||||||
queryFn: () => Api.posts(page),
|
queryFn: () => Api.posts(page),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Route = createFileRoute('/')({
|
const Home = () => {
|
||||||
loaderDeps: ({ search: { page } }) => ({ page }),
|
|
||||||
loader: ({ context: { queryClient }, deps: { page } }) => queryClient.ensureQueryData(postsQueryOptions(page)),
|
|
||||||
validateSearch: (search: Record<string, unknown>): { page?: number } => {
|
|
||||||
return {
|
|
||||||
page: search?.page !== undefined ? Number(search?.page ?? 0) : undefined,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
component: Home,
|
|
||||||
});
|
|
||||||
|
|
||||||
function Home() {
|
|
||||||
const { page } = Route.useSearch();
|
const { page } = Route.useSearch();
|
||||||
const { data: postsQuery, isFetching } = useSuspenseQuery(postsQueryOptions(page));
|
const { data: postsQuery, isFetching } = useSuspenseQuery(postsQueryOptions(page));
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Snackbar open={isFetching} message="Updating" />
|
<Snackbar open={isFetching} message={t('Updating')} />
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
{postsQuery.data.map((post) => (
|
{postsQuery.data.map((post) => (
|
||||||
<Grid item xs={12} md={6} lg={4} sx={{ display: 'flex' }}>
|
<Grid key={post.id} item xs={12} md={6} lg={4} sx={{ display: 'flex' }}>
|
||||||
<Post key={post.id} post={post} />
|
<Post post={post} />
|
||||||
</Grid>
|
</Grid>
|
||||||
))}
|
))}
|
||||||
<Grid item xs={12} sx={{ display: 'flex', justifyContent: 'center' }}>
|
<Grid item xs={12} sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
@ -44,7 +36,7 @@ function Home() {
|
|||||||
{...item}
|
{...item}
|
||||||
component={Link}
|
component={Link}
|
||||||
to="/"
|
to="/"
|
||||||
search={{ page: item.page ? item.page - 1 : undefined }}
|
search={{ page: (item.page ?? 0) > 0 ? (item.page ?? 1) - 1 : undefined }}
|
||||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
onClick={(e) => item.onClick(e as any)}
|
onClick={(e) => item.onClick(e as any)}
|
||||||
/>
|
/>
|
||||||
@ -54,4 +46,15 @@ function Home() {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export const Route = createFileRoute(ROUTES.INDEX)({
|
||||||
|
loaderDeps: ({ search: { page } }) => ({ page }),
|
||||||
|
loader: ({ context: { queryClient }, deps: { page } }) => queryClient.ensureQueryData(postsQueryOptions(page)),
|
||||||
|
validateSearch: (search: Record<string, unknown>): { page?: number } => {
|
||||||
|
return {
|
||||||
|
page: search?.page !== undefined ? Number(search?.page ?? 0) : undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
component: Home,
|
||||||
|
});
|
||||||
|
|||||||
37
exam/react/src/routes/profile/$id.tsx
Normal file
37
exam/react/src/routes/profile/$id.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { Snackbar } from '@mui/material';
|
||||||
|
import { queryOptions, useSuspenseQuery } from '@tanstack/react-query';
|
||||||
|
import { createFileRoute, redirect } from '@tanstack/react-router';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
import Api from '../../api/Api';
|
||||||
|
import Profile from '../../components/Profile/Profile';
|
||||||
|
import { ROUTES } from '../../types/Routes';
|
||||||
|
|
||||||
|
const profileQueryOptions = (id?: number) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ['profile', { id }],
|
||||||
|
queryFn: () => Api.user(id),
|
||||||
|
});
|
||||||
|
|
||||||
|
const ProfilePage = () => {
|
||||||
|
const { id } = Route.useParams();
|
||||||
|
const { data: profileQuery, isFetching } = useSuspenseQuery(profileQueryOptions(id));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Snackbar open={isFetching} message={t('Updating')} />
|
||||||
|
<Profile user={profileQuery} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Route = createFileRoute(`${ROUTES.PROFILE}/$id`)({
|
||||||
|
params: {
|
||||||
|
parse: ({ id }) => ({ id: parseInt(id) }),
|
||||||
|
stringify: ({ id }) => ({ id: id.toString() }),
|
||||||
|
},
|
||||||
|
loader: ({ context: { queryClient }, params: { id } }) => queryClient.ensureQueryData(profileQueryOptions(id)),
|
||||||
|
beforeLoad: () => {
|
||||||
|
if (!Api.hasAuth()) throw redirect({ to: ROUTES.INDEX });
|
||||||
|
},
|
||||||
|
component: ProfilePage,
|
||||||
|
});
|
||||||
31
exam/react/src/routes/profile/index.tsx
Normal file
31
exam/react/src/routes/profile/index.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { Snackbar } from '@mui/material';
|
||||||
|
import { queryOptions, useSuspenseQuery } from '@tanstack/react-query';
|
||||||
|
import { createFileRoute, redirect } from '@tanstack/react-router';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
import Api from '../../api/Api';
|
||||||
|
import Profile from '../../components/Profile/Profile';
|
||||||
|
import { ROUTES } from '../../types/Routes';
|
||||||
|
|
||||||
|
const profileQueryOptions = queryOptions({
|
||||||
|
queryKey: ['profile'],
|
||||||
|
queryFn: () => Api.user(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const ProfilePage = () => {
|
||||||
|
const { data: profileQuery, isFetching } = useSuspenseQuery(profileQueryOptions);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Snackbar open={isFetching} message={t('Updating')} />
|
||||||
|
<Profile user={profileQuery} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Route = createFileRoute(`${ROUTES.PROFILE}/`)({
|
||||||
|
loader: ({ context: { queryClient } }) => queryClient.ensureQueryData(profileQueryOptions),
|
||||||
|
beforeLoad: () => {
|
||||||
|
if (!Api.hasAuth()) throw redirect({ to: ROUTES.INDEX });
|
||||||
|
},
|
||||||
|
component: ProfilePage,
|
||||||
|
});
|
||||||
@ -1,27 +1,14 @@
|
|||||||
import type {} from '@redux-devtools/extension';
|
import type {} from '@redux-devtools/extension';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { devtools, persist } from 'zustand/middleware';
|
import { devtools, persist } from 'zustand/middleware';
|
||||||
import { User } from '../types/User';
|
|
||||||
|
|
||||||
interface GuestBookState {
|
interface GuestBookState {}
|
||||||
user: User | undefined;
|
|
||||||
token: string | undefined;
|
|
||||||
setUser: (user?: User, token?: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const useGuestBookStore = create<GuestBookState>()(
|
const useGuestBookStore = create<GuestBookState>()(
|
||||||
devtools(
|
devtools(
|
||||||
persist(
|
persist(() => ({}), {
|
||||||
(set) => ({
|
name: 'guestbook-storage',
|
||||||
user: undefined,
|
})
|
||||||
// FIXME: Currentlay auth token, switch this to be reauth token
|
|
||||||
token: undefined,
|
|
||||||
setUser: (user?: User, token?: string) => set(() => ({ user, token })),
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: 'guestbook-storage',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
4
exam/react/src/types/Routes.ts
Normal file
4
exam/react/src/types/Routes.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export enum ROUTES {
|
||||||
|
INDEX = '/',
|
||||||
|
PROFILE = '/profile',
|
||||||
|
}
|
||||||
20
exam/react/src/utils/date.ts
Normal file
20
exam/react/src/utils/date.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Timestamp } from '../types/Timestamp';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert date to nicer format
|
||||||
|
* @param date Timestamp in question
|
||||||
|
* @returns Formatted string
|
||||||
|
*/
|
||||||
|
const convertDate = (date: Timestamp): string => {
|
||||||
|
return new Date(date.date).toLocaleString(navigator.languages[0] ?? 'de-DE', {
|
||||||
|
timeZone: date.timezone,
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default convertDate;
|
||||||
@ -1,5 +1,10 @@
|
|||||||
import { TFunction } from 'i18next';
|
import { TFunction } from 'i18next';
|
||||||
|
|
||||||
|
export enum ERRORS {
|
||||||
|
NOT_FOUND = 'NotFound',
|
||||||
|
UNAUTHORIZED = 'Unauthorized',
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return translated error
|
* Return translated error
|
||||||
* @param error Error object
|
* @param error Error object
|
||||||
@ -18,9 +23,9 @@ const handleError = (
|
|||||||
|
|
||||||
if (error.code) {
|
if (error.code) {
|
||||||
switch (error.code) {
|
switch (error.code) {
|
||||||
case 'NotFound':
|
case ERRORS.NOT_FOUND:
|
||||||
return t(error.code, { context: `${error.entity}:${context}` });
|
return t(error.code, { context: `${error.entity}:${context}` });
|
||||||
case 'Unauthorized':
|
case ERRORS.UNAUTHORIZED:
|
||||||
return t(error.code, { context });
|
return t(error.code, { context });
|
||||||
default:
|
default:
|
||||||
return t('Unknown', { context });
|
return t('Unknown', { context });
|
||||||
|
|||||||
@ -1,12 +1,33 @@
|
|||||||
import { TanStackRouterVite } from '@tanstack/router-plugin/vite';
|
import { TanStackRouterVite } from '@tanstack/router-plugin/vite';
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
|
import { visualizer } from 'rollup-plugin-visualizer';
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [TanStackRouterVite(), react()],
|
plugins: [TanStackRouterVite(), react(), visualizer({ emitFile: true, open: true, filename: 'stats.html' })],
|
||||||
build: {
|
build: {
|
||||||
outDir: '../dist',
|
outDir: '../dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks: (id) => {
|
||||||
|
if (id.includes('node_modules')) {
|
||||||
|
if (id.includes('@mui/material')) {
|
||||||
|
return 'vendor_mui';
|
||||||
|
}
|
||||||
|
if (id.includes('@tanstack')) {
|
||||||
|
return 'vendor_tanstack';
|
||||||
|
}
|
||||||
|
if (id.includes('react')) {
|
||||||
|
return 'vendor_react';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'vendor'; // all other package goes here
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
base: '/phpCourse/exam/dist',
|
base: '/phpCourse/exam/dist',
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user