Files
holiday-plans/eurocamp-search-v2.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

281 lines
9.4 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() {
// Ensure directories exist
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
fs.mkdirSync(PRICES_DIR, { recursive: true });
console.log('Starting Eurocamp search for La Grande Métairie...');
console.log('Screenshot dir:', SCREENSHOT_DIR);
console.log('Prices dir:', PRICES_DIR);
const browser = await chromium.launch({
headless: true,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-blink-features=AutomationControlled'
]
});
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',
timezoneId: 'Europe/London'
});
// Add headers to appear more like a real browser
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',
url: 'https://www.eurocamp.co.uk/campsites/france/brittany/la-grande-metairie-campsite',
prices: [],
screenshots: [],
status: 'in_progress',
errors: [],
notes: []
};
try {
// Step 1: Go to the campsite page
console.log('\n=== Step 1: Loading campsite page ===');
await page.goto(results.url, {
waitUntil: 'domcontentloaded',
timeout: 90000
});
console.log('Page loaded, waiting for content...');
await page.waitForTimeout(5000);
// Take screenshot
const screenshot1 = path.join(SCREENSHOT_DIR, 'eurocamp-01-initial.png');
await page.screenshot({ path: screenshot1, fullPage: false });
results.screenshots.push(screenshot1);
console.log('✓ Screenshot saved:', screenshot1);
// Log page title
const title = await page.title();
results.notes.push(`Page title: ${title}`);
console.log('Page title:', title);
// Save page HTML for debugging
const html = await page.content();
fs.writeFileSync(path.join(SCREENSHOT_DIR, 'eurocamp-page.html'), html);
console.log('✓ HTML saved');
// Step 2: Try to find and interact with the booking widget
console.log('\n=== Step 2: Looking for booking elements ===');
// Look for various booking elements
const bookingElements = await page.evaluate(() => {
const elements = [];
// Look for buttons
document.querySelectorAll('button, a').forEach(el => {
const text = el.textContent.trim();
if (text.toLowerCase().includes('book') ||
text.toLowerCase().includes('check') ||
text.toLowerCase().includes('search') ||
text.toLowerCase().includes('availability')) {
elements.push({
tag: el.tagName,
text: text.substring(0, 100),
class: el.className,
id: el.id
});
}
});
// Look for date inputs
document.querySelectorAll('input').forEach(el => {
elements.push({
tag: 'INPUT',
type: el.type,
placeholder: el.placeholder,
name: el.name,
id: el.id
});
});
return elements;
});
console.log('Found booking elements:', JSON.stringify(bookingElements.slice(0, 15), null, 2));
results.notes.push(`Found ${bookingElements.length} potential booking elements`);
// Step 3: Try to find prices on the page
console.log('\n=== Step 3: Looking for prices ===');
const priceInfo = await page.evaluate(() => {
const prices = [];
const bodyText = document.body.innerText;
// Look for price patterns
const priceRegex = /£[\d,]+/g;
const matches = bodyText.match(priceRegex);
if (matches) {
matches.slice(0, 20).forEach(m => prices.push(m));
}
// Look for accommodation cards
const cards = document.querySelectorAll('[class*="accommodation"], [class*="card"], [class*="result"], [class*="price"]');
const cardInfo = [];
cards.forEach(card => {
const text = card.textContent.trim();
if (text.length > 50 && text.length < 1000) {
cardInfo.push(text.substring(0, 300));
}
});
return { prices, cards: cardInfo.slice(0, 10) };
});
console.log('Prices found on page:', priceInfo.prices);
results.notes.push(`Prices found: ${priceInfo.prices.join(', ')}`);
if (priceInfo.cards.length > 0) {
console.log('\nAccommodation cards found:');
priceInfo.cards.forEach((card, i) => {
console.log(`Card ${i + 1}:`, card.substring(0, 150) + '...');
});
}
// Step 4: Try to construct a direct booking URL
console.log('\n=== Step 4: Trying direct search URL ===');
// Eurocamp uses various URL patterns for search
const searchUrls = [
`https://www.eurocamp.co.uk/search?campsite=la-grande-metairie&checkIn=2026-07-18&nights=14&adults=2&children=1&childAge1=6`,
`https://www.eurocamp.co.uk/campsites/france/brittany/la-grande-metairie-campsite?checkIn=2026-07-18&nights=14&adults=2&children=1&childAge1=6`,
`https://www.eurocamp.co.uk/booking?campsite=la-grande-metairie&arrival=2026-07-18&departure=2026-08-02&adults=2&children=1&childAges=6`
];
for (const searchUrl of searchUrls) {
try {
console.log('Trying URL:', searchUrl);
await page.goto(searchUrl, {
waitUntil: 'domcontentloaded',
timeout: 60000
});
await page.waitForTimeout(3000);
const urlName = searchUrl.split('?')[0].split('/').pop();
const screenshot = path.join(SCREENSHOT_DIR, `eurocamp-search-${urlName}.png`);
await page.screenshot({ path: screenshot, fullPage: false });
results.screenshots.push(screenshot);
console.log('✓ Screenshot saved for search URL');
// Check for prices on this page
const searchPrices = await page.evaluate(() => {
const prices = [];
const bodyText = document.body.innerText;
const priceRegex = /£[\d,]+/g;
const matches = bodyText.match(priceRegex);
if (matches) {
matches.forEach(m => {
if (!prices.includes(m)) prices.push(m);
});
}
return prices;
});
if (searchPrices.length > 0) {
console.log('Found prices on search page:', searchPrices);
results.prices.push(...searchPrices);
}
} catch (e) {
console.log('Error with URL:', e.message);
results.errors.push(`Search URL error: ${e.message}`);
}
}
// Take final screenshot
const finalScreenshot = path.join(SCREENSHOT_DIR, 'eurocamp-final.png');
await page.screenshot({ path: finalScreenshot, fullPage: true });
results.screenshots.push(finalScreenshot);
// Get final URL
results.url = page.url();
console.log('\nFinal URL:', results.url);
// Step 5: Check if we need to try a different approach
console.log('\n=== Step 5: Trying to find actual booking form ===');
// Look for iframe booking widgets
const frames = page.frames();
console.log(`Found ${frames.length} frames`);
for (const frame of frames) {
if (frame !== page.mainFrame()) {
console.log('Checking frame:', frame.url());
try {
const frameContent = await frame.content();
if (frameContent.includes('price') || frameContent.includes('£')) {
console.log('Found potential price content in frame');
results.notes.push('Found booking iframe with potential price data');
}
} catch (e) {
console.log('Could not access frame:', e.message);
}
}
}
results.status = 'completed';
} catch (error) {
console.error('Error:', error.message);
results.errors.push(error.message);
results.status = 'error';
// Take error screenshot
const errorScreenshot = path.join(SCREENSHOT_DIR, 'eurocamp-error.png');
await page.screenshot({ path: errorScreenshot, fullPage: true });
results.screenshots.push(errorScreenshot);
} finally {
await browser.close();
}
// Remove duplicates from prices
results.prices = [...new Set(results.prices)];
// 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 ===');
console.log('Output file:', outputPath);
return results;
}
searchEurocamp().then(results => {
console.log('\n=== Search Complete ===');
console.log('Status:', results.status);
console.log('Prices found:', results.prices);
console.log('Screenshots:', results.screensshots);
console.log('Errors:', results.errors);
}).catch(err => {
console.error('Fatal error:', err);
process.exit(1);
});