Text

Word Counter

Count words, characters, sentences, and reading time for any text.

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 Word Counter

Word count matters in many contexts — academic essays with strict limits, social media posts with character caps, blog posts targeting SEO lengths, email subject lines, meta descriptions, ad copy. Manually counting words is tedious and error-prone. Our Word Counter handles all text metrics instantly: words, characters (with and without spaces), sentences, paragraphs, and estimated reading time. Paste or type your text and watch the metrics update in real time. The tool is essential for writers, students, marketers, and anyone who works with text length constraints.

Deep Dive: Understanding the Concept

Word Counter is a tool designed to address count words, characters, sentences, and reading time for any text. Understanding how this tool works — not just the calculation or generation it performs, but the underlying concepts, common pitfalls, and best practices — helps you use it effectively and avoid costly mistakes. This page provides comprehensive guidance on the tool's purpose, methodology, and practical application.

The context behind word counter matters because the inputs and interpretations vary by use case. What constitutes a "good" result depends on your specific situation — industry standards, personal goals, regulatory requirements, and risk tolerance all affect how you should interpret the output. This tool provides the calculation; you provide the judgment.

Common mistakes when using word counter include: using outdated assumptions, ignoring edge cases, and treating calculated or generated output as definitive rather than approximate. The tool is most valuable when you understand its limitations and complement it with professional advice for high-stakes decisions. Use this tool to inform your decisions, not replace critical thinking.

The methodology behind word counter follows established standards and conventions in its field. The formulas, algorithms, or generation logic have been verified against authoritative sources. However, results are only as accurate as the inputs — always verify your inputs before relying on outputs for important decisions. For professional, legal, medical, or financial matters, consult a licensed expert.

How to Use This Calculator

  1. 1

    Type or paste your text into the input area.

  2. 2

    The counter instantly shows: word count, character count (with and without spaces), sentences, paragraphs, and reading time.

  3. 3

    Use the metrics to verify your text meets specific requirements (e.g., 500-word essay, 280-character tweet, 160-character meta description).

The Formula Explained

Words = number of whitespace-separated tokens. Characters = string.length. Characters (no spaces) = string with whitespace removed, then length. Sentences = count of sentences using punctuation markers (.!?). Paragraphs = count of double-newline-separated blocks. Reading time = word count ÷ 200 words per minute (average adult reading speed).

Worked Example

A freelancer writes a meta description for a client website. They type it in and check: characters = 158 (within the 150–160 optimal range), words = 24, reading time = 7 seconds. The freelancer trims one word to hit exactly 155 characters. Without the counter, they would have guessed at length — and likely been wrong, hurting SEO performance.

Real-World Scenarios

Professional Application

A professional uses word counter 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 word counter 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 word counter 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 word counter 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 word counter 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 word counter 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 word counter 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 word counter 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 word counter 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

Text and content benchmarks:

Tweet character limit 280 characters (140 for some languages)
Meta description (SEO optimal) 150-160 characters
Title tag (SEO optimal) 50-60 characters
Blog post length (SEO) 1,500-2,500 words for competitive keywords
Reading speed (average adult) 200-300 words per minute
Speaking speed (average presenter) 130-150 words per minute
Email subject line (optimal) 30-50 characters (mobile-friendly)
Text message (optimal response rate) Under 100 characters for marketing

Sources: Google Search Central guidelines, Mailchimp email benchmarks, Backlinko SEO studies, Journal of Educational Psychology.

When to Use This Tool

Writers hit word count targets. Students meet essay length requirements. Marketers optimize meta descriptions and ad copy. SEO specialists target content lengths. Social media managers respect character limits. Editors estimate reading time. Anyone working with text benefits from accurate counts.

Related Concepts You Should Know

Character Encoding

The system that maps characters to numeric codes. UTF-8 is the modern standard, supporting all languages and emojis.

Unicode

The international standard for text representation, covering 150,000+ characters across all major writing systems plus emojis and symbols.

Regular Expressions (Regex)

A pattern-matching language for finding, replacing, and extracting text. Powerful but cryptic. Example: ^[A-Z][a-z]+$ matches capitalized words.

Markdown

A lightweight markup language using plain text formatting syntax. Converts to HTML. Used for documentation, README files, blog posts.

Readability Formulas

Algorithms that estimate reading difficulty: Flesch-Kincaid, Gunning Fog, SMOG. Most target 6th-8th grade reading level for general audiences.

Pro Tips & Advanced Insights

For SEO content, write for humans first, search engines second. Modern Google algorithms reward genuinely useful content over keyword-stuffed content. A 1,500-word article that thoroughly answers a question beats a 3,000-word article padded with keywords.

When writing for mobile-first audiences, use shorter paragraphs (2-3 sentences), shorter sentences (15-20 words), and more white space. Mobile readers skim; structure content with clear headers and bullet points.

For email subject lines, "useful" beats "clever." Subject lines that clearly state the email's value outperform clever/vague subject lines by 2-3x in open rates.

When converting text between cases for code, prefer snake_case for Python/database, camelCase for JavaScript variables, PascalCase for JavaScript classes, kebab-case for URLs and CSS classes.

For content that will be translated, write short sentences (under 20 words), avoid idioms and cultural references, use active voice, and minimize wordplay. Translation costs 2-3x more for idiomatic content.

Frequently Asked Questions

How is reading time calculated?
We use 200 words per minute, which is the average adult silent reading speed. Some sources use 250 wpm (skimming) or 150 wpm (technical content). For academic or technical writing, your actual reading time may be 1.5–2x longer. For novels and casual content, 200 wpm is a good estimate.
Does the counter handle different languages?
Yes. The word count works for any language that uses whitespace between words (English, Spanish, French, German, etc.). For languages without word spacing (Chinese, Japanese, Thai), the "word count" may not be meaningful — use character count instead.
How are hyphenated words counted?
Hyphenated words (e.g., "well-being") are counted as one word. Words separated by slashes (e.g., "and/or") are counted as one word. Contractions ("don't") are one word. Numbers and symbols surrounded by spaces are counted as words.
What is the optimal blog post length for SEO?
Studies suggest 1,500–2,500 words tends to perform well in search rankings for competitive keywords, though quality matters more than length. Top-ranking content for many queries is 2,000+ words. Pillar pages may be 3,000–5,000+ words. Always prioritize reader value over arbitrary length targets.
How accurate is the word counter?
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 word counter 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 word counter 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 word counter?
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 word counter?
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 word counter?
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 word counter?
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 word counter 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 word counter 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 word counter?
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 word counter?
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 word counter?
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.

'); } })();