Internal Tooling / Product UI
Deal Estimation InterfaceBDG
A modern React-based rebuild of BDG's internal media sales estimation tool, demonstrating complex, data-driven UI workflows.
This project is a recreation of work I did as a Product Engineer at BDG in 2024, where I built a deal estimation page for an internal ad sales tool used by over 250 people across the organization. Because the app sits behind authentication, I rebuilt the core features with a frontend-oriented stack, using Next.js, TypeScript, Prisma, and Neon, to make the work publicly viewable.
Frontend
Hotwire (Turbo & Stimulus)
Backend
Ruby on Rails • Active Storage • Solid Queue
Styling
Tailwind CSS
01
The Problem
Salespeople were estimating deals in spreadsheets.
BDG's 250+ person ad sales team managed deals through an internal Deal Desk app. Salespeople estimated deals manually across multiple spreadsheets, copying values between files and totaling by hand before presenting to clients. It worked, but it was slow and error-prone for a revenue-critical workflow.
The app had its own issues. It was scaffolded with Avo, which helped early on but made custom design work harder over time. The form workflow was especially clunky: adding a line item opened a side drawer with a long scrolling form full of fields that weren't always relevant.


02
Constraints
An internal tool in the middle of a framework migration.
Avo migration in progress
The team was incrementally moving away from Avo. Some pages still used it while others had been rebuilt. I couldn't rip everything out at once.
Drawer pattern was established
Users expected forms to open in side panels. I could improve what happened inside the drawer, but the interaction model itself was set.
Background job infrastructure transitioning
The team was moving from Sidekiq to Solid Queue. Redis had become cumbersome to maintain and I was building during that transition.
Non-technical users
The primary users were salespeople. Every interaction needed to be self-explanatory.
03
My Approach
Strip it down, then build what's missing.
I started by auditing what was getting in the way. Avo's defaults were adding interaction overhead users didn't benefit from. I replaced scaffolded views with purpose-built interfaces, starting with the most-used pages and working outward until the whole app had a consistent feel.
Simplify the forms first
Broke the monolithic drawer form into a step-based journey with each action only rendering the fields relevant to that step.
Build the estimation page
A dedicated interface for adjusting rates, adding line items, and watching changes ripple through deal totals in real time.
Fix the export crash
Moved file generation to a background job so users could keep working while their report processing asynchronously
04
Interaction & UI Decisions
Every field in the drawer needed to earn its place.
The biggest decision was breaking one long drawer form into a branching flow. Instead of showing every field at once, the drawer adapted to the user's current action. Each step only rendered what was relevant.
For the estimation page, the goal was making calculations visible. Changing a rate updated line item totals, product subtotals, the grand total, and stat cards simultaneously. It needed to feel spreadsheet-like, but with more structure.
Exports stayed non-blocking: click "Export," keep working, and download from the "Downloaded" tab when ready.
The updated branched flow designed to replace the single scrolling form.
05
Implementation Details
Turbo, Stimulus, and a background job that doesn't crash.
The original implementation used Hotwire. Turbo Frames handled partial updates so the drawer, stat cards, and line item table each re-rendered independently. Turbo Streams pushed real-time updates when background jobs completed, swapping an “In Progress” message for a download button without a page refresh. Stimulus managed client-side interactions that didn't need a server round trip.
For exports, I used Solid Queue to generate files asynchronously as the team transitioned away from Sidekiq and Redis. Active Storage handled file attachment and a cleanup job removed old reports to prevent storage bloat.
Turbo Frames for partial updates
Stat cards, line item tables, and drawer content were each wrapped in Turbo Frames, allowing targeted re-renders without refreshing the entire page.
Turbo Streams for real-time feedback
When an export finished, a Turbo Stream updated only that report’s table row, replacing the “Processing” state with a download button.
Solid Queue for async exports
The export job now runs in the background, with progress shown immediately and completed files available in the “Downloaded” tab.
06
The Rebuild
Same problems, different stack.
The recreation includes the estimation page, simplified drawer forms, and the async export flow. The full branching form workflow is stripped back and authentication is removed, but core interactions and data relationships are preserved.
Choosing the stack
Next.js was new to me and a good fit for this project. Server components mapped well to my data-fetching patterns, server actions replaced what Turbo had handled for forms, and TypeScript added full-stack type safety. Tailwind stayed as the one tool I used in both versions.
Neon over Supabase
I initially chose Supabase for its built-in auth, but auth felt unnecessary for a portfolio piece and the free tier kept pausing during gaps between sessions. Neon was simpler: always-on and no unused features.
Prisma over Drizzle
I wanted to replicate how Rails' Active Record lets you interact with data as objects. Prisma's API was the closest match. Drizzle was more performant by reputation but read more like raw SQL.
// Active Record (Rails)
Order.find(id)
// Prisma
prisma.order.findUnique({ where: { id } })
// Drizzle
db.select().from(orders).where(eq(orders.id, id))07
Outcome
From spreadsheets to a sandbox.
The estimation page gave the sales team a dedicated interface for modeling deals, replacing the spreadsheet workflow. The async export resolved an app-crashing bug by moving file generation to a background job. The UI cleanup brought visual consistency to an app that had been a patchwork of Avo defaults and custom styles.
08
Reflections
Sitting closer to the problem.
Early on, I sat in on a meeting with the sales team to understand their workflow. At the time I treated it as a formality. I had my tasks and knew what to build. Looking back, that was a missed opportunity. Understanding how users actually work, not just what they need, is what separates a correct implementation from a genuinely useful one.
This project changed how I think about what I build. Before Deal Desk, most of my work was on reusable UI components for brand websites. This was the first time I built something with a direct impact on someone's daily productivity, and it shifted my perspective on what front-end engineering can accomplish.