Freight Quote Calculator
blocklogisticsMultimodal Air, Ocean, and Ground freight rate estimator with CBM volumetric weight calculation, dynamic route pricing, value-added services, and live comparison quotes.
Installation
$pnpm dlx uipkge-ng@latest add freight-quote-calculator$npx uipkge-ng@latest add freight-quote-calculator$yarn dlx uipkge-ng@latest add freight-quote-calculator$bunx uipkge-ng@latest add freight-quote-calculatorManifest:
https://uipkge.dev/r/angular/freight-quote-calculator.jsonInstalls to:src/app/components/blocks/freight-quote-calculator/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
PortOptioninterface PortOption {
code: string
name: string
}CurrencyConfiginterface CurrencyConfig {
code: string
symbol: string
label: string
rate: number
}Files installed (2)
src/app/components/blocks/freight-quote-calculator/freight-quote-calculator.component.ts42.5 kB
// Multimodal Air, Ocean, and Ground freight rate estimator with CBM volumetric weight // calculation, dynamic route pricing, value-added services, and live comparison quotes. // Port of the React block 1:1. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { ArrowLeftRight, ArrowRight, Boxes, Check, CheckCircle2, Clock, Download, FileText, Info, Leaf, LucideAngularModule, Package, Plane, Scale, Ship, Truck, Zap, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardFooterComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiCheckboxComponent } from '@/ui/checkbox/checkbox.component' import { UiInputComponent } from '@/ui/input/input.component' import { UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, } from '@/ui/select/select.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' export type FreightMode = 'ocean-fcl' | 'ocean-lcl' | 'air' | 'ground' interface PortOption { code: string name: string } interface CurrencyConfig { code: string symbol: string label: string rate: number } const ORIGIN_PORTS: PortOption[] = [ { code: 'CNSHA', name: 'Shanghai Port (CNSHA)' }, { code: 'CNNGB', name: 'Ningbo-Zhoushan Port (CNNGB)' }, { code: 'SGSIN', name: 'Port of Singapore (SGSIN)' }, { code: 'DEHAM', name: 'Port of Hamburg (DEHAM)' }, { code: 'NLRTM', name: 'Port of Rotterdam (NLRTM)' }, { code: 'JPTYO', name: 'Port of Tokyo (JPTYO)' }, ] const DESTINATION_PORTS: PortOption[] = [ { code: 'USLAX', name: 'Port of Los Angeles (USLAX)' }, { code: 'USNYC', name: 'Port of New York & New Jersey (USNYC)' }, { code: 'USORD', name: 'Chicago O’Hare Logistics Hub (USORD)' }, { code: 'GBFXT', name: 'Port of Felixstowe (GBFXT)' }, { code: 'AUMEL', name: 'Port of Melbourne (AUMEL)' }, { code: 'AEJEA', name: 'Jebel Ali Port (AEJEA)' }, ] const CURRENCIES: Record<string, CurrencyConfig> = { USD: { code: 'USD', symbol: '$', label: 'USD ($)', rate: 1.0 }, EUR: { code: 'EUR', symbol: '€', label: 'EUR (€)', rate: 0.92 }, GBP: { code: 'GBP', symbol: '£', label: 'GBP (£)', rate: 0.79 }, CNY: { code: 'CNY', symbol: '¥', label: 'CNY (¥)', rate: 7.23 }, SGD: { code: 'SGD', symbol: 'S$', label: 'SGD (S$)', rate: 1.34 }, } const CURRENCY_LIST = [ CURRENCIES['USD']!, CURRENCIES['EUR']!, CURRENCIES['GBP']!, CURRENCIES['CNY']!, CURRENCIES['SGD']!, ] const CUSTOMS_USD = 150 const INSURANCE_USD = 85 const LIFTGATE_USD = 50 @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-freight-quote-calculator, [ui-freight-quote-calculator]', standalone: true, // React renders the root <div> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardFooterComponent, UiCardHeaderComponent, UiCardTitleComponent, UiCheckboxComponent, UiInputComponent, UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, UiSeparatorComponent, ], template: ` <div data-slot="freight-quote-calculator" [class]="rootClass"> <!-- Header Section --> <div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div class="space-y-1.5"> <div class="flex items-center gap-2"> <span ui-badge variant="outline" class="gap-1.5 px-2.5 py-0.5 text-xs font-medium"> <lucide-icon [img]="Boxes" class="text-primary size-3.5" /> Multimodal Logistics </span> <span ui-badge variant="secondary" class="text-xs font-normal">Spot Rates Live</span> </div> <h2 class="text-foreground text-2xl font-bold tracking-tight sm:text-3xl"> Freight Rate & Cargo Quote Calculator </h2> <p class="text-muted-foreground text-sm"> Compare spot rates across Ocean FCL/LCL, Air Freight, and Ground Trucking with instant CBM volumetric analysis. </p> </div> <div class="flex items-center gap-2 sm:self-start"> <span class="text-muted-foreground text-xs font-medium">Currency:</span> <ui-select [value]="selectedCurrency()" (valueChange)="selectedCurrency.set($event)"> <button ui-select-trigger class="w-[125px] text-xs font-medium"> <ui-select-value [placeholder]="selectedCurrency()" /> </button> <ui-select-content align="end"> @for (curr of currencyList; track curr.code) { <ui-select-item [value]="curr.code" class="text-xs">{{ curr.label }}</ui-select-item> } </ui-select-content> </ui-select> </div> </div> <div class="grid grid-cols-1 gap-8 lg:grid-cols-12 lg:items-start"> <!-- Left Column --> <div class="space-y-6 lg:col-span-7"> <!-- 1. Route --> <div ui-card class="border-border shadow-xs"> <div ui-card-header class="pb-4"> <div class="flex flex-wrap items-center justify-between"> <h3 ui-card-title class="text-base font-semibold">1. Shipping Route</h3> <span class="text-muted-foreground text-xs font-medium">International Corridors</span> </div> <p ui-card-description class="text-xs">Select origin loading port and destination discharge terminal.</p> </div> <div ui-card-content class="space-y-4"> <div class="grid grid-cols-1 gap-3 sm:grid-cols-[1fr,auto,1fr] sm:items-end"> <div class="space-y-1.5"> <label class="text-foreground text-xs font-medium" for="fq-origin">Origin (Port / Hub)</label> <ui-select [value]="origin()" (valueChange)="origin.set($event)"> <button ui-select-trigger id="fq-origin" class="text-xs"> <ui-select-value /> </button> <ui-select-content> @for (p of originPorts; track p.code) { <ui-select-item [value]="p.code" class="text-xs">{{ p.name }}</ui-select-item> } </ui-select-content> </ui-select> </div> <div class="flex justify-center pb-0.5 sm:pb-0"> <button ui-button type="button" variant="outline" size="icon" class="size-9 shrink-0" aria-label="Swap Origin and Destination" (click)="swapRoute()" > <lucide-icon [img]="ArrowLeftRight" class="size-4" /> </button> </div> <div class="space-y-1.5"> <label class="text-foreground text-xs font-medium" for="fq-destination"> Destination (Port / Hub) </label> <ui-select [value]="destination()" (valueChange)="destination.set($event)"> <button ui-select-trigger id="fq-destination" class="text-xs"> <ui-select-value /> </button> <ui-select-content> @for (p of destinationPorts; track p.code) { <ui-select-item [value]="p.code" class="text-xs">{{ p.name }}</ui-select-item> } </ui-select-content> </ui-select> </div> </div> <div class="bg-muted/40 border-border/80 flex flex-wrap items-center justify-between rounded-lg border px-3 py-2 text-xs" > <div class="flex items-center gap-2"> <lucide-icon [img]="Ship" class="text-primary size-4 shrink-0" /> <span class="text-foreground font-medium">Transpacific Direct Corridor</span> </div> <span class="text-muted-foreground tabular-nums">Distance: ~5,800 NM (10,740 km)</span> </div> </div> </div> <!-- 2. Mode --> <div ui-card class="border-border shadow-xs"> <div ui-card-header class="pb-4"> <div class="flex flex-wrap items-center justify-between"> <h3 ui-card-title class="text-base font-semibold">2. Cargo Freight Mode</h3> <span class="text-muted-foreground text-xs">Select primary transport</span> </div> <p ui-card-description class="text-xs"> Choose your preferred multimodal transit mode to evaluate rates. </p> </div> <div ui-card-content> <div class="grid grid-cols-1 gap-3 sm:grid-cols-2"> <button type="button" [class]="modeCardClass('ocean-fcl')" (click)="selectedMode.set('ocean-fcl')"> <div class="flex w-full items-start justify-between"> <div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="Ship" class="size-4" /> </div> <span ui-badge variant="default" class="text-xs">Recommended</span> </div> <div> <div class="text-foreground text-sm font-semibold">Ocean FCL (Full Container)</div> <div class="text-muted-foreground mt-0.5 text-xs">20ft / 40ft dedicated sea freight container</div> </div> <div class="border-border/50 flex w-full items-center justify-between border-t pt-1 text-xs"> <span class="text-muted-foreground">Est. Transit: 14-18 days</span> <span class="text-foreground font-semibold tabular-nums">{{ formatMoney(oceanFclBaseUsd()) }}</span> </div> </button> <button type="button" [class]="modeCardClass('ocean-lcl')" (click)="selectedMode.set('ocean-lcl')"> <div class="flex w-full items-start justify-between"> <div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="Boxes" class="size-4" /> </div> <span ui-badge variant="outline" class="text-xs font-normal">Economy</span> </div> <div> <div class="text-foreground text-sm font-semibold">Ocean LCL (Shared)</div> <div class="text-muted-foreground mt-0.5 text-xs"> Consolidated sea freight priced by CBM volume </div> </div> <div class="border-border/50 flex w-full items-center justify-between border-t pt-1 text-xs"> <span class="text-muted-foreground">Est. Transit: 18-22 days</span> <span class="text-foreground font-semibold tabular-nums">{{ formatMoney(oceanLclBaseUsd()) }}</span> </div> </button> <button type="button" [class]="modeCardClass('air')" (click)="selectedMode.set('air')"> <div class="flex w-full items-start justify-between"> <div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="Plane" class="size-4" /> </div> <span ui-badge variant="secondary" class="text-xs">Fastest</span> </div> <div> <div class="text-foreground text-sm font-semibold">Express Air Freight</div> <div class="text-muted-foreground mt-0.5 text-xs">Priority belly/freighter aircraft dispatch</div> </div> <div class="border-border/50 flex w-full items-center justify-between border-t pt-1 text-xs"> <span class="text-muted-foreground">Est. Transit: 3-5 days</span> <span class="text-foreground font-semibold tabular-nums">{{ formatMoney(airFreightBaseUsd()) }}</span> </div> </button> <button type="button" [class]="modeCardClass('ground')" (click)="selectedMode.set('ground')"> <div class="flex w-full items-start justify-between"> <div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="Truck" class="size-4" /> </div> <span ui-badge variant="outline" class="text-xs font-normal">Overland</span> </div> <div> <div class="text-foreground text-sm font-semibold">Ground Freight</div> <div class="text-muted-foreground mt-0.5 text-xs"> Regional linehaul, FTL & pallet LTL trucking </div> </div> <div class="border-border/50 flex w-full items-center justify-between border-t pt-1 text-xs"> <span class="text-muted-foreground">Est. Transit: 5-7 days</span> <span class="text-foreground font-semibold tabular-nums">{{ formatMoney(groundFreightBaseUsd()) }}</span> </div> </button> </div> </div> </div> <!-- 3. Dimensions --> <div ui-card class="border-border shadow-xs"> <div ui-card-header class="pb-4"> <div class="flex flex-wrap items-center justify-between"> <h3 ui-card-title class="text-base font-semibold">3. Cargo Dimensions & Volumetric Weight</h3> <span ui-badge variant="outline" class="gap-1 text-xs font-normal"> <lucide-icon [img]="Scale" class="text-primary size-3" /> IATA 1:6 Standard </span> </div> <p ui-card-description class="text-xs"> Enter pallet quantity, unit dimensions, and individual gross weight. </p> </div> <div ui-card-content class="space-y-5"> <div class="grid grid-cols-2 gap-3 sm:grid-cols-5"> <div class="col-span-2 space-y-1.5 sm:col-span-1"> <label class="text-foreground text-xs font-medium" for="fq-pallets">Quantity</label> <div class="relative"> <ui-input id="fq-pallets" type="number" [value]="pallets()" (valueChange)="setPallets($event)" class="pr-8 text-xs tabular-nums" /> <span class="text-muted-foreground absolute top-1/2 right-2.5 -translate-y-1/2 text-xs">pal</span> </div> </div> <div class="space-y-1.5"> <label class="text-foreground text-xs font-medium" for="fq-length">Length (L)</label> <div class="relative"> <ui-input id="fq-length" type="number" [value]="lengthCm()" (valueChange)="setLengthCm($event)" class="pr-8 text-xs tabular-nums" /> <span class="text-muted-foreground absolute top-1/2 right-2.5 -translate-y-1/2 text-xs">cm</span> </div> </div> <div class="space-y-1.5"> <label class="text-foreground text-xs font-medium" for="fq-width">Width (W)</label> <div class="relative"> <ui-input id="fq-width" type="number" [value]="widthCm()" (valueChange)="setWidthCm($event)" class="pr-8 text-xs tabular-nums" /> <span class="text-muted-foreground absolute top-1/2 right-2.5 -translate-y-1/2 text-xs">cm</span> </div> </div> <div class="space-y-1.5"> <label class="text-foreground text-xs font-medium" for="fq-height">Height (H)</label> <div class="relative"> <ui-input id="fq-height" type="number" [value]="heightCm()" (valueChange)="setHeightCm($event)" class="pr-8 text-xs tabular-nums" /> <span class="text-muted-foreground absolute top-1/2 right-2.5 -translate-y-1/2 text-xs">cm</span> </div> </div> <div class="col-span-2 space-y-1.5 sm:col-span-1"> <label class="text-foreground text-xs font-medium" for="fq-weight">Weight/Pallet</label> <div class="relative"> <ui-input id="fq-weight" type="number" [value]="grossWeightKg()" (valueChange)="setGrossWeightKg($event)" class="pr-8 text-xs tabular-nums" /> <span class="text-muted-foreground absolute top-1/2 right-2.5 -translate-y-1/2 text-xs">kg</span> </div> </div> </div> <div class="border-border/80 bg-muted/30 space-y-3 rounded-lg border p-4"> <div class="flex flex-wrap items-center justify-between text-xs"> <span class="text-foreground flex items-center gap-1.5 font-semibold"> <lucide-icon [img]="Package" class="text-primary size-3.5" /> Cargo Metric Computations </span> <span class="text-muted-foreground">Standard Euro/US Pallet Basis</span> </div> <div class="grid grid-cols-2 gap-3 sm:grid-cols-4"> <div class="bg-card border-border/80 rounded-md border p-2.5"> <div class="text-muted-foreground text-xs">Total Volume</div> <div class="text-foreground mt-0.5 text-lg font-bold tabular-nums"> {{ totalVolumeCbm().toFixed(2) }} <span class="text-muted-foreground text-xs font-normal">CBM</span> </div> <div class="text-muted-foreground mt-1 text-xs"> {{ singlePalletVolumeCbm().toFixed(2) }} m³ / unit </div> </div> <div class="bg-card border-border/80 rounded-md border p-2.5"> <div class="text-muted-foreground text-xs">Actual Gross Wt.</div> <div class="text-foreground mt-0.5 text-lg font-bold tabular-nums"> {{ totalGrossWeightKg().toLocaleString('en-US') }} <span class="text-muted-foreground text-xs font-normal">kg</span> </div> <div class="text-muted-foreground mt-1 text-xs">{{ pallets() }} × {{ grossWeightKg() }} kg</div> </div> <div class="bg-card border-border/80 rounded-md border p-2.5"> <div class="text-muted-foreground text-xs">Chargeable Wt. (Air)</div> <div class="text-foreground mt-0.5 text-lg font-bold tabular-nums"> {{ airVolumetricWeightKg().toLocaleString('en-US') }} <span class="text-muted-foreground text-xs font-normal">kg</span> </div> <div class="text-muted-foreground mt-1 text-xs">Volumetric (1:6000)</div> </div> <div class="bg-card border-border/80 rounded-md border p-2.5"> <div class="text-muted-foreground text-xs">Ocean Revenue Ton</div> <div class="text-foreground mt-0.5 text-lg font-bold tabular-nums"> {{ oceanRevenueTon().toFixed(2) }} <span class="text-muted-foreground text-xs font-normal">RT</span> </div> <div class="text-muted-foreground mt-1 text-xs">Max(CBM, Weight/T)</div> </div> </div> <p class="text-muted-foreground flex items-center gap-1.5 pt-1 text-xs"> <lucide-icon [img]="Info" class="text-primary size-3.5 shrink-0" /> <span> Computed Total Volume ({{ totalVolumeCbm().toFixed(2) }} CBM) & Chargeable Weight ({{ airVolumetricWeightKg().toLocaleString('en-US') }} kg) apply dynamically to quotes. </span> </p> </div> </div> </div> <!-- 4. Services --> <div ui-card class="border-border shadow-xs"> <div ui-card-header class="pb-3"> <div class="flex flex-wrap items-center justify-between"> <h3 ui-card-title class="text-base font-semibold">4. Value-Added Freight Services</h3> <span class="text-muted-foreground text-xs font-medium">Optional Add-ons</span> </div> <p ui-card-description class="text-xs"> Enhance shipment handling with customs, all-risk insurance, and destination equipment. </p> </div> <div ui-card-content class="space-y-3"> <label class="border-border hover:border-primary/40 bg-card flex cursor-pointer items-start justify-between gap-3 rounded-lg border p-3.5 transition-colors" > <div class="flex items-start gap-3"> <ui-checkbox [checked]="addCustoms()" (checkedChange)="addCustoms.set(!!$event)" /> <div class="space-y-0.5"> <div class="flex items-center gap-2"> <span class="text-foreground text-sm font-medium">Customs Clearance</span> <span ui-badge variant="outline" class="text-xs font-normal tabular-nums" >+{{ formatMoney(customsUsd) }}</span > </div> <p class="text-muted-foreground text-xs"> Export filing, ISF 10+2 documentation, automated customs broker import clearance. </p> </div> </div> </label> <label class="border-border hover:border-primary/40 bg-card flex cursor-pointer items-start justify-between gap-3 rounded-lg border p-3.5 transition-colors" > <div class="flex items-start gap-3"> <ui-checkbox [checked]="addInsurance()" (checkedChange)="addInsurance.set(!!$event)" /> <div class="space-y-0.5"> <div class="flex items-center gap-2"> <span class="text-foreground text-sm font-medium">All-Risk Cargo Insurance</span> <span ui-badge variant="outline" class="text-xs font-normal tabular-nums" >+{{ formatMoney(insuranceUsd) }}</span > </div> <p class="text-muted-foreground text-xs"> Comprehensive door-to-door insurance coverage up to $100,000 against damage or loss. </p> </div> </div> </label> <label class="border-border hover:border-primary/40 bg-card flex cursor-pointer items-start justify-between gap-3 rounded-lg border p-3.5 transition-colors" > <div class="flex items-start gap-3"> <ui-checkbox [checked]="addLiftgate()" (checkedChange)="addLiftgate.set(!!$event)" /> <div class="space-y-0.5"> <div class="flex items-center gap-2"> <span class="text-foreground text-sm font-medium">Liftgate at Delivery</span> <span ui-badge variant="outline" class="text-xs font-normal tabular-nums" >+{{ formatMoney(liftgateUsd) }}</span > </div> <p class="text-muted-foreground text-xs"> Hydraulic liftgate truck delivery for destinations without a dedicated loading dock. </p> </div> </div> </label> </div> </div> </div> <!-- Right Column --> <div class="space-y-6 lg:sticky lg:top-8 lg:col-span-5"> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="pb-4"> <div class="flex flex-wrap items-center justify-between"> <span class="text-muted-foreground text-xs font-semibold tracking-wider uppercase" >Instant Spot Rates</span > <span ui-badge variant="default" class="gap-1 text-xs"> <lucide-icon [img]="Zap" class="size-3" /> Live Quote </span> </div> <div class="border-border/60 mt-2 flex flex-wrap items-center justify-between border-b pb-3 text-xs"> <div class="text-foreground flex items-center gap-1.5 font-medium"> <span>{{ origin() }}</span> <lucide-icon [img]="ArrowRight" class="text-muted-foreground size-3" /> <span>{{ destination() }}</span> </div> <span class="text-muted-foreground tabular-nums" >{{ pallets() }} Pallets · {{ totalVolumeCbm().toFixed(2) }} CBM</span > </div> <div class="mt-4 space-y-1"> <div class="text-muted-foreground text-xs font-medium">Total Estimated Landed Freight Cost</div> <div class="flex items-baseline gap-2"> <span class="text-foreground text-4xl font-bold tracking-tight tabular-nums">{{ formatMoney(activeTotalPriceUsd()) }}</span> <span class="text-muted-foreground text-xs font-medium">({{ selectedCurrency() }})</span> </div> <div class="text-muted-foreground flex items-center gap-3 pt-1 text-xs"> <span class="flex items-center gap-1"> <lucide-icon [img]="Clock" class="text-primary size-3" /> Transit: {{ activeTransitTime() }} </span> <span>·</span> <span class="flex items-center gap-1"> <lucide-icon [img]="Leaf" class="text-success size-3" /> Est. {{ activeCarbonEmission() }} </span> </div> </div> </div> <div ui-card-content class="space-y-4 pt-0"> <div class="space-y-2"> <span class="text-foreground text-xs font-semibold tracking-wider uppercase" >Compare Multimodal Options:</span > <div [class]="quoteOptionClass('ocean-fcl')" (click)="selectedMode.set('ocean-fcl')"> <div class="flex flex-wrap items-center justify-between"> <div class="flex items-center gap-2"> <lucide-icon [img]="Ship" class="text-primary size-4 shrink-0" /> <div> <div class="text-foreground flex items-center gap-1.5 text-xs font-semibold"> Ocean FCL (20ft Container) <span ui-badge variant="default" class="px-1.5 py-0.5 text-xs">Recommended</span> </div> <div class="text-muted-foreground text-xs">Transit: 14-18 days · 1.2 tCO2</div> </div> </div> <div class="text-right"> <div class="text-foreground text-sm font-bold tabular-nums"> {{ formatMoney(oceanFclBaseUsd() + addonsTotalUsd()) }} </div> <div class="text-muted-foreground text-xs">Port-to-Port</div> </div> </div> </div> <div [class]="quoteOptionClass('ocean-lcl')" (click)="selectedMode.set('ocean-lcl')"> <div class="flex flex-wrap items-center justify-between"> <div class="flex items-center gap-2"> <lucide-icon [img]="Boxes" class="text-primary size-4 shrink-0" /> <div> <div class="text-foreground flex items-center gap-1.5 text-xs font-semibold"> Ocean LCL (Shared) <span ui-badge variant="outline" class="px-1.5 py-0.5 text-xs font-normal">Economy</span> </div> <div class="text-muted-foreground text-xs">Transit: 18-22 days · 0.8 tCO2</div> </div> </div> <div class="text-right"> <div class="text-foreground text-sm font-bold tabular-nums"> {{ formatMoney(oceanLclBaseUsd() + addonsTotalUsd()) }} </div> <div class="text-muted-foreground text-xs">CFS-to-CFS</div> </div> </div> </div> <div [class]="quoteOptionClass('air')" (click)="selectedMode.set('air')"> <div class="flex flex-wrap items-center justify-between"> <div class="flex items-center gap-2"> <lucide-icon [img]="Plane" class="text-primary size-4 shrink-0" /> <div> <div class="text-foreground flex items-center gap-1.5 text-xs font-semibold"> Express Air Freight <span ui-badge variant="secondary" class="px-1.5 py-0.5 text-xs">Fastest</span> </div> <div class="text-muted-foreground text-xs">Transit: 3-5 days · 4.8 tCO2</div> </div> </div> <div class="text-right"> <div class="text-foreground text-sm font-bold tabular-nums"> {{ formatMoney(airFreightBaseUsd() + addonsTotalUsd()) }} </div> <div class="text-muted-foreground text-xs">Airport-to-Airport</div> </div> </div> </div> </div> <ui-separator /> <div class="space-y-2 text-xs"> <span class="text-muted-foreground font-medium tracking-wider uppercase">Itemized Breakdown</span> <div class="space-y-1.5"> <div class="flex flex-wrap items-center justify-between"> <span class="text-muted-foreground">Base Freight Carrier Rate</span> <span class="text-foreground font-medium tabular-nums">{{ formatMoney(activeBasePriceUsd()) }}</span> </div> @if (addCustoms()) { <div class="flex flex-wrap items-center justify-between"> <span class="text-muted-foreground">Customs Clearance Service</span> <span class="text-foreground font-medium tabular-nums">+{{ formatMoney(customsUsd) }}</span> </div> } @if (addInsurance()) { <div class="flex flex-wrap items-center justify-between"> <span class="text-muted-foreground">All-Risk Cargo Insurance</span> <span class="text-foreground font-medium tabular-nums">+{{ formatMoney(insuranceUsd) }}</span> </div> } @if (addLiftgate()) { <div class="flex flex-wrap items-center justify-between"> <span class="text-muted-foreground">Destination Liftgate Equipment</span> <span class="text-foreground font-medium tabular-nums">+{{ formatMoney(liftgateUsd) }}</span> </div> } <div class="text-muted-foreground flex flex-wrap items-center justify-between"> <span>Bunker / Fuel Surcharge (BAF)</span> <span class="text-success font-medium">Included</span> </div> </div> </div> <ui-separator /> <ul class="text-muted-foreground space-y-1.5 text-xs"> <li class="flex items-center gap-2"> <lucide-icon [img]="Check" class="text-primary size-3.5 shrink-0" /> <span>Rate locked for 7 calendar days</span> </li> <li class="flex items-center gap-2"> <lucide-icon [img]="Check" class="text-primary size-3.5 shrink-0" /> <span>IATA & FMC compliant licensed forwarders</span> </li> <li class="flex items-center gap-2"> <lucide-icon [img]="Check" class="text-primary size-3.5 shrink-0" /> <span>Automated EDI customs documentation & bill of lading</span> </li> </ul> </div> <div ui-card-footer class="flex flex-col gap-2.5 pt-2"> <button ui-button class="w-full gap-2 font-semibold shadow-xs" size="lg" (click)="handleBookQuote()"> @if (isBooked()) { <lucide-icon [img]="CheckCircle2" class="text-success size-4" /> Freight Quote Booked! } @else { Book Freight Quote <lucide-icon [img]="ArrowRight" class="size-4" /> } </button> <button ui-button aria-label="Download attachment" variant="outline" class="w-full gap-2 text-xs" size="default" (click)="handleDownloadPdf()" > @if (!isDownloading()) { <lucide-icon [img]="Download" class="size-3.5" /> } @else { <lucide-icon [img]="FileText" class="size-3.5 animate-pulse" /> } {{ isDownloading() ? 'Generating PDF Manifest...' : 'Download Detailed Quote PDF' }} </button> </div> </div> </div> </div> </div> `, }) export class UiFreightQuoteCalculatorComponent { protected readonly ArrowLeftRight = ArrowLeftRight protected readonly ArrowRight = ArrowRight protected readonly Boxes = Boxes protected readonly Check = Check protected readonly CheckCircle2 = CheckCircle2 protected readonly Clock = Clock protected readonly Download = Download protected readonly FileText = FileText protected readonly Info = Info protected readonly Leaf = Leaf protected readonly Package = Package protected readonly Plane = Plane protected readonly Scale = Scale protected readonly Ship = Ship protected readonly Truck = Truck protected readonly Zap = Zap protected readonly originPorts = ORIGIN_PORTS protected readonly destinationPorts = DESTINATION_PORTS protected readonly currencyList = CURRENCY_LIST protected readonly customsUsd = CUSTOMS_USD protected readonly insuranceUsd = INSURANCE_USD protected readonly liftgateUsd = LIFTGATE_USD @Input() set initialOrigin(v: string) { if (v) this.origin.set(v) } @Input() set initialDestination(v: string) { if (v) this.destination.set(v) } @Input() set initialPallets(v: number | string) { const n = Number(v) if (n !== undefined && n !== null && !Number.isNaN(n)) this.pallets.set(n) } @Input() set initialLengthCm(v: number | string) { const n = Number(v) if (n !== undefined && n !== null && !Number.isNaN(n)) this.lengthCm.set(n) } @Input() set initialWidthCm(v: number | string) { const n = Number(v) if (n !== undefined && n !== null && !Number.isNaN(n)) this.widthCm.set(n) } @Input() set initialHeightCm(v: number | string) { const n = Number(v) if (n !== undefined && n !== null && !Number.isNaN(n)) this.heightCm.set(n) } @Input() set initialGrossWeightKg(v: number | string) { const n = Number(v) if (n !== undefined && n !== null && !Number.isNaN(n)) this.grossWeightKg.set(n) } @Input() set initialCurrency(v: string) { if (v) this.selectedCurrency.set(v) } @Input('class') className?: string readonly origin = signal('CNSHA') readonly destination = signal('USLAX') readonly pallets = signal(6) readonly lengthCm = signal(120) readonly widthCm = signal(80) readonly heightCm = signal(160) readonly grossWeightKg = signal(400) readonly selectedCurrency = signal('USD') readonly addCustoms = signal(false) readonly addInsurance = signal(false) readonly addLiftgate = signal(false) readonly selectedMode = signal<FreightMode>('ocean-fcl') readonly isBooked = signal(false) readonly isDownloading = signal(false) private bookTimer: ReturnType<typeof setTimeout> | undefined private downloadTimer: ReturnType<typeof setTimeout> | undefined readonly singlePalletVolumeCbm = computed(() => { const l = Number(this.lengthCm()) || 0 const w = Number(this.widthCm()) || 0 const h = Number(this.heightCm()) || 0 return (l * w * h) / 1_000_000 }) readonly totalVolumeCbm = computed(() => (Number(this.pallets()) || 0) * this.singlePalletVolumeCbm()) readonly totalGrossWeightKg = computed(() => (Number(this.pallets()) || 0) * (Number(this.grossWeightKg()) || 0)) readonly airVolumetricWeightKg = computed(() => Math.round(this.totalVolumeCbm() * (1000 / 6))) readonly oceanRevenueTon = computed(() => Math.max(this.totalVolumeCbm(), this.totalGrossWeightKg() / 1000)) readonly fclContainersCount = computed(() => Math.max(1, Math.ceil((Number(this.pallets()) || 0) / 10))) readonly oceanFclBaseUsd = computed(() => this.fclContainersCount() * 2850) readonly oceanLclBaseUsd = computed(() => Math.round((this.totalVolumeCbm() || 1) * 140 + 130)) readonly airFreightBaseUsd = computed(() => Math.round((this.airVolumetricWeightKg() || 100) * 4.18)) readonly groundFreightBaseUsd = computed(() => (Number(this.pallets()) || 1) * 265) readonly addonsTotalUsd = computed( () => (this.addCustoms() ? CUSTOMS_USD : 0) + (this.addInsurance() ? INSURANCE_USD : 0) + (this.addLiftgate() ? LIFTGATE_USD : 0), ) readonly activeBasePriceUsd = computed(() => { switch (this.selectedMode()) { case 'ocean-fcl': return this.oceanFclBaseUsd() case 'ocean-lcl': return this.oceanLclBaseUsd() case 'air': return this.airFreightBaseUsd() case 'ground': return this.groundFreightBaseUsd() } }) readonly activeTotalPriceUsd = computed(() => this.activeBasePriceUsd() + this.addonsTotalUsd()) readonly activeTransitTime = computed(() => { switch (this.selectedMode()) { case 'ocean-fcl': return '14-18 days' case 'ocean-lcl': return '18-22 days' case 'air': return '3-5 days' case 'ground': return '5-7 days' } }) readonly activeCarbonEmission = computed(() => { switch (this.selectedMode()) { case 'ocean-fcl': return `${(1.2 * this.fclContainersCount()).toFixed(1)} tCO2` case 'ocean-lcl': return `${(this.totalVolumeCbm() * 0.087).toFixed(1)} tCO2` case 'air': return `${(this.airVolumetricWeightKg() * 0.003125).toFixed(1)} tCO2` case 'ground': return `${(Number(this.pallets()) * 0.26).toFixed(1)} tCO2` } }) get rootClass(): string { return cn('mx-auto w-full max-w-6xl space-y-8 p-4 sm:p-6 lg:p-8', this.className) } modeCardClass(mode: FreightMode): string { return cn( 'border-border focus-visible:ring-ring relative flex flex-col justify-between gap-3 rounded-lg border p-3.5 text-left transition-colors outline-none focus-visible:ring-2', this.selectedMode() === mode ? 'border-primary bg-primary/5 ring-primary ring-1' : 'hover:border-border/80 hover:bg-muted/30 bg-card', ) } quoteOptionClass(mode: FreightMode): string { return cn( 'border-border hover:border-primary/50 cursor-pointer rounded-lg border p-3 transition-colors', this.selectedMode() === mode ? 'border-primary bg-primary/5 ring-primary ring-1' : 'bg-card', ) } formatMoney(amountInUsd: number): string { const current = CURRENCIES[this.selectedCurrency()] ?? CURRENCIES['USD']! const converted = amountInUsd * current.rate return new Intl.NumberFormat('en-US', { style: 'currency', currency: current.code, minimumFractionDigits: 2, maximumFractionDigits: 2, }).format(converted) } swapRoute(): void { const prev = this.origin() this.origin.set(this.destination()) this.destination.set(prev) } setPallets(raw: string | number): void { this.pallets.set(Math.max(1, parseInt(String(raw), 10) || 1)) } setLengthCm(raw: string | number): void { this.lengthCm.set(Math.max(1, parseInt(String(raw), 10) || 1)) } setWidthCm(raw: string | number): void { this.widthCm.set(Math.max(1, parseInt(String(raw), 10) || 1)) } setHeightCm(raw: string | number): void { this.heightCm.set(Math.max(1, parseInt(String(raw), 10) || 1)) } setGrossWeightKg(raw: string | number): void { this.grossWeightKg.set(Math.max(1, parseInt(String(raw), 10) || 1)) } handleBookQuote(): void { this.isBooked.set(true) clearTimeout(this.bookTimer) this.bookTimer = setTimeout(() => this.isBooked.set(false), 3500) } handleDownloadPdf(): void { this.isDownloading.set(true) clearTimeout(this.downloadTimer) this.downloadTimer = setTimeout(() => this.isDownloading.set(false), 2000) } }src/app/components/blocks/freight-quote-calculator/index.ts0.1 kB
export { UiFreightQuoteCalculatorComponent, type FreightMode } from './freight-quote-calculator.component'
Raw manifest:https://uipkge.dev/r/angular/freight-quote-calculator.json