Freelance

Time Tracking Calculator

Calculate total billable hours and earnings from start, end, and break times.

7
By 7bc.site Editorial Team
Last updated: January 2025 Reviewed by Finance Experts 8 min read

Calculator

Enter your values. Results update automatically.

$

Result

Live calculation output.

Enter values to see results

About the Time Tracking Calculator

Tracking time accurately is the foundation of profitable freelancing — yet most freelancers estimate time after the fact, leading to under-billing and lost revenue. Our Time Tracking Calculator takes the guesswork out by computing exact work hours from start and end times, accounting for breaks, and multiplying by your hourly rate to show billable earnings. Use it daily to log hours, weekly to verify invoices, or project-by-project to confirm profitability. The calculator handles overnight shifts, multiple breaks, and any hourly rate — and the precision it provides typically uncovers 5–15% of billable time that freelancers otherwise lose to estimation.

Deep Dive: Understanding the Concept

Accurate time tracking is the foundation of profitable freelancing and consulting — yet most independent professionals estimate time after the fact, systematically under-billing by 15-25%. Memory-based estimates are biased by optimism ("that took about 2 hours" when it actually took 2.5) and recency effects (the last task feels representative of all tasks). The difference between estimated and actual time compounds across projects: a freelancer who under-bills 20% on every project effectively works 1 day per week for free. Real-time time tracking eliminates this leakage.

The billable utilization metric — the percentage of working hours that are actually billable to clients — is the most important number in a service business. Most freelancers assume 80-90% utilization; the reality is 50-65% for new freelancers (heavy client acquisition time) and 65-75% for established ones. The gap between assumed and actual utilization explains why so many freelancers earn less than they expected: a 40-hour week at 60% utilization yields only 24 billable hours, not 40. Every hour spent on admin, marketing, sales, email, and professional development is non-billable.

The psychology of time tracking reveals why it is so difficult to maintain. Tracking time feels bureaucratic and breaks flow. The "I'll remember and log it later" approach fails because by end of day, 30-40% of time details are forgotten or blurred. The solution is passive tracking: tools like Toggl, Harvest, and Clockify offer one-click timers that run in the background. The friction is minimal (one click to start, one to stop) and the data is far more accurate than retrospective estimates. Most freelancers who switch from manual to automated tracking discover they have been under-billing by 15-25%.

Time tracking data reveals patterns that transform business decisions. Most freelancers discover: (1) certain client types consistently require more time than quoted (scope creep), (2) certain tasks are far less profitable than they appear (email and meetings eat 20-30% of the day), (3) their most productive hours are 2-3x more efficient than their least productive. This data enables strategic decisions: raise rates for unprofitable client types, batch email to reduce context-switching, and schedule deep work during peak productivity hours. Without time data, these insights remain invisible.

How to Use This Calculator

  1. 1

    Enter your Start Time (24-hour format, e.g., 14:30 for 2:30 PM).

  2. 2

    Enter your End Time. If the shift crosses midnight, the calculator handles it automatically.

  3. 3

    Enter Break Time in minutes (lunch, personal time, non-billable breaks).

  4. 4

    Enter your Hourly Rate.

  5. 5

    The calculator shows total hours worked and total billable amount.

The Formula Explained

If End Time > Start Time: Work Hours = (End − Start) − Break Time. If End Time < Start Time (overnight): Work Hours = (End + 24 hours − Start) − Break Time. Billable Amount = Work Hours × Hourly Rate. Overtime (if applicable) = Max(0, Work Hours − Standard Hours) × Overtime Rate × Hourly Rate.

Worked Example

A freelance designer works on a client project from 09:15 to 17:45 with a 45-minute lunch break. Their rate is $85/hour. Work Hours = (17:45 − 09:15) − 0:45 = 8:30 − 0:45 = 7.75 hours. Billable Amount = 7.75 × $85 = $658.75. The designer logs this daily and bills the client $658.75 for the day's work — far more accurate than estimating "about 8 hours" and inadvertently under-billing.

Real-World Scenarios

Professional Application

A professional uses time tracking calculator to make an informed decision. By entering accurate data and interpreting the results in context, they identify the optimal approach for their situation. The tool saves 15-30 minutes compared to manual calculation or research, and the accuracy eliminates human error.

Key takeaway: For professional use, always verify inputs against authoritative sources and interpret results in the context of your specific industry and situation.

Personal Use Case

An individual uses time tracking calculator for a personal decision — comparing options, understanding trade-offs, and building confidence in their choice. The structured output removes guesswork and provides a clear basis for action. Even for personal decisions, the tool's accuracy and consistency add significant value over ad-hoc methods.

Key takeaway: For personal decisions, the tool provides a structured framework. Combine the output with your own judgment and preferences for the best outcome.

Educational Context

A student or learner uses time tracking calculator to understand the underlying concepts. By experimenting with different inputs and observing how outputs change, they build intuition for the relationships between variables. This interactive exploration is far more effective than passive reading for developing genuine understanding.

Key takeaway: For learning, experiment with different inputs to build intuition. The tool reveals relationships and patterns that textbook descriptions cannot.

Common Mistakes to Avoid

Using outdated input values

Rates, thresholds, and benchmark data change annually. Always verify inputs against current official sources before relying on results. Using last year's tax brackets or interest rates produces results that look precise but are materially wrong.

Treating estimates as exact predictions

Calculations involving future values (investment growth, loan costs) depend on assumptions that cannot be known with certainty. Treat results as ranges, not point estimates. Run the calculation with multiple assumption values to understand the range of possible outcomes.

Ignoring edge cases and limitations

Every tool has limitations — specific scenarios where the standard formula or logic does not apply. Read the tool's documentation and FAQ to understand edge cases. When in doubt, consult a professional for situations that fall outside normal parameters.

Not verifying inputs before trusting outputs

Garbage in, garbage out. A time tracking calculator is only as accurate as its inputs. Spend 30 seconds confirming your inputs are correct before relying on the output. The calculation is instant; the consequences of wrong inputs can be long-lasting.

Confusing precision with accuracy

A time tracking calculator that displays 8 decimal places is not more accurate than one displaying 2 — it is more precise. Accuracy depends on input quality and methodology correctness. Excessive precision creates false confidence. Report results to a precision that reflects input quality.

Best Practices from Experts

Verify inputs before trusting outputs

Spend 30 seconds confirming your inputs are correct before relying on the time tracking calculator output. The calculation is instant; the consequences of wrong inputs can be long-lasting. Cross-check critical inputs against authoritative sources.

Document your inputs and assumptions

For important calculations, record: what inputs you used, what assumptions you made, when you did it, and what the output was. This creates an audit trail, makes future updates easier, and helps you spot when assumptions have become outdated.

Cross-check critical results

For high-stakes decisions, verify the time tracking calculator result using a different method or tool. If two approaches produce significantly different answers, investigate the discrepancy before proceeding. Most errors are caught by cross-checking.

Consider sensitivity to assumptions

Run the time tracking calculator with several different input values to understand how sensitive the output is to each assumption. If small input changes produce large output changes, the conclusion is fragile and warrants additional research.

Consult a professional for high-stakes decisions

For decisions involving significant money, legal implications, or personal safety, the time tracking calculator is a starting point — not a replacement for professional advice. Use the tool to prepare for conversations with licensed professionals who can provide personalized guidance.

Industry Benchmarks & Reference Data

Freelance business benchmarks:

Average freelance hourly rate (all industries) $20-150/hour (median ~$50/hour)
Freelance developer rates $60-200/hour (junior to senior)
Freelance designer rates $50-150/hour
Billable utilization rate (target) 60-75% of working hours
Effective tax rate ($75k freelancer income) 25-35% (federal + SE tax + state)
Annual business expenses (typical) $5,000-15,000 (software, equipment, services)
Recommended emergency fund 3-6 months of personal expenses
Client acquisition cost (freelancers) $200-2,000 per new client

Sources: Upwork Freelance Forward 2024, Freelancers Union, Contently Rates Survey, BLS. Rates vary by location, niche, and experience.

When to Use This Tool

Freelancers log daily billable hours with precision. Hourly employees verify timesheet accuracy. Agencies bill clients for actual time worked. Contractors track multiple projects per day. Lawyers and accountants track billable time in 6-minute increments. Anyone paid by the hour uses this calculator to ensure accurate compensation.

