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>
This commit is contained in:
Daniel Kovalevich
2026-03-30 15:52:56 -04:00
parent f917fb8014
commit 93efb907ff
20 changed files with 281 additions and 109 deletions

423
public/pages/alerts.html Normal file
View File

@@ -0,0 +1,423 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Route &amp; Weather Alerts | 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: 400px; 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">Route &amp; Weather Alerts</h1>
<p class="text-lg text-gray-400 max-w-3xl">Know about closures, construction, and weather conditions on your permitted route BEFORE you depart.</p>
</div>
</section>
<!-- Important Info Box -->
<section class="max-w-7xl mx-auto px-4 pt-8 w-full">
<div class="bg-blue-50 border border-blue-200 rounded-2xl p-6">
<div class="flex items-start gap-3">
<span class="text-2xl flex-shrink-0"></span>
<div>
<h3 class="font-bold text-blue-900 text-lg mb-1">Important — Single-Trip Permitted Loads</h3>
<p class="text-blue-800 text-sm leading-relaxed">Single-trip permitted loads <strong>MUST</strong> follow their permitted route exactly. If a closure or construction zone is on your permitted route, <strong>contact the permit authority for a route amendment before departing.</strong> Deviating from a permitted route without authorization can result in fines, permit revocation, and liability issues.</p>
</div>
</div>
</div>
</section>
<!-- Stats Bar -->
<section id="stats-bar" class="max-w-7xl mx-auto px-4 pt-6 w-full">
<!-- populated by JS -->
</section>
<!-- Filters -->
<section class="max-w-7xl mx-auto px-4 pt-6 w-full">
<div class="bg-white rounded-2xl shadow-lg p-6">
<h2 class="text-lg font-bold text-slate-900 mb-4">Filter Alerts</h2>
<div class="flex flex-wrap items-end gap-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Type</label>
<select id="filter-type" class="border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-amber-400 focus:border-amber-400">
<option value="all">All Types</option>
<option value="construction">🚧 Construction</option>
<option value="closure">⛔ Closure</option>
<option value="wind">💨 Wind</option>
<option value="winter">❄️ Winter Storm</option>
<option value="fog">🌫️ Fog</option>
<option value="thunderstorm">⛈️ Thunderstorm</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Severity</label>
<select id="filter-severity" class="border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-amber-400 focus:border-amber-400">
<option value="all">All Severities</option>
<option value="minor">Minor</option>
<option value="moderate">Moderate</option>
<option value="major">Major</option>
<option value="critical">Critical</option>
<option value="advisory">Advisory</option>
<option value="watch">Watch</option>
<option value="warning">Warning</option>
</select>
</div>
<div class="flex items-center gap-2">
<input type="checkbox" id="filter-oversize" class="w-4 h-4 text-amber-500 border-slate-300 rounded focus:ring-amber-400">
<label for="filter-oversize" class="text-sm font-medium text-slate-700">Affects oversize only</label>
</div>
<button onclick="resetFilters()" class="text-sm text-amber-600 hover:text-amber-700 font-medium underline">Reset</button>
</div>
</div>
</section>
<!-- Tab Toggle -->
<section class="max-w-7xl mx-auto px-4 pt-6 w-full">
<div class="flex gap-2">
<button id="tab-all" onclick="setTab('all')" class="px-5 py-2.5 rounded-xl text-sm font-semibold transition-colors">All Alerts</button>
<button id="tab-route" onclick="setTab('route')" class="px-5 py-2.5 rounded-xl text-sm font-semibold transition-colors">🚧 Route Conditions</button>
<button id="tab-weather" onclick="setTab('weather')" class="px-5 py-2.5 rounded-xl text-sm font-semibold transition-colors">🌦️ Weather Alerts</button>
</div>
</section>
<!-- Map -->
<section class="max-w-7xl mx-auto px-4 pt-6 w-full">
<div class="bg-white rounded-2xl shadow-lg p-4">
<div id="map"></div>
</div>
</section>
<!-- Route Conditions Cards -->
<section id="route-section" class="max-w-7xl mx-auto px-4 pt-8 w-full">
<h2 class="text-2xl font-bold text-slate-900 mb-4 flex items-center gap-2">🚧 Route Conditions</h2>
<div id="route-cards" class="grid md:grid-cols-2 gap-6">
<!-- populated by JS -->
</div>
<p id="route-empty" class="hidden text-slate-500 text-center py-8">No route conditions match your filters.</p>
</section>
<!-- Weather Alerts Cards -->
<section id="weather-section" class="max-w-7xl mx-auto px-4 pt-8 pb-8 w-full">
<h2 class="text-2xl font-bold text-slate-900 mb-4 flex items-center gap-2">🌦️ Weather Alerts</h2>
<div id="weather-cards" class="grid md:grid-cols-2 gap-6">
<!-- populated by JS -->
</div>
<p id="weather-empty" class="hidden text-slate-500 text-center py-8">No weather alerts match your filters.</p>
</section>
<div id="main-footer"></div>
<script src="/js/api.js"></script>
<script src="/js/nav.js"></script>
<script>
renderNav('alerts');
renderBanner();
renderFooter();
(async () => {
const alertData = await PilotEdge.getAlerts();
const MOCK_ROUTE_CONDITIONS = alertData.routeConditions;
const MOCK_WEATHER_ALERTS = alertData.weatherAlerts;
// ── State ──
let activeTab = 'all';
let map, markersLayer;
// ── Helpers ──
function formatDate(dateStr) {
return new Date(dateStr + 'T00:00:00').toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
}
function formatDateTime(isoStr) {
return new Date(isoStr).toLocaleString('en-US', {
month: 'short', day: 'numeric', year: 'numeric',
hour: 'numeric', minute: '2-digit', timeZoneName: 'short'
});
}
// ── Badge helpers ──
const typeBadge = {
construction: { label: '🚧 Construction', cls: 'bg-orange-100 text-orange-800' },
closure: { label: '⛔ Closure', cls: 'bg-red-100 text-red-800' },
wind: { label: '💨 Wind', cls: 'bg-blue-100 text-blue-800' },
winter: { label: '❄️ Winter Storm', cls: 'bg-indigo-100 text-indigo-800' },
fog: { label: '🌫️ Fog', cls: 'bg-gray-100 text-gray-700' },
thunderstorm: { label: '⛈️ Thunderstorm', cls: 'bg-purple-100 text-purple-800' }
};
const routeSeverityBadge = {
minor: { label: 'Minor', cls: 'bg-green-100 text-green-800' },
moderate: { label: 'Moderate', cls: 'bg-amber-100 text-amber-800' },
major: { label: 'Major', cls: 'bg-orange-100 text-orange-800' },
critical: { label: 'Critical', cls: 'bg-red-100 text-red-800' }
};
const weatherSeverityBadge = {
advisory: { label: 'Advisory', cls: 'bg-yellow-100 text-yellow-800' },
watch: { label: 'Watch', cls: 'bg-orange-100 text-orange-800' },
warning: { label: 'Warning', cls: 'bg-red-100 text-red-800' }
};
// ── Filtering ──
function getFilters() {
return {
type: document.getElementById('filter-type').value,
severity: document.getElementById('filter-severity').value,
oversizeOnly: document.getElementById('filter-oversize').checked
};
}
function filterRouteConditions() {
const f = getFilters();
return MOCK_ROUTE_CONDITIONS.filter(rc => {
if (f.type !== 'all' && rc.type !== f.type) return false;
if (f.severity !== 'all' && rc.severity !== f.severity) return false;
if (f.oversizeOnly && !rc.affectsOversize) return false;
return true;
});
}
function filterWeatherAlerts() {
const f = getFilters();
return MOCK_WEATHER_ALERTS.filter(wa => {
if (f.type !== 'all' && wa.type !== f.type) return false;
if (f.severity !== 'all' && wa.severity !== f.severity) return false;
return true;
});
}
function resetFilters() {
document.getElementById('filter-type').value = 'all';
document.getElementById('filter-severity').value = 'all';
document.getElementById('filter-oversize').checked = false;
renderAll();
}
// ── Stats bar ──
function renderStats() {
const routeCount = MOCK_ROUTE_CONDITIONS.length;
const weatherCount = MOCK_WEATHER_ALERTS.length;
const oversizeCount = MOCK_ROUTE_CONDITIONS.filter(r => r.affectsOversize).length;
document.getElementById('stats-bar').innerHTML = `
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div class="bg-white rounded-2xl shadow-lg p-5 flex items-center gap-4">
<div class="w-12 h-12 bg-orange-100 rounded-xl flex items-center justify-center text-2xl">🚧</div>
<div>
<p class="text-2xl font-bold text-slate-900">${routeCount}</p>
<p class="text-sm text-slate-500">Active Route Conditions</p>
</div>
</div>
<div class="bg-white rounded-2xl shadow-lg p-5 flex items-center gap-4">
<div class="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center text-2xl">🌦️</div>
<div>
<p class="text-2xl font-bold text-slate-900">${weatherCount}</p>
<p class="text-sm text-slate-500">Active Weather Alerts</p>
</div>
</div>
<div class="bg-white rounded-2xl shadow-lg p-5 flex items-center gap-4">
<div class="w-12 h-12 bg-red-100 rounded-xl flex items-center justify-center text-2xl">⚠️</div>
<div>
<p class="text-2xl font-bold text-slate-900">${oversizeCount}</p>
<p class="text-sm text-slate-500">Alerts Affecting Oversize</p>
</div>
</div>
</div>
`;
}
// ── Tabs ──
function setTab(tab) {
activeTab = tab;
renderAll();
}
function renderTabButtons() {
['all', 'route', 'weather'].forEach(t => {
const btn = document.getElementById('tab-' + t);
if (t === activeTab) {
btn.className = 'px-5 py-2.5 rounded-xl text-sm font-semibold transition-colors bg-amber-500 text-slate-900 shadow-md';
} else {
btn.className = 'px-5 py-2.5 rounded-xl text-sm font-semibold transition-colors bg-white text-slate-600 hover:bg-slate-100 shadow';
}
});
}
// ── Map ──
function initMap() {
map = L.map('map').setView([38.5, -98], 4);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap contributors',
maxZoom: 18
}).addTo(map);
markersLayer = L.layerGroup().addTo(map);
}
function createIcon(color) {
return L.divIcon({
className: '',
html: `<div style="width:28px;height:28px;background:${color};border:3px solid white;border-radius:50%;box-shadow:0 2px 6px rgba(0,0,0,.35);"></div>`,
iconSize: [28, 28],
iconAnchor: [14, 14],
popupAnchor: [0, -16]
});
}
function renderMap() {
markersLayer.clearLayers();
const routes = (activeTab === 'all' || activeTab === 'route') ? filterRouteConditions() : [];
const weather = (activeTab === 'all' || activeTab === 'weather') ? filterWeatherAlerts() : [];
const routeColors = { construction: '#f97316', closure: '#ef4444' };
const weatherColors = { wind: '#3b82f6', winter: '#6366f1', fog: '#6b7280', thunderstorm: '#a855f7' };
routes.forEach(rc => {
const color = routeColors[rc.type] || '#f97316';
const sevInfo = routeSeverityBadge[rc.severity] || routeSeverityBadge.moderate;
const marker = L.marker([rc.location.lat, rc.location.lng], { icon: createIcon(color) });
marker.bindPopup(`
<div style="max-width:280px;">
<div style="font-weight:700;font-size:14px;margin-bottom:4px;">${rc.route}</div>
<div style="font-size:12px;color:#64748b;margin-bottom:6px;">${rc.location.desc}, ${rc.location.state}</div>
<div style="font-size:12px;margin-bottom:6px;">${rc.description}</div>
<div style="font-size:11px;color:#94a3b8;">${formatDate(rc.startDate)} ${formatDate(rc.endDate)}</div>
${rc.affectsOversize ? '<div style="margin-top:6px;font-weight:700;color:#dc2626;font-size:12px;">⚠️ AFFECTS OVERSIZE</div>' : ''}
</div>
`);
markersLayer.addLayer(marker);
});
weather.forEach(wa => {
const color = weatherColors[wa.type] || '#3b82f6';
const marker = L.marker([wa.lat, wa.lng], { icon: createIcon(color) });
marker.bindPopup(`
<div style="max-width:280px;">
<div style="font-weight:700;font-size:14px;margin-bottom:4px;">${wa.region}</div>
<div style="font-size:12px;color:#64748b;margin-bottom:6px;">Routes: ${wa.routes.join(', ')}</div>
<div style="font-size:12px;margin-bottom:6px;">${wa.description}</div>
<div style="font-size:11px;color:#94a3b8;">Valid: ${formatDateTime(wa.validFrom)} ${formatDateTime(wa.validTo)}</div>
</div>
`);
markersLayer.addLayer(marker);
});
}
// ── Route Condition Cards ──
function renderRouteCards() {
const section = document.getElementById('route-section');
const container = document.getElementById('route-cards');
const empty = document.getElementById('route-empty');
if (activeTab === 'weather') { section.classList.add('hidden'); return; }
section.classList.remove('hidden');
const items = filterRouteConditions();
if (items.length === 0) {
container.innerHTML = '';
empty.classList.remove('hidden');
return;
}
empty.classList.add('hidden');
container.innerHTML = items.map(rc => {
const tb = typeBadge[rc.type] || typeBadge.construction;
const sb = routeSeverityBadge[rc.severity] || routeSeverityBadge.moderate;
return `
<div class="bg-white rounded-2xl shadow-lg p-6 border-l-4 ${rc.type === 'closure' ? 'border-red-500' : 'border-orange-500'}">
<div class="flex flex-wrap items-center gap-2 mb-3">
<span class="px-2.5 py-1 rounded-lg text-xs font-bold ${tb.cls}">${tb.label}</span>
<span class="px-2.5 py-1 rounded-lg text-xs font-bold ${sb.cls}">${sb.label}</span>
${rc.affectsOversize ? '<span class="px-2.5 py-1 rounded-lg text-xs font-bold bg-red-600 text-white animate-pulse">⚠️ AFFECTS OVERSIZE</span>' : ''}
</div>
<h3 class="text-lg font-bold text-slate-900 mb-1">${rc.route}</h3>
<p class="text-sm text-slate-500 mb-2">${rc.location.desc}, ${rc.location.state}</p>
<p class="text-sm text-slate-700 mb-3">${rc.description}</p>
<div class="flex flex-wrap items-center gap-4 text-xs text-slate-500">
<span>📅 ${formatDate(rc.startDate)} ${formatDate(rc.endDate)}</span>
<span>📡 ${rc.source}</span>
</div>
</div>
`;
}).join('');
}
// ── Weather Alert Cards ──
function renderWeatherCards() {
const section = document.getElementById('weather-section');
const container = document.getElementById('weather-cards');
const empty = document.getElementById('weather-empty');
if (activeTab === 'route') { section.classList.add('hidden'); return; }
section.classList.remove('hidden');
const items = filterWeatherAlerts();
if (items.length === 0) {
container.innerHTML = '';
empty.classList.remove('hidden');
return;
}
empty.classList.add('hidden');
const borderColors = { wind: 'border-blue-500', winter: 'border-indigo-500', fog: 'border-gray-400', thunderstorm: 'border-purple-500' };
container.innerHTML = items.map(wa => {
const tb = typeBadge[wa.type] || typeBadge.wind;
const sb = weatherSeverityBadge[wa.severity] || weatherSeverityBadge.advisory;
const border = borderColors[wa.type] || 'border-blue-500';
return `
<div class="bg-white rounded-2xl shadow-lg p-6 border-l-4 ${border}">
<div class="flex flex-wrap items-center gap-2 mb-3">
<span class="px-2.5 py-1 rounded-lg text-xs font-bold ${tb.cls}">${tb.label}</span>
<span class="px-2.5 py-1 rounded-lg text-xs font-bold ${sb.cls}">${sb.label}</span>
</div>
<h3 class="text-lg font-bold text-slate-900 mb-1">${wa.region}</h3>
<p class="text-sm text-slate-500 mb-2">Affected routes: ${wa.routes.join(', ')}</p>
<p class="text-sm text-slate-700 mb-3">${wa.description}</p>
<div class="flex flex-wrap items-center gap-4 text-xs text-slate-500">
<span>🕐 ${formatDateTime(wa.validFrom)} ${formatDateTime(wa.validTo)}</span>
<span>📡 ${wa.source}</span>
</div>
</div>
`;
}).join('');
}
// ── Section visibility ──
function renderSectionVisibility() {
document.getElementById('route-section').classList.toggle('hidden', activeTab === 'weather');
document.getElementById('weather-section').classList.toggle('hidden', activeTab === 'route');
}
// ── Master render ──
function renderAll() {
renderTabButtons();
renderMap();
renderRouteCards();
renderWeatherCards();
}
// ── Init ──
initMap();
renderStats();
renderAll();
// Wire up filter change events
document.getElementById('filter-type').addEventListener('change', renderAll);
document.getElementById('filter-severity').addEventListener('change', renderAll);
document.getElementById('filter-oversize').addEventListener('change', renderAll);
})();
</script>
</body>
</html>

