0% found this document useful (0 votes)
35 views31 pages

ToolHive: Single-File SPA Toolkit

The document outlines the creation of a single-file multi-tool website called 'ToolHive' using modern vanilla JavaScript. It describes the architecture, including single-file design, SPA with hash-based routing, dynamic content rendering, and event handling, along with instructions on how to use the code. The document also includes a complete HTML file for the application, showcasing various tools and their functionalities.

Uploaded by

aleemr302
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
35 views31 pages

ToolHive: Single-File SPA Toolkit

The document outlines the creation of a single-file multi-tool website called 'ToolHive' using modern vanilla JavaScript. It describes the architecture, including single-file design, SPA with hash-based routing, dynamic content rendering, and event handling, along with instructions on how to use the code. The document also includes a complete HTML file for the application, showcasing various tools and their functionalities.

Uploaded by

aleemr302
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Of course!

Building a complete, single-file multi-tool website is an excellent project that perfectly


demonstrates the power of modern vanilla JavaScript. This architecture is efficient, portable,
and a great way to learn about creating Single-Page Applications (SPAs) from scratch.
Here is the complete solution, "ToolHive," crafted into a single [Link] file as you requested.

Overview of the Solution


1.​ Single-File Architecture: All the necessary code—HTML, CSS, and JavaScript—is
contained within one file. You can save this code as [Link] and open it in any modern
web browser to run the application.
2.​ SPA with Hash-Based Routing: The site operates as a Single-Page Application.
Navigation between pages (like from Home to the Word Counter tool) happens instantly
without a page refresh. This is managed by a custom JavaScript router that listens for
changes in the URL hash (e.g., #home, #tools/word-counter).
3.​ Dynamic Content Rendering: The core content of the page is dynamically rendered by
JavaScript. I have created a pageTemplates object where the HTML for each "page" is
stored as a template literal. When you navigate, the router selects the appropriate
template, injects it into the main <div id="app"></div>, and then runs an initializer function.
4.​ Event Handling: Each tool has a corresponding initializer function (e.g.,
initWordCounter()). This function is crucial: it runs after a tool's HTML is placed on the
page and is responsible for attaching the necessary event listeners (like 'click' or 'input') to
the buttons and forms, making the tool interactive.
5.​ External Libraries via CDN: For specialized tools like the Hash Generator, PDF
Converter, and QR Code Generator, we load popular, lightweight libraries from a Content
Delivery Network (CDN). This is done via <script> tags at the bottom of the <body>,
ensuring they are available to our main script without needing local files.

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.

The Complete [Link] File for ToolHive


<!DOCTYPE html>​
<html lang="en">​
<head>​
<meta charset="UTF-8">​
<meta name="viewport" content="width=device-width,
initial-scale=1.0">​
<title>ToolHive 🐝
- Your All-in-One Toolkit</title>​
<link rel="preconnect" href="[Link]
<link rel="preconnect" href="[Link]
crossorigin>​
<link
href="[Link]
700&display=swap" rel="stylesheet">​

<style>​
/* CSS Reset and Global Styles */​
:root {​
--primary-color: #6d28d9;​
--primary-color-light: #8b5cf6;​
--background-color: #f1f5f9;​
--surface-color: #ffffff;​
--text-color: #334155;​
--text-color-light: #64748b;​
--border-color: #e2e8f0;​
--shadow-color: rgba(109, 40, 217, 0.1);​
--success-color: #22c55e;​
--error-color: #ef4444;​
}​

* {​
margin: 0;​
padding: 0;​
box-sizing: border-box;​
}​

body {​
font-family: 'Inter', sans-serif;​
background-color: var(--background-color);​
color: var(--text-color);​
line-height: 1.6;​
display: flex;​
flex-direction: column;​
min-height: 100vh;​
}​

/* Layout */​
.container {​
width: 100%;​
max-width: 1200px;​
margin: 0 auto;​
padding: 0 20px;​
}​

header {​
background-color: var(--surface-color);​
box-shadow: 0 2px 4px rgba(0,0,0,0.05);​
padding: 1rem 0;​
position: sticky;​
top: 0;​
z-index: 1000;​
}​

.header-content {​
display: flex;​
justify-content: space-between;​
align-items: center;​
}​

.logo {​
font-size: 1.5rem;​
font-weight: 700;​
color: var(--primary-color);​
text-decoration: none;​
}​

nav ul {​
list-style: none;​
display: flex;​
gap: 1.5rem;​
}​

nav a {​
text-decoration: none;​
color: var(--text-color);​
font-weight: 500;​
transition: color 0.3s ease;​
}​

nav a:hover, nav [Link] {​
color: var(--primary-color);​
}​

main {​
flex-grow: 1;​
padding: 2rem 0;​
}​

footer {​
background-color: var(--text-color);​
color: var(--background-color);​
text-align: center;​
padding: 1.5rem 0;​
margin-top: auto;​
}​

/* Components */​
.btn {​
display: inline-block;​
background-color: var(--primary-color);​
color: white;​
padding: 0.75rem 1.5rem;​
border-radius: 8px;​
text-decoration: none;​
font-weight: 600;​
border: none;​
cursor: pointer;​
transition: background-color 0.3s ease, transform 0.2s
ease;​
}​

.btn:hover {​
background-color: var(--primary-color-light);​
transform: translateY(-2px);​
}​

.btn-secondary {​
background-color: var(--surface-color);​
color: var(--primary-color);​
border: 1px solid var(--primary-color);​
}​

.btn-secondary:hover {​
background-color: #f1f5f9;​
}​

.card {​
background-color: var(--surface-color);​
border-radius: 12px;​
padding: 1.5rem;​
box-shadow: 0 4px 12px var(--shadow-color);​
transition: transform 0.3s ease, box-shadow 0.3s ease;​
height: 100%;​
}​

.card:hover {​
transform: translateY(-5px);​
box-shadow: 0 8px 20px var(--shadow-color);​
}​

.tool-card {​
display: flex;​
flex-direction: column;​
text-decoration: none;​
color: var(--text-color);​
}​

.tool-card-icon {​
font-size: 2rem;​
margin-bottom: 1rem;​
color: var(--primary-color);​
width: 48px;​
height: 48px;​
}​

.tool-card h3 {​
font-size: 1.1rem;​
margin-bottom: 0.5rem;​
color: var(--text-color);​
}​

.tool-card p {​
font-size: 0.9rem;​
color: var(--text-color-light);​
flex-grow: 1;​
}​

.tools-grid {​
display: grid;​
grid-template-columns: repeat(auto-fill, minmax(280px,
1fr));​
gap: 1.5rem;​
}​

/* Form Elements */​
.form-group {​
margin-bottom: 1.5rem;​
}​

.form-label {​
display: block;​
margin-bottom: 0.5rem;​
font-weight: 600;​
}​

.form-input, .form-textarea {​
width: 100%;​
padding: 0.75rem 1rem;​
border-radius: 8px;​
border: 1px solid var(--border-color);​
font-size: 1rem;​
font-family: 'Inter', sans-serif;​
transition: border-color 0.3s, box-shadow 0.3s;​
}​

.form-input:focus, .form-textarea:focus {​
outline: none;​
border-color: var(--primary-color);​
box-shadow: 0 0 0 3px var(--shadow-color);​
}​

.form-textarea {​
min-height: 150px;​
resize: vertical;​
}​

.output-box {​
background-color: var(--background-color);​
padding: 1rem;​
border-radius: 8px;​
border: 1px solid var(--border-color);​
min-height: 50px;​
white-space: pre-wrap;​
word-wrap: break-word;​
margin-top: 1rem;​
font-family: monospace;​
}​

.output-box strong {​
font-family: 'Inter', sans-serif;​
}​

/* Page Specific Styles */​
.hero {​
text-align: center;​
padding: 4rem 0;​
}​

.hero h1 {​
font-size: 3rem;​
font-weight: 700;​
margin-bottom: 1rem;​
}​

.hero p {​
font-size: 1.25rem;​
color: var(--text-color-light);​
max-width: 600px;​
margin: 0 auto 2rem;​
}​

.page-header {​
margin-bottom: 2rem;​
border-bottom: 1px solid var(--border-color);​
padding-bottom: 1rem;​
}​

.page-header h1 {​
font-size: 2.5rem;​
}​

.page-header p {​
color: var(--text-color-light);​
font-size: 1.1rem;​
}​

/* Tool Specific Styles */​
.button-group {​
display: flex;​
flex-wrap: wrap;​
gap: 0.5rem;​
}​

#passwordStrength {​
height: 10px;​
border-radius: 5px;​
transition: width 0.3s ease, background-color 0.3s ease;​
}​

