first commit

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

418
alerts.html Normal file
View File

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