Wanderpin
- 9 Devlogs
- 40 Total hours
A trip planner built around a 3D globe - drop pins, get a route with distances and country counts. Search a city or click the map to add stops
A trip planner built around a 3D globe - drop pins, get a route with distances and country counts. Search a city or click the map to add stops
Everything in this batch is about the moment someone lands: a blank globe is intimidating, so I built a real first-run, made it personal, and fixed a jank I’d been ignoring.
New visitors get a welcome dialog: Plan a trip, or Remember one you took. Either way there are starter templates (a week in Italy, around the world, a California road trip) you can drop in with one tap and start dragging stops around.
A short guided tour spotlights real elements: search, the view toggle, Play. It measures each target’s live bounding rect, re-measures on resize and focus, flips above/below by screen half, and hides itself the moment you focus the search box so it never fights you while typing. Dismissals persist so it doesn’t nag on return visits.
Build’s green, all staged, not yet committed. Next: watch real first sessions, do people tap through a template or skip straight to search?
Live: https://wanderpin-ecru.vercel.app/
One update, mostly one stubborn visual I’d twice given up on: real lifted route arcs on the 3D map.
deck.gl’s ArcLayer drifts on a MapLibre globe (it only knows Web Mercator), and my earlier hand-rolled layer compiled, ran, threw zero errors, and painted nothing. The fix came from reading MapLibre’s own shaders: inject its projection prelude and call projectTileFor3D(pos, elevation).
The thing I’d missed every time - elevation is in meters above the sphere, fed through five per-frame uniforms, not a raw matrix. Then the polish fights: a z-fight speckle (fixed by not writing depth), a 50 km minimum lift that made short hops launch like fireworks, and a fat vertical wall at city zoom (now fades out past ~zoom 10).
New “Days” view in the trip panel. Weekend trip? You’ll never see it. Month-long? Hit “Organize into days” and stops group into collapsible days you can split, reorder, fit the globe to, or play one at a time. A day is a contiguous slice of the route, so “move to Day 3” repositions the stop and re-normalizes - the route and itinerary stay honest. Days travel on share links too.
Live at site, (Also squashed a deploy bug: one .tsx API route made Vercel type-check the whole api/ folder with no tsconfig - a tiny api/tsconfig.json fixed it.)
This batch turns a Wanderpin trip from a thing on your screen into a thing you can send to a friend - with a real link preview, an embeddable globe, and a reel that finally looks good on short hops.
Hit Share and you get a public link (/t/slug) in a box, plus Copy, the native share sheet, and WhatsApp / Instagram / X buttons. The interesting part is what happens when someone opens that link:
/t/:slug is server-rendered (api/share.ts) and serves a dynamically generated Open Graph image (api/og.tsx via @vercel/og) - so pasting a Wanderpin link into a chat unfurls into a proper preview card instead of a blank rectangle./embed/:slug renders just the spinning globe (a tiny route check in main.tsx swaps the whole app for EmbedView), and the share dialog hands you the <iframe> snippet.pg_cron job, so the table stays small and old links quietly die.The globe fly-through looks flat when two stops are 20 km apart - the arc is a stub and the camera barely moves. So now the Play reel checks each leg: anything under 200 km crosses into the 3D map (terrain + buildings) with a zoom picked from the distance, longer legs stay as globe arcs. It asks once before playing, and video recording stays globe-only so captures never go blank.
replaceState("/") the instant it loaded, so /t/slug disappeared mid-playback and you couldn’t re-share what you were watching. Moved that reset onto the “Create yourslongs.flyTo signature mismatch before it shipped - the map handle wants { zoom }, not a bare number.It all builds clean, Try it: https://wanderpin-ecru.vercel.app/
Two big things landed this week: the map went from 26 hand-picked places to ~40,000, and zooming into the globe now drops you into a real 3D city.
Wanderpin used to know about 26 famous spots. Now it knows ~40k: cities, towns, UNESCO sites, landmarks, and the world’s most-visited attractions - each tagged with a one-word vibe (Coastal, Alpine, Arid, Heritage, Sacred, Volcanic…).
The fun part is the vibes aren’t hand-typed. They’re derived at build time from real data: a Köppen climate raster (so Cairo reads “Arid” and Athens “Mediterranean”), distance to an actual coastline for “Coastal”, GeoNames elevation/feature codes for peaks and castles, and Wikidata’s annual-visitor numbers to rank “most visited”. Facts come from Wikipedia and UNESCO descriptions. Search is now instant and offline - type “Firenze” and Florence shows up.
The home view is a space globe (three.js). The close-up is a different engine entirely - maplibre with 3D terrain and buildings. I wired up a level-of-detail handoff between them: zoom into a pin and it crosses from globe to 3D automatically, and zoom back out and it returns to the globe.
The trick to making it not feel janky: when you start zooming in, it quietly mounts the 3D map hidden and lets it pull tiles in the background. Only once the tiles are warm does it crossfade. No blank loading flash.
idle event to know tiles were ready - but that event sometimes never fires. The globe just kept zooming into black. Fix: commit after a 2.2s fallback even if idle is silent.map.on("zoom") captured the callback once at mount - back when the view was still “globe” - so its if (view !== "3d") return guard killed every event forever. Fix: call the latest callback through a ref.Both are real bugs that would’ve bitten actual users on slow connections, not just test artifacts.
Globe -> 3D -> globe round-trips cleanly on both laptop and mobile, with zero console errors. Try it: https://wanderpin-ecru.vercel.app/
Live: https://wanderpin-ecru.vercel.app/
You drop a few pins on a globe and hit Play - and instead of a checklist, you get a cinematic fly-through you can actually share. This update was about chasing one idea until the app felt alive: the journey is the hero, the logistics hide in a drawer.
Play used to just stop. Now it lands: the camera pulls back to frame the route, the arcs light up, and a date-stamped card counts up your distance, stops, countries and time. A little classifier scores the trip and picks the camera - a city hop zooms in, a round-the-world trip shows the marble. You also pick how you travel each leg (walk/train/flight…), which bends both the arc shape and the trip’s duration.
Then three swings: export the fly-through as a video (recorded straight off the globe canvas, no server), “been there” pins that glow gold once visited, and a living globe - a warm daylight terminator plus each stop’s local time and current weather.
Geocode 500’d in prod, every call. Not a geocoding bug at all: type: "module" means Vercel runs the API as native ESM, and Node demands file extensions on imports. from "./_nominatim" crashed at load. One .js fixed it.
The terminator looked gloomy. Darkening the night side just made an already-night map muddy. I flipped it - glow the sunlit side instead - and it finally read as “half the globe is lit.”
Two suns. Every clear-sky stop showed ☀️ 06:28 · ☀️ 15° - my day marker next to the weather glyph.
ngl used AI to build this image 😛
You open the app or you can :) (check it out - https://wanderpin-ecru.vercel.app/ ). A night-side Earth, city lights glowing, a slow spin. You drop a pin in Paris. Another in Tokyo. The arc draws across the globe, and right next to all of that, a panel quietly tells you your dream weighs 2.2 tonnes of CO₂.
That panel was killing the magic. This phase was about fixing it.
I stopped showing the numbers on the dream surface. Same numbers, just hidden one click deep behind a “Trip details” disclosure. In their place: one warm line that grows with the trip - “4 stops · 4 countries · An adventure taking shape.” You build a journey. The app reacts like it’s excited about it.
Surprise me, the most fun thing the app could do, was buried under a dice icon. It’s the front door now: Take me somewhere. The empty state stopped being homework - “Your map’s a blank canvas, click to start” - and became an invitation: “Where are you dreaming of?”
Drop your very first pin and the app whispers back: “Your journey begins in Santorini ✨”. Hover over a stop and a small card appears: “Why go - its iconic caldera is the rim of a colossal volcanic eruption.” Hit Play and the screen goes cinematic. Full-screen overlay, place name, vibe, progress dots, the route lighting up beneath.
The trip used to be a spreadsheet. Now it’s a memory you haven’t made yet.
Almost shipped a bug that would’ve made this look broken. On a phone, the empty-state “Take me somewhere” lives inside a bottom sheet. Tap it, and the surprise card was rendering behind the still-open sheet. To the user, nothing happened. To me, a testing pass on an iPhone-shaped viewport caught it the morning of. One-line fix. The kind that disappears in the diff but would have left every mobile user thinking the button was broken.
Empty state, surprise card, first pin, the reel - all warm now. The numbers are still there for the planners, behind one small toggle. The dreamers don’t see them at all.
When Play ends, the camera pulls back, the whole route lights up, and the same numbers I just buried come surging back as a celebration - “You crossed 4 countries. Today, that’s a journey.” Same data, different feeling. The closing frame becomes a postcard you can share.
The whole phase is one belief: people don’t open a travel app because they want to know how many kilometres a trip is. They open it because they want, for a minute, to imagine going somewhere. So show them the somewhere.
This update gave Wanderpin proper short links, a saved-trips library, and a route optimizer. But the part I’ll remember is a 100 KB mystery.
Sharing used to cram the whole trip into a giant #... URL. Now there’s a tiny Supabase backend: hit Share and you get a clean …/t/aB3xY9 link that anyone can open, no login. Reads come straight from the browser (a public read policy), and writes go through a serverless function with the secret key, so nobody can scribble on the database from the client. When someone opens a shared link, the route flies stop to stop and a little “Make your own” card invites them to start their own.
Spinning the globe was dropping stray pins, because react-globe.gl fires a click even at the end of a drag. Added a pointer-distance guard (under ~5px = a real click). Clicking open ocean now offers an “Add anyway” instead of a useless pin, and there’s an “Optimize route” button that reorders stops with nearest-neighbour + 2-opt to cut total distance.
Share kept failing in the browser with a 413 (payload too large). But the function worked fine when I hit it with curl. Same 5-stop trip, totally different size.
I had the user dump their saved trip in the console. Five stops. 403,682 characters. Each stop had two mystery keys: __threeObjPoint and __threeObjLabel, ~100 KB each.
Turns out react-globe.gl mutates the data objects you hand it - it bolts the entire Three.js render object (meshes, geometry, materials) straight onto your data. I was passing my live trip array to the globe, so it was quietly fattening every stop by 100 KB, which then got saved to localStorage and sent on publish. The hash link looked fine only because it re-packs specific fields, so it never saw the junk.
Fix was two-fold: hand the globe throwaway copies so it scribbles on those instead of my real data, and sanitize trips to known fields on save/load (which also cleans up the bloated localStorage on next visit). Back to ~1 KB per trip.
Live at https://wanderpin-ecru.vercel.app/ - build a trip, optimize it, save it, share a short link, watch it fly.
Honest caveats: shared trips are snapshots (open a link, get a copy - not live collaboration), and the globe’s three.js bundle is still the heavy chunk (lazy-loaded, so it only hits the 3D view). Lesson logged: never trust a viz library not to mutate your state.
Phase 1 let you drop pins on a globe. Phase 2 turns that into an actual trip planner: reorder stops, edit them, share a link, and hit Play to watch the camera fly your whole route.
The feature I’m happiest with. There’s now a Play button that flies the camera stop to stop along your route, advancing the highlighted pin as it goes. On the 3D globe it looks the way I wanted the whole app to feel.
The nice surprise: it was almost free. Back in phase 1 both the globe and the flat map exposed the same tiny flyTo handle. So the tour loop doesn’t know or care which view you’re on
Live at https://wanderpin-ecru.vercel.app/. Every phase builds clean (tsc + Vite) and deploys to Vercel on push. The loop now: build a trip, reorder and annotate it, watch it fly, share the link.
Check it out yourself -> https://wanderpin-ecru.vercel.app/
A little travel app where you drop pins on a spinning 3D globe (or a flat map if you prefer), and it stitches them into a trip with distances and country counts. Here’s where it’s at after the first couple of sessions.
Spent a bit making the Nominatim calls polite: searches are debounced so a fast typer doesn’t fire a request per keystroke.
The core loop works end to end: search or click -> pin lands on globe and map -> trip panel updates distance (haversine) and country count -> reload and it’s all still there. Next up is editing/reordering stops and tidying the mobile layout