Related Concepts You Should Know

Self-Employment Tax

The 15.3% tax freelancers pay on net business income, covering both employer and employee portions of Social Security and Medicare.

Quarterly Estimated Tax

Federal tax payments freelancers must make four times per year (April 15, June 15, September 15, January 15) to cover income and SE tax.

Billable Utilization

The percentage of working hours that are billed to clients. Most freelancers achieve 50-70%; the rest is admin, marketing, and sales.

Value-Based Pricing

Pricing based on value delivered to the client rather than time spent. A project saving a client $100k can charge $10k even if it took 20 hours.

Retainer Pricing

A fixed monthly fee for a defined scope of work. Provides predictable income and reduces client acquisition time. Typically 10-15% below project rates.

Pro Tips & Advanced Insights

Always quote project rates, not hourly rates, for fixed-scope work. Project rates capture the value you provide. Hourly rates penalize you for being efficient — the faster you work, the less you earn.

Build a "rate floor" below which you will not accept work. Calculate this as your minimum viable hourly rate using our Hourly Rate Calculator. Decline projects below this floor, even during slow periods.

Set aside 30% of every payment for taxes immediately. Open a separate "tax savings" account. The moment a client payment arrives, transfer 30%. This prevents the most common freelancer financial disaster.

Maintain a 3-6 month emergency fund separate from business reserves. Freelance income is volatile. A 3-6 month personal emergency fund covers living expenses during slow periods.

Increase rates annually, even with existing clients. Cost of living rises 2-4% annually. Your rates should rise at least as fast. Communicate increases 60 days in advance with a value justification.

Frequently Asked Questions

How do I handle overnight shifts?
Simply enter the actual times. If you start at 22:00 and end at 06:00, the calculator automatically detects that end is earlier than start and adds 24 hours. Total = (06:00 + 24:00) − 22:00 = 8 hours. No special handling needed.
Should I bill for breaks?
It depends on the agreement. Short breaks (5–15 minutes) are typically billable. Lunch breaks (30+ minutes) are typically not billed. Discuss expectations with clients upfront. If you bill for breaks, enter 0 in the Break Time field.
How precise should time tracking be?
For most freelance work, 15-minute increments are standard. Legal and consulting work often uses 6-minute (0.1 hour) increments. Track time as you work using a timer rather than estimating at day's end — memory-based estimates consistently undercount time by 10–20%.
Can I track multiple projects in one day?
Yes — run the calculator once per project with that project's start/end times and rate. Sum the billable amounts for your daily total. For complex multi-project tracking with auto-logging, dedicated time tracking software (Toggl, Harvest) may be more efficient.
How accurate is the time tracking calculator?
The calculation itself is 100% accurate — the formulas are mathematically proven. However, accuracy of results depends entirely on the accuracy of your inputs. Always verify input values against authoritative sources before relying on results for important decisions.
Can I use the time tracking calculator for professional/business purposes?
Yes, with appropriate caveats. The tool performs standard calculations used across industries. However, for high-stakes decisions (legal, financial, medical), consult a licensed professional. This tool helps you prepare for those conversations, not replace them.
Does the time tracking calculator work on mobile devices?
Yes. The tool is fully responsive and optimized for mobile use. Touch-friendly inputs, appropriate keyboards (numeric where relevant), and a layout that adapts to any screen size. You get the same functionality on phone, tablet, or desktop.
Is my data safe when using the time tracking calculator?
Yes. All calculations run entirely in your browser using JavaScript. The values you enter never leave your device, are never transmitted to our servers, and are never logged. You can verify this by checking your browser's network tab — no data is sent as you type.
How often should I recalculate using the time tracking calculator?
It depends on the volatility of your inputs. For calculations involving tax rates, market values, or time-sensitive data, recalculate whenever inputs change materially. For stable calculations (math constants, fixed formulas), one-time calculation suffices.
Where can I learn more about the concepts behind the time tracking calculator?
For deeper understanding, consult category-specific resources: IRS publications for tax calculations, Investopedia for finance concepts, Khan Academy for math fundamentals, and academic textbooks for rigorous treatments. Wikipedia articles often provide good overviews with links to primary sources.
How accurate is the time tracking calculator?
The calculation itself is 100% accurate — the formulas are mathematically proven. However, accuracy of results depends entirely on the accuracy of your inputs. Always verify input values against authoritative sources before relying on results for important decisions.
Can I use the time tracking calculator for professional/business purposes?
Yes, with appropriate caveats. The tool performs standard calculations used across industries. However, for high-stakes decisions (legal, financial, medical), consult a licensed professional. This tool helps you prepare for those conversations, not replace them.
Does the time tracking calculator work on mobile devices?
Yes. The tool is fully responsive and optimized for mobile use. Touch-friendly inputs, appropriate keyboards (numeric where relevant), and a layout that adapts to any screen size. You get the same functionality on phone, tablet, or desktop.
Is my data safe when using the time tracking calculator?
Yes. All calculations run entirely in your browser using JavaScript. The values you enter never leave your device, are never transmitted to our servers, and are never logged. You can verify this by checking your browser's network tab.
How often should I recalculate using the time tracking calculator?
It depends on the volatility of your inputs. For calculations involving rates, market values, or time-sensitive data, recalculate whenever inputs change materially. For stable calculations, one-time calculation may suffice.
Where can I learn more about the concepts behind the time tracking calculator?
For deeper understanding, consult category-specific resources: IRS publications for tax calculations, Investopedia for finance concepts, Khan Academy for math fundamentals, and academic textbooks for rigorous treatments. Wikipedia articles often provide good overviews with links to primary sources.

References & Further Reading

Our calculators are built using formulas and data from these authoritative sources. We recommend them for deeper understanding of the concepts behind each tool.

