Recovered from backup
This commit is contained in:
20
frontend/src/components/App.jsx
Normal file
20
frontend/src/components/App.jsx
Normal file
@@ -0,0 +1,20 @@
|
||||
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 { ThemeProvider } from '../contexts/ThemeContext.jsx'
|
||||
|
||||
import Home from '../pages/Home.jsx'
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<>
|
||||
<ThemeProvider>
|
||||
<Home />
|
||||
</ThemeProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
55
frontend/src/components/Card.jsx
Normal file
55
frontend/src/components/Card.jsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import "../css/Card.css";
|
||||
import { useTheme } from "../contexts/ThemeContext";
|
||||
|
||||
const Card = ({ title, status, children, styleMode, className }) => {
|
||||
const cardRef = useRef(null);
|
||||
const [shortTitle, setShortTitle] = useState(title);
|
||||
const { theme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
const checkSize = () => {
|
||||
if (cardRef.current) {
|
||||
const width = cardRef.current.offsetWidth;
|
||||
if (width < 300 && title.length > 15) {
|
||||
setShortTitle(title.slice(0, 10) + ".");
|
||||
} else {
|
||||
setShortTitle(title);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkSize();
|
||||
window.addEventListener("resize", checkSize);
|
||||
return () => window.removeEventListener("resize", checkSize);
|
||||
}, [title]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={cardRef}
|
||||
className={styleMode === "override" ? `${className}` :
|
||||
`col-xl-3 col-sm-6 d-flex flex-column align-items-center p-3 card-container ${className}`}
|
||||
>
|
||||
<div className={`card p-3 w-100 ${theme}`}>
|
||||
<h3 className="text-center">{shortTitle}</h3>
|
||||
<div className="card-content">{children}</div>
|
||||
{status ? <span className="status text-center mt-2">{status}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Card.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
styleMode: PropTypes.oneOf(["override", ""]), // Nueva prop opcional
|
||||
className: PropTypes.string, // Nueva prop opcional
|
||||
};
|
||||
|
||||
Card.defaultProps = {
|
||||
styleMode: "",
|
||||
};
|
||||
|
||||
export default Card;
|
||||
27
frontend/src/components/CardContainer.jsx
Normal file
27
frontend/src/components/CardContainer.jsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import Card from "./Card.jsx";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const CardContainer = ({ cards, className }) => {
|
||||
return (
|
||||
<div className={`row justify-content-center g-0 ${className}`}>
|
||||
{cards.map((card, index) => (
|
||||
<Card key={index} title={card.title} status={card.status} styleMode={card.styleMode} className={card.className}>
|
||||
<p className="card-text text-center">{card.content}</p>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CardContainer.propTypes = {
|
||||
cards: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
title: PropTypes.string.isRequired,
|
||||
content: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default CardContainer;
|
||||
15
frontend/src/components/Dashboard.jsx
Normal file
15
frontend/src/components/Dashboard.jsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const Dashboard = (props) => {
|
||||
return (
|
||||
<main className='container justify-content-center'>
|
||||
{props.children}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
Dashboard.propTypes = {
|
||||
children: PropTypes.node
|
||||
}
|
||||
|
||||
export default Dashboard;
|
||||
22
frontend/src/components/Header.jsx
Normal file
22
frontend/src/components/Header.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import '../css/Header.css';
|
||||
import { useTheme } from "../contexts/ThemeContext";
|
||||
|
||||
|
||||
const Header = (props) => {
|
||||
const { theme } = useTheme();
|
||||
|
||||
return (
|
||||
<header className={`justify-content-center text-center mb-4 ${theme}`}>
|
||||
<h1>{props.title}</h1>
|
||||
<p className='subtitle'>{props.subtitle}</p>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
Header.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
subtitle: PropTypes.string
|
||||
}
|
||||
|
||||
export default Header;
|
||||
83
frontend/src/components/HistoryCharts.jsx
Normal file
83
frontend/src/components/HistoryCharts.jsx
Normal file
@@ -0,0 +1,83 @@
|
||||
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 PropTypes from "prop-types";
|
||||
|
||||
import { useTheme } from "../contexts/ThemeContext.jsx";
|
||||
import { DataProvider, useData } from "../contexts/DataContext.jsx";
|
||||
import { ConfigProvider, useConfig } from "../contexts/ConfigContext.jsx";
|
||||
|
||||
ChartJS.register(LineElement, PointElement, LinearScale, CategoryScale, Filler);
|
||||
|
||||
const HistoryCharts = () => {
|
||||
return (
|
||||
<DataProvider apiUrl="https://contaminus.miarma.net/api/v1/sensors">
|
||||
<ConfigProvider>
|
||||
<HistoryChartsContent />
|
||||
</ConfigProvider>
|
||||
</DataProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const HistoryChartsContent = () => {
|
||||
const { config } = useConfig();
|
||||
const { data, loading } = useData();
|
||||
const { theme } = useTheme();
|
||||
|
||||
const optionsDark = config?.appConfig?.historyChartConfig?.chartOptionsDark ?? {};
|
||||
const optionsLight = config?.appConfig?.historyChartConfig?.chartOptionsLight ?? {};
|
||||
const options = theme === "dark" ? optionsDark : optionsLight;
|
||||
const timeLabels = config?.appConfig?.historyChartConfig?.timeLabels ?? [];
|
||||
|
||||
if (loading) return <p>Cargando datos...</p>;
|
||||
|
||||
const temperatureData = [];
|
||||
const humidityData = [];
|
||||
const pollutionLevels = [];
|
||||
|
||||
data?.forEach(sensor => {
|
||||
if (sensor.value != null) {
|
||||
if (sensor.sensor_type === "MQ-135") {
|
||||
pollutionLevels.push(sensor.value);
|
||||
} else if (sensor.sensor_type === "DHT-11") {
|
||||
temperatureData.push(sensor.value);
|
||||
humidityData.push(sensor.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const historyData = [
|
||||
{ title: "🌡️ Temperatura", data: temperatureData.length ? temperatureData : [0], borderColor: "#00FF85", backgroundColor: "rgba(0, 255, 133, 0.2)" },
|
||||
{ title: "💧 Humedad", data: humidityData.length ? humidityData : [0], borderColor: "#00D4FF", backgroundColor: "rgba(0, 212, 255, 0.2)" },
|
||||
{ title: "☁️ Contaminación", data: pollutionLevels.length ? pollutionLevels : [0], borderColor: "#FFA500", backgroundColor: "rgba(255, 165, 0, 0.2)" }
|
||||
];
|
||||
|
||||
return (
|
||||
<CardContainer
|
||||
cards={historyData.map(({ title, data, borderColor, backgroundColor }) => ({
|
||||
title,
|
||||
content: (
|
||||
<Line
|
||||
data={{
|
||||
labels: timeLabels,
|
||||
datasets: [{ data, borderColor, backgroundColor, fill: true, tension: 0.4 }]
|
||||
}}
|
||||
options={options}
|
||||
/>
|
||||
),
|
||||
styleMode: "override",
|
||||
className: "col-lg-4 col-xxs-12 d-flex flex-column align-items-center p-3 card-container"
|
||||
}))}
|
||||
className=""
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
HistoryChartsContent.propTypes = {
|
||||
options: PropTypes.object,
|
||||
timeLabels: PropTypes.array,
|
||||
data: PropTypes.array
|
||||
};
|
||||
|
||||
export default HistoryCharts;
|
||||
86
frontend/src/components/PollutionMap.jsx
Normal file
86
frontend/src/components/PollutionMap.jsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { MapContainer, TileLayer, Circle, Popup } from 'react-leaflet';
|
||||
|
||||
|
||||
import { ConfigProvider } from '../contexts/ConfigContext.jsx';
|
||||
import { useConfig } from '../contexts/ConfigContext.jsx';
|
||||
|
||||
import { DataProvider } from '../contexts/DataContext.jsx';
|
||||
import { useData } from '../contexts/DataContext.jsx';
|
||||
|
||||
const PollutionCircles = ({ data }) => {
|
||||
return data.map(({ lat, lng, level }, index) => {
|
||||
const baseColor = level < 20 ? '#00FF85' : level < 60 ? '#FFA500' : '#FF0000';
|
||||
const steps = 4;
|
||||
const maxRadius = 400;
|
||||
const stepSize = maxRadius / steps;
|
||||
|
||||
return (
|
||||
<div key={index}>
|
||||
{[...Array(steps)].map((_, i) => {
|
||||
const radius = stepSize * (i + 1);
|
||||
const opacity = 0.6 * ((i + 1) / steps);
|
||||
return (
|
||||
<Circle
|
||||
key={`${index}-${i}`}
|
||||
center={[lat, lng]}
|
||||
pathOptions={{ color: baseColor, fillColor: baseColor, fillOpacity: opacity, weight: 1 }}
|
||||
radius={radius}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<Circle
|
||||
center={[lat, lng]}
|
||||
pathOptions={{ color: baseColor, fillColor: baseColor, fillOpacity: 0.8, weight: 2 }}
|
||||
radius={50}
|
||||
>
|
||||
<Popup>Contaminación: {level} µg/m³</Popup>
|
||||
</Circle>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const PollutionMap = () => {
|
||||
return (
|
||||
<DataProvider apiUrl="https://contaminus.miarma.net/api/v1/sensors">
|
||||
<ConfigProvider>
|
||||
<PollutionMapContent />
|
||||
</ConfigProvider>
|
||||
</DataProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const PollutionMapContent = () => {
|
||||
const { config } = useConfig();
|
||||
const SEVILLA = config?.userConfig.city;
|
||||
|
||||
const { data, loading } = useData();
|
||||
|
||||
if (loading) return <p>Cargando datos...</p>;
|
||||
|
||||
const pollutionData = data.map((sensor) => ({
|
||||
lat: sensor.lat,
|
||||
lng: sensor.lon,
|
||||
level: sensor.value
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className='p-3'>
|
||||
<MapContainer center={SEVILLA} zoom={13} scrollWheelZoom={false} style={mapStyles}>
|
||||
<TileLayer
|
||||
attribution='© Contribuidores de <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
/>
|
||||
<PollutionCircles data={pollutionData} />
|
||||
</MapContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const mapStyles = {
|
||||
height: '500px',
|
||||
width: '100%',
|
||||
borderRadius: '20px'
|
||||
};
|
||||
|
||||
export default PollutionMap;
|
||||
48
frontend/src/components/SummaryCards.jsx
Normal file
48
frontend/src/components/SummaryCards.jsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import CardContainer from './CardContainer';
|
||||
|
||||
import { DataProvider } from '../contexts/DataContext';
|
||||
import { useData } from '../contexts/DataContext';
|
||||
|
||||
const SummaryCards = () => {
|
||||
return (
|
||||
<DataProvider apiUrl="https://contaminus.miarma.net/api/v1/sensors?_sort=timestamp&_order=desc">
|
||||
<SummaryCardsContent />
|
||||
</DataProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const SummaryCardsContent = () => {
|
||||
const { data } = useData();
|
||||
|
||||
const CardsData = [
|
||||
{ id: 1, title: "🌡️ Temperatura", content: "N/A", status: "Esperando datos..." },
|
||||
{ id: 2, title: "💧 Humedad", content: "N/A", status: "Esperando datos..." },
|
||||
{ id: 3, title: "☁️ Contaminación", content: "N/A", status: "Esperando datos..." },
|
||||
{ id: 4, title: "🛤️ Carretera", content: "N/A", status: "Esperando datos..." }
|
||||
];
|
||||
|
||||
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 🌤️";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<CardContainer cards={CardsData} />
|
||||
);
|
||||
}
|
||||
|
||||
SummaryCards.propTypes = {
|
||||
data: PropTypes.array
|
||||
};
|
||||
|
||||
export default SummaryCards;
|
||||
12
frontend/src/components/ThemeButton.jsx
Normal file
12
frontend/src/components/ThemeButton.jsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { useTheme } from "../contexts/ThemeContext.jsx";
|
||||
import "../css/ThemeButton.css";
|
||||
|
||||
export default function ThemeButton() {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<button className="theme-toggle" onClick={toggleTheme}>
|
||||
{theme === "dark" ? "☀️" : "🌙"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user