Finance

Salary to Hourly Calculator

Convert any annual salary to hourly, daily, weekly, and monthly rates.

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 Salary to Hourly Calculator

When a job offer arrives with an annual salary number, it is hard to compare against hourly contract work, freelance rates, or even other salaried positions with different hours. Our Salary to Hourly Calculator instantly converts any annual salary into equivalent hourly, daily, weekly, and monthly pay rates, accounting for the hours you actually work per week and the weeks you actually work per year. This is essential for freelancers comparing contract rates to full-time offers, for employees evaluating whether their salary is competitive, and for anyone negotiating a pay rate. The calculator reveals that $100,000/year at 60 hours/week is very different from $100,000/year at 35 hours/week — a distinction the headline number hides.

Deep Dive: Understanding the Concept

Salary to Hourly Calculator is one of those financial concepts that appears deceptively simple but contains layers of nuance that separate successful businesses from failed ones. At its core, the calculation involves straightforward arithmetic — but the inputs, assumptions, and interpretations require careful thought. Financial professionals spend years mastering not just the math, but the judgment calls around what to include, what to exclude, and how to interpret results in specific business contexts.

The history of salary to hourly calculator calculation reveals how financial thinking has evolved. What began as simple bookkeeping has become sophisticated financial modeling, with different methodologies for different industries, jurisdictions, and use cases. Understanding the principles behind the calculation matters more than memorizing formulas — principles transfer across contexts; rote formulas break when circumstances change.

A common mistake among non-financial business owners is treating this calculation as a one-time exercise rather than an ongoing analytical practice. In reality, the inputs change constantly — costs fluctuate, market conditions shift, regulations evolve, personal circumstances change. The number you calculate today may be dramatically different in six months. Regular recalculation is not optional; it is essential for sound decision-making.

Professional financial advisors and CPAs typically charge $200–$500 per hour for guidance on calculations like these. The reason is not that the math is difficult — it is that the judgment calls around what to include, how to interpret results, and what actions to take require expertise that takes years to develop. This tool democratizes access to the calculation itself, but for high-stakes decisions, professional advice remains worth the investment.

How to Use This Calculator

  1. 1

    Enter your Annual Salary in dollars.

  2. 2

    Enter the Hours per Week you actually work (use real numbers, including overtime, not just contracted hours).

  3. 3

    Enter the Weeks per Year you actually work (subtract vacation, holidays, and unpaid breaks).

  4. 4

    The calculator shows equivalent hourly, daily, weekly, and monthly rates instantly.

The Formula Explained

Weekly Pay = Annual Salary ÷ Weeks Worked per Year. Daily Pay = Weekly Pay ÷ Days Worked per Week. Hourly Pay = Weekly Pay ÷ Hours Worked per Week. Monthly Pay = Annual Salary ÷ 12. The standard assumption is 40 hours/week and 52 weeks/year (2,080 hours), but you can adjust these to reflect real working patterns — part-time schedules, contract roles with unpaid weeks, or salaried positions with significant overtime.

Worked Example

A marketing manager earns $85,000/year. They work 45 hours per week (not the contracted 40) and work 48 weeks per year (4 weeks of combined vacation and holidays). Hourly Rate = $85,000 ÷ (45 × 48) = $85,000 ÷ 2,160 = $39.35/hour. If they had worked the assumed 40 hours and 52 weeks (2,080 hours), the calculation would have shown $40.87/hour — making the salary look better than the actual hourly value of their time. The difference between assumed and real hours is why this calculator matters.

Real-World Scenarios

Professional Use Case

A marketing manager uses the salary to hourly calculator to optimize a quarterly campaign. By inputting real business data, they identify the optimal configuration that improves results by 15–25% compared to previous approaches. The 30-minute investment in proper calculation pays dividends throughout the quarter.

Key takeaway: For professional applications, the time invested in proper calculation typically returns 5–10x in improved outcomes.

Educational Context

A student learning about finance concepts uses this tool to verify homework, explore what-if scenarios, and build intuition for how inputs affect outputs. Interactive calculation builds deeper understanding than static textbook examples.

Key takeaway: For learning, calculators that show intermediate steps and allow rapid iteration build intuition faster than manual calculation methods.

Personal Decision

An individual uses the salary to hourly calculator for a significant personal decision — comparing options, understanding trade-offs, and building confidence in their choice. The structured calculation removes emotion and reveals the mathematically optimal path.

Key takeaway: For personal decisions, structured calculation surfaces options that emotional reasoning might miss. Even when you "trust your gut," running the numbers first improves decision quality.

Common Mistakes to Avoid

Using outdated assumption values

Financial calculations are only as good as their inputs. Tax rates, interest rates, contribution limits, and cost figures change annually. Using last year's numbers produces results that look precise but are materially wrong. Always verify inputs against current official sources.

Ignoring regional and jurisdictional variations

Federal figures (tax brackets, contribution limits) differ from state figures, which differ from local figures. A calculation that works for California may be wrong for Texas or New York. Always specify the jurisdiction and verify against local rules.

Treating calculated estimates as exact predictions

Calculations involving future values (investment growth, loan costs, retirement projections) depend on assumptions about rates, inflation, and time horizons that cannot be known with certainty. Treat results as ranges, not point estimates. Add sensitivity analysis for critical assumptions.

Overlooking tax implications

Many financial calculations have tax consequences that significantly affect outcomes. Investment gains, loan interest deductions, retirement contributions, and business expenses all interact with tax rules. A pre-tax calculation may look very different from an after-tax reality.

Failing to revisit calculations when circumstances change

