Versión 0.0.6 estable
This commit is contained in:
commit
650b2e2df7
30 changed files with 5776 additions and 0 deletions
20
.eslintrc.cjs
Normal file
20
.eslintrc.cjs
Normal file
|
@ -0,0 +1,20 @@
|
|||
/* eslint-env node */
|
||||
|
||||
module.exports = {
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react/jsx-runtime',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
||||
settings: { react: { version: '18.2' } },
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
4646
package-lock.json
generated
Normal file
4646
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
66
package.json
Normal file
66
package.json
Normal file
|
@ -0,0 +1,66 @@
|
|||
{
|
||||
"name": "@chimera-pe/react-saas",
|
||||
"version": "0.0.5",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/icons-material": "^5.14.1",
|
||||
"@mui/lab": "^5.0.0-alpha.137",
|
||||
"@mui/material": "^5.14.1",
|
||||
"@mui/x-date-pickers": "^6.10.1",
|
||||
"@reduxjs/toolkit": "^1.9.5",
|
||||
"axios": "^1.4.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"final-form": "^4.20.9",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"mui-rff": "^6.2.0",
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18",
|
||||
"react-final-form": "^6.5.9",
|
||||
"react-polyglot": "^0.7.2",
|
||||
"react-redux": "^8.1.1",
|
||||
"react-router-dom": "^6.14.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.14",
|
||||
"@types/react-dom": "^18.2.6",
|
||||
"@vitejs/plugin-react": "^4.0.1",
|
||||
"eslint": "^8.44.0",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.1",
|
||||
"vite": "^4.4.0"
|
||||
},
|
||||
"description": "Componente integrador con SaaS",
|
||||
"main": "./dist/react-saas.umd.cjs",
|
||||
"module": "./dist/react-saas.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/react-saas.js",
|
||||
"require": "./dist/react-saas.umd.cjs"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.chimera.com.pe/chimera/react-saas.git"
|
||||
},
|
||||
"keywords": [
|
||||
"saas"
|
||||
],
|
||||
"author": "Germán Enríquez",
|
||||
"license": "ISC"
|
||||
}
|
2
src/Constantes.js
Normal file
2
src/Constantes.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
export const saasURL="https://saas.chimera.com.pe/backend";
|
||||
export const authURL="https://saas.chimera.com.pe/oauth";
|
9
src/api/inicializarApi.jsx
Normal file
9
src/api/inicializarApi.jsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
import axios from "axios";
|
||||
import {saasURL} from "../Constantes";
|
||||
|
||||
export const identidadApi = (url = saasURL,aplicacion) => axios({
|
||||
url: `${url}/identidad/`,
|
||||
params: {
|
||||
codigoAplicacion: aplicacion
|
||||
}
|
||||
});
|
30
src/api/loginApi.jsx
Normal file
30
src/api/loginApi.jsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import axios from "axios";
|
||||
import {authURL} from "../Constantes";
|
||||
|
||||
export const loginApi = (url = authURL) => ({
|
||||
login: (clientCredentials,data) => axios({
|
||||
url: `${url}/oauth/token`,
|
||||
headers: {
|
||||
"content-type": "application/x-www-form-urlencoded",
|
||||
Authorization: `Basic ${clientCredentials}`
|
||||
},
|
||||
method: "post",
|
||||
data: {
|
||||
username: data.correo,
|
||||
password: data.password,
|
||||
"grant_type": "password"
|
||||
}
|
||||
}),
|
||||
refreshToken: (clientCredentials,refreshToken) => axios({
|
||||
url: `${url}/oauth/token`,
|
||||
headers: {
|
||||
"content-type": "application/x-www-form-urlencoded",
|
||||
Authorization: `Basic ${clientCredentials}`
|
||||
},
|
||||
method: "post",
|
||||
data: {
|
||||
"refresh_token": refreshToken,
|
||||
"grant_type": "refresh_token"
|
||||
}
|
||||
})
|
||||
});
|
15
src/components/Cargando.jsx
Normal file
15
src/components/Cargando.jsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
import {Box,CircularProgress} from "@mui/material";
|
||||
|
||||
const Cargando=() => (
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
|
||||
export default Cargando;
|
31
src/components/Error.jsx
Normal file
31
src/components/Error.jsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import {Box,Alert,AlertTitle} from "@mui/material";
|
||||
import {useTranslate} from "react-polyglot";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const Error = ({titulo,texto,align = "center",severity = "error"}) => {
|
||||
const translate = useTranslate();
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: align
|
||||
}}>
|
||||
<Alert severity={severity}>
|
||||
<AlertTitle>{translate(titulo)}</AlertTitle>
|
||||
{texto && translate(texto)}
|
||||
</Alert>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Error.propTypes={
|
||||
titulo: PropTypes.string.isRequired,
|
||||
texto: PropTypes.string,
|
||||
align: PropTypes.string,
|
||||
severity: PropTypes.string
|
||||
};
|
||||
|
||||
export default Error;
|
60
src/components/Idioma.jsx
Normal file
60
src/components/Idioma.jsx
Normal file
|
@ -0,0 +1,60 @@
|
|||
import {useEffect} from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import {useDispatch,useSelector} from "react-redux";
|
||||
import {I18n} from "react-polyglot";
|
||||
import {enGB,es} from "date-fns/locale"
|
||||
import {LocalizationProvider} from "@mui/x-date-pickers";
|
||||
import {AdapterDateFns} from "@mui/x-date-pickers/AdapterDateFns";
|
||||
import {cambiarIdioma} from "../redux";
|
||||
import saasMessages from "../i18n";
|
||||
|
||||
const supportedLocales={en: enGB,es};
|
||||
|
||||
const IdiomaInner=({messages,children}) => {
|
||||
const idioma=useSelector(store => store.ui.idioma);
|
||||
|
||||
return (
|
||||
<I18n locale={idioma} messages={messages[idioma]}>
|
||||
<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={supportedLocales[idioma]}>
|
||||
{children}
|
||||
</LocalizationProvider>
|
||||
</I18n>
|
||||
);
|
||||
};
|
||||
|
||||
IdiomaInner.propTypes={
|
||||
messages: PropTypes.object,
|
||||
children: PropTypes.element.isRequired
|
||||
};
|
||||
|
||||
const Idioma=({messages,idioma,children}) => {
|
||||
const dispatch=useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
if(idioma){
|
||||
dispatch(cambiarIdioma(idioma));
|
||||
}
|
||||
},[dispatch,idioma]);
|
||||
|
||||
const m={};
|
||||
Object.keys(messages).forEach(key => {
|
||||
m[key]={
|
||||
...messages[key],
|
||||
saas: saasMessages[key]
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<IdiomaInner messages={m}>
|
||||
{children}
|
||||
</IdiomaInner>
|
||||
);
|
||||
};
|
||||
|
||||
Idioma.propTypes={
|
||||
messages: PropTypes.object,
|
||||
idioma: PropTypes.string,
|
||||
children: PropTypes.element.isRequired
|
||||
};
|
||||
|
||||
export default Idioma;
|
79
src/components/Inicializar.jsx
Normal file
79
src/components/Inicializar.jsx
Normal file
|
@ -0,0 +1,79 @@
|
|||
import {useEffect} from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import {Box} from "@mui/material";
|
||||
import {useDispatch,useSelector} from "react-redux";
|
||||
import {inicializar} from "../redux";
|
||||
import Cargando from "./Cargando";
|
||||
import Error from "./Error";
|
||||
import Notificacion from "./Notificacion";
|
||||
import Idioma from "./Idioma";
|
||||
import Tema from "./Tema";
|
||||
import MainRouter from "./MainRouter";
|
||||
|
||||
const InicializarInner = ({devURL,children}) => {
|
||||
const aplicacion = useSelector(store => store.aplicacion);
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
minHeight: "100vh",
|
||||
justifyContent: "flex-start",
|
||||
backgroundColor: "background.default"
|
||||
}}>
|
||||
{aplicacion.inicializando ?
|
||||
<Cargando />
|
||||
: (aplicacion.error || !aplicacion.inicializado) ?
|
||||
<Error titulo={"saas.inicializar.error.titulo"} texto={"saas.inicializar.error.mensaje"} />
|
||||
:
|
||||
<>
|
||||
<MainRouter devURL={devURL} requiereLogin={aplicacion.instancia.requiereLogin}>
|
||||
{children}
|
||||
</MainRouter>
|
||||
<Notificacion />
|
||||
</>
|
||||
}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
InicializarInner.propTypes={
|
||||
devURL: PropTypes.string,
|
||||
children: PropTypes.element.isRequired
|
||||
};
|
||||
|
||||
const Inicializar = ({
|
||||
aplicacion,
|
||||
devSaasURL,
|
||||
devAuthURL,
|
||||
messages,
|
||||
idioma,
|
||||
children
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(inicializar({devURL: devSaasURL,aplicacion: aplicacion}));
|
||||
},[dispatch,aplicacion,devSaasURL]);
|
||||
|
||||
return (
|
||||
<Idioma messages={messages} idioma={idioma}>
|
||||
<Tema>
|
||||
<InicializarInner devURL={devAuthURL}>
|
||||
{children}
|
||||
</InicializarInner>
|
||||
</Tema>
|
||||
</Idioma>
|
||||
);
|
||||
};
|
||||
|
||||
Inicializar.propTypes={
|
||||
aplicacion: PropTypes.string.isRequired,
|
||||
devSaasURL: PropTypes.string,
|
||||
devAuthURL: PropTypes.string,
|
||||
messages: PropTypes.object,
|
||||
idioma: PropTypes.string,
|
||||
children: PropTypes.element.isRequired
|
||||
};
|
||||
|
||||
export default Inicializar;
|
211
src/components/Login.jsx
Normal file
211
src/components/Login.jsx
Normal file
|
@ -0,0 +1,211 @@
|
|||
import {useEffect} from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import {useSelector,useDispatch} from "react-redux";
|
||||
import {Navigate,useLocation} from "react-router-dom";
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
Grid,
|
||||
Typography,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardContent,
|
||||
InputAdornment,
|
||||
Button,
|
||||
CircularProgress,
|
||||
AppBar,
|
||||
Toolbar,
|
||||
Link
|
||||
} from "@mui/material";
|
||||
import {Email,Lock} from "@mui/icons-material";
|
||||
import {Form} from "react-final-form";
|
||||
import {TextField} from "mui-rff";
|
||||
import {useTranslate} from "react-polyglot";
|
||||
import {requestToken} from "../redux";
|
||||
import {useCheckLogin,useNotificar} from "../hooks";
|
||||
|
||||
const FormularioLogin = ({devURL}) => {
|
||||
const dispatch = useDispatch();
|
||||
const translate = useTranslate();
|
||||
const notificar = useNotificar();
|
||||
const {cargando,error} = useSelector(store => store.login);
|
||||
const instancia = useSelector(store => store.aplicacion.instancia);
|
||||
|
||||
const submit = values => {
|
||||
dispatch(requestToken({
|
||||
devURL: devURL,
|
||||
clientCredentials: instancia.clientCredentials,
|
||||
data: {
|
||||
correo: values.correo,
|
||||
password: values.password
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
const validate = values => {
|
||||
const errors = {correo: undefined,password: undefined};
|
||||
|
||||
if(!values.correo) {
|
||||
errors.correo = translate("saas.login.validacion.correo");
|
||||
}
|
||||
if(!values.password) {
|
||||
errors.password = translate("saas.login.validacion.password");
|
||||
}
|
||||
return errors;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if(error) {
|
||||
notificar("saas.login.error","error");
|
||||
}
|
||||
},[notificar,error]);
|
||||
|
||||
return (
|
||||
<Form
|
||||
onSubmit={submit}
|
||||
validate={validate}
|
||||
render={({handleSubmit}) => (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<TextField
|
||||
id="correo"
|
||||
name="correo"
|
||||
label={translate("saas.login.correo")}
|
||||
variant="outlined"
|
||||
autoComplete="off"
|
||||
disabled={cargando}
|
||||
autoFocus
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<Email color="primary" />
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
label={translate("saas.login.password")}
|
||||
variant="outlined"
|
||||
autoComplete="current-password"
|
||||
disabled={cargando}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<Lock color="primary" />
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Grid container>
|
||||
<Grid item xs={6}></Grid>
|
||||
<Grid item xs={6} align="right">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
disabled={cargando}
|
||||
>
|
||||
{cargando ?
|
||||
<CircularProgress size={24} thickness={4} />
|
||||
:
|
||||
translate("saas.login.ingresar")
|
||||
}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
FormularioLogin.propTypes = {
|
||||
devURL: PropTypes.string
|
||||
};
|
||||
|
||||
const Login = ({devURL}) => {
|
||||
const instancia = useSelector(store => store.aplicacion.instancia);
|
||||
const translate = useTranslate();
|
||||
const location = useLocation();
|
||||
const autenticado = useCheckLogin(devURL);
|
||||
const {from} = location.state || {from: {pathname: "/"}};
|
||||
|
||||
if(!instancia.requiereLogin || autenticado) {
|
||||
return <Navigate to={from} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
minHeight: "100vh",
|
||||
backgroundColor: "primary.main",
|
||||
"&::before": {
|
||||
content: '""',
|
||||
backgroundImage: `url(${instancia.logo})`,
|
||||
backgroundSize: "contain",
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundPosition: "right",
|
||||
position: "absolute",
|
||||
top: "20%",
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
width: "50%",
|
||||
opacity: 0.02,
|
||||
filter: "grayscale(100%)"
|
||||
}
|
||||
}}>
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
flexGrow: 1
|
||||
}}>
|
||||
<Container maxWidth="md">
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} lg={5} align="center">
|
||||
<img src={instancia.logo} alt={instancia.nombre} style={{maxWidth: "100%"}} />
|
||||
<Typography variant="h3" align="center">{translate("aplicacion.nombre",{smart_count: 1})}</Typography>
|
||||
<Typography variant="h5" align="center">{instancia.nombre}</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} lg={7} sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
zIndex: 5
|
||||
}}>
|
||||
<Card elevation={5}>
|
||||
<CardHeader title={translate("saas.login.titulo")} titleTypographyProps={{align: "center"}} />
|
||||
<CardContent sx={{
|
||||
"& .MuiTextField-root": {
|
||||
mb: 2
|
||||
},
|
||||
}}>
|
||||
<FormularioLogin devURL={devURL} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
</Box>
|
||||
<AppBar position="static" color="primary">
|
||||
<Toolbar sx={{justifyContent: "center"}}>
|
||||
<Typography variant="caption">
|
||||
{translate("saas.copy")}
|
||||
<Link href="//chimera.com.pe" color="inherit" target="_blank" rel="noreferrer">Chimera Software</Link>
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Login.propTypes = {
|
||||
devURL: PropTypes.string
|
||||
};
|
||||
|
||||
export default Login;
|
48
src/components/MainRouter.jsx
Normal file
48
src/components/MainRouter.jsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
import PropTypes from "prop-types";
|
||||
import {
|
||||
BrowserRouter,
|
||||
Routes,
|
||||
Route,
|
||||
Navigate,
|
||||
useLocation
|
||||
} from "react-router-dom";
|
||||
import {useCheckLogin} from "../hooks";
|
||||
import Login from "./Login";
|
||||
|
||||
const RequiereAuth = ({devURL,redirectTo,children}) => {
|
||||
const location = useLocation();
|
||||
const autenticado = useCheckLogin(devURL);
|
||||
|
||||
return autenticado ? children : <Navigate to={redirectTo} state={{from: location}} replace />;
|
||||
};
|
||||
|
||||
RequiereAuth.propTypes={
|
||||
devURL: PropTypes.string,
|
||||
redirectTo: PropTypes.string.isRequired,
|
||||
children: PropTypes.element.isRequired
|
||||
};
|
||||
|
||||
const MainRouter = ({devURL,requiereLogin,children}) => (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login devURL={devURL} />} />
|
||||
<Route
|
||||
path="/*"
|
||||
element={requiereLogin ? (
|
||||
<RequiereAuth devURL={devURL} redirectTo="/login">
|
||||
{children}
|
||||
</RequiereAuth>
|
||||
) : children
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
MainRouter.propTypes={
|
||||
devURL: PropTypes.string,
|
||||
requiereLogin: PropTypes.bool,
|
||||
children: PropTypes.element.isRequired
|
||||
};
|
||||
|
||||
export default MainRouter;
|
36
src/components/Notificacion.jsx
Normal file
36
src/components/Notificacion.jsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
import {useEffect,useState} from "react";
|
||||
import {useSelector,useDispatch} from "react-redux";
|
||||
import {Snackbar,Alert} from "@mui/material";
|
||||
import {useTranslate} from "react-polyglot";
|
||||
import {ocultarNotificacion} from "../redux";
|
||||
|
||||
const Notificacion=() => {
|
||||
const [open,setOpen]=useState(false);
|
||||
const dispatch=useDispatch();
|
||||
const translate=useTranslate();
|
||||
const notificacion=useSelector(store => store.notificaciones[0]);
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(!!notificacion);
|
||||
},[notificacion]);
|
||||
|
||||
const handleClose=() => {
|
||||
setOpen(false);
|
||||
dispatch(ocultarNotificacion());
|
||||
};
|
||||
|
||||
return (
|
||||
<Snackbar
|
||||
open={open}
|
||||
message={notificacion && notificacion.mensaje && notificacion.tipo === "default" && translate(notificacion.mensaje)}
|
||||
autoHideDuration={5000}
|
||||
onClose={handleClose}
|
||||
>
|
||||
{notificacion && notificacion.mensaje && notificacion.tipo !== "default" &&
|
||||
<Alert severity={notificacion.tipo}>{translate(notificacion.mensaje)}</Alert>
|
||||
}
|
||||
</Snackbar>
|
||||
);
|
||||
};
|
||||
|
||||
export default Notificacion;
|
42
src/components/SaasApp.jsx
Normal file
42
src/components/SaasApp.jsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
import PropTypes from "prop-types";
|
||||
import {Provider} from "react-redux";
|
||||
import {store} from "../redux";
|
||||
import Inicializar from "./Inicializar";
|
||||
|
||||
const SaasApp = ({
|
||||
customReducers,
|
||||
aplicacion,
|
||||
devSaasURL,
|
||||
devAuthURL,
|
||||
dev = false,
|
||||
idioma,
|
||||
messages,
|
||||
children
|
||||
}) => {
|
||||
return (
|
||||
<Provider store={store(customReducers)}>
|
||||
<Inicializar
|
||||
aplicacion={aplicacion}
|
||||
devSaasURL={dev ? devSaasURL : undefined}
|
||||
devAuthURL={dev ? devAuthURL : undefined}
|
||||
idioma={idioma}
|
||||
messages={messages}
|
||||
>
|
||||
{children}
|
||||
</Inicializar>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
SaasApp.propTypes={
|
||||
customReducers: PropTypes.object,
|
||||
aplicacion: PropTypes.string.isRequired,
|
||||
devSaasURL: PropTypes.string,
|
||||
devAuthURL: PropTypes.string,
|
||||
dev: PropTypes.bool,
|
||||
idioma: PropTypes.string,
|
||||
messages: PropTypes.object,
|
||||
children: PropTypes.element.isRequired
|
||||
};
|
||||
|
||||
export default SaasApp;
|
64
src/components/Tema.jsx
Normal file
64
src/components/Tema.jsx
Normal file
|
@ -0,0 +1,64 @@
|
|||
import {useMemo,useEffect} from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import {useDispatch,useSelector} from "react-redux";
|
||||
import {CssBaseline,useMediaQuery} from "@mui/material";
|
||||
import {ThemeProvider,createTheme} from "@mui/material/styles";
|
||||
import {grey} from "@mui/material/colors";
|
||||
import {cambiarTema} from "../redux";
|
||||
|
||||
const Tema=({children}) => {
|
||||
const dispatch=useDispatch();
|
||||
const {instancia}=useSelector(store => store.aplicacion);
|
||||
const tema=useSelector(store => store.ui.tema);
|
||||
const prefersDarkMode=useMediaQuery("(prefers-color-scheme: dark)");
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(cambiarTema(prefersDarkMode ? "dark" : "light"));
|
||||
},[dispatch,prefersDarkMode]);
|
||||
|
||||
const theme=useMemo(() => createTheme({
|
||||
palette: {
|
||||
mode: tema,
|
||||
primary: {
|
||||
main: instancia.color.primary
|
||||
},
|
||||
secondary: {
|
||||
main: instancia.color.secondary
|
||||
},
|
||||
error: {
|
||||
main: instancia.color.error
|
||||
},
|
||||
warning: {
|
||||
main: instancia.color.warning
|
||||
},
|
||||
info: {
|
||||
main: instancia.color.info
|
||||
},
|
||||
success: {
|
||||
main: instancia.color.success
|
||||
},
|
||||
...(tema === "light" && {
|
||||
background: {
|
||||
default: grey["A200"],
|
||||
paper: grey[100]
|
||||
}
|
||||
})
|
||||
},
|
||||
shape: {
|
||||
borderRadius: 6
|
||||
}
|
||||
}),[tema,instancia]);
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
Tema.propTypes={
|
||||
children: PropTypes.element.isRequired
|
||||
};
|
||||
|
||||
export default Tema;
|
9
src/components/index.jsx
Normal file
9
src/components/index.jsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
import Cargando from "./Cargando";
|
||||
import Error from "./Error";
|
||||
import SaasApp from "./SaasApp";
|
||||
|
||||
export {
|
||||
Cargando,
|
||||
Error,
|
||||
SaasApp
|
||||
};
|
7
src/hooks/index.jsx
Normal file
7
src/hooks/index.jsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
import useCheckLogin from "./useCheckLogin";
|
||||
import useNotificar from "./useNotificar";
|
||||
|
||||
export {
|
||||
useCheckLogin,
|
||||
useNotificar
|
||||
};
|
24
src/hooks/useCheckLogin.jsx
Normal file
24
src/hooks/useCheckLogin.jsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import {useEffect} from "react";
|
||||
import {useDispatch,useSelector} from "react-redux";
|
||||
import {logout, refreshToken} from "../redux";
|
||||
|
||||
const useCheckLogin = (devURL) => {
|
||||
const dispatch = useDispatch();
|
||||
const login = useSelector(store => store.login);
|
||||
const instancia = useSelector(store => store.aplicacion.instancia);
|
||||
|
||||
useEffect(() => {
|
||||
if(login.autenticado && !!login.expiracion && new Date(login.expiracion) < new Date()){
|
||||
if(login.refreshToken){
|
||||
dispatch(refreshToken(devURL,instancia.clientCredentials,login.refreshToken));
|
||||
}
|
||||
else{
|
||||
dispatch(logout());
|
||||
}
|
||||
}
|
||||
},[devURL,instancia.clientCredentials,login,dispatch]);
|
||||
|
||||
return login.autenticado;
|
||||
};
|
||||
|
||||
export default useCheckLogin;
|
12
src/hooks/useNotificar.jsx
Normal file
12
src/hooks/useNotificar.jsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
import {useCallback} from "react";
|
||||
import {useDispatch} from "react-redux";
|
||||
import {mostrarNotificacion} from "../redux";
|
||||
|
||||
const useNotificar = () => {
|
||||
const dispatch = useDispatch();
|
||||
return useCallback((mensaje,tipo = "default") => {
|
||||
dispatch(mostrarNotificacion({mensaje,tipo}));
|
||||
},[dispatch]);
|
||||
};
|
||||
|
||||
export default useNotificar;
|
22
src/i18n/es.jsx
Normal file
22
src/i18n/es.jsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
const es={
|
||||
inicializar: {
|
||||
error: {
|
||||
titulo: "Error iniciando aplicación",
|
||||
mensaje: "No fue posible conectarnos con el servicio"
|
||||
}
|
||||
},
|
||||
login: {
|
||||
titulo: "Acceder al sistema",
|
||||
correo: "Correo electrónico",
|
||||
password: "Contraseña",
|
||||
ingresar: "Acceder",
|
||||
validacion: {
|
||||
correo: "Debe ingresar una dirección de correo",
|
||||
password: "Debe ingresar una contraseña"
|
||||
},
|
||||
error: "Usuario o contraseña incorrectos"
|
||||
},
|
||||
copy: "Todos los derechos reservados "
|
||||
};
|
||||
|
||||
export default es;
|
7
src/i18n/index.jsx
Normal file
7
src/i18n/index.jsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
import es from "./es";
|
||||
|
||||
const messages={
|
||||
es: es
|
||||
};
|
||||
|
||||
export default messages;
|
12
src/index.js
Normal file
12
src/index.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import {SaasApp} from "./components";
|
||||
import {useNotificar} from "./hooks";
|
||||
import {Cargando,Error} from "./components";
|
||||
import {logout} from "./redux";
|
||||
|
||||
export {
|
||||
SaasApp,
|
||||
useNotificar,
|
||||
Cargando,
|
||||
Error,
|
||||
logout
|
||||
};
|
17
src/redux/index.jsx
Normal file
17
src/redux/index.jsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import {store} from "./store";
|
||||
import {inicializar} from "./inicializarSlice";
|
||||
import {refreshToken,requestToken,logout} from "./loginSlice";
|
||||
import {mostrarNotificacion,ocultarNotificacion} from "./notificacionSlice";
|
||||
import {cambiarIdioma,cambiarTema} from "./uiSlice";
|
||||
|
||||
export {
|
||||
store,
|
||||
inicializar,
|
||||
refreshToken,
|
||||
requestToken,
|
||||
logout,
|
||||
mostrarNotificacion,
|
||||
ocultarNotificacion,
|
||||
cambiarIdioma,
|
||||
cambiarTema
|
||||
};
|
57
src/redux/inicializarSlice.jsx
Normal file
57
src/redux/inicializarSlice.jsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
import {createSlice,createAsyncThunk} from "@reduxjs/toolkit";
|
||||
import {identidadApi} from "../api/inicializarApi";
|
||||
|
||||
const colorDefault = {
|
||||
primary: "#1C6CCC",
|
||||
secondary: "#17A7FF",
|
||||
error: "#f44336",
|
||||
warning: "#ff9800",
|
||||
info: "#2196f3",
|
||||
success: "#4caf50"
|
||||
};
|
||||
|
||||
const inicializarSlice = createSlice({
|
||||
name: "inicializar",
|
||||
initialState: {
|
||||
inicializando: true,
|
||||
inicializado: false,
|
||||
instancia: {
|
||||
color: colorDefault
|
||||
},
|
||||
error: null
|
||||
},
|
||||
extraReducers(builder) {
|
||||
builder
|
||||
.addCase(inicializar.pending,state => {
|
||||
state.inicializando = true;
|
||||
})
|
||||
.addCase(inicializar.fulfilled,(state,action) => {
|
||||
state.inicializando = false;
|
||||
state.inicializado = true;
|
||||
state.instancia = {
|
||||
...action.payload,
|
||||
abreviatura: action.payload.nombre.match(/\b([A-Z])/g).join(""),
|
||||
color: {
|
||||
...colorDefault,
|
||||
...action.payload.color
|
||||
}
|
||||
};
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(inicializar.rejected,(state,action) => {
|
||||
state.inicializando = false;
|
||||
state.inicializado = false;
|
||||
state.instancia = {
|
||||
color: colorDefault
|
||||
};
|
||||
state.error = action.payload;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const inicializar = createAsyncThunk("inicializar",async (payload) => {
|
||||
const response = await identidadApi(payload.devURL,payload.aplicacion);
|
||||
return response.data;
|
||||
});
|
||||
|
||||
export default inicializarSlice.reducer;
|
94
src/redux/loginSlice.jsx
Normal file
94
src/redux/loginSlice.jsx
Normal file
|
@ -0,0 +1,94 @@
|
|||
import {createSlice,createAsyncThunk} from "@reduxjs/toolkit";
|
||||
import {loginApi} from "../api/loginApi";
|
||||
import jwtDecode from "jwt-decode";
|
||||
|
||||
const loginSlice = createSlice({
|
||||
name: "login",
|
||||
initialState: {
|
||||
cargando: false,
|
||||
autenticado: false,
|
||||
token: null,
|
||||
refreshToken: null,
|
||||
expiracion: null,
|
||||
usuario: null,
|
||||
perfiles: [],
|
||||
error: null
|
||||
},
|
||||
reducers: {
|
||||
logout: state => {
|
||||
state.cargando = false;
|
||||
state.autenticado = false;
|
||||
state.token = null;
|
||||
state.refreshToken = null;
|
||||
state.expiracion = null;
|
||||
state.usuario = null;
|
||||
state.perfiles = [];
|
||||
state.error = null;
|
||||
}
|
||||
},
|
||||
extraReducers(builder) {
|
||||
builder
|
||||
.addCase(requestToken.pending,state => {
|
||||
state.cargando = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(requestToken.fulfilled,(state,action) => {
|
||||
const token = action.payload.access_token;
|
||||
const jwtToken = jwtDecode(token);
|
||||
const expiracion = new Date();
|
||||
expiracion.setSeconds(expiracion.getSeconds() + action.payload.expires_in);
|
||||
state.cargando = false;
|
||||
state.autenticado = true;
|
||||
state.token = token;
|
||||
state.refreshToken = action.payload.refresh_token;
|
||||
state.expiracion = expiracion.getTime();
|
||||
state.usuario = jwtToken.name;
|
||||
state.perfiles = jwtToken.authorities;
|
||||
})
|
||||
.addCase(requestToken.rejected,(state,action) => {
|
||||
console.log(action);
|
||||
state.cargando = false;
|
||||
state.autenticado = false;
|
||||
state.token = null;
|
||||
state.refreshToken = null;
|
||||
state.expiracion = null;
|
||||
state.usuario = null;
|
||||
state.perfiles = [];
|
||||
state.error = action.error?.message;
|
||||
})
|
||||
.addCase(refreshToken.pending,state => {
|
||||
state.cargando = true;
|
||||
})
|
||||
.addCase(refreshToken.fulfilled,(state,action) => {
|
||||
const expiracion = new Date();
|
||||
expiracion.setSeconds(expiracion.getSeconds() + action.payload.expires_in);
|
||||
state.token = action.payload.access_token;
|
||||
state.refreshToken = action.payload.refresh_token;
|
||||
state.expiracion = expiracion.getTime();
|
||||
})
|
||||
.addCase(refreshToken.rejected,(state,action) => {
|
||||
state.cargando = false;
|
||||
state.autenticado = false;
|
||||
state.token = null;
|
||||
state.refreshToken = null;
|
||||
state.expiracion = null;
|
||||
state.usuario = null;
|
||||
state.perfiles = [];
|
||||
state.error = action.error?.message;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const requestToken = createAsyncThunk("login/requestToken",async (payload) => {
|
||||
const response = await loginApi(payload.devURL).login(payload.clientCredentials,payload.data);
|
||||
return response.data;
|
||||
});
|
||||
|
||||
export const refreshToken = createAsyncThunk("login/refreshToken",async (devURL,clientCredentials,refreshToken) => {
|
||||
const response = await loginApi(devURL).refreshToken(clientCredentials,refreshToken);
|
||||
return response.data;
|
||||
});
|
||||
|
||||
export const {logout} = loginSlice.actions;
|
||||
|
||||
export default loginSlice.reducer;
|
20
src/redux/notificacionSlice.jsx
Normal file
20
src/redux/notificacionSlice.jsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import {createSlice} from "@reduxjs/toolkit";
|
||||
|
||||
const notificacionSlice=createSlice({
|
||||
name: "notificacion",
|
||||
initialState: [],
|
||||
reducers: {
|
||||
mostrarNotificacion: (state,action) => {
|
||||
state.push(action.payload);
|
||||
},
|
||||
ocultarNotificacion: state => {
|
||||
state.pop();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const getNotificacion=store => Array.isArray(store.notificacion) && store.notificacion.length ? store.notificacion[0] : null;
|
||||
|
||||
export const {mostrarNotificacion,ocultarNotificacion}=notificacionSlice.actions;
|
||||
|
||||
export default notificacionSlice.reducer;
|
15
src/redux/store.jsx
Normal file
15
src/redux/store.jsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
import {configureStore} from "@reduxjs/toolkit";
|
||||
import uiSlice from "./uiSlice";
|
||||
import loginSlice from "./loginSlice";
|
||||
import notificacionSlice from "./notificacionSlice";
|
||||
import inicializarSlice from "./inicializarSlice";
|
||||
|
||||
export const store=(customReducers) => configureStore({
|
||||
reducer: {
|
||||
ui: uiSlice,
|
||||
aplicacion: inicializarSlice,
|
||||
login: loginSlice,
|
||||
notificaciones: notificacionSlice,
|
||||
...customReducers
|
||||
}
|
||||
});
|
24
src/redux/uiSlice.jsx
Normal file
24
src/redux/uiSlice.jsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import {createSlice} from "@reduxjs/toolkit";
|
||||
|
||||
const uiSlice=createSlice({
|
||||
name: "ui",
|
||||
initialState: {
|
||||
tema: "light",
|
||||
temaSeleccionado: false,
|
||||
idioma: "es",
|
||||
},
|
||||
reducers: {
|
||||
cambiarTema: (state,action) => {
|
||||
state.tema=action.payload;
|
||||
state.temaSeleccionado=true;
|
||||
localStorage.setItem("tema",action.payload);
|
||||
},
|
||||
cambiarIdioma: (state,action) => {
|
||||
state.idioma=action.payload;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const {cambiarTema,cambiarIdioma}=uiSlice.actions;
|
||||
|
||||
export default uiSlice.reducer;
|
73
vite.config.js
Normal file
73
vite.config.js
Normal file
|
@ -0,0 +1,73 @@
|
|||
import {defineConfig} from "vite";
|
||||
import {resolve} from "path";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname,"src/index.js"),
|
||||
name: "react-saas",
|
||||
fileName: "react-saas"
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [
|
||||
"react",
|
||||
"react-dom",
|
||||
"react/jsx-runtime",
|
||||
"react-is",
|
||||
"@emotion/react",
|
||||
"@emotion/styled",
|
||||
"@mui/icons-material",
|
||||
"@mui/lab",
|
||||
"@mui/material",
|
||||
"@mui/material/styles",
|
||||
"@mui/material/colors",
|
||||
"@mui/x-date-pickers",
|
||||
"@mui/x-date-pickers/AdapterDateFns",
|
||||
"@reduxjs/toolkit",
|
||||
"axios",
|
||||
"date-fns/locale",
|
||||
"jwt-decode",
|
||||
"mui-rff",
|
||||
"react-polyglot",
|
||||
"react-redux",
|
||||
"react-router-dom",
|
||||
"object-assign",
|
||||
"prop-types",
|
||||
"react-final-form",
|
||||
"final-form"
|
||||
],
|
||||
output: {
|
||||
globals: {
|
||||
"react": "React",
|
||||
"react-dom": "ReactDOM",
|
||||
"react/jsx-runtime": "ReactJSX",
|
||||
"react-is": "ReactIS",
|
||||
"@emotion/react": "EmotionReact",
|
||||
"@emotion/styled": "EmotionStyled",
|
||||
"@mui/icons-material": "MuiIconsMaterial",
|
||||
"@mui/lab": "MuiLab",
|
||||
"@mui/material": "MuiMaterial",
|
||||
"@mui/material/styles": "MuiMaterialStyles",
|
||||
"@mui/material/colors": "MuiMaterialColors",
|
||||
"@mui/x-date-pickers": "MuiXDatePickers",
|
||||
"@mui/x-date-pickers/AdapterDateFns": "MuiXDatePickersAdapter",
|
||||
"@reduxjs/toolkit": "ReduxToolkit",
|
||||
"axios": "Axios",
|
||||
"date-fns/locale": "DateFNSLocale",
|
||||
"jwt-decode": "JWTDecode",
|
||||
"mui-rff": "MUIRFF",
|
||||
"react-polyglot": "ReactPolyglot",
|
||||
"react-redux": "ReactRedux",
|
||||
"react-router-dom": "ReactRouterDom",
|
||||
"object-assign": "ObjectAssign",
|
||||
"prop-types": "PropTypes",
|
||||
"react-final-form": "ReactFinalForm",
|
||||
"final-form": "FinalForm"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
Loading…
Add table
Reference in a new issue