Trips - shareable multi-stop planner

I was planning a day out in Mumbai with friends and needed three things in one place: what time we were at each stop, how we were getting between them, and a way to send the whole thing to everyone. Google Maps does the route but not the schedule. A WhatsApp message does the schedule but not the map. A shared doc does neither.

So I vibe coded one in an evening: a single HTML file, no build step, no backend, no accounts.

A morning in south Mumbai: Gateway of India at 08:00, Hutatma Chowk at 08:30, Marine Drive at 09:00, Malabar Hill at 11:30, walking then metro then taxi between them.
A morning in south Mumbai: Gateway of India at 08:00, Hutatma Chowk at 08:30, Marine Drive at 09:00, Malabar Hill at 11:30, walking then metro then taxi between them.

The plan is the URL

The requirement that decided everything else was sharing. Not sharing as a feature with an invite flow, just: my friends need to open this. The only thing everyone can already open is a link.

So there is no database. The trip is the query string. Here are the first two stops of the day above, with the spaces decoded so it is readable:

?s=Gateway%20of%20India%2C%20A%20Ward|18.92196|72.83456|2026-04-04T08:00,
   Hutatma%20Chowk%2C%20Fort|18.93274|72.83163|2026-04-04T08:30
&m=0:walk,1:metro,2:taxi

s is the stops, pipe separated: name, latitude, longitude, time. Each name is percent-encoded before the stops are joined, which is why the comma inside “Gateway of India, A Ward” does not split the list. m maps each gap between two stops to a transport mode, keyed on the index of the stop it leaves from.

Every edit calls history.pushState, which is where undo comes from. The back button is the undo button. That is half the reason state went into the URL and not into memory: no undo stack to maintain, no shortcut to teach anyone, and it works the same on a phone. Delete a stop by mistake, press back, it is there again along with the mode that went with it.

window.addEventListener("popstate", () => {
	_skipPush = true;
	stateFromURL();
	render();
});

The _skipPush flag stops the restore from pushing a fresh entry on top of the one it just came from.

Reordering a day is not reordering a list

The obvious way to move a stop is the one-liner:

const [item] = waypoints.splice(from, 1);
waypoints.splice(to, 0, item);

The time sits on the stop, so it travels with it. That is correct for a list of objects and wrong for a plan of a day.

When you drag Malabar Hill above Marine Drive you are not saying “move 11:30 to Malabar Hill”. You are saying “let’s be at Malabar Hill at 09:00 instead”. The time belongs to the slot in the day, not to the place. Try it both ways:

naive splice              what it does now
08:00  Gateway of India   08:00  Gateway of India
08:30  Hutatma Chowk      08:30  Hutatma Chowk
11:30  Malabar Hill       09:00  Malabar Hill
09:00  Marine Drive       11:30  Marine Drive

The naive rule produces a day that runs backwards. The fix moves only the location and writes it back into the fixed slots:

function reorderWaypoint(from, to) {
	// Only move name + coordinates. Times, modes, and slot positions stay fixed.
	const names = waypoints.map((w) => w.name);
	const lats = waypoints.map((w) => w.lat);
	const lngs = waypoints.map((w) => w.lng);
	const [n] = names.splice(from, 1);
	names.splice(to, 0, n);
	const [la] = lats.splice(from, 1);
	lats.splice(to, 0, la);
	const [ln] = lngs.splice(from, 1);
	lngs.splice(to, 0, ln);
	waypoints.forEach((w, j) => {
		w.name = names[j];
		w.lat = lats[j];
		w.lng = lngs[j];
	});
}

Uglier than a two-line splice, and right for what the thing actually is.

What it deliberately does not do

It draws straight lines between stops, not routes. Routing means an API, a key and a rate limit, and a plan does not need one: you want the shape of the day, and two pins with a line between them say enough. When you actually need directions there is a button that hands the whole trip to Google Maps as /maps/dir/lat,lng/lat,lng/.

Search is Nominatim, which is free and needs no key. 300 ms debounce, results biased towards whatever the map is currently showing, coordinates rounded to five decimal places. Five is roughly a metre, which is more precision than a day plan needs and keeps the link shorter.

The share dialog, showing the trip as plain text with arrows between stops, the full link, and buttons to copy text, copy link, share, or open in Google Maps

Deploying it

Push to main, GitHub Actions runs wrangler pages deploy ., done. There is no build step because there is nothing to build. The repo is the artifact. The only things the page fetches are Leaflet 1.9.4 and CartoDB’s map tiles, and the page itself is about 14.5 kB gzipped.

It is vibe coded and it looks it: one long file, styles inline on elements, a helper called _i() that returns SVG markup as strings. For something built in an evening so a group chat could agree on a plan, that was the right trade. The reorder is the part I actually thought about.

esc

Pages

HomeBlog

Writing

Trips - shareable multi-stop plannerRdamn - online playground IDEsSurveys & SimulationsR Visual

Theme

Elsewhere

EmailLinkedInXGitHubSubscribe