388
public/pages/bridges.html Normal file
View File

@@ -0,0 +1,388 @@
<!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>

294
public/pages/calendar.html Normal file
View File

@@ -0,0 +1,294 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Seasonal Restriction Calendar | PilotEdge</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
.gantt-bar { transition: opacity 0.2s; }
.gantt-bar:hover { opacity: 0.85; }
</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">Seasonal Restriction Calendar</h1>
<p class="text-lg text-gray-400 max-w-3xl">Plan around seasonal closures, weight restrictions, and travel blackouts that affect oversize load movement across the country.</p>
</div>
</section>
<!-- Filters -->
<section class="max-w-7xl mx-auto px-4 pt-8 w-full">
<div class="bg-white rounded-2xl shadow-lg p-6">
<div class="flex flex-wrap items-end gap-4">
<div class="flex-1 min-w-[180px]">
<label class="block text-sm font-semibold text-slate-700 mb-1">Restriction Type</label>
<select id="filter-type" onchange="applyFilters()" 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="">All Types</option>
<option value="spring_weight">🔵 Spring Weight</option>
<option value="winter_closure">🟣 Winter Closure</option>
<option value="wind_season">🟡 Wind Season</option>
<option value="holiday_blackout">🔴 Holiday Blackout</option>
<option value="harvest_season">🟢 Harvest Season</option>
</select>
</div>
<div class="flex-1 min-w-[180px]">
<label class="block text-sm font-semibold text-slate-700 mb-1">State</label>
<select id="filter-state" onchange="applyFilters()" 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="">All States</option>
</select>
</div>
<button onclick="resetFilters()" class="bg-slate-100 hover:bg-slate-200 text-slate-700 font-semibold px-4 py-2 rounded-lg transition-colors text-sm">
Reset Filters
</button>
</div>
</div>
</section>
<!-- Calendar Gantt Chart -->
<section class="max-w-7xl mx-auto px-4 py-8 w-full">
<div class="bg-white rounded-2xl shadow-lg p-6 overflow-x-auto">
<h2 class="text-xl font-bold text-slate-900 mb-1">12-Month Timeline</h2>
<p class="text-sm text-slate-500 mb-6">Colored bars show when each restriction is active. Hover for details.</p>
<div id="gantt-chart"></div>
<div id="gantt-empty" class="hidden text-center py-12 text-slate-400">
<p class="text-lg font-medium">No restrictions match the current filters.</p>
</div>
</div>
</section>
<!-- Legend -->
<section class="max-w-7xl mx-auto px-4 pb-4 w-full">
<div class="flex flex-wrap gap-4 justify-center text-sm">
<span class="flex items-center gap-1.5"><span class="w-4 h-3 rounded" style="background:#3b82f6"></span> Spring Weight</span>
<span class="flex items-center gap-1.5"><span class="w-4 h-3 rounded" style="background:#6366f1"></span> Winter Closure</span>
<span class="flex items-center gap-1.5"><span class="w-4 h-3 rounded" style="background:#f59e0b"></span> Wind Season</span>
<span class="flex items-center gap-1.5"><span class="w-4 h-3 rounded" style="background:#ef4444"></span> Holiday Blackout</span>
<span class="flex items-center gap-1.5"><span class="w-4 h-3 rounded" style="background:#22c55e"></span> Harvest Season</span>
</div>
</section>
<!-- Restriction Detail Cards -->
<section class="max-w-7xl mx-auto px-4 pb-8 w-full">
<h2 class="text-xl font-bold text-slate-900 mb-4">Restriction Details</h2>
<div id="restriction-cards" class="grid md:grid-cols-2 gap-6"></div>
<div id="cards-empty" class="hidden text-center py-12 text-slate-400">
<p class="text-lg font-medium">No restrictions match the current filters.</p>
</div>
</section>
<div id="main-footer"></div>
<script src="/js/api.js"></script>
<script src="/js/nav.js"></script>
<script>
renderNav('calendar');
renderBanner();
renderFooter();
(async () => {
const MOCK_SEASONAL_RESTRICTIONS = await PilotEdge.getSeasonalRestrictions();
// ---- Constants ----
const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
const TYPE_BADGES = {
spring_weight: { label:'Spring Weight', bg:'bg-blue-100', text:'text-blue-800' },
winter_closure: { label:'Winter Closure', bg:'bg-indigo-100', text:'text-indigo-800' },
wind_season: { label:'Wind Season', bg:'bg-amber-100', text:'text-amber-800' },
holiday_blackout:{ label:'Holiday Blackout', bg:'bg-red-100', text:'text-red-800' },
harvest_season: { label:'Harvest Season', bg:'bg-green-100', text:'text-green-800' }
};
// ---- Populate state filter ----
const states = [...new Set(MOCK_SEASONAL_RESTRICTIONS.map(r => r.stateName))].sort();
const stateSelect = document.getElementById('filter-state');
states.forEach(s => {
const r = MOCK_SEASONAL_RESTRICTIONS.find(x => x.stateName === s);
stateSelect.add(new Option(`${s} (${r.state})`, r.state));
});
// ---- Filtering ----
function getFiltered() {
const type = document.getElementById('filter-type').value;
const state = document.getElementById('filter-state').value;
return MOCK_SEASONAL_RESTRICTIONS.filter(r =>
(!type || r.type === type) && (!state || r.state === state)
);
}
function applyFilters() {
renderGantt(getFiltered());
renderCards(getFiltered());
}
function resetFilters() {
document.getElementById('filter-type').value = '';
document.getElementById('filter-state').value = '';
applyFilters();
}
// ---- Month span helper (handles wrap-around) ----
function getActiveMonths(r) {
const months = [];
if (r.startMonth <= r.endMonth) {
for (let m = r.startMonth; m <= r.endMonth; m++) months.push(m);
} else {
for (let m = r.startMonth; m <= 12; m++) months.push(m);
for (let m = 1; m <= r.endMonth; m++) months.push(m);
}
return months;
}
// ---- Gantt Chart ----
function renderGantt(restrictions) {
const chart = document.getElementById('gantt-chart');
const empty = document.getElementById('gantt-empty');
if (restrictions.length === 0) {
chart.classList.add('hidden');
empty.classList.remove('hidden');
return;
}
chart.classList.remove('hidden');
empty.classList.add('hidden');
// Build the grid: header row + one row per restriction
let html = '<div style="min-width:800px;">';
// Month header row
html += '<div class="grid" style="grid-template-columns: 220px repeat(12, 1fr); gap: 0;">';
html += '<div class="text-xs font-semibold text-slate-500 py-2 pr-3 text-right">Restriction</div>';
MONTHS.forEach((m, i) => {
const now = new Date();
const isCurrentMonth = (now.getMonth() === i);
html += `<div class="text-xs font-semibold text-center py-2 ${isCurrentMonth ? 'text-amber-600 bg-amber-50 rounded-t' : 'text-slate-500'}">${m}</div>`;
});
html += '</div>';
// Restriction rows
restrictions.forEach(r => {
const active = getActiveMonths(r);
const badge = TYPE_BADGES[r.type] || { label: r.type, bg:'bg-slate-100', text:'text-slate-700' };
html += '<div class="grid border-t border-slate-100" style="grid-template-columns: 220px repeat(12, 1fr); gap: 0;">';
// Label cell
html += `<div class="flex items-center py-2 pr-3 gap-2 justify-end">
<span class="text-xs font-medium text-slate-700 text-right truncate" title="${r.title}${r.stateName}">${r.stateName}</span>
<span class="flex-shrink-0 text-[10px] font-bold px-1.5 py-0.5 rounded ${badge.bg} ${badge.text}">${r.state}</span>
</div>`;
// Month cells
for (let m = 1; m <= 12; m++) {
const isActive = active.includes(m);
const now = new Date();
const isCurrentMonth = (now.getMonth() + 1 === m);
if (isActive) {
const isStart = m === r.startMonth;
const isEnd = m === r.endMonth;
const roundL = isStart ? 'rounded-l-full' : '';
const roundR = isEnd ? 'rounded-r-full' : '';
html += `<div class="flex items-center py-2 px-0.5 ${isCurrentMonth ? 'bg-amber-50' : ''}">
<div class="gantt-bar w-full h-6 ${roundL} ${roundR} flex items-center justify-center cursor-default"
style="background:${r.color}; opacity:0.8;"
title="${r.title}\n${MONTHS[r.startMonth-1]} ${r.startDay} ${MONTHS[r.endMonth-1]} ${r.endDay}">
${isStart ? `<span class="text-[9px] text-white font-bold drop-shadow pl-1">${r.startDay}</span>` : ''}
${isEnd ? `<span class="text-[9px] text-white font-bold drop-shadow pr-1">${r.endDay}</span>` : ''}
</div>
</div>`;
} else {
html += `<div class="flex items-center py-2 px-0.5 ${isCurrentMonth ? 'bg-amber-50' : ''}">
<div class="w-full h-6 bg-slate-50 ${m === 1 ? 'rounded-l' : ''} ${m === 12 ? 'rounded-r' : ''}"></div>
</div>`;
}
}
html += '</div>';
});
html += '</div>';
chart.innerHTML = html;
}
// ---- Detail Cards ----
function renderCards(restrictions) {
const container = document.getElementById('restriction-cards');
const empty = document.getElementById('cards-empty');
if (restrictions.length === 0) {
container.classList.add('hidden');
empty.classList.remove('hidden');
return;
}
container.classList.remove('hidden');
empty.classList.add('hidden');
container.innerHTML = restrictions.map(r => {
const badge = TYPE_BADGES[r.type] || { label: r.type, bg:'bg-slate-100', text:'text-slate-700' };
const active = getActiveMonths(r);
const monthRange = `${MONTHS[r.startMonth-1]} ${r.startDay} ${MONTHS[r.endMonth-1]} ${r.endDay}`;
// Month pills
const monthPills = MONTHS.map((m, i) => {
const isActive = active.includes(i + 1);
return `<span class="inline-block w-7 text-center text-[10px] font-semibold rounded py-0.5 ${isActive ? 'text-white' : 'text-slate-400 bg-slate-100'}" ${isActive ? `style="background:${r.color}"` : ''}>${m.charAt(0)}${m.charAt(1)}</span>`;
}).join('');
return `
<div class="bg-white rounded-2xl shadow-lg border border-slate-100 overflow-hidden">
<div class="px-6 pt-5 pb-4">
<div class="flex items-start justify-between gap-3 mb-3">
<div>
<h3 class="font-bold text-slate-900 text-lg leading-tight">${r.title}</h3>
<p class="text-sm text-slate-500 mt-0.5">${r.stateName} (${r.state})</p>
</div>
<span class="flex-shrink-0 text-xs font-bold px-2.5 py-1 rounded-full ${badge.bg} ${badge.text}">${badge.label}</span>
</div>
<!-- Active period -->
<div class="mb-4">
<div class="flex items-center gap-2 text-sm text-slate-700 font-medium mb-2">
<svg class="w-4 h-4 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
${monthRange}
</div>
<div class="flex gap-1 flex-wrap">${monthPills}</div>
</div>
<!-- Routes -->
<div class="mb-3">
<p class="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-1">Affected Routes</p>
<p class="text-sm text-slate-700">${r.routes}</p>
</div>
<!-- Description -->
<div class="mb-3">
<p class="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-1">Description</p>
<p class="text-sm text-slate-600">${r.description}</p>
</div>
<!-- Impact -->
<div class="bg-amber-50 border border-amber-200 rounded-xl px-4 py-3">
<p class="text-xs font-semibold text-amber-700 uppercase tracking-wider mb-1">⚠️ Impact on Oversize Loads</p>
<p class="text-sm text-amber-900">${r.impact}</p>
</div>
</div>
</div>
`;
}).join('');
}
// ---- Initial render ----
applyFilters();
})();
</script>
</body>
</html>

