first commit

This commit is contained in:
Daniel Kovalevich
2026-03-30 13:56:24 -04:00
commit 260f7c4928
17 changed files with 5335 additions and 0 deletions

206
PLAN.md Normal file
View File

@@ -0,0 +1,206 @@
# Pilot POC — Oversize Load Resource Platform
## Vision
A centralized web platform serving truck drivers, carriers, and escort/pilot vehicle operators in the oversize/overdimensional freight industry. Starts as a free information resource with soft lead-generation (escort service order form), then evolves into a revenue-generating SaaS platform.
## Target Users
- **Truck drivers** hauling oversize loads (occasional or specialized)
- **Carriers** (trucking companies) who book oversize hauls
- **Pilot/escort vehicle operators** seeking loads and customers
---
## Confirmed Modules
### Module 1 — State-by-State Regulations Map ✅
- Interactive US map (click a state → see its oversize permit thresholds and escort requirements)
- Data: width/height/length/weight thresholds per state for permits AND escort requirements
- Route checker: enter origin, destination, and load dimensions → see which states require permits/escorts
- **Main challenge:** Data collection (YOUR competitive advantage as a domain expert)
- **Status:** V1 POC built with mock data for all 50 states + DC
### Module 2 — Permit Route Parser ✅ (Future — high value)
- Parse the routing section of state permits (PDF/text), geocode waypoints, generate navigable route
- Export to Google Maps, Apple Maps, Waze, InRoute via deep links or GPX/KML
- **Main challenge:** Permit format variability across 48+ states — start with 5-10 most common
- AI/LLM parsing makes this much more feasible than before — could be a killer feature
### Module 3 — Oversize Load Board ✅
- Carriers/drivers post loads: origin, destination, date, dimensions, escort requirements
- Escort operators subscribe (monthly/annual) to view and bid
- Niche-specific to oversize loads (nothing like this exists today)
- **Status:** V1 POC built with 12 mock listings
### Module 4 — Escort Operator Locator Map ✅
- Operators create profiles and drop pins on their location
- Carriers see nearby available escorts on a map
- Includes: ratings, certifications, vehicle type, availability status
- **Status:** V1 POC built with 12 mock operators
### Module 5 — Oversize-Friendly Truck Stops & Parking ✅
- Database of truck stops, rest areas, and parking that can accommodate oversize loads
- Include: entrance/exit dimensions, lot square footage, facilities (fuel, food, restrooms, showers)
- **5a — Satellite Imagery:** Embed satellite view showing property layout, adjacent fields, etc.
- Data collection via structured user submissions (see Module 11)
### Module 6 — Bridge & Overpass Clearance Database ✅
- Height/width restrictions for bridges, overpasses, tunnels
- Integrate with route planning — flag conflicts with load dimensions
- Data sources: National Bridge Inventory (public federal data), state DOTs
### Module 7 — Permitted Route Condition Alerts ✅
- **Important context:** Single-trip permitted loads MUST follow their permitted route — they cannot detour. Annual/blanket permit holders have more flexibility but are expected to consult state 511/DOT.
- This module alerts users about construction zones, road closures, and other disruptions ON their permitted route BEFORE they depart
- Value: Know your route is impacted so you can contact the permit authority for a route amendment or delay departure, rather than discovering the issue en route
- Aggregate road closure data from state DOT 511 APIs
- For annual/blanket permit holders: general route condition awareness for interstates and US routes
### Module 8 — Weather & Wind Alerts for Wide Loads ✅
- Wind speed/direction forecasts along a planned route
- Wide loads are extremely sensitive to crosswinds
- Data source: National Weather Service API (free)
### Module 9 — Document Vault ✅
- Secure storage for permits, insurance certs, certifications, licenses
- Accessible from the road on mobile devices
- For all user types: truck drivers, carriers, AND escort operators
### Module 10 — Regulatory Change Alerts ✅
- Monitor state DOT regulation changes
- Notify users via email/push notifications
- Keeps the regulations database current AND provides value to users
### Module 11 — Data-Anchored User Contributions ✅
- **NOT a forum** — structured information submissions anchored to specific data points
- Users can submit facts about specific things (truck stops, weigh stations, routes, regulations)
- Users can comment on existing entries to add context or updates
- Users can flag information as outdated, incorrect, or confirm it's still accurate
- All contributions are tied to a specific entity (a truck stop, a weigh station, a route segment) — no free-floating discussion
- Avoids forum drama while preserving crowdsourcing value
- Examples:
- "South entrance of [truck stop] is blocked by construction as of 3/28"
- Flag a weigh station as "open" or "closed" (like Trucker Path)
- "This parking area has ~2 acres of gravel that can fit a 16' wide load"
### Module 12 — State-by-State Equipment Requirements ✅
- **Escort vehicle requirements:** certification needs, vehicle specs, lighting, signage, height poles, safety equipment per state
- **Truck/trailer (carrier) requirements:** OVERSIZE LOAD signs (roof-mounted vs bumper-mounted varies by state), traffic cones, fire extinguishers, reflective triangles, road flares, first aid kits, flags, lights, etc.
- Presented alongside regulation data (extends Module 1)
- Valuable because requirements vary significantly and non-compliance = fines and delays
### Module 13 — Weigh Station / Inspection Station Info ✅
- Location, hours, PrePass applicability
- **Crowd-sourced open/closed status** — drivers flag stations as currently open or closed (like Trucker Path)
- User-reported wait times
- Leverages Module 11 (data-anchored contributions) for user reports
### Module 14 — State DOT Contact Directory ✅
- Phone numbers for every state's permit office, oversize/overweight division, state police non-emergency line
- Organized by state, easily searchable
- Critical for on-the-road situations: route amendments, permit questions, incident reporting
- Low effort to build, high practical value
### Module 15 — Seasonal Restriction Calendar ✅
- Visual calendar showing when seasonal restrictions apply per state
- Spring weight restrictions (common in northern states, MarchMay)
- Winter mountain pass closures
- Harvest season restrictions in agricultural states
- Holiday blackout periods for oversize travel
- Helps carriers and escorts plan loads weeks/months in advance
---
## Future / Maybe Modules
### Multi-Load Trip Planner ⚠️ (needs implementation thought)
- For carriers shipping multiple related loads (e.g., 3 wind turbine blades, modular building sections)
- Coordinate timing, escort assignments, staging areas
- Deferred until core platform is established
---
## Modules Cut (with reasons)
### ~~Trip Cost Estimator~~ ❌
- Removed: Owner doesn't have knowledge of trucking fuel rates or permit costs
- Could revisit later once permit ordering service is established and rate data is available
### ~~Permit Application Self-Service Portal~~ ❌
- Removed: Owner wants to learn the permit ordering process and offer it as a paid service (order-on-behalf-of), NOT help carriers self-serve
- Permit ordering will be a transaction/service revenue stream, not a free tool
### ~~Rate/Mileage Calculator for Escorts~~ ❌
- Removed: Too many pricing variables (pickup/dropoff location, deadhead, load availability at destination, experience level, escort position — lead vs chase vs high pole, etc.)
### ~~Load Tracking / Trip Status~~ ❌
- Removed: Once an escort is assigned to a load, they're driving together — tracking is irrelevant. The escort and truck are at the same location until delivery or until the escort is no longer needed.
- Could be a future API/integration option for carriers' in-house operations, but not a core platform feature.
### ~~Invoicing / Bookkeeping~~ ❌
- Removed: Not a service or feature the owner wants to offer.
### ~~Emergency Breakdown Procedures~~ ❌
- Removed: Breakdowns are emergencies — no time to look up regulations. You do what you can to create a safe situation.
---
## Key Industry Insights (from owner)
### Permit Types & Routing Rules
- **Single-trip permits:** Have a specific permitted route. Driver MUST adhere to it. No detouring.
- **Annual/blanket permits:** For loads that are legally extradimensional but within certain parameters. Driver is supposed to consult state's 511 or DOT for routing advice, but many drivers skip the consultation and stick to interstate freeways and US routes/highways.
- All escorted loads have a set route that driver and escort are legally required to follow.
### Community Dynamics
- Truck drivers are not known for online etiquette
- Open forums tend to devolve into arguments, brand loyalty drama, ego contests, and clique infighting
- Solution: Data-anchored contributions (Module 11) — get crowdsourcing benefits without moderation burden
---
## Business Model
### Free Tier (traffic/lead generation)
- Regulations map + equipment requirements (Modules 1, 12)
- Truck stop/parking database (Module 5)
- Weather/wind alerts (Module 8)
- Bridge clearance data (Module 6)
- Weigh station info (Module 13)
- Route condition alerts (Module 7)
- Data-anchored contributions (Module 11)
- Escort service order form (your direct business)
### Subscription Tier(s)
- Load board access for escort operators (Module 3)
- Document vault (Module 9)
- Permit route parser (Module 2)
- Regulatory change alerts (Module 10)
### Transaction/Service Revenue
- Escort service bookings (your core business via the order form)
- Permit ordering service (order-on-behalf-of — future, once you learn the process)
- Featured listings for escort operators on the locator map
### Advertising
- Truck stop/service advertising on the parking database
- Equipment/insurance ads targeted to the industry
---
## Recommended Launch Sequence
1. **Phase 1 — Foundation:** Main website + escort service order form + state regulations map + equipment requirements (Modules 1, 12)
2. **Phase 2 — Engagement:** Truck stop/parking database (Module 5) + data-anchored contributions (Module 11) + weigh stations with crowd-sourced status (Module 13)
3. **Phase 3 — Revenue:** Load board (Module 3) + escort locator map (Module 4)
4. **Phase 4 — Awareness:** Bridge clearance database (Module 6) + route condition alerts (Module 7) + weather/wind alerts (Module 8)
5. **Phase 5 — Premium:** Permit route parser (Module 2) + document vault (Module 9) + regulatory change alerts (Module 10)
6. **Phase 6 — Services:** Permit ordering service (once process is learned)
---
## V1 POC Status (completed)
- ✅ Landing page with hero, features, and CTAs
- ✅ State regulations interactive map (50 states + DC, mock data)
- ✅ Escort service order form (multi-section)
- ✅ Load board with search/filter (12 mock listings)
- ✅ Escort operator locator map + list (12 mock operators)
- ✅ Shared navigation, POC banner, and footer

50
README.md Normal file
View File

