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 (); }
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 (
{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 ? (
) : (
{ 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 */}
Clean, professional UI • Subtle 3D design • Fully responsive
setDestination(e.target.value)} />
Generate Itinerary
{!destination && (
{generated && destination && (
{travellers} travellers} />
)}
{activeTab === "Hotels" && (
{loadingHotels?"Loading…":hotelsLive?"Live":"Fallback"}} />
)}
{activeTab === "Places" && (
)}
{activeTab === "Vehicle" && (
{loadingFlights?"Loading…":flights?"Live":"Fallback"}} />
)}
{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"}> )}
{(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
{places.map((p) => (
))}
{p.name}
Best time: {p.bestTime}
Ticket: {currencyFmt(p.ticketINR)}
{p.note}
{(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)}
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
Connected to Amadeus Hotel Offers.
Flights
Connected to Amadeus Flight Offers.
Payments
Hooks to save confirmed booking in Supabase.
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 ================================================================
// .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
Post a Comment