Skip to main content
RapidDev - Software Development Agency

How to Build Travel itinerary app with V0

Build a travel itinerary planner with V0 featuring day-by-day trip planning, activity scheduling with time slots, budget tracking, map integration via react-leaflet, and shareable public trip links. You'll create a visual timeline, drag-to-reorder activities, and collaborative editing — all in about 1-2 hours.

What you'll build

  • Trip planner with day-by-day itinerary view using Tabs for switching between days
  • Activity cards with time slots, location name, cost tracking, and category Badge labels
  • Map overview of all activities using react-leaflet (free, no API key required)
  • Budget tracking with running total and per-category breakdown
  • Shareable public trip link using unique share_token without authentication
  • Trip collaboration with editable permissions for invited users
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate10 min read1-2 hoursV0 FreeApril 2026RapidDev Engineering Team
TL;DR

Build a travel itinerary planner with V0 featuring day-by-day trip planning, activity scheduling with time slots, budget tracking, map integration via react-leaflet, and shareable public trip links. You'll create a visual timeline, drag-to-reorder activities, and collaborative editing — all in about 1-2 hours.

What you're building

Planning a multi-day trip involves coordinating activities, tracking budgets, and sharing plans with travel companions. Spreadsheets are cumbersome and maps apps do not track schedules. A dedicated itinerary planner combines scheduling, budgeting, and mapping in one interface.

V0 generates the itinerary layout, activity forms, and map integration from prompts. react-leaflet provides free map rendering without API keys. Supabase stores trips, days, and activities with shareable public links via unique tokens.

The architecture uses Server Components for trip overview pages, a client component for the interactive day planner with drag-to-reorder, Server Actions for CRUD operations, and a public share route that uses RLS policies to allow unauthenticated access via share_token.

Final result

A travel itinerary planner with day-by-day scheduling, activity management, map visualization, budget tracking, and shareable public trip links.

Tech stack

V0AI Code Generator
Next.jsFull-Stack Framework
Tailwind CSSStyling
shadcn/uiComponent Library
SupabaseDatabase
react-leafletMaps

