[dev]add redux , api service and snackbar

This commit is contained in:
susie-laptop 2025-02-28 15:03:06 -05:00
parent 4b6fec87cc
commit ee53e2ddd3
11 changed files with 355 additions and 34 deletions

127
package-lock.json generated
View File

@ -9,12 +9,13 @@
"version": "0.0.0",
"dependencies": {
"@popperjs/core": "^2.11.8",
"@reduxjs/toolkit": "^2.6.0",
"bootstrap": "^5.3.3",
"bootstrap-icons": "^1.11.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-icons": "^5.5.0",
"react-router-dom": "^7.2.0"
"react-redux": "^9.2.0",
"react-router-dom": "^7.2.0",
"redux-persist": "^6.0.0"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
@ -955,6 +956,29 @@
"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": {
"version": "4.34.8",
"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",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz",
"integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==",
"dev": true,
"devOptional": true,
"dependencies": {
"csstype": "^3.0.2"
}
@ -1278,6 +1302,11 @@
"@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": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz",
@ -1557,21 +1586,6 @@
"@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": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@ -1810,7 +1824,7 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true
"devOptional": true
},
"node_modules/data-view-buffer": {
"version": "1.0.2",
@ -2733,6 +2747,15 @@
"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": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz",
@ -3681,20 +3704,34 @@
"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": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"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": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
@ -3754,6 +3791,27 @@
"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": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@ -3796,6 +3854,11 @@
"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": {
"version": "2.0.0-next.5",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
@ -4385,6 +4448,14 @@
"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": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz",

View File

@ -11,10 +11,13 @@
},
"dependencies": {
"@popperjs/core": "^2.11.8",
"@reduxjs/toolkit": "^2.6.0",
"bootstrap": "^5.3.3",
"react": "^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": {
"@eslint/js": "^9.21.0",

View 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;

View 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;

View File

@ -1,11 +1,13 @@
import React from 'react';
import HeaderWithNav from './HeaderWithNav';
import "../styles/layouts.scss"
import '../styles/style.scss';
import NotificationSnackbar from '../components/NotificationSnackbar';
export default function Mainlayout({ children }) {
return (
<>
<HeaderWithNav />
<NotificationSnackbar />
{children}
</>
);

View File

@ -3,12 +3,20 @@ import { createRoot } from 'react-dom/client';
import './styles/base.scss';
import App from './App.jsx';
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(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
<PersistGate loading={<Loading />} persistor={persistor}>
<BrowserRouter>
<Provider store={store}>
<App />
</Provider>
</BrowserRouter>
</PersistGate>
</StrictMode>
);

91
src/services/api.js Normal file
View 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 };

View 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;
}

View 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
View 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;

View File

@ -1,3 +1,7 @@
.dropdown-toggle {
outline: 0;
}
.snackbar {
top:6.5rem;
}