mirror of
https://github.com/mblanke/holiday-travel-app.git
synced 2026-03-01 13:30:20 -05:00
Initial commit: Holiday Travel App with resort comparison, trip management, and multi-provider search
This commit is contained in:
82
app/api/search/route.ts
Normal file
82
app/api/search/route.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { z } from "zod";
|
||||
import type { SearchCriteria, Deal } from "@/lib/types";
|
||||
import { fetchCityDeals } from "@/lib/providers/yowDeals";
|
||||
import { buildSkyscannerLinks, buildGoogleFlightsLinks, buildAirCanadaLinks, buildAirTransatLinks } from "@/lib/providers/linkBuilders";
|
||||
import { scoreDeal } from "@/lib/score";
|
||||
|
||||
const schema = z.object({
|
||||
origin: z.string().min(3).max(4),
|
||||
destinations: z.array(z.string().min(3)).nonempty(),
|
||||
startDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
||||
endDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
||||
tripLengthMin: z.number().int().positive(),
|
||||
tripLengthMax: z.number().int().positive(),
|
||||
budget: z.number().int().positive().optional().nullable(),
|
||||
currency: z.string().default("CAD").optional(),
|
||||
nonStopOnly: z.boolean().optional(),
|
||||
sources: z.array(z.string()).optional(),
|
||||
preferences: z.object({
|
||||
beach: z.number().min(0).max(10).optional(),
|
||||
pool: z.number().min(0).max(10).optional(),
|
||||
golf: z.number().min(0).max(10).optional(),
|
||||
spa: z.number().min(0).max(10).optional(),
|
||||
food: z.number().min(0).max(10).optional(),
|
||||
nightlife: z.number().min(0).max(10).optional(),
|
||||
shopping: z.number().min(0).max(10).optional(),
|
||||
culture: z.number().min(0).max(10).optional(),
|
||||
outdoor: z.number().min(0).max(10).optional(),
|
||||
family: z.number().min(0).max(10).optional(),
|
||||
}).optional()
|
||||
});
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const parsed = schema.parse(body) as SearchCriteria;
|
||||
const sources = (parsed.sources && parsed.sources.length ? parsed.sources : ["Deals", "Skyscanner", "GoogleFlights", "AirCanada", "AirTransat"]).map(s => s.toLowerCase());
|
||||
|
||||
const tasks: Promise<Deal[]>[] = [];
|
||||
|
||||
if (sources.includes("deals")) tasks.push(fetchCityDeals(parsed));
|
||||
if (sources.includes("skyscanner")) tasks.push(buildSkyscannerLinks(parsed));
|
||||
if (sources.includes("googleflights")) tasks.push(buildGoogleFlightsLinks(parsed));
|
||||
if (sources.includes("aircanada")) tasks.push(buildAirCanadaLinks(parsed));
|
||||
if (sources.includes("airtransat")) tasks.push(buildAirTransatLinks(parsed));
|
||||
|
||||
// Demo mode fallback
|
||||
const demo = process.env.DEMO === "true";
|
||||
|
||||
let results: Deal[] = [];
|
||||
if (demo) {
|
||||
results = [
|
||||
{ id: "demo1", title: "🔥 Ottawa → Cancun (7 nights)", source: "Demo", link: "#", price: 685, currency: "CAD", startDate: parsed.startDate, endDate: parsed.startDate, nights: 7, origin: parsed.origin, destination: parsed.destinations[0], stops: 0 },
|
||||
{ id: "demo2", title: "Ottawa → Punta Cana (7 nights)", source: "Demo", link: "#", price: 712, currency: "CAD", startDate: parsed.startDate, endDate: parsed.startDate, nights: 7, origin: parsed.origin, destination: parsed.destinations[0], stops: 1 }
|
||||
];
|
||||
} else {
|
||||
const settled = await Promise.allSettled(tasks);
|
||||
for (const s of settled) {
|
||||
if (s.status === "fulfilled") results.push(...s.value);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter and score
|
||||
if (parsed.budget) results = results.filter(r => !r.price || r.price <= (parsed.budget || 0));
|
||||
if (parsed.nonStopOnly) results = results.filter(r => r.stops == null || r.stops === 0);
|
||||
|
||||
results.forEach(r => r.score = scoreDeal(r, parsed.preferences));
|
||||
results.sort((a,b) => (b.score || 0) - (a.score || 0));
|
||||
|
||||
// Optional n8n webhook
|
||||
const webhook = process.env.N8N_WEBHOOK_URL;
|
||||
if (webhook) {
|
||||
try {
|
||||
await fetch(webhook, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ criteria: parsed, top10: results.slice(0,10) }) });
|
||||
} catch(e) { /* ignore */ }
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ results }), { headers: { "content-type": "application/json" } });
|
||||
} catch (e: any) {
|
||||
return new Response(JSON.stringify({ error: e?.message || "Invalid payload" }), { status: 400, headers: { "content-type": "application/json" } });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user