1
0

Refactor frontend components and contexts for improved structure and functionality

- Removed unnecessary comments and documentation from CardContainer, Header, HistoryCharts, MenuButton, PollutionMap, SideMenu, SummaryCards, and ThemeButton components.
- Updated import paths to use aliasing for cleaner code.
- Replaced the old context implementations (ConfigContext, DataContext, ThemeContext) with new hooks and context structure for better state management.
- Introduced a new axios instance for API calls to streamline requests.
- Added new utility functions for date and error parsing.
- Updated the main entry point and pages to reflect new context and component structures.
- Created new configuration files for development and production environments.
- Enhanced data fetching logic with improved error handling and loading states.
This commit is contained in:
Jose
2025-05-06 03:52:24 +02:00
parent 5d1b2feeab
commit fa1b457004
33 changed files with 563 additions and 476 deletions

9
frontend/jsconfig.json Normal file
View File

@@ -0,0 +1,9 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"]
}

View File

@@ -13,6 +13,7 @@
"@fortawesome/free-regular-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"axios": "^1.9.0",
"bootstrap": "^5.3.3",
"chart.js": "^4.4.8",
"leaflet": "^1.9.4",
@@ -1718,6 +1719,12 @@
"node": ">= 0.4"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -1734,6 +1741,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/axios": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1827,7 +1845,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -1934,6 +1951,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -2094,6 +2123,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
@@ -2111,7 +2149,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
@@ -2199,7 +2236,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -2209,7 +2245,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -2247,7 +2282,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
@@ -2260,7 +2294,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -2656,6 +2689,26 @@
"dev": true,
"license": "ISC"
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@@ -2672,6 +2725,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/form-data": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -2691,7 +2759,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -2742,7 +2809,6 @@
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz",
"integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
@@ -2767,7 +2833,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
@@ -2842,7 +2907,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -2907,7 +2971,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -2920,7 +2983,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
@@ -2936,7 +2998,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@@ -3558,12 +3619,32 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -3911,6 +3992,12 @@
"react-is": "^16.13.1"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",

View File

@@ -15,6 +15,7 @@
"@fortawesome/free-regular-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"axios": "^1.9.0",
"bootstrap": "^5.3.3",
"chart.js": "^4.4.8",
"leaflet": "^1.9.4",

View File

@@ -0,0 +1,98 @@
{
"userConfig": {
"city": [
37.38283,
-5.97317
]
},
"appConfig": {
"endpoints": {
"DATA_URL": "https://contaminus.miarma.net/api/raw/v1",
"LOGIC_URL": "https://contaminus.miarma.net/api/v1",
"GET_GROUPS": "/groups",
"GET_GROUP_BY_ID": "/groups/{0}",
"GET_GROUP_DEVICES": "/groups/{0}/devices",
"POST_GROUPS": "/groups",
"PUT_GROUP_BY_ID": "/groups/{0}",
"GET_DEVICES": "/devices",
"GET_DEVICE_BY_ID": "/devices/{0}",
"GET_DEVICE_SENSORS": "/devices/{0}/sensors",
"GET_DEVICE_LATEST_VALUES": "/devices/{0}/latest",
"GET_DEVICE_POLLUTION_MAP": "/devices/{0}/pollution-map",
"GET_DEVICE_HISTORY": "/devices/{0}/history",
"POST_DEVICES": "/devices",
"PUT_DEVICE_BY_ID": "/devices/{0}",
"GET_SENSORS": "/sensors",
"GET_SENSOR_BY_ID": "/sensors/{0}",
"GET_SENSOR_VALUES": "/sensors/{0}/values",
"POST_SENSORS": "/sensors",
"PUT_SENSOR_BY_ID": "/sensors/{0}",
"GET_ACTUATORS": "/actuators",
"GET_ACTUATOR_BY_ID": "/actuators/{0}",
"POST_ACTUATORS": "/actuators",
"PUT_ACTUATOR_BY_ID": "/actuators/{0}",
"GET_GPS_VALUES": "/gps-values",
"GET_GPS_VALUE_BY_ID": "/gps-values/{0}",
"POST_GPS_VALUES": "/gps-values",
"GET_AIR_VALUES": "/air-values",
"GET_AIR_VALUE_BY_ID": "/air-values/{0}",
"POST_AIR_VALUES": "/air-values"
},
"historyChartConfig": {
"chartOptionsDark": {
"responsive": true,
"maintainAspectRatio": false,
"scales": {
"x": {
"grid": {
"color": "rgba(255, 255, 255, 0.1)"
},
"ticks": {
"color": "#E0E0E0"
}
},
"y": {
"grid": {
"color": "rgba(255, 255, 255, 0.1)"
},
"ticks": {
"color": "#E0E0E0"
}
}
},
"plugins": {
"legend": {
"display": false
}
}
},
"chartOptionsLight": {
"responsive": true,
"maintainAspectRatio": false,
"scales": {
"x": {
"grid": {
"color": "rgba(0, 0, 0, 0.1)"
},
"ticks": {
"color": "#333"
}
},
"y": {
"grid": {
"color": "rgba(0, 0, 0, 0.1)"
},
"ticks": {
"color": "#333"
}
}
},
"plugins": {
"legend": {
"display": false
}
}
}
}
}
}

