jgest-react-vite 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -0
- package/index.js +21 -0
- package/package.json +17 -0
- package/template/.env +1 -0
- package/template/README.md +116 -0
- package/template/eslint.config.js +26 -0
- package/template/index.html +13 -0
- package/template/package.json +40 -0
- package/template/pnpm-lock.yaml +3339 -0
- package/template/public/vite.svg +1 -0
- package/template/src/App.tsx +30 -0
- package/template/src/Router.tsx +17 -0
- package/template/src/components/main/ThemeSwitch.tsx +80 -0
- package/template/src/components/shared/MyModal.tsx +51 -0
- package/template/src/components/shared/MyPopover.tsx +44 -0
- package/template/src/hooks/shared/useNotification.ts +25 -0
- package/template/src/hooks/shared/useSetTimezone.ts +20 -0
- package/template/src/index.css +9 -0
- package/template/src/layouts/MainLayout.tsx +23 -0
- package/template/src/main.tsx +10 -0
- package/template/src/modules/MainPage/MainPageContent.tsx +100 -0
- package/template/src/pages/MainPage/MainPage.tsx +12 -0
- package/template/src/providers/NotificationProvider/NotificationProvider.tsx +61 -0
- package/template/src/providers/NotificationProvider/context/NotificationContext.tsx +20 -0
- package/template/src/services/ApiService.ts +54 -0
- package/template/src/services/ToBeTested/ApiService-New.ts +94 -0
- package/template/src/store/ThemeStore/Themes/themes.ts +28 -0
- package/template/src/store/ThemeStore/themeStore.ts +28 -0
- package/template/tsconfig.app.json +28 -0
- package/template/tsconfig.json +7 -0
- package/template/tsconfig.node.json +26 -0
- package/template/vite.config.ts +18 -0
|
@@ -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>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Suspense } from 'react';
|
|
2
|
+
import { CircularProgress, CssBaseline } from '@mui/material';
|
|
3
|
+
import { ThemeProvider } from '@mui/material/styles';
|
|
4
|
+
import AppRouter from './Router';
|
|
5
|
+
import { themeStore } from './store/ThemeStore/themeStore';
|
|
6
|
+
import { themes } from './store/ThemeStore/Themes/themes';
|
|
7
|
+
import { NotificationProvider } from './providers/NotificationProvider/NotificationProvider';
|
|
8
|
+
|
|
9
|
+
function App() {
|
|
10
|
+
const { mode } = themeStore();
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<ThemeProvider theme={themes[mode]}>
|
|
14
|
+
<CssBaseline />
|
|
15
|
+
<NotificationProvider>
|
|
16
|
+
<Suspense
|
|
17
|
+
fallback={
|
|
18
|
+
<main className="h-screen w-screen flex justify-center items-center ">
|
|
19
|
+
<CircularProgress />
|
|
20
|
+
</main>
|
|
21
|
+
}
|
|
22
|
+
>
|
|
23
|
+
<AppRouter />
|
|
24
|
+
</Suspense>
|
|
25
|
+
</NotificationProvider>
|
|
26
|
+
</ThemeProvider>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default App
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { BrowserRouter, Routes, Route } from 'react-router-dom'
|
|
2
|
+
import { lazy } from 'react';
|
|
3
|
+
|
|
4
|
+
//Importamos las paginas de nuestra aplicación de forma lazy para mejorar el rendimiento
|
|
5
|
+
const MainPage = lazy(() => import('./pages/MainPage/MainPage'));
|
|
6
|
+
|
|
7
|
+
//Creamos el router de nuestra aplicación
|
|
8
|
+
const AppRouter = () => {
|
|
9
|
+
return (
|
|
10
|
+
<BrowserRouter>
|
|
11
|
+
<Routes>
|
|
12
|
+
<Route path="/" element={<MainPage />} />
|
|
13
|
+
</Routes>
|
|
14
|
+
</BrowserRouter>
|
|
15
|
+
);
|
|
16
|
+
};
|
|
17
|
+
export default AppRouter;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Botón alternador (switch) para cambiar el tema (Claro/Oscuro)
|
|
2
|
+
import { styled } from '@mui/material/styles';
|
|
3
|
+
import { themeStore } from '../../store/ThemeStore/themeStore';
|
|
4
|
+
import Switch from '@mui/material/Switch';
|
|
5
|
+
import { useEffect } from 'react';
|
|
6
|
+
|
|
7
|
+
// Personalización de estilos del componente Switch de Material UI
|
|
8
|
+
const ThemeSwitch = styled(Switch)(({ theme }) => ({
|
|
9
|
+
width: 62,
|
|
10
|
+
height: 34,
|
|
11
|
+
padding: 7,
|
|
12
|
+
'& .MuiSwitch-switchBase': {
|
|
13
|
+
margin: 1,
|
|
14
|
+
padding: 0,
|
|
15
|
+
transform: 'translateX(6px)',
|
|
16
|
+
'&.Mui-checked': {
|
|
17
|
+
color: '#fff',
|
|
18
|
+
transform: 'translateX(22px)',
|
|
19
|
+
'& .MuiSwitch-thumb:before': {
|
|
20
|
+
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="${encodeURIComponent(
|
|
21
|
+
'#fff',
|
|
22
|
+
)}" d="M4.2 2.5l-.7 1.8-1.8.7 1.8.7.7 1.8.6-1.8L6.7 5l-1.9-.7-.6-1.8zm15 8.3a6.7 6.7 0 11-6.6-6.6 5.8 5.8 0 006.6 6.6z"/></svg>')`,
|
|
23
|
+
},
|
|
24
|
+
'& + .MuiSwitch-track': {
|
|
25
|
+
opacity: 1,
|
|
26
|
+
backgroundColor: '#aab4be',
|
|
27
|
+
...theme.applyStyles('dark', {
|
|
28
|
+
backgroundColor: '#8796A5',
|
|
29
|
+
}),
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
'& .MuiSwitch-thumb': {
|
|
34
|
+
backgroundColor: '#001e3c',
|
|
35
|
+
width: 32,
|
|
36
|
+
height: 32,
|
|
37
|
+
'&::before': {
|
|
38
|
+
content: "''",
|
|
39
|
+
position: 'absolute',
|
|
40
|
+
width: '100%',
|
|
41
|
+
height: '100%',
|
|
42
|
+
left: 0,
|
|
43
|
+
top: 0,
|
|
44
|
+
backgroundRepeat: 'no-repeat',
|
|
45
|
+
backgroundPosition: 'center',
|
|
46
|
+
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="${encodeURIComponent(
|
|
47
|
+
'#fff',
|
|
48
|
+
)}" d="M9.305 1.667V3.75h1.389V1.667h-1.39zm-4.707 1.95l-.982.982L5.09 6.072l.982-.982-1.473-1.473zm10.802 0L13.927 5.09l.982.982 1.473-1.473-.982-.982zM10 5.139a4.872 4.872 0 00-4.862 4.86A4.872 4.872 0 0010 14.862 4.872 4.872 0 0014.86 10 4.872 4.872 0 0010 5.139zm0 1.389A3.462 3.462 0 0113.471 10a3.462 3.462 0 01-3.473 3.472A3.462 3.462 0 016.527 10 3.462 3.462 0 0110 6.528zM1.665 9.305v1.39h2.083v-1.39H1.666zm14.583 0v1.39h2.084v-1.39h-2.084zM5.09 13.928L3.616 15.4l.982.982 1.473-1.473-.982-.982zm9.82 0l-.982.982 1.473 1.473.982-.982-1.473-1.473zM9.305 16.25v2.083h1.389V16.25h-1.39z"/></svg>')`,
|
|
49
|
+
},
|
|
50
|
+
...theme.applyStyles('dark', {
|
|
51
|
+
backgroundColor: '#003892',
|
|
52
|
+
}),
|
|
53
|
+
},
|
|
54
|
+
'& .MuiSwitch-track': {
|
|
55
|
+
opacity: 1,
|
|
56
|
+
backgroundColor: '#aab4be',
|
|
57
|
+
borderRadius: 20 / 2,
|
|
58
|
+
...theme.applyStyles('dark', {
|
|
59
|
+
backgroundColor: '#8796A5',
|
|
60
|
+
}),
|
|
61
|
+
},
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
export default function ThemeToggle() {
|
|
65
|
+
// Obtiene el estado del tema y la función para alternarlo desde el store
|
|
66
|
+
const { mode, toggleTheme } = themeStore();
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (mode === 'dark') {
|
|
69
|
+
document.documentElement.classList.add('dark');
|
|
70
|
+
} else {
|
|
71
|
+
document.documentElement.classList.remove('dark');
|
|
72
|
+
}
|
|
73
|
+
}, [mode]);
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
|
|
77
|
+
<ThemeSwitch onClick={toggleTheme} />
|
|
78
|
+
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { Box, Paper } from '@mui/material';
|
|
3
|
+
|
|
4
|
+
// Interfaz para las propiedades del layout modal
|
|
5
|
+
interface ModalProps {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
minWidth?: string | number;
|
|
8
|
+
onClose?: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Componente de layout para modales con fondo oscurecido y centrado
|
|
12
|
+
const MyModal = ({ children, minWidth, onClose }: ModalProps) => {
|
|
13
|
+
return (
|
|
14
|
+
<Box
|
|
15
|
+
sx={{
|
|
16
|
+
position: 'fixed',
|
|
17
|
+
top: 0,
|
|
18
|
+
left: 0,
|
|
19
|
+
width: '100%',
|
|
20
|
+
height: '100%',
|
|
21
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
22
|
+
display: 'flex',
|
|
23
|
+
justifyContent: 'center',
|
|
24
|
+
alignItems: 'center',
|
|
25
|
+
zIndex: 1300,
|
|
26
|
+
backdropFilter: 'blur(3px)',
|
|
27
|
+
}}
|
|
28
|
+
onClick={onClose}
|
|
29
|
+
>
|
|
30
|
+
<Paper
|
|
31
|
+
elevation={24}
|
|
32
|
+
sx={{
|
|
33
|
+
position: 'relative',
|
|
34
|
+
maxWidth: '90vw',
|
|
35
|
+
minWidth: minWidth || 800,
|
|
36
|
+
maxHeight: '90vh',
|
|
37
|
+
overflowY: 'auto',
|
|
38
|
+
p: 4,
|
|
39
|
+
borderRadius: 2,
|
|
40
|
+
boxShadow: 24,
|
|
41
|
+
m: 2,
|
|
42
|
+
}}
|
|
43
|
+
onClick={(e) => e.stopPropagation()}
|
|
44
|
+
>
|
|
45
|
+
{children}
|
|
46
|
+
</Paper>
|
|
47
|
+
</Box>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export default MyModal;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Popover, Box } from "@mui/material";
|
|
2
|
+
|
|
3
|
+
// Componente de layout para popovers con posicionamiento automático
|
|
4
|
+
const MyPopover = ({ children, anchorEl, handleClosePopover }: { children: React.ReactNode; anchorEl: HTMLElement | null; handleClosePopover: () => void }) => {
|
|
5
|
+
return (
|
|
6
|
+
<Popover
|
|
7
|
+
open={Boolean(anchorEl)}
|
|
8
|
+
anchorEl={anchorEl}
|
|
9
|
+
onClose={handleClosePopover}
|
|
10
|
+
anchorOrigin={{
|
|
11
|
+
vertical: 'bottom',
|
|
12
|
+
horizontal: 'left',
|
|
13
|
+
}}
|
|
14
|
+
transformOrigin={{
|
|
15
|
+
vertical: 'top',
|
|
16
|
+
horizontal: 'left',
|
|
17
|
+
}}
|
|
18
|
+
disableScrollLock
|
|
19
|
+
slotProps={{
|
|
20
|
+
paper: {
|
|
21
|
+
sx: {
|
|
22
|
+
maxWidth: '80vw',
|
|
23
|
+
maxHeight: '70vh',
|
|
24
|
+
display: 'flex',
|
|
25
|
+
flexDirection: 'column',
|
|
26
|
+
p: 2,
|
|
27
|
+
margin: '8px'
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}}
|
|
31
|
+
sx={{
|
|
32
|
+
'& .MuiPopover-paper': {
|
|
33
|
+
overflow: 'visible'
|
|
34
|
+
}
|
|
35
|
+
}}
|
|
36
|
+
>
|
|
37
|
+
<Box sx={{ overflowY: 'auto', flex: 1 }}>
|
|
38
|
+
{children}
|
|
39
|
+
</Box>
|
|
40
|
+
</Popover>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default MyPopover;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
|
|
2
|
+
import { useContext } from 'react';
|
|
3
|
+
import { NotificationContext } from '../../providers/NotificationProvider/context/NotificationContext';
|
|
4
|
+
|
|
5
|
+
// Hook personalizado para acceder fácilmente al contexto de notificaciones
|
|
6
|
+
export const useNotification = () => {
|
|
7
|
+
const ctx = useContext(NotificationContext);
|
|
8
|
+
if (!ctx) {
|
|
9
|
+
throw new Error('useNotification debe ser usado dentro de un NotificationProvider');
|
|
10
|
+
}
|
|
11
|
+
return ctx;
|
|
12
|
+
};
|
|
13
|
+
/*
|
|
14
|
+
Ejemplo de uso en un componente:
|
|
15
|
+
|
|
16
|
+
const { showNotification } = useNotification();
|
|
17
|
+
try {
|
|
18
|
+
await api.delete(`/v1/admin/api-error-logs/${id}`);
|
|
19
|
+
showNotification('Log deleted successfully', 'success');
|
|
20
|
+
refetch();
|
|
21
|
+
} catch (error: any) {
|
|
22
|
+
showNotification(error);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
*/
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Hook para formatear fechas a la zona horaria local (Europe/Madrid)
|
|
2
|
+
const useSetTimezone = () => {
|
|
3
|
+
const setTimezone = (date: string) => {
|
|
4
|
+
try {
|
|
5
|
+
const toDate = new Date(date);
|
|
6
|
+
|
|
7
|
+
// Validaciones para fechas inválidas o vacías
|
|
8
|
+
if (Number.isNaN(toDate.getTime())) return "-";
|
|
9
|
+
if (toDate.getTime() === 0) return "-";
|
|
10
|
+
|
|
11
|
+
// Ajuste de la zona horaria
|
|
12
|
+
toDate.setTime(toDate.getTime() + toDate.getTimezoneOffset() * -60000);
|
|
13
|
+
return toDate.toLocaleString("es-ES", { timeZone: "Europe/Madrid" } as Intl.DateTimeFormatOptions);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
console.error('Error al establecer la zona horaria:', error);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return { setTimezone };
|
|
19
|
+
}
|
|
20
|
+
export default useSetTimezone;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import ThemeToggle from '../components/main/ThemeSwitch';
|
|
2
|
+
const MainLayout = ({ Nav, children, Footer }: { Nav?: React.ReactNode ; children: React.ReactNode; Footer?: React.ReactNode }) => {
|
|
3
|
+
|
|
4
|
+
return (
|
|
5
|
+
<main className="h-screen w-full flex flex-col overflow-hidden">
|
|
6
|
+
|
|
7
|
+
<nav>
|
|
8
|
+
{Nav}
|
|
9
|
+
<ThemeToggle />
|
|
10
|
+
</nav>
|
|
11
|
+
|
|
12
|
+
<section className="w-full flex-1 min-h-0 flex flex-col">
|
|
13
|
+
{children}
|
|
14
|
+
</section>
|
|
15
|
+
|
|
16
|
+
<footer>
|
|
17
|
+
{Footer}
|
|
18
|
+
</footer>
|
|
19
|
+
</main>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default MainLayout;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
2
|
+
import { Box } from '@mui/material';
|
|
3
|
+
|
|
4
|
+
// Generamos las posiciones de los sparkles una sola vez (fuera del render)
|
|
5
|
+
const SPARKLES = Array.from({ length: 20 }, (_, i) => ({
|
|
6
|
+
id: i,
|
|
7
|
+
top: `${Math.random() * 80 + 10}%`,
|
|
8
|
+
left: `${Math.random() * 80 + 10}%`,
|
|
9
|
+
delay: `${Math.random() * 2}s`,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
const MainPageContent = () => {
|
|
13
|
+
const [animationPhase, setAnimationPhase] = useState<'growing' | 'spinning'>('growing');
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const timer = setTimeout(() => setAnimationPhase('spinning'), 1000);
|
|
17
|
+
return () => clearTimeout(timer);
|
|
18
|
+
}, []);
|
|
19
|
+
|
|
20
|
+
const sparkles = useMemo(() => SPARKLES, []);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Box
|
|
24
|
+
sx={{
|
|
25
|
+
position: 'relative',
|
|
26
|
+
display: 'flex',
|
|
27
|
+
alignItems: 'center',
|
|
28
|
+
justifyContent: 'center',
|
|
29
|
+
width: '100%',
|
|
30
|
+
flex: 1,
|
|
31
|
+
minHeight: 0,
|
|
32
|
+
overflow: 'hidden',
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
{/* Sparkles */}
|
|
36
|
+
{animationPhase === 'spinning' && (
|
|
37
|
+
<Box
|
|
38
|
+
sx={{
|
|
39
|
+
position: 'absolute',
|
|
40
|
+
inset: 0,
|
|
41
|
+
pointerEvents: 'none',
|
|
42
|
+
'& .sparkle': {
|
|
43
|
+
position: 'absolute',
|
|
44
|
+
width: '10px',
|
|
45
|
+
height: '10px',
|
|
46
|
+
borderRadius: '50%',
|
|
47
|
+
background: 'radial-gradient(circle, #fff, #ffcc00, transparent)',
|
|
48
|
+
animation: 'sparkleAnim 1.5s infinite ease-in-out',
|
|
49
|
+
},
|
|
50
|
+
'@keyframes sparkleAnim': {
|
|
51
|
+
'0%, 100%': { transform: 'scale(0) rotate(0deg)', opacity: 0 },
|
|
52
|
+
'50%': { transform: 'scale(1.5) rotate(45deg)', opacity: 1 },
|
|
53
|
+
},
|
|
54
|
+
}}
|
|
55
|
+
>
|
|
56
|
+
{sparkles.map(({ id, top, left, delay }) => (
|
|
57
|
+
<Box
|
|
58
|
+
key={id}
|
|
59
|
+
className="sparkle"
|
|
60
|
+
sx={{
|
|
61
|
+
top,
|
|
62
|
+
left,
|
|
63
|
+
animationDelay: delay,
|
|
64
|
+
boxShadow: '0 0 10px 2px rgba(255, 204, 0, 0.5)',
|
|
65
|
+
}}
|
|
66
|
+
/>
|
|
67
|
+
))}
|
|
68
|
+
</Box>
|
|
69
|
+
)}
|
|
70
|
+
|
|
71
|
+
{/* Logo animado */}
|
|
72
|
+
<Box
|
|
73
|
+
component="img"
|
|
74
|
+
src="https://stickerapp.es/cdn-assets/images/stickers/663t.png"
|
|
75
|
+
alt="Logo"
|
|
76
|
+
sx={{
|
|
77
|
+
width: { xs: '55vmin', sm: '40vmin', md: '30vmin' },
|
|
78
|
+
height: 'auto',
|
|
79
|
+
objectFit: 'contain',
|
|
80
|
+
transformOrigin: 'center center',
|
|
81
|
+
zIndex: 1,
|
|
82
|
+
animation:
|
|
83
|
+
animationPhase === 'growing'
|
|
84
|
+
? 'growScale 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards'
|
|
85
|
+
: 'spin3D 3s linear infinite',
|
|
86
|
+
'@keyframes growScale': {
|
|
87
|
+
from: { transform: 'scale(0)', opacity: 0 },
|
|
88
|
+
to: { transform: 'scale(1)', opacity: 1 },
|
|
89
|
+
},
|
|
90
|
+
'@keyframes spin3D': {
|
|
91
|
+
from: { transform: 'rotateY(0deg)' },
|
|
92
|
+
to: { transform: 'rotateY(360deg)' },
|
|
93
|
+
},
|
|
94
|
+
}}
|
|
95
|
+
/>
|
|
96
|
+
</Box>
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export default MainPageContent;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import MainLayout from "../../layouts/MainLayout";
|
|
2
|
+
import MainPageContent from "../../modules/MainPage/MainPageContent.tsx";
|
|
3
|
+
|
|
4
|
+
const MainPage = () => {
|
|
5
|
+
return (
|
|
6
|
+
<MainLayout>
|
|
7
|
+
<MainPageContent/>
|
|
8
|
+
</MainLayout>
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
}
|
|
12
|
+
export default MainPage;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { useState, useCallback, useMemo, type ReactNode } from 'react';
|
|
2
|
+
import {Snackbar, Alert, AlertTitle, type AlertColor } from '@mui/material';
|
|
3
|
+
import { NotificationContext, type ErrorResponse } from './context/NotificationContext';
|
|
4
|
+
|
|
5
|
+
export const NotificationProvider = ({ children }: { children: ReactNode }) => {
|
|
6
|
+
// Estados para controlar la visibilidad y contenido del Snackbar
|
|
7
|
+
const [open, setOpen] = useState(false);
|
|
8
|
+
const [message, setMessage] = useState('');
|
|
9
|
+
const [title, setTitle] = useState<string | null>(null);
|
|
10
|
+
const [severity, setSeverity] = useState<AlertColor>('success');
|
|
11
|
+
|
|
12
|
+
// Función para mostrar notificaciones generales
|
|
13
|
+
const showNotification = useCallback(
|
|
14
|
+
(msg: string | ErrorResponse, sev: AlertColor = 'success') => {
|
|
15
|
+
if (typeof msg === 'string') {
|
|
16
|
+
// Manejo de mensajes simples de texto
|
|
17
|
+
setMessage(msg);
|
|
18
|
+
setTitle(null);
|
|
19
|
+
setSeverity(sev);
|
|
20
|
+
} else {
|
|
21
|
+
// Manejo de objetos de error (ErrorResponse)
|
|
22
|
+
setTitle(`Error ${msg.status}: ${msg.error}`);
|
|
23
|
+
setMessage(`${msg.message}${msg.path ?? ''}`);
|
|
24
|
+
setSeverity('error');
|
|
25
|
+
}
|
|
26
|
+
setOpen(true);
|
|
27
|
+
},
|
|
28
|
+
[]
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// Función específica para mostrar notificaciones de error
|
|
32
|
+
const showErrorNotification = useCallback((error: ErrorResponse) => {
|
|
33
|
+
setTitle(`Error ${error.status}: ${error.error}`);
|
|
34
|
+
setMessage(`${error.message}${error.path ?? ''}`);
|
|
35
|
+
setSeverity('error');
|
|
36
|
+
setOpen(true);
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
// Memorización del valor del contexto para evitar renderizados innecesarios
|
|
40
|
+
const value = useMemo(
|
|
41
|
+
() => ({ showNotification, showErrorNotification }),
|
|
42
|
+
[showNotification, showErrorNotification]
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<NotificationContext.Provider value={value}>
|
|
47
|
+
{children}
|
|
48
|
+
<Snackbar
|
|
49
|
+
open={open}
|
|
50
|
+
autoHideDuration={5000} // Se cierra automáticamente a los 5 segundos
|
|
51
|
+
onClose={() => setOpen(false)}
|
|
52
|
+
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
|
53
|
+
>
|
|
54
|
+
<Alert onClose={() => setOpen(false)} severity={severity} sx={{ width: '100%', minWidth: 300 }}>
|
|
55
|
+
{title && <AlertTitle>{title}</AlertTitle>}
|
|
56
|
+
{message}
|
|
57
|
+
</Alert>
|
|
58
|
+
</Snackbar>
|
|
59
|
+
</NotificationContext.Provider>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createContext } from 'react';
|
|
2
|
+
import type { AlertColor } from '@mui/material';
|
|
3
|
+
|
|
4
|
+
// Interfaz para la estructura de respuesta de error
|
|
5
|
+
export interface ErrorResponse {
|
|
6
|
+
status: number;
|
|
7
|
+
error: string;
|
|
8
|
+
message: string;
|
|
9
|
+
path: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Interfaz que define el tipo de datos y funciones expuestas por el contexto de notificaciones
|
|
13
|
+
export interface NotificationContextType {
|
|
14
|
+
showNotification: (msg: string | ErrorResponse, sev?: AlertColor) => void;
|
|
15
|
+
showErrorNotification: (error: ErrorResponse) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Creación del contexto con valor inicial indefinido
|
|
19
|
+
export const NotificationContext = createContext<NotificationContextType | undefined>(undefined);
|
|
20
|
+
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ErrorResponse } from '../providers/NotificationProvider/context/NotificationContext';
|
|
2
|
+
import axios, { type AxiosError } from 'axios';
|
|
3
|
+
|
|
4
|
+
const API_BASE_URL = import.meta.env.VITE_APP_API_URL;
|
|
5
|
+
|
|
6
|
+
const instance = axios.create({
|
|
7
|
+
baseURL: API_BASE_URL,
|
|
8
|
+
headers: {
|
|
9
|
+
'Content-Type': 'application/json',
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
instance.interceptors.response.use(
|
|
14
|
+
(response) => response.data, // Retornar directamente la data de la respuesta
|
|
15
|
+
(error: AxiosError) => {
|
|
16
|
+
const errorData = error.response?.data as any || {};
|
|
17
|
+
|
|
18
|
+
// Creamos el ErrorResponse
|
|
19
|
+
const errorResponse: ErrorResponse = {
|
|
20
|
+
status: errorData.status || error.response?.status || 500,
|
|
21
|
+
error: errorData.error || error.code || 'Error',
|
|
22
|
+
message: errorData.message || error.message || 'An unexpected error occurred',
|
|
23
|
+
path: errorData.path || error.config?.url || ''
|
|
24
|
+
}
|
|
25
|
+
return Promise.reject(errorResponse);
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// Clase de servicio para realizar peticiones HTTP a la API de Ibio
|
|
30
|
+
class ApiService {
|
|
31
|
+
// Método privado para realizar peticiones HTTP genéricas
|
|
32
|
+
|
|
33
|
+
get<T>(endpoint: string, headers?: any): Promise<T> {
|
|
34
|
+
return instance.get<any, T>(endpoint, { headers });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
post<T>(endpoint: string, body: any, headers?: any): Promise<T> {
|
|
38
|
+
return instance.post<any, T>(endpoint, body, { headers });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
put<T>(endpoint: string, body: any, headers?: any): Promise<T> {
|
|
42
|
+
return instance.put<any, T>(endpoint, body, { headers });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
patch<T>(endpoint: string, body: any, headers?: any): Promise<T> {
|
|
46
|
+
return instance.patch<any, T>(endpoint, body, { headers });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
delete<T>(endpoint: string, headers?: any): Promise<T> {
|
|
50
|
+
return instance.delete<any, T>(endpoint, { headers });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const api = new ApiService();
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { ErrorResponse } from '../../providers/NotificationProvider/context/NotificationContext';
|
|
2
|
+
import axios, { type AxiosError, type AxiosInstance } from 'axios';
|
|
3
|
+
|
|
4
|
+
// Clase de servicio para realizar peticiones HTTP a la API de Ibio
|
|
5
|
+
export class ApiService<ItemType, ResponseType = ItemType> {
|
|
6
|
+
protected api: AxiosInstance;
|
|
7
|
+
protected endpoint: string;
|
|
8
|
+
protected baseURL: string;
|
|
9
|
+
protected headers: any;
|
|
10
|
+
protected credentials: boolean;
|
|
11
|
+
|
|
12
|
+
constructor(endpoint?: string, baseURL: string = import.meta.env.VITE_APP_API_URL, credentials?: boolean, headers?: any) {
|
|
13
|
+
this.endpoint = endpoint ?? '';
|
|
14
|
+
this.baseURL = baseURL;
|
|
15
|
+
this.headers = headers ?? { 'Content-Type': 'application/json', 'X-API-Key': import.meta.env.VITE_APP_API_KEY as string, };
|
|
16
|
+
this.credentials = credentials ?? false;
|
|
17
|
+
|
|
18
|
+
this.api = axios.create({
|
|
19
|
+
baseURL: this.baseURL,
|
|
20
|
+
withCredentials: this.credentials,
|
|
21
|
+
headers: this.headers,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
this.api.interceptors.response.use(
|
|
25
|
+
(response) => response.data, // Retornar directamente la data de la respuesta
|
|
26
|
+
(error: AxiosError) => {
|
|
27
|
+
const errorData = error.response?.data as any || {};
|
|
28
|
+
|
|
29
|
+
// Creamos el ErrorResponse estructurado siguiendo tu patrón actual
|
|
30
|
+
const errorResponse: ErrorResponse = {
|
|
31
|
+
status: errorData.status || error.response?.status || 500,
|
|
32
|
+
error: errorData.error || error.code || 'Error',
|
|
33
|
+
message: errorData.message || error.message || 'An unexpected error occurred',
|
|
34
|
+
path: errorData.path || error.config?.url || ''
|
|
35
|
+
}
|
|
36
|
+
return Promise.reject(errorResponse);
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
protected parseResponse(data: any): ItemType {
|
|
42
|
+
return this.parseItemResponse(data);
|
|
43
|
+
}
|
|
44
|
+
protected parseItemResponse(data: any): ItemType {
|
|
45
|
+
return data as unknown as ItemType; // Implementación por defecto, puede ser sobrescrita
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
protected async handleRequest<R>(
|
|
49
|
+
request: Promise<any>,
|
|
50
|
+
isArray: boolean = false,
|
|
51
|
+
): Promise<R> {
|
|
52
|
+
try {
|
|
53
|
+
const response = await request;
|
|
54
|
+
|
|
55
|
+
// Handle array extraction if needed
|
|
56
|
+
if (isArray && response && typeof response === 'object' && !Array.isArray(response)) {
|
|
57
|
+
if ('data' in response) {
|
|
58
|
+
return response.data as R;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return response as R;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error('Error in API request:', error);
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get(endpoint: string = this.endpoint, headers: any = this.headers): Promise<ItemType[]> {
|
|
70
|
+
return this.handleRequest<ResponseType[]>(this.api.get(endpoint, { headers }), true)
|
|
71
|
+
.then((data) => (data || []).map((item) => this.parseResponse(item)));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
post(endpoint: string, body: any, headers?: any): Promise<ItemType> {
|
|
75
|
+
return this.handleRequest<ResponseType>(this.api.post(endpoint, body, { headers }))
|
|
76
|
+
.then((data) => this.parseResponse(data));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
put(endpoint: string, body: any, headers?: any): Promise<ItemType> {
|
|
80
|
+
return this.handleRequest<ResponseType>(this.api.put(endpoint, body, { headers }))
|
|
81
|
+
.then((data) => this.parseResponse(data));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
patch(endpoint: string, body: any, headers?: any): Promise<ItemType> {
|
|
85
|
+
return this.handleRequest<ResponseType>(this.api.patch(endpoint, body, { headers }))
|
|
86
|
+
.then((data) => this.parseResponse(data));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
delete(endpoint: string, headers?: any): Promise<ItemType> {
|
|
90
|
+
return this.handleRequest<ResponseType>(this.api.delete(endpoint, { headers }))
|
|
91
|
+
.then((data) => this.parseResponse(data));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|