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>
This commit is contained in:
+221
-59
@@ -3,6 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Freight Calculator Pro</title>
|
<title>Freight Calculator Pro</title>
|
||||||
|
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--primary: #2563eb; --primary-hover: #1d4ed8; --bg-color: #f1f5f9;
|
--primary: #2563eb; --primary-hover: #1d4ed8; --bg-color: #f1f5f9;
|
||||||
@@ -27,7 +28,7 @@
|
|||||||
.container { width: 100%; max-width: 900px; display: flex; flex-direction: column; }
|
.container { width: 100%; max-width: 900px; display: flex; flex-direction: column; }
|
||||||
|
|
||||||
/* Layout */
|
/* Layout */
|
||||||
.branding-header { display: flex; align-items: center; gap: 15px; margin-bottom: 10px; min-height: 60px; }
|
.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-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; }
|
#business-name-display { font-size: 1.5rem; font-weight: 700; color: var(--primary); display: none; }
|
||||||
.main-title { text-align: center; margin-bottom: 24px; }
|
.main-title { text-align: center; margin-bottom: 24px; }
|
||||||
@@ -36,7 +37,8 @@
|
|||||||
|
|
||||||
/* Controls */
|
/* Controls */
|
||||||
.top-controls { position: absolute; top: 20px; right: 20px; display: flex; gap: 12px; }
|
.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; font-size: 1.1rem; display: flex; align-items: center; justify-content: center; box-shadow: var(--shadow); }
|
.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 */
|
/* 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; }
|
.card { background: var(--card-bg); border-radius: var(--border-radius); box-shadow: var(--shadow); padding: 32px; display: flex; flex-direction: column; gap: 20px; }
|
||||||
@@ -67,30 +69,71 @@
|
|||||||
/* Modal */
|
/* Modal */
|
||||||
.modal-overlay { position: fixed; inset: 0; background: var(--modal-bg); z-index: 2000; display: none; justify-content: center; align-items: center; }
|
.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-overlay.open { display: flex; }
|
||||||
.modal-content { background: var(--card-bg); width: 90%; max-width: 600px; border-radius: var(--border-radius); box-shadow: 0 10px 25px rgba(0,0,0,0.2); display: flex; flex-direction: column; max-height: 85vh; }
|
.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; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; }
|
|
||||||
.modal-body { padding: 20px; overflow-y: auto; }
|
.modal-header { padding: 20px 24px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; }
|
||||||
.tabs { display: flex; gap: 10px; margin-bottom: 20px; border-bottom: 1px solid var(--border-color); }
|
.modal-header h2 { font-size: 1.1rem; font-weight: 700; }
|
||||||
.tab-btn { background: none; border: none; padding: 10px 20px; cursor: pointer; color: var(--text-muted); border-bottom: 3px solid transparent; }
|
.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; }
|
.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 { display: none; }
|
||||||
.tab-content.active { display: block; }
|
.tab-content.active { display: block; }
|
||||||
.settings-group { margin-bottom: 20px; }
|
|
||||||
.btn-secondary { background: transparent; border: 1px solid var(--border-color); color: var(--text-muted); padding: 8px 16px; border-radius: 6px; cursor: pointer; }
|
|
||||||
.btn-danger { color: #ef4444; border-color: #ef4444; }
|
|
||||||
|
|
||||||
/* History */
|
/* Settings form */
|
||||||
.history-list { list-style: none; max-height: 300px; overflow-y: auto; border: 1px solid var(--border-color); border-radius: 8px; }
|
.settings-group { margin-bottom: 22px; }
|
||||||
.history-item { padding: 12px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; cursor: pointer; }
|
.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-item:hover { background: var(--table-hover); }
|
||||||
.history-class { font-weight: bold; color: var(--primary); }
|
.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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="top-controls">
|
<div class="top-controls">
|
||||||
<button id="theme-toggle" class="icon-btn">🌙</button>
|
<button id="theme-toggle" class="icon-btn"><i data-lucide="moon"></i></button>
|
||||||
<button id="settings-btn" class="icon-btn">⚙️</button>
|
<button id="settings-btn" class="icon-btn"><i data-lucide="settings"></i></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -124,7 +167,7 @@
|
|||||||
<div class="result-box">
|
<div class="result-box">
|
||||||
<div class="result-label">Freight Class</div>
|
<div class="result-label">Freight Class</div>
|
||||||
<div class="result-value" id="class-result">--</div>
|
<div class="result-value" id="class-result">--</div>
|
||||||
<div style="margin-bottom:20px; color:var(--text-muted)">Density: <span id="density-result">0.00</span> lb/ft³</div>
|
<div class="density-info">Density: <span id="density-result">0.00</span> lb/ft³</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -138,20 +181,58 @@
|
|||||||
<!-- Settings Modal -->
|
<!-- Settings Modal -->
|
||||||
<div id="settings-modal" class="modal-overlay">
|
<div id="settings-modal" class="modal-overlay">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header"><h2>Settings</h2><button class="btn-secondary" onclick="document.getElementById('settings-modal').classList.remove('open')">X</button></div>
|
<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 class="modal-body">
|
||||||
<div class="tabs"><button class="tab-btn active" onclick="switchTab('branding')">Branding</button><button class="tab-btn" onclick="switchTab('history')">History</button></div>
|
|
||||||
<div id="tab-branding" class="tab-content active">
|
<div id="tab-branding" class="tab-content active">
|
||||||
<div class="settings-group"><label>Business Name</label><input type="text" id="setting-name"></div>
|
<div class="settings-group">
|
||||||
<div class="settings-group"><label>Color</label><input type="color" id="setting-color"></div>
|
<label for="setting-name">Business Name</label>
|
||||||
<div class="settings-group"><label>Logo</label><input type="file" id="setting-logo" accept="image/*"></div>
|
<input type="text" id="setting-name" placeholder="Your company name">
|
||||||
<button class="btn-secondary" onclick="clearBranding()">Reset</button>
|
</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>
|
||||||
<div id="tab-history" class="tab-content">
|
<div id="tab-history" class="tab-content">
|
||||||
<button class="btn-secondary btn-danger" onclick="appState.history=[]; saveState(); renderHistory();">Clear History</button>
|
<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 id="history-list" class="history-list"></div>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -167,67 +248,159 @@
|
|||||||
{ min: 3, class: 250, desc: "Mattresses" }, { min: 2, class: 300, desc: "Cabinets" },
|
{ 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" }
|
{ 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 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)); }
|
function saveState() { localStorage.setItem('freightCalcState', JSON.stringify(appState)); }
|
||||||
|
|
||||||
// Init
|
// Init
|
||||||
window.onload = () => {
|
window.onload = () => {
|
||||||
document.body.className = appState.theme === 'dark' ? 'dark-mode' : '';
|
document.body.className = appState.theme === 'dark' ? 'dark-mode' : '';
|
||||||
document.getElementById('theme-toggle').innerText = appState.theme === 'dark' ? '☀️' : '🌙';
|
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);
|
document.documentElement.style.setProperty('--primary', appState.branding.color);
|
||||||
applyBranding();
|
applyBrandingToPage();
|
||||||
renderHistory();
|
renderHistory();
|
||||||
densityRanges.forEach(r => {
|
densityRanges.forEach(r => {
|
||||||
const row = document.createElement('tr'); row.id = `row-${r.class}`;
|
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>`;
|
row.innerHTML = `<td>Class ${r.class}</td><td>${r.min}+ lbs/ft³</td><td>${r.desc}</td>`;
|
||||||
document.getElementById('ref-table-body').appendChild(row);
|
document.getElementById('ref-table-body').appendChild(row);
|
||||||
});
|
});
|
||||||
|
lucide.createIcons();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Theme
|
// Theme
|
||||||
document.getElementById('theme-toggle').onclick = () => {
|
document.getElementById('theme-toggle').onclick = () => {
|
||||||
document.body.classList.toggle('dark-mode');
|
document.body.classList.toggle('dark-mode');
|
||||||
appState.theme = document.body.classList.contains('dark-mode') ? 'dark' : 'light';
|
appState.theme = document.body.classList.contains('dark-mode') ? 'dark' : 'light';
|
||||||
document.getElementById('theme-toggle').innerText = appState.theme === 'dark' ? '☀️' : '🌙';
|
document.getElementById('theme-toggle').innerHTML = appState.theme === 'dark' ? '<i data-lucide="sun"></i>' : '<i data-lucide="moon"></i>';
|
||||||
|
lucide.createIcons();
|
||||||
saveState();
|
saveState();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Branding
|
// Apply branding visuals to page (header logo/name)
|
||||||
function applyBranding() {
|
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;
|
const b = appState.branding;
|
||||||
document.getElementById('setting-name').value = b.name;
|
document.getElementById('setting-name').value = b.name;
|
||||||
document.getElementById('setting-color').value = b.color;
|
document.getElementById('setting-color').value = b.color;
|
||||||
|
document.getElementById('color-hex-label').textContent = b.color;
|
||||||
const logo = document.getElementById('business-logo');
|
const preview = document.getElementById('logo-preview');
|
||||||
const name = document.getElementById('business-name-display');
|
if (b.logo) { preview.src = b.logo; preview.style.display = 'block'; }
|
||||||
|
else { preview.src = ''; preview.style.display = 'none'; }
|
||||||
logo.src = b.logo; logo.style.display = b.logo ? 'block' : 'none';
|
|
||||||
name.innerText = b.name; name.style.display = b.name ? 'block' : 'none';
|
|
||||||
}
|
}
|
||||||
document.getElementById('setting-name').oninput = (e) => { appState.branding.name = e.target.value; applyBranding(); saveState(); };
|
|
||||||
document.getElementById('setting-color').oninput = (e) => { appState.branding.color = e.target.value; document.documentElement.style.setProperty('--primary', e.target.value); saveState(); };
|
// 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) => {
|
document.getElementById('setting-logo').onchange = (e) => {
|
||||||
const reader = new FileReader(); reader.onload = evt => { appState.branding.logo = evt.target.result; applyBranding(); saveState(); };
|
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]);
|
reader.readAsDataURL(e.target.files[0]);
|
||||||
};
|
};
|
||||||
function clearBranding() { appState.branding = {name:'',color:'#2563eb',logo:''}; applyBranding(); saveState(); }
|
|
||||||
|
// 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
|
// History
|
||||||
function renderHistory() {
|
function renderHistory() {
|
||||||
const list = document.getElementById('history-list'); list.innerHTML = '';
|
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 => {
|
appState.history.forEach(item => {
|
||||||
const div = document.createElement('div'); div.className = 'history-item';
|
const div = document.createElement('div'); div.className = 'history-item';
|
||||||
div.innerHTML = `<span>${item.L}"x${item.W}"x${item.H}" | ${item.Weight}lbs</span><span class="history-class">Class ${item.Class}</span>`;
|
div.innerHTML = `<span>${item.L}"×${item.W}"×${item.H}" · ${item.Weight} lbs</span><span class="history-class">Class ${item.Class}</span>`;
|
||||||
div.onclick = () => {
|
div.onclick = () => {
|
||||||
document.getElementById('length').value=item.L; document.getElementById('width').value=item.W;
|
document.getElementById('length').value = item.L;
|
||||||
document.getElementById('height').value=item.H; document.getElementById('weight').value=item.Weight;
|
document.getElementById('width').value = item.W;
|
||||||
document.getElementById('density-result').innerText=item.Density;
|
document.getElementById('height').value = item.H;
|
||||||
document.getElementById('class-result').innerText=item.Class;
|
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.getElementById('results-area').classList.add('active');
|
||||||
document.querySelectorAll('tbody tr').forEach(r => r.classList.remove('highlight-row'));
|
document.querySelectorAll('tbody tr').forEach(r => r.classList.remove('highlight-row'));
|
||||||
document.getElementById(`row-${item.Class}`).classList.add('highlight-row');
|
document.getElementById(`row-${item.Class}`).classList.add('highlight-row');
|
||||||
document.getElementById('settings-modal').classList.remove('open');
|
revertAndClose();
|
||||||
};
|
};
|
||||||
list.appendChild(div);
|
list.appendChild(div);
|
||||||
});
|
});
|
||||||
@@ -239,7 +412,6 @@
|
|||||||
const W = parseFloat(document.getElementById('width').value);
|
const W = parseFloat(document.getElementById('width').value);
|
||||||
const H = parseFloat(document.getElementById('height').value);
|
const H = parseFloat(document.getElementById('height').value);
|
||||||
const WT = parseFloat(document.getElementById('weight').value);
|
const WT = parseFloat(document.getElementById('weight').value);
|
||||||
|
|
||||||
if (!L || !W || !H || !WT) return;
|
if (!L || !W || !H || !WT) return;
|
||||||
|
|
||||||
const density = WT / ((L * W * H) / 1728);
|
const density = WT / ((L * W * H) / 1728);
|
||||||
@@ -250,22 +422,12 @@
|
|||||||
if (appState.history.length > 20) appState.history.pop();
|
if (appState.history.length > 20) appState.history.pop();
|
||||||
saveState(); renderHistory();
|
saveState(); renderHistory();
|
||||||
|
|
||||||
document.getElementById('density-result').innerText = density.toFixed(2);
|
document.getElementById('density-result').textContent = density.toFixed(2);
|
||||||
document.getElementById('class-result').innerText = cls;
|
document.getElementById('class-result').textContent = cls;
|
||||||
document.getElementById('results-area').classList.add('active');
|
document.getElementById('results-area').classList.add('active');
|
||||||
|
|
||||||
document.querySelectorAll('tbody tr').forEach(r => r.classList.remove('highlight-row'));
|
document.querySelectorAll('tbody tr').forEach(r => r.classList.remove('highlight-row'));
|
||||||
document.getElementById(`row-${cls}`).classList.add('highlight-row');
|
document.getElementById(`row-${cls}`).classList.add('highlight-row');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modal
|
|
||||||
document.getElementById('settings-btn').onclick = () => document.getElementById('settings-modal').classList.add('open');
|
|
||||||
function switchTab(t) {
|
|
||||||
document.querySelectorAll('.tab-content').forEach(e=>e.classList.remove('active'));
|
|
||||||
document.querySelectorAll('.tab-btn').forEach(e=>e.classList.remove('active'));
|
|
||||||
document.getElementById('tab-'+t).classList.add('active');
|
|
||||||
event.target.classList.add('active');
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+2
-1
@@ -5,7 +5,8 @@
|
|||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron .",
|
"start": "electron .",
|
||||||
"build": "electron-builder --win"
|
"build": "electron-builder --win",
|
||||||
|
"serve": "node server.js"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
const http = require('http');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const PORT = 3010;
|
||||||
|
|
||||||
|
const mimeTypes = {
|
||||||
|
'.html': 'text/html',
|
||||||
|
'.js': 'application/javascript',
|
||||||
|
'.css': 'text/css',
|
||||||
|
'.png': 'image/png',
|
||||||
|
'.ico': 'image/x-icon',
|
||||||
|
'.json': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
const filePath = path.join(__dirname, req.url === '/' ? 'index.html' : req.url);
|
||||||
|
const ext = path.extname(filePath);
|
||||||
|
const contentType = mimeTypes[ext] || 'application/octet-stream';
|
||||||
|
|
||||||
|
fs.readFile(filePath, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
||||||
|
res.end('404 Not Found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.writeHead(200, { 'Content-Type': contentType });
|
||||||
|
res.end(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(PORT, () => {
|
||||||
|
console.log(`Freight Calculator Pro running at http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user