// 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 (); }
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 (
{children} ); }
function SectionTitle({ icon: Icon, title, right }: any) { return (); }
function AuthMini({ onSignIn }: { onSignIn: (email: string) => void }) { const [email, setEmail] = useState(""); return (
{React.createElement(modeConfig[mode].Icon, { className: "w-5 h-5" })}
{/* Header */}
{sessionEmail ? (
) : (
)}
{/* Form */}
Clean, professional UI • Subtle 3D design • Fully responsive
setDestination(e.target.value)} />
Generate Itinerary
{!destination && (
{generated && destination && (
{travellers} travellers} />
)}
{activeTab === "Hotels" && (
)}
{activeTab === "Places" && (
)}
{activeTab === "Vehicle" && (
)}
{activeTab === "Cost" && costs && (
{costs.nights} nights} />
)}
{activeTab === "Restaurants" && (
)}
{activeTab === "Booking" && (
)}
)}
{booking && (
)}
);
}
function Card({ children, className = "" }: any) { return (Smart Travel Planner
AI‑powered trip planning made easy
Plan your trip
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}))}
Enter a destination to continue
)}{["Itinerary","Hotels","Places","Vehicle","Cost","Restaurants","Booking"].map((tab) => ())}
{activeTab === "Itinerary" && (
{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"}> )}
{hotels.map((h) => (
))}
{h.name}
{h.distanceKm} km from center • ⭐{h.rating}
Amenities: {h.amenities.join(", ")}
{currencyFmt(h.pricePerNightINR)} / night
{places.map((p) => (
))}
{p.name}
Best time: {p.bestTime}
Ticket: {currencyFmt(p.ticketINR)}
{p.note}
{vehicles.map((v) => (
{v.refundable ? "Refundable" : "Non‑refundable"}
))}
{v.name}
Depart {v.depart} • Arrive {v.arrive}
{currencyFmt(v.pricePerPersonINR)} / person
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)}
)}
{restaurants.map((r) => (
))}
{r.name}
⭐{r.rating} • {r.popular}
Avg meal: {currencyFmt(r.avgMealINR)}
Hotels
Connect to Booking/Expedia/OTA via API.
Trains / Flights / Buses
Integrate Amadeus/Skyscanner/Kiwi/Rail APIs.
Payments
Hook to Razorpay/Stripe/Paytm.
Booking preview
Type: {booking.type}
{JSON.stringify(booking.item, null, 2)}
{Icon && }
{right}{title}
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
Post a Comment