Ruben Romero před 3 roky
rodič
revize
ed3563ec3c

+ 59 - 0
src/constants/httpStatusCodes.ts

@@ -0,0 +1,59 @@
+const httpStatusCodes = {
+  ACCEPTED: 202,
+  BAD_GATEWAY: 502,
+  BAD_REQUEST: 400,
+  CONFLICT: 409,
+  CONTINUE: 100,
+  CREATED: 201,
+  EXPECTATION_FAILED: 417,
+  FAILED_DEPENDENCY: 424,
+  FORBIDDEN: 403,
+  GATEWAY_TIMEOUT: 504,
+  GONE: 410,
+  HTTP_VERSION_NOT_SUPPORTED: 505,
+  IM_A_TEAPOT: 418,
+  INSUFFICIENT_SPACE_ON_RESOURCE: 419,
+  INSUFFICIENT_STORAGE: 507,
+  INTERNAL_SERVER_ERROR: 500,
+  LENGTH_REQUIRED: 411,
+  LOCKED: 423,
+  METHOD_FAILURE: 420,
+  METHOD_NOT_ALLOWED: 405,
+  MOVED_PERMANENTLY: 301,
+  MOVED_TEMPORARILY: 302,
+  MULTI_STATUS: 207,
+  MULTIPLE_CHOICES: 300,
+  NETWORK_AUTHENTICATION_REQUIRED: 511,
+  NO_CONTENT: 204,
+  NON_AUTHORITATIVE_INFORMATION: 203,
+  NOT_ACCEPTABLE: 406,
+  NOT_FOUND: 404,
+  NOT_IMPLEMENTED: 501,
+  NOT_MODIFIED: 304,
+  OK: 200,
+  PARTIAL_CONTENT: 206,
+  PAYMENT_REQUIRED: 402,
+  PERMANENT_REDIRECT: 308,
+  PRECONDITION_FAILED: 412,
+  PRECONDITION_REQUIRED: 428,
+  PROCESSING: 102,
+  PROXY_AUTHENTICATION_REQUIRED: 407,
+  REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
+  REQUEST_TIMEOUT: 408,
+  REQUEST_TOO_LONG: 413,
+  REQUEST_URI_TOO_LONG: 414,
+  REQUESTED_RANGE_NOT_SATISFIABLE: 416,
+  RESET_CONTENT: 205,
+  SEE_OTHER: 303,
+  SERVICE_UNAVAILABLE: 503,
+  SWITCHING_PROTOCOLS: 101,
+  TEMPORARY_REDIRECT: 307,
+  TOO_MANY_REQUESTS: 429,
+  UNAUTHORIZED: 401,
+  UNAVAILABLE_FOR_LEGAL_REASONS: 451,
+  UNPROCESSABLE_ENTITY: 422,
+  UNSUPPORTED_MEDIA_TYPE: 415,
+  USE_PROXY: 305,
+};
+
+export default Object.freeze(httpStatusCodes);

+ 2 - 0
src/constants/index.ts

@@ -0,0 +1,2 @@
+import httpCodes from "./httpStatusCodes";
+export { httpCodes };

+ 7 - 0
src/hooks/index.ts

@@ -0,0 +1,7 @@
+export * from "./useAlert";
+export * from "./useApp";
+export * from "./useHttp";
+export * from "./useModel";
+export * from "./useModels";
+export * from "./useNotifications";
+export * from "./useQuery";

+ 66 - 0
src/hooks/useAlert.ts

