// Smart Travel Planner – Next.js 14 (App Router) + Tailwind + Supabase + Mock AI // ----------------------------------------------------------------------------- // This is a deploy‑ready, one‑page travel planner with: // - Attractive header with plane logo + slogan // - Form: destination, dates, travellers, optional budget, transport (with icons) // - Currency switcher (default INR) + live FX endpoint (stub provided) // - Generate Itinerary → tabs: Itinerary, Hotels, Places, Vehicle, Cost, Restaurants, Booking // - Supabase Auth (email magic link) + Trip save/load (schema below) // - Booking & Payments UI (demo) with API placeholders for real providers // - Fully responsive (mobile → tablet → desktop) with subtle 3D (neumorphic) styling // - AI Itinerary generator stub (server route) with deterministic fallback // // HOW TO USE // ----------------------------------------------------------------------------- // 1) Create a new Next.js app (App Router): // npx create-next-app@latest smart-travel-planner --ts --eslint // cd smart-travel-planner // 2) Install deps: // npm i framer-motion lucide-react @supabase/supabase-js // npm i -D tailwindcss postcss autoprefixer // npx tailwindcss init -p // 3) Replace/add the files exactly as below. // 4) Create .env.local with your Supabase keys (see below) and optionally OPENAI_API_KEY (if you wire real AI). // 5) Run: npm run dev → then deploy to Vercel. // // FILES START ================================================================ // --- file: package.json (merge scripts if needed) --- { "name": "smart-travel-planner", "version": "1.0.0", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint" }, "dependencies": { "@supabase/supabase-js": "^2.45.0", "framer-motion": "^11.0.0", "lucide-react": "^0.441.0", "next": "^14.2.5", "react": "^18.3.1", "react-dom": "^18.3.1" }, "devDependencies": { "autoprefixer": "^10.4.19", "postcss": "^8.4.41", "tailwindcss": "^3.4.10", "typescript": "^5.5.4" } } // --- file: tailwind.config.ts --- import type { Config } from "tailwindcss"; export default { content: ["./app/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./pages/**/*.{ts,tsx}"], theme: { extend: {} }, plugins: [], } satisfies Config; // --- file: app/globals.css --- @tailwind base; @tailwind components; @tailwind utilities; :root { color-scheme: light; } html, body { height: 100%; } // --- file: app/layout.tsx --- import "./globals.css"; import type { ReactNode } from "react"; export const metadata = { title: "Smart Travel Planner", description: "AI-powered trip planning made easy" }; export default function RootLayout({ children }: { children: ReactNode }) { return ( {children} ); } // --- file: lib/supabaseClient.ts --- "use client"; import { createClient } from "@supabase/supabase-js"; export const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ); // --- file: app/api/fx/route.ts --- import { NextResponse } from "next/server"; // Live FX placeholder: base INR. Replace implementation with a real FX provider. export async function GET() { try { // Example for live call: // const res = await fetch("https://api.exchangerate.host/latest?base=INR"); // const json = await res.json(); // return NextResponse.json({ rates: json.rates }); return NextResponse.json({ rates: { INR: 1, USD: 0.012, EUR: 0.011, GBP: 0.0095, JPY: 1.9, AED: 0.044, AUD: 0.018, CAD: 0.017, SGD: 0.016 }, }); } catch (e) { return NextResponse.json({ error: "FX fetch failed" }, { status: 500 }); } } // --- file: app/api/ai-itinerary/route.ts --- import { NextResponse } from "next/server"; // Simple deterministic server itinerary; plug real model if desired export async function POST(req: Request) { const { destination, days, places } = await req.json(); const plan = Array.from({ length: days }).map((_, i) => ({ day: i + 1, title: `Explore ${destination} – Day ${i + 1}`, morning: places?.[i % (places?.length || 1)]?.name || "City walk", afternoon: "Local market & cafe crawl", evening: "Sunset viewpoint & dinner", tips: "Book tickets in advance if possible.", })); return NextResponse.json({ plan }); } // --- file: app/api/trips/route.ts --- import { NextResponse } from "next/server"; // This is a demo-only endpoint (no DB). For Supabase, use client libs from the page export async function POST(req: Request) { const data = await req.json(); // Normally validate & store to DB here. return NextResponse.json({ ok: true, data }); } // --- file: app/page.tsx --- "use client"; import React, { useEffect, useMemo, useState } from "react"; import { motion } from "framer-motion"; import { Plane, Calendar as CalendarIcon, Users, IndianRupee, Bike, Car, Bus, Train, PlaneTakeoff, MapPin, Hotel, UtensilsCrossed, Wallet, Cog, Search, CreditCard, CheckCircle, LogIn, LogOut } from "lucide-react"; import { supabase } from "@/lib/supabaseClient"; // --- Utilities --- const dayDiff = (a: string, b: string) => { const ms = Math.max(0, new Date(b).setHours(0, 0, 0, 0) - new Date(a).setHours(0, 0, 0, 0)); return Math.floor(ms / (1000 * 60 * 60 * 24)) + 1; }; const enumerateDates = (start: string, end: string) => { const days: Date[] = []; let d = new Date(start); const last = new Date(end); while (d <= last) { days.push(new Date(d)); d.setDate(d.getDate() + 1); } return days; }; const DEFAULT_RATES: Record = { INR: 1, USD: 0.012, EUR: 0.011, GBP: 0.0095, JPY: 1.9, AED: 0.044, AUD: 0.018, CAD: 0.017, SGD: 0.016 }; const CURRENCIES = Object.keys(DEFAULT_RATES); function useCurrency() { const [currency, setCurrency] = useState("INR"); const [rates, setRates] = useState(DEFAULT_RATES); const [refreshing, setRefreshing] = useState(false); async function refreshRates() { try { setRefreshing(true); const res = await fetch("/api/fx"); if (res.ok) { const j = await res.json(); if (j?.rates) setRates(j.rates); } } finally { setRefreshing(false); } } function convertINR(amountInINR: number, to = currency) { const rate = rates[to] ?? 1; return amountInINR * rate; } return { currency, setCurrency, rates, convertINR, refreshRates, refreshing }; } function seededRandom(seed: string) { let x = 0; for (let i = 0; i < seed.length; i++) x ^= seed.charCodeAt(i) << ((i % 4) * 8); return () => { x ^= x << 13; x ^= x >>> 17; x ^= x << 5; return (x >>> 0) / 4294967296; }; } function guessRegionCostFactor(destination: string) { const d = destination.toLowerCase(); if (/switzerland|iceland|norway|japan|singapore|uae|uk|london|paris|new york|sydney/.test(d)) return 3.0; if (/india|vietnam|nepal|indonesia|thailand|turkey|mexico|portugal/.test(d)) return 1.0; return 2.0; } function createHotels(dest: string, count = 6) { const rnd = seededRandom(dest + "hotels"); return Array.from({ length: count }).map((_, i) => ({ id: `h${i}`, name: `${dest} Grand ${i + 1}`, rating: (3 + rnd() * 2).toFixed(1), distanceKm: (0.5 + rnd() * 8).toFixed(1), pricePerNightINR: Math.floor(1500 + rnd() * 7000), amenities: ["Wi‑Fi", "Breakfast", rnd() > 0.5 ? "Pool" : "Gym"] })); } function createPlaces(dest: string, count = 6) { const rnd = seededRandom(dest + "places"); const samples = ["Historic Fort","City Museum","Botanical Garden","Scenic Viewpoint","Art District","Riverside Walk","Night Market","National Park"]; return Array.from({ length: count }).map((_, i) => ({ id: `p${i}`, name: `${dest} ${samples[Math.floor(rnd() * samples.length)]}`, bestTime: rnd() > 0.5 ? "Morning" : "Evening", ticketINR: Math.floor(100 + rnd() * 800), note: rnd() > 0.6 ? "Guided tours available" : "Free entry on Sundays" })); } function createRestaurants(dest: string, count = 6) { const rnd = seededRandom(dest + "food"); const cuisines = ["Indian","Italian","Asian","Mexican","Seafood","Vegan","BBQ","Bakery"]; return Array.from({ length: count }).map((_, i) => ({ id: `r${i}`, name: `${dest} ${cuisines[Math.floor(rnd() * cuisines.length)]} Kitchen`, rating: (3 + rnd() * 2).toFixed(1), avgMealINR: Math.floor(250 + rnd() * 1200), popular: rnd() > 0.5 ? "Bestseller" : "Family friendly" })); } function createVehicles(dest: string, mode: string = "flight") { const rnd = seededRandom(dest + mode); const rows: any[] = []; const n = 5; for (let i = 0; i < n; i++) { const base = 900 + rnd() * 4500; rows.push({ id: `${mode}-${i}`, name: mode === "flight" ? `${dest} Airways ${100 + i}` : mode === "train" ? `${dest} Express ${200 + i}` : mode === "bus" ? `${dest} Coach ${300 + i}` : mode === "car" ? `Self‑Drive Hatchback ${i + 1}` : `Touring Bike ${i + 1}`, depart: `${8 + i}:00`, arrive: `${10 + i}:30`, pricePerPersonINR: Math.floor(base * (mode === "flight" ? 2.2 : mode === "train" ? 0.6 : mode === "bus" ? 0.5 : 0.4)), refundable: rnd() > 0.5 }); } return rows; } function estimateCosts({ destination, startDate, endDate, travellers, mode }: any) { const nights = Math.max(1, dayDiff(startDate, endDate) - 1); const factor = guessRegionCostFactor(destination); const hotelPerNightINR = 2500 * factor; const rooms = Math.ceil(travellers / 2); const hotelTotal = hotelPerNightINR * rooms * nights; const foodPerPersonPerDayINR = 900 * factor; const localTransportPerDayINR = 600 * factor; const sightseeingPerPersonINR = 800 * factor; const vehicleOptions = createVehicles(destination, mode); const transportPerPersonINR = vehicleOptions[1]?.pricePerPersonINR ?? 2000; const persons = travellers; const stayFoodLocal = nights * (foodPerPersonPerDayINR * persons + localTransportPerDayINR); const sightseeing = sightseeingPerPersonINR * persons; const toFrom = transportPerPersonINR * persons; const subtotal = hotelTotal + stayFoodLocal + sightseeing + toFrom; const contingency = 0.1 * subtotal; const total = Math.round(subtotal + contingency); return { nights, rooms, hotelTotal: Math.round(hotelTotal), stayFoodLocal: Math.round(stayFoodLocal), sightseeing: Math.round(sightseeing), toFrom: Math.round(toFrom), contingency: Math.round(contingency), total }; } const modeConfig: Record = { bike: { label: "Bike", Icon: Bike }, car: { label: "Car", Icon: Car }, bus: { label: "Bus", Icon: Bus }, train: { label: "Train", Icon: Train }, flight: { label: "Flight", Icon: PlaneTakeoff } }; export default function Page() { const [destination, setDestination] = useState(""); const today = new Date().toISOString().slice(0, 10); const [startDate, setStartDate] = useState(today); const [endDate, setEndDate] = useState(today); const [travellers, setTravellers] = useState(1); const [budget, setBudget] = useState(""); const [mode, setMode] = useState("flight"); const [generated, setGenerated] = useState(false); const [activeTab, setActiveTab] = useState("Itinerary"); const [booking, setBooking] = useState(null); const { currency, setCurrency, convertINR, refreshRates, refreshing } = useCurrency(); const [sessionEmail, setSessionEmail] = useState(null); useEffect(() => { const { data: sub } = supabase.auth.onAuthStateChange((_e, session) => { setSessionEmail(session?.user?.email ?? null); }); return () => sub.subscription.unsubscribe(); }, []); const hotels = useMemo(() => (destination ? createHotels(destination) : []), [destination]); const places = useMemo(() => (destination ? createPlaces(destination) : []), [destination]); const restaurants = useMemo(() => (destination ? createRestaurants(destination) : []), [destination]); const vehicles = useMemo(() => (destination ? createVehicles(destination, mode) : []), [destination, mode]); const costs = useMemo(() => (destination ? estimateCosts({ destination, startDate, endDate, travellers, mode }) : null), [destination, startDate, endDate, travellers, mode]); const days = useMemo(() => (destination ? enumerateDates(startDate, endDate) : []), [destination, startDate, endDate]); function currencyFmt(inr: number) { return new Intl.NumberFormat(undefined, { style: "currency", currency }).format(convertINR(inr)); } function Badge({ children }: any) { return {children}; } function ModeDropdown() { return (
{React.createElement(modeConfig[mode].Icon, { className: "w-5 h-5" })}
); } async function handleGenerate() { setGenerated(true); setActiveTab("Itinerary"); } async function signIn(email: string) { const { error } = await supabase.auth.signInWithOtp({ email, options: { emailRedirectTo: typeof window !== 'undefined' ? window.location.origin : undefined } }); if (error) alert(error.message); else alert("Magic link sent to " + email); } async function signOut() { await supabase.auth.signOut(); } async function saveTrip() { if (!sessionEmail) { alert("Sign in first"); return; } const { data, error } = await supabase.from("trips").insert({ destination, start_date: startDate, end_date: endDate, travellers, budget: budget ? Number(budget) : null, mode, currency, costs, created_at: new Date().toISOString() }).select(); if (error) alert(error.message); else alert("Saved! id=" + data?.[0]?.id); } const [aiPlan, setAiPlan] = useState(null); useEffect(() => { (async () => { if (!generated || !destination) return; try { const res = await fetch("/api/ai-itinerary", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ destination, days: Math.max(1, days.length), places }) }); if (res.ok) { const j = await res.json(); setAiPlan(j.plan); } } catch { setAiPlan(null); } })(); }, [generated, destination, startDate, endDate]); return (
{/* Header */}

Smart Travel Planner

AI‑powered trip planning made easy

{sessionEmail ? ( ) : ( )}
{/* Form */}

Plan your trip

Clean, professional UI • Subtle 3D design • Fully responsive
setDestination(e.target.value)} />
setStartDate(e.target.value)} />
setEndDate(e.target.value)} />
setTravellers(Math.max(1, Number(e.target.value || 1)))} />
setBudget(e.target.value)} />
{Object.entries(modeConfig).map(([key, { Icon }]) => (
{key}
))}
Generate Itinerary
{!destination && (

Enter a destination to continue

)}
{generated && destination && (
{["Itinerary","Hotels","Places","Vehicle","Cost","Restaurants","Booking"].map((tab) => ())}
{activeTab === "Itinerary" && ( {travellers} travellers} />
{days.map((d, i) => (
Day {i + 1}
{d.toDateString()}
{aiPlan ? ({aiPlan[i]?.morning || "Explore"} • {aiPlan[i]?.evening || "Dinner"}) : (<>Explore {destination} • Suggested: {places[i % places.length]?.name || "City walk"} )}
))}
)} {activeTab === "Hotels" && (
{hotels.map((h) => (
{h.name}
{h.distanceKm} km from center • ⭐{h.rating}
Amenities: {h.amenities.join(", ")}
{currencyFmt(h.pricePerNightINR)} / night
))}
)} {activeTab === "Places" && (
{places.map((p) => (
{p.name}
Best time: {p.bestTime}
Ticket: {currencyFmt(p.ticketINR)}
{p.note}
))}
)} {activeTab === "Vehicle" && (
{vehicles.map((v) => (
{v.name}
Depart {v.depart} • Arrive {v.arrive}
{currencyFmt(v.pricePerPersonINR)} / person
{v.refundable ? "Refundable" : "Non‑refundable"}
))}
)} {activeTab === "Cost" && costs && ( {costs.nights} nights} />
Hotel ({costs.rooms} room{costs.rooms>1?"s":""})
{currencyFmt(costs.hotelTotal)}
Food + Local transport
{currencyFmt(costs.stayFoodLocal)}
Sightseeing
{currencyFmt(costs.sightseeing)}
To/From ({travellers} ppl)
{currencyFmt(costs.toFrom)}
Contingency (10%)
{currencyFmt(costs.contingency)}
Total (all travellers)
{currencyFmt(costs.total)}
{Boolean(budget) && (
Budget: {currencyFmt(Number(budget))} • {Number(budget) >= costs.total ? (Within budget) : (Over budget)}
)}
)} {activeTab === "Restaurants" && (
{restaurants.map((r) => (
{r.name}
⭐{r.rating} • {r.popular}
Avg meal: {currencyFmt(r.avgMealINR)}
))}
)} {activeTab === "Booking" && (
Hotels
Connect to Booking/Expedia/OTA via API.
Trains / Flights / Buses
Integrate Amadeus/Skyscanner/Kiwi/Rail APIs.
Payments
Hook to Razorpay/Stripe/Paytm.
)}
)}
{booking && (
Booking preview
Type: {booking.type}
{JSON.stringify(booking.item, null, 2)}
)}