Prerequisites

  • A V0 account (free tier works for this project)
  • A Supabase project (free tier works — connect via V0's Connect panel)
  • No map API key needed — react-leaflet uses free OpenStreetMap tiles
  • Trip ideas to plan (destinations, activities, accommodations)

Build steps

1

Set up the trip planning database schema

Open V0 and create a new project. Use the Connect panel to add Supabase. Create the trips, itinerary_days, activities, and trip_collaborators tables with a share_token for public links.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Create a Supabase schema for a travel itinerary app:
3// 1. trips table: id (uuid PK), owner_id (uuid FK), title (text), destination (text), start_date (date), end_date (date), cover_image_url (text), budget_cents (int nullable), share_token (text UNIQUE DEFAULT gen_random_uuid()), is_public (boolean DEFAULT false), created_at (timestamptz)
4// 2. itinerary_days table: id (uuid PK), trip_id (uuid FK), date (date), notes (text)
5// 3. activities table: id (uuid PK), day_id (uuid FK), title (text), description (text), start_time (time), end_time (time nullable), location_name (text), latitude (numeric nullable), longitude (numeric nullable), cost_cents (int DEFAULT 0), category (text — 'transport', 'food', 'activity', 'accommodation', 'other'), position (int for ordering)
6// 4. trip_collaborators table: trip_id (uuid FK), user_id (uuid FK), can_edit (boolean DEFAULT false), PRIMARY KEY(trip_id, user_id)
7// RLS: owners and collaborators can access trips. Public trips accessible via share_token without auth.
8// Seed a sample 3-day trip to Tokyo with 4 activities per day.

Expected result: Four tables created with RLS policies supporting both authenticated access and public share_token access. Sample Tokyo trip seeded.

2

Build the day-by-day itinerary view with activity cards

Create the main trip page with Tabs for switching between days. Each day shows activity Cards in chronological order with time, location, cost, and category Badge.

app/trips/[id]/page.tsx
1import { createClient } from '@/lib/supabase/server'
2import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
3import { Card, CardContent } from '@/components/ui/card'
4import { Badge } from '@/components/ui/badge'
5import { Button } from '@/components/ui/button'
6import { Separator } from '@/components/ui/separator'
7
8export default async function TripPage({ params }: { params: Promise<{ id: string }> }) {
9 const { id } = await params
10 const supabase = await createClient()
11
12 const { data: trip } = await supabase.from('trips').select('*').eq('id', id).single()
13 const { data: days } = await supabase
14 .from('itinerary_days')
15 .select('*, activities(*)')
16 .eq('trip_id', id)
17 .order('date')
18
19 const totalBudget = days?.flatMap(d => d.activities).reduce((sum: number, a: any) => sum + (a.cost_cents ?? 0), 0) ?? 0
20
21 return (
22 <div className="max-w-4xl mx-auto p-6">
23 <h1 className="text-3xl font-bold">{trip?.title}</h1>
24 <p className="text-muted-foreground">{trip?.destination}</p>
25 <p className="text-sm mt-2">Total spent: ${(totalBudget / 100).toFixed(2)}
26 {trip?.budget_cents && ` / $${(trip.budget_cents / 100).toFixed(2)} budget`}
27 </p>
28 <Separator className="my-4" />
29 <Tabs defaultValue={days?.[0]?.id}>
30 <TabsList>
31 {days?.map((day, i) => (
32 <TabsTrigger key={day.id} value={day.id}>Day {i + 1}</TabsTrigger>
33 ))}
34 </TabsList>
35 {days?.map((day) => (
36 <TabsContent key={day.id} value={day.id} className="space-y-3">
37 <p className="text-sm text-muted-foreground">{new Date(day.date).toLocaleDateString()}</p>
38 {day.activities
39 ?.sort((a: any, b: any) => a.position - b.position)
40 .map((activity: any) => (
41 <Card key={activity.id}>
42 <CardContent className="p-4 flex justify-between items-start">
43 <div>
44 <p className="font-semibold">{activity.title}</p>
45 <p className="text-sm text-muted-foreground">
46 {activity.start_time} {activity.location_name}
47 </p>
48 </div>
49 <div className="flex items-center gap-2">
50 <Badge variant="outline">{activity.category}</Badge>
51 {activity.cost_cents > 0 && (
52 <Badge variant="secondary">${(activity.cost_cents / 100).toFixed(2)}</Badge>
53 )}
54 </div>
55 </CardContent>
56 </Card>
57 ))}
58 <Button variant="outline" size="sm">+ Add Activity</Button>
59 </TabsContent>
60 ))}
61 </Tabs>
62 </div>
63 )
64}

Pro tip: Use Design Mode (Option+D) to visually polish the day timeline layout — adjust Card spacing, category Badge colors, and add visual time connectors between activities at zero credit cost.

Expected result: A trip page with Tab navigation between days. Each day shows activity Cards chronologically with time, location, category Badge, and cost.

3

Integrate the map overview with react-leaflet

Add a map page that shows all activities across all days as pins. Each pin shows the activity name and time when clicked. react-leaflet uses free OpenStreetMap tiles — no API key needed.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Add a map overview page at app/trips/[id]/map/page.tsx.
3// Requirements:
4// - Install and use react-leaflet with OpenStreetMap tiles (free, no API key)
5// - Fetch all activities for this trip that have latitude and longitude
6// - Display each activity as a marker on the map
7// - Clicking a marker shows a popup with: activity title, time, location name, category Badge, and cost
8// - Color-code markers by category (transport=blue, food=orange, activity=green, accommodation=purple)
9// - Auto-center and zoom the map to fit all markers
10// - Use dynamic(() => import(...), { ssr: false }) since Leaflet doesn't support SSR
11// - Add a link back to the itinerary view
12// Important: Leaflet CSS must be imported for the map to render correctly.

Expected result: A map page showing all trip activities as colored markers. Clicking a marker shows activity details in a popup. The map auto-centers to fit all pins.

4

Build the shareable public trip link

Create a public share page that fetches a trip using its share_token without requiring authentication. The RLS policy allows SELECT on trips where is_public is true and the token matches.

app/share/[token]/page.tsx
1import { createClient } from '@/lib/supabase/server'
2import { Card, CardContent } from '@/components/ui/card'
3import { Badge } from '@/components/ui/badge'
4import { Separator } from '@/components/ui/separator'
5import { notFound } from 'next/navigation'
6
7export default async function SharedTrip({ params }: { params: Promise<{ token: string }> }) {
8 const { token } = await params
9 const supabase = await createClient()
10
11 const { data: trip } = await supabase
12 .from('trips')
13 .select('*, itinerary_days(*, activities(*))')
14 .eq('share_token', token)
15 .eq('is_public', true)
16 .single()
17
18 if (!trip) notFound()
19
20 return (
21 <div className="max-w-3xl mx-auto p-6">
22 <h1 className="text-3xl font-bold">{trip.title}</h1>
23 <p className="text-muted-foreground">{trip.destination}</p>
24 <Separator className="my-4" />
25 {trip.itinerary_days
26 ?.sort((a: any, b: any) => a.date.localeCompare(b.date))
27 .map((day: any, i: number) => (
28 <div key={day.id} className="mb-6">
29 <h2 className="text-xl font-semibold mb-3">Day {i + 1} {new Date(day.date).toLocaleDateString()}</h2>
30 {day.activities
31 ?.sort((a: any, b: any) => a.position - b.position)
32 .map((activity: any) => (
33 <Card key={activity.id} className="mb-2">
34 <CardContent className="p-3 flex justify-between">
35 <div>
36 <p className="font-medium">{activity.title}</p>
37 <p className="text-sm text-muted-foreground">{activity.start_time} {activity.location_name}</p>
38 </div>
39 <Badge variant="outline">{activity.category}</Badge>
40 </CardContent>
41 </Card>
42 ))}
43 </div>
44 ))}
45 </div>
46 )
47}

Expected result: A read-only public trip view accessible without login. Anyone with the share link sees the full itinerary with days and activities.

5

Add Server Actions for trip and activity management

Create Server Actions for creating trips, adding days and activities, reordering activities, and toggling the share link. These handle all the CRUD operations the planner needs.

app/actions/trips.ts
1'use server'
2
3import { createClient } from '@/lib/supabase/server'
4import { revalidatePath } from 'next/cache'
5
6export async function createTrip(formData: FormData) {
7 const supabase = await createClient()
8 const { data: { user } } = await supabase.auth.getUser()
9 if (!user) return { error: 'Not authenticated' }
10
11 const title = formData.get('title') as string
12 const destination = formData.get('destination') as string
13 const startDate = formData.get('startDate') as string
14 const endDate = formData.get('endDate') as string
15
16 const { data: trip } = await supabase.from('trips').insert({
17 owner_id: user.id, title, destination,
18 start_date: startDate, end_date: endDate,
19 }).select('id').single()
20
21 // Auto-create itinerary days
22 const start = new Date(startDate)
23 const end = new Date(endDate)
24 const days = []
25 for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
26 days.push({ trip_id: trip!.id, date: d.toISOString().split('T')[0] })
27 }
28 await supabase.from('itinerary_days').insert(days)
29
30 revalidatePath('/trips')
31 return { tripId: trip!.id }
32}
33
34export async function addActivity(formData: FormData) {
35 const supabase = await createClient()
36 const dayId = formData.get('dayId') as string
37 const { data: maxPos } = await supabase
38 .from('activities').select('position')
39 .eq('day_id', dayId).order('position', { ascending: false }).limit(1).single()
40
41 await supabase.from('activities').insert({
42 day_id: dayId,
43 title: formData.get('title') as string,
44 start_time: formData.get('startTime') as string,
45 location_name: formData.get('location') as string,
46 category: formData.get('category') as string,
47 cost_cents: parseInt(formData.get('cost') as string || '0') * 100,
48 position: (maxPos?.position ?? 0) + 1,
49 })
50 revalidatePath(`/trips`)
51}
52
53export async function toggleShareLink(tripId: string, isPublic: boolean) {
54 const supabase = await createClient()
55 await supabase.from('trips').update({ is_public: isPublic }).eq('id', tripId)
56 revalidatePath(`/trips/${tripId}`)
57}

