ToolHive: Single-File SPA Toolkit
ToolHive: Single-File SPA Toolkit
How to Use
1. Copy the Code: Copy the entire block of code below.
2. Save the File: Paste the code into a new text file and save it as [Link].
3. Open in Browser: Double-click the saved [Link] file to open it in your web browser
(like Chrome, Firefox, or Edge).
The website will be fully functional right away. Below the code, I've also included a simple
"Download" button for your convenience.
<ul>
<li><a href="#home">Home</a></li>
<li><a href="#tools">All Tools</a></li>
<li><a href="#blog">Blog</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
</nav>
</div>
</div>
</header>
<main id="app">
</main>
<footer>
<div class="container">
<p>© 2025 ToolHive. Created with
JavaScript.</p>
❤️ and pure
</div>
</footer>
<script
src="[Link]
js"></script>
<script
src="[Link]
js"></script>
<script
src="[Link]
[Link]"></script>
<script>
[Link]('DOMContentLoaded', () => {
// --- DATA ---
// Centralized data for all tools. This makes it easy to add
new tools.
const toolsData = [
{ id: 'word-counter', name: 'Word Counter', description:
'Count words, characters, and estimate reading time.', icon: '<svg
xmlns="[Link] fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round"
stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0
112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5
4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0
0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"
/></svg>' },
{ id: 'case-converter', name: 'Case Converter',
description: 'Convert text to uppercase, lowercase, title case, etc.',
icon: '<svg xmlns="[Link] fill="none" viewBox="0
0 24 24" stroke-width="1.5" stroke="currentColor"><path
stroke-linecap="round" stroke-linejoin="round" d="M3 4.5h14.25M3
9h9.75M3 13.5h9.75m4.5-4.5v12m0 0l-3.75-3.75M17.25 21L21 17.25"
/></svg>' },
{ id: 'password-generator', name: 'Password Generator',
description: 'Create strong, secure passwords and check strength.',
icon: '<svg xmlns="[Link] fill="none" viewBox="0
0 24 24" stroke-width="1.5" stroke="currentColor"><path
stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0
013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5
17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.4
99c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z" /></svg>' },
{ id: 'char-counter', name: 'Character Counter',
description: 'Count characters with and without spaces.', icon: '<svg
xmlns="[Link] fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round"
stroke-linejoin="round" d="M7.5 3.75H6A2.25 2.25 0 003.75 6v1.5M16.5
3.75H18A2.25 2.25 0 0120.25 6v1.5m0 9V18A2.25 2.25 0 0118
20.25h-1.5m-9 0H6A2.25 2.25 0 013.75 18v-1.5M15 12a3 3 0 11-6 0 3 3 0
016 0z" /></svg>' },
{ id: 'space-remover', name: 'Extra Space Remover',
description: 'Remove leading, trailing, and duplicate spaces.', icon:
'<svg xmlns="[Link] fill="none" viewBox="0 0 24
24" stroke-width="1.5" stroke="currentColor"><path
stroke-linecap="round" stroke-linejoin="round" d="M16.023
9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181
3.183a8.25 8.25 0 0011.667 0l3.181-3.183m-4.991-2.691v-2.985a5.25 5.25
0 00-10.5 0v2.985" /></svg>' },
{ id: 'name-generator', name: 'Random Name Generator',
description: 'Generate random first and last names.', icon: '<svg
xmlns="[Link] fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round"
stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0
017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112
21.75c-2.676 0-5.216-.584-7.499-1.632z" /></svg>' },
{ id: 'roman-converter', name: 'Roman Numeral Converter',
description: 'Convert numbers to Roman numerals and vice versa.',
icon: '<svg xmlns="[Link] fill="none" viewBox="0
0 24 24" stroke-width="1.5" stroke="currentColor"><path
stroke-linecap="round" stroke-linejoin="round" d="M8.25 7.5h7.5m-7.5
3H12m-3.75 3h7.5M3 12h18M3 12a9 9 0 1118 0 9 9 0 01-18 0z" /></svg>'
},
{ id: 'hash-generator', name: 'Hash Generator',
description: 'Generate MD5 and SHA-256 hashes from text.', icon: '<svg
xmlns="[Link] fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round"
stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75
11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0
00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25
2.25z" /></svg>' },
{ id: 'number-to-words', name: 'Number to Words',
description: 'Convert numerical digits into written words.', icon:
'<svg xmlns="[Link] fill="none" viewBox="0 0 24
24" stroke-width="1.5" stroke="currentColor"><path
stroke-linecap="round" stroke-linejoin="round" d="M19.5
14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5
7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5
3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504
1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0
00-9-9z" /></svg>' },
{ id: 'text-reverser', name: 'Text Reverser', description:
'Reverse any text, string, or sentence easily.', icon: '<svg
xmlns="[Link] fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round"
stroke-linejoin="round" d="M9 15L3 9m0 0l6-6M3 9h12a6 6 0 010 12h-3"
/></svg>' },
{ id: 'text-to-pdf', name: 'Text to PDF', description:
'Convert plain text into a downloadable PDF document.', icon: '<svg
xmlns="[Link] fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round"
stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0
00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0
00-3.375-3.375H8.25m.165 13.5h5.832m-5.832 0l-1.423-1.423A2.25 2.25 0
016.34 15.632V6.342a2.25 2.25 0 011.423-2.036l1.423-1.423m10.5
11.25l-1.423-1.423a2.25 2.25 0 00-2.036-1.423H10.5M4.5 19.5L3
18m16.5-16.5L21 3" /></svg>' },
{ id: 'image-compressor', name: 'Image Compressor',
description: 'Compress JPEG/PNG images locally in your browser.',
icon: '<svg xmlns="[Link] fill="none" viewBox="0
0 24 24" stroke-width="1.5" stroke="currentColor"><path
stroke-linecap="round" stroke-linejoin="round" d="M6.827 6.175A2.31
2.31 0 015.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507
2.25 9.574V18a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75
18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 00-1.134-.175
2.31 2.31 0 01-1.64-1.055l-.822-1.316a2.192 2.192 0 00-1.736-1.039
48.774 48.774 0 00-5.232 0 2.192 2.192 0 00-1.736 1.039l-.821 1.316z"
/><path stroke-linecap="round" stroke-linejoin="round" d="M16.5
12.75a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0zM18.75
10.5h.008v.008h-.008v-.008z" /></svg>' },
{ id: 'qr-code-generator', name: 'QR Code Generator',
description: 'Generate a QR code from any text or URL.', icon: '<svg
xmlns="[Link] fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round"
stroke-linejoin="round" d="M3.75 4.5A.75.75 0 014.5 3.75h4.5a.75.75 0
010 1.5h-3v3a.75.75 0 01-1.5 0v-4.5zM3.75 19.5v-4.5a.75.75 0 011.5
0v3h3a.75.75 0 010 1.5h-4.5a.75.75 0 01-.75-.75zM19.5 3.75h-4.5a.75.75
0 000 1.5h3v3a.75.75 0 001.5 0v-4.5a.75.75 0 00-.75-.75zM19.5
19.5a.75.75 0 01-.75.75h-3a.75.75 0 010-1.5h3v-3a.75.75 0 011.5
0v4.5z" /><path stroke-linecap="round" stroke-linejoin="round" d="M9
12h6" /></svg>' },
];
const blogPosts = {
'sample-post': {
title: 'The Importance of a Strong Password',
content: `
<p>In today's digital world, your password is
often the only thing standing between your sensitive personal
information and a malicious actor. A weak password is like leaving
your front door unlocked. This post will explore why strong passwords
matter and how you can create them.</p>
<h2>What Makes a Password "Strong"?</h2>
<p>A strong password typically has a combination
of the following characteristics:</p>
<ul>
<li><strong>Length:</strong> Longer is better.
Aim for at least 12-16 characters.</li>
<li><strong>Complexity:</strong> It includes a
mix of uppercase letters, lowercase letters, numbers, and symbols
(e.g., !, @, #, $).</li>
<li><strong>Unpredictability:</strong> It
avoids common words, phrases, and easily guessable information like
your birthday or pet's name.</li>
</ul>
<h2>Why You Shouldn't Reuse Passwords</h2>
<p>Data breaches are unfortunately common. When a
service you use is breached, hackers often leak lists of usernames and
passwords. If you reuse the same password across multiple sites, a
breach on one insecure site can compromise your accounts everywhere,
including your email and banking.</p>
<h2>How to Create and Manage Strong Passwords</h2>
<p>Remembering dozens of complex, unique passwords
is impossible for most people. Here are two strategies:</p>
<ol>
<li><strong>Password Managers:</strong> These
are applications that generate and store highly complex passwords for
all your accounts. You only need to remember one master password.</li>
<li><strong>Use a Generator:</strong> For
one-off needs or to see what a strong password looks like, you can use
a tool to generate one for you.</li>
</ol>
<p>Ready to create a strong password now? Try our
<a href="#tools/password-generator">Secure Password Generator tool</a>
and see the difference.</p>
`
}
};
const app = [Link]('app');
const navLinks = [Link]('#main-nav a');
// --- PAGE TEMPLATES ---
// An object holding functions that return the HTML for each
page.
const pageTemplates = {
home: () => `
<div class="hero">
<div class="container">
<h1>ToolHive🐝 </h1>
<p>A modern, fast, and easy-to-use collection
of online tools to boost your productivity. All tools run directly in
your browser for speed and privacy.</p>
<a href="#tools" class="btn">Explore All
Tools</a>
</div>
</div>
<div class="container">
<h2 style="text-align: center; margin-bottom:
2rem; font-size: 1.8rem;">Featured Tools</h2>
<div class="tools-grid">
${[Link](tool => `
<a href="#tools/${[Link]}"
class="tool-card card">
<div
class="tool-card-icon">${[Link]}</div>
<h3>${[Link]}</h3>
<p>${[Link]}</p>
</a>
`).join('')}
</div>
</div>
`,
tools: () => `
<div class="container">
<div class="page-header">
<h1>All Tools</h1>
<p>Find the right tool for the job. Filter by
name below.</p>
</div>
<div class="form-group">
class="form-input" placeholder="
</div>
🔍
<input type="search" id="toolSearch"
Search for a tool...">
[Link]('.button-group').addEventListener('click', (e)
=> {
if ([Link] === 'BUTTON') {
const text = [Link];
const caseType = [Link];
switch (caseType) {
case 'upper':
[Link] = [Link]();
break;
case 'lower':
[Link] = [Link]();
break;
case 'title':
[Link] =
[Link]().split(' ').map(word => [Link](0).toUpperCase()
+ [Link](1)).join(' ');
break;
case 'sentence':
[Link] =
[Link]().replace(/(^\s*\w|[.!?]\s*\w)/g, c =>
[Link]());
break;
}
}
});
}
},
'password-generator': {
ui: () => `
<div class="form-group">
<label class="form-label">Generated
Password:</label>
<div style="display: flex; gap: 10px;">
<input type="text" id="passwordOutput"
class="form-input" readonly>
<button id="copyPasswordBtn" class="btn
btn-secondary">Copy</button>
</div>
</div>
<div class="form-group">
<label for="passwordLength"
class="form-label">Length: <span
id="passwordLengthValue">16</span></label>
<input type="range" id="passwordLength"
class="form-input" min="8" max="32" value="16">
</div>
<div class="form-group">
<button id="generatePasswordBtn"
class="btn">Generate New Password</button>
</div>
<hr style="margin: 2rem 0; border: none;
border-top: 1px solid var(--border-color);">
<h3 style="margin-bottom: 1rem;">Password Strength
Checker</h3>
<div class="form-group">
<label for="passwordStrengthInput"
class="form-label">Check a password:</label>
<input type="text" id="passwordStrengthInput"
class="form-input">
<div id="passwordStrength" style="margin-top:
0.5rem;"></div>
<div id="passwordStrengthText"
class="output-box" style="margin-top: 0.5rem;"></div>
</div>
`,
init: () => {
const output =
[Link]('passwordOutput');
const lengthSlider =
[Link]('passwordLength');
const lengthValue =
[Link]('passwordLengthValue');
const generateBtn =
[Link]('generatePasswordBtn');
const copyBtn =
[Link]('copyPasswordBtn');
const strengthInput =
[Link]('passwordStrengthInput');
const strengthBar =
[Link]('passwordStrength');
const strengthText =
[Link]('passwordStrengthText');
[Link]('input', () =>
[Link] = [Link]);
const generatePassword = () => {
const length = [Link];
const chars =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&
*()_+~`|}{[]:;?><,./-=';
let password = '';
for (let i = 0; i < length; i++) {
password +=
[Link]([Link]([Link]() * [Link]));
}
[Link] = password;
};
[Link]('click',
generatePassword);
[Link]('click', () => {
[Link]([Link]).then(() => {
[Link] = 'Copied!';
setTimeout(() => [Link] =
'Copy', 2000);
});
});
[Link]('input', () => {
const pass = [Link];
let score = 0;
if ([Link] > 8) score++;
if ([Link] > 12) score++;
if (/[a-z]/.test(pass)) score++;
if (/[A-Z]/.test(pass)) score++;
if (/[0-9]/.test(pass)) score++;
if (/[^A-Za-z0-9]/.test(pass)) score++;
const width = (score / 6) * 100;
[Link] = `${width}%`;
if (score <= 2) {
[Link] =
'var(--error-color)';
[Link] = 'Weak';
} else if (score <= 4) {
[Link] =
'orange';
[Link] = 'Moderate';
} else {
[Link] =
'var(--success-color)';
[Link] = 'Strong';
}
});
generatePassword();
[Link] = 'Enter a password to
check its strength.';
}
},
'char-counter': {
ui: () => `
<div class="form-group">
<label for="charCounterInput"
class="form-label">Enter your text:</label>
<textarea id="charCounterInput"
class="form-textarea"></textarea>
</div>
<div class="output-box" id="charCounterOutput">
<strong>With spaces:</strong> 0 |
<strong>Without spaces:</strong> 0
</div>
`,
init: () => {
const input =
[Link]('charCounterInput');
const output =
[Link]('charCounterOutput');
[Link]('input', () => {
const text = [Link];
const withSpaces = [Link];
const withoutSpaces = [Link](/\s/g,
'').length;
[Link] = `
<strong>With spaces:</strong>
${withSpaces} |
<strong>Without spaces:</strong>
${withoutSpaces}
`;
});
}
},
'space-remover': {
ui: () => `
<div class="form-group">
<label for="spaceRemoverInput"
class="form-label">Enter Text:</label>
<textarea id="spaceRemoverInput"
class="form-textarea"></textarea>
</div>
<div class="form-group">
<button id="removeSpacesBtn"
class="btn">Remove Extra Spaces</button>
</div>
`,
init: () => {
const input =
[Link]('spaceRemoverInput');
const btn =
[Link]('removeSpacesBtn');
[Link]('click', () => {
// 1. Trim leading/trailing spaces
// 2. Replace multiple spaces with a single
space
[Link] =
[Link]().replace(/\s+/g, ' ');
});
}
},
'name-generator': {
ui: () => `
<div class="form-group">
<label class="form-label">Generated
Name:</label>
<div class="output-box"
id="nameGeneratorOutput" style="font-size: 1.2rem; text-align:
center;">Click the button to generate a name.</div>
</div>
<div class="form-group" style="text-align:
center;">
<button id="generateNameBtn"
class="btn">Generate Name</button>
</div>
`,
init: () => {
const firstNames = ["Liam", "Olivia", "Noah",
"Emma", "Oliver", "Ava", "Elijah", "Charlotte", "William", "Sophia",
"James", "Amelia", "Benjamin", "Isabella", "Lucas", "Mia"];
const lastNames = ["Smith", "Johnson", "Williams",
"Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez",
"Martinez", "Hernandez", "Lopez", "Gonzalez", "Wilson"];
const output =
[Link]('nameGeneratorOutput');
const btn =
[Link]('generateNameBtn');
[Link]('click', () => {
const firstName =
firstNames[[Link]([Link]() * [Link])];
const lastName =
lastNames[[Link]([Link]() * [Link])];
[Link] = `${firstName}
${lastName}`;
});
}
},
'roman-converter': {
ui: () => `
<div class="form-group">
<label for="numberInput"
class="form-label">Number (1-3999)</label>
<input type="number" id="numberInput"
class="form-input" placeholder="e.g., 1994">
</div>
<div class="form-group">
<label for="romanInput"
class="form-label">Roman Numeral</label>
<input type="text" id="romanInput"
class="form-input" placeholder="e.g., MCMXCIV">
</div>
`,
init: () => {
const numInput =
[Link]('numberInput');
const romanInput =
[Link]('romanInput');
const toRoman = (num) => {
if (num < 1 || num > 3999) return '';
const val = [1000, 900, 500, 400, 100, 90, 50,
40, 10, 9, 5, 4, 1];
const sym = ["M", "CM", "D", "CD", "C", "XC",
"L", "XL", "X", "IX", "V", "IV", "I"];
let result = '';
for (let i = 0; i < [Link]; i++) {
while (num >= val[i]) {
result += sym[i];
num -= val[i];
}
}
return result;
};
const fromRoman = (str) => {
str = [Link]();
const map = { M: 1000, D: 500, C: 100, L: 50,
X: 10, V: 5, I: 1 };
let num = 0, i = 0;
while(i < [Link]) {
const current = map[str[i]];
const next = map[str[i + 1]];
if(current < next) {
num -= current;
} else {
num += current;
}
i++;
}
return num;
};
[Link]('input', () =>
[Link] = toRoman(parseInt([Link], 10)));
[Link]('input', () =>
[Link] = fromRoman([Link]));
}
},
'hash-generator': {
ui: () => `
<div class="form-group">
<label for="hashInput"
class="form-label">Input Text:</label>
<textarea id="hashInput"
class="form-textarea"></textarea>
</div>
<div class="form-group">
<label class="form-label">MD5 Hash:</label>
<div class="output-box" id="md5Output"></div>
</div>
<div class="form-group">
<label class="form-label">SHA-256
Hash:</label>
<div class="output-box"
id="sha256Output"></div>
</div>
`,
init: () => {
const input =
[Link]('hashInput');
const md5Out =
[Link]('md5Output');
const sha256Out =
[Link]('sha256Output');
[Link]('input', () => {
const text = [Link];
if(text) {
[Link] =
CryptoJS.MD5(text).toString();
[Link] =
CryptoJS.SHA256(text).toString();
} else {
[Link] = '';
[Link] = '';
}
});
}
},
'number-to-words': {
ui: () => `
<div class="form-group">
<label for="numToWordInput"
class="form-label">Enter a number:</label>
<input type="number" id="numToWordInput"
class="form-input" placeholder="e.g., 12345">
</div>
<div class="output-box"
id="numToWordOutput"></div>
`,
init: () => {
const input =
[Link]('numToWordInput');
const output =
[Link]('numToWordOutput');
const toWords = (num) => {
const a = ['','one ','two ','three ','four ',
'five ','six ','seven ','eight ','nine ','ten ','eleven ','twelve
','thirteen ','fourteen ','fifteen ','sixteen ','seventeen ','eighteen
','nineteen '];
const b = ['', '',
'twenty','thirty','forty','fifty',
'sixty','seventy','eighty','ninety'];
if ((num = [Link]()).length > 9) return
'overflow';
let n = ('000000000' +
num).substr(-9).match(/^(\d{2})(\d{2})(\d{2})(\d{1})(\d{2})$/);
if (!n) return; var str = '';
str += (n[1] != 0) ? (a[Number(n[1])] ||
b[n[1][0]] + ' ' + a[n[1][1]]) + 'crore ' : '';
str += (n[2] != 0) ? (a[Number(n[2])] ||
b[n[2][0]] + ' ' + a[n[2][1]]) + 'lakh ' : '';
str += (n[3] != 0) ? (a[Number(n[3])] ||
b[n[3][0]] + ' ' + a[n[3][1]]) + 'thousand ' : '';
str += (n[4] != 0) ? (a[Number(n[4])] ||
b[n[4][0]] + ' ' + a[n[4][1]]) + 'hundred ' : '';
str += (n[5] != 0) ? ((str != '') ? 'and ' :
'') + (a[Number(n[5])] || b[n[5][0]] + ' ' + a[n[5][1]]) : '';
return [Link]();
};
[Link]('input', () => {
const val = [Link];
if(val == 0) [Link] = 'zero';
else if(val) [Link] =
toWords(val);
else [Link] = '';
});
}
},
'text-reverser': {
ui: () => `
<div class="form-group">
<label for="reverserInput"
class="form-label">Text to Reverse:</label>
<textarea id="reverserInput"
class="form-textarea"></textarea>
</div>
<div class="form-group">
<button id="reverseBtn" class="btn">Reverse
Text</button>
</div>
`,
init: () => {
const input =
[Link]('reverserInput');
[Link]('reverseBtn').addEventListener('click', () =>
{
[Link] =
[Link]('').reverse().join('');
});
}
},
'text-to-pdf': {
ui: () => `
<div class="form-group">
<label for="pdfInput" class="form-label">Text
for PDF:</label>
<textarea id="pdfInput" class="form-textarea"
placeholder="Enter the text you want to convert to a PDF
file."></textarea>
</div>
<div class="form-group">
<label for="pdfFilename"
class="form-label">Filename:</label>
<input type="text" id="pdfFilename"
class="form-input" value="[Link]">
</div>
<button id="generatePdfBtn" class="btn">Generate &
Download PDF</button>
`,
init: () => {
const { jsPDF } = [Link];
const input = [Link]('pdfInput');
const filenameInput =
[Link]('pdfFilename');
const btn =
[Link]('generatePdfBtn');
[Link]('click', () => {
const doc = new jsPDF();
const text = [Link];
const filename = [Link] ||
'[Link]';
[Link](text, 10, 10);
[Link](filename);
});
}
},
'image-compressor': {
ui: () => `
<div class="form-group">
<label for="imageUpload"
class="form-label">Upload an Image (JPG/PNG):</label>
<input type="file" id="imageUpload"
class="form-input" accept="image/jpeg, image/png">
</div>
<div class="form-group">
<label for="imageQuality"
class="form-label">Quality (0.1 to 1.0): <span
id="qualityValue">0.7</span></label>
<input type="range" id="imageQuality"
class="form-input" min="0.1" max="1.0" step="0.1" value="0.7">
</div>
<div id="imageCompressorInfo" class="output-box"
style="display: none;"></div>
<div id="imageCompressorOutput"></div>
`,
init: () => {
const uploader =
[Link]('imageUpload');
const qualitySlider =
[Link]('imageQuality');
const qualityValue =
[Link]('qualityValue');
const outputDiv =
[Link]('imageCompressorOutput');
const infoDiv =
[Link]('imageCompressorInfo');
[Link]('input', () =>
[Link] = [Link]);
[Link]('change', (e) => {
const file = [Link][0];
if (!file) return;
const reader = new FileReader();
[Link] = (event) => {
const img = new Image();
[Link] = () => {
const canvas =
[Link]('canvas');
const ctx = [Link]('2d');
[Link] = [Link];
[Link] = [Link];
[Link](img, 0, 0);
const quality =
parseFloat([Link]);
const dataUrl =
[Link]([Link], quality);
const originalSize = ([Link] /
1024).toFixed(2);
const newSize = ([Link] * 0.75
/ 1024).toFixed(2); // Approximate size from base64
[Link] = `
<strong>Original Size:</strong>
${originalSize} KB<br>
<strong>Compressed Size:</strong>
~${newSize} KB<br>
<strong>Reduction:</strong> ${((1
- newSize / originalSize) * 100).toFixed(1)}%
`;
[Link] = 'block';
[Link] = `
<img src="${dataUrl}"
style="max-width: 100%; border-radius: 8px;" alt="Compressed Image">
<a href="${dataUrl}"
download="compressed-${[Link]}" class="btn" style="margin-top:
1rem;">Download Compressed Image</a>
`;
};
[Link] = [Link];
};
[Link](file);
});
}
},
'qr-code-generator': {
ui: () => `
<div class="form-group">
<label for="qrInput" class="form-label">Text
or URL for QR Code:</label>
<textarea id="qrInput" class="form-textarea"
placeholder="[Link]
</div>
<div id="qrCodeOutput"></div>
`,
init: () => {
const input = [Link]('qrInput');
const output =
[Link]('qrCodeOutput');
let qrCode = null;
const generateQR = () => {
const text = [Link]();
[Link] = ''; // Clear previous QR
code
if (text) {
qrCode = new QRCode(output, {
text: text,
width: 256,
height: 256,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: [Link].H
});
}
};
[Link]('input', generateQR);
generateQR(); // Initial generation if there's
pre-filled text (none in this case)
}
},
};
// --- ROUTER ---
// The main function that handles page changes.
const router = () => {
const hash = [Link] || '#home';
const [path, param] = [Link](1).split('/');
// Clear the main app container
[Link] = '';
// Update active nav link
[Link](link => {
[Link]('active');
if ([Link]('href') === `#${path}`) {
[Link]('active');
}
});
// Route to the correct page content
if (path === 'home') {
[Link] = [Link]();
} else if (path === 'tools') {
if (param) { // Individual tool page
const tool = [Link](t => [Link] === param);
if (tool) {
[Link] = [Link](tool);
const toolContainer =
[Link]('tool-container');
const implementation =
toolImplementations[[Link]];
if (implementation) {
[Link] =
[Link]();
[Link](); // CRUCIAL:
Initialize the tool's JS
}
} else {
[Link] = [Link]();
}
} else { // All tools page
[Link] = [Link]();
initToolsPage();
}
} else if (path === 'blog') {
if (param) { // Individual blog post
const post = blogPosts[param];
if (post) {
[Link] = [Link](post);
} else {
[Link] = [Link]();
}
} else { // Blog listing page
[Link] = [Link]();
}
} else if (path === 'contact') {
[Link] = [Link]();
initContactPage();
} else {
[Link] = [Link]();
}
[Link](0, 0); // Scroll to top on page change
};
// --- PAGE INITIALIZERS ---
function initToolsPage() {
const searchInput = [Link]('toolSearch');
const toolCards =
[Link]('#tools-grid-container .tool-card');
[Link]('input', (e) => {
const searchTerm = [Link]();
[Link](card => {
const toolName = [Link];
if ([Link](searchTerm)) {
[Link] = 'flex';
} else {
[Link] = 'none';
}
});
});
}
function initContactPage() {
const form = [Link]('contactForm');
[Link]('submit', (e) => {
[Link]();
alert('Thank you for your message! (This is a demo and
does not send an email).');
[Link]();
});
}
// --- EVENT LISTENERS ---
// Listen for hash changes to trigger the router
[Link]('hashchange', router);
// Load the initial page based on the URL
router();
});
</script>
</body>
</html>
Download Option
You can download the file you just created using the button below. I've created this button with a
small piece of JavaScript that packages the code above into a downloadable file.
<button id="downloadBtn" style="background-color: #6d28d9; color: white; padding: 12px 24px;
⬇️
border-radius: 8px; font-size: 16px; font-weight: 600; border: none; cursor: pointer; font-family:
'Inter', sans-serif;"> Download [Link] </button>
<script> [Link]('downloadBtn').addEventListener('click', () => { const
fileContent = [Link]; const blob = new Blob([fileContent], {
type: 'text/html' }); const a = [Link]('a'); [Link] = [Link](blob);
[Link] = '[Link]'; [Link](a); [Link]();
[Link](a); [Link]([Link]); }); </script>