From fa1b4570045500f654657d9414361a65a62b610b Mon Sep 17 00:00:00 2001 From: Jose Date: Tue, 6 May 2025 03:52:24 +0200 Subject: [PATCH] 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. --- frontend/jsconfig.json | 9 ++ frontend/package-lock.json | 115 ++++++++++++++-- frontend/package.json | 1 + .../{settings.json => settings.dev.json} | 0 frontend/public/config/settings.prod.json | 98 +++++++++++++ frontend/src/api/axiosInstance.js | 14 ++ frontend/src/components/App.jsx | 37 +---- frontend/src/components/Card.jsx | 29 +--- frontend/src/components/CardContainer.jsx | 19 --- frontend/src/components/Header.jsx | 24 +--- frontend/src/components/HistoryCharts.jsx | 43 ++---- frontend/src/components/MenuButton.jsx | 26 +--- frontend/src/components/PollutionMap.jsx | 27 +--- frontend/src/components/SideMenu.jsx | 32 +---- frontend/src/components/SummaryCards.jsx | 39 ++---- frontend/src/components/ThemeButton.jsx | 24 +--- frontend/src/context/ConfigContext.jsx | 41 ++++++ frontend/src/context/DataContext.jsx | 23 ++++ frontend/src/context/ThemeContext.jsx | 31 +++++ frontend/src/contexts/ConfigContext.jsx | 60 -------- frontend/src/contexts/DataContext.jsx | 67 --------- frontend/src/contexts/ThemeContext.jsx | 55 -------- frontend/src/hooks/useConfig.js | 4 + frontend/src/hooks/useData.js | 130 ++++++++++++++++++ frontend/src/hooks/useDataContext.js | 4 + frontend/src/hooks/useTheme.js | 10 ++ frontend/src/main.jsx | 4 +- frontend/src/pages/Dashboard.jsx | 6 +- frontend/src/pages/Home.jsx | 2 +- frontend/src/util/date.js | 19 --- frontend/src/util/dateParser.js | 28 ++++ frontend/src/util/errorParser.js | 10 ++ frontend/vite.config.js | 8 +- 33 files changed, 563 insertions(+), 476 deletions(-) create mode 100644 frontend/jsconfig.json rename frontend/public/config/{settings.json => settings.dev.json} (100%) create mode 100644 frontend/public/config/settings.prod.json create mode 100644 frontend/src/api/axiosInstance.js create mode 100644 frontend/src/context/ConfigContext.jsx create mode 100644 frontend/src/context/DataContext.jsx create mode 100644 frontend/src/context/ThemeContext.jsx delete mode 100644 frontend/src/contexts/ConfigContext.jsx delete mode 100644 frontend/src/contexts/DataContext.jsx delete mode 100644 frontend/src/contexts/ThemeContext.jsx create mode 100644 frontend/src/hooks/useConfig.js create mode 100644 frontend/src/hooks/useData.js create mode 100644 frontend/src/hooks/useDataContext.js create mode 100644 frontend/src/hooks/useTheme.js delete mode 100644 frontend/src/util/date.js create mode 100644 frontend/src/util/dateParser.js create mode 100644 frontend/src/util/errorParser.js diff --git a/frontend/jsconfig.json b/frontend/jsconfig.json new file mode 100644 index 0000000..30e99a0 --- /dev/null +++ b/frontend/jsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src"] + } \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d76aca8..cd90e81 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index fdac182..55e50ca 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/public/config/settings.json b/frontend/public/config/settings.dev.json similarity index 100% rename from frontend/public/config/settings.json rename to frontend/public/config/settings.dev.json diff --git a/frontend/public/config/settings.prod.json b/frontend/public/config/settings.prod.json new file mode 100644 index 0000000..f000526 --- /dev/null +++ b/frontend/public/config/settings.prod.json @@ -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 + } + } + } + } + } +} \ No newline at end of file diff --git a/frontend/src/api/axiosInstance.js b/frontend/src/api/axiosInstance.js new file mode 100644 index 0000000..5a4f265 --- /dev/null +++ b/frontend/src/api/axiosInstance.js @@ -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; diff --git a/frontend/src/components/App.jsx b/frontend/src/components/App.jsx index 18ff5b5..be449d7 100644 --- a/frontend/src/components/App.jsx +++ b/frontend/src/components/App.jsx @@ -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); diff --git a/frontend/src/components/Card.jsx b/frontend/src/components/Card.jsx index 1adb6bc..1893a0c 100644 --- a/frontend/src/components/Card.jsx +++ b/frontend/src/components/Card.jsx @@ -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); diff --git a/frontend/src/components/CardContainer.jsx b/frontend/src/components/CardContainer.jsx index 8a8030f..6f57d68 100644 --- a/frontend/src/components/CardContainer.jsx +++ b/frontend/src/components/CardContainer.jsx @@ -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 (
diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx index 5bafe17..2cd5da3 100644 --- a/frontend/src/components/Header.jsx +++ b/frontend/src/components/Header.jsx @@ -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(); diff --git a/frontend/src/components/HistoryCharts.jsx b/frontend/src/components/HistoryCharts.jsx index af6e3b4..2982fb1 100644 --- a/frontend/src/components/HistoryCharts.jsx +++ b/frontend/src/components/HistoryCharts.jsx @@ -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

Error al cargar configuración: {configError}

; if (!config) return

Configuración no disponible.

; - 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

Cargando datos...

; + if (error) return

Datos no disponibles.

