Files
PilotEdge/alerts.html
Daniel Kovalevich 260f7c4928 first commit
2026-03-30 13:56:24 -04:00

419 lines
18 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>