Files
holiday-plans/eurocamp-booking-search.js
Sean C a27fcfef61 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
2026-03-15 23:18:43 +00:00

261 lines
9.2 KiB
JavaScript

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);
});