Big changes on API and Frontend
This commit is contained in:
@@ -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 };
|
||||
Reference in New Issue
Block a user