mirror of
https://github.com/Funkoala14/KnowledgeBase_OOIN.git
synced 2025-06-08 12:01:53 +08:00
[dev]add redux , api service and snackbar
This commit is contained in:
parent
4b6fec87cc
commit
ee53e2ddd3
127
package-lock.json
generated
127
package-lock.json
generated
@ -9,12 +9,13 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
|
"@reduxjs/toolkit": "^2.6.0",
|
||||||
"bootstrap": "^5.3.3",
|
"bootstrap": "^5.3.3",
|
||||||
"bootstrap-icons": "^1.11.3",
|
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-redux": "^9.2.0",
|
||||||
"react-router-dom": "^7.2.0"
|
"react-router-dom": "^7.2.0",
|
||||||
|
"redux-persist": "^6.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.21.0",
|
"@eslint/js": "^9.21.0",
|
||||||
@ -955,6 +956,29 @@
|
|||||||
"url": "https://opencollective.com/popperjs"
|
"url": "https://opencollective.com/popperjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@reduxjs/toolkit": {
|
||||||
|
"version": "2.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.6.0.tgz",
|
||||||
|
"integrity": "sha512-mWJCYpewLRyTuuzRSEC/IwIBBkYg2dKtQas8mty5MaV2iXzcmicS3gW554FDeOvLnY3x13NIk8MB1e8wHO7rqQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"immer": "^10.0.3",
|
||||||
|
"redux": "^5.0.1",
|
||||||
|
"redux-thunk": "^3.1.0",
|
||||||
|
"reselect": "^5.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
|
||||||
|
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.34.8",
|
"version": "4.34.8",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz",
|
||||||
@ -1264,7 +1288,7 @@
|
|||||||
"version": "19.0.10",
|
"version": "19.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz",
|
||||||
"integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==",
|
"integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
@ -1278,6 +1302,11 @@
|
|||||||
"@types/react": "^19.0.0"
|
"@types/react": "^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/use-sync-external-store": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="
|
||||||
|
},
|
||||||
"node_modules/@vitejs/plugin-react": {
|
"node_modules/@vitejs/plugin-react": {
|
||||||
"version": "4.3.4",
|
"version": "4.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz",
|
||||||
@ -1557,21 +1586,6 @@
|
|||||||
"@popperjs/core": "^2.11.8"
|
"@popperjs/core": "^2.11.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bootstrap-icons": {
|
|
||||||
"version": "1.11.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.11.3.tgz",
|
|
||||||
"integrity": "sha512-+3lpHrCw/it2/7lBL15VR0HEumaBss0+f/Lb6ZvHISn1mlK83jjFpooTLsMWbIjJMDjDjOExMsTxnXSIT4k4ww==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/twbs"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/bootstrap"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||||
@ -1810,7 +1824,7 @@
|
|||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"dev": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
"node_modules/data-view-buffer": {
|
"node_modules/data-view-buffer": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
@ -2733,6 +2747,15 @@
|
|||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/immer": {
|
||||||
|
"version": "10.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
|
||||||
|
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/immer"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/immutable": {
|
"node_modules/immutable": {
|
||||||
"version": "4.3.7",
|
"version": "4.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz",
|
||||||
@ -3681,20 +3704,34 @@
|
|||||||
"react": "^19.0.0"
|
"react": "^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-icons": {
|
|
||||||
"version": "5.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
|
|
||||||
"integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/react-redux": {
|
||||||
|
"version": "9.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||||
|
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
|
"use-sync-external-store": "^1.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^18.2.25 || ^19",
|
||||||
|
"react": "^18.0 || ^19",
|
||||||
|
"redux": "^5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-refresh": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.14.2",
|
"version": "0.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
|
||||||
@ -3754,6 +3791,27 @@
|
|||||||
"node": ">=8.10.0"
|
"node": ">=8.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/redux": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="
|
||||||
|
},
|
||||||
|
"node_modules/redux-persist": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"redux": ">4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/redux-thunk": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"redux": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/reflect.getprototypeof": {
|
"node_modules/reflect.getprototypeof": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
||||||
@ -3796,6 +3854,11 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/reselect": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="
|
||||||
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "2.0.0-next.5",
|
"version": "2.0.0-next.5",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
|
||||||
@ -4385,6 +4448,14 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-sync-external-store": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "6.2.0",
|
"version": "6.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz",
|
||||||
|
@ -11,10 +11,13 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
|
"@reduxjs/toolkit": "^2.6.0",
|
||||||
"bootstrap": "^5.3.3",
|
"bootstrap": "^5.3.3",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-router-dom": "^7.2.0"
|
"react-redux": "^9.2.0",
|
||||||
|
"react-router-dom": "^7.2.0",
|
||||||
|
"redux-persist": "^6.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.21.0",
|
"@eslint/js": "^9.21.0",
|
||||||
|
22
src/components/NotificationSnackbar.jsx
Normal file
22
src/components/NotificationSnackbar.jsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
import { hideNotification } from '../store/notification.slice.js';
|
||||||
|
import Snackbar from './Snackbar.jsx';
|
||||||
|
|
||||||
|
const NotificationSnackbar = () => {
|
||||||
|
const notification = useSelector((state) => state.notification);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
if (!notification) return null; // 没有通知时不渲染
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Snackbar
|
||||||
|
type={notification.type}
|
||||||
|
message={notification.message}
|
||||||
|
duration={notification.duration}
|
||||||
|
onClose={() => dispatch(hideNotification())}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotificationSnackbar;
|
62
src/components/Snackbar.jsx
Normal file
62
src/components/Snackbar.jsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
|
const Snackbar = ({ type = 'primary', message, duration = 3000, onClose }) => {
|
||||||
|
if (!message) return null;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (message) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (onClose) onClose();
|
||||||
|
}, duration);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [duration, onClose]);
|
||||||
|
|
||||||
|
const icons = {
|
||||||
|
success: 'check-circle-fill',
|
||||||
|
primary: 'info-fill',
|
||||||
|
warning: 'exclamation-triangle-fill',
|
||||||
|
danger: 'exclamation-triangle-fill',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<svg xmlns='http://www.w3.org/2000/svg' className='d-none'>
|
||||||
|
<symbol id='check-circle-fill' viewBox='0 0 16 16'>
|
||||||
|
<path d='M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z' />
|
||||||
|
</symbol>
|
||||||
|
<symbol id='info-fill' viewBox='0 0 16 16'>
|
||||||
|
<path d='M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2z' />
|
||||||
|
</symbol>
|
||||||
|
<symbol id='exclamation-triangle-fill' viewBox='0 0 16 16'>
|
||||||
|
<path d='M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z' />
|
||||||
|
</symbol>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<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`}
|
||||||
|
role='alert'
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className='bi flex-shrink-0 me-2'
|
||||||
|
role='img'
|
||||||
|
aria-label={`${type}:`}
|
||||||
|
width='16'
|
||||||
|
height='16'
|
||||||
|
fill='currentColor'
|
||||||
|
>
|
||||||
|
<use xlinkHref={`#${icons[type]}`} />
|
||||||
|
</svg>
|
||||||
|
<div className='flex-fill'>{message}</div>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='btn-close flex-end'
|
||||||
|
data-bs-dismiss='alert'
|
||||||
|
aria-label='Close'
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Snackbar;
|
@ -1,11 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import HeaderWithNav from './HeaderWithNav';
|
import HeaderWithNav from './HeaderWithNav';
|
||||||
import "../styles/layouts.scss"
|
import '../styles/style.scss';
|
||||||
|
import NotificationSnackbar from '../components/NotificationSnackbar';
|
||||||
|
|
||||||
export default function Mainlayout({ children }) {
|
export default function Mainlayout({ children }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HeaderWithNav />
|
<HeaderWithNav />
|
||||||
|
<NotificationSnackbar />
|
||||||
{children}
|
{children}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
16
src/main.jsx
16
src/main.jsx
@ -3,12 +3,20 @@ import { createRoot } from 'react-dom/client';
|
|||||||
import './styles/base.scss';
|
import './styles/base.scss';
|
||||||
import App from './App.jsx';
|
import App from './App.jsx';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import 'bootstrap'
|
import 'bootstrap';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import store, { persistor } from './store/store.js';
|
||||||
|
import { PersistGate } from 'redux-persist/integration/react';
|
||||||
|
import Loading from './components/Loading.jsx';
|
||||||
|
|
||||||
createRoot(document.getElementById('root')).render(
|
createRoot(document.getElementById('root')).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<BrowserRouter>
|
<PersistGate loading={<Loading />} persistor={persistor}>
|
||||||
<App />
|
<BrowserRouter>
|
||||||
</BrowserRouter>
|
<Provider store={store}>
|
||||||
|
<App />
|
||||||
|
</Provider>
|
||||||
|
</BrowserRouter>
|
||||||
|
</PersistGate>
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
);
|
);
|
||||||
|
91
src/services/api.js
Normal file
91
src/services/api.js
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
// Create Axios instance with base URL
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: 'api',
|
||||||
|
withCredentials: true, // Include cookies if needed
|
||||||
|
});
|
||||||
|
|
||||||
|
// Request Interceptor
|
||||||
|
api.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('Request error:', error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Response Interceptor
|
||||||
|
api.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
// Handle errors in the response
|
||||||
|
if (error.response) {
|
||||||
|
// monitor /verify
|
||||||
|
if (error.response.status === 401 && error.config.url === '/check-token') {
|
||||||
|
if (window.location.pathname !== '/login' && window.location.pathname !== '/register') {
|
||||||
|
window.location.href = '/login';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The request was made and the server responded with a status code
|
||||||
|
console.error('API Error Response:', error.response.status, error.response.data.message);
|
||||||
|
// alert(`Error: ${error.response.status} - ${error.response.data.message || 'Something went wrong'}`);
|
||||||
|
} else if (error.request) {
|
||||||
|
// The request was made but no response was received
|
||||||
|
console.error('API Error Request:', error.request);
|
||||||
|
// alert('Network error: No response from server');
|
||||||
|
} else {
|
||||||
|
// Something happened in setting up the request
|
||||||
|
console.error('API Error Message:', error.message);
|
||||||
|
// alert('Error: ' + error.message);
|
||||||
|
}
|
||||||
|
return Promise.reject(error); // Reject the promise
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Define common HTTP methods
|
||||||
|
const get = async (url, params = {}) => {
|
||||||
|
const res = await api.get(url, { params });
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle POST requests for JSON data
|
||||||
|
const post = async (url, data, isMultipart = false) => {
|
||||||
|
const headers = isMultipart
|
||||||
|
? { 'Content-Type': 'multipart/form-data' } // For file uploads
|
||||||
|
: { 'Content-Type': 'application/json' }; // For JSON data
|
||||||
|
|
||||||
|
const res = await api.post(url, data, { headers });
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle PUT requests
|
||||||
|
const put = async (url, data) => {
|
||||||
|
const res = await api.put(url, data, {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle DELETE requests
|
||||||
|
const del = async (url) => {
|
||||||
|
const res = await api.delete(url);
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const upload = async (url, data) => {
|
||||||
|
const axiosInstance = await axios.create({
|
||||||
|
baseURL: '/api',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const res = await axiosInstance.post(url, data);
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
export { get, post, put, del, upload };
|
13
src/services/userServices.js
Normal file
13
src/services/userServices.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { get, post } from "./api";
|
||||||
|
|
||||||
|
export const checkToken = async () => {
|
||||||
|
const response = await get("/check-token");
|
||||||
|
const { user, message } = response;
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const login = async (config) => {
|
||||||
|
const response = await post('login', config);
|
||||||
|
const {message, user} = response;
|
||||||
|
return user;
|
||||||
|
}
|
14
src/store/notification.slice.js
Normal file
14
src/store/notification.slice.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
const notificationSlice = createSlice({
|
||||||
|
name: 'notification',
|
||||||
|
initialState: null,
|
||||||
|
reducers: {
|
||||||
|
showNotification: (state, action) => action.payload,
|
||||||
|
hideNotification: () => null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { showNotification, hideNotification } = notificationSlice.actions;
|
||||||
|
const notificationReducer = notificationSlice.reducer;
|
||||||
|
export default notificationReducer;
|
31
src/store/store.js
Normal file
31
src/store/store.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
const rootRducer = combineReducers({
|
||||||
|
notification: notificationReducer,
|
||||||
|
});
|
||||||
|
|
||||||
|
const persistConfig = {
|
||||||
|
key: 'root',
|
||||||
|
storage: sessionStorage,
|
||||||
|
whitelist: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Persist configuration
|
||||||
|
const persistedReducer = persistReducer(persistConfig, rootRducer);
|
||||||
|
|
||||||
|
const store = configureStore({
|
||||||
|
reducer: persistedReducer,
|
||||||
|
middleware: (getDefaultMiddleware) =>
|
||||||
|
getDefaultMiddleware({
|
||||||
|
serializableCheck: false, // Disable serializable check for redux-persist
|
||||||
|
}),
|
||||||
|
devTools: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create the persistor to manage rehydrating the store
|
||||||
|
export const persistor = persistStore(store);
|
||||||
|
|
||||||
|
export default store;
|
@ -1,3 +1,7 @@
|
|||||||
.dropdown-toggle {
|
.dropdown-toggle {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.snackbar {
|
||||||
|
top:6.5rem;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user