`); w.document.close(); setTimeout(() => w.print(), 500); }); break; case 'hourly-rate-calculator': onChange(() => { const income = val('hr-income'), exp = val('hr-expenses'), tax = val('hr-tax'), hrs = val('hr-hours'), vac = val('hr-vacation'), sick = val('hr-sick'), bill = val('hr-billable'); if ([income, exp, tax, hrs, vac, sick, bill].some(v => isNaN(v))) return renderResult('

Fill in all fields.

'); const taxReserve = income * tax / 100 / (1 - tax / 100); const targetRev = income + exp + taxReserve; const workWeeks = 52 - vac - sick / 5; const billableHrs = workWeeks * hrs * bill / 100; if (billableHrs <= 0) return renderResult('

Invalid billable hours.

'); const rate = targetRev / billableHrs; renderResult(` ${resultCard('Target Hourly Rate', fmtCurrency(rate), 'Minimum rate to hit your income goal', 'violet')} ${resultCard('Annual Revenue Target', fmtCurrency(targetRev, 0), 'Income + expenses + tax reserve')} ${resultCard('Annual Billable Hours', fmtNum(billableHrs, 0), `${workWeeks.toFixed(1)} work weeks × ${hrs}h × ${bill}% billable`)} `); }); break; case 'salary-to-hourly-calculator': onChange(() => { const s = val('sh-salary'), h = val('sh-hours'), w = val('sh-weeks'); if (isNaN(s) || isNaN(h) || isNaN(w) || h === 0 || w === 0) return renderResult('

Fill in all fields.

'); const weekly = s / w; const daily = weekly / 5; const hourly = weekly / h; const monthly = s / 12; renderResult(` ${resultCard('Hourly Rate', fmtCurrency(hourly), `Based on ${h} hours/week, ${w} weeks/year`, 'cyan')} ${resultCard('Daily Rate', fmtCurrency(daily), 'Assuming 5-day work week')} ${resultCard('Weekly Pay', fmtCurrency(weekly, 0))} ${resultCard('Monthly Pay', fmtCurrency(monthly, 0))} `); }); break; case 'discount-calculator': onChange(() => { const p = val('dc-price'), d = val('dc-discount'); if (isNaN(p) || isNaN(d)) return renderResult('

Enter original price and discount %.

'); const savings = p * d / 100; const final = p - savings; renderResult(` ${resultCard('Final Price', fmtCurrency(final), 'Price after discount', 'fuchsia')} ${resultCard('You Save', fmtCurrency(savings), `${fmtPct(d, 0)} off original price`, 'emerald')} `); }); break; case 'compound-interest-calculator': onChange(() => { const p = val('ci-principal') || 0, m = val('ci-monthly') || 0, r = val('ci-rate'), yrs = val('ci-years'); if (isNaN(r) || isNaN(yrs)) return renderResult('

Enter rate and years.

'); const n = 12; const rate = r / 100 / n; const periods = n * yrs; const fvPrincipal = p * Math.pow(1 + rate, periods); const fvContrib = rate === 0 ? m * periods : m * ((Math.pow(1 + rate, periods) - 1) / rate); const fv = fvPrincipal + fvContrib; const totalContrib = p + m * periods; const interest = fv - totalContrib; renderResult(` ${resultCard('Future Value', fmtCurrency(fv, 0), `After ${yrs} years at ${r}% annual return`, 'green')} ${resultCard('Total Contributions', fmtCurrency(totalContrib, 0), 'What you put in')} ${resultCard('Interest Earned', fmtCurrency(interest, 0), `${((interest / fv) * 100).toFixed(1)}% of final value`, 'emerald')} `); }); break; case 'loan-calculator': onChange(() => { const p = val('ln-amount'), r = val('ln-rate'), yrs = val('ln-years'); if (isNaN(p) || isNaN(r) || isNaN(yrs)) return renderResult('

Enter all values.

'); const mr = r / 100 / 12; const n = yrs * 12; const mp = mr === 0 ? p / n : p * (mr * Math.pow(1 + mr, n)) / (Math.pow(1 + mr, n) - 1); const total = mp * n; const interest = total - p; renderResult(` ${resultCard('Monthly Payment', fmtCurrency(mp), 'Fixed monthly payment', 'orange')} ${resultCard('Total Interest', fmtCurrency(interest, 0), 'Interest paid over the life of the loan', 'red')} ${resultCard('Total Cost', fmtCurrency(total, 0), 'Principal + interest')} `); }); break; case 'mortgage-calculator': onChange(() => { const price = val('mt-price'), down = val('mt-down'), r = val('mt-rate'), yrs = val('mt-years'), tax = val('mt-tax'), ins = val('mt-insurance'), pmi = val('mt-pmi'); if (isNaN(price) || isNaN(down) || isNaN(r) || isNaN(yrs)) return renderResult('

Enter home price, down payment, rate, and term.

'); const loan = price - down; const mr = r / 100 / 12; const n = yrs * 12; const pi = mr === 0 ? loan / n : loan * (mr * Math.pow(1 + mr, n)) / (Math.pow(1 + mr, n) - 1); const monthlyTax = (price * (tax || 0) / 100) / 12; const monthlyIns = (ins || 0) / 12; const monthlyPmi = (down / price < 0.2) ? (loan * (pmi || 0) / 100) / 12 : 0; const total = pi + monthlyTax + monthlyIns + monthlyPmi; renderResult(` ${resultCard('Monthly Payment', fmtCurrency(total), 'P&I + taxes + insurance + PMI', 'blue')}
Principal & Interest${fmtCurrency(pi)}
Property Tax${fmtCurrency(monthlyTax)}
Insurance${fmtCurrency(monthlyIns)}
${monthlyPmi > 0 ? `
PMI${fmtCurrency(monthlyPmi)}
` : ''}
Loan amount: ${fmtCurrency(loan, 0)} • Down payment: ${fmtPct((down / price) * 100, 1)}
`); }); break; case 'tax-calculator': onChange(() => { const income = val('tx-income'), status = $('tx-status').value; if (isNaN(income)) return renderResult('

Enter your taxable income.

'); const brackets2024 = { single: [[0, 11600, 0.10], [11600, 47150, 0.12], [47150, 100525, 0.22], [100525, 191950, 0.24], [191950, 243725, 0.32], [243725, 609350, 0.35], [609350, Infinity, 0.37]], married: [[0, 23200, 0.10], [23200, 94300, 0.12], [94300, 201050, 0.22], [201050, 383900, 0.24], [383900, 487450, 0.32], [487450, 731200, 0.35], [731200, Infinity, 0.37]], head: [[0, 16550, 0.10], [16550, 63100, 0.12], [63100, 100500, 0.22], [100500, 191950, 0.24], [191950, 243700, 0.32], [243700, 609350, 0.35], [609350, Infinity, 0.37]], }; const brackets = brackets2024[status]; let tax = 0; for (const [lo, hi, rate] of brackets) { if (income > lo) tax += (Math.min(income, hi) - lo) * rate; } const eff = (tax / income) * 100; const takehome = income - tax; renderResult(` ${resultCard('Federal Tax', fmtCurrency(tax, 0), 'Estimated federal income tax', 'red')} ${resultCard('Effective Rate', fmtPct(eff), 'Average tax rate (total ÷ income)')} ${resultCard('Take-Home', fmtCurrency(takehome, 0), 'After federal tax (before state, FICA)', 'emerald')}
Federal only. Does not include state tax, self-employment tax (15.3% for freelancers), or FICA. For accurate planning, consult a CPA.
`); }); break; case 'sales-tax-calculator': onChange(() => { const amt = val('st-amount'), rate = val('st-rate'), mode = $('st-mode').value; if (isNaN(amt) || isNaN(rate)) return renderResult('

Enter amount and tax rate.

'); let preTax, tax, total; if (mode === 'add') { preTax = amt; tax = amt * rate / 100; total = amt + tax; } else { total = amt; preTax = amt / (1 + rate / 100); tax = amt - preTax; } renderResult(` ${resultCard(mode === 'add' ? 'Total (with tax)' : 'Pre-Tax Amount', fmtCurrency(total), '', 'teal')} ${resultCard('Tax Amount', fmtCurrency(tax), `${fmtPct(rate, 2)} rate`)} ${resultCard(mode === 'add' ? 'Pre-Tax Amount' : 'Total (with tax)', fmtCurrency(preTax))} `); }); break; case 'currency-converter': onChange(() => { const amt = val('cc-amount'), from = parseFloat($('cc-from').value), to = parseFloat($('cc-to').value); if (isNaN(amt)) return renderResult('

Enter an amount.

'); const usd = amt / from; const result = usd * to; const fromTxt = $('cc-from').options[$('cc-from').selectedIndex].text.split(' — ')[0]; const toTxt = $('cc-to').options[$('cc-to').selectedIndex].text.split(' — ')[0]; renderResult(` ${resultCard(`${amt.toLocaleString()} ${fromTxt} =`, `${result.toLocaleString('en-US', { maximumFractionDigits: 2 })} ${toTxt}`, 'Approximate conversion (mid-market rate)', 'lime')}
Rate: 1 ${fromTxt} ≈ ${(to / from).toFixed(4)} ${toTxt}. Rates are approximate and updated periodically. Banks and payment processors typically add a 1–3% spread.
`); }); break; case 'time-tracking-calculator': onChange(() => { const start = $('tt-start').value, end = $('tt-end').value, brk = val('tt-break') || 0, rate = val('tt-rate'); if (!start || !end) return renderResult('

Enter start and end times.

'); const toMin = (t) => { const [h, m] = t.split(':').map(Number); return h * 60 + m; }; let mins = toMin(end) - toMin(start); if (mins < 0) mins += 24 * 60; // overnight mins -= brk; if (mins < 0) mins = 0; const hrs = mins / 60; const pay = isNaN(rate) ? 0 : hrs * rate; renderResult(` ${resultCard('Total Hours', fmtNum(hrs, 2), `${mins} minutes worked`, 'purple')} ${resultCard('Billable Amount', fmtCurrency(pay), isNaN(rate) ? 'Enter hourly rate to compute' : `${fmtCurrency(rate, 2)} × ${hrs.toFixed(2)}h`, 'emerald')} `); }); break; case 'freelance-tax-calculator': onChange(() => { const inc = val('ft-income'), exp = val('ft-expenses'), status = $('ft-status').value; if (isNaN(inc) || isNaN(exp)) return renderResult('

Enter income and expenses.

'); const net = inc - exp; if (net <= 0) return renderResult('

Expenses exceed income.

'); const seIncome = net * 0.9235; const seTax = seIncome * 0.153; const seDeduction = seTax / 2; const brackets2024 = status === 'married' ? [[0,23200,0.10],[23200,94300,0.12],[94300,201050,0.22],[201050,383900,0.24],[383900,487450,0.32],[487450,731200,0.35],[731200,Infinity,0.37]] : [[0,11600,0.10],[11600,47150,0.12],[47150,100525,0.22],[100525,191950,0.24],[191950,243725,0.32],[243725,609350,0.35],[609350,Infinity,0.37]]; const stdDed = status === 'married' ? 29200 : 14600; const taxableInc = Math.max(0, net - seDeduction - stdDed); let incomeTax = 0; for (const [lo, hi, r] of brackets2024) { if (taxableInc > lo) incomeTax += (Math.min(taxableInc, hi) - lo) * r; } const total = seTax + incomeTax; const quarterly = total / 4; renderResult(` ${resultCard('Self-Employment Tax', fmtCurrency(seTax, 0), '15.3% on net business income', 'pink')} ${resultCard('Federal Income Tax', fmtCurrency(incomeTax, 0), 'After standard deduction and SE deduction')} ${resultCard('Total Federal Tax', fmtCurrency(total, 0), `${fmtPct((total / inc) * 100, 1)} of gross income`, 'red')} ${resultCard('Quarterly Payment', fmtCurrency(quarterly, 0), 'Estimated payment due each quarter', 'amber')}
Estimate only — does not include state tax, credits, or itemized deductions. Consult a CPA for filing.
`); }); break; case 'project-quote-generator': onChange(() => { const rate = val('pq-rate'), scopeText = $('pq-scope').value, expenses = val('pq-expenses') || 0, cont = val('pq-contingency') || 0; if (isNaN(rate) || !scopeText.trim()) return renderResult('

Enter hourly rate and at least one scope item.

'); let labor = 0; const items = []; scopeText.split('\n').forEach(line => { const parts = line.split(',').map(s => s.trim()); if (parts.length >= 2) { const hrs = parseFloat(parts[1]); if (!isNaN(hrs)) { labor += hrs * rate; items.push({ desc: parts[0], hrs, cost: hrs * rate }); } } }); const subtotal = labor + expenses; const contAmt = subtotal * cont / 100; const total = subtotal + contAmt; renderResult(`
${items.map(i => `
${i.desc} (${i.hrs}h)${fmtCurrency(i.cost)}
`).join('')}
${resultCard('Subtotal', fmtCurrency(subtotal), `${fmtCurrency(labor)} labor + ${fmtCurrency(expenses)} expenses`)} ${resultCard('Contingency', fmtCurrency(contAmt), `${fmtPct(cont, 0)} buffer for scope changes`, 'amber')} ${resultCard('Total Quote', fmtCurrency(total), 'Recommended project price', 'indigo')} `); }); break; case 'email-signature-generator': $('es-copy')?.addEventListener('click', async () => { const sig = buildSignature(); try { await navigator.clipboard.writeText(sig); const b = $('es-copy'); const orig = b.innerHTML; b.innerHTML = '✓ Copied!'; setTimeout(() => b.innerHTML = orig, 1500); } catch (e) { alert('Could not copy. Please select and copy manually.'); } }); function buildSignature() { const name = $('es-name')?.value || ''; const title = $('es-title')?.value || ''; const company = $('es-company')?.value || ''; const phone = $('es-phone')?.value || ''; const email = $('es-email')?.value || ''; const website = $('es-website')?.value || ''; const linkedin = $('es-linkedin')?.value || ''; const cta = $('es-cta')?.value || ''; const lines = []; if (name) lines.push(`
${name}
`); if (title || company) lines.push(`
${[title, company].filter(Boolean).join(' | ')}
`); lines.push('
'); if (phone) lines.push(`
📞 ${phone}
`); if (email) lines.push(`
✉️ ${email}
`); if (website) lines.push(`
🌐 ${website}
`); if (linkedin) lines.push(`
in: ${linkedin}
`); if (cta) lines.push(`
${cta}
`); return `
${lines.join('')}
`; } function updateES() { renderResult(`
Preview (click "Copy Signature" to copy):
${buildSignature()}
`); } onChange(updateES); break; case 'meta-tag-generator': $('mt-copy')?.addEventListener('click', async () => { const title = $('mt-title')?.value || ''; const desc = $('mt-desc')?.value || ''; const url = $('mt-url')?.value || ''; const image = $('mt-image')?.value || ''; const site = $('mt-site')?.value || ''; const author = $('mt-author')?.value || ''; let tags = `${title}\n\n\n`; if (author) tags += `\n`; tags += `\n\n\n`; if (site) tags += `\n`; tags += `\n\n\n`; if (image) tags += `\n`; tags += `\n\n\n\n\n`; if (image) tags += `\n`; try { await navigator.clipboard.writeText(tags); const b = $('mt-copy'); const orig = b.innerHTML; b.innerHTML = '✓ Copied!'; setTimeout(() => b.innerHTML = orig, 1500); renderResult(`
${tags.replace(/`); } catch (e) { alert('Copy failed'); } }); break; case 'password-generator': const pwLen = $('pw-length'); pwLen?.addEventListener('input', () => { $('pw-len-label').textContent = pwLen.value; }); function genPassword() { const len = parseInt(pwLen.value); let chars = ''; if ($('pw-upper').checked) chars += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; if ($('pw-lower').checked) chars += 'abcdefghijklmnopqrstuvwxyz'; if ($('pw-numbers').checked) chars += '0123456789'; if ($('pw-symbols').checked) chars += '!@#$%^&*()_+-=[]{}|;:,.<>?'; if ($('pw-exclude').checked) chars = chars.replace(/[l1IO0]/g, ''); if (!chars) return renderResult('

Select at least one character type.

'); const arr = new Uint32Array(len); crypto.getRandomValues(arr); let pw = ''; for (let i = 0; i < len; i++) pw += chars[arr[i] % chars.length]; const entropy = len * Math.log2(chars.length); renderResult(`
Generated Password
${pw}
${resultCard('Entropy', `${entropy.toFixed(0)} bits`, entropy >= 100 ? 'Excellent — uncrackable' : entropy >= 60 ? 'Strong' : 'Increase length', entropy >= 100 ? 'emerald' : 'amber')}
Length: ${len} • Character set: ${chars.length} chars • Generated locally with crypto.getRandomValues()
`); } $('pw-generate')?.addEventListener('click', genPassword); $('pw-copy')?.addEventListener('click', async () => { const txt = document.querySelector('#tool-result .font-mono')?.textContent || ''; try { await navigator.clipboard.writeText(txt); alert('Password copied!'); } catch (e) {} }); genPassword(); break; case 'qr-code-generator': function updateQRInputs() { const type = $('qr-type').value; const wrap = $('qr-inputs'); const inputs = { url: `
`, text: `
`, email: `
`, phone: `
`, sms: `
`, wifi: `
`, }; wrap.innerHTML = inputs[type] || inputs.url; } $('qr-type')?.addEventListener('change', updateQRInputs); updateQRInputs(); $('qr-generate')?.addEventListener('click', () => { const type = $('qr-type').value; let data = ''; if (type === 'wifi') { const ssid = $('qr-ssid')?.value || ''; const pass = $('qr-pass')?.value || ''; data = `WIFI:T:WPA;S:${ssid};P:${pass};;`; } else if (type === 'email') { data = `mailto:${$('qr-content')?.value || ''}`; } else if (type === 'phone') { data = `tel:${$('qr-content')?.value || ''}`; } else if (type === 'sms') { data = `sms:${$('qr-content')?.value || ''}`; } else { data = $('qr-content')?.value || ''; } if (!data) return renderResult('

Enter content first.

'); const size = parseInt($('qr-size').value) || 256; const fg = $('qr-fg').value.replace('#', ''); const url = `https://api.qrserver.com/v1/create-qr-code/?size=${size}x${size}&color=${fg}&bgcolor=ffffff&data=${encodeURIComponent(data)}`; renderResult(`
QR Code
QR codes are static and never expire. Generated via qrserver API.
`); }); break; case 'og-image-generator': let ogCanvas, ogCtx; function drawOG() { const title = $('og-title')?.value || 'Your Title Here'; const subtitle = $('og-subtitle')?.value || ''; const bg = $('og-bg').value; const gradients = { indigo: ['#0d9488', '#BE05C1'], emerald: ['#10b981', '#0d9488'], amber: ['#f59e0b', '#f43f5e'], slate: ['#1e293b', '#0f172a'], }; const [c1, c2] = gradients[bg]; const canvas = document.createElement('canvas'); canvas.width = 1200; canvas.height = 630; const ctx = canvas.getContext('2d'); const grad = ctx.createLinearGradient(0, 0, 1200, 630); grad.addColorStop(0, c1); grad.addColorStop(1, c2); ctx.fillStyle = grad; ctx.fillRect(0, 0, 1200, 630); // Decorative circles ctx.globalAlpha = 0.1; ctx.fillStyle = '#fffbeb'; ctx.beginPath(); ctx.arc(1050, 100, 200, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(100, 550, 150, 0, Math.PI * 2); ctx.fill(); ctx.globalAlpha = 1; // Title ctx.fillStyle = '#fffbeb'; ctx.font = 'bold 72px Inter, Arial, sans-serif'; ctx.textAlign = 'left'; // Word wrap const words = title.split(' '); let line = '', y = 280; const maxWidth = 1000; for (let i = 0; i < words.length; i++) { const test = line + words[i] + ' '; if (ctx.measureText(test).width > maxWidth && i > 0) { ctx.fillText(line, 100, y); line = words[i] + ' '; y += 90; } else line = test; } ctx.fillText(line, 100, y); // Subtitle if (subtitle) { ctx.font = '500 36px Inter, Arial'; ctx.fillStyle = 'rgba(255,255,255,0.85)'; ctx.fillText(subtitle, 100, 530); } // Brand bar ctx.fillStyle = '#fffbeb'; ctx.fillRect(100, 200, 60, 6); ogCanvas = canvas; const dataUrl = canvas.toDataURL('image/png'); renderResult(`
OG Image Preview Download PNG (1200×630)
`); } $('og-generate')?.addEventListener('click', drawOG); $('og-download')?.addEventListener('click', () => { if (!ogCanvas) drawOG(); const a = document.createElement('a'); a.href = ogCanvas.toDataURL('image/png'); a.download = 'og-image.png'; a.click(); }); break; case 'invoice-number-generator': let inCounter = parseInt(localStorage.getItem('in_counter') || '0'); function genInv() { const prefix = $('in-prefix')?.value || ''; const suffix = $('in-suffix')?.value || ''; const start = parseInt($('in-start')?.value || '1'); const pad = parseInt($('in-pad')?.value || '3'); const num = start + inCounter; const numStr = String(num).padStart(pad, '0'); const result = `${prefix}${numStr}${suffix}`; renderResult(` ${resultCard('Invoice Number', result, 'Copy and use on your next invoice', 'amber')}
Counter: ${inCounter} • Stored in this browser only.
`); } $('in-generate')?.addEventListener('click', () => { inCounter = 0; genInv(); }); $('in-next')?.addEventListener('click', () => { inCounter++; localStorage.setItem('in_counter', inCounter); genInv(); }); $('in-reset')?.addEventListener('click', () => { inCounter = 0; localStorage.setItem('in_counter', 0); genInv(); }); genInv(); break; case 'social-media-bio-generator': $('bio-generate')?.addEventListener('click', () => { const platform = $('bio-platform').value; const prof = $('bio-profession').value || ''; const value = $('bio-value').value || ''; const personality = $('bio-personality').value || ''; const cta = $('bio-cta').value || ''; const limits = { instagram: 150, twitter: 160, linkedin: 220, tiktok: 80 }; const emojis = { instagram: '📸 ', twitter: '', linkedin: '', tiktok: '' }; const variants = [ `${emojis[platform]}${prof}${value ? ' | ' + value : ''}${personality ? ' | ' + personality : ''}${cta ? ' | ' + cta : ''}`, `${prof}${value ? ' — ' + value : ''}${cta ? ' ⬇️ ' + cta : ''}`, `${prof} 📍 ${personality || 'Worldwide'}${value ? ' | ' + value : ''} | ${cta || 'DM for info'}`, `${value || prof}. ${personality}. ${cta || 'Let\'s connect.'}`, ]; const limit = limits[platform]; const html = variants.map((v, i) => { const trimmed = v.substring(0, limit); return `
Variant ${i + 1} (${trimmed.length}/${limit})
${trimmed}
`; }).join(''); renderResult(html); document.querySelectorAll('.bio-copy').forEach(b => b.addEventListener('click', async () => { await navigator.clipboard.writeText(b.dataset.text); b.textContent = '✓ Copied!'; setTimeout(() => b.textContent = 'Copy', 1500); })); }); break; case 'hashtag-generator': $('ht-generate')?.addEventListener('click', () => { const niche = $('ht-niche').value.trim().toLowerCase(); const platform = $('ht-platform').value; if (!niche) return renderResult('

Enter a niche or topic.

'); const words = niche.split(/[\s,]+/).filter(w => w.length > 2); const baseTag = words.map(w => w.replace(/[^a-z0-9]/g, '')).join(''); const high = ['success', 'motivation', 'entrepreneur', 'business', 'goals', 'inspiration', 'lifestyle', 'growth']; const med = [baseTag, baseTag + 'community', baseTag + 'tips', baseTag + 'life', baseTag + 'daily', 'online' + baseTag, baseTag + 'coach']; const niche_tags = [baseTag + 'expert', baseTag + 'specialist', baseTag + 'community', 'virtual' + baseTag, baseTag + 'forbeginners']; const count = platform === 'instagram' ? 20 : platform === 'tiktok' ? 5 : 3; const all = [...high, ...med, ...niche_tags].slice(0, count).map(t => '#' + t.replace(/\s/g, '')); const html = `
${all.length} hashtags for ${platform}:
${all.join(' ')}
`; renderResult(html); $('ht-copy').classList.remove('hidden'); $('ht-copy').onclick = async () => { await navigator.clipboard.writeText(all.join(' ')); $('ht-copy').textContent = '✓ Copied!'; setTimeout(() => $('ht-copy').textContent = 'Copy All', 1500); }; }); break; case 'cta-generator': $('cta-generate')?.addEventListener('click', () => { const offer = $('cta-offer').value.trim(); const benefit = $('cta-benefit').value.trim(); const style = $('cta-style').value; if (!offer) return renderResult('

Enter an offer.

'); const templates = { action: [`Get ${offer}`, `Start ${offer}`, `Try ${offer}`, `Claim ${offer}`, `Grab ${offer}`], benefit: [`Get ${offer} — ${benefit}`, `Start ${offer} to ${benefit}`, `${benefit} with ${offer}`, `${offer}: ${benefit}`], urgency: [`Claim ${offer} before it's gone`, `Limited: ${offer}`, `Get ${offer} today`, `Don't miss ${offer}`], curiosity: [`See what ${offer} can do`, `Discover ${offer}`, `Find out why thousands chose ${offer}`, `What ${offer} really feels like`], }; let pool = style === 'mix' ? Object.values(templates).flat() : templates[style]; const picks = pool.sort(() => Math.random() - 0.5).slice(0, 6); const html = picks.map((c, i) => `
${c}
`).join(''); renderResult(html); document.querySelectorAll('.cta-copy').forEach(b => b.addEventListener('click', async () => { await navigator.clipboard.writeText(b.dataset.text); b.textContent = '✓'; setTimeout(() => b.textContent = 'Copy', 1200); })); }); break; case 'business-name-generator': $('bn-generate')?.addEventListener('click', () => { const kws = $('bn-keywords').value.split(',').map(s => s.trim()).filter(s => s); const style = $('bn-style').value; if (kws.length === 0) return renderResult('

Enter at least one keyword.

'); const suffixes = ['Labs', 'Studio', 'Hub', 'Pro', 'Now', 'Co', 'HQ', 'Works', 'Lab', 'Spot']; const prefixes = ['Get', 'My', 'Go', 'Try', 'Use', 'The']; const names = new Set(); kws.forEach(k => { const cap = k.charAt(0).toUpperCase() + k.slice(1); if (style === 'compound' || style === 'mix') { suffixes.forEach(s => names.add(cap + s)); if (kws.length > 1) kws.forEach(k2 => { if (k !== k2) { const c2 = k2.charAt(0).toUpperCase() + k2.slice(1); names.add(cap + c2); names.add(c2 + cap); } }); } if (style === 'alliterative' || style === 'mix') { const letter = k.charAt(0).toUpperCase(); ['Apex', 'Alpha', 'Arc', 'Aero', 'Beacon', 'Bold', 'Bright', 'Cosmic', 'Clever', 'Dream', 'Dynamic', 'Echo', 'Epic', 'Flux', 'Forge', 'Glow', 'Hyper', 'Iron', 'Lumen', 'Nova', 'Optic', 'Prime', 'Pulse', 'Quantum', 'Rapid', 'Stellar', 'Swift', 'Tetra', 'Ultra', 'Vivid', 'Zen'].forEach(p => { if (p.charAt(0) === letter) names.add(p + cap); }); } if (style === 'descriptive' || style === 'mix') { prefixes.forEach(p => names.add(p + cap)); names.add(cap + 'App'); names.add(cap + 'Tool'); names.add(cap + 'HQ'); } if (style === 'abstract' || style === 'mix') { const vowels = 'aeiou'; const end = k.slice(-1); if (!vowels.includes(end)) names.add(k + 'ly'); names.add(k + 'io'); names.add(k + 'ify'); names.add(k + 'ster'); } }); const arr = Array.from(names).sort(() => Math.random() - 0.5).slice(0, 18); const html = arr.map(n => `
${n}
`).join(''); renderResult(`
${html}
Check domain availability and trademark status before committing to any name.
`); document.querySelectorAll('.bn-copy').forEach(b => b.addEventListener('click', async () => { await navigator.clipboard.writeText(b.dataset.text); b.textContent = '✓'; setTimeout(() => b.textContent = 'Copy', 1200); })); }); break; case 'pomodoro-timer': let pdInterval = null, pdSeconds = 25 * 60, pdMode = 'work', pdCount = parseInt(localStorage.getItem('pd_count') || '0'); $('pd-count').textContent = pdCount; function pdUpdateDisplay() { const m = Math.floor(pdSeconds / 60), s = pdSeconds % 60; $('pd-display').textContent = `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; const colors = { work: 'amber ', short: 'text-teal-bright', long: 'text-amber' }; $('pd-display').className = `text-6xl font-bold font-mono tabular ${colors[pdMode]} my-4`; $('pd-status').textContent = pdMode === 'work' ? 'Focus time — eliminate distractions' : `${pdMode === 'long' ? 'Long' : 'Short'} break — step away from screen`; } function pdTick() { if (pdSeconds > 0) { pdSeconds--; pdUpdateDisplay(); } else { clearInterval(pdInterval); pdInterval = null; if (pdMode === 'work') { pdCount++; localStorage.setItem('pd_count', pdCount); $('pd-count').textContent = pdCount; pdMode = pdCount % 4 === 0 ? 'long' : 'short'; pdSeconds = (pdMode === 'long' ? parseInt($('pd-long').value) : parseInt($('pd-short').value)) * 60; } else { pdMode = 'work'; pdSeconds = parseInt($('pd-work').value) * 60; } pdUpdateDisplay(); alert('Time\'s up! Switching to ' + (pdMode === 'work' ? 'work' : 'break') + ' session.'); } } $('pd-start')?.addEventListener('click', () => { if (pdInterval) return; if (pdMode === 'work' && pdSeconds === parseInt($('pd-work').value) * 60) {} // already set pdInterval = setInterval(pdTick, 1000); }); $('pd-pause')?.addEventListener('click', () => { if (pdInterval) { clearInterval(pdInterval); pdInterval = null; } }); $('pd-reset')?.addEventListener('click', () => { if (pdInterval) { clearInterval(pdInterval); pdInterval = null; } pdMode = 'work'; pdSeconds = parseInt($('pd-work').value) * 60; pdUpdateDisplay(); }); ['pd-work', 'pd-short', 'pd-long'].forEach(id => $(id)?.addEventListener('change', () => { if (!pdInterval) { pdSeconds = (pdMode === 'work' ? parseInt($('pd-work').value) : pdMode === 'short' ? parseInt($('pd-short').value) : parseInt($('pd-long').value)) * 60; pdUpdateDisplay(); } })); pdUpdateDisplay(); break; case 'age-calculator': onChange(() => { const dob = $('ag-dob')?.value, asof = $('ag-asof')?.value; if (!dob) return renderResult('

Enter date of birth.

'); const birth = new Date(dob); const target = asof ? new Date(asof) : new Date(); if (birth > target) return renderResult('

Date of birth is after target date.

'); let years = target.getFullYear() - birth.getFullYear(); let months = target.getMonth() - birth.getMonth(); let days = target.getDate() - birth.getDate(); if (days < 0) { months--; days += new Date(target.getFullYear(), target.getMonth(), 0).getDate(); } if (months < 0) { years--; months += 12; } const totalDays = Math.floor((target - birth) / 86400000); const totalHours = totalDays * 24; const nextBday = new Date(target.getFullYear(), birth.getMonth(), birth.getDate()); if (nextBday < target) nextBday.setFullYear(nextBday.getFullYear() + 1); const daysToBday = Math.ceil((nextBday - target) / 86400000); renderResult(` ${resultCard('Exact Age', `${years}y ${months}m ${days}d`, 'Years, months, days', 'amber')} ${resultCard('Total Days Lived', fmtNum(totalDays, 0), `${fmtNum(totalHours, 0)} hours`, 'blue')} ${resultCard('Next Birthday', `${daysToBday} days`, 'Until your next birthday', 'rose')} `); }); break; case 'date-difference-calculator': onChange(() => { const s = $('dd-start')?.value, e = $('dd-end')?.value; if (!s || !e) return renderResult('

Select both dates.

'); const d1 = new Date(s), d2 = new Date(e); const days = Math.abs(Math.floor((d2 - d1) / 86400000)); const weeks = Math.floor(days / 7); const weekdays = $('dd-weekdays').checked ? countWeekdays(d1, d2) : null; // months/years approximate let earlier = d1 < d2 ? d1 : d2, later = d1 < d2 ? d2 : d1; let years = later.getFullYear() - earlier.getFullYear(); let months = later.getMonth() - earlier.getMonth(); if (later.getDate() < earlier.getDate()) months--; if (months < 0) { years--; months += 12; } renderResult(` ${resultCard('Total Days', fmtNum(days, 0), `${fmtNum(weeks, 1)} weeks`, 'blue')} ${weekdays !== null ? resultCard('Business Days', fmtNum(weekdays, 0), 'Excluding weekends', 'teal') : ''} ${resultCard('Years / Months', `${years}y ${months}m`, 'Approximate calendar duration')} `); }); function countWeekdays(d1, d2) { let count = 0; const cur = new Date(Math.min(d1, d2)); const end = new Date(Math.max(d1, d2)); while (cur <= end) { const day = cur.getDay(); if (day !== 0 && day !== 6) count++; cur.setDate(cur.getDate() + 1); } return count; } break; case 'countdown-timer': let cdInterval = null; function cdTick() { const target = new Date($('cd-target').value).getTime(); if (isNaN(target)) return; const now = Date.now(); const diff = target - now; if (diff <= 0) { clearInterval(cdInterval); cdInterval = null; $('cd-days').textContent = '0'; $('cd-hours').textContent = '0'; $('cd-mins').textContent = '0'; $('cd-secs').textContent = '0'; $('cd-status').textContent = $('cd-name').value ? `${$('cd-name').value} has arrived!` : 'Countdown complete!'; return; } const d = Math.floor(diff / 86400000); const h = Math.floor((diff % 86400000) / 3600000); const m = Math.floor((diff % 3600000) / 60000); const s = Math.floor((diff % 60000) / 1000); $('cd-days').textContent = d; $('cd-hours').textContent = h; $('cd-mins').textContent = m; $('cd-secs').textContent = s; $('cd-status').textContent = $('cd-name').value ? `Counting down to ${$('cd-name').value}` : 'Counting down...'; } $('cd-start')?.addEventListener('click', () => { if (!$('cd-target').value) return alert('Please select a target date and time.'); if (cdInterval) clearInterval(cdInterval); cdTick(); cdInterval = setInterval(cdTick, 1000); }); break; case 'time-zone-converter': function tzConvert() { const from = $('tz-from').value, to = $('tz-to').value, time = $('tz-time').value; if (!time) return renderResult('

Enter source date and time.

'); // Parse as if in source zone const localDate = new Date(time); // Get source time in UTC by formatting in source zone and re-parsing const fromStr = localDate.toLocaleString('en-US', { timeZone: from }); const toStr = localDate.toLocaleString('en-US', { timeZone: to, year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', weekday: 'long' }); const fromStrFmt = localDate.toLocaleString('en-US', { timeZone: from, year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', weekday: 'long' }); renderResult(` ${resultCard('Converted Time', toStr, `${from} → ${to}`, 'teal')}
Source (${from}):
${fromStrFmt}
`); } onChange(tzConvert); $('tz-swap')?.addEventListener('click', () => { const f = $('tz-from').value, t = $('tz-to').value; $('tz-from').value = t; $('tz-to').value = f; tzConvert(); }); break; case 'percentage-calculator': function pcUpdateLabels() { const mode = $('pc-mode').value; const labels = { of: ['X (percentage)', 'Y (whole number)'], is: ['X (part)', 'Y (whole)'], change: ['Original value (X)', 'New value (Y)'], }; $('pc-label-a').textContent = labels[mode][0]; $('pc-label-b').textContent = labels[mode][1]; } $('pc-mode')?.addEventListener('change', pcUpdateLabels); pcUpdateLabels(); onChange(() => { const a = val('pc-a'), b = val('pc-b'), mode = $('pc-mode').value; if (isNaN(a) || isNaN(b)) return renderResult('

Enter both values.

'); let result, label, formula; if (mode === 'of') { result = (a / 100) * b; label = `${a}% of ${b}`; formula = `(${a} ÷ 100) × ${b} = ${result}`; } else if (mode === 'is') { if (b === 0) return renderResult('

Cannot divide by zero.

'); result = (a / b) * 100; label = `${a} is what % of ${b}`; formula = `(${a} ÷ ${b}) × 100 = ${result.toFixed(4)}%`; renderResult(`${resultCard('Result', fmtPct(result), label)}
${formula}
`); return; } else { if (a === 0) return renderResult('

Original value cannot be zero.

'); result = ((b - a) / a) * 100; label = `% change from ${a} to ${b}`; formula = `((${b} − ${a}) ÷ ${a}) × 100 = ${result.toFixed(4)}%`; renderResult(`${resultCard('% Change', fmtPct(result), result >= 0 ? 'Increase' : 'Decrease', result >= 0 ? 'emerald' : 'red')}
${formula}
`); return; } renderResult(`${resultCard('Result', fmtNum(result, 4), label)}
${formula}
`); }); break; case 'average-calculator': onChange(() => { const text = $('av-input').value; const nums = text.split(/[\s,\n]+/).map(s => parseFloat(s)).filter(n => !isNaN(n)); if (nums.length === 0) return renderResult('

Enter numbers separated by commas, spaces, or new lines.

'); const sum = nums.reduce((a, b) => a + b, 0); const mean = sum / nums.length; const sorted = [...nums].sort((a, b) => a - b); const median = sorted.length % 2 === 0 ? (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2 : sorted[Math.floor(sorted.length / 2)]; const counts = {}; let maxCount = 0; let modes = []; nums.forEach(n => { counts[n] = (counts[n] || 0) + 1; if (counts[n] > maxCount) { maxCount = counts[n]; modes = [n]; } else if (counts[n] === maxCount) modes.push(n); }); const mode = maxCount === 1 ? 'No mode' : modes.join(', '); const range = Math.max(...nums) - Math.min(...nums); renderResult(` ${resultCard('Mean (Average)', fmtNum(mean), 'Sum ÷ count', 'emerald')} ${resultCard('Median', fmtNum(median), 'Middle value when sorted')} ${resultCard('Mode', mode.toString(), maxCount > 1 ? `Appears ${maxCount} times` : 'All values are unique')} ${resultCard('Range', fmtNum(range), `Max (${fmtNum(Math.max(...nums))}) − Min (${fmtNum(Math.min(...nums))})`)}
${resultCard('Sum', fmtNum(sum, 4))} ${resultCard('Count', nums.length.toString())}
`); }); break; case 'bmi-calculator': function bmUpdateLabels() { const u = $('bm-units').value; $('bm-weight-label').textContent = u === 'metric' ? 'Weight (kg)' : 'Weight (lbs)'; $('bm-height-label').textContent = u === 'metric' ? 'Height (cm)' : 'Height (inches)'; } $('bm-units')?.addEventListener('change', bmUpdateLabels); bmUpdateLabels(); onChange(() => { const w = val('bm-weight'), h = val('bm-height'), u = $('bm-units').value; if (isNaN(w) || isNaN(h) || h === 0) return renderResult('

Enter weight and height.

'); let bmi; if (u === 'metric') bmi = w / Math.pow(h / 100, 2); else bmi = 703 * w / Math.pow(h, 2); let cat, color; if (bmi < 18.5) { cat = 'Underweight'; color = 'blue'; } else if (bmi < 25) { cat = 'Normal weight'; color = 'emerald'; } else if (bmi < 30) { cat = 'Overweight'; color = 'amber'; } else { cat = 'Obese'; color = 'red'; } // healthy range const hM = u === 'metric' ? h / 100 : h * 0.0254; const loKg = 18.5 * hM * hM, hiKg = 24.9 * hM * hM; const lo = u === 'metric' ? `${loKg.toFixed(1)} kg` : `${(loKg * 2.20462).toFixed(1)} lbs`; const hi = u === 'metric' ? `${hiKg.toFixed(1)} kg` : `${(hiKg * 2.20462).toFixed(1)} lbs`; renderResult(` ${resultCard('Your BMI', fmtNum(bmi, 1), cat, color)}
Healthy weight range for your height:
${lo} – ${hi}
Under
<18.5
Normal
18.5–24.9
Over
25–29.9
Obese
≥30
`); }); break; case 'calorie-calculator': onChange(() => { const age = val('cl-age'), gender = $('cl-gender').value, weight = val('cl-weight'), height = val('cl-height'), activity = parseFloat($('cl-activity').value); if ([age, weight, height].some(v => isNaN(v))) return renderResult('

Fill in all fields.

'); let bmr; if (gender === 'male') bmr = 10 * weight + 6.25 * height - 5 * age + 5; else bmr = 10 * weight + 6.25 * height - 5 * age - 161; const tdee = bmr * activity; renderResult(` ${resultCard('BMR (at rest)', fmtCurrency(Math.round(bmr), 0).replace('$', ''), 'Calories if you slept all day', 'orange')} ${resultCard('TDEE (maintenance)', fmtCurrency(Math.round(tdee), 0).replace('$', ''), 'Calories to maintain weight', 'green')}
${resultCard('Weight Loss', fmtCurrency(Math.round(tdee - 500), 0).replace('$', ''), '~1 lb/week deficit', 'red')} ${resultCard('Muscle Gain', fmtCurrency(Math.round(tdee + 300), 0).replace('$', ''), '~0.5 lb/week surplus', 'emerald')}
Estimates use Mifflin-St Jeor equation. For accurate planning, track intake for 2 weeks and adjust based on actual results. Women should not eat below 1,200 cal/day; men not below 1,500.
`); }); break; case 'unit-converter': const ucData = { length: { base: 'm', units: { m: 1, km: 1000, cm: 0.01, mm: 0.001, mi: 1609.344, yd: 0.9144, ft: 0.3048, in: 0.0254 } }, weight: { base: 'kg', units: { kg: 1, g: 0.001, mg: 0.000001, lb: 0.45359237, oz: 0.028349523, t: 1000 } }, volume: { base: 'l', units: { l: 1, ml: 0.001, 'm³': 1000, gal: 3.785411784, qt: 0.946352946, pt: 0.473176473, cup: 0.2365882365, 'fl oz': 0.0295735296 } }, area: { base: 'm²', units: { 'm²': 1, 'km²': 1000000, 'cm²': 0.0001, 'mi²': 2589988.110336, 'ft²': 0.09290304, 'yd²': 0.83612736, 'acre': 4046.8564224, 'hectare': 10000 } }, speed: { base: 'm/s', units: { 'm/s': 1, 'km/h': 0.277778, 'mph': 0.44704, 'knot': 0.514444, 'ft/s': 0.3048 } }, }; function ucUpdateUnits() { const cat = $('uc-category').value; if (cat === 'temperature') { $('uc-from').innerHTML = ''; $('uc-to').innerHTML = ''; return; } const units = Object.keys(ucData[cat].units); $('uc-from').innerHTML = units.map(u => ``).join(''); $('uc-to').innerHTML = units.map((u, i) => ``).join(''); } $('uc-category')?.addEventListener('change', ucUpdateUnits); ucUpdateUnits(); onChange(() => { const cat = $('uc-category').value, v = val('uc-value'), from = $('uc-from').value, to = $('uc-to').value; if (isNaN(v)) return renderResult('

Enter a value.

'); let result; if (cat === 'temperature') { let c; if (from === 'C') c = v; else if (from === 'F') c = (v - 32) * 5 / 9; else c = v - 273.15; if (to === 'C') result = c; else if (to === 'F') result = c * 9 / 5 + 32; else result = c + 273.15; } else { const base = v * ucData[cat].units[from]; result = base / ucData[cat].units[to]; } renderResult(` ${resultCard('Result', `${fmtNum(result, 6)} ${to}`, `${v} ${from} = ?`, 'violet')}
${v} ${from} = ${fmtNum(result, 6)} ${to}
`); }); break; case 'random-number-generator': $('rn-generate')?.addEventListener('click', () => { const min = parseInt($('rn-min').value), max = parseInt($('rn-max').value), count = Math.min(parseInt($('rn-count').value), 100), unique = $('rn-unique').checked; if (isNaN(min) || isNaN(max)) return renderResult('

Enter min and max.

'); if (min > max) return renderResult('

Min must be less than max.

'); const range = max - min + 1; if (unique && count > range) return renderResult(`

Cannot generate ${count} unique numbers from a range of ${range}.

`); let nums = []; if (unique) { const pool = Array.from({ length: range }, (_, i) => min + i); for (let i = pool.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [pool[i], pool[j]] = [pool[j], pool[i]]; } nums = pool.slice(0, count); } else { for (let i = 0; i < count; i++) nums.push(Math.floor(Math.random() * range) + min); } renderResult(` ${resultCard(count === 1 ? 'Random Number' : 'Random Numbers', nums.join(', '), `Range: ${min}–${max}${unique ? ' • unique' : ''}`, 'slate')} `); }); break; case 'word-counter': onChange(() => { const text = $('wc-input').value; const chars = text.length; const charsNoSpace = text.replace(/\s/g, '').length; const words = text.trim() ? text.trim().split(/\s+/).length : 0; const sentences = text.trim() ? (text.match(/[.!?]+/g) || []).length || 1 : 0; const paragraphs = text.trim() ? text.split(/\n\s*\n/).filter(p => p.trim()).length : 0; const readMin = Math.max(1, Math.ceil(words / 200)); renderResult(`
${resultCard('Words', words.toString(), '', 'rose')} ${resultCard('Characters', chars.toString(), `${charsNoSpace} without spaces`)} ${resultCard('Sentences', sentences.toString())} ${resultCard('Paragraphs', paragraphs.toString())}
${resultCard('Reading Time', `${readMin} min`, 'At 200 words per minute', 'amber')} `); }); break; case 'case-converter': function ccConvert(text, type) { const words = text.split(/[\s_\-]+/).filter(w => w); switch (type) { case 'upper': return text.toUpperCase(); case 'lower': return text.toLowerCase(); case 'title': return text.toLowerCase().replace(/\b\w/g, c => c.toUpperCase()); case 'sentence': return text.toLowerCase().replace(/(^\s*\w|[.!?]\s*\w)/g, c => c.toUpperCase()); case 'camel': return words.map((w, i) => i === 0 ? w.toLowerCase() : w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(''); case 'pascal': return words.map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(''); case 'snake': return words.map(w => w.toLowerCase()).join('_'); case 'kebab': return words.map(w => w.toLowerCase()).join('-'); case 'constant': return words.map(w => w.toUpperCase()).join('_'); } return text; } document.querySelectorAll('.cc-btn').forEach(b => b.addEventListener('click', () => { const text = $('cc-input').value; if (!text) return renderResult('

Enter some text first.

'); const converted = ccConvert(text, b.dataset.case); renderResult(` ${resultCard(b.textContent.trim(), converted, `Applied ${b.dataset.case} case`, 'cyan')}
${converted}
`); $('cc-copy').onclick = async () => { await navigator.clipboard.writeText(converted); $('cc-copy').textContent = '✓ Copied!'; setTimeout(() => $('cc-copy').textContent = 'Copy Output', 1500); }; })); break; case 'lorem-ipsum-generator': const liWords = 'lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua enim ad minim veniam quis nostrud exercitation ullamco laboris nisi aliquip ex ea commodo consequat duis aute irure in reprehenderit voluptate velit esse cillum eu fugiat nulla pariatur excepteur sint occaecat cupidatat non proident sunt culpa qui officia deserunt mollit anim id est laborum'.split(' '); function liSentence() { const len = 8 + Math.floor(Math.random() * 12); const words = []; for (let i = 0; i < len; i++) words.push(liWords[Math.floor(Math.random() * liWords.length)]); words[0] = words[0].charAt(0).toUpperCase() + words[0].slice(1); return words.join(' ') + '.'; } function liParagraph() { const sentences = 3 + Math.floor(Math.random() * 4); return Array.from({ length: sentences }, liSentence).join(' '); } $('li-generate')?.addEventListener('click', () => { const unit = $('li-unit').value, count = parseInt($('li-count').value) || 5, classic = $('li-classic').checked; let result = ''; if (unit === 'paragraphs') { const paras = Array.from({ length: count }, liParagraph); if (classic) paras[0] = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' + paras[0]; result = paras.join('\n\n'); } else if (unit === 'sentences') { result = Array.from({ length: count }, liSentence).join(' '); if (classic) result = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' + result; } else { result = Array.from({ length: count }, () => liWords[Math.floor(Math.random() * liWords.length)]).join(' '); if (classic) result = 'lorem ipsum dolor sit amet consectetur adipiscing elit ' + result; } renderResult(`
${result}
`); $('li-copy').onclick = async () => { await navigator.clipboard.writeText(result); $('li-copy').textContent = '✓ Copied!'; setTimeout(() => $('li-copy').textContent = 'Copy Text', 1500); }; }); break; case 'text-repeater': $('tr-generate')?.addEventListener('click', () => { const text = $('tr-text').value; const count = Math.min(parseInt($('tr-count').value) || 1, 10000); const sep = $('tr-sep').value === '\\n' ? '\n' : $('tr-sep').value === '\\t' ? '\t' : $('tr-sep').value; if (!text) return renderResult('

Enter text to repeat.

'); const result = Array(count).fill(text).join(sep); renderResult(` ${resultCard('Repeated Text', `${count} times`, `Length: ${result.length} chars`, 'fuchsia')}
${result}
`); $('tr-copy').onclick = async () => { await navigator.clipboard.writeText(result); $('tr-copy').textContent = '✓ Copied!'; setTimeout(() => $('tr-copy').textContent = 'Copy Result', 1500); }; }); break; case 'slug-generator': function sgGenerate() { const text = $('sg-input').value; const sep = $('sg-sep').value; const removeStop = $('sg-stopwords').checked; if (!text) return renderResult('

Enter text to slugify.

'); let slug = text.toLowerCase().trim() .normalize('NFD').replace(/[\u0300-\u036f]/g, '') // strip accents .replace(/[^a-z0-9\s-]/g, '') // remove special chars .replace(/[\s_]+/g, sep) // spaces/underscores → sep .replace(new RegExp(`\\${sep}+`, 'g'), sep) // collapse multiple seps .replace(new RegExp(`^\\${sep}|\\${sep}$`, 'g'), ''); // trim seps if (removeStop) { const stop = ['a', 'an', 'the', 'of', 'in', 'on', 'at', 'to', 'for', 'and', 'or', 'but']; slug = slug.split(sep).filter(w => !stop.includes(w)).join(sep); } renderResult(` ${resultCard('URL Slug', slug, `${slug.length} characters`, 'indigo')}
/${slug}
Use as the URL path after your domain (e.g., example.com/${slug}).
`); $('sg-copy').onclick = async () => { await navigator.clipboard.writeText(slug); $('sg-copy').textContent = '✓ Copied!'; setTimeout(() => $('sg-copy').textContent = 'Copy Slug', 1500); }; } onChange(sgGenerate); break; default: renderResult('

Calculator for this tool is loading. Refer to the formula and examples below.

'); } })();