111
public/pages/contacts.html Normal file
View File

@@ -0,0 +1,111 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>State DOT Contacts | PilotEdge</title>
<script src="https://cdn.tailwindcss.com"></script>
</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 DOT Contact Directory</h1>
<p class="text-lg text-gray-400 max-w-3xl">Find permit office phone numbers, state police non-emergency lines, and online portal links for every state.</p>
</div>
</section>
<!-- Search / Filter Bar -->
<section class="max-w-7xl mx-auto px-4 py-8 w-full">
<div class="bg-white rounded-2xl shadow-lg p-6">
<label for="state-search" class="block text-sm font-semibold text-slate-700 mb-2">Search States</label>
<input
type="text"
id="state-search"
placeholder="Type a state name to filter…"
class="w-full border border-slate-300 rounded-lg px-4 py-3 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none text-slate-900"
>
</div>
</section>
<!-- Contact Cards Grid -->
<section class="max-w-7xl mx-auto px-4 pb-12 w-full">
<div id="contacts-grid" class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Populated by JS -->
</div>
<p id="no-results" class="hidden text-center text-slate-500 py-12 text-lg">No states match your search.</p>
</section>
<div id="main-footer"></div>
<script src="/js/api.js"></script>
<script src="/js/nav.js"></script>
<script>
renderNav('contacts');
renderBanner();
renderFooter();
(async () => {
const MOCK_STATE_CONTACTS = await PilotEdge.getContacts();
const grid = document.getElementById('contacts-grid');
const noResults = document.getElementById('no-results');
const searchInput = document.getElementById('state-search');
function buildCard(abbr, c) {
return `
<div class="bg-white rounded-2xl shadow-lg p-6 flex flex-col" data-state="${c.name.toLowerCase()}">
<h3 class="text-lg font-bold text-slate-900 mb-1">${c.name} <span class="text-slate-400 font-medium text-sm">(${abbr})</span></h3>
<div class="mt-3 space-y-2 text-sm text-slate-700 flex-1">
<p class="flex items-start gap-2">
<span class="shrink-0">📞</span>
<span><span class="font-medium">Permit Office:</span> <a href="tel:${c.permit.replace(/[^+\d]/g, '')}" class="text-amber-600 hover:text-amber-700 font-semibold">${c.permit}</a></span>
</p>
<p class="flex items-start gap-2">
<span class="shrink-0">🚔</span>
<span><span class="font-medium">State Police:</span> <a href="tel:${c.police.replace(/[^+\d]/g, '')}" class="text-amber-600 hover:text-amber-700 font-semibold">${c.police}</a></span>
</p>
<p class="flex items-start gap-2">
<span class="shrink-0">✉️</span>
<span><span class="font-medium">Email:</span> <a href="mailto:${c.email}" class="text-amber-600 hover:text-amber-700 font-semibold break-all">${c.email}</a></span>
</p>
<p class="flex items-start gap-2">
<span class="shrink-0">🕐</span>
<span><span class="font-medium">Hours:</span> ${c.hours}</span>
</p>
</div>
<a href="${c.portal}" target="_blank" rel="noopener noreferrer"
class="mt-4 inline-flex items-center justify-center gap-1 bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold text-sm px-4 py-2 rounded-lg transition-colors shadow-md">
Visit Permit Portal <span class="text-xs">↗</span>
</a>
</div>`;
}
function renderCards(filter) {
const term = (filter || '').toLowerCase();
let html = '';
let count = 0;
Object.keys(MOCK_STATE_CONTACTS).forEach(abbr => {
const c = MOCK_STATE_CONTACTS[abbr];
if (!term || c.name.toLowerCase().includes(term) || abbr.toLowerCase().includes(term)) {
html += buildCard(abbr, c);
count++;
}
});
grid.innerHTML = html;
noResults.classList.toggle('hidden', count > 0);
}
searchInput.addEventListener('input', () => renderCards(searchInput.value));
renderCards();
})();
</script>
</body>
</html>

262
public/pages/documents.html Normal file
View File

