1
0

Big changes on API and Frontend

This commit is contained in:
Jose
2025-03-11 23:58:11 +01:00
parent 0e5e93cb73
commit 6cc3c6525e
19 changed files with 389 additions and 142 deletions

View File

@@ -29,6 +29,8 @@ public class Constants {
public static final String PUT_DEVICE_BY_ID = API_PREFIX + "/devices/:deviceId";
public static final String GET_DEVICE_ACTUATORS = API_PREFIX + "/devices/:deviceId/actuators";
public static final String GET_DEVICE_LATEST_VALUES = API_PREFIX + "/devices/:deviceId/latest";
public static final String GET_DEVICE_POLLUTION_MAP = API_PREFIX + "/devices/:deviceId/pollution-map";
public static final String GET_DEVICE_HISTORY = API_PREFIX + "/devices/:deviceId/history";
public static final String GET_SENSORS = API_PREFIX + "/sensors";
public static final String GET_SENSOR_BY_ID = API_PREFIX + "/sensors/:sensorId";
@@ -41,13 +43,6 @@ public class Constants {
public static final String POST_ACTUATORS = API_PREFIX + "/actuators";
public static final String PUT_ACTUATOR_BY_ID = API_PREFIX + "/actuators/:actuatorId";
public static final String GET_GPS_VALUES = API_PREFIX + "/gps-values";
public static final String GET_GPS_VALUE_BY_ID = API_PREFIX + "/gps-values/:valueId";
public static final String POST_GPS_VALUES = API_PREFIX + "/gps-values";
public static final String GET_AIR_VALUES = API_PREFIX + "/air-values";
public static final String GET_AIR_VALUE_BY_ID = API_PREFIX + "/air-values/:valueId";
public static final String POST_AIR_VALUES = API_PREFIX + "/air-values";
private Constants() {
throw new AssertionError("Utility class cannot be instantiated.");

View File

@@ -50,6 +50,8 @@ public class ApiVerticle extends AbstractVerticle {
router.route(HttpMethod.GET, Constants.GET_DEVICE_LATEST_VALUES).handler(this::getDeviceLatestValuesHandler);
router.route(HttpMethod.POST, Constants.POST_DEVICES).handler(this::postDeviceHandler);
router.route(HttpMethod.PUT, Constants.PUT_DEVICE_BY_ID).handler(this::putDeviceByIdHandler);
router.route(HttpMethod.GET, Constants.GET_DEVICE_POLLUTION_MAP).handler(this::getPollutionMapHandler);
router.route(HttpMethod.GET, Constants.GET_DEVICE_HISTORY).handler(this::getDeviceHistoryHandler);
// Sensor Routes
router.route(HttpMethod.GET, Constants.GET_SENSORS).handler(this::getSensorsHandler);
@@ -246,6 +248,26 @@ public class ApiVerticle extends AbstractVerticle {
sendQuery(query, context);
}
private void getPollutionMapHandler(RoutingContext context) {
String deviceId = context.request().getParam("deviceId");
String query = QueryBuilder
.select("*")
.from("v_pollution_map")
.where("deviceId = ?", deviceId)
.build();
sendQuery(query, context);
}
private void getDeviceHistoryHandler(RoutingContext context) {
String deviceId = context.request().getParam("deviceId");
String query = QueryBuilder
.select("*")
.from("v_sensor_history_by_device")
.where("deviceId = ?", deviceId)
.build();
sendQuery(query, context);
}
// Sensor Handlers
private void getSensorsHandler(RoutingContext context) {
String query = QueryBuilder.select("*").from("sensors").build();

View File

@@ -14,7 +14,7 @@ public class MainVerticle extends AbstractVerticle {
getVertx().deployVerticle(new DatabaseVerticle(), options);
getVertx().deployVerticle(new ApiVerticle(), options);
getVertx().deployVerticle(new HttpServerVerticle());
//getVertx().deployVerticle(new HttpServerVerticle());
}
@Override

View File

@@ -19,7 +19,8 @@
"react": "^19.0.0",
"react-chartjs-2": "^5.3.0",
"react-dom": "^19.0.0",
"react-leaflet": "^5.0.0"
"react-leaflet": "^5.0.0",
"react-router-dom": "^7.3.0"
},
"devDependencies": {
"@eslint/js": "^9.19.0",
@@ -1424,6 +1425,12 @@
"@babel/types": "^7.20.7"
}
},
"node_modules/@types/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
@@ -1917,6 +1924,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/cookie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -3922,6 +3938,46 @@
"node": ">=0.10.0"
}
},
"node_modules/react-router": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.3.0.tgz",
"integrity": "sha512-466f2W7HIWaNXTKM5nHTqNxLrHTyXybm7R0eBlVSt0k/u55tTCDO194OIx/NrYD4TS5SXKTNekXfT37kMKUjgw==",
"license": "MIT",
"dependencies": {
"@types/cookie": "^0.6.0",
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0",
"turbo-stream": "2.4.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router-dom": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.3.0.tgz",
"integrity": "sha512-z7Q5FTiHGgQfEurX/FBinkOXhWREJIAB2RiU24lvcBa82PxUpwqvs/PAXb9lJyPjTs2jrl6UkLvCZVGJPeNuuQ==",
"license": "MIT",
"dependencies": {
"react-router": "7.3.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -4104,6 +4160,12 @@
"semver": "bin/semver.js"
}
},
"node_modules/set-cookie-parser": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
"license": "MIT"
},
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -4399,6 +4461,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/turbo-stream": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
"integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
"license": "ISC"
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",

