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.
|
||||
+1020
-193
File diff suppressed because it is too large
Load Diff
+7
-2
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "freight-calculator-pro",
|
||||
"version": "1.0.0",
|
||||
"version": "1.3.0",
|
||||
"description": "Professional Freight Class Calculator",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"build": "electron-builder --win"
|
||||
"build": "electron-builder --win",
|
||||
"serve": "node server.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
@@ -16,6 +17,10 @@
|
||||
"build": {
|
||||
"appId": "com.freightcalc.pro",
|
||||
"productName": "Freight Calculator Pro",
|
||||
"files": [
|
||||
"**/*",
|
||||
"!tauri/**"
|
||||
],
|
||||
"win": {
|
||||
"target": "nsis",
|
||||
"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