Reorganize frontend into public/ with pages/ and js/ subdirectories
- public/index.html — landing page at root - public/pages/ — all feature pages (regulations, loadboard, etc.) - public/js/ — api.js, nav.js, mock data files - All links updated to absolute paths (/pages/, /js/) - Express static path updated to serve from public/ - Seed script path updated for new mock data location - README updated with new project structure and setup guide - Added .env.example template Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
233
public/index.html
Normal file
233
public/index.html
Normal 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="/pages/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="/pages/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="/pages/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="/pages/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="/pages/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="/pages/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="/pages/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="/pages/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="/pages/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="/pages/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="/pages/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="/pages/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="/pages/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="/pages/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="/js/nav.js"></script>
|
||||
<script>
|
||||
renderNav('home');
|
||||
renderBanner();
|
||||
renderFooter();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
448
public/js/api.js
Normal file
448
public/js/api.js
Normal file
@@ -0,0 +1,448 @@
|
||||
// =====================================================================
|
||||
// PilotEdge API Client
|
||||
// Fetches data from the backend API and transforms responses to match
|
||||
// the shapes expected by the existing frontend rendering code.
|
||||
// Replace mock-data.js and mock-data-extended.js with this file.
|
||||
// =====================================================================
|
||||
|
||||
const API_BASE = '/api';
|
||||
|
||||
const PilotEdge = {
|
||||
// Auth token management
|
||||
_token: localStorage.getItem('pilotedge_token'),
|
||||
|
||||
setToken(token) {
|
||||
this._token = token;
|
||||
if (token) localStorage.setItem('pilotedge_token', token);
|
||||
else localStorage.removeItem('pilotedge_token');
|
||||
},
|
||||
|
||||
getToken() {
|
||||
return this._token;
|
||||
},
|
||||
|
||||
getUser() {
|
||||
const raw = localStorage.getItem('pilotedge_user');
|
||||
return raw ? JSON.parse(raw) : null;
|
||||
},
|
||||
|
||||
setUser(user) {
|
||||
if (user) localStorage.setItem('pilotedge_user', JSON.stringify(user));
|
||||
else localStorage.removeItem('pilotedge_user');
|
||||
},
|
||||
|
||||
// Core fetch wrapper
|
||||
async request(path, options = {}) {
|
||||
const headers = { 'Content-Type': 'application/json', ...options.headers };
|
||||
if (this._token) headers['Authorization'] = `Bearer ${this._token}`;
|
||||
|
||||
const res = await fetch(`${API_BASE}${path}`, { ...options, headers });
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
const err = new Error(body.error || `API error ${res.status}`);
|
||||
err.status = res.status;
|
||||
throw err;
|
||||
}
|
||||
return res.json();
|
||||
},
|
||||
|
||||
async get(path) { return this.request(path); },
|
||||
async post(path, data) { return this.request(path, { method: 'POST', body: JSON.stringify(data) }); },
|
||||
async put(path, data) { return this.request(path, { method: 'PUT', body: JSON.stringify(data) }); },
|
||||
async del(path) { return this.request(path, { method: 'DELETE' }); },
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Auth
|
||||
// -----------------------------------------------------------------
|
||||
async register(email, password, name, role) {
|
||||
const res = await this.post('/auth/register', { email, password, name, role });
|
||||
this.setToken(res.token);
|
||||
this.setUser(res.user);
|
||||
return res;
|
||||
},
|
||||
|
||||
async login(email, password) {
|
||||
const res = await this.post('/auth/login', { email, password });
|
||||
this.setToken(res.token);
|
||||
this.setUser(res.user);
|
||||
return res;
|
||||
},
|
||||
|
||||
logout() {
|
||||
this.setToken(null);
|
||||
this.setUser(null);
|
||||
},
|
||||
|
||||
async me() { return this.get('/auth/me'); },
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Regulations — returns MOCK_STATE_REGULATIONS-compatible shape
|
||||
// -----------------------------------------------------------------
|
||||
async getRegulations() {
|
||||
const states = await this.get('/regulations');
|
||||
return states.map(s => ({
|
||||
name: s.name,
|
||||
abbr: s.abbr,
|
||||
lat: s.lat,
|
||||
lng: s.lng,
|
||||
permitWidth: s.regulation?.permitWidth || '',
|
||||
permitHeight: s.regulation?.permitHeight || '',
|
||||
permitLength: s.regulation?.permitLength || '',
|
||||
permitWeight: s.regulation?.permitWeight || '',
|
||||
escortWidth: s.regulation?.escortWidth || '',
|
||||
escortHeight: s.regulation?.escortHeight || '',
|
||||
escortLength: s.regulation?.escortLength || '',
|
||||
escortWeight: s.regulation?.escortWeight || '',
|
||||
travel: s.regulation?.travelRestrictions || '',
|
||||
holidays: s.regulation?.holidays || '',
|
||||
agency: s.regulation?.agency || '',
|
||||
url: s.regulation?.url || '',
|
||||
notes: s.regulation?.notes || '',
|
||||
}));
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Equipment — returns MOCK_STATE_EQUIPMENT-compatible shape
|
||||
// Object keyed by state abbr: { TX: { escort: {...}, carrier: {...} } }
|
||||
// -----------------------------------------------------------------
|
||||
async getEquipment() {
|
||||
const states = await this.get('/regulations');
|
||||
const equipment = {};
|
||||
for (const s of states) {
|
||||
if (!s.equipmentRequirements) {
|
||||
const full = await this.get(`/regulations/${s.abbr}`);
|
||||
s.equipmentRequirements = full.equipmentRequirements || [];
|
||||
}
|
||||
if (s.equipmentRequirements.length > 0) {
|
||||
equipment[s.abbr] = {};
|
||||
for (const eq of s.equipmentRequirements) {
|
||||
const obj = {
|
||||
certification: eq.certification || '',
|
||||
vehicle: eq.vehicle || '',
|
||||
signs: eq.signs || '',
|
||||
lights: eq.lights || '',
|
||||
heightPole: eq.heightPole || '',
|
||||
flags: eq.flags || '',
|
||||
communication: eq.communication || '',
|
||||
safety: eq.safetyGear || '',
|
||||
};
|
||||
if (eq.type === 'escort') equipment[s.abbr].escort = obj;
|
||||
else if (eq.type === 'carrier') {
|
||||
// Parse carrier safetyGear back to individual fields
|
||||
const gear = eq.safetyGear || '';
|
||||
equipment[s.abbr].carrier = {
|
||||
signs: eq.signs || '',
|
||||
flags: eq.flags || '',
|
||||
lights: eq.lights || '',
|
||||
cones: extractGearField(gear, 'Cones'),
|
||||
fireExtinguisher: extractGearField(gear, 'Fire ext'),
|
||||
triangles: extractGearField(gear, 'Triangles'),
|
||||
flares: extractGearField(gear, 'Flares'),
|
||||
firstAid: extractGearField(gear, 'First aid'),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return equipment;
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Contacts — returns MOCK_STATE_CONTACTS-compatible shape
|
||||
// Object keyed by state abbr: { AL: { name, permit, police, email, hours, portal } }
|
||||
// -----------------------------------------------------------------
|
||||
async getContacts() {
|
||||
const contacts = await this.get('/contacts');
|
||||
const result = {};
|
||||
for (const c of contacts) {
|
||||
result[c.state.abbr] = {
|
||||
name: c.state.name,
|
||||
permit: c.permitPhone,
|
||||
police: c.policePhone,
|
||||
email: c.email,
|
||||
hours: c.hours,
|
||||
portal: c.portalUrl,
|
||||
};
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Calendar — returns MOCK_SEASONAL_RESTRICTIONS-compatible shape
|
||||
// -----------------------------------------------------------------
|
||||
async getSeasonalRestrictions() {
|
||||
const restrictions = await this.get('/calendar');
|
||||
return restrictions.map(r => ({
|
||||
id: r.id,
|
||||
state: r.state?.abbr || '',
|
||||
stateName: r.state?.name || '',
|
||||
type: r.type,
|
||||
title: r.name,
|
||||
startMonth: r.startMonth,
|
||||
startDay: 1,
|
||||
endMonth: r.endMonth,
|
||||
endDay: 28,
|
||||
description: r.description,
|
||||
color: getRestrictionColor(r.type),
|
||||
routes: '',
|
||||
impact: '',
|
||||
}));
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Truck Stops — returns MOCK_TRUCK_STOPS-compatible shape
|
||||
// -----------------------------------------------------------------
|
||||
async getTruckStops() {
|
||||
const stops = await this.get('/truckstops');
|
||||
return stops.map(ts => ({
|
||||
id: ts.id,
|
||||
name: ts.name,
|
||||
type: 'truck_stop',
|
||||
location: {
|
||||
city: ts.address?.split(',')[0]?.trim() || '',
|
||||
state: ts.state?.abbr || '',
|
||||
lat: ts.lat,
|
||||
lng: ts.lng,
|
||||
},
|
||||
oversizeFriendly: ts.hasOversizeParking,
|
||||
entranceWidth: ts.entranceWidth || '',
|
||||
entranceHeight: ts.entranceHeight || '',
|
||||
lotSize: ts.lotSqFt ? `${ts.lotSqFt} sq ft` : '',
|
||||
oversizeCapacity: '',
|
||||
facilities: ts.facilities || [],
|
||||
description: '',
|
||||
comments: (ts.contributions || []).map(c => ({
|
||||
user: c.user?.name || 'Anonymous',
|
||||
date: c.createdAt,
|
||||
text: c.content,
|
||||
})),
|
||||
}));
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Bridges — returns MOCK_BRIDGE_CLEARANCES-compatible shape
|
||||
// -----------------------------------------------------------------
|
||||
async getBridges() {
|
||||
const bridges = await this.get('/bridges');
|
||||
return bridges.map(b => ({
|
||||
id: b.id,
|
||||
route: b.route,
|
||||
mileMarker: '',
|
||||
type: b.name.split(' at ')[0] || 'Bridge',
|
||||
location: {
|
||||
desc: b.name.split(' at ')[1] || b.name,
|
||||
city: '',
|
||||
state: b.state?.abbr || '',
|
||||
lat: b.lat,
|
||||
lng: b.lng,
|
||||
},
|
||||
clearanceHeight: `${b.heightClearance}'`,
|
||||
clearanceWidth: b.widthClearance ? `${b.widthClearance}'` : 'Unrestricted',
|
||||
weightLimit: b.weightLimit ? `${b.weightLimit} lbs` : 'No posted limit',
|
||||
notes: '',
|
||||
}));
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Weigh Stations — returns MOCK_WEIGH_STATIONS-compatible shape
|
||||
// -----------------------------------------------------------------
|
||||
async getWeighStations() {
|
||||
const stations = await this.get('/weighstations');
|
||||
return stations.map(ws => ({
|
||||
id: ws.id,
|
||||
name: ws.name,
|
||||
route: ws.route,
|
||||
location: {
|
||||
city: '',
|
||||
state: ws.state?.abbr || '',
|
||||
lat: ws.lat,
|
||||
lng: ws.lng,
|
||||
},
|
||||
hours: ws.hours,
|
||||
prePass: ws.prePass,
|
||||
currentStatus: ws.currentStatus,
|
||||
direction: ws.direction,
|
||||
lastFlagged: ws.lastStatusUpdate || null,
|
||||
flaggedBy: '',
|
||||
notes: '',
|
||||
}));
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Alerts — returns MOCK_ROUTE_CONDITIONS + MOCK_WEATHER_ALERTS shapes
|
||||
// -----------------------------------------------------------------
|
||||
async getAlerts() {
|
||||
const alerts = await this.get('/alerts');
|
||||
const routeConditions = [];
|
||||
const weatherAlerts = [];
|
||||
|
||||
for (const a of alerts) {
|
||||
if (a.type === 'weather' || a.type === 'wind') {
|
||||
weatherAlerts.push({
|
||||
id: a.id,
|
||||
type: a.type,
|
||||
severity: a.severity,
|
||||
region: a.state?.name || '',
|
||||
routes: a.route ? a.route.split(', ') : [],
|
||||
description: a.description,
|
||||
validFrom: a.startsAt,
|
||||
validTo: a.endsAt,
|
||||
source: 'NWS',
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
});
|
||||
} else {
|
||||
routeConditions.push({
|
||||
id: a.id,
|
||||
type: a.type,
|
||||
severity: a.severity,
|
||||
route: a.route,
|
||||
location: {
|
||||
desc: a.description.substring(0, 60),
|
||||
state: a.state?.abbr || '',
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
description: a.description,
|
||||
startDate: a.startsAt,
|
||||
endDate: a.endsAt,
|
||||
source: 'State DOT',
|
||||
affectsOversize: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
return { routeConditions, weatherAlerts };
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Load Board — returns MOCK_LOAD_BOARD-compatible shape
|
||||
// -----------------------------------------------------------------
|
||||
async getLoads() {
|
||||
const data = await this.get('/loads?limit=100');
|
||||
return (data.loads || []).map(l => ({
|
||||
id: l.id,
|
||||
carrier: l.poster?.name || 'Unknown',
|
||||
origin: parseLocation(l.origin),
|
||||
destination: parseLocation(l.destination),
|
||||
departureDate: l.pickupDate,
|
||||
dimensions: {
|
||||
width: l.width,
|
||||
height: l.height,
|
||||
length: l.length,
|
||||
weight: l.weight,
|
||||
},
|
||||
description: l.description,
|
||||
escortsNeeded: l.escortsNeeded,
|
||||
status: l.status,
|
||||
postedDate: l.createdAt,
|
||||
contact: l.poster?.name || '',
|
||||
}));
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Escort Operators — returns MOCK_ESCORT_OPERATORS-compatible shape
|
||||
// -----------------------------------------------------------------
|
||||
async getEscortOperators() {
|
||||
const escorts = await this.get('/escorts');
|
||||
return escorts.map(e => ({
|
||||
id: e.id,
|
||||
name: e.user?.name || 'Unknown',
|
||||
location: {
|
||||
city: '',
|
||||
state: '',
|
||||
lat: e.lat,
|
||||
lng: e.lng,
|
||||
},
|
||||
status: e.availability,
|
||||
certifications: e.certifications || [],
|
||||
vehicleType: e.vehicleType,
|
||||
rating: e.rating,
|
||||
totalJobs: e.ratingCount || 0,
|
||||
experience: '',
|
||||
contact: e.user?.email || '',
|
||||
phone: e.phone,
|
||||
bio: e.bio,
|
||||
}));
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Documents — returns MOCK_DOCUMENTS-compatible shape
|
||||
// -----------------------------------------------------------------
|
||||
async getDocuments() {
|
||||
try {
|
||||
const docs = await this.get('/documents');
|
||||
return docs.map(d => ({
|
||||
id: d.id,
|
||||
name: d.filename,
|
||||
type: d.type,
|
||||
state: '',
|
||||
uploadDate: d.createdAt,
|
||||
expiryDate: d.expiresAt,
|
||||
fileSize: formatFileSize(d.sizeBytes),
|
||||
status: d.expiresAt && new Date(d.expiresAt) < new Date() ? 'expired' : 'active',
|
||||
}));
|
||||
} catch (err) {
|
||||
// If not authenticated, return empty array
|
||||
if (err.status === 401) return [];
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Orders
|
||||
// -----------------------------------------------------------------
|
||||
async submitOrder(orderData) {
|
||||
return this.post('/orders', orderData);
|
||||
},
|
||||
|
||||
async getOrders() {
|
||||
return this.get('/orders');
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Contributions
|
||||
// -----------------------------------------------------------------
|
||||
async submitContribution(entityType, entityId, type, content) {
|
||||
return this.post('/contributions', { entityType, entityId, type, content });
|
||||
},
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Helper functions
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
function extractGearField(gear, label) {
|
||||
const match = gear.match(new RegExp(`${label}:\\s*([^;]+)`));
|
||||
return match ? match[1].trim() : '';
|
||||
}
|
||||
|
||||
function getRestrictionColor(type) {
|
||||
const colors = {
|
||||
spring_weight: '#3b82f6',
|
||||
winter_closure: '#8b5cf6',
|
||||
harvest: '#f59e0b',
|
||||
holiday_blackout: '#ef4444',
|
||||
};
|
||||
return colors[type] || '#6b7280';
|
||||
}
|
||||
|
||||
function parseLocation(str) {
|
||||
// Parse "City, ST" into { city, state, lat: 0, lng: 0 }
|
||||
if (!str) return { city: '', state: '', lat: 0, lng: 0 };
|
||||
const parts = str.split(',').map(s => s.trim());
|
||||
return {
|
||||
city: parts[0] || '',
|
||||
state: parts[1] || '',
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
if (!bytes) return '0 B';
|
||||
const units = ['B', 'KB', 'MB', 'GB'];
|
||||
let i = 0;
|
||||
let size = bytes;
|
||||
while (size >= 1024 && i < units.length - 1) { size /= 1024; i++; }
|
||||
return `${Math.round(size * 10) / 10} ${units[i]}`;
|
||||
}
|
||||
775
public/js/mock-data-extended.js
Normal file
775
public/js/mock-data-extended.js
Normal 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
public/js/mock-data.js
Normal file
630
public/js/mock-data.js
Normal 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
public/js/nav.js
Normal file
193
public/js/nav.js
Normal file
@@ -0,0 +1,193 @@
|
||||
// =============================================
|
||||
// Shared Navigation, Banner, and Footer
|
||||
// Include on every page via <script src="/js/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="/" 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="/" 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="/pages/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="/pages/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="/pages/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="/pages/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="/pages/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="/pages/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="/pages/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="/pages/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="/pages/loadboard.html" class="${linkClass('loadboard')} px-3 py-2 rounded-md text-sm transition-colors">Load Board</a>
|
||||
<a href="/pages/locator.html" class="${linkClass('locator')} px-3 py-2 rounded-md text-sm transition-colors">Find Escorts</a>
|
||||
<a href="/pages/documents.html" class="${linkClass('documents')} px-3 py-2 rounded-md text-sm transition-colors">Documents</a>
|
||||
|
||||
<a href="/pages/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="/" 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="/pages/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="/pages/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="/pages/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="/pages/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="/pages/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="/pages/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="/pages/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="/pages/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="/pages/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="/pages/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="/pages/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="/pages/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="/pages/regulations.html" class="hover:text-white transition-colors">State Regulations Map</a></li>
|
||||
<li><a href="/pages/contacts.html" class="hover:text-white transition-colors">DOT Contact Directory</a></li>
|
||||
<li><a href="/pages/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="/pages/truckstops.html" class="hover:text-white transition-colors">Truck Stops & Parking</a></li>
|
||||
<li><a href="/pages/bridges.html" class="hover:text-white transition-colors">Bridge Clearances</a></li>
|
||||
<li><a href="/pages/weighstations.html" class="hover:text-white transition-colors">Weigh Stations</a></li>
|
||||
<li><a href="/pages/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="/pages/loadboard.html" class="hover:text-white transition-colors">Load Board</a></li>
|
||||
<li><a href="/pages/locator.html" class="hover:text-white transition-colors">Find Escort Vehicles</a></li>
|
||||
<li><a href="/pages/documents.html" class="hover:text-white transition-colors">Document Vault</a></li>
|
||||
<li><a href="/pages/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">
|
||||
© 2026 PilotEdge. All rights reserved. | <span class="text-amber-400">V1 Proof of Concept</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
`;
|
||||
}
|
||||
423
public/pages/alerts.html
Normal file
423
public/pages/alerts.html
Normal file
@@ -0,0 +1,423 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Route & 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 & Weather Alerts</h1>
|
||||
<p class="text-lg text-gray-400 max-w-3xl">Know about closures, construction, and weather conditions on your permitted route BEFORE you depart.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Important Info Box -->
|
||||
<section class="max-w-7xl mx-auto px-4 pt-8 w-full">
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-2xl p-6">
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="text-2xl flex-shrink-0">ℹ️</span>
|
||||
<div>
|
||||
<h3 class="font-bold text-blue-900 text-lg mb-1">Important — Single-Trip Permitted Loads</h3>
|
||||
<p class="text-blue-800 text-sm leading-relaxed">Single-trip permitted loads <strong>MUST</strong> follow their permitted route exactly. If a closure or construction zone is on your permitted route, <strong>contact the permit authority for a route amendment before departing.</strong> Deviating from a permitted route without authorization can result in fines, permit revocation, and liability issues.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Stats Bar -->
|
||||
<section id="stats-bar" class="max-w-7xl mx-auto px-4 pt-6 w-full">
|
||||
<!-- populated by JS -->
|
||||
</section>
|
||||
|
||||
<!-- Filters -->
|
||||
<section class="max-w-7xl mx-auto px-4 pt-6 w-full">
|
||||
<div class="bg-white rounded-2xl shadow-lg p-6">
|
||||
<h2 class="text-lg font-bold text-slate-900 mb-4">Filter Alerts</h2>
|
||||
<div class="flex flex-wrap items-end gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Type</label>
|
||||
<select id="filter-type" class="border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-amber-400 focus:border-amber-400">
|
||||
<option value="all">All Types</option>
|
||||
<option value="construction">🚧 Construction</option>
|
||||
<option value="closure">⛔ Closure</option>
|
||||
<option value="wind">💨 Wind</option>
|
||||
<option value="winter">❄️ Winter Storm</option>
|
||||
<option value="fog">🌫️ Fog</option>
|
||||
<option value="thunderstorm">⛈️ Thunderstorm</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">Severity</label>
|
||||
<select id="filter-severity" class="border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-amber-400 focus:border-amber-400">
|
||||
<option value="all">All Severities</option>
|
||||
<option value="minor">Minor</option>
|
||||
<option value="moderate">Moderate</option>
|
||||
<option value="major">Major</option>
|
||||
<option value="critical">Critical</option>
|
||||
<option value="advisory">Advisory</option>
|
||||
<option value="watch">Watch</option>
|
||||
<option value="warning">Warning</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="checkbox" id="filter-oversize" class="w-4 h-4 text-amber-500 border-slate-300 rounded focus:ring-amber-400">
|
||||
<label for="filter-oversize" class="text-sm font-medium text-slate-700">Affects oversize only</label>
|
||||
</div>
|
||||
<button onclick="resetFilters()" class="text-sm text-amber-600 hover:text-amber-700 font-medium underline">Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tab Toggle -->
|
||||
<section class="max-w-7xl mx-auto px-4 pt-6 w-full">
|
||||
<div class="flex gap-2">
|
||||
<button id="tab-all" onclick="setTab('all')" class="px-5 py-2.5 rounded-xl text-sm font-semibold transition-colors">All Alerts</button>
|
||||
<button id="tab-route" onclick="setTab('route')" class="px-5 py-2.5 rounded-xl text-sm font-semibold transition-colors">🚧 Route Conditions</button>
|
||||
<button id="tab-weather" onclick="setTab('weather')" class="px-5 py-2.5 rounded-xl text-sm font-semibold transition-colors">🌦️ Weather Alerts</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Map -->
|
||||
<section class="max-w-7xl mx-auto px-4 pt-6 w-full">
|
||||
<div class="bg-white rounded-2xl shadow-lg p-4">
|
||||
<div id="map"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Route Conditions Cards -->
|
||||
<section id="route-section" class="max-w-7xl mx-auto px-4 pt-8 w-full">
|
||||
<h2 class="text-2xl font-bold text-slate-900 mb-4 flex items-center gap-2">🚧 Route Conditions</h2>
|
||||
<div id="route-cards" class="grid md:grid-cols-2 gap-6">
|
||||
<!-- populated by JS -->
|
||||
</div>
|
||||
<p id="route-empty" class="hidden text-slate-500 text-center py-8">No route conditions match your filters.</p>
|
||||
</section>
|
||||
|
||||
<!-- Weather Alerts Cards -->
|
||||
<section id="weather-section" class="max-w-7xl mx-auto px-4 pt-8 pb-8 w-full">
|
||||
<h2 class="text-2xl font-bold text-slate-900 mb-4 flex items-center gap-2">🌦️ Weather Alerts</h2>
|
||||
<div id="weather-cards" class="grid md:grid-cols-2 gap-6">
|
||||
<!-- populated by JS -->
|
||||
</div>
|
||||
<p id="weather-empty" class="hidden text-slate-500 text-center py-8">No weather alerts match your filters.</p>
|
||||
</section>
|
||||
|
||||
<div id="main-footer"></div>
|
||||
|
||||
<script src="/js/api.js"></script>
|
||||
<script src="/js/nav.js"></script>
|
||||
<script>
|
||||
renderNav('alerts');
|
||||
renderBanner();
|
||||
renderFooter();
|
||||
|
||||
(async () => {
|
||||
const alertData = await PilotEdge.getAlerts();
|
||||
const MOCK_ROUTE_CONDITIONS = alertData.routeConditions;
|
||||
const MOCK_WEATHER_ALERTS = alertData.weatherAlerts;
|
||||
|
||||
// ── State ──
|
||||
let activeTab = 'all';
|
||||
let map, markersLayer;
|
||||
|
||||
// ── Helpers ──
|
||||
function formatDate(dateStr) {
|
||||
return new Date(dateStr + 'T00:00:00').toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
||||
}
|
||||
|
||||
function formatDateTime(isoStr) {
|
||||
return new Date(isoStr).toLocaleString('en-US', {
|
||||
month: 'short', day: 'numeric', year: 'numeric',
|
||||
hour: 'numeric', minute: '2-digit', timeZoneName: 'short'
|
||||
});
|
||||
}
|
||||
|
||||
// ── Badge helpers ──
|
||||
const typeBadge = {
|
||||
construction: { label: '🚧 Construction', cls: 'bg-orange-100 text-orange-800' },
|
||||
closure: { label: '⛔ Closure', cls: 'bg-red-100 text-red-800' },
|
||||
wind: { label: '💨 Wind', cls: 'bg-blue-100 text-blue-800' },
|
||||
winter: { label: '❄️ Winter Storm', cls: 'bg-indigo-100 text-indigo-800' },
|
||||
fog: { label: '🌫️ Fog', cls: 'bg-gray-100 text-gray-700' },
|
||||
thunderstorm: { label: '⛈️ Thunderstorm', cls: 'bg-purple-100 text-purple-800' }
|
||||
};
|
||||
|
||||
const routeSeverityBadge = {
|
||||
minor: { label: 'Minor', cls: 'bg-green-100 text-green-800' },
|
||||
moderate: { label: 'Moderate', cls: 'bg-amber-100 text-amber-800' },
|
||||
major: { label: 'Major', cls: 'bg-orange-100 text-orange-800' },
|
||||
critical: { label: 'Critical', cls: 'bg-red-100 text-red-800' }
|
||||
};
|
||||
|
||||
const weatherSeverityBadge = {
|
||||
advisory: { label: 'Advisory', cls: 'bg-yellow-100 text-yellow-800' },
|
||||
watch: { label: 'Watch', cls: 'bg-orange-100 text-orange-800' },
|
||||
warning: { label: 'Warning', cls: 'bg-red-100 text-red-800' }
|
||||
};
|
||||
|
||||
// ── Filtering ──
|
||||
function getFilters() {
|
||||
return {
|
||||
type: document.getElementById('filter-type').value,
|
||||
severity: document.getElementById('filter-severity').value,
|
||||
oversizeOnly: document.getElementById('filter-oversize').checked
|
||||
};
|
||||
}
|
||||
|
||||
function filterRouteConditions() {
|
||||
const f = getFilters();
|
||||
return MOCK_ROUTE_CONDITIONS.filter(rc => {
|
||||
if (f.type !== 'all' && rc.type !== f.type) return false;
|
||||
if (f.severity !== 'all' && rc.severity !== f.severity) return false;
|
||||
if (f.oversizeOnly && !rc.affectsOversize) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function filterWeatherAlerts() {
|
||||
const f = getFilters();
|
||||
return MOCK_WEATHER_ALERTS.filter(wa => {
|
||||
if (f.type !== 'all' && wa.type !== f.type) return false;
|
||||
if (f.severity !== 'all' && wa.severity !== f.severity) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
document.getElementById('filter-type').value = 'all';
|
||||
document.getElementById('filter-severity').value = 'all';
|
||||
document.getElementById('filter-oversize').checked = false;
|
||||
renderAll();
|
||||
}
|
||||
|
||||
// ── Stats bar ──
|
||||
function renderStats() {
|
||||
const routeCount = MOCK_ROUTE_CONDITIONS.length;
|
||||
const weatherCount = MOCK_WEATHER_ALERTS.length;
|
||||
const oversizeCount = MOCK_ROUTE_CONDITIONS.filter(r => r.affectsOversize).length;
|
||||
|
||||
document.getElementById('stats-bar').innerHTML = `
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div class="bg-white rounded-2xl shadow-lg p-5 flex items-center gap-4">
|
||||
<div class="w-12 h-12 bg-orange-100 rounded-xl flex items-center justify-center text-2xl">🚧</div>
|
||||
<div>
|
||||
<p class="text-2xl font-bold text-slate-900">${routeCount}</p>
|
||||
<p class="text-sm text-slate-500">Active Route Conditions</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-2xl shadow-lg p-5 flex items-center gap-4">
|
||||
<div class="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center text-2xl">🌦️</div>
|
||||
<div>
|
||||
<p class="text-2xl font-bold text-slate-900">${weatherCount}</p>
|
||||
<p class="text-sm text-slate-500">Active Weather Alerts</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-2xl shadow-lg p-5 flex items-center gap-4">
|
||||
<div class="w-12 h-12 bg-red-100 rounded-xl flex items-center justify-center text-2xl">⚠️</div>
|
||||
<div>
|
||||
<p class="text-2xl font-bold text-slate-900">${oversizeCount}</p>
|
||||
<p class="text-sm text-slate-500">Alerts Affecting Oversize</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ── Tabs ──
|
||||
function setTab(tab) {
|
||||
activeTab = tab;
|
||||
renderAll();
|
||||
}
|
||||
|
||||
function renderTabButtons() {
|
||||
['all', 'route', 'weather'].forEach(t => {
|
||||
const btn = document.getElementById('tab-' + t);
|
||||
if (t === activeTab) {
|
||||
btn.className = 'px-5 py-2.5 rounded-xl text-sm font-semibold transition-colors bg-amber-500 text-slate-900 shadow-md';
|
||||
} else {
|
||||
btn.className = 'px-5 py-2.5 rounded-xl text-sm font-semibold transition-colors bg-white text-slate-600 hover:bg-slate-100 shadow';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Map ──
|
||||
function initMap() {
|
||||
map = L.map('map').setView([38.5, -98], 4);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors',
|
||||
maxZoom: 18
|
||||
}).addTo(map);
|
||||
markersLayer = L.layerGroup().addTo(map);
|
||||
}
|
||||
|
||||
function createIcon(color) {
|
||||
return L.divIcon({
|
||||
className: '',
|
||||
html: `<div style="width:28px;height:28px;background:${color};border:3px solid white;border-radius:50%;box-shadow:0 2px 6px rgba(0,0,0,.35);"></div>`,
|
||||
iconSize: [28, 28],
|
||||
iconAnchor: [14, 14],
|
||||
popupAnchor: [0, -16]
|
||||
});
|
||||
}
|
||||
|
||||
function renderMap() {
|
||||
markersLayer.clearLayers();
|
||||
const routes = (activeTab === 'all' || activeTab === 'route') ? filterRouteConditions() : [];
|
||||
const weather = (activeTab === 'all' || activeTab === 'weather') ? filterWeatherAlerts() : [];
|
||||
|
||||
const routeColors = { construction: '#f97316', closure: '#ef4444' };
|
||||
const weatherColors = { wind: '#3b82f6', winter: '#6366f1', fog: '#6b7280', thunderstorm: '#a855f7' };
|
||||
|
||||
routes.forEach(rc => {
|
||||
const color = routeColors[rc.type] || '#f97316';
|
||||
const sevInfo = routeSeverityBadge[rc.severity] || routeSeverityBadge.moderate;
|
||||
const marker = L.marker([rc.location.lat, rc.location.lng], { icon: createIcon(color) });
|
||||
marker.bindPopup(`
|
||||
<div style="max-width:280px;">
|
||||
<div style="font-weight:700;font-size:14px;margin-bottom:4px;">${rc.route}</div>
|
||||
<div style="font-size:12px;color:#64748b;margin-bottom:6px;">${rc.location.desc}, ${rc.location.state}</div>
|
||||
<div style="font-size:12px;margin-bottom:6px;">${rc.description}</div>
|
||||
<div style="font-size:11px;color:#94a3b8;">${formatDate(rc.startDate)} – ${formatDate(rc.endDate)}</div>
|
||||
${rc.affectsOversize ? '<div style="margin-top:6px;font-weight:700;color:#dc2626;font-size:12px;">⚠️ AFFECTS OVERSIZE</div>' : ''}
|
||||
</div>
|
||||
`);
|
||||
markersLayer.addLayer(marker);
|
||||
});
|
||||
|
||||
weather.forEach(wa => {
|
||||
const color = weatherColors[wa.type] || '#3b82f6';
|
||||
const marker = L.marker([wa.lat, wa.lng], { icon: createIcon(color) });
|
||||
marker.bindPopup(`
|
||||
<div style="max-width:280px;">
|
||||
<div style="font-weight:700;font-size:14px;margin-bottom:4px;">${wa.region}</div>
|
||||
<div style="font-size:12px;color:#64748b;margin-bottom:6px;">Routes: ${wa.routes.join(', ')}</div>
|
||||
<div style="font-size:12px;margin-bottom:6px;">${wa.description}</div>
|
||||
<div style="font-size:11px;color:#94a3b8;">Valid: ${formatDateTime(wa.validFrom)} – ${formatDateTime(wa.validTo)}</div>
|
||||
</div>
|
||||
`);
|
||||
markersLayer.addLayer(marker);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Route Condition Cards ──
|
||||
function renderRouteCards() {
|
||||
const section = document.getElementById('route-section');
|
||||
const container = document.getElementById('route-cards');
|
||||
const empty = document.getElementById('route-empty');
|
||||
|
||||
if (activeTab === 'weather') { section.classList.add('hidden'); return; }
|
||||
section.classList.remove('hidden');
|
||||
|
||||
const items = filterRouteConditions();
|
||||
if (items.length === 0) {
|
||||
container.innerHTML = '';
|
||||
empty.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
empty.classList.add('hidden');
|
||||
|
||||
container.innerHTML = items.map(rc => {
|
||||
const tb = typeBadge[rc.type] || typeBadge.construction;
|
||||
const sb = routeSeverityBadge[rc.severity] || routeSeverityBadge.moderate;
|
||||
return `
|
||||
<div class="bg-white rounded-2xl shadow-lg p-6 border-l-4 ${rc.type === 'closure' ? 'border-red-500' : 'border-orange-500'}">
|
||||
<div class="flex flex-wrap items-center gap-2 mb-3">
|
||||
<span class="px-2.5 py-1 rounded-lg text-xs font-bold ${tb.cls}">${tb.label}</span>
|
||||
<span class="px-2.5 py-1 rounded-lg text-xs font-bold ${sb.cls}">${sb.label}</span>
|
||||
${rc.affectsOversize ? '<span class="px-2.5 py-1 rounded-lg text-xs font-bold bg-red-600 text-white animate-pulse">⚠️ AFFECTS OVERSIZE</span>' : ''}
|
||||
</div>
|
||||
<h3 class="text-lg font-bold text-slate-900 mb-1">${rc.route}</h3>
|
||||
<p class="text-sm text-slate-500 mb-2">${rc.location.desc}, ${rc.location.state}</p>
|
||||
<p class="text-sm text-slate-700 mb-3">${rc.description}</p>
|
||||
<div class="flex flex-wrap items-center gap-4 text-xs text-slate-500">
|
||||
<span>📅 ${formatDate(rc.startDate)} – ${formatDate(rc.endDate)}</span>
|
||||
<span>📡 ${rc.source}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// ── Weather Alert Cards ──
|
||||
function renderWeatherCards() {
|
||||
const section = document.getElementById('weather-section');
|
||||
const container = document.getElementById('weather-cards');
|
||||
const empty = document.getElementById('weather-empty');
|
||||
|
||||
if (activeTab === 'route') { section.classList.add('hidden'); return; }
|
||||
section.classList.remove('hidden');
|
||||
|
||||
const items = filterWeatherAlerts();
|
||||
if (items.length === 0) {
|
||||
container.innerHTML = '';
|
||||
empty.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
empty.classList.add('hidden');
|
||||
|
||||
const borderColors = { wind: 'border-blue-500', winter: 'border-indigo-500', fog: 'border-gray-400', thunderstorm: 'border-purple-500' };
|
||||
|
||||
container.innerHTML = items.map(wa => {
|
||||
const tb = typeBadge[wa.type] || typeBadge.wind;
|
||||
const sb = weatherSeverityBadge[wa.severity] || weatherSeverityBadge.advisory;
|
||||
const border = borderColors[wa.type] || 'border-blue-500';
|
||||
return `
|
||||
<div class="bg-white rounded-2xl shadow-lg p-6 border-l-4 ${border}">
|
||||
<div class="flex flex-wrap items-center gap-2 mb-3">
|
||||
<span class="px-2.5 py-1 rounded-lg text-xs font-bold ${tb.cls}">${tb.label}</span>
|
||||
<span class="px-2.5 py-1 rounded-lg text-xs font-bold ${sb.cls}">${sb.label}</span>
|
||||
</div>
|
||||
<h3 class="text-lg font-bold text-slate-900 mb-1">${wa.region}</h3>
|
||||
<p class="text-sm text-slate-500 mb-2">Affected routes: ${wa.routes.join(', ')}</p>
|
||||
<p class="text-sm text-slate-700 mb-3">${wa.description}</p>
|
||||
<div class="flex flex-wrap items-center gap-4 text-xs text-slate-500">
|
||||
<span>🕐 ${formatDateTime(wa.validFrom)} – ${formatDateTime(wa.validTo)}</span>
|
||||
<span>📡 ${wa.source}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// ── Section visibility ──
|
||||
function renderSectionVisibility() {
|
||||
document.getElementById('route-section').classList.toggle('hidden', activeTab === 'weather');
|
||||
document.getElementById('weather-section').classList.toggle('hidden', activeTab === 'route');
|
||||
}
|
||||
|
||||
// ── Master render ──
|
||||
function renderAll() {
|
||||
renderTabButtons();
|
||||
renderMap();
|
||||
renderRouteCards();
|
||||
renderWeatherCards();
|
||||
}
|
||||
|
||||
// ── Init ──
|
||||
initMap();
|
||||
renderStats();
|
||||
renderAll();
|
||||
|
||||
// Wire up filter change events
|
||||
document.getElementById('filter-type').addEventListener('change', renderAll);
|
||||
document.getElementById('filter-severity').addEventListener('change', renderAll);
|
||||
document.getElementById('filter-oversize').addEventListener('change', renderAll);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
388
public/pages/bridges.html
Normal file
388
public/pages/bridges.html
Normal file
@@ -0,0 +1,388 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Bridge & Overpass Clearance Database | PilotEdge</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<style>
|
||||
#map { height: 450px; width: 100%; border-radius: 0.75rem; }
|
||||
.leaflet-popup-content { margin: 8px 12px; }
|
||||
.leaflet-popup-content-wrapper { border-radius: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-slate-50 min-h-screen flex flex-col">
|
||||
|
||||
<div id="main-nav"></div>
|
||||
<div id="poc-banner"></div>
|
||||
|
||||
<!-- Page Header -->
|
||||
<section class="bg-slate-900 text-white pt-24 pb-12 px-4">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<h1 class="text-3xl md:text-4xl font-bold mb-3">Bridge & 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 & 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 & Overpass Map
|
||||
</h2>
|
||||
<p class="text-sm text-slate-500 mb-4">Red markers indicate restricted clearances. Click a marker for details.</p>
|
||||
<div id="map"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Bridge List -->
|
||||
<section class="max-w-7xl mx-auto px-4 mt-8 pb-8 w-full">
|
||||
<div class="bg-white rounded-2xl shadow-lg p-6">
|
||||
<h2 class="text-lg font-bold text-slate-900 mb-1 flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"/></svg>
|
||||
Clearance Database
|
||||
</h2>
|
||||
<p class="text-sm text-slate-500 mb-4" id="results-count"></p>
|
||||
<div id="bridge-list" class="space-y-4"></div>
|
||||
<div id="no-results" class="hidden text-center py-12 text-slate-400">
|
||||
<svg class="w-12 h-12 mx-auto mb-3 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
<p class="font-medium">No bridges match your filters.</p>
|
||||
<p class="text-sm mt-1">Try adjusting your search criteria.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div id="main-footer"></div>
|
||||
|
||||
<script src="/js/api.js"></script>
|
||||
<script src="/js/nav.js"></script>
|
||||
<script>
|
||||
renderNav('bridges');
|
||||
renderBanner();
|
||||
renderFooter();
|
||||
|
||||
(async () => {
|
||||
const MOCK_BRIDGE_CLEARANCES = await PilotEdge.getBridges();
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
// Parse a clearance string like '13\'6"' or '14\'0"' into decimal feet.
|
||||
// Returns NaN for unparseable values (e.g. "Varies", "No restriction").
|
||||
function parseClearanceFeet(str) {
|
||||
if (!str) return NaN;
|
||||
// Handle range like "12'6\" — 13'6\" (varies)" — use the lower value
|
||||
const rangeParts = str.split('—').map(s => s.trim());
|
||||
const target = rangeParts[0];
|
||||
// Match pattern like 13'6" or 15'5"
|
||||
const match = target.match(/(\d+)['']\s*(\d+)?/);
|
||||
if (match) {
|
||||
const feet = parseInt(match[1], 10);
|
||||
const inches = match[2] ? parseInt(match[2], 10) : 0;
|
||||
return feet + inches / 12;
|
||||
}
|
||||
// Try plain number
|
||||
const plain = parseFloat(target);
|
||||
return isNaN(plain) ? NaN : plain;
|
||||
}
|
||||
|
||||
function parseWidthFeet(str) {
|
||||
if (!str || str.toLowerCase().includes('no restriction')) return NaN;
|
||||
const match = str.match(/(\d+)['']\s*(\d+)?/);
|
||||
if (match) {
|
||||
const feet = parseInt(match[1], 10);
|
||||
const inches = match[2] ? parseInt(match[2], 10) : 0;
|
||||
return feet + inches / 12;
|
||||
}
|
||||
return NaN;
|
||||
}
|
||||
|
||||
function heightBadgeClass(heightFt) {
|
||||
if (isNaN(heightFt)) return 'bg-slate-100 text-slate-600';
|
||||
if (heightFt < 14) return 'bg-red-100 text-red-700 ring-1 ring-red-300';
|
||||
if (heightFt <= 15) return 'bg-amber-100 text-amber-700 ring-1 ring-amber-300';
|
||||
return 'bg-green-100 text-green-700 ring-1 ring-green-300';
|
||||
}
|
||||
|
||||
function heightIcon(heightFt) {
|
||||
if (isNaN(heightFt)) return '❓';
|
||||
if (heightFt < 14) return '🔴';
|
||||
if (heightFt <= 15) return '🟡';
|
||||
return '🟢';
|
||||
}
|
||||
|
||||
// --- State filter population ---
|
||||
const states = [...new Set(MOCK_BRIDGE_CLEARANCES.map(b => b.location.state))].sort();
|
||||
const stateSelect = document.getElementById('filter-state');
|
||||
states.forEach(s => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = s;
|
||||
opt.textContent = s;
|
||||
stateSelect.appendChild(opt);
|
||||
});
|
||||
|
||||
// --- Map setup ---
|
||||
const map = L.map('map').setView([39.5, -98.5], 4);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors',
|
||||
maxZoom: 18
|
||||
}).addTo(map);
|
||||
|
||||
const redIcon = L.divIcon({
|
||||
className: '',
|
||||
html: `<svg width="28" height="28" viewBox="0 0 28 28"><polygon points="14,2 26,24 2,24" fill="#dc2626" stroke="#991b1b" stroke-width="1.5"/><text x="14" y="19" text-anchor="middle" fill="white" font-size="11" font-weight="bold">!</text></svg>`,
|
||||
iconSize: [28, 28],
|
||||
iconAnchor: [14, 24],
|
||||
popupAnchor: [0, -22]
|
||||
});
|
||||
|
||||
let markers = [];
|
||||
|
||||
function addMarkers(bridges) {
|
||||
markers.forEach(m => map.removeLayer(m));
|
||||
markers = [];
|
||||
bridges.forEach(b => {
|
||||
const heightFt = parseClearanceFeet(b.clearanceHeight);
|
||||
const marker = L.marker([b.location.lat, b.location.lng], { icon: redIcon }).addTo(map);
|
||||
marker.bindPopup(`
|
||||
<div style="min-width:220px;">
|
||||
<div style="font-weight:700;font-size:14px;margin-bottom:4px;">${b.route} — MM ${b.mileMarker}</div>
|
||||
<div style="font-size:12px;color:#64748b;margin-bottom:6px;">${b.location.desc}, ${b.location.city}, ${b.location.state}</div>
|
||||
<div style="display:grid;grid-template-columns:auto 1fr;gap:2px 8px;font-size:12px;">
|
||||
<span style="font-weight:600;">Height:</span><span>${b.clearanceHeight} ${heightIcon(heightFt)}</span>
|
||||
<span style="font-weight:600;">Width:</span><span>${b.clearanceWidth}</span>
|
||||
<span style="font-weight:600;">Weight:</span><span>${b.weightLimit}</span>
|
||||
<span style="font-weight:600;">Type:</span><span>${b.type}</span>
|
||||
</div>
|
||||
${b.notes ? `<div style="font-size:11px;color:#64748b;margin-top:6px;border-top:1px solid #e2e8f0;padding-top:6px;">${b.notes}</div>` : ''}
|
||||
</div>
|
||||
`);
|
||||
markers.push(marker);
|
||||
});
|
||||
}
|
||||
|
||||
// --- Render bridge cards ---
|
||||
function renderBridgeList(bridges) {
|
||||
const container = document.getElementById('bridge-list');
|
||||
const noResults = document.getElementById('no-results');
|
||||
const resultsCount = document.getElementById('results-count');
|
||||
|
||||
if (bridges.length === 0) {
|
||||
container.innerHTML = '';
|
||||
noResults.classList.remove('hidden');
|
||||
resultsCount.textContent = '0 bridges found';
|
||||
return;
|
||||
}
|
||||
|
||||
noResults.classList.add('hidden');
|
||||
resultsCount.textContent = `${bridges.length} bridge${bridges.length !== 1 ? 's' : ''} found`;
|
||||
|
||||
container.innerHTML = bridges.map(b => {
|
||||
const heightFt = parseClearanceFeet(b.clearanceHeight);
|
||||
const badgeCls = heightBadgeClass(heightFt);
|
||||
const isWarning = b._warning;
|
||||
|
||||
return `
|
||||
<div class="border ${isWarning ? 'border-red-300 bg-red-50/40' : 'border-slate-200'} rounded-xl p-5 hover:shadow-md transition-shadow" id="card-${b.id}">
|
||||
<div class="flex flex-col md:flex-row md:items-start md:justify-between gap-4">
|
||||
<!-- Left -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-3 flex-wrap">
|
||||
<h3 class="font-bold text-slate-900 text-base">${b.route}</h3>
|
||||
<span class="text-xs px-2 py-0.5 rounded-full bg-slate-100 text-slate-600 font-medium">MM ${b.mileMarker}</span>
|
||||
<span class="text-xs px-2 py-0.5 rounded-full bg-slate-100 text-slate-600 font-medium">${b.type}</span>
|
||||
${isWarning ? '<span class="text-xs px-2 py-0.5 rounded-full bg-red-100 text-red-700 font-semibold">⚠️ Clearance Issue</span>' : ''}
|
||||
</div>
|
||||
<p class="text-sm text-slate-500 mt-1">${b.location.desc} — ${b.location.city}, ${b.location.state}</p>
|
||||
${b.notes ? `<p class="text-xs text-slate-400 mt-2 leading-relaxed">${b.notes}</p>` : ''}
|
||||
</div>
|
||||
<!-- Right: Badges -->
|
||||
<div class="flex flex-wrap md:flex-col gap-2 md:items-end flex-shrink-0">
|
||||
<div class="text-center">
|
||||
<div class="text-[10px] uppercase tracking-wider font-semibold text-slate-400 mb-0.5">Height</div>
|
||||
<span class="inline-block text-sm font-bold px-3 py-1 rounded-lg ${badgeCls}">${b.clearanceHeight}</span>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-[10px] uppercase tracking-wider font-semibold text-slate-400 mb-0.5">Width</div>
|
||||
<span class="inline-block text-xs font-medium px-3 py-1 rounded-lg bg-slate-100 text-slate-600">${b.clearanceWidth}</span>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-[10px] uppercase tracking-wider font-semibold text-slate-400 mb-0.5">Weight</div>
|
||||
<span class="inline-block text-xs font-medium px-3 py-1 rounded-lg bg-slate-100 text-slate-600">${b.weightLimit}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// --- Filter logic ---
|
||||
function getFilteredBridges() {
|
||||
const stateVal = document.getElementById('filter-state').value;
|
||||
const routeVal = document.getElementById('filter-route').value.trim().toLowerCase();
|
||||
const maxHeightVal = parseFloat(document.getElementById('filter-max-height').value);
|
||||
|
||||
return MOCK_BRIDGE_CLEARANCES.filter(b => {
|
||||
if (stateVal && b.location.state !== stateVal) return false;
|
||||
if (routeVal && !b.route.toLowerCase().includes(routeVal)) return false;
|
||||
if (!isNaN(maxHeightVal)) {
|
||||
const h = parseClearanceFeet(b.clearanceHeight);
|
||||
if (!isNaN(h) && h > maxHeightVal) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function applyFilters() {
|
||||
const bridges = getFilteredBridges();
|
||||
addMarkers(bridges);
|
||||
renderBridgeList(bridges);
|
||||
}
|
||||
|
||||
document.getElementById('filter-state').addEventListener('change', applyFilters);
|
||||
document.getElementById('filter-route').addEventListener('input', applyFilters);
|
||||
document.getElementById('filter-max-height').addEventListener('input', applyFilters);
|
||||
|
||||
document.getElementById('clear-filters-btn').addEventListener('click', () => {
|
||||
document.getElementById('filter-state').value = '';
|
||||
document.getElementById('filter-route').value = '';
|
||||
document.getElementById('filter-max-height').value = '';
|
||||
document.getElementById('load-height').value = '';
|
||||
document.getElementById('load-width').value = '';
|
||||
document.getElementById('load-warnings').classList.add('hidden');
|
||||
MOCK_BRIDGE_CLEARANCES.forEach(b => delete b._warning);
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
// --- Check Your Load ---
|
||||
document.getElementById('check-load-btn').addEventListener('click', () => {
|
||||
const loadHeight = parseFloat(document.getElementById('load-height').value);
|
||||
const loadWidth = parseFloat(document.getElementById('load-width').value);
|
||||
const warningsDiv = document.getElementById('load-warnings');
|
||||
|
||||
// Reset warnings
|
||||
MOCK_BRIDGE_CLEARANCES.forEach(b => delete b._warning);
|
||||
|
||||
if (isNaN(loadHeight) && isNaN(loadWidth)) {
|
||||
warningsDiv.innerHTML = '<p class="text-sm text-slate-500">Please enter at least one dimension to check.</p>';
|
||||
warningsDiv.classList.remove('hidden');
|
||||
applyFilters();
|
||||
return;
|
||||
}
|
||||
|
||||
const problems = [];
|
||||
MOCK_BRIDGE_CLEARANCES.forEach(b => {
|
||||
const reasons = [];
|
||||
if (!isNaN(loadHeight)) {
|
||||
const bh = parseClearanceFeet(b.clearanceHeight);
|
||||
if (!isNaN(bh) && bh <= loadHeight) {
|
||||
reasons.push(`Height clearance ${b.clearanceHeight} is at or below your load height of ${loadHeight}'`);
|
||||
}
|
||||
}
|
||||
if (!isNaN(loadWidth)) {
|
||||
const bw = parseWidthFeet(b.clearanceWidth);
|
||||
if (!isNaN(bw) && bw <= loadWidth) {
|
||||
reasons.push(`Width clearance ${b.clearanceWidth} is at or below your load width of ${loadWidth}'`);
|
||||
}
|
||||
}
|
||||
if (reasons.length > 0) {
|
||||
b._warning = true;
|
||||
problems.push({ bridge: b, reasons });
|
||||
}
|
||||
});
|
||||
|
||||
if (problems.length === 0) {
|
||||
warningsDiv.innerHTML = `
|
||||
<div class="bg-green-50 border border-green-200 rounded-xl p-4">
|
||||
<p class="text-green-800 font-semibold flex items-center gap-2">
|
||||
<span class="text-lg">✅</span> No clearance issues found for your load dimensions.
|
||||
</p>
|
||||
<p class="text-green-600 text-sm mt-1">All bridges in the database can accommodate your ${!isNaN(loadHeight) ? loadHeight + "' height" : ''}${!isNaN(loadHeight) && !isNaN(loadWidth) ? ' and ' : ''}${!isNaN(loadWidth) ? loadWidth + "' width" : ''}.</p>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
warningsDiv.innerHTML = `
|
||||
<div class="bg-red-50 border border-red-200 rounded-xl p-4">
|
||||
<p class="text-red-800 font-semibold flex items-center gap-2">
|
||||
<span class="text-lg">🚨</span> ${problems.length} bridge${problems.length !== 1 ? 's' : ''} may restrict your route
|
||||
</p>
|
||||
<ul class="mt-3 space-y-2">
|
||||
${problems.map(p => `
|
||||
<li class="text-sm bg-white border border-red-100 rounded-lg p-3">
|
||||
<div class="font-semibold text-red-700">${p.bridge.route} — MM ${p.bridge.mileMarker} (${p.bridge.location.city}, ${p.bridge.location.state})</div>
|
||||
${p.reasons.map(r => `<div class="text-red-600 mt-0.5">• ${r}</div>`).join('')}
|
||||
</li>
|
||||
`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
warningsDiv.classList.remove('hidden');
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
// --- Initial render ---
|
||||
applyFilters();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
294
public/pages/calendar.html
Normal file
294
public/pages/calendar.html
Normal file
@@ -0,0 +1,294 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Seasonal Restriction Calendar | PilotEdge</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
.gantt-bar { transition: opacity 0.2s; }
|
||||
.gantt-bar:hover { opacity: 0.85; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-slate-50 min-h-screen flex flex-col">
|
||||
|
||||
<div id="main-nav"></div>
|
||||
<div id="poc-banner"></div>
|
||||
|
||||
<!-- Page Header -->
|
||||
<section class="bg-slate-900 text-white pt-24 pb-12 px-4">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<h1 class="text-3xl md:text-4xl font-bold mb-3">Seasonal Restriction Calendar</h1>
|
||||
<p class="text-lg text-gray-400 max-w-3xl">Plan around seasonal closures, weight restrictions, and travel blackouts that affect oversize load movement across the country.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Filters -->
|
||||
<section class="max-w-7xl mx-auto px-4 pt-8 w-full">
|
||||
<div class="bg-white rounded-2xl shadow-lg p-6">
|
||||
<div class="flex flex-wrap items-end gap-4">
|
||||
<div class="flex-1 min-w-[180px]">
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Restriction Type</label>
|
||||
<select id="filter-type" onchange="applyFilters()" class="w-full border border-slate-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
<option value="">All Types</option>
|
||||
<option value="spring_weight">🔵 Spring Weight</option>
|
||||
<option value="winter_closure">🟣 Winter Closure</option>
|
||||
<option value="wind_season">🟡 Wind Season</option>
|
||||
<option value="holiday_blackout">🔴 Holiday Blackout</option>
|
||||
<option value="harvest_season">🟢 Harvest Season</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-1 min-w-[180px]">
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">State</label>
|
||||
<select id="filter-state" onchange="applyFilters()" class="w-full border border-slate-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
<option value="">All States</option>
|
||||
</select>
|
||||
</div>
|
||||
<button onclick="resetFilters()" class="bg-slate-100 hover:bg-slate-200 text-slate-700 font-semibold px-4 py-2 rounded-lg transition-colors text-sm">
|
||||
Reset Filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Calendar Gantt Chart -->
|
||||
<section class="max-w-7xl mx-auto px-4 py-8 w-full">
|
||||
<div class="bg-white rounded-2xl shadow-lg p-6 overflow-x-auto">
|
||||
<h2 class="text-xl font-bold text-slate-900 mb-1">12-Month Timeline</h2>
|
||||
<p class="text-sm text-slate-500 mb-6">Colored bars show when each restriction is active. Hover for details.</p>
|
||||
<div id="gantt-chart"></div>
|
||||
<div id="gantt-empty" class="hidden text-center py-12 text-slate-400">
|
||||
<p class="text-lg font-medium">No restrictions match the current filters.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Legend -->
|
||||
<section class="max-w-7xl mx-auto px-4 pb-4 w-full">
|
||||
<div class="flex flex-wrap gap-4 justify-center text-sm">
|
||||
<span class="flex items-center gap-1.5"><span class="w-4 h-3 rounded" style="background:#3b82f6"></span> Spring Weight</span>
|
||||
<span class="flex items-center gap-1.5"><span class="w-4 h-3 rounded" style="background:#6366f1"></span> Winter Closure</span>
|
||||
<span class="flex items-center gap-1.5"><span class="w-4 h-3 rounded" style="background:#f59e0b"></span> Wind Season</span>
|
||||
<span class="flex items-center gap-1.5"><span class="w-4 h-3 rounded" style="background:#ef4444"></span> Holiday Blackout</span>
|
||||
<span class="flex items-center gap-1.5"><span class="w-4 h-3 rounded" style="background:#22c55e"></span> Harvest Season</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Restriction Detail Cards -->
|
||||
<section class="max-w-7xl mx-auto px-4 pb-8 w-full">
|
||||
<h2 class="text-xl font-bold text-slate-900 mb-4">Restriction Details</h2>
|
||||
<div id="restriction-cards" class="grid md:grid-cols-2 gap-6"></div>
|
||||
<div id="cards-empty" class="hidden text-center py-12 text-slate-400">
|
||||
<p class="text-lg font-medium">No restrictions match the current filters.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div id="main-footer"></div>
|
||||
|
||||
<script src="/js/api.js"></script>
|
||||
<script src="/js/nav.js"></script>
|
||||
<script>
|
||||
renderNav('calendar');
|
||||
renderBanner();
|
||||
renderFooter();
|
||||
|
||||
(async () => {
|
||||
const MOCK_SEASONAL_RESTRICTIONS = await PilotEdge.getSeasonalRestrictions();
|
||||
|
||||
// ---- Constants ----
|
||||
const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
||||
|
||||
const TYPE_BADGES = {
|
||||
spring_weight: { label:'Spring Weight', bg:'bg-blue-100', text:'text-blue-800' },
|
||||
winter_closure: { label:'Winter Closure', bg:'bg-indigo-100', text:'text-indigo-800' },
|
||||
wind_season: { label:'Wind Season', bg:'bg-amber-100', text:'text-amber-800' },
|
||||
holiday_blackout:{ label:'Holiday Blackout', bg:'bg-red-100', text:'text-red-800' },
|
||||
harvest_season: { label:'Harvest Season', bg:'bg-green-100', text:'text-green-800' }
|
||||
};
|
||||
|
||||
// ---- Populate state filter ----
|
||||
const states = [...new Set(MOCK_SEASONAL_RESTRICTIONS.map(r => r.stateName))].sort();
|
||||
const stateSelect = document.getElementById('filter-state');
|
||||
states.forEach(s => {
|
||||
const r = MOCK_SEASONAL_RESTRICTIONS.find(x => x.stateName === s);
|
||||
stateSelect.add(new Option(`${s} (${r.state})`, r.state));
|
||||
});
|
||||
|
||||
// ---- Filtering ----
|
||||
function getFiltered() {
|
||||
const type = document.getElementById('filter-type').value;
|
||||
const state = document.getElementById('filter-state').value;
|
||||
return MOCK_SEASONAL_RESTRICTIONS.filter(r =>
|
||||
(!type || r.type === type) && (!state || r.state === state)
|
||||
);
|
||||
}
|
||||
|
||||
function applyFilters() {
|
||||
renderGantt(getFiltered());
|
||||
renderCards(getFiltered());
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
document.getElementById('filter-type').value = '';
|
||||
document.getElementById('filter-state').value = '';
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
// ---- Month span helper (handles wrap-around) ----
|
||||
function getActiveMonths(r) {
|
||||
const months = [];
|
||||
if (r.startMonth <= r.endMonth) {
|
||||
for (let m = r.startMonth; m <= r.endMonth; m++) months.push(m);
|
||||
} else {
|
||||
for (let m = r.startMonth; m <= 12; m++) months.push(m);
|
||||
for (let m = 1; m <= r.endMonth; m++) months.push(m);
|
||||
}
|
||||
return months;
|
||||
}
|
||||
|
||||
// ---- Gantt Chart ----
|
||||
function renderGantt(restrictions) {
|
||||
const chart = document.getElementById('gantt-chart');
|
||||
const empty = document.getElementById('gantt-empty');
|
||||
|
||||
if (restrictions.length === 0) {
|
||||
chart.classList.add('hidden');
|
||||
empty.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
chart.classList.remove('hidden');
|
||||
empty.classList.add('hidden');
|
||||
|
||||
// Build the grid: header row + one row per restriction
|
||||
let html = '<div style="min-width:800px;">';
|
||||
|
||||
// Month header row
|
||||
html += '<div class="grid" style="grid-template-columns: 220px repeat(12, 1fr); gap: 0;">';
|
||||
html += '<div class="text-xs font-semibold text-slate-500 py-2 pr-3 text-right">Restriction</div>';
|
||||
MONTHS.forEach((m, i) => {
|
||||
const now = new Date();
|
||||
const isCurrentMonth = (now.getMonth() === i);
|
||||
html += `<div class="text-xs font-semibold text-center py-2 ${isCurrentMonth ? 'text-amber-600 bg-amber-50 rounded-t' : 'text-slate-500'}">${m}</div>`;
|
||||
});
|
||||
html += '</div>';
|
||||
|
||||
// Restriction rows
|
||||
restrictions.forEach(r => {
|
||||
const active = getActiveMonths(r);
|
||||
const badge = TYPE_BADGES[r.type] || { label: r.type, bg:'bg-slate-100', text:'text-slate-700' };
|
||||
|
||||
html += '<div class="grid border-t border-slate-100" style="grid-template-columns: 220px repeat(12, 1fr); gap: 0;">';
|
||||
|
||||
// Label cell
|
||||
html += `<div class="flex items-center py-2 pr-3 gap-2 justify-end">
|
||||
<span class="text-xs font-medium text-slate-700 text-right truncate" title="${r.title} — ${r.stateName}">${r.stateName}</span>
|
||||
<span class="flex-shrink-0 text-[10px] font-bold px-1.5 py-0.5 rounded ${badge.bg} ${badge.text}">${r.state}</span>
|
||||
</div>`;
|
||||
|
||||
// Month cells
|
||||
for (let m = 1; m <= 12; m++) {
|
||||
const isActive = active.includes(m);
|
||||
const now = new Date();
|
||||
const isCurrentMonth = (now.getMonth() + 1 === m);
|
||||
|
||||
if (isActive) {
|
||||
const isStart = m === r.startMonth;
|
||||
const isEnd = m === r.endMonth;
|
||||
const roundL = isStart ? 'rounded-l-full' : '';
|
||||
const roundR = isEnd ? 'rounded-r-full' : '';
|
||||
|
||||
html += `<div class="flex items-center py-2 px-0.5 ${isCurrentMonth ? 'bg-amber-50' : ''}">
|
||||
<div class="gantt-bar w-full h-6 ${roundL} ${roundR} flex items-center justify-center cursor-default"
|
||||
style="background:${r.color}; opacity:0.8;"
|
||||
title="${r.title}\n${MONTHS[r.startMonth-1]} ${r.startDay} – ${MONTHS[r.endMonth-1]} ${r.endDay}">
|
||||
${isStart ? `<span class="text-[9px] text-white font-bold drop-shadow pl-1">${r.startDay}</span>` : ''}
|
||||
${isEnd ? `<span class="text-[9px] text-white font-bold drop-shadow pr-1">${r.endDay}</span>` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
} else {
|
||||
html += `<div class="flex items-center py-2 px-0.5 ${isCurrentMonth ? 'bg-amber-50' : ''}">
|
||||
<div class="w-full h-6 bg-slate-50 ${m === 1 ? 'rounded-l' : ''} ${m === 12 ? 'rounded-r' : ''}"></div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
chart.innerHTML = html;
|
||||
}
|
||||
|
||||
// ---- Detail Cards ----
|
||||
function renderCards(restrictions) {
|
||||
const container = document.getElementById('restriction-cards');
|
||||
const empty = document.getElementById('cards-empty');
|
||||
|
||||
if (restrictions.length === 0) {
|
||||
container.classList.add('hidden');
|
||||
empty.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
container.classList.remove('hidden');
|
||||
empty.classList.add('hidden');
|
||||
|
||||
container.innerHTML = restrictions.map(r => {
|
||||
const badge = TYPE_BADGES[r.type] || { label: r.type, bg:'bg-slate-100', text:'text-slate-700' };
|
||||
const active = getActiveMonths(r);
|
||||
const monthRange = `${MONTHS[r.startMonth-1]} ${r.startDay} – ${MONTHS[r.endMonth-1]} ${r.endDay}`;
|
||||
|
||||
// Month pills
|
||||
const monthPills = MONTHS.map((m, i) => {
|
||||
const isActive = active.includes(i + 1);
|
||||
return `<span class="inline-block w-7 text-center text-[10px] font-semibold rounded py-0.5 ${isActive ? 'text-white' : 'text-slate-400 bg-slate-100'}" ${isActive ? `style="background:${r.color}"` : ''}>${m.charAt(0)}${m.charAt(1)}</span>`;
|
||||
}).join('');
|
||||
|
||||
return `
|
||||
<div class="bg-white rounded-2xl shadow-lg border border-slate-100 overflow-hidden">
|
||||
<div class="px-6 pt-5 pb-4">
|
||||
<div class="flex items-start justify-between gap-3 mb-3">
|
||||
<div>
|
||||
<h3 class="font-bold text-slate-900 text-lg leading-tight">${r.title}</h3>
|
||||
<p class="text-sm text-slate-500 mt-0.5">${r.stateName} (${r.state})</p>
|
||||
</div>
|
||||
<span class="flex-shrink-0 text-xs font-bold px-2.5 py-1 rounded-full ${badge.bg} ${badge.text}">${badge.label}</span>
|
||||
</div>
|
||||
|
||||
<!-- Active period -->
|
||||
<div class="mb-4">
|
||||
<div class="flex items-center gap-2 text-sm text-slate-700 font-medium mb-2">
|
||||
<svg class="w-4 h-4 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
|
||||
${monthRange}
|
||||
</div>
|
||||
<div class="flex gap-1 flex-wrap">${monthPills}</div>
|
||||
</div>
|
||||
|
||||
<!-- Routes -->
|
||||
<div class="mb-3">
|
||||
<p class="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-1">Affected Routes</p>
|
||||
<p class="text-sm text-slate-700">${r.routes}</p>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="mb-3">
|
||||
<p class="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-1">Description</p>
|
||||
<p class="text-sm text-slate-600">${r.description}</p>
|
||||
</div>
|
||||
|
||||
<!-- Impact -->
|
||||
<div class="bg-amber-50 border border-amber-200 rounded-xl px-4 py-3">
|
||||
<p class="text-xs font-semibold text-amber-700 uppercase tracking-wider mb-1">⚠️ Impact on Oversize Loads</p>
|
||||
<p class="text-sm text-amber-900">${r.impact}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// ---- Initial render ----
|
||||
applyFilters();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
111
public/pages/contacts.html
Normal file
111
public/pages/contacts.html
Normal file
@@ -0,0 +1,111 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>State DOT Contacts | PilotEdge</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body class="bg-slate-50 min-h-screen flex flex-col">
|
||||
|
||||
<div id="main-nav"></div>
|
||||
<div id="poc-banner"></div>
|
||||
|
||||
<!-- Page Header -->
|
||||
<section class="bg-slate-900 text-white pt-24 pb-12 px-4">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<h1 class="text-3xl md:text-4xl font-bold mb-3">State DOT Contact Directory</h1>
|
||||
<p class="text-lg text-gray-400 max-w-3xl">Find permit office phone numbers, state police non-emergency lines, and online portal links for every state.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Search / Filter Bar -->
|
||||
<section class="max-w-7xl mx-auto px-4 py-8 w-full">
|
||||
<div class="bg-white rounded-2xl shadow-lg p-6">
|
||||
<label for="state-search" class="block text-sm font-semibold text-slate-700 mb-2">Search States</label>
|
||||
<input
|
||||
type="text"
|
||||
id="state-search"
|
||||
placeholder="Type a state name to filter…"
|
||||
class="w-full border border-slate-300 rounded-lg px-4 py-3 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none text-slate-900"
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Contact Cards Grid -->
|
||||
<section class="max-w-7xl mx-auto px-4 pb-12 w-full">
|
||||
<div id="contacts-grid" class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<!-- Populated by JS -->
|
||||
</div>
|
||||
<p id="no-results" class="hidden text-center text-slate-500 py-12 text-lg">No states match your search.</p>
|
||||
</section>
|
||||
|
||||
<div id="main-footer"></div>
|
||||
|
||||
<script src="/js/api.js"></script>
|
||||
<script src="/js/nav.js"></script>
|
||||
<script>
|
||||
renderNav('contacts');
|
||||
renderBanner();
|
||||
renderFooter();
|
||||
|
||||
(async () => {
|
||||
const MOCK_STATE_CONTACTS = await PilotEdge.getContacts();
|
||||
|
||||
const grid = document.getElementById('contacts-grid');
|
||||
const noResults = document.getElementById('no-results');
|
||||
const searchInput = document.getElementById('state-search');
|
||||
|
||||
function buildCard(abbr, c) {
|
||||
return `
|
||||
<div class="bg-white rounded-2xl shadow-lg p-6 flex flex-col" data-state="${c.name.toLowerCase()}">
|
||||
<h3 class="text-lg font-bold text-slate-900 mb-1">${c.name} <span class="text-slate-400 font-medium text-sm">(${abbr})</span></h3>
|
||||
<div class="mt-3 space-y-2 text-sm text-slate-700 flex-1">
|
||||
<p class="flex items-start gap-2">
|
||||
<span class="shrink-0">📞</span>
|
||||
<span><span class="font-medium">Permit Office:</span> <a href="tel:${c.permit.replace(/[^+\d]/g, '')}" class="text-amber-600 hover:text-amber-700 font-semibold">${c.permit}</a></span>
|
||||
</p>
|
||||
<p class="flex items-start gap-2">
|
||||
<span class="shrink-0">🚔</span>
|
||||
<span><span class="font-medium">State Police:</span> <a href="tel:${c.police.replace(/[^+\d]/g, '')}" class="text-amber-600 hover:text-amber-700 font-semibold">${c.police}</a></span>
|
||||
</p>
|
||||
<p class="flex items-start gap-2">
|
||||
<span class="shrink-0">✉️</span>
|
||||
<span><span class="font-medium">Email:</span> <a href="mailto:${c.email}" class="text-amber-600 hover:text-amber-700 font-semibold break-all">${c.email}</a></span>
|
||||
</p>
|
||||
<p class="flex items-start gap-2">
|
||||
<span class="shrink-0">🕐</span>
|
||||
<span><span class="font-medium">Hours:</span> ${c.hours}</span>
|
||||
</p>
|
||||
</div>
|
||||
<a href="${c.portal}" target="_blank" rel="noopener noreferrer"
|
||||
class="mt-4 inline-flex items-center justify-center gap-1 bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold text-sm px-4 py-2 rounded-lg transition-colors shadow-md">
|
||||
Visit Permit Portal <span class="text-xs">↗</span>
|
||||
</a>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderCards(filter) {
|
||||
const term = (filter || '').toLowerCase();
|
||||
let html = '';
|
||||
let count = 0;
|
||||
|
||||
Object.keys(MOCK_STATE_CONTACTS).forEach(abbr => {
|
||||
const c = MOCK_STATE_CONTACTS[abbr];
|
||||
if (!term || c.name.toLowerCase().includes(term) || abbr.toLowerCase().includes(term)) {
|
||||
html += buildCard(abbr, c);
|
||||
count++;
|
||||
}
|
||||
});
|
||||
|
||||
grid.innerHTML = html;
|
||||
noResults.classList.toggle('hidden', count > 0);
|
||||
}
|
||||
|
||||
searchInput.addEventListener('input', () => renderCards(searchInput.value));
|
||||
|
||||
renderCards();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
262
public/pages/documents.html
Normal file
262
public/pages/documents.html
Normal file
@@ -0,0 +1,262 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document Vault | PilotEdge</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body class="bg-slate-50 min-h-screen flex flex-col">
|
||||
|
||||
<div id="main-nav"></div>
|
||||
<div id="poc-banner"></div>
|
||||
|
||||
<!-- Page Header -->
|
||||
<section class="bg-slate-900 text-white pt-24 pb-12 px-4">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<h1 class="text-3xl md:text-4xl font-bold mb-3">Document Vault</h1>
|
||||
<p class="text-lg text-gray-400 max-w-3xl">Securely store and manage your permits, insurance certificates, and professional certifications — all in one place.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="flex-1 max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-10">
|
||||
|
||||
<!-- Stats Bar -->
|
||||
<div id="stats-bar" class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8"></div>
|
||||
|
||||
<!-- Expiry Warnings -->
|
||||
<div id="expiry-warnings" class="mb-8"></div>
|
||||
|
||||
<!-- Filter Bar -->
|
||||
<div class="bg-white rounded-2xl shadow-lg p-4 sm:p-6 mb-8">
|
||||
<div class="flex flex-col sm:flex-row gap-4">
|
||||
<div class="flex-1">
|
||||
<input id="search-input" type="text" placeholder="Search documents…"
|
||||
class="w-full border border-slate-300 rounded-lg px-4 py-2.5 text-sm focus:ring-2 focus:ring-amber-500 focus:border-amber-500 outline-none transition-colors">
|
||||
</div>
|
||||
<select id="type-filter"
|
||||
class="border border-slate-300 rounded-lg px-4 py-2.5 text-sm focus:ring-2 focus:ring-amber-500 focus:border-amber-500 outline-none transition-colors">
|
||||
<option value="all">All Types</option>
|
||||
<option value="permit">Permit</option>
|
||||
<option value="insurance">Insurance</option>
|
||||
<option value="certification">Certification</option>
|
||||
<option value="registration">Registration</option>
|
||||
</select>
|
||||
<select id="status-filter"
|
||||
class="border border-slate-300 rounded-lg px-4 py-2.5 text-sm focus:ring-2 focus:ring-amber-500 focus:border-amber-500 outline-none transition-colors">
|
||||
<option value="all">All Statuses</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="expired">Expired</option>
|
||||
</select>
|
||||
<button onclick="openUploadModal()"
|
||||
class="bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold px-5 py-2.5 rounded-lg transition-colors shadow-md hover:shadow-lg text-sm whitespace-nowrap">
|
||||
+ Upload Document
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Document List -->
|
||||
<div id="document-list" class="grid gap-4"></div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div id="empty-state" class="hidden text-center py-16">
|
||||
<p class="text-5xl mb-4">📄</p>
|
||||
<p class="text-slate-500 text-lg">No documents match your filters.</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Upload Modal -->
|
||||
<div id="upload-modal" class="fixed inset-0 z-50 hidden items-center justify-center bg-black/50 p-4">
|
||||
<div class="bg-white rounded-2xl shadow-2xl max-w-md w-full p-6 relative">
|
||||
<button onclick="closeUploadModal()" class="absolute top-4 right-4 text-slate-400 hover:text-slate-600 text-xl leading-none">×</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 & drop or click to browse<br>
|
||||
<span class="text-xs">(disabled in POC)</span>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="alert('POC demo — upload not functional.'); closeUploadModal();"
|
||||
class="w-full bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold py-2.5 rounded-lg transition-colors shadow-md text-sm">
|
||||
Upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="main-footer"></div>
|
||||
|
||||
<script src="/js/api.js"></script>
|
||||
<script src="/js/nav.js"></script>
|
||||
<script>
|
||||
renderNav('documents');
|
||||
renderBanner();
|
||||
renderFooter();
|
||||
|
||||
(async () => {
|
||||
const MOCK_DOCUMENTS = await PilotEdge.getDocuments();
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────
|
||||
const today = new Date();
|
||||
const MS_PER_DAY = 86400000;
|
||||
|
||||
function daysUntil(dateStr) {
|
||||
return Math.ceil((new Date(dateStr) - today) / MS_PER_DAY);
|
||||
}
|
||||
|
||||
function fmtDate(dateStr) {
|
||||
return new Date(dateStr).toLocaleDateString('en-US', { year:'numeric', month:'short', day:'numeric' });
|
||||
}
|
||||
|
||||
const typeBadge = {
|
||||
permit: 'bg-blue-100 text-blue-700',
|
||||
insurance: 'bg-green-100 text-green-700',
|
||||
certification: 'bg-purple-100 text-purple-700',
|
||||
registration: 'bg-slate-200 text-slate-700'
|
||||
};
|
||||
|
||||
function capitalize(s) { return s.charAt(0).toUpperCase() + s.slice(1); }
|
||||
|
||||
// ── Stats ───────────────────────────────────────────
|
||||
function renderStats(docs) {
|
||||
const total = docs.length;
|
||||
const active = docs.filter(d => d.status === 'active').length;
|
||||
const expired = docs.filter(d => d.status === 'expired').length;
|
||||
const expiring = docs.filter(d => d.status === 'active' && daysUntil(d.expiryDate) <= 30 && daysUntil(d.expiryDate) > 0).length;
|
||||
|
||||
const items = [
|
||||
{ label:'Total Documents', value:total, color:'bg-slate-900 text-white' },
|
||||
{ label:'Active Permits', value:active, color:'bg-green-600 text-white' },
|
||||
{ label:'Expiring Soon', value:expiring, color:'bg-amber-500 text-white' },
|
||||
{ label:'Expired', value:expired, color:'bg-red-600 text-white' }
|
||||
];
|
||||
|
||||
document.getElementById('stats-bar').innerHTML = items.map(s => `
|
||||
<div class="${s.color} rounded-2xl shadow-lg p-5 text-center">
|
||||
<div class="text-3xl font-bold">${s.value}</div>
|
||||
<div class="text-sm mt-1 opacity-90">${s.label}</div>
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
// ── Expiry Warnings ─────────────────────────────────
|
||||
function renderExpiryWarnings(docs) {
|
||||
const soon = docs.filter(d => d.status === 'active' && daysUntil(d.expiryDate) <= 30 && daysUntil(d.expiryDate) > 0);
|
||||
const el = document.getElementById('expiry-warnings');
|
||||
if (!soon.length) { el.innerHTML = ''; return; }
|
||||
el.innerHTML = `
|
||||
<div class="bg-amber-50 border border-amber-300 rounded-2xl p-5">
|
||||
<h3 class="font-bold text-amber-800 text-lg mb-3">⚠️ Expiring Within 30 Days</h3>
|
||||
<div class="space-y-2">
|
||||
${soon.map(d => `
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between bg-white rounded-xl px-4 py-3 shadow-sm border border-amber-200">
|
||||
<div>
|
||||
<span class="font-semibold text-slate-900">${d.name}</span>
|
||||
<span class="inline-block ml-2 text-xs font-medium px-2 py-0.5 rounded-full ${typeBadge[d.type]}">${capitalize(d.type)}</span>
|
||||
</div>
|
||||
<span class="text-amber-700 font-medium text-sm mt-1 sm:mt-0">Expires ${fmtDate(d.expiryDate)} (${daysUntil(d.expiryDate)} day${daysUntil(d.expiryDate) === 1 ? '' : 's'})</span>
|
||||
</div>`).join('')}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Document Cards ──────────────────────────────────
|
||||
function renderDocuments(docs) {
|
||||
const list = document.getElementById('document-list');
|
||||
const empty = document.getElementById('empty-state');
|
||||
|
||||
if (!docs.length) {
|
||||
list.innerHTML = '';
|
||||
empty.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
empty.classList.add('hidden');
|
||||
|
||||
list.innerHTML = docs.map(d => {
|
||||
const statusBadge = d.status === 'active'
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-red-100 text-red-700';
|
||||
return `
|
||||
<div class="bg-white rounded-2xl shadow-lg p-5 sm:p-6 hover:shadow-xl transition-shadow border border-slate-100">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex flex-wrap items-center gap-2 mb-2">
|
||||
<h3 class="font-bold text-slate-900 text-base truncate">${d.name}</h3>
|
||||
<span class="inline-block text-xs font-medium px-2.5 py-0.5 rounded-full ${typeBadge[d.type]}">${capitalize(d.type)}</span>
|
||||
<span class="inline-block text-xs font-medium px-2.5 py-0.5 rounded-full ${statusBadge}">${capitalize(d.status)}</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-x-5 gap-y-1 text-sm text-slate-500">
|
||||
${d.state ? `<span>📍 ${d.state}</span>` : ''}
|
||||
<span>📤 Uploaded ${fmtDate(d.uploadDate)}</span>
|
||||
<span>📅 Expires ${fmtDate(d.expiryDate)}</span>
|
||||
<span>💾 ${d.fileSize}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<button onclick="alert('Viewing: ${d.name}')" class="px-3 py-1.5 text-sm font-medium rounded-lg border border-slate-300 text-slate-700 hover:bg-slate-50 transition-colors">View</button>
|
||||
<button onclick="alert('Downloading: ${d.name}')" class="px-3 py-1.5 text-sm font-medium rounded-lg border border-amber-400 text-amber-700 hover:bg-amber-50 transition-colors">Download</button>
|
||||
<button onclick="alert('Delete requested for: ${d.name}')" class="px-3 py-1.5 text-sm font-medium rounded-lg border border-red-300 text-red-600 hover:bg-red-50 transition-colors">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// ── Filtering ────────────────────────────────────────
|
||||
function applyFilters() {
|
||||
const query = document.getElementById('search-input').value.toLowerCase();
|
||||
const type = document.getElementById('type-filter').value;
|
||||
const status = document.getElementById('status-filter').value;
|
||||
|
||||
const filtered = MOCK_DOCUMENTS.filter(d => {
|
||||
if (query && !d.name.toLowerCase().includes(query) && !d.id.toLowerCase().includes(query)) return false;
|
||||
if (type !== 'all' && d.type !== type) return false;
|
||||
if (status !== 'all' && d.status !== status) return false;
|
||||
return true;
|
||||
});
|
||||
renderDocuments(filtered);
|
||||
}
|
||||
|
||||
document.getElementById('search-input').addEventListener('input', applyFilters);
|
||||
document.getElementById('type-filter').addEventListener('change', applyFilters);
|
||||
document.getElementById('status-filter').addEventListener('change', applyFilters);
|
||||
|
||||
// ── Upload Modal ─────────────────────────────────────
|
||||
function openUploadModal() {
|
||||
const modal = document.getElementById('upload-modal');
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
}
|
||||
|
||||
function closeUploadModal() {
|
||||
const modal = document.getElementById('upload-modal');
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
}
|
||||
|
||||
// ── Initial Render ───────────────────────────────────
|
||||
renderStats(MOCK_DOCUMENTS);
|
||||
renderExpiryWarnings(MOCK_DOCUMENTS);
|
||||
renderDocuments(MOCK_DOCUMENTS);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
302
public/pages/loadboard.html
Normal file
302
public/pages/loadboard.html
Normal file
@@ -0,0 +1,302 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Oversize Load Board | PilotEdge</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body class="bg-slate-50 min-h-screen flex flex-col">
|
||||
|
||||
<div id="main-nav"></div>
|
||||
<div id="poc-banner"></div>
|
||||
|
||||
<!-- Page Header -->
|
||||
<section class="bg-slate-900 text-white pt-24 pb-12 px-4">
|
||||
<div class="max-w-7xl mx-auto flex flex-col md:flex-row md:items-end md:justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-3xl md:text-4xl font-bold mb-3">Oversize Load Board</h1>
|
||||
<p class="text-lg text-gray-400">Active loads needing escort/pilot vehicle services. Carriers post free — escort operators browse and bid.</p>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<button onclick="showPostModal()" class="bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold px-5 py-2.5 rounded-lg transition-colors whitespace-nowrap">
|
||||
+ Post a Load
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Filters -->
|
||||
<section class="max-w-7xl mx-auto px-4 pt-8">
|
||||
<div class="bg-white rounded-2xl shadow-lg p-6">
|
||||
<div class="flex flex-col md:flex-row gap-4 items-end">
|
||||
<div class="flex-1">
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Search</label>
|
||||
<input type="text" id="search-input" oninput="filterLoads()" placeholder="Search by location, carrier, or description..." class="w-full border border-slate-300 rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Status</label>
|
||||
<select id="status-filter" onchange="filterLoads()" class="border border-slate-300 rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
<option value="all">All</option>
|
||||
<option value="posted" selected>Posted (Available)</option>
|
||||
<option value="in_transit">In Transit</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Escorts Needed</label>
|
||||
<select id="escorts-filter" onchange="filterLoads()" class="border border-slate-300 rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
<option value="all">Any</option>
|
||||
<option value="1">1 Escort</option>
|
||||
<option value="2">2 Escorts</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Sort</label>
|
||||
<select id="sort-select" onchange="filterLoads()" class="border border-slate-300 rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
<option value="date_asc">Departure (Soonest)</option>
|
||||
<option value="date_desc">Departure (Latest)</option>
|
||||
<option value="posted_desc">Recently Posted</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Results Count -->
|
||||
<section class="max-w-7xl mx-auto px-4 pt-4">
|
||||
<p id="results-count" class="text-sm text-slate-500 font-medium"></p>
|
||||
</section>
|
||||
|
||||
<!-- Load Listings -->
|
||||
<section class="max-w-7xl mx-auto px-4 py-4 pb-8">
|
||||
<div id="load-list" class="space-y-4">
|
||||
<!-- Populated by JS -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Subscription CTA -->
|
||||
<section class="max-w-7xl mx-auto px-4 pb-8">
|
||||
<div class="bg-gradient-to-r from-slate-800 to-slate-900 rounded-2xl p-8 text-center text-white">
|
||||
<h3 class="text-xl font-bold mb-2">Escort Vehicle Operator?</h3>
|
||||
<p class="text-gray-400 mb-4 max-w-lg mx-auto">Get unlimited load board access, instant notifications for new loads in your area, and priority bidding with a PilotEdge subscription.</p>
|
||||
<button class="bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold px-6 py-3 rounded-lg transition-colors">
|
||||
Subscribe — Starting at $49/mo
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Post Load Modal (simplified for POC) -->
|
||||
<div id="post-modal" class="fixed inset-0 bg-black/50 z-50 hidden flex items-center justify-center p-4">
|
||||
<div class="bg-white rounded-2xl shadow-2xl max-w-lg w-full max-h-[90vh] overflow-y-auto p-8">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-bold text-slate-900">Post a Load</h2>
|
||||
<button onclick="closePostModal()" class="text-slate-400 hover:text-slate-600 text-2xl">×</button>
|
||||
</div>
|
||||
<div class="bg-amber-50 border border-amber-200 rounded-xl p-4 mb-6">
|
||||
<p class="text-amber-900 text-sm"><strong>POC Note:</strong> In production, this form would create a real listing. For now, this demonstrates the posting flow.</p>
|
||||
</div>
|
||||
<form onsubmit="handlePostLoad(event)" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Carrier / Company Name</label>
|
||||
<input type="text" required class="w-full border border-slate-300 rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Origin</label>
|
||||
<input type="text" required placeholder="City, State" class="w-full border border-slate-300 rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Destination</label>
|
||||
<input type="text" required placeholder="City, State" class="w-full border border-slate-300 rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Departure Date</label>
|
||||
<input type="date" required class="w-full border border-slate-300 rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Load Description</label>
|
||||
<textarea required rows="2" placeholder="What's being hauled?" class="w-full border border-slate-300 rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none"></textarea>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Dimensions (W×H×L)</label>
|
||||
<input type="text" placeholder="16'×14'×135'" class="w-full border border-slate-300 rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Escorts Needed</label>
|
||||
<select class="w-full border border-slate-300 rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
<option>1</option>
|
||||
<option>2</option>
|
||||
<option>3+</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="w-full bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold py-3 rounded-lg transition-colors">
|
||||
Post Load
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="main-footer"></div>
|
||||
|
||||
<script src="/js/api.js"></script>
|
||||
<script src="/js/nav.js"></script>
|
||||
<script>
|
||||
renderNav('loadboard');
|
||||
renderBanner();
|
||||
renderFooter();
|
||||
|
||||
(async () => {
|
||||
const MOCK_LOAD_BOARD = await PilotEdge.getLoads();
|
||||
|
||||
function getStatusBadge(status) {
|
||||
if (status === 'posted') return '<span class="bg-green-100 text-green-800 text-xs font-bold px-3 py-1 rounded-full">AVAILABLE</span>';
|
||||
if (status === 'in_transit') return '<span class="bg-blue-100 text-blue-800 text-xs font-bold px-3 py-1 rounded-full">IN TRANSIT</span>';
|
||||
return '<span class="bg-slate-100 text-slate-600 text-xs font-bold px-3 py-1 rounded-full">' + status.toUpperCase() + '</span>';
|
||||
}
|
||||
|
||||
function daysUntil(dateStr) {
|
||||
const today = new Date();
|
||||
today.setHours(0,0,0,0);
|
||||
const target = new Date(dateStr);
|
||||
const diff = Math.ceil((target - today) / (1000 * 60 * 60 * 24));
|
||||
if (diff < 0) return 'Departed';
|
||||
if (diff === 0) return 'Today';
|
||||
if (diff === 1) return 'Tomorrow';
|
||||
return `In ${diff} days`;
|
||||
}
|
||||
|
||||
function renderLoads(loads) {
|
||||
const container = document.getElementById('load-list');
|
||||
document.getElementById('results-count').textContent = `${loads.length} load${loads.length !== 1 ? 's' : ''} found`;
|
||||
|
||||
if (loads.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="bg-white rounded-2xl shadow-lg p-12 text-center">
|
||||
<p class="text-slate-500 text-lg">No loads match your filters.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = loads.map(load => `
|
||||
<div class="bg-white rounded-2xl shadow-lg p-6 hover:shadow-xl transition-shadow">
|
||||
<div class="flex flex-col lg:flex-row lg:items-start justify-between gap-4">
|
||||
<!-- Route & Info -->
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
${getStatusBadge(load.status)}
|
||||
<span class="text-xs text-slate-400 font-medium">${load.id}</span>
|
||||
</div>
|
||||
|
||||
<!-- Route -->
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<div class="text-center">
|
||||
<p class="font-bold text-slate-900 text-lg">${load.origin.city}, ${load.origin.state}</p>
|
||||
</div>
|
||||
<div class="flex-shrink-0 text-amber-500 text-2xl px-2">→</div>
|
||||
<div class="text-center">
|
||||
<p class="font-bold text-slate-900 text-lg">${load.destination.city}, ${load.destination.state}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<p class="text-slate-600 mb-3">${load.description}</p>
|
||||
|
||||
<!-- Carrier -->
|
||||
<p class="text-sm text-slate-500">Posted by <span class="font-semibold text-slate-700">${load.carrier}</span></p>
|
||||
</div>
|
||||
|
||||
<!-- Details Sidebar -->
|
||||
<div class="lg:w-72 flex-shrink-0 bg-slate-50 rounded-xl p-4 space-y-3">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-slate-500">Departure</span>
|
||||
<span class="text-sm font-bold text-slate-900">${new Date(load.departureDate).toLocaleDateString('en-US', {month:'short', day:'numeric', year:'numeric'})} <span class="text-amber-600">(${daysUntil(load.departureDate)})</span></span>
|
||||
</div>
|
||||
<div class="border-t border-slate-200"></div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-slate-500">Width</span>
|
||||
<span class="text-sm font-bold text-slate-900">${load.dimensions.width}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-slate-500">Height</span>
|
||||
<span class="text-sm font-bold text-slate-900">${load.dimensions.height}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-slate-500">Length</span>
|
||||
<span class="text-sm font-bold text-slate-900">${load.dimensions.length}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-slate-500">Weight</span>
|
||||
<span class="text-sm font-bold text-slate-900">${load.dimensions.weight}</span>
|
||||
</div>
|
||||
<div class="border-t border-slate-200"></div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm text-slate-500">Escorts Needed</span>
|
||||
<span class="bg-amber-100 text-amber-800 text-xs font-bold px-3 py-1 rounded-full">${load.escortsNeeded} vehicle${load.escortsNeeded > 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
${load.status === 'posted' ? `
|
||||
<button class="w-full mt-2 bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold py-2 rounded-lg transition-colors text-sm">
|
||||
Contact Carrier
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function filterLoads() {
|
||||
const search = document.getElementById('search-input').value.toLowerCase();
|
||||
const statusFilter = document.getElementById('status-filter').value;
|
||||
const escortsFilter = document.getElementById('escorts-filter').value;
|
||||
const sortBy = document.getElementById('sort-select').value;
|
||||
|
||||
let filtered = MOCK_LOAD_BOARD.filter(load => {
|
||||
if (statusFilter !== 'all' && load.status !== statusFilter) return false;
|
||||
if (escortsFilter !== 'all' && load.escortsNeeded !== parseInt(escortsFilter)) return false;
|
||||
if (search) {
|
||||
const searchText = `${load.origin.city} ${load.origin.state} ${load.destination.city} ${load.destination.state} ${load.carrier} ${load.description}`.toLowerCase();
|
||||
if (!searchText.includes(search)) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// Sort
|
||||
filtered.sort((a, b) => {
|
||||
if (sortBy === 'date_asc') return new Date(a.departureDate) - new Date(b.departureDate);
|
||||
if (sortBy === 'date_desc') return new Date(b.departureDate) - new Date(a.departureDate);
|
||||
if (sortBy === 'posted_desc') return new Date(b.postedDate) - new Date(a.postedDate);
|
||||
return 0;
|
||||
});
|
||||
|
||||
renderLoads(filtered);
|
||||
}
|
||||
|
||||
// Initial render
|
||||
filterLoads();
|
||||
|
||||
function showPostModal() {
|
||||
document.getElementById('post-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closePostModal() {
|
||||
document.getElementById('post-modal').classList.add('hidden');
|
||||
}
|
||||
|
||||
function handlePostLoad(e) {
|
||||
e.preventDefault();
|
||||
closePostModal();
|
||||
alert('Load posted! (POC demo — in production, this would create a real listing.)');
|
||||
}
|
||||
|
||||
// Close modal on backdrop click
|
||||
document.getElementById('post-modal').addEventListener('click', function(e) {
|
||||
if (e.target === this) closePostModal();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
305
public/pages/locator.html
Normal file
305
public/pages/locator.html
Normal file
@@ -0,0 +1,305 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Find Escort Vehicles | PilotEdge</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<style>
|
||||
#locator-map { height: 500px; width: 100%; border-radius: 0.75rem; }
|
||||
.operator-card.active { ring: 2px solid #f59e0b; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-slate-50 min-h-screen flex flex-col">
|
||||
|
||||
<div id="main-nav"></div>
|
||||
<div id="poc-banner"></div>
|
||||
|
||||
<!-- Page Header -->
|
||||
<section class="bg-slate-900 text-white pt-24 pb-12 px-4">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<h1 class="text-3xl md:text-4xl font-bold mb-3">Find Escort Vehicles</h1>
|
||||
<p class="text-lg text-gray-400 max-w-3xl">Browse available pilot/escort vehicle operators near your load's departure point. Click a marker on the map or browse the list below.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Filter Bar -->
|
||||
<section class="max-w-7xl mx-auto px-4 pt-8">
|
||||
<div class="bg-white rounded-2xl shadow-lg p-6">
|
||||
<div class="flex flex-col md:flex-row gap-4">
|
||||
<div class="flex-1">
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Search by State or Name</label>
|
||||
<input type="text" id="op-search" oninput="filterOperators()" placeholder="e.g. Texas, Mike's Pilot Car..." class="w-full border border-slate-300 rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Status</label>
|
||||
<select id="op-status" onchange="filterOperators()" class="border border-slate-300 rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
<option value="all">All</option>
|
||||
<option value="available">Available Now</option>
|
||||
<option value="on_job">On a Job</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Certified In</label>
|
||||
<select id="op-cert" onchange="filterOperators()" class="border border-slate-300 rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
<option value="all">Any State</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Map + List Layout -->
|
||||
<section class="max-w-7xl mx-auto px-4 py-8">
|
||||
<div class="grid lg:grid-cols-5 gap-6">
|
||||
<!-- Map (3 cols) -->
|
||||
<div class="lg:col-span-3">
|
||||
<div class="bg-white rounded-2xl shadow-lg p-4">
|
||||
<div id="locator-map"></div>
|
||||
</div>
|
||||
|
||||
<!-- Legend -->
|
||||
<div class="flex gap-6 mt-3 px-2 text-sm text-slate-500">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-4 h-4 rounded-full bg-green-500 inline-block border-2 border-white shadow"></span>
|
||||
Available
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-4 h-4 rounded-full bg-amber-500 inline-block border-2 border-white shadow"></span>
|
||||
On a Job
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Operator List (2 cols) -->
|
||||
<div class="lg:col-span-2">
|
||||
<div id="operator-count" class="text-sm text-slate-500 font-medium mb-3"></div>
|
||||
<div id="operator-list" class="space-y-4 max-h-[560px] overflow-y-auto pr-1">
|
||||
<!-- Populated by JS -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Operator Detail Panel -->
|
||||
<section id="operator-detail" class="max-w-7xl mx-auto px-4 pb-8 hidden">
|
||||
<div class="bg-white rounded-2xl shadow-lg p-8">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 id="op-detail-name" class="text-2xl font-bold text-slate-900"></h2>
|
||||
<button onclick="document.getElementById('operator-detail').classList.add('hidden')" class="text-slate-400 hover:text-slate-600 text-2xl">×</button>
|
||||
</div>
|
||||
<div id="op-detail-content">
|
||||
<!-- Populated by JS -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA for Operators -->
|
||||
<section class="max-w-7xl mx-auto px-4 pb-8">
|
||||
<div class="bg-gradient-to-r from-slate-800 to-slate-900 rounded-2xl p-8 text-white">
|
||||
<div class="grid md:grid-cols-2 gap-8 items-center">
|
||||
<div>
|
||||
<h3 class="text-xl font-bold mb-2">Are You an Escort Vehicle Operator?</h3>
|
||||
<p class="text-gray-400">Add your location to our map and get discovered by carriers and truck drivers looking for escort services in your area.</p>
|
||||
</div>
|
||||
<div class="text-center md:text-right">
|
||||
<button onclick="alert('Coming soon! In production, this would open a registration/profile creation flow.')" class="bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold px-6 py-3 rounded-lg transition-colors">
|
||||
Register Your Service →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div id="main-footer"></div>
|
||||
|
||||
<script src="/js/api.js"></script>
|
||||
<script src="/js/nav.js"></script>
|
||||
<script>
|
||||
renderNav('locator');
|
||||
renderBanner();
|
||||
renderFooter();
|
||||
|
||||
(async () => {
|
||||
const MOCK_ESCORT_OPERATORS = await PilotEdge.getEscortOperators();
|
||||
|
||||
// Populate certification filter
|
||||
const allCerts = new Set();
|
||||
MOCK_ESCORT_OPERATORS.forEach(op => op.certifications.forEach(c => allCerts.add(c)));
|
||||
const certSelect = document.getElementById('op-cert');
|
||||
Array.from(allCerts).sort().forEach(state => {
|
||||
certSelect.add(new Option(state, state));
|
||||
});
|
||||
|
||||
// Initialize map
|
||||
const locatorMap = L.map('locator-map').setView([37.5, -95], 4);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors',
|
||||
maxZoom: 18
|
||||
}).addTo(locatorMap);
|
||||
|
||||
let markers = {};
|
||||
|
||||
function addOperatorMarkers(operators) {
|
||||
// Clear existing markers
|
||||
Object.values(markers).forEach(m => locatorMap.removeLayer(m));
|
||||
markers = {};
|
||||
|
||||
operators.forEach(op => {
|
||||
const color = op.status === 'available' ? '#22c55e' : '#f59e0b';
|
||||
const marker = L.circleMarker([op.location.lat, op.location.lng], {
|
||||
radius: 10,
|
||||
fillColor: color,
|
||||
color: '#fff',
|
||||
weight: 2,
|
||||
opacity: 1,
|
||||
fillOpacity: 0.9
|
||||
}).addTo(locatorMap);
|
||||
|
||||
marker.bindPopup(`
|
||||
<div style="min-width:200px;">
|
||||
<strong style="font-size:14px;">${op.name}</strong><br>
|
||||
<span style="color:#666; font-size:12px;">${op.location.city}, ${op.location.state}</span><br>
|
||||
<span style="color:${op.status === 'available' ? '#16a34a' : '#d97706'}; font-size:12px; font-weight:600;">${op.status === 'available' ? '● Available' : '● On a Job'}</span><br>
|
||||
<span style="font-size:12px;">⭐ ${op.rating} · ${op.totalJobs} jobs</span><br><br>
|
||||
<button onclick="showOperatorDetail('${op.id}')" style="background:#f59e0b; color:#0f172a; font-weight:700; padding:6px 16px; border-radius:6px; border:none; cursor:pointer; width:100%;">
|
||||
View Profile
|
||||
</button>
|
||||
</div>
|
||||
`);
|
||||
|
||||
markers[op.id] = marker;
|
||||
});
|
||||
}
|
||||
|
||||
function renderOperatorList(operators) {
|
||||
const container = document.getElementById('operator-list');
|
||||
document.getElementById('operator-count').textContent = `${operators.length} operator${operators.length !== 1 ? 's' : ''} found`;
|
||||
|
||||
if (operators.length === 0) {
|
||||
container.innerHTML = `<div class="bg-white rounded-xl p-8 text-center text-slate-500">No operators match your filters.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = operators.map(op => `
|
||||
<div class="bg-white rounded-xl shadow-md p-5 hover:shadow-lg transition-shadow cursor-pointer" onclick="showOperatorDetail('${op.id}')">
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<h3 class="font-bold text-slate-900">${op.name}</h3>
|
||||
<span class="flex-shrink-0 w-3 h-3 rounded-full ${op.status === 'available' ? 'bg-green-500' : 'bg-amber-500'} mt-1.5"></span>
|
||||
</div>
|
||||
<p class="text-sm text-slate-500 mb-2">${op.location.city}, ${op.location.state}</p>
|
||||
<div class="flex items-center gap-3 text-sm mb-3">
|
||||
<span class="text-amber-500">⭐ ${op.rating}</span>
|
||||
<span class="text-slate-400">·</span>
|
||||
<span class="text-slate-600">${op.totalJobs} jobs</span>
|
||||
<span class="text-slate-400">·</span>
|
||||
<span class="text-slate-600">${op.experience}</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
${op.certifications.map(c => `<span class="bg-slate-100 text-slate-600 text-xs font-medium px-2 py-0.5 rounded">${c}</span>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function showOperatorDetail(id) {
|
||||
const op = MOCK_ESCORT_OPERATORS.find(o => o.id === id);
|
||||
if (!op) return;
|
||||
|
||||
// Center map on operator
|
||||
locatorMap.setView([op.location.lat, op.location.lng], 7);
|
||||
if (markers[id]) markers[id].openPopup();
|
||||
|
||||
document.getElementById('op-detail-name').textContent = op.name;
|
||||
document.getElementById('op-detail-content').innerHTML = `
|
||||
<div class="grid md:grid-cols-2 gap-8">
|
||||
<div>
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<span class="inline-block w-4 h-4 rounded-full ${op.status === 'available' ? 'bg-green-500' : 'bg-amber-500'}"></span>
|
||||
<span class="font-semibold ${op.status === 'available' ? 'text-green-700' : 'text-amber-700'}">${op.status === 'available' ? 'Available for Jobs' : 'Currently on a Job'}</span>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="bg-slate-50 px-4 py-3 rounded-lg">
|
||||
<span class="text-sm text-slate-500 block">Location</span>
|
||||
<span class="font-semibold text-slate-900">${op.location.city}, ${op.location.state}</span>
|
||||
</div>
|
||||
<div class="bg-slate-50 px-4 py-3 rounded-lg">
|
||||
<span class="text-sm text-slate-500 block">Experience</span>
|
||||
<span class="font-semibold text-slate-900">${op.experience} · ${op.totalJobs} completed jobs</span>
|
||||
</div>
|
||||
<div class="bg-slate-50 px-4 py-3 rounded-lg">
|
||||
<span class="text-sm text-slate-500 block">Rating</span>
|
||||
<span class="font-semibold text-slate-900">⭐ ${op.rating} / 5.0</span>
|
||||
</div>
|
||||
<div class="bg-slate-50 px-4 py-3 rounded-lg">
|
||||
<span class="text-sm text-slate-500 block">Vehicle</span>
|
||||
<span class="font-semibold text-slate-900">${op.vehicleType}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<h3 class="font-bold text-slate-900 mb-2">Certified In</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
${op.certifications.map(c => `<span class="bg-amber-100 text-amber-800 text-sm font-semibold px-3 py-1 rounded-full">${c}</span>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h3 class="font-bold text-slate-900 mb-2">About</h3>
|
||||
<p class="text-slate-600">${op.bio}</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-slate-50 rounded-xl p-4 space-y-2">
|
||||
<h3 class="font-bold text-slate-900 mb-2">Contact</h3>
|
||||
<p class="text-sm"><span class="text-slate-500">Email:</span> <a href="mailto:${op.contact}" class="text-amber-600 hover:text-amber-700 font-medium">${op.contact}</a></p>
|
||||
<p class="text-sm"><span class="text-slate-500">Phone:</span> <a href="tel:${op.phone}" class="text-amber-600 hover:text-amber-700 font-medium">${op.phone}</a></p>
|
||||
</div>
|
||||
|
||||
${op.status === 'available' ? `
|
||||
<a href="order.html" class="block mt-4 bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold py-3 rounded-lg transition-colors text-center">
|
||||
Request This Operator
|
||||
</a>
|
||||
` : `
|
||||
<div class="mt-4 bg-slate-100 text-slate-500 font-medium py-3 rounded-lg text-center text-sm">
|
||||
Currently unavailable — check back later
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const detailEl = document.getElementById('operator-detail');
|
||||
detailEl.classList.remove('hidden');
|
||||
detailEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
|
||||
function filterOperators() {
|
||||
const search = document.getElementById('op-search').value.toLowerCase();
|
||||
const statusFilter = document.getElementById('op-status').value;
|
||||
const certFilter = document.getElementById('op-cert').value;
|
||||
|
||||
let filtered = MOCK_ESCORT_OPERATORS.filter(op => {
|
||||
if (statusFilter !== 'all' && op.status !== statusFilter) return false;
|
||||
if (certFilter !== 'all' && !op.certifications.includes(certFilter)) return false;
|
||||
if (search) {
|
||||
const searchText = `${op.name} ${op.location.city} ${op.location.state} ${op.certifications.join(' ')} ${op.bio}`.toLowerCase();
|
||||
if (!searchText.includes(search)) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
renderOperatorList(filtered);
|
||||
addOperatorMarkers(filtered);
|
||||
}
|
||||
|
||||
// Initial render
|
||||
filterOperators();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
241
public/pages/order.html
Normal file
241
public/pages/order.html
Normal 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"">
|
||||
</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"">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Overall Length</label>
|
||||
<input type="text" name="length" class="w-full border border-slate-300 rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none" placeholder="135'">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Gross Weight</label>
|
||||
<input type="text" name="weight" class="w-full border border-slate-300 rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none" placeholder="185,000 lbs">
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Trailer Type</label>
|
||||
<select name="trailer_type" class="w-full border border-slate-300 rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
<option value="">Select...</option>
|
||||
<option>Lowboy / RGN</option>
|
||||
<option>Flatbed</option>
|
||||
<option>Step Deck</option>
|
||||
<option>Extendable / Stretch</option>
|
||||
<option>Double Drop</option>
|
||||
<option>Multi-Axle / Schnabel</option>
|
||||
<option>Perimeter (Beam/Bolster)</option>
|
||||
<option>Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Number of Axles</label>
|
||||
<select name="axles" class="w-full border border-slate-300 rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
<option value="">Select...</option>
|
||||
<option>5 axles (standard)</option>
|
||||
<option>6 axles</option>
|
||||
<option>7 axles</option>
|
||||
<option>8 axles</option>
|
||||
<option>9+ axles</option>
|
||||
<option>13+ axles (superload)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Route Details -->
|
||||
<div class="bg-white rounded-2xl shadow-lg p-8">
|
||||
<h2 class="text-xl font-bold text-slate-900 mb-6 flex items-center">
|
||||
<span class="w-8 h-8 bg-amber-100 rounded-lg flex items-center justify-center text-amber-600 mr-3 text-sm font-bold">3</span>
|
||||
Route & Schedule
|
||||
</h2>
|
||||
<div class="grid md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Pickup Location <span class="text-red-500">*</span></label>
|
||||
<input type="text" name="pickup" required class="w-full border border-slate-300 rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none" placeholder="City, State or full address">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Delivery Location <span class="text-red-500">*</span></label>
|
||||
<input type="text" name="delivery" required class="w-full border border-slate-300 rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none" placeholder="City, State or full address">
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid md:grid-cols-3 gap-4 mb-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Departure Date <span class="text-red-500">*</span></label>
|
||||
<input type="date" name="departure_date" required class="w-full border border-slate-300 rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Flexibility</label>
|
||||
<select name="flexibility" class="w-full border border-slate-300 rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
<option>Exact date</option>
|
||||
<option>± 1 day</option>
|
||||
<option>± 2-3 days</option>
|
||||
<option>± 1 week</option>
|
||||
<option>Flexible</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">States on Route</label>
|
||||
<input type="text" name="states" class="w-full border border-slate-300 rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none" placeholder="e.g. TX, OK, KS">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Do you already have permits?</label>
|
||||
<div class="flex gap-4 mt-1">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" name="has_permits" value="yes" class="accent-amber-500"> <span class="text-slate-700">Yes, permits are in hand</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" name="has_permits" value="no" checked class="accent-amber-500"> <span class="text-slate-700">No, still need permits</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" name="has_permits" value="some" class="accent-amber-500"> <span class="text-slate-700">Some states</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Escort Requirements -->
|
||||
<div class="bg-white rounded-2xl shadow-lg p-8">
|
||||
<h2 class="text-xl font-bold text-slate-900 mb-6 flex items-center">
|
||||
<span class="w-8 h-8 bg-amber-100 rounded-lg flex items-center justify-center text-amber-600 mr-3 text-sm font-bold">4</span>
|
||||
Escort Requirements
|
||||
</h2>
|
||||
<div class="grid md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Escort Vehicles Needed</label>
|
||||
<select name="escorts_needed" class="w-full border border-slate-300 rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
<option>1 (front only)</option>
|
||||
<option>1 (rear only)</option>
|
||||
<option>2 (front and rear)</option>
|
||||
<option>3+ (complex move)</option>
|
||||
<option>Not sure — help me determine</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Height Pole Required?</label>
|
||||
<select name="height_pole" class="w-full border border-slate-300 rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
<option>No</option>
|
||||
<option>Yes</option>
|
||||
<option>Not sure</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Special Requirements or Notes</label>
|
||||
<textarea name="notes" rows="4" class="w-full border border-slate-300 rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none" placeholder="Any additional details — night travel needs, multiple loads, specific equipment requirements, etc."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit -->
|
||||
<div class="flex flex-col sm:flex-row gap-4 items-center justify-between">
|
||||
<p class="text-sm text-slate-500">Fields marked with <span class="text-red-500">*</span> are required. We typically respond within 2-4 hours.</p>
|
||||
<button type="submit" class="bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold px-8 py-4 rounded-xl text-lg transition-colors shadow-lg hover:shadow-xl whitespace-nowrap">
|
||||
Submit Request →
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Success Message (hidden by default) -->
|
||||
<div id="success-message" class="hidden">
|
||||
<div class="bg-white rounded-2xl shadow-lg p-12 text-center">
|
||||
<div class="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center text-4xl mx-auto mb-6">✅</div>
|
||||
<h2 class="text-2xl font-bold text-slate-900 mb-3">Request Submitted!</h2>
|
||||
<p class="text-slate-600 mb-6 max-w-md mx-auto">Thank you for your escort service request. We've received your details and will get back to you within 2-4 hours with availability and pricing.</p>
|
||||
<div class="bg-slate-50 rounded-xl p-4 mb-6 inline-block">
|
||||
<p class="text-sm text-slate-500">Reference Number</p>
|
||||
<p id="ref-number" class="text-xl font-bold text-slate-900">PE-2026-0001</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="index.html" class="inline-block bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold px-6 py-3 rounded-lg transition-colors">
|
||||
← Back to Home
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div id="main-footer"></div>
|
||||
|
||||
<script src="/js/nav.js"></script>
|
||||
<script>
|
||||
renderNav('order');
|
||||
renderBanner();
|
||||
renderFooter();
|
||||
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
// In production, this would POST to an API
|
||||
const refNum = 'PE-2026-' + String(Math.floor(Math.random() * 9000) + 1000);
|
||||
document.getElementById('ref-number').textContent = refNum;
|
||||
document.getElementById('order-form').classList.add('hidden');
|
||||
document.getElementById('success-message').classList.remove('hidden');
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
444
public/pages/regulations.html
Normal file
444
public/pages/regulations.html
Normal file
@@ -0,0 +1,444 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>State Regulations Map | PilotEdge</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<style>
|
||||
#map { height: 550px; width: 100%; border-radius: 0.75rem; }
|
||||
.state-detail-row { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; }
|
||||
.leaflet-popup-content { margin: 8px 12px; }
|
||||
.leaflet-popup-content-wrapper { border-radius: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-slate-50 min-h-screen flex flex-col">
|
||||
|
||||
<div id="main-nav"></div>
|
||||
<div id="poc-banner"></div>
|
||||
|
||||
<!-- Page Header -->
|
||||
<section class="bg-slate-900 text-white pt-24 pb-12 px-4">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<h1 class="text-3xl md:text-4xl font-bold mb-3">State-by-State Regulations Map</h1>
|
||||
<p class="text-lg text-gray-400 max-w-3xl">Click any state marker to view oversize load permit thresholds, escort requirements, and travel restrictions.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Map Section -->
|
||||
<section class="max-w-7xl mx-auto px-4 py-8 w-full">
|
||||
<div class="bg-white rounded-2xl shadow-lg p-4">
|
||||
<div id="map"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Route Checker -->
|
||||
<section class="max-w-7xl mx-auto px-4 pb-8">
|
||||
<div class="bg-white rounded-2xl shadow-lg p-8">
|
||||
<h2 class="text-2xl font-bold text-slate-900 mb-2">Quick Route Checker</h2>
|
||||
<p class="text-slate-600 mb-6">Enter your load dimensions and route to see which states require permits and escorts.</p>
|
||||
|
||||
<div class="grid md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Load Width</label>
|
||||
<div class="flex">
|
||||
<input type="number" id="rc-width-ft" placeholder="ft" min="0" max="30" class="w-1/2 border border-slate-300 rounded-l-lg px-3 py-2 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
<input type="number" id="rc-width-in" placeholder="in" min="0" max="11" class="w-1/2 border border-l-0 border-slate-300 rounded-r-lg px-3 py-2 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Load Height</label>
|
||||
<div class="flex">
|
||||
<input type="number" id="rc-height-ft" placeholder="ft" min="0" max="25" class="w-1/2 border border-slate-300 rounded-l-lg px-3 py-2 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
<input type="number" id="rc-height-in" placeholder="in" min="0" max="11" class="w-1/2 border border-l-0 border-slate-300 rounded-r-lg px-3 py-2 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Overall Length</label>
|
||||
<div class="flex">
|
||||
<input type="number" id="rc-length-ft" placeholder="ft" min="0" max="200" class="w-1/2 border border-slate-300 rounded-l-lg px-3 py-2 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
<input type="number" id="rc-length-in" placeholder="in" min="0" max="11" class="w-1/2 border border-l-0 border-slate-300 rounded-r-lg px-3 py-2 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Gross Weight</label>
|
||||
<input type="text" id="rc-weight" placeholder="e.g. 120,000 lbs" class="w-full border border-slate-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-4 mb-6">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Origin State</label>
|
||||
<select id="rc-origin" class="w-full border border-slate-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
<option value="">Select state...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Destination State</label>
|
||||
<select id="rc-destination" class="w-full border border-slate-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
<option value="">Select state...</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button onclick="checkRoute()" class="bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold px-6 py-3 rounded-lg transition-colors shadow-md">
|
||||
Check Route Requirements
|
||||
</button>
|
||||
|
||||
<div id="route-results" class="mt-6 hidden">
|
||||
<!-- Results populated by JS -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- State Detail Panel (populated on click) -->
|
||||
<section id="state-detail" class="max-w-7xl mx-auto px-4 pb-8 hidden">
|
||||
<div class="bg-white rounded-2xl shadow-lg p-8">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 id="state-detail-name" class="text-2xl font-bold text-slate-900"></h2>
|
||||
<button onclick="document.getElementById('state-detail').classList.add('hidden')" class="text-slate-400 hover:text-slate-600 text-2xl">×</button>
|
||||
</div>
|
||||
<div id="state-detail-content">
|
||||
<!-- Populated by JS -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Equipment Requirements Section (Module 12) -->
|
||||
<section id="equipment" class="max-w-7xl mx-auto px-4 pb-8">
|
||||
<div class="bg-white rounded-2xl shadow-lg p-8">
|
||||
<h2 class="text-2xl font-bold text-slate-900 mb-2">State Equipment Requirements</h2>
|
||||
<p class="text-slate-600 mb-6">Equipment your escort vehicle and truck/trailer must carry — varies by state. Select a state to see requirements.</p>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-4 mb-6">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Select State</label>
|
||||
<select id="equip-state" onchange="showEquipment()" class="w-full border border-slate-300 rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
<option value="">Choose a state...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<p class="text-sm text-slate-500">Detailed equipment data available for 12 major trucking states. More coming soon.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="equip-content" class="hidden">
|
||||
<!-- Populated by JS -->
|
||||
</div>
|
||||
|
||||
<div id="equip-no-data" class="hidden">
|
||||
<div class="bg-slate-50 rounded-xl p-8 text-center">
|
||||
<p class="text-slate-500 text-lg mb-2">Equipment data for this state is coming soon.</p>
|
||||
<p class="text-slate-400 text-sm">We're actively adding detailed equipment requirements for all 50 states.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div id="main-footer"></div>
|
||||
|
||||
<script src="/js/api.js"></script>
|
||||
<script src="/js/nav.js"></script>
|
||||
<script>
|
||||
renderNav('regulations');
|
||||
renderBanner();
|
||||
renderFooter();
|
||||
|
||||
(async () => {
|
||||
const MOCK_STATE_REGULATIONS = await PilotEdge.getRegulations();
|
||||
const MOCK_STATE_EQUIPMENT = await PilotEdge.getEquipment();
|
||||
|
||||
// Initialize map centered on continental US
|
||||
const map = L.map('map').setView([39.5, -98.5], 4);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© 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>
|
||||
244
public/pages/truckstops.html
Normal file
244
public/pages/truckstops.html
Normal file
@@ -0,0 +1,244 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Truck Stops & Parking | PilotEdge</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<style>
|
||||
#map { height: 450px; width: 100%; border-radius: 0.75rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-slate-50 min-h-screen flex flex-col">
|
||||
|
||||
<div id="main-nav"></div>
|
||||
<div id="poc-banner"></div>
|
||||
|
||||
<!-- Page Header -->
|
||||
<section class="bg-slate-900 text-white pt-24 pb-12 px-4">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<h1 class="text-3xl md:text-4xl font-bold mb-3">Truck Stops & Parking for Oversize Loads</h1>
|
||||
<p class="text-lg text-gray-400 max-w-3xl">Find oversize-friendly truck stops, rest areas, and staging locations across the US. Community-verified with driver comments and entrance dimensions.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Filter Bar -->
|
||||
<section class="max-w-7xl mx-auto px-4 pt-8 w-full">
|
||||
<div class="bg-white rounded-2xl shadow-lg p-6">
|
||||
<div class="flex flex-col md:flex-row gap-4 items-end">
|
||||
<div class="flex-1">
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Search Truck Stops</label>
|
||||
<input type="text" id="ts-search" oninput="filterStops()" placeholder="e.g. Iowa 80, Amarillo, TX..." class="w-full border border-slate-300 rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
</div>
|
||||
<div class="flex items-center gap-2 pb-1">
|
||||
<input type="checkbox" id="ts-oversize-only" onchange="filterStops()" class="w-4 h-4 text-amber-500 border-slate-300 rounded focus:ring-amber-400">
|
||||
<label for="ts-oversize-only" class="text-sm font-semibold text-slate-700 whitespace-nowrap">Oversize Friendly Only</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Map -->
|
||||
<section class="max-w-7xl mx-auto px-4 pt-8 w-full">
|
||||
<div class="bg-white rounded-2xl shadow-lg p-4">
|
||||
<div id="map"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Truck Stop Cards -->
|
||||
<section class="max-w-7xl mx-auto px-4 pt-8 pb-8 w-full">
|
||||
<div id="ts-list" class="space-y-6"></div>
|
||||
</section>
|
||||
|
||||
<!-- Submit a Location -->
|
||||
<section class="max-w-7xl mx-auto px-4 pb-12 w-full">
|
||||
<div class="bg-amber-50 border-2 border-amber-200 rounded-2xl p-8 text-center">
|
||||
<h2 class="text-2xl font-bold text-slate-900 mb-2">Know an Oversize-Friendly Location?</h2>
|
||||
<p class="text-slate-600 mb-4">Help the community by suggesting truck stops, rest areas, or staging lots that accommodate oversize loads.</p>
|
||||
<button onclick="alert('POC: This would open a submission form for new truck stop locations. Feature coming in production release.')" class="bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold px-6 py-3 rounded-lg transition-colors shadow-md hover:shadow-lg">
|
||||
📍 Submit a Location
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div id="main-footer"></div>
|
||||
|
||||
<script src="/js/api.js"></script>
|
||||
<script src="/js/nav.js"></script>
|
||||
<script>
|
||||
renderNav('truckstops');
|
||||
renderBanner();
|
||||
renderFooter();
|
||||
|
||||
(async () => {
|
||||
const MOCK_TRUCK_STOPS = await PilotEdge.getTruckStops();
|
||||
|
||||
// Facility emoji mapping
|
||||
const facilityIcons = {
|
||||
fuel: '⛽', food: '🍔', restrooms: '🚻', showers: '🚿',
|
||||
mechanic: '🔧', scale: '⚖️', ev_charging: '🔌', hotel: '🏨',
|
||||
trucking_museum: '🏛️', barber: '💈', chiropractor: '🦴', iron_skillet: '🍳'
|
||||
};
|
||||
|
||||
let map, markers = [];
|
||||
|
||||
function initMap() {
|
||||
map = L.map('map').setView([39.5, -98.5], 4);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© 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>
|
||||
269
public/pages/weighstations.html
Normal file
269
public/pages/weighstations.html
Normal file
@@ -0,0 +1,269 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Weigh Stations | PilotEdge</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<style>
|
||||
#map { height: 450px; width: 100%; border-radius: 0.75rem; }
|
||||
.leaflet-popup-content { margin: 8px 12px; }
|
||||
.leaflet-popup-content-wrapper { border-radius: 12px; }
|
||||
.toast-msg {
|
||||
position: fixed; bottom: 1.5rem; right: 1.5rem; z-index: 9999;
|
||||
background: #0f172a; color: #fbbf24; padding: 0.75rem 1.25rem;
|
||||
border-radius: 0.75rem; font-weight: 600; box-shadow: 0 8px 30px rgba(0,0,0,.25);
|
||||
animation: slideUp .3s ease-out;
|
||||
}
|
||||
@keyframes slideUp { from { opacity:0; transform:translateY(12px); } to { opacity:1; transform:translateY(0); } }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-slate-50 min-h-screen flex flex-col">
|
||||
|
||||
<div id="main-nav"></div>
|
||||
<div id="poc-banner"></div>
|
||||
|
||||
<!-- Page Header -->
|
||||
<section class="bg-slate-900 text-white pt-24 pb-12 px-4">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<h1 class="text-3xl md:text-4xl font-bold mb-3">Weigh Stations & Inspection Stations</h1>
|
||||
<p class="text-lg text-gray-400 max-w-3xl">Live crowd-sourced status — see which stations are open or closed right now.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Stats Bar -->
|
||||
<section class="max-w-7xl mx-auto px-4 -mt-6 w-full relative z-10">
|
||||
<div id="stats-bar" class="grid grid-cols-3 gap-4"></div>
|
||||
</section>
|
||||
|
||||
<!-- Map Section -->
|
||||
<section class="max-w-7xl mx-auto px-4 py-8 w-full">
|
||||
<div class="bg-white rounded-2xl shadow-lg p-4">
|
||||
<div id="map"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Filter Bar -->
|
||||
<section class="max-w-7xl mx-auto px-4 pb-6 w-full">
|
||||
<div class="bg-white rounded-2xl shadow-lg p-6">
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Search</label>
|
||||
<input id="filter-search" type="text" placeholder="Name, state, or route…"
|
||||
class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">Status</label>
|
||||
<select id="filter-status"
|
||||
class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
<option value="all">All Stations</option>
|
||||
<option value="open">Open Only</option>
|
||||
<option value="closed">Closed Only</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700 mb-1">PrePass</label>
|
||||
<select id="filter-prepass"
|
||||
class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
|
||||
<option value="all">Any</option>
|
||||
<option value="yes">PrePass Accepted</option>
|
||||
<option value="no">No PrePass</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<button id="btn-clear-filters"
|
||||
class="w-full bg-slate-200 hover:bg-slate-300 text-slate-700 font-semibold px-4 py-2 rounded-lg text-sm transition-colors">
|
||||
Clear Filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Station Cards -->
|
||||
<section class="max-w-7xl mx-auto px-4 pb-16 w-full">
|
||||
<div id="station-list" class="grid md:grid-cols-2 xl:grid-cols-3 gap-6"></div>
|
||||
<p id="no-results" class="hidden text-center text-slate-500 py-12 text-lg">No stations match your filters.</p>
|
||||
</section>
|
||||
|
||||
<div id="main-footer"></div>
|
||||
|
||||
<script src="/js/api.js"></script>
|
||||
<script src="/js/nav.js"></script>
|
||||
<script>
|
||||
renderNav('weighstations');
|
||||
renderBanner();
|
||||
renderFooter();
|
||||
|
||||
(async () => {
|
||||
const MOCK_WEIGH_STATIONS = await PilotEdge.getWeighStations();
|
||||
|
||||
// ── Local mutable copy of station data ──
|
||||
const stations = JSON.parse(JSON.stringify(MOCK_WEIGH_STATIONS));
|
||||
|
||||
// ── Map setup ──
|
||||
const map = L.map('map').setView([39.5, -98.35], 4);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© 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>
|
||||
Reference in New Issue
Block a user