; const temperatureData = []; const humidityData = []; diff --git a/frontend/src/components/MenuButton.jsx b/frontend/src/components/MenuButton.jsx index 643c0b4..28cb0aa 100644 --- a/frontend/src/components/MenuButton.jsx +++ b/frontend/src/components/MenuButton.jsx @@ -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 ( ); } + +export default ThemeButton; \ No newline at end of file diff --git a/frontend/src/context/ConfigContext.jsx b/frontend/src/context/ConfigContext.jsx new file mode 100644 index 0000000..25b59bc --- /dev/null +++ b/frontend/src/context/ConfigContext.jsx @@ -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 ( + + {children} + + ); +}; + +ConfigProvider.propTypes = { + children: PropTypes.node.isRequired, +}; + +export {ConfigContext}; \ No newline at end of file diff --git a/frontend/src/context/DataContext.jsx b/frontend/src/context/DataContext.jsx new file mode 100644 index 0000000..82d049f --- /dev/null +++ b/frontend/src/context/DataContext.jsx @@ -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 ( + + {children} + + ); +}; + +DataProvider.propTypes = { + config: PropTypes.shape({ + baseUrl: PropTypes.string.isRequired, + params: PropTypes.object, + }).isRequired, + children: PropTypes.node.isRequired, +}; \ No newline at end of file diff --git a/frontend/src/context/ThemeContext.jsx b/frontend/src/context/ThemeContext.jsx new file mode 100644 index 0000000..043a201 --- /dev/null +++ b/frontend/src/context/ThemeContext.jsx @@ -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 ( + + {children} + + ); +}; diff --git a/frontend/src/contexts/ConfigContext.jsx b/frontend/src/contexts/ConfigContext.jsx deleted file mode 100644 index 5012697..0000000 --- a/frontend/src/contexts/ConfigContext.jsx +++ /dev/null @@ -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 ( - - {children} - - ); -}; - -ConfigProvider.propTypes = { - children: PropTypes.node.isRequired, -}; - -export const useConfig = () => useContext(ConfigContext); diff --git a/frontend/src/contexts/DataContext.jsx b/frontend/src/contexts/DataContext.jsx deleted file mode 100644 index 079d641..0000000 --- a/frontend/src/contexts/DataContext.jsx +++ /dev/null @@ -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 ( - - {children} - - ); -}; - -DataProvider.propTypes = { - children: PropTypes.node.isRequired, - config: PropTypes.shape({ - baseUrl: PropTypes.string.isRequired, - params: PropTypes.object, - }).isRequired, -}; - -export const useData = () => useContext(DataContext); \ No newline at end of file diff --git a/frontend/src/contexts/ThemeContext.jsx b/frontend/src/contexts/ThemeContext.jsx deleted file mode 100644 index 3337073..0000000 --- a/frontend/src/contexts/ThemeContext.jsx +++ /dev/null @@ -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 ( - - {children} - - ); -} - -ThemeProvider.propTypes = { - children: PropTypes.node.isRequired, -}; - -export function useTheme() { - return useContext(ThemeContext); -} diff --git a/frontend/src/hooks/useConfig.js b/frontend/src/hooks/useConfig.js new file mode 100644 index 0000000..024ee84 --- /dev/null +++ b/frontend/src/hooks/useConfig.js @@ -0,0 +1,4 @@ +import { useContext } from "react"; +import { ConfigContext } from "@/context/ConfigContext.jsx"; + +export const useConfig = () => useContext(ConfigContext); diff --git a/frontend/src/hooks/useData.js b/frontend/src/hooks/useData.js new file mode 100644 index 0000000..bc19f1e --- /dev/null +++ b/frontend/src/hooks/useData.js @@ -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, + }; +}; diff --git a/frontend/src/hooks/useDataContext.js b/frontend/src/hooks/useDataContext.js new file mode 100644 index 0000000..e139079 --- /dev/null +++ b/frontend/src/hooks/useDataContext.js @@ -0,0 +1,4 @@ +import { useContext } from "react"; +import { DataContext } from "@/context/DataContext"; + +export const useDataContext = () => useContext(DataContext); diff --git a/frontend/src/hooks/useTheme.js b/frontend/src/hooks/useTheme.js new file mode 100644 index 0000000..330a5e0 --- /dev/null +++ b/frontend/src/hooks/useTheme.js @@ -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 "); + } + return context; +}; diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 452ec5e..d01e062 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -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 diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx index 9c56e17..e1938fb 100644 --- a/frontend/src/pages/Dashboard.jsx +++ b/frontend/src/pages/Dashboard.jsx @@ -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'; diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index 0c76da0..ea2aec9 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -1,4 +1,4 @@ -import '../css/Home.css'; +import '@/css/Home.css'; const Home = () => { return ( diff --git a/frontend/src/util/date.js b/frontend/src/util/date.js deleted file mode 100644 index 2fc42d9..0000000 --- a/frontend/src/util/date.js +++ /dev/null @@ -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 }; \ No newline at end of file diff --git a/frontend/src/util/dateParser.js b/frontend/src/util/dateParser.js new file mode 100644 index 0000000..ddc5775 --- /dev/null +++ b/frontend/src/util/dateParser.js @@ -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); + } +}; diff --git a/frontend/src/util/errorParser.js b/frontend/src/util/errorParser.js new file mode 100644 index 0000000..971bcd8 --- /dev/null +++ b/frontend/src/util/errorParser.js @@ -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"; + } +}; diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 421fae7..1807cd4 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -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: {