View File

@@ -0,0 +1,14 @@
import axios from "axios";
const createAxiosInstance = (baseURL, token) => {
const instance = axios.create({
baseURL,
headers: {
...(token && { Authorization: `Bearer ${token}` }),
},
});
return instance;
};
export default createAxiosInstance;

View File

@@ -1,45 +1,18 @@
import '../css/App.css'
import '@/css/App.css'
import 'leaflet/dist/leaflet.css'
import 'bootstrap/dist/css/bootstrap.min.css'
import 'bootstrap/dist/js/bootstrap.bundle.min.js'
import Home from '../pages/Home.jsx'
import Dashboard from '../pages/Dashboard.jsx'
import Home from '@/pages/Home.jsx'
import Dashboard from '@/pages/Dashboard.jsx'
import MenuButton from './MenuButton.jsx'
import SideMenu from './SideMenu.jsx'
import ThemeButton from '../components/ThemeButton.jsx'
import Header from '../components/Header.jsx'
import ThemeButton from '@/components/ThemeButton.jsx'
import Header from '@/components/Header.jsx'
import { Routes, Route } from 'react-router-dom'
import { useState } from 'react'
/**
* App.jsx
*
* Este archivo define el componente App, que es el componente principal de la aplicación.
*
* Importaciones:
* - '../css/App.css': Archivo CSS que contiene los estilos globales de la aplicación.
* - 'leaflet/dist/leaflet.css': Archivo CSS que contiene los estilos para los mapas de Leaflet.
* - 'bootstrap/dist/css/bootstrap.min.css': Archivo CSS que contiene los estilos de Bootstrap.
* - 'bootstrap/dist/js/bootstrap.bundle.min.js': Archivo JS que contiene los scripts de Bootstrap.
* - Header: Componente que representa el encabezado de la página.
* - Home: Componente que representa la página principal de la aplicación.
* - MenuButton: Componente que representa el botón del menú lateral.
* - SideMenu: Componente que representa el menú lateral.
* - ThemeButton: Componente que representa el botón de cambio de tema.
*
* Funcionalidad:
* - App: Componente principal que renderiza la página Home.
* - Planea añadir un React Router en el futuro.
* - El componente Header muestra el título y subtítulo de la página.
* - El componente MenuButton muestra un botón para abrir el menú lateral.
* - El componente SideMenu muestra un menú lateral con opciones de navegación.
* - El componente ThemeButton muestra un botón para cambiar el tema de la aplicación.
* - El componente Home contiene el contenido principal de la aplicación.
*
*/
const App = () => {
const [isSideMenuOpen, setIsSideMenuOpen] = useState(false);

View File

@@ -1,32 +1,7 @@
import PropTypes from "prop-types";
import { useState, useEffect, useRef } from "react";
import "../css/Card.css";
import { useTheme } from "../contexts/ThemeContext";
/**
* Card.jsx
*
* Este archivo define el componente Card, que representa una tarjeta individual con un título, estado y contenido.
*
* Importaciones:
* - PropTypes: Librería para la validación de tipos de propiedades en componentes de React.
* - useState, useEffect, useRef: Hooks de React para manejar estados, efectos secundarios y referencias.
* - "../css/Card.css": Archivo CSS que contiene los estilos para las tarjetas.
* - useTheme: Hook personalizado para acceder al contexto del tema.
*
* Funcionalidad:
* - Card: Componente que renderiza una tarjeta con un título, estado y contenido.
* - Utiliza el hook `useTheme` para aplicar la clase correspondiente al tema actual.
* - Ajusta el título de la tarjeta según el tamaño de la tarjeta.
*
* PropTypes:
* - Card espera una propiedad `title` que es un string requerido.
* - Card espera una propiedad `status` que es un string requerido.
* - Card espera una propiedad `children` que es un nodo de React requerido.
* - Card espera una propiedad `styleMode` que es opcional y puede ser "override" o una cadena vacía.
* - Card espera una propiedad `className` que es un string opcional.
*
*/
import "@/css/Card.css";
import { useTheme } from "@/hooks/useTheme";
const Card = ({ title, status, children, styleMode, className, titleIcon }) => {
const cardRef = useRef(null);

View File

@@ -1,25 +1,6 @@
import Card from "./Card.jsx";
import PropTypes from "prop-types";
/**
* CardContainer.jsx
*
* Este archivo define el componente CardContainer, que actúa como contenedor para múltiples componentes Card.
*
* Importaciones:
* - Card: Componente que representa una tarjeta individual.
* - PropTypes: Librería para la validación de tipos de propiedades en componentes de React.
*
* Funcionalidad:
* - CardContainer: Componente que renderiza un contenedor (`div`) con una fila de tarjetas (`Card`).
* - Utiliza `props.cards` para mapear y renderizar cada tarjeta con su contenido.
*
* PropTypes:
* - CardContainer espera una propiedad `cards` que es un array de objetos con las propiedades `title`, `content` y `status`.
* - CardContainer espera una propiedad `className` que es un string opcional.
*
*/
const CardContainer = ({ cards, className }) => {
return (
<div className={`row justify-content-center g-0 ${className}`}>

View File

@@ -1,26 +1,6 @@
import PropTypes from 'prop-types';
import '../css/Header.css';
import { useTheme } from "../contexts/ThemeContext";
/**
* Header.jsx
*
* Este archivo define el componente Header, que muestra el encabezado de la página con un título y un subtítulo.
*
* Importaciones:
* - PropTypes: Librería para la validación de tipos de propiedades en componentes de React.
* - "../css/Header.css": Archivo CSS que contiene los estilos para el encabezado.
* - useTheme: Hook personalizado para acceder al contexto del tema.
*
* Funcionalidad:
* - Header: Componente que renderiza un encabezado con un título y un subtítulo.
* - Utiliza el hook `useTheme` para aplicar la clase correspondiente al tema actual.
*
* PropTypes:
* - Header espera una propiedad `title` que es un string requerido.
* - Header espera una propiedad `subtitle` que es un string opcional.
*
*/
import '@/css/Header.css';
import { useTheme } from "@/hooks/useTheme";
const Header = (props) => {
const { theme } = useTheme();

View File

@@ -1,39 +1,13 @@
import { Line } from "react-chartjs-2";
import { Chart as ChartJS, LineElement, PointElement, LinearScale, CategoryScale, Filler } from "chart.js";
import CardContainer from "./CardContainer";
import "../css/HistoryCharts.css";
import "@/css/HistoryCharts.css";
import PropTypes from "prop-types";
import { useTheme } from "../contexts/ThemeContext.jsx";
import { DataProvider, useData } from "../contexts/DataContext.jsx";
import { useConfig } from "../contexts/ConfigContext.jsx";
/**
* HistoryCharts.jsx
*
* Este archivo define el componente HistoryCharts, que muestra gráficos históricos de datos obtenidos de sensores.
*
* Importaciones:
* - Line: Componente de react-chartjs-2 para renderizar gráficos de líneas.
* - ChartJS, LineElement, PointElement, LinearScale, CategoryScale, Filler: Módulos de chart.js para configurar y registrar los elementos del gráfico.
* - CardContainer: Componente que actúa como contenedor para las tarjetas.
* - "../css/HistoryCharts.css": Archivo CSS que contiene los estilos para los gráficos históricos.
* - PropTypes: Librería para la validación de tipos de propiedades en componentes de React.
* - useTheme: Hook personalizado para acceder al contexto del tema.
* - DataProvider, useData: Funciones del contexto de datos para obtener y manejar datos.
* - useConfig: Hook personalizado para acceder al contexto de configuración.
*
* Funcionalidad:
* - HistoryCharts: Componente que configura la solicitud de datos y utiliza el DataProvider para obtener datos de sensores.
* - Muestra mensajes de carga y error según el estado de la configuración.
* - HistoryChartsContent: Componente que procesa los datos obtenidos y renderiza los gráficos históricos.
* - Utiliza el hook `useData` para acceder a los datos de sensores.
* - Renderiza gráficos de líneas con diferentes colores según el tipo de dato (temperatura, humedad, contaminación).
*
* PropTypes:
* - HistoryChartsContent espera propiedades `options` (objeto), `timeLabels` (array) y `data` (array).
*
*/
import { useTheme } from "@/hooks/useTheme";
import { DataProvider } from "@/context/DataContext.jsx";
import { useDataContext } from "@/hooks/useDataContext";
import { useConfig } from "@/hooks/useConfig";
ChartJS.register(LineElement, PointElement, LinearScale, CategoryScale, Filler);
@@ -44,8 +18,8 @@ const HistoryCharts = () => {
if (configError) return <p>Error al cargar configuración: {configError}</p>;
if (!config) return <p>Configuración no disponible.</p>;
const BASE = config.appConfig.endpoints.LOGIC_URL;
const ENDPOINT = config.appConfig.endpoints.sensors;
const BASE = config.appConfig.endpoints.DATA_URL;
const ENDPOINT = config.appConfig.endpoints.GET_SENSORS;
const reqConfig = {
baseUrl: `${BASE}${ENDPOINT}`,
@@ -61,7 +35,7 @@ const HistoryCharts = () => {
const HistoryChartsContent = () => {
const { config } = useConfig();
const { data, loading } = useData();
const { data, loading, error } = useDataContext();
const { theme } = useTheme();
const optionsDark = config?.appConfig?.historyChartConfig?.chartOptionsDark ?? {};
@@ -74,6 +48,7 @@ const HistoryChartsContent = () => {
]
if (loading) return <p>Cargando datos...</p>;
if (error) return <p>Datos no disponibles.</p>;
const temperatureData = [];
const humidityData = [];

View File

@@ -1,28 +1,10 @@
import "../css/MenuButton.css";
import "@/css/MenuButton.css";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBars } from '@fortawesome/free-solid-svg-icons';
import PropTypes from "prop-types";
/** ⚠️ EN PRUEBAS ⚠️
* MenuButton.jsx
*
* Este archivo define el componente MenuButton, que muestra un botón de menú con un icono de barras.
*
* Importaciones:
* - "../css/MenuButton.css": Archivo CSS que contiene los estilos para el botón de menú.
* - FontAwesomeIcon, faBars: Componentes e iconos de FontAwesome para mostrar el icono de barras.
* - PropTypes: Librería para la validación de tipos de propiedades en componentes de React.
*
* Funcionalidad:
* - MenuButton: Componente que renderiza un botón con un icono de barras.
* - Utiliza la propiedad `onClick` para manejar el evento de clic del botón.
*
* PropTypes:
* - MenuButton espera una propiedad `onClick` que es una función requerida.
* ⚠️ EN PRUEBAS ⚠️ **/
export default function MenuButton({ onClick }) {
const MenuButton = ({ onClick }) => {
return (
<button className="menuBtn" onClick={onClick}>
<FontAwesomeIcon icon={faBars} />
@@ -32,4 +14,6 @@ export default function MenuButton({ onClick }) {
MenuButton.propTypes = {
onClick: PropTypes.func.isRequired,
};
};
export default MenuButton;

View File

@@ -1,29 +1,10 @@
import { MapContainer, TileLayer, Circle, Popup } from 'react-leaflet';
import PropTypes from 'prop-types';
import { useConfig } from '../contexts/ConfigContext.jsx';
import { useConfig } from '@/hooks/useConfig.js';
import { DataProvider } from '../contexts/DataContext.jsx';
import { useData } from '../contexts/DataContext.jsx';
/**
* PollutionMap.jsx
*
* Este archivo define el componente PollutionMap, que muestra un mapa con los niveles de contaminación en diferentes ubicaciones.
*
* Importaciones:
* - MapContainer, TileLayer, Circle, Popup: Componentes de react-leaflet para renderizar el mapa y los círculos de contaminación.
* - useConfig: Hook personalizado para acceder al contexto de configuración.
* - DataProvider, useData: Funciones del contexto de datos para obtener y manejar datos.
*
* Funcionalidad:
* - PollutionMap: Componente que configura la solicitud de datos y utiliza el DataProvider para obtener datos de sensores.
* - Muestra mensajes de carga y error según el estado de la configuración.
* - PollutionMapContent: Componente que procesa los datos obtenidos y renderiza los círculos de contaminación en el mapa.
* - Utiliza el hook `useData` para acceder a los datos de sensores.
* - Renderiza círculos de diferentes colores y tamaños según el nivel de contaminación.
*
*/
import { DataProvider } from '@/context/DataContext.jsx';
import { useDataContext } from '@/hooks/useDataContext';
const PollutionCircles = ({ data }) => {
return data.map(({ lat, lng, level }, index) => {
@@ -83,7 +64,7 @@ const PollutionMap = ({ deviceId }) => {
const PollutionMapContent = () => {
const { config, configLoading, configError } = useConfig();
const { data, dataLoading, dataError } = useData();
const { data, dataLoading, dataError } = useDataContext();
if (configLoading) return <p>Cargando configuración...</p>;
if (configError) return <p>Error al cargar configuración: {configError}</p>;

View File

@@ -1,36 +1,16 @@
import "../css/SideMenu.css";
import "@/css/SideMenu.css";
import PropTypes from 'prop-types';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faTimes, faHome } from '@fortawesome/free-solid-svg-icons';
import { DataProvider } from '../contexts/DataContext';
import { useData } from '../contexts/DataContext';
import { DataProvider } from '@/context/DataContext';
import { useDataContext } from "@/hooks/useDataContext";
import { useConfig } from '../contexts/ConfigContext';
import { useTheme } from "../contexts/ThemeContext";
import { useConfig } from '@/hooks/useConfig.js';
import { useTheme } from "@/hooks/useTheme";
import Card from './Card';
/** ⚠️ EN PRUEBAS ⚠️
* SideMenu.jsx
*
* Este archivo define el componente SideMenu, que muestra un menú lateral con enlaces de navegación.
*
* Importaciones:
* - "../css/SideMenu.css": Archivo CSS que contiene los estilos para el menú lateral.
* - PropTypes: Librería para la validación de tipos de propiedades en componentes de React.
* - FontAwesomeIcon, faTimes: Componentes e iconos de FontAwesome para mostrar el icono de cerrar.
*
* Funcionalidad:
* - SideMenu: Componente que renderiza un menú lateral con enlaces de navegación.
* - Utiliza la propiedad `isOpen` para determinar si el menú debe estar visible.
* - Utiliza la propiedad `onClose` para manejar el evento de cierre del menú.
*
* PropTypes:
* - SideMenu espera una propiedad `isOpen` que es un booleano requerido.
* - SideMenu espera una propiedad `onClose` que es una función requerida.
* ⚠️ EN PRUEBAS ⚠️ **/
const SideMenu = ({ isOpen, onClose }) => {
const { config, configLoading, configError } = useConfig();
@@ -54,7 +34,7 @@ const SideMenu = ({ isOpen, onClose }) => {
};
const SideMenuContent = ({ isOpen, onClose }) => {
const { data, dataLoading, dataError } = useData();
const { data, dataLoading, dataError } = useDataContext();
const { theme } = useTheme();
if (dataLoading) return <p>Cargando datos...</p>;

View File

@@ -4,34 +4,11 @@ import CardContainer from './CardContainer';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCloud, faClock, faTemperature0, faWater } from '@fortawesome/free-solid-svg-icons';
import { DataProvider } from '../contexts/DataContext';
import { useData } from '../contexts/DataContext';
import { DataProvider } from '@/context/DataContext';
import { useDataContext } from '@/hooks/useDataContext';
import { useConfig } from '../contexts/ConfigContext';
import { timestampToTime, formatTime } from '../util/date.js';
/**
* SummaryCards.jsx
*
* Este archivo define el componente SummaryCards, que muestra tarjetas resumen con información relevante obtenida de sensores.
*
* Importaciones:
* - PropTypes: Librería para la validación de tipos de propiedades en componentes de React.
* - CardContainer: Componente que actúa como contenedor para las tarjetas.
* - DataProvider, useData: Funciones del contexto de datos para obtener y manejar datos.
* - useConfig: Hook personalizado para acceder al contexto de configuración.
*
* Funcionalidad:
* - SummaryCards: Componente que configura la solicitud de datos y utiliza el DataProvider para obtener datos de sensores.
* - Muestra mensajes de carga y error según el estado de la configuración.
* - SummaryCardsContent: Componente que procesa los datos obtenidos y actualiza el contenido de las tarjetas.
* - Utiliza el hook `useData` para acceder a los datos de sensores.
* - Actualiza el contenido y estado de las tarjetas según los datos obtenidos.
*
* PropTypes:
* - SummaryCards espera una propiedad `data` que es un array.
*
*/
import { useConfig } from '@/hooks/useConfig.js';
import { DateParser } from '@/util/dateParser';
const SummaryCards = ({ deviceId }) => {
const { config, configLoading, configError } = useConfig();
@@ -51,13 +28,13 @@ const SummaryCards = ({ deviceId }) => {
return (
<DataProvider config={reqConfig}>
<SummaryCardsContent deviceId={deviceId} />
<SummaryCardsContent />
</DataProvider>
);
}
const SummaryCardsContent = () => {
const { data, dataLoading, dataError } = useData();
const { data, dataLoading, dataError } = useDataContext();
if (dataLoading) return <p>Cargando datos...</p>;
if (dataError) return <p>Error al cargar datos: {dataError}</p>;
@@ -74,7 +51,7 @@ const SummaryCardsContent = () => {
let coData = data[1];
let tempData = data[2];
let lastTime = timestampToTime(coData.airValuesTimestamp);
let lastTime = DateParser.timestampToString(coData.airValuesTimestamp);
let lastDate = new Date(coData.airValuesTimestamp);
CardsData[0].content = tempData.temperature + "°C";
@@ -83,7 +60,7 @@ const SummaryCardsContent = () => {
CardsData[1].status = "Humedad actual";
CardsData[2].content = coData.carbonMonoxide + " ppm";
CardsData[2].status = "Nivel de CO actual";
CardsData[3].content = formatTime(lastTime);
CardsData[3].content = lastTime;
CardsData[3].status = "Día " + lastDate.toLocaleDateString();
}

View File

@@ -1,23 +1,7 @@
import { useTheme } from "../contexts/ThemeContext.jsx";
import "../css/ThemeButton.css";
import { useTheme } from "@/hooks/useTheme";
import "@/css/ThemeButton.css";
/**
* ThemeButton.jsx
*
* Este archivo define el componente ThemeButton, que permite a los usuarios cambiar entre temas claro y oscuro.
*
* Importaciones:
* - useTheme: Hook personalizado para acceder al contexto del tema.
* - "../css/ThemeButton.css": Archivo CSS que contiene los estilos para el botón de cambio de tema.
*
* Funcionalidad:
* - ThemeButton: Componente que renderiza un botón para alternar entre temas claro y oscuro.
* - Utiliza el hook `useTheme` para acceder al tema actual y la función para cambiarlo.
* - El botón muestra un icono de sol (☀️) si el tema actual es oscuro, y un icono de luna (🌙) si el tema actual es claro.
*
*/
export default function ThemeButton() {
const ThemeButton = () => {
const { theme, toggleTheme } = useTheme();
return (
@@ -26,3 +10,5 @@ export default function ThemeButton() {
</button>
);
}
export default ThemeButton;

View File

@@ -0,0 +1,41 @@
import { createContext, useState, useEffect } from "react";
import PropTypes from "prop-types";
const ConfigContext = createContext();
export const ConfigProvider = ({ children }) => {
const [config, setConfig] = useState(null);
const [configLoading, setLoading] = useState(true);
const [configError, setError] = useState(null);
useEffect(() => {
const fetchConfig = async () => {
try {
const response = import.meta.env.MODE === 'production'
? await fetch("/config/settings.prod.json")
: await fetch("/config/settings.dev.json");
if (!response.ok) throw new Error("Error al cargar settings.*.json");
const json = await response.json();
setConfig(json);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchConfig();
}, []);
return (
<ConfigContext.Provider value={{ config, configLoading, configError }}>
{children}
</ConfigContext.Provider>
);
};
ConfigProvider.propTypes = {
children: PropTypes.node.isRequired,
};
export {ConfigContext};

View File

@@ -0,0 +1,23 @@
import { createContext } from "react";
import PropTypes from "prop-types";
import { useData } from "@/hooks/useData";
export const DataContext = createContext();
export const DataProvider = ({ config, children }) => {
const data = useData(config);
return (
<DataContext.Provider value={data}>
{children}
</DataContext.Provider>
);
};
DataProvider.propTypes = {
config: PropTypes.shape({
baseUrl: PropTypes.string.isRequired,
params: PropTypes.object,
}).isRequired,
children: PropTypes.node.isRequired,
};

View File

@@ -0,0 +1,31 @@
import { createContext, useEffect, useState } from "react";
export const ThemeContext = createContext();
export const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState(() => {
return (
localStorage.getItem("theme") ||
(window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")
);
});
useEffect(() => {
const root = document.documentElement;
document.body.classList.remove("light", "dark");
document.body.classList.add(theme);
root.classList.remove("light", "dark");
root.classList.add(theme);
localStorage.setItem("theme", theme);
}, [theme]);
const toggleTheme = () => {
setTheme((prev) => (prev === "light" ? "dark" : "light"));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};

View File

@@ -1,60 +0,0 @@
import { createContext, useContext, useState, useEffect } from "react";
import PropTypes from "prop-types";
/**
* ConfigContext.jsx
*
* Este archivo define el contexto de configuración para la aplicación, permitiendo cargar y manejar la configuración desde un archivo externo.
*
* Importaciones:
* - createContext, useContext, useState, useEffect: Funciones de React para crear y utilizar contextos, manejar estados y efectos secundarios.
* - PropTypes: Librería para la validación de tipos de propiedades en componentes de React.
*
* Funcionalidad:
* - ConfigContext: Contexto que almacena la configuración cargada, el estado de carga y cualquier error ocurrido durante la carga de la configuración.
* - ConfigProvider: Proveedor de contexto que maneja la carga de la configuración y proporciona el estado de la configuración a los componentes hijos.
* - Utiliza `fetch` para cargar la configuración desde un archivo JSON.
* - Maneja el estado de carga y errores durante la carga de la configuración.
* - useConfig: Hook personalizado para acceder al contexto de configuración.
*
* PropTypes:
* - ConfigProvider espera un único hijo (`children`) que es requerido y debe ser un nodo de React.
*
*/
const ConfigContext = createContext();
export const ConfigProvider = ({ children }) => {
const [config, setConfig] = useState(null);
const [configLoading, setLoading] = useState(true);
const [configError, setError] = useState(null);
useEffect(() => {
const fetchConfig = async () => {
try {
const response = await fetch("/config/settings.json");
if (!response.ok) throw new Error("Error al cargar settings.json");
const json = await response.json();
setConfig(json);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchConfig();
}, []);
return (
<ConfigContext.Provider value={{ config, configLoading, configError }}>
{children}
</ConfigContext.Provider>
);
};
ConfigProvider.propTypes = {
children: PropTypes.node.isRequired,
};
export const useConfig = () => useContext(ConfigContext);

View File

@@ -1,67 +0,0 @@
import { createContext, useContext, useState, useEffect } from "react";
import PropTypes from "prop-types";
/**
* DataContext.jsx
*
* Este archivo define el contexto de datos para la aplicación, permitiendo obtener y manejar datos de una fuente externa.
*
* Importaciones:
* - createContext, useContext, useState, useEffect: Funciones de React para crear y utilizar contextos, manejar estados y efectos secundarios.
* - PropTypes: Librería para la validación de tipos de propiedades en componentes de React.
*
* Funcionalidad:
* - DataContext: Contexto que almacena los datos obtenidos, el estado de carga y cualquier error ocurrido durante la obtención de datos.
* - DataProvider: Proveedor de contexto que maneja la obtención de datos y proporciona el estado de los datos a los componentes hijos.
* - Utiliza `fetch` para obtener datos de una URL construida a partir de la configuración proporcionada.
* - Maneja el estado de carga y errores durante la obtención de datos.
* - useData: Hook personalizado para acceder al contexto de datos.
*
* PropTypes:
* - DataProvider espera un único hijo (`children`) que es requerido y debe ser un nodo de React.
* - DataProvider también espera una configuración (`config`) que debe incluir `baseUrl` (string) y opcionalmente `params` (objeto).
*
*/
const DataContext = createContext();
export const DataProvider = ({ children, config }) => {
const [data, setData] = useState(null);
const [dataLoading, setLoading] = useState(true);
const [dataError, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const queryParams = new URLSearchParams(config.params).toString();
const url = `${config.baseUrl}?${queryParams}`;
const response = await fetch(url);
if (!response.ok) throw new Error("Error al obtener datos");
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [config]);
return (
<DataContext.Provider value={{ data, dataLoading, dataError }}>
{children}
</DataContext.Provider>
);
};
DataProvider.propTypes = {
children: PropTypes.node.isRequired,
config: PropTypes.shape({
baseUrl: PropTypes.string.isRequired,
params: PropTypes.object,
}).isRequired,
};
export const useData = () => useContext(DataContext);

View File

@@ -1,55 +0,0 @@
import { createContext, useContext, useEffect, useState } from "react";
import PropTypes from "prop-types";
/**
* ThemeContext.jsx
*
* Este archivo define el contexto de tema para la aplicación, permitiendo cambiar entre temas claro y oscuro.
*
* Importaciones:
* - createContext, useContext, useEffect, useState: Funciones de React para crear y utilizar contextos, manejar efectos secundarios y estados.
* - PropTypes: Librería para la validación de tipos de propiedades en componentes de React.
*
* Funcionalidad:
* - ThemeContext: Contexto que almacena el tema actual y la función para cambiarlo.
* - ThemeProvider: Proveedor de contexto que maneja el estado del tema y proporciona la función para alternar entre temas.
* - Utiliza `localStorage` para persistir el tema seleccionado.
* - Aplica la clase correspondiente al `body` del documento para reflejar el tema actual.
* - useTheme: Hook personalizado para acceder al contexto del tema.
*
* PropTypes:
* - ThemeProvider espera un único hijo (`children`) que es requerido y debe ser un nodo de React.
*
*/
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState(() => {
return localStorage.getItem("theme") || "light";
});
useEffect(() => {
document.body.classList.remove("light", "dark");
document.body.classList.add(theme);
localStorage.setItem("theme", theme);
}, [theme]);
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === "light" ? "dark" : "light"));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
ThemeProvider.propTypes = {
children: PropTypes.node.isRequired,
};
export function useTheme() {
return useContext(ThemeContext);
}

View File

@@ -0,0 +1,4 @@
import { useContext } from "react";
import { ConfigContext } from "@/context/ConfigContext.jsx";
export const useConfig = () => useContext(ConfigContext);

View File

@@ -0,0 +1,130 @@
import { useState, useEffect, useCallback, useRef } from "react";
import axios from "axios";
export const useData = (config) => {
const [data, setData] = useState(null);
const [dataLoading, setLoading] = useState(true);
const [dataError, setError] = useState(null);
const configRef = useRef();
useEffect(() => {
if (config?.baseUrl) {
configRef.current = config;
}
}, [config]);
const getAuthHeaders = () => ({
"Content-Type": "application/json",
"Authorization": `Bearer ${localStorage.getItem("token")}`,
});
const fetchData = useCallback(async () => {
const current = configRef.current;
if (!current?.baseUrl) return;
setLoading(true);
setError(null);
try {
const response = await axios.get(current.baseUrl, {
headers: getAuthHeaders(),
params: current.params,
});
setData(response.data);
} catch (err) {
setError(err.response?.data?.message || err.message);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
if (config?.baseUrl) {
fetchData();
}
}, [config, fetchData]);
const getData = async (url, params = {}) => {
try {
const response = await axios.get(url, {
headers: getAuthHeaders(),
params,
});
return { data: response.data, error: null };
} catch (err) {
return {
data: null,
error: err.response?.data?.message || err.message,
};
}
};
const postData = async (endpoint, payload) => {
const headers = {
Authorization: `Bearer ${localStorage.getItem("token")}`,
...(payload instanceof FormData ? {} : { "Content-Type": "application/json" }),
};
const response = await axios.post(endpoint, payload, { headers });
await fetchData();
return response.data;
};
const postDataValidated = async (endpoint, payload) => {
try {
const headers = {
Authorization: `Bearer ${localStorage.getItem("token")}`,
...(payload instanceof FormData ? {} : { "Content-Type": "application/json" }),
};
const response = await axios.post(endpoint, payload, { headers });
return { data: response.data, errors: null };
} catch (err) {
const raw = err.response?.data?.message;
let parsed = {};
try {
parsed = JSON.parse(raw);
} catch {
return { data: null, errors: { general: raw || err.message } };
}
return { data: null, errors: parsed };
}
};
const putData = async (endpoint, payload) => {
const response = await axios.put(endpoint, payload, {
headers: getAuthHeaders(),
});
await fetchData();
return response.data;
};
const deleteData = async (endpoint) => {
const response = await axios.delete(endpoint, {
headers: getAuthHeaders(),
});
await fetchData();
return response.data;
};
const deleteDataWithBody = async (endpoint, payload) => {
const response = await axios.delete(endpoint, {
headers: getAuthHeaders(),
data: payload,
});
await fetchData();
return response.data;
};
return {
data,
dataLoading,
dataError,
getData,
postData,
postDataValidated,
putData,
deleteData,
deleteDataWithBody,
};
};

View File

@@ -0,0 +1,4 @@
import { useContext } from "react";
import { DataContext } from "@/context/DataContext";
export const useDataContext = () => useContext(DataContext);

View File

@@ -0,0 +1,10 @@
import { useContext } from "react";
import { ThemeContext } from "@/context/ThemeContext";
export const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme debe usarse dentro de un <ThemeProvider>");
}
return context;
};

View File

@@ -4,8 +4,8 @@ import { BrowserRouter } from 'react-router-dom'
import './css/index.css'
import App from './components/App.jsx'
import { ThemeProvider } from './contexts/ThemeContext.jsx'
import { ConfigProvider } from './contexts/ConfigContext.jsx'
import { ThemeProvider } from './context/ThemeContext.jsx'
import { ConfigProvider } from './context/ConfigContext.jsx'
/**
* main.jsx

View File

@@ -1,6 +1,6 @@
import PollutionMap from '../components/PollutionMap.jsx'
import HistoryCharts from '../components/HistoryCharts.jsx'
import SummaryCards from '../components/SummaryCards.jsx'
import PollutionMap from '@/components/PollutionMap.jsx'
import HistoryCharts from '@/components/HistoryCharts.jsx'
import SummaryCards from '@/components/SummaryCards.jsx'
import { useParams } from 'react-router-dom';

View File

@@ -1,4 +1,4 @@
import '../css/Home.css';
import '@/css/Home.css';
const Home = () => {
return (

View File

@@ -1,19 +0,0 @@
const timestampToTime = (timestamp) => {
const date = new Date(timestamp);
return date.toLocaleTimeString();
}
const timestampToDate = (timestamp) => {
const date = new Date(timestamp);
return date.toLocaleDateString();
}
const formatTime = (date) => {
if (date.length === 8) {
return date.slice(0,5);
} else {
return `0${date.slice(0,3)}`;
}
}
export { timestampToTime, timestampToDate, formatTime };

View File

@@ -0,0 +1,28 @@
export const DateParser = {
// ...deja sqlToString como está si la sigues usando...
timestampToString: (timestamp) => {
if (!timestamp) return '—';
// 1) Si viene un número, lo convertimos a ISO-string
let tsString = timestamp;
if (typeof timestamp === 'number') {
tsString = new Date(timestamp).toISOString();
}
// 2) Ahora hacemos el split sabiendo que tsString es algo tipo "YYYY-MM-DDTHH:mm:…"
const [datePart] = tsString.split('T');
const [year, month, day] = datePart.split('-');
return `${day}/${month}/${year}`;
},
isoToStringWithTime: (isoString) => {
if (!isoString) return '—';
const date = new Date(isoString);
if (isNaN(date)) return '—';
return new Intl.DateTimeFormat('es-ES', {
day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit', hour12: false,
timeZone: 'Europe/Madrid'
}).format(date);
}
};

View File

@@ -0,0 +1,10 @@
export const errorParser = (err) => {
const message = err.response?.data?.message;
try {
const parsed = JSON.parse(message);
return Object.values(parsed)[0];
// eslint-disable-next-line no-unused-vars
} catch (e) {
return message || err.message || "Unknown error";
}
};

View File

@@ -5,7 +5,13 @@ import cleanPlugin from 'vite-plugin-clean'
// https://vite.dev/config/
export default defineConfig({
server: {
port: 5173,
host: "localhost",
port: 3000,
},
resolve: {
alias: {
'@/': '/src/',
},
},
plugins: [react(), cleanPlugin()],
build: {