Umanga Shrestha/Work/DRAYOS

Case study · 2019 – 2026

Six years of DRAYOS

DRAYOS is a transportation management system for US drayage — the trucks that move containers between ports, rail yards and warehouses. Over 500 carriers run their daily operations on it: dispatch, billing, tracking, driver planning, hundreds of screens. I joined PortPro in December 2019, one of its earliest hires, and owned the frontend until March 2026, working from Kathmandu, ten hours from the rest of the team.

This page is what that looked like. The screens are rebuilt for this site with placeholder data — the product belongs to PortPro — but the layouts, the decisions and the numbers are the real ones.

What I walked into

It started late on a Tuesday night — 2 December 2019, 10:30 PM in Kathmandu. A friend opened a group chat with me and the CEO of a small logistics startup in the US, and wrote three words: meet our CEO.

He was direct. Their frontend developer had disappeared in the middle of a sprint. A new design was supposed to go live that day. Three new customers were being onboarded that week. The product was a web dashboard for trucking companies — tables and cards, mostly — and it did not work on smaller screens. Could I make the whole thing responsive? I said yes. I was four years into frontend and UI work, freelancing part-time on Upwork. My sister's wedding was that Thursday.

An hour later I had cloned the repo, and he was asking whether I could give him something to deploy by the next morning. Maybe get some less sleep. The code was what he was afraid of: CSS overwritten in so many places that nothing could be debugged, a SCSS compiler that did not run, and a dispatcher table so wide that the only honest answer was a horizontal scroll — too much data. I did the cards first: alignment, spacing, font sizes, responsive. He reviewed over screen-share at one in the morning, my time.

My first changes went live on 4 December, about thirty-eight hours after that first message. The plan I wrote on night one — convert the CSS to SCSS, get the compiler working, then clean the code properly — took until July 2020 to finish, because customers came first. Six years later I was still on that dashboard.

Dispatch, the screen people live in

Dispatchers spend their whole day on one screen, moving containers through a lifecycle: arriving, to be picked up, to be delivered, to be returned, dropped. The original board showed counts in a row of tiles and a paginated table underneath. Every question — what needs a driver right now, what is stuck at the terminal — meant scrolling and filtering by hand.

2020. Rebuilt for this page from a reference screenshot; placeholder data.

The redesign made the lifecycle the interface. Six cards along the top summarise each stage and act as filters — click Containers need to be returned and the board below becomes that list. The grid got denser so a dispatcher sees more at once, cells carry their own controls (inline editing, dropdowns, popups), a load can be created directly in the grid, and the whole board goes full-screen. Getting that much interactive UI into one view without overlap and collision problems was most of the work.

2026. Rebuilt for this page; placeholder data. The real board is virtualized and scrolls thousands of rows.

The design system

The SCSS foundation went in on 14 July 2020 — the plan from the first night, seven months late because customers came first — and from that day every style change went through it. Built from there in SCSS and React and maintained for six years: a shared component library — modals, data grids, custom selects, a date-time picker, form controls, tooltips, loaders, a chat UI, an icon set, route timeline indicators — with theming on CSS custom properties and a platform-wide dark mode. It replaced a copy-the-last-page workflow where developers reused stale markup and the UI drifted a little further with every new screen.

A sample of the system, redrawn for this page. Light theme.

The same components on the dark tokens. The hazmat / hot / overweight row colours are the real values; in dark they move to the row edge so the text stays readable.

Every grid, virtualized

The datasets behind these boards run to millions of rows. Rendering them the ordinary way made the heaviest screens lag exactly when dispatchers needed them most. I virtualized every data grid in the product, not just dispatch, and added memoization across the heaviest operational screens, so thousands of rows render and scroll without lag.

Before — render everything
12,000 loads → 12,000 rows in the DOM. Every scroll, filter or edit re-lays out all of them.
viewport
1<tr>
2<tr>
3<tr>
4<tr>
5<tr>
6<tr>
7<tr>
8<tr>
9<tr>
10<tr>
11<tr>
12<tr>
13<tr>
14<tr>
DOM rows12,000
After — render the viewport
12,000 loads → ~30 rows in the DOM, swapped as you scroll. Memoized cells re-render only when their data changes.
viewport
1
2
3
4
5<tr>
6<tr>
7<tr>
8<tr>
9<tr>
10<tr>
11
12
13
14
DOM rows~30

Illustration, not a benchmark: the change is in how many rows exist at once, not how many the user can reach.

The design system, as rules

By 2025 I was the bottleneck: every frontend implementation came to me for review, then I re-tested the pages. So I encoded the design system into Cursor rules — which components to use, the spacing and layout defaults, how a screen is scaffolded from the backend's API JSON — so a developer with no UI background could generate on-standard UI from a single prompt. A small Electron app did the local setup (GitHub auth, keys, clone) so product managers could open the codebase in Cursor and make UI changes without pulling in an engineer.

Cursor · prompt
@rules/drayos-ui.mdc

Build the Chassis list page from this response:

GET /api/chassis
[{ "chassisNo": "TSXZ251717",
   "type": "20' Standard",
   "owner": "Own",
   "status": "Available",
   "yard": "Northstar Yard",
   "lastInspection": "2026-08-30" }]

Standard list page: search, filter, add;
status as chip; row actions; empty state.
from the rules file
  • list pages use <PageHeader> + <DataGrid>, never a raw <table>
  • status fields render as <StatusChip tone=…>
  • controls are 32px; primary action top-right, green
  • dates through formatDate(); never raw ISO strings
  • empty state: <EmptyState icon title action>

A representative prompt and the kind of screen it produced. Rules and screen redrawn for this page; the real rules file and codebase belong to PortPro.

Six years, one product, ten hours from the team. Async by default, with live overlap when it mattered; the doc and the working screen did the job a meeting would have.