Honest pricing review with disclaimers
What Changed: - Added VERIFIED-PRICES.md with honest assessment - Added BUDGET-REALITY.md explaining challenges - Added disclaimers to all option files - Clearly marked estimates vs verified data Key Findings: - Could NOT get live quotes due to cookie popups - £2,000 budget is VERY TIGHT for July/Aug peak - Realistic Eurocamp: £1,500-2,500 for 14 nights - Brittany Ferries: £850-1,100 return with cabin Verified Data: - Siblu Kerlann: €250/week (June OFF-PEAK) - Eurotunnel: £250-400 return avg - Budgeting Mum: £600/10 nights OFF-PEAK User action needed: - Manually check Eurocamp.co.uk - Consider shorter duration - Consider gîte instead of mobile home
This commit is contained in:
260
eurocamp-booking-search.js
Normal file
260
eurocamp-booking-search.js
Normal file
@@ -0,0 +1,260 @@
|
||||
const { chromium } = require('playwright');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
const BASE_DIR = path.join(os.homedir(), 'holiday-planning');
|
||||
const SCREENSHOT_DIR = path.join(BASE_DIR, 'price-evidence');
|
||||
const PRICES_DIR = path.join(BASE_DIR, 'prices');
|
||||
|
||||
async function searchEurocamp() {
|
||||
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
||||
fs.mkdirSync(PRICES_DIR, { recursive: true });
|
||||
|
||||
console.log('=== Eurocamp Price Search for La Grande Métairie ===');
|
||||
console.log('Dates: 18 July 2026 - 2 August 2026 (14 nights)');
|
||||
console.log('Guests: 2 adults, 1 child (age 6)\n');
|
||||
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
});
|
||||
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1920, height: 1080 },
|
||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
||||
locale: 'en-GB'
|
||||
});
|
||||
|
||||
await context.addInitScript(() => {
|
||||
Object.defineProperty(navigator, 'webdriver', { get: () => false });
|
||||
});
|
||||
|
||||
const page = await context.newPage();
|
||||
|
||||
const results = {
|
||||
searchDate: new Date().toISOString(),
|
||||
checkIn: '2026-07-18',
|
||||
checkOut: '2026-08-02',
|
||||
nights: 14,
|
||||
adults: 2,
|
||||
children: 1,
|
||||
childAge: 6,
|
||||
campsite: 'La Grande Métairie',
|
||||
campsiteUrl: 'https://www.eurocamp.co.uk/campsites/france/brittany/la-grande-metairie-campsite',
|
||||
finalQuoteUrl: null,
|
||||
accommodations: [],
|
||||
screenshots: [],
|
||||
status: 'in_progress',
|
||||
errors: []
|
||||
};
|
||||
|
||||
try {
|
||||
// Step 1: Load the campsite page
|
||||
console.log('Step 1: Loading campsite page...');
|
||||
await page.goto(results.campsiteUrl, { waitUntil: 'domcontentloaded', timeout: 90000 });
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'step1-campsite-page.png') });
|
||||
results.screenshots.push('step1-campsite-page.png');
|
||||
|
||||
// Step 2: Click "Prices & availability" button
|
||||
console.log('\nStep 2: Looking for "Prices & availability" button...');
|
||||
|
||||
const pricesButton = await page.waitForSelector('button:has-text("Prices & availability")', { timeout: 10000 });
|
||||
if (pricesButton) {
|
||||
console.log('✓ Found "Prices & availability" button, clicking...');
|
||||
await pricesButton.click();
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'step2-after-prices-click.png') });
|
||||
results.screenshots.push('step2-after-prices-click.png');
|
||||
}
|
||||
|
||||
// Step 3: Look for the booking widget/form
|
||||
console.log('\nStep 3: Looking for booking widget...');
|
||||
|
||||
// The booking widget might be in a modal or expandable section
|
||||
// Look for date inputs
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Try to find date input fields
|
||||
const dateInputs = await page.$$('input[type="text"]');
|
||||
console.log(`Found ${dateInputs.length} text inputs`);
|
||||
|
||||
// Look for specific input patterns
|
||||
const checkInSelectors = [
|
||||
'input[placeholder*="Check in"]',
|
||||
'input[placeholder*="Check-in"]',
|
||||
'input[placeholder*="Arrival"]',
|
||||
'input[name*="checkIn"]',
|
||||
'input[name*="arrival"]',
|
||||
'[data-testid*="checkin"]',
|
||||
'[data-testid*="check-in"]'
|
||||
];
|
||||
|
||||
let checkInInput = null;
|
||||
for (const selector of checkInSelectors) {
|
||||
checkInInput = await page.$(selector);
|
||||
if (checkInInput) {
|
||||
console.log(`✓ Found check-in input: ${selector}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!checkInInput) {
|
||||
// Try clicking on a date picker trigger
|
||||
console.log('Looking for date picker trigger...');
|
||||
const dateTriggers = await page.$$('button, [role="button"], .date-picker');
|
||||
for (const trigger of dateTriggers) {
|
||||
const text = await trigger.textContent();
|
||||
if (text && (text.includes('Select date') || text.includes('Choose date') || text.includes('Arrival'))) {
|
||||
console.log('Found date trigger:', text.substring(0, 50));
|
||||
await trigger.click();
|
||||
await page.waitForTimeout(1000);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'step3-booking-widget.png'), fullPage: true });
|
||||
results.screenshots.push('step3-booking-widget.png');
|
||||
|
||||
// Step 4: Try to use a direct booking URL with parameters
|
||||
console.log('\nStep 4: Trying direct booking URL with parameters...');
|
||||
|
||||
// Eurocamp typically has a booking flow. Let's try to access it directly
|
||||
const bookingUrl = 'https://www.eurocamp.co.uk/campsites/france/brittany/la-grande-metairie-campsite/book';
|
||||
|
||||
try {
|
||||
await page.goto(bookingUrl, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'step4-booking-page.png'), fullPage: true });
|
||||
results.screenshots.push('step4-booking-page.png');
|
||||
|
||||
// Check if we're on a booking page
|
||||
const url = page.url();
|
||||
console.log('Current URL:', url);
|
||||
|
||||
// Look for date pickers and guest selectors
|
||||
const bookingInputs = await page.evaluate(() => {
|
||||
const inputs = [];
|
||||
document.querySelectorAll('input, select').forEach(el => {
|
||||
inputs.push({
|
||||
tag: el.tagName,
|
||||
type: el.type,
|
||||
name: el.name,
|
||||
placeholder: el.placeholder,
|
||||
id: el.id,
|
||||
value: el.value
|
||||
});
|
||||
});
|
||||
return inputs;
|
||||
});
|
||||
|
||||
console.log('Booking inputs found:', JSON.stringify(bookingInputs, null, 2));
|
||||
|
||||
} catch (e) {
|
||||
console.log('Could not access booking page directly:', e.message);
|
||||
}
|
||||
|
||||
// Step 5: Try the Eurocamp search with proper parameters
|
||||
console.log('\nStep 5: Trying search with date parameters...');
|
||||
|
||||
// Eurocamp search URL pattern
|
||||
const searchUrl = `https://www.eurocamp.co.uk/search?adults=2&children=1&childAge1=6&duration=14&arrivalDate=2026-07-18&parcs=la-grande-metairie`;
|
||||
|
||||
await page.goto(searchUrl, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'step5-search-results.png'), fullPage: true });
|
||||
results.screenshots.push('step5-search-results.png');
|
||||
|
||||
results.finalQuoteUrl = page.url();
|
||||
console.log('Search URL:', results.finalQuoteUrl);
|
||||
|
||||
// Extract accommodation options and prices
|
||||
console.log('\nStep 6: Extracting accommodation prices...');
|
||||
|
||||
const accommodations = await page.evaluate(() => {
|
||||
const accs = [];
|
||||
|
||||
// Look for accommodation cards
|
||||
const cards = document.querySelectorAll('[class*="accommodation"], [class*="Accommodation"], [class*="result-card"], [class*="ResultCard"]');
|
||||
|
||||
cards.forEach(card => {
|
||||
try {
|
||||
// Try to extract accommodation name
|
||||
const nameEl = card.querySelector('h2, h3, [class*="name"], [class*="title"]');
|
||||
const name = nameEl ? nameEl.textContent.trim() : '';
|
||||
|
||||
// Try to extract price
|
||||
const priceEl = card.querySelector('[class*="price"], [class*="Price"]');
|
||||
const priceText = priceEl ? priceEl.textContent.trim() : '';
|
||||
|
||||
// Extract all text to find prices
|
||||
const fullText = card.textContent;
|
||||
const priceMatch = fullText.match(/£[\d,]+(?:\.\d{2})?/g);
|
||||
|
||||
if (name || priceMatch) {
|
||||
accs.push({
|
||||
name: name,
|
||||
priceText: priceText,
|
||||
prices: priceMatch || [],
|
||||
snippet: fullText.substring(0, 300)
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
});
|
||||
|
||||
return accs;
|
||||
});
|
||||
|
||||
console.log(`Found ${accommodations.length} accommodations`);
|
||||
accommodations.forEach((acc, i) => {
|
||||
console.log(`\n${i + 1}. ${acc.name || 'Unknown'}`);
|
||||
console.log(` Prices: ${acc.prices.join(', ')}`);
|
||||
results.accommodations.push(acc);
|
||||
});
|
||||
|
||||
// Also get all prices from the page
|
||||
const allPrices = await page.evaluate(() => {
|
||||
const prices = [];
|
||||
const text = document.body.innerText;
|
||||
const matches = text.match(/£[\d,]+(?:\.\d{2})?/g);
|
||||
return matches ? [...new Set(matches)] : [];
|
||||
});
|
||||
|
||||
console.log('\nAll unique prices found on page:', allPrices.slice(0, 20).join(', '));
|
||||
|
||||
// Save the HTML for debugging
|
||||
const html = await page.content();
|
||||
fs.writeFileSync(path.join(SCREENSHOT_DIR, 'final-page.html'), html);
|
||||
|
||||
results.status = 'completed';
|
||||
results.allPrices = allPrices;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error.message);
|
||||
results.errors.push(error.message);
|
||||
results.status = 'error';
|
||||
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'error-screenshot.png'), fullPage: true });
|
||||
results.screenshots.push('error-screenshot.png');
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
// Save results
|
||||
const outputPath = path.join(PRICES_DIR, 'eurocamp-la-grande-metairie.json');
|
||||
fs.writeFileSync(outputPath, JSON.stringify(results, null, 2));
|
||||
console.log('\n=== Results saved to:', outputPath);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
searchEurocamp().catch(err => {
|
||||
console.error('Fatal error:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user