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:
2
lib/providers/index.ts
Normal file
2
lib/providers/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./yowDeals";
|
||||
export * from "./linkBuilders";
|
||||
125
lib/providers/linkBuilders.ts
Normal file
125
lib/providers/linkBuilders.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import type { Deal, SearchCriteria } from "../types";
|
||||
import { enumerateDatePairs } from "../date";
|
||||
|
||||
function toLower(s: string) { return (s||"").toLowerCase(); }
|
||||
|
||||
export async function buildSkyscannerLinks(c: SearchCriteria): Promise<Deal[]> {
|
||||
const origin = toLower(c.origin);
|
||||
const out: Deal[] = [];
|
||||
const pairs = enumerateDatePairs(c.startDate, c.endDate, c.tripLengthMin, c.tripLengthMax, 6);
|
||||
|
||||
for (const dest of c.destinations) {
|
||||
for (const p of pairs) {
|
||||
const path = `https://www.skyscanner.ca/transport/flights/${origin}/${toLower(dest)}/${p.out}/${p.back}/`;
|
||||
out.push({
|
||||
id: `sky-${origin}-${dest}-${p.out}-${p.back}`,
|
||||
title: `${c.origin} → ${dest} (${p.nights} nights)`,
|
||||
source: "Skyscanner Link",
|
||||
link: path,
|
||||
price: null,
|
||||
currency: c.currency || "CAD",
|
||||
startDate: `${p.out.slice(0,4)}-${p.out.slice(4,6)}-${p.out.slice(6,8)}`,
|
||||
endDate: `${p.back.slice(0,4)}-${p.back.slice(4,6)}-${p.back.slice(6,8)}`,
|
||||
nights: p.nights,
|
||||
origin: c.origin,
|
||||
destination: dest,
|
||||
});
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export async function buildGoogleFlightsLinks(c: SearchCriteria): Promise<Deal[]> {
|
||||
const out: Deal[] = [];
|
||||
const pairs = enumerateDatePairs(c.startDate, c.endDate, c.tripLengthMin, c.tripLengthMax, 4);
|
||||
for (const dest of c.destinations) {
|
||||
for (const p of pairs) {
|
||||
const q = encodeURIComponent(`flights from ${c.origin} to ${dest} ${p.out} to ${p.back}`);
|
||||
const link = `https://www.google.com/travel/flights?q=${q}`;
|
||||
out.push({
|
||||
id: `gfl-${c.origin}-${dest}-${p.out}-${p.back}`,
|
||||
title: `Google Flights: ${c.origin} → ${dest} (${p.nights} nights)`,
|
||||
source: "Google Flights Link",
|
||||
link,
|
||||
price: null,
|
||||
currency: c.currency || "CAD",
|
||||
startDate: `${p.out.slice(0,4)}-${p.out.slice(4,6)}-${p.out.slice(6,8)}`,
|
||||
endDate: `${p.back.slice(0,4)}-${p.back.slice(4,6)}-${p.back.slice(6,8)}`,
|
||||
nights: p.nights,
|
||||
origin: c.origin,
|
||||
destination: dest,
|
||||
});
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export async function buildAirCanadaLinks(c: SearchCriteria): Promise<Deal[]> {
|
||||
const out: Deal[] = [];
|
||||
const pairs = enumerateDatePairs(c.startDate, c.endDate, c.tripLengthMin, c.tripLengthMax, 3);
|
||||
for (const dest of c.destinations) {
|
||||
for (const p of pairs) {
|
||||
const sd = `${p.out.slice(0,4)}-${p.out.slice(4,6)}-${p.out.slice(6,8)}`;
|
||||
const ed = `${p.back.slice(0,4)}-${p.back.slice(4,6)}-${p.back.slice(6,8)}`;
|
||||
const params = new URLSearchParams({
|
||||
org1: c.origin.toUpperCase(),
|
||||
dest1: dest.toUpperCase(),
|
||||
departureDate1: sd,
|
||||
returnDate1: ed,
|
||||
tripType: "2",
|
||||
lang: "en-CA"
|
||||
});
|
||||
const link = `https://www.aircanada.com/ca/en/aco/home/book/travel.html?${params.toString()}`;
|
||||
out.push({
|
||||
id: `ac-${c.origin}-${dest}-${p.out}-${p.back}`,
|
||||
title: `Air Canada: ${c.origin} → ${dest} (${p.nights} nights)`,
|
||||
source: "Air Canada Link",
|
||||
link,
|
||||
price: null,
|
||||
currency: c.currency || "CAD",
|
||||
startDate: sd,
|
||||
endDate: ed,
|
||||
nights: p.nights,
|
||||
origin: c.origin,
|
||||
destination: dest,
|
||||
});
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export async function buildAirTransatLinks(c: SearchCriteria): Promise<Deal[]> {
|
||||
const out: Deal[] = [];
|
||||
const pairs = enumerateDatePairs(c.startDate, c.endDate, c.tripLengthMin, c.tripLengthMax, 3);
|
||||
for (const dest of c.destinations) {
|
||||
for (const p of pairs) {
|
||||
const sd = `${p.out.slice(0,4)}-${p.out.slice(4,6)}-${p.out.slice(6,8)}`;
|
||||
const ed = `${p.back.slice(0,4)}-${p.back.slice(4,6)}-${p.back.slice(6,8)}`;
|
||||
// Air Transat vacation packages search
|
||||
const params = new URLSearchParams({
|
||||
depCity: c.origin.toUpperCase(),
|
||||
destCode: dest.toUpperCase(),
|
||||
depDate: sd,
|
||||
retDate: ed,
|
||||
adults: "2",
|
||||
children: "0",
|
||||
infants: "0"
|
||||
});
|
||||
const link = `https://www.airtransat.com/en-CA/flights-hotels?${params.toString()}`;
|
||||
out.push({
|
||||
id: `at-${c.origin}-${dest}-${p.out}-${p.back}`,
|
||||
title: `Air Transat: ${c.origin} → ${dest} (${p.nights} nights)`,
|
||||
source: "Air Transat Link",
|
||||
link,
|
||||
price: null,
|
||||
currency: c.currency || "CAD",
|
||||
startDate: sd,
|
||||
endDate: ed,
|
||||
nights: p.nights,
|
||||
origin: c.origin,
|
||||
destination: dest,
|
||||
});
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
42
lib/providers/yowDeals.ts
Normal file
42
lib/providers/yowDeals.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import * as cheerio from "cheerio";
|
||||
import type { Deal, SearchCriteria } from "../types";
|
||||
|
||||
const SITE_BY_ORIGIN: Record<string, string> = {
|
||||
"YOW": "https://www.yowdeals.com",
|
||||
"YYZ": "https://www.yyzdeals.com",
|
||||
"YUL": "https://www.yuldeals.com",
|
||||
"YVR": "https://www.yvrdeals.com",
|
||||
"YYC": "https://www.yycdeals.com",
|
||||
"YEG": "https://www.yegdeals.com",
|
||||
};
|
||||
|
||||
export async function fetchCityDeals(criteria: SearchCriteria): Promise<Deal[]> {
|
||||
const site = SITE_BY_ORIGIN[(criteria.origin || "").toUpperCase()] || SITE_BY_ORIGIN["YOW"];
|
||||
try {
|
||||
const res = await fetch(site, { cache: "no-store" });
|
||||
const html = await res.text();
|
||||
const $ = cheerio.load(html);
|
||||
const deals: Deal[] = [];
|
||||
|
||||
$("h2.post-title a, .post h2 a, .post-title a").each((_, el) => {
|
||||
const title = $(el).text().trim();
|
||||
const link = $(el).attr("href") || site;
|
||||
const priceMatch = title.match(/\$\s?(\d+[\,\d+]*)/);
|
||||
const price = priceMatch ? parseInt(priceMatch[1].replace(/,/g, "")) : null;
|
||||
const id = `dealsite-${Buffer.from(link).toString("base64").slice(0,16)}`;
|
||||
deals.push({
|
||||
id,
|
||||
title,
|
||||
source: new URL(site).host.replace("www.",""),
|
||||
link,
|
||||
price,
|
||||
currency: "CAD",
|
||||
origin: criteria.origin.toUpperCase(),
|
||||
});
|
||||
});
|
||||
|
||||
return deals.slice(0, 20); // don't flood
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user