const { useState, useEffect } = React; const DEFAULTS = window.CITY_DEFAULTS; const LABELS = { en: { planTrip: "Plan Your Trip", destinations: "Destinations", addCity: "Add another city", remove: "Remove", requestProposal: "Request Proposal", reviewProposal: "Review & Submit Proposal", summaryPanel: "Budget Summary", budgetBox: "Total Trip Budget", perDay: "Per Day", breakdown: "Breakdown by Category", accommodation: "Accommodation", transportation: "Transportation", food: "Food & Dining", extras: "Gifts & Extras", city: "City", days: "Days", dailyCost: "Daily Cost", cityTotal: "City Total", detailsPerCity: "Details per City", totalBudget: "Total Budget", perDayAverage: "Per Day Average", tripOverview: "Trip Overview", tripDuration: "Trip Duration", citiesVisited: "Cities Visited", downloadHTML: "Download HTML Report", downloadPDF: "Download PDF Report", shareEmail: "Share via Email", shareWhatsApp: "Share on WhatsApp", shareTwitter: "Share on Twitter", shareFacebook: "Share on Facebook", confirmSend: "Confirm & Send Email", cancel: "Cancel", email: "Email Address", phone: "Phone Number", fullName: "Full Name", birthDate: "Birth Date", passportNumber: "Passport Number", wiseaccount: "WISE account IBAN EUR", personalID: "Personal ID / National ID", issueDate: "Issue Date", expiryDate: "Expiry Date", issuingAuthority: "Issuing Authority", mealsOptions: "My meal options for this period are:", accommodationType: "Accommodation Type", stars: "Stars", spendAmount: "Amount of Money", attractions: "Paid Attractions", safetyBuffer: "Consider adding", safetyBufferRest: "as a safety buffer for unexpected expenses.", withBuffer: "Total with Buffer" }, pt: { planTrip: "Planeje Sua Viagem", destinations: "Destinos", addCity: "Adicionar outra cidade", remove: "Remover", requestProposal: "Solicitar Proposta", reviewProposal: "Revisar e Enviar Proposta", summaryPanel: "Resumo do Orçamento", budgetBox: "Orçamento Total da Viagem", perDay: "Por Dia", breakdown: "Distribuição por Categoria", accommodation: "Acomodação", transportation: "Transporte", food: "Alimentação", extras: "Presentes & Extras", city: "Cidade", days: "Dias", dailyCost: "Custo Diário", cityTotal: "Total da Cidade", detailsPerCity: "Detalhes por Cidade", totalBudget: "Orçamento Total", perDayAverage: "Média por Dia", tripOverview: "Visão Geral da Viagem", tripDuration: "Duração da Viagem", citiesVisited: "Cidades Visitadas", downloadHTML: "Baixar Relatório HTML", downloadPDF: "Baixar Relatório PDF", shareEmail: "Compartilhar por Email", shareWhatsApp: "Compartilhar no WhatsApp", shareTwitter: "Compartilhar no Twitter", shareFacebook: "Compartilhar no Facebook", confirmSend: "Confirmar e Enviar E-mail", cancel: "Cancelar", email: "E-mail", phone: "Telefone", fullName: "Nome Completo", birthDate: "Data de Nascimento", passportNumber: "Número do Passaporte", wiseaccount: "IBAN da conta WISE em EURO", personalID: "Identidade / CPF", issueDate: "Data de Emissão", expiryDate: "Data de Validade", issuingAuthority: "Órgão Emissor", mealsOptions: "Minhas opções de refeição para esse período:", accommodationType: "Tipo de Acomodação", stars: "Estrelas", spendAmount: "Valor de Gastos", attractions: "Atrações Pagas", safetyBuffer: "Considere adicionar", safetyBufferRest: "como reserva para imprevistos.", withBuffer: "Total com Reserva" } }; const transportOptionsByLang = { en: [ { label: "Flight ✈️", value: "Flight", icon: "✈️" }, { label: "Train 🚆", value: "Train", icon: "🚆" }, { label: "Bus 🚌", value: "Bus", icon: "🚌" }, { label: "Car 🚗", value: "Car", icon: "🚗" }, { label: "Boat ⛴️", value: "Boat", icon: "⛴️" } ], pt: [ { label: "Voo ✈️", value: "Flight", icon: "✈️" }, { label: "Trem 🚆", value: "Train", icon: "🚆" }, { label: "Ônibus 🚌", value: "Bus", icon: "🚌" }, { label: "Carro 🚗", value: "Car", icon: "🚗" }, { label: "Barco ⛴️", value: "Boat", icon: "⛴️" } ] }; const transportPriceOptionsByLang = { en: [ { label: "very cheap", value: "very cheap" }, { label: "cheap", value: "cheap" }, { label: "average", value: "average" }, { label: "expensive", value: "expensive" }, { label: "very expensive", value: "very expensive" } ], pt: [ { label: "muito barato", value: "very cheap" }, { label: "barato", value: "cheap" }, { label: "médio", value: "average" }, { label: "caro", value: "expensive" }, { label: "muito caro", value: "very expensive" } ] }; const foodOptionsByLang = { en: ["Breakfast", "Snack1", "Lunch", "Snack2", "Dinner", "Snack3"], pt: ["Café da manhã", "Lanche1", "Almoço", "Lanche2", "Jantar", "Lanche3"] }; const starsOptionsByLang = { en: [ { label: "1 star", value: "1" }, { label: "2 stars", value: "2" }, { label: "3 stars", value: "3" }, { label: "4 stars", value: "4" }, { label: "5 stars", value: "5" } ], pt: [ { label: "1 estrela", value: "1" }, { label: "2 estrelas", value: "2" }, { label: "3 estrelas", value: "3" }, { label: "4 estrelas", value: "4" }, { label: "5 estrelas", value: "5" } ] }; const accomOptionsByLang = { en: ["Hotel", "Hostel", "Airbnb", "Own lodge", "Others"], pt: ["Hotel", "Hostel", "Airbnb", "Próprio", "Outros"] }; const spendOptionsByLang = { en: [ { label: "No spend", value: "No spend" }, { label: "very small", value: "Very Small" }, { label: "small", value: "Small" }, { label: "average", value: "Average" }, { label: "high", value: "High" }, { label: "very high", value: "Very High" } ], pt: [ { label: "Nada", value: "No spend" }, { label: "muito pouco", value: "Very Small" }, { label: "pouco", value: "Small" }, { label: "mais ou menos", value: "Average" }, { label: "muito", value: "High" }, { label: "muitíssimo", value: "Very High" } ] }; const attractionsOptionsByLang = { en: [ { label: "0", value: "0" }, { label: "1", value: "1" }, { label: "2", value: "2" }, { label: "3", value: "3" }, { label: "4", value: "4" }, { label: "5", value: "5" }, { label: "6", value: "6" }, { label: "7", value: "7" }, { label: "8", value: "8" }, { label: "9", value: "9" }, { label: "10", value: "10" }, { label: ">10", value: ">10" } ], pt: [ { label: "0", value: "0" }, { label: "1", value: "1" }, { label: "2", value: "2" }, { label: "3", value: "3" }, { label: "4", value: "4" }, { label: "5", value: "5" }, { label: "6", value: "6" }, { label: "7", value: "7" }, { label: "8", value: "8" }, { label: "9", value: "9" }, { label: "10", value: "10" }, { label: ">10", value: ">10" } ] }; function lookupMetric(cityRaw, metric) { if (!cityRaw) return DEFAULTS && DEFAULTS[metric] ? DEFAULTS[metric] : {}; const canon = v => v ? v.normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim().toLowerCase() : ""; const inputCanon = canon(cityRaw); // First, try to find it as a city const cityKey = Object.keys(window.CITY_MAPPING || {}).find(c => canon(c) === inputCanon); let country, level; if (!cityKey) { // If not found as a city, check if it's a country name const countryKey = Object.keys(window.COUNTRY_TIERS || {}).find(c => canon(c) === inputCanon); if (countryKey) { // User entered a country name country = countryKey; level = "medium"; // Default level for countries } else { // Not found as city or country, use defaults return DEFAULTS && DEFAULTS[metric] ? DEFAULTS[metric] : {}; } } else { // Found as a city const cityData = window.CITY_MAPPING[cityKey]; country = cityData.country; level = cityData.level; } const tier = window.COUNTRY_TIERS[country]; if (!tier || !window.BUDGET_TIERS[tier]) { return DEFAULTS && DEFAULTS[metric] ? DEFAULTS[metric] : {}; } const basePrices = window.BUDGET_TIERS[tier][metric]; if (!basePrices) { return DEFAULTS && DEFAULTS[metric] ? DEFAULTS[metric] : {}; } const multiplier = window.COST_MULTIPLIERS[level] || 1.0; const adjustedPrices = {}; if (metric === "MEAL_PRICES" || metric === "SPEND_LEVELS" || metric === "ATTRACTION_PRICES") { Object.keys(basePrices).forEach(key => { adjustedPrices[key] = Math.round(basePrices[key] * multiplier); }); } else if (metric === "HOTEL_PRICES") { Object.keys(basePrices).forEach(accomType => { adjustedPrices[accomType] = {}; Object.keys(basePrices[accomType]).forEach(stars => { adjustedPrices[accomType][stars] = Math.round(basePrices[accomType][stars] * multiplier); }); }); } return adjustedPrices; } function diffDays(start, end) { if (!start || !end) return 1; const a = new Date(start), b = new Date(end); const ms = (b - a); return Math.max(1, Math.round(ms/(1000*60*60*24))); } async function calculateCityTotalsByCategory(city, idx, cities) { const meals = lookupMetric(city.city, "MEAL_PRICES"); const accoms = lookupMetric(city.city, "HOTEL_PRICES"); const spends = lookupMetric(city.city, "SPEND_LEVELS"); const atts = lookupMetric(city.city, "ATTRACTION_PRICES"); const accom = city.accomType && city.stars && accoms[city.accomType] ? (accoms[city.accomType][city.stars] || 0) : 0; let transport = 0; if (idx > 0 && city.transport && cities[idx-1].city && city.city) { const prevCityName = cities[idx-1].city; const currentCityName = city.city; let prevCoords = window.GEOCODE_CACHE[prevCityName]; let currentCoords = window.GEOCODE_CACHE[currentCityName]; if (!prevCoords && window.geocodeCity) { prevCoords = await window.geocodeCity(prevCityName); if (prevCoords) window.GEOCODE_CACHE[prevCityName] = prevCoords; } if (!currentCoords && window.geocodeCity) { currentCoords = await window.geocodeCity(currentCityName); if (currentCoords) window.GEOCODE_CACHE[currentCityName] = currentCoords; } if (prevCoords && currentCoords) { const distance = window.calculateDistance( prevCoords.lat, prevCoords.lon, currentCoords.lat, currentCoords.lon ); const cityData = window.CITY_MAPPING[city.city]; const tier = cityData && cityData.country ? (window.COUNTRY_TIERS[cityData.country] || "Medium") : "Medium"; const rates = window.TRANSPORT_RATES[tier] || window.TRANSPORT_RATES["Medium"]; const rate = rates ? rates[city.transport] : undefined; if (rate) { transport = rate.base + (distance * rate.perKm); if (city.transportPrice && window.TRANSPORT_COST_MULTIPLIERS[city.transportPrice]) { transport *= window.TRANSPORT_COST_MULTIPLIERS[city.transportPrice]; } } } } let food = 0; Object.keys(meals).forEach(meal => { if (city.foodChecks && city.foodChecks[meal]) food += meals[meal] || 0; }); const extras = (city.spend ? (spends[city.spend] || 0) : 0) + (city.attractions ? (atts[city.attractions] || 0) : 0); return { accommodation: accom, transport, food, extras }; } async function sumTotalsByCategory(cities) { const results = await Promise.all( cities.map((city, idx) => calculateCityTotalsByCategory(city, idx, cities)) ); return results.reduce((agg, curr) => ({ accommodation: agg.accommodation + curr.accommodation, transport: agg.transport + curr.transport, food: agg.food + curr.food, extras: agg.extras + curr.extras }), {accommodation:0, transport:0, food:0, extras:0}); } async function calculateCityTotal(city, idx, cities) { const cat = await calculateCityTotalsByCategory(city, idx, cities); return cat.accommodation + cat.transport + cat.food + cat.extras; } function generateReport(cities, cityTotals, tripTotal, tripDays, startDate, endDate, totalsCat) { const reportHTML = `

Trip Budget Report

${startDate ? new Date(startDate).toLocaleDateString() : "--"} to ${endDate ? new Date(endDate).toLocaleDateString() : "--"}

Trip Overview
${tripTotal.toFixed(2)}€ Total Budget
${(tripTotal/tripDays).toFixed(2)}€/day Per Day Average
${tripDays} days Trip Duration
${cities.length} Cities Visited
Budget Breakdown by Category
🏨 Accommodation ${totalsCat.accommodation.toFixed(2)}€
💰 transportation ${totalsCat.transport.toFixed(2)}€
🍽️ Food & Dining ${totalsCat.food.toFixed(2)}€
🎁 Gifts & Extras ${totalsCat.extras.toFixed(2)}€
Cities & Daily Breakdown
${cities.map((city, idx) => { const days = city.startDate && city.endDate ? Math.max(1, Math.round((new Date(city.endDate) - new Date(city.startDate))/(1000*60*60*24))) : 1; const total = cityTotals[idx] || 0; const daily = days ? (total/days).toFixed(2) : total.toFixed(2); return ` `; }).join('')}
City Days Daily Cost City Total
${city.city} ${days} ${daily}€ ${total.toFixed(2)}€
Details per City
${cities.map((city, idx) => `

${city.city}

Dates: ${city.startDate} to ${city.endDate} Accommodation: ${city.stars}★ ${city.accomType}
Transport: ${city.transport} (${city.transportPrice}) Spending: ${city.spend}
`).join('')}
Total Trip Budget: €${tripTotal.toFixed(2)}
`; return reportHTML; } function transportIcon(transportType) { const icons = { "Flight": "✈️", "Train": "🚆", "Bus": "🚌", "Car": "🚗", "Boat": "⛴️" }; return icons[transportType] || "➡️"; } function TripPlannerApp() { const [cities, setCities] = useState([ { city: "", startDate: "", endDate: "", transport: "", transportPrice: "", foodChecks: {}, stars: "", accomType: "", spend: "", attractions: "" } ]); const [language, setLanguage] = useState('en'); const transportOptions = transportOptionsByLang[language]; const transportPriceOptions = transportPriceOptionsByLang[language]; const foodOptions = foodOptionsByLang[language]; const starsOptions = starsOptionsByLang[language]; const accomOptions = accomOptionsByLang[language]; const spendOptions = spendOptionsByLang[language]; const attractionsOptions = attractionsOptionsByLang[language]; const [submitted, setSubmitted] = useState(false); const [filteredCities, setFilteredCities] = useState([]); const [cityTotals, setCityTotals] = useState([]); const [totalsCat, setTotalsCat] = useState({accommodation:0, transport:0, food:0, extras:0}); const [showProposalForm, setShowProposalForm] = useState(false); const [proposalData, setProposalData] = useState({ email: '', phone: '', fullName: '', passportNumber: '', issueDate: '', expiryDate: '', issuingAuthority: '', personalID: '', birthDate: '', wiseaccount: '', }); const [showReviewModal, setShowReviewModal] = useState(false); const [reportHTML, setReportHTML] = useState(''); const [currency, setCurrency] = useState('EUR'); // Add this line // Add these new state variables in TripPlannerApp component, after existing useState declarations: const [showBudgetBubble, setShowBudgetBubble] = useState(false); const [bubblePosition, setBubblePosition] = useState({ x: 50, y: 100 }); // Add this useEffect to detect when total budget card is visible (runs after calculations) useEffect(() => { const checkBudgetCard = () => { if (tripTotal > 0) { const budgetBox = document.querySelector('.budget-box'); if (budgetBox) { const rect = budgetBox.getBoundingClientRect(); const isVisible = rect.top < window.innerHeight && rect.bottom > 0; setShowBudgetBubble(!isVisible); } else { setShowBudgetBubble(true); } } }; checkBudgetCard(); window.addEventListener('scroll', checkBudgetCard); window.addEventListener('resize', checkBudgetCard); return () => { window.removeEventListener('scroll', checkBudgetCard); window.removeEventListener('resize', checkBudgetCard); }; }, [tripTotal, cities]); const allCities = Object.keys(window.CITY_MAPPING || {}).sort(); useEffect(() => { async function calc() { const totals = await Promise.all( cities.map((city, idx) => calculateCityTotal(city, idx, cities)) ); setCityTotals(totals); const catTotals = await sumTotalsByCategory(cities); setTotalsCat(catTotals); } calc(); }, [cities]); const handleCityChange = (idx, field, value) => { const updated = [...cities]; updated[idx][field] = value; setCities(updated); setSubmitted(false); if (field === "city" && value && value.length >= 1) { const filtered = allCities.filter(city => city.toLowerCase().startsWith(value.toLowerCase()) ); setFilteredCities(filtered); } else { setFilteredCities([]); } }; const handleFoodCheck = (idx, meal) => { const updated = [...cities]; // Map PT display names to English storage keys const englishKey = meal === "Café da manhã" ? "Breakfast" : meal === "Lanche1" ? "Snack1" : meal === "Almoço" ? "Lunch" : meal === "Lanche2" ? "Snack2" : meal === "Jantar" ? "Dinner" : meal === "Lanche3" ? "Snack3" : meal; updated[idx].foodChecks = {...(updated[idx].foodChecks||{}), [englishKey]: !updated[idx]?.foodChecks?.[englishKey]}; setCities(updated); setSubmitted(false); }; // Add this function near the top with other helper functions function formatCurrency(amount, currency) { const rate = currency === 'BRL' ? 6.2 : 1; // EUR to BRL ≈ 6.2 (update as needed) const converted = amount * rate; const symbol = currency === 'BRL' ? 'R$ ' : '€'; return symbol + converted.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } const addCity = () => { setCities([...cities, { city: "", startDate: "", endDate: "", transport: "", transportPrice: "", foodChecks: {}, stars: "", accomType: "", spend: "", attractions: "" }]); setSubmitted(false); }; const removeCity = (idx) => { if (cities.length === 1) return; setCities(cities.filter((_, i) => i !== idx)); setSubmitted(false); }; const calculate = e => { e.preventDefault(); setSubmitted(true); }; const resetAll = () => { setCities([{ city: "", startDate: "", endDate: "", transport: "", transportPrice: "", foodChecks: {}, stars: "", accomType: "", spend: "", attractions: "" }]); setSubmitted(false); setFilteredCities([]); }; const getCityDays = (city) => city.startDate && city.endDate ? diffDays(city.startDate, city.endDate) : 1; const startDate = cities[0]?.startDate || ""; const endDate = cities.length > 0 ? cities[cities.length-1].endDate : ""; const tripDays = (startDate && endDate) ? diffDays(startDate, endDate) : 1; const cityDays = cities.map(getCityDays); const cityPerDay = cityTotals.map((total, i) => cityDays[i] ? (total/cityDays[i]) : total); const tripTotal = cityTotals.reduce((a,b) => a+b, 0); const breakdown = [ { key:"accommodation", label:"Accommodation", icon:"🏨", className:"fill-accomod", value: totalsCat.accommodation }, { key:"transportation", label:"Transportation", icon:"💰", className:"fill-transport", value: totalsCat.transport }, { key:"food", label:"Food & Dining", icon:"🍽️", className:"fill-food", value: totalsCat.food }, { key:"extras", label:"Gifts & Extras", icon:"🎁", className:"fill-extras", value: totalsCat.extras } ]; const breakdownTotal = breakdown.reduce((a,b)=>a+b.value,0); const buffer = tripTotal * 0.15; const citiesToShow = filteredCities.length > 0 ? filteredCities : []; const handleProposalSubmit = (e) => { e.preventDefault(); // Generate the HTML report const html = generateReport(cities, cityTotals, tripTotal, tripDays, startDate, endDate, totalsCat); setReportHTML(html); setShowReviewModal(true); setShowProposalForm(false); }; const confirmAndSendEmail = () => { // Create hidden form for email submission const form = document.createElement('form'); form.method = 'POST'; form.action = 'https://formsubmit.co/contact@tunvolo.com'; form.style.display = 'none'; // Create hidden input for redirect URL const redirectInput = document.createElement('input'); redirectInput.type = 'hidden'; redirectInput.name = '_next'; // This special field tells formsubmit.co the redirect URL // Replace with your desired URL - e.g., your own website's thank-you page redirectInput.value = 'https://tunvolo.com/tool/'; // Append it to the form form.appendChild(redirectInput); // Add all proposal data Object.keys(proposalData).forEach(key => { const input = document.createElement('input'); input.type = 'hidden'; input.name = key; input.value = proposalData[key]; form.appendChild(input); }); // Create detailed trip summary with all budget info const tripSummary = ` === TRIP BUDGET PROPOSAL === TRIP OVERVIEW: - Dates: ${startDate ? new Date(startDate).toLocaleDateString() : '--'} to ${endDate ? new Date(endDate).toLocaleDateString() : '--'} - Duration: ${tripDays} days - Cities: ${cities.map(c => c.city).join(' → ')} - Total Budget: €${tripTotal.toFixed(2)} - Per Day Average: €${(tripTotal/tripDays).toFixed(2)} BUDGET BREAKDOWN BY CATEGORY: - 🏨 Accommodation: €${totalsCat.accommodation.toFixed(2)} - 💰 Transportation: €${totalsCat.transport.toFixed(2)} - 🍽️ Food & Dining: €${totalsCat.food.toFixed(2)} - 🎁 Gifts & Extras: €${totalsCat.extras.toFixed(2)} CITIES & DAILY BREAKDOWN: ${cities.map((city, idx) => { const days = city.startDate && city.endDate ? Math.max(1, Math.round((new Date(city.endDate) - new Date(city.startDate))/(1000*60*60*24))) : 1; const total = cityTotals[idx] || 0; const daily = days ? (total/days).toFixed(2) : total.toFixed(2); return `- ${city.city}: ${days} days | €${daily}/day | Total: €${total.toFixed(2)}`; }).join('\n')} DETAILS PER CITY: ${cities.map((city, idx) => ` ${city.city}: Dates: ${city.startDate} to ${city.endDate} Accommodation: ${city.stars}★ ${city.accomType} Transport: ${city.transport} (${city.transportPrice}) Meals: ${foodOptions.filter(f => city.foodChecks && city.foodChecks[f]).join(', ')} Spending Level: ${city.spend} Attractions: ${city.attractions} `).join('\n')} SAFETY BUFFER RECOMMENDATION: - Suggested Buffer: €${buffer.toFixed(2)} (15%) - Total with Buffer: €${(tripTotal + buffer).toFixed(2)} `; // Add trip summary const tripInput = document.createElement('input'); tripInput.type = 'hidden'; tripInput.name = 'trip_summary'; tripInput.value = tripSummary; form.appendChild(tripInput); // Add subject const subjectInput = document.createElement('input'); subjectInput.type = 'hidden'; subjectInput.name = '_subject'; subjectInput.value = `New Travel Proposal Request from ${proposalData.fullName}`; form.appendChild(subjectInput); // Add captcha disable const captchaInput = document.createElement('input'); captchaInput.type = 'hidden'; captchaInput.name = '_captcha'; captchaInput.value = 'false'; form.appendChild(captchaInput); document.body.appendChild(form); form.submit(); setShowReviewModal(false); alert('Thank you! Your proposal has been sent successfully! We will contact you back as soon as possible.'); }; // Add this new draggable bubble component right before the return statement: const BudgetBubble = () => { if (!showBudgetBubble || tripTotal === 0) return null; const handleMouseDown = (e) => { e.preventDefault(); e.stopPropagation(); const handleMouseMove = (moveEvent) => { const deltaX = moveEvent.clientX - startX; const deltaY = moveEvent.clientY - startY; setBubblePosition({ x: Math.max(10, Math.min(startBubbleX + deltaX, window.innerWidth - 120)), y: Math.max(10, Math.min(startBubbleY + deltaY, window.innerHeight - 100)) }); }; const handleMouseUp = () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); }; const handleTouchStart = (e) => { e.preventDefault(); e.stopPropagation(); const handleTouchMove = (moveEvent) => { const touch = moveEvent.touches[0]; const deltaX = touch.clientX - startX; const deltaY = touch.clientY - startY; setBubblePosition({ x: Math.max(10, Math.min(startBubbleX + deltaX, window.innerWidth - 120)), y: Math.max(10, Math.min(startBubbleY + deltaY, window.innerHeight - 100)) }); }; const handleTouchEnd = () => { document.removeEventListener('touchmove', handleTouchMove); document.removeEventListener('touchend', handleTouchEnd); }; document.addEventListener('touchmove', handleTouchMove, { passive: false }); document.addEventListener('touchend', handleTouchEnd); }; const currencySymbol = currency === 'BRL' ? 'R$' : '€'; return (
{formatCurrency(tripTotal, currency)}
{formatCurrency(tripTotal/tripDays, currency)}/day
); }; return ( <>
{/* Language Selector */}
{/* Currency Selector */}
💲 {LABELS[language].planTrip}
{LABELS[language].destinations}
{cities.map((city, idx) => (
{idx === 0 ? (
{language === "en" ? "I will stay in" : "Ficarei em"}  handleCityChange(idx, "city", e.target.value)} required list="city-suggestions" /> , {language === "en" ? "from" : "de"} handleCityChange(idx, "startDate", e.target.value)} required /> {language === "en" ? "till" : "até"} handleCityChange(idx, "endDate", e.target.value)} required />.  {LABELS[language].mealsOptions}
) : (
{language === "en" ? "I am going to" : "Eu vou para"}  handleCityChange(idx, "city", e.target.value)} required style={{minWidth:85}} list="city-suggestions" />  {language === "en" ? "by" : "de"} {language === "en" ? "and I will stay from" : "e ficarei de"} handleCityChange(idx, "startDate", e.target.value)} required /> {language === "en" ? "till" : "até"} handleCityChange(idx, "endDate", e.target.value)} required />.  {LABELS[language].mealsOptions}
)}
{foodOptions.map(meal => ( ))}
{language === "en" ? "and I will stay in a" : "e vou me hospedar em"} .
{language === "en" ? "I pretend to spend a" : "Pretendo gastar um"} {language === "en" ? "amount of money during this period with gifts or any extras." : "valor durante esse período em presentes ou extras." }
{language === "en" ? "Also, I am open to pay for at least" : "Também estou aberto a pagar por pelo menos"} {language === 'en' ? "paid attractions." : "atrações pagas."}
{LABELS[language].days} in {LABELS[language].city.toLowerCase()}: {getCityDays(city)}
{getCityDays(city) > 0 && cityTotals[idx] ? `${LABELS[language].perDay}: ${formatCurrency(cityTotals[idx]/getCityDays(city), currency)}` : "" }
{LABELS[language].cityTotal}: {formatCurrency(cityTotals[idx] || 0, currency)}
))} {citiesToShow.map(cityName => (
{LABELS[language].summaryPanel}
📅 {startDate ? new Date(startDate).toLocaleDateString() : "--"} - {endDate ? new Date(endDate).toLocaleDateString() : "--"} {tripDays > 0 && ( {tripDays} {LABELS[language].days} )}
📍 {cities.map((c, idx) => idx > 0 ? <> {transportIcon(c.transport) || "➔"} {c.city} : c.city )}
🍽️ {cities.map((c, idx) => (idx > 0 ? " ➔ " : "") + foodOptions.filter(f => c.foodChecks && c.foodChecks[f]).join(", ") )}
🏨 {cities.map((c, idx) => (idx > 0 ? " ➔ " : "") + (c.stars ? c.stars + "★ " : "") + (c.accomType || "") )}
🧾 {cities.map((c, idx) => (idx > 0 ? " ➔ " : "") + (c.spend || "") )}
🎟️ {cities.map((c, idx) => (idx > 0 ? " ➔ " : "") + (c.attractions || ""))}
🛫 {cities.filter((c, idx) => idx>0).map((c, idx) => (idx > 0 ? " ➔ " : "") + (transportIcon(c.transport) || "") )}
{LABELS[language].budgetBox}
{formatCurrency(tripTotal, currency)}
{tripDays > 0 ? `${formatCurrency(tripTotal/tripDays, currency)} ${LABELS[language].perDay}` : "--"}
{currency === 'BRL' ? 'R$' : '€'}
{LABELS[language].breakdown}
{breakdown.map(cat =>
{cat.icon} {LABELS[language][cat.key]}
{formatCurrency(cat.value, currency)} {breakdownTotal ? (cat.value/breakdownTotal*100).toFixed(1)+"%" : "--"}
)}
Share it:
🌟 Consider adding €{buffer.toFixed(2)} (15%) as a safety buffer for unexpected expenses.
{showProposalForm && (
📋 {LABELS[language].requestProposal}

Passport Information

)} {showReviewModal && (
{LABELS[language].reviewProposal}

{LABELS[language].fullName} / {LABELS[language].personalID}

{LABELS[language].fullName}: {proposalData.fullName}
{LABELS[language].email}: {proposalData.email}
{LABELS[language].phone}: {proposalData.phone}
{LABELS[language].birthDate}: {proposalData.birthDate ? new Date(proposalData.birthDate).toLocaleDateString() : ''}

{LABELS[language].passportNumber}

{LABELS[language].passportNumber}: {proposalData.passportNumber}
{LABELS[language].issueDate}: {proposalData.issueDate ? new Date(proposalData.issueDate).toLocaleDateString() : ''}
{LABELS[language].expiryDate}: {proposalData.expiryDate ? new Date(proposalData.expiryDate).toLocaleDateString() : ''}
{LABELS[language].issuingAuthority}: {proposalData.issuingAuthority}
{LABELS[language].personalID}: {proposalData.personalID}

{LABELS[language].wiseaccount}

{LABELS[language].wiseaccount}: {proposalData.wiseaccount}
{LABELS[language].personalID}: {proposalData.personalID}

{LABELS[language].tripOverview}

{LABELS[language].tripOverview} {LABELS[language].days}: {startDate ? new Date(startDate).toLocaleDateString() : '--'} {LABELS[language].to} {endDate ? new Date(endDate).toLocaleDateString() : '--'}
{LABELS[language].tripDuration}: {tripDays} {LABELS[language].days}
{LABELS[language].citiesVisited}: {cities.map(c => c.city).join(' → ')}
{LABELS[language].totalBudget}: €{tripTotal.toFixed(2)}
{LABELS[language].perDayAverage}: €{(tripTotal/tripDays).toFixed(2)}

{LABELS[language].breakdown}

🏨 {LABELS[language].accommodation}: €{totalsCat.accommodation.toFixed(2)}
💰 {LABELS[language].transportation}: €{totalsCat.transport.toFixed(2)}
🍽️ {LABELS[language].food}: €{totalsCat.food.toFixed(2)}
🎁 {LABELS[language].extras}: €{totalsCat.extras.toFixed(2)}

{LABELS[language].detailsPerCity}

{cities.map((city, idx) => { const days = city.startDate && city.endDate ? Math.max(1, Math.round((new Date(city.endDate) - new Date(city.startDate))/(1000*60*60*24))) : 1; const total = cityTotals[idx] || 0; const daily = days ? (total/days).toFixed(2) : total.toFixed(2); return ( ); })}
{LABELS[language].city} {LABELS[language].days} {LABELS[language].dailyCost} {LABELS[language].totalBudget}
{city.city} {days} €{daily} €{total.toFixed(2)}

{LABELS[language].detailsPerCity}

{cities.map((city, idx) => (

{city.city}

{LABELS[language].tripOverview} {LABELS[language].days}: {city.startDate} to {city.endDate}
{LABELS[language].accommodation}: {city.stars}★ {city.accomType}
{idx > 0 &&
{LABELS[language].transportation}: {city.transport} ({city.transportPrice})
}
{LABELS[language].mealsOptions} {foodOptions.filter(f => city.foodChecks && city.foodChecks[f]).join(', ') || 'None selected'}
{LABELS[language].spendAmount}: {city.spend}
{LABELS[language].attractions}: {city.attractions}
))}

{LABELS[language].reviewProposal} contact@tunvolo.com

)} ); } // ... ReactDOM line ReactDOM.createRoot(document.getElementById("app-root")).render();