Files
FreightCalculatorPro/index.html
T
Devon Bontrager 25973cabbc Add Lucide icons, web server, and settings page cleanup
- Replace emoji icons with Lucide SVG icons (moon, sun, settings, x, upload, rotate-ccw, trash-2, save)
- Add Node.js dev server on port 3010 (server.js + npm run serve)
- Redesign settings modal: tabbed layout, styled form fields, Save/Cancel footer
- Fix dark mode: btn-danger hover now uses rgba instead of hardcoded light pink
- Fix stale pendingBranding: history item click now calls revertAndClose()
- Collapse branding header when no name/logo is set
- Move density row inline style to .density-info CSS class
- Normalise all innerText to textContent

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 15:57:19 -04:00

434 lines
25 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">
<title>Freight Calculator Pro</title>
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
<style>
:root {
--primary: #2563eb; --primary-hover: #1d4ed8; --bg-color: #f1f5f9;
--card-bg: #ffffff; --text-main: #1e293b; --text-muted: #64748b;
--border-radius: 12px; --shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
--input-bg: #ffffff; --input-border: #e2e8f0;
--input-focus-ring: rgba(37, 99, 235, 0.1); --border-color: #e2e8f0;
--table-header-bg: #f8fafc; --table-hover: #f1f5f9;
--highlight-bg: #dbeafe; --highlight-text: #1e3a8a;
--modal-bg: rgba(0, 0, 0, 0.5);
}
body.dark-mode {
--primary: #60a5fa; --primary-hover: #3b82f6; --bg-color: #0f172a;
--card-bg: #1e293b; --text-main: #f1f5f9; --text-muted: #94a3b8;
--shadow: 0 4px 6px -1px rgb(0 0 0 / 0.5); --input-bg: #0f172a;
--input-border: #334155; --input-focus-ring: rgba(96, 165, 250, 0.2);
--border-color: #334155; --table-header-bg: #0f172a; --table-hover: #334155;
--highlight-bg: #1e3a8a; --highlight-text: #ffffff;
}
* { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Segoe UI', sans-serif; transition: background-color 0.3s, color 0.3s; }
body { background-color: var(--bg-color); color: var(--text-main); display: flex; justify-content: center; min-height: 100vh; padding: 20px; }
.container { width: 100%; max-width: 900px; display: flex; flex-direction: column; }
/* Layout */
.branding-header { display: flex; align-items: center; gap: 15px; margin-bottom: 10px; }
#business-logo { max-height: 60px; max-width: 200px; object-fit: contain; display: none; }
#business-name-display { font-size: 1.5rem; font-weight: 700; color: var(--primary); display: none; }
.main-title { text-align: center; margin-bottom: 24px; }
h1 { font-size: 1.8rem; margin-bottom: 4px; }
.content-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; margin-bottom: 24px; }
/* Controls */
.top-controls { position: absolute; top: 20px; right: 20px; display: flex; gap: 12px; }
.icon-btn { background: var(--card-bg); color: var(--text-main); border: 1px solid var(--input-border); width: 40px; height: 40px; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; box-shadow: var(--shadow); }
.icon-btn svg { width: 18px; height: 18px; stroke: var(--text-main); }
/* Cards & Inputs */
.card { background: var(--card-bg); border-radius: var(--border-radius); box-shadow: var(--shadow); padding: 32px; display: flex; flex-direction: column; gap: 20px; }
.form-group { display: flex; flex-direction: column; gap: 8px; }
label { font-weight: 600; font-size: 0.9rem; color: var(--text-muted); }
.input-wrapper { position: relative; display: flex; align-items: center; }
input[type="number"], input[type="text"] { width: 100%; padding: 12px; border: 2px solid var(--input-border); border-radius: 8px; font-size: 1rem; background: var(--input-bg); color: var(--text-main); }
input:focus { border-color: var(--primary); outline: none; box-shadow: 0 0 0 3px var(--input-focus-ring); }
.unit { position: absolute; right: 16px; color: var(--text-muted); font-size: 0.85rem; pointer-events: none; }
.dimensions-row { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; }
button.calculate-btn { background: var(--primary); color: white; border: none; padding: 16px; border-radius: 8px; font-weight: 600; cursor: pointer; margin-top: 10px; }
button.calculate-btn:hover { background: var(--primary-hover); }
/* Results */
.results-container { display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; height: 100%; opacity: 0.5; }
.results-container.active { opacity: 1; }
.result-box { background: var(--bg-color); border: 2px dashed var(--border-color); border-radius: var(--border-radius); padding: 40px; width: 100%; }
.result-value { font-size: 4rem; font-weight: 800; color: var(--primary); margin: 16px 0; }
.result-label { font-size: 1.1rem; font-weight: 600; color: var(--text-muted); text-transform: uppercase; }
/* Table */
.reference-section { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 12px; border-bottom: 1px solid var(--border-color); text-align: left; }
th { background: var(--table-header-bg); color: var(--text-muted); }
.highlight-row { background: var(--highlight-bg) !important; font-weight: bold; color: var(--highlight-text); }
/* Modal */
.modal-overlay { position: fixed; inset: 0; background: var(--modal-bg); z-index: 2000; display: none; justify-content: center; align-items: center; }
.modal-overlay.open { display: flex; }
.modal-content { background: var(--card-bg); width: 90%; max-width: 480px; border-radius: var(--border-radius); box-shadow: 0 20px 40px rgba(0,0,0,0.25); display: flex; flex-direction: column; max-height: 85vh; }
.modal-header { padding: 20px 24px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; }
.modal-header h2 { font-size: 1.1rem; font-weight: 700; }
.modal-header .icon-btn { width: 32px; height: 32px; box-shadow: none; }
.modal-header .icon-btn svg { width: 16px; height: 16px; }
.modal-tabs { display: flex; border-bottom: 1px solid var(--border-color); padding: 0 24px; flex-shrink: 0; }
.tab-btn { background: none; border: none; padding: 12px 16px; cursor: pointer; color: var(--text-muted); border-bottom: 3px solid transparent; font-size: 0.9rem; font-weight: 500; margin-bottom: -1px; }
.tab-btn.active { color: var(--primary); border-bottom-color: var(--primary); font-weight: 600; }
.modal-body { padding: 24px; overflow-y: auto; flex: 1; }
.tab-content { display: none; }
.tab-content.active { display: block; }
/* Settings form */
.settings-group { margin-bottom: 22px; }
.settings-group:last-child { margin-bottom: 0; }
.settings-group > label {
display: block; font-size: 0.75rem; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.06em;
color: var(--text-muted); margin-bottom: 8px;
}
.settings-group input[type="text"] { padding: 10px 14px; font-size: 0.95rem; border-width: 1.5px; }
.color-row { display: flex; align-items: center; gap: 12px; }
input[type="color"] { width: 44px; height: 44px; padding: 3px; border: 1.5px solid var(--input-border); border-radius: 8px; background: var(--input-bg); cursor: pointer; }
#color-hex-label { font-family: 'Courier New', monospace; font-size: 0.9rem; color: var(--text-muted); }
.logo-upload-area { display: flex; flex-direction: column; gap: 10px; align-items: flex-start; }
#logo-preview { max-height: 56px; max-width: 180px; object-fit: contain; display: none; border-radius: 6px; border: 1px solid var(--border-color); padding: 4px; }
.file-upload-btn { display: inline-flex; align-items: center; gap: 6px; padding: 8px 14px; border: 1.5px solid var(--input-border); border-radius: 7px; cursor: pointer; font-size: 0.85rem; font-weight: 500; color: var(--text-muted); background: var(--input-bg); transition: border-color 0.2s, color 0.2s; }
.file-upload-btn:hover { border-color: var(--primary); color: var(--primary); }
.file-upload-btn svg { width: 14px; height: 14px; stroke: currentColor; }
#setting-logo { display: none; }
.danger-zone { margin-top: 28px; padding-top: 20px; border-top: 1px solid var(--border-color); }
/* History tab */
.history-controls { display: flex; justify-content: flex-end; margin-bottom: 14px; }
.history-list { list-style: none; overflow-y: auto; border: 1px solid var(--border-color); border-radius: 8px; max-height: 280px; }
.history-item { padding: 12px 16px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; cursor: pointer; font-size: 0.9rem; }
.history-item:last-child { border-bottom: none; }
.history-item:hover { background: var(--table-hover); }
.history-class { font-weight: 700; color: var(--primary); }
.density-info { margin-bottom: 20px; color: var(--text-muted); }
/* Buttons */
.btn-primary { background: var(--primary); color: white; border: none; padding: 10px 20px; border-radius: 8px; font-weight: 600; cursor: pointer; font-size: 0.9rem; display: inline-flex; align-items: center; gap: 7px; }
.btn-primary:hover { background: var(--primary-hover); }
.btn-primary svg { width: 15px; height: 15px; stroke: currentColor; }
.btn-secondary { background: transparent; border: 1px solid var(--border-color); color: var(--text-muted); padding: 10px 20px; border-radius: 8px; cursor: pointer; font-size: 0.9rem; display: inline-flex; align-items: center; gap: 7px; }
.btn-secondary:hover { background: var(--table-hover); }
.btn-secondary svg { width: 15px; height: 15px; stroke: currentColor; }
.btn-danger { color: #ef4444; border-color: #fca5a5; }
.btn-danger:hover { background: rgba(239, 68, 68, 0.1); }
.modal-footer { padding: 16px 24px; border-top: 1px solid var(--border-color); display: flex; justify-content: flex-end; gap: 10px; flex-shrink: 0; }
</style>
</head>
<body>
<div class="top-controls">
<button id="theme-toggle" class="icon-btn"><i data-lucide="moon"></i></button>
<button id="settings-btn" class="icon-btn"><i data-lucide="settings"></i></button>
</div>
<div class="container">
<div class="branding-header">
<img id="business-logo" src="" alt="Logo">
<div id="business-name-display"></div>
</div>
<div class="main-title">
<h1>Freight Class Calculator</h1>
</div>
<div class="content-grid">
<div class="card">
<div class="form-group">
<label>Dimensions</label>
<div class="dimensions-row">
<div class="input-wrapper"><input type="number" id="length" placeholder="L" min="0" step="0.1"><span class="unit">in</span></div>
<div class="input-wrapper"><input type="number" id="width" placeholder="W" min="0" step="0.1"><span class="unit">in</span></div>
<div class="input-wrapper"><input type="number" id="height" placeholder="H" min="0" step="0.1"><span class="unit">in</span></div>
</div>
</div>
<div class="form-group">
<label>Total Weight</label>
<div class="input-wrapper"><input type="number" id="weight" placeholder="0" min="0" step="0.1"><span class="unit">lbs</span></div>
</div>
<button class="calculate-btn" onclick="calculateFreightClass()">Calculate Class</button>
</div>
<div class="card results-container" id="results-area">
<div class="result-box">
<div class="result-label">Freight Class</div>
<div class="result-value" id="class-result">--</div>
<div class="density-info">Density: <span id="density-result">0.00</span> lb/ft³</div>
</div>
</div>
</div>
<div class="card reference-section">
<h3>Density Reference</h3>
<table><thead><tr><th>Class</th><th>Density</th><th>Items</th></tr></thead><tbody id="ref-table-body"></tbody></table>
</div>
</div>
<!-- Settings Modal -->
<div id="settings-modal" class="modal-overlay">
<div class="modal-content">
<div class="modal-header">
<h2>Settings</h2>
<button class="icon-btn" id="modal-close-btn"><i data-lucide="x"></i></button>
</div>
<div class="modal-tabs">
<button class="tab-btn active" data-tab="branding" onclick="switchTab('branding', event)">Branding</button>
<button class="tab-btn" data-tab="history" onclick="switchTab('history', event)">History</button>
</div>
<div class="modal-body">
<div id="tab-branding" class="tab-content active">
<div class="settings-group">
<label for="setting-name">Business Name</label>
<input type="text" id="setting-name" placeholder="Your company name">
</div>
<div class="settings-group">
<label>Brand Color</label>
<div class="color-row">
<input type="color" id="setting-color">
<span id="color-hex-label">#2563eb</span>
</div>
</div>
<div class="settings-group">
<label>Logo</label>
<div class="logo-upload-area">
<img id="logo-preview" src="" alt="Logo preview">
<label for="setting-logo" class="file-upload-btn">
<i data-lucide="upload"></i> Choose File
</label>
<input type="file" id="setting-logo" accept="image/*">
</div>
</div>
<div class="danger-zone">
<button class="btn-secondary btn-danger" id="reset-branding-btn">
<i data-lucide="rotate-ccw"></i> Reset Branding
</button>
</div>
</div>
<div id="tab-history" class="tab-content">
<div class="history-controls">
<button class="btn-secondary btn-danger" id="clear-history-btn">
<i data-lucide="trash-2"></i> Clear All
</button>
</div>
<div id="history-list" class="history-list"></div>
</div>
</div>
<div class="modal-footer" id="modal-footer">
<button class="btn-secondary" id="modal-cancel-btn">Cancel</button>
<button class="btn-primary" id="modal-save-btn">
<i data-lucide="save"></i> Save Changes
</button>
</div>
</div>
</div>
<script>
const densityRanges = [
{ min: 50, class: 50, desc: "Heavy machinery" }, { min: 35, class: 55, desc: "Bricks, cement" },
{ min: 30, class: 60, desc: "Car parts" }, { min: 22.5, class: 65, desc: "Car parts, books" },
{ min: 15, class: 70, desc: "Food items" }, { min: 13.5, class: 77.5, desc: "Tires" },
{ min: 12, class: 85, desc: "Crated machinery" }, { min: 10.5, class: 92.5, desc: "Computers" },
{ min: 9, class: 100, desc: "Boat motors" }, { min: 8, class: 110, desc: "TVs" },
{ min: 7, class: 125, desc: "Furniture" }, { min: 6, class: 150, desc: "Sheet metal" },
{ min: 5, class: 175, desc: "Clothing" }, { min: 4, class: 200, desc: "Aircraft parts" },
{ min: 3, class: 250, desc: "Mattresses" }, { min: 2, class: 300, desc: "Cabinets" },
{ min: 1, class: 400, desc: "Deer antlers" }, { min: -1, class: 500, desc: "Gold dust" }
];
let appState = JSON.parse(localStorage.getItem('freightCalcState')) || { theme: 'light', branding: { name:'', color:'#2563eb', logo:'' }, history:[] };
let pendingBranding = null;
function saveState() { localStorage.setItem('freightCalcState', JSON.stringify(appState)); }
// Init
window.onload = () => {
document.body.className = appState.theme === 'dark' ? 'dark-mode' : '';
document.getElementById('theme-toggle').innerHTML = appState.theme === 'dark' ? '<i data-lucide="sun"></i>' : '<i data-lucide="moon"></i>';
document.documentElement.style.setProperty('--primary', appState.branding.color);
applyBrandingToPage();
renderHistory();
densityRanges.forEach(r => {
const row = document.createElement('tr'); row.id = `row-${r.class}`;
row.innerHTML = `<td>Class ${r.class}</td><td>${r.min}+ lbs/ft³</td><td>${r.desc}</td>`;
document.getElementById('ref-table-body').appendChild(row);
});
lucide.createIcons();
};
// Theme
document.getElementById('theme-toggle').onclick = () => {
document.body.classList.toggle('dark-mode');
appState.theme = document.body.classList.contains('dark-mode') ? 'dark' : 'light';
document.getElementById('theme-toggle').innerHTML = appState.theme === 'dark' ? '<i data-lucide="sun"></i>' : '<i data-lucide="moon"></i>';
lucide.createIcons();
saveState();
};
// Apply branding visuals to page (header logo/name)
function applyBrandingToPage() {
const b = appState.branding;
const header = document.querySelector('.branding-header');
const logo = document.getElementById('business-logo');
const nameEl = document.getElementById('business-name-display');
logo.src = b.logo || '';
logo.style.display = b.logo ? 'block' : 'none';
nameEl.textContent = b.name;
nameEl.style.display = b.name ? 'block' : 'none';
header.style.display = (b.logo || b.name) ? 'flex' : 'none';
}
// Populate settings form fields from appState
function populateSettingsForm() {
const b = appState.branding;
document.getElementById('setting-name').value = b.name;
document.getElementById('setting-color').value = b.color;
document.getElementById('color-hex-label').textContent = b.color;
const preview = document.getElementById('logo-preview');
if (b.logo) { preview.src = b.logo; preview.style.display = 'block'; }
else { preview.src = ''; preview.style.display = 'none'; }
}
// Live input handlers — update appState and preview but don't persist
document.getElementById('setting-name').oninput = (e) => {
appState.branding.name = e.target.value;
applyBrandingToPage();
};
document.getElementById('setting-color').oninput = (e) => {
appState.branding.color = e.target.value;
document.getElementById('color-hex-label').textContent = e.target.value;
document.documentElement.style.setProperty('--primary', e.target.value);
};
document.getElementById('setting-logo').onchange = (e) => {
if (!e.target.files[0]) return;
const reader = new FileReader();
reader.onload = evt => {
appState.branding.logo = evt.target.result;
const preview = document.getElementById('logo-preview');
preview.src = evt.target.result;
preview.style.display = 'block';
applyBrandingToPage();
};
reader.readAsDataURL(e.target.files[0]);
};
// Reset branding (staged — requires Save to persist)
document.getElementById('reset-branding-btn').onclick = () => {
appState.branding = { name: '', color: '#2563eb', logo: '' };
document.documentElement.style.setProperty('--primary', '#2563eb');
applyBrandingToPage();
populateSettingsForm();
};
// Clear history (immediate)
document.getElementById('clear-history-btn').onclick = () => {
appState.history = [];
saveState();
renderHistory();
};
// Modal open
document.getElementById('settings-btn').onclick = () => {
pendingBranding = JSON.parse(JSON.stringify(appState.branding));
populateSettingsForm();
switchTab('branding');
document.getElementById('settings-modal').classList.add('open');
lucide.createIcons();
};
// Cancel / close — revert unsaved changes
function revertAndClose() {
if (pendingBranding) {
appState.branding = pendingBranding;
document.documentElement.style.setProperty('--primary', pendingBranding.color);
applyBrandingToPage();
pendingBranding = null;
}
document.getElementById('settings-modal').classList.remove('open');
}
document.getElementById('modal-close-btn').onclick = revertAndClose;
document.getElementById('modal-cancel-btn').onclick = revertAndClose;
// Save — persist to localStorage
document.getElementById('modal-save-btn').onclick = () => {
saveState();
pendingBranding = null;
document.getElementById('settings-modal').classList.remove('open');
};
// Tabs
function switchTab(t, e) {
document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active'));
document.getElementById('tab-' + t).classList.add('active');
const btn = e ? e.target : document.querySelector(`.tab-btn[data-tab="${t}"]`);
if (btn) btn.classList.add('active');
document.getElementById('modal-footer').style.display = t === 'branding' ? 'flex' : 'none';
}
// History
function renderHistory() {
const list = document.getElementById('history-list');
list.innerHTML = '';
if (appState.history.length === 0) {
list.innerHTML = '<div style="padding:16px; text-align:center; color:var(--text-muted); font-size:0.9rem;">No calculations yet</div>';
return;
}
appState.history.forEach(item => {
const div = document.createElement('div'); div.className = 'history-item';
div.innerHTML = `<span>${item.L}"×${item.W}"×${item.H}" · ${item.Weight} lbs</span><span class="history-class">Class ${item.Class}</span>`;
div.onclick = () => {
document.getElementById('length').value = item.L;
document.getElementById('width').value = item.W;
document.getElementById('height').value = item.H;
document.getElementById('weight').value = item.Weight;
document.getElementById('density-result').textContent = item.Density;
document.getElementById('class-result').textContent = item.Class;
document.getElementById('results-area').classList.add('active');
document.querySelectorAll('tbody tr').forEach(r => r.classList.remove('highlight-row'));
document.getElementById(`row-${item.Class}`).classList.add('highlight-row');
revertAndClose();
};
list.appendChild(div);
});
}
// Calc
function calculateFreightClass() {
const L = parseFloat(document.getElementById('length').value);
const W = parseFloat(document.getElementById('width').value);
const H = parseFloat(document.getElementById('height').value);
const WT = parseFloat(document.getElementById('weight').value);
if (!L || !W || !H || !WT) return;
const density = WT / ((L * W * H) / 1728);
let cls = 500;
for (let r of densityRanges) if (density >= r.min) { cls = r.class; break; }
appState.history.unshift({ L, W, H, Weight: WT, Density: density.toFixed(2), Class: cls });
if (appState.history.length > 20) appState.history.pop();
saveState(); renderHistory();
document.getElementById('density-result').textContent = density.toFixed(2);
document.getElementById('class-result').textContent = cls;
document.getElementById('results-area').classList.add('active');
document.querySelectorAll('tbody tr').forEach(r => r.classList.remove('highlight-row'));
document.getElementById(`row-${cls}`).classList.add('highlight-row');
}
</script>
</body>
</html>