What We're Building
In this guide, you'll build an AI-powered trip planner that generates personalized travel itineraries based on user preferences. The app will collect trip details (destination, dates, budget, interests), use Claude API to generate intelligent day-by-day itineraries, display routes on interactive Mapbox maps, and track budget allocations across activities.
Key features include:
- Smart Preferences Form - Collect destination, travel dates, budget, interests, and travel style
- AI Itinerary Generation - Claude generates detailed day-by-day plans with timing, locations, and tips
- Interactive Map Visualization - See your entire trip route with Mapbox markers and directions
- Real Destination Data - Pull actual places, ratings, and photos from Google Places API
- Budget Tracking - Monitor spending across accommodations, activities, food, and transport
- Save and Share Trips - Store itineraries in Supabase and share with travel companions
Prerequisites
Before starting this guide, make sure you have the following:
- Node.js 18+ and npm/yarn installed
- Basic knowledge of React/Next.js and TypeScript
- Anthropic API key (sign up at console.anthropic.com)
- Supabase account (free tier works perfectly)
- Mapbox access token (free tier includes 50,000 map loads/month)
- Google Cloud account with Places API enabled (optional but recommended)
- VS Code with Cursor or similar AI-assisted IDE
Tech Stack Specification
Here's the technology stack we'll use for this build:
| Layer | Technology | Why This Choice |
|---|---|---|
| Frontend | Next.js 14, Tailwind CSS, Mapbox GL | Server components for fast loading, beautiful maps for trip visualization |
| Backend | Next.js API Routes | Serverless functions for AI processing and API integrations |
| Database | Supabase (PostgreSQL) | Store trips, destinations, user preferences with real-time sync |
| AI | Claude API (Anthropic) | Superior reasoning for itinerary generation and travel recommendations |
| External APIs | Google Places, Booking.com | Real destination data, reviews, photos, and accommodation options |
| Hosting | Vercel | Seamless Next.js deployment with edge functions |
AI Agent Workflow
Here's how to leverage AI tools throughout this build to maximize productivity:
Project Scaffolding with Claude Code
Use Claude Code to scaffold the entire project structure, including the database schema, API routes, and component architecture. Claude excels at understanding the full context of travel planning requirements.
# Example prompt for Claude Code
Create a Next.js 14 trip planner app with:
- App router structure with /trips, /trips/new, /trips/[id] routes
- Supabase integration for trips, destinations, activities tables
- Mapbox component for displaying itinerary routes
- Form component collecting: destination, dates, budget, interests, pace
- API route for Claude itinerary generation
- TypeScript types for Trip, Destination, Activity, DayPlan
UI Generation with v0.dev
Use v0.dev to rapidly generate the trip planning UI components. The visual nature of travel apps makes v0 particularly effective for creating engaging interfaces.
When prompting v0 for travel UI, include "with card-based day layout, timeline view, and map sidebar" for itinerary displays. Request "dreamy travel imagery placeholders" to set the right visual tone.
Development with Cursor
Cursor's AI assistance is invaluable for implementing Mapbox integrations (which can be tricky), debugging API responses from Google Places, and optimizing the Claude prompt for better itineraries.
Step-by-Step Build Guide
Phase 1: Trip Preferences Input Form
Start by creating a beautiful multi-step form that collects all the information needed to generate a personalized itinerary. This form should feel like the beginning of an adventure.
// components/TripPreferencesForm.tsx
import { useState } from 'react';
interface TripPreferences {
destination: string;
startDate: string;
endDate: string;
budget: number;
currency: string;
travelers: number;
interests: string[];
pace: 'relaxed' | 'moderate' | 'packed';
accommodation: 'budget' | 'mid-range' | 'luxury';
}
const interestOptions = [
'History & Culture', 'Food & Dining', 'Nature & Outdoors',
'Art & Museums', 'Nightlife', 'Shopping',
'Adventure Sports', 'Local Experiences', 'Photography'
];
export default function TripPreferencesForm({
onSubmit
}: {
onSubmit: (prefs: TripPreferences) => void
}) {
const [step, setStep] = useState(1);
const [preferences, setPreferences] = useState<TripPreferences>({
destination: '',
startDate: '',
endDate: '',
budget: 2000,
currency: 'USD',
travelers: 2,
interests: [],
pace: 'moderate',
accommodation: 'mid-range'
});
// Multi-step form logic...
return (
<form className="max-w-2xl mx-auto p-8">
{step === 1 && (
<div className="space-y-6">
<h2 className="text-2xl font-bold">Where to?</h2>
<DestinationAutocomplete
value={preferences.destination}
onChange={(v) => setPreferences({...preferences, destination: v})}
/>
<DateRangePicker
startDate={preferences.startDate}
endDate={preferences.endDate}
onChange={(start, end) => setPreferences({
...preferences,
startDate: start,
endDate: end
})}
/>
</div>
)}
{/* Additional steps for budget, interests, pace */}
</form>
);
}
Phase 2: Destination Database with Embeddings
Set up Supabase tables to store destinations, activities, and user trips. We'll use pgvector for semantic search of destinations based on user interests.
-- Supabase SQL: Create tables for trip planner
-- Enable pgvector extension for semantic search
CREATE EXTENSION IF NOT EXISTS vector;
-- Destinations table with embeddings
CREATE TABLE destinations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
country TEXT NOT NULL,
description TEXT,
latitude DECIMAL(10, 8),
longitude DECIMAL(11, 8),
image_url TEXT,
tags TEXT[],
embedding vector(1536), -- For semantic search
created_at TIMESTAMPTZ DEFAULT now()
);
-- Trips table
CREATE TABLE trips (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id),
destination_id UUID REFERENCES destinations(id),
title TEXT NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
budget DECIMAL(10, 2),
currency TEXT DEFAULT 'USD',
travelers INTEGER DEFAULT 1,
preferences JSONB,
itinerary JSONB, -- AI-generated itinerary
status TEXT DEFAULT 'draft',
created_at TIMESTAMPTZ DEFAULT now()
);
-- Activities table for each day
CREATE TABLE activities (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
trip_id UUID REFERENCES trips(id) ON DELETE CASCADE,
day_number INTEGER NOT NULL,
time_slot TEXT, -- 'morning', 'afternoon', 'evening'
title TEXT NOT NULL,
description TEXT,
location TEXT,
latitude DECIMAL(10, 8),
longitude DECIMAL(11, 8),
duration_minutes INTEGER,
estimated_cost DECIMAL(8, 2),
category TEXT,
booking_url TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);
Phase 3: AI Itinerary Generation
This is the heart of the application - using Claude to generate intelligent, personalized itineraries. The key is crafting a detailed prompt that includes all user preferences and destination context.
// app/api/generate-itinerary/route.ts
import Anthropic from '@anthropic-ai/sdk';
import { NextRequest, NextResponse } from 'next/server';
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
export async function POST(request: NextRequest) {
const { preferences, destinationInfo } = await request.json();
const tripDays = calculateDays(preferences.startDate, preferences.endDate);
const dailyBudget = preferences.budget / tripDays;
const systemPrompt = `You are an expert travel planner with deep knowledge
of destinations worldwide. You create personalized, realistic itineraries
that balance activities with rest, respect local customs, and optimize
for the traveler's interests and budget.
Always provide:
- Specific place names (not generic "visit a museum")
- Realistic timing with travel time between locations
- Local tips and cultural insights
- Budget estimates in the specified currency
- Latitude/longitude for each location
- Backup options for weather-dependent activities`;
const userPrompt = `Create a detailed ${tripDays}-day itinerary for
${preferences.travelers} traveler(s) visiting ${preferences.destination}.
TRIP DETAILS:
- Dates: ${preferences.startDate} to ${preferences.endDate}
- Total Budget: ${preferences.budget} ${preferences.currency}
- Daily Budget: ~${dailyBudget.toFixed(0)} ${preferences.currency}
- Accommodation Style: ${preferences.accommodation}
- Travel Pace: ${preferences.pace}
- Interests: ${preferences.interests.join(', ')}
DESTINATION CONTEXT:
${destinationInfo.description}
Popular areas: ${destinationInfo.neighborhoods?.join(', ')}
Best time to visit attractions: ${destinationInfo.tips}
Return a JSON object with this structure:
{
"title": "Trip title",
"summary": "2-3 sentence trip overview",
"days": [
{
"dayNumber": 1,
"date": "YYYY-MM-DD",
"theme": "Day theme (e.g., 'Historic Center Exploration')",
"activities": [
{
"timeSlot": "morning|afternoon|evening",
"startTime": "09:00",
"endTime": "12:00",
"title": "Activity name",
"description": "What you'll do and why it's special",
"location": "Specific place name",
"address": "Full address",
"latitude": 0.000000,
"longitude": 0.000000,
"duration": 180,
"estimatedCost": 25,
"category": "culture|food|nature|adventure|shopping|nightlife",
"tips": "Local insider tip",
"bookingRequired": false,
"bookingUrl": null
}
],
"meals": {
"breakfast": { "name": "Place", "cuisine": "Type", "priceRange": "$$" },
"lunch": { "name": "Place", "cuisine": "Type", "priceRange": "$$" },
"dinner": { "name": "Place", "cuisine": "Type", "priceRange": "$$" }
},
"dailyBudget": {
"activities": 50,
"food": 60,
"transport": 20,
"total": 130
}
}
],
"budgetBreakdown": {
"accommodation": 800,
"activities": 400,
"food": 500,
"transport": 200,
"misc": 100,
"total": 2000
},
"packingTips": ["Item 1", "Item 2"],
"importantNotes": ["Note about local customs", "Safety tip"]
}`;
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 8000,
messages: [
{ role: 'user', content: userPrompt }
],
system: systemPrompt,
});
const itinerary = JSON.parse(response.content[0].text);
return NextResponse.json({ itinerary });
}
function calculateDays(start: string, end: string): number {
const startDate = new Date(start);
const endDate = new Date(end);
return Math.ceil((endDate - startDate) / (1000 * 60 * 60 * 24)) + 1;
}
Claude excels at travel planning because it understands context deeply - it knows that visiting the Louvre requires 3-4 hours, that you shouldn't schedule outdoor activities during Paris's rainy season, and that certain neighborhoods are better for evening dining. This contextual reasoning creates more realistic itineraries than template-based systems.
Phase 4: Day-by-Day Planning UI
Create an intuitive interface that displays the AI-generated itinerary in a timeline format, allowing users to view, edit, and customize their plans.
// components/ItineraryTimeline.tsx
import { Itinerary, DayPlan, Activity } from '@/types/trip';
interface ItineraryTimelineProps {
itinerary: Itinerary;
onActivityClick: (activity: Activity) => void;
onActivityEdit: (dayNum: number, activity: Activity) => void;
}
export default function ItineraryTimeline({
itinerary,
onActivityClick,
onActivityEdit
}: ItineraryTimelineProps) {
return (
<div className="space-y-8">
{itinerary.days.map((day) => (
<div key={day.dayNumber} className="bg-white rounded-xl shadow-sm border p-6">
<div className="flex items-center justify-between mb-6">
<div>
<span className="text-sm text-cyan-600 font-medium">
Day {day.dayNumber}
</span>
<h3 className="text-xl font-bold text-gray-900">
{day.theme}
</h3>
<p className="text-gray-500">{formatDate(day.date)}</p>
</div>
<div className="text-right">
<p className="text-sm text-gray-500">Daily Budget</p>
<p className="text-lg font-semibold">
${day.dailyBudget.total}
</p>
</div>
</div>
<div className="relative pl-8 border-l-2 border-cyan-200">
{day.activities.map((activity, idx) => (
<ActivityCard
key={idx}
activity={activity}
onClick={() => onActivityClick(activity)}
onEdit={(updated) => onActivityEdit(day.dayNumber, updated)}
/>
))}
</div>
</div>
))}
</div>
);
}
Phase 5: Map Visualization of Routes
Integrate Mapbox to display the full trip route with markers for each activity. Users can see their entire journey visualized on an interactive map.
// components/TripMap.tsx
import { useEffect, useRef, useState } from 'react';
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN!;
interface TripMapProps {
itinerary: Itinerary;
selectedDay?: number;
onMarkerClick: (activity: Activity) => void;
}
export default function TripMap({
itinerary,
selectedDay,
onMarkerClick
}: TripMapProps) {
const mapContainer = useRef<HTMLDivElement>(null);
const map = useRef<mapboxgl.Map>(null);
const [markers, setMarkers] = useState<mapboxgl.Marker[]>([]);
useEffect(() => {
if (!mapContainer.current) return;
map.current = new mapboxgl.Map({
container: mapContainer.current,
style: 'mapbox://styles/mapbox/streets-v12',
center: [itinerary.centerLongitude, itinerary.centerLatitude],
zoom: 12
});
return () => map.current?.remove();
}, []);
useEffect(() => {
if (!map.current) return;
// Clear existing markers
markers.forEach(m => m.remove());
// Get activities for selected day or all days
const activities = selectedDay
? itinerary.days.find(d => d.dayNumber === selectedDay)?.activities || []
: itinerary.days.flatMap(d => d.activities);
// Add markers with day colors
const newMarkers = activities.map((activity, idx) => {
const el = document.createElement('div');
el.className = 'custom-marker';
el.innerHTML = `<span>${idx + 1}</span>`;
el.style.backgroundColor = getDayColor(activity.dayNumber);
const marker = new mapboxgl.Marker(el)
.setLngLat([activity.longitude, activity.latitude])
.setPopup(
new mapboxgl.Popup({ offset: 25 })
.setHTML(`
<h3 class="font-bold">${activity.title}</h3>
<p class="text-sm">${activity.startTime} - ${activity.endTime}</p>
<p class="text-sm text-gray-600">${activity.location}</p>
`)
)
.addTo(map.current!);
el.addEventListener('click', () => onMarkerClick(activity));
return marker;
});
setMarkers(newMarkers);
// Draw route line between activities
if (activities.length > 1) {
drawRoute(map.current, activities);
}
}, [itinerary, selectedDay]);
return (
<div
ref={mapContainer}
className="w-full h-[500px] rounded-xl overflow-hidden"
/>
);
}
Phase 6: Budget Tracking Feature
Implement a budget tracker that aggregates costs across all activities and provides visual breakdowns by category and day.
// components/BudgetTracker.tsx
import { useMemo } from 'react';
import { PieChart, Pie, Cell, ResponsiveContainer } from 'recharts';
interface BudgetTrackerProps {
itinerary: Itinerary;
totalBudget: number;
currency: string;
}
export default function BudgetTracker({
itinerary,
totalBudget,
currency
}: BudgetTrackerProps) {
const budgetData = useMemo(() => {
const breakdown = itinerary.budgetBreakdown;
return [
{ name: 'Accommodation', value: breakdown.accommodation, color: '#0891b2' },
{ name: 'Activities', value: breakdown.activities, color: '#06b6d4' },
{ name: 'Food & Dining', value: breakdown.food, color: '#22d3ee' },
{ name: 'Transport', value: breakdown.transport, color: '#67e8f9' },
{ name: 'Miscellaneous', value: breakdown.misc, color: '#a5f3fc' },
];
}, [itinerary]);
const totalEstimated = budgetData.reduce((sum, item) => sum + item.value, 0);
const remaining = totalBudget - totalEstimated;
const isOverBudget = remaining < 0;
return (
<div className="bg-white rounded-xl shadow-sm border p-6">
<h3 className="text-lg font-bold mb-4">Budget Overview</h3>
<div className="flex items-center gap-8">
<div className="w-48 h-48">
<ResponsiveContainer>
<PieChart>
<Pie
data={budgetData}
innerRadius={50}
outerRadius={80}
paddingAngle={2}
dataKey="value"
>
{budgetData.map((entry, idx) => (
<Cell key={idx} fill={entry.color} />
))}
</Pie>
</PieChart>
</ResponsiveContainer>
</div>
<div className="flex-1 space-y-3">
{budgetData.map((item) => (
<div key={item.name} className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: item.color }}
/>
<span className="text-sm text-gray-600">{item.name}</span>
</div>
<span className="font-medium">
{currency} {item.value.toLocaleString()}
</span>
</div>
))}
</div>
</div>
<div className={`mt-6 p-4 rounded-lg ${isOverBudget ? 'bg-red-50' : 'bg-green-50'}`}>
<div className="flex justify-between items-center">
<span className="font-medium">
{isOverBudget ? 'Over Budget' : 'Remaining'}
</span>
<span className={`text-xl font-bold ${isOverBudget ? 'text-red-600' : 'text-green-600'}`}>
{currency} {Math.abs(remaining).toLocaleString()}
</span>
</div>
</div>
</div>
);
}
Common Issues & Solutions
Here are some common issues you might encounter and how to solve them:
If Claude's response isn't valid JSON, wrap the API call with error handling and request a retry. Add "Return ONLY valid JSON, no markdown or explanations" to your prompt. Consider using Claude's response_format parameter when available.
Ensure Mapbox CSS is imported and the access token is set before creating the map. Check that coordinates are in [longitude, latitude] order (Mapbox uses GeoJSON format). Verify markers are added after the map's 'load' event fires.
Implement caching for place details in Supabase to reduce API calls. Use session tokens for autocomplete requests. Consider batching requests and adding debounce to search inputs.
Additional tips for smooth development:
- Itinerary Generation Timeout - Claude can take 10-30 seconds for complex itineraries. Show a loading state with travel tips while waiting.
- Coordinate Accuracy - When Claude provides coordinates, validate them against known bounds for the destination. Major errors can place activities in the wrong city.
- Budget Calculations - Always show estimates as ranges since actual costs vary. Include a 10-15% buffer recommendation.
Next Steps
Once you have the core trip planner working, consider adding these enhancements to create a production-ready application:
- Booking Integration - Connect to Booking.com or Expedia APIs to allow direct hotel and activity reservations from the itinerary
- Collaborative Trips - Add real-time collaboration so travel groups can edit itineraries together using Supabase Realtime
- Offline Mode - Use service workers to cache itineraries and maps for offline access during travel
- Flight Integration - Add Amadeus or Skyscanner API to include flight options and optimize arrival/departure timing
- Weather Awareness - Integrate weather forecasts to suggest indoor alternatives or optimal activity timing
- Export Options - Generate PDF itineraries and calendar exports (.ics) for travelers
Follow the Vibe Coding Enthusiast
Follow JD — product updates on LinkedIn, personal takes on X.