Engineering Diary, Day 5: The FocusJet Widget Saga & Resurrecting 70 Dead Gallery Images
Two Fires, One Day
Today was one of those days where you think you're shipping a quick fix and end up redesigning an entire architecture. We had two problems: 70 broken gallery images across our 100 destination pages, and a FocusJet minimized widget that kept trapping users on the wrong page. Both got solved. Both taught us something.
Part I: Resurrecting 70 Dead Unsplash URLs
We discovered that 70 out of 340 unique Unsplash gallery URLs across our destination content were returning 404 errors. These are the hero images visitors see when browsing destinations like Bali, Santorini, or the Maldives. Broken images on a luxury platform is like a five-star hotel with missing paintings — it destroys trust instantly.
The Problem
Unsplash periodically removes photos (photographer takedowns, policy violations, account deletions). Our destination data had hardcoded photo IDs that pointed to deleted images. Manual replacement of 70 URLs across a 3,000-line TypeScript file was not an option.
The Programmatic Fix
We wrote a three-stage Node.js pipeline:
- Stage 1: Detection. Extracted all 340 unique Unsplash URLs from
destinations.ts, ran HEAD requests in parallel, and identified 70 that returned non-200 status codes. - Stage 2: Search. For each broken URL, we queried the Unsplash search API (
/napi/search/photos) with the destination name as the search term. The script extracted the first high-quality result's photo ID from theurls.rawfield. First pass found 57/70 replacements; a retry with alternative search queries (e.g., "Musandam fjords" instead of "Musandam") found the remaining 13. - Stage 3: Replacement. A final script performed string replacements in
destinations.ts, swapping old photo IDs for verified new ones.
Verification: all 340 URLs now return HTTP 200. Zero manual edits. The entire pipeline took less time to write than manually replacing 10 images would have.
Lesson: When your content has external dependencies (CDN images, API endpoints, third-party embeds), you need automated health checks. We are building a CI step that runs weekly URL verification on all destination gallery images.
Part II: The FocusJet Widget — Four Iterations to Get It Right
FocusJet is our aviation-themed Pomodoro timer. Users select an aircraft, set a route, and "fly" a focus session with a 3D globe visualization. The feature was working perfectly in full-screen mode. The problem was: what happens when the user minimizes it?
Iteration 1: Bottom-Right Card
First attempt: a floating card in the bottom-right corner, similar to a music player mini-widget. Problem: it overlapped with our AI Concierge chat widget, which lives in the same corner. Two floating widgets fighting for the same 200x200 pixel zone is a UX disaster.
Iteration 2: Full-Width Top Bar
Moved the widget to a full-width bar at the top of the viewport. Problem: it competed with our fixed navbar (position: fixed; top: 0; z-index: 9999). Two fixed bars at the top created a visual collision. Screenshot from CEO confirmed: it looked broken.
Iteration 3: Floating Capsule Below Navbar
Compact capsule positioned at top: 80px (just below the 80px navbar). Visually clean, no overlap. But then came the real feedback: "Users still cannot click anything. They are trapped on the FocusJet page."
This was the fundamental insight we had been missing. All three iterations shared the same architectural flaw: the widget was rendered inside the FocusJet page component. When the user "minimized," they were still on /tools/focus-jet — the page's <main> element still occupied the viewport, the router still pointed to FocusJet, and the user could not navigate anywhere.
Iteration 4: Global Context + Layout-Level Widget
The correct architecture required three changes:
- Global state via React Context. We extracted the entire FocusJet reducer state into a
FocusJetProviderthat wraps the layout. This means the timer, pause state, aircraft selection, and route all live above any individual page. - Layout-level widget rendering. The
FocusJetWidgetcomponent now lives inModeAwareLayout.tsxalongside theChatWidget— not inside the FocusJet page. It renders on every page in the app when a session is active. - Page navigation on minimize. When the user clicks "minimize," we call
router.push("/")to navigate them to the homepage. The FocusJet page returnsnullwhen in widget mode. The user is free to browse destinations, explore the fleet, read the blog — all while their focus timer ticks in a slim bar below the navbar.
localStorage Persistence
The CEO's next requirement: "Even if the user refreshes the page, the timer must continue." This meant the global context was not enough — we needed persistence.
Our solution: on every state change (and on beforeunload), we serialize the FocusJet state to localStorage with a _savedAt timestamp. On page load, the provider checks for persisted state. If found, it calculates the real seconds elapsed while the page was closed:
- If the timer was running (not paused), we add
(Date.now() - _savedAt) / 1000toelapsedSeconds - If the elapsed time exceeds the total duration, we transition directly to the "landing" phase
- If the timer was paused, we restore the exact same state (no time added)
This means a user can start a 45-minute focus session, close their laptop, open it 30 minutes later, and see the timer showing 15 minutes remaining. The session survives browser tabs, page refreshes, and full browser restarts.
The Final Widget Design
We matched a reference screenshot from the CEO: a slim light bar with a green active dot, the aircraft name in a pill badge, the ICAO route code, a monospace countdown timer, and a pause/resume button. It sits at z-index: 9998 (one below the navbar's 9999), uses backdrop-filter: blur(12px) for a frosted glass effect, and occupies just 36 pixels of vertical space. Clicking anywhere on the bar navigates back to the full FocusJet page and expands the session.
The key architectural lesson: if your minimized component needs to be visible across every page of your app, it cannot live inside any single page. It must live at the layout level with global state. This is the same pattern Spotify uses for their "now playing" bar — and for good reason.
Technical Insights
useReducer Initial State Gotcha
React's useReducer only reads its initial state argument on the first render. Our loadPersistedState() call at the top of the provider body runs on every render but only matters once. This is harmless but wasteful. The correct optimization is to use the lazy initializer form: useReducer(reducer, undefined, loadPersistedState). Filed as a follow-up task.
z-index Hierarchy for Fixed Elements
With three fixed-position elements (navbar, FocusJet widget, chat widget), we needed a clear stacking order:
z-9999: Navbar (always on top)z-9999: Chat widget (same level as navbar, different corner)z-9998: FocusJet widget bar (just below, full width under navbar)
Elapsed Time Calculation Precision
Using Math.floor() for the seconds-away calculation means we slightly undercount elapsed time (by up to 999ms). This is intentional — for a focus timer, it is better to give the user slightly more time than to cut them short. Over-counting could cause the timer to land prematurely, which feels like a bug.
What Shipped Today
| Change | Files | Impact |
|---|---|---|
| Gallery URL resurrection | destinations.ts (80 replacements) | 70 broken images fixed across 100 destination pages |
| FocusJet global widget | 5 files (2 new, 3 modified) | Timer persists across navigation and refresh |
| Chat widget cleanup | ChatWidget.tsx | Removed proactive greeting bubble |
Tomorrow
The FocusJet widget is functional but there are polish items: haptic feedback on mobile pause/resume, a subtle "ding" sound when the timer completes, and accessibility improvements (screen reader announcements for timer state changes). The gallery health check CI step is also on the backlog.
Sometimes the simplest-looking feature — "minimize a timer" — requires the deepest architectural changes. The four iterations were not failures; each one revealed a constraint we did not know existed. That is the nature of building products with real users giving real-time feedback.
Manténgase informado
Ofertas de tramos vacíos, nuevas rutas y análisis de aviación — en su bandeja de entrada.