- 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>
270 lines
12 KiB
HTML
270 lines
12 KiB
HTML
<!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>
|