558 lines
20 KiB
HTML
558 lines
20 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="es">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>ADIF – Limitaciones Temporales de Velocidad</title>
|
||
|
||
<!-- Leaflet -->
|
||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||
|
||
<!-- Leaflet MarkerCluster -->
|
||
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css" />
|
||
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css" />
|
||
<script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"></script>
|
||
|
||
<style>
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
|
||
body {
|
||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100vh;
|
||
background: #1a1a2e;
|
||
color: #e0e0e0;
|
||
}
|
||
|
||
header {
|
||
background: #16213e;
|
||
padding: 10px 18px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
border-bottom: 2px solid #0f3460;
|
||
flex-shrink: 0;
|
||
z-index: 1000;
|
||
}
|
||
header h1 {
|
||
font-size: 1.1rem;
|
||
font-weight: 600;
|
||
color: #e94560;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
header .subtitle { font-size: 0.78rem; color: #888; }
|
||
|
||
#counter-badge {
|
||
margin-left: auto;
|
||
background: #0f3460;
|
||
padding: 4px 12px;
|
||
border-radius: 20px;
|
||
font-size: 0.82rem;
|
||
color: #a8d8ea;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.main-layout { display: flex; flex: 1; overflow: hidden; }
|
||
|
||
/* ── Sidebar ── */
|
||
#sidebar {
|
||
width: 300px;
|
||
min-width: 260px;
|
||
background: #16213e;
|
||
border-right: 1px solid #0f3460;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
z-index: 500;
|
||
}
|
||
|
||
.panel-section {
|
||
padding: 11px 14px;
|
||
border-bottom: 1px solid #0f3460;
|
||
flex-shrink: 0;
|
||
}
|
||
.panel-section h2 {
|
||
font-size: 0.7rem;
|
||
text-transform: uppercase;
|
||
letter-spacing: 1px;
|
||
color: #555;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
/* Layer toggle buttons */
|
||
.layer-btns { display: flex; gap: 6px; flex-wrap: wrap; }
|
||
.layer-btn {
|
||
padding: 5px 10px;
|
||
border-radius: 6px;
|
||
font-size: 0.75rem;
|
||
cursor: pointer;
|
||
border: 1.5px solid #1a4a80;
|
||
background: #0f3460;
|
||
color: #a8d8ea;
|
||
transition: background 0.15s, border-color 0.15s;
|
||
font-weight: 500;
|
||
}
|
||
.layer-btn.active { background: #e94560; border-color: #e94560; color: #fff; }
|
||
.layer-btn:hover:not(.active) { border-color: #a8d8ea; }
|
||
|
||
/* Overlay toggle */
|
||
.overlay-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-top: 6px;
|
||
}
|
||
.toggle-switch {
|
||
position: relative; width: 38px; height: 20px; cursor: pointer;
|
||
}
|
||
.toggle-switch input { opacity: 0; width: 0; height: 0; }
|
||
.toggle-slider {
|
||
position: absolute; inset: 0;
|
||
background: #0f3460; border-radius: 20px;
|
||
transition: background 0.2s;
|
||
}
|
||
.toggle-slider:before {
|
||
content: ''; position: absolute;
|
||
width: 14px; height: 14px; border-radius: 50%;
|
||
background: #888; left: 3px; top: 3px;
|
||
transition: transform 0.2s, background 0.2s;
|
||
}
|
||
.toggle-switch input:checked + .toggle-slider { background: #0f3460; }
|
||
.toggle-switch input:checked + .toggle-slider:before {
|
||
transform: translateX(18px); background: #e94560;
|
||
}
|
||
.overlay-label { font-size: 0.82rem; color: #ccc; }
|
||
.overlay-sublabel { font-size: 0.7rem; color: #666; }
|
||
|
||
/* Search */
|
||
#search-input {
|
||
width: 100%;
|
||
padding: 7px 10px;
|
||
background: #0f3460;
|
||
border: 1px solid #1a4a80;
|
||
border-radius: 6px;
|
||
color: #e0e0e0;
|
||
font-size: 0.85rem;
|
||
outline: none;
|
||
}
|
||
#search-input:focus { border-color: #e94560; }
|
||
#search-input::placeholder { color: #555; }
|
||
|
||
/* Speed chips */
|
||
.speed-filters { display: flex; flex-wrap: wrap; gap: 6px; }
|
||
.speed-chip {
|
||
padding: 4px 9px;
|
||
border-radius: 12px;
|
||
font-size: 0.73rem;
|
||
cursor: pointer;
|
||
border: 2px solid transparent;
|
||
font-weight: 700;
|
||
color: #fff;
|
||
user-select: none;
|
||
transition: opacity 0.2s, border-color 0.2s;
|
||
}
|
||
.speed-chip.inactive { opacity: 0.3; }
|
||
.speed-chip:hover { border-color: rgba(255,255,255,0.45); }
|
||
|
||
/* Legend */
|
||
.legend-grid {
|
||
display: grid;
|
||
grid-template-columns: 13px 1fr;
|
||
gap: 5px 8px;
|
||
align-items: center;
|
||
}
|
||
.legend-dot {
|
||
width: 13px; height: 13px; border-radius: 50%;
|
||
border: 1.5px solid rgba(255,255,255,0.25);
|
||
}
|
||
.legend-label { font-size: 0.78rem; color: #aaa; }
|
||
|
||
/* Results list */
|
||
#results-list { flex: 1; overflow-y: auto; padding: 6px 0; }
|
||
#results-list::-webkit-scrollbar { width: 5px; }
|
||
#results-list::-webkit-scrollbar-thumb { background: #0f3460; border-radius: 3px; }
|
||
|
||
.result-item {
|
||
padding: 8px 14px;
|
||
border-bottom: 1px solid #0d1b38;
|
||
cursor: pointer;
|
||
transition: background 0.12s;
|
||
}
|
||
.result-item:hover { background: #0f3460; }
|
||
.ri-line {
|
||
font-size: 0.68rem; color: #777;
|
||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||
margin-bottom: 2px;
|
||
}
|
||
.ri-station {
|
||
font-size: 0.83rem; font-weight: 500; color: #ddd;
|
||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||
}
|
||
.ri-meta { margin-top: 4px; display: flex; gap: 6px; align-items: center; }
|
||
.ri-speed-badge {
|
||
font-size: 0.71rem; padding: 1px 7px;
|
||
border-radius: 9px; color: #fff; font-weight: 700; flex-shrink: 0;
|
||
}
|
||
.ri-reason {
|
||
font-size: 0.69rem; color: #666;
|
||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||
}
|
||
|
||
/* Map */
|
||
#map { flex: 1; }
|
||
|
||
/* Leaflet layer control ajuste dark */
|
||
.leaflet-control-layers {
|
||
background: #16213e !important;
|
||
color: #ccc !important;
|
||
border: 1px solid #0f3460 !important;
|
||
}
|
||
.leaflet-control-layers label { color: #ccc !important; }
|
||
.leaflet-control-layers-separator { border-top-color: #0f3460 !important; }
|
||
|
||
/* Popup */
|
||
.ltv-popup { min-width: 240px; font-family: 'Segoe UI', system-ui, sans-serif; }
|
||
.ltv-popup .pop-header {
|
||
background: #16213e; color: #e94560;
|
||
padding: 7px 10px; font-weight: 700; font-size: 0.82rem;
|
||
border-radius: 3px 3px 0 0;
|
||
margin: -5px -12px 8px -12px;
|
||
}
|
||
.ltv-popup table { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
|
||
.ltv-popup td:first-child { color: #888; padding: 3px 8px 3px 0; white-space: nowrap; vertical-align: top; }
|
||
.ltv-popup td:last-child { color: #111; font-weight: 500; }
|
||
.ltv-popup .speed-big { font-size: 1.5rem; font-weight: 800; line-height: 1; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header>
|
||
<div>
|
||
<h1>🚆 ADIF – Limitaciones Temporales de Velocidad</h1>
|
||
<div class="subtitle">Red ferroviaria española · Datos LTV activos</div>
|
||
</div>
|
||
<div id="counter-badge">Cargando…</div>
|
||
</header>
|
||
|
||
<div class="main-layout">
|
||
<div id="sidebar">
|
||
|
||
<!-- Mapa base -->
|
||
<div class="panel-section">
|
||
<h2>Mapa base</h2>
|
||
<div class="layer-btns">
|
||
<button class="layer-btn active" data-base="osm">OSM Estándar</button>
|
||
<button class="layer-btn" data-base="topo">OSM Topo</button>
|
||
<button class="layer-btn" data-base="dark">Dark</button>
|
||
<button class="layer-btn" data-base="satellite">Satélite</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Capa ferroviaria -->
|
||
<div class="panel-section">
|
||
<h2>Capa ferroviaria – OpenRailwayMap</h2>
|
||
<div class="overlay-row">
|
||
<label class="toggle-switch">
|
||
<input type="checkbox" id="toggle-railway" checked />
|
||
<span class="toggle-slider"></span>
|
||
</label>
|
||
<div>
|
||
<div class="overlay-label">Infraestructura ferroviaria</div>
|
||
<div class="overlay-sublabel">Líneas, estaciones y señales</div>
|
||
</div>
|
||
</div>
|
||
<div style="margin-top:8px;" class="layer-btns" id="orm-style-btns">
|
||
<button class="layer-btn active" data-orm="standard">Estándar</button>
|
||
<button class="layer-btn" data-orm="maxspeed">Velocidades</button>
|
||
<button class="layer-btn" data-orm="signals">Señales</button>
|
||
<button class="layer-btn" data-orm="electrification">Electrif.</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Búsqueda -->
|
||
<div class="panel-section">
|
||
<h2>Buscar restricción</h2>
|
||
<input id="search-input" type="text" placeholder="Línea, estación, motivo…" />
|
||
</div>
|
||
|
||
<!-- Filtro velocidad -->
|
||
<div class="panel-section">
|
||
<h2>Filtrar por velocidad</h2>
|
||
<div class="speed-filters" id="speed-filters"></div>
|
||
</div>
|
||
|
||
<!-- Leyenda -->
|
||
<div class="panel-section">
|
||
<h2>Leyenda LTV</h2>
|
||
<div class="legend-grid" id="legend-grid"></div>
|
||
</div>
|
||
|
||
<div id="results-list"></div>
|
||
</div>
|
||
|
||
<div id="map"></div>
|
||
</div>
|
||
|
||
<script>
|
||
// ── Rangos de velocidad ────────────────────────────────────────────────────
|
||
const SPEED_RANGES = [
|
||
{ max: 30, color: '#d63031', label: '≤ 30 km/h' },
|
||
{ max: 60, color: '#e17055', label: '31 – 60 km/h' },
|
||
{ max: 100, color: '#fdcb6e', label: '61 – 100 km/h' },
|
||
{ max: 160, color: '#00b894', label: '101 – 160 km/h' },
|
||
{ max: Infinity, color: '#74b9ff', label: '> 160 km/h' },
|
||
];
|
||
|
||
function speedColor(s) {
|
||
return SPEED_RANGES.find(r => s <= r.max)?.color ?? '#74b9ff';
|
||
}
|
||
function rangeLabel(s) {
|
||
return SPEED_RANGES.find(r => s <= r.max)?.label ?? '';
|
||
}
|
||
|
||
function makeIcon(color) {
|
||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="22" height="30" viewBox="0 0 22 30">
|
||
<path d="M11 0C5 0 0 5 0 11c0 8.3 11 19 11 19S22 19.3 22 11C22 5 17 0 11 0z"
|
||
fill="${color}" stroke="rgba(0,0,0,0.5)" stroke-width="1.2"/>
|
||
<circle cx="11" cy="11" r="4.5" fill="rgba(255,255,255,0.9)"/>
|
||
</svg>`;
|
||
return L.divIcon({
|
||
html: svg, className: '',
|
||
iconSize: [22, 30], iconAnchor: [11, 30], popupAnchor: [0, -30],
|
||
});
|
||
}
|
||
|
||
// ── Capas base ─────────────────────────────────────────────────────────────
|
||
const baseLayers = {
|
||
osm: L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||
maxZoom: 19,
|
||
}),
|
||
topo: L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', {
|
||
attribution: '© <a href="https://opentopomap.org">OpenTopoMap</a>',
|
||
maxZoom: 17,
|
||
}),
|
||
dark: L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
||
attribution: '© <a href="https://carto.com/">CARTO</a>',
|
||
subdomains: 'abcd', maxZoom: 19,
|
||
}),
|
||
satellite: L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
||
attribution: '© <a href="https://www.esri.com/">Esri</a>',
|
||
maxZoom: 19,
|
||
}),
|
||
};
|
||
|
||
// ── Capas OpenRailwayMap ───────────────────────────────────────────────────
|
||
const ORM_ATTR = 'Mapa: © <a href="https://www.openstreetmap.org/copyright">OSM</a> | '
|
||
+ 'Ferroviario: © <a href="https://www.openrailwaymap.org/">OpenRailwayMap</a>';
|
||
|
||
const ormLayers = {
|
||
standard: L.tileLayer('https://{s}.tiles.openrailwaymap.org/standard/{z}/{x}/{y}.png', { attribution: ORM_ATTR, maxZoom: 19, opacity: 0.85 }),
|
||
maxspeed: L.tileLayer('https://{s}.tiles.openrailwaymap.org/maxspeed/{z}/{x}/{y}.png', { attribution: ORM_ATTR, maxZoom: 19, opacity: 0.85 }),
|
||
signals: L.tileLayer('https://{s}.tiles.openrailwaymap.org/signals/{z}/{x}/{y}.png', { attribution: ORM_ATTR, maxZoom: 19, opacity: 0.85 }),
|
||
electrification:L.tileLayer('https://{s}.tiles.openrailwaymap.org/electrification/{z}/{x}/{y}.png',{ attribution: ORM_ATTR, maxZoom: 19, opacity: 0.85 }),
|
||
};
|
||
|
||
// ── Inicializar mapa ───────────────────────────────────────────────────────
|
||
const map = L.map('map', { zoomControl: true }).setView([40.4, -3.7], 6);
|
||
|
||
let currentBase = baseLayers.osm;
|
||
let currentOrm = ormLayers.standard;
|
||
let ormVisible = true;
|
||
|
||
currentBase.addTo(map);
|
||
currentOrm.addTo(map);
|
||
|
||
// ── Cluster ────────────────────────────────────────────────────────────────
|
||
const clusterGroup = L.markerClusterGroup({
|
||
maxClusterRadius: 40,
|
||
iconCreateFunction: cluster => {
|
||
const count = cluster.getChildCount();
|
||
const size = count < 10 ? 30 : count < 50 ? 36 : 44;
|
||
return L.divIcon({
|
||
html: `<div style="
|
||
background: rgba(233,69,96,0.85);
|
||
border: 2px solid rgba(255,255,255,0.6);
|
||
border-radius: 50%;
|
||
width:${size}px; height:${size}px;
|
||
display:flex; align-items:center; justify-content:center;
|
||
color:#fff; font-weight:700; font-size:${size < 36 ? 11 : 13}px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
|
||
">${count}</div>`,
|
||
className: '', iconSize: [size, size],
|
||
});
|
||
},
|
||
});
|
||
map.addLayer(clusterGroup);
|
||
|
||
// ── Estado ─────────────────────────────────────────────────────────────────
|
||
let allItems = [];
|
||
let activeRanges = new Set(SPEED_RANGES.map(r => r.label));
|
||
let searchText = '';
|
||
let markerMap = new Map();
|
||
|
||
// ── Construir UI estática ──────────────────────────────────────────────────
|
||
const legendGrid = document.getElementById('legend-grid');
|
||
const filtersDiv = document.getElementById('speed-filters');
|
||
|
||
SPEED_RANGES.forEach(r => {
|
||
legendGrid.innerHTML +=
|
||
`<div class="legend-dot" style="background:${r.color}"></div>
|
||
<div class="legend-label">${r.label}</div>`;
|
||
|
||
const chip = document.createElement('div');
|
||
chip.className = 'speed-chip';
|
||
chip.style.background = r.color;
|
||
chip.textContent = r.label;
|
||
chip.dataset.range = r.label;
|
||
chip.addEventListener('click', () => {
|
||
if (activeRanges.has(r.label)) {
|
||
activeRanges.delete(r.label);
|
||
chip.classList.add('inactive');
|
||
} else {
|
||
activeRanges.add(r.label);
|
||
chip.classList.remove('inactive');
|
||
}
|
||
applyFilters();
|
||
});
|
||
filtersDiv.appendChild(chip);
|
||
});
|
||
|
||
// ── Popup ──────────────────────────────────────────────────────────────────
|
||
function buildPopup(item) {
|
||
const color = speedColor(item.speed);
|
||
return `<div class="ltv-popup">
|
||
<div class="pop-header">${item.trayectoEstacion || '—'}</div>
|
||
<table>
|
||
<tr><td>Línea</td><td>${item.line}</td></tr>
|
||
<tr><td>PK inicio</td><td>${item.pk}</td></tr>
|
||
<tr><td>PK fin</td><td>${item.pkEnd}</td></tr>
|
||
<tr><td>Vía</td><td>${item.track}</td></tr>
|
||
<tr><td>Velocidad</td><td>
|
||
<span class="speed-big" style="color:${color}">${item.speed}</span>
|
||
<small style="color:#888"> km/h</small>
|
||
</td></tr>
|
||
<tr><td>Motivo</td><td>${item.reason}</td></tr>
|
||
<tr><td>Código LTV</td><td style="font-family:monospace">${item.codigoLtv}</td></tr>
|
||
${item.horarioLtv ? `<tr><td>Horario</td><td>${item.horarioLtv}</td></tr>` : ''}
|
||
</table>
|
||
</div>`;
|
||
}
|
||
|
||
// ── Filtros ────────────────────────────────────────────────────────────────
|
||
function matchesFilters(item) {
|
||
if (!activeRanges.has(rangeLabel(item.speed))) return false;
|
||
if (searchText) {
|
||
const q = searchText.toLowerCase();
|
||
return (
|
||
item.line?.toLowerCase().includes(q) ||
|
||
item.trayectoEstacion?.toLowerCase().includes(q) ||
|
||
item.reason?.toLowerCase().includes(q) ||
|
||
item.codigoLtv?.toLowerCase().includes(q)
|
||
);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
function applyFilters() {
|
||
clusterGroup.clearLayers();
|
||
const list = document.getElementById('results-list');
|
||
list.innerHTML = '';
|
||
let count = 0;
|
||
|
||
allItems.forEach((item, idx) => {
|
||
if (!matchesFilters(item)) return;
|
||
count++;
|
||
const marker = markerMap.get(idx);
|
||
if (marker) clusterGroup.addLayer(marker);
|
||
|
||
const color = speedColor(item.speed);
|
||
const div = document.createElement('div');
|
||
div.className = 'result-item';
|
||
div.innerHTML = `
|
||
<div class="ri-line">${item.line}</div>
|
||
<div class="ri-station">${item.trayectoEstacion || 'Sin estación'}</div>
|
||
<div class="ri-meta">
|
||
<span class="ri-speed-badge" style="background:${color}">${item.speed} km/h</span>
|
||
<span class="ri-reason" title="${item.reason}">${item.reason}</span>
|
||
</div>`;
|
||
div.addEventListener('click', () => {
|
||
const m = markerMap.get(idx);
|
||
if (!m) return;
|
||
map.setView([item.lat, item.lng], 14);
|
||
clusterGroup.zoomToShowLayer(m, () => m.openPopup());
|
||
});
|
||
list.appendChild(div);
|
||
});
|
||
|
||
document.getElementById('counter-badge').textContent =
|
||
`${count} restricción${count !== 1 ? 'es' : ''}`;
|
||
}
|
||
|
||
// ── Cargar JSON ────────────────────────────────────────────────────────────
|
||
fetch('adif.json')
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
allItems = data.items;
|
||
allItems.forEach((item, idx) => {
|
||
if (!item.lat || !item.lng) return;
|
||
const marker = L.marker([item.lat, item.lng], { icon: makeIcon(speedColor(item.speed)) })
|
||
.bindPopup(buildPopup(item), { maxWidth: 320 });
|
||
markerMap.set(idx, marker);
|
||
});
|
||
applyFilters();
|
||
})
|
||
.catch(() => {
|
||
document.getElementById('counter-badge').textContent = 'Error al cargar datos';
|
||
});
|
||
|
||
// ── Controles de capa base ─────────────────────────────────────────────────
|
||
document.querySelectorAll('[data-base]').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const key = btn.dataset.base;
|
||
if (!baseLayers[key] || currentBase === baseLayers[key]) return;
|
||
map.removeLayer(currentBase);
|
||
currentBase = baseLayers[key];
|
||
currentBase.addTo(map);
|
||
currentBase.bringToBack();
|
||
// asegurar que ORM queda encima
|
||
if (ormVisible) currentOrm.bringToFront();
|
||
document.querySelectorAll('[data-base]').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
});
|
||
});
|
||
|
||
// ── Controles ORM style ────────────────────────────────────────────────────
|
||
document.querySelectorAll('[data-orm]').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const key = btn.dataset.orm;
|
||
if (!ormLayers[key] || currentOrm === ormLayers[key]) return;
|
||
map.removeLayer(currentOrm);
|
||
currentOrm = ormLayers[key];
|
||
if (ormVisible) currentOrm.addTo(map);
|
||
document.querySelectorAll('[data-orm]').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
});
|
||
});
|
||
|
||
// ── Toggle capa ferroviaria ────────────────────────────────────────────────
|
||
document.getElementById('toggle-railway').addEventListener('change', e => {
|
||
ormVisible = e.target.checked;
|
||
if (ormVisible) {
|
||
currentOrm.addTo(map);
|
||
} else {
|
||
map.removeLayer(currentOrm);
|
||
}
|
||
});
|
||
|
||
// ── Búsqueda ───────────────────────────────────────────────────────────────
|
||
document.getElementById('search-input').addEventListener('input', e => {
|
||
searchText = e.target.value.trim();
|
||
applyFilters();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|