adding freight web and holding

This commit is contained in:
JB
2025-12-24 00:38:53 -05:00
parent 0c8630b8ba
commit f39e9132ad
164 changed files with 5559 additions and 156655 deletions

2
packages/freight-web/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules/
*.env

View File

@@ -0,0 +1,6 @@
node_modules/
dist/
build/
coverage/
*.min.js
*.min.css

View File

@@ -0,0 +1,8 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 100,
"tabWidth": 4,
"plugins": ["@ripple-ts/prettier-plugin"]
}

View File

@@ -0,0 +1,49 @@
# Ripple Basic Template
A minimal Ripple application template with TypeScript and Vite.
## Getting Started
1. Install dependencies:
```bash
npm install # or pnpm or yarn
```
2. Start the development server:
```bash
npm run dev
```
3. Build for production:
```bash
npm run build
```
## Code Formatting
This template includes Prettier with the Ripple plugin for consistent code formatting.
### Available Commands
- `npm run format` - Format all files
- `npm run format:check` - Check if files are formatted correctly
### Configuration
Prettier is configured in `.prettierrc` with the following settings:
- Uses tabs for indentation
- Single quotes for strings
- 100 character line width
- Includes the `@ripple-ts/prettier-plugin` for `.ripple` file formatting
### VS Code Integration
For the best development experience, install the [Prettier VS Code extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) and the [Ripple VS Code extension](https://marketplace.visualstudio.com/items?itemName=ripple-ts.vscode-plugin).
## Learn More
- [Ripple Documentation](https://github.com/Ripple-TS/ripple)
- [Vite Documentation](https://vitejs.dev/)

View File

@@ -0,0 +1,3 @@
import ripple from '@ripple-ts/eslint-plugin';
export default [...ripple.configs.recommended];

View File

@@ -0,0 +1,36 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="shortcut icon" type="image/ico" href="/src/assets/favicon.ico" />
<link
href="https://cdn.jsdelivr.net/npm/beercss@3.12.13/dist/cdn/beer.min.css"
rel="stylesheet"
/>
<script
type="module"
src="https://cdn.jsdelivr.net/npm/beercss@3.12.13/dist/cdn/beer.min.js"
></script>
<!-- <script
type="module"
src="https://cdn.jsdelivr.net/npm/material-dynamic-colors@1.1.2/dist/cdn/material-dynamic-colors.min.js"
></script> -->
<link
rel="stylesheet"
type="text/css"
href="https://cdn.jsdelivr.net/npm/@phosphor-icons/web@2.1.1/src/regular/style.css"
/>
<link
rel="stylesheet"
type="text/css"
href="https://cdn.jsdelivr.net/npm/@phosphor-icons/web@2.1.1/src/fill/style.css"
/>
<link rel="stylesheet" type="text/css" href="/src/assets/global.css" />
<title>Ripple App</title>
</head>
<body id="root">
<noscript>You need to enable JavaScript to run this app.</noscript>
<script src="/src/index.ts" type="module"></script>
</body>
</html>

View File

@@ -0,0 +1,35 @@
{
"name": "freight-web",
"version": "1.0.0",
"description": "A Ripple application created with create-ripple",
"type": "module",
"engines": {
"node": ">=20.0.0"
},
"scripts": {
"start": "vite",
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"serve": "vite preview",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
"license": "MIT",
"devDependencies": {
"eslint": "^9.0.0",
"@ripple-ts/eslint-plugin": "latest",
"prettier": "^3.6.2",
"@ripple-ts/prettier-plugin": "latest",
"typescript": "^5.9.2",
"vite": "^7.1.4",
"@ripple-ts/vite-plugin": "latest",
"@ripple-ts/typescript-plugin": "latest"
},
"dependencies": {
"lucide-ripple": "^0.0.6",
"ripple": "latest",
"vite-tsconfig-paths": "^5.1.4",
"@star-kitten/eve": "workspace:^0.0.0"
}
}

View File

@@ -0,0 +1,29 @@
import { track } from 'ripple';
import { Quoute } from './components/calculator/Quoute.ripple';
import { Header } from './components/Header.ripple';
import { Footer } from './components/Footer.ripple';
export component App() {
<Header />
<main class="responsive">
<div class="grid">
<div class="s8">
<Quoute />
</div>
<div class="s4">
<article class="border medium no-padding center-align middle-align">
<div class="padding">
<h5>{'Placeholder Contract Info'}</h5>
</div>
</article>
</div>
</div>
</main>
<Footer />
<style>
main.responsive {
max-inline-size: min(150vw, 100rem);
}
</style>
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,39 @@
:root,
body.dark {
--primary: #78dc77;
--on-primary: #00390a;
--primary-container: #005313;
--on-primary-container: #94f990;
--secondary: #baccb3;
--on-secondary: #253423;
--secondary-container: #3b4b38;
--on-secondary-container: #d5e8cf;
--tertiary: #a0cfd4;
--on-tertiary: #00363b;
--tertiary-container: #1f4d52;
--on-tertiary-container: #bcebf0;
--error: #ffb4ab;
--on-error: #690005;
--error-container: #93000a;
--on-error-container: #ffb4ab;
--background: #1a1c19;
--on-background: #e2e3dd;
--surface: #121411;
--on-surface: #e2e3dd;
--surface-variant: #424940;
--on-surface-variant: #c2c9bd;
--outline: #8c9388;
--outline-variant: #424940;
--shadow: #000000;
--scrim: #000000;
--inverse-surface: #e2e3dd;
--inverse-on-surface: #2f312d;
--inverse-primary: #006e1c;
--surface-dim: #121411;
--surface-bright: #383a36;
--surface-container-lowest: #0c0f0c;
--surface-container-low: #1a1c19;
--surface-container: #1e201d;
--surface-container-high: #282b27;
--surface-container-highest: #333531;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,5 @@
export component Footer() {
<footer>
<p>{'© 2025 Asgard Logistics'}</p>
</footer>
}

View File

@@ -0,0 +1,12 @@
export component Header() {
<header class="secondary-container">
<nav>
<button class="circle transparent">
<i>{'rocket_launch'}</i>
</button>
<h3 class="max left-align">{'Asgard Logistics'}</h3>
<a href="#" class="active">{'Freight Calculator'}</a>
<a href="#">{'View Rates'}</a>
</nav>
</header>
}

View File

@@ -0,0 +1,126 @@
import { track, effect } from 'ripple';
import { Icon } from '../core/Icon.ripple';
import { RouteSelect } from './RouteSelect.ripple';
import { formatNumber } from '../../lib/utils';
import { fetchRoutes, type Route } from '../../lib/route';
import { RouteDetails } from './RouteDetals.ripple';
import { janice } from '@star-kitten/eve/third-party';
export component Quoute() {
const routes = #[] as TrackedArray<Route>;
let rush = track(false);
let cargo = track('');
effect(async () => {
const fetchedRoutes = await fetchRoutes();
routes.push(...fetchedRoutes);
});
const selectedRouteId = track(-1);
const selectedRoute = track(() => {
return routes.find((r) => r.id === @selectedRouteId);
});
<article>
<header>
<h4>
<i>{'calculate'}</i>
{' Freight Calculator'}
</h4>
</header>
<section>
<fieldset>
<legend>{'1. Select a route'}</legend>
<RouteSelect {@routes} onChange={(id) => (@selectedRouteId = id)} />
if (@selectedRoute) {
<RouteDetails route={@selectedRoute} />
}
</fieldset>
<fieldset>
<legend>{'2. Add cargo'}</legend>
<div class="field border label textarea extra">
<textarea onChange={(e) => (@cargo = (e.target as HTMLTextArea).value)} />
<label>
{`Paste your cargo here... (one item per line, e.g. Tritanium 10000)`}
</label>
</div>
</fieldset>
<fieldset>
<legend>{'3. Apply adjustments (optional)'}</legend>
<div class="field middle-align">
<nav>
<div class="max">
<h6>{'Add Rush Shipping'}</h6>
<span class="helper">
{'Guaranteed delivery within 24 hours or the entire contract is free!'}
</span>
</div>
<label class="switch">
<input type="checkbox" onChange={(e) => (@rush = !@rush)} />
<span />
</label>
</nav>
</div>
<div class="field suffix">
<input type="number" min="1" placeholder="Additional Volume (m³)" />
<Icon name="cube" />
<span class="helper">
{'In addition to the calculated volume from your cargo.'}
</span>
</div>
<div class="field suffix">
<input type="number" min="0" placeholder="Additional Collateral (ISK)" />
<i>{'currency_exchange'}</i>
<span class="helper">
{'In addition to the calculated value of your cargo.'}
</span>
</div>
</fieldset>
</section>
<nav>
<button class="circle small">
<i>{'done'}</i>
</button>
<div>{'Select a route'}</div>
<hr class="max" />
<button class="circle small">{'2'}</button>
<div>{'Add cargo'}</div>
<hr class="max" />
<button class="circle small" disabled>{'3'}</button>
<div>{'Apply adjustments'}</div>
<hr class="max" />
<button class="circle small" disabled>{'4'}</button>
<div>{'Calculate'}</div>
</nav>
<br />
// Calculate quote logic goes here
<button
class="responsive large-elevate extra"
onClick={async () => {
console.log('Calculating quote...');
console.log('Selected Route ID:', @selectedRouteId);
console.log('Rush Shipping:', @rush);
console.log('Cargo:', @cargo);
const appraisal = await janice.appraiseItems(
@cargo,
2,
'DUyi5Q3Dod48IoswUBkEfNRs8Qf3cwNN',
);
console.log('Appraised Cargo:', appraisal);
}}
>
{'Calculate Quote'}
</button>
</article>
<style>
pre {
color: var(--secondary);
}
</style>
}

View File

@@ -0,0 +1,48 @@
import type { Route } from '../../lib/route';
import { formatNumber } from '../../lib/utils';
export component RouteDetails({ route }: { route: Route }) {
<div class="s12">
<fieldset>
<legend>
{'Route Details'}
</legend>
<div class="grid">
<div class="s4">
<fieldset>
<legend>{'Max Volume'}</legend>
<h6>{formatNumber(route.max_volume)}</h6>
</fieldset>
</div>
<div class="s4">
<fieldset>
<legend>{'Min Volume'}</legend>
<h6>{formatNumber(route.min_volume || 0)}</h6>
</fieldset>
</div>
<div class="s4">
<fieldset>
<legend>{'Restrictions'}</legend>
{'No Assembled Ships or Containers'}
</fieldset>
</div>
<div class="s6">
<fieldset>
<legend>{'Base Price'}</legend>
<h6>{(route.isk_m3 ? formatNumber(route.isk_m3 || 0) + ' ISK/m³ + ' : route.isk_flatrate + ' ISK flatrate + ') + formatNumber(route.collat_pct) + '% of total collateral'}</h6>
</fieldset>
</div>
<div class="s6">
<fieldset>
<legend>
{'Rush Fee '}
<i>{'warning'}</i>
<div class="tooltip top">{'Guaranteed delivery in 24 hours or your money back'}</div>
</legend>
<h6>{'+' + (route.rush_pct ? formatNumber(route.rush_pct) + '%' : route.rush_fee + ' ISK')}</h6>
</fieldset>
</div>
</div>
</fieldset>
</div>
}

View File

@@ -0,0 +1,63 @@
import { track, type TrackedArray } from 'ripple';
import { Selector } from './Selector.ripple';
import type { Route } from '../../lib/route';
import { formatNumber } from '../../lib/utils';
export interface RouteSelectProps {
routes: TrackedArray<Route>;
onChange?: (id: string) => void;
}
export component RouteSelect({ routes, onChange }: RouteSelectProps) {
let typeTab = track('JF');
<div>
<nav class="tabbed small">
<a class={'JF' === @typeTab ? 'active' : ''} onClick={() => {
@typeTab = 'JF';
onChange?.('');
}}>
<i>{'rocket'}</i>
<span>{'Jump Freighter'}</span>
</a>
<a class={'DST' === @typeTab ? 'active' : ''} onClick={() => {
@typeTab = 'DST';
onChange?.('');
}}>
<i>{'pallet'}</i>
<span>{'Deep Space Transport'}</span>
</a>
<a class={'BR' === @typeTab ? 'active' : ''} onClick={() => {
@typeTab = 'BR';
onChange?.('');
}}>
<i>{'visibility_off'}</i>
<span>{'Blockade Runner'}</span>
</a>
</nav>
<div class={'page padding' + ('JF' === @typeTab ? ' active' : '')}>
<Selector
routes={@routes.filter((r) => r.route_type === 'JF')}
onChange={(routeId: string) => {
onChange?.(routeId);
}}
/>
</div>
<div class={'page padding' + ('DST' === @typeTab ? ' active' : '')}>
<Selector
routes={@routes.filter((r) => r.route_type === 'DST')}
onChange={(routeId: string) => {
onChange?.(routeId);
}}
/>
</div>
<div class={'page padding' + ('BR' === @typeTab ? ' active' : '')}>
<Selector
routes={@routes.filter((r) => r.route_type === 'BR')}
onChange={(routeId: string) => {
onChange?.(routeId);
}}
/>
</div>
</div>
}

View File

@@ -0,0 +1,74 @@
import { track, type TrackedArray } from 'ripple';
import type { Route } from '../../lib/route';
import { formatNumber } from '../../lib/utils';
export interface OriginDestProps {
routes: TrackedArray<Route>;
onChange?: (routeId: string) => void;
}
export component Selector({ routes, onChange }: OriginDestProps) {
const selectedRoute = track<Route>(undefined);
let origin = track('');
const origins = track(() => {
const uniqueOrigins = new Set<string>();
for (const route of routes) {
uniqueOrigins.add(route.origin);
}
return Array.from(uniqueOrigins);
});
<div class="grid">
<div class="s6">
<fieldset>
<legend>{'Origin'}</legend>
<div class="field suffix border">
<select
onChange={(e) => {
@origin = (e.target as HTMLSelectElement).value;
@selectedRoute = undefined;
onChange?.('');
}}
>
<option value="">{'Select origin'}</option>
for (const text of @origins) {
<option value={text}>{text}</option>
}
</select>
<i>{'arrow_drop_down'}</i>
</div>
</fieldset>
</div>
<div class="s6">
<fieldset>
<legend>{'Destination'}</legend>
<div class="field suffix border">
<select
onChange={(e) => {
const id = (e.target as HTMLSelectElement).value;
@selectedRoute = @routes.find((r: Route) => r.id === id);
onChange?.(id);
}}
>
<option value="">{'Select a route'}</option>
for (const route of @routes) {
const rt = route as Route;
if (rt.origin === @origin) {
<option value={rt.id}>{rt.destination}</option>
}
}
</select>
<i>{'arrow_drop_down'}</i>
</div>
</fieldset>
</div>
</div>
<style>
.field {
margin-bottom: 0.5em;
}
</style>
}

View File

@@ -0,0 +1,8 @@
export interface IconProps {
name: string;
style?: Record<string, string | number>;
}
export component Icon({ name, style }: IconProps) {
<i class={`ph ph-${name}`} {style} />
}

View File

@@ -0,0 +1,7 @@
import { mount } from 'ripple';
// @ts-expect-error: known issue, we're working on it
import { App } from './App.ripple';
mount(App, {
target: document.getElementById('root'),
});

View File

@@ -0,0 +1,38 @@
export interface Route {
id: string;
route_type: 'JF' | 'DST' | 'BR';
origin: string;
origin_type: string;
destination: string;
detination_type: string;
max_volume: number;
max_collat: string;
collat_pct: number;
isk_m3?: number;
isk_flatrate?: string;
min_volume?: number;
rush_pct?: number;
rush_fee?: string;
}
export async function fetchRoutes(): Promise<Route[]> {
const response = await fetch(
'https://docs.google.com/spreadsheets/d/e/2PACX-1vSuUV7yKNkgSFFLz2LSUqD4WRQ71usoRuYUMKL00HsJHz52VfgFAIkE8w2DG59V93kCkEKFlqFux-OI/pub?gid=0&single=true&output=csv'
);
if (!response.ok) {
throw new Error(`Failed to fetch routes: ${response.status} ${response.statusText}`);
}
const text = await response.text();
const lines = text.trim().split('\n');
const routes: Route[] = [];
const headers = lines[0].split(',').map((h) => h.trim());
for (let i = 1; i < lines.length; i++) {
const values = lines[i].split(',').map((v) => v.trim());
const row: any = {};
for (let j = 0; j < headers.length; j++) {
row[headers[j]] = values[j];
}
routes.push(row);
}
return routes;
}

View File

@@ -0,0 +1,3 @@
export function formatNumber(num: number): string {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}

View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"moduleResolution": "bundler",
"jsx": "preserve",
"jsxImportSource": "ripple",
"noEmit": true,
"isolatedModules": true,
"plugins": [{ "name": "@ripple-ts/typescript-plugin" }],
"paths": {
"@*": ["./src/*"]
}
}
}

View File

@@ -0,0 +1,13 @@
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';
import { ripple } from '@ripple-ts/vite-plugin';
export default defineConfig({
plugins: [tsconfigPaths(), ripple()],
server: {
port: 3000,
},
build: {
target: 'esnext',
},
});