Travel Hacker Budget Planner

Travel Hacker Budget Planner

Budget Overview

Total Budget

$0.00

Total Spent

$0.00

Remaining

$0.00

Trip Duration

0 Days

Spending by Category

Set Up Your Budget

Budget Categories

Track Your Expenses

Recent Expenses

Tropical Storm Travel Risk

New Construction Home Price Estimator

${expense.description}

${expense.date} - ${expense.category}

${formatCurrency(expense.amount * conversionRate, state.baseCurrency)}
`; expenseList.appendChild(li); }); } // --- EVENT HANDLERS & ACTIONS --- window.changeTab = (tabIndex) => { state.currentTab = tabIndex; renderTabs(); }; window.navigateTabs = (direction) => { if (direction === 'next' && state.currentTab < tabs.length - 1) { state.currentTab++; } else if (direction === 'prev' && state.currentTab > 0) { state.currentTab--; } renderTabs(); }; function addBudgetCategory() { state.categories.push({ name: 'New Category', budget: 0 }); renderBudgetSetup(); } window.removeBudgetCategory = (index) => { state.categories.splice(index, 1); renderBudgetSetup(); updateUI(); }; window.updateCategoryName = (index, newName) => { state.categories[index].name = newName; updateUI(); }; window.updateCategoryBudget = (index, newBudget) => { state.categories[index].budget = parseFloat(newBudget) || 0; updateDashboard(); }; function addExpense() { const date = document.getElementById('expense-date').value; const description = document.getElementById('expense-description').value; const category = document.getElementById('expense-category').value; const amount = parseFloat(document.getElementById('expense-amount').value); if (!date || !description || !category || isNaN(amount)) { console.error("Invalid expense input"); return; } state.expenses.push({ date, description, category, amount }); document.getElementById('expense-description').value = ''; document.getElementById('expense-amount').value = ''; updateUI(); } window.removeExpense = (index) => { state.expenses.splice(index, 1); updateUI(); }; // --- PDF GENERATION (FIXED) --- async function generatePdf() { const { jsPDF } = window.jspdf; const dashboardEl = document.getElementById('dashboard-tab'); const downloadButton = document.getElementById('pdf-download-button'); if (!dashboardEl || !downloadButton) return; downloadButton.style.display = 'none'; const canvas = await html2canvas(dashboardEl, { scale: 2, useCORS: true, windowWidth: dashboardEl.scrollWidth, windowHeight: dashboardEl.scrollHeight }); downloadButton.style.display = 'block'; const pdf = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' }); const pdfWidth = pdf.internal.pageSize.getWidth(); const pdfHeight = pdf.internal.pageSize.getHeight(); const margin = 10; const contentWidth = pdfWidth - (margin * 2); const canvasWidth = canvas.width; const canvasHeight = canvas.height; // Add title to the first page pdf.setFontSize(18); pdf.text(`${state.tripName || 'Budget'} - Summary`, pdfWidth / 2, margin + 5, { align: 'center' }); let yCanvas = 0; let page = 1; while (yCanvas < canvasHeight) { if (page > 1) { pdf.addPage(); } let yPdf = margin; let pageContentHeight = pdfHeight - (margin * 2); if (page === 1) { // Account for title space on the first page const titleSpace = 20; yPdf += titleSpace; pageContentHeight -= titleSpace; } // Calculate the height of the canvas slice for the current page let sliceHeightInCanvasPixels = canvasWidth * pageContentHeight / contentWidth; sliceHeightInCanvasPixels = Math.min(sliceHeightInCanvasPixels, canvasHeight - yCanvas); const pageCanvas = document.createElement('canvas'); pageCanvas.width = canvasWidth; pageCanvas.height = sliceHeightInCanvasPixels; const pageCtx = pageCanvas.getContext('2d'); // Draw the slice from the main canvas pageCtx.drawImage(canvas, 0, yCanvas, canvasWidth, sliceHeightInCanvasPixels, 0, 0, canvasWidth, sliceHeightInCanvasPixels); // Add the image slice to the PDF const pageImgData = pageCanvas.toDataURL('image/png'); const sliceHeightInPdfMm = contentWidth * sliceHeightInCanvasPixels / canvasWidth; pdf.addImage(pageImgData, 'PNG', margin, yPdf, contentWidth, sliceHeightInPdfMm); yCanvas += sliceHeightInCanvasPixels; page++; } pdf.save(`${(state.tripName || 'budget').replace(/\s+/g, '_')}_Summary.pdf`); } // --- STORM RISK CALCULATOR LOGIC --- function calculateStormRisk() { const startDate = new Date(document.getElementById('travel-start-date').value); const endDate = new Date(document.getElementById('travel-end-date').value); const basinKey = document.getElementById('ocean-basin').value; const destination = document.getElementById('travel-destination').value || "your destination"; if (isNaN(startDate) || isNaN(endDate) || !basinKey) { console.error("Invalid date or basin for storm risk calculation."); return; } const basin = stormSeasons[basinKey]; let riskScore = 0; for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) { const month = d.getMonth() + 1; let monthInSeason = (basin.season[0] <= basin.season[1]) ? (month >= basin.season[0] && month <= basin.season[1]) : (month >= basin.season[0] || month <= basin.season[1]); if (monthInSeason) { riskScore += 1; let monthInPeak = (basin.peak[0] <= basin.peak[1]) ? (month >= basin.peak[0] && month <= basin.peak[1]) : (month >= basin.peak[0] || month <= basin.peak[1]); if (monthInPeak) riskScore += 2; } } const duration = Math.ceil(Math.abs(endDate - startDate) / (1000 * 60 * 60 * 24)) + 1; const avgRisk = duration > 0 ? riskScore / duration : 0; displayRiskResult(avgRisk, basin.name, destination); } function displayRiskResult(avgRisk, basinName, destination) { const resultDiv = document.getElementById('risk-result'); let level, cssClass, summary, recommendations; if (avgRisk > 1.5) { level = 'High'; cssClass = 'risk-high'; summary = `Your travel to ${destination} occurs during the **peak** of the ${basinName} tropical storm season. The probability of storm activity is significantly elevated.`; recommendations = `
  • Closely monitor weather forecasts before and during your trip.
  • Purchase comprehensive travel insurance that covers weather-related cancellations.
  • Be prepared for potential travel disruptions.
  • `; } else if (avgRisk > 0) { level = 'Moderate'; cssClass = 'risk-moderate'; summary = `Your travel to ${destination} falls within the official ${basinName} tropical storm season, but outside the typical peak period. There is a chance of storm activity.`; recommendations = `
  • Stay aware of weather forecasts leading up to your travel dates.
  • Understand your airline and hotel cancellation policies.
  • Pack a small emergency kit.
  • `; } else { level = 'Low'; cssClass = 'risk-low'; summary = `Your travel to ${destination} is outside the typical ${basinName} tropical storm season. The risk of a tropical cyclone is low, but not zero.`; recommendations = `
  • Off-season storms are rare but possible; it's always wise to be aware of local weather.
  • Enjoy your trip!
  • `; } resultDiv.className = `p-4 rounded-lg ${cssClass}`; resultDiv.innerHTML = `

    Risk Level: ${level}

    ${summary}

    Recommendations:

      ${recommendations}
    `; resultDiv.classList.remove('hidden'); } // --- HOME PRICE ESTIMATOR LOGIC --- function estimateHomePrice() { const sqft = parseFloat(document.getElementById('square-footage').value); const stories = document.getElementById('stories').value; const quality = document.getElementById('quality-level').value; const garageSize = parseInt(document.getElementById('garage-size').value); if (isNaN(sqft) || sqft <= 0) { console.error("Invalid square footage"); return; } const { base_per_sqft, quality_multiplier, story_multiplier, garage_cost_per_car, cost_breakdown_percentages } = homeConstructionCosts; const adjustedBaseCost = base_per_sqft * quality_multiplier[quality] * story_multiplier[stories]; const mainStructureCost = adjustedBaseCost * sqft; const garageCost = garageSize * garage_cost_per_car; const totalCost = mainStructureCost + garageCost; displayHomePriceResult(totalCost, cost_breakdown_percentages); } function displayHomePriceResult(totalCost, percentages) { const resultDiv = document.getElementById('home-price-result'); const breakdownContainer = document.getElementById('home-cost-breakdown'); const totalHomePriceEl = document.getElementById('total-home-price'); breakdownContainer.innerHTML = ''; for (const category in percentages) { const cost = totalCost * percentages[category]; const itemEl = document.createElement('div'); itemEl.className = 'flex justify-between items-center bg-white p-3 rounded-lg shadow-sm'; itemEl.innerHTML = ` ${category} ${formatCurrency(cost, 'USD')} `; breakdownContainer.appendChild(itemEl); } totalHomePriceEl.textContent = formatCurrency(totalCost, 'USD'); resultDiv.classList.remove('hidden'); } // --- UTILITY FUNCTIONS --- function formatCurrency(amount, currencyCode) { try { return new Intl.NumberFormat('en-US', { style: 'currency', currency: currencyCode, minimumFractionDigits: 2 }).format(amount); } catch (e) { return `$${amount.toFixed(2)}`; } } function calculateDuration() { if (!state.startDate || !state.endDate) return 0; const start = new Date(state.startDate); const end = new Date(state.endDate); if (start > end) return 0; const diffTime = Math.abs(end - start); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1; return diffDays; } // --- START THE APP --- init(); });
    Scroll to Top