@@ -0,0 +1,66 @@
+import React from "react";
+
+const AlertContext = React.createContext();
+
+export function AlertProvider(props) {
+  const [open, setOpen] = React.useState(false);
+  const [position, setPosition] = React.useState({
+    vertical: "bottom",
+    horizontal: "right",
+  });
+  const [severity, setSeverity] = React.useState("info");
+  const [message, setMessage] = React.useState("");
+
+  React.useEffect(() => {
+    let mounted = true;
+    if (mounted) {
+      setTimeout(() => {
+        setOpen(false);
+      }, 5000);
+    }
+    return () => {
+      mounted = false;
+    };
+  }, [open]);
+
+  const showAlert = React.useCallback(
+    ({ message, severity = "info", position = null }) => {
+      setOpen(true);
+      setMessage(message);
+      setSeverity(severity);
+      if (position) setPosition(position);
+    },
+    []
+  );
+
+  const memData = React.useMemo(() => {
+    // const closeAlert = () => {
+    //   setOpen(false);
+    //   setTimeout(() => {
+    //     setPosition(defaultPlace);
+    //     setSeverity(defaultColor);
+    //     setIcon(defaultIcon);
+    //     setMessage(defaultMessage);
+    //   }, 2000);
+    // };
+    return {
+      open,
+      position,
+      severity,
+      message,
+      showAlert,
+      // closeAlert,
+    };
+  }, [open, position, severity, message, showAlert]);
+
+  return <AlertContext.Provider value={memData} {...props} />;
+}
+
+export function useAlert() {
+  const context = React.useContext(AlertContext);
+  if (!context) {
+    // eslint-disable-next-line no-throw-literal
+    throw "error: alert context not defined.";
+  }
+  return context;
+}

+ 36 - 0
src/hooks/useApp.ts

@@ -0,0 +1,36 @@
+import React from "react";
+
+const localStorageKey = "usr_jwt";
+const AppContext = React.createContext();
+
+export function AppProvider(props) {
+  const [token, setToken] = React.useState(null);
+
+  React.useEffect(() => {
+    const jwt = localStorage.getItem(localStorageKey);
+    setToken(jwt);
+  }, []);
+
+  React.useEffect(() => {
+    if (token && token !== "") {
+      localStorage.setItem(localStorageKey, token);
+    } else if (localStorage.getItem(localStorageKey)) {
+      localStorage.removeItem(localStorageKey);
+    }
+  }, [token]);
+
+  const memData = React.useMemo(() => {
+    return { token, setToken };
+  }, [token, setToken]);
+
+  return <AppContext.Provider value={memData} {...props} />;
+}
+
+export function useApp() {
+  const context = React.useContext(AppContext);
+  if (!context) {
+    // eslint-disable-next-line no-throw-literal
+    throw "error: app context not defined.";
+  }
+  return context;
+}

+ 143 - 0
src/hooks/useHttp.ts