#qrCodeOutput {​
width: 256px;​
height: 256px;​
margin: 1rem auto;​
padding: 1rem;​
background: white;​
border: 1px solid var(--border-color);​
border-radius: 8px;​
}​
#qrCodeOutput img {​
width: 100% !important;​
height: 100% !important;​
}​

#imageCompressorOutput {​
max-width: 100%;​
margin-top: 1rem;​
}​

/* Blog Styles */​
.blog-post {​
background: var(--surface-color);​
padding: 2rem;​
border-radius: 12px;​
}​
.blog-post h1 { margin-bottom: 1rem; }​
.blog-post h2 { margin-top: 1.5rem; margin-bottom: 0.5rem; }​
.blog-post p, .blog-post ul { margin-bottom: 1rem; }​
.blog-post ul { padding-left: 1.5rem; }​

/* Responsive Design */​
@media (max-width: 768px) {​
.header-content {​
flex-direction: column;​
gap: 1rem;​
}​
.hero h1 {​
font-size: 2.5rem;​
}​
.page-header h1 {​
font-size: 2rem;​
}​
}​
</style>​
</head>​
<body>​

<header>​
<div class="container">​
<div class="header-content">​
<a href="#home" class="logo">ToolHive
<nav id="main-nav">​
🐝 </a>​

