Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fc93c2ee57 | |||
| e5ed1d3c9f | |||
| 3c3ef02f20 | |||
| fc88d7ab4b | |||
| 25973cabbc |
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": "0.0.1",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "freight-calculator",
|
||||||
|
"runtimeExecutable": "node",
|
||||||
|
"runtimeArgs": ["server.js"],
|
||||||
|
"port": 3010
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Two desktop shells — same frontend
|
||||||
|
|
||||||
|
This repo ships **two interchangeable desktop shells** that both load the exact same `index.html`:
|
||||||
|
|
||||||
|
1. **Electron** (original, root of repo) — heavier (~70MB installer) but mature/familiar
|
||||||
|
2. **Tauri** (`tauri/` subfolder) — much lighter (~2MB installer, ~8MB exe) using the OS's built-in WebView2 instead of bundled Chromium
|
||||||
|
|
||||||
|
`index.html` (plus `vendor/`) is the **single source of truth** for the UI — never duplicate or fork it. The Tauri shell's `tauri/dist/` is a *generated* copy (gitignored, rebuilt by `tauri/sync-frontend.js` before each `dev`/`build`run) — edit `index.html` at the repo root, never inside `tauri/dist/`.
|
||||||
|
|
||||||
|
### Electron commands (run from repo root)
|
||||||
|
- `npm start` — run the app as an Electron desktop window (loads `index.html` via `main.js`)
|
||||||
|
- `npm run serve` — run a plain Node HTTP server (`server.js`) at http://localhost:3010, serving the same `index.html` for browser-based testing
|
||||||
|
- `npm run build` — package the Windows installer via `electron-builder` (NSIS, output goes to `dist/`)
|
||||||
|
|
||||||
|
### Tauri commands (run from `tauri/`)
|
||||||
|
- `npm run dev` — sync the frontend into `tauri/dist/` and launch the Tauri dev shell
|
||||||
|
- `npm run build` — sync the frontend and produce MSI + NSIS installers (output in `tauri/src-tauri/target/release/bundle/`)
|
||||||
|
- `npm run serve` — same static server as the Electron side, for browser-based testing
|
||||||
|
- Requires the Rust toolchain (`rustup`/`cargo`) plus MSVC Build Tools and WebView2 (Tauri prerequisites) — see https://tauri.app/start/prerequisites/
|
||||||
|
|
||||||
|
There is no test suite, linter, or build step for the app code itself — `index.html` is loaded directly (as a file in Electron/Tauri, or statically served). To verify a change, just reload the app/page in whichever shell you're using.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
This is a single-page Electron desktop app (also runnable as a static page) for calculating NMFC freight class from shipment dimensions and weight. Almost all logic lives in one file:
|
||||||
|
|
||||||
|
- **`index.html`** — contains everything: markup, CSS (`<style>` block), and all application JavaScript (`<script>` block starting around line 370). There are no separate JS/CSS files or build/bundling steps; what you see in `index.html` is what runs.
|
||||||
|
- **`main.js`** — minimal Electron bootstrap (creates the `BrowserWindow`, loads `index.html`, with `nodeIntegration: true` / `contextIsolation: false`)
|
||||||
|
- **`server.js`** — minimal static file server used only for `npm run serve` (browser-based dev/testing instead of Electron)
|
||||||
|
- **`vendor/`** — local copies of third-party JS libraries (`lucide.js`, `jspdf.umd.min.js`, `jspdf.plugin.autotable.min.js`), referenced by `index.html` via relative `<script src="vendor/...">` tags so the app runs **fully offline** with no CDN dependency. Not gitignored — these files are committed and get bundled into the installer by `electron-builder` automatically. **Do not switch these back to CDN URLs** — if a library needs upgrading, download the new version into `vendor/` and update the filename/reference together.
|
||||||
|
|
||||||
|
### Structure inside `index.html`'s script
|
||||||
|
|
||||||
|
The script is organized into clearly marked sections (look for the `// ─── Section ───` banner comments) in this order:
|
||||||
|
|
||||||
|
1. **Data** — `densityRanges` (density → freight class lookup table with descriptions) and `nmfcData` (searchable list of NMFC codes/descriptions/classes)
|
||||||
|
2. **State** — `appState`, persisted to `localStorage` under the key `freightCalcState` via `saveState()`. Defaults are merged with `Object.assign` so new fields can be added without breaking existing saved state. Shape: `{ theme, branding: {name, color, logo}, history[], units: {dim, wt}, defaults: {L, W, H, Weight} }`. The working `pieces` array (current shipment line items being entered) is ephemeral and NOT persisted.
|
||||||
|
3. **Init** — `init()`, wires up the page on load, applies branding, populates settings/defaults forms, renders the density reference table
|
||||||
|
4. **Theme** — light/dark toggle
|
||||||
|
5. **Branding** — company name/color/logo customization, applied to the page header and PDF export
|
||||||
|
6. **Defaults tab** — user-configurable default piece dimensions/weight pre-filled on new entries
|
||||||
|
7. **Modal** — settings modal open/close/revert logic (`pendingBranding` holds unsaved edits so Cancel can discard them)
|
||||||
|
8. **Tabs** — settings modal tab switching (Branding / Defaults / History)
|
||||||
|
9. **History** — calculation history stored in `appState.history` (capped at 50 entries), CSV export/import, restore-from-history (re-applies the exact units the entry was saved in)
|
||||||
|
10. **Unit toggles** — switching between in/cm and lbs/kg; conversions are applied at calculation time via `dimFactor`/`wtFactor`, not by mutating stored values
|
||||||
|
11. **Pieces** — managing the dynamic list of shipment pieces (`renderPieces`, `addPiece`)
|
||||||
|
12. **Validation & Clear** — input validation/error display, form reset
|
||||||
|
13. **Density gauge** — visual gauge showing where the calculated density falls
|
||||||
|
14. **Calculate** — `calculateFreightClass()`: the core calculation. Converts all dimensions/weights to inches/lbs, computes total cubic feet and total weight, derives density, looks up the class from `densityRanges` (first range where `density >= min` wins), saves a history entry, and updates the UI/gauge
|
||||||
|
15. **Copy to clipboard** — copies the result summary as text
|
||||||
|
16. **PDF Export** — `exportPDF()` builds a branded PDF report using `jsPDF` + `jspdf-autotable` (loaded from local `vendor/` copies via `<script>` tags in the `<head>` — see `vendor/` note above), including logo, company branding, pieces table, and footer
|
||||||
|
17. **NMFC Search** — live search over `nmfcData` by code or description; selecting a result highlights the matching row in the on-page density reference table
|
||||||
|
|
||||||
|
### Key conventions
|
||||||
|
|
||||||
|
- All persisted user data lives in one `appState` object in `localStorage`; always go through `saveState()` after mutating it.
|
||||||
|
- Unit conversions (`in`↔`cm`, `lbs`↔`kg`) happen only at calculation/display time — stored values keep their original units alongside a `units` field so they can be restored exactly.
|
||||||
|
- External libraries (`lucide` icons, `jsPDF`, `jspdf-autotable`) are vendored locally in `vendor/` and loaded via relative `<script>` tags in the `<head>` — **not** from CDNs — so the app works completely offline.
|
||||||
+971
-144
File diff suppressed because it is too large
Load Diff
+7
-2
@@ -1,11 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "freight-calculator-pro",
|
"name": "freight-calculator-pro",
|
||||||
"version": "1.0.0",
|
"version": "1.3.0",
|
||||||
"description": "Professional Freight Class Calculator",
|
"description": "Professional Freight Class Calculator",
|
||||||
"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",
|
||||||
@@ -16,6 +17,10 @@
|
|||||||
"build": {
|
"build": {
|
||||||
"appId": "com.freightcalc.pro",
|
"appId": "com.freightcalc.pro",
|
||||||
"productName": "Freight Calculator Pro",
|
"productName": "Freight Calculator Pro",
|
||||||
|
"files": [
|
||||||
|
"**/*",
|
||||||
|
"!tauri/**"
|
||||||
|
],
|
||||||
"win": {
|
"win": {
|
||||||
"target": "nsis",
|
"target": "nsis",
|
||||||
"icon": "icon.ico"
|
"icon": "icon.ico"
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
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) => {
|
||||||
|
// Strip query string / hash and decode URI components before resolving,
|
||||||
|
// then confirm the resolved path stays inside __dirname (no ../ escapes).
|
||||||
|
const urlPath = decodeURIComponent(req.url.split('?')[0].split('#')[0]);
|
||||||
|
const reqPath = urlPath === '/' ? 'index.html' : urlPath.replace(/^\/+/, '');
|
||||||
|
const filePath = path.join(__dirname, reqPath);
|
||||||
|
if (!filePath.startsWith(__dirname + path.sep) && filePath !== __dirname) {
|
||||||
|
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
||||||
|
res.end('403 Forbidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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}`);
|
||||||
|
});
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
node_modules/
|
||||||
|
src-tauri/target/
|
||||||
|
src-tauri/gen/schemas
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Generated by sync-frontend.js — copy of ../index.html and ../vendor/, not source of truth
|
||||||
|
dist/
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "freight-calculator-pro-tauri",
|
||||||
|
"version": "1.3.0",
|
||||||
|
"description": "Professional Freight Class Calculator (Tauri shell)",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"sync": "node sync-frontend.js",
|
||||||
|
"dev": "npm run sync && tauri dev",
|
||||||
|
"build": "npm run sync && tauri build",
|
||||||
|
"serve": "node ../server.js"
|
||||||
|
},
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@tauri-apps/cli": "^2.11.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
/target/
|
||||||
|
/gen/schemas
|
||||||
Generated
+4941
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
|||||||
|
[package]
|
||||||
|
name = "freight-calculator-pro"
|
||||||
|
version = "1.3.0"
|
||||||
|
description = "Professional Freight Class Calculator"
|
||||||
|
authors = ["Devon Bontrager"]
|
||||||
|
license = "ISC"
|
||||||
|
repository = "https://gitea.netbird.zimspace.uk:5938/debont80/FreightCalculatorPro.git"
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.77.2"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "app_lib"
|
||||||
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "2.6.2", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde_json = "1.0"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
log = "0.4"
|
||||||
|
tauri = { version = "2.11.2", features = [] }
|
||||||
|
tauri-plugin-log = "2"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "enables the default permissions",
|
||||||
|
"windows": [
|
||||||
|
"main"
|
||||||
|
],
|
||||||
|
"permissions": [
|
||||||
|
"core:default"
|
||||||
|
]
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.7 KiB |
@@ -0,0 +1,16 @@
|
|||||||
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
pub fn run() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.setup(|app| {
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
|
app.handle().plugin(
|
||||||
|
tauri_plugin_log::Builder::default()
|
||||||
|
.level(log::LevelFilter::Info)
|
||||||
|
.build(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application");
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
app_lib::run();
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
|
"productName": "Freight Calculator Pro",
|
||||||
|
"version": "1.3.0",
|
||||||
|
"identifier": "com.freightcalc.pro",
|
||||||
|
"build": {
|
||||||
|
"frontendDist": "../dist"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"title": "Freight Calculator Pro",
|
||||||
|
"width": 950,
|
||||||
|
"height": 950,
|
||||||
|
"minWidth": 800,
|
||||||
|
"minHeight": 700,
|
||||||
|
"resizable": true,
|
||||||
|
"fullscreen": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"targets": "all",
|
||||||
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// Copies the canonical frontend (../index.html and ../vendor/) into ./dist
|
||||||
|
// before each Tauri dev/build run, so the Tauri shell always packages the
|
||||||
|
// exact same UI as the Electron version — no manual sync, no drift.
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const ROOT = path.join(__dirname, '..');
|
||||||
|
const DIST = path.join(__dirname, 'dist');
|
||||||
|
|
||||||
|
function copyRecursive(src, dest) {
|
||||||
|
const stat = fs.statSync(src);
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
fs.mkdirSync(dest, { recursive: true });
|
||||||
|
for (const entry of fs.readdirSync(src)) {
|
||||||
|
copyRecursive(path.join(src, entry), path.join(dest, entry));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fs.copyFileSync(src, dest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.rmSync(DIST, { recursive: true, force: true });
|
||||||
|
fs.mkdirSync(DIST, { recursive: true });
|
||||||
|
copyRecursive(path.join(ROOT, 'index.html'), path.join(DIST, 'index.html'));
|
||||||
|
copyRecursive(path.join(ROOT, 'vendor'), path.join(DIST, 'vendor'));
|
||||||
|
|
||||||
|
console.log(`Synced frontend (index.html + vendor/) from ${ROOT} -> ${DIST}`);
|
||||||
Vendored
+10
File diff suppressed because one or more lines are too long
Vendored
+398
File diff suppressed because one or more lines are too long
Vendored
+19522
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user