Family Fun Search
🏢
📍 Your Location
👨‍👩‍👧‍👦 Your Family
Under 2 2–5 6–9 10–12 Teens
📏 Distance & Budget
Distance
Unit
Min $
Max $
🎯 What to Look For
🔎 Search Engines
🔵 Google 🟣 Foursquare* 🟢 HERE 🟤 OSM 🔷 Azure 🎫 Viator
⚙️ Features
🌤️ Weather
🏷️ Coupons
👶 Stroller-friendly
🧠 Sensory-friendly
♿ Wheelchair accessible
🏳️‍🌈 LGBTQ+ friendly
🚻 Has restrooms
⭐ Family reviews
🔔 Deal alerts
0 results
Show
🗺️
Ready to explore!
Set your location, fill in your family details, and click Find Family Adventures.
Ready — set your location and search Family Fun Finder v1.0
`); w.document.close(); setTimeout(() => w.print(), 500); return; } if (type === 'share') { try { await navigator.clipboard.writeText(itinMarkdown); const btn = document.querySelector('.itin-exp-btn[onclick*="share"]'); const orig = btn.innerHTML; btn.innerHTML = '✅ Copied!'; setTimeout(() => btn.innerHTML = orig, 2000); } catch(e) { alert('Copy failed — try selecting and copying the text manually.'); } return; } if (type === 'calendar') { const startDate = document.getElementById('itin-date').value || new Date().toISOString().split('T')[0]; const days = parseInt(document.getElementById('itin-days').value) || 1; const dtStart = startDate.replace(/-/g,''); const endD = new Date(new Date(startDate).getTime() + days * 86400000); const dtEnd = endD.toISOString().split('T')[0].replace(/-/g,''); const ics = [ 'BEGIN:VCALENDAR','VERSION:2.0','PRODID:-//Family Fun Finder//EN', 'BEGIN:VEVENT', `DTSTART;VALUE=DATE:${dtStart}`, `DTEND;VALUE=DATE:${dtEnd}`, 'SUMMARY:Family Trip — Family Fun Finder', `DESCRIPTION:${itinMarkdown.substring(0,400).replace(/\n/g,'\\n').replace(/,/g,'\\,')}`, `UID:fff-${Date.now()}@familyfunfinder.com`, 'END:VEVENT','END:VCALENDAR', ].join('\r\n'); const blob = new Blob([ics], {type:'text/calendar'}); const a = Object.assign(document.createElement('a'), { href: URL.createObjectURL(blob), download: 'family-trip.ics' }); a.click(); URL.revokeObjectURL(a.href); } } /* ═══════════════════════════════════════════════════════════════ WEATHER MODAL — dual forecast (local + searched) ═══════════════════════════════════════════════════════════════ */ let weatherLocalData = null; let weatherSearchData = null; function openWeatherModal() { document.getElementById('weather-overlay').classList.add('open'); weatherSearchData = null; // Load local forecast if (userLat && userLng) { fetchLocalWeather(userLat, userLng, null); } else { const locText = document.getElementById('loc-manual').value; if (locText) fetchLocalWeather(null, null, locText); else renderBothForecasts(); } } function closeWeatherModal() { document.getElementById('weather-overlay').classList.remove('open'); } function handleWeatherOverlayClick(e) { if (e.target === document.getElementById('weather-overlay')) closeWeatherModal(); } function fetchWeatherForLocation() { const input = document.getElementById('weather-loc-input').value.trim(); if (!input) return; fetchSearchWeather(null, null, input); } async function callForecastAPI(lat, lng, locationStr) { const payload = {}; if (lat && lng) { payload.lat = lat; payload.lng = lng; } if (locationStr) payload.location = locationStr; const resp = await fetch('/api/forecast', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload), }); const data = await resp.json(); if (!resp.ok || data.error) throw new Error(data.error || 'Forecast failed'); return data; } async function fetchLocalWeather(lat, lng, locationStr) { const body = document.getElementById('weather-body'); body.innerHTML = `
Loading your forecast…
`; try { weatherLocalData = await callForecastAPI(lat, lng, locationStr); renderBothForecasts(); } catch(err) { weatherLocalData = null; renderBothForecasts(); } } async function fetchSearchWeather(lat, lng, locationStr) { // Show spinner only in the search section const existing = document.getElementById('weather-search-section'); if (existing) { existing.innerHTML = `
Fetching ${locationStr}…
`; } else { renderBothForecasts(); // ensure local is shown, search spinner appended } try { weatherSearchData = await callForecastAPI(lat, lng, locationStr); renderBothForecasts(); } catch(err) { weatherSearchData = { error: err.message }; renderBothForecasts(); } } function buildForecastCards(days) { return days.map((d, i) => { const isToday = i === 0; return `
${isToday ? 'Today' : d.day_short}
${d.month_day}
${d.icon}
${d.description}
${d.high}° ${d.low}°
💧 ${d.humidity}% 💨 ${d.wind_mph} mph ${d.precip_pct > 0 ? `🌧️ ${d.precip_pct}%` : ''}
`; }).join(''); } function buildForecastSection(data, isLocal) { if (!data) return isLocal ? `
📍
No local forecast — enter your location above.
` : ''; if (data.error) return `
⚠️ ${data.error}
`; const days = data.forecast || []; if (!days.length) return '
No data available.
'; const today = days[0]; return `
${isLocal ? '📍' : '🔍'} ${data.location}
Feels like ${today.feels_like}°F · ${today.description}
${buildForecastCards(days)}
`; } function renderBothForecasts() { const body = document.getElementById('weather-body'); const localHtml = buildForecastSection(weatherLocalData, true); let searchHtml = ''; if (weatherSearchData) { searchHtml = `
${buildForecastSection(weatherSearchData, false)}
`; } else { searchHtml = `
`; } body.innerHTML = localHtml + searchHtml + `
Powered by OpenWeatherMap
`; } // Keep old name for compatibility function renderWeatherForecast(data) { renderBothForecasts(); }
🗓️ Trip Itinerary Builder
Select venues to include
Search for places or save favorites first, then open this panel.
AI-Powered Trip Planning
Select venues above, set your dates and preferences,
then click Build My Itinerary to get a
day-by-day plan built just for your family.