Full-Stack Group Vacation Planner
Trip Clipper
- Central variable
- Trip energy level
- Geo stack
- OSRM / Nominatim / Photon / Leaflet
- Dependency model
- Zero API keys required
The tension — Group travel has two failure modes: overplanning, which produces rigid itineraries and exhausted travelers, and underplanning, which produces missed opportunities and last-minute conflict. Most tools address neither — they’re just shared lists. Trip Clipper is built around that tension. Every feature either adds structure or enforces restraint, and the line between them is deliberate.
The structure — Energy level is the app’s central variable. Set once at trip creation, it doesn’t just describe the trip — it configures it. Daily time thresholds, amber warnings on overloaded days, and activity caps in Good Enough Mode all derive from that single setting, so the trip’s intended pace enforces itself through the UI rather than through willpower. The workflow is equally opinionated: wizard to idea board to tradeoff board to daily schedule. Each stage gates the next, preventing the common failure of scheduling activities before deciding which ones are worth keeping.
The decisions — The scoring model separates what can be computed from what shouldn’t be. In the Tradeoff Board, budget and proximity scores are auto-calculated from price and Haversine distance; uniqueness and group appeal are left to human judgment. The geo stack — OSRM for routing, Nominatim for geocoding, Photon for autocomplete, Leaflet for maps — was chosen to be fully self-contained: no API keys, no rate limit exposure, no third-party billing. Geographic intelligence that works the same in development as it does in production.
The Problem
Group trip planning is chaos. Activities live in group chats, budgets get lost in spreadsheets, and nobody can agree on a schedule. Most groups either overplan into exhaustion or underplan into missed opportunities — with no tool purpose-built for the messy, collaborative reality of travel with friends or family.
The Solution
Trip Clipper provides a structured workflow from trip setup through daily scheduling. A guided wizard sets energy level, travelers, cities, and budget. The Idea Board lets you browse, rate, and select activities per city — with outlier detection to flag picks far from the cluster. The Tradeoff Board runs a weighted scoring matrix to compare options side-by-side. The Daily Schedule uses drag-and-drop with OSRM-powered route times between stops — and Good Enough Mode prevents overplanning by capping activities per day based on trip energy.
Key Features
Drag-and-Drop Daily Schedule
Powered by @dnd-kit — drag activities into Morning, Afternoon, and Evening slots. Buffer blocks for meals, rest, and transit. OSRM-powered route time estimates between consecutive activities with per-gap drive/walk toggle.
Tradeoff Board & Idea Board
Rate activities across four dimensions, flag geographic outliers, and compare N items side-by-side with visual score bars. Category filtering ensures you only compare like-for-like options.
Good Enough Mode & Overplanning Nudges
Toggle caps activities per day based on trip energy, dims unchecked items at capacity, shows progress banners, and gently nudges when a day has too many activities or transport segments.
Build & Deliver
// In-memory cache: avoids duplicate OSRM calls for the same coordinate pair
const _routeCache = new Map();
export async function fetchRouteTime(lat1, lng1, lat2, lng2, mode = 'driving') {
const cacheKey = `${lat1},${lng1}-${lat2},${lng2}-${mode}`;
if (_routeCache.has(cacheKey)) return _routeCache.get(cacheKey);
// OSRM demo only exposes a driving profile — walking time is estimated
const url = `https://router.project-osrm.org/route/v1/driving/
${lng1},${lat1};${lng2},${lat2}?overview=false`;
const data = await fetch(url).then(r => r.json());
const route = data.routes[0];
const driveResult = {
durationMin: Math.round(route.duration / 60),
distanceMi: +(route.distance / 1609.34).toFixed(1),
mode: 'driving',
};
_routeCache.set(driveCacheKey, driveResult);
if (mode === 'walking') {
// ~3 mph ≈ 20 min/mile — derived from the routed driving distance
return { ...driveResult, durationMin: Math.round(driveResult.distanceMi * 20), mode: 'walking' };
}
return driveResult;
}
Core workflow
Drag-and-Drop Schedule Builder
@dnd-kit powered scheduling with Morning/Afternoon/Evening slots, buffer blocks for meals and transit, OSRM route time estimates between consecutive activities, daily travel totals with energy-based amber warnings, and smart auto-fill suggestions that cluster unscheduled activities by proximity.
// Budget and proximity are computable from data — uniqueness and group appeal are not
const calculateAutoScores = (item) => {
const price = parseFloat(item.price) || 0;
const budgetScore =
price === 0 ? 10 :
price <= 20 ? 8 :
price <= 50 ? 6 :
price <= 100 ? 4 : 2;
const dist = getDistance(hotel.lat, hotel.lng, item.lat, item.lng);
const timeScore =
dist <= 1.0 ? 10 :
dist <= 3.0 ? 8 :
dist <= 10.0 ? 6 :
dist <= 30.0 ? 4 : 2;
return { budgetScore, timeScore };
};
// Objective fields are auto-scored; subjective fields come from human ratings
const buildOption = (item) => ({
scores: {
budget: budgetScore, // computed from price
time: timeScore, // computed from distance to hotel
uniqueness: item.rating_uniqueness || 5, // human-rated on Idea Board
group: item.rating_group || 5, // human-rated on Idea Board
}
});
Group consensus
Tradeoff Board & Idea Board
The Idea Board surfaces geocoded activities per leg with 1-10 rating sliders and Haversine distance from hotel, and automatically flags outliers far from the cluster. The Tradeoff Board runs a weighted scoring matrix — auto-scoring budget from price and time from distance, manual ratings for uniqueness and group appeal, adjustable weights — with visual score bars, category filtering, and one-click winner selection. Photon-powered place autocomplete with proximity bias for quick address entry.
// Energy level set once at trip creation — configures every behavioral limit downstream
const THRESHOLDS = {
chill: { daily: 15, gap: 8, maxActivities: 2, maxTransits: 1, dailyTimeMin: 30, gapTimeMin: 15 },
balanced: { daily: 30, gap: 15, maxActivities: 4, maxTransits: 3, dailyTimeMin: 60, gapTimeMin: 30 },
packed: { daily: 60, gap: 25, maxActivities: 7, maxTransits: 5, dailyTimeMin: 120, gapTimeMin: 45 },
};
const ENERGY_CAPS = { chill: 2, balanced: 4, packed: 7 };
// DailySchedule — amber warnings fire when daily travel or gap thresholds are exceeded
const limits = THRESHOLDS[tripEnergy] || THRESHOLDS.balanced;
// CityItinerary — Good Enough Mode disables selection once the day is at capacity
const maxPerDay = ENERGY_CAPS[tripEnergy] || 4;
Trip governance
Good Enough Mode & Readiness Checklist
Energy-based activity caps prevent overplanning. Progress banners, capacity dimming, and 'This day is set' indicators enforce trip pacing. Readiness Checklist tracks hotel, activity, logistics, and schedule completeness per leg. Undo system with 5-second countdown toast for all destructive actions.
Architecture
Frontend
React 19, Vite 7, Tailwind CSS 4, @dnd-kit, React Leaflet 5
Backend
Node.js, Express 5, MySQL 8 (mysql2)
Deployment
Leaflet maps, Nominatim geocoding, OSRM routing, Photon autocomplete — all free, no API keys required