cambios generales en interfaz, metodo de gestion de lineas y otros arreglos menores

This commit is contained in:
2025-07-11 20:50:11 +02:00
parent 3b0ea97ea1
commit 9a780e1884
10 changed files with 387 additions and 94 deletions

View File

@@ -18,10 +18,10 @@ datos = {
"El punto", "Estación de Sevilla", "Nuevo Mercado" "El punto", "Estación de Sevilla", "Nuevo Mercado"
], ],
"Línea 2": [ "Línea 2": [
"Zafra", "Nuevo Mercado", "Villa de Madrid", "Avenida Italia", "Zafra", "Nuevo Mercado", "Villa de Madrid", "Avenida Italia Estación de Sevilla",
"Estación de Sevilla", "El Punto", "Estación de Ferrocarril", "Juzgados", "El Punto", "Estación de Ferrocarril", "Juzgados",
"Barrio Obrero", "El Porvenir", "Las Delicias", "Isla Chica", "Barrio Obrero", "El Porvenir", "Las Delicias", "Isla Chica",
"Bda, José Antonio", "Fuerzas Armadas Los Rosales", "Palacio de Deporte", "Bda. José Antonio", "Fuerzas Armadas Los Rosales", "Palacio de Deportes",
"Ciencias de la Educación (Universidad)", "Biblioteca (Universidad)", "Ciencias de la Educación (Universidad)", "Biblioteca (Universidad)",
"Monumento al Fútbol", "Relaciones Laborales (Universidad)", "Monumento al Fútbol", "Relaciones Laborales (Universidad)",
"Vista Alegre-Universidad", "Centro Comercial Holea", "Plaza las Amapolas", "Vista Alegre-Universidad", "Centro Comercial Holea", "Plaza las Amapolas",
@@ -31,7 +31,7 @@ datos = {
"Bda. Navidad", "Molino de la Vega", "Paseo de las Palmeras", "Bda. Navidad", "Molino de la Vega", "Paseo de las Palmeras",
"Julio Caro Baroja (Aqualon)" "Julio Caro Baroja (Aqualon)"
], ],
"Línea 131": ["Puente de Vallecas"] "Línea 141": ["Puente de Vallecas"]
} }
@app.route("/") @app.route("/")

89
package-lock.json generated
View File