@@ -0,0 +1,262 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document Vault | PilotEdge</title>
<script src="https://cdn.tailwindcss.com"></script>
</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">Document Vault</h1>
<p class="text-lg text-gray-400 max-w-3xl">Securely store and manage your permits, insurance certificates, and professional certifications — all in one place.</p>
</div>
</section>
<!-- Main Content -->
<main class="flex-1 max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-10">
<!-- Stats Bar -->
<div id="stats-bar" class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8"></div>
<!-- Expiry Warnings -->
<div id="expiry-warnings" class="mb-8"></div>
<!-- Filter Bar -->
<div class="bg-white rounded-2xl shadow-lg p-4 sm:p-6 mb-8">
<div class="flex flex-col sm:flex-row gap-4">
<div class="flex-1">
<input id="search-input" type="text" placeholder="Search documents…"
class="w-full border border-slate-300 rounded-lg px-4 py-2.5 text-sm focus:ring-2 focus:ring-amber-500 focus:border-amber-500 outline-none transition-colors">
</div>
<select id="type-filter"
class="border border-slate-300 rounded-lg px-4 py-2.5 text-sm focus:ring-2 focus:ring-amber-500 focus:border-amber-500 outline-none transition-colors">
<option value="all">All Types</option>
<option value="permit">Permit</option>
<option value="insurance">Insurance</option>
<option value="certification">Certification</option>
<option value="registration">Registration</option>
</select>
<select id="status-filter"
class="border border-slate-300 rounded-lg px-4 py-2.5 text-sm focus:ring-2 focus:ring-amber-500 focus:border-amber-500 outline-none transition-colors">
<option value="all">All Statuses</option>
<option value="active">Active</option>
<option value="expired">Expired</option>
</select>
<button onclick="openUploadModal()"
class="bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold px-5 py-2.5 rounded-lg transition-colors shadow-md hover:shadow-lg text-sm whitespace-nowrap">
+ Upload Document
</button>
</div>
</div>
<!-- Document List -->
<div id="document-list" class="grid gap-4"></div>
<!-- Empty State -->
<div id="empty-state" class="hidden text-center py-16">
<p class="text-5xl mb-4">📄</p>
<p class="text-slate-500 text-lg">No documents match your filters.</p>
</div>
</main>
<!-- Upload Modal -->
<div id="upload-modal" class="fixed inset-0 z-50 hidden items-center justify-center bg-black/50 p-4">
<div class="bg-white rounded-2xl shadow-2xl max-w-md w-full p-6 relative">
<button onclick="closeUploadModal()" class="absolute top-4 right-4 text-slate-400 hover:text-slate-600 text-xl leading-none">&times;</button>
<h2 class="text-xl font-bold text-slate-900 mb-1">Upload Document</h2>
<p class="text-sm text-amber-600 mb-5">⚠️ POC demo — file upload not functional.</p>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Document Type</label>
<select id="upload-type"
class="w-full border border-slate-300 rounded-lg px-4 py-2.5 text-sm focus:ring-2 focus:ring-amber-500 focus:border-amber-500 outline-none">
<option value="permit">Permit</option>
<option value="insurance">Insurance</option>
<option value="certification">Certification</option>
<option value="registration">Registration</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Document Name</label>
<input id="upload-name" type="text" placeholder="e.g. TX Single Trip Permit"
class="w-full border border-slate-300 rounded-lg px-4 py-2.5 text-sm focus:ring-2 focus:ring-amber-500 focus:border-amber-500 outline-none">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Choose File</label>
<div class="border-2 border-dashed border-slate-300 rounded-lg p-6 text-center text-slate-400 text-sm">
Drag &amp; drop or click to browse<br>
<span class="text-xs">(disabled in POC)</span>
</div>
</div>
<button onclick="alert('POC demo — upload not functional.'); closeUploadModal();"
class="w-full bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold py-2.5 rounded-lg transition-colors shadow-md text-sm">
Upload
</button>
</div>
</div>
</div>
<div id="main-footer"></div>
<script src="/js/api.js"></script>
<script src="/js/nav.js"></script>
<script>
renderNav('documents');
renderBanner();
renderFooter();
(async () => {
const MOCK_DOCUMENTS = await PilotEdge.getDocuments();
// ── Helpers ──────────────────────────────────────────
const today = new Date();
const MS_PER_DAY = 86400000;
function daysUntil(dateStr) {
return Math.ceil((new Date(dateStr) - today) / MS_PER_DAY);
}
function fmtDate(dateStr) {
return new Date(dateStr).toLocaleDateString('en-US', { year:'numeric', month:'short', day:'numeric' });
}
const typeBadge = {
permit: 'bg-blue-100 text-blue-700',
insurance: 'bg-green-100 text-green-700',
certification: 'bg-purple-100 text-purple-700',
registration: 'bg-slate-200 text-slate-700'
};
function capitalize(s) { return s.charAt(0).toUpperCase() + s.slice(1); }
// ── Stats ───────────────────────────────────────────
function renderStats(docs) {
const total = docs.length;
const active = docs.filter(d => d.status === 'active').length;
const expired = docs.filter(d => d.status === 'expired').length;
const expiring = docs.filter(d => d.status === 'active' && daysUntil(d.expiryDate) <= 30 && daysUntil(d.expiryDate) > 0).length;
const items = [
{ label:'Total Documents', value:total, color:'bg-slate-900 text-white' },
{ label:'Active Permits', value:active, color:'bg-green-600 text-white' },
{ label:'Expiring Soon', value:expiring, color:'bg-amber-500 text-white' },
{ label:'Expired', value:expired, color:'bg-red-600 text-white' }
];
document.getElementById('stats-bar').innerHTML = items.map(s => `
<div class="${s.color} rounded-2xl shadow-lg p-5 text-center">
<div class="text-3xl font-bold">${s.value}</div>
<div class="text-sm mt-1 opacity-90">${s.label}</div>
</div>`).join('');
}
// ── Expiry Warnings ─────────────────────────────────
function renderExpiryWarnings(docs) {
const soon = docs.filter(d => d.status === 'active' && daysUntil(d.expiryDate) <= 30 && daysUntil(d.expiryDate) > 0);
const el = document.getElementById('expiry-warnings');
if (!soon.length) { el.innerHTML = ''; return; }
el.innerHTML = `
<div class="bg-amber-50 border border-amber-300 rounded-2xl p-5">
<h3 class="font-bold text-amber-800 text-lg mb-3">⚠️ Expiring Within 30 Days</h3>
<div class="space-y-2">
${soon.map(d => `
<div class="flex flex-col sm:flex-row sm:items-center justify-between bg-white rounded-xl px-4 py-3 shadow-sm border border-amber-200">
<div>
<span class="font-semibold text-slate-900">${d.name}</span>
<span class="inline-block ml-2 text-xs font-medium px-2 py-0.5 rounded-full ${typeBadge[d.type]}">${capitalize(d.type)}</span>
</div>
<span class="text-amber-700 font-medium text-sm mt-1 sm:mt-0">Expires ${fmtDate(d.expiryDate)} (${daysUntil(d.expiryDate)} day${daysUntil(d.expiryDate) === 1 ? '' : 's'})</span>
</div>`).join('')}
</div>
</div>`;
}
// ── Document Cards ──────────────────────────────────
function renderDocuments(docs) {
const list = document.getElementById('document-list');
const empty = document.getElementById('empty-state');
if (!docs.length) {
list.innerHTML = '';
empty.classList.remove('hidden');
return;
}
empty.classList.add('hidden');
list.innerHTML = docs.map(d => {
const statusBadge = d.status === 'active'
? 'bg-green-100 text-green-700'
: 'bg-red-100 text-red-700';
return `
<div class="bg-white rounded-2xl shadow-lg p-5 sm:p-6 hover:shadow-xl transition-shadow border border-slate-100">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div class="flex-1 min-w-0">
<div class="flex flex-wrap items-center gap-2 mb-2">
<h3 class="font-bold text-slate-900 text-base truncate">${d.name}</h3>
<span class="inline-block text-xs font-medium px-2.5 py-0.5 rounded-full ${typeBadge[d.type]}">${capitalize(d.type)}</span>
<span class="inline-block text-xs font-medium px-2.5 py-0.5 rounded-full ${statusBadge}">${capitalize(d.status)}</span>
</div>
<div class="flex flex-wrap gap-x-5 gap-y-1 text-sm text-slate-500">
${d.state ? `<span>📍 ${d.state}</span>` : ''}
<span>📤 Uploaded ${fmtDate(d.uploadDate)}</span>
<span>📅 Expires ${fmtDate(d.expiryDate)}</span>
<span>💾 ${d.fileSize}</span>
</div>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<button onclick="alert('Viewing: ${d.name}')" class="px-3 py-1.5 text-sm font-medium rounded-lg border border-slate-300 text-slate-700 hover:bg-slate-50 transition-colors">View</button>
<button onclick="alert('Downloading: ${d.name}')" class="px-3 py-1.5 text-sm font-medium rounded-lg border border-amber-400 text-amber-700 hover:bg-amber-50 transition-colors">Download</button>
<button onclick="alert('Delete requested for: ${d.name}')" class="px-3 py-1.5 text-sm font-medium rounded-lg border border-red-300 text-red-600 hover:bg-red-50 transition-colors">Delete</button>
</div>
</div>
</div>`;
}).join('');
}
// ── Filtering ────────────────────────────────────────
function applyFilters() {
const query = document.getElementById('search-input').value.toLowerCase();
const type = document.getElementById('type-filter').value;
const status = document.getElementById('status-filter').value;
const filtered = MOCK_DOCUMENTS.filter(d => {
if (query && !d.name.toLowerCase().includes(query) && !d.id.toLowerCase().includes(query)) return false;
if (type !== 'all' && d.type !== type) return false;
if (status !== 'all' && d.status !== status) return false;
return true;
});
renderDocuments(filtered);
}
document.getElementById('search-input').addEventListener('input', applyFilters);
document.getElementById('type-filter').addEventListener('change', applyFilters);
document.getElementById('status-filter').addEventListener('change', applyFilters);
// ── Upload Modal ─────────────────────────────────────
function openUploadModal() {
const modal = document.getElementById('upload-modal');
modal.classList.remove('hidden');
modal.classList.add('flex');
}
function closeUploadModal() {
const modal = document.getElementById('upload-modal');
modal.classList.add('hidden');
modal.classList.remove('flex');
}
// ── Initial Render ───────────────────────────────────
renderStats(MOCK_DOCUMENTS);
renderExpiryWarnings(MOCK_DOCUMENTS);
renderDocuments(MOCK_DOCUMENTS);
})();
</script>
</body>
</html>

302
public/pages/loadboard.html Normal file
View File

