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

302
public/pages/loadboard.html Normal file
View 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">&times;</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>