added loading skeletons and fixed some minor stuff along the way.
This commit is contained in:
parent
c400ccc400
commit
9861deb49f
|
@ -16,6 +16,7 @@
|
|||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"axios": "^1.7.7",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.26.2",
|
||||
|
@ -13779,6 +13780,15 @@
|
|||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jwt-decode": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
|
||||
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"axios": "^1.7.7",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.26.2",
|
||||
|
|
48
src/App.js
48
src/App.js
|
@ -1,14 +1,14 @@
|
|||
// src/App.js
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { BrowserRouter as Router, Route, Routes, Navigate } from 'react-router-dom';
|
||||
import { BrowserRouter as Router, Route, Routes, Navigate, useNavigate } from 'react-router-dom';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
import Login from './components/Login';
|
||||
import Boxes from './components/Boxes';
|
||||
import Items from './components/Items';
|
||||
import Navbar from './components/Navbar'; // Correct import here
|
||||
import Admin from './components/Admin'; // Correct import here
|
||||
import Navbar from './components/Navbar';
|
||||
import Admin from './components/Admin';
|
||||
import { createContext } from 'react';
|
||||
import ErrorBoundary from './components/ErrorBoundary';
|
||||
import './styles.css'
|
||||
|
||||
export const AppContext = createContext();
|
||||
|
@ -16,6 +16,40 @@ export const PRIMARY_COLOR = '#333';
|
|||
export const SECONDARY_COLOR = '#ffffff';
|
||||
export const BACKGROUND_COLOR = '#dddddd';
|
||||
|
||||
// New component for token expiration check
|
||||
function TokenExpirationCheck({ setToken }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const checkTokenExpiration = () => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return;
|
||||
|
||||
try {
|
||||
const decodedToken = jwtDecode(token);
|
||||
if (Date.now() >= decodedToken.exp * 1000) {
|
||||
setToken(null);
|
||||
localStorage.removeItem('token');
|
||||
navigate('/api/v1/login');
|
||||
}
|
||||
} catch (error) {
|
||||
localStorage.removeItem('token');
|
||||
navigate('/api/v1/login');
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('focus', checkTokenExpiration);
|
||||
const interval = setInterval(checkTokenExpiration, 60000);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('focus', checkTokenExpiration);
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [navigate, setToken]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [token, setToken] = useState(localStorage.getItem('token'));
|
||||
|
||||
|
@ -27,18 +61,17 @@ function App() {
|
|||
|
||||
return (
|
||||
<AppContext.Provider value={{ token, setToken }}>
|
||||
<ErrorBoundary>
|
||||
<Router>
|
||||
<TokenExpirationCheck setToken={setToken} />
|
||||
<Navbar />
|
||||
<AppRoutes token={token} setToken={setToken} />
|
||||
</Router>
|
||||
</ErrorBoundary>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function AppRoutes({ token, setToken }) {
|
||||
const { id } = useParams(); // Move useParams here
|
||||
const { id } = useParams();
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -65,4 +98,5 @@ function AppRoutes({ token, setToken }) {
|
|||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
|
@ -18,7 +18,8 @@ import {
|
|||
DialogContent,
|
||||
DialogActions,
|
||||
Alert,
|
||||
CircularProgress
|
||||
CircularProgress,
|
||||
Skeleton
|
||||
} from '@mui/material';
|
||||
import { Delete as DeleteIcon, Edit as EditIcon } from '@mui/icons-material';
|
||||
import { useParams, useLocation } from 'react-router-dom';
|
||||
|
@ -51,6 +52,26 @@ const Item = React.memo(({ item, onDelete, onEdit, itemImages }) => (
|
|||
</TableRow>
|
||||
));
|
||||
|
||||
const LoadingSkeleton = () => (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Skeleton variant="circular" width={40} height={40} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton variant="text" width={100} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton variant="text" width="80%" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box display="flex" gap={1}>
|
||||
<Skeleton variant="circular" width={30} height={30} />
|
||||
<Skeleton variant="circular" width={30} height={30} />
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
export default function Items({ token }) {
|
||||
const { id: boxId } = useParams();
|
||||
const [items, setItems] = useState([]);
|
||||
|
@ -294,7 +315,12 @@ export default function Items({ token }) {
|
|||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{filteredItems.map((item) => (
|
||||
{loadingItems ? (
|
||||
[...Array(5)].map((_, i) => (
|
||||
<LoadingSkeleton key={i} />
|
||||
))
|
||||
) : (
|
||||
filteredItems.map((item) => (
|
||||
<Item
|
||||
key={item.ID}
|
||||
item={item}
|
||||
|
@ -302,7 +328,8 @@ export default function Items({ token }) {
|
|||
onDelete={handleDeleteItem}
|
||||
onEdit={handleEditItem}
|
||||
/>
|
||||
))}
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// src/utils/errorHandler.js
|
||||
// src/services/errorHandler.js
|
||||
import axios from 'axios';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(message, status, details = {}) {
|
||||
|
@ -9,21 +10,80 @@ export class ApiError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
// Enhanced API client with error handling
|
||||
export const createApiClient = () => {
|
||||
const client = axios.create({
|
||||
baseURL: process.env.REACT_APP_API_URL
|
||||
baseURL: process.env.REACT_APP_API_URL,
|
||||
headers: {
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
'X-Frame-Options': 'DENY',
|
||||
'X-XSS-Protection': '1; mode=block'
|
||||
}
|
||||
});
|
||||
|
||||
// Add request interceptor for token validation
|
||||
client.interceptors.request.use(async (config) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return config;
|
||||
|
||||
try {
|
||||
const decodedToken = jwtDecode(token);
|
||||
const currentTime = Date.now() / 1000;
|
||||
|
||||
if (decodedToken.exp < currentTime) {
|
||||
localStorage.removeItem('token');
|
||||
window.location.href = '/api/v1/login';
|
||||
throw new ApiError('Session expired', 401);
|
||||
}
|
||||
|
||||
// Refresh token if expiring soon
|
||||
if (decodedToken.exp - currentTime < 300) {
|
||||
try {
|
||||
const response = await axios.post('/api/v1/refresh-token', { token });
|
||||
localStorage.setItem('token', response.data.token);
|
||||
config.headers.Authorization = `Bearer ${response.data.token}`;
|
||||
} catch (error) {
|
||||
localStorage.removeItem('token');
|
||||
window.location.href = '/api/v1/login';
|
||||
throw new ApiError('Failed to refresh token', 401);
|
||||
}
|
||||
}
|
||||
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
} catch (error) {
|
||||
localStorage.removeItem('token');
|
||||
window.location.href = '/api/v1/login';
|
||||
throw new ApiError('Invalid token', 401);
|
||||
}
|
||||
|
||||
// Add CSRF token if available
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
|
||||
if (csrfToken) {
|
||||
config.headers['X-CSRF-Token'] = csrfToken;
|
||||
}
|
||||
|
||||
return config;
|
||||
});
|
||||
|
||||
// Enhanced response interceptor
|
||||
client.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
window.location.href = '/login';
|
||||
window.location.href = '/api/v1/login';
|
||||
return Promise.reject(new ApiError('Session expired', 401));
|
||||
}
|
||||
|
||||
// Handle rate limiting
|
||||
if (error.response?.status === 429) {
|
||||
return Promise.reject(new ApiError('Too many requests. Please try again later.', 429));
|
||||
}
|
||||
|
||||
// Handle server errors
|
||||
if (error.response?.status >= 500) {
|
||||
return Promise.reject(new ApiError('Server error. Please try again later.', error.response.status));
|
||||
}
|
||||
|
||||
return Promise.reject(new ApiError(
|
||||
error.response?.data?.message || 'An error occurred',
|
||||
error.response?.status,
|
||||
|
|
Loading…
Reference in New Issue