mirror of
https://github.com/Funkoala14/KnowledgeBase_OOIN.git
synced 2025-06-08 05:38:13 +08:00
[dev]login&signup pages
This commit is contained in:
parent
f88a14ee31
commit
d9e0b78bee
106
package-lock.json
generated
106
package-lock.json
generated
@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@reduxjs/toolkit": "^2.6.0",
|
||||
"axios": "^1.8.1",
|
||||
"bootstrap": "^5.3.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
@ -1535,6 +1536,11 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/available-typed-arrays": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
||||
@ -1550,6 +1556,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz",
|
||||
"integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@ -1662,7 +1678,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
@ -1786,6 +1801,17 @@
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@ -1934,6 +1960,14 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/doctrine": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
||||
@ -1950,7 +1984,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
@ -2035,7 +2068,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
@ -2044,7 +2076,6 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
@ -2080,7 +2111,6 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
@ -2092,7 +2122,6 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
@ -2469,6 +2498,25 @@
|
||||
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/for-each": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
||||
@ -2484,6 +2532,20 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
||||
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@ -2502,7 +2564,6 @@
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
@ -2549,7 +2610,6 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
@ -2573,7 +2633,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
@ -2643,7 +2702,6 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@ -2703,7 +2761,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@ -2715,7 +2772,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
@ -2730,7 +2786,6 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
@ -3339,11 +3394,29 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
@ -3676,6 +3749,11 @@
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
|
@ -12,6 +12,7 @@
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@reduxjs/toolkit": "^2.6.0",
|
||||
"axios": "^1.8.1",
|
||||
"bootstrap": "^5.3.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
|
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" className="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
24
src/App.jsx
24
src/App.jsx
@ -1,7 +1,27 @@
|
||||
import HeaderWithNav from './layouts/HeaderWithNav';
|
||||
import AppRouter from './router';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppRouter from './router/router';
|
||||
import { checkAuthThunk } from './store/auth/auth.thunk';
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { login } from './store/auth/auth.slice';
|
||||
|
||||
function App() {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(login({id:111, username: 'test'}))
|
||||
// handleCheckAuth();
|
||||
}, [dispatch]);
|
||||
|
||||
const handleCheckAuth = async () => {
|
||||
console.log('app handleCheckAuth');
|
||||
await dispatch(checkAuthThunk()).unwrap();
|
||||
if (user) navigate('/');
|
||||
};
|
||||
|
||||
return <AppRouter></AppRouter>;
|
||||
}
|
||||
|
||||
|
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" className="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
@ -23,7 +23,7 @@ const Snackbar = ({ type = 'primary', message, duration = 3000, onClose }) => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`snackbar alert alert-${type} d-flex align-items-center justify-content-between position-fixed top-10 start-50 translate-middle w-50 alert-dismissible z-2`}
|
||||
className={`snackbar alert alert-${type} d-flex align-items-center justify-content-between position-fixed top-10 start-50 translate-middle w-50 alert-dismissible z-2 gap-2`}
|
||||
role='alert'
|
||||
>
|
||||
<SvgIcon className={icons[type]} />
|
||||
|
@ -1,13 +1,23 @@
|
||||
import React from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { logoutThunk } from '../store/auth/auth.thunk';
|
||||
|
||||
export default function HeaderWithNav() {
|
||||
const dispatch = useDispatch();
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
|
||||
const handleLogout = () => {
|
||||
dispatch(logoutThunk());
|
||||
};
|
||||
|
||||
return (
|
||||
<header className=' navbar navbar-expand-lg'>
|
||||
<nav className='navbar navbar-expand-lg border-bottom p-3 mb-3 w-100'>
|
||||
<div className='container-fluid'>
|
||||
<a className='navbar-brand' href='#'>
|
||||
<Link className='navbar-brand' to='#'>
|
||||
OOIN 智能知识库
|
||||
</a>
|
||||
</Link>
|
||||
<button
|
||||
className='navbar-toggler'
|
||||
type='button'
|
||||
@ -22,29 +32,79 @@ export default function HeaderWithNav() {
|
||||
<div className='collapse navbar-collapse' id='navbarText'>
|
||||
<ul className='navbar-nav me-auto mb-lg-0'>
|
||||
<li className='nav-item'>
|
||||
<a className='nav-link active' aria-current='page' href='#'>
|
||||
<Link className='nav-link active' aria-current='page' to='#'>
|
||||
知识库
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className='nav-item'>
|
||||
<a className='nav-link' href='#'>
|
||||
<Link className='nav-link' to='#'>
|
||||
Chat
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
<hr className='d-lg-none' />
|
||||
<ul className='navbar-nav mb-2 mb-lg-0'>
|
||||
<li className='nav-item'>
|
||||
<a className='nav-link text-dark' href='#'>
|
||||
Log in
|
||||
{!!user ? (
|
||||
<div className='flex-shrink-0 dropdown'>
|
||||
<a
|
||||
href='#'
|
||||
className='d-block link-dark text-decoration-none dropdown-toggle'
|
||||
data-bs-toggle='dropdown'
|
||||
aria-expanded='false'
|
||||
>
|
||||
{/* <img
|
||||
src='https://github.com/mdo.png'
|
||||
alt='mdo'
|
||||
width='32'
|
||||
height='32'
|
||||
className='rounded-circle'
|
||||
/> */}
|
||||
Hi, { user.username }
|
||||
</a>
|
||||
</li>
|
||||
<li className='nav-item'>
|
||||
<a className='nav-link text-dark' href='#'>
|
||||
Sign up
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul
|
||||
className='dropdown-menu text-small shadow'
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: '0px 0px auto auto',
|
||||
margin: '0px',
|
||||
transform: 'translate(0px, 34px)',
|
||||
}}
|
||||
>
|
||||
<li>
|
||||
<Link className='dropdown-item' to='#'>
|
||||
Settings
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link className='dropdown-item' to='#'>
|
||||
Profile
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<hr className='dropdown-divider' />
|
||||
</li>
|
||||
<li>
|
||||
<Link className='dropdown-item' to='#' onClick={handleLogout}>
|
||||
Sign out
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<hr className='d-lg-none' />
|
||||
<ul className='navbar-nav mb-2 mb-lg-0'>
|
||||
<li className='nav-item'>
|
||||
<Link className='nav-link text-dark' to='/login'>
|
||||
Log in
|
||||
</Link>
|
||||
</li>
|
||||
<li className='nav-item'>
|
||||
<Link className='nav-link text-dark' to='/signup'>
|
||||
Sign up
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
@ -29,12 +29,12 @@ export default function KnowledgeCard({ title, description, documents, date, acc
|
||||
</div>
|
||||
<div className='mt-3 d-flex justify-content-between align-items-end'>
|
||||
{access === 'full' ? (
|
||||
<span className='badge bg-success-subtle d-flex align-items-center gap-1'>
|
||||
<span className='badge bg-success-subtle text-success d-flex align-items-center gap-1'>
|
||||
<SvgIcon className={'circle-yes'} />
|
||||
完全访问
|
||||
</span>
|
||||
) : access === 'read' ? (
|
||||
<span className='badge bg-primary-subtle d-flex align-items-center gap-1'>
|
||||
<span className='badge bg-warning-subtle text-warning d-flex align-items-center gap-1'>
|
||||
<SvgIcon className={'eye'} />
|
||||
只读访问
|
||||
</span>
|
||||
|
96
src/pages/auth/Login.jsx
Normal file
96
src/pages/auth/Login.jsx
Normal file
@ -0,0 +1,96 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { checkAuthThunk, loginThunk } from '../../store/auth/auth.thunk';
|
||||
|
||||
export default function Login() {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [errors, setErrors] = useState({});
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
|
||||
useEffect(() => {
|
||||
handleCheckAuth();
|
||||
}, [dispatch]);
|
||||
|
||||
const handleCheckAuth = async () => {
|
||||
console.log('login page handleCheckAuth');
|
||||
await dispatch(checkAuthThunk()).unwrap();
|
||||
if (user) navigate('/');
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors = {};
|
||||
if (!username) {
|
||||
newErrors.username = 'Username is required';
|
||||
}
|
||||
if (!password) {
|
||||
newErrors.password = 'Password is required';
|
||||
} else if (password.length < 6) {
|
||||
newErrors.password = 'Password must be at least 6 characters';
|
||||
}
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
setSubmitted(true);
|
||||
console.log(validateForm());
|
||||
|
||||
if (validateForm()) {
|
||||
console.log('Form submitted successfully!');
|
||||
console.log('Username:', username);
|
||||
console.log('Password:', password);
|
||||
|
||||
dispatch(loginThunk({ username, password }));
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className='position-absolute top-50 start-50 translate-middle d-flex flex-column gap-4 align-items-center'>
|
||||
<div className='title text-center h1'>OOIN 智能知识库</div>
|
||||
<form
|
||||
className='auth-form login-form d-flex flex-column gap-3 align-items-center'
|
||||
onSubmit={handleSubmit}
|
||||
noValidate
|
||||
>
|
||||
<div className='input-group has-validation'>
|
||||
<input
|
||||
type='text'
|
||||
className={`form-control form-control-lg${submitted && errors.username ? ' is-invalid' : ''}`}
|
||||
id='username'
|
||||
placeholder='Username'
|
||||
required
|
||||
onChange={(e) => setUsername(e.target.value.trim())}
|
||||
></input>
|
||||
{submitted && errors.username && <div className='invalid-feedback'>{errors.username}</div>}
|
||||
</div>
|
||||
<div className='input-group has-validation'>
|
||||
<input
|
||||
type='password'
|
||||
id='password'
|
||||
placeholder='Password'
|
||||
required
|
||||
className={`form-control form-control-lg${submitted && errors.password ? ' is-invalid' : ''}`}
|
||||
aria-describedby='passwordHelpBlock'
|
||||
onChange={(e) => setPassword(e.target.value.trim())}
|
||||
></input>
|
||||
{submitted && errors.password && <div className='invalid-feedback'>{errors.password}</div>}
|
||||
</div>
|
||||
<Link to='#' className='find-password text-body-secondary'>
|
||||
Forgot password?
|
||||
</Link>
|
||||
<button type='submit' className='btn btn-dark btn-lg w-100'>
|
||||
Login
|
||||
</button>
|
||||
</form>
|
||||
<Link to='/signup' className='go-to-signup w-100 link-underline-light h5 text-center'>
|
||||
Need Account?
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
114
src/pages/auth/Signup.jsx
Normal file
114
src/pages/auth/Signup.jsx
Normal file
@ -0,0 +1,114 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { checkAuthThunk, signupThunk } from '../../store/auth/auth.thunk';
|
||||
|
||||
export default function Signup() {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const [username, setUsername] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [errors, setErrors] = useState({});
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
|
||||
useEffect(() => {
|
||||
handleCheckAuth();
|
||||
}, [dispatch]);
|
||||
|
||||
const handleCheckAuth = async () => {
|
||||
console.log('signup page handleCheckAuth');
|
||||
await dispatch(checkAuthThunk()).unwrap();
|
||||
if (user) navigate('/');
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors = {};
|
||||
if (!username) {
|
||||
newErrors.username = 'Username is required';
|
||||
}
|
||||
|
||||
if (!email) {
|
||||
newErrors.email = 'Email is required';
|
||||
} else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(email)) {
|
||||
newErrors.email = 'Invalid email address';
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
newErrors.password = 'Password is required';
|
||||
} else if (password.length < 6) {
|
||||
newErrors.password = 'Password must be at least 6 characters';
|
||||
}
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
setSubmitted(true);
|
||||
console.log(validateForm());
|
||||
|
||||
if (validateForm()) {
|
||||
console.log('Form submitted successfully!');
|
||||
console.log('Username:', username);
|
||||
console.log('Email:', email);
|
||||
console.log('Password:', password);
|
||||
|
||||
dispatch(signupThunk({ username, password, email }));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='position-absolute top-50 start-50 translate-middle d-flex flex-column gap-4 align-items-center'>
|
||||
<div className='title text-center h1'>OOIN 智能知识库</div>
|
||||
<form
|
||||
className='auth-form login-form d-flex flex-column gap-3 align-items-center'
|
||||
onSubmit={handleSubmit}
|
||||
noValidate
|
||||
>
|
||||
<div className='input-group has-validation'>
|
||||
<input
|
||||
type='text'
|
||||
className={`form-control form-control-lg${submitted && errors.username ? ' is-invalid' : ''}`}
|
||||
id='username'
|
||||
placeholder='Username'
|
||||
required
|
||||
onChange={(e) => setUsername(e.target.value.trim())}
|
||||
></input>
|
||||
{submitted && errors.username && <div className='invalid-feedback'>{errors.username}</div>}
|
||||
</div>
|
||||
<div className='input-group has-validation'>
|
||||
<input
|
||||
type='email'
|
||||
className={`form-control form-control-lg${submitted && errors.email ? ' is-invalid' : ''}`}
|
||||
id='email'
|
||||
placeholder='Email'
|
||||
required
|
||||
onChange={(e) => setEmail(e.target.value.trim())}
|
||||
></input>
|
||||
{submitted && errors.email && <div className='invalid-feedback'>{errors.email}</div>}
|
||||
</div>
|
||||
<div className='input-group has-validation'>
|
||||
<input
|
||||
type='password'
|
||||
id='password'
|
||||
placeholder='Password'
|
||||
required
|
||||
className={`form-control form-control-lg${submitted && errors.password ? ' is-invalid' : ''}`}
|
||||
aria-describedby='passwordHelpBlock'
|
||||
onChange={(e) => setPassword(e.target.value.trim())}
|
||||
></input>
|
||||
{submitted && errors.password && <div className='invalid-feedback'>{errors.password}</div>}
|
||||
</div>
|
||||
<button type='submit' className='btn btn-dark btn-lg w-100'>
|
||||
Sign Up
|
||||
</button>
|
||||
</form>
|
||||
<Link to='/login' className='go-to-signup w-100 link-underline-light h5 text-center'>
|
||||
Already have account?
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
import React, { Suspense } from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import Mainlayout from './layouts/Mainlayout';
|
||||
import KnowledgeBase from './pages/KnowledgeBase/KnowledgeBase';
|
||||
import Loading from './components/Loading';
|
||||
|
||||
function AppRouter() {
|
||||
return (
|
||||
// <Suspense fallback={<Loading />}>
|
||||
<Routes>
|
||||
<Route
|
||||
path='/'
|
||||
element={
|
||||
<Mainlayout>
|
||||
<KnowledgeBase />
|
||||
</Mainlayout>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
// </Suspense>
|
||||
);
|
||||
}
|
||||
export default AppRouter;
|
12
src/router/protectedRoute.jsx
Normal file
12
src/router/protectedRoute.jsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Navigate, Outlet } from 'react-router-dom';
|
||||
|
||||
function ProtectedRoute() {
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
|
||||
return <Outlet />
|
||||
// return !!user ? <Outlet /> : <Navigate to='/login' replace />;
|
||||
}
|
||||
|
||||
export default ProtectedRoute;
|
34
src/router/router.jsx
Normal file
34
src/router/router.jsx
Normal file
@ -0,0 +1,34 @@
|
||||
import React, { Suspense } from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
import Mainlayout from '../layouts/Mainlayout';
|
||||
import KnowledgeBase from '../pages/KnowledgeBase/KnowledgeBase';
|
||||
import Loading from '../components/Loading';
|
||||
import Login from '../pages/auth/Login';
|
||||
import Signup from '../pages/auth/Signup';
|
||||
import ProtectedRoute from './protectedRoute';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
function AppRouter() {
|
||||
const { id } = useSelector((state) => state.auth);
|
||||
|
||||
return (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Routes>
|
||||
<Route element={<ProtectedRoute />}>
|
||||
<Route
|
||||
path='/'
|
||||
element={
|
||||
<Mainlayout>
|
||||
<KnowledgeBase />
|
||||
</Mainlayout>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
<Route path='/login' element={<Login />} />
|
||||
<Route path='/signup' element={<Signup />} />
|
||||
<Route path='*' element={<Navigate to={!!id ? '/' : '/login'} replace />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
export default AppRouter;
|
59
src/store/auth/auth.slice.js
Normal file
59
src/store/auth/auth.slice.js
Normal file
@ -0,0 +1,59 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { checkAuthThunk, loginThunk, logoutThunk, signupThunk } from './auth.thunk';
|
||||
|
||||
const setPending = (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
state.user = null;
|
||||
};
|
||||
|
||||
const setFulfilled = (state, action) => {
|
||||
state.user = action.payload;
|
||||
state.loading = false;
|
||||
state.error = null;
|
||||
};
|
||||
|
||||
const setRejected = (state, action) => {
|
||||
state.error = action.payload;
|
||||
state.loading = false;
|
||||
};
|
||||
|
||||
const authSlice = createSlice({
|
||||
name: 'auth',
|
||||
initialState: { loading: false, error: null, user: {id: 123, username: 'test'} },
|
||||
reducers: {
|
||||
login: (state, action) => {
|
||||
state.user = action.payload;
|
||||
},
|
||||
logout: (state) => {
|
||||
state.user = null;
|
||||
state.error = null;
|
||||
state.loading = false;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(checkAuthThunk.pending, setPending)
|
||||
.addCase(checkAuthThunk.fulfilled, setFulfilled)
|
||||
.addCase(checkAuthThunk.rejected, setRejected)
|
||||
|
||||
.addCase(loginThunk.pending, setPending)
|
||||
.addCase(loginThunk.fulfilled, setFulfilled)
|
||||
.addCase(loginThunk.rejected, setRejected)
|
||||
|
||||
.addCase(signupThunk.pending, setPending)
|
||||
.addCase(signupThunk.fulfilled, setFulfilled)
|
||||
.addCase(signupThunk.rejected, setRejected)
|
||||
|
||||
.addCase(logoutThunk.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(logoutThunk.fulfilled)
|
||||
.addCase(logoutThunk.rejected, setRejected);
|
||||
},
|
||||
});
|
||||
|
||||
export const { login, logout } = authSlice.actions;
|
||||
const authReducer = authSlice.reducer;
|
||||
export default authReducer;
|
77
src/store/auth/auth.thunk.js
Normal file
77
src/store/auth/auth.thunk.js
Normal file
@ -0,0 +1,77 @@
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { get, post } from '../../services/api';
|
||||
import { showNotification } from '../notification.slice';
|
||||
import { logout } from './auth.slice';
|
||||
|
||||
export const loginThunk = createAsyncThunk(
|
||||
'auth/login',
|
||||
async ({ username, password }, { rejectWithValue, dispatch }) => {
|
||||
try {
|
||||
const { message, user } = await post('/login', { username, password });
|
||||
if (!user) {
|
||||
throw new Error(message || 'Something went wrong');
|
||||
}
|
||||
return user;
|
||||
} catch (error) {
|
||||
const errorMessage = error.response?.data?.message || 'Something went wrong';
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: errorMessage,
|
||||
type: 'danger',
|
||||
})
|
||||
);
|
||||
return rejectWithValue(errorMessage);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const signupThunk = createAsyncThunk('auth/signup', async (config, { rejectWithValue, dispatch }) => {
|
||||
try {
|
||||
const { message, user } = await post('/signup', config);
|
||||
if (!user) {
|
||||
throw new Error(message || 'Something went wrong');
|
||||
}
|
||||
return user;
|
||||
} catch (error) {
|
||||
const errorMessage = error.response?.data?.message || 'Signup failed. Please try again.';
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: errorMessage,
|
||||
type: 'danger',
|
||||
})
|
||||
);
|
||||
return rejectWithValue(errorMessage);
|
||||
}
|
||||
});
|
||||
|
||||
export const checkAuthThunk = createAsyncThunk('auth/check', async (_, { rejectWithValue, dispatch }) => {
|
||||
try {
|
||||
const { user, message } = await get('/check-token');
|
||||
if (!user) {
|
||||
dispatch(logout());
|
||||
throw new Error(message || 'No token found');
|
||||
}
|
||||
return user;
|
||||
} catch (error) {
|
||||
dispatch(logout());
|
||||
return rejectWithValue(error.response?.data || 'Token verification failed');
|
||||
}
|
||||
});
|
||||
|
||||
// Async thunk for logging out
|
||||
export const logoutThunk = createAsyncThunk('auth/logout', async (_, { rejectWithValue, dispatch }) => {
|
||||
try {
|
||||
// Send the logout request to the server (this assumes your server clears any session-related info)
|
||||
await post('/logout');
|
||||
dispatch(logout());
|
||||
} catch (error) {
|
||||
const errorMessage = error.response?.data?.message || 'Log out failed';
|
||||
dispatch(
|
||||
showNotification({
|
||||
message: errorMessage,
|
||||
type: 'danger',
|
||||
})
|
||||
);
|
||||
return rejectWithValue(errorMessage);
|
||||
}
|
||||
});
|
@ -2,7 +2,7 @@ import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
const notificationSlice = createSlice({
|
||||
name: 'notification',
|
||||
initialState: null,
|
||||
initialState: null, // type(success/primary/warning/danger), message, duration
|
||||
reducers: {
|
||||
showNotification: (state, action) => action.payload,
|
||||
hideNotification: () => null,
|
||||
|
@ -2,15 +2,17 @@ import { combineReducers, configureStore } from '@reduxjs/toolkit';
|
||||
import { persistReducer, persistStore } from 'redux-persist';
|
||||
import sessionStorage from 'redux-persist/lib/storage/session';
|
||||
import notificationReducer from './notification.slice.js';
|
||||
import authReducer from './auth/auth.slice.js';
|
||||
|
||||
const rootRducer = combineReducers({
|
||||
auth: authReducer,
|
||||
notification: notificationReducer,
|
||||
});
|
||||
|
||||
const persistConfig = {
|
||||
key: 'root',
|
||||
storage: sessionStorage,
|
||||
whitelist: [],
|
||||
whitelist: ['auth'],
|
||||
};
|
||||
|
||||
// Persist configuration
|
||||
|
@ -35,3 +35,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.auth-form {
|
||||
input {
|
||||
min-width: 300px !important;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user