Life events — marriage, job changes, relocations, business growth, market crashes — invalidate previous calculations. Build a habit of recalculating whenever significant changes occur, not just on an annual schedule.

Best Practices from Experts

Document all assumptions used in the calculation

Record the date, source, and specific values used for each input. This creates an audit trail, makes future updates easier, and helps you spot when assumptions have become outdated.

Cross-check results against multiple methods

For important calculations, verify the result using a different method or tool. If two approaches produce significantly different answers, investigate the discrepancy before relying on either result.

Consider both best-case and worst-case scenarios

Run the calculation with optimistic, base case, and pessimistic assumptions. The range of outcomes tells you more than any single point estimate. Plan for the worst case; hope for the best.

Consult a professional for high-stakes decisions

For decisions involving more than $10,000 or significant life impact, the cost of professional advice (typically $200–$500) is trivial compared to the cost of mistakes. Use this tool to prepare for those conversations, not replace them.

Recalculate annually at minimum

Set a calendar reminder to review and update calculations yearly. Tax brackets, contribution limits, and benchmark rates change annually. Outdated calculations produce confidently wrong results.

Industry Benchmarks & Reference Data

The following reference data represents typical values used in financial calculations. Always verify against current official sources before relying on these figures for important decisions.

Historical stock market return (S&P 500) 7–10% annualized (4–6% after inflation)
Historical bond market return 4–6% annualized
Long-term inflation rate (US) 2–3% annually (Federal Reserve target)
Mortgage interest rates (30-year fixed, 2024) 6.0–7.5% (varies by credit score and market)
Personal loan interest rates 6–36% depending on credit
Credit card interest rates 15–25% average
Federal income tax brackets (2024) 10%, 12%, 22%, 24%, 32%, 35%, 37% (progressive)
Standard deduction (2024 single filer) $14,600
401(k) contribution limit (2024) $23,000 ($30,500 if 50+)
IRA contribution limit (2024) $7,000 ($8,000 if 50+)

Sources: IRS.gov, Federal Reserve Economic Data (FRED), Bureau of Labor Statistics, S&P Dow Jones Indices. Figures current as of 2024; verify before use.

When to Use This Tool

Employees use this to evaluate whether a salary offer beats their current hourly contractor rate. Freelancers use it to compare contract rates against "what a full-time role would pay." Managers use it to understand the real cost of overtime. Career changers use it to set income goals for self-employment. Anyone considering a pay cut for "work-life balance" uses it to quantify exactly how much each freed hour is worth.

Related Concepts You Should Know

Compound Growth

The mathematical principle that returns earned on previous returns produce exponential growth over time. The foundation of long-term investing and the reason starting early matters more than investing large amounts.

Time Value of Money

The principle that money available now is worth more than the same amount in the future due to its earning capacity. Foundation of discounted cash flow analysis and present value calculations.

Risk-Adjusted Return

Return on investment adjusted for the risk taken to achieve it. A 10% return from a low-risk bond is more valuable than 10% from a speculative startup investment. Sharpe ratio is a common measure.

Opportunity Cost

The return you forgo by choosing one investment over another. If you invest $10,000 in a 5% bond instead of a 7% stock, your opportunity cost is 2% per year.

Liquidity

How easily an asset can be converted to cash without losing value. Cash is fully liquid; real estate is illiquid (takes months to sell); stocks are semi-liquid (days to settle).

Pro Tips & Advanced Insights

When calculating future values, use real (inflation-adjusted) rates rather than nominal rates. A 7% nominal return with 3% inflation is only 4% real return — and 4% is the number that matters for purchasing power.

Build sensitivity analysis into your calculations: run scenarios with returns 2% higher and 2% lower than your base assumption. The range of outcomes tells you more than any single estimate.

Document your assumptions and revisit them annually. Tax brackets, contribution limits, and benchmark rates change yearly. Calculations using outdated inputs produce confidently wrong results.

For tax-related calculations, always use marginal tax rate (your top bracket) for additional income decisions and effective tax rate (total tax ÷ total income) for overall burden analysis. Mixing these produces errors.

When comparing investment options, calculate after-tax returns, not pre-tax. Tax-advantaged accounts (401k, IRA, Roth) can significantly outperform taxable accounts even with identical pre-tax returns.

Frequently Asked Questions

Should I use 40 hours or my actual hours?
For comparison to contract rates, use your actual hours including overtime — this gives a true hourly value. For official pay comparisons and HR purposes, use your contracted hours. The calculator lets you set both so you can see how unpaid overtime quietly reduces your effective hourly rate.
How many weeks should I use?
The standard is 52, but most salaried employees receive 2–4 weeks of paid time off. For honest hourly comparison, subtract unpaid time off (sabbaticals, unpaid parental leave). For paid time off, you can keep 52 because the salary covers those weeks. For contractors considering full-time roles, subtract typical PTO to compare apples to apples.
Does this account for benefits?
No — this calculator only converts cash salary. Benefits like health insurance, retirement matching, stock options, and bonuses can add 20–40% to total compensation. When comparing a salary to a contract rate, add the value of benefits to the salary, then recalculate.
Why is my hourly rate lower than freelancers I know?
Salaried employees receive benefits, paid time off, and stability that contractors must self-fund. A $50/hour employee with benefits is roughly equivalent to a $75–$85/hour contractor once you account for self-employment tax, health insurance, retirement contributions, and unpaid time off.
How accurate is the salary to hourly 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 salary to hourly 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 salary to hourly 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 salary to hourly 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 salary to hourly 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 salary to hourly 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.

'); } })();