@@ -0,0 +1,143 @@
+import React from "react";
+import { useAlert } from "./useAlert";
+import { useHistory } from "react-router";
+import { httpCodes } from "../constants";
+// import { auth } from "../services";
+import { useApp } from "./useApp";
+
+const { REACT_APP_API_URL: baseUrl } = process.env;
+
+const defaultHeaders = {
+  "Content-Type": "application/json",
+  Accept: "application/json",
+};
+
+const makeHeaders = (token) =>
+  token
+    ? {
+        ...defaultHeaders,
+        Authorization: `Bearer ${token}`,
+      }
+    : defaultHeaders;
+
+const paramsToQuery = (params) =>
+  Object.keys(params)
+    .map(
+      (key) => encodeURIComponent(key) + "=" + encodeURIComponent(params[key])
+    )
+    .join("&");
+
+const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
+
+export function useHttp({
+  req = "GET",
+  url = null,
+  params = null,
+  body = null,
+  alert = false,
+}) {
+  const { showAlert } = useAlert();
+  const { token } = useApp();
+  const [response, setResponse] = React.useState(null);
+  const [error, setError] = React.useState(null);
+  const [loading, setLoading] = React.useState(true);
+  const history = useHistory();
+
+  const refresh = React.useCallback(
+    async (showAlert, inlineParams = {}) => {
+      try {
+        if (!url || !params) {
+          setResponse(null);
+          setError(null);
+          setLoading(true);
+          return;
+        }
+        if (inlineParams.loading === false) {
+          setLoading(() => false);
+        } else {
+          setLoading(() => true);
+        }
+        // let token = null;
+        // if (auth.currentUser) {
+        //   token = await auth.currentUser.getIdToken();
+        // }
+        let fetchReq = {
+          method: req,
+          headers: makeHeaders(token),
+        };
+        if (body) {
+          fetchReq = { ...fetchReq, body: JSON.stringify(body) };
+        }
+        const paramsFinal = { ...params, ...inlineParams };
+        const str = `${baseUrl}${url}${
+          params && Object.keys(paramsFinal).length > 0
+            ? `?${paramsToQuery(paramsFinal)}`
+            : ""
+        }`;
+        const httpRes = await fetch(str, fetchReq);
+        const resBody = await httpRes.json();
+        switch (httpRes.status) {
+          case httpCodes.OK:
+            setResponse(resBody);
+            setError(null);
+            alert &&
+              showAlert({
+                severity: "success",
+                message: resBody.mensaje
+                  ? capitalize(resBody.mensaje)
+                  : "Solicitud completada correctamente!",
+              });
+            break;
+          case httpCodes.BAD_REQUEST:
+            window["scrollTo"]({ top: 0, behavior: "smooth" });
+            setError(resBody.errores);
+            alert &&
+              showAlert({
+                severity: "warning",
+                message: resBody.mensaje
+                  ? capitalize(resBody.mensaje)
+                  : "Datos erróneos o inválidos.",
+              });
+            break;
+          case httpCodes.UNAUTHORIZED:
+            history.push("/no-autorizado");
+            break;
+          case httpCodes.SERVER_ERROR:
+          default:
+            alert &&
+              showAlert({
+                severity: "error",
+                message: resBody.mensaje
+                  ? capitalize(resBody.mensaje)
+                  : "Ocurrió un error en el servidor.",
+              });
+        }
+      } catch (error) {
+        alert &&
+          showAlert({
+            severity: "error",
+            message: "No se pudo establecer conexión con el servidor.",
+          });
+        console.error(error);
+      } finally {
+        setLoading(false);
+      }
+    },
+    [body, params, req, url, alert, history, token]
+  );
+
+  React.useEffect(() => {
+    let mounted = true;
+    if (mounted) {
+      refresh(showAlert);
+    }
+    return () => {
+      mounted = false;
+    };
+  }, [refresh, showAlert]);
+
+  return React.useMemo(
+    () => [response, loading, error, refresh],
+    [response, loading, error, refresh]
+  );
+}

+ 91 - 0
src/hooks/useModel.ts

@@ -0,0 +1,91 @@
+import React from "react";
+import { useHistory } from "react-router-dom";
+import { emptyRequest, getRequest, postRequest } from "../constants/requests";
+import { capitalizeFirst } from "../utilities";
+import { useHttp } from "./useHttp";
+
+const empty = emptyRequest();
+
+export function useModel({
+  name,
+  id,
+  fields = null,
+  expand = null,
+  extraParams = null,
+  redirectOnPost = false,
+  path = "guardar",
+}) {
+  const [modelRequest, setProfileRequest] = React.useState(empty);
+  const [model, modelLoading, modelError, refreshModel] = useHttp(modelRequest);
+
+  const [updateRequest, setUpdateRequest] = React.useState(empty);
+  const [postResult, postResultLoading, postResultError] =
+    useHttp(updateRequest);
+  const history = useHistory();
+
+  const updateModel = React.useCallback(
+    (newModel, alert = true) => {
+      if (!postResultLoading) {
+        if (newModel.id) {
+          newModel = { [`id${capitalizeFirst(name)}`]: newModel.id };
+          delete newModel.id;
+        }
+        const updateReq = postRequest(`${name}/${path}`, newModel);
+        updateReq.alert = alert;
+        setUpdateRequest(updateReq);
+      }
+    },
+    [name, postResultLoading, path]
+  );
+
+  React.useEffect(() => {
+    let mounted = true;
+    if (mounted && postResult && redirectOnPost && !postResultError) {
+      const { pathname } = history.location;
+      const redirectTo = pathname.split("/").filter((e) => e !== "");
+      history.push("/" + redirectTo[0]);
+    }
+    return () => {
+      mounted = false;
+    };
+  }, [postResult, redirectOnPost, postResultError, history]);
+
+  React.useEffect(() => {
+    if (!name || !id) return;
+    let params = { [`id${capitalizeFirst(name)}`]: id };
+    if (fields) params = { ...params, fields };
+    if (expand) params = { ...params, expand };
+    if (extraParams) params = { ...params, ...extraParams };
+    const modelReq = getRequest(name, params);
+    setProfileRequest(modelReq);
+  }, [name, id, fields, expand, extraParams]);
+
+  return React.useMemo(() => {
+    let modelTmp = null;
+    if (model && model.resultado && model.resultado.length > 0) {
+      modelTmp = model.resultado[0];
+      if (model.detalle) modelTmp.detalleExtra = model.detalle;
+    }
+    let finalError = {};
+    if (modelError) finalError = { ...modelError };
+    if (postResultError) finalError = { ...finalError, ...postResultError };
+    return {
+      model: modelTmp,
+      modelLoading,
+      modelError: finalError,
+      refreshModel,
+      updateModel,
+      updateModelResult: postResult,
+      updateModelLoading: postResultLoading,
+    };
+  }, [
+    model,
+    modelLoading,
+    modelError,
+    refreshModel,
+    postResult,
+    postResultLoading,
+    postResultError,
+    updateModel,
+  ]);
+}