View File

@@ -22,7 +22,8 @@
"react": "^19.0.0",
"react-chartjs-2": "^5.3.0",
"react-dom": "^19.0.0",
"react-leaflet": "^5.0.0"
"react-leaflet": "^5.0.0",
"react-router-dom": "^7.3.0"
},
"devDependencies": {
"@eslint/js": "^9.19.0",

View File

@@ -16,6 +16,9 @@
"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",

View File

@@ -4,11 +4,13 @@ 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 MenuButton from './MenuButton.jsx'
import SideMenu from './SideMenu.jsx'
import ThemeButton from '../components/ThemeButton.jsx'
import Header from '../components/Header.jsx'
import { Routes, Route } from 'react-router-dom'
import { useState } from 'react'
/**
@@ -51,13 +53,15 @@ const App = () => {
return (
<>
{/* Planeo añadir un React Router */}
<MenuButton onClick={toggleSideMenu} />
<SideMenu isOpen={isSideMenuOpen} onClose={toggleSideMenu} />
<ThemeButton />
<div className={isSideMenuOpen ? 'blur m-0 p-0' : 'm-0 p-0'} onClick={closeSideMenu}>
<Header title='Contamin' subtitle='Midiendo la calidad del aire y las calles en Sevilla 🌿🚛' />
<Home />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard/:deviceId" element={<Dashboard />} />
</Routes>
</div>
</>
);

View File

@@ -1,31 +0,0 @@
import PropTypes from 'prop-types';
/**
* Dashboard.jsx
*
* Este archivo define el componente Dashboard, que actúa como contenedor para los componentes principales de la página.
*
* Importaciones:
* - PropTypes: Librería para la validación de tipos de propiedades en componentes de React.
*
* Funcionalidad:
* - Dashboard: Componente que renderiza un contenedor principal (`main`) con los componentes hijos pasados como `props.children`.
*
* PropTypes:
* - Dashboard espera una propiedad `children` que es un nodo de React.
*
*/
const Dashboard = (props) => {
return (
<main className='container justify-content-center'>
{props.children}
</main>
);
}
Dashboard.propTypes = {
children: PropTypes.node
}
export default Dashboard;

View File