<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...">​

<div class="tools-grid" id="tools-grid-container">​


${[Link](tool => `​
<a href="#tools/${[Link]}"
class="tool-card card" data-tool-name="${[Link]()}">​
<div
class="tool-card-icon">${[Link]}</div>​
<h3>${[Link]}</h3>​
<p>${[Link]}</p>​
</a>​
`).join('')}​
</div>​
</div>​
`,​
toolPage: (tool) => `​
<div class="container">​
<div class="page-header">​
<h1>${[Link]}</h1>​
<p>${[Link]}</p>​
</div>​
<div class="card" id="tool-container">​
</div>​
</div>​
`,​
blog: () => `​
<div class="container">​
<div class="page-header">​
<h1>Blog</h1>​
<p>Thoughts and articles about productivity
and web tools.</p>​
</div>​
<div class="tools-grid">​
<a href="#blog/sample-post" class="card
tool-card">​
<h3>The Importance of a Strong
Password</h3>​
<p>Learn why a strong, unique password is
your first line of defense in the digital world and how to create
one.</p>​
</a>​
</div>​
</div>​
`,​
blogPost: (post) => `​
<div class="container">​
<div class="blog-post">​
<h1>${[Link]}</h1>​
${[Link]}​
</div>​
</div>​
`,​
contact: () => `​
<div class="container">​
<div class="page-header">​
<h1>Contact Us</h1>​
<p>Have a question or a suggestion for a new
tool? Let us know!</p>​
</div>​
<div class="card">​
<form id="contactForm">​
<div class="form-group">​
<label for="name"
class="form-label">Name</label>​
<input type="text" id="name"
class="form-input" required>​
</div>​
<div class="form-group">​
<label for="email"
class="form-label">Email</label>​
<input type="email" id="email"
class="form-input" required>​
</div>​
<div class="form-group">​
<label for="message"
class="form-label">Message</label>​
<textarea id="message"
class="form-textarea" required></textarea>​
</div>​
<button type="submit" class="btn">Send
Message</button>​
</form>​
</div>​
</div>​
`,​
notFound: () => `​
<div class="container" style="text-align: center;">​
<div class="page-header">​
<h1>404 - Page Not Found</h1>​
<p>Oops! The page you are looking for does not
exist.</p>​
<a href="#home" class="btn" style="margin-top:
1rem;">Go to Homepage</a>​
</div>​
</div>​
`​
};​

// --- TOOL UI & INITIALIZERS ---​
// Each tool has an object with two parts:​
// 1. ui: A function that returns the specific HTML for the
tool's interface.​
// 2. init: A function that gets called after the UI is on the
page to add event listeners.​
const toolImplementations = {​
'word-counter': {​
ui: () => `​
<div class="form-group">​
<label for="wordCounterInput"
class="form-label">Enter your text below:</label>​
<textarea id="wordCounterInput"
class="form-textarea"></textarea>​
</div>​
<div class="output-box" id="wordCounterOutput">​
<strong>Words:</strong> 0 | ​
<strong>Characters:</strong> 0 | ​
<strong>Reading Time:</strong> ~0 min​
</div>​
`,​
init: () => {​
const input =
[Link]('wordCounterInput');​
const output =
[Link]('wordCounterOutput');​
[Link]('input', () => {​
const text = [Link];​
const words =
[Link]().split(/\s+/).filter(word => [Link] > 0);​
const wordCount = [Link]() === '' ? 0 :
[Link];​
const charCount = [Link];​
const readingTime = [Link](wordCount /
200); // Avg reading speed​
[Link] = `​
<strong>Words:</strong> ${wordCount} | ​
<strong>Characters:</strong> ${charCount}
| ​
<strong>Reading Time:</strong>
~${readingTime} min​
`;​
});​
}​
},​
'case-converter': {​
ui: () => `​
<div class="form-group">​
<label for="caseConverterInput"
class="form-label">Enter Text:</label>​
<textarea id="caseConverterInput"
class="form-textarea"></textarea>​
</div>​
<div class="form-group button-group">​
<button class="btn" data-case="upper">UPPER
CASE</button>​
<button class="btn" data-case="lower">lower
case</button>​
<button class="btn" data-case="title">Title
Case</button>​
<button class="btn"
data-case="sentence">Sentence case.</button>​
</div>​
`,​
init: () => {​
const input =
[Link]('caseConverterInput');​

[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>

You might also like