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.
Files changed (32) hide show
  1. package/README.md +116 -0
  2. package/index.js +21 -0
  3. package/package.json +17 -0
  4. package/template/.env +1 -0
  5. package/template/README.md +116 -0
  6. package/template/eslint.config.js +26 -0
  7. package/template/index.html +13 -0
  8. package/template/package.json +40 -0
  9. package/template/pnpm-lock.yaml +3339 -0
  10. package/template/public/vite.svg +1 -0
  11. package/template/src/App.tsx +30 -0
  12. package/template/src/Router.tsx +17 -0
  13. package/template/src/components/main/ThemeSwitch.tsx +80 -0
  14. package/template/src/components/shared/MyModal.tsx +51 -0
  15. package/template/src/components/shared/MyPopover.tsx +44 -0
  16. package/template/src/hooks/shared/useNotification.ts +25 -0
  17. package/template/src/hooks/shared/useSetTimezone.ts +20 -0
  18. package/template/src/index.css +9 -0
  19. package/template/src/layouts/MainLayout.tsx +23 -0
  20. package/template/src/main.tsx +10 -0
  21. package/template/src/modules/MainPage/MainPageContent.tsx +100 -0
  22. package/template/src/pages/MainPage/MainPage.tsx +12 -0
  23. package/template/src/providers/NotificationProvider/NotificationProvider.tsx +61 -0
  24. package/template/src/providers/NotificationProvider/context/NotificationContext.tsx +20 -0
  25. package/template/src/services/ApiService.ts +54 -0
  26. package/template/src/services/ToBeTested/ApiService-New.ts +94 -0
  27. package/template/src/store/ThemeStore/Themes/themes.ts +28 -0
  28. package/template/src/store/ThemeStore/themeStore.ts +28 -0
  29. package/template/tsconfig.app.json +28 -0
  30. package/template/tsconfig.json +7 -0
  31. package/template/tsconfig.node.json +26 -0
  32. 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,9 @@
1
+ @import 'tailwindcss';
2
+
3
+ html,
4
+ body,
5
+ #root {
6
+ height: 100%;
7
+ margin: 0;
8
+ padding: 0;
9
+ }
@@ -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,10 @@
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.tsx'
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
@@ -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
+