@@ -44,7 +44,7 @@ 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.baseUrl;
const BASE = config.appConfig.endpoints.BASE_URL;
const ENDPOINT = config.appConfig.endpoints.sensors;
const reqConfig = {

View File

@@ -1,4 +1,5 @@
import { MapContainer, TileLayer, Circle, Popup } from 'react-leaflet';
import PropTypes from 'prop-types';
import { useConfig } from '../contexts/ConfigContext.jsx';
@@ -57,18 +58,19 @@ const PollutionCircles = ({ data }) => {
});
};
const PollutionMap = () => {
const PollutionMap = ({ deviceId }) => {
const { config, configLoading, configError } = useConfig();
if (configLoading) return <p>Cargando configuración...</p>;
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.baseUrl;
const ENDPOINT = config.appConfig.endpoints.sensors;
const BASE = config.appConfig.endpoints.BASE_URL;
const ENDPOINT = config.appConfig.endpoints.GET_DEVICE_POLLUTION_MAP;
let endp = ENDPOINT.replace('{0}', deviceId);
const reqConfig = {
baseUrl: `${BASE}/${ENDPOINT}`,
baseUrl: `${BASE}/${endp}`,
params: {}
}
@@ -93,10 +95,10 @@ const PollutionMapContent = () => {
const SEVILLA = config?.userConfig.city;
const pollutionData = data.map((sensor) => ({
lat: sensor.lat,
lng: sensor.lon,
level: sensor.value
const pollutionData = data.map((measure) => ({
lat: measure.lat,
lng: measure.lon,
level: measure.carbonMonoxide
}));
return (
@@ -118,4 +120,8 @@ const mapStyles = {
borderRadius: '20px'
};
PollutionMap.propTypes = {
deviceId: PropTypes.number.isRequired
};
export default PollutionMap;

View File

@@ -3,6 +3,14 @@ import PropTypes from 'prop-types';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faTimes } from '@fortawesome/free-solid-svg-icons';
import { DataProvider } from '../contexts/DataContext';
import { useData } from '../contexts/DataContext';
import { useConfig } from '../contexts/ConfigContext';
import { useTheme } from "../contexts/ThemeContext";
import Card from './Card';
/** ⚠️ EN PRUEBAS ⚠️
* SideMenu.jsx
*
@@ -24,16 +32,56 @@ import { faTimes } from '@fortawesome/free-solid-svg-icons';
* ⚠️ EN PRUEBAS ⚠️ **/
const SideMenu = ({ isOpen, onClose }) => {
const { config, configLoading, configError } = useConfig();
if (configLoading) return <p>Cargando configuración...</p>;
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.BASE_URL;
const ENDPOINT = config.appConfig.endpoints.GET_DEVICES;
const reqConfig = {
baseUrl: `${BASE}/${ENDPOINT}`,
params: {}
}
return (
<div className={`side-menu ${isOpen ? 'open' : ''}`}>
<DataProvider config={reqConfig}>
<SideMenuContent isOpen={isOpen} onClose={onClose} />
</DataProvider>
);
};
const SideMenuContent = ({ isOpen, onClose }) => {
const { data, dataLoading, dataError } = useData();
const { theme } = useTheme();
if (dataLoading) return <p>Cargando datos...</p>;
if (dataError) return <p>Error al cargar datos: {dataError}</p>;
if (!data) return <p>Datos no disponibles.</p>;
return (
<div className={`side-menu ${isOpen ? 'open' : ''} ${theme}`}>
<button className="close-btn" onClick={onClose}>
<FontAwesomeIcon icon={faTimes} />
</button>
<ul>
<li><a href="#inicio">ɪɴɪᴄɪᴏ</a></li>
<li><a href="#mapa">ᴍᴀᴘᴀ</a></li>
<li><a href="#historico">ʜɪsᴛʀɪ</a></li>
</ul>
<div className="d-flex flex-column gap-3 mt-5">
{data.map(device => {
return (
<a href={`/dashboard/${device.deviceId}`} key={device.deviceId} style={{ textDecoration: 'none' }}>
<Card
title={device.deviceName}
status={`ID: ${device.deviceId}`}
styleMode={"override"}
className={"col-12"}
>
{[]}
</Card>
</a>
);
})}
</div>
</div>
);
};
@@ -43,4 +91,9 @@ SideMenu.propTypes = {
onClose: PropTypes.func.isRequired
}
SideMenuContent.propTypes = {
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired
}
export default SideMenu;

View File

@@ -1,12 +1,14 @@
import PropTypes from 'prop-types';
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 { useConfig } from '../contexts/ConfigContext';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCloud, faGauge, faTemperature0, faWater } from '@fortawesome/free-solid-svg-icons';
import { timestampToTime } from '../util/date.js';
/**
* SummaryCards.jsx
@@ -31,54 +33,59 @@ import { faCloud, faGauge, faTemperature0, faWater } from '@fortawesome/free-sol
*
*/
const SummaryCards = () => {
const SummaryCards = ({ deviceId }) => {
const { config, configLoading, configError } = useConfig();
if (configLoading) return <p>Cargando configuración...</p>;
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.baseUrl;
const ENDPOINT = config.appConfig.endpoints.sensors;
const BASE = config.appConfig.endpoints.BASE_URL;
const ENDPOINT = config.appConfig.endpoints.GET_DEVICE_LATEST_VALUES;
const endp = ENDPOINT.replace('{0}', deviceId);
const reqConfig = {
baseUrl: `${BASE}/${ENDPOINT}`,
params: {
_sort: 'timestamp',
_order: 'desc',
_limit: 1
}
baseUrl: `${BASE}/${endp}`,
params: {}
}
return (
<DataProvider config={reqConfig}>
<SummaryCardsContent />
<SummaryCardsContent deviceId={deviceId} />
</DataProvider>
);
}
const SummaryCardsContent = () => {
const { data } = useData();
const { data, dataLoading, dataError } = useData();
if (dataLoading) return <p>Cargando datos...</p>;
if (dataError) return <p>Error al cargar datos: {dataError}</p>;
if (!data) return <p>Datos no disponibles.</p>;
const CardsData = [
{ id: 1, title: "Temperatura", content: "N/A", status: "Esperando datos...", titleIcon: <FontAwesomeIcon icon={faTemperature0} /> },
{ id: 2, title: "Humedad", content: "N/A", status: "Esperando datos...", titleIcon: <FontAwesomeIcon icon={faWater} /> },
{ id: 3, title: "Contaminación", content: "N/A", status: "Esperando datos...", titleIcon: <FontAwesomeIcon icon={faCloud} /> },
{ id: 4, title: "Presión", content: "N/A", status: "Esperando datos...", titleIcon: <FontAwesomeIcon icon={faGauge} /> }
{ id: 3, title: "Nivel de CO", content: "N/A", status: "Esperando datos...", titleIcon: <FontAwesomeIcon icon={faCloud} /> },
{ id: 4, title: "Actualizado a las", content: "N/A", status: "Esperando datos...", titleIcon: <FontAwesomeIcon icon={faClock} /> }
];
if (data) {
data.forEach((sensor) => {
if (sensor.sensor_type === "MQ-135") {
CardsData[2].content = `${sensor.value} µg/m³`;
CardsData[2].status = sensor.value > 100 ? "Alta contaminación 😷" : "Aire moderado 🌤️";
} else if (sensor.sensor_type === "DHT-11") {
CardsData[1].content = `${sensor.humidity}%`;
CardsData[1].status = sensor.humidity > 70 ? "Humedad alta 🌧️" : "Nivel normal 🌤️";
CardsData[0].content = `${sensor.temperature}°C`;
CardsData[0].status = sensor.temperature > 30 ? "Calor intenso ☀️" : "Clima agradable 🌤️";
}
});
let coData = data[1];
let tempData = data[2];
let lastTime = timestampToTime(coData.airValuesTimestamp);
let lastDate = new Date(coData.airValuesTimestamp);
CardsData[0].content = tempData.temperature + "°C";
CardsData[0].status = "Temperatura actual";
CardsData[1].content = tempData.humidity + "%";
CardsData[1].status = "Humedad actual";
CardsData[2].content = coData.carbonMonoxide + " ppm";
CardsData[2].status = "Nivel de CO actual";
CardsData[3].content = lastTime.slice(0, 5);
CardsData[3].status = "Día " + lastDate.toLocaleDateString();
}
return (
@@ -87,7 +94,7 @@ const SummaryCardsContent = () => {
}
SummaryCards.propTypes = {
data: PropTypes.array
deviceId: PropTypes.number.isRequired
};
export default SummaryCards;

67
frontend/src/css/Home.css Normal file
View File

@@ -0,0 +1,67 @@
.home-container {
font-family: Arial, sans-serif;
text-align: center;
background-color: #f4f4f4;
padding: 20px;
}
.hero-section {
background: #4CAF50;
color: white;
padding: 50px 20px;
border-radius: 8px;
}
.hero-title {
font-size: 3em;
margin-bottom: 10px;
}
.hero-description {
font-size: 1.2em;
margin-bottom: 20px;
}
.cta-button {
background-color: #fff;
color: #4CAF50;
border: none;
padding: 15px 30px;
font-size: 1.1em;
cursor: pointer;
border-radius: 5px;
transition: background-color 0.3s ease;
}
.cta-button:hover {
background-color: #388E3C;
color: white;
}
.about-section {
margin-top: 50px;
}
.features {
display: flex;
justify-content: space-around;
margin-top: 30px;
}
.feature {
background-color: #ffffff;
padding: 20px;
border-radius: 8px;
width: 30%;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.feature h3 {
font-size: 1.5em;
color: #333;
}
.feature p {
font-size: 1.1em;
color: #666;
}

View File

@@ -4,14 +4,22 @@
left: -350px;
width: 350px;
height: 100%;
background-color: #333;
color: white;
transition: left 0.3s ease;
padding: 30px;
box-shadow: 2px 0 5px rgba(0,0,0,0.5);
z-index: 1000;
}
.side-menu.light {
background-color: white;
color: black;
}
.side-menu.dark {
background-color: #333;
color: white;
}
.side-menu.open {
left: 0;
}
@@ -27,11 +35,18 @@
right: 20px;
background: none;
border: none;
color: white;
font-size: 30px;
cursor: pointer;
}
.side-menu .close-btn.light {
color: black;
}
.side-menu .close-btn.dark {
color: white;
}
.side-menu .close-btn .fa-times {
width: 30px;
height: 30px;
@@ -40,24 +55,3 @@
.side-menu .close-btn:hover {
color: var(--primary-color);
}
.side-menu ul {
list-style: none;
padding: 0;
}
.side-menu ul li {
margin: 20px 0;
}
.side-menu ul li a:hover {
color: var(--primary-color);
}
.side-menu ul li a {
color: white;
text-decoration: none;
font-size: 30px;
font-weight: 600;
/*text-transform: uppercase;*/
}

View File

@@ -1,5 +1,6 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import './css/index.css'
import App from './components/App.jsx'
@@ -29,7 +30,9 @@ createRoot(document.getElementById('root')).render(
<StrictMode>
<ThemeProvider>
<ConfigProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</ConfigProvider>
</ThemeProvider>
</StrictMode>,

View File

@@ -0,0 +1,35 @@
import PollutionMap from '../components/PollutionMap.jsx'
import HistoryCharts from '../components/HistoryCharts.jsx'
import SummaryCards from '../components/SummaryCards.jsx'
import { useParams } from 'react-router-dom';
/**
* Dashboard.jsx
*
* Este archivo define el componente Dashboard, que es el panel de control de un device.
*
* Importaciones:
* - PollutionMap: Un componente que muestra un mapa de la contaminación.
* - HistoryCharts: Un componente que muestra gráficos históricos de la contaminación.
* - SummaryCards: Un componente que muestra tarjetas resumen con información relevante.
*
* Funcionalidad:
* - El componente Home utiliza una estructura de JSX para organizar y renderizar los componentes importados.
* - El componente Dashboard contiene los componentes SummaryCards, PollutionMap y HistoryCharts.
*
*/
const Dashboard = () => {
const { deviceId } = useParams();
return (
<main className='container justify-content-center'>
<SummaryCards deviceId={deviceId} />
<PollutionMap deviceId={deviceId}/>
<HistoryCharts deviceId={deviceId} />
</main>
);
}
export default Dashboard;

View File

@@ -1,35 +1,41 @@
import Dashboard from '../components/Dashboard.jsx'
import PollutionMap from '../components/PollutionMap.jsx'
import HistoryCharts from '../components/HistoryCharts.jsx'
import SummaryCards from '../components/SummaryCards.jsx'
/**
* Home.jsx
*
* Este archivo define el componente Home, que es una página principal de la aplicación.
*
* Importaciones:
* - Dashboard: Un componente que actúa como contenedor para los componentes principales de la página.
* - PollutionMap: Un componente que muestra un mapa de la contaminación.
* - HistoryCharts: Un componente que muestra gráficos históricos de la contaminación.
* - SummaryCards: Un componente que muestra tarjetas resumen con información relevante.
*
* Funcionalidad:
* - El componente Home utiliza una estructura de JSX para organizar y renderizar los componentes importados.
* - El componente Dashboard contiene los componentes SummaryCards, PollutionMap y HistoryCharts.
*
*/
import '../css/Home.css';
const Home = () => {
return (
<>
<Dashboard>
<SummaryCards />
<PollutionMap />
<HistoryCharts />
</Dashboard>
</>
)
<div className="home-container">
<header className="hero-section">
<h1 className="hero-title">ContaminUS</h1>
<p className="hero-description">
Proyecto universitario para monitorear la calidad del aire usando sensores IoT.
</p>
<button className="cta-button">Explorar Proyecto</button>
</header>
<section className="about-section">
<h2>Sobre el Proyecto</h2>
<p>
ContaminUS es una solución basada en tecnologías IoT para medir la calidad del aire en tiempo real.
Este proyecto busca crear una herramienta accesible para estudiantes, investigadores y comunidades
interesadas en el monitoreo ambiental.
</p>
<div className="features">
<div className="feature">
<h3>Medición en tiempo real</h3>
<p>Monitorea la calidad del aire con sensores MQ-135 y DHT11, mostrando datos precisos y actualizados.</p>
</div>
<div className="feature">
<h3>Aplicación web interactiva</h3>
<p>Visualiza los datos de calidad del aire mediante mapas interactivos y gráficos.</p>
</div>
<div className="feature">
<h3>Colaboración en la universidad</h3>
<p>El proyecto está orientado a estudiantes que deseen aprender y colaborar con el análisis de datos ambientales.</p>
</div>
</div>
</section>
</div>
);
}
export default Home;

11
frontend/src/util/date.js Normal file
View File

@@ -0,0 +1,11 @@
const timestampToTime = (timestamp) => {
const date = new Date(timestamp);
return date.toLocaleTimeString();
}
const timestampToDate = (timestamp) => {
const date = new Date(timestamp);
return date.toLocaleDateString();
}
export { timestampToTime, timestampToDate };

View File

@@ -3,6 +3,9 @@ import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
server: {
port: 8080,
},
plugins: [react()],
build: {
rollupOptions: {