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"
],
"Línea 2": [
"Zafra", "Nuevo Mercado", "Villa de Madrid", "Avenida Italia",
"Estación de Sevilla", "El Punto", "Estación de Ferrocarril", "Juzgados",
"Zafra", "Nuevo Mercado", "Villa de Madrid", "Avenida Italia Estación de Sevilla",
"El Punto", "Estación de Ferrocarril", "Juzgados",
"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)",
"Monumento al Fútbol", "Relaciones Laborales (Universidad)",
"Vista Alegre-Universidad", "Centro Comercial Holea", "Plaza las Amapolas",
@@ -31,7 +31,7 @@ datos = {
"Bda. Navidad", "Molino de la Vega", "Paseo de las Palmeras",
"Julio Caro Baroja (Aqualon)"
],
"Línea 131": ["Puente de Vallecas"]
"Línea 141": ["Puente de Vallecas"]
}
@app.route("/")

89
package-lock.json generated
View File

@@ -8,11 +8,14 @@
"name": "mi-app-paradas-react",
"version": "0.1.0",
"dependencies": {
"@fontsource/roboto": "^5.2.6",
"@material/web": "^2.3.0",
"bootstrap": "^5.3.3",
"cra-template": "1.2.0",
"cra-template": "^1.2.0",
"flask": "^0.2.10",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-scripts": "5.0.1",
"react-scripts": "^5.0.1",
"web-vitals": "^4.2.4"
}
},
@@ -2305,6 +2308,15 @@
"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": {
"version": "0.13.0",
"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",
"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": {
"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",
@@ -4804,6 +4844,7 @@
"url": "https://opencollective.com/bootstrap"
}
],
"license": "MIT",
"peerDependencies": {
"@popperjs/core": "^2.11.8"
}
@@ -5433,6 +5474,7 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/cra-template/-/cra-template-1.2.0.tgz",
"integrity": "sha512-06WBUmTq79NvqU91Y9OPCXv/ENy/UkUmQS0lBrOYCl/4f4l67idnGbBARDGLopCHfff6pf6UftcFRWHg+CVfRw==",
"license": "MIT",
"engines": {
"node": ">=14"
}
@@ -7459,6 +7501,12 @@
"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": {
"version": "3.2.0",
"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",
"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": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
@@ -12595,6 +12674,7 @@
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
@@ -12723,6 +12803,7 @@
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
"integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
"license": "MIT",
"dependencies": {
"scheduler": "^0.25.0"
},
@@ -12752,6 +12833,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
"integrity": "sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.16.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.3",
@@ -15234,7 +15316,8 @@
"node_modules/web-vitals": {
"version": "4.2.4",
"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": {
"version": "6.1.0",

View File

@@ -3,11 +3,14 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@fontsource/roboto": "^5.2.6",
"@material/web": "^2.3.0",
"bootstrap": "^5.3.3",
"cra-template": "1.2.0",
"cra-template": "^1.2.0",
"flask": "^0.2.10",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-scripts": "5.0.1",
"react-scripts": "^5.0.1",
"web-vitals": "^4.2.4"
},
"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 'bootstrap/dist/css/bootstrap.min.css';
// Importa la fuente Roboto de Google Fonts
import "@fontsource/roboto"; // npm install @fontsource/roboto
function App() {
const [lineaSeleccionada, setLineaSeleccionada] = useState('');
@@ -11,8 +20,10 @@ function App() {
const audioRef = useRef(null);
const [theme, setTheme] = useState('light');
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": [
"Zafra", "Julio Caro Baroja (Aqualon)", "Avda. Alemania (Esquina Ruiz de Alda)",
"Avda. Alemania (Plaza de Toros)", "Bda. Navidad", "Don Bosco",
@@ -27,10 +38,10 @@ function App() {
"El punto", "Estación de Sevilla", "Nuevo Mercado"
],
"Línea 2": [
"Zafra", "Nuevo Mercado", "Villa de Madrid", "Avenida Italia",
"Estación de Sevilla", "El Punto", "Estación de Ferrocarril", "Juzgados",
"Zafra", "Nuevo Mercado", "Villa de Madrid", "Avenida Italia Estación de Sevilla",
"El Punto", "Estación de Ferrocarril", "Juzgados",
"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)",
"Monumento al Fútbol", "Relaciones Laborales (Universidad)",
"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",
"Julio Caro Baroja (Aqualon)"
],
"Línea 131": ["Puente de Vallecas"],
"Línea T32": ["Profesor Raúl Vázquez"],
"xx": ["tristante"]
};
"Línea 3": [
"Zafra", "Plaza de la Vera Cruz", "San Andrés", "Av. Manuel Siurot", "La Morana",
"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) => {
setLineaSeleccionada(event.target.value);
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) => {
setParadaSeleccionada(event.target.value);
};
const handleTipoParadaChange = (event) => {
setTipoParada(event.target.value);
};
const handleVolumeChange = (event) => {
setVolume(event.target.value);
// Aplica el volumen global a cualquier audio que esté sonando
if (audioRef.current) {
audioRef.current.volume = event.target.value;
}
@@ -71,6 +117,7 @@ function App() {
} else if (lineaSeleccionada && paradaSeleccionada) {
const playAudio = (audioFile) => {
const nuevoAudio = new Audio(audioFile);
nuevoAudio.volume = volume; // Aplica volumen global
nuevoAudio.play().catch(error => {
console.error("Error al reproducir audio:", error);
});
@@ -111,35 +158,114 @@ function App() {
}
};
const reproducirColision = () => {
const audioColision = new Audio('/audio/colision.wav');
audioColision.volume = volume; // Aplica volumen global
audioColision.play().catch(error => {
console.error("Error al reproducir audio:", error);
});
};
const reproducirCanceladora = (audioNombre) => {
const audioCanceladora = new Audio(`/audio/${audioNombre}.mp3`);
audioCanceladora.volume = volume; // Aplica volumen global
audioCanceladora.play().catch(error => {
console.error("Error al reproducir audio:", error);
});
};
useEffect(() => {
const cargarAudios = () => {
const audioActual = new Audio(`/audio/parada_actual.wav`);
const audioSiguiente = new Audio(`/audio/parada_siguiente.wav`);
// Elimina variables no usadas
// const audioActual = new Audio(`/audio/parada_actual.wav`);
// const audioSiguiente = new Audio(`/audio/parada_siguiente.wav`);
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();
}, []);
// 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(() => {
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('light-theme', theme === 'light');
}, [theme]);
@@ -148,91 +274,172 @@ function App() {
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 (
<div className="container mt-4">
<h1 className="mb-4">Aplicación de Megafonía</h1>
<div
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">
<div className="card-body">
<div className="mb-3">
<label htmlFor="linea" className="form-label">Línea:</label>
<select id="linea" value={lineaSeleccionada} onChange={handleLineaChange} className="form-select">
<option value="">Selecciona una línea</option>
{Object.keys(datos).map((linea) => (
<option key={linea} value={linea}>{linea}</option>
{/* Dropdown de ciudad */}
<div style={{ marginBottom: "1.5rem" }}>
<label htmlFor="ciudad">Ciudad:</label>
<md-filled-select
id="ciudad"
value={ciudadSeleccionada}
onInput={e => {
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>
))}
</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>
))}
</select>
</md-filled-select>
</div>
<div>
<label htmlFor="parada" className="form-label">Parada:</label>
<select id="parada" value={paradaSeleccionada} onChange={handleParadaChange} className="form-select">
<option value="">Selecciona una parada</option>
{datos[lineaSeleccionada]?.map((parada) => (
<option key={parada} value={parada}>{parada}</option>
<label htmlFor="parada">Parada:</label>
<md-filled-select
key={paradasKey}
id="parada"
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 className="card mb-4">
<div className="card-body">hora_de_salida.wav
<div className="mb-3">
<div className="d-flex flex-column">
<label className="form-check-label">
<input type="radio" value="actual" checked={tipoParada === 'actual'} onChange={handleTipoParadaChange} className="form-check-input me-2" />
<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="tipoParada" style={{ marginBottom: 8, display: "block", fontWeight: 500 }}>
Tipo de parada:
</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
</label>
<label className="form-check-label">
<input type="radio" value="siguiente" checked={tipoParada === 'siguiente'} onChange={handleTipoParadaChange} className="form-check-input me-2" />
<label style={{ display: "flex", alignItems: "center", gap: 8, cursor: "pointer" }}>
<md-radio
name="tipoParada"
value="siguiente"
checked={tipoParada === "siguiente"}
onChange={e => setTipoParada(e.target.value)}
style={{ verticalAlign: "middle" }}
></md-radio>
Parada siguiente
</label>
<label className="form-check-label">
<input type="radio" value="noUsar" checked={tipoParada === 'noUsar'} onChange={handleTipoParadaChange} className="form-check-input me-2" />
No usar
<label style={{ display: "flex", alignItems: "center", gap: 8, cursor: "pointer" }}>
<md-radio
name="tipoParada"
value="noUsar"
checked={tipoParada === "noUsar"}
onChange={e => setTipoParada(e.target.value)}
style={{ verticalAlign: "middle" }}
></md-radio>
No usar (para EMT Madrid)
</label>
</div>
</div>
<div>
<label htmlFor="volume" className="form-label">Volumen:</label>
<input type="range" id="volume" min="0" max="1" step="0.1" value={volume} onChange={handleVolumeChange} className="form-range" />
<label htmlFor="volume">Volumen:</label>
<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 className="d-grid gap-2">
<button onClick={reproducirAudio} className="btn btn-primary" disabled={!lineaSeleccionada || !paradaSeleccionada}>
<div style={{ display: "flex", flexDirection: "column", gap: "1rem", marginBottom: "1.5rem" }}>
<md-filled-button
onClick={reproducirAudio}
disabled={!lineaSeleccionada || !paradaSeleccionada}
>
{playing ? "Detener" : "Reproducir"}
</button>
<button onClick={toggleTheme} className="btn btn-secondary">
</md-filled-button>
<md-outlined-button onClick={toggleTheme}>
Cambiar tema a {theme === 'light' ? 'oscuro' : 'claro'}
</button>
<button onClick={reproducirColision} className="btn btn-danger">Anti-Colisión</button>
</md-outlined-button>
<md-filled-button onClick={reproducirColision} style={{ background: "#d32f2f", color: "#fff" }}>
Anti-Colisión
</md-filled-button>
</div>
<div className="card mb-4">
<div className="card-body">
<h5 className="card-title">Canceladora</h5>
<div className="d-grid gap-2">
<button onClick={() => reproducirCanceladora('hora_de_salida')} className="btn btn-primary">
<div style={{ border: "1px solid #ccc", borderRadius: 12, marginBottom: "1.5rem", boxShadow: "0 2px 4px #0001" }}>
<div style={{ padding: "1rem" }}>
<h5>Canceladora</h5>
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
<md-filled-button onClick={() => reproducirCanceladora('hora_de_salida')}>
Hora de Salida
</button>
<button onClick={() => reproducirCanceladora('atencion_a_su_saldo')} className="btn btn-primary">
</md-filled-button>
<md-filled-button onClick={() => reproducirCanceladora('atencion_a_su_saldo')}>
Atención a su Saldo
</button>
<button onClick={() => reproducirCanceladora('saldo_insuficiente')} className="btn btn-primary">
</md-filled-button>
<md-filled-button onClick={() => reproducirCanceladora('saldo_insuficiente')}>
Saldo Insuficiente
</button>
</md-filled-button>
</div>
</div>
</div>