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:
Daniel Kovalevich
2026-03-30 15:52:56 -04:00
parent f917fb8014
commit 93efb907ff
20 changed files with 281 additions and 109 deletions

305
public/pages/locator.html Normal file
View 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">&times;</button>
</div>
<div id="op-detail-content">
<!-- Populated by JS -->
</div>
</div>
</section>
<!-- CTA for Operators -->
<section class="max-w-7xl mx-auto px-4 pb-8">
<div class="bg-gradient-to-r from-slate-800 to-slate-900 rounded-2xl p-8 text-white">
<div class="grid md:grid-cols-2 gap-8 items-center">
<div>
<h3 class="text-xl font-bold mb-2">Are You an Escort Vehicle Operator?</h3>
<p class="text-gray-400">Add your location to our map and get discovered by carriers and truck drivers looking for escort services in your area.</p>
</div>
<div class="text-center md:text-right">
<button onclick="alert('Coming soon! In production, this would open a registration/profile creation flow.')" class="bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold px-6 py-3 rounded-lg transition-colors">
Register Your Service →
</button>
</div>
</div>
</div>
</section>
<div id="main-footer"></div>
<script src="/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: '&copy; OpenStreetMap contributors',
maxZoom: 18
}).addTo(locatorMap);
let markers = {};
function addOperatorMarkers(operators) {
// Clear existing markers
Object.values(markers).forEach(m => locatorMap.removeLayer(m));
markers = {};
operators.forEach(op => {
const color = op.status === 'available' ? '#22c55e' : '#f59e0b';
const marker = L.circleMarker([op.location.lat, op.location.lng], {
radius: 10,
fillColor: color,
color: '#fff',
weight: 2,
opacity: 1,
fillOpacity: 0.9
}).addTo(locatorMap);
marker.bindPopup(`
<div style="min-width:200px;">
<strong style="font-size:14px;">${op.name}</strong><br>
<span style="color:#666; font-size:12px;">${op.location.city}, ${op.location.state}</span><br>
<span style="color:${op.status === 'available' ? '#16a34a' : '#d97706'}; font-size:12px; font-weight:600;">${op.status === 'available' ? '● Available' : '● On a Job'}</span><br>
<span style="font-size:12px;">⭐ ${op.rating} · ${op.totalJobs} jobs</span><br><br>
<button onclick="showOperatorDetail('${op.id}')" style="background:#f59e0b; color:#0f172a; font-weight:700; padding:6px 16px; border-radius:6px; border:none; cursor:pointer; width:100%;">
View Profile
</button>
</div>
`);
markers[op.id] = marker;
});
}
function renderOperatorList(operators) {
const container = document.getElementById('operator-list');
document.getElementById('operator-count').textContent = `${operators.length} operator${operators.length !== 1 ? 's' : ''} found`;
if (operators.length === 0) {
container.innerHTML = `<div class="bg-white rounded-xl p-8 text-center text-slate-500">No operators match your filters.</div>`;
return;
}
container.innerHTML = operators.map(op => `
<div class="bg-white rounded-xl shadow-md p-5 hover:shadow-lg transition-shadow cursor-pointer" onclick="showOperatorDetail('${op.id}')">
<div class="flex items-start justify-between mb-2">
<h3 class="font-bold text-slate-900">${op.name}</h3>
<span class="flex-shrink-0 w-3 h-3 rounded-full ${op.status === 'available' ? 'bg-green-500' : 'bg-amber-500'} mt-1.5"></span>
</div>
<p class="text-sm text-slate-500 mb-2">${op.location.city}, ${op.location.state}</p>
<div class="flex items-center gap-3 text-sm mb-3">
<span class="text-amber-500">⭐ ${op.rating}</span>
<span class="text-slate-400">·</span>
<span class="text-slate-600">${op.totalJobs} jobs</span>
<span class="text-slate-400">·</span>
<span class="text-slate-600">${op.experience}</span>
</div>
<div class="flex flex-wrap gap-1.5">
${op.certifications.map(c => `<span class="bg-slate-100 text-slate-600 text-xs font-medium px-2 py-0.5 rounded">${c}</span>`).join('')}
</div>
</div>
`).join('');
}
function showOperatorDetail(id) {
const op = MOCK_ESCORT_OPERATORS.find(o => o.id === id);
if (!op) return;
// Center map on operator
locatorMap.setView([op.location.lat, op.location.lng], 7);
if (markers[id]) markers[id].openPopup();
document.getElementById('op-detail-name').textContent = op.name;
document.getElementById('op-detail-content').innerHTML = `
<div class="grid md:grid-cols-2 gap-8">
<div>
<div class="flex items-center gap-3 mb-4">
<span class="inline-block w-4 h-4 rounded-full ${op.status === 'available' ? 'bg-green-500' : 'bg-amber-500'}"></span>
<span class="font-semibold ${op.status === 'available' ? 'text-green-700' : 'text-amber-700'}">${op.status === 'available' ? 'Available for Jobs' : 'Currently on a Job'}</span>
</div>
<div class="space-y-3">
<div class="bg-slate-50 px-4 py-3 rounded-lg">
<span class="text-sm text-slate-500 block">Location</span>
<span class="font-semibold text-slate-900">${op.location.city}, ${op.location.state}</span>
</div>
<div class="bg-slate-50 px-4 py-3 rounded-lg">
<span class="text-sm text-slate-500 block">Experience</span>
<span class="font-semibold text-slate-900">${op.experience} · ${op.totalJobs} completed jobs</span>
</div>
<div class="bg-slate-50 px-4 py-3 rounded-lg">
<span class="text-sm text-slate-500 block">Rating</span>
<span class="font-semibold text-slate-900">⭐ ${op.rating} / 5.0</span>
</div>
<div class="bg-slate-50 px-4 py-3 rounded-lg">
<span class="text-sm text-slate-500 block">Vehicle</span>
<span class="font-semibold text-slate-900">${op.vehicleType}</span>
</div>
</div>
</div>
<div>
<div class="mb-4">
<h3 class="font-bold text-slate-900 mb-2">Certified In</h3>
<div class="flex flex-wrap gap-2">
${op.certifications.map(c => `<span class="bg-amber-100 text-amber-800 text-sm font-semibold px-3 py-1 rounded-full">${c}</span>`).join('')}
</div>
</div>
<div class="mb-4">
<h3 class="font-bold text-slate-900 mb-2">About</h3>
<p class="text-slate-600">${op.bio}</p>
</div>
<div class="bg-slate-50 rounded-xl p-4 space-y-2">
<h3 class="font-bold text-slate-900 mb-2">Contact</h3>
<p class="text-sm"><span class="text-slate-500">Email:</span> <a href="mailto:${op.contact}" class="text-amber-600 hover:text-amber-700 font-medium">${op.contact}</a></p>
<p class="text-sm"><span class="text-slate-500">Phone:</span> <a href="tel:${op.phone}" class="text-amber-600 hover:text-amber-700 font-medium">${op.phone}</a></p>
</div>
${op.status === 'available' ? `
<a href="order.html" class="block mt-4 bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold py-3 rounded-lg transition-colors text-center">
Request This Operator
</a>
` : `
<div class="mt-4 bg-slate-100 text-slate-500 font-medium py-3 rounded-lg text-center text-sm">
Currently unavailable — check back later
</div>
`}
</div>
</div>
`;
const detailEl = document.getElementById('operator-detail');
detailEl.classList.remove('hidden');
detailEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function filterOperators() {
const search = document.getElementById('op-search').value.toLowerCase();
const statusFilter = document.getElementById('op-status').value;
const certFilter = document.getElementById('op-cert').value;
let filtered = MOCK_ESCORT_OPERATORS.filter(op => {
if (statusFilter !== 'all' && op.status !== statusFilter) return false;
if (certFilter !== 'all' && !op.certifications.includes(certFilter)) return false;
if (search) {
const searchText = `${op.name} ${op.location.city} ${op.location.state} ${op.certifications.join(' ')} ${op.bio}`.toLowerCase();
if (!searchText.includes(search)) return false;
}
return true;
});
renderOperatorList(filtered);
addOperatorMarkers(filtered);
}
// Initial render
filterOperators();
})();
</script>
</body>
</html>