+ 87 - 0
src/hooks/useModels.ts

@@ -0,0 +1,87 @@
+import React from "react";
+import { emptyRequest, getRequest, deleteRequest } from "../constants/requests";
+import { useHttp } from "./useHttp";
+
+const empty = emptyRequest();
+
+export function useModels({
+  name,
+  fields = null,
+  expand = null,
+  ordenar = null,
+  limite = null,
+  pagina = null,
+  extraParams = null,
+}) {
+  const [modelRequest, setModelsRequest] = React.useState(empty);
+  const [modelsPage, setModelsPage] = React.useState(null);
+  const [models, modelsLoading, modelsError, refreshModels] =
+    useHttp(modelRequest);
+
+  const [delRequest, setDelRequest] = React.useState(empty);
+  const [deleteResult, deleteResultLoading] = useHttp(delRequest);
+
+  const deleteModel = React.useCallback(
+    async (id) => {
+      if (!deleteResultLoading) {
+        const deleteReq = deleteRequest(name, id);
+        deleteReq.alert = true;
+        setDelRequest(deleteReq);
+      }
+    },
+    [name, deleteResultLoading]
+  );
+
+  React.useEffect(() => {
+    if (!name) {
+      setModelsRequest(empty);
+      return;
+    }
+    let params = {};
+    if (fields) params = { ...params, fields };
+    if (expand) params = { ...params, expand };
+    if (ordenar) params = { ...params, ordenar };
+    if (limite) params = { ...params, limite };
+    if (pagina) params = { ...params, pagina };
+    if (extraParams) params = { ...params, ...extraParams };
+    const modelReq = getRequest(name, params);
+    setModelsRequest(modelReq);
+  }, [name, fields, expand, ordenar, limite, pagina, extraParams]);
+
+  React.useEffect(() => {
+    if (!modelsLoading && !modelsError && models) {
+      const { paginacion } = models;
+      setModelsPage(paginacion);
+    }
+  }, [models, modelsLoading, modelsError]);
+
+  React.useEffect(() => {
+    if (!deleteResultLoading && deleteResult) {
+      refreshModels();
+    }
+  }, [deleteResult, deleteResultLoading, refreshModels]);
+
+  return React.useMemo(() => {
+    let resultado = [];
+    if (models && models.resultado && models.resultado.length > 0) {
+      resultado = [...models.resultado];
+    }
+    let modelsLoadingFinal = modelsLoading || deleteResultLoading;
+    return [
+      resultado,
+      modelsLoadingFinal,
+      modelsError,
+      modelsPage,
+      refreshModels,
+      deleteModel,
+    ];
+  }, [
+    models,
+    modelsLoading,
+    modelsError,
+    modelsPage,
+    refreshModels,
+    deleteResultLoading,
+    deleteModel,
+  ]);
+}

