Big changes on API and Frontend
This commit is contained in:
70
frontend/package-lock.json
generated
70
frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
67
frontend/src/css/Home.css
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -39,25 +54,4 @@
|
||||
|
||||
.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;*/
|
||||
}
|
||||
@@ -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>
|
||||
<App />
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</ConfigProvider>
|
||||
</ThemeProvider>
|
||||
</StrictMode>,
|
||||
|
||||
35
frontend/src/pages/Dashboard.jsx
Normal file
35
frontend/src/pages/Dashboard.jsx
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
export default Home;
|
||||
11
frontend/src/util/date.js
Normal file
11
frontend/src/util/date.js
Normal 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 };
|
||||
@@ -3,6 +3,9 @@ import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
server: {
|
||||
port: 8080,
|
||||
},
|
||||
plugins: [react()],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
|
||||
Reference in New Issue
Block a user