From 7c566075828fc9a62efbf7f78603b66f3a9fd76a Mon Sep 17 00:00:00 2001 From: Nathnael Date: Wed, 19 Aug 2026 10:11:48 +0000 Subject: [PATCH 01/75] fix(filters): honor a date filter's own operator, not always "between" DateBody always initialized its op state to DEFAULT_OP.date ("between"), ignoring a def's `operators` restriction. A single-operator exact-date filter (e.g. `operators: ["before"]`) opened on the range UI with no way off it, since OperatorSelect hides itself when there's only one choice. Default to the def's first allowed operator instead. --- .../backoffice/src/components/filters/bodies/DateBody.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/edr-freight-web/backoffice/src/components/filters/bodies/DateBody.tsx b/apps/edr-freight-web/backoffice/src/components/filters/bodies/DateBody.tsx index b2032170d..32970abef 100644 --- a/apps/edr-freight-web/backoffice/src/components/filters/bodies/DateBody.tsx +++ b/apps/edr-freight-web/backoffice/src/components/filters/bodies/DateBody.tsx @@ -15,7 +15,11 @@ import type { FilterBodyProps } from "./TextBody"; // i18n.language !== "en" branch here when this body is first wired into a // record-management page (Phase 4 of the filter-bar rollout). export function DateBody({ def, value, onChange, onClose }: FilterBodyProps) { - const [op, setOp] = useState(value?.op ?? DEFAULT_OP.date); + // DEFAULT_OP.date is always "between" — a def restricted to a single + // non-default operator (e.g. `operators: ["before"]` for an exact-date + // filter) would otherwise open on the range UI with no way to switch off + // it, since OperatorSelect hides itself when there's only one choice. + const [op, setOp] = useState(value?.op ?? def.operators?.[0] ?? DEFAULT_OP.date); // Mantine 9's date inputs speak `YYYY-MM-DD` strings, not Date objects. const [from, setFrom] = useState(value?.v[0]?.slice(0, 10) ?? null); const [to, setTo] = useState(value?.v[1]?.slice(0, 10) ?? null); From 6687def4afc36c36e6c9483e7af982efb297d0f5 Mon Sep 17 00:00:00 2001 From: Nathnael Date: Wed, 19 Aug 2026 10:12:03 +0000 Subject: [PATCH 02/75] feat(reports): use the shared FilterBar instead of ReportFilters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swaps ReportView's bespoke ReportFilters for the same FilterBar/useFilters combo BookingRequestsPage uses — pills, saved-view-ready state, URL sync. toFilterDefs() maps the report catalog's own filter vocabulary (daterange/date/select/multiselect/text) onto FilterDef, matched against every report definition's backend param handling. The search box only shows for reports that actually implement `search` server-side, so it's not a dead control on the rest. ReportFilters.tsx is now dead, removed. --- .../src/components/reports/ReportFilters.tsx | 110 -------------- .../src/components/reports/ReportView.tsx | 139 ++++++++++++++---- 2 files changed, 110 insertions(+), 139 deletions(-) delete mode 100644 apps/edr-freight-web/backoffice/src/components/reports/ReportFilters.tsx diff --git a/apps/edr-freight-web/backoffice/src/components/reports/ReportFilters.tsx b/apps/edr-freight-web/backoffice/src/components/reports/ReportFilters.tsx deleted file mode 100644 index e44e97ec8..000000000 --- a/apps/edr-freight-web/backoffice/src/components/reports/ReportFilters.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { Group, MultiSelect, Select, TextInput } from "@mantine/core"; -import { DateInput, DatePickerInput } from "@mantine/dates"; -import { Search } from "lucide-react"; - -import { getDateRangePresets } from "@/components/common/dateRangePresets"; -import type { ReportFilterDef } from "@/types/reports"; - -export interface ReportFilterValues { - [param: string]: string | undefined; -} - -interface ReportFiltersProps { - filters: ReportFilterDef[]; - values: ReportFilterValues; - onChange: (values: ReportFilterValues) => void; -} - -const toDate = (value: string | undefined): Date | null => (value ? new Date(value) : null); -const fromDate = (value: string | null): string | undefined => value ?? undefined; - -/** Renders one widget per report-declared filter and reports raw param values back up. */ -export function ReportFilters({ filters, values, onChange }: ReportFiltersProps) { - if (!filters.length) return null; - - const set = (patch: ReportFilterValues) => onChange({ ...values, ...patch }); - - return ( - - {filters.map((filter) => { - switch (filter.type) { - case "daterange": - return ( - - set({ [`${filter.key}From`]: fromDate(from), [`${filter.key}To`]: fromDate(to) }) - } - presets={getDateRangePresets()} - radius="md" - size="sm" - clearable - w={230} - /> - ); - case "date": - return ( - set({ [filter.key]: fromDate(d) })} - radius="md" - size="sm" - clearable - w={150} - /> - ); - case "select": - return ( - + setDraft((d) => ({ ...d, [field.name]: e.target.value })) + } + /> + + {field.unit} + + +

