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:
9
frontend/jsconfig.json
Normal file
9
frontend/jsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
115
frontend/package-lock.json
generated
115
frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
98
frontend/public/config/settings.prod.json
Normal file
98
frontend/public/config/settings.prod.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
frontend/src/api/axiosInstance.js
Normal file
14
frontend/src/api/axiosInstance.js
Normal 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;
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}`}>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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;
|
||||
@@ -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>;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
41
frontend/src/context/ConfigContext.jsx
Normal file
41
frontend/src/context/ConfigContext.jsx
Normal 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};
|
||||
23
frontend/src/context/DataContext.jsx
Normal file
23
frontend/src/context/DataContext.jsx
Normal 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,
|
||||
};
|
||||
31
frontend/src/context/ThemeContext.jsx
Normal file
31
frontend/src/context/ThemeContext.jsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
4
frontend/src/hooks/useConfig.js
Normal file
4
frontend/src/hooks/useConfig.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import { useContext } from "react";
|
||||
import { ConfigContext } from "@/context/ConfigContext.jsx";
|
||||
|
||||
export const useConfig = () => useContext(ConfigContext);
|
||||
130
frontend/src/hooks/useData.js
Normal file
130
frontend/src/hooks/useData.js
Normal 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,
|
||||
};
|
||||
};
|
||||
4
frontend/src/hooks/useDataContext.js
Normal file
4
frontend/src/hooks/useDataContext.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import { useContext } from "react";
|
||||
import { DataContext } from "@/context/DataContext";
|
||||
|
||||
export const useDataContext = () => useContext(DataContext);
|
||||
10
frontend/src/hooks/useTheme.js
Normal file
10
frontend/src/hooks/useTheme.js
Normal 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;
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import '../css/Home.css';
|
||||
import '@/css/Home.css';
|
||||
|
||||
const Home = () => {
|
||||
return (
|
||||
|
||||
@@ -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 };
|
||||
28
frontend/src/util/dateParser.js
Normal file
28
frontend/src/util/dateParser.js
Normal 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);
|
||||
}
|
||||
};
|
||||
10
frontend/src/util/errorParser.js
Normal file
10
frontend/src/util/errorParser.js
Normal 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";
|
||||
}
|
||||
};
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user