Add Node.js/Express backend with PostgreSQL and wire frontend to API
- Server: Express.js with 13 API route files (auth, regulations, contacts, calendar, truck stops, bridges, weigh stations, alerts, load board, escort locator, orders, documents, contributions) - Database: PostgreSQL with Prisma ORM, 15 models covering all modules - Auth: JWT + bcrypt with role-based access control (driver/carrier/escort/admin) - Geospatial: Haversine distance filtering on truck stops, bridges, escorts - Seed script: Imports all existing mock data (51 states, contacts, equipment, truck stops, bridges, weigh stations, alerts, seasonal restrictions) - Frontend: All 10 data-driven pages now fetch from /api instead of mock-data.js - API client (api.js): Compatibility layer that transforms API responses to match existing frontend rendering code, minimizing page-level changes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
101
contacts.html
101
contacts.html
@@ -42,67 +42,70 @@
|
||||
|
||||
<div id="main-footer"></div>
|
||||
|
||||
<script src="mock-data.js"></script>
|
||||
<script src="mock-data-extended.js"></script>
|
||||
<script src="api.js"></script>
|
||||
<script src="nav.js"></script>
|
||||
<script>
|
||||
renderNav('contacts');
|
||||
renderBanner();
|
||||
renderFooter();
|
||||
|
||||
const grid = document.getElementById('contacts-grid');
|
||||
const noResults = document.getElementById('no-results');
|
||||
const searchInput = document.getElementById('state-search');
|
||||
(async () => {
|
||||
const MOCK_STATE_CONTACTS = await PilotEdge.getContacts();
|
||||
|
||||
function buildCard(abbr, c) {
|
||||
return `
|
||||
<div class="bg-white rounded-2xl shadow-lg p-6 flex flex-col" data-state="${c.name.toLowerCase()}">
|
||||
<h3 class="text-lg font-bold text-slate-900 mb-1">${c.name} <span class="text-slate-400 font-medium text-sm">(${abbr})</span></h3>
|
||||
<div class="mt-3 space-y-2 text-sm text-slate-700 flex-1">
|
||||
<p class="flex items-start gap-2">
|
||||
<span class="shrink-0">📞</span>
|
||||
<span><span class="font-medium">Permit Office:</span> <a href="tel:${c.permit.replace(/[^+\d]/g, '')}" class="text-amber-600 hover:text-amber-700 font-semibold">${c.permit}</a></span>
|
||||
</p>
|
||||
<p class="flex items-start gap-2">
|
||||
<span class="shrink-0">🚔</span>
|
||||
<span><span class="font-medium">State Police:</span> <a href="tel:${c.police.replace(/[^+\d]/g, '')}" class="text-amber-600 hover:text-amber-700 font-semibold">${c.police}</a></span>
|
||||
</p>
|
||||
<p class="flex items-start gap-2">
|
||||
<span class="shrink-0">✉️</span>
|
||||
<span><span class="font-medium">Email:</span> <a href="mailto:${c.email}" class="text-amber-600 hover:text-amber-700 font-semibold break-all">${c.email}</a></span>
|
||||
</p>
|
||||
<p class="flex items-start gap-2">
|
||||
<span class="shrink-0">🕐</span>
|
||||
<span><span class="font-medium">Hours:</span> ${c.hours}</span>
|
||||
</p>
|
||||
</div>
|
||||
<a href="${c.portal}" target="_blank" rel="noopener noreferrer"
|
||||
class="mt-4 inline-flex items-center justify-center gap-1 bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold text-sm px-4 py-2 rounded-lg transition-colors shadow-md">
|
||||
Visit Permit Portal <span class="text-xs">↗</span>
|
||||
</a>
|
||||
</div>`;
|
||||
}
|
||||
const grid = document.getElementById('contacts-grid');
|
||||
const noResults = document.getElementById('no-results');
|
||||
const searchInput = document.getElementById('state-search');
|
||||
|
||||
function renderCards(filter) {
|
||||
const term = (filter || '').toLowerCase();
|
||||
let html = '';
|
||||
let count = 0;
|
||||
function buildCard(abbr, c) {
|
||||
return `
|
||||
<div class="bg-white rounded-2xl shadow-lg p-6 flex flex-col" data-state="${c.name.toLowerCase()}">
|
||||
<h3 class="text-lg font-bold text-slate-900 mb-1">${c.name} <span class="text-slate-400 font-medium text-sm">(${abbr})</span></h3>
|
||||
<div class="mt-3 space-y-2 text-sm text-slate-700 flex-1">
|
||||
<p class="flex items-start gap-2">
|
||||
<span class="shrink-0">📞</span>
|
||||
<span><span class="font-medium">Permit Office:</span> <a href="tel:${c.permit.replace(/[^+\d]/g, '')}" class="text-amber-600 hover:text-amber-700 font-semibold">${c.permit}</a></span>
|
||||
</p>
|
||||
<p class="flex items-start gap-2">
|
||||
<span class="shrink-0">🚔</span>
|
||||
<span><span class="font-medium">State Police:</span> <a href="tel:${c.police.replace(/[^+\d]/g, '')}" class="text-amber-600 hover:text-amber-700 font-semibold">${c.police}</a></span>
|
||||
</p>
|
||||
<p class="flex items-start gap-2">
|
||||
<span class="shrink-0">✉️</span>
|
||||
<span><span class="font-medium">Email:</span> <a href="mailto:${c.email}" class="text-amber-600 hover:text-amber-700 font-semibold break-all">${c.email}</a></span>
|
||||
</p>
|
||||
<p class="flex items-start gap-2">
|
||||
<span class="shrink-0">🕐</span>
|
||||
<span><span class="font-medium">Hours:</span> ${c.hours}</span>
|
||||
</p>
|
||||
</div>
|
||||
<a href="${c.portal}" target="_blank" rel="noopener noreferrer"
|
||||
class="mt-4 inline-flex items-center justify-center gap-1 bg-amber-500 hover:bg-amber-600 text-slate-900 font-bold text-sm px-4 py-2 rounded-lg transition-colors shadow-md">
|
||||
Visit Permit Portal <span class="text-xs">↗</span>
|
||||
</a>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
Object.keys(MOCK_STATE_CONTACTS).forEach(abbr => {
|
||||
const c = MOCK_STATE_CONTACTS[abbr];
|
||||
if (!term || c.name.toLowerCase().includes(term) || abbr.toLowerCase().includes(term)) {
|
||||
html += buildCard(abbr, c);
|
||||
count++;
|
||||
}
|
||||
});
|
||||
function renderCards(filter) {
|
||||
const term = (filter || '').toLowerCase();
|
||||
let html = '';
|
||||
let count = 0;
|
||||
|
||||
grid.innerHTML = html;
|
||||
noResults.classList.toggle('hidden', count > 0);
|
||||
}
|
||||
Object.keys(MOCK_STATE_CONTACTS).forEach(abbr => {
|
||||
const c = MOCK_STATE_CONTACTS[abbr];
|
||||
if (!term || c.name.toLowerCase().includes(term) || abbr.toLowerCase().includes(term)) {
|
||||
html += buildCard(abbr, c);
|
||||
count++;
|
||||
}
|
||||
});
|
||||
|
||||
searchInput.addEventListener('input', () => renderCards(searchInput.value));
|
||||
grid.innerHTML = html;
|
||||
noResults.classList.toggle('hidden', count > 0);
|
||||
}
|
||||
|
||||
renderCards();
|
||||
searchInput.addEventListener('input', () => renderCards(searchInput.value));
|
||||
|
||||
renderCards();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user