Files
PilotEdge/public/pages/bridges.html
Daniel Kovalevich 93efb907ff Reorganize frontend into public/ with pages/ and js/ subdirectories
- public/index.html — landing page at root
- public/pages/ — all feature pages (regulations, loadboard, etc.)
- public/js/ — api.js, nav.js, mock data files
- All links updated to absolute paths (/pages/, /js/)
- Express static path updated to serve from public/
- Seed script path updated for new mock data location
- README updated with new project structure and setup guide
- Added .env.example template

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-30 15:52:56 -04:00

389 lines
18 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bridge & Overpass Clearance Database | PilotEdge</title>
<script src="https://cdn.tailwindcss.com"></script>
<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>
<style>
#map { height: 450px; width: 100%; border-radius: 0.75rem; }
.leaflet-popup-content { margin: 8px 12px; }
.leaflet-popup-content-wrapper { border-radius: 12px; }
</style>
</head>
<body class="bg-slate-50 min-h-screen flex flex-col">
<div id="main-nav"></div>
<div id="poc-banner"></div>
<!-- Page Header -->
<section class="bg-slate-900 text-white pt-24 pb-12 px-4">
<div class="max-w-7xl mx-auto">
<h1 class="text-3xl md:text-4xl font-bold mb-3">Bridge &amp; Overpass Clearance Database</h1>
<p class="text-lg text-gray-400 max-w-3xl">Check height and width restrictions for bridges, tunnels, and overpasses along major trucking corridors. Plan your oversize load route with confidence.</p>
</div>
</section>
<!-- Check Your Load -->
<section class="max-w-7xl mx-auto px-4 -mt-6 relative z-10 w-full">
<div class="bg-white rounded-2xl shadow-lg p-6 md:p-8">
<h2 class="text-xl font-bold text-slate-900 mb-1 flex items-center gap-2">
<span class="text-2xl">⚠️</span> Check Your Load
</h2>
<p class="text-sm text-slate-500 mb-5">Enter your load dimensions to identify bridges that may restrict your route.</p>
<div class="grid sm:grid-cols-3 gap-4 items-end">
<div>
<label for="load-height" class="block text-sm font-medium text-slate-700 mb-1">Load Height (feet)</label>
<input id="load-height" type="number" step="0.1" min="0" placeholder="e.g. 14.5" class="w-full border border-slate-300 rounded-lg px-4 py-2.5 text-sm focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none" />
</div>
<div>
<label for="load-width" class="block text-sm font-medium text-slate-700 mb-1">Load Width (feet)</label>
<input id="load-width" type="number" step="0.1" min="0" placeholder="e.g. 12.0" class="w-full border border-slate-300 rounded-lg px-4 py-2.5 text-sm focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none" />
</div>
<div>
<button id="check-load-btn" class="w-full bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold px-6 py-2.5 rounded-lg transition-colors shadow-md hover:shadow-lg text-sm">
Check Clearances
</button>
</div>
</div>
<div id="load-warnings" class="mt-4 hidden"></div>
</div>
</section>
<!-- Filters -->
<section class="max-w-7xl mx-auto px-4 mt-8 w-full">
<div class="bg-white rounded-2xl shadow-lg p-6">
<h2 class="text-lg font-bold text-slate-900 mb-4 flex items-center gap-2">
<svg class="w-5 h-5 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"/></svg>
Filter &amp; Search
</h2>
<div class="grid sm:grid-cols-3 gap-4">
<div>
<label for="filter-state" class="block text-sm font-medium text-slate-700 mb-1">State</label>
<select id="filter-state" class="w-full border border-slate-300 rounded-lg px-4 py-2.5 text-sm focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none bg-white">
<option value="">All States</option>
</select>
</div>
<div>
<label for="filter-route" class="block text-sm font-medium text-slate-700 mb-1">Route Name</label>
<input id="filter-route" type="text" placeholder="e.g. I-95, US-20" class="w-full border border-slate-300 rounded-lg px-4 py-2.5 text-sm focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none" />
</div>
<div>
<label for="filter-max-height" class="block text-sm font-medium text-slate-700 mb-1">Max Clearance Height (ft)</label>
<input id="filter-max-height" type="number" step="0.1" min="0" placeholder="Show bridges ≤ this height" class="w-full border border-slate-300 rounded-lg px-4 py-2.5 text-sm focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none" />
</div>
</div>
<div class="mt-3 text-right">
<button id="clear-filters-btn" class="text-sm text-amber-600 hover:text-amber-800 font-medium transition-colors">Clear Filters</button>
</div>
</div>
</section>
<!-- Map Section -->
<section class="max-w-7xl mx-auto px-4 mt-8 w-full">
<div class="bg-white rounded-2xl shadow-lg p-6">
<h2 class="text-lg font-bold text-slate-900 mb-4 flex items-center gap-2">
<svg class="w-5 h-5 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"/></svg>
Bridge &amp; Overpass Map
</h2>
<p class="text-sm text-slate-500 mb-4">Red markers indicate restricted clearances. Click a marker for details.</p>
<div id="map"></div>
</div>
</section>
<!-- Bridge List -->
<section class="max-w-7xl mx-auto px-4 mt-8 pb-8 w-full">
<div class="bg-white rounded-2xl shadow-lg p-6">
<h2 class="text-lg font-bold text-slate-900 mb-1 flex items-center gap-2">
<svg class="w-5 h-5 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"/></svg>
Clearance Database
</h2>
<p class="text-sm text-slate-500 mb-4" id="results-count"></p>
<div id="bridge-list" class="space-y-4"></div>
<div id="no-results" class="hidden text-center py-12 text-slate-400">
<svg class="w-12 h-12 mx-auto mb-3 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
<p class="font-medium">No bridges match your filters.</p>
<p class="text-sm mt-1">Try adjusting your search criteria.</p>
</div>
</div>
</section>
<div id="main-footer"></div>
<script src="/js/api.js"></script>
<script src="/js/nav.js"></script>
<script>
renderNav('bridges');
renderBanner();
renderFooter();
(async () => {
const MOCK_BRIDGE_CLEARANCES = await PilotEdge.getBridges();
// --- Helpers ---
// Parse a clearance string like '13\'6"' or '14\'0"' into decimal feet.
// Returns NaN for unparseable values (e.g. "Varies", "No restriction").
function parseClearanceFeet(str) {
if (!str) return NaN;
// Handle range like "12'6\" — 13'6\" (varies)" — use the lower value
const rangeParts = str.split('—').map(s => s.trim());
const target = rangeParts[0];
// Match pattern like 13'6" or 15'5"
const match = target.match(/(\d+)['']\s*(\d+)?/);
if (match) {
const feet = parseInt(match[1], 10);
const inches = match[2] ? parseInt(match[2], 10) : 0;
return feet + inches / 12;
}
// Try plain number
const plain = parseFloat(target);
return isNaN(plain) ? NaN : plain;
}
function parseWidthFeet(str) {
if (!str || str.toLowerCase().includes('no restriction')) return NaN;
const match = str.match(/(\d+)['']\s*(\d+)?/);
if (match) {
const feet = parseInt(match[1], 10);
const inches = match[2] ? parseInt(match[2], 10) : 0;
return feet + inches / 12;
}
return NaN;
}
function heightBadgeClass(heightFt) {
if (isNaN(heightFt)) return 'bg-slate-100 text-slate-600';
if (heightFt < 14) return 'bg-red-100 text-red-700 ring-1 ring-red-300';
if (heightFt <= 15) return 'bg-amber-100 text-amber-700 ring-1 ring-amber-300';
return 'bg-green-100 text-green-700 ring-1 ring-green-300';
}
function heightIcon(heightFt) {
if (isNaN(heightFt)) return '❓';
if (heightFt < 14) return '🔴';
if (heightFt <= 15) return '🟡';
return '🟢';
}
// --- State filter population ---
const states = [...new Set(MOCK_BRIDGE_CLEARANCES.map(b => b.location.state))].sort();
const stateSelect = document.getElementById('filter-state');
states.forEach(s => {
const opt = document.createElement('option');
opt.value = s;
opt.textContent = s;
stateSelect.appendChild(opt);
});
// --- Map setup ---
const map = L.map('map').setView([39.5, -98.5], 4);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap contributors',
maxZoom: 18
}).addTo(map);
const redIcon = L.divIcon({
className: '',
html: `<svg width="28" height="28" viewBox="0 0 28 28"><polygon points="14,2 26,24 2,24" fill="#dc2626" stroke="#991b1b" stroke-width="1.5"/><text x="14" y="19" text-anchor="middle" fill="white" font-size="11" font-weight="bold">!</text></svg>`,
iconSize: [28, 28],
iconAnchor: [14, 24],
popupAnchor: [0, -22]
});
let markers = [];
function addMarkers(bridges) {
markers.forEach(m => map.removeLayer(m));
markers = [];
bridges.forEach(b => {
const heightFt = parseClearanceFeet(b.clearanceHeight);
const marker = L.marker([b.location.lat, b.location.lng], { icon: redIcon }).addTo(map);
marker.bindPopup(`
<div style="min-width:220px;">
<div style="font-weight:700;font-size:14px;margin-bottom:4px;">${b.route} — MM ${b.mileMarker}</div>
<div style="font-size:12px;color:#64748b;margin-bottom:6px;">${b.location.desc}, ${b.location.city}, ${b.location.state}</div>
<div style="display:grid;grid-template-columns:auto 1fr;gap:2px 8px;font-size:12px;">
<span style="font-weight:600;">Height:</span><span>${b.clearanceHeight} ${heightIcon(heightFt)}</span>
<span style="font-weight:600;">Width:</span><span>${b.clearanceWidth}</span>
<span style="font-weight:600;">Weight:</span><span>${b.weightLimit}</span>
<span style="font-weight:600;">Type:</span><span>${b.type}</span>
</div>
${b.notes ? `<div style="font-size:11px;color:#64748b;margin-top:6px;border-top:1px solid #e2e8f0;padding-top:6px;">${b.notes}</div>` : ''}
</div>
`);
markers.push(marker);
});
}
// --- Render bridge cards ---
function renderBridgeList(bridges) {
const container = document.getElementById('bridge-list');
const noResults = document.getElementById('no-results');
const resultsCount = document.getElementById('results-count');
if (bridges.length === 0) {
container.innerHTML = '';
noResults.classList.remove('hidden');
resultsCount.textContent = '0 bridges found';
return;
}
noResults.classList.add('hidden');
resultsCount.textContent = `${bridges.length} bridge${bridges.length !== 1 ? 's' : ''} found`;
container.innerHTML = bridges.map(b => {
const heightFt = parseClearanceFeet(b.clearanceHeight);
const badgeCls = heightBadgeClass(heightFt);
const isWarning = b._warning;
return `
<div class="border ${isWarning ? 'border-red-300 bg-red-50/40' : 'border-slate-200'} rounded-xl p-5 hover:shadow-md transition-shadow" id="card-${b.id}">
<div class="flex flex-col md:flex-row md:items-start md:justify-between gap-4">
<!-- Left -->
<div class="flex-1 min-w-0">
<div class="flex items-center gap-3 flex-wrap">
<h3 class="font-bold text-slate-900 text-base">${b.route}</h3>
<span class="text-xs px-2 py-0.5 rounded-full bg-slate-100 text-slate-600 font-medium">MM ${b.mileMarker}</span>
<span class="text-xs px-2 py-0.5 rounded-full bg-slate-100 text-slate-600 font-medium">${b.type}</span>
${isWarning ? '<span class="text-xs px-2 py-0.5 rounded-full bg-red-100 text-red-700 font-semibold">⚠️ Clearance Issue</span>' : ''}
</div>
<p class="text-sm text-slate-500 mt-1">${b.location.desc}${b.location.city}, ${b.location.state}</p>
${b.notes ? `<p class="text-xs text-slate-400 mt-2 leading-relaxed">${b.notes}</p>` : ''}
</div>
<!-- Right: Badges -->
<div class="flex flex-wrap md:flex-col gap-2 md:items-end flex-shrink-0">
<div class="text-center">
<div class="text-[10px] uppercase tracking-wider font-semibold text-slate-400 mb-0.5">Height</div>
<span class="inline-block text-sm font-bold px-3 py-1 rounded-lg ${badgeCls}">${b.clearanceHeight}</span>
</div>
<div class="text-center">
<div class="text-[10px] uppercase tracking-wider font-semibold text-slate-400 mb-0.5">Width</div>
<span class="inline-block text-xs font-medium px-3 py-1 rounded-lg bg-slate-100 text-slate-600">${b.clearanceWidth}</span>
</div>
<div class="text-center">
<div class="text-[10px] uppercase tracking-wider font-semibold text-slate-400 mb-0.5">Weight</div>
<span class="inline-block text-xs font-medium px-3 py-1 rounded-lg bg-slate-100 text-slate-600">${b.weightLimit}</span>
</div>
</div>
</div>
</div>
`;
}).join('');
}
// --- Filter logic ---
function getFilteredBridges() {
const stateVal = document.getElementById('filter-state').value;
const routeVal = document.getElementById('filter-route').value.trim().toLowerCase();
const maxHeightVal = parseFloat(document.getElementById('filter-max-height').value);
return MOCK_BRIDGE_CLEARANCES.filter(b => {
if (stateVal && b.location.state !== stateVal) return false;
if (routeVal && !b.route.toLowerCase().includes(routeVal)) return false;
if (!isNaN(maxHeightVal)) {
const h = parseClearanceFeet(b.clearanceHeight);
if (!isNaN(h) && h > maxHeightVal) return false;
}
return true;
});
}
function applyFilters() {
const bridges = getFilteredBridges();
addMarkers(bridges);
renderBridgeList(bridges);
}
document.getElementById('filter-state').addEventListener('change', applyFilters);
document.getElementById('filter-route').addEventListener('input', applyFilters);
document.getElementById('filter-max-height').addEventListener('input', applyFilters);
document.getElementById('clear-filters-btn').addEventListener('click', () => {
document.getElementById('filter-state').value = '';
document.getElementById('filter-route').value = '';
document.getElementById('filter-max-height').value = '';
document.getElementById('load-height').value = '';
document.getElementById('load-width').value = '';
document.getElementById('load-warnings').classList.add('hidden');
MOCK_BRIDGE_CLEARANCES.forEach(b => delete b._warning);
applyFilters();
});
// --- Check Your Load ---
document.getElementById('check-load-btn').addEventListener('click', () => {
const loadHeight = parseFloat(document.getElementById('load-height').value);
const loadWidth = parseFloat(document.getElementById('load-width').value);
const warningsDiv = document.getElementById('load-warnings');
// Reset warnings
MOCK_BRIDGE_CLEARANCES.forEach(b => delete b._warning);
if (isNaN(loadHeight) && isNaN(loadWidth)) {
warningsDiv.innerHTML = '<p class="text-sm text-slate-500">Please enter at least one dimension to check.</p>';
warningsDiv.classList.remove('hidden');
applyFilters();
return;
}
const problems = [];
MOCK_BRIDGE_CLEARANCES.forEach(b => {
const reasons = [];
if (!isNaN(loadHeight)) {
const bh = parseClearanceFeet(b.clearanceHeight);
if (!isNaN(bh) && bh <= loadHeight) {
reasons.push(`Height clearance ${b.clearanceHeight} is at or below your load height of ${loadHeight}'`);
}
}
if (!isNaN(loadWidth)) {
const bw = parseWidthFeet(b.clearanceWidth);
if (!isNaN(bw) && bw <= loadWidth) {
reasons.push(`Width clearance ${b.clearanceWidth} is at or below your load width of ${loadWidth}'`);
}
}
if (reasons.length > 0) {
b._warning = true;
problems.push({ bridge: b, reasons });
}
});
if (problems.length === 0) {
warningsDiv.innerHTML = `
<div class="bg-green-50 border border-green-200 rounded-xl p-4">
<p class="text-green-800 font-semibold flex items-center gap-2">
<span class="text-lg">✅</span> No clearance issues found for your load dimensions.
</p>
<p class="text-green-600 text-sm mt-1">All bridges in the database can accommodate your ${!isNaN(loadHeight) ? loadHeight + "' height" : ''}${!isNaN(loadHeight) && !isNaN(loadWidth) ? ' and ' : ''}${!isNaN(loadWidth) ? loadWidth + "' width" : ''}.</p>
</div>
`;
} else {
warningsDiv.innerHTML = `
<div class="bg-red-50 border border-red-200 rounded-xl p-4">
<p class="text-red-800 font-semibold flex items-center gap-2">
<span class="text-lg">🚨</span> ${problems.length} bridge${problems.length !== 1 ? 's' : ''} may restrict your route
</p>
<ul class="mt-3 space-y-2">
${problems.map(p => `
<li class="text-sm bg-white border border-red-100 rounded-lg p-3">
<div class="font-semibold text-red-700">${p.bridge.route} — MM ${p.bridge.mileMarker} (${p.bridge.location.city}, ${p.bridge.location.state})</div>
${p.reasons.map(r => `<div class="text-red-600 mt-0.5">• ${r}</div>`).join('')}
</li>
`).join('')}
</ul>
</div>
`;
}
warningsDiv.classList.remove('hidden');
applyFilters();
});
// --- Initial render ---
applyFilters();
})();
</script>
</body>
</html>