Files
PilotEdge/public/pages/calendar.html
Daniel Kovalevich 93efb907ff 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>
2026-03-30 15:52:56 -04:00

295 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
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>Seasonal Restriction Calendar | PilotEdge</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
.gantt-bar { transition: opacity 0.2s; }
.gantt-bar:hover { opacity: 0.85; }
</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">Seasonal Restriction Calendar</h1>
<p class="text-lg text-gray-400 max-w-3xl">Plan around seasonal closures, weight restrictions, and travel blackouts that affect oversize load movement across the country.</p>
</div>
</section>
<!-- Filters -->
<section class="max-w-7xl mx-auto px-4 pt-8 w-full">
<div class="bg-white rounded-2xl shadow-lg p-6">
<div class="flex flex-wrap items-end gap-4">
<div class="flex-1 min-w-[180px]">
<label class="block text-sm font-semibold text-slate-700 mb-1">Restriction Type</label>
<select id="filter-type" onchange="applyFilters()" class="w-full border border-slate-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
<option value="">All Types</option>
<option value="spring_weight">🔵 Spring Weight</option>
<option value="winter_closure">🟣 Winter Closure</option>
<option value="wind_season">🟡 Wind Season</option>
<option value="holiday_blackout">🔴 Holiday Blackout</option>
<option value="harvest_season">🟢 Harvest Season</option>
</select>
</div>
<div class="flex-1 min-w-[180px]">
<label class="block text-sm font-semibold text-slate-700 mb-1">State</label>
<select id="filter-state" onchange="applyFilters()" class="w-full border border-slate-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
<option value="">All States</option>
</select>
</div>
<button onclick="resetFilters()" class="bg-slate-100 hover:bg-slate-200 text-slate-700 font-semibold px-4 py-2 rounded-lg transition-colors text-sm">
Reset Filters
</button>
</div>
</div>
</section>
<!-- Calendar Gantt Chart -->
<section class="max-w-7xl mx-auto px-4 py-8 w-full">
<div class="bg-white rounded-2xl shadow-lg p-6 overflow-x-auto">
<h2 class="text-xl font-bold text-slate-900 mb-1">12-Month Timeline</h2>
<p class="text-sm text-slate-500 mb-6">Colored bars show when each restriction is active. Hover for details.</p>
<div id="gantt-chart"></div>
<div id="gantt-empty" class="hidden text-center py-12 text-slate-400">
<p class="text-lg font-medium">No restrictions match the current filters.</p>
</div>
</div>
</section>
<!-- Legend -->
<section class="max-w-7xl mx-auto px-4 pb-4 w-full">
<div class="flex flex-wrap gap-4 justify-center text-sm">
<span class="flex items-center gap-1.5"><span class="w-4 h-3 rounded" style="background:#3b82f6"></span> Spring Weight</span>
<span class="flex items-center gap-1.5"><span class="w-4 h-3 rounded" style="background:#6366f1"></span> Winter Closure</span>
<span class="flex items-center gap-1.5"><span class="w-4 h-3 rounded" style="background:#f59e0b"></span> Wind Season</span>
<span class="flex items-center gap-1.5"><span class="w-4 h-3 rounded" style="background:#ef4444"></span> Holiday Blackout</span>
<span class="flex items-center gap-1.5"><span class="w-4 h-3 rounded" style="background:#22c55e"></span> Harvest Season</span>
</div>
</section>
<!-- Restriction Detail Cards -->
<section class="max-w-7xl mx-auto px-4 pb-8 w-full">
<h2 class="text-xl font-bold text-slate-900 mb-4">Restriction Details</h2>
<div id="restriction-cards" class="grid md:grid-cols-2 gap-6"></div>
<div id="cards-empty" class="hidden text-center py-12 text-slate-400">
<p class="text-lg font-medium">No restrictions match the current filters.</p>
</div>
</section>
<div id="main-footer"></div>
<script src="/js/api.js"></script>
<script src="/js/nav.js"></script>
<script>
renderNav('calendar');
renderBanner();
renderFooter();
(async () => {
const MOCK_SEASONAL_RESTRICTIONS = await PilotEdge.getSeasonalRestrictions();
// ---- Constants ----
const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
const TYPE_BADGES = {
spring_weight: { label:'Spring Weight', bg:'bg-blue-100', text:'text-blue-800' },
winter_closure: { label:'Winter Closure', bg:'bg-indigo-100', text:'text-indigo-800' },
wind_season: { label:'Wind Season', bg:'bg-amber-100', text:'text-amber-800' },
holiday_blackout:{ label:'Holiday Blackout', bg:'bg-red-100', text:'text-red-800' },
harvest_season: { label:'Harvest Season', bg:'bg-green-100', text:'text-green-800' }
};
// ---- Populate state filter ----
const states = [...new Set(MOCK_SEASONAL_RESTRICTIONS.map(r => r.stateName))].sort();
const stateSelect = document.getElementById('filter-state');
states.forEach(s => {
const r = MOCK_SEASONAL_RESTRICTIONS.find(x => x.stateName === s);
stateSelect.add(new Option(`${s} (${r.state})`, r.state));
});
// ---- Filtering ----
function getFiltered() {
const type = document.getElementById('filter-type').value;
const state = document.getElementById('filter-state').value;
return MOCK_SEASONAL_RESTRICTIONS.filter(r =>
(!type || r.type === type) && (!state || r.state === state)
);
}
function applyFilters() {
renderGantt(getFiltered());
renderCards(getFiltered());
}
function resetFilters() {
document.getElementById('filter-type').value = '';
document.getElementById('filter-state').value = '';
applyFilters();
}
// ---- Month span helper (handles wrap-around) ----
function getActiveMonths(r) {
const months = [];
if (r.startMonth <= r.endMonth) {
for (let m = r.startMonth; m <= r.endMonth; m++) months.push(m);
} else {
for (let m = r.startMonth; m <= 12; m++) months.push(m);
for (let m = 1; m <= r.endMonth; m++) months.push(m);
}
return months;
}
// ---- Gantt Chart ----
function renderGantt(restrictions) {
const chart = document.getElementById('gantt-chart');
const empty = document.getElementById('gantt-empty');
if (restrictions.length === 0) {
chart.classList.add('hidden');
empty.classList.remove('hidden');
return;
}
chart.classList.remove('hidden');
empty.classList.add('hidden');
// Build the grid: header row + one row per restriction
let html = '<div style="min-width:800px;">';
// Month header row
html += '<div class="grid" style="grid-template-columns: 220px repeat(12, 1fr); gap: 0;">';
html += '<div class="text-xs font-semibold text-slate-500 py-2 pr-3 text-right">Restriction</div>';
MONTHS.forEach((m, i) => {
const now = new Date();
const isCurrentMonth = (now.getMonth() === i);
html += `<div class="text-xs font-semibold text-center py-2 ${isCurrentMonth ? 'text-amber-600 bg-amber-50 rounded-t' : 'text-slate-500'}">${m}</div>`;
});
html += '</div>';
// Restriction rows
restrictions.forEach(r => {
const active = getActiveMonths(r);
const badge = TYPE_BADGES[r.type] || { label: r.type, bg:'bg-slate-100', text:'text-slate-700' };
html += '<div class="grid border-t border-slate-100" style="grid-template-columns: 220px repeat(12, 1fr); gap: 0;">';
// Label cell
html += `<div class="flex items-center py-2 pr-3 gap-2 justify-end">
<span class="text-xs font-medium text-slate-700 text-right truncate" title="${r.title}${r.stateName}">${r.stateName}</span>
<span class="flex-shrink-0 text-[10px] font-bold px-1.5 py-0.5 rounded ${badge.bg} ${badge.text}">${r.state}</span>
</div>`;
// Month cells
for (let m = 1; m <= 12; m++) {
const isActive = active.includes(m);
const now = new Date();
const isCurrentMonth = (now.getMonth() + 1 === m);
if (isActive) {
const isStart = m === r.startMonth;
const isEnd = m === r.endMonth;
const roundL = isStart ? 'rounded-l-full' : '';
const roundR = isEnd ? 'rounded-r-full' : '';
html += `<div class="flex items-center py-2 px-0.5 ${isCurrentMonth ? 'bg-amber-50' : ''}">
<div class="gantt-bar w-full h-6 ${roundL} ${roundR} flex items-center justify-center cursor-default"
style="background:${r.color}; opacity:0.8;"
title="${r.title}\n${MONTHS[r.startMonth-1]} ${r.startDay} ${MONTHS[r.endMonth-1]} ${r.endDay}">
${isStart ? `<span class="text-[9px] text-white font-bold drop-shadow pl-1">${r.startDay}</span>` : ''}
${isEnd ? `<span class="text-[9px] text-white font-bold drop-shadow pr-1">${r.endDay}</span>` : ''}
</div>
</div>`;
} else {
html += `<div class="flex items-center py-2 px-0.5 ${isCurrentMonth ? 'bg-amber-50' : ''}">
<div class="w-full h-6 bg-slate-50 ${m === 1 ? 'rounded-l' : ''} ${m === 12 ? 'rounded-r' : ''}"></div>
</div>`;
}
}
html += '</div>';
});
html += '</div>';
chart.innerHTML = html;
}
// ---- Detail Cards ----
function renderCards(restrictions) {
const container = document.getElementById('restriction-cards');
const empty = document.getElementById('cards-empty');
if (restrictions.length === 0) {
container.classList.add('hidden');
empty.classList.remove('hidden');
return;
}
container.classList.remove('hidden');
empty.classList.add('hidden');
container.innerHTML = restrictions.map(r => {
const badge = TYPE_BADGES[r.type] || { label: r.type, bg:'bg-slate-100', text:'text-slate-700' };
const active = getActiveMonths(r);
const monthRange = `${MONTHS[r.startMonth-1]} ${r.startDay} ${MONTHS[r.endMonth-1]} ${r.endDay}`;
// Month pills
const monthPills = MONTHS.map((m, i) => {
const isActive = active.includes(i + 1);
return `<span class="inline-block w-7 text-center text-[10px] font-semibold rounded py-0.5 ${isActive ? 'text-white' : 'text-slate-400 bg-slate-100'}" ${isActive ? `style="background:${r.color}"` : ''}>${m.charAt(0)}${m.charAt(1)}</span>`;
}).join('');
return `
<div class="bg-white rounded-2xl shadow-lg border border-slate-100 overflow-hidden">
<div class="px-6 pt-5 pb-4">
<div class="flex items-start justify-between gap-3 mb-3">
<div>
<h3 class="font-bold text-slate-900 text-lg leading-tight">${r.title}</h3>
<p class="text-sm text-slate-500 mt-0.5">${r.stateName} (${r.state})</p>
</div>
<span class="flex-shrink-0 text-xs font-bold px-2.5 py-1 rounded-full ${badge.bg} ${badge.text}">${badge.label}</span>
</div>
<!-- Active period -->
<div class="mb-4">
<div class="flex items-center gap-2 text-sm text-slate-700 font-medium mb-2">
<svg class="w-4 h-4 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
${monthRange}
</div>
<div class="flex gap-1 flex-wrap">${monthPills}</div>
</div>
<!-- Routes -->
<div class="mb-3">
<p class="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-1">Affected Routes</p>
<p class="text-sm text-slate-700">${r.routes}</p>
</div>
<!-- Description -->
<div class="mb-3">
<p class="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-1">Description</p>
<p class="text-sm text-slate-600">${r.description}</p>
</div>
<!-- Impact -->
<div class="bg-amber-50 border border-amber-200 rounded-xl px-4 py-3">
<p class="text-xs font-semibold text-amber-700 uppercase tracking-wider mb-1">⚠️ Impact on Oversize Loads</p>
<p class="text-sm text-amber-900">${r.impact}</p>
</div>
</div>
</div>
`;
}).join('');
}
// ---- Initial render ----
applyFilters();
})();
</script>
</body>
</html>