TicketingHub: Redesigning the Availability Calendar

March 5, 2026
 · 
4 min read
1. The Problem
  • TicketingHub is a booking platform used by tour operators, attractions, and activity providers worldwide.
  • Who uses the availability calendar: Suppliers managing their day-to-day operations — checking bookings, assigning guides, spotting gaps.
  • The pain: For suppliers with 10, 50, or 291 products, the availability page was unusable. It loaded everything — every product, every time slot, every day — in a single request.
  • The numbers: Largest Core Web Vital (LCP) measured at 11.78 seconds. Some suppliers hit HTTP 500 errors because the URL exceeded length limits from passing 200+ product IDs.
  • The root cause: The dashboard was calling its own API over HTTP — the frontend engine made a network roundtrip to a backend sitting in the same application. Serialization, middleware, retry logic — all for data that was one database query away.
2. The Design Hypothesis
  • Insight: Nobody looks at 291 products at once. Operators care about subsets — "Downtown tours," "Weekend activities," "VIP experiences."
  • Concept: Saved Views — let users create named collections of products and switch between them with tabs. Load only what the active view contains.
  • Design goals:
    • Zero-config first visit (it should just work)
    • Fast view switching (tab-based, no page reload for selection)
    • Minimal UI footprint (tabs, not a settings page)
    • Familiar interaction patterns (Notion-like tabs, modal for creation)

3. The Design Decisions
  • Default "All Products" view: Auto-created on first visit so existing users aren't greeted with an empty screen. Deletable if they want curated views only.
  • Tab bar with inline delete: Each view is a tab with a product count badge. Delete via an X icon that appears on hover — confirmation dialog prevents accidents.
  • Lazy-loaded creation modal: The product selection modal doesn't load until the user clicks "+ New View." For suppliers with hundreds of products, this keeps the base page fast.
  • Modal UX details:
    • Search filtering to find products quickly
    • Select all / deselect all scoped to visible (filtered) results
    • Selected state: light blue background, bold blue product name, larger checkbox
    • Pinned header (name input, search) and footer (count, actions) with scrollable product list
    • Real-time footer counter: "12 of 47 products"
  • Empty state: When a supplier has zero products, a clean "No products yet" message instead of a broken calendar.

4. The Engineering
Before: For every page load.
Dashboard Controller


Tickethub SDK (lib/tickethub/)


HTTP Request


Rack MiddlewareRoutingAPI Controller


JSON SerializationResponse


SDK Parses JSONOpenStruct Objects


Dashboard Renders View
After: One hop.
Dashboard Controller


ActiveRecord QueryPostgreSQL


Dashboard Renders View
Three SDK calls replaced with direct queries
Result: Page loads dropped from 11+ seconds to under 2 seconds.
4a. The Architecture
  • New AvailabilityView model with data integrity validation (product IDs must belong to the supplier)
  • Manager pattern for CRUD (thin controllers, business logic in managers)
  • Race condition handling on default view creation (find_or_create_by)
  • Bootstrap Modal lifecycle management for dynamically injected content
4b. The Details That Ship Quality
  • Full I18n across 24 locales
  • Turbo Drive compatibility (disabled globally, so Rails UJS for confirmations)
  • Duplicate form ID warnings resolved
  • XHR edge case guarded (204 when no data, not a 500)

5. The Result
  • Performance: ~80-85% reduction in page load time for large suppliers
  • Reliability: No more 500 errors from URL length limits
  • Usability: Operators can create focused views for their workflow — morning tours, weekend events, specific locations
  • Code quality: Full test coverage (model, manager, request, service specs), clean architecture, zero linter warnings
Before — SDK HTTP calls:

3 separate HTTP roundtrips to the app's own API

availability = current_supplier.endpoint[:availability].get(from: from, to: to)
guide_shifts = current_supplier.endpoint[:guide_shifts].get(filter: { from: from, to: to })
products     = current_supplier.endpoint[:products].get
After — direct ActiveRecord queries:
availability = ProductAvailability.for_products(products, from, to)
guide_shifts = supplier.guide_shifts.where(starts_at: from..to).includes(:guide)
products     = supplier.products.active.where(id: view.product_ids)

Same data, same database, zero network hops.


6. Reflection
  • Iteration is the process: The modal went through 10+ design iterations in a single development cycle — from basic list to search + select all + highlighted selection state. Each round was driven by actually using it.
  • Performance is a design decision: The saved views feature wasn't just about organization — it was the mechanism that made performance possible. Design and engineering solved the same problem from different angles.
  • Legacy systems are opportunities: The deprecated SDK wasn't just technical debt — it was the bottleneck users felt every day. Replacing it wasn't refactoring for its own sake; it was the single highest-impact change possible.
View