+ {invalid(field) + ? field.integer + ? "Must be a whole number above zero" + : "Must be above zero" + : field.hint} +

+ + ))} + + + ))} + + {!canEdit && ( +

+ You can view these standards but not change them. +

+ )} + + ); +} diff --git a/apps/edr-freight-web/backoffice/src/services/operationsStandards.service.ts b/apps/edr-freight-web/backoffice/src/services/operationsStandards.service.ts new file mode 100644 index 000000000..f9086570a --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/services/operationsStandards.service.ts @@ -0,0 +1,51 @@ +import { api as client } from "../auth/http"; +import { unwrap } from "@/utils/endpoint"; +import { URL_CONSTANTS } from "@/constants/URLS"; +import type { ApiResponse } from "@/types/apiResponse"; + +const BASE = URL_CONSTANTS.OPERATIONS_STANDARDS.BASE; + +/** + * The railway's operating standards — the numbers the operations reports + * measure actual performance against. One row, edited here. + */ +export interface OperationsStandards { + id: string; + stationStandardHoursEthiopia: number; + stationStandardHoursDjibouti: number; + cycleStandardHoursContainer: number; + cycleStandardHoursBulkDmp: number; + cycleStandardHoursBulkNagad: number; + cycleStandardHoursBulkBcc: number; + defaultLegStandardHours: number; + delayToleranceMinutes: number; + chargedTonsFull20ft: number; + chargedTonsFull40ft: number; + chargedTonsEmpty20ft: number; + chargedTonsEmpty40ft: number; + chargedTonsPerWagonGeneral: number; + chargedTonsPerWagonPerishable: number; + defaultFullTrainsetWagons: number; + updatedAt?: string; +} + +export type OperationsStandardsPatch = Partial< + Omit +>; + +export const operationsStandardsService = { + get: async (): Promise => { + const response = await client.get>(BASE); + return unwrap(response.data); + }, + + update: async ( + patch: OperationsStandardsPatch, + ): Promise => { + const response = await client.patch>( + BASE, + patch, + ); + return unwrap(response.data); + }, +}; diff --git a/apps/edr-freight-web/backoffice/src/services/ruleEngine/ruleEngine.service.ts b/apps/edr-freight-web/backoffice/src/services/ruleEngine/ruleEngine.service.ts index 48f944c21..fc7bce8f0 100644 --- a/apps/edr-freight-web/backoffice/src/services/ruleEngine/ruleEngine.service.ts +++ b/apps/edr-freight-web/backoffice/src/services/ruleEngine/ruleEngine.service.ts @@ -96,6 +96,7 @@ const RESOURCE_BASE: Record = { "weight-limit-rules": URL_CONSTANTS.RULE_ENGINE.WEIGHT_LIMIT_RULES, yards: URL_CONSTANTS.RULE_ENGINE.YARDS, "yard-distances": URL_CONSTANTS.RULE_ENGINE.YARD_DISTANCES, + "operations-targets": URL_CONSTANTS.RULE_ENGINE.OPERATIONS_TARGETS, "shipping-lines": URL_CONSTANTS.RULE_ENGINE.SHIPPING_LINES, rates: URL_CONSTANTS.RULE_ENGINE.RATES, "approval-rules": URL_CONSTANTS.RULE_ENGINE.APPROVAL_RULES, diff --git a/apps/edr-freight-web/backoffice/src/theme/freight-brand.ts b/apps/edr-freight-web/backoffice/src/theme/freight-brand.ts index 6c1efd3f3..4117f503c 100644 --- a/apps/edr-freight-web/backoffice/src/theme/freight-brand.ts +++ b/apps/edr-freight-web/backoffice/src/theme/freight-brand.ts @@ -86,7 +86,7 @@ export const freightMantineTheme = createTheme({ black: "#10202F", fontFamily: - '"Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', + '"Space Grotesk", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', defaultRadius: "md", @@ -123,7 +123,7 @@ export const freightMantineTheme = createTheme({ }, headings: { - fontFamily: '"Inter", var(--mantine-font-family)', + fontFamily: '"Space Grotesk", var(--mantine-font-family)', fontWeight: "700", sizes: { h1: { fontSize: "36px", lineHeight: "1.1", fontWeight: "800" }, diff --git a/apps/edr-freight-web/backoffice/src/types/rule-engine/index.ts b/apps/edr-freight-web/backoffice/src/types/rule-engine/index.ts index 5d886043a..9f530d95b 100644 --- a/apps/edr-freight-web/backoffice/src/types/rule-engine/index.ts +++ b/apps/edr-freight-web/backoffice/src/types/rule-engine/index.ts @@ -11,7 +11,8 @@ export type RuleEngineResourceSlug = | "shipping-lines" | "rates" | "approval-rules" - | "transit-agents"; + | "transit-agents" + | "operations-targets"; /** * Mirrors the API's shared `PaginationMeta` (@edr/types). The `has*` flags are From 174bc1a7bd409f7548ccffacadab538a512aa9d3 Mon Sep 17 00:00:00 2001 From: Marshal Date: Wed, 19 Aug 2026 17:07:48 +0000 Subject: [PATCH 07/75] Implement new feature for user authentication and improve error handling --- .../src/pages/EDRFreightLandingPage.tsx | 1763 ++++++++++++----- 1 file changed, 1315 insertions(+), 448 deletions(-) diff --git a/apps/edr-freight-web/portal/src/pages/EDRFreightLandingPage.tsx b/apps/edr-freight-web/portal/src/pages/EDRFreightLandingPage.tsx index b6f4787d6..59e6f2ad6 100644 --- a/apps/edr-freight-web/portal/src/pages/EDRFreightLandingPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/EDRFreightLandingPage.tsx @@ -1,139 +1,571 @@ +import { useEffect, useRef, useState } from "react"; import { Link } from "react-router-dom"; import { ArrowRight, - BarChart3, + BellRing, + Briefcase, + Building2, + ChartColumn, + Check, CheckCircle2, Clock3, - Globe2, + FileCheck, + FileText, Mail, + Map as MapIcon, MapPin, Menu, + Package, + PackageSearch, Phone, - ShieldCheck, - Train, + Radar, + Receipt, + Route as RouteIcon, + Satellite, + Ship, + Timer, + TrainFront, + TrendingDown, + TrendingUp, Truck, - Users, } from "lucide-react"; +/* ------------------------------------------------------------------ */ +/* Motion helpers */ +/* ------------------------------------------------------------------ */ + +/** Reveals children once they scroll into view. No animation library needed. */ +function useReveal() { + const ref = useRef(null); + const [shown, setShown] = useState(false); + + useEffect(() => { + const node = ref.current; + if (!node) return; + if (typeof IntersectionObserver === "undefined") { + setShown(true); + return; + } + + const observer = new IntersectionObserver( + (entries) => { + if (entries.some((entry) => entry.isIntersecting)) { + setShown(true); + observer.disconnect(); + } + }, + { rootMargin: "0px 0px -12% 0px", threshold: 0.15 }, + ); + + observer.observe(node); + return () => observer.disconnect(); + }, []); + + return { ref, shown }; +} + +function Reveal({ + children, + delay = 0, + className = "", +}: { + children: React.ReactNode; + delay?: number; + className?: string; +}) { + const { ref, shown } = useReveal(); + + return ( +
+ {children} +
+ ); +} + +/** Counts up to `value` when scrolled into view. Keeps prefix/suffix intact. */ +function CountUp({ + value, + decimals = 0, + prefix = "", + suffix = "", + duration = 1400, +}: { + value: number; + decimals?: number; + prefix?: string; + suffix?: string; + duration?: number; +}) { + const { ref, shown } = useReveal(); + const [display, setDisplay] = useState(0); + + useEffect(() => { + if (!shown) return; + if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) { + setDisplay(value); + return; + } + + let frame = 0; + const start = performance.now(); + + const tick = (now: number) => { + const progress = Math.min(1, (now - start) / duration); + // easeOutCubic + setDisplay(value * (1 - Math.pow(1 - progress, 3))); + if (progress < 1) frame = requestAnimationFrame(tick); + }; + + frame = requestAnimationFrame(tick); + return () => cancelAnimationFrame(frame); + }, [shown, value, duration]); + + return ( + + {prefix} + {display.toFixed(decimals)} + {suffix} + + ); +} + +/* ------------------------------------------------------------------ */ +/* Content */ +/* ------------------------------------------------------------------ */ + +const navLinks = [ + { label: "Features", href: "#features" }, + { label: "Live ops", href: "#showcase" }, + { label: "Corridors", href: "#corridors" }, + { label: "How it works", href: "#how" }, + { label: "Contact", href: "#contact" }, +]; + +const heroTrust = [ + "Telebirr & CBE Birr payments", + "Fayda ID verified", + "Customs-ready documents", +]; + +const trackingStops = [ + { name: "Mojo Dry Port", time: "Loaded · 06:40", state: "done" }, + { name: "Adama", time: "Departed · 08:15", state: "done" }, + { name: "Dire Dawa", time: "ETA 14:30", state: "current" }, + { name: "Dewele (Border)", time: "ETA 18:05", state: "idle" }, + { name: "Djibouti Port", time: "ETA 22:40", state: "idle" }, +] as const; + const stats = [ { - label: "Active Corridors", - value: "24+", - icon: Globe2, + icon: RouteIcon, + value: , + label: "Addis Ababa → Djibouti · electrified", + trend: "Standard gauge", + down: false, + chart: [20, 24, 28, 30, 34, 36, 40, 44], }, { - label: "Monthly Shipments", - value: "12K+", - icon: Truck, + icon: Package, + value: , + label: "Monthly consignments booked", + trend: "+18% vs last month", + down: false, + chart: [16, 22, 19, 26, 30, 28, 36, 44], }, { - label: "Fleet Coverage", - value: "16 Trains", - icon: Train, + icon: TrainFront, + value: , + label: "Train sets in active rotation", + trend: "14 running now", + down: false, + fleet: true, }, { - label: "On-time Delivery", - value: "100%", - icon: Clock3, + icon: Timer, + value: , + label: "Booking to confirmed wagon", + trend: "-62% since launch", + down: true, + chart: [44, 40, 36, 30, 26, 20, 16, 12], }, ]; +const stations = [ + { name: "Sebeta", hub: false }, + { name: "Addis Ababa", hub: true, note: "Km 0" }, + { name: "Mojo", hub: true }, + { name: "Adama", hub: false }, + { name: "Awash", hub: false }, + { name: "Mieso", hub: false }, + { name: "Dire Dawa", hub: true }, + { name: "Dewele", hub: true, note: "Customs", border: true }, + { name: "Ali Sabieh", hub: false }, + { name: "Djibouti Port", hub: true, note: "Km 752" }, +]; + +const lanes = [ + { from: "Mojo Dry Port", to: "Djibouti Port", transit: "~16h", kind: "Containers" }, + { from: "Addis Ababa", to: "Djibouti Port", transit: "~19h", kind: "Mixed cargo" }, + { from: "Djibouti Port", to: "Mojo Dry Port", transit: "~16h", kind: "Imports" }, + { from: "Dire Dawa", to: "Djibouti Port", transit: "~8h", kind: "Bulk" }, +]; + +const steps = [ + { + num: "01", + title: "Verify & register", + body: "Sign up with your company TIN, verify with Fayda ID, and get trade access approved by EDR.", + }, + { + num: "02", + title: "Book & pay", + body: "Pick origin, destination, cargo type and schedule. Pay with Telebirr, CBE Birr or contract credit.", + }, + { + num: "03", + title: "Load & clear", + body: "Wagon allocation, loading confirmation, interchange document and customs stamps — all digital.", + }, + { + num: "04", + title: "Track & settle", + body: "Follow the train live, get gate notifications, and download the final invoice when the cargo is released.", + }, +]; + +const audiences = [ + { + icon: Building2, + title: "Shippers & importers", + body: "Book, pay, track and download documents from one dashboard.", + items: ["Consignment booking", "Live tracking", "Invoices & receipts"], + }, + { + icon: Ship, + title: "Shipping lines", + body: "Manage container inventory, bookings and interchange with the railway.", + items: ["Container management", "Booking completion", "Interchange documents"], + }, + { + icon: Briefcase, + title: "Transit agents", + body: "Handle customs declarations, stamps and signatures on behalf of clients.", + items: ["Digital stamps", "E-signatures", "Compliance checks"], + }, + { + icon: TrainFront, + title: "EDR operations", + body: "Train sets, locomotives, wagons, schedules, maintenance and incidents.", + items: ["Train builder", "Fleet & fuel", "Incident reporting"], + }, +]; + +/* ------------------------------------------------------------------ */ +/* Feature previews */ +/* ------------------------------------------------------------------ */ + +function Chip({ + children, + tone = "green", + icon: Icon, +}: { + children: React.ReactNode; + tone?: "green" | "amber" | "muted"; + icon?: React.ElementType; +}) { + const tones = { + green: "bg-emerald-50 text-emerald-700", + amber: "bg-amber-50 text-amber-700", + muted: "bg-slate-100 text-slate-500", + }; + + return ( + + {Icon ? : null} + {children} + + ); +} + +function BookingPreview() { + return ( +
+
+
+

CN-2026-08421

+

+ Mojo → Djibouti · 40ft × 8 +

+
+ Confirmed +
+ +
+ + Depart Thu 21 Aug · TS-14 + + ETB 184,200 +
+
+ ); +} + +function GpsPreview() { + return ( +
+
+ +
+ +
+ {["Mojo", "Adama", "Awash", "Dire Dawa", "Djibouti"].map((s) => ( + {s} + ))} +
+ +
+ Loco 3421 · 64 km/h + + ETA 14:30 + +
+
+ ); +} + +function DocsPreview() { + const docs = [ + { name: "Interchange document", status: "Signed" }, + { name: "Customs declaration", status: "Stamped" }, + { name: "Commercial invoice", status: "Pending" }, + ]; + + return ( +
+ {docs.map((doc) => { + const pending = doc.status === "Pending"; + + return ( +
+ + {doc.name} + + {doc.status} + +
+ ); + })} +
+ ); +} + +function BillingPreview() { + return ( +
+
+
+

INV-1042 · Aug

+

+ ETB 1,284,600 +

+
+ Paid +
+ +
+ {["Telebirr", "CBE Birr", "eBirr", "Card"].map((method) => ( + + {method} + + ))} +
+
+ ); +} + +function MilePreview() { + const legs = [ + { icon: Truck, name: "Truck" }, + { icon: TrainFront, name: "Rail" }, + { icon: Truck, name: "Truck" }, + ]; + + return ( +
+
+ {legs.map((leg, index) => { + const Icon = leg.icon; + const isRail = index === 1; + + return ( +
+
+ + + + {leg.name} +
+ + {index < legs.length - 1 ? ( + + ) : null} +
+ ); + })} +
+ +
+ Warehouse, Gerji + Consignee, Djibouti +
+
+ ); +} + +function AnalyticsPreview() { + const bars = [22, 30, 26, 36, 40, 34, 46, 52, 48, 56]; + + return ( +
+
+ {bars.map((height, index) => ( + = 8 ? "bg-edr-primary" : "bg-emerald-200" + }`} + style={{ + height: `${height}%`, + animationDelay: `${index * 60}ms`, + }} + /> + ))} +
+ +
+ + Corridor throughput · TEU/week + + +12% +
+
+ ); +} + const features = [ { - title: "Real-time Shipment Tracking", - description: - "Track consignments across Addis Ababa, Dire Dawa, Djibouti, Mojo, and all major freight corridors.", + icon: PackageSearch, + title: "Consignment booking", + body: "Book containers or bulk cargo, pick a train schedule, and get a confirmed wagon allocation in minutes.", + preview: , + }, + { + icon: Radar, + title: "Live GPS tracking", + body: "Locomotive telemetry and wagon-level position across every station from Mojo to Djibouti Port.", + preview: , + }, + { + icon: FileCheck, + title: "Customs & documents", + body: "Interchange documents, stamps and e-signatures generated and shared with transit agents automatically.", + preview: , + }, + { + icon: Receipt, + title: "Billing & payments", + body: "Contract rates, invoices and settlement via Telebirr, CBE Birr, eBirr or card — with full audit trail.", + preview: , + }, + { icon: Truck, + title: "First & last mile", + body: "Request trucking to and from the railhead and see it on the same timeline as the rail leg.", + preview: , }, { - title: "Rail Freight Operations", - description: - "Monitor train schedules, operational performance, maintenance, and corridor activity.", - icon: Train, - }, - { - title: "Smart Analytics Dashboard", - description: - "Visualize operational insights, shipment trends, and corridor performance in real time.", - icon: BarChart3, - }, - { - title: "Enterprise-grade Security", - description: - "Secure portal access, billing workflows, document management, and customer operations.", - icon: ShieldCheck, + icon: ChartColumn, + title: "Operations analytics", + body: "Corridor throughput, on-time performance, fleet utilisation and incident reporting for operators.", + preview: , }, ]; -const corridors = [ - "Addis Ababa → Djibouti", - "Dire Dawa → Djibouti", - "Adama → Dire Dawa", - "Mojo → Djibouti", - "Awash → Holhol", - "Mieso → Aysha", -]; +/* ------------------------------------------------------------------ */ +/* Page */ +/* ------------------------------------------------------------------ */ export default function EDRFreightLandingPage() { return (
+ + {/* Navbar */} -
+
-
-
- -
+ + + + -
-

EDR Freight

- -

+ + EDR Freight + Rail Logistics Platform -

-
-
+ + + -
{/* Hero */} -
-
+
+
+
-
+
-
- - Ethiopia–Djibouti Railway Freight Platform -
+ + + + Live on the Addis Ababa – Djibouti corridor + + -

- Smarter Railway Freight Logistics For Modern Operations -

+ +

+ Move cargo by rail. + Track every wagon. +

+
-

- EDR Freight enables logistics companies and railway operators to - manage shipments, monitor freight corridors, optimize train - operations, and streamline enterprise logistics workflows. -

+ +

+ EDR Freight is the booking, tracking and billing platform for the + Ethio-Djibouti Railway. Book consignments, schedule train sets, + clear customs documents and settle invoices — in one portal. +

+
-
- - Get Started - - - - - Login - -
- -
- {[ - "Real-time Tracking", - "Railway Analytics", - "Secure Operations", - "Multi-corridor Freight", - ].map((item) => ( -
+
+ - - {item} -
- ))} -
+ Book a shipment + + + + + See how it works + +
+ + + +
+ {heroTrust.map((item) => ( + + + {item} + + ))} +
+
- {/* Hero Dashboard Card */} -
-
-
-
-

- Freight Operations + {/* Live tracking card */} + +

+
+
+

CN-2026-08417

+

+ Mojo Dry Port → Djibouti Port · 40ft × 12

- -

Live Statistics

-
- -
+ + + In transit +
-
- {stats.map((item) => { - const Icon = item.icon; +
    + {trackingStops.map((stop, index) => { + const isLast = index === trackingStops.length - 1; + const done = stop.state === "done"; + const current = stop.state === "current"; return ( -
    -
    - -
    +
  1. + + + {!isLast ? ( + + ) : null} + -

    {item.value}

    - -

    - {item.label} -

    -
  2. + + + {stop.name} + + + {stop.time} + + + ); })} -
+ -
-
-
-

- Corridor Performance +

+ {[ + ["Train", "TS-14 · 2 locos"], + ["Wagons", "12 / 12 loaded"], + ["Next gate", "Dewele customs"], + ].map(([key, value]) => ( +
+

+ {key}

- -

- 99% -

+

{value}

- -
- Operational -
-
- -
-
-
+ ))}
+ +
+
-
-
-
- -
+ {/* Stats */} +
+
+ {stats.map((stat, index) => { + const Icon = stat.icon; + const TrendIcon = stat.down ? TrendingDown : TrendingUp; -
-

16 Trains Active

+ return ( + +
+
+ + + -

- Across all freight corridors + {stat.trend} +

+ +

+ {stat.value}

+

+ {stat.label} +

+ + {stat.fleet ? ( +
+ {Array.from({ length: 16 }).map((_, unit) => ( + + ))} +
+ ) : ( +
+ {stat.chart!.map((height, bar) => ( + = stat.chart!.length - 3 + ? "bg-edr-primary" + : "bg-emerald-200" + }`} + style={{ + height: `${(height / 48) * 100}%`, + animationDelay: `${bar * 60}ms`, + }} + /> + ))} +
+ )}
-
-
-
+ + ); + })}
{/* Features */} -
-
-
-
- Platform Features -
- -

- Everything needed for freight operations -

- -

- Centralized railway freight operations with live shipment - visibility, operational monitoring, customer management, and - intelligent logistics insights. +

+
+ +

+ PLATFORM

-
+

+ Everything between the dry port and the quay +

+ -
- {features.map((feature) => { - const Icon = feature.icon; + +

+ One portal for shippers, shipping lines, transit agents and railway + operations. Built around the real workflow of the corridor, not a + generic TMS. +

+
+
- return ( -
-
- +
+ {features.map((feature, index) => { + const Icon = feature.icon; + + return ( + +
+
+ {feature.preview}
-

{feature.title}

+
+
+ + + +

{feature.title}

+
-

- {feature.description} -

-
- ); - })} -
+

+ {feature.body} +

+ + + Learn more + + +
+ + + ); + })} +
+
+ + {/* Showcase */} +
+
+ +
+ Freight train hauling containers along the corridor + +
+ +
+ + + TS-14 · Awash → Mieso · 68 km/h + +
+ +
+ {[ + ["98.4%", "On-time"], + ["312", "Wagons moving"], + ["16h 20m", "Avg transit"], + ].map(([value, key]) => ( +
+

{value}

+

{key}

+
+ ))} +
+
+ + + +

+ LIVE OPERATIONS +

+

+ See the whole train, not just a tracking number +

+

+ GPS from every locomotive, wagon-level loading status, gate events at + Dewele customs and ETA that updates as the train moves. Shippers and + operators look at the same map. +

+ +
    + {[ + { icon: Satellite, text: "Locomotive telemetry every 30 seconds" }, + { icon: BellRing, text: "SMS & in-app alerts on gate and border events" }, + { icon: MapIcon, text: "Shareable tracking link for your consignee" }, + ].map((point) => { + const Icon = point.icon; + + return ( +
  • + + + + {point.text} +
  • + ); + })} +
+
{/* Corridors */} -
+
-
-
-
- Freight Corridors -
+ +

+ NETWORK +

+

+ One line. Two countries. Every station on the timeline. +

+

+ Standard-gauge, fully electrified. Book any origin–destination pair + along the corridor and see wagon position station by station. +

+
-

- Connected logistics infrastructure -

+ {/* Animated corridor */} + +
+ + {stations.map((station) => ( + + ))} +
-

- Efficiently move freight across strategic Ethiopia–Djibouti - railway corridors with operational visibility and optimized - transport coordination. -

- -
- {corridors.map((corridor) => ( -
+ {stations.map((station) => ( + + -
- - {corridor} -
- ))} -
+ {station.name} + + {station.note ? ( + + {station.note} + + ) : null} + + ))}
+
-
-
-
-

- Operational Insights +

+ {lanes.map((lane, index) => ( + +
+

+ {lane.from} + + {lane.to} +

+

+ {lane.transit} transit · {lane.kind}

- -

- Freight Performance -

- -
- -
-
- -
- {[ - { - label: "Bookings Processed", - value: "9,842", - progress: "92%", - }, - { - label: "On-time Shipments", - value: "99%", - progress: "99%", - }, - { - label: "Customer Satisfaction", - value: "99%", - progress: "99%", - }, - ].map((item) => ( -
-
- {item.label} - - - {item.value} - -
- -
-
-
-
- ))} -
- -
-
-
- -
- -
-

- Enterprise-ready Platform -

- -

- Designed for large-scale freight and railway operations. -

-
-
-
-
+ + ))}
+ {/* How it works */} +
+
+ +

+ HOW IT WORKS +

+

+ From booking to delivered in four steps +

+

+ No phone calls, no paper manifests. Every party — shipper, shipping + line, transit agent and station — works from the same record. +

+ + + Create a free account + + +
+ +
+ {steps.map((step, index) => ( + +
+ + {step.num} + + +
+

{step.title}

+

+ {step.body} +

+
+
+
+ ))} +
+
+
+ + {/* Audiences */} +
+ +

+ Built for everyone on the corridor +

+
+ +
+ {audiences.map((audience, index) => { + const Icon = audience.icon; + + return ( + +
+ +

{audience.title}

+

+ {audience.body} +

+ +
    + {audience.items.map((item) => ( +
  • + + {item} +
  • + ))} +
+
+
+ ); + })} +
+
+ {/* Contact */} -
-
-
-
-
- Contact Us -
+
+
+ +

+ CONTACT +

+

+ Let’s move freight smarter +

+

+ Contact EDR Freight for partnership opportunities, enterprise + onboarding, or logistics support. +

-

- Let’s move freight smarter -

+
+ {[ + { icon: Mail, label: "Email", value: "support@edrfreight.com" }, + { icon: Phone, label: "Phone", value: "+251 11 000 0000" }, + { icon: MapPin, label: "Head Office", value: "Addis Ababa, Ethiopia" }, + ].map((row) => { + const Icon = row.icon; -

- Contact EDR Freight for partnership opportunities, enterprise - onboarding, or logistics support. -

- -
-
-
- + return ( +
+ + + +
+

{row.label}

+

{row.value}

+
- -
-

Email

-

- support@edrfreight.com -

-
-
- -
-
- -
- -
-

Phone

-

+251 11 000 0000

-
-
- -
-
- -
- -
-

Head Office

-

- Addis Ababa, Ethiopia -

-
-
-
+ ); + })}
+ - {/* Contact Form */} -
+ +

Send us a message

-

We’ll get back to you as soon as possible.

@@ -504,117 +1129,359 @@ export default function EDRFreightLandingPage() {