@@ -0,0 +1,302 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Oversize Load Board | PilotEdge</title>
<script src="https://cdn.tailwindcss.com"></script>
</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 flex flex-col md:flex-row md:items-end md:justify-between gap-4">
<div>
<h1 class="text-3xl md:text-4xl font-bold mb-3">Oversize Load Board</h1>
<p class="text-lg text-gray-400">Active loads needing escort/pilot vehicle services. Carriers post free — escort operators browse and bid.</p>
</div>
<div class="flex gap-3">
<button onclick="showPostModal()" class="bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold px-5 py-2.5 rounded-lg transition-colors whitespace-nowrap">
+ Post a Load
</button>
</div>
</div>
</section>
<!-- Filters -->
<section class="max-w-7xl mx-auto px-4 pt-8">
<div class="bg-white rounded-2xl shadow-lg p-6">
<div class="flex flex-col md:flex-row gap-4 items-end">
<div class="flex-1">
<label class="block text-sm font-semibold text-slate-700 mb-1">Search</label>
<input type="text" id="search-input" oninput="filterLoads()" placeholder="Search by location, carrier, or description..." 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">
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Status</label>
<select id="status-filter" onchange="filterLoads()" class="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="all">All</option>
<option value="posted" selected>Posted (Available)</option>
<option value="in_transit">In Transit</option>
</select>
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Escorts Needed</label>
<select id="escorts-filter" onchange="filterLoads()" class="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="all">Any</option>
<option value="1">1 Escort</option>
<option value="2">2 Escorts</option>
</select>
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Sort</label>
<select id="sort-select" onchange="filterLoads()" class="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="date_asc">Departure (Soonest)</option>
<option value="date_desc">Departure (Latest)</option>
<option value="posted_desc">Recently Posted</option>
</select>
</div>
</div>
</div>
</section>
<!-- Results Count -->
<section class="max-w-7xl mx-auto px-4 pt-4">
<p id="results-count" class="text-sm text-slate-500 font-medium"></p>
</section>
<!-- Load Listings -->
<section class="max-w-7xl mx-auto px-4 py-4 pb-8">
<div id="load-list" class="space-y-4">
<!-- Populated by JS -->
</div>
</section>
<!-- Subscription CTA -->
<section class="max-w-7xl mx-auto px-4 pb-8">
<div class="bg-gradient-to-r from-slate-800 to-slate-900 rounded-2xl p-8 text-center text-white">
<h3 class="text-xl font-bold mb-2">Escort Vehicle Operator?</h3>
<p class="text-gray-400 mb-4 max-w-lg mx-auto">Get unlimited load board access, instant notifications for new loads in your area, and priority bidding with a PilotEdge subscription.</p>
<button class="bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold px-6 py-3 rounded-lg transition-colors">
Subscribe — Starting at $49/mo
</button>
</div>
</section>
<!-- Post Load Modal (simplified for POC) -->
<div id="post-modal" class="fixed inset-0 bg-black/50 z-50 hidden flex items-center justify-center p-4">
<div class="bg-white rounded-2xl shadow-2xl max-w-lg w-full max-h-[90vh] overflow-y-auto p-8">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-slate-900">Post a Load</h2>
<button onclick="closePostModal()" class="text-slate-400 hover:text-slate-600 text-2xl">&times;</button>
</div>
<div class="bg-amber-50 border border-amber-200 rounded-xl p-4 mb-6">
<p class="text-amber-900 text-sm"><strong>POC Note:</strong> In production, this form would create a real listing. For now, this demonstrates the posting flow.</p>
</div>
<form onsubmit="handlePostLoad(event)" class="space-y-4">
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Carrier / Company Name</label>
<input type="text" required 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">
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Origin</label>
<input type="text" required placeholder="City, State" 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">
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Destination</label>
<input type="text" required placeholder="City, State" 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">
</div>
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Departure Date</label>
<input type="date" required 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">
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Load Description</label>
<textarea required rows="2" placeholder="What's being hauled?" 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"></textarea>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Dimensions (W×H×L)</label>
<input type="text" placeholder="16'×14'×135'" 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">
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Escorts Needed</label>
<select 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>1</option>
<option>2</option>
<option>3+</option>
</select>
</div>
</div>
<button type="submit" class="w-full bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold py-3 rounded-lg transition-colors">
Post Load
</button>
</form>
</div>
</div>
<div id="main-footer"></div>
<script src="/js/api.js"></script>
<script src="/js/nav.js"></script>
<script>
renderNav('loadboard');
renderBanner();
renderFooter();
(async () => {
const MOCK_LOAD_BOARD = await PilotEdge.getLoads();
function getStatusBadge(status) {
if (status === 'posted') return '<span class="bg-green-100 text-green-800 text-xs font-bold px-3 py-1 rounded-full">AVAILABLE</span>';
if (status === 'in_transit') return '<span class="bg-blue-100 text-blue-800 text-xs font-bold px-3 py-1 rounded-full">IN TRANSIT</span>';
return '<span class="bg-slate-100 text-slate-600 text-xs font-bold px-3 py-1 rounded-full">' + status.toUpperCase() + '</span>';
}
function daysUntil(dateStr) {
const today = new Date();
today.setHours(0,0,0,0);
const target = new Date(dateStr);
const diff = Math.ceil((target - today) / (1000 * 60 * 60 * 24));
if (diff < 0) return 'Departed';
if (diff === 0) return 'Today';
if (diff === 1) return 'Tomorrow';
return `In ${diff} days`;
}
function renderLoads(loads) {
const container = document.getElementById('load-list');
document.getElementById('results-count').textContent = `${loads.length} load${loads.length !== 1 ? 's' : ''} found`;
if (loads.length === 0) {
container.innerHTML = `
<div class="bg-white rounded-2xl shadow-lg p-12 text-center">
<p class="text-slate-500 text-lg">No loads match your filters.</p>
</div>
`;
return;
}
container.innerHTML = loads.map(load => `
<div class="bg-white rounded-2xl shadow-lg p-6 hover:shadow-xl transition-shadow">
<div class="flex flex-col lg:flex-row lg:items-start justify-between gap-4">
<!-- Route & Info -->
<div class="flex-1">
<div class="flex items-center gap-3 mb-3">
${getStatusBadge(load.status)}
<span class="text-xs text-slate-400 font-medium">${load.id}</span>
</div>
<!-- Route -->
<div class="flex items-center gap-3 mb-3">
<div class="text-center">
<p class="font-bold text-slate-900 text-lg">${load.origin.city}, ${load.origin.state}</p>
</div>
<div class="flex-shrink-0 text-amber-500 text-2xl px-2">→</div>
<div class="text-center">
<p class="font-bold text-slate-900 text-lg">${load.destination.city}, ${load.destination.state}</p>
</div>
</div>
<!-- Description -->
<p class="text-slate-600 mb-3">${load.description}</p>
<!-- Carrier -->
<p class="text-sm text-slate-500">Posted by <span class="font-semibold text-slate-700">${load.carrier}</span></p>
</div>
<!-- Details Sidebar -->
<div class="lg:w-72 flex-shrink-0 bg-slate-50 rounded-xl p-4 space-y-3">
<div class="flex justify-between">
<span class="text-sm text-slate-500">Departure</span>
<span class="text-sm font-bold text-slate-900">${new Date(load.departureDate).toLocaleDateString('en-US', {month:'short', day:'numeric', year:'numeric'})} <span class="text-amber-600">(${daysUntil(load.departureDate)})</span></span>
</div>
<div class="border-t border-slate-200"></div>
<div class="flex justify-between">
<span class="text-sm text-slate-500">Width</span>
<span class="text-sm font-bold text-slate-900">${load.dimensions.width}</span>
</div>
<div class="flex justify-between">
<span class="text-sm text-slate-500">Height</span>
<span class="text-sm font-bold text-slate-900">${load.dimensions.height}</span>
</div>
<div class="flex justify-between">
<span class="text-sm text-slate-500">Length</span>
<span class="text-sm font-bold text-slate-900">${load.dimensions.length}</span>
</div>
<div class="flex justify-between">
<span class="text-sm text-slate-500">Weight</span>
<span class="text-sm font-bold text-slate-900">${load.dimensions.weight}</span>
</div>
<div class="border-t border-slate-200"></div>
<div class="flex justify-between items-center">
<span class="text-sm text-slate-500">Escorts Needed</span>
<span class="bg-amber-100 text-amber-800 text-xs font-bold px-3 py-1 rounded-full">${load.escortsNeeded} vehicle${load.escortsNeeded > 1 ? 's' : ''}</span>
</div>
${load.status === 'posted' ? `
<button class="w-full mt-2 bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold py-2 rounded-lg transition-colors text-sm">
Contact Carrier
</button>
` : ''}
</div>
</div>
</div>
`).join('');
}
function filterLoads() {
const search = document.getElementById('search-input').value.toLowerCase();
const statusFilter = document.getElementById('status-filter').value;
const escortsFilter = document.getElementById('escorts-filter').value;
const sortBy = document.getElementById('sort-select').value;
let filtered = MOCK_LOAD_BOARD.filter(load => {
if (statusFilter !== 'all' && load.status !== statusFilter) return false;
if (escortsFilter !== 'all' && load.escortsNeeded !== parseInt(escortsFilter)) return false;
if (search) {
const searchText = `${load.origin.city} ${load.origin.state} ${load.destination.city} ${load.destination.state} ${load.carrier} ${load.description}`.toLowerCase();
if (!searchText.includes(search)) return false;
}
return true;
});
// Sort
filtered.sort((a, b) => {
if (sortBy === 'date_asc') return new Date(a.departureDate) - new Date(b.departureDate);
if (sortBy === 'date_desc') return new Date(b.departureDate) - new Date(a.departureDate);
if (sortBy === 'posted_desc') return new Date(b.postedDate) - new Date(a.postedDate);
return 0;
});
renderLoads(filtered);
}
// Initial render
filterLoads();
function showPostModal() {
document.getElementById('post-modal').classList.remove('hidden');
}
function closePostModal() {
document.getElementById('post-modal').classList.add('hidden');
}
function handlePostLoad(e) {
e.preventDefault();
closePostModal();
alert('Load posted! (POC demo — in production, this would create a real listing.)');
}
// Close modal on backdrop click
document.getElementById('post-modal').addEventListener('click', function(e) {
if (e.target === this) closePostModal();
});
})();
</script>
</body>
</html>

305
public/pages/locator.html Normal file
View File

