Files
PilotEdge/regulations.html
Daniel Kovalevich f917fb8014 Add Node.js/Express backend with PostgreSQL and wire frontend to API
- Server: Express.js with 13 API route files (auth, regulations, contacts,
  calendar, truck stops, bridges, weigh stations, alerts, load board,
  escort locator, orders, documents, contributions)
- Database: PostgreSQL with Prisma ORM, 15 models covering all modules
- Auth: JWT + bcrypt with role-based access control (driver/carrier/escort/admin)
- Geospatial: Haversine distance filtering on truck stops, bridges, escorts
- Seed script: Imports all existing mock data (51 states, contacts, equipment,
  truck stops, bridges, weigh stations, alerts, seasonal restrictions)
- Frontend: All 10 data-driven pages now fetch from /api instead of mock-data.js
- API client (api.js): Compatibility layer that transforms API responses to
  match existing frontend rendering code, minimizing page-level changes

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

445 lines
21 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>State Regulations Map | 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: 550px; width: 100%; border-radius: 0.75rem; }
.state-detail-row { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; }
.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">State-by-State Regulations Map</h1>
<p class="text-lg text-gray-400 max-w-3xl">Click any state marker to view oversize load permit thresholds, escort requirements, and travel restrictions.</p>
</div>
</section>
<!-- Map Section -->
<section class="max-w-7xl mx-auto px-4 py-8 w-full">
<div class="bg-white rounded-2xl shadow-lg p-4">
<div id="map"></div>
</div>
</section>
<!-- Route Checker -->
<section class="max-w-7xl mx-auto px-4 pb-8">
<div class="bg-white rounded-2xl shadow-lg p-8">
<h2 class="text-2xl font-bold text-slate-900 mb-2">Quick Route Checker</h2>
<p class="text-slate-600 mb-6">Enter your load dimensions and route to see which states require permits and escorts.</p>
<div class="grid md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Load Width</label>
<div class="flex">
<input type="number" id="rc-width-ft" placeholder="ft" min="0" max="30" class="w-1/2 border border-slate-300 rounded-l-lg px-3 py-2 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
<input type="number" id="rc-width-in" placeholder="in" min="0" max="11" class="w-1/2 border border-l-0 border-slate-300 rounded-r-lg px-3 py-2 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
</div>
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Load Height</label>
<div class="flex">
<input type="number" id="rc-height-ft" placeholder="ft" min="0" max="25" class="w-1/2 border border-slate-300 rounded-l-lg px-3 py-2 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
<input type="number" id="rc-height-in" placeholder="in" min="0" max="11" class="w-1/2 border border-l-0 border-slate-300 rounded-r-lg px-3 py-2 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
</div>
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Overall Length</label>
<div class="flex">
<input type="number" id="rc-length-ft" placeholder="ft" min="0" max="200" class="w-1/2 border border-slate-300 rounded-l-lg px-3 py-2 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
<input type="number" id="rc-length-in" placeholder="in" min="0" max="11" class="w-1/2 border border-l-0 border-slate-300 rounded-r-lg px-3 py-2 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
</div>
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Gross Weight</label>
<input type="text" id="rc-weight" placeholder="e.g. 120,000 lbs" class="w-full border border-slate-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
</div>
</div>
<div class="grid md:grid-cols-2 gap-4 mb-6">
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Origin State</label>
<select id="rc-origin" class="w-full border border-slate-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
<option value="">Select state...</option>
</select>
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Destination State</label>
<select id="rc-destination" class="w-full border border-slate-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
<option value="">Select state...</option>
</select>
</div>
</div>
<button onclick="checkRoute()" class="bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold px-6 py-3 rounded-lg transition-colors shadow-md">
Check Route Requirements
</button>
<div id="route-results" class="mt-6 hidden">
<!-- Results populated by JS -->
</div>
</div>
</section>
<!-- State Detail Panel (populated on click) -->
<section id="state-detail" class="max-w-7xl mx-auto px-4 pb-8 hidden">
<div class="bg-white rounded-2xl shadow-lg p-8">
<div class="flex items-center justify-between mb-6">
<h2 id="state-detail-name" class="text-2xl font-bold text-slate-900"></h2>
<button onclick="document.getElementById('state-detail').classList.add('hidden')" class="text-slate-400 hover:text-slate-600 text-2xl">&times;</button>
</div>
<div id="state-detail-content">
<!-- Populated by JS -->
</div>
</div>
</section>
<!-- Equipment Requirements Section (Module 12) -->
<section id="equipment" class="max-w-7xl mx-auto px-4 pb-8">
<div class="bg-white rounded-2xl shadow-lg p-8">
<h2 class="text-2xl font-bold text-slate-900 mb-2">State Equipment Requirements</h2>
<p class="text-slate-600 mb-6">Equipment your escort vehicle and truck/trailer must carry — varies by state. Select a state to see requirements.</p>
<div class="grid md:grid-cols-2 gap-4 mb-6">
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Select State</label>
<select id="equip-state" onchange="showEquipment()" class="w-full border border-slate-300 rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
<option value="">Choose a state...</option>
</select>
</div>
<div class="flex items-end">
<p class="text-sm text-slate-500">Detailed equipment data available for 12 major trucking states. More coming soon.</p>
</div>
</div>
<div id="equip-content" class="hidden">
<!-- Populated by JS -->
</div>
<div id="equip-no-data" class="hidden">
<div class="bg-slate-50 rounded-xl p-8 text-center">
<p class="text-slate-500 text-lg mb-2">Equipment data for this state is coming soon.</p>
<p class="text-slate-400 text-sm">We're actively adding detailed equipment requirements for all 50 states.</p>
</div>
</div>
</div>
</section>
<div id="main-footer"></div>
<script src="api.js"></script>
<script src="nav.js"></script>
<script>
renderNav('regulations');
renderBanner();
renderFooter();
(async () => {
const MOCK_STATE_REGULATIONS = await PilotEdge.getRegulations();
const MOCK_STATE_EQUIPMENT = await PilotEdge.getEquipment();
// Initialize map centered on continental US
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);
// Custom marker icon
const stateIcon = L.divIcon({
className: 'custom-marker',
html: '<div style="background:#f59e0b; color:#0f172a; font-weight:800; font-size:11px; width:32px; height:32px; border-radius:50%; display:flex; align-items:center; justify-content:center; border:2px solid #0f172a; box-shadow:0 2px 6px rgba(0,0,0,0.3); cursor:pointer;" class="state-marker-dot"></div>',
iconSize: [32, 32],
iconAnchor: [16, 16]
});
// Add markers for each state
MOCK_STATE_REGULATIONS.forEach(state => {
const marker = L.marker([state.lat, state.lng], {
icon: L.divIcon({
className: 'custom-marker',
html: `<div style="background:#f59e0b; color:#0f172a; font-weight:800; font-size:10px; width:32px; height:32px; border-radius:50%; display:flex; align-items:center; justify-content:center; border:2px solid #0f172a; box-shadow:0 2px 6px rgba(0,0,0,0.3); cursor:pointer;">${state.abbr}</div>`,
iconSize: [32, 32],
iconAnchor: [16, 16]
})
}).addTo(map);
marker.bindPopup(`
<div style="min-width:200px;">
<strong style="font-size:16px;">${state.name}</strong><br>
<span style="color:#666; font-size:12px;">Click below for full details</span><br><br>
<strong>Permit required over:</strong><br>
Width: ${state.permitWidth} | Height: ${state.permitHeight}<br>
<br>
<button onclick="showStateDetail('${state.abbr}')" style="background:#f59e0b; color:#0f172a; font-weight:700; padding:6px 16px; border-radius:6px; border:none; cursor:pointer; width:100%;">
View Full Details
</button>
</div>
`);
});
function showStateDetail(abbr) {
const state = MOCK_STATE_REGULATIONS.find(s => s.abbr === abbr);
if (!state) return;
map.closePopup();
document.getElementById('state-detail-name').textContent = `${state.name} (${state.abbr})`;
document.getElementById('state-detail-content').innerHTML = `
<div class="grid md:grid-cols-2 gap-8">
<!-- Permit Thresholds -->
<div>
<h3 class="text-lg font-bold text-slate-900 mb-4 flex items-center">
<span class="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center text-blue-600 mr-2">📄</span>
Permit Required Over
</h3>
<div class="space-y-3">
<div class="flex justify-between items-center bg-slate-50 px-4 py-2 rounded-lg">
<span class="text-slate-600 font-medium">Width</span>
<span class="font-bold text-slate-900">${state.permitWidth}</span>
</div>
<div class="flex justify-between items-center bg-slate-50 px-4 py-2 rounded-lg">
<span class="text-slate-600 font-medium">Height</span>
<span class="font-bold text-slate-900">${state.permitHeight}</span>
</div>
<div class="flex justify-between items-center bg-slate-50 px-4 py-2 rounded-lg">
<span class="text-slate-600 font-medium">Length</span>
<span class="font-bold text-slate-900">${state.permitLength}</span>
</div>
<div class="flex justify-between items-center bg-slate-50 px-4 py-2 rounded-lg">
<span class="text-slate-600 font-medium">Weight</span>
<span class="font-bold text-slate-900">${state.permitWeight}</span>
</div>
</div>
</div>
<!-- Escort Requirements -->
<div>
<h3 class="text-lg font-bold text-slate-900 mb-4 flex items-center">
<span class="w-8 h-8 bg-amber-100 rounded-lg flex items-center justify-center text-amber-600 mr-2">🚗</span>
Escort Required Over
</h3>
<div class="space-y-3">
<div class="bg-slate-50 px-4 py-2 rounded-lg">
<span class="text-slate-600 font-medium">Width:</span>
<span class="font-semibold text-slate-900 ml-2">${state.escortWidth}</span>
</div>
<div class="bg-slate-50 px-4 py-2 rounded-lg">
<span class="text-slate-600 font-medium">Height:</span>
<span class="font-semibold text-slate-900 ml-2">${state.escortHeight}</span>
</div>
<div class="bg-slate-50 px-4 py-2 rounded-lg">
<span class="text-slate-600 font-medium">Length:</span>
<span class="font-semibold text-slate-900 ml-2">${state.escortLength}</span>
</div>
<div class="bg-slate-50 px-4 py-2 rounded-lg">
<span class="text-slate-600 font-medium">Weight:</span>
<span class="font-semibold text-slate-900 ml-2">${state.escortWeight}</span>
</div>
</div>
</div>
</div>
<!-- Travel Restrictions -->
<div class="mt-8 grid md:grid-cols-2 gap-8">
<div>
<h3 class="text-lg font-bold text-slate-900 mb-4 flex items-center">
<span class="w-8 h-8 bg-green-100 rounded-lg flex items-center justify-center text-green-600 mr-2">🕐</span>
Travel Restrictions
</h3>
<div class="bg-slate-50 px-4 py-3 rounded-lg space-y-2">
<p><span class="font-medium text-slate-600">Hours:</span> <span class="text-slate-900">${state.travel}</span></p>
<p><span class="font-medium text-slate-600">Holidays:</span> <span class="text-slate-900">${state.holidays}</span></p>
</div>
</div>
<div>
<h3 class="text-lg font-bold text-slate-900 mb-4 flex items-center">
<span class="w-8 h-8 bg-purple-100 rounded-lg flex items-center justify-center text-purple-600 mr-2">🏛️</span>
Permit Agency
</h3>
<div class="bg-slate-50 px-4 py-3 rounded-lg space-y-2">
<p class="font-semibold text-slate-900">${state.agency}</p>
<a href="${state.url}" target="_blank" class="text-amber-600 hover:text-amber-700 text-sm font-medium">${state.url} ↗</a>
</div>
</div>
</div>
<!-- Notes -->
<div class="mt-6 bg-amber-50 border border-amber-200 rounded-xl px-6 py-4">
<p class="text-amber-900"><strong>Notes:</strong> ${state.notes}</p>
</div>
`;
const detailEl = document.getElementById('state-detail');
detailEl.classList.remove('hidden');
detailEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
// Populate route checker dropdowns
const originSelect = document.getElementById('rc-origin');
const destSelect = document.getElementById('rc-destination');
MOCK_STATE_REGULATIONS.forEach(state => {
originSelect.add(new Option(state.name, state.abbr));
destSelect.add(new Option(state.name, state.abbr));
});
function checkRoute() {
const origin = document.getElementById('rc-origin').value;
const dest = document.getElementById('rc-destination').value;
const widthFt = parseInt(document.getElementById('rc-width-ft').value) || 0;
const widthIn = parseInt(document.getElementById('rc-width-in').value) || 0;
const heightFt = parseInt(document.getElementById('rc-height-ft').value) || 0;
const heightIn = parseInt(document.getElementById('rc-height-in').value) || 0;
if (!origin || !dest) {
alert('Please select both origin and destination states.');
return;
}
const resultsDiv = document.getElementById('route-results');
resultsDiv.classList.remove('hidden');
const widthTotal = widthFt + widthIn / 12;
const heightTotal = heightFt + heightIn / 12;
const originState = MOCK_STATE_REGULATIONS.find(s => s.abbr === origin);
const destState = MOCK_STATE_REGULATIONS.find(s => s.abbr === dest);
// For POC, just show origin and destination state requirements
const states = [originState];
if (origin !== dest) states.push(destState);
let html = `
<div class="bg-blue-50 border border-blue-200 rounded-xl p-4 mb-4">
<p class="text-blue-900 font-medium">📍 Route: ${originState.name}${destState.name}</p>
<p class="text-blue-700 text-sm mt-1">Showing requirements for origin and destination states. In production, all transit states would be included.</p>
</div>
<div class="space-y-4">
`;
states.forEach(state => {
const needsPermitW = widthTotal > 8.5;
const needsPermitH = heightTotal > 13.5;
const needsEscortW = widthTotal > 14;
const needsEscortH = heightTotal > 15;
html += `
<div class="border border-slate-200 rounded-xl p-5">
<div class="flex items-center justify-between mb-3">
<h4 class="font-bold text-lg text-slate-900">${state.name} (${state.abbr})</h4>
<div class="flex gap-2">
${needsPermitW || needsPermitH ? '<span class="bg-blue-100 text-blue-800 text-xs font-bold px-3 py-1 rounded-full">PERMIT NEEDED</span>' : '<span class="bg-green-100 text-green-800 text-xs font-bold px-3 py-1 rounded-full">NO PERMIT</span>'}
${needsEscortW || needsEscortH ? '<span class="bg-amber-100 text-amber-800 text-xs font-bold px-3 py-1 rounded-full">ESCORT NEEDED</span>' : ''}
</div>
</div>
<div class="grid grid-cols-2 gap-2 text-sm">
<p><span class="text-slate-500">Permit over:</span> W ${state.permitWidth} / H ${state.permitHeight}</p>
<p><span class="text-slate-500">Escort over:</span> W ${state.escortWidth.split(';')[0]}</p>
<p><span class="text-slate-500">Travel:</span> ${state.travel}</p>
<p><span class="text-slate-500">Agency:</span> ${state.agency}</p>
</div>
</div>
`;
});
html += '</div>';
resultsDiv.innerHTML = html;
}
// Equipment Requirements (Module 12)
const equipSelect = document.getElementById('equip-state');
MOCK_STATE_REGULATIONS.forEach(state => {
equipSelect.add(new Option(state.name + ' (' + state.abbr + ')', state.abbr));
});
// Auto-scroll to equipment section if URL has #equipment
if (window.location.hash === '#equipment') {
document.getElementById('equipment').scrollIntoView({ behavior: 'smooth' });
}
function renderEquipRow(label, value) {
return `<div class="flex justify-between items-start bg-slate-50 px-4 py-2.5 rounded-lg">
<span class="text-slate-600 font-medium text-sm flex-shrink-0 mr-4">${label}</span>
<span class="font-semibold text-slate-900 text-sm text-right">${value}</span>
</div>`;
}
function showEquipment() {
const abbr = document.getElementById('equip-state').value;
const contentDiv = document.getElementById('equip-content');
const noDataDiv = document.getElementById('equip-no-data');
if (!abbr) {
contentDiv.classList.add('hidden');
noDataDiv.classList.add('hidden');
return;
}
const equip = typeof MOCK_STATE_EQUIPMENT !== 'undefined' ? MOCK_STATE_EQUIPMENT[abbr] : null;
if (!equip) {
contentDiv.classList.add('hidden');
noDataDiv.classList.remove('hidden');
return;
}
noDataDiv.classList.add('hidden');
contentDiv.classList.remove('hidden');
const e = equip.escort;
const c = equip.carrier;
contentDiv.innerHTML = `
<div class="grid md:grid-cols-2 gap-8">
<div>
<h3 class="text-lg font-bold text-slate-900 mb-4 flex items-center">
<span class="w-8 h-8 bg-amber-100 rounded-lg flex items-center justify-center text-amber-600 mr-2">🚗</span>
Escort Vehicle Requirements
</h3>
<div class="space-y-2">
${renderEquipRow('Certification', e.certification)}
${renderEquipRow('Vehicle', e.vehicle)}
${renderEquipRow('Signs', e.signs)}
${renderEquipRow('Lights', e.lights)}
${renderEquipRow('Height Pole', e.heightPole)}
${renderEquipRow('Flags', e.flags)}
${renderEquipRow('Communication', e.communication)}
${renderEquipRow('Safety Gear', e.safety)}
</div>
</div>
<div>
<h3 class="text-lg font-bold text-slate-900 mb-4 flex items-center">
<span class="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center text-blue-600 mr-2">🚛</span>
Truck & Trailer Requirements
</h3>
<div class="space-y-2">
${renderEquipRow('OVERSIZE LOAD Signs', c.signs)}
${renderEquipRow('Flags', c.flags)}
${renderEquipRow('Warning Lights', c.lights)}
${renderEquipRow('Traffic Cones', c.cones)}
${renderEquipRow('Fire Extinguisher', c.fireExtinguisher)}
${renderEquipRow('Reflective Triangles', c.triangles)}
${renderEquipRow('Road Flares', c.flares)}
${renderEquipRow('First Aid Kit', c.firstAid)}
</div>
</div>
</div>
`;
}
})();
</script>
</body>
</html>