@@ -8,11 +8,14 @@
"name": "mi-app-paradas-react", "name": "mi-app-paradas-react",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@fontsource/roboto": "^5.2.6",
"@material/web": "^2.3.0",
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"cra-template": "1.2.0", "cra-template": "^1.2.0",
"flask": "^0.2.10",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-scripts": "5.0.1", "react-scripts": "^5.0.1",
"web-vitals": "^4.2.4" "web-vitals": "^4.2.4"
} }
}, },
@@ -2305,6 +2308,15 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0" "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
} }
}, },
"node_modules/@fontsource/roboto": {
"version": "5.2.6",
"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.2.6.tgz",
"integrity": "sha512-hzarG7yAhMoP418smNgfY4fO7UmuUEm5JUtbxCoCcFHT0hOJB+d/qAEyoNjz7YkPU5OjM2LM8rJnW8hfm0JLaA==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@humanwhocodes/config-array": { "node_modules/@humanwhocodes/config-array": {
"version": "0.13.0", "version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@@ -2778,6 +2790,34 @@
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
"integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==" "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw=="
}, },
"node_modules/@lit-labs/ssr-dom-shim": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.3.0.tgz",
"integrity": "sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ==",
"license": "BSD-3-Clause"
},
"node_modules/@lit/reactive-element": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.0.tgz",
"integrity": "sha512-L2qyoZSQClcBmq0qajBVbhYEcG6iK0XfLn66ifLe/RfC0/ihpc+pl0Wdn8bJ8o+hj38cG0fGXRgSS20MuXn7qA==",
"license": "BSD-3-Clause",
"dependencies": {
"@lit-labs/ssr-dom-shim": "^1.2.0"
}
},
"node_modules/@material/web": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@material/web/-/web-2.3.0.tgz",
"integrity": "sha512-r7ccZHthMk5tM05goPJ965hQ99ptMyZt7i8Xi8+RmEqK3ZXaMtjx+s4p+9OVX+vgOS3zpop+g1yGYXtOOlrwUg==",
"license": "Apache-2.0",
"workspaces": [
"catalog"
],
"dependencies": {
"lit": "^2.8.0 || ^3.0.0",
"tslib": "^2.4.0"
}
},
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
"version": "5.1.1-v1", "version": "5.1.1-v1",
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
@@ -4804,6 +4844,7 @@
"url": "https://opencollective.com/bootstrap" "url": "https://opencollective.com/bootstrap"
} }
], ],
"license": "MIT",
"peerDependencies": { "peerDependencies": {
"@popperjs/core": "^2.11.8" "@popperjs/core": "^2.11.8"
} }
@@ -5433,6 +5474,7 @@
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/cra-template/-/cra-template-1.2.0.tgz", "resolved": "https://registry.npmjs.org/cra-template/-/cra-template-1.2.0.tgz",
"integrity": "sha512-06WBUmTq79NvqU91Y9OPCXv/ENy/UkUmQS0lBrOYCl/4f4l67idnGbBARDGLopCHfff6pf6UftcFRWHg+CVfRw==", "integrity": "sha512-06WBUmTq79NvqU91Y9OPCXv/ENy/UkUmQS0lBrOYCl/4f4l67idnGbBARDGLopCHfff6pf6UftcFRWHg+CVfRw==",
"license": "MIT",
"engines": { "engines": {
"node": ">=14" "node": ">=14"
} }
@@ -7459,6 +7501,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/flask": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/flask/-/flask-0.2.10.tgz",
"integrity": "sha512-Zy69KIuFsoX0o2W5uDHHk1GHMwRT8QT0AboPQY7b1soCfJ2pfwKxHF2AGVwJmDUjzawgOavoY/UsXnZ7fk3lVQ==",
"license": "MIT"
},
"node_modules/flat-cache": { "node_modules/flat-cache": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
@@ -10178,6 +10226,37 @@
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
}, },
"node_modules/lit": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lit/-/lit-3.3.0.tgz",
"integrity": "sha512-DGVsqsOIHBww2DqnuZzW7QsuCdahp50ojuDaBPC7jUDRpYoH0z7kHBBYZewRzer75FwtrkmkKk7iOAwSaWdBmw==",
"license": "BSD-3-Clause",
"dependencies": {
"@lit/reactive-element": "^2.1.0",
"lit-element": "^4.2.0",
"lit-html": "^3.3.0"
}
},
"node_modules/lit-element": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.0.tgz",
"integrity": "sha512-MGrXJVAI5x+Bfth/pU9Kst1iWID6GHDLEzFEnyULB/sFiRLgkd8NPK/PeeXxktA3T6EIIaq8U3KcbTU5XFcP2Q==",
"license": "BSD-3-Clause",
"dependencies": {
"@lit-labs/ssr-dom-shim": "^1.2.0",
"@lit/reactive-element": "^2.1.0",
"lit-html": "^3.3.0"
}
},
"node_modules/lit-html": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.0.tgz",
"integrity": "sha512-RHoswrFAxY2d8Cf2mm4OZ1DgzCoBKUKSPvA1fhtSELxUERq2aQQ2h05pO9j81gS1o7RIRJ+CePLogfyahwmynw==",
"license": "BSD-3-Clause",
"dependencies": {
"@types/trusted-types": "^2.0.2"
}
},
"node_modules/loader-runner": { "node_modules/loader-runner": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
@@ -12595,6 +12674,7 @@
"version": "19.0.0", "version": "19.0.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
"license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -12723,6 +12803,7 @@
"version": "19.0.0", "version": "19.0.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
"integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"scheduler": "^0.25.0" "scheduler": "^0.25.0"
}, },
@@ -12752,6 +12833,7 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
"integrity": "sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==", "integrity": "sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/core": "^7.16.0", "@babel/core": "^7.16.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.3", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.3",
@@ -15234,7 +15316,8 @@
"node_modules/web-vitals": { "node_modules/web-vitals": {
"version": "4.2.4", "version": "4.2.4",
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz",
"integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==" "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==",
"license": "Apache-2.0"
}, },
"node_modules/webidl-conversions": { "node_modules/webidl-conversions": {
"version": "6.1.0", "version": "6.1.0",

View File

@@ -3,11 +3,14 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@fontsource/roboto": "^5.2.6",
"@material/web": "^2.3.0",
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"cra-template": "1.2.0", "cra-template": "^1.2.0",
"flask": "^0.2.10",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-scripts": "5.0.1", "react-scripts": "^5.0.1",
"web-vitals": "^4.2.4" "web-vitals": "^4.2.4"
}, },
"scripts": { "scripts": {

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,6 +1,15 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef, useMemo } from 'react';
import '@material/web/button/filled-button.js';
import '@material/web/button/outlined-button.js';
import '@material/web/select/filled-select.js';
import '@material/web/select/select-option.js';
import '@material/web/radio/radio.js';
import '@material/web/slider/slider.js';
// import '@material/web/card/card.js'; // Temporalmente comentado
import './App.css'; import './App.css';
import 'bootstrap/dist/css/bootstrap.min.css';
// Importa la fuente Roboto de Google Fonts
import "@fontsource/roboto"; // npm install @fontsource/roboto
function App() { function App() {
const [lineaSeleccionada, setLineaSeleccionada] = useState(''); const [lineaSeleccionada, setLineaSeleccionada] = useState('');
@@ -11,8 +20,10 @@ function App() {
const audioRef = useRef(null); const audioRef = useRef(null);
const [theme, setTheme] = useState('light'); const [theme, setTheme] = useState('light');
const [playing, setPlaying] = useState(false); const [playing, setPlaying] = useState(false);
const [ciudadSeleccionada, setCiudadSeleccionada] = useState('');
const datos = { // Diccionario de paradas por línea para evitar mezclas y garantizar independencia
const paradasPorLinea = useMemo(() => ({
"Línea 1": [ "Línea 1": [
"Zafra", "Julio Caro Baroja (Aqualon)", "Avda. Alemania (Esquina Ruiz de Alda)", "Zafra", "Julio Caro Baroja (Aqualon)", "Avda. Alemania (Esquina Ruiz de Alda)",
"Avda. Alemania (Plaza de Toros)", "Bda. Navidad", "Don Bosco", "Avda. Alemania (Plaza de Toros)", "Bda. Navidad", "Don Bosco",
@@ -27,10 +38,10 @@ function App() {
"El punto", "Estación de Sevilla", "Nuevo Mercado" "El punto", "Estación de Sevilla", "Nuevo Mercado"
], ],
"Línea 2": [ "Línea 2": [
"Zafra", "Nuevo Mercado", "Villa de Madrid", "Avenida Italia", "Zafra", "Nuevo Mercado", "Villa de Madrid", "Avenida Italia Estación de Sevilla",
"Estación de Sevilla", "El Punto", "Estación de Ferrocarril", "Juzgados", "El Punto", "Estación de Ferrocarril", "Juzgados",
"Barrio Obrero", "El Porvenir", "Las Delicias", "Isla Chica", "Barrio Obrero", "El Porvenir", "Las Delicias", "Isla Chica",
"Bda, José Antonio", "Fuerzas Armadas Los Rosales", "Palacio de Deporte", "Bda. José Antonio", "Fuerzas Armadas Los Rosales", "Palacio de Deportes",
"Ciencias de la Educación (Universidad)", "Biblioteca (Universidad)", "Ciencias de la Educación (Universidad)", "Biblioteca (Universidad)",
"Monumento al Fútbol", "Relaciones Laborales (Universidad)", "Monumento al Fútbol", "Relaciones Laborales (Universidad)",
"Vista Alegre-Universidad", "Centro Comercial Holea", "Plaza las Amapolas", "Vista Alegre-Universidad", "Centro Comercial Holea", "Plaza las Amapolas",
@@ -40,25 +51,60 @@ function App() {
"Bda. Navidad", "Molino de la Vega", "Paseo de las Palmeras", "Bda. Navidad", "Molino de la Vega", "Paseo de las Palmeras",
"Julio Caro Baroja (Aqualon)" "Julio Caro Baroja (Aqualon)"
], ],
"Línea 131": ["Puente de Vallecas"], "Línea 3": [
"Línea T32": ["Profesor Raúl Vázquez"], "Zafra", "Plaza de la Vera Cruz", "San Andrés", "Av. Manuel Siurot", "La Morana",
"xx": ["tristante"] "Obispado", "Colegio Mayor San Pablo", "Ciudad Deportiva", "Av. Manuel Siurot (Montessori)",
}; "Santuario Nuestra Sra. de la Cinta", "Legión Española (O. Alta)", "Montevideo (Legión Española)",
"Orden Baja", "Zenobia (IPFD)", "Cruce Cementerio", "La Orden", "Avda. Santa Marta",
"Gómez de Avellaneda", "Urb. Santa Mª del Pilar (Verdeluz)", "Honduras (Saladillo)",
"Alanís de la Sierra", "Galaroza (H. Palomo)", "Jabugo", "Pérez Cubillas", "Jabugo",
"Isla Chica", "Las Delicias", "El Árbol", "Polideportivo Andrés Estrada", "Bomberos",
"San Sebastián (Bomberos)", "San Sebastián (Plaza los Litri)", "San Andrés",
"Plaza de la Merced (Universidad)", "Molino de la Vega", "Paseo de las Palmeras", "Zafra"
],
"Línea 141": ["Puente de Vallecas"],
"Línea T32": ["Profesor Raúl Vázquez"]
}), []);
// Estructura de líneas con ciudad
const datos = [
{ ciudad: "Huelva", linea: "Línea 1" },
{ ciudad: "Huelva", linea: "Línea 2" },
{ ciudad: "Huelva", linea: "Línea 3" },
{ ciudad: "Madrid", linea: "Línea 141" },
{ ciudad: "Madrid", linea: "Línea T32" }
];
const ciudades = [
"Huelva",
"Madrid",
"Sevilla"
];
// Filtra las líneas por ciudad seleccionada
const lineasFiltradas = datos.filter(d => d.ciudad === ciudadSeleccionada);
// Busca las paradas de la línea seleccionada de forma segura
const paradasFiltradas = paradasPorLinea[lineaSeleccionada] || [];
const handleLineaChange = (event) => { const handleLineaChange = (event) => {
setLineaSeleccionada(event.target.value); setLineaSeleccionada(event.target.value);
setParadaSeleccionada(''); setParadaSeleccionada('');
// Recarga interna: fuerza el componente de paradas a "resetearse"
// Si usas un key en el select de paradas, cambiará el componente y limpiará el menú
setParadasKey(Math.random());
}; };
// Key para forzar recarga del select de paradas
const [paradasKey, setParadasKey] = useState(0);
const handleParadaChange = (event) => { const handleParadaChange = (event) => {
setParadaSeleccionada(event.target.value); setParadaSeleccionada(event.target.value);
}; };
const handleTipoParadaChange = (event) => {
setTipoParada(event.target.value);
};
const handleVolumeChange = (event) => { const handleVolumeChange = (event) => {
setVolume(event.target.value); setVolume(event.target.value);
// Aplica el volumen global a cualquier audio que esté sonando
if (audioRef.current) { if (audioRef.current) {
audioRef.current.volume = event.target.value; audioRef.current.volume = event.target.value;
} }
@@ -71,6 +117,7 @@ function App() {
} else if (lineaSeleccionada && paradaSeleccionada) { } else if (lineaSeleccionada && paradaSeleccionada) {
const playAudio = (audioFile) => { const playAudio = (audioFile) => {
const nuevoAudio = new Audio(audioFile); const nuevoAudio = new Audio(audioFile);
nuevoAudio.volume = volume; // Aplica volumen global
nuevoAudio.play().catch(error => { nuevoAudio.play().catch(error => {
console.error("Error al reproducir audio:", error); console.error("Error al reproducir audio:", error);
}); });
@@ -111,35 +158,114 @@ function App() {
} }
}; };
const reproducirColision = () => { const reproducirColision = () => {
const audioColision = new Audio('/audio/colision.wav'); const audioColision = new Audio('/audio/colision.wav');
audioColision.volume = volume; // Aplica volumen global
audioColision.play().catch(error => { audioColision.play().catch(error => {
console.error("Error al reproducir audio:", error); console.error("Error al reproducir audio:", error);
}); });
}; };
useEffect(() => { const reproducirCanceladora = (audioNombre) => {
const cargarAudios = () => { const audioCanceladora = new Audio(`/audio/${audioNombre}.mp3`);
const audioActual = new Audio(`/audio/parada_actual.wav`); audioCanceladora.volume = volume; // Aplica volumen global
const audioSiguiente = new Audio(`/audio/parada_siguiente.wav`); audioCanceladora.play().catch(error => {
console.error("Error al reproducir audio:", error);
for (const linea in datos) {
datos[linea].forEach(parada => {
const lineaSinEspacios = linea.split(" ")[1] || linea.split(" ")[0];
const audio = new Audio(`/audio/${lineaSinEspacios}-${parada.toLowerCase().replace(/ /g, "_")}.wav`);
}); });
}
const audioVallecas = new Audio(`/audio/vallecas.wav`);
const audioColision = new Audio(`/audio/colision.m4a`);
}; };
cargarAudios(); useEffect(() => {
}, []); // Elimina variables no usadas
// const audioActual = new Audio(`/audio/parada_actual.wav`);
// const audioSiguiente = new Audio(`/audio/parada_siguiente.wav`);
// Usar paradasPorLinea para evitar error de undefined
Object.entries(paradasPorLinea).forEach(([linea, paradas]) => {
paradas.forEach(parada => {
const lineaSinEspacios = linea.split(" ")[1] || linea.split(" ")[0];
// Solo crea el objeto, no lo asignes a una variable no usada
new Audio(`/audio/${lineaSinEspacios}-${parada.toLowerCase().replace(/ /g, "_")}.wav`);
});
});
}, [paradasPorLinea]);
useEffect(() => { useEffect(() => {
localStorage.setItem('theme', theme); localStorage.setItem('theme', theme);
// Material 3 green-based color schemes (from your images)
const lightScheme = {
'--md-sys-color-primary': '#4C7A1F', // P-40
'--md-sys-color-on-primary': '#FFFFFF', // P-100
'--md-sys-color-primary-container': '#B6F397', // P-90
'--md-sys-color-on-primary-container': '#17360D', // P-10
'--md-sys-color-secondary': '#6A745F', // S-40
'--md-sys-color-on-secondary': '#FFFFFF', // S-100
'--md-sys-color-secondary-container': '#D7E2C7', // S-90
'--md-sys-color-on-secondary-container': '#23281B', // S-10
'--md-sys-color-tertiary': '#2B6A78', // T-40
'--md-sys-color-on-tertiary': '#FFFFFF', // T-100
'--md-sys-color-tertiary-container': '#B6EAF3', // T-90
'--md-sys-color-on-tertiary-container': '#102A2F', // T-10
'--md-sys-color-error': '#BA1A1A', // E-40
'--md-sys-color-on-error': '#FFFFFF', // E-100
'--md-sys-color-error-container': '#FFDAD6', // E-90
'--md-sys-color-on-error-container': '#410002', // E-10
'--md-sys-color-background': '#F7F9F3', // N-98
'--md-sys-color-on-background': '#1A1C18', // N-10
'--md-sys-color-surface': '#F7F9F3', // N-98
'--md-sys-color-on-surface': '#1A1C18', // N-10
'--md-sys-color-surface-variant': '#E1E3D6', // NV-80
'--md-sys-color-on-surface-variant': '#45483C', // NV-30
'--md-sys-color-outline': '#76786B', // NV-50
'--md-sys-color-outline-variant': '#C5C8B7', // NV-80
'--md-sys-color-surface-dim': '#E3E3DB', // N-87
'--md-sys-color-surface-bright': '#F7F9F3', // N-98
'--md-sys-color-surface-container-lowest': '#FFFFFF', // N-100
'--md-sys-color-surface-container-low': '#F1F3EB', // N-96
'--md-sys-color-surface-container': '#ECEEE6', // N-94
'--md-sys-color-surface-container-high': '#E7E9E1', // N-92
'--md-sys-color-surface-container-highest': '#E3E3DB', // N-90
};
const darkScheme = {
'--md-sys-color-primary': '#B6F397', // P-80
'--md-sys-color-on-primary': '#17360D', // P-20
'--md-sys-color-primary-container': '#2B5A1B', // P-30
'--md-sys-color-on-primary-container': '#D6FFD6', // P-90
'--md-sys-color-secondary': '#B6BFA8', // S-80
'--md-sys-color-on-secondary': '#23281B', // S-20
'--md-sys-color-secondary-container': '#3C4430', // S-30
'--md-sys-color-on-secondary-container': '#E7F3D7', // S-90
'--md-sys-color-tertiary': '#8FE6F3', // T-80
'--md-sys-color-on-tertiary': '#102A2F', // T-20
'--md-sys-color-tertiary-container': '#1B4A5A', // T-30
'--md-sys-color-on-tertiary-container': '#D6F6FF', // T-90
'--md-sys-color-error': '#FFDAD6', // E-80
'--md-sys-color-on-error': '#BA1A1A', // E-20
'--md-sys-color-error-container': '#93000A', // E-30
'--md-sys-color-on-error-container': '#FFDAD6', // E-90
'--md-sys-color-background': '#23281B', // N-6
'--md-sys-color-on-background': '#E3E3DB', // N-90
'--md-sys-color-surface': '#23281B', // N-6
'--md-sys-color-on-surface': '#E3E3DB', // N-90
'--md-sys-color-surface-variant': '#45483C', // NV-30
'--md-sys-color-on-surface-variant': '#C5C8B7', // NV-80
'--md-sys-color-outline': '#76786B', // NV-60
'--md-sys-color-outline-variant': '#45483C', // NV-30
'--md-sys-color-surface-dim': '#1A1C18', // N-4
'--md-sys-color-surface-bright': '#3C4430', // N-24
'--md-sys-color-surface-container-lowest': '#181A14', // N-4
'--md-sys-color-surface-container-low': '#23281B', // N-10
'--md-sys-color-surface-container': '#23281B', // N-12
'--md-sys-color-surface-container-high': '#2B2F23', // N-17
'--md-sys-color-surface-container-highest': '#353A2B', // N-22
};
const scheme = theme === 'dark' ? darkScheme : lightScheme;
for (const [key, value] of Object.entries(scheme)) {
document.body.style.setProperty(key, value);
}
document.body.classList.toggle('dark-theme', theme === 'dark'); document.body.classList.toggle('dark-theme', theme === 'dark');
document.body.classList.toggle('light-theme', theme === 'light'); document.body.classList.toggle('light-theme', theme === 'light');
}, [theme]); }, [theme]);
@@ -148,91 +274,172 @@ function App() {
setTheme(theme === 'light' ? 'dark' : 'light'); setTheme(theme === 'light' ? 'dark' : 'light');
}; };
const reproducirCanceladora = (audioNombre) => {
const audioCanceladora = new Audio(`/audio/${audioNombre}.mp3`);
audioCanceladora.play().catch(error => {
console.error("Error al reproducir audio:", error);
});
};
return ( return (
<div className="container mt-4"> <div
<h1 className="mb-4">Aplicación de Megafonía</h1> style={{
maxWidth: 480,
margin: "2rem auto",
fontFamily: "'Roboto', Arial, Helvetica, sans-serif"
}}
>
<h1 style={{ marginBottom: "1.5rem" }}>Aplicación de Megafonía</h1>
<div className="card mb-4"> {/* Dropdown de ciudad */}
<div className="card-body"> <div style={{ marginBottom: "1.5rem" }}>
<div className="mb-3"> <label htmlFor="ciudad">Ciudad:</label>
<label htmlFor="linea" className="form-label">Línea:</label> <md-filled-select
<select id="linea" value={lineaSeleccionada} onChange={handleLineaChange} className="form-select"> id="ciudad"
<option value="">Selecciona una línea</option> value={ciudadSeleccionada}
{Object.keys(datos).map((linea) => ( onInput={e => {
<option key={linea} value={linea}>{linea}</option> setCiudadSeleccionada(e.target.value);
setLineaSeleccionada('');
setParadaSeleccionada('');
setParadasKey(Math.random());
}}
style={{ width: "100%" }}
>
<md-select-option value="">
<div slot="headline">Selecciona una ciudad</div>
</md-select-option>
{ciudades.map((ciudad) => (
<md-select-option key={ciudad} value={ciudad}>
<div slot="headline">{ciudad}</div>
</md-select-option>
))} ))}
</select> </md-filled-select>
</div>
<div style={{ border: "1px solid #ccc", borderRadius: 12, marginBottom: "1.5rem", boxShadow: "0 2px 4px #0001" }}>
<div style={{ padding: "1rem" }}>
<div style={{ marginBottom: "1.5rem" }}>
<label htmlFor="linea">Línea:</label>
<md-filled-select
id="linea"
value={lineaSeleccionada}
onInput={e => handleLineaChange({ target: { value: e.target.value } })}
style={{ width: "100%" }}
disabled={!ciudadSeleccionada}
>
<md-select-option value="">
<div slot="headline">Selecciona una línea</div>
</md-select-option>
{lineasFiltradas.map((l) => (
<md-select-option key={l.linea} value={l.linea}>
<div slot="headline">{l.linea}</div>
</md-select-option>
))}
</md-filled-select>
</div> </div>
<div> <div>
<label htmlFor="parada" className="form-label">Parada:</label> <label htmlFor="parada">Parada:</label>
<select id="parada" value={paradaSeleccionada} onChange={handleParadaChange} className="form-select"> <md-filled-select
<option value="">Selecciona una parada</option> key={paradasKey}
{datos[lineaSeleccionada]?.map((parada) => ( id="parada"
<option key={parada} value={parada}>{parada}</option> value={paradaSeleccionada}
onInput={e => handleParadaChange({ target: { value: e.target.value } })}
style={{ width: "100%" }}
disabled={!lineaSeleccionada}
>
<md-select-option value="">
<div slot="headline">Selecciona una parada</div>
</md-select-option>
{paradasFiltradas.map((parada) => (
<md-select-option key={parada} value={parada}>
<div slot="headline">{parada}</div>
</md-select-option>
))} ))}
</select> </md-filled-select>
</div> </div>
</div> </div>
</div> </div>
<div className="card mb-4"> <div style={{ border: "1px solid #ccc", borderRadius: 12, marginBottom: "1.5rem", boxShadow: "0 2px 4px #0001" }}>
<div className="card-body">hora_de_salida.wav <div style={{ padding: "1rem" }}>
<div className="mb-3"> <div style={{ marginBottom: "1.5rem" }}>
<div className="d-flex flex-column"> <label htmlFor="tipoParada" style={{ marginBottom: 8, display: "block", fontWeight: 500 }}>
<label className="form-check-label"> Tipo de parada:
<input type="radio" value="actual" checked={tipoParada === 'actual'} onChange={handleTipoParadaChange} className="form-check-input me-2" /> </label>
<div
id="tipoParada"
style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}
>
<label style={{ display: "flex", alignItems: "center", gap: 8, cursor: "pointer" }}>
<md-radio
name="tipoParada"
value="actual"
checked={tipoParada === "actual"}
onChange={e => setTipoParada(e.target.value)}
style={{ verticalAlign: "middle" }}
></md-radio>
Parada actual Parada actual
</label> </label>
<label className="form-check-label"> <label style={{ display: "flex", alignItems: "center", gap: 8, cursor: "pointer" }}>
<input type="radio" value="siguiente" checked={tipoParada === 'siguiente'} onChange={handleTipoParadaChange} className="form-check-input me-2" /> <md-radio
name="tipoParada"
value="siguiente"
checked={tipoParada === "siguiente"}
onChange={e => setTipoParada(e.target.value)}
style={{ verticalAlign: "middle" }}
></md-radio>
Parada siguiente Parada siguiente
</label> </label>
<label className="form-check-label"> <label style={{ display: "flex", alignItems: "center", gap: 8, cursor: "pointer" }}>
<input type="radio" value="noUsar" checked={tipoParada === 'noUsar'} onChange={handleTipoParadaChange} className="form-check-input me-2" /> <md-radio
No usar name="tipoParada"
value="noUsar"
checked={tipoParada === "noUsar"}
onChange={e => setTipoParada(e.target.value)}
style={{ verticalAlign: "middle" }}
></md-radio>
No usar (para EMT Madrid)
</label> </label>
</div> </div>
</div> </div>
<div> <div>
<label htmlFor="volume" className="form-label">Volumen:</label> <label htmlFor="volume">Volumen:</label>
<input type="range" id="volume" min="0" max="1" step="0.1" value={volume} onChange={handleVolumeChange} className="form-range" /> <md-slider
id="volume"
min={0}
max={1}
step={0.1}
value={volume}
onInput={e => handleVolumeChange({ target: { value: Number(e.target.value) } })}
style={{ width: "100%" }}
></md-slider>
</div> </div>
</div> </div>
</div> </div>
<div className="d-grid gap-2"> <div style={{ display: "flex", flexDirection: "column", gap: "1rem", marginBottom: "1.5rem" }}>
<button onClick={reproducirAudio} className="btn btn-primary" disabled={!lineaSeleccionada || !paradaSeleccionada}> <md-filled-button
onClick={reproducirAudio}
disabled={!lineaSeleccionada || !paradaSeleccionada}
>
{playing ? "Detener" : "Reproducir"} {playing ? "Detener" : "Reproducir"}
</button> </md-filled-button>
<button onClick={toggleTheme} className="btn btn-secondary"> <md-outlined-button onClick={toggleTheme}>
Cambiar tema a {theme === 'light' ? 'oscuro' : 'claro'} Cambiar tema a {theme === 'light' ? 'oscuro' : 'claro'}
</button> </md-outlined-button>
<button onClick={reproducirColision} className="btn btn-danger">Anti-Colisión</button> <md-filled-button onClick={reproducirColision} style={{ background: "#d32f2f", color: "#fff" }}>
Anti-Colisión
</md-filled-button>
</div> </div>
<div style={{ border: "1px solid #ccc", borderRadius: 12, marginBottom: "1.5rem", boxShadow: "0 2px 4px #0001" }}>
<div className="card mb-4"> <div style={{ padding: "1rem" }}>
<div className="card-body"> <h5>Canceladora</h5>
<h5 className="card-title">Canceladora</h5> <div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
<div className="d-grid gap-2"> <md-filled-button onClick={() => reproducirCanceladora('hora_de_salida')}>
<button onClick={() => reproducirCanceladora('hora_de_salida')} className="btn btn-primary">
Hora de Salida Hora de Salida
</button> </md-filled-button>
<button onClick={() => reproducirCanceladora('atencion_a_su_saldo')} className="btn btn-primary"> <md-filled-button onClick={() => reproducirCanceladora('atencion_a_su_saldo')}>
Atención a su Saldo Atención a su Saldo
</button> </md-filled-button>
<button onClick={() => reproducirCanceladora('saldo_insuficiente')} className="btn btn-primary"> <md-filled-button onClick={() => reproducirCanceladora('saldo_insuficiente')}>
Saldo Insuficiente Saldo Insuficiente
</button> </md-filled-button>
</div> </div>
</div> </div>
</div> </div>