+ 28 - 0
src/hooks/useNotifications.ts

@@ -0,0 +1,28 @@
+import React from "react";
+
+const NotificationsContext = React.createContext();
+
+// const defaultNotifications = () => [1, 2];
+
+export function NotificationsProvider(props) {
+  const [notifications, setNotifications] = React.useState([]);
+
+  // COMPONENTE SE MONTÓ
+  React.useEffect(() => {}, []);
+
+  const memData = React.useMemo(() => {
+    // AQUI SE PONE TODO LO Q EL HOOK EXPORTA
+    return { notifications, setNotifications };
+  }, [notifications]);
+
+  return <NotificationsContext.Provider value={memData} {...props} />;
+}
+
+export function useNotifications() {
+  const context = React.useContext(NotificationsContext);
+  if (!context) {
+    // eslint-disable-next-line no-throw-literal
+    throw "error: notifications context not defined.";
+  }
+  return context;
+}

+ 7 - 0
src/hooks/useQuery.ts

@@ -0,0 +1,7 @@
+import React from "react";
+import { useLocation } from "react-router-dom";
+
+export function useQuery() {
+  const search = useLocation().search;
+  return React.useMemo(() => new URLSearchParams(search), [search]);
+}

+ 0 - 5
src/index.ts

@@ -1,5 +0,0 @@
-function add(a: number, b: number): number {
-  return a + b;
-}
-
-export { add };

+ 47 - 0
src/services/httpService.ts

@@ -0,0 +1,47 @@
+const { REACT_APP_API_URL: baseUrl } = process.env;
+const localStorageKey = "usr_jwt";
+
+const getCurrentToken = () => {
+  return new Promise((resolve, reject) => {
+    const jwt = localStorage.getItem(localStorageKey);
+    if (!jwt) reject("No hay sesión.");
+    resolve(jwt);
+  });
+};
+
+const getHeaders = (token) => ({
+  "Content-Type": "application/json",
+  Accept: "application/json",
+  Authorization: `Bearer ${token}`,
+});
+
+const getHeadersWithoutToken = () => ({
+  "Content-Type": "application/json",
+  Accept: "application/json",
+});
+
+const HttpService = {
+  get: async (url, auth = true) => {
+    let token = null;
+    if (auth) token = await getCurrentToken();
+    const response = await fetch(baseUrl + url, {
+      method: "GET",
+      headers: auth ? getHeaders(token) : getHeadersWithoutToken(),
+    });
+
+    return response.json();
+  },
+  post: async (url, data, auth = true) => {
+    let token = null;
+    if (auth) token = await getCurrentToken();
+    const response = await fetch(baseUrl + url, {
+      method: "POST",
+      headers: auth ? getHeaders(token) : getHeadersWithoutToken(),
+      body: JSON.stringify(data),
+    });
+
+    return response.json();
+  },
+};
+
+export default HttpService;

+ 1 - 0
src/services/index.ts

@@ -0,0 +1 @@
+export * from "./httpService";

+ 0 - 0
src/types/index.ts


+ 1 - 0
src/utils/index.ts

@@ -0,0 +1 @@
+export * from "./requests";

+ 33 - 0
src/utils/requests.ts

@@ -0,0 +1,33 @@
+import { capitalizeFirst } from "../utilities";
+
+const emptyRequest = () => ({
+  req: null,
+  url: null,
+  params: null,
+  body: null,
+});
+
+const getRequest = (url, params = {}) => ({
+  req: "GET",
+  url,
+  params,
+  body: null,
+});
+
+const postRequest = (url, body, params = {}) => ({
+  req: "POST",
+  url,
+  params,
+  body,
+});
+
+const deleteRequest = (url, id, params = {}) => ({
+  req: "DELETE",
+  url: `${url}/eliminar`,
+  params: {
+    ...params,
+    [`id${capitalizeFirst(url)}`]: id,
+  },
+});
+
+export { emptyRequest, getRequest, postRequest, deleteRequest };