@@ -0,0 +1,50 @@
# PilotEdge — Oversize Load Resource Platform (V1 POC)
## What Is This?
A proof-of-concept web application for truck drivers, carriers, and escort/pilot vehicle operators in the oversize/overdimensional freight industry.
> ⚠️ **All regulation data shown is SIMULATED for demonstration purposes and must be verified with actual state DOT regulations before any real-world use.**
## How to View
1. Open `index.html` in any modern web browser (Chrome, Edge, Firefox)
2. An internet connection is required (for map tiles and the CSS library)
3. Navigate between pages using the top navigation bar
## Pages
### Core
| Page | File | Description |
|------|------|-------------|
| **Home** | `index.html` | Platform overview and all feature modules |
| **Regulations Map** | `regulations.html` | Interactive US map + equipment requirements by state |
| **Request Service** | `order.html` | Escort vehicle service request form |
### Road Intelligence
| Page | File | Description |
|------|------|-------------|
| **Truck Stops** | `truckstops.html` | Oversize-friendly parking with user comments |
| **Bridge Clearances** | `bridges.html` | Height/width/weight restrictions for bridges |
| **Weigh Stations** | `weighstations.html` | Crowd-sourced open/closed status |
| **Route & Weather** | `alerts.html` | Construction, closures, and wind/weather alerts |
### Services & Resources
| Page | File | Description |
|------|------|-------------|
| **Load Board** | `loadboard.html` | Oversize load listings needing escort services |
| **Find Escorts** | `locator.html` | Map of available escort vehicle operators |
| **DOT Contacts** | `contacts.html` | State permit office phone/email directory |
| **Seasonal Calendar** | `calendar.html` | Seasonal restrictions and closure calendar |
| **Document Vault** | `documents.html` | Store permits, insurance, certifications |
## Tech Stack (POC)
- HTML / CSS / JavaScript (no build step required)
- [Tailwind CSS](https://tailwindcss.com/) via Play CDN
- [Leaflet.js](https://leafletjs.com/) for interactive maps via CDN
- Mock data (hardcoded — not yet connected to APIs or databases)
## Next Steps
- Replace mock data with real, verified state regulation data
- Connect to backend APIs and a database
- Add user accounts, authentication, and profiles
- Integrate payment processing for subscriptions
- Deploy to a web hosting service (Vercel, Netlify, or similar)

418
alerts.html Normal file
View File

@@ -0,0 +1,418 @@
<!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="mock-data.js"></script>
<script src="mock-data-extended.js"></script>
<script src="nav.js"></script>
<script>
renderNav('alerts');
renderBanner();
renderFooter();
// ── 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>

385
bridges.html Normal file
View File

@@ -0,0 +1,385 @@
<!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="mock-data.js"></script>
<script src="mock-data-extended.js"></script>
<script src="nav.js"></script>
<script>
renderNav('bridges');
renderBanner();
renderFooter();
// --- 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>

291
calendar.html Normal file
View File

@@ -0,0 +1,291 @@
<!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="mock-data.js"></script>
<script src="mock-data-extended.js"></script>
<script src="nav.js"></script>
<script>
renderNav('calendar');
renderBanner();
renderFooter();
// ---- 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>

108
contacts.html Normal file
View File

@@ -0,0 +1,108 @@
<!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="mock-data.js"></script>
<script src="mock-data-extended.js"></script>
<script src="nav.js"></script>
<script>
renderNav('contacts');
renderBanner();
renderFooter();
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>

259
documents.html Normal file
View File

@@ -0,0 +1,259 @@
<!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="mock-data.js"></script>
<script src="mock-data-extended.js"></script>
<script src="nav.js"></script>
<script>
renderNav('documents');
renderBanner();
renderFooter();
// ── 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>

233
index.html Normal file
View File

@@ -0,0 +1,233 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PilotEdge — Your Oversize Load Resource</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: { brand: { 50:'#fffbeb', 100:'#fef3c7', 200:'#fde68a', 400:'#fbbf24', 500:'#f59e0b', 600:'#d97706', 700:'#b45309' } }
}
}
}
</script>
<style>
.hero-gradient { background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #334155 100%); }
.card-hover { transition: transform 0.2s, box-shadow 0.2s; }
.card-hover:hover { transform: translateY(-4px); box-shadow: 0 12px 24px rgba(0,0,0,0.15); }
</style>
</head>
<body class="bg-slate-50 min-h-screen flex flex-col">
<div id="main-nav"></div>
<div id="poc-banner"></div>
<!-- Hero Section -->
<section class="hero-gradient text-white pt-24 pb-20 px-4">
<div class="max-w-7xl mx-auto text-center">
<div class="mb-6">
<span class="inline-block bg-amber-500/20 text-amber-400 text-sm font-semibold px-4 py-1 rounded-full border border-amber-500/30">
Built by Industry Professionals
</span>
</div>
<h1 class="text-4xl md:text-6xl font-extrabold mb-6 leading-tight">
Your Complete Resource for<br>
<span class="text-amber-400">Oversize Load Hauling</span>
</h1>
<p class="text-lg md:text-xl text-gray-300 max-w-3xl mx-auto mb-10">
State regulations, escort vehicle services, load matching, and oversize-friendly parking —
everything truck drivers and carriers need, all in one place.
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="regulations.html" 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">
Explore Regulations Map
</a>
<a href="order.html" class="bg-white/10 hover:bg-white/20 text-white font-bold px-8 py-4 rounded-xl text-lg transition-colors border border-white/20">
Request Escort Service
</a>
</div>
</div>
</section>
<!-- Stats Bar -->
<section class="bg-slate-900 border-y border-slate-700">
<div class="max-w-7xl mx-auto px-4 py-6 grid grid-cols-2 md:grid-cols-4 gap-6 text-center">
<div>
<div class="text-3xl font-bold text-amber-400">50</div>
<div class="text-sm text-gray-400">States Covered</div>
</div>
<div>
<div class="text-3xl font-bold text-amber-400">15</div>
<div class="text-sm text-gray-400">Tools & Resources</div>
</div>
<div>
<div class="text-3xl font-bold text-amber-400">24/7</div>
<div class="text-sm text-gray-400">Online Access</div>
</div>
<div>
<div class="text-3xl font-bold text-amber-400">FREE</div>
<div class="text-sm text-gray-400">Core Resources</div>
</div>
</div>
</section>
<!-- Features Grid -->
<section class="max-w-7xl mx-auto px-4 py-16">
<div class="text-center mb-12">
<h2 class="text-3xl md:text-4xl font-bold text-slate-900 mb-4">Everything You Need, One Platform</h2>
<p class="text-lg text-slate-600 max-w-2xl mx-auto">Whether you're a truck driver, carrier, or escort operator — PilotEdge has the tools to make oversize hauling easier and safer.</p>
</div>
<!-- Row 1: Core Features -->
<h3 class="text-xs font-bold text-slate-400 uppercase tracking-widest mb-4">Core Tools</h3>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6 mb-10">
<a href="regulations.html" class="bg-white rounded-2xl p-7 shadow-md card-hover block">
<div class="w-12 h-12 bg-amber-100 rounded-xl flex items-center justify-center text-xl mb-4">🗺️</div>
<h3 class="text-lg font-bold text-slate-900 mb-2">State Regulations Map</h3>
<p class="text-slate-600 text-sm mb-3">Permit thresholds, escort requirements, and equipment rules for all 50 states.</p>
<span class="text-amber-600 font-semibold text-sm">Explore Map →</span>
</a>
<a href="order.html" class="bg-white rounded-2xl p-7 shadow-md card-hover block">
<div class="w-12 h-12 bg-amber-100 rounded-xl flex items-center justify-center text-xl mb-4">📋</div>
<h3 class="text-lg font-bold text-slate-900 mb-2">Request Escort Service</h3>
<p class="text-slate-600 text-sm mb-3">Submit your load details and route — we'll match you with available escort vehicles.</p>
<span class="text-amber-600 font-semibold text-sm">Request Service →</span>
</a>
<a href="loadboard.html" class="bg-white rounded-2xl p-7 shadow-md card-hover block">
<div class="w-12 h-12 bg-amber-100 rounded-xl flex items-center justify-center text-xl mb-4">📦</div>
<h3 class="text-lg font-bold text-slate-900 mb-2">Oversize Load Board</h3>
<p class="text-slate-600 text-sm mb-3">Browse and post loads that need escorts. Connect carriers with pilot vehicles.</p>
<span class="text-amber-600 font-semibold text-sm">View Loads →</span>
</a>
</div>
<!-- Row 2: Road Intelligence -->
<h3 class="text-xs font-bold text-slate-400 uppercase tracking-widest mb-4">Road Intelligence</h3>
<div class="grid md:grid-cols-2 lg:grid-cols-4 gap-6 mb-10">
<a href="truckstops.html" class="bg-white rounded-2xl p-7 shadow-md card-hover block">
<div class="w-12 h-12 bg-green-100 rounded-xl flex items-center justify-center text-xl mb-4"></div>
<h3 class="text-lg font-bold text-slate-900 mb-2">Truck Stops & Parking</h3>
<p class="text-slate-600 text-sm mb-3">Oversize-friendly locations with entrance dimensions and user reviews.</p>
<span class="text-amber-600 font-semibold text-sm">Find Stops →</span>
</a>
<a href="bridges.html" class="bg-white rounded-2xl p-7 shadow-md card-hover block">
<div class="w-12 h-12 bg-red-100 rounded-xl flex items-center justify-center text-xl mb-4">🌉</div>
<h3 class="text-lg font-bold text-slate-900 mb-2">Bridge Clearances</h3>
<p class="text-slate-600 text-sm mb-3">Height, width, and weight restrictions for bridges and overpasses.</p>
<span class="text-amber-600 font-semibold text-sm">Check Clearances →</span>
</a>
<a href="weighstations.html" class="bg-white rounded-2xl p-7 shadow-md card-hover block">
<div class="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center text-xl mb-4">⚖️</div>
<h3 class="text-lg font-bold text-slate-900 mb-2">Weigh Stations</h3>
<p class="text-slate-600 text-sm mb-3">Live crowd-sourced open/closed status and inspection info.</p>
<span class="text-amber-600 font-semibold text-sm">View Stations →</span>
</a>
<a href="alerts.html" class="bg-white rounded-2xl p-7 shadow-md card-hover block">
<div class="w-12 h-12 bg-orange-100 rounded-xl flex items-center justify-center text-xl mb-4">⚠️</div>
<h3 class="text-lg font-bold text-slate-900 mb-2">Route & Weather Alerts</h3>
<p class="text-slate-600 text-sm mb-3">Construction, closures, and wind conditions on your route.</p>
<span class="text-amber-600 font-semibold text-sm">View Alerts →</span>
</a>
</div>
<!-- Row 3: Resources & Services -->
<h3 class="text-xs font-bold text-slate-400 uppercase tracking-widest mb-4">Resources & Services</h3>
<div class="grid md:grid-cols-2 lg:grid-cols-4 gap-6 mb-10">
<a href="locator.html" class="bg-white rounded-2xl p-7 shadow-md card-hover block">
<div class="w-12 h-12 bg-amber-100 rounded-xl flex items-center justify-center text-xl mb-4">📍</div>
<h3 class="text-lg font-bold text-slate-900 mb-2">Find Escorts</h3>
<p class="text-slate-600 text-sm mb-3">Locate pilot/escort vehicles near your load departure point.</p>
<span class="text-amber-600 font-semibold text-sm">Find Escorts →</span>
</a>
<a href="contacts.html" class="bg-white rounded-2xl p-7 shadow-md card-hover block">
<div class="w-12 h-12 bg-purple-100 rounded-xl flex items-center justify-center text-xl mb-4">📞</div>
<h3 class="text-lg font-bold text-slate-900 mb-2">DOT Contacts</h3>
<p class="text-slate-600 text-sm mb-3">Permit office phone numbers and emails for every state.</p>
<span class="text-amber-600 font-semibold text-sm">View Directory →</span>
</a>
<a href="calendar.html" class="bg-white rounded-2xl p-7 shadow-md card-hover block">
<div class="w-12 h-12 bg-indigo-100 rounded-xl flex items-center justify-center text-xl mb-4">📅</div>
<h3 class="text-lg font-bold text-slate-900 mb-2">Seasonal Calendar</h3>
<p class="text-slate-600 text-sm mb-3">Weight restrictions, closures, and blackout periods by state and season.</p>
<span class="text-amber-600 font-semibold text-sm">View Calendar →</span>
</a>
<a href="documents.html" class="bg-white rounded-2xl p-7 shadow-md card-hover block">
<div class="w-12 h-12 bg-sky-100 rounded-xl flex items-center justify-center text-xl mb-4">🗂️</div>
<h3 class="text-lg font-bold text-slate-900 mb-2">Document Vault</h3>
<p class="text-slate-600 text-sm mb-3">Store permits, insurance, and certifications — accessible from the road.</p>
<span class="text-amber-600 font-semibold text-sm">Manage Docs →</span>
</a>
</div>
<!-- Coming Soon -->
<h3 class="text-xs font-bold text-slate-400 uppercase tracking-widest mb-4">Coming Soon</h3>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
<div class="bg-white rounded-2xl p-7 shadow-md border-2 border-dashed border-slate-200 opacity-75">
<div class="w-12 h-12 bg-slate-100 rounded-xl flex items-center justify-center text-xl mb-4">🧭</div>
<h3 class="text-lg font-bold text-slate-900 mb-2">Permit Route Parser</h3>
<p class="text-slate-600 text-sm mb-3">Upload a state permit and get turn-by-turn navigation in your favorite map app.</p>
<span class="text-slate-400 font-semibold text-sm">Coming Soon</span>
</div>
<div class="bg-white rounded-2xl p-7 shadow-md border-2 border-dashed border-slate-200 opacity-75">
<div class="w-12 h-12 bg-slate-100 rounded-xl flex items-center justify-center text-xl mb-4">🔔</div>
<h3 class="text-lg font-bold text-slate-900 mb-2">Regulatory Change Alerts</h3>
<p class="text-slate-600 text-sm mb-3">Get notified when states update their oversize regulations and requirements.</p>
<span class="text-slate-400 font-semibold text-sm">Coming Soon</span>
</div>
</div>
</section>
<!-- How It Works -->
<section class="bg-slate-900 text-white py-16 px-4">
<div class="max-w-7xl mx-auto">
<div class="text-center mb-12">
<h2 class="text-3xl md:text-4xl font-bold mb-4">How It Works</h2>
<p class="text-lg text-gray-400">Three simple steps for carriers and truck drivers</p>
</div>
<div class="grid md:grid-cols-3 gap-8">
<div class="text-center">
<div class="w-16 h-16 bg-amber-500 rounded-full flex items-center justify-center text-2xl font-bold text-slate-900 mx-auto mb-5">1</div>
<h3 class="text-xl font-semibold mb-3">Check Regulations</h3>
<p class="text-gray-400">Use our interactive map to see what permits and escorts your load requires in each state on your route.</p>
</div>
<div class="text-center">
<div class="w-16 h-16 bg-amber-500 rounded-full flex items-center justify-center text-2xl font-bold text-slate-900 mx-auto mb-5">2</div>
<h3 class="text-xl font-semibold mb-3">Find or Request Escorts</h3>
<p class="text-gray-400">Browse available escort operators near your route, or submit a service request with your load details.</p>
</div>
<div class="text-center">
<div class="w-16 h-16 bg-amber-500 rounded-full flex items-center justify-center text-2xl font-bold text-slate-900 mx-auto mb-5">3</div>
<h3 class="text-xl font-semibold mb-3">Move Your Load</h3>
<p class="text-gray-400">Hit the road with confidence knowing your permits, escorts, and route are all handled.</p>
</div>
</div>
</div>
</section>
<!-- CTA Section -->
<section class="max-w-7xl mx-auto px-4 py-16 text-center">
<div class="bg-gradient-to-r from-amber-500 to-amber-600 rounded-3xl p-12 shadow-xl">
<h2 class="text-3xl md:text-4xl font-bold text-slate-900 mb-4">Need an Escort Vehicle?</h2>
<p class="text-lg text-slate-800 mb-8 max-w-2xl mx-auto">
Whether it's a single pilot car or a full escort team, we've got you covered.
Tell us about your load and we'll handle the rest.
</p>
<a href="order.html" class="inline-block bg-slate-900 hover:bg-slate-800 text-white font-bold px-8 py-4 rounded-xl text-lg transition-colors shadow-lg">
Request Escort Service →
</a>
</div>
</section>
<div id="main-footer"></div>
<script src="nav.js"></script>
<script>
renderNav('home');
renderBanner();
renderFooter();
</script>
</body>
</html>

298
loadboard.html Normal file
View File

@@ -0,0 +1,298 @@
<!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="mock-data.js"></script>
<script src="nav.js"></script>
<script>
renderNav('loadboard');
renderBanner();
renderFooter();
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>

301
locator.html Normal file
View File

@@ -0,0 +1,301 @@
<!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="mock-data.js"></script>
<script src="nav.js"></script>
<script>
renderNav('locator');
renderBanner();
renderFooter();
// 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>

775
mock-data-extended.js Normal file
View File

@@ -0,0 +1,775 @@
// =====================================================================
// EXTENDED MOCK DATA — Additional modules for V1 POC
// All data is SIMULATED for demonstration purposes.
// Include AFTER mock-data.js: <script src="mock-data-extended.js">
// =====================================================================
// =====================================================================
// STATE DOT CONTACTS (Module 14)
// =====================================================================
const MOCK_STATE_CONTACTS = {
AL: { name:"Alabama", permit:"(334) 242-5100", police:"(800) 392-8800", email:"permits@aldot.example.gov", hours:"M-F 7am-5pm CT", portal:"https://www.dot.state.al.us/" },
AK: { name:"Alaska", permit:"(907) 365-1200", police:"(907) 269-5511", email:"permits@dot.alaska.example.gov", hours:"M-F 8am-4:30pm AKT", portal:"https://dot.alaska.gov/" },
AZ: { name:"Arizona", permit:"(602) 712-7355", police:"(602) 223-2000", email:"permits@azdot.example.gov", hours:"M-F 7am-5pm MST", portal:"https://azdot.gov/" },
AR: { name:"Arkansas", permit:"(501) 569-2381", police:"(501) 618-8000", email:"permits@ardot.example.gov", hours:"M-F 7am-4:30pm CT", portal:"https://www.ardot.gov/" },
CA: { name:"California", permit:"(916) 654-4849", police:"(800) 835-5247", email:"permits@caltrans.example.gov", hours:"M-F 8am-5pm PT", portal:"https://dot.ca.gov/" },
CO: { name:"Colorado", permit:"(303) 757-9539", police:"(303) 239-4501", email:"permits@cdot.example.gov", hours:"M-F 7am-5pm MT", portal:"https://www.codot.gov/" },
CT: { name:"Connecticut", permit:"(860) 594-2874", police:"(860) 685-8190", email:"permits@ct.example.gov", hours:"M-F 8am-4:30pm ET", portal:"https://portal.ct.gov/dot" },
DE: { name:"Delaware", permit:"(302) 326-4650", police:"(302) 739-5901", email:"permits@deldot.example.gov", hours:"M-F 8am-4pm ET", portal:"https://deldot.gov/" },
FL: { name:"Florida", permit:"(850) 410-5777", police:"(850) 617-2000", email:"permits@fdot.example.gov", hours:"M-F 7am-6pm ET", portal:"https://www.fdot.gov/" },
GA: { name:"Georgia", permit:"(404) 635-8040", police:"(404) 624-7000", email:"permits@dot.ga.example.gov", hours:"M-F 7:30am-4:30pm ET", portal:"https://www.dot.ga.gov/" },
HI: { name:"Hawaii", permit:"(808) 692-7675", police:"(808) 586-1352", email:"permits@hawaii.example.gov", hours:"M-F 7:45am-4:30pm HST", portal:"https://hidot.hawaii.gov/" },
ID: { name:"Idaho", permit:"(208) 334-8418", police:"(208) 884-7000", email:"permits@itd.example.gov", hours:"M-F 7am-5pm MT", portal:"https://itd.idaho.gov/" },
IL: { name:"Illinois", permit:"(217) 785-1477", police:"(217) 782-6637", email:"permits@idot.example.gov", hours:"M-F 7am-4:30pm CT", portal:"https://idot.illinois.gov/" },
IN: { name:"Indiana", permit:"(317) 615-7320", police:"(317) 232-8248", email:"permits@indot.example.gov", hours:"M-F 7:30am-4pm ET", portal:"https://www.in.gov/indot/" },
IA: { name:"Iowa", permit:"(515) 237-3264", police:"(515) 725-6090", email:"permits@iowadot.example.gov", hours:"M-F 7am-4:30pm CT", portal:"https://iowadot.gov/" },
KS: { name:"Kansas", permit:"(785) 296-3618", police:"(785) 296-6800", email:"permits@ksdot.example.gov", hours:"M-F 8am-5pm CT", portal:"https://www.ksdot.gov/" },
KY: { name:"Kentucky", permit:"(502) 564-4540", police:"(502) 227-8700", email:"permits@kytc.example.gov", hours:"M-F 8am-4:30pm ET", portal:"https://transportation.ky.gov/" },
LA: { name:"Louisiana", permit:"(225) 379-1436", police:"(225) 925-6006", email:"permits@dotd.la.example.gov", hours:"M-F 7:30am-4pm CT", portal:"https://www.dotd.la.gov/" },
ME: { name:"Maine", permit:"(207) 624-3600", police:"(207) 624-7076", email:"permits@maine.example.gov", hours:"M-F 8am-4pm ET", portal:"https://www.maine.gov/mdot/" },
MD: { name:"Maryland", permit:"(410) 582-5734", police:"(410) 486-3101", email:"permits@mdot.example.gov", hours:"M-F 8am-4:30pm ET", portal:"https://www.roads.maryland.gov/" },
MA: { name:"Massachusetts", permit:"(857) 368-9640", police:"(508) 820-2300", email:"permits@massdot.example.gov", hours:"M-F 8:30am-5pm ET", portal:"https://www.mass.gov/massdot" },
MI: { name:"Michigan", permit:"(517) 335-0945", police:"(517) 332-2521", email:"permits@michigan.example.gov", hours:"M-F 8am-4:30pm ET", portal:"https://www.michigan.gov/mdot" },
MN: { name:"Minnesota", permit:"(651) 296-6000", police:"(651) 201-7100", email:"permits@mndot.example.gov", hours:"M-F 7:30am-4pm CT", portal:"https://www.dot.state.mn.us/" },
MS: { name:"Mississippi", permit:"(601) 359-7685", police:"(601) 987-1212", email:"permits@mdot.ms.example.gov", hours:"M-F 7am-5pm CT", portal:"https://mdot.ms.gov/" },
MO: { name:"Missouri", permit:"(573) 751-7100", police:"(573) 751-3313", email:"permits@modot.example.gov", hours:"M-F 7:30am-4:30pm CT", portal:"https://www.modot.org/" },
MT: { name:"Montana", permit:"(406) 444-6130", police:"(406) 444-3780", email:"permits@mdt.example.gov", hours:"M-F 8am-5pm MT", portal:"https://www.mdt.mt.gov/" },
NE: { name:"Nebraska", permit:"(402) 471-0034", police:"(402) 471-4545", email:"permits@dot.ne.example.gov", hours:"M-F 8am-5pm CT", portal:"https://dot.nebraska.gov/" },
NV: { name:"Nevada", permit:"(775) 888-7410", police:"(775) 687-5300", email:"permits@dot.nv.example.gov", hours:"M-F 7am-5pm PT", portal:"https://www.dot.nv.gov/" },
NH: { name:"New Hampshire", permit:"(603) 227-6100", police:"(603) 223-4381", email:"permits@nh.example.gov", hours:"M-F 8am-4pm ET", portal:"https://www.nh.gov/dot/" },
NJ: { name:"New Jersey", permit:"(609) 530-2345", police:"(609) 882-2000", email:"permits@njdot.example.gov", hours:"M-F 8am-4:30pm ET", portal:"https://www.nj.gov/transportation/" },
NM: { name:"New Mexico", permit:"(505) 827-4565", police:"(505) 827-9300", email:"permits@dot.nm.example.gov", hours:"M-F 8am-5pm MT", portal:"https://www.dot.nm.gov/" },
NY: { name:"New York", permit:"(518) 457-1014", police:"(518) 457-6811", email:"permits@nysdot.example.gov", hours:"M-F 8am-4pm ET", portal:"https://www.dot.ny.gov/" },
NC: { name:"North Carolina", permit:"(919) 733-7752", police:"(919) 733-7952", email:"permits@ncdot.example.gov", hours:"M-F 8am-5pm ET", portal:"https://www.ncdot.gov/" },
ND: { name:"North Dakota", permit:"(701) 328-2543", police:"(701) 328-2455", email:"permits@dot.nd.example.gov", hours:"M-F 8am-5pm CT", portal:"https://www.dot.nd.gov/" },
OH: { name:"Ohio", permit:"(614) 351-2300", police:"(614) 466-2660", email:"permits@odot.example.gov", hours:"M-F 7am-5pm ET", portal:"https://www.transportation.ohio.gov/" },
OK: { name:"Oklahoma", permit:"(405) 521-2558", police:"(405) 425-2424", email:"permits@odot.ok.example.gov", hours:"M-F 7:30am-4:30pm CT", portal:"https://oklahoma.gov/odot.html" },
OR: { name:"Oregon", permit:"(503) 378-6699", police:"(503) 378-3720", email:"permits@odot.or.example.gov", hours:"M-F 7:30am-4:30pm PT", portal:"https://www.oregon.gov/odot/" },
PA: { name:"Pennsylvania", permit:"(717) 787-3156", police:"(717) 783-5599", email:"permits@penndot.example.gov", hours:"M-F 8am-4pm ET", portal:"https://www.penndot.pa.gov/" },
RI: { name:"Rhode Island", permit:"(401) 222-2481", police:"(401) 444-1000", email:"permits@ridot.example.gov", hours:"M-F 8:30am-4pm ET", portal:"https://www.dot.ri.gov/" },
SC: { name:"South Carolina", permit:"(803) 737-1290", police:"(803) 896-7920", email:"permits@scdot.example.gov", hours:"M-F 8am-5pm ET", portal:"https://www.scdot.org/" },
SD: { name:"South Dakota", permit:"(605) 773-3571", police:"(605) 773-3105", email:"permits@sddot.example.gov", hours:"M-F 8am-5pm CT", portal:"https://dot.sd.gov/" },
TN: { name:"Tennessee", permit:"(615) 741-3821", police:"(615) 251-5175", email:"permits@tn.example.gov", hours:"M-F 7am-4:30pm CT", portal:"https://www.tn.gov/tdot.html" },
TX: { name:"Texas", permit:"(512) 465-7603", police:"(512) 424-2000", email:"permits@txdmv.example.gov", hours:"M-F 7am-6pm CT", portal:"https://www.txdmv.gov/" },
UT: { name:"Utah", permit:"(801) 965-4468", police:"(801) 887-3800", email:"permits@udot.example.gov", hours:"M-F 8am-5pm MT", portal:"https://www.udot.utah.gov/" },
VT: { name:"Vermont", permit:"(802) 828-2070", police:"(802) 244-8727", email:"permits@vtrans.example.gov", hours:"M-F 7:45am-4:30pm ET", portal:"https://vtrans.vermont.gov/" },
VA: { name:"Virginia", permit:"(804) 497-1560", police:"(804) 674-2000", email:"permits@vdot.example.gov", hours:"M-F 8am-5pm ET", portal:"https://www.virginiadot.org/" },
WA: { name:"Washington", permit:"(360) 704-6340", police:"(360) 596-4000", email:"permits@wsdot.example.gov", hours:"M-F 7am-5pm PT", portal:"https://wsdot.wa.gov/" },
WV: { name:"West Virginia", permit:"(304) 558-3063", police:"(304) 746-2100", email:"permits@wvdoh.example.gov", hours:"M-F 7am-4pm ET", portal:"https://transportation.wv.gov/" },
WI: { name:"Wisconsin", permit:"(608) 266-7320", police:"(608) 266-3212", email:"permits@wisdot.example.gov", hours:"M-F 7am-4:30pm CT", portal:"https://wisconsindot.gov/" },
WY: { name:"Wyoming", permit:"(307) 777-4375", police:"(307) 777-4301", email:"permits@wydot.example.gov", hours:"M-F 8am-5pm MT", portal:"https://www.dot.state.wy.us/" },
DC: { name:"District of Columbia", permit:"(202) 673-6813", police:"(202) 727-9099", email:"permits@ddot.example.gov", hours:"M-F 8:30am-4:30pm ET", portal:"https://ddot.dc.gov/" }
};
// =====================================================================
// STATE EQUIPMENT REQUIREMENTS (Module 12)
// Detailed data for major trucking states; others show "Data coming soon"
// =====================================================================
const MOCK_STATE_EQUIPMENT = {
TX: {
escort: {
certification: "Required — must complete TxDMV-approved pilot/escort vehicle course",
vehicle: "Passenger car, pickup, or SUV — no commercial vehicles",
signs: "OVERSIZE LOAD sign, minimum 7' wide × 18\" tall, yellow background, black letters, front and rear",
lights: "2 amber rotating or strobe lights mounted on roof, visible from 500'",
heightPole: "Required when leading overheight loads exceeding 15'",
flags: "18\" red/orange fluorescent flags at 4 corners of vehicle",
communication: "CB radio required — channel 19 monitored at all times",
safety: "First aid kit, 10BC fire extinguisher, flashlight, reflective vest"
},
carrier: {
signs: "OVERSIZE LOAD banner — roof-mounted or bumper-mounted permitted, yellow/black, min 7'×18\"",
flags: "18\" red/orange flags at each corner and extremity of load",
lights: "Amber flashing lights at widest points of load, front and rear",
cones: "Not required by state, but recommended",
fireExtinguisher: "10BC rated fire extinguisher required",
triangles: "3 reflective triangles required",
flares: "Not required",
firstAid: "Not required by state"
}
},
CA: {
escort: {
certification: "Required — CHP-approved Pilot Car Escort Training (PCET) certification",
vehicle: "Must pass annual CHP inspection — car, pickup, or SUV",
signs: "OVERSIZE LOAD sign front and rear, min 6' wide × 18\" tall, reflective",
lights: "Amber flashing light on roof, visible 360°",
heightPole: "Required for all overheight loads — must extend to load height plus 6\"",
flags: "Red/orange fluorescent flags, 18\" min, at 4 corners",
communication: "Two-way radio or CB required",
safety: "First aid kit, fire extinguisher, 3 reflective triangles, reflective vest, flashlight"
},
carrier: {
signs: "OVERSIZE LOAD banner — ROOF-MOUNTED REQUIRED, reflective, min 7'×18\"",
flags: "Red/orange flags at extremities, 18\" min — required day and night",
lights: "Amber warning lights at widest points, amber flashers on truck",
cones: "6 traffic cones required (28\" min height)",
fireExtinguisher: "10BC rated required",
triangles: "3 reflective triangles required",
flares: "3 fuses/flares required",
firstAid: "First aid kit required"
}
},
OH: {
escort: {
certification: "Not state-mandated, but ODOT recommends completion of training course",
vehicle: "Passenger vehicle or light-duty truck",
signs: "OVERSIZE LOAD sign, yellow/black, min 5' wide × 10\" tall",
lights: "Amber rotating or flashing light on roof",
heightPole: "Required when escorting overheight loads",
flags: "Orange flags at corners of vehicle",
communication: "CB radio recommended, cell phone minimum",
safety: "Fire extinguisher, first aid kit recommended"
},
carrier: {
signs: "OVERSIZE LOAD banner — bumper-mounted permitted, min 7'×18\"",
flags: "Red/orange flags at extremities of load",
lights: "Amber flashing lights on widest points of load",
cones: "Not required",
fireExtinguisher: "Required",
triangles: "3 reflective triangles required",
flares: "Not required",
firstAid: "Not required"
}
},
PA: {
escort: {
certification: "Required — PennDOT approved training course",
vehicle: "Single-unit vehicle, must be registered and insured",
signs: "OVERSIZE LOAD sign, yellow/black, front and rear",
lights: "Amber 360° flashing/rotating light on roof, visible 500'",
heightPole: "Required for overheight loads on Turnpike; recommended elsewhere",
flags: "Red/orange flags at 4 corners",
communication: "CB radio required",
safety: "First aid kit, fire extinguisher, reflective vest, flashlight"
},
carrier: {
signs: "OVERSIZE LOAD sign — front and rear of vehicle, roof-mounted on tractor",
flags: "Red/orange fluorescent flags at extremities and corners of load",
lights: "Amber rotating light on cab, amber lights at widest points",
cones: "Not required",
fireExtinguisher: "10BC rated required",
triangles: "3 reflective triangles required",
flares: "Not required",
firstAid: "Not required"
}
},
FL: {
escort: {
certification: "Not state-mandated — no formal certification program",
vehicle: "Any passenger vehicle or light truck",
signs: "OVERSIZE LOAD sign, yellow/black, displayed prominently",
lights: "Amber rotating or strobe light on roof",
heightPole: "Required for overheight loads when escort is in front",
flags: "Red/orange flags at corners of escort vehicle",
communication: "CB radio or two-way radio required",
safety: "Fire extinguisher recommended, first aid kit recommended"
},
carrier: {
signs: "OVERSIZE LOAD banner — bumper-mounted or roof-mounted, min 7'×18\"",
flags: "18\" red/orange flags at each extremity of load",
lights: "Amber warning lights at widest points",
cones: "Not required",
fireExtinguisher: "Required",
triangles: "3 reflective triangles required",
flares: "Not required",
firstAid: "Not required"
}
},
GA: {
escort: {
certification: "Not state-mandated — training recommended",
vehicle: "Passenger vehicle or light-duty truck",
signs: "OVERSIZE LOAD sign front and rear",
lights: "Amber flashing or rotating light, roof-mounted",
heightPole: "Required for overheight loads",
flags: "Red/orange flags at corners",
communication: "CB radio or cell phone",
safety: "Fire extinguisher, first aid kit"
},
carrier: {
signs: "OVERSIZE LOAD banner — bumper-mounted permitted",
flags: "Red/orange flags at extremities",
lights: "Amber flashers at widest points",
cones: "Not required",
fireExtinguisher: "Required",
triangles: "3 triangles required",
flares: "Not required",
firstAid: "Not required"
}
},
IL: {
escort: {
certification: "Not state-mandated",
vehicle: "Single-unit passenger vehicle",
signs: "OVERSIZE LOAD sign, front and rear, yellow/black",
lights: "Amber rotating light on roof",
heightPole: "Required for overheight escorts",
flags: "Red/orange flags at 4 corners",
communication: "CB radio required for loads over 14' wide",
safety: "Fire extinguisher, reflective vest"
},
carrier: {
signs: "OVERSIZE LOAD banner — front and rear, roof or bumper mount allowed",
flags: "Red/orange flags at load extremities and corners, 18\" min",
lights: "Amber flashers on widest points, rotating amber on cab",
cones: "Not required",
fireExtinguisher: "Required",
triangles: "3 reflective triangles required",
flares: "Not required",
firstAid: "Not required"
}
},
NY: {
escort: {
certification: "Required — NYSDOT approved escort vehicle operator course",
vehicle: "Passenger vehicle, under 10,000 lbs GVW",
signs: "OVERSIZE LOAD sign front and rear, black on yellow, reflective",
lights: "Amber 360° light, roof-mounted, visible 500'",
heightPole: "Required for all overheight loads in the state",
flags: "18\" red/orange flags at all 4 corners",
communication: "CB radio required, cell phone as backup",
safety: "First aid kit, fire extinguisher, 3 reflective triangles, reflective vest"
},
carrier: {
signs: "OVERSIZE LOAD sign — ROOF-MOUNTED REQUIRED on tractor",
flags: "Red/orange flags at all corners and extremities, 18\" min",
lights: "Amber rotating on cab, amber flashers at widest points of load",
cones: "Not required",
fireExtinguisher: "10BC required",
triangles: "3 reflective triangles required",
flares: "3 fusees required",
firstAid: "Not required"
}
},
NC: {
escort: {
certification: "Not state-mandated — recommended",
vehicle: "Passenger vehicle or light truck",
signs: "OVERSIZE LOAD sign front and rear",
lights: "Amber rotating/flashing on roof",
heightPole: "Required for overheight loads",
flags: "Red/orange flags at corners",
communication: "CB radio recommended",
safety: "Fire extinguisher, first aid kit recommended"
},
carrier: {
signs: "OVERSIZE LOAD banner — bumper-mounted permitted",
flags: "Red/orange flags at extremities",
lights: "Amber flashers at widest points",
cones: "Not required",
fireExtinguisher: "Required",
triangles: "3 required",
flares: "Not required",
firstAid: "Not required"
}
},
LA: {
escort: {
certification: "Not state-mandated",
vehicle: "Passenger vehicle or light truck",
signs: "OVERSIZE LOAD sign, front and rear, yellow/black",
lights: "Amber rotating light on roof",
heightPole: "Required for overheight loads exceeding 14'6\"",
flags: "Red/orange flags at all corners",
communication: "CB radio required",
safety: "Fire extinguisher, first aid kit"
},
carrier: {
signs: "OVERSIZE LOAD banner — bumper or roof mount",
flags: "Red/orange flags at extremities, 18\" min",
lights: "Amber warning lights at widest points",
cones: "Not required",
fireExtinguisher: "Required",
triangles: "3 required",
flares: "Not required",
firstAid: "Not required"
}
},
OK: {
escort: {
certification: "Not state-mandated",
vehicle: "Passenger vehicle or pickup truck",
signs: "OVERSIZE LOAD sign, yellow/black, front and rear",
lights: "Amber rotating or strobe on roof",
heightPole: "Required for overheight loads",
flags: "Red/orange flags at 4 corners",
communication: "CB radio required",
safety: "Fire extinguisher, flashlight"
},
carrier: {
signs: "OVERSIZE LOAD banner — bumper-mounted or roof-mounted accepted",
flags: "Red/orange flags at all corners and extremities",
lights: "Amber flashers at widest/tallest/longest points",
cones: "Not required",
fireExtinguisher: "Required",
triangles: "3 reflective triangles required",
flares: "Not required",
firstAid: "Not required"
}
},
IN: {
escort: {
certification: "Not state-mandated — INDOT recommends training",
vehicle: "Passenger vehicle or light truck",
signs: "OVERSIZE LOAD sign, front and rear",
lights: "Amber rotating/flashing light on roof",
heightPole: "Required for overheight loads",
flags: "Orange flags at corners",
communication: "CB radio or cell phone",
safety: "Fire extinguisher recommended"
},
carrier: {
signs: "OVERSIZE LOAD banner — front and rear, bumper mount OK",
flags: "Red/orange flags at extremities",
lights: "Amber flashers at widest points",
cones: "Not required",
fireExtinguisher: "Required",
triangles: "3 required",
flares: "Not required",
firstAid: "Not required"
}
}
};
// =====================================================================
// TRUCK STOPS & PARKING (Module 5)
// =====================================================================
const MOCK_TRUCK_STOPS = [
{
id:"TS-001", name:"Sapp Bros. Travel Center", type:"truck_stop",
location:{ city:"Amarillo", state:"TX", lat:35.19, lng:-101.78 },
oversizeFriendly:true, entranceWidth:"26'", entranceHeight:"No restriction",
lotSize:"4.2 acres total", oversizeCapacity:"5-6 oversize loads",
facilities:["fuel","food","restrooms","showers","mechanic","scale"],
description:"Large open lot on the south side. Adjacent gravel area can fit extra-wide loads. South entrance is best for oversize.",
comments:[
{ user:"TruckerMike_TX", date:"2026-03-15", text:"Parked a 16'4\" wide wind blade here. South lot had plenty of room. Use the south entrance off the service road." },
{ user:"HighPolePete", date:"2026-02-28", text:"South entrance is the only one that works for oversize. North has tight posts. Good fuel prices." }
]
},
{
id:"TS-002", name:"Iowa 80 Truckstop", type:"truck_stop",
location:{ city:"Walcott", state:"IA", lat:41.58, lng:-90.77 },
oversizeFriendly:true, entranceWidth:"30'+", entranceHeight:"No restriction",
lotSize:"8+ acres total", oversizeCapacity:"10+ oversize loads",
facilities:["fuel","food","restrooms","showers","mechanic","scale","trucking_museum","barber","chiropractor"],
description:"World's largest truck stop. Massive open lot on east side regularly used for oversize staging. Multiple wide entrances.",
comments:[
{ user:"MidwestHauler", date:"2026-03-20", text:"Best oversize parking on I-80. East lot is huge — I've seen 3 blade trucks staged here at once." },
{ user:"BladRunner_IA", date:"2026-03-05", text:"Great staging area for wind energy loads heading south on I-80 to I-35." }
]
},
{
id:"TS-003", name:"Petro Stopping Center", type:"truck_stop",
location:{ city:"Rochelle", state:"IL", lat:41.92, lng:-89.07 },
oversizeFriendly:true, entranceWidth:"24'", entranceHeight:"No restriction",
lotSize:"3.8 acres", oversizeCapacity:"3-4 oversize loads",
facilities:["fuel","food","restrooms","showers","iron_skillet"],
description:"I-39/I-88 junction location. Open area behind main lot can accommodate oversized loads. Wind blade traffic common.",
comments:[
{ user:"PrairiePilot", date:"2026-03-10", text:"Regular stop for blade loads on I-39 corridor. Back lot is gravel but solid ground." }
]
},
{
id:"TS-004", name:"Breezewood Travel Plaza", type:"rest_area",
location:{ city:"Breezewood", state:"PA", lat:39.99, lng:-78.24 },
oversizeFriendly:false, entranceWidth:"14'", entranceHeight:"13'8\" (canopy)",
lotSize:"2 acres", oversizeCapacity:"1-2 standard oversize only",
facilities:["fuel","food","restrooms"],
description:"Famous I-70/I-76 interchange. Very tight for oversize — canopy restricts height. NOT recommended for wide or tall loads.",
comments:[
{ user:"KeystoneEscort", date:"2026-03-22", text:"DO NOT bring oversize through the main fuel canopy. There's a pull-off east of the plaza on the service road that works in a pinch." },
{ user:"NE_Hauler", date:"2026-02-15", text:"Avoid this place with anything over 12' wide. The whole town is a bottleneck. Plan to fuel before or after." }
]
},
{
id:"TS-005", name:"Buc-ee's", type:"truck_stop",
location:{ city:"Terrell", state:"TX", lat:32.72, lng:-96.22 },
oversizeFriendly:true, entranceWidth:"28'", entranceHeight:"No restriction",
lotSize:"5+ acres", oversizeCapacity:"4-5 oversize loads",
facilities:["fuel","food","restrooms","ev_charging"],
description:"Massive lot with wide lanes. Oversize can park on the outer perimeter. No dedicated truck parking but plenty of space.",
comments:[
{ user:"LoneStarOS", date:"2026-03-18", text:"Clean restrooms and great food. Park on the far east side — wide open and easy in/out." }
]
},
{
id:"TS-006", name:"Pilot Travel Center", type:"truck_stop",
location:{ city:"Salina", state:"KS", lat:38.81, lng:-97.59 },
oversizeFriendly:true, entranceWidth:"24'", entranceHeight:"No restriction",
lotSize:"3.5 acres", oversizeCapacity:"3-4 oversize loads",
facilities:["fuel","food","restrooms","showers","scale"],
description:"I-70/I-135 junction. Good oversize parking behind the main lot. Flat gravel area to the south.",
comments:[
{ user:"KSWindHauler", date:"2026-03-12", text:"Solid mid-Kansas stop. South gravel lot handles blade trucks fine." }
]
},
{
id:"TS-007", name:"Little America Travel Center", type:"truck_stop",
location:{ city:"Little America", state:"WY", lat:41.54, lng:-110.07 },
oversizeFriendly:true, entranceWidth:"30'+", entranceHeight:"No restriction",
lotSize:"6 acres", oversizeCapacity:"6-8 oversize loads",
facilities:["fuel","food","restrooms","hotel","showers"],
description:"Remote I-80 oasis with enormous lot. Very oversize-friendly — wide open spaces. Common staging point for loads heading through Wyoming.",
comments:[
{ user:"RockyMtnLog", date:"2026-03-08", text:"Best oversize stop in southern Wyoming. Huge lot, 24/7 fuel. Wind can be brutal though — check conditions before stopping." },
{ user:"WYO_Escort", date:"2026-02-20", text:"We stage here all the time for loads heading east or west on I-80. Hotel is decent for overnight." }
]
},
{
id:"TS-008", name:"Bosselman Travel Center", type:"truck_stop",
location:{ city:"North Platte", state:"NE", lat:41.11, lng:-100.77 },
oversizeFriendly:true, entranceWidth:"24'", entranceHeight:"No restriction",
lotSize:"3 acres", oversizeCapacity:"3-4 oversize loads",
facilities:["fuel","food","restrooms","showers","mechanic"],
description:"I-80 corridor stop. Open area north of main lot usable for oversize. Regular wind turbine traffic.",
comments:[
{ user:"NE_OversizeOps", date:"2026-03-01", text:"Good stop on the I-80 wind corridor. North lot is gravel and fits wide loads." }
]
},
{
id:"TS-009", name:"Flying J Travel Center", type:"truck_stop",
location:{ city:"Dillon", state:"SC", lat:34.41, lng:-79.39 },
oversizeFriendly:true, entranceWidth:"22'", entranceHeight:"No restriction",
lotSize:"3.2 acres", oversizeCapacity:"2-3 oversize loads",
facilities:["fuel","food","restrooms","showers","scale"],
description:"I-95/I-20 junction. Some oversize parking on the east side. Gets busy — arrive early.",
comments:[
{ user:"PeachStateP", date:"2026-03-14", text:"East side has room for 2-3 oversize rigs. Tight during peak hours though." }
]
},
{
id:"TS-010", name:"Love's Travel Stop", type:"truck_stop",
location:{ city:"Eloy", state:"AZ", lat:32.74, lng:-111.54 },
oversizeFriendly:true, entranceWidth:"26'", entranceHeight:"No restriction",
lotSize:"4 acres", oversizeCapacity:"4-5 oversize loads",
facilities:["fuel","food","restrooms","showers"],
description:"I-10 corridor between Phoenix and Tucson. Open desert lot on south side handles oversize well.",
comments:[
{ user:"DesertSun_TX", date:"2026-03-25", text:"Good I-10 stop. South lot is flat and open. Carry extra water — it's the desert." }
]
}
];
// =====================================================================
// BRIDGE & OVERPASS CLEARANCES (Module 6)
// =====================================================================
const MOCK_BRIDGE_CLEARANCES = [
{ id:"BR-001", route:"I-95 NB", mileMarker:"67.2", type:"Overpass",
location:{ desc:"Fort McHenry Tunnel approach", city:"Baltimore", state:"MD", lat:39.26, lng:-76.58 },
clearanceHeight:"13'6\"", clearanceWidth:"No restriction", weightLimit:"80,000 lbs (standard)",
notes:"Major bottleneck for overheight loads on I-95 NB. Overheight loads must use I-695 bypass around Baltimore." },
{ id:"BR-002", route:"I-70 WB", mileMarker:"213.5", type:"Tunnel",
location:{ desc:"Eisenhower-Johnson Memorial Tunnel", city:"Silver Plume", state:"CO", lat:39.68, lng:-105.91 },
clearanceHeight:"13'11\"", clearanceWidth:"13'0\" (per lane)", weightLimit:"Route-specific",
notes:"Highest point on the Interstate system. Oversize loads often must use US-6 Loveland Pass detour. Hazmat prohibited." },
{ id:"BR-003", route:"I-64 WB", mileMarker:"58.1", type:"Tunnel",
location:{ desc:"East River Mountain Tunnel", city:"Near Bluefield", state:"WV", lat:37.37, lng:-81.10 },
clearanceHeight:"15'5\"", clearanceWidth:"12'0\" (per lane)", weightLimit:"80,000 lbs",
notes:"Two-lane bore with restricted width. Overwide loads must use alternate routes. Frequent delays." },
{ id:"BR-004", route:"I-35 SB", mileMarker:"429.8", type:"Overpass",
location:{ desc:"Downtown Dallas interchange", city:"Dallas", state:"TX", lat:32.79, lng:-96.80 },
clearanceHeight:"14'0\"", clearanceWidth:"No restriction", weightLimit:"Standard",
notes:"Several low overpasses through downtown Dallas on I-35E. Overheight loads should use I-35W bypass through Fort Worth." },
{ id:"BR-005", route:"I-76 EB", mileMarker:"161.3", type:"Overpass",
location:{ desc:"Pennsylvania Turnpike — Valley Forge area", city:"King of Prussia", state:"PA", lat:40.09, lng:-75.38 },
clearanceHeight:"14'2\"", clearanceWidth:"No restriction", weightLimit:"Turnpike limits apply",
notes:"PA Turnpike has numerous older overpasses with restricted clearances. Contact Turnpike Commission for oversize routing." },
{ id:"BR-006", route:"US-20 EB", mileMarker:"N/A", type:"Overpass (multiple)",
location:{ desc:"Route 20 through Connecticut", city:"Various", state:"CT", lat:41.60, lng:-72.75 },
clearanceHeight:"12'6\" — 13'6\" (varies)", clearanceWidth:"Restricted at several points", weightLimit:"Varies by bridge",
notes:"Multiple low clearance overpasses along US-20 through CT. Not recommended for overheight. Use I-84 or I-91 where possible." },
{ id:"BR-007", route:"I-90 WB", mileMarker:"52.4", type:"Overpass",
location:{ desc:"Chicago Skyway / Dan Ryan interchange", city:"Chicago", state:"IL", lat:41.72, lng:-87.56 },
clearanceHeight:"14'0\"", clearanceWidth:"No restriction", weightLimit:"Standard",
notes:"Chicago metro area has multiple low overpasses. Oversize loads require IDOT-approved routing through the metro." },
{ id:"BR-008", route:"I-24 EB", mileMarker:"174.9", type:"Overpass",
location:{ desc:"Chattanooga downtown area", city:"Chattanooga", state:"TN", lat:35.04, lng:-85.28 },
clearanceHeight:"14'4\"", clearanceWidth:"No restriction", weightLimit:"Standard",
notes:"Series of overpasses through downtown Chattanooga with varying clearances. Check permit routing carefully." },
{ id:"BR-009", route:"I-10 EB", mileMarker:"768.3", type:"Overpass",
location:{ desc:"Houston ship channel bridge approaches", city:"Houston", state:"TX", lat:29.74, lng:-95.09 },
clearanceHeight:"Varies — 14'6\" to 16'2\"", clearanceWidth:"No restriction", weightLimit:"Standard",
notes:"Complex interchange area with varying clearances. Follow permitted route exactly. Some ramps have lower clearances than mainline." },
{ id:"BR-010", route:"I-81 SB", mileMarker:"300.1", type:"Overpass",
location:{ desc:"Harrisburg area", city:"Harrisburg", state:"PA", lat:40.28, lng:-76.87 },
clearanceHeight:"14'6\"", clearanceWidth:"No restriction", weightLimit:"Standard",
notes:"Several overpasses in the Harrisburg metro with clearances between 14' and 15'. Common corridor for northeast oversize traffic." }
];
// =====================================================================
// WEIGH STATIONS (Module 13)
// =====================================================================
const MOCK_WEIGH_STATIONS = [
{ id:"WS-001", name:"Hillsboro Weigh Station", route:"I-35 NB",
location:{ city:"Hillsboro", state:"TX", lat:31.98, lng:-97.13 },
hours:"24/7", prePass:true,
currentStatus:"open", lastFlagged:"2026-03-29T14:30:00Z", flaggedBy:"TruckerJoe42",
notes:"Oversize loads almost always pulled in for inspection. Have permits readily accessible." },
{ id:"WS-002", name:"Ehrenberg Port of Entry", route:"I-10 WB",
location:{ city:"Ehrenberg", state:"AZ", lat:33.60, lng:-114.52 },
hours:"24/7", prePass:true,
currentStatus:"open", lastFlagged:"2026-03-29T11:00:00Z", flaggedBy:"DesertSun",
notes:"California/Arizona border checkpoint. All oversize must stop. Can have long lines during morning hours." },
{ id:"WS-003", name:"Darien Weigh Station", route:"I-95 NB",
location:{ city:"Darien", state:"GA", lat:31.38, lng:-81.44 },
hours:"6am-10pm", prePass:true,
currentStatus:"open", lastFlagged:"2026-03-29T09:15:00Z", flaggedBy:"SE_Hauler",
notes:"Florida/Georgia border area. Oversize permits checked regularly." },
{ id:"WS-004", name:"Upton Weigh Station", route:"I-80 EB",
location:{ city:"Upton", state:"WY", lat:41.00, lng:-104.62 },
hours:"24/7", prePass:false,
currentStatus:"closed", lastFlagged:"2026-03-29T16:45:00Z", flaggedBy:"WYO_Escort",
notes:"Wyoming/Nebraska border area. All commercial traffic must stop when open. Wind closures may affect station hours." },
{ id:"WS-005", name:"Moriarty Port of Entry", route:"I-40 EB",
location:{ city:"Moriarty", state:"NM", lat:34.99, lng:-106.05 },
hours:"24/7", prePass:true,
currentStatus:"open", lastFlagged:"2026-03-29T10:30:00Z", flaggedBy:"SW_Oversize",
notes:"East of Albuquerque. All commercial vehicles must report. Oversize permits verified." },
{ id:"WS-006", name:"Robertson County Scales", route:"I-65 NB",
location:{ city:"Cross Plains", state:"TN", lat:36.53, lng:-86.69 },
hours:"6am-10pm", prePass:true,
currentStatus:"open", lastFlagged:"2026-03-29T08:00:00Z", flaggedBy:"VolunteerEscort",
notes:"North of Nashville. Moderate traffic. Oversize loads may be pulled in for permit check." },
{ id:"WS-007", name:"Lodi Weigh Station", route:"I-71 SB",
location:{ city:"Lodi", state:"OH", lat:41.04, lng:-82.01 },
hours:"7am-7pm", prePass:true,
currentStatus:"closed", lastFlagged:"2026-03-28T17:00:00Z", flaggedBy:"BuckeyePilot",
notes:"Between Cleveland and Columbus on I-71. Intermittent operation — often closed on weekends." },
{ id:"WS-008", name:"Clearfield Weigh Station", route:"I-80 WB",
location:{ city:"Clearfield", state:"PA", lat:41.01, lng:-78.44 },
hours:"6am-10pm", prePass:false,
currentStatus:"open", lastFlagged:"2026-03-29T12:00:00Z", flaggedBy:"PA_HeavyHaul",
notes:"Central PA on I-80. Oversize loads inspected — have PA permit documentation ready." },
{ id:"WS-009", name:"Woodburn Port of Entry", route:"I-5 NB",
location:{ city:"Woodburn", state:"OR", lat:45.15, lng:-122.85 },
hours:"24/7", prePass:true,
currentStatus:"open", lastFlagged:"2026-03-29T07:30:00Z", flaggedBy:"PNW_Pilot",
notes:"Major I-5 checkpoint. All commercial vehicles must stop. Oregon has strict oversize enforcement." },
{ id:"WS-010", name:"Fargo Weigh Station", route:"I-94 WB",
location:{ city:"West Fargo", state:"ND", lat:46.87, lng:-96.92 },
hours:"7am-9pm", prePass:true,
currentStatus:"open", lastFlagged:"2026-03-29T13:00:00Z", flaggedBy:"PrairieHauler",
notes:"Minnesota/North Dakota border area. Oil field and wind energy traffic frequent. Oversize permits checked." },
{ id:"WS-011", name:"Marshall Weigh Station", route:"I-20 EB",
location:{ city:"Marshall", state:"TX", lat:32.54, lng:-94.37 },
hours:"24/7", prePass:true,
currentStatus:"open", lastFlagged:"2026-03-29T15:00:00Z", flaggedBy:"LoneStarEscort",
notes:"East Texas near Louisiana border. All commercial vehicles must stop when open." },
{ id:"WS-012", name:"Valdosta Weigh Station", route:"I-75 NB",
location:{ city:"Valdosta", state:"GA", lat:30.87, lng:-83.28 },
hours:"6am-10pm", prePass:true,
currentStatus:"closed", lastFlagged:"2026-03-28T20:00:00Z", flaggedBy:"SE_Oversized",
notes:"Florida/Georgia border on I-75. Oversize loads always pulled in when station is open." }
];
// =====================================================================
// ROUTE CONDITIONS (Module 7)
// =====================================================================
const MOCK_ROUTE_CONDITIONS = [
{ id:"RC-001", type:"construction", severity:"major",
route:"I-10 WB", location:{ desc:"Between Tucson and Phoenix", state:"AZ", lat:32.43, lng:-111.57 },
description:"Lane closure restricting width to 11'6\". Oversize loads over 12' wide CANNOT pass — contact ADOT for route amendment.",
startDate:"2026-03-01", endDate:"2026-06-30", source:"ADOT 511", affectsOversize:true },
{ id:"RC-002", type:"closure", severity:"critical",
route:"I-40 EB", location:{ desc:"Bridge replacement near Flagstaff", state:"AZ", lat:35.17, lng:-111.68 },
description:"Full eastbound closure 9pm-5am nightly. Oversize loads on single-trip permits must contact ADOT for revised routing or schedule around closure.",
startDate:"2026-03-15", endDate:"2026-05-15", source:"ADOT 511", affectsOversize:true },
{ id:"RC-003", type:"construction", severity:"moderate",
route:"I-35 NB/SB", location:{ desc:"Oklahoma City metro area", state:"OK", lat:35.47, lng:-97.52 },
description:"Ongoing I-35/I-44 interchange reconstruction. Lane shifts and temporary barriers — oversize loads limited to 14' wide through work zone.",
startDate:"2026-01-15", endDate:"2026-09-30", source:"ODOT", affectsOversize:true },
{ id:"RC-004", type:"construction", severity:"minor",
route:"I-80 EB", location:{ desc:"Near North Platte", state:"NE", lat:41.12, lng:-100.78 },
description:"Shoulder work — right lane may be narrowed. Oversize loads over 14' wide should use caution. No permits affected.",
startDate:"2026-04-01", endDate:"2026-04-15", source:"NDOT", affectsOversize:false },
{ id:"RC-005", type:"closure", severity:"major",
route:"US-93 NB", location:{ desc:"Mountain pass south of Missoula", state:"MT", lat:46.32, lng:-113.86 },
description:"Seasonal closure for snow removal. Route impassable for all traffic. Oversize loads must use I-90 alternate.",
startDate:"2025-11-15", endDate:"2026-04-30", source:"MDT", affectsOversize:true },
{ id:"RC-006", type:"construction", severity:"moderate",
route:"I-75 SB", location:{ desc:"Atlanta metro — I-285 interchange", state:"GA", lat:33.85, lng:-84.36 },
description:"Interchange reconstruction. Lane restrictions and temporary barriers. Oversize loads over 12' wide must travel between 9pm-5am through work zone.",
startDate:"2026-02-01", endDate:"2026-08-30", source:"GDOT", affectsOversize:true }
];
// =====================================================================
// WEATHER / WIND ALERTS (Module 8)
// =====================================================================
const MOCK_WEATHER_ALERTS = [
{ id:"WX-001", type:"wind", severity:"warning",
region:"Texas Panhandle / Western Oklahoma",
routes:["I-40", "US-287", "US-83"],
description:"High Wind Warning — sustained winds 40-50 mph with gusts to 65 mph. Wide loads (12'+) should NOT travel.",
validFrom:"2026-03-30T06:00:00Z", validTo:"2026-03-30T22:00:00Z",
source:"NWS Amarillo", lat:35.22, lng:-101.83 },
{ id:"WX-002", type:"wind", severity:"advisory",
region:"Wyoming I-80 corridor",
routes:["I-80", "US-30"],
description:"Wind Advisory — crosswinds 25-35 mph with gusts to 50 mph. Light/high-profile oversize loads use extreme caution.",
validFrom:"2026-03-30T12:00:00Z", validTo:"2026-03-31T06:00:00Z",
source:"NWS Riverton", lat:41.54, lng:-107.22 },
{ id:"WX-003", type:"winter", severity:"warning",
region:"Colorado Rocky Mountains",
routes:["I-70 west of Denver", "US-6 Loveland Pass"],
description:"Winter Storm Warning — 12-18\" snow expected above 9,000'. Chain law in effect on I-70 in mountains. Oversize travel not recommended.",
validFrom:"2026-03-30T00:00:00Z", validTo:"2026-03-31T12:00:00Z",
source:"NWS Denver", lat:39.68, lng:-105.91 },
{ id:"WX-004", type:"fog", severity:"advisory",
region:"Central California Valley",
routes:["I-5", "CA-99"],
description:"Dense Fog Advisory — visibility below 1/4 mile. Oversize loads should delay departure until fog lifts. Expected to clear by 10am.",
validFrom:"2026-03-30T04:00:00Z", validTo:"2026-03-30T17:00:00Z",
source:"NWS Hanford", lat:36.60, lng:-119.77 },
{ id:"WX-005", type:"thunderstorm", severity:"watch",
region:"Central Texas / Oklahoma",
routes:["I-35", "I-44", "US-69"],
description:"Severe Thunderstorm Watch — potential for large hail and damaging winds. Wide loads should seek shelter if storms develop.",
validFrom:"2026-03-30T18:00:00Z", validTo:"2026-03-31T02:00:00Z",
source:"NWS Norman", lat:34.20, lng:-97.40 }
];
// =====================================================================
// SEASONAL RESTRICTIONS (Module 15)
// =====================================================================
const MOCK_SEASONAL_RESTRICTIONS = [
{ id:"SR-001", state:"MN", stateName:"Minnesota", type:"spring_weight", color:"#3b82f6",
title:"Spring Weight Restrictions",
startMonth:3, startDay:1, endMonth:5, endDay:15,
routes:"Most state highways (interstates generally exempt)",
description:"Weight restrictions to prevent road damage during spring thaw. Exact dates vary annually based on frost conditions. Check MnDOT for current year dates.",
impact:"Reduced weight limits — overweight loads may need to delay or use interstate-only routing." },
{ id:"SR-002", state:"WI", stateName:"Wisconsin", type:"spring_weight", color:"#3b82f6",
title:"Spring Weight Restrictions (Frost Law)",
startMonth:3, startDay:1, endMonth:5, endDay:15,
routes:"State and county highways (interstates exempt)",
description:"Annual frost law restrictions. Posted roads have reduced weight limits during spring thaw period.",
impact:"Overweight loads restricted on posted routes. Plan for interstate-only routing." },
{ id:"SR-003", state:"ND", stateName:"North Dakota", type:"spring_weight", color:"#3b82f6",
title:"Spring Load Restrictions",
startMonth:3, startDay:1, endMonth:5, endDay:31,
routes:"State highways and county roads",
description:"Annual spring load restrictions. Oil field and wind energy loads frequently affected.",
impact:"Weight limits reduced on state highways. Heavy loads should delay or use approved routes." },
{ id:"SR-004", state:"CO", stateName:"Colorado", type:"winter_closure", color:"#6366f1",
title:"Mountain Pass Seasonal Closures",
startMonth:10, startDay:15, endMonth:5, endDay:30,
routes:"Independence Pass (CO-82), Cottonwood Pass, Hagerman Pass, various unpaved passes",
description:"High mountain passes close for winter. I-70 remains open but may have chain laws and traction requirements.",
impact:"Oversize loads must use lower-elevation routes. I-70 Eisenhower Tunnel has height/width restrictions." },
{ id:"SR-005", state:"MT", stateName:"Montana", type:"winter_closure", color:"#6366f1",
title:"Mountain Pass Restrictions",
startMonth:11, startDay:1, endMonth:4, endDay:30,
routes:"Beartooth Highway (US-212), Going-to-the-Sun Road, various forest roads",
description:"High passes close for winter. Major corridors (I-90, I-15) remain open but conditions vary.",
impact:"Alternative routing may be required. Check MDT road conditions before departure." },
{ id:"SR-006", state:"WY", stateName:"Wyoming", type:"wind_season", color:"#f59e0b",
title:"High Wind Season",
startMonth:11, startDay:1, endMonth:4, endDay:30,
routes:"I-80, I-25 (especially southern Wyoming)",
description:"Severe crosswinds are common October through April, especially on I-80. Wind closures can last hours or days.",
impact:"Wide loads (12'+) frequently delayed or stopped. Monitor WYDOT road conditions. Budget extra travel days." },
{ id:"SR-007", state:"TX", stateName:"Texas", type:"holiday_blackout", color:"#ef4444",
title:"Holiday Travel Blackouts",
startMonth:1, startDay:1, endMonth:12, endDay:31,
routes:"All routes statewide",
description:"No oversize load movement on: New Year's Day, Memorial Day weekend (Sat-Mon), July 4th, Labor Day weekend (Sat-Mon), Thanksgiving (Thu-Sun), Christmas (Dec 24-26).",
impact:"Schedule loads to avoid holiday blackout periods. Loads in transit must park and wait." },
{ id:"SR-008", state:"IA", stateName:"Iowa", type:"spring_weight", color:"#3b82f6",
title:"Spring Frost Restrictions",
startMonth:2, startDay:15, endMonth:5, endDay:15,
routes:"County roads and some state highways",
description:"Frost-related weight restrictions. Wind energy loads frequently affected as many wind farm access roads are county-maintained.",
impact:"Heavy loads may need to delay. County permits may be suspended during restriction period." },
{ id:"SR-009", state:"NY", stateName:"New York", type:"holiday_blackout", color:"#ef4444",
title:"NYC Metro Oversize Blackouts",
startMonth:1, startDay:1, endMonth:12, endDay:31,
routes:"All routes in NYC boroughs, George Washington Bridge, major bridges/tunnels",
description:"NYC restricts oversize movement year-round except with special NYPD escort. Additional blackouts during holidays, events, and UN General Assembly (September).",
impact:"Route around NYC whenever possible. Allow 2+ weeks for NYPD escort coordination." },
{ id:"SR-010", state:"OR", stateName:"Oregon", type:"harvest_season", color:"#22c55e",
title:"Harvest Season Restrictions",
startMonth:8, startDay:1, endMonth:10, endDay:31,
routes:"Agricultural areas — Willamette Valley, Eastern Oregon farming regions",
description:"Increased farm equipment on roads during harvest. Some county roads may have temporary restrictions or slow farm equipment traffic.",
impact:"Expect slower travel through agricultural areas. Farm equipment may block narrow roads." }
];
// =====================================================================
// MOCK DOCUMENTS (Module 9 — Document Vault demo)
// =====================================================================
const MOCK_DOCUMENTS = [
{ id:"DOC-001", name:"TX Single Trip Permit — Load #LB-2026-006", type:"permit", state:"TX",
uploadDate:"2026-03-28", expiryDate:"2026-04-15", fileSize:"245 KB", status:"active" },
{ id:"DOC-002", name:"OK Single Trip Permit — Load #LB-2026-006", type:"permit", state:"OK",
uploadDate:"2026-03-28", expiryDate:"2026-04-15", fileSize:"198 KB", status:"active" },
{ id:"DOC-003", name:"Commercial Auto Insurance — Policy #CAI-2026-445", type:"insurance",
uploadDate:"2026-01-15", expiryDate:"2026-07-15", fileSize:"1.2 MB", status:"active" },
{ id:"DOC-004", name:"General Liability Certificate", type:"insurance",
uploadDate:"2026-01-15", expiryDate:"2027-01-15", fileSize:"890 KB", status:"active" },
{ id:"DOC-005", name:"TX Pilot Car Certification", type:"certification", state:"TX",
uploadDate:"2025-09-10", expiryDate:"2027-09-10", fileSize:"156 KB", status:"active" },
{ id:"DOC-006", name:"CA PCET Certification", type:"certification", state:"CA",
uploadDate:"2025-06-20", expiryDate:"2027-06-20", fileSize:"178 KB", status:"active" },
{ id:"DOC-007", name:"OH Annual Oversize Permit — 2025", type:"permit", state:"OH",
uploadDate:"2025-03-01", expiryDate:"2026-02-28", fileSize:"312 KB", status:"expired" },
{ id:"DOC-008", name:"Vehicle Registration — 2024 Ford F-150", type:"registration",
uploadDate:"2026-02-01", expiryDate:"2027-02-01", fileSize:"98 KB", status:"active" }
];

630
mock-data.js Normal file
View File

@@ -0,0 +1,630 @@
// =====================================================================
// MOCK DATA — All data is SIMULATED for POC/demonstration purposes.
// Regulation data MUST be verified with official state DOT sources
// before any real-world use. Do NOT rely on these values for routing,
// permitting, or escort decisions.
// =====================================================================
const MOCK_STATE_REGULATIONS = [
{ name:"Alabama", abbr:"AL", lat:32.32, lng:-86.90,
permitWidth:"8'6\"", permitHeight:"13'6\"", permitLength:"60'", permitWeight:"80,000 lbs",
escortWidth:"14' (1 front); 16'+ (front & rear)", escortHeight:"15'6\" (1 escort)", escortLength:"100' (1); 120'+ (2)", escortWeight:"150,000+ lbs",
travel:"30 min after sunrise to 30 min before sunset", holidays:"No movement New Year's, Memorial Day, July 4th, Labor Day, Thanksgiving, Christmas",
agency:"ALDOT Maintenance Bureau", url:"https://www.dot.state.al.us/", notes:"Superloads require 10 business days advance notice. Annual permits available for routine oversize." },
{ name:"Alaska", abbr:"AK", lat:63.59, lng:-154.49,
permitWidth:"10'0\"", permitHeight:"15'0\"", permitLength:"75'", permitWeight:"80,000 lbs (varies by route)",
escortWidth:"14' (1 front); 16'+ (front & rear)", escortHeight:"17'0\" (height pole)", escortLength:"110' (1); 130'+ (2)", escortWeight:"200,000+ lbs",
travel:"Daylight hours only", holidays:"No movement on major state holidays",
agency:"Alaska DOT&PF", url:"https://dot.alaska.gov/", notes:"Extreme weather may restrict movement. Many routes are single-lane — plan accordingly." },
{ name:"Arizona", abbr:"AZ", lat:34.05, lng:-111.09,
permitWidth:"8'6\"", permitHeight:"14'0\"", permitLength:"65'", permitWeight:"80,000 lbs",
escortWidth:"12' (1 front); 16'+ (front & rear)", escortHeight:"16'0\" (1 escort)", escortLength:"100' (1); 120'+ (2)", escortWeight:"160,000+ lbs",
travel:"Sunrise to sunset", holidays:"No movement on major holidays",
agency:"ADOT Permits Office", url:"https://azdot.gov/", notes:"I-10, I-17, I-40 have specific oversize restrictions during peak hours." },
{ name:"Arkansas", abbr:"AR", lat:34.80, lng:-92.20,
permitWidth:"8'6\"", permitHeight:"13'6\"", permitLength:"60'", permitWeight:"80,000 lbs",
escortWidth:"14' (1 front); 16'+ (front & rear)", escortHeight:"15'0\" (1 escort)", escortLength:"100' (1); 115'+ (2)", escortWeight:"150,000+ lbs",
travel:"30 min before sunrise to 30 min after sunset", holidays:"No movement on major holidays",
agency:"ArDOT Permits", url:"https://www.ardot.gov/", notes:"Many two-lane roads with limited shoulders — plan escorts carefully." },
{ name:"California", abbr:"CA", lat:36.78, lng:-119.42,
permitWidth:"8'6\"", permitHeight:"14'0\"", permitLength:"65'", permitWeight:"80,000 lbs",
escortWidth:"12' (1 front); 14'+ (front & rear)", escortHeight:"15'0\" (1 escort); 17'+ (height pole)", escortLength:"100' (1); 120'+ (2)", escortWeight:"150,000+ lbs",
travel:"Sunrise to sunset; some routes restricted to off-peak", holidays:"No movement on major holidays; Caltrans may restrict specific dates",
agency:"Caltrans Transportation Permits", url:"https://dot.ca.gov/programs/traffic-operations/transportation-permits", notes:"Strict environmental and bridge restrictions. Some routes require Caltrans survey. Pilot cars must meet CA certification requirements." },
{ name:"Colorado", abbr:"CO", lat:39.11, lng:-105.36,
permitWidth:"8'6\"", permitHeight:"14'6\"", permitLength:"70'", permitWeight:"80,000 lbs",
escortWidth:"13' (1 front); 16'+ (front & rear)", escortHeight:"16'0\" (1 escort)", escortLength:"100' (1); 120'+ (2)", escortWeight:"160,000+ lbs",
travel:"Sunrise to sunset", holidays:"No movement on major holidays",
agency:"CDOT Permits Unit", url:"https://www.codot.gov/", notes:"Mountain passes may have seasonal restrictions. I-70 tunnel restrictions for overheight." },
{ name:"Connecticut", abbr:"CT", lat:41.60, lng:-72.76,
permitWidth:"8'6\"", permitHeight:"13'6\"", permitLength:"60'", permitWeight:"80,000 lbs",
escortWidth:"12' (1 front); 14'+ (front & rear)", escortHeight:"14'6\" (1 escort)", escortLength:"90' (1); 110'+ (2)", escortWeight:"120,000+ lbs",
travel:"Sunrise to sunset; some routes night-only", holidays:"No movement on major holidays or weekends",
agency:"CT DOT Office of Permits", url:"https://portal.ct.gov/dot", notes:"Dense state with many low overpasses. Weekend travel restrictions on many routes." },
{ name:"Delaware", abbr:"DE", lat:39.00, lng:-75.50,
permitWidth:"8'6\"", permitHeight:"13'6\"", permitLength:"60'", permitWeight:"80,000 lbs",
escortWidth:"12' (1 front); 15'+ (front & rear)", escortHeight:"14'6\" (1 escort)", escortLength:"95' (1); 115'+ (2)", escortWeight:"130,000+ lbs",
travel:"Sunrise to sunset", holidays:"No movement on major holidays",
agency:"DelDOT Permits", url:"https://deldot.gov/", notes:"Small state but major freight corridor along I-95 and US-13." },
{ name:"Florida", abbr:"FL", lat:27.66, lng:-81.52,
permitWidth:"8'6\"", permitHeight:"13'6\"", permitLength:"60'", permitWeight:"80,000 lbs",
escortWidth:"14' (1 front); 16'+ (front & rear)", escortHeight:"15'0\" (1 escort)", escortLength:"100' (1); 120'+ (2)", escortWeight:"160,000+ lbs",
travel:"30 min before sunrise to 30 min after sunset", holidays:"No movement on major holidays",
agency:"FDOT Permits Office", url:"https://www.fdot.gov/", notes:"Bridge restrictions on many coastal routes. Hurricane season may affect permit availability." },
{ name:"Georgia", abbr:"GA", lat:32.16, lng:-82.90,
permitWidth:"8'6\"", permitHeight:"13'6\"", permitLength:"60'", permitWeight:"80,000 lbs",
escortWidth:"14' (1 front); 16'+ (front & rear)", escortHeight:"15'6\" (1 escort)", escortLength:"100' (1); 120'+ (2)", escortWeight:"150,000+ lbs",
travel:"30 min after sunrise to 30 min before sunset", holidays:"No movement on major holidays",
agency:"GDOT Permits Office", url:"https://www.dot.ga.gov/", notes:"I-285 (Atlanta perimeter) has specific oversize restrictions during rush hours." },
{ name:"Hawaii", abbr:"HI", lat:19.90, lng:-155.58,
permitWidth:"9'0\"", permitHeight:"14'0\"", permitLength:"65'", permitWeight:"80,000 lbs (varies by island)",
escortWidth:"12' (1 front); 14'+ (front & rear)", escortHeight:"14'6\" (1 escort)", escortLength:"80' (1); 100'+ (2)", escortWeight:"100,000+ lbs",
travel:"Varies by island and route", holidays:"No movement on major holidays",
agency:"Hawaii DOT Highways", url:"https://hidot.hawaii.gov/", notes:"Island-specific regulations. Inter-island transport requires barge. Very limited oversize routing on most islands." },
{ name:"Idaho", abbr:"ID", lat:44.07, lng:-114.74,
permitWidth:"8'6\"", permitHeight:"14'0\"", permitLength:"75'", permitWeight:"105,500 lbs",
escortWidth:"14' (1 front); 16'+ (front & rear)", escortHeight:"16'0\" (1 escort)", escortLength:"105' (1); 125'+ (2)", escortWeight:"180,000+ lbs",
travel:"30 min after sunrise to 30 min before sunset", holidays:"No movement on major holidays",
agency:"ITD Permits", url:"https://itd.idaho.gov/", notes:"Higher weight limits than most states. Mountain passes may have seasonal closures." },
{ name:"Illinois", abbr:"IL", lat:40.63, lng:-89.40,
permitWidth:"8'6\"", permitHeight:"13'6\"", permitLength:"60'", permitWeight:"80,000 lbs",
escortWidth:"14' (1 front); 16'+ (front & rear)", escortHeight:"15'0\" (1 escort)", escortLength:"100' (1); 115'+ (2)", escortWeight:"150,000+ lbs",
travel:"Sunrise to sunset", holidays:"No movement on major holidays",
agency:"IDOT Permits", url:"https://idot.illinois.gov/", notes:"Chicago metro area has extensive oversize restrictions. I-294 and I-90/94 may require off-peak travel." },
{ name:"Indiana", abbr:"IN", lat:40.27, lng:-86.13,
permitWidth:"8'6\"", permitHeight:"13'6\"", permitLength:"60'", permitWeight:"80,000 lbs",
escortWidth:"14' (1 front); 16'+ (front & rear)", escortHeight:"15'6\" (1 escort)", escortLength:"100' (1); 120'+ (2)", escortWeight:"150,000+ lbs",
travel:"30 min after sunrise to 30 min before sunset", holidays:"No movement on major holidays",
agency:"INDOT Permits", url:"https://www.in.gov/indot/", notes:"Major crossroads state — high oversize traffic on I-65, I-70, I-69." },
{ name:"Iowa", abbr:"IA", lat:41.88, lng:-93.10,
permitWidth:"8'6\"", permitHeight:"13'6\"", permitLength:"60'", permitWeight:"80,000 lbs",
escortWidth:"14'6\" (1 front); 16'+ (front & rear)", escortHeight:"15'6\" (1 escort)", escortLength:"100' (1); 120'+ (2)", escortWeight:"156,000+ lbs",
travel:"Sunrise to sunset", holidays:"No movement on major holidays",
agency:"Iowa DOT Motor Vehicle", url:"https://iowadot.gov/", notes:"Wind turbine corridor — frequent oversize loads on I-35 and I-80. Annual permits available." },
{ name:"Kansas", abbr:"KS", lat:39.01, lng:-98.48,
permitWidth:"8'6\"", permitHeight:"14'0\"", permitLength:"65'", permitWeight:"85,500 lbs",
escortWidth:"14' (1 front); 16'+ (front & rear)", escortHeight:"16'0\" (1 escort)", escortLength:"110' (1); 130'+ (2)", escortWeight:"160,000+ lbs",
travel:"30 min after sunrise to 30 min before sunset", holidays:"No movement on major holidays",
agency:"KDOT Division of Operations", url:"https://www.ksdot.gov/", notes:"Wide open terrain but high wind exposure. I-70 is primary east-west oversize corridor." },
{ name:"Kentucky", abbr:"KY", lat:37.84, lng:-84.27,
permitWidth:"8'6\"", permitHeight:"13'6\"", permitLength:"60'", permitWeight:"80,000 lbs",
escortWidth:"14' (1 front); 15'+ (front & rear)", escortHeight:"15'0\" (1 escort)", escortLength:"100' (1); 115'+ (2)", escortWeight:"150,000+ lbs",
travel:"30 min after sunrise to 30 min before sunset", holidays:"No movement on major holidays",
agency:"KYTC Department of Vehicle Regulation", url:"https://transportation.ky.gov/", notes:"Mountainous terrain in eastern KY limits some oversize routing." },
{ name:"Louisiana", abbr:"LA", lat:30.98, lng:-91.96,
permitWidth:"8'6\"", permitHeight:"13'6\"", permitLength:"60'", permitWeight:"80,000 lbs",
escortWidth:"14' (1 front); 16'+ (front & rear)", escortHeight:"15'0\" (1 escort)", escortLength:"100' (1); 120'+ (2)", escortWeight:"160,000+ lbs",
travel:"30 min after sunrise to 30 min before sunset", holidays:"No movement on major holidays; Mardi Gras restrictions in some areas",
agency:"DOTD Permits", url:"https://www.dotd.la.gov/", notes:"Many bridges with weight restrictions. Petrochemical industry generates frequent heavy/oversize loads." },
{ name:"Maine", abbr:"ME", lat:45.37, lng:-69.45,
permitWidth:"8'6\"", permitHeight:"13'6\"", permitLength:"60'", permitWeight:"80,000 lbs",
escortWidth:"13' (1 front); 15'+ (front & rear)", escortHeight:"14'6\" (1 escort)", escortLength:"95' (1); 110'+ (2)", escortWeight:"130,000+ lbs",
travel:"Sunrise to sunset", holidays:"No movement on major holidays",
agency:"MaineDOT", url:"https://www.maine.gov/mdot/", notes:"Wind energy projects in northern Maine. Limited highway infrastructure in rural areas." },
{ name:"Maryland", abbr:"MD", lat:39.05, lng:-76.64,
permitWidth:"8'6\"", permitHeight:"13'6\"", permitLength:"55'", permitWeight:"80,000 lbs",
escortWidth:"12' (1 front); 14'+ (front & rear)", escortHeight:"14'6\" (1 escort)", escortLength:"90' (1); 110'+ (2)", escortWeight:"120,000+ lbs",
travel:"Sunrise to sunset; night moves possible with special authorization", holidays:"No movement on major holidays or weekends without special permit",
agency:"MDOT SHA Hauling Permits", url:"https://www.roads.maryland.gov/", notes:"Baltimore/DC metro restrictions. Chesapeake Bay Bridge has strict oversize limitations." },
{ name:"Massachusetts", abbr:"MA", lat:42.41, lng:-71.38,
permitWidth:"8'6\"", permitHeight:"13'6\"", permitLength:"55'", permitWeight:"80,000 lbs",
escortWidth:"12' (1 front); 14'+ (front & rear)", escortHeight:"14'0\" (1 escort)", escortLength:"85' (1); 105'+ (2)", escortWeight:"120,000+ lbs",
travel:"Varies by route — many night-only requirements", holidays:"No movement on weekends or holidays without special permit",
agency:"MassDOT Permits", url:"https://www.mass.gov/massdot", notes:"Very restricted for oversize. Many moves must be done at night. Low overpasses throughout." },
{ name:"Michigan", abbr:"MI", lat:44.31, lng:-85.60,
permitWidth:"8'6\"", permitHeight:"13'6\"", permitLength:"60'", permitWeight:"80,000 lbs",
escortWidth:"14'6\" (1 front); 16'+ (front & rear)", escortHeight:"15'0\" (1 escort)", escortLength:"100' (1); 120'+ (2)", escortWeight:"160,000+ lbs",
travel:"30 min after sunrise to 30 min before sunset", holidays:"No movement on major holidays",
agency:"MDOT Permits Unit", url:"https://www.michigan.gov/mdot", notes:"Mackinac Bridge has strict oversize limitations and may require escort by bridge authority." },
{ name:"Minnesota", abbr:"MN", lat:46.73, lng:-94.69,
permitWidth:"8'6\"", permitHeight:"13'6\"", permitLength:"75'", permitWeight:"80,000 lbs",
escortWidth:"14'6\" (1 front); 16'+ (front & rear)", escortHeight:"15'6\" (1 escort)", escortLength:"100' (1); 120'+ (2)", escortWeight:"160,000+ lbs",
travel:"30 min after sunrise to 30 min before sunset", holidays:"No movement on major holidays",
agency:"MnDOT Office of Freight & Commercial Vehicle Operations", url:"https://www.dot.state.mn.us/", notes:"Spring weight restrictions on many roads. Wind energy transport corridor." },
{ name:"Mississippi", abbr:"MS", lat:32.35, lng:-89.40,
permitWidth:"8'6\"", permitHeight:"13'6\"", permitLength:"60'", permitWeight:"80,000 lbs",
escortWidth:"14' (1 front); 16'+ (front & rear)", escortHeight:"15'0\" (1 escort)", escortLength:"100' (1); 115'+ (2)", escortWeight:"150,000+ lbs",
travel:"Sunrise to sunset", holidays:"No movement on major holidays",
agency:"MDOT Permits Division", url:"https://mdot.ms.gov/", notes:"Bridge weight restrictions on many state routes." },
{ name:"Missouri", abbr:"MO", lat:37.96, lng:-91.83,
permitWidth:"8'6\"", permitHeight:"14'0\"", permitLength:"65'", permitWeight:"80,000 lbs",
escortWidth:"14' (1 front); 16'+ (front & rear)", escortHeight:"15'6\" (1 escort)", escortLength:"100' (1); 120'+ (2)", escortWeight:"160,000+ lbs",
travel:"30 min after sunrise to 30 min before sunset", holidays:"No movement on major holidays",
agency:"MoDOT Motor Carrier Services", url:"https://www.modot.org/", notes:"Major I-70 and I-44 freight corridor. Kansas City and St. Louis metro restrictions." },
{ name:"Montana", abbr:"MT", lat:46.88, lng:-110.36,
permitWidth:"8'6\"", permitHeight:"14'0\"", permitLength:"75'", permitWeight:"80,000 lbs",
escortWidth:"14'6\" (1 front); 16'+ (front & rear)", escortHeight:"16'0\" (1 escort)", escortLength:"110' (1); 130'+ (2)", escortWeight:"180,000+ lbs",
travel:"30 min after sunrise to 30 min before sunset", holidays:"No movement on major holidays",
agency:"MDT Motor Carrier Services", url:"https://www.mdt.mt.gov/", notes:"Long distances between services. Mountain passes may close seasonally. Mining equipment transport common." },
{ name:"Nebraska", abbr:"NE", lat:41.49, lng:-99.90,
permitWidth:"8'6\"", permitHeight:"14'6\"", permitLength:"75'", permitWeight:"80,000 lbs",
escortWidth:"14'6\" (1 front); 16'+ (front & rear)", escortHeight:"16'0\" (1 escort)", escortLength:"110' (1); 125'+ (2)", escortWeight:"160,000+ lbs",
travel:"30 min after sunrise to 30 min before sunset", holidays:"No movement on major holidays",
agency:"NDOT Permits Division", url:"https://dot.nebraska.gov/", notes:"Major wind energy corridor. I-80 primary oversize route." },
{ name:"Nevada", abbr:"NV", lat:38.80, lng:-116.42,
permitWidth:"8'6\"", permitHeight:"14'0\"", permitLength:"70'", permitWeight:"80,000 lbs",
escortWidth:"14' (1 front); 16'+ (front & rear)", escortHeight:"16'0\" (1 escort)", escortLength:"105' (1); 125'+ (2)", escortWeight:"170,000+ lbs",
travel:"Sunrise to sunset", holidays:"No movement on major holidays",
agency:"NDOT Permits", url:"https://www.dot.nv.gov/", notes:"Desert conditions — carry extra water and supplies. Solar/mining equipment transport frequent." },
{ name:"New Hampshire", abbr:"NH", lat:43.19, lng:-71.57,
permitWidth:"8'6\"", permitHeight:"13'6\"", permitLength:"60'", permitWeight:"80,000 lbs",
escortWidth:"12' (1 front); 14'+ (front & rear)", escortHeight:"14'6\" (1 escort)", escortLength:"90' (1); 110'+ (2)", escortWeight:"120,000+ lbs",
travel:"Sunrise to sunset", holidays:"No movement on major holidays",
agency:"NHDOT Bureau of Highway Maintenance", url:"https://www.nh.gov/dot/", notes:"Mountain terrain with tight curves. Many covered bridges with strict height/weight limits." },
{ name:"New Jersey", abbr:"NJ", lat:40.06, lng:-74.41,
permitWidth:"8'6\"", permitHeight:"13'6\"", permitLength:"55'", permitWeight:"80,000 lbs",
escortWidth:"12' (1 front); 14'+ (front & rear)", escortHeight:"14'0\" (1 escort)", escortLength:"85' (1); 105'+ (2)", escortWeight:"120,000+ lbs",
travel:"Night moves often required in metro areas", holidays:"No movement on weekends or holidays",
agency:"NJDOT Permits Bureau", url:"https://www.nj.gov/transportation/", notes:"Very dense state — many moves require night travel. NJ Turnpike and Garden State Parkway have strict restrictions." },
{ name:"New Mexico", abbr:"NM", lat:34.52, lng:-105.87,
permitWidth:"8'6\"", permitHeight:"14'0\"", permitLength:"65'", permitWeight:"80,000 lbs",
escortWidth:"14' (1 front); 16'+ (front & rear)", escortHeight:"16'0\" (1 escort)", escortLength:"110' (1); 125'+ (2)", escortWeight:"170,000+ lbs",
travel:"Sunrise to sunset", holidays:"No movement on major holidays",
agency:"NMDOT Motor Transportation Division", url:"https://www.dot.nm.gov/", notes:"Long distances between services. High winds in eastern plains. I-25 and I-40 main corridors." },
{ name:"New York", abbr:"NY", lat:43.00, lng:-75.00,
permitWidth:"8'6\"", permitHeight:"13'6\"", permitLength:"55'", permitWeight:"80,000 lbs",
escortWidth:"12' (1 front); 14'+ (front & rear)", escortHeight:"14'0\" (1 escort)", escortLength:"85' (1); 100'+ (2)", escortWeight:"120,000+ lbs",
travel:"Varies — night moves required in NYC metro", holidays:"No movement on holidays or weekends in metro areas",
agency:"NYSDOT Special Hauling Permits", url:"https://www.dot.ny.gov/", notes:"NYC boroughs have extreme restrictions — most oversize prohibited. Upstate much more permissive. Thruway has separate permit process." },
{ name:"North Carolina", abbr:"NC", lat:35.76, lng:-79.02,
permitWidth:"8'6\"", permitHeight:"13'6\"", permitLength:"60'", permitWeight:"80,000 lbs",
escortWidth:"14' (1 front); 16'+ (front & rear)", escortHeight:"15'0\" (1 escort)", escortLength:"100' (1); 115'+ (2)", escortWeight:"150,000+ lbs",
travel:"30 min after sunrise to 30 min before sunset", holidays:"No movement on major holidays",
agency:"NCDOT Oversize/Overweight Permits", url:"https://www.ncdot.gov/", notes:"Mountain routes in western NC have significant restrictions. I-85 and I-40 primary corridors." },
{ name:"North Dakota", abbr:"ND", lat:47.55, lng:-101.00,
permitWidth:"8'6\"", permitHeight:"14'0\"", permitLength:"75'", permitWeight:"105,500 lbs",
escortWidth:"14'6\" (1 front); 16'+ (front & rear)", escortHeight:"16'0\" (1 escort)", escortLength:"110' (1); 130'+ (2)", escortWeight:"200,000+ lbs",
travel:"30 min after sunrise to 30 min before sunset", holidays:"No movement on major holidays",
agency:"NDDOT Motor Carrier", url:"https://www.dot.nd.gov/", notes:"Oil field equipment transport very common. Higher weight limits than most states. Spring weight restrictions." },
{ name:"Ohio", abbr:"OH", lat:40.42, lng:-82.91,
permitWidth:"8'6\"", permitHeight:"13'6\"", permitLength:"60'", permitWeight:"80,000 lbs",
escortWidth:"14' (1 front); 16'+ (front & rear)", escortHeight:"15'0\" (1 escort)", escortLength:"100' (1); 115'+ (2)", escortWeight:"150,000+ lbs",
travel:"30 min after sunrise to 30 min before sunset", holidays:"No movement on major holidays",
agency:"ODOT Office of Permits", url:"https://www.transportation.ohio.gov/", notes:"Major manufacturing state — frequent transformer and heavy equipment loads. I-75, I-71, I-77 main corridors." },
{ name:"Oklahoma", abbr:"OK", lat:35.47, lng:-97.52,
permitWidth:"8'6\"", permitHeight:"14'0\"", permitLength:"65'", permitWeight:"80,000 lbs",
escortWidth:"14' (1 front); 16'+ (front & rear)", escortHeight:"16'0\" (1 escort)", escortLength:"110' (1); 125'+ (2)", escortWeight:"160,000+ lbs",
travel:"30 min after sunrise to 30 min before sunset", holidays:"No movement on major holidays",
agency:"ODOT Motor Carrier Permits", url:"https://oklahoma.gov/odot.html", notes:"Wind energy transport major industry. Turnpike system has separate oversize rules." },
{ name:"Oregon", abbr:"OR", lat:43.80, lng:-120.55,
permitWidth:"8'6\"", permitHeight:"14'0\"", permitLength:"65'", permitWeight:"80,000 lbs",
escortWidth:"14' (1 front); 16'+ (front & rear)", escortHeight:"15'6\" (1 escort)", escortLength:"105' (1); 120'+ (2)", escortWeight:"160,000+ lbs",
travel:"30 min after sunrise to 30 min before sunset", holidays:"No movement on major holidays",
agency:"ODOT Motor Carrier Transportation Division", url:"https://www.oregon.gov/odot/", notes:"Cascade Range creates routing challenges. Wind farm transport increasing. Portland metro restrictions." },
{ name:"Pennsylvania", abbr:"PA", lat:41.20, lng:-77.19,
permitWidth:"8'6\"", permitHeight:"13'6\"", permitLength:"60'", permitWeight:"80,000 lbs",
escortWidth:"13' (1 front); 15'+ (front & rear)", escortHeight:"14'6\" (1 escort)", escortLength:"95' (1); 110'+ (2)", escortWeight:"130,000+ lbs",
travel:"Sunrise to sunset; Turnpike 11pm-5am for superloads", holidays:"No movement on major holidays",
agency:"PennDOT Permits", url:"https://www.penndot.pa.gov/", notes:"Pennsylvania Turnpike has separate permit process. Many old bridges with weight limits. Hilly terrain throughout." },
{ name:"Rhode Island", abbr:"RI", lat:41.58, lng:-71.48,
permitWidth:"8'6\"", permitHeight:"13'6\"", permitLength:"55'", permitWeight:"80,000 lbs",
escortWidth:"12' (1 front); 14'+ (front & rear)", escortHeight:"14'0\" (1 escort)", escortLength:"85' (1); 100'+ (2)", escortWeight:"110,000+ lbs",
travel:"Varies — often night only", holidays:"No movement on weekends or holidays",
agency:"RIDOT Permits", url:"https://www.dot.ri.gov/", notes:"Smallest state but dense infrastructure. Most moves cross into MA or CT — coordinate multi-state." },
{ name:"South Carolina", abbr:"SC", lat:33.84, lng:-81.16,
permitWidth:"8'6\"", permitHeight:"13'6\"", permitLength:"60'", permitWeight:"80,000 lbs",
escortWidth:"14' (1 front); 16'+ (front & rear)", escortHeight:"15'0\" (1 escort)", escortLength:"100' (1); 115'+ (2)", escortWeight:"150,000+ lbs",
travel:"30 min after sunrise to 30 min before sunset", holidays:"No movement on major holidays",
agency:"SCDOT Oversize/Overweight Permits", url:"https://www.scdot.org/", notes:"Port of Charleston generates significant oversize freight. I-26 and I-95 main corridors." },
{ name:"South Dakota", abbr:"SD", lat:43.97, lng:-99.90,
permitWidth:"8'6\"", permitHeight:"14'0\"", permitLength:"75'", permitWeight:"80,000 lbs",
escortWidth:"14'6\" (1 front); 16'+ (front & rear)", escortHeight:"16'0\" (1 escort)", escortLength:"110' (1); 130'+ (2)", escortWeight:"180,000+ lbs",
travel:"30 min after sunrise to 30 min before sunset", holidays:"No movement on major holidays",
agency:"SDDOT Motor Carrier", url:"https://dot.sd.gov/", notes:"Wind energy and oil field transport. Spring weight restrictions on many routes." },
{ name:"Tennessee", abbr:"TN", lat:35.52, lng:-86.58,
permitWidth:"8'6\"", permitHeight:"13'6\"", permitLength:"60'", permitWeight:"80,000 lbs",
escortWidth:"14' (1 front); 16'+ (front & rear)", escortHeight:"15'0\" (1 escort)", escortLength:"100' (1); 115'+ (2)", escortWeight:"150,000+ lbs",
travel:"30 min after sunrise to 30 min before sunset", holidays:"No movement on major holidays",
agency:"TDOT Permits Office", url:"https://www.tn.gov/tdot.html", notes:"Major north-south corridor for oversize. Nashville/Memphis metro restrictions during peak hours." },
{ name:"Texas", abbr:"TX", lat:31.97, lng:-99.90,
permitWidth:"8'6\"", permitHeight:"14'0\"", permitLength:"65'", permitWeight:"80,000 lbs",
escortWidth:"14' (1 front); 16'+ (front & rear)", escortHeight:"17'0\" (height pole vehicle)", escortLength:"110' (1); 125'+ (2)", escortWeight:"200,000+ lbs (route-specific)",
travel:"30 min after sunrise to 30 min before sunset", holidays:"No movement on major state/federal holidays",
agency:"TxDMV Motor Carrier Division", url:"https://www.txdmv.gov/oversize-overweight-permits", notes:"Largest volume of oversize permits nationally. Annual permits available for routine oversize. Superloads over 254,300 lbs require TxDOT route study. Wind energy transport very common." },
{ name:"Utah", abbr:"UT", lat:39.32, lng:-111.09,
permitWidth:"8'6\"", permitHeight:"14'0\"", permitLength:"65'", permitWeight:"80,000 lbs",
escortWidth:"14' (1 front); 16'+ (front & rear)", escortHeight:"16'0\" (1 escort)", escortLength:"105' (1); 120'+ (2)", escortWeight:"170,000+ lbs",
travel:"Sunrise to sunset", holidays:"No movement on major holidays",
agency:"UDOT Motor Carrier Permits", url:"https://www.udot.utah.gov/", notes:"Canyon roads and mountain passes create routing challenges. Mining and energy equipment transport common." },
{ name:"Vermont", abbr:"VT", lat:44.56, lng:-72.58,
permitWidth:"8'6\"", permitHeight:"13'6\"", permitLength:"60'", permitWeight:"80,000 lbs",
escortWidth:"12' (1 front); 14'+ (front & rear)", escortHeight:"14'6\" (1 escort)", escortLength:"90' (1); 105'+ (2)", escortWeight:"120,000+ lbs",
travel:"Sunrise to sunset", holidays:"No movement on major holidays",
agency:"VTrans Permits", url:"https://vtrans.vermont.gov/", notes:"Covered bridges limit many routes. Mountain roads with tight switchbacks. Wind energy transport increasing." },
{ name:"Virginia", abbr:"VA", lat:37.43, lng:-78.66,
permitWidth:"8'6\"", permitHeight:"13'6\"", permitLength:"60'", permitWeight:"80,000 lbs",
escortWidth:"13' (1 front); 15'+ (front & rear)", escortHeight:"14'6\" (1 escort)", escortLength:"95' (1); 110'+ (2)", escortWeight:"140,000+ lbs",
travel:"Sunrise to sunset", holidays:"No movement on major holidays",
agency:"VDOT Permits", url:"https://www.virginiadot.org/", notes:"Northern VA/DC metro restrictions very strict. Hampton Roads port generates oversize traffic. Blue Ridge Parkway prohibited for oversize." },
{ name:"Washington", abbr:"WA", lat:47.75, lng:-120.74,
permitWidth:"8'6\"", permitHeight:"14'0\"", permitLength:"65'", permitWeight:"80,000 lbs",
escortWidth:"14' (1 front); 16'+ (front & rear)", escortHeight:"15'6\" (1 escort)", escortLength:"105' (1); 120'+ (2)", escortWeight:"160,000+ lbs",
travel:"30 min after sunrise to 30 min before sunset", holidays:"No movement on major holidays",
agency:"WSDOT Motor Carrier", url:"https://wsdot.wa.gov/", notes:"Cascade passes may close in winter. Seattle/Tacoma metro restrictions. Port traffic generates oversize loads." },
{ name:"West Virginia", abbr:"WV", lat:38.60, lng:-80.45,
permitWidth:"8'6\"", permitHeight:"13'6\"", permitLength:"60'", permitWeight:"80,000 lbs",
escortWidth:"13' (1 front); 15'+ (front & rear)", escortHeight:"15'0\" (1 escort)", escortLength:"95' (1); 110'+ (2)", escortWeight:"140,000+ lbs",
travel:"Sunrise to sunset", holidays:"No movement on major holidays",
agency:"WV DOH Permits", url:"https://transportation.wv.gov/", notes:"Very mountainous — many routes not suitable for oversize. Tunnels and narrow roads throughout. Coal/energy equipment transport." },
{ name:"Wisconsin", abbr:"WI", lat:43.78, lng:-88.79,
permitWidth:"8'6\"", permitHeight:"13'6\"", permitLength:"60'", permitWeight:"80,000 lbs",
escortWidth:"14' (1 front); 16'+ (front & rear)", escortHeight:"15'6\" (1 escort)", escortLength:"100' (1); 120'+ (2)", escortWeight:"150,000+ lbs",
travel:"30 min after sunrise to 30 min before sunset", holidays:"No movement on major holidays",
agency:"WisDOT Oversize Permits", url:"https://wisconsindot.gov/", notes:"Spring weight restrictions March-May. Milwaukee metro restrictions. Manufacturing equipment transport common." },
{ name:"Wyoming", abbr:"WY", lat:43.08, lng:-107.29,
permitWidth:"8'6\"", permitHeight:"14'0\"", permitLength:"75'", permitWeight:"80,000 lbs",
escortWidth:"14'6\" (1 front); 16'+ (front & rear)", escortHeight:"16'0\" (1 escort)", escortLength:"110' (1); 130'+ (2)", escortWeight:"180,000+ lbs",
travel:"30 min after sunrise to 30 min before sunset", holidays:"No movement on major holidays",
agency:"WYDOT Permits", url:"https://www.dot.state.wy.us/", notes:"Extreme wind conditions common. I-80 frequently closed for wind. Wind energy and mining equipment transport." },
{ name:"District of Columbia", abbr:"DC", lat:38.91, lng:-77.04,
permitWidth:"8'0\"", permitHeight:"12'6\"", permitLength:"50'", permitWeight:"70,000 lbs",
escortWidth:"10' (1 front); 12'+ (front & rear, police escort may be required)", escortHeight:"13'6\" (requires police escort)", escortLength:"75' (police escort required)", escortWeight:"100,000+ lbs (special authorization only)",
travel:"Night only — typically 9pm to 6am", holidays:"No movement on holidays, weekends, or during special events",
agency:"DDOT Permits", url:"https://ddot.dc.gov/", notes:"Extremely restricted for oversize. Most loads must travel at night with police escort. Avoid if possible — route around DC." }
];
// =====================================================================
// MOCK LOAD BOARD DATA
// =====================================================================
const MOCK_LOAD_BOARD = [
{
id: "LB-2026-001",
carrier: "Midwest Heavy Haul LLC",
origin: { city: "Des Moines", state: "IA", lat: 41.59, lng: -93.62 },
destination: { city: "Amarillo", state: "TX", lat: 35.22, lng: -101.83 },
departureDate: "2026-04-05",
dimensions: { width: "16'2\"", height: "14'8\"", length: "135'", weight: "185,000 lbs" },
description: "Wind turbine blade — 3-blade shipment, this is blade 1 of 3",
escortsNeeded: 2,
status: "posted",
postedDate: "2026-03-28",
contact: "dispatch@midwestheavy.example.com"
},
{
id: "LB-2026-002",
carrier: "Southern Power Transport Inc",
origin: { city: "Chattanooga", state: "TN", lat: 35.05, lng: -85.31 },
destination: { city: "Savannah", state: "GA", lat: 32.08, lng: -81.10 },
departureDate: "2026-04-08",
dimensions: { width: "14'0\"", height: "15'6\"", length: "95'", weight: "245,000 lbs" },
description: "Industrial transformer for Georgia Power substation",
escortsNeeded: 2,
status: "posted",
postedDate: "2026-03-27",
contact: "loads@southernpower.example.com"
},
{
id: "LB-2026-003",
carrier: "Pacific Coast Logistics",
origin: { city: "Sacramento", state: "CA", lat: 38.58, lng: -121.49 },
destination: { city: "Reno", state: "NV", lat: 39.53, lng: -119.81 },
departureDate: "2026-04-03",
dimensions: { width: "18'0\"", height: "13'2\"", length: "72'", weight: "95,000 lbs" },
description: "Pre-fabricated modular building section for data center",
escortsNeeded: 2,
status: "posted",
postedDate: "2026-03-29",
contact: "dispatch@pacificcoast.example.com"
},
{
id: "LB-2026-004",
carrier: "Keystone Crane & Rigging",
origin: { city: "Harrisburg", state: "PA", lat: 40.27, lng: -76.88 },
destination: { city: "Newark", state: "NJ", lat: 40.74, lng: -74.17 },
departureDate: "2026-04-10",
dimensions: { width: "12'4\"", height: "14'0\"", length: "110'", weight: "145,000 lbs" },
description: "Liebherr LTM 1300 crane boom section",
escortsNeeded: 1,
status: "posted",
postedDate: "2026-03-30",
contact: "ops@keystonecrane.example.com"
},
{
id: "LB-2026-005",
carrier: "Hoosier Heavy Transport",
origin: { city: "Gary", state: "IN", lat: 41.59, lng: -87.35 },
destination: { city: "Memphis", state: "TN", lat: 35.15, lng: -90.05 },
departureDate: "2026-04-07",
dimensions: { width: "11'8\"", height: "13'0\"", length: "82'", weight: "120,000 lbs" },
description: "Steel bridge girder — first of 4-piece set",
escortsNeeded: 1,
status: "posted",
postedDate: "2026-03-26",
contact: "freight@hoosierheavy.example.com"
},
{
id: "LB-2026-006",
carrier: "Lone Star Oversize LLC",
origin: { city: "Houston", state: "TX", lat: 29.76, lng: -95.37 },
destination: { city: "Oklahoma City", state: "OK", lat: 35.47, lng: -97.52 },
departureDate: "2026-04-12",
dimensions: { width: "15'0\"", height: "16'2\"", length: "68'", weight: "198,000 lbs" },
description: "Caterpillar 6060 mining excavator",
escortsNeeded: 2,
status: "posted",
postedDate: "2026-03-29",
contact: "dispatch@lonestoros.example.com"
},
{
id: "LB-2026-007",
carrier: "Carolina Modular Transport",
origin: { city: "Charlotte", state: "NC", lat: 35.23, lng: -80.84 },
destination: { city: "Jacksonville", state: "FL", lat: 30.33, lng: -81.66 },
departureDate: "2026-04-14",
dimensions: { width: "16'0\"", height: "14'4\"", length: "76'", weight: "82,000 lbs" },
description: "Modular home — half-section, 2 loads total",
escortsNeeded: 1,
status: "posted",
postedDate: "2026-03-30",
contact: "loads@carolinamod.example.com"
},
{
id: "LB-2026-008",
carrier: "Gulf Pipeline Services",
origin: { city: "Baton Rouge", state: "LA", lat: 30.45, lng: -91.19 },
destination: { city: "Hattiesburg", state: "MS", lat: 31.33, lng: -89.29 },
departureDate: "2026-04-06",
dimensions: { width: "12'0\"", height: "12'6\"", length: "95'", weight: "110,000 lbs" },
description: "48-inch pipeline section, coated and ready for burial",
escortsNeeded: 1,
status: "in_transit",
postedDate: "2026-03-22",
contact: "ops@gulfpipeline.example.com"
},
{
id: "LB-2026-009",
carrier: "Badger State Heavy Haul",
origin: { city: "Milwaukee", state: "WI", lat: 43.04, lng: -87.91 },
destination: { city: "Peoria", state: "IL", lat: 40.69, lng: -89.59 },
departureDate: "2026-04-15",
dimensions: { width: "14'8\"", height: "15'0\"", length: "88'", weight: "165,000 lbs" },
description: "Caterpillar diesel generator for manufacturing plant",
escortsNeeded: 1,
status: "posted",
postedDate: "2026-03-28",
contact: "dispatch@badgerheavy.example.com"
},
{
id: "LB-2026-010",
carrier: "Rocky Mountain Logistics",
origin: { city: "Billings", state: "MT", lat: 45.78, lng: -108.50 },
destination: { city: "Spokane", state: "WA", lat: 47.66, lng: -117.43 },
departureDate: "2026-04-09",
dimensions: { width: "13'6\"", height: "15'8\"", length: "104'", weight: "210,000 lbs" },
description: "Pressure vessel for refinery — requires height pole",
escortsNeeded: 2,
status: "posted",
postedDate: "2026-03-27",
contact: "freight@rockymtnlog.example.com"
},
{
id: "LB-2026-011",
carrier: "Palmetto Oversize Inc",
origin: { city: "Columbia", state: "SC", lat: 34.00, lng: -81.03 },
destination: { city: "Montgomery", state: "AL", lat: 32.37, lng: -86.30 },
departureDate: "2026-04-11",
dimensions: { width: "11'4\"", height: "14'2\"", length: "78'", weight: "135,000 lbs" },
description: "Industrial boiler for power plant",
escortsNeeded: 1,
status: "posted",
postedDate: "2026-03-29",
contact: "dispatch@palmettoos.example.com"
},
{
id: "LB-2026-012",
carrier: "Desert Sun Transport",
origin: { city: "Phoenix", state: "AZ", lat: 33.45, lng: -112.07 },
destination: { city: "Denver", state: "CO", lat: 39.74, lng: -104.99 },
departureDate: "2026-04-16",
dimensions: { width: "14'6\"", height: "17'4\"", length: "92'", weight: "175,000 lbs" },
description: "Mining haul truck bed — Komatsu 930E component",
escortsNeeded: 2,
status: "posted",
postedDate: "2026-03-30",
contact: "loads@desertsuntx.example.com"
}
];
// =====================================================================
// MOCK ESCORT OPERATORS
// =====================================================================
const MOCK_ESCORT_OPERATORS = [
{
id: "EO-001", name: "Mike's Pilot Car Service",
location: { city: "Dallas", state: "TX", lat: 32.78, lng: -96.80 },
status: "available",
certifications: ["TX", "OK", "LA", "AR", "NM"],
vehicleType: "2024 Ford F-150 — Amber lights, height pole, full signage",
rating: 4.9, totalJobs: 342,
experience: "12 years",
contact: "mike@mikespilotcar.example.com",
phone: "(214) 555-0187",
bio: "Veteran pilot car operator specializing in wind energy and heavy haul escort across the southern plains."
},
{
id: "EO-002", name: "Pacific Escort Services",
location: { city: "Sacramento", state: "CA", lat: 38.58, lng: -121.49 },
status: "available",
certifications: ["CA", "NV", "OR", "AZ"],
vehicleType: "2023 Chevy Silverado — CA certified, height pole, oversize banners",
rating: 4.8, totalJobs: 218,
experience: "8 years",
contact: "dispatch@pacificescort.example.com",
phone: "(916) 555-0234",
bio: "California-certified pilot car service. Experienced in Caltrans routes and mountain pass escorts."
},
{
id: "EO-003", name: "Buckeye Pilot Vehicles",
location: { city: "Columbus", state: "OH", lat: 39.96, lng: -82.99 },
status: "available",
certifications: ["OH", "PA", "IN", "MI", "WV", "KY"],
vehicleType: "2025 RAM 1500 — Full lighting package, CB radio, GPS tracking",
rating: 4.7, totalJobs: 189,
experience: "6 years",
contact: "info@buckeyepilot.example.com",
phone: "(614) 555-0312",
bio: "Midwest corridor specialist. Regular runs through the Ohio Valley and Great Lakes region."
},
{
id: "EO-004", name: "Keystone Escort Co",
location: { city: "Harrisburg", state: "PA", lat: 40.27, lng: -76.88 },
status: "on_job",
certifications: ["PA", "NJ", "NY", "DE", "MD", "CT"],
vehicleType: "2024 Ford F-250 — Night lighting package, height pole, DOT signage",
rating: 4.9, totalJobs: 276,
experience: "10 years",
contact: "dispatch@keystoneescort.example.com",
phone: "(717) 555-0145",
bio: "Northeast corridor expert. Experienced with night moves in metro areas and PA Turnpike superloads."
},
{
id: "EO-005", name: "Peach State Pilots",
location: { city: "Atlanta", state: "GA", lat: 33.75, lng: -84.39 },
status: "available",
certifications: ["GA", "SC", "NC", "FL", "AL", "TN"],
vehicleType: "2023 Toyota Tundra — Full safety package, height pole, LED arrows",
rating: 4.6, totalJobs: 154,
experience: "5 years",
contact: "dispatch@peachstatepilots.example.com",
phone: "(404) 555-0278",
bio: "Southeast specialist covering the I-85 and I-75 corridors. Port of Savannah regular."
},
{
id: "EO-006", name: "Sunshine Escort Vehicles",
location: { city: "Orlando", state: "FL", lat: 28.54, lng: -81.38 },
status: "available",
certifications: ["FL", "GA", "AL", "SC"],
vehicleType: "2024 Chevy Colorado — Amber lights, OVERSIZE LOAD signs, CB radio",
rating: 4.5, totalJobs: 98,
experience: "3 years",
contact: "info@sunshineescort.example.com",
phone: "(407) 555-0391",
bio: "Florida specialist. Experienced with coastal routes and bridge clearances."
},
{
id: "EO-007", name: "Crossroads Pilot Service",
location: { city: "Indianapolis", state: "IN", lat: 39.77, lng: -86.16 },
status: "available",
certifications: ["IN", "OH", "IL", "MI", "KY"],
vehicleType: "2024 Ford Ranger — Height pole, full lighting, dual CB radios",
rating: 4.8, totalJobs: 231,
experience: "9 years",
contact: "dispatch@crossroadspilot.example.com",
phone: "(317) 555-0456",
bio: "Indianapolis-based, covering the crossroads of America. Specialize in I-65 and I-70 corridor escorts."
},
{
id: "EO-008", name: "Prairie Pilot Cars",
location: { city: "Springfield", state: "IL", lat: 39.78, lng: -89.65 },
status: "on_job",
certifications: ["IL", "MO", "IA", "WI", "IN"],
vehicleType: "2023 GMC Sierra — Full escort package, arrow board",
rating: 4.7, totalJobs: 167,
experience: "7 years",
contact: "ops@prairiepilot.example.com",
phone: "(217) 555-0523",
bio: "Central Illinois based. Wind turbine blade escort specialist on I-39 and I-55 corridors."
},
{
id: "EO-009", name: "Tar Heel Escorts LLC",
location: { city: "Charlotte", state: "NC", lat: 35.23, lng: -80.84 },
status: "available",
certifications: ["NC", "SC", "VA", "TN", "GA"],
vehicleType: "2025 Toyota Tacoma — Full pilot car setup, height pole, GPS",
rating: 4.6, totalJobs: 143,
experience: "4 years",
contact: "info@tarheelescorts.example.com",
phone: "(704) 555-0612",
bio: "Carolina specialist. Experienced with I-85 corridor and mountain routes in western NC."
},
{
id: "EO-010", name: "Bayou Escort Services",
location: { city: "Baton Rouge", state: "LA", lat: 30.45, lng: -91.19 },
status: "available",
certifications: ["LA", "TX", "MS", "AR"],
vehicleType: "2024 Ford F-150 — Full light bar, height pole, waterproof signage",
rating: 4.8, totalJobs: 205,
experience: "11 years",
contact: "dispatch@bayouescort.example.com",
phone: "(225) 555-0789",
bio: "Gulf Coast specialist. Regular petrochemical and refinery equipment escorts. Expert in LA bridge routes."
},
{
id: "EO-011", name: "Sooner Pilot Vehicle Co",
location: { city: "Oklahoma City", state: "OK", lat: 35.47, lng: -97.52 },
status: "available",
certifications: ["OK", "TX", "KS", "AR", "MO"],
vehicleType: "2024 RAM 1500 — Full escort setup, dual amber lights",
rating: 4.7, totalJobs: 178,
experience: "8 years",
contact: "info@soonerpilot.example.com",
phone: "(405) 555-0834",
bio: "Oklahoma wind energy corridor specialist. Regular blade and tower section escorts on I-35 and I-40."
},
{
id: "EO-012", name: "Volunteer State Escorts",
location: { city: "Nashville", state: "TN", lat: 36.16, lng: -86.78 },
status: "on_job",
certifications: ["TN", "KY", "AL", "GA", "MS", "AR"],
vehicleType: "2023 Chevy Silverado — Amber light bar, height pole, arrow board, CB",
rating: 4.9, totalJobs: 289,
experience: "14 years",
contact: "dispatch@volunteerescorts.example.com",
phone: "(615) 555-0956",
bio: "One of Tennessee's most experienced escort services. Specialize in transformer and heavy equipment moves throughout the Southeast."
}
];

193
nav.js Normal file
View File

@@ -0,0 +1,193 @@
// =============================================
// Shared Navigation, Banner, and Footer
// Include on every page via <script src="nav.js">
// =============================================
function renderNav(activePage) {
const nav = document.getElementById('main-nav');
if (!nav) return;
const isActive = (id) => id === activePage;
const linkClass = (id) => isActive(id) ? 'text-amber-400 font-semibold' : 'text-gray-300 hover:text-white';
const dropLinkClass = (id) => isActive(id) ? 'bg-amber-50 text-amber-700 font-semibold' : 'text-slate-700 hover:bg-slate-50';
const regulationsActive = ['regulations','equipment','contacts','calendar'].includes(activePage);
const roadIntelActive = ['truckstops','bridges','weighstations','alerts'].includes(activePage);
nav.innerHTML = `
<nav class="bg-slate-900 text-white fixed top-0 w-full z-50 shadow-lg">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16">
<a href="index.html" class="flex items-center space-x-2 flex-shrink-0">
<span class="text-2xl">🚛</span>
<span class="text-xl font-bold text-amber-400 tracking-tight">PilotEdge</span>
</a>
<!-- Desktop Nav -->
<div class="hidden lg:flex items-center space-x-1">
<a href="index.html" class="${linkClass('home')} px-3 py-2 rounded-md text-sm transition-colors">Home</a>
<!-- Regulations Dropdown -->
<div class="relative group">
<button class="${regulationsActive ? 'text-amber-400 font-semibold' : 'text-gray-300 hover:text-white'} px-3 py-2 rounded-md text-sm transition-colors flex items-center gap-1">
Regulations
<svg class="w-3.5 h-3.5 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div class="absolute left-0 top-full pt-1 hidden group-hover:block" style="min-width:220px;">
<div class="bg-white rounded-xl shadow-xl border border-slate-200 py-2">
<a href="regulations.html" class="${dropLinkClass('regulations')} block px-4 py-2.5 text-sm transition-colors">
<div class="font-medium">State Regulations Map</div>
<div class="text-xs text-slate-400 mt-0.5">Permits & escort thresholds</div>
</a>
<a href="regulations.html#equipment" class="${dropLinkClass('equipment')} block px-4 py-2.5 text-sm transition-colors">
<div class="font-medium">Equipment Requirements</div>
<div class="text-xs text-slate-400 mt-0.5">Escort & carrier gear by state</div>
</a>
<a href="contacts.html" class="${dropLinkClass('contacts')} block px-4 py-2.5 text-sm transition-colors">
<div class="font-medium">DOT Contact Directory</div>
<div class="text-xs text-slate-400 mt-0.5">Permit office phone & email</div>
</a>
<a href="calendar.html" class="${dropLinkClass('calendar')} block px-4 py-2.5 text-sm transition-colors">
<div class="font-medium">Seasonal Calendar</div>
<div class="text-xs text-slate-400 mt-0.5">Restrictions & closures</div>
</a>
</div>
</div>
</div>
<!-- Road Intel Dropdown -->
<div class="relative group">
<button class="${roadIntelActive ? 'text-amber-400 font-semibold' : 'text-gray-300 hover:text-white'} px-3 py-2 rounded-md text-sm transition-colors flex items-center gap-1">
Road Intel
<svg class="w-3.5 h-3.5 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div class="absolute left-0 top-full pt-1 hidden group-hover:block" style="min-width:220px;">
<div class="bg-white rounded-xl shadow-xl border border-slate-200 py-2">
<a href="truckstops.html" class="${dropLinkClass('truckstops')} block px-4 py-2.5 text-sm transition-colors">
<div class="font-medium">Truck Stops & Parking</div>
<div class="text-xs text-slate-400 mt-0.5">Oversize-friendly locations</div>
</a>
<a href="bridges.html" class="${dropLinkClass('bridges')} block px-4 py-2.5 text-sm transition-colors">
<div class="font-medium">Bridge Clearances</div>
<div class="text-xs text-slate-400 mt-0.5">Height & width restrictions</div>
</a>
<a href="weighstations.html" class="${dropLinkClass('weighstations')} block px-4 py-2.5 text-sm transition-colors">
<div class="font-medium">Weigh Stations</div>
<div class="text-xs text-slate-400 mt-0.5">Live open/closed status</div>
</a>
<a href="alerts.html" class="${dropLinkClass('alerts')} block px-4 py-2.5 text-sm transition-colors">
<div class="font-medium">Route & Weather Alerts</div>
<div class="text-xs text-slate-400 mt-0.5">Closures, construction, wind</div>
</a>
</div>
</div>
</div>
<a href="loadboard.html" class="${linkClass('loadboard')} px-3 py-2 rounded-md text-sm transition-colors">Load Board</a>
<a href="locator.html" class="${linkClass('locator')} px-3 py-2 rounded-md text-sm transition-colors">Find Escorts</a>
<a href="documents.html" class="${linkClass('documents')} px-3 py-2 rounded-md text-sm transition-colors">Documents</a>
<a href="order.html" class="ml-2 bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold px-4 py-2 rounded-lg transition-colors shadow-md hover:shadow-lg text-sm">
Request Service
</a>
</div>
<!-- Mobile menu button -->
<button onclick="toggleMobileMenu()" class="lg:hidden text-gray-300 hover:text-white p-2" aria-label="Toggle menu">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
</svg>
</button>
</div>
</div>
<!-- Mobile Menu -->
<div id="mobile-menu" class="lg:hidden hidden border-t border-slate-700 px-4 pb-4 max-h-[80vh] overflow-y-auto">
<a href="index.html" class="block py-2 px-3 mt-2 rounded ${isActive('home') ? 'text-amber-400 bg-slate-800' : 'text-gray-300'}">Home</a>
<div class="mt-2 mb-1 px-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">Regulations</div>
<a href="regulations.html" class="block py-2 px-3 rounded ${isActive('regulations') ? 'text-amber-400 bg-slate-800' : 'text-gray-300'}">State Regulations Map</a>
<a href="regulations.html#equipment" class="block py-2 px-3 rounded ${isActive('equipment') ? 'text-amber-400 bg-slate-800' : 'text-gray-300'}">Equipment Requirements</a>
<a href="contacts.html" class="block py-2 px-3 rounded ${isActive('contacts') ? 'text-amber-400 bg-slate-800' : 'text-gray-300'}">DOT Contacts</a>
<a href="calendar.html" class="block py-2 px-3 rounded ${isActive('calendar') ? 'text-amber-400 bg-slate-800' : 'text-gray-300'}">Seasonal Calendar</a>
<div class="mt-2 mb-1 px-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">Road Intel</div>
<a href="truckstops.html" class="block py-2 px-3 rounded ${isActive('truckstops') ? 'text-amber-400 bg-slate-800' : 'text-gray-300'}">Truck Stops & Parking</a>
<a href="bridges.html" class="block py-2 px-3 rounded ${isActive('bridges') ? 'text-amber-400 bg-slate-800' : 'text-gray-300'}">Bridge Clearances</a>
<a href="weighstations.html" class="block py-2 px-3 rounded ${isActive('weighstations') ? 'text-amber-400 bg-slate-800' : 'text-gray-300'}">Weigh Stations</a>
<a href="alerts.html" class="block py-2 px-3 rounded ${isActive('alerts') ? 'text-amber-400 bg-slate-800' : 'text-gray-300'}">Route & Weather Alerts</a>
<div class="mt-2 mb-1 px-3 text-xs font-semibold text-slate-500 uppercase tracking-wider">Services</div>
<a href="loadboard.html" class="block py-2 px-3 rounded ${isActive('loadboard') ? 'text-amber-400 bg-slate-800' : 'text-gray-300'}">Load Board</a>
<a href="locator.html" class="block py-2 px-3 rounded ${isActive('locator') ? 'text-amber-400 bg-slate-800' : 'text-gray-300'}">Find Escorts</a>
<a href="documents.html" class="block py-2 px-3 rounded ${isActive('documents') ? 'text-amber-400 bg-slate-800' : 'text-gray-300'}">Document Vault</a>
<a href="order.html" class="block py-2 px-3 mt-3 bg-amber-500 text-slate-900 font-bold rounded-lg text-center">Request Service</a>
</div>
</nav>
`;
}
function toggleMobileMenu() {
const menu = document.getElementById('mobile-menu');
if (menu) menu.classList.toggle('hidden');
}
function renderBanner() {
const banner = document.getElementById('poc-banner');
if (!banner) return;
banner.innerHTML = `
<div class="bg-amber-100 border-b border-amber-300 text-amber-900 text-center text-sm py-2 mt-16 px-4">
⚠️ <strong>POC / Demo</strong> — All regulation data is simulated for demonstration purposes. Verify with official state DOT sources before real-world use.
</div>
`;
}
function renderFooter() {
const footer = document.getElementById('main-footer');
if (!footer) return;
footer.innerHTML = `
<footer class="bg-slate-900 text-gray-400 mt-16">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div class="grid md:grid-cols-4 gap-8">
<div>
<div class="flex items-center space-x-2 mb-4">
<span class="text-2xl">🚛</span>
<span class="text-lg font-bold text-amber-400">PilotEdge</span>
</div>
<p class="text-sm">Your complete resource for oversize and overdimensional load hauling. Built by industry professionals, for industry professionals.</p>
</div>
<div>
<h4 class="text-white font-semibold mb-3">Regulations</h4>
<ul class="space-y-2 text-sm">
<li><a href="regulations.html" class="hover:text-white transition-colors">State Regulations Map</a></li>
<li><a href="contacts.html" class="hover:text-white transition-colors">DOT Contact Directory</a></li>
<li><a href="calendar.html" class="hover:text-white transition-colors">Seasonal Calendar</a></li>
</ul>
</div>
<div>
<h4 class="text-white font-semibold mb-3">Road Intel</h4>
<ul class="space-y-2 text-sm">
<li><a href="truckstops.html" class="hover:text-white transition-colors">Truck Stops & Parking</a></li>
<li><a href="bridges.html" class="hover:text-white transition-colors">Bridge Clearances</a></li>
<li><a href="weighstations.html" class="hover:text-white transition-colors">Weigh Stations</a></li>
<li><a href="alerts.html" class="hover:text-white transition-colors">Route & Weather Alerts</a></li>
</ul>
</div>
<div>
<h4 class="text-white font-semibold mb-3">Services</h4>
<ul class="space-y-2 text-sm">
<li><a href="loadboard.html" class="hover:text-white transition-colors">Load Board</a></li>
<li><a href="locator.html" class="hover:text-white transition-colors">Find Escort Vehicles</a></li>
<li><a href="documents.html" class="hover:text-white transition-colors">Document Vault</a></li>
<li><a href="order.html" class="hover:text-white transition-colors">Request Escort Service</a></li>
</ul>
</div>
</div>
<div class="border-t border-slate-700 mt-8 pt-6 text-center text-sm">
&copy; 2026 PilotEdge. All rights reserved. | <span class="text-amber-400">V1 Proof of Concept</span>
</div>
</div>
</footer>
`;
}

241
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="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>

440
regulations.html Normal file
View File

@@ -0,0 +1,440 @@
<!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="mock-data.js"></script>
<script src="mock-data-extended.js"></script>
<script src="nav.js"></script>
<script>
renderNav('regulations');
renderBanner();
renderFooter();
// 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>

241
truckstops.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>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="mock-data.js"></script>
<script src="mock-data-extended.js"></script>
<script src="nav.js"></script>
<script>
renderNav('truckstops');
renderBanner();
renderFooter();
// 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>

266
weighstations.html Normal file
View File

@@ -0,0 +1,266 @@
<!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="mock-data.js"></script>
<script src="mock-data-extended.js"></script>
<script src="nav.js"></script>
<script>
renderNav('weighstations');
renderBanner();
renderFooter();
// ── 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>