Building HeatSync Part 4: Removing Friction

Jan 27, 2026

After defining the MVP and getting the core flow working, I had a functional tool. Upload a PDF, enter a name, see the events. It worked. But using it felt clunky.

Every extraction required:

  1. Go to the meet website
  2. Find the heat sheet link
  3. Download the PDF
  4. Switch to HeatSync
  5. Upload the PDF
  6. Enter swimmer name
  7. Wait
  8. Download the .ics file

That’s seven steps of friction. Each step is a chance for the user to give up.

The Insight

Heat sheets are always posted on meet websites. Always. Meet coordinators upload them to a team management platform (SwimTopia, TeamUnify, etc.), and parents get a link.

So why make users download and re-upload? The URL is right there.

URL Paste

I added a simple URL input field. Paste the heat sheet URL, enter the swimmer name, done. The backend fetches the PDF directly.

Before: Download → Switch apps → Upload → Extract
After:  Paste URL → Extract

This sounds minor. It’s not.

After adding this feature, I never used the file upload again. Not once. The URL flow is just that much better.

iOS Safari Clipboard Woes

Of course, nothing is ever simple.

iOS Safari doesn’t support navigator.clipboard.readText(). The “paste from clipboard” button that worked perfectly on desktop just didn’t work on the device where people actually use this tool (phones at the pool).

The workaround is ugly but effective:

  1. Try the modern clipboard API first
  2. If it fails (iOS Safari), focus the input field
  3. Show a toast: “Tap and hold, then select Paste”

Not elegant, but it works. Users on iOS can still paste; they just have to do it the old-fashioned way.

Form State Management

Another friction point: what happens after extraction?

In the first version, the form just sat there. Users would accidentally re-submit, or change the swimmer name and wonder why nothing updated.

I implemented a simple state machine:

StateUI Behavior
uploadForm enabled, ready for input
extractingForm disabled, spinner in button
searchResults visible, form disabled

When extraction finds events, the form locks and shows results. A “Start Over” button resets everything.

One edge case: what if extraction finds zero events? Originally, the form would lock in search state with an empty result (useless).

Fix: if no events found, stay in upload state so users can correct their input. Show a toast notification instead of locking them out.

Small details, but they compound.

Session Date Display

Heat sheets often span multiple days. When you’re looking at a list of events, you need to know which day each event is on.

I added session date display to each event card:

Event 12: Girls 11-12 100 Free • Saturday, Jan 18
Heat 3, Lane 4 • Seed: 1:05.32

One gotcha: JavaScript date handling with timezones. Dates stored as UTC were displaying as the wrong day for users in certain timezones. Fix: force UTC display for session dates.

Mobile Responsiveness

The tool is meant to be used at the pool, on a phone. Every UI decision went through the “can I use this with wet fingers?” test.

  • Large tap targets
  • No tiny buttons
  • Readable text at arm’s length
  • Results that don’t require horizontal scrolling

TailwindCSS makes this easy with responsive prefixes (sm:, md:, etc.), but you still have to think about it deliberately.


At this point, HeatSync was pleasant to use. But there were two big problems left: performance and accuracy.

Performance came first.


This is Part 4 of a series on building HeatSync. ← Part 3: Making It Work | Part 5: The Performance Problem →

Tags: