Freelance Rate Calculator

RateCraft Pro — Freelance Project Rate, Profit & PDF Proposal Studio https://cdn.tailwindcss.com https://unpkg.com/lucide@latest https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js tailwind.config = { darkMode: ‘class’, theme: { extend: { fontFamily: { sans: [‘Inter’, ‘system-ui’, ‘sans-serif’], mono: [‘JetBrains Mono’, ‘monospace’], }, colors: { brand: { 50: ‘#eef2ff’, 100: ‘#e0e7ff’, 500: ‘#6366f1’, 600: ‘#4f46e5’, 700: ‘#4338ca’, } } } } } /* Custom Range Slider Styling */ input[type=”range”] { -webkit-appearance: none; appearance: none; background: transparent; cursor: pointer; } input[type=”range”]::-webkit-slider-runnable-track { height: 8px; border-radius: 9999px; background: #e2e8f0; } .dark input[type=”range”]::-webkit-slider-runnable-track { background: #334155; } input[type=”range”]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; margin-top: -5px; background-color: #4f46e5; height: 18px; width: 18px; border-radius: 9999px; border: 2px solid #ffffff; box-shadow: 0 2px 6px rgba(79, 70, 229, 0.4); transition: transform 0.15s ease, background-color 0.15s ease; } input[type=”range”]::-webkit-slider-thumb:hover { transform: scale(1.15); background-color: #4338ca; } input[type=”range”]::-moz-range-track { height: 8px; border-radius: 9999px; background: #e2e8f0; } .dark input[type=”range”]::-moz-range-track { background: #334155; } input[type=”range”]::-moz-range-thumb { background-color: #4f46e5; height: 18px; width: 18px; border-radius: 9999px; border: 2px solid #ffffff; box-shadow: 0 2px 6px rgba(79, 70, 229, 0.4); transition: transform 0.15s ease; } /* Smooth transitions for revenue bar segments */ .bar-segment { transition: width 0.35s cubic-bezier(0.4, 0, 0.2, 1); } /* Executive A4 Proposal Sheet Styling */ .proposal-a4-sheet { background: #ffffff; color: #0f172a; width: 100%; max-width: 820px; margin: 0 auto; box-shadow: 0 20px 50px rgba(15, 23, 42, 0.12); border-radius: 16px; } /* Dedicated @media print rules: hides calculator UI and prints ONLY the crisp client proposal */ @media print { @page { size: A4 portrait; margin: 12mm; } html, body { background: #ffffff !important; color: #0f172a !important; margin: 0 !important; padding: 0 !important; -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; } .no-print { display: none !important; } #proposalStudioSection { display: block !important; margin: 0 !important; padding: 0 !important; } #proposalSheetWrapper { padding: 0 !important; background: transparent !important; border: none !important; box-shadow: none !important; } .proposal-a4-sheet { max-width: 100% !important; width: 100% !important; box-shadow: none !important; border: none !important; border-radius: 0 !important; padding: 0 !important; margin: 0 !important; } }

RateCraft Pro

Currency $ USD S$ SGD € EUR £ GBP A$ AUD C$ CAD
Quick Load Profile:

1. Financial & Capacity Inputs

Two-way synced sliders & exact number inputs

Target Monthly Take-Home
$
Net personal pay after tax & overhead $60,000 / yr net
Billable Hours Per Week
hrs
Billable Reality: Most freelancers bill 55%–65% of their workweek. Keep 10–15 hrs/wk for client calls, proposals & admin.
Monthly Software & Business Expenses
$
SaaS tools, AI subscriptions, hosting, insurance $3,000 / yr
Paid Vacation / Time Off Weeks
wks
Public holidays, leave & sick days 48 active weeks / yr
Estimated Tax Buffer (%)
%
Income tax, CPF/self-employment contributions +$1,250 / mo
Target Profit Margin (%)
%
Business growth buffer above break-even rate +$1,625 / mo profit buffer

2. Project Scope for Quote Generation

Powers the 3-tier pricing & PDF client proposal

Project / Engagement Title
Estimated Base Project Hours
hrs
≈ 2.5 workdays (at 8h/day) 0.8 billable weeks
Ready to send a formal client quote? Customize PDF Quote
Walk-Away Minimum
$65 / hr
Break-Even Day (8h): $520/day
+25% Margin
Recommended Rate
$81 / hr
Target Day Rate (8h): $650/day
Target Gross Rev
$8,125 / mo
Annual Target: $97,500/yr

Monthly Revenue & Profit Breakdown

Allocation of your $8,125/mo target gross revenue (Break-even floor: $6,500/mo)

100 billable hrs/mo
Net Income
61.5%
$5,000
Tax Reserve
15.4%
$1,250
Overhead
3.1%
$250
Profit Buffer
20.0%
$1,625

3-Tier Value-Anchored Pricing

20 hr base scope

Customize the deliverables in each box below—changes sync directly to your printable PDF proposal

Tier 1 • Essential 1.0x Base
$1,625
20 hrs • $81/hr eff.
Editable Deliverables (1 per line)
★ Recommended
Tier 2 • Growth 1.35x Value
$2,194
20 hrs • $110/hr eff.
Editable Deliverables (1 per line)
Tier 3 • Turnkey 1.8x VIP
$2,925
20 hrs • $146/hr eff.
Editable Deliverables (1 per line)

Branded Client Proposal & PDF Export Studio

Ready for Client Send-Off

Customize your studio branding, client details, and package display below, then download a crisp A4 PDF quote.

Customize Client Proposal

Live Preview →
Pricing Display Mode on Proposal Show All 3 Pricing Tiers (Recommended) Highlight Single Tier: Tier 1 (Starter) Highlight Single Tier: Tier 2 (Growth) Highlight Single Tier: Tier 3 (Turnkey VIP)
Your Studio / Freelancer Info
Your Name / Studio Brand
Email / Website / Portfolio
Prepared For (Client Info)
Client Contact Name
Client Company
Metadata & Validity
Proposal Ref #
Valid Until
Payment Terms & Schedule
Executive Summary / Custom Scope Notes This proposal outlines the recommended scope, timeline, and investment options designed to achieve your project goals with predictable delivery and measurable ROI.
Project Proposal & Investment Quote

Alex Rivera Studio

hello@alexrivera.studio • alexrivera.studio

Proposal Reference
PROP-2026-084
Issued: Oct 27, 2026
Valid Until: Nov 10, 2026
Prepared Exclusively For
Sarah Chen
Lumina Retail Pte Ltd
Project Engagement
Website Redesign & Conversion Optimization
Base Scope: ~20 hrs Est. Turnaround: ~1–2 weeks Currency: USD ($)

1. Executive Summary & Scope Overview

This proposal outlines the recommended scope, timeline, and investment options designed to achieve your project goals with predictable delivery and measurable ROI.

2. Investment Options & Deliverables

Out-of-scope additions billed at $81/hr
3. Payment Terms & Schedule

50% deposit upon project kickoff, 50% upon final handoff & approval.

4. Acceptance & Authorization

To approve this proposal and reserve your project kickoff slot, please sign or reply with your selected package tier.