Expected result: Creating a trip auto-generates itinerary_days for each date in the range. Activities are added with automatic position ordering. Share link toggles public visibility.

Complete code

app/trips/[id]/page.tsx
1import { createClient } from '@/lib/supabase/server'
2import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
3import { Card, CardContent } from '@/components/ui/card'
4import { Badge } from '@/components/ui/badge'
5import { Separator } from '@/components/ui/separator'
6import { Button } from '@/components/ui/button'
7
8export default async function TripPage({
9 params,
10}: {
11 params: Promise<{ id: string }>
12}) {
13 const { id } = await params
14 const supabase = await createClient()
15
16 const { data: trip } = await supabase
17 .from('trips')
18 .select('*')
19 .eq('id', id)
20 .single()
21
22 const { data: days } = await supabase
23 .from('itinerary_days')
24 .select('*, activities(*)')
25 .eq('trip_id', id)
26 .order('date')
27
28 const total = days
29 ?.flatMap((d) => d.activities)
30 .reduce((sum: number, a: any) => sum + (a.cost_cents ?? 0), 0) ?? 0
31
32 return (
33 <div className="max-w-4xl mx-auto p-6">
34 <h1 className="text-3xl font-bold">{trip?.title}</h1>
35 <p className="text-muted-foreground">{trip?.destination}</p>
36 <p className="text-sm mt-1">Budget: ${(total / 100).toFixed(2)}</p>
37 <Separator className="my-4" />
38 <Tabs defaultValue={days?.[0]?.id}>
39 <TabsList>
40 {days?.map((day, i) => (
41 <TabsTrigger key={day.id} value={day.id}>
42 Day {i + 1}
43 </TabsTrigger>
44 ))}
45 </TabsList>
46 {days?.map((day) => (
47 <TabsContent key={day.id} value={day.id} className="space-y-3">
48 {day.activities
49 ?.sort((a: any, b: any) => a.position - b.position)
50 .map((act: any) => (
51 <Card key={act.id}>
52 <CardContent className="p-4 flex justify-between">
53 <div>
54 <p className="font-semibold">{act.title}</p>
55 <p className="text-sm text-muted-foreground">
56 {act.start_time} - {act.location_name}
57 </p>
58 </div>
59 <Badge variant="outline">{act.category}</Badge>
60 </CardContent>
61 </Card>
62 ))}
63 <Button variant="outline" size="sm">+ Add Activity</Button>
64 </TabsContent>
65 ))}
66 </Tabs>
67 </div>
68 )
69}

