2026React 19TypeScriptViteReact Router v7TanStack Query v5react-hook-formZod v4MapLibre GL JS v5Framer Motion v12CSS Modulesi18nextVercel FunctionsDuffel APIVitestPlaywrightGitHub Actions
What I Built
- Split-view interface: MapLibre GL JS v5 with OpenFreeMap tiles (no API key) rendering curved GeoJSON arc routes and animated price-bubble markers per result
- BFF layer via Vercel Serverless Functions proxying Duffel and RapidAPI — API credentials never reach the client bundle
- Flight search form validated with react-hook-form and a Zod v4 schema resolver; airport autocomplete with debounced search against a local IATA/city dataset
- Deal Score algorithm (0–100) computed relative to the full result set: price 60%, duration 30%, stops 10%; CO₂ estimated per passenger with a simplified ICAO method
- Search history (last 8 queries in localStorage) and a /saved route code-split with React.lazy; React.memo on FlightCard prevents re-renders from map interaction events
- Framer Motion v12 stagger on result cards, spring-animated detail modal, fade toasts, and layout animations on sidebar collapse
- Accessibility: / and Esc keyboard shortcuts, focus trap in the modal, full ARIA labelling and keyboard navigation on autocomplete and custom selects
- Unit tests with Vitest and Testing Library covering deal scoring, CO₂ and formatters; Playwright E2E on core search flows; CI pipeline on GitHub Actions
How It Works
- 1The user fills in origin, destination and travel dates; Zod validates the form schema and react-hook-form surfaces field-level errors.
- 2On submit, a request goes to /api/offers — a Vercel serverless function that forwards it to the Duffel sandbox API and shapes the response.
- 3TanStack Query caches results for 5 minutes; the map renders arc lines between airports and places animated price markers at the destination.
- 4Deal Score ranks all returned offers; clicking a card opens a spring-animated modal with the full itinerary, layover breakdown and CO₂ card.
- 5Airport pins on the map are interactive: clicking one populates the corresponding form field and updates the arc layer in real time.