Plan

// Smart Travel Planner – Next.js 14 (App Router) + Tailwind + Supabase + Amadeus + Razorpay // ----------------------------------------------------------------------------- // Upgrades in this version: // - LIVE data hooks: // • Amadeus Flights (v2/shopping/flight-offers) // • Amadeus Hotels (v3/shopping/hotel-offers) // - Razorpay Checkout for payments (INR) // - Supabase Auth + save trips + save bookings (payments success) // - Graceful fallback to mock data if APIs fail // - Mobile-first, responsive, subtle 3D UI // // 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 razorpay // npm i -D tailwindcss postcss autoprefixer // npx tailwindcss init -p // 3) Replace/add the files exactly as below. // 4) Create .env.local with keys (see bottom) → run: npm run dev → test. // 5) Deploy to Vercel (add env vars there as well). // // FILES START ================================================================ // --- file: package.json (merge scripts if needed) --- { "name": "smart-travel-planner", "version": "1.1.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", "razorpay": "^2.9.3", "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"; export const runtime = "nodejs"; // Live FX placeholder: base INR. Replace with real FX provider. export async function GET() { try { 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"; export const runtime = "nodejs"; 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/amadeus/flights/route.ts --- import { NextResponse } from "next/server"; export const runtime = "nodejs"; async function getAmadeusToken() { const body = new URLSearchParams({ grant_type: "client_credentials", client_id: process.env.AMADEUS_CLIENT_ID || "", client_secret: process.env.AMADEUS_CLIENT_SECRET || "", }); const res = await fetch("https://test.api.amadeus.com/v1/security/oauth2/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body, cache: "no-store", }); if (!res.ok) throw new Error("Failed to fetch Amadeus token"); const j = await res.json(); return j.access_token as string; } export async function POST(req: Request) { try { const { origin, destination, departureDate, returnDate, adults = 1 } = await req.json(); const token = await getAmadeusToken(); const url = new URL("https://test.api.amadeus.com/v2/shopping/flight-offers"); url.searchParams.set("originLocationCode", origin); url.searchParams.set("destinationLocationCode", destination); url.searchParams.set("departureDate", departureDate); if (returnDate) url.searchParams.set("returnDate", returnDate); url.searchParams.set("adults", String(adults)); url.searchParams.set("currencyCode", "INR"); const res2 = await fetch(url.toString(), { headers: { Authorization: `Bearer ${token}` }, cache: "no-store" }); const data = await res2.json(); return NextResponse.json(data); } catch (e: any) { return NextResponse.json({ error: e.message }, { status: 500 }); } } // --- file: app/api/amadeus/hotels/route.ts --- import { NextResponse } from "next/server"; export const runtime = "nodejs"; async function getToken() { const body = new URLSearchParams({ grant_type: "client_credentials", client_id: process.env.AMADEUS_CLIENT_ID || "", client_secret: process.env.AMADEUS_CLIENT_SECRET || "", }); const res = await fetch("https://test.api.amadeus.com/v1/security/oauth2/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body, cache: "no-store", }); if (!res.ok) throw new Error("Failed to fetch Amadeus token"); const j = await res.json(); return j.access_token as string; } export async function POST(req: Request) { try { const { cityCode, checkInDate, checkOutDate, adults = 1 } = await req.json(); const token = await getToken(); const url = new URL("https://test.api.amadeus.com/v3/shopping/hotel-offers"); url.searchParams.set("cityCode", cityCode); if (checkInDate) url.searchParams.set("checkInDate", checkInDate); if (checkOutDate) url.searchParams.set("checkOutDate", checkOutDate); url.searchParams.set("adults", String(adults)); url.searchParams.set("currency", "INR"); const res2 = await fetch(url.toString(), { headers: { Authorization: `Bearer ${token}` }, cache: "no-store" }); const data = await res2.json(); return NextResponse.json(data); } catch (e: any) { return NextResponse.json({ error: e.message }, { status: 500 }); } } // --- file: app/api/razorpay/order/route.ts --- import { NextResponse } from "next/server"; import Razorpay from "razorpay"; export const runtime = "nodejs"; export async function POST(req: Request) { try { const { amountINR, receipt = "rcpt_1" } = await req.json(); if (!process.env.RAZORPAY_KEY_ID || !process.env.RAZORPAY_KEY_SECRET) throw new Error("Missing Razorpay keys"); const instance = new Razorpay({ key_id: process.env.RAZORPAY_KEY_ID, key_secret: process.env.RAZORPAY_KEY_SECRET }); const order = await instance.orders.create({ amount: Math.round(Number(amountINR) * 100), currency: "INR", receipt }); return NextResponse.json(order); } catch (e: any) { return NextResponse.json({ error: e.message }, { status: 500 }); } } // --- 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, Plane as PlaneIcon } 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 [flights, setFlights] = useState(null); const [hotelsLive, setHotelsLive] = useState(null); const [loadingFlights, setLoadingFlights] = useState(false); const [loadingHotels, setLoadingHotels] = useState(false); const hotelsMock = useMemo(() => (destination ? createHotels(destination) : []), [destination]); const places = useMemo(() => (destination ? createPlaces(destination) : []), [destination]); const restaurants = useMemo(() => (destination ? createRestaurants(destination) : []), [destination]); const vehiclesMock = 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"); // kick off live fetches if (destination) { fetchFlights(); fetchHotels(); } } async function fetchFlights() { try { setLoadingFlights(true); // NOTE: origin hard-coded to DEL for demo; swap with a From input in Booking tab const body = { origin: "DEL", destination: guessCityCode(destination), departureDate: startDate, returnDate: endDate, adults: travellers }; const res = await fetch("/api/amadeus/flights", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); const j = await res.json(); const list = (j?.data || []).slice(0, 9).map((f: any, idx: number) => ({ id: f.id || `flt-${idx}`, priceINR: Number(f.price?.total || 0), itineraries: f.itineraries, validatingAirlineCodes: f.validatingAirlineCodes, })); setFlights(list); } catch (e) { setFlights(null); } finally { setLoadingFlights(false); } } function guessCityCode(dest: string) { // very rough mapping; replace with proper lookup const d = dest.trim().toLowerCase(); if (d.includes("delhi")) return "DEL"; if (d.includes("mumbai")) return "BOM"; if (d.includes("bangalore") || d.includes("bengaluru")) return "BLR"; if (d.includes("dubai")) return "DXB"; if (d.includes("singapore")) return "SIN"; if (d.includes("london")) return "LON"; if (d.includes("paris")) return "PAR"; if (d.includes("new york")) return "NYC"; if (d.includes("tokyo")) return "TYO"; if (d.includes("sydney")) return "SYD"; return (dest.slice(0,3).toUpperCase()); } async function fetchHotels() { try { setLoadingHotels(true); const body = { cityCode: guessCityCode(destination), checkInDate: startDate, checkOutDate: endDate, adults: travellers }; const res = await fetch("/api/amadeus/hotels", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); const j = await res.json(); const list = (j?.data || []).slice(0, 9).map((h: any, idx: number) => ({ id: h.hotel?.hotelId || `h-${idx}`, name: h.hotel?.name || `Hotel ${idx+1}`, address: h.hotel?.address?.lines?.join(", ") || "", rating: h.hotel?.rating || "", distanceKm: h.hotel?.distance?.value || null, pricePerNightINR: Number(h.offers?.[0]?.price?.total || 0), amenities: h.offers?.[0]?.amenities || [], })); setHotelsLive(list); } catch (e) { setHotelsLive(null); } finally { setLoadingHotels(false); } } // Razorpay loader useEffect(() => { const id = "rzp-script"; if (document.getElementById(id)) return; const s = document.createElement("script"); s.id = id; s.src = "https://checkout.razorpay.com/v1/checkout.js"; s.async = true; document.body.appendChild(s); }, []); async function handleRazorpayPayment(amountINR: number, meta: any) { try { const res = await fetch("/api/razorpay/order", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ amountINR, receipt: `rcpt_${Date.now()}` }) }); const order = await res.json(); const options: any = { key: process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID, amount: order.amount, currency: order.currency, name: "Smart Travel Planner", description: meta?.description || "Trip Booking Payment", order_id: order.id, handler: async (response: any) => { alert("✅ Payment Success: " + response.razorpay_payment_id); if (sessionEmail) { await supabase.from("bookings").insert({ user_email: sessionEmail, type: meta?.type || "unknown", destination, payload: meta || {}, amount_inr: amountINR, currency: "INR", payment_id: response.razorpay_payment_id, created_at: new Date().toISOString(), }); } }, theme: { color: "#4F46E5" }, }; // @ts-ignore const rzp = new window.Razorpay(options); rzp.open(); } catch (e: any) { alert("Payment init failed: " + e.message); } } 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 ? ( ) : ( { 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); }} /> )}
{/* 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" && ( {loadingHotels?"Loading…":hotelsLive?"Live":"Fallback"}} />
{(hotelsLive || hotelsMock).map((h: any) => (
{h.name}
{h.distanceKm? `${h.distanceKm} km from center • `: ""}{h.rating? `⭐${h.rating}`: ""}
{h.address &&
{h.address}
}
{h.pricePerNightINR? currencyFmt(h.pricePerNightINR): "—"} / night
))}
)} {activeTab === "Places" && (
{places.map((p) => (
{p.name}
Best time: {p.bestTime}
Ticket: {currencyFmt(p.ticketINR)}
{p.note}
))}
)} {activeTab === "Vehicle" && ( {loadingFlights?"Loading…":flights?"Live":"Fallback"}} />
{(flights || vehiclesMock).map((v: any, idx: number) => (
{v.validatingAirlineCodes? v.validatingAirlineCodes.join(", ") : v.name}
{v.itineraries? `${v.itineraries[0]?.segments?.[0]?.departure?.iataCode} → ${v.itineraries[0]?.segments?.slice(-1)[0]?.arrival?.iataCode}` : `${v.depart} → ${v.arrive}`}
{currencyFmt(v.priceINR ?? v.pricePerPersonINR)}
))}
)} {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
Connected to Amadeus Hotel Offers.
Flights
Connected to Amadeus Flight Offers.
Payments
Hooks to save confirmed booking in Supabase.
)}
)}
{booking && (
Booking preview
Type: {booking.type}
{JSON.stringify(booking.item, null, 2)}
)}

Live data via Amadeus (test env). Payments via Razorpay (test keys). Use your credentials for production. Currency defaults to INR and can be changed. Replace endpoints/adapters with live providers for real‑time trains and more granular hotel/flight search.

); } 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 ================================================================ // .env.local (create this file with your keys) // NEXT_PUBLIC_SUPABASE_URL=your_project_url // NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_key // AMADEUS_CLIENT_ID=your_amadeus_client_id // AMADEUS_CLIENT_SECRET=your_amadeus_client_secret // RAZORPAY_KEY_ID=your_razorpay_key_id // RAZORPAY_KEY_SECRET=your_razorpay_key_secret // NEXT_PUBLIC_RAZORPAY_KEY_ID=your_razorpay_key_id // SUPABASE: SQL for tables (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 "trips insert" on public.trips for insert with check (true); // create policy "trips select" on public.trips for select using (true); // // create table public.bookings ( // id bigint generated by default as identity primary key, // created_at timestamptz default now(), // user_email text, // type text, -- 'hotel' | 'flight' | 'trip' | ... // destination text, // payload jsonb, // amount_inr numeric, // currency text, // payment_id text // ); // alter table public.bookings enable row level security; // create policy "bookings insert" on public.bookings for insert with check (true); // create policy "bookings select" on public.bookings for select using (true); // NOTES // - Amadeus endpoints use TEST base URLs; for production switch to live creds. // - flights/route.ts expects IATA codes; a simple guesser is provided, replace with a proper lookup. // - Razorpay checkout runs in test mode with your test keys; store payment_id in bookings. // - If Amadeus fails/keys missing, UI gracefully falls back to mock data.

Comments

Popular posts from this blog

Travel Planner

TP

Travel planner 2