Customization ideas

Add packing list

Create a checklist component for packing items linked to trips. Users can check off items as they pack with a progress indicator.

Add weather forecast integration

Fetch weather forecasts for the trip destination and dates using Open-Meteo API. Display daily weather icons on each day tab.

Add flight and hotel booking links

Integrate affiliate links to booking sites for flights and hotels at the destination. Display them as suggestions based on trip dates.

Add photo journal

Let users upload photos to each day using Supabase Storage. Create a trip photo gallery that maps images to itinerary days.

Common pitfalls

Pitfall: Importing Leaflet in a Server Component without dynamic import

How to avoid: Use dynamic(() => import('../components/map'), { ssr: false }) to load the map component only on the client side.

Pitfall: Not auto-generating itinerary_days when creating a trip

How to avoid: In the createTrip Server Action, loop from start_date to end_date and insert one itinerary_days row per date.

Pitfall: Making the share_token guessable

How to avoid: Use gen_random_uuid() for share_token, generating 128-bit UUIDs that are practically unguessable.

Best practices

  • Use react-leaflet with OpenStreetMap tiles for free map rendering — no API key or billing required
  • Use dynamic import with { ssr: false } for the map component since Leaflet requires browser APIs
  • Use Tabs from shadcn/ui for day switching — clean UX for multi-day itineraries
  • Auto-generate itinerary_days from start/end dates when creating a trip so the planner is ready immediately
  • Use Design Mode (Option+D) to polish activity Card layouts and category Badge colors at zero credit cost
  • Use gen_random_uuid() for share_token to generate secure, unguessable public links
  • Store costs in cents as integers to avoid floating-point rounding errors in budget calculations

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a travel itinerary planner with Next.js App Router and Supabase. I need: 1) Day-by-day trip planning with activity scheduling, 2) Map integration showing all activities as pins, 3) Budget tracking per activity and total, 4) Shareable public trip links via unique token. Help me design the schema and the public share RLS policy.

Build Prompt

Create a shareable public trip page at app/share/[token]/page.tsx that: 1) Fetches a trip by share_token from Supabase without authentication, 2) Uses an RLS policy that allows SELECT on trips WHERE is_public = true AND share_token matches, 3) Displays the full itinerary in read-only mode with day-by-day activities, 4) Shows a clean layout without edit buttons. Handle the case where the token is invalid with notFound().

Frequently asked questions

Do I need a map API key?

No. react-leaflet uses OpenStreetMap tiles which are completely free and require no API key. Just install react-leaflet and import the tile layer — maps work immediately.

How do shareable trip links work?

Each trip has a unique share_token (UUID). When is_public is toggled on, the RLS policy allows unauthenticated users to read the trip via the token. The share URL looks like yourdomain.com/share/abc123-def456.

What V0 plan do I need?

V0 Free tier works. The itinerary app uses standard Server Components, dynamic imports for the map, and shadcn/ui components. No paid integrations required.

Can multiple people edit a trip?

Yes. The trip_collaborators table stores invited users with can_edit permissions. RLS policies allow collaborators to read and optionally edit the trip alongside the owner.

How do I deploy this app?

Click Share > Publish in V0. The Supabase connection is auto-configured. The map component works identically in production since it uses public OpenStreetMap tiles.

Can RapidDev help build a custom travel platform?

Yes. RapidDev has built 600+ apps including travel platforms with itinerary planning, booking integration, and social sharing features. Book a free consultation to discuss your travel app requirements.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help building your app?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.