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

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