Authorized Signature Date
// Helper to compute default dates function getFormattedToday() { const d = new Date(); return d.toLocaleDateString(‘en-US’, { month: ‘short’, day: ‘numeric’, year: ‘numeric’ }); } function getDefaultValidUntilISO() { const d = new Date(); d.setDate(d.getDate() + 14); return d.toISOString().split(‘T’)[0]; } function formatISODateToReadable(isoStr) { if (!isoStr) return ’14 Days from Issue’; const parts = isoStr.split(‘-‘); if (parts.length !== 3) return isoStr; const dateObj = new Date(Number(parts[0]), Number(parts[1]) – 1, Number(parts[2])); return dateObj.toLocaleDateString(‘en-US’, { month: ‘short’, day: ‘numeric’, year: ‘numeric’ }); } // Default state values const DEFAULT_STATE = { currencySymbol: ‘$’, currencyCode: ‘USD’, monthlyIncome: 5000, billableHoursPerWeek: 25, monthlyExpenses: 250, vacationWeeks: 4, taxPercent: 20, profitMarginPercent: 25, projectName: ‘Website Redesign & Conversion Optimization’, projectHours: 20, // Editable Tier Names & Deliverables tier1Name: ‘Starter Scope’, tier1Deliverables: ‘Core agreed project deliverables\n1 round of consolidated revisions\nStandard delivery timeline\nAsync email status updates’, tier2Name: ‘Growth & Optimization’, tier2Deliverables: ‘Everything in Starter Scope, plus:\nUp to 3 rounds of revisions\nPriority Slack/Teams communication\nConversion & QA optimization pass’, tier3Name: ‘Turnkey VIP Partner’, tier3Deliverables: ‘Everything in Growth Package, plus:\nFast-track priority delivery\n1-on-1 Kickoff Strategy Workshop\nFull source files + 14-day post-launch support’, // Proposal Customization State proposalMode: ‘all’, studioName: ‘Alex Rivera Studio’, studioContact: ‘hello@alexrivera.studio • alexrivera.studio’, clientName: ‘Sarah Chen’, clientCompany: ‘Lumina Retail Pte Ltd’, proposalRef: ‘PROP-2026-084’, proposalValidUntil: getDefaultValidUntilISO(), paymentTerms: ‘50% deposit upon project kickoff, 50% upon final handoff & approval.’, proposalNotes: ‘This proposal outlines the recommended scope, timeline, and investment options designed to achieve your project goals with predictable delivery and measurable ROI.’ }; // Preset Profiles const PRESETS = { parttime: { monthlyIncome: 2000, billableHoursPerWeek: 12, monthlyExpenses: 80, vacationWeeks: 4, taxPercent: 15, profitMarginPercent: 20, projectHours: 12 }, fulltime: { monthlyIncome: 5000, billableHoursPerWeek: 25, monthlyExpenses: 250, vacationWeeks: 4, taxPercent: 20, profitMarginPercent: 25, projectHours: 20 }, senior: { monthlyIncome: 9500, billableHoursPerWeek: 30, monthlyExpenses: 450, vacationWeeks: 5, taxPercent: 25, profitMarginPercent: 35, projectHours: 35 } }; // Current State let state = { …DEFAULT_STATE }; // DOM Elements Reference const els = { currencySelect: document.getElementById(‘currencySelect’), currencySymbols: document.querySelectorAll(‘.currency-symbol’), resetBtn: document.getElementById(‘resetBtn’), themeToggleBtn: document.getElementById(‘themeToggleBtn’), // Inputs & Sliders incomeInput: document.getElementById(‘incomeInput’), incomeSlider: document.getElementById(‘incomeSlider’), hoursInput: document.getElementById(‘hoursInput’), hoursSlider: document.getElementById(‘hoursSlider’), expensesInput: document.getElementById(‘expensesInput’), expensesSlider: document.getElementById(‘expensesSlider’), vacationInput: document.getElementById(‘vacationInput’), vacationSlider: document.getElementById(‘vacationSlider’), taxInput: document.getElementById(‘taxInput’), taxSlider: document.getElementById(‘taxSlider’), profitMarginInput: document.getElementById(‘profitMarginInput’), profitMarginSlider: document.getElementById(‘profitMarginSlider’), projectNameInput: document.getElementById(‘projectNameInput’), projectHoursInput: document.getElementById(‘projectHoursInput’), projectHoursSlider: document.getElementById(‘projectHoursSlider’), // Input Hints annualTakeHomeHint: document.getElementById(‘annualTakeHomeHint’), annualExpensesHint: document.getElementById(‘annualExpensesHint’), workingWeeksHint: document.getElementById(‘workingWeeksHint’), taxReserveHint: document.getElementById(‘taxReserveHint’), profitBufferHint: document.getElementById(‘profitBufferHint’), projectDaysEquivalent: document.getElementById(‘projectDaysEquivalent’), projectWeeksEquivalent: document.getElementById(‘projectWeeksEquivalent’), // Hero Metrics minHourlyRateDisplay: document.getElementById(‘minHourlyRateDisplay’), minDayRateDisplay: document.getElementById(‘minDayRateDisplay’), targetRateBadge: document.getElementById(‘targetRateBadge’), targetHourlyRateDisplay: document.getElementById(‘targetHourlyRateDisplay’), targetDayRateDisplay: document.getElementById(‘targetDayRateDisplay’), grossMonthlyDisplay: document.getElementById(‘grossMonthlyDisplay’), grossAnnualDisplay: document.getElementById(‘grossAnnualDisplay’), // Revenue Breakdown breakdownSubtitleGross: document.getElementById(‘breakdownSubtitleGross’), breakdownFloorGross: document.getElementById(‘breakdownFloorGross’), billableCapacityBadge: document.getElementById(‘billableCapacityBadge’), barTakeHome: document.getElementById(‘barTakeHome’), barTax: document.getElementById(‘barTax’), barOverhead: document.getElementById(‘barOverhead’), barProfit: document.getElementById(‘barProfit’), pctTakeHome: document.getElementById(‘pctTakeHome’), pctTax: document.getElementById(‘pctTax’), pctOverhead: document.getElementById(‘pctOverhead’), pctProfit: document.getElementById(‘pctProfit’), valTakeHome: document.getElementById(‘valTakeHome’), valTax: document.getElementById(‘valTax’), valOverhead: document.getElementById(‘valOverhead’), valProfit: document.getElementById(‘valProfit’), // Pricing Tiers & Editable Deliverables scopeHoursBadge: document.getElementById(‘scopeHoursBadge’), tier1NameInput: document.getElementById(‘tier1NameInput’), tier1Price: document.getElementById(‘tier1Price’), tier1Meta: document.getElementById(‘tier1Meta’), tier1DeliverablesInput: document.getElementById(‘tier1DeliverablesInput’), tier2NameInput: document.getElementById(‘tier2NameInput’), tier2Price: document.getElementById(‘tier2Price’), tier2Meta: document.getElementById(‘tier2Meta’), tier2DeliverablesInput: document.getElementById(‘tier2DeliverablesInput’), tier3NameInput: document.getElementById(‘tier3NameInput’), tier3Price: document.getElementById(‘tier3Price’), tier3Meta: document.getElementById(‘tier3Meta’), tier3DeliverablesInput: document.getElementById(‘tier3DeliverablesInput’), copyAllTiersBtn: document.getElementById(‘copyAllTiersBtn’), // Proposal Studio Form Inputs proposalModeSelect: document.getElementById(‘proposalModeSelect’), studioNameInput: document.getElementById(‘studioNameInput’), studioContactInput: document.getElementById(‘studioContactInput’), clientNameInput: document.getElementById(‘clientNameInput’), clientCompanyInput: document.getElementById(‘clientCompanyInput’), proposalRefInput: document.getElementById(‘proposalRefInput’), proposalValidInput: document.getElementById(‘proposalValidInput’), paymentTermsInput: document.getElementById(‘paymentTermsInput’), proposalNotesInput: document.getElementById(‘proposalNotesInput’), // Printable Proposal Preview Sheet Elements printableProposalSheet: document.getElementById(‘printableProposalSheet’), pdfStudioName: document.getElementById(‘pdfStudioName’), pdfStudioContact: document.getElementById(‘pdfStudioContact’), pdfRefNumber: document.getElementById(‘pdfRefNumber’), pdfIssueDate: document.getElementById(‘pdfIssueDate’), pdfValidDate: document.getElementById(‘pdfValidDate’), pdfClientName: document.getElementById(‘pdfClientName’), pdfClientCompany: document.getElementById(‘pdfClientCompany’), pdfProjectTitle: document.getElementById(‘pdfProjectTitle’), pdfEstimatedHoursBadge: document.getElementById(‘pdfEstimatedHoursBadge’), pdfEstimatedTimelineBadge: document.getElementById(‘pdfEstimatedTimelineBadge’), pdfCurrencyBadge: document.getElementById(‘pdfCurrencyBadge’), pdfExecutiveNotes: document.getElementById(‘pdfExecutiveNotes’), pdfAdditionalRateNote: document.getElementById(‘pdfAdditionalRateNote’), pdfTiersContainer: document.getElementById(‘pdfTiersContainer’), pdfPaymentTerms: document.getElementById(‘pdfPaymentTerms’), // PDF Export Buttons headerDownloadPdfBtn: document.getElementById(‘headerDownloadPdfBtn’), downloadPdfBtn: document.getElementById(‘downloadPdfBtn’), downloadPdfBtnText: document.getElementById(‘downloadPdfBtnText’), sidebarDownloadPdfBtn: document.getElementById(‘sidebarDownloadPdfBtn’), printVectorPdfBtn: document.getElementById(‘printVectorPdfBtn’), toastContainer: document.getElementById(‘toastContainer’) }; /** * Formats a number into currency string (e.g., S$5,000) */ function formatMoney(amount, decimals = 0) { const safeAmount = Number.isFinite(amount) ? amount : 0; const formatted = safeAmount.toLocaleString(‘en-US’, { minimumFractionDigits: decimals, maximumFractionDigits: decimals }); return `${state.currencySymbol}${formatted}`; } /** * Escapes HTML special characters for safe rendering inside the printable sheet */ function escapeHtml(str) { return String(str || ”) .replace(/&/g, ‘&’) .replace(//g, ‘>’) .replace(/”/g, ‘"’) .replace(/’/g, ‘'’); } /** * Core Calculation Engine */ function calculateMetrics() { const monthlyTakeHome = Math.max(0, Number(state.monthlyIncome) || 0); const billableHoursPerWeek = Math.max(1, Number(state.billableHoursPerWeek) || 1); const monthlyExpenses = Math.max(0, Number(state.monthlyExpenses) || 0); const vacationWeeks = Math.min(48, Math.max(0, Number(state.vacationWeeks) || 0)); const taxPercent = Math.min(80, Math.max(0, Number(state.taxPercent) || 0)); const profitMarginPercent = Math.min(100, Math.max(0, Number(state.profitMarginPercent) || 0)); const projectHours = Math.max(1, Number(state.projectHours) || 1); // Working time math const workingWeeksPerYear = Math.max(1, 52 – vacationWeeks); const annualBillableHours = billableHoursPerWeek * workingWeeksPerYear; const monthlyBillableHours = annualBillableHours / 12; // Tax & Break-Even Floor Math const taxRateDecimal = taxPercent / 100; const preTaxIncomeNeeded = taxRateDecimal line.trim()) .filter(Boolean); } /** * Renders the dynamic Pricing Tiers inside the Printable A4 Proposal Preview */ function renderProposalSheetTiers(m) { const tierConfigs = { tier1: { key: ‘tier1’, badge: ‘Option 1 • Essential’, name: state.tier1Name || ‘Starter Scope’, price: formatMoney(m.tier1Price), meta: `${m.projectHours} hrs base scope`, deliverables: parseDeliverables(state.tier1Deliverables), featured: false }, tier2: { key: ‘tier2’, badge: ‘Option 2 • Recommended’, name: state.tier2Name || ‘Growth & Optimization’, price: formatMoney(m.tier2Price), meta: `${m.projectHours} hrs priority scope`, deliverables: parseDeliverables(state.tier2Deliverables), featured: true }, tier3: { key: ‘tier3’, badge: ‘Option 3 • Turnkey VIP’, name: state.tier3Name || ‘Turnkey VIP Partner’, price: formatMoney(m.tier3Price), meta: `${m.projectHours} hrs VIP turnkey scope`, deliverables: parseDeliverables(state.tier3Deliverables), featured: false } }; const mode = state.proposalMode || ‘all’; const keysToRender = mode === ‘all’ ? [‘tier1’, ‘tier2’, ‘tier3’] : [mode]; els.pdfTiersContainer.className = mode === ‘all’ ? ‘grid grid-cols-1 md:grid-cols-3 gap-3.5’ : ‘grid grid-cols-1 gap-4’; els.pdfTiersContainer.innerHTML = keysToRender.map(k => { const t = tierConfigs[k] || tierConfigs.tier2; const isSingle = mode !== ‘all’; const borderClass = t.featured || isSingle ? ‘border-2 border-indigo-600 bg-indigo-50/20’ : ‘border border-slate-200 bg-white’; const deliverablesHtml = t.deliverables.map(item => `
  • ✓ ${escapeHtml(item)}
  • `).join(”); return `
    ${escapeHtml(t.badge)} ${t.featured && !isSingle ? ‘Best Value‘ : ”}
    ${escapeHtml(t.name)}
    ${t.price} ${escapeHtml(t.meta)}
      ${deliverablesHtml}
    `; }).join(”); } /** * Updates the entire UI and Live Proposal Preview */ function updateUI() { const m = calculateMetrics(); // Update currency symbols on input fields els.currencySymbols.forEach(span => { span.textContent = state.currencySymbol; }); // Update Input Hints els.annualTakeHomeHint.textContent = `${formatMoney(m.monthlyTakeHome * 12)} / yr net`; els.annualExpensesHint.textContent = `${formatMoney(m.monthlyExpenses * 12)} / yr`; els.workingWeeksHint.textContent = `${m.workingWeeksPerYear} active weeks / yr (${Math.round(m.annualBillableHours)} billable hrs/yr)`; els.taxReserveHint.textContent = `+${formatMoney(Math.round(m.monthlyTaxReserve))} / mo tax reserve`; els.profitBufferHint.textContent = `+${formatMoney(Math.round(m.monthlyProfitBuffer))} / mo profit buffer`; const workdays = (m.projectHours / 8).toFixed(1); const billableWeeks = (m.projectHours / m.billableHoursPerWeek).toFixed(1); els.projectDaysEquivalent.textContent = `≈ ${workdays} workdays (at 8h/day)`; els.projectWeeksEquivalent.textContent = `${billableWeeks} billable wks`; // Update Hero Cards els.minHourlyRateDisplay.textContent = formatMoney(Math.round(m.minHourlyRate)); els.minDayRateDisplay.textContent = `${formatMoney(Math.round(m.minDayRate))}/day`; els.targetRateBadge.textContent = `+${m.profitMarginPercent}% Margin`; els.targetHourlyRateDisplay.textContent = formatMoney(Math.round(m.targetHourlyRate)); els.targetDayRateDisplay.textContent = `${formatMoney(Math.round(m.targetDayRate))}/day`; els.grossMonthlyDisplay.textContent = formatMoney(Math.round(m.targetGrossMonthly)); els.grossAnnualDisplay.textContent = `${formatMoney(Math.round(m.targetGrossAnnual))}/yr`; // Update 4-Part Revenue & Profit Breakdown Bar els.breakdownSubtitleGross.textContent = `${formatMoney(Math.round(m.targetGrossMonthly))}/mo`; els.breakdownFloorGross.textContent = `${formatMoney(Math.round(m.breakEvenGrossMonthly))}/mo`; els.billableCapacityBadge.innerHTML = ` ${Math.round(m.monthlyBillableHours)} billable hrs/mo`; const totalGross = Math.max(1, m.targetGrossMonthly); const takeHomePct = (m.monthlyTakeHome / totalGross) * 100; const taxPct = (m.monthlyTaxReserve / totalGross) * 100; const overheadPct = (m.monthlyExpenses / totalGross) * 100; const profitPct = (m.monthlyProfitBuffer / totalGross) * 100; els.barTakeHome.style.width = `${takeHomePct.toFixed(1)}%`; els.barTax.style.width = `${taxPct.toFixed(1)}%`; els.barOverhead.style.width = `${overheadPct.toFixed(1)}%`; els.barProfit.style.width = `${profitPct.toFixed(1)}%`; els.pctTakeHome.textContent = `${takeHomePct.toFixed(1)}% of rev`; els.pctTax.textContent = `${taxPct.toFixed(1)}% of rev`; els.pctOverhead.textContent = `${overheadPct.toFixed(1)}% of rev`; els.pctProfit.textContent = `${profitPct.toFixed(1)}% of rev`; els.valTakeHome.textContent = formatMoney(Math.round(m.monthlyTakeHome)); els.valTax.textContent = formatMoney(Math.round(m.monthlyTaxReserve)); els.valOverhead.textContent = formatMoney(Math.round(m.monthlyExpenses)); els.valProfit.textContent = formatMoney(Math.round(m.monthlyProfitBuffer)); // Update Project Pricing Tiers els.scopeHoursBadge.textContent = `${m.projectHours} hr base scope`; els.tier1Price.textContent = formatMoney(m.tier1Price); els.tier1Meta.textContent = `${m.projectHours} hrs • ${formatMoney(Math.round(m.tier1Price / m.projectHours))}/hr eff.`; els.tier2Price.textContent = formatMoney(m.tier2Price); els.tier2Meta.textContent = `${m.projectHours} hrs • ${formatMoney(Math.round(m.tier2Price / m.projectHours))}/hr eff.`; els.tier3Price.textContent = formatMoney(m.tier3Price); els.tier3Meta.textContent = `${m.projectHours} hrs • ${formatMoney(Math.round(m.tier3Price / m.projectHours))}/hr eff.`; // Update Live Printable PDF Proposal Sheet els.pdfStudioName.textContent = state.studioName || ‘Freelance Studio’; els.pdfStudioContact.textContent = state.studioContact || ”; els.pdfRefNumber.textContent = state.proposalRef || ‘PROP-001’; els.pdfIssueDate.textContent = getFormattedToday(); els.pdfValidDate.textContent = formatISODateToReadable(state.proposalValidUntil); els.pdfClientName.textContent = state.clientName || ‘Valued Client’; els.pdfClientCompany.textContent = state.clientCompany || ”; els.pdfProjectTitle.textContent = state.projectName || ‘Custom Project Engagement’; const estWeeks = Math.max(1, Math.ceil(m.projectHours / m.billableHoursPerWeek)); els.pdfEstimatedHoursBadge.textContent = `Base Scope: ~${m.projectHours} hrs`; els.pdfEstimatedTimelineBadge.textContent = `Est. Turnaround: ~${estWeeks} ${estWeeks === 1 ? ‘week’ : ‘weeks’}`; els.pdfCurrencyBadge.textContent = `Currency: ${state.currencyCode} (${state.currencySymbol})`; els.pdfExecutiveNotes.textContent = state.proposalNotes || ”; els.pdfAdditionalRateNote.textContent = `Out-of-scope additions billed at ${formatMoney(Math.round(m.targetHourlyRate))}/hr`; els.pdfPaymentTerms.textContent = state.paymentTerms || ”; renderProposalSheetTiers(m); if (window.lucide) { lucide.createIcons(); } } /** * Generates clean text quote for clipboard copying */ function generateFullProposalText(m) { const projTitle = (state.projectName || ‘Custom Project’).trim(); const estWeeks = Math.max(1, Math.ceil(m.projectHours / m.billableHoursPerWeek)); const t1Bullets = parseDeliverables(state.tier1Deliverables).map(b => `• ${b}`).join(‘\n’); const t2Bullets = parseDeliverables(state.tier2Deliverables).map(b => `• ${b}`).join(‘\n’); const t3Bullets = parseDeliverables(state.tier3Deliverables).map(b => `• ${b}`).join(‘\n’); return `PROPOSAL ESTIMATE: ${projTitle.toUpperCase()} Prepared For: ${state.clientName} (${state.clientCompany}) Prepared By: ${state.studioName} (${state.studioContact}) Reference: ${state.proposalRef} | Valid Until: ${formatISODateToReadable(state.proposalValidUntil)} Estimated Scope: ~${m.projectHours} hours (Approx. ${estWeeks} ${estWeeks === 1 ? ‘week’ : ‘weeks’}) ————————————————– OPTION 1: ${state.tier1Name} — ${formatMoney(m.tier1Price)} (${state.currencyCode}) ${t1Bullets} OPTION 2: ${state.tier2Name} (Recommended) — ${formatMoney(m.tier2Price)} (${state.currencyCode}) ${t2Bullets} OPTION 3: ${state.tier3Name} — ${formatMoney(m.tier3Price)} (${state.currencyCode}) ${t3Bullets} ————————————————– Payment Terms: ${state.paymentTerms} Standard Hourly Rate for out-of-scope additions: ${formatMoney(Math.round(m.targetHourlyRate))}/hr`; } /** * Generates single tier quote text */ function generateSingleTierText(tierNumber) { const m = calculateMetrics(); const projTitle = (state.projectName || ‘Custom Project’).trim(); const tiers = { 1: { name: state.tier1Name, price: formatMoney(m.tier1Price), bullets: parseDeliverables(state.tier1Deliverables) }, 2: { name: state.tier2Name, price: formatMoney(m.tier2Price), bullets: parseDeliverables(state.tier2Deliverables) }, 3: { name: state.tier3Name, price: formatMoney(m.tier3Price), bullets: parseDeliverables(state.tier3Deliverables) } }; const selected = tiers[tierNumber]; return `PROJECT QUOTE: ${projTitle} Prepared For: ${state.clientName} (${state.clientCompany}) Package: ${selected.name} Investment: ${selected.price} (${state.currencyCode}) Included Deliverables: • ${selected.bullets.join(‘\n• ‘)} Payment Terms: ${state.paymentTerms} Additional out-of-scope work billed at ${formatMoney(Math.round(m.targetHourlyRate))}/hr.`; } /** * Exports the #printableProposalSheet directly as a downloadable .pdf file using html2pdf.js */ async function downloadPrintablePdfProposal() { const sheetEl = els.printableProposalSheet; if (!sheetEl) return; const safeClient = (state.clientCompany || state.clientName || ‘Client’) .replace(/[^a-zA-Z0-9_-]/g, ‘-‘) .replace(/-+/g, ‘-‘) .replace(/^-|-$/g, ”); const fileName = `Proposal-${safeClient || ‘Client’}-${state.proposalRef || ‘Quote’}.pdf`; // If html2pdf is loaded, generate direct PDF download if (typeof window.html2pdf === ‘function’) { const originalText = els.downloadPdfBtnText.textContent; els.downloadPdfBtnText.textContent = ‘Generating PDF…’; els.downloadPdfBtn.disabled = true; try { const opt = { margin: [10, 10, 10, 10], filename: fileName, image: { type: ‘jpeg’, quality: 0.98 }, html2canvas: { scale: 2, useCORS: true, backgroundColor: ‘#ffffff’, scrollY: 0 }, jsPDF: { unit: ‘mm’, format: ‘a4’, orientation: ‘portrait’ } }; await window.html2pdf().set(opt).from(sheetEl).save(); showToast(`Downloaded “${fileName}”!`); } catch (err) { // Fallback to native print dialog if canvas security blocks export window.print(); } finally { els.downloadPdfBtnText.textContent = originalText; els.downloadPdfBtn.disabled = false; } } else { // Fallback to native vector print dialog window.print(); } } /** * Copies text to clipboard using document.execCommand(‘copy’) for iframe compatibility */ function copyToClipboard(text, toastMessage) { const textArea = document.createElement(‘textarea’); textArea.value = text; textArea.style.position = ‘fixed’; textArea.style.left = ‘-9999px’; textArea.style.top = ‘0’; document.body.appendChild(textArea); textArea.focus(); textArea.select(); try { document.execCommand(‘copy’); showToast(toastMessage); } catch (err) { showToast(‘Could not copy automatically — please use Export PDF.’); } finally { document.body.removeChild(textArea); } } /** * Displays a non-intrusive toast confirmation */ function showToast(message) { const toast = document.createElement(‘div’); toast.className = ‘flex items-center gap-2.5 px-4 py-3 rounded-xl bg-slate-900 dark:bg-white text-white dark:text-slate-900 text-xs sm:text-sm font-semibold shadow-xl border border-slate-700 dark:border-slate-200 transform transition-all duration-300 translate-y-2 opacity-0’; toast.innerHTML = ` ✓ ${escapeHtml(message)} `; els.toastContainer.appendChild(toast); requestAnimationFrame(() => { toast.classList.remove(‘translate-y-2’, ‘opacity-0’); }); setTimeout(() => { toast.classList.add(‘translate-y-2’, ‘opacity-0’); setTimeout(() => toast.remove(), 300); }, 2800); } /** * Helper to bind a number input and range slider pair two-ways */ function bindInputAndSlider(inputEl, sliderEl, stateKey) { inputEl.addEventListener(‘input’, (e) => { const val = parseFloat(e.target.value); if (!Number.isNaN(val)) { state[stateKey] = val; sliderEl.value = val; updateUI(); } }); sliderEl.addEventListener(‘input’, (e) => { const val = parseFloat(e.target.value); state[stateKey] = val; inputEl.value = val; updateUI(); }); } /** * Helper to bind text/textarea/select inputs to state */ function bindTextField(el, stateKey, eventName = ‘input’) { if (!el) return; el.addEventListener(eventName, (e) => { state[stateKey] = e.target.value; updateUI(); }); } /** * Syncs all DOM inputs from the state object */ function syncInputsFromState() { els.incomeInput.value = state.monthlyIncome; els.incomeSlider.value = state.monthlyIncome; els.hoursInput.value = state.billableHoursPerWeek; els.hoursSlider.value = state.billableHoursPerWeek; els.expensesInput.value = state.monthlyExpenses; els.expensesSlider.value = state.monthlyExpenses; els.vacationInput.value = state.vacationWeeks; els.vacationSlider.value = state.vacationWeeks; els.taxInput.value = state.taxPercent; els.taxSlider.value = state.taxPercent; els.profitMarginInput.value = state.profitMarginPercent; els.profitMarginSlider.value = state.profitMarginPercent; els.projectNameInput.value = state.projectName; els.projectHoursInput.value = state.projectHours; els.projectHoursSlider.value = state.projectHours; els.tier1NameInput.value = state.tier1Name; els.tier1DeliverablesInput.value = state.tier1Deliverables; els.tier2NameInput.value = state.tier2Name; els.tier2DeliverablesInput.value = state.tier2Deliverables; els.tier3NameInput.value = state.tier3Name; els.tier3DeliverablesInput.value = state.tier3Deliverables; els.proposalModeSelect.value = state.proposalMode; els.studioNameInput.value = state.studioName; els.studioContactInput.value = state.studioContact; els.clientNameInput.value = state.clientName; els.clientCompanyInput.value = state.clientCompany; els.proposalRefInput.value = state.proposalRef; els.proposalValidInput.value = state.proposalValidUntil; els.paymentTermsInput.value = state.paymentTerms; els.proposalNotesInput.value = state.proposalNotes; } // Initialize Event Listeners function init() { // Core numeric sliders bindInputAndSlider(els.incomeInput, els.incomeSlider, ‘monthlyIncome’); bindInputAndSlider(els.hoursInput, els.hoursSlider, ‘billableHoursPerWeek’); bindInputAndSlider(els.expensesInput, els.expensesSlider, ‘monthlyExpenses’); bindInputAndSlider(els.vacationInput, els.vacationSlider, ‘vacationWeeks’); bindInputAndSlider(els.taxInput, els.taxSlider, ‘taxPercent’); bindInputAndSlider(els.profitMarginInput, els.profitMarginSlider, ‘profitMarginPercent’); bindInputAndSlider(els.projectHoursInput, els.projectHoursSlider, ‘projectHours’); // Project & Tier editable fields bindTextField(els.projectNameInput, ‘projectName’); bindTextField(els.tier1NameInput, ‘tier1Name’); bindTextField(els.tier1DeliverablesInput, ‘tier1Deliverables’); bindTextField(els.tier2NameInput, ‘tier2Name’); bindTextField(els.tier2DeliverablesInput, ‘tier2Deliverables’); bindTextField(els.tier3NameInput, ‘tier3Name’); bindTextField(els.tier3DeliverablesInput, ‘tier3Deliverables’); // Proposal Customization fields bindTextField(els.proposalModeSelect, ‘proposalMode’, ‘change’); bindTextField(els.studioNameInput, ‘studioName’); bindTextField(els.studioContactInput, ‘studioContact’); bindTextField(els.clientNameInput, ‘clientName’); bindTextField(els.clientCompanyInput, ‘clientCompany’); bindTextField(els.proposalRefInput, ‘proposalRef’); bindTextField(els.proposalValidInput, ‘proposalValidUntil’, ‘change’); bindTextField(els.paymentTermsInput, ‘paymentTerms’); bindTextField(els.proposalNotesInput, ‘proposalNotes’); // Currency Selector els.currencySelect.addEventListener(‘change’, (e) => { const selectedOption = e.target.options[e.target.selectedIndex]; state.currencySymbol = e.target.value; state.currencyCode = selectedOption.getAttribute(‘data-code’) || ‘USD’; updateUI(); showToast(`Currency switched to ${state.currencySymbol} (${state.currencyCode})`); }); // Preset Buttons document.querySelectorAll(‘.preset-btn’).forEach(btn => { btn.addEventListener(‘click’, () => { const presetKey = btn.getAttribute(‘data-preset’); if (PRESETS[presetKey]) { state = { …state, …PRESETS[presetKey] }; syncInputsFromState(); updateUI(); showToast(`Loaded “${btn.textContent.trim()}” preset`); } }); }); // Reset to Defaults Button els.resetBtn.addEventListener(‘click’, () => { state = { …DEFAULT_STATE, proposalValidUntil: getDefaultValidUntilISO() }; els.currencySelect.value = DEFAULT_STATE.currencySymbol; syncInputsFromState(); updateUI(); showToast(‘All inputs reset to default values’); }); // Copy Individual Tier Buttons document.querySelectorAll(‘.copy-tier-btn’).forEach(btn => { btn.addEventListener(‘click’, () => { const tierNum = btn.getAttribute(‘data-tier’); const text = generateSingleTierText(tierNum); copyToClipboard(text, `Tier ${tierNum} client quote copied to clipboard!`); }); }); // Copy Full 3-Tier Proposal Button els.copyAllTiersBtn.addEventListener(‘click’, () => { const m = calculateMetrics(); const text = generateFullProposalText(m); copyToClipboard(text, ‘Full 3-Tier Client Proposal copied to clipboard!’); }); // PDF Download & Print Buttons els.downloadPdfBtn.addEventListener(‘click’, downloadPrintablePdfProposal); els.sidebarDownloadPdfBtn.addEventListener(‘click’, downloadPrintablePdfProposal); els.headerDownloadPdfBtn.addEventListener(‘click’, () => { document.getElementById(‘proposalStudioSection’).scrollIntoView({ behavior: ‘smooth’ }); setTimeout(downloadPrintablePdfProposal, 450); }); els.printVectorPdfBtn.addEventListener(‘click’, () => { window.print(); }); // Dark Mode Toggle els.themeToggleBtn.addEventListener(‘click’, () => { document.documentElement.classList.toggle(‘dark’); }); // Initial Render syncInputsFromState(); updateUI(); } window.addEventListener(‘DOMContentLoaded’, init);