@@ -0,0 +1,305 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Find Escort Vehicles | 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>
#locator-map { height: 500px; width: 100%; border-radius: 0.75rem; }
.operator-card.active { ring: 2px solid #f59e0b; }
</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">Find Escort Vehicles</h1>
<p class="text-lg text-gray-400 max-w-3xl">Browse available pilot/escort vehicle operators near your load's departure point. Click a marker on the map or browse the list below.</p>
</div>
</section>
<!-- Filter Bar -->
<section class="max-w-7xl mx-auto px-4 pt-8">
<div class="bg-white rounded-2xl shadow-lg p-6">
<div class="flex flex-col md:flex-row gap-4">
<div class="flex-1">
<label class="block text-sm font-semibold text-slate-700 mb-1">Search by State or Name</label>
<input type="text" id="op-search" oninput="filterOperators()" placeholder="e.g. Texas, Mike's Pilot Car..." 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">
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Status</label>
<select id="op-status" onchange="filterOperators()" class="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="all">All</option>
<option value="available">Available Now</option>
<option value="on_job">On a Job</option>
</select>
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Certified In</label>
<select id="op-cert" onchange="filterOperators()" class="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="all">Any State</option>
</select>
</div>
</div>
</div>
</section>
<!-- Map + List Layout -->
<section class="max-w-7xl mx-auto px-4 py-8">
<div class="grid lg:grid-cols-5 gap-6">
<!-- Map (3 cols) -->
<div class="lg:col-span-3">
<div class="bg-white rounded-2xl shadow-lg p-4">
<div id="locator-map"></div>
</div>
<!-- Legend -->
<div class="flex gap-6 mt-3 px-2 text-sm text-slate-500">
<div class="flex items-center gap-2">
<span class="w-4 h-4 rounded-full bg-green-500 inline-block border-2 border-white shadow"></span>
Available
</div>
<div class="flex items-center gap-2">
<span class="w-4 h-4 rounded-full bg-amber-500 inline-block border-2 border-white shadow"></span>
On a Job
</div>
</div>
</div>
<!-- Operator List (2 cols) -->
<div class="lg:col-span-2">
<div id="operator-count" class="text-sm text-slate-500 font-medium mb-3"></div>
<div id="operator-list" class="space-y-4 max-h-[560px] overflow-y-auto pr-1">
<!-- Populated by JS -->
</div>
</div>
</div>
</section>
<!-- Operator Detail Panel -->
<section id="operator-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="op-detail-name" class="text-2xl font-bold text-slate-900"></h2>
<button onclick="document.getElementById('operator-detail').classList.add('hidden')" class="text-slate-400 hover:text-slate-600 text-2xl">&times;</button>
</div>
<div id="op-detail-content">
<!-- Populated by JS -->
</div>
</div>
</section>
<!-- CTA for Operators -->
<section class="max-w-7xl mx-auto px-4 pb-8">
<div class="bg-gradient-to-r from-slate-800 to-slate-900 rounded-2xl p-8 text-white">
<div class="grid md:grid-cols-2 gap-8 items-center">
<div>
<h3 class="text-xl font-bold mb-2">Are You an Escort Vehicle Operator?</h3>
<p class="text-gray-400">Add your location to our map and get discovered by carriers and truck drivers looking for escort services in your area.</p>
</div>
<div class="text-center md:text-right">
<button onclick="alert('Coming soon! In production, this would open a registration/profile creation flow.')" class="bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold px-6 py-3 rounded-lg transition-colors">
Register Your Service →
</button>
</div>
</div>
</div>
</section>
<div id="main-footer"></div>
<script src="/js/api.js"></script>
<script src="/js/nav.js"></script>
<script>
renderNav('locator');
renderBanner();
renderFooter();
(async () => {
const MOCK_ESCORT_OPERATORS = await PilotEdge.getEscortOperators();
// Populate certification filter
const allCerts = new Set();
MOCK_ESCORT_OPERATORS.forEach(op => op.certifications.forEach(c => allCerts.add(c)));
const certSelect = document.getElementById('op-cert');
Array.from(allCerts).sort().forEach(state => {
certSelect.add(new Option(state, state));
});
// Initialize map
const locatorMap = L.map('locator-map').setView([37.5, -95], 4);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap contributors',
maxZoom: 18
}).addTo(locatorMap);
let markers = {};
function addOperatorMarkers(operators) {
// Clear existing markers
Object.values(markers).forEach(m => locatorMap.removeLayer(m));
markers = {};
operators.forEach(op => {
const color = op.status === 'available' ? '#22c55e' : '#f59e0b';
const marker = L.circleMarker([op.location.lat, op.location.lng], {
radius: 10,
fillColor: color,
color: '#fff',
weight: 2,
opacity: 1,
fillOpacity: 0.9
}).addTo(locatorMap);
marker.bindPopup(`
<div style="min-width:200px;">
<strong style="font-size:14px;">${op.name}</strong><br>
<span style="color:#666; font-size:12px;">${op.location.city}, ${op.location.state}</span><br>
<span style="color:${op.status === 'available' ? '#16a34a' : '#d97706'}; font-size:12px; font-weight:600;">${op.status === 'available' ? '● Available' : '● On a Job'}</span><br>
<span style="font-size:12px;">⭐ ${op.rating} · ${op.totalJobs} jobs</span><br><br>
<button onclick="showOperatorDetail('${op.id}')" style="background:#f59e0b; color:#0f172a; font-weight:700; padding:6px 16px; border-radius:6px; border:none; cursor:pointer; width:100%;">
View Profile
</button>
</div>
`);
markers[op.id] = marker;
});
}
function renderOperatorList(operators) {
const container = document.getElementById('operator-list');
document.getElementById('operator-count').textContent = `${operators.length} operator${operators.length !== 1 ? 's' : ''} found`;
if (operators.length === 0) {
container.innerHTML = `<div class="bg-white rounded-xl p-8 text-center text-slate-500">No operators match your filters.</div>`;
return;
}
container.innerHTML = operators.map(op => `
<div class="bg-white rounded-xl shadow-md p-5 hover:shadow-lg transition-shadow cursor-pointer" onclick="showOperatorDetail('${op.id}')">
<div class="flex items-start justify-between mb-2">
<h3 class="font-bold text-slate-900">${op.name}</h3>
<span class="flex-shrink-0 w-3 h-3 rounded-full ${op.status === 'available' ? 'bg-green-500' : 'bg-amber-500'} mt-1.5"></span>
</div>
<p class="text-sm text-slate-500 mb-2">${op.location.city}, ${op.location.state}</p>
<div class="flex items-center gap-3 text-sm mb-3">
<span class="text-amber-500">⭐ ${op.rating}</span>
<span class="text-slate-400">·</span>
<span class="text-slate-600">${op.totalJobs} jobs</span>
<span class="text-slate-400">·</span>
<span class="text-slate-600">${op.experience}</span>
</div>
<div class="flex flex-wrap gap-1.5">
${op.certifications.map(c => `<span class="bg-slate-100 text-slate-600 text-xs font-medium px-2 py-0.5 rounded">${c}</span>`).join('')}
</div>
</div>
`).join('');
}
function showOperatorDetail(id) {
const op = MOCK_ESCORT_OPERATORS.find(o => o.id === id);
if (!op) return;
// Center map on operator
locatorMap.setView([op.location.lat, op.location.lng], 7);
if (markers[id]) markers[id].openPopup();
document.getElementById('op-detail-name').textContent = op.name;
document.getElementById('op-detail-content').innerHTML = `
<div class="grid md:grid-cols-2 gap-8">
<div>
<div class="flex items-center gap-3 mb-4">
<span class="inline-block w-4 h-4 rounded-full ${op.status === 'available' ? 'bg-green-500' : 'bg-amber-500'}"></span>
<span class="font-semibold ${op.status === 'available' ? 'text-green-700' : 'text-amber-700'}">${op.status === 'available' ? 'Available for Jobs' : 'Currently on a Job'}</span>
</div>
<div class="space-y-3">
<div class="bg-slate-50 px-4 py-3 rounded-lg">
<span class="text-sm text-slate-500 block">Location</span>
<span class="font-semibold text-slate-900">${op.location.city}, ${op.location.state}</span>
</div>
<div class="bg-slate-50 px-4 py-3 rounded-lg">
<span class="text-sm text-slate-500 block">Experience</span>
<span class="font-semibold text-slate-900">${op.experience} · ${op.totalJobs} completed jobs</span>
</div>
<div class="bg-slate-50 px-4 py-3 rounded-lg">
<span class="text-sm text-slate-500 block">Rating</span>
<span class="font-semibold text-slate-900">⭐ ${op.rating} / 5.0</span>
</div>
<div class="bg-slate-50 px-4 py-3 rounded-lg">
<span class="text-sm text-slate-500 block">Vehicle</span>
<span class="font-semibold text-slate-900">${op.vehicleType}</span>
</div>
</div>
</div>
<div>
<div class="mb-4">
<h3 class="font-bold text-slate-900 mb-2">Certified In</h3>
<div class="flex flex-wrap gap-2">
${op.certifications.map(c => `<span class="bg-amber-100 text-amber-800 text-sm font-semibold px-3 py-1 rounded-full">${c}</span>`).join('')}
</div>
</div>
<div class="mb-4">
<h3 class="font-bold text-slate-900 mb-2">About</h3>
<p class="text-slate-600">${op.bio}</p>
</div>
<div class="bg-slate-50 rounded-xl p-4 space-y-2">
<h3 class="font-bold text-slate-900 mb-2">Contact</h3>
<p class="text-sm"><span class="text-slate-500">Email:</span> <a href="mailto:${op.contact}" class="text-amber-600 hover:text-amber-700 font-medium">${op.contact}</a></p>
<p class="text-sm"><span class="text-slate-500">Phone:</span> <a href="tel:${op.phone}" class="text-amber-600 hover:text-amber-700 font-medium">${op.phone}</a></p>
</div>
${op.status === 'available' ? `
<a href="order.html" class="block mt-4 bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold py-3 rounded-lg transition-colors text-center">
Request This Operator
</a>
` : `
<div class="mt-4 bg-slate-100 text-slate-500 font-medium py-3 rounded-lg text-center text-sm">
Currently unavailable — check back later
</div>
`}
</div>
</div>
`;
const detailEl = document.getElementById('operator-detail');
detailEl.classList.remove('hidden');
detailEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function filterOperators() {
const search = document.getElementById('op-search').value.toLowerCase();
const statusFilter = document.getElementById('op-status').value;
const certFilter = document.getElementById('op-cert').value;
let filtered = MOCK_ESCORT_OPERATORS.filter(op => {
if (statusFilter !== 'all' && op.status !== statusFilter) return false;
if (certFilter !== 'all' && !op.certifications.includes(certFilter)) return false;
if (search) {
const searchText = `${op.name} ${op.location.city} ${op.location.state} ${op.certifications.join(' ')} ${op.bio}`.toLowerCase();
if (!searchText.includes(search)) return false;
}
return true;
});
renderOperatorList(filtered);
addOperatorMarkers(filtered);
}
// Initial render
filterOperators();
})();
</script>
</body>
</html>

241
public/pages/order.html Normal file
View File

@@ -0,0 +1,241 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Request Escort Service | PilotEdge</title>
<script src="https://cdn.tailwindcss.com"></script>
</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">Request Escort Vehicle Service</h1>
<p class="text-lg text-gray-400 max-w-3xl">Tell us about your load and route — we'll match you with available escort/pilot vehicles and get back to you promptly.</p>
</div>
</section>
<!-- Order Form -->
<section class="max-w-4xl mx-auto px-4 py-8">
<form id="order-form" onsubmit="handleSubmit(event)" class="space-y-8">
<!-- Contact Information -->
<div class="bg-white rounded-2xl shadow-lg p-8">
<h2 class="text-xl font-bold text-slate-900 mb-6 flex items-center">
<span class="w-8 h-8 bg-amber-100 rounded-lg flex items-center justify-center text-amber-600 mr-3 text-sm font-bold">1</span>
Contact Information
</h2>
<div class="grid md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Full Name <span class="text-red-500">*</span></label>
<input type="text" name="name" required 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" placeholder="John Smith">
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Company Name</label>
<input type="text" name="company" 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" placeholder="ABC Trucking LLC">
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Email <span class="text-red-500">*</span></label>
<input type="email" name="email" required 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" placeholder="john@abctrucking.com">
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Phone <span class="text-red-500">*</span></label>
<input type="tel" name="phone" required 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" placeholder="(555) 123-4567">
</div>
</div>
</div>
<!-- Load Details -->
<div class="bg-white rounded-2xl shadow-lg p-8">
<h2 class="text-xl font-bold text-slate-900 mb-6 flex items-center">
<span class="w-8 h-8 bg-amber-100 rounded-lg flex items-center justify-center text-amber-600 mr-3 text-sm font-bold">2</span>
Load Details
</h2>
<div class="mb-4">
<label class="block text-sm font-semibold text-slate-700 mb-1">Load Description <span class="text-red-500">*</span></label>
<textarea name="load_desc" required rows="3" 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" placeholder="e.g. Wind turbine blade, 135' long, loaded on extendable trailer"></textarea>
</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Width</label>
<input type="text" name="width" 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" placeholder="16'2&quot;">
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Height</label>
<input type="text" name="height" 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" placeholder="14'8&quot;">
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Overall Length</label>
<input type="text" name="length" 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" placeholder="135'">
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Gross Weight</label>
<input type="text" name="weight" 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" placeholder="185,000 lbs">
</div>
</div>
<div class="grid md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Trailer Type</label>
<select name="trailer_type" 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="">Select...</option>
<option>Lowboy / RGN</option>
<option>Flatbed</option>
<option>Step Deck</option>
<option>Extendable / Stretch</option>
<option>Double Drop</option>
<option>Multi-Axle / Schnabel</option>
<option>Perimeter (Beam/Bolster)</option>
<option>Other</option>
</select>
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Number of Axles</label>
<select name="axles" 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="">Select...</option>
<option>5 axles (standard)</option>
<option>6 axles</option>
<option>7 axles</option>
<option>8 axles</option>
<option>9+ axles</option>
<option>13+ axles (superload)</option>
</select>
</div>
</div>
</div>
<!-- Route Details -->
<div class="bg-white rounded-2xl shadow-lg p-8">
<h2 class="text-xl font-bold text-slate-900 mb-6 flex items-center">
<span class="w-8 h-8 bg-amber-100 rounded-lg flex items-center justify-center text-amber-600 mr-3 text-sm font-bold">3</span>
Route & Schedule
</h2>
<div class="grid md:grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Pickup Location <span class="text-red-500">*</span></label>
<input type="text" name="pickup" required 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" placeholder="City, State or full address">
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Delivery Location <span class="text-red-500">*</span></label>
<input type="text" name="delivery" required 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" placeholder="City, State or full address">
</div>
</div>
<div class="grid md:grid-cols-3 gap-4 mb-4">
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Departure Date <span class="text-red-500">*</span></label>
<input type="date" name="departure_date" required 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">
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Flexibility</label>
<select name="flexibility" 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>Exact date</option>
<option>± 1 day</option>
<option>± 2-3 days</option>
<option>± 1 week</option>
<option>Flexible</option>
</select>
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">States on Route</label>
<input type="text" name="states" 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" placeholder="e.g. TX, OK, KS">
</div>
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Do you already have permits?</label>
<div class="flex gap-4 mt-1">
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" name="has_permits" value="yes" class="accent-amber-500"> <span class="text-slate-700">Yes, permits are in hand</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" name="has_permits" value="no" checked class="accent-amber-500"> <span class="text-slate-700">No, still need permits</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" name="has_permits" value="some" class="accent-amber-500"> <span class="text-slate-700">Some states</span>
</label>
</div>
</div>
</div>
<!-- Escort Requirements -->
<div class="bg-white rounded-2xl shadow-lg p-8">
<h2 class="text-xl font-bold text-slate-900 mb-6 flex items-center">
<span class="w-8 h-8 bg-amber-100 rounded-lg flex items-center justify-center text-amber-600 mr-3 text-sm font-bold">4</span>
Escort Requirements
</h2>
<div class="grid md:grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Escort Vehicles Needed</label>
<select name="escorts_needed" 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>1 (front only)</option>
<option>1 (rear only)</option>
<option>2 (front and rear)</option>
<option>3+ (complex move)</option>
<option>Not sure — help me determine</option>
</select>
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Height Pole Required?</label>
<select name="height_pole" 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>No</option>
<option>Yes</option>
<option>Not sure</option>
</select>
</div>
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Special Requirements or Notes</label>
<textarea name="notes" rows="4" 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" placeholder="Any additional details — night travel needs, multiple loads, specific equipment requirements, etc."></textarea>
</div>
</div>
<!-- Submit -->
<div class="flex flex-col sm:flex-row gap-4 items-center justify-between">
<p class="text-sm text-slate-500">Fields marked with <span class="text-red-500">*</span> are required. We typically respond within 2-4 hours.</p>
<button type="submit" class="bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold px-8 py-4 rounded-xl text-lg transition-colors shadow-lg hover:shadow-xl whitespace-nowrap">
Submit Request →
</button>
</div>
</form>
<!-- Success Message (hidden by default) -->
<div id="success-message" class="hidden">
<div class="bg-white rounded-2xl shadow-lg p-12 text-center">
<div class="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center text-4xl mx-auto mb-6"></div>
<h2 class="text-2xl font-bold text-slate-900 mb-3">Request Submitted!</h2>
<p class="text-slate-600 mb-6 max-w-md mx-auto">Thank you for your escort service request. We've received your details and will get back to you within 2-4 hours with availability and pricing.</p>
<div class="bg-slate-50 rounded-xl p-4 mb-6 inline-block">
<p class="text-sm text-slate-500">Reference Number</p>
<p id="ref-number" class="text-xl font-bold text-slate-900">PE-2026-0001</p>
</div>
<div>
<a href="index.html" class="inline-block bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold px-6 py-3 rounded-lg transition-colors">
← Back to Home
</a>
</div>
</div>
</div>
</section>
<div id="main-footer"></div>
<script src="/js/nav.js"></script>
<script>
renderNav('order');
renderBanner();
renderFooter();
function handleSubmit(e) {
e.preventDefault();
// In production, this would POST to an API
const refNum = 'PE-2026-' + String(Math.floor(Math.random() * 9000) + 1000);
document.getElementById('ref-number').textContent = refNum;
document.getElementById('order-form').classList.add('hidden');
document.getElementById('success-message').classList.remove('hidden');
window.scrollTo({ top: 0, behavior: 'smooth' });
}
</script>
</body>
</html>