Demo data shown for worldwide destinations. Currency defaults to INR and can be changed. Replace endpoints/adapters with live providers for real‑time hotels, flights, trains, and payments.

); } function Card({ children, className = "" }: any) { return ({children}); } function SectionTitle({ icon: Icon, title, right }: any) { return (
{Icon && }

{title}

{right}
); } function AuthMini({ onSignIn }: { onSignIn: (email: string) => void }) { const [email, setEmail] = useState(""); return (
setEmail(e.target.value)} placeholder="your@email" className="rounded-xl border bg-white/80 px-2 py-1 text-sm"/>
); } // FILES END ================================================================ // SUPABASE: .env.local (create this file) // NEXT_PUBLIC_SUPABASE_URL=your_project_url // NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_key // SUPABASE: SQL for trips table (run in Supabase SQL editor) // create table public.trips ( // id bigint generated by default as identity primary key, // created_at timestamptz default now(), // user_email text, // destination text, // start_date date, // end_date date, // travellers int, // budget numeric, // mode text, // currency text, // costs jsonb // ); // alter table public.trips enable row level security; // create policy "users can insert their own trips" on public.trips for insert with check (true); // create policy "users can select their trips" on public.trips for select using (true); // NOTES // - Currency endpoint /api/fx currently returns static sample rates. Swap with a real FX provider for live prices. // - AI itinerary uses /api/ai-itinerary stub; plug your model provider if needed. // - Booking/Payment blocks are UI-only; connect to third-party APIs for live availability and checkout.

Comments

Popular posts from this blog

Travel Planner

TP

Travel planner 2