View File

@@ -0,0 +1,444 @@
<!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="/js/api.js"></script>
<script src="/js/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>

View File

@@ -0,0 +1,244 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Truck Stops & Parking | 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; }
</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">Truck Stops &amp; Parking for Oversize Loads</h1>
<p class="text-lg text-gray-400 max-w-3xl">Find oversize-friendly truck stops, rest areas, and staging locations across the US. Community-verified with driver comments and entrance dimensions.</p>
</div>
</section>
<!-- Filter Bar -->
<section class="max-w-7xl mx-auto px-4 pt-8 w-full">
<div class="bg-white rounded-2xl shadow-lg p-6">
<div class="flex flex-col md:flex-row gap-4 items-end">
<div class="flex-1">
<label class="block text-sm font-semibold text-slate-700 mb-1">Search Truck Stops</label>
<input type="text" id="ts-search" oninput="filterStops()" placeholder="e.g. Iowa 80, Amarillo, TX..." 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">
</div>
<div class="flex items-center gap-2 pb-1">
<input type="checkbox" id="ts-oversize-only" onchange="filterStops()" class="w-4 h-4 text-amber-500 border-slate-300 rounded focus:ring-amber-400">
<label for="ts-oversize-only" class="text-sm font-semibold text-slate-700 whitespace-nowrap">Oversize Friendly Only</label>
</div>
</div>
</div>
</section>
<!-- Map -->
<section class="max-w-7xl mx-auto px-4 pt-8 w-full">
<div class="bg-white rounded-2xl shadow-lg p-4">
<div id="map"></div>
</div>
</section>
<!-- Truck Stop Cards -->
<section class="max-w-7xl mx-auto px-4 pt-8 pb-8 w-full">
<div id="ts-list" class="space-y-6"></div>
</section>
<!-- Submit a Location -->
<section class="max-w-7xl mx-auto px-4 pb-12 w-full">
<div class="bg-amber-50 border-2 border-amber-200 rounded-2xl p-8 text-center">
<h2 class="text-2xl font-bold text-slate-900 mb-2">Know an Oversize-Friendly Location?</h2>
<p class="text-slate-600 mb-4">Help the community by suggesting truck stops, rest areas, or staging lots that accommodate oversize loads.</p>
<button onclick="alert('POC: This would open a submission form for new truck stop locations. Feature coming in production release.')" class="bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold px-6 py-3 rounded-lg transition-colors shadow-md hover:shadow-lg">
📍 Submit a Location
</button>
</div>
</section>
<div id="main-footer"></div>
<script src="/js/api.js"></script>
<script src="/js/nav.js"></script>
<script>
renderNav('truckstops');
renderBanner();
renderFooter();
(async () => {
const MOCK_TRUCK_STOPS = await PilotEdge.getTruckStops();
// Facility emoji mapping
const facilityIcons = {
fuel: '⛽', food: '🍔', restrooms: '🚻', showers: '🚿',
mechanic: '🔧', scale: '⚖️', ev_charging: '🔌', hotel: '🏨',
trucking_museum: '🏛️', barber: '💈', chiropractor: '🦴', iron_skillet: '🍳'
};
let map, markers = [];
function initMap() {
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);
addMarkers(MOCK_TRUCK_STOPS);
}
function addMarkers(stops) {
markers.forEach(m => map.removeLayer(m));
markers = [];
stops.forEach(stop => {
const color = stop.oversizeFriendly ? '#22c55e' : '#ef4444';
const marker = L.circleMarker([stop.location.lat, stop.location.lng], {
radius: 9, fillColor: color, color: '#fff', weight: 2, opacity: 1, fillOpacity: 0.85
}).addTo(map);
marker.bindPopup(`
<div style="min-width:200px">
<strong style="font-size:14px">${stop.name}</strong><br>
<span style="color:#64748b">${stop.location.city}, ${stop.location.state}</span><br>
<span style="display:inline-block;margin-top:4px;padding:2px 8px;border-radius:9999px;font-size:11px;font-weight:600;color:#fff;background:${stop.oversizeFriendly ? '#22c55e' : '#ef4444'}">
${stop.oversizeFriendly ? 'OVERSIZE FRIENDLY' : 'NOT RECOMMENDED'}
</span><br>
<button onclick="scrollToCard('${stop.id}')" style="margin-top:8px;background:#f59e0b;color:#0f172a;border:none;padding:6px 14px;border-radius:8px;font-weight:700;font-size:12px;cursor:pointer">View Details</button>
</div>
`);
markers.push(marker);
});
}
function scrollToCard(id) {
const el = document.getElementById('card-' + id);
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
el.classList.add('ring-2', 'ring-amber-400');
setTimeout(() => el.classList.remove('ring-2', 'ring-amber-400'), 2000);
}
}
function renderStops(stops) {
const list = document.getElementById('ts-list');
if (!stops.length) {
list.innerHTML = '<div class="text-center py-12 text-slate-500">No truck stops match your filters.</div>';
return;
}
list.innerHTML = stops.map(stop => `
<div id="card-${stop.id}" class="bg-white rounded-2xl shadow-lg p-6 transition-all duration-300">
<div class="flex flex-col md:flex-row md:items-start md:justify-between gap-4 mb-4">
<div>
<h3 class="text-xl font-bold text-slate-900">${stop.name}</h3>
<p class="text-slate-500">${stop.location.city}, ${stop.location.state}</p>
</div>
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-bold whitespace-nowrap ${stop.oversizeFriendly ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}">
${stop.oversizeFriendly ? '✅ OVERSIZE FRIENDLY' : '⛔ NOT RECOMMENDED FOR OVERSIZE'}
</span>
</div>
<!-- Specs Grid -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4">
<div class="bg-slate-50 rounded-xl p-3 text-center">
<div class="text-xs text-slate-500 font-semibold mb-1">Entrance Width</div>
<div class="text-sm font-bold text-slate-800">${stop.entranceWidth}</div>
</div>
<div class="bg-slate-50 rounded-xl p-3 text-center">
<div class="text-xs text-slate-500 font-semibold mb-1">Entrance Height</div>
<div class="text-sm font-bold text-slate-800">${stop.entranceHeight}</div>
</div>
<div class="bg-slate-50 rounded-xl p-3 text-center">
<div class="text-xs text-slate-500 font-semibold mb-1">Lot Size</div>
<div class="text-sm font-bold text-slate-800">${stop.lotSize}</div>
</div>
<div class="bg-slate-50 rounded-xl p-3 text-center">
<div class="text-xs text-slate-500 font-semibold mb-1">Oversize Capacity</div>
<div class="text-sm font-bold text-slate-800">${stop.oversizeCapacity}</div>
</div>
</div>
<!-- Facilities -->
<div class="flex flex-wrap gap-2 mb-4">
${stop.facilities.map(f => `
<span class="bg-slate-100 rounded-full px-3 py-1 text-xs font-medium text-slate-700">
${facilityIcons[f] || '📌'} ${f.replace(/_/g, ' ')}
</span>
`).join('')}
</div>
<!-- Description -->
<p class="text-sm text-slate-600 mb-5">${stop.description}</p>
<!-- Comments Section -->
<div class="border-t border-slate-200 pt-4">
<h4 class="text-sm font-bold text-slate-900 mb-3">💬 Driver Comments (${stop.comments.length})</h4>
<div class="space-y-3 mb-4">
${stop.comments.map(c => `
<div class="bg-slate-50 rounded-xl p-3">
<div class="flex items-center gap-2 mb-1">
<span class="text-xs font-bold text-slate-700">${c.user}</span>
<span class="text-xs text-slate-400">${c.date}</span>
</div>
<p class="text-sm text-slate-600">${c.text}</p>
</div>
`).join('')}
</div>
<div id="comment-form-${stop.id}" class="hidden">
<textarea id="comment-text-${stop.id}" rows="3" placeholder="Share your experience at this location..." 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 mb-2"></textarea>
<div class="flex gap-2">
<button onclick="submitComment('${stop.id}')" class="bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold px-4 py-2 rounded-lg text-sm transition-colors">Submit Comment</button>
<button onclick="toggleCommentForm('${stop.id}')" class="bg-slate-200 hover:bg-slate-300 text-slate-700 font-bold px-4 py-2 rounded-lg text-sm transition-colors">Cancel</button>
</div>
</div>
<button id="comment-btn-${stop.id}" onclick="toggleCommentForm('${stop.id}')" class="text-sm font-semibold text-amber-600 hover:text-amber-700 transition-colors">
+ Add Comment
</button>
</div>
</div>
`).join('');
}
function toggleCommentForm(id) {
const form = document.getElementById('comment-form-' + id);
const btn = document.getElementById('comment-btn-' + id);
const visible = !form.classList.contains('hidden');
form.classList.toggle('hidden');
btn.classList.toggle('hidden');
}
function submitComment(id) {
const text = document.getElementById('comment-text-' + id).value.trim();
if (!text) { alert('Please enter a comment.'); return; }
alert('POC: Comment submitted! In production, this would save to the database.\n\nYour comment: "' + text + '"');
document.getElementById('comment-text-' + id).value = '';
toggleCommentForm(id);
}
function filterStops() {
const query = document.getElementById('ts-search').value.toLowerCase();
const oversizeOnly = document.getElementById('ts-oversize-only').checked;
const filtered = MOCK_TRUCK_STOPS.filter(stop => {
const matchSearch = !query ||
stop.name.toLowerCase().includes(query) ||
stop.location.city.toLowerCase().includes(query) ||
stop.location.state.toLowerCase().includes(query);
const matchOversize = !oversizeOnly || stop.oversizeFriendly;
return matchSearch && matchOversize;
});
renderStops(filtered);
addMarkers(filtered);
}
// Initialize
initMap();
renderStops(MOCK_TRUCK_STOPS);
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,269 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Weigh Stations | 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; }
.toast-msg {
position: fixed; bottom: 1.5rem; right: 1.5rem; z-index: 9999;
background: #0f172a; color: #fbbf24; padding: 0.75rem 1.25rem;
border-radius: 0.75rem; font-weight: 600; box-shadow: 0 8px 30px rgba(0,0,0,.25);
animation: slideUp .3s ease-out;
}
@keyframes slideUp { from { opacity:0; transform:translateY(12px); } to { opacity:1; transform:translateY(0); } }
</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">Weigh Stations &amp; Inspection Stations</h1>
<p class="text-lg text-gray-400 max-w-3xl">Live crowd-sourced status — see which stations are open or closed right now.</p>
</div>
</section>
<!-- Stats Bar -->
<section class="max-w-7xl mx-auto px-4 -mt-6 w-full relative z-10">
<div id="stats-bar" class="grid grid-cols-3 gap-4"></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>
<!-- Filter Bar -->
<section class="max-w-7xl mx-auto px-4 pb-6 w-full">
<div class="bg-white rounded-2xl shadow-lg p-6">
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Search</label>
<input id="filter-search" type="text" placeholder="Name, state, or route…"
class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Status</label>
<select id="filter-status"
class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
<option value="all">All Stations</option>
<option value="open">Open Only</option>
<option value="closed">Closed Only</option>
</select>
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">PrePass</label>
<select id="filter-prepass"
class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
<option value="all">Any</option>
<option value="yes">PrePass Accepted</option>
<option value="no">No PrePass</option>
</select>
</div>
<div class="flex items-end">
<button id="btn-clear-filters"
class="w-full bg-slate-200 hover:bg-slate-300 text-slate-700 font-semibold px-4 py-2 rounded-lg text-sm transition-colors">
Clear Filters
</button>
</div>
</div>
</div>
</section>
<!-- Station Cards -->
<section class="max-w-7xl mx-auto px-4 pb-16 w-full">
<div id="station-list" class="grid md:grid-cols-2 xl:grid-cols-3 gap-6"></div>
<p id="no-results" class="hidden text-center text-slate-500 py-12 text-lg">No stations match your filters.</p>
</section>
<div id="main-footer"></div>
<script src="/js/api.js"></script>
<script src="/js/nav.js"></script>
<script>
renderNav('weighstations');
renderBanner();
renderFooter();
(async () => {
const MOCK_WEIGH_STATIONS = await PilotEdge.getWeighStations();
// ── Local mutable copy of station data ──
const stations = JSON.parse(JSON.stringify(MOCK_WEIGH_STATIONS));
// ── Map setup ──
const map = L.map('map').setView([39.5, -98.35], 4);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap contributors', maxZoom: 18
}).addTo(map);
// Store markers keyed by station id so flag buttons can update them
const markerMap = {};
function markerColor(status) {
return status === 'open' ? '#22c55e' : '#ef4444';
}
function buildPopup(s) {
const badge = s.currentStatus === 'open'
? '<span style="background:#22c55e;color:#fff;padding:2px 8px;border-radius:9999px;font-size:12px;font-weight:700;">OPEN</span>'
: '<span style="background:#ef4444;color:#fff;padding:2px 8px;border-radius:9999px;font-size:12px;font-weight:700;">CLOSED</span>';
return `<div style="min-width:200px;">
<strong style="font-size:14px;">${s.name}</strong><br>
<span style="color:#64748b;font-size:12px;">${s.route} · ${s.location.city}, ${s.location.state}</span><br>
<div style="margin:6px 0;">${badge}</div>
<span style="font-size:12px;"><strong>Hours:</strong> ${s.hours}</span><br>
<span style="font-size:11px;color:#94a3b8;">Last flagged: ${fmtTime(s.lastFlagged)} by ${s.flaggedBy}</span>
</div>`;
}
function addMarker(s) {
const m = L.circleMarker([s.location.lat, s.location.lng], {
radius: 9, fillColor: markerColor(s.currentStatus), color: '#fff',
weight: 2, fillOpacity: 0.9
}).addTo(map).bindPopup(buildPopup(s));
markerMap[s.id] = m;
}
stations.forEach(addMarker);
// ── Helpers ──
function fmtTime(iso) {
const d = new Date(iso);
return d.toLocaleString('en-US', { month:'short', day:'numeric', hour:'numeric', minute:'2-digit' });
}
function showToast(msg) {
const el = document.createElement('div');
el.className = 'toast-msg';
el.textContent = msg;
document.body.appendChild(el);
setTimeout(() => el.remove(), 2500);
}
// ── Stats bar ──
function renderStats() {
const open = stations.filter(s => s.currentStatus === 'open').length;
const closed = stations.length - open;
document.getElementById('stats-bar').innerHTML = [
{ label:'Open Stations', value:open, color:'bg-green-500' },
{ label:'Closed Stations', value:closed, color:'bg-red-500' },
{ label:'Total Stations', value:stations.length, color:'bg-amber-500' }
].map(s => `
<div class="bg-white rounded-2xl shadow-lg p-5 flex items-center gap-4">
<div class="${s.color} text-white rounded-xl w-12 h-12 flex items-center justify-center text-xl font-bold">${s.value}</div>
<span class="text-slate-700 font-semibold text-sm">${s.label}</span>
</div>
`).join('');
}
renderStats();
// ── Card rendering ──
function renderCards(list) {
const container = document.getElementById('station-list');
const noResults = document.getElementById('no-results');
if (!list.length) { container.innerHTML = ''; noResults.classList.remove('hidden'); return; }
noResults.classList.add('hidden');
container.innerHTML = list.map(s => {
const isOpen = s.currentStatus === 'open';
const badge = isOpen
? '<span class="inline-block bg-green-500 text-white text-xs font-bold px-3 py-1 rounded-full uppercase tracking-wide">Open</span>'
: '<span class="inline-block bg-red-500 text-white text-xs font-bold px-3 py-1 rounded-full uppercase tracking-wide">Closed</span>';
const prePassBadge = s.prePass
? '<span class="inline-block bg-blue-100 text-blue-700 text-xs font-semibold px-2.5 py-0.5 rounded-full">✓ PrePass</span>'
: '<span class="inline-block bg-slate-100 text-slate-500 text-xs font-semibold px-2.5 py-0.5 rounded-full">No PrePass</span>';
return `
<div class="bg-white rounded-2xl shadow-lg p-6 flex flex-col justify-between" id="card-${s.id}">
<div>
<div class="flex items-start justify-between gap-2 mb-2">
<h3 class="text-lg font-bold text-slate-900 leading-tight">${s.name}</h3>
${badge}
</div>
<p class="text-sm text-slate-500 mb-1">${s.route} · ${s.location.city}, ${s.location.state}</p>
<p class="text-xs text-slate-400 mb-3">Last reported: ${fmtTime(s.lastFlagged)} by <span class="font-medium text-slate-600">${s.flaggedBy}</span></p>
<div class="flex flex-wrap gap-2 mb-3">
<span class="inline-block bg-slate-100 text-slate-600 text-xs font-semibold px-2.5 py-0.5 rounded-full">🕒 ${s.hours}</span>
${prePassBadge}
</div>
<p class="text-sm text-slate-600 leading-relaxed">${s.notes}</p>
</div>
<div class="flex gap-2 mt-5">
<button onclick="flagStation('${s.id}','open')"
class="flex-1 text-sm font-semibold px-3 py-2 rounded-lg transition-colors ${isOpen ? 'bg-green-500 text-white' : 'bg-green-100 text-green-700 hover:bg-green-200'}">
🟢 Flag as Open
</button>
<button onclick="flagStation('${s.id}','closed')"
class="flex-1 text-sm font-semibold px-3 py-2 rounded-lg transition-colors ${!isOpen ? 'bg-red-500 text-white' : 'bg-red-100 text-red-700 hover:bg-red-200'}">
🔴 Flag as Closed
</button>
</div>
</div>`;
}).join('');
}
// ── Flag a station ──
function flagStation(id, newStatus) {
const s = stations.find(st => st.id === id);
if (!s) return;
s.currentStatus = newStatus;
s.lastFlagged = new Date().toISOString();
s.flaggedBy = 'You';
// Update map marker
const marker = markerMap[id];
if (marker) {
marker.setStyle({ fillColor: markerColor(newStatus) });
marker.setPopupContent(buildPopup(s));
}
renderStats();
applyFilters();
showToast('Thanks for reporting! Status updated.');
}
// ── Filtering ──
function applyFilters() {
const q = document.getElementById('filter-search').value.trim().toLowerCase();
const status = document.getElementById('filter-status').value;
const prepass = document.getElementById('filter-prepass').value;
const filtered = stations.filter(s => {
if (q && !(s.name.toLowerCase().includes(q) || s.location.state.toLowerCase().includes(q) || s.route.toLowerCase().includes(q) || s.location.city.toLowerCase().includes(q))) return false;
if (status !== 'all' && s.currentStatus !== status) return false;
if (prepass === 'yes' && !s.prePass) return false;
if (prepass === 'no' && s.prePass) return false;
return true;
});
renderCards(filtered);
}
document.getElementById('filter-search').addEventListener('input', applyFilters);
document.getElementById('filter-status').addEventListener('change', applyFilters);
document.getElementById('filter-prepass').addEventListener('change', applyFilters);
document.getElementById('btn-clear-filters').addEventListener('click', () => {
document.getElementById('filter-search').value = '';
document.getElementById('filter-status').value = 'all';
document.getElementById('filter-prepass').value = 'all';
applyFilters();
});
// Initial render
applyFilters();
})();
</script>
</body>
</html>