```html
HOUSE IT LTD – Multi Tool Hub
HOUSE IT LTD
– Multi Tool Hub –
Unlock your productivity with our comprehensive suite of futuristic, AI-enhanced frontend tools.
```
---
### `style.css` (Significantly Enhanced)
```css
/* --- CSS Variables (Abhinav Prompt Theme) --- */
:root {
/* Background Colors */
--main-bg: #0d0f14;
--navbar-bg: rgba(28, 34, 45, 0.8); /* Slightly transparent for depth */
--button-normal-bg: #1c222d;
--button-hover-bg: #42f8f5; /* Primary Accent */
--card-bg: #1c222d;
--modal-overlay-bg: rgba(0, 0, 0, 0.85); /* Slightly less transparent overlay */
/* Accent Colors */
--primary-accent: #42f8f5;
--accent-shade: #3ff7f3;
--secondary-accent: #00b8d4; /* Added for subtle variations */
/* Text Colors */
--primary-text: #ffffff;
--secondary-text: #c5d1de;
--tertiary-text: #9ca3af; /* For less important info */
/* Glow/Border Effects */
--button-border-color: var(--primary-accent);
--glow-shadow: rgba(66, 248, 245, 0.7); /* Slightly stronger glow */
--card-glow-shadow: rgba(66, 248, 245, 0.4);
--modal-border-color: var(--primary-accent);
/* Fonts */
--font-main: 'Inter', sans-serif;
--font-display: 'Orbitron', sans-serif; /* Futuristic display font */
/* Transitions */
--transition-speed-fast: 0.2s;
--transition-speed-normal: 0.3s;
--transition-speed-slow: 0.5s;
}
/* --- Global Styles --- */
html {
scroll-behavior: smooth; /* Enables smooth scrolling for anchor links */
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-main);
background-color: var(--main-bg);
color: var(--primary-text);
line-height: 1.7;
overflow-x: hidden; /* Prevent horizontal scrollbar */
display: flex;
flex-direction: column; /* Stack header, main, footer */
min-height: 100vh; /* Ensure body takes at least full viewport height */
}
.container {
width: 95%;
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
h1, h2, h3, h4 {
color: var(--primary-text);
font-family: var(--font-display); /* Use display font for headings */
margin-bottom: 15px;
line-height: 1.3;
}
h1 { font-size: 3.5em; }
h2 { font-size: 2em; }
h3 { font-size: 1.5em; }
h4 { font-size: 1.2em; }
p {
color: var(--secondary-text);
margin-bottom: 15px;
font-size: 1.05em;
}
a {
color: var(--primary-accent);
text-decoration: none;
transition: color var(--transition-speed-fast) ease, text-shadow var(--transition-speed-fast) ease;
}
a:hover {
color: var(--accent-shade);
text-shadow: 0 0 8px var(--accent-shade);
}
/* --- Navbar --- */
.navbar {
background-color: var(--navbar-bg);
padding: 18px 0;
position: sticky;
top: 0;
z-index: 1000;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5);
backdrop-filter: blur(10px); /* Subtle blur effect */
-webkit-backdrop-filter: blur(10px); /* For Safari */
}
.navbar .container {
display: flex;
justify-content: space-between;
align-items: center;
}
.navbar .logo {
font-size: 2em;
font-weight: 700;
color: var(--primary-accent);
text-shadow: 0 0 10px var(--primary-accent), 0 0 20px var(--accent-shade);
letter-spacing: 1px;
}
.navbar nav ul {
list-style: none;
display: flex;
align-items: center; /* Vertically align nav items */
}
.navbar nav ul li {
margin-left: 35px;
}
.navbar nav ul li a {
font-size: 1.1em;
font-weight: 500;
color: var(--secondary-text);
position: relative;
padding-bottom: 8px; /* Space for the underline/glow */
transition: color var(--transition-speed-fast) ease, text-shadow var(--transition-speed-fast) ease;
}
.navbar nav ul li a::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 0; /* Start with no underline */
height: 3px; /* Thicker underline */
background-color: var(--primary-accent);
transition: width var(--transition-speed-fast) ease;
box-shadow: 0 0 10px var(--primary-accent); /* Glow */
}
.navbar nav ul li a:hover {
color: var(--primary-text);
}
.navbar nav ul li a:hover::after {
width: 100%; /* Underline expands on hover */
}
/* --- Hero Section --- */
#hero {
text-align: center;
padding: 100px 0;
margin-bottom: 60px;
background: linear-gradient(135deg, rgba(28, 34, 45, 0.7) 0%, rgba(13, 15, 20, 0.9) 100%);
border-radius: 12px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5), inset 0 0 20px rgba(66, 248, 245, 0.1); /* Subtle inner glow */
position: relative;
overflow: hidden; /* Contain background effects */
}
/* Subtle animated background for Hero */
#hero::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: radial-gradient(circle, rgba(66, 248, 245, 0.05) 10%, transparent 50%);
animation: subtleGlow 15s infinite alternate ease-in-out;
z-index: 0;
}
@keyframes subtleGlow {
0% { transform: scale(1); opacity: 0.5; }
50% { transform: scale(1.2); opacity: 0.8; }
100% { transform: scale(1); opacity: 0.5; }
}
#hero h1 {
font-size: 4em; /* Larger headline */
margin-bottom: 10px;
color: var(--primary-text);
text-shadow: 0 0 15px var(--primary-accent), 0 0 30px var(--accent-shade);
animation: neonTextGlow 2s infinite alternate;
}
@keyframes neonTextGlow {
from { text-shadow: 0 0 5px var(--primary-accent), 0 0 10px var(--primary-accent); }
to { text-shadow: 0 0 10px var(--accent-shade), 0 0 20px var(--accent-shade); }
}
.hero-subtitle {
font-family: var(--font-main); /* Use main font for subtitle */
font-size: 1.8em;
color: var(--secondary-accent);
margin-bottom: 25px;
font-weight: 500;
letter-spacing: 1px;
}
.hero-description {
font-size: 1.2em;
color: var(--tertiary-text);
max-width: 70%;
margin: 0 auto;
}
/* --- Tools Grid --- */
.tools-grid {
display: grid;
grid-template-columns: repeat(3, 1fr); /* Desktop: 3 columns */
gap: 35px; /* Increased gap */
padding-bottom: 80px; /* More bottom padding */
padding-top: 20px; /* Slight top padding */
}
@media (max-width: 992px) {
.tools-grid {
grid-template-columns: repeat(2, 1fr); /* Tablet: 2 columns */
}
}
@media (max-width: 768px) {
.tools-grid {
grid-template-columns: 1fr; /* Mobile: 1 column */
}
#hero h1 { font-size: 2.5em; }
.hero-subtitle { font-size: 1.5em; }
.hero-description { font-size: 1.1em; }
}
/* --- Tool Card --- */
.tool-card {
background-color: var(--card-bg);
border: 1px solid rgba(66, 248, 245, 0.1); /* Subtle border */
border-radius: 12px; /* More rounded corners */
padding: 35px; /* Increased padding */
text-align: center;
cursor: pointer;
transition: transform var(--transition-speed-normal) ease, box-shadow var(--transition-speed-normal) ease, border-color var(--transition-speed-normal) ease, background-color var(--transition-speed-normal) ease;
display: flex;
flex-direction: column;
justify-content: space-between;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.4); /* Deeper shadow */
}
.tool-card:hover {
transform: translateY(-8px) scale(1.03); /* More pronounced lift and scale */
border-color: var(--primary-accent);
box-shadow: 0 15px 40px var(--card-glow-shadow), 0 0 25px var(--glow-shadow); /* Enhanced glow */
background-color: rgba(28, 34, 45, 0.9); /* Slightly brighter on hover */
}
.tool-card .tool-title {
font-size: 1.6em;
color: var(--primary-text);
margin-bottom: 12px;
text-shadow: 0 0 8px var(--primary-accent);
}
.tool-card .tool-description {
font-size: 0.95em;
color: var(--tertiary-text); /* Use tertiary text for descriptions */
flex-grow: 1;
margin-bottom: 25px; /* More space before button */
}
/* --- Buttons --- */
.btn-glow {
background-color: var(--button-normal-bg);
color: var(--primary-text);
border: 2px solid var(--button-border-color);
padding: 14px 30px; /* Larger buttons */
border-radius: 8px; /* More rounded */
font-size: 1em;
font-weight: bold;
font-family: var(--font-main); /* Ensure button text uses main font */
cursor: pointer;
transition: background-color var(--transition-speed-normal) ease, color var(--transition-speed-normal) ease, box-shadow var(--transition-speed-normal) ease, transform var(--transition-speed-normal) ease;
box-shadow: 0 0 5px var(--glow-shadow); /* Initial subtle glow */
display: inline-block;
text-align: center;
letter-spacing: 0.5px;
}
.btn-glow:hover {
background-color: var(--button-hover-bg);
color: var(--main-bg); /* Dark text on bright hover */
box-shadow: 0 0 18px var(--glow-shadow); /* Stronger hover glow */
transform: translateY(-3px) scale(1.02); /* Lift and slight scale */
}
.btn-glow:active {
transform: translateY(0) scale(1); /* Return to original position */
box-shadow: 0 0 8px var(--glow-shadow); /* Softer glow when pressed */
}
/* --- Modals --- */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--modal-overlay-bg);
display: flex;
justify-content: center;
align-items: center;
z-index: 1001;
opacity: 0;
visibility: hidden;
transition: opacity var(--transition-speed-slow) ease, visibility var(--transition-speed-slow) ease, transform var(--transition-speed-slow) ease;
transform: scale(0.95); /* Start slightly scaled down */
}
.modal.active {
opacity: 1;
visibility: visible;
transform: scale(1); /* Scale up to normal */
}
.modal-content {
background-color: var(--card-bg); /* Use card background for modal */
padding: 40px 50px; /* More padding */
border-radius: 15px; /* More rounded */
width: 90%; /* Responsive width */
max-width: 650px; /* Max width */
max-height: 85%;
overflow-y: auto;
box-shadow: 0 15px 45px var(--glow-shadow), inset 0 0 30px rgba(66, 248, 245, 0.2); /* Stronger shadow and inner glow */
border: 2px solid var(--modal-border-color);
position: relative;
transform: scale(1); /* Ensure content is at normal scale */
opacity: 1; /* Ensure content is visible */
transition: transform var(--transition-speed-slow) ease, opacity var(--transition-speed-slow) ease;
}
/* Improve transition for modal content itself */
.modal.active .modal-content {
animation: modalFadeIn var(--transition-speed-slow) forwards;
}
@keyframes modalFadeIn {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
}
.modal-content h2 {
font-size: 2.5em; /* Larger modal titles */
margin-bottom: 25px;
text-align: center;
color: var(--primary-text);
text-shadow: 0 0 8px var(--primary-accent);
}
.modal-close {
position: absolute;
top: 20px;
right: 20px;
font-size: 2.5em; /* Larger close icon */
color: var(--tertiary-text);
cursor: pointer;
transition: color var(--transition-speed-fast) ease, transform var(--transition-speed-fast) ease;
z-index: 10; /* Ensure it's above content */
}
.modal-close:hover {
color: var(--primary-accent);
transform: scale(1.1); /* Slight scale on hover */
}
/* --- Input Fields & Form Elements within Modals --- */
.modal-content label {
display: block;
margin-bottom: 10px; /* More space */
color: var(--secondary-text);
font-weight: 500; /* Slightly bolder labels */
font-size: 1.05em;
}
.modal-content input[type="text"],
.modal-content input[type="number"],
.modal-content input[type="file"],
.modal-content textarea,
.modal-content select {
width: 100%;
padding: 15px 12px; /* Larger padding */
margin-bottom: 20px; /* More space between inputs */
background-color: rgba(28, 34, 45, 0.7); /* Darker, slightly transparent inputs */
color: var(--primary-text);
border: 1px solid rgba(66, 248, 245, 0.15); /* Subtle accent border */
border-radius: 8px; /* Rounded inputs */
font-size: 1em;
outline: none;
transition: border-color var(--transition-speed-fast) ease, box-shadow var(--transition-speed-fast) ease, background-color var(--transition-speed-fast) ease;
}
.modal-content input[type="text"]:focus,
.modal-content input[type="number"]:focus,
.modal-content input[type="file"]:focus,
.modal-content textarea:focus,
.modal-content select:focus {
border-color: var(--primary-accent);
box-shadow: 0 0 12px var(--glow-shadow); /* Focus glow */
background-color: rgba(28, 34, 45, 0.9); /* Slightly brighter on focus */
}
/* Style for file input label */
.tool-input-group input[type="file"] {
padding: 10px; /* Smaller padding for file input itself */
border: none; /* Hide default file input border */
background: transparent;
box-shadow: none;
}
.tool-input-group input[type="file"] + label { /* Style the associated label */
display: block; /* Ensure it takes full width */
background-color: var(--button-normal-bg);
color: var(--primary-text);
border: 1px solid var(--primary-accent);
padding: 12px 20px;
border-radius: 8px;
cursor: pointer;
transition: background-color var(--transition-speed-normal) ease, box-shadow var(--transition-speed-normal) ease;
margin-top: 10px;
text-align: center;
}
.tool-input-group input[type="file"] + label:hover {
background-color: var(--primary-accent);
color: var(--main-bg);
box-shadow: 0 0 15px var(--glow-shadow);
}
.modal-content .button-group {
text-align: center;
margin-top: 30px; /* More space above buttons */
}
/* Specific styling for tool content within modal */
.tool-specific-content {
margin-top: 20px;
}
.tool-specific-content p {
margin-bottom: 15px;
}
.tool-specific-content button {
margin-right: 10px;
}
/* --- Tool Specific Styling Examples --- */
/* Example: QR Code Generator */
#qr-code-display {
display: block;
margin: 25px auto; /* Center the image */
max-width: 200px;
height: auto;
border: 4px solid var(--primary-accent);
border-radius: 8px;
box-shadow: 0 0 15px var(--glow-shadow);
}
#qr-code-display:hover {
box-shadow: 0 0 25px var(--accent-shade);
}
/* Example: Color Picker Tool */
.color-picker-inputs {
display: flex;
flex-wrap: wrap; /* Allow wrapping on smaller screens */
gap: 15px;
align-items: center;
margin-bottom: 20px;
justify-content: center; /* Center the group if it wraps */
}
.color-picker-inputs label {
min-width: 80px;
text-align: right; /* Align label text */
}
.color-input {
flex-grow: 1;
display: flex;
align-items: center;
min-width: 120px; /* Ensure inputs have some width */
}
.color-input input[type="number"] {
width: 70px; /* Adjust width for number inputs */
margin-right: 5px;
padding: 10px 8px; /* Smaller padding for numbers */
}
.color-display-swatch { /* Used for manual color preview */
width: 40px; /* Slightly larger swatch */
height: 40px;
border-radius: 5px;
border: 1px solid #555;
display: inline-block;
margin-left: 10px;
vertical-align: middle;
box-shadow: inset 0 0 10px rgba(0,0,0,0.5); /* Inner shadow */
}
/* Example: Timer/Stopwatch Display */
#stopwatch-display {
font-size: 2.5em;
margin-bottom: 20px;
padding: 10px 20px;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 8px;
border: 1px solid var(--primary-accent);
display: inline-block;
}
/* Example: QR Scanner Preview */
#qr-scan-preview video {
border-radius: 8px;
border: 2px solid var(--primary-accent);
box-shadow: 0 0 15px var(--glow-shadow);
}
#qr-canvas {
border-radius: 8px; /* Match video border-radius */
}
/* Example: Tool Output Areas */
.tool-output {
margin-top: 25px; /* More space */
padding: 20px;
background-color: rgba(18, 21, 26, 0.6); /* Darker background */
border: 1px dashed var(--primary-accent);
border-radius: 8px;
word-wrap: break-word;
font-size: 0.95em;
color: var(--primary-text);
box-shadow: inset 0 0 10px rgba(66, 248, 245, 0.2); /* Inner glow */
}
.tool-output p {
margin-bottom: 10px; /* Less margin between paragraphs in output */
}
.tool-output strong {
color: var(--primary-text);
font-family: var(--font-display); /* Use display font for key results */
}
.tool-output a.btn-glow { /* Style download links within output */
margin-top: 15px;
padding: 10px 20px;
font-size: 0.9em;
}
/* --- Footer --- */
footer {
background-color: var(--navbar-bg); /* Same as navbar */
color: var(--tertiary-text); /* Use tertiary text */
text-align: center;
padding: 50px 0; /* More padding */
margin-top: 60px; /* Space above footer */
box-shadow: 0 -4px 15px rgba(0, 0, 0, 0.5); /* Shadow at the top */
backdrop-filter: blur(10px); /* Subtle blur */
-webkit-backdrop-filter: blur(10px);
}
footer h2 {
color: var(--primary-text);
text-shadow: 0 0 8px var(--primary-accent);
margin-bottom: 15px;
}
footer p {
margin-bottom: 12px;
font-size: 1.05em;
}
/* --- Scrollbar Styling (Optional, for better aesthetics if needed) --- */
/* ::-webkit-scrollbar { width: 10px; }
::-webkit-scrollbar-track { background: var(--main-bg); border-radius: 10px; }
::-webkit-scrollbar-thumb { background: var(--primary-accent); border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: var(--accent-shade); } */
```
---
### `script.js` (Minor Adjustments for Polish)
*(Mostly minor tweaks for consistency with new CSS, like slightly adjusting button text/disabling states for smoother UI flow)*
```javascript
// --- CSS Variables ---
const CSS_VARS = {
mainBg: getComputedStyle(document.documentElement).getPropertyValue('--main-bg').trim(),
primaryAccent: getComputedStyle(document.documentElement).getPropertyValue('--primary-accent').trim(),
glowShadow: getComputedStyle(document.documentElement).getPropertyValue('--glow-shadow').trim(),
};
// --- DOM Elements ---
const toolButtons = document.querySelectorAll('.tool-card .btn-glow');
const modalContainer = document.getElementById('modal-container');
// --- Tool Definitions ---
const tools = {
// --- Image Tools ---
"image-converter": {
title: "Image Converter",
description: "Convert images between various formats easily.",
renderModalContent: () => `
Image Converter
`,
init: () => {
const imageFile = document.getElementById('image-file');
const outputFormat = document.getElementById('output-format');
const convertBtn = document.getElementById('convert-image-btn');
const outputDiv = document.getElementById('image-conversion-output');
convertBtn.addEventListener('click', async () => {
const file = imageFile.files[0];
if (!file) {
alert("Please select an image first!");
return;
}
// Disable button during processing
convertBtn.disabled = true;
convertBtn.textContent = 'Converting...';
const reader = new FileReader();
reader.onload = async (e) => {
const img = new Image();
img.onload = async () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const format = outputFormat.value;
const quality = (format === 'jpeg' || format === 'webp') ? 0.8 : 1;
canvas.toBlob(async (blob) => {
if (blob) {
const url = URL.createObjectURL(blob);
const filename = file.name.split('.').slice(0, -1).join('.') || 'converted_image';
outputDiv.innerHTML = `
Conversion successful! Download your converted image:
Download
`;
outputDiv.classList.remove('hidden');
} else {
outputDiv.innerHTML = `Error creating blob.
`;
outputDiv.classList.remove('hidden');
}
// Re-enable button
convertBtn.disabled = false;
convertBtn.textContent = 'Convert';
}, `image/${format}`, quality);
};
img.onerror = () => {
outputDiv.innerHTML = `Error loading image.
`;
outputDiv.classList.remove('hidden');
// Re-enable button
convertBtn.disabled = false;
convertBtn.textContent = 'Convert';
};
img.src = e.target.result;
};
reader.onerror = () => {
outputDiv.innerHTML = `Error reading file.
`;
outputDiv.classList.remove('hidden');
// Re-enable button
convertBtn.disabled = false;
convertBtn.textContent = 'Convert';
};
reader.readAsDataURL(file);
});
}
},
"image-compressor": {
title: "Image Compressor",
description: "Reduce image file sizes without losing quality.",
renderModalContent: () => `
Image Compressor
`,
init: () => {
const compressFile = document.getElementById('compress-file');
const qualitySlider = document.getElementById('compression-quality');
const qualityValueSpan = document.getElementById('quality-value');
const compressBtn = document.getElementById('compress-image-btn');
const outputDiv = document.getElementById('compression-output');
qualitySlider.addEventListener('input', () => {
qualityValueSpan.textContent = qualitySlider.value;
});
compressBtn.addEventListener('click', () => {
const file = compressFile.files[0];
const quality = parseInt(qualitySlider.value) / 100;
if (!file) {
alert("Please select an image first!");
return;
}
// Disable button during processing
compressBtn.disabled = true;
compressBtn.textContent = 'Compressing...';
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
canvas.toBlob(async (blob) => {
if (blob) {
const url = URL.createObjectURL(blob);
const fileSizeKB = (blob.size / 1024).toFixed(2);
const originalFilename = file.name.split('.').slice(0, -1).join('.') || 'image';
const fileExt = file.name.split('.').pop() || 'png'; // Default to png if no extension
outputDiv.innerHTML = `
Compressed Image (${fileSizeKB} KB):
Download Compressed Image
`;
outputDiv.classList.remove('hidden');
} else {
outputDiv.innerHTML = `Error creating blob.
`;
outputDiv.classList.remove('hidden');
}
// Re-enable button
compressBtn.disabled = false;
compressBtn.textContent = 'Compress';
}, file.type, quality);
};
img.onerror = () => {
outputDiv.innerHTML = `Error loading image.
`;
outputDiv.classList.remove('hidden');
// Re-enable button
compressBtn.disabled = false;
compressBtn.textContent = 'Compress';
};
img.src = e.target.result;
};
reader.onerror = () => {
outputDiv.innerHTML = `Error reading file.
`;
outputDiv.classList.remove('hidden');
// Re-enable button
compressBtn.disabled = false;
compressBtn.textContent = 'Compress';
};
reader.readAsDataURL(file);
});
}
},
"image-cropper": {
title: "Image Cropper",
description: "Crop and resize your images with precision.",
renderModalContent: () => `
Image Cropper
`,
init: () => {
const cropFile = document.getElementById('crop-file');
const cropperCanvas = document.getElementById('cropper-canvas');
const cropperContext = cropperCanvas.getContext('2d');
const cropOverlay = document.getElementById('crop-overlay');
const startCropBtn = document.getElementById('start-crop-btn');
const applyCropBtn = document.getElementById('apply-crop-btn');
const outputDiv = document.getElementById('cropper-output');
const cropXInput = document.getElementById('crop-x');
const cropYInput = document.getElementById('crop-y');
const cropWidthInput = document.getElementById('crop-width');
const cropHeightInput = document.getElementById('crop-height');
let imgObj = null;
let isCroppingMode = false;
let startX, startY;
let currentCropRect = { x: 0, y: 0, width: 0, height: 0 };
const drawImage = () => {
if (!imgObj) return;
const canvasWidth = Math.min(imgObj.width, window.innerWidth * 0.8);
const canvasHeight = (imgObj.height / imgObj.width) * canvasWidth;
cropperCanvas.width = canvasWidth;
cropperCanvas.height = canvasHeight;
cropperContext.drawImage(imgObj, 0, 0, canvasWidth, canvasHeight);
};
const updateCropInputs = () => {
cropXInput.value = currentCropRect.x.toFixed(0);
cropYInput.value = currentCropRect.y.toFixed(0);
cropWidthInput.value = currentCropRect.width.toFixed(0);
cropHeightInput.value = currentCropRect.height.toFixed(0);
};
const drawCropOverlay = () => {
if (!isCroppingMode || !imgObj) return;
cropOverlay.style.left = `${currentCropRect.x}px`;
cropOverlay.style.top = `${currentCropRect.y}px`;
cropOverlay.style.width = `${currentCropRect.width}px`;
cropOverlay.style.height = `${currentCropRect.height}px`;
cropOverlay.style.display = 'block';
updateCropInputs();
};
cropFile.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
imgObj = new Image();
imgObj.onload = () => {
drawImage();
cropOverlay.style.display = 'none';
applyCropBtn.disabled = true;
isCroppingMode = false;
currentCropRect = { x: 0, y: 0, width: 0, height: 0 };
updateCropInputs();
};
imgObj.onerror = () => alert('Error loading image.');
imgObj.src = event.target.result;
};
reader.readAsDataURL(file);
});
startCropBtn.addEventListener('click', () => {
if (!imgObj) {
alert('Please load an image first.');
return;
}
isCroppingMode = true;
cropOverlay.style.cursor = 'crosshair';
cropOverlay.style.display = 'block';
});
cropperCanvas.addEventListener('mousedown', (e) => {
if (!isCroppingMode) return;
const rect = cropperCanvas.getBoundingClientRect();
startX = e.clientX - rect.left;
startY = e.clientY - rect.top;
currentCropRect = { x: startX, y: startY, width: 0, height: 0 };
drawCropOverlay();
});
cropperCanvas.addEventListener('mousemove', (e) => {
if (!isCroppingMode) return;
const rect = cropperCanvas.getBoundingClientRect();
const currentX = e.clientX - rect.left;
const currentY = e.clientY - rect.top;
currentCropRect.width = Math.max(0, currentX - startX);
currentCropRect.height = Math.max(0, currentY - startY);
drawCropOverlay();
});
cropperCanvas.addEventListener('mouseup', () => {
if (!isCroppingMode) return;
if (currentCropRect.width > 0 && currentCropRect.height > 0) {
isCroppingMode = false;
cropOverlay.style.cursor = 'default';
applyCropBtn.disabled = false;
} else {
cropOverlay.style.display = 'none';
isCroppingMode = false;
}
});
applyCropBtn.addEventListener('click', () => {
if (!imgObj || currentCropRect.width === 0 || currentCropRect.height === 0) {
alert('No crop area defined or image not loaded.');
return;
}
// Disable button during processing
applyCropBtn.disabled = true;
applyCropBtn.textContent = 'Cropping...';
const croppedCanvas = document.createElement('canvas');
const croppedContext = croppedCanvas.getContext('2d');
const originalWidth = imgObj.width;
const originalHeight = imgObj.height;
const canvasWidth = cropperCanvas.width;
const canvasHeight = cropperCanvas.height;
const cropXRatio = currentCropRect.x / canvasWidth;
const cropYRatio = currentCropRect.y / canvasHeight;
const cropWidthRatio = currentCropRect.width / canvasWidth;
const cropHeightRatio = currentCropRect.height / canvasHeight;
const finalCropX = Math.floor(cropXRatio * originalWidth);
const finalCropY = Math.floor(cropYRatio * originalHeight);
const finalCropWidth = Math.floor(cropWidthRatio * originalWidth);
const finalCropHeight = Math.floor(cropHeightRatio * originalHeight);
croppedCanvas.width = finalCropWidth;
croppedCanvas.height = finalCropHeight;
croppedContext.drawImage(
imgObj,
finalCropX, finalCropY,
finalCropWidth, finalCropHeight,
0, 0,
finalCropWidth, finalCropHeight
);
croppedCanvas.toBlob((blob) => {
if (blob) {
const url = URL.createObjectURL(blob);
const fileName = cropFile.files[0].name.split('.').slice(0, -1).join('.') || 'image';
const fileExt = cropFile.files[0].name.split('.').pop() || 'png';
outputDiv.innerHTML = `
Cropped Image:
Download Cropped Image
`;
outputDiv.classList.remove('hidden');
} else {
outputDiv.innerHTML = `Error generating cropped image.
`;
outputDiv.classList.remove('hidden');
}
// Re-enable button
applyCropBtn.disabled = false;
applyCropBtn.textContent = 'Apply Crop';
}, `image/${fileExt}`);
isCroppingMode = false;
cropOverlay.style.display = 'none';
applyCropBtn.disabled = true;
});
}
},
// --- Calculation Tools ---
"age-calculator": {
title: "Age Calculator",
description: "Calculate age based on date of birth.",
renderModalContent: () => `
Age Calculator
`,
init: () => {
const dobInput = document.getElementById('dob');
const calculateBtn = document.getElementById('calculate-age-btn');
const resultDiv = document.getElementById('age-result');
calculateBtn.addEventListener('click', () => {
const dobValue = dobInput.value;
if (!dobValue) {
resultDiv.innerHTML = `Please enter a date of birth.
`;
resultDiv.classList.remove('hidden');
return;
}
const dob = new Date(dobValue);
const today = new Date();
const birthDate = new Date(dob.getFullYear(), dob.getMonth(), dob.getDate());
let age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
if (isNaN(age) || age < 0) {
resultDiv.innerHTML = `Please enter a valid date of birth.
`;
} else {
resultDiv.innerHTML = `Your age is: ${age} years.
`;
}
resultDiv.classList.remove('hidden');
});
}
},
"emi-calculator": {
title: "EMI Calculator",
description: "Calculate your Equated Monthly Installment.",
renderModalContent: () => `
EMI Calculator
`,
init: () => {
const loanAmountInput = document.getElementById('loan-amount');
const annualInterestRateInput = document.getElementById('annual-interest-rate');
const loanTenureYearsInput = document.getElementById('loan-tenure-years');
const calculateBtn = document.getElementById('calculate-emi-btn');
const resultDiv = document.getElementById('emi-result');
calculateBtn.addEventListener('click', () => {
const principal = parseFloat(loanAmountInput.value);
const annualRate = parseFloat(annualInterestRateInput.value);
const years = parseInt(loanTenureYearsInput.value);
if (isNaN(principal) || isNaN(annualRate) || isNaN(years) || principal <= 0 || annualRate < 0 || years <= 0) {
resultDiv.innerHTML = `Please enter valid loan details (rate can be 0).
`;
resultDiv.classList.remove('hidden');
return;
}
const monthlyRate = (annualRate / 12) / 100;
const tenureMonths = years * 12;
let emi;
if (monthlyRate === 0) {
emi = principal / tenureMonths;
} else {
emi = principal * monthlyRate * Math.pow(1 + monthlyRate, tenureMonths) / (Math.pow(1 + monthlyRate, tenureMonths) - 1);
}
const totalPayment = emi * tenureMonths;
const totalInterest = totalPayment - principal;
resultDiv.innerHTML = `
Your EMI: ₹${emi.toFixed(2)}
Total Payment: ₹${totalPayment.toFixed(2)}
Total Interest: ₹${totalInterest.toFixed(2)}
`;
resultDiv.classList.remove('hidden');
});
}
},
"sip-calculator": {
title: "SIP Calculator",
description: "Calculate your Systematic Investment Plan returns.",
renderModalContent: () => `
SIP Calculator
`,
init: () => {
const sipAmountInput = document.getElementById('sip-amount');
const sipAnnualReturnInput = document.getElementById('sip-annual-return');
const sipYearsInput = document.getElementById('sip-years');
const calculateBtn = document.getElementById('calculate-sip-btn');
const resultDiv = document.getElementById('sip-result');
calculateBtn.addEventListener('click', () => {
const monthlyInvestment = parseFloat(sipAmountInput.value);
const annualReturn = parseFloat(sipAnnualReturnInput.value);
const years = parseInt(sipYearsInput.value);
if (isNaN(monthlyInvestment) || isNaN(annualReturn) || isNaN(years) || monthlyInvestment <= 0 || annualReturn < 0 || years <= 0) {
resultDiv.innerHTML = `Please enter valid SIP details.
`;
resultDiv.classList.remove('hidden');
return;
}
const monthlyRate = (annualReturn / 12) / 100;
const numberOfMonths = years * 12;
let futureValue = 0;
for (let i = 0; i < numberOfMonths; i++) {
futureValue += monthlyInvestment;
futureValue *= (1 + monthlyRate);
}
futureValue = Math.round(futureValue);
const totalInvested = monthlyInvestment * numberOfMonths;
const totalGains = futureValue - totalInvested;
resultDiv.innerHTML = `
Estimated Maturity Amount: ₹${futureValue.toLocaleString()}
Total Investment: ₹${totalInvested.toLocaleString()}
Total Gains: ₹${totalGains.toLocaleString()}
`;
resultDiv.classList.remove('hidden');
});
}
},
// --- QR & Password Tools ---
"qr-generator": {
title: "QR Code Generator",
description: "Generate QR codes from text or URLs.",
renderModalContent: () => `
QR Code Generator
`,
init: () => {
const qrTextInput = document.getElementById('qr-text');
const generateBtn = document.getElementById('generate-qr-btn');
const qrCodeDisplay = document.getElementById('qr-code-display');
const downloadLink = document.querySelector('#qr-code-display').nextElementSibling;
const outputDiv = qrCodeDisplay.closest('.tool-output');
generateBtn.addEventListener('click', () => {
const text = qrTextInput.value.trim();
if (!text) {
alert("Please enter text or a URL to generate a QR code.");
return;
}
const qrCodeUrl = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(text)}`;
qrCodeDisplay.src = qrCodeUrl;
downloadLink.href = qrCodeUrl;
outputDiv.classList.remove('hidden');
});
}
},
"password-generator": {
title: "Password Generator",
description: "Create strong, unique passwords.",
renderModalContent: () => `
Password Generator
`,
init: () => {
const lengthInput = document.getElementById('password-length');
const includeUppercase = document.getElementById('include-uppercase');
const includeLowercase = document.getElementById('include-lowercase');
const includeNumbers = document.getElementById('include-numbers');
const includeSymbols = document.getElementById('include-symbols');
const generateBtn = document.getElementById('generate-password-btn');
const resultDiv = document.getElementById('password-result');
const generatePassword = () => {
const length = parseInt(lengthInput.value);
const uppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const lowercaseChars = "abcdefghijklmnopqrstuvwxyz";
const numberChars = "0123456789";
const symbolChars = "!@#$%^&*()_+[]{}|;:,.<>?";
let allowedChars = "";
if (includeUppercase.checked) allowedChars += uppercaseChars;
if (includeLowercase.checked) allowedChars += lowercaseChars;
if (includeNumbers.checked) allowedChars += numberChars;
if (includeSymbols.checked) allowedChars += symbolChars;
if (allowedChars.length === 0) {
return "Please select at least one character type.";
}
let password = "";
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * allowedChars.length);
password += allowedChars[randomIndex];
}
return password;
};
generateBtn.addEventListener('click', () => {
const password = generatePassword();
resultDiv.innerHTML = `Your generated password:
${password}
`;
resultDiv.classList.remove('hidden');
});
}
},
"word-counter": {
title: "Word Counter",
description: "Count words, characters, and sentences.",
renderModalContent: () => `
Word Counter
`,
init: () => {
const textArea = document.getElementById('text-to-count');
const countBtn = document.getElementById('count-words-btn');
const wordCountEl = document.getElementById('word-count');
const charCountEl = document.getElementById('char-count');
const sentenceCountEl = document.getElementById('sentence-count');
const resultsDiv = document.getElementById('word-count-results');
const countMetrics = (text) => {
const words = text.trim().split(/\s+/).filter(word => word.length > 0).length;
const characters = text.length;
const sentences = (text.match(/[.!?]+/g) || []).length;
return { words, characters, sentences };
};
const updateCounts = () => {
const text = textArea.value;
const metrics = countMetrics(text);
wordCountEl.textContent = metrics.words;
charCountEl.textContent = metrics.characters;
sentenceCountEl.textContent = metrics.sentences;
resultsDiv.classList.remove('hidden');
};
countBtn.addEventListener('click', updateCounts);
textArea.addEventListener('input', updateCounts);
}
},
"base64-tool": {
title: "Base64 Encoder/Decoder",
description: "Encode and decode text using Base64.",
renderModalContent: () => `
Base64 Encoder/Decoder
`,
init: () => {
const textInput = document.getElementById('base64-text');
const encodeBtn = document.getElementById('encode-base64-btn');
const decodeBtn = document.getElementById('decode-base64-btn');
const resultDiv = document.getElementById('base64-result');
encodeBtn.addEventListener('click', () => {
const text = textInput.value;
if (!text) {
alert("Please enter text to encode.");
return;
}
try {
const encoded = btoa(text);
resultDiv.innerHTML = `Encoded Text:
${encoded}
`;
resultDiv.classList.remove('hidden');
} catch (e) {
resultDiv.innerHTML = `Error during encoding: ${e.message}
`;
resultDiv.classList.remove('hidden');
}
});
decodeBtn.addEventListener('click', () => {
const text = textInput.value;
if (!text) {
alert("Please enter Base64 text to decode.");
return;
}
try {
const decoded = atob(text);
resultDiv.innerHTML = `Decoded Text:
${decoded}
`;
resultDiv.classList.remove('hidden');
} catch (e) {
resultDiv.innerHTML = `Error during decoding. Ensure the input is valid Base64. Error: ${e.message}
`;
resultDiv.classList.remove('hidden');
}
});
}
},
"color-picker": {
title: "Color Picker Tool",
description: "Pick colors from your screen or generate codes.",
renderModalContent: () => `
Color Picker
`,
init: () => {
const startPickerBtn = document.getElementById('start-color-picker');
const hexInput = document.getElementById('hex-input');
const rgbInput = document.getElementById('rgb-input');
const hslInput = document.getElementById('hsl-input');
const manualR = document.getElementById('manual-r');
const manualG = document.getElementById('manual-g');
const manualB = document.getElementById('manual-b');
const manualSwatch = document.getElementById('manual-color-swatch');
const copyHexBtn = document.getElementById('copy-hex-btn');
const copyRgbBtn = document.getElementById('copy-rgb-btn');
let pickedColorHex = '';
const rgbToHex = (r, g, b) => {
const toHex = c => {
const hex = c.toString(16);
return hex.length === 1 ? "0" + hex : hex;
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
};
const rgbToHsl = (r, g, b) => {
r /= 255, g /= 255, b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0;
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return [h * 100, s * 100, l * 100];
};
const updateColorDisplay = (r, g, b) => {
const hex = rgbToHex(r, g, b);
const [h, s, l] = rgbToHsl(r, g, b);
hexInput.value = hex.toUpperCase();
rgbInput.value = `rgb(${r}, ${g}, ${b})`;
hslInput.value = `hsl(${h.toFixed(0)}°, ${s.toFixed(0)}%, ${l.toFixed(0)}%)`;
manualSwatch.style.backgroundColor = hex;
pickedColorHex = hex;
};
const inputs = [manualR, manualG, manualB];
inputs.forEach(input => {
input.addEventListener('input', () => {
const r = parseInt(manualR.value) || 0;
const g = parseInt(manualG.value) || 0;
const b = parseInt(manualB.value) || 0;
updateColorDisplay(r, g, b);
});
});
updateColorDisplay(0, 0, 0);
startPickerBtn.addEventListener('click', async () => {
if (!('EyeDropper' in window)) {
alert('Your browser does not support the EyeDropper API for picking colors from the screen.');
return;
}
const eyeDropper = new EyeDropper();
try {
const result = await eyeDropper.open();
const color = result.sRGBHex;
hexInput.value = color.toUpperCase();
const r = parseInt(color.substring(1, 3), 16);
const g = parseInt(color.substring(3, 5), 16);
const b = parseInt(color.substring(5, 7), 16);
rgbInput.value = `rgb(${r}, ${g}, ${b})`;
manualR.value = r;
manualG.value = g;
manualB.value = b;
updateColorDisplay(r, g, b);
} catch (e) {
console.error("EyeDropper API failed:", e);
alert('Color picking failed. Please try again.');
}
});
copyHexBtn.addEventListener('click', () => {
if (navigator.clipboard && pickedColorHex) {
navigator.clipboard.writeText(pickedColorHex.toUpperCase())
.then(() => alert('HEX color copied to clipboard!'))
.catch(err => console.error('Failed to copy HEX: ', err));
} else {
alert('Clipboard API not available or no color selected. Copy manually.');
}
});
copyRgbBtn.addEventListener('click', () => {
if (navigator.clipboard && rgbInput.value) {
navigator.clipboard.writeText(rgbInput.value)
.then(() => alert('RGB color copied to clipboard!'))
.catch(err => console.error('Failed to copy RGB: ', err));
} else {
alert('Clipboard API not available or no color selected. Copy manually.');
}
});
}
},
"text-to-speech": {
title: "Text-to-Speech",
description: "Convert written text into spoken words.",
renderModalContent: () => `
Text-to-Speech
`,
init: () => {
const ttsTextInput = document.getElementById('tts-text');
const ttsVoiceSelect = document.getElementById('tts-voice');
const ttsRateInput = document.getElementById('tts-rate');
const ttsPitchInput = document.getElementById('tts-pitch');
const ttsSpeakBtn = document.getElementById('tts-speak-btn');
const ttsStopBtn = document.getElementById('tts-stop-btn');
const ttsRateValueSpan = document.getElementById('tts-rate-value');
const ttsPitchValueSpan = document.getElementById('tts-pitch-value');
let synth = null;
let utterance = null;
const populateVoiceList = () => {
if (!synth) return;
const voices = synth.getVoices().filter(voice => voice.lang.startsWith('en'));
ttsVoiceSelect.innerHTML = '';
if (voices.length === 0) {
ttsVoiceSelect.innerHTML = '';
return;
}
voices.forEach((voice, i) => {
const option = document.createElement('option');
option.textContent = `${voice.name} (${voice.lang})`;
option.setAttribute('data-lang', voice.lang);
option.setAttribute('data-name', voice.name);
ttsVoiceSelect.appendChild(option);
});
};
const initializeSpeechSynthesis = () => {
if ('speechSynthesis' in window) {
synth = window.speechSynthesis;
if (synth.onvoiceschanged === undefined) {
setTimeout(populateVoiceList, 100);
} else {
synth.onvoiceschanged = populateVoiceList;
}
populateVoiceList();
} else {
alert('Your browser does not support the Speech Synthesis API.');
ttsSpeakBtn.disabled = true;
}
};
initializeSpeechSynthesis();
ttsRateInput.addEventListener('input', () => {
ttsRateValueSpan.textContent = ttsRateInput.value;
});
ttsPitchInput.addEventListener('input', () => {
ttsPitchValueSpan.textContent = ttsPitchInput.value;
});
ttsSpeakBtn.addEventListener('click', () => {
if (!synth) {
alert('Speech synthesis is not available.');
return;
}
if (synth.speaking) {
alert('Already speaking. Please stop first.');
return;
}
const text = ttsTextInput.value;
if (!text) {
alert('Please enter text to speak.');
return;
}
utterance = new SpeechSynthesisUtterance(text);
utterance.rate = parseFloat(ttsRateInput.value);
utterance.pitch = parseFloat(ttsPitchInput.value);
const selectedVoiceOption = ttsVoiceSelect.selectedOptions[0];
if (selectedVoiceOption && !selectedVoiceOption.disabled) {
const voiceName = selectedVoiceOption.getAttribute('data-name');
const voiceLang = selectedVoiceOption.getAttribute('data-lang');
const voices = synth.getVoices();
const selectedSynthVoice = voices.find(voice => voice.name === voiceName && voice.lang === voiceLang);
if (selectedSynthVoice) {
utterance.voice = selectedSynthVoice;
} else {
utterance.lang = voiceLang;
}
} else {
utterance.lang = 'en-US';
}
utterance.onend = () => {
ttsSpeakBtn.disabled = false;
};
utterance.onerror = (event) => {
console.error('SpeechSynthesisUtterance error:', event);
alert(`Speech synthesis error: ${event.error}`);
ttsSpeakBtn.disabled = false;
};
ttsSpeakBtn.disabled = true;
synth.speak(utterance);
});
ttsStopBtn.addEventListener('click', () => {
if (synth && synth.speaking) {
synth.cancel();
ttsSpeakBtn.disabled = false;
}
});
}
},
"speech-to-text": {
title: "Speech to Text",
description: "Convert spoken words into written text.",
renderModalContent: () => `
Speech to Text
Idle
`,
init: () => {
const startBtn = document.getElementById('stt-start-btn');
const stopBtn = document.getElementById('stt-stop-btn');
const statusSpan = document.getElementById('stt-status');
const outputTextArea = document.getElementById('stt-output-text');
const resultDiv = document.getElementById('stt-result');
let recognition = null;
let finalTranscript = '';
const initializeSpeechRecognition = () => {
if (!('webkitSpeechRecognition' in window)) {
alert('Your browser does not support the Speech Recognition API.');
startBtn.disabled = true;
return;
}
recognition = new webkitSpeechRecognition();
recognition.continuous = true;
recognition.interimResults = true;
recognition.lang = 'en-US';
recognition.onstart = () => {
statusSpan.textContent = 'Listening...';
startBtn.disabled = true;
stopBtn.disabled = false;
finalTranscript = '';
outputTextArea.value = '';
};
recognition.onresult = (event) => {
let interimTranscript = '';
for (let i = event.resultIndex; i < event.results.length; ++i) {
if (event.results[i].isFinal) {
finalTranscript += event.results[i][0].transcript + ' ';
} else {
interimTranscript += event.results[i][0].transcript;
}
}
outputTextArea.value = finalTranscript + interimTranscript;
resultDiv.classList.remove('hidden');
};
recognition.onerror = (event) => {
console.error('Speech Recognition Error:', event.error);
statusSpan.textContent = `Error: ${event.error}`;
startBtn.disabled = false;
stopBtn.disabled = true;
if (event.error === 'no-speech' || event.error === 'audio-capture' || event.error === 'not-allowed') {
alert('Could not recognize speech. Please check your microphone permissions or try again.');
}
recognition.stop();
};
recognition.onend = () => {
statusSpan.textContent = 'Idle';
startBtn.disabled = false;
stopBtn.disabled = true;
outputTextArea.value = finalTranscript.trim();
};
};
initializeSpeechRecognition();
startBtn.addEventListener('click', () => {
if (recognition) {
recognition.start();
} else {
alert('Speech recognition not initialized.');
}
});
stopBtn.addEventListener('click', () => {
if (recognition) {
recognition.stop();
statusSpan.textContent = 'Processing...';
}
});
}
},
"json-formatter": {
title: "JSON Formatter",
description: "Pretty-print and validate JSON data.",
renderModalContent: () => `
JSON Formatter & Validator
`,
init: () => {
const jsonInput = document.getElementById('json-input');
const formatBtn = document.getElementById('format-json-btn');
const validateBtn = document.getElementById('validate-json-btn');
const jsonOutputPre = document.getElementById('json-output');
const jsonOutputContainer = document.getElementById('json-output-container');
const validationStatus = document.getElementById('json-validation-status');
formatBtn.addEventListener('click', () => {
const jsonString = jsonInput.value;
try {
const parsedJson = JSON.parse(jsonString);
const formattedJson = JSON.stringify(parsedJson, null, 4);
jsonOutputPre.textContent = formattedJson;
jsonOutputContainer.classList.remove('hidden');
validationStatus.textContent = '';
} catch (e) {
jsonOutputPre.textContent = 'Error: Invalid JSON input.';
jsonOutputContainer.classList.remove('hidden');
validationStatus.textContent = 'Validation Failed!';
validationStatus.style.color = 'red';
}
});
validateBtn.addEventListener('click', () => {
const jsonString = jsonInput.value;
try {
JSON.parse(jsonString);
jsonOutputPre.textContent = 'JSON is valid.';
jsonOutputContainer.classList.remove('hidden');
validationStatus.textContent = 'Valid JSON';
validationStatus.style.color = 'lightgreen';
} catch (e) {
jsonOutputPre.textContent = `Validation Error: ${e.message}`;
jsonOutputContainer.classList.remove('hidden');
validationStatus.textContent = 'Validation Failed!';
validationStatus.style.color = 'red';
}
});
}
},
"unit-converter": {
title: "Unit Converter",
description: "Convert between various units of measurement.",
renderModalContent: () => `
Unit Converter
`,
init: () => {
const conversionTypeSelect = document.getElementById('conversion-type');
const conversionFormDiv = document.getElementById('conversion-form');
const resultDiv = document.getElementById('conversion-result');
const conversionData = {
length: {
name: "Length",
units: {
meter: { name: "Meter (m)", toMeter: 1 }, kilometer: { name: "Kilometer (km)", toMeter: 1000 }, centimeter: { name: "Centimeter (cm)", toMeter: 0.01 }, millimeter: { name: "Millimeter (mm)", toMeter: 0.001 }, mile: { name: "Mile (mi)", toMeter: 1609.34 }, yard: { name: "Yard (yd)", toMeter: 0.9144 }, foot: { name: "Foot (ft)", toMeter: 0.3048 }, inch: { name: "Inch (in)", toMeter: 0.0254 }
}
},
weight: {
name: "Weight",
units: {
kilogram: { name: "Kilogram (kg)", toKg: 1 }, gram: { name: "Gram (g)", toKg: 0.001 }, milligram: { name: "Milligram (mg)", toKg: 0.000001 }, pound: { name: "Pound (lb)", toKg: 0.453592 }, ounce: { name: "Ounce (oz)", toKg: 0.0283495 }
}
},
temperature: {
name: "Temperature",
units: {
celsius: { name: "Celsius (°C)", toCelsius: val => val }, fahrenheit: { name: "Fahrenheit (°F)", toCelsius: f => (f - 32) * 5 / 9 }, kelvin: { name: "Kelvin (K)", toCelsius: k => k - 273.15 }
}
},
speed: {
name: "Speed",
units: {
mps: { name: "Meters per second (m/s)", toMps: 1 }, kph: { name: "Kilometers per hour (km/h)", toMps: val => val * 1000 / 3600 }, mph: { name: "Miles per hour (mph)", toMps: val => val * 1609.34 / 3600 }, fps: { name: "Feet per second (ft/s)", toMps: val => val * 0.3048 }
}
},
volume: {
name: "Volume",
units: {
liter: { name: "Liter (L)", toLiter: 1 }, milliliter: { name: "Milliliter (mL)", toLiter: 0.001 }, gallon: { name: "Gallon (US)", toLiter: 3.78541 }, quart: { name: "Quart (US)", toLiter: 0.946353 }, pint: { name: "Pint (US)", toLiter: 0.473176 }, cup: { name: "Cup (US)", toLiter: 0.24 }, fluidounce: { name: "Fluid Ounce (US)", toLiter: 0.0295735 }
}
}
};
const renderForm = (type) => {
const config = conversionData[type];
if (!config) return '';
let formHtml = `
`;
return formHtml;
};
const performConversion = () => {
const type = conversionTypeSelect.value;
const config = conversionData[type];
const fromUnitKey = document.getElementById('from-unit').value;
const toUnitKey = document.getElementById('to-unit').value;
const value = parseFloat(document.getElementById('conversion-value').value);
if (!config || !fromUnitKey || !toUnitKey || isNaN(value)) {
resultDiv.innerHTML = `Please enter valid inputs.
`;
resultDiv.classList.remove('hidden');
return;
}
let result = '';
try {
const fromUnit = config.units[fromUnitKey];
const toUnit = config.units[toUnitKey];
let valueInBaseUnit;
let resultInTargetUnit;
if (type === 'temperature') {
valueInBaseUnit = fromUnit.toCelsius(value);
resultInTargetUnit = toUnit.toCelsius === undefined ? valueInBaseUnit : toUnit.toCelsius(valueInBaseUnit);
} else {
const fromMultiplier = fromUnit[`to${type.charAt(0).toUpperCase() + type.slice(1)}`];
const toMultiplier = toUnit[`to${type.charAt(0).toUpperCase() + type.slice(1)}`];
if (fromMultiplier === undefined || toMultiplier === undefined) {
throw new Error("Multiplier not found for unit.");
}
valueInBaseUnit = value * fromMultiplier;
resultInTargetUnit = valueInBaseUnit / toMultiplier;
}
const fromUnitName = fromUnit.name.split('(')[0].trim();
const toUnitName = toUnit.name.split('(')[0].trim();
result = `${value} ${fromUnitName} = ${resultInTargetUnit.toFixed(6)} ${toUnitName}`;
resultDiv.innerHTML = `${result}
`;
resultDiv.classList.remove('hidden');
} catch (e) {
console.error("Conversion error:", e);
resultDiv.innerHTML = `Conversion failed. Check units and values.
`;
resultDiv.classList.remove('hidden');
}
};
conversionFormDiv.innerHTML = renderForm(conversionTypeSelect.value);
document.getElementById('convert-units-btn').addEventListener('click', performConversion);
conversionTypeSelect.addEventListener('change', () => {
conversionFormDiv.innerHTML = renderForm(conversionTypeSelect.value);
document.getElementById('convert-units-btn').addEventListener('click', performConversion);
});
}
},
"bmi-calculator": {
title: "BMI Calculator",
description: "Calculate your Body Mass Index.",
renderModalContent: () => `
BMI Calculator
`,
init: () => {
const heightInput = document.getElementById('bmi-height');
const weightInput = document.getElementById('bmi-weight');
const calculateBtn = document.getElementById('calculate-bmi-btn');
const resultDiv = document.getElementById('bmi-result');
calculateBtn.addEventListener('click', () => {
const height = parseFloat(heightInput.value);
const weight = parseFloat(weightInput.value);
if (isNaN(height) || isNaN(weight) || height <= 0 || weight <= 0) {
resultDiv.innerHTML = `Please enter valid height and weight.
`;
resultDiv.classList.remove('hidden');
return;
}
const bmi = weight / (height * height);
let interpretation = '';
if (bmi < 18.5) interpretation = 'Underweight';
else if (bmi >= 18.5 && bmi < 25) interpretation = 'Normal weight';
else if (bmi >= 25 && bmi < 30) interpretation = 'Overweight';
else interpretation = 'Obesity';
resultDiv.innerHTML = `Your BMI is: ${bmi.toFixed(2)} (${interpretation})
`;
resultDiv.classList.remove('hidden');
});
}
},
"timer-stopwatch": {
title: "Timer / Stopwatch",
description: "Track time with a versatile timer and stopwatch.",
renderModalContent: () => `
Timer / Stopwatch
00:00:00
`,
init: () => {
// Stopwatch elements
const stopwatchDisplay = document.getElementById('stopwatch-display');
const startStopwatchBtn = document.getElementById('start-stopwatch-btn');
const stopStopwatchBtn = document.getElementById('stop-stopwatch-btn');
const resetStopwatchBtn = document.getElementById('reset-stopwatch-btn');
const lapStopwatchBtn = document.getElementById('lap-stopwatch-btn');
const lapList = document.getElementById('lap-list');
let stopwatchInterval = null;
let startTime = 0;
let elapsedTime = 0;
let lapStartTime = 0;
let laps = [];
// Timer elements
const timerDurationInput = document.getElementById('timer-duration');
const startTimerBtn = document.getElementById('start-timer-btn');
const stopTimerBtn = document.getElementById('stop-timer-btn');
const resetTimerBtn = document.getElementById('reset-timer-btn');
const timerInfoDiv = document.getElementById('timer-info');
let timerInterval = null;
let timerRemainingTime = 0;
const formatTime = (ms) => {
const totalSeconds = Math.floor(ms / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
};
// --- Stopwatch Logic ---
const updateStopwatchDisplay = () => {
const currentTime = Date.now();
elapsedTime = currentTime - startTime;
stopwatchDisplay.textContent = formatTime(elapsedTime);
};
startStopwatchBtn.addEventListener('click', () => {
if (stopwatchInterval === null) {
startTime = Date.now() - elapsedTime;
stopwatchInterval = setInterval(updateStopwatchDisplay, 10);
lapStartTime = Date.now() - elapsedTime;
startStopwatchBtn.textContent = 'Resume';
startStopwatchBtn.disabled = true;
stopStopwatchBtn.disabled = false;
lapStopwatchBtn.disabled = false;
}
});
stopStopwatchBtn.addEventListener('click', () => {
if (stopwatchInterval !== null) {
clearInterval(stopwatchInterval);
stopwatchInterval = null;
startStopwatchBtn.textContent = 'Resume';
startStopwatchBtn.disabled = false;
stopStopwatchBtn.disabled = true;
lapStopwatchBtn.disabled = true;
}
});
resetStopwatchBtn.addEventListener('click', () => {
clearInterval(stopwatchInterval);
stopwatchInterval = null;
startTime = 0;
elapsedTime = 0;
lapStartTime = 0;
laps = [];
stopwatchDisplay.textContent = '00:00:00';
lapList.innerHTML = '';
startStopwatchBtn.textContent = 'Start';
startStopwatchBtn.disabled = false;
stopStopwatchBtn.disabled = true;
lapStopwatchBtn.disabled = true;
});
lapStopwatchBtn.addEventListener('click', () => {
if (stopwatchInterval !== null) {
const lapTime = Date.now() - lapStartTime;
laps.push({ time: lapTime, number: laps.length + 1 });
const lapItem = document.createElement('li');
lapItem.textContent = `Lap ${laps[laps.length - 1].number}: ${formatTime(lapTime)}`;
lapList.appendChild(lapItem);
lapStartTime = Date.now();
}
});
// --- Timer Logic ---
const updateTimerDisplay = () => {
if (timerRemainingTime <= 0) {
clearInterval(timerInterval);
timerInterval = null;
timerInfoDiv.innerHTML = `Time's up!
`;
timerInfoDiv.classList.remove('hidden');
startTimerBtn.disabled = false;
stopTimerBtn.disabled = true;
alert("Time's up!");
return;
}
timerRemainingTime -= 10;
const formatted = formatTime(timerRemainingTime);
timerInfoDiv.innerHTML = `Time Remaining: ${formatted}
`;
timerInfoDiv.classList.remove('hidden');
};
startTimerBtn.addEventListener('click', () => {
if (timerInterval === null) {
if (timerRemainingTime <= 0) {
timerRemainingTime = parseInt(timerDurationInput.value) * 1000 || 60000;
}
timerInfoDiv.innerHTML = `Time Remaining: ${formatTime(timerRemainingTime)}
`;
timerInfoDiv.classList.remove('hidden');
timerInterval = setInterval(updateTimerDisplay, 10);
startTimerBtn.disabled = true;
stopTimerBtn.disabled = false;
}
});
stopTimerBtn.addEventListener('click', () => {
if (timerInterval !== null) {
clearInterval(timerInterval);
timerInterval = null;
startTimerBtn.disabled = false;
stopTimerBtn.disabled = true;
}
});
resetTimerBtn.addEventListener('click', () => {
clearInterval(timerInterval);
timerInterval = null;
timerRemainingTime = 0;
timerInfoDiv.innerHTML = '';
timerInfoDiv.classList.add('hidden');
startTimerBtn.disabled = false;
stopTimerBtn.disabled = true;
timerDurationInput.value = 60;
});
// --- Mode Switching ---
const timerModeBtn = document.getElementById('timer-mode-btn');
const stopwatchModeBtn = document.getElementById('stopwatch-mode-btn');
const timerControls = document.getElementById('timer-controls');
const stopwatchControls = document.getElementById('stopwatch-controls');
timerModeBtn.addEventListener('click', () => {
timerControls.style.display = 'block';
stopwatchControls.style.display = 'none';
resetStopwatchBtn.click();
});
stopwatchModeBtn.addEventListener('click', () => {
stopwatchControls.style.display = 'block';
timerControls.style.display = 'none';
resetTimerBtn.click();
});
// Ensure stopwatch is visible by default
stopwatchControls.style.display = 'block';
timerControls.style.display = 'none';
}
},
// --- Media Tools ---
"video-converter": {
title: "Video Converter",
description: "Convert video files between formats.",
renderModalContent: () => `
Video Converter
`,
init: () => {
const videoFile = document.getElementById('video-file');
const outputFormat = document.getElementById('output-video-format');
const convertBtn = document.getElementById('convert-video-btn');
const outputDiv = document.getElementById('video-conversion-output');
convertBtn.addEventListener('click', () => {
const file = videoFile.files[0];
const format = outputFormat.value;
if (!file) {
alert("Please select a video file first!");
return;
}
// Disable button during processing
convertBtn.disabled = true;
convertBtn.textContent = 'Converting...';
const reader = new FileReader();
reader.onload = (e) => {
const convertedBlob = new Blob([e.target.result], { type: `video/${format}` });
const convertedUrl = URL.createObjectURL(convertedBlob);
const canPlay = VideoElement.canPlayType(`video/${format}`);
if (canPlay !== "" && convertedUrl) {
outputDiv.innerHTML = `
Browser might support ${format}. Here's the file:
Download ${format.toUpperCase()}
`;
outputDiv.classList.remove('hidden');
} else {
outputDiv.innerHTML = `Browser does not natively support direct conversion or playback of ${format}. For comprehensive video conversion, consider libraries like FFmpeg.js.
`;
outputDiv.classList.remove('hidden');
}
// Re-enable button
convertBtn.disabled = false;
convertBtn.textContent = 'Convert';
};
reader.onerror = () => {
outputDiv.innerHTML = `Error reading file.
`;
outputDiv.classList.remove('hidden');
// Re-enable button
convertBtn.disabled = false;
convertBtn.textContent = 'Convert';
};
reader.readAsArrayBuffer(file);
});
}
},
"audio-converter": {
title: "Audio Converter",
description: "Convert audio files between formats.",
renderModalContent: () => `
Audio Converter
`,
init: () => {
const audioFile = document.getElementById('audio-file');
const outputFormat = document.getElementById('output-audio-format');
const convertBtn = document.getElementById('convert-audio-btn');
const outputDiv = document.getElementById('audio-conversion-output');
const bufferToWav = (audioBuffer) => { // Re-defining helper locally for clarity
const numChannels = audioBuffer.numberOfChannels;
const sampleRate = audioBuffer.sampleRate;
const format = 1; // PCM format
const bitDepth = 16; // 16-bit
const frameLength = audioBuffer.length;
const blockAlign = numChannels * (bitDepth / 8);
const byteRate = sampleRate * blockAlign;
const bufferLength = frameLength * blockAlign;
const wavLength = 44 + bufferLength;
const wav = new Uint8Array(wavLength);
const view = new DataView(wav.buffer);
const writeString = (offset, string) => {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
};
writeString(0, 'RIFF');
view.setUint32(4, wavLength, true);
writeString(8, 'WAVE');
writeString(12, 'fmt ');
view.setUint32(16, 16, true);
view.setUint16(20, format, true);
view.setUint16(22, numChannels, true);
view.setUint32(24, sampleRate, true);
view.setUint32(28, byteRate, true);
view.setUint16(32, blockAlign, true);
view.setUint16(34, bitDepth, true);
writeString(36, 'data');
view.setUint32(40, bufferLength, true);
for (let i = 0; i < frameLength; i++) {
for (let channel = 0; channel < numChannels; channel++) {
const sample = audioBuffer.getChannelData(channel)[i];
const sample16 = Math.max(-1, Math.min(1, sample));
view.setInt16(44 + (i * blockAlign + channel * (bitDepth / 8)), sample16 < 0 ? sample16 * 0x8000 : sample16 * 0x7FFF, true);
}
}
return new Blob([wav], { type: 'audio/wav' });
};
convertBtn.addEventListener('click', () => {
const file = audioFile.files[0];
const format = outputFormat.value;
if (!file) {
alert("Please select an audio file first!");
return;
}
// Disable button during processing
convertBtn.disabled = true;
convertBtn.textContent = 'Converting...';
const reader = new FileReader();
reader.onload = (e) => {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
audioContext.decodeAudioData(e.target.result, (buffer) => {
if (format === 'wav') {
const wavBlob = bufferToWav(buffer);
const wavUrl = URL.createObjectURL(wavBlob);
outputDiv.innerHTML = `
Converted to WAV:
Download WAV
`;
outputDiv.classList.remove('hidden');
} else {
outputDiv.innerHTML = `Conversion to ${format.toUpperCase()} is not directly supported by this basic example. WAV is supported.
`;
outputDiv.classList.remove('hidden');
}
// Re-enable button
convertBtn.disabled = false;
convertBtn.textContent = 'Convert';
}, (err) => {
console.error("Error decoding audio:", err);
outputDiv.innerHTML = `Error decoding audio file. It might be in an unsupported format.
`;
outputDiv.classList.remove('hidden');
// Re-enable button
convertBtn.disabled = false;
convertBtn.textContent = 'Convert';
});
};
reader.onerror = () => {
outputDiv.innerHTML = `Error reading file.
`;
outputDiv.classList.remove('hidden');
// Re-enable button
convertBtn.disabled = false;
convertBtn.textContent = 'Convert';
};
reader.readAsArrayBuffer(file);
});
}
},
"audio-trimmer": {
title: "Audio Trimmer",
description: "Trim and cut audio files with ease.",
renderModalContent: () => `
Audio Trimmer
`,
init: () => {
const trimAudioFile = document.getElementById('trim-audio-file');
const trimStartInput = document.getElementById('trim-start-time');
const trimEndInput = document.getElementById('trim-end-time');
const trimBtn = document.getElementById('trim-audio-btn');
const outputDiv = document.getElementById('audio-trimming-output');
const bufferToWav = (audioBuffer) => { // Re-using helper
const numChannels = audioBuffer.numberOfChannels;
const sampleRate = audioBuffer.sampleRate;
const format = 1; // PCM format
const bitDepth = 16; // 16-bit
const frameLength = audioBuffer.length;
const blockAlign = numChannels * (bitDepth / 8);
const byteRate = sampleRate * blockAlign;
const bufferLength = frameLength * blockAlign;
const wavLength = 44 + bufferLength;
const wav = new Uint8Array(wavLength);
const view = new DataView(wav.buffer);
const writeString = (offset, string) => {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
};
writeString(0, 'RIFF');
view.setUint32(4, wavLength, true);
writeString(8, 'WAVE');
writeString(12, 'fmt ');
view.setUint32(16, 16, true);
view.setUint16(20, format, true);
view.setUint16(22, numChannels, true);
view.setUint32(24, sampleRate, true);
view.setUint32(28, byteRate, true);
view.setUint16(32, blockAlign, true);
view.setUint16(34, bitDepth, true);
writeString(36, 'data');
view.setUint32(40, bufferLength, true);
for (let i = 0; i < frameLength; i++) {
for (let channel = 0; channel < numChannels; channel++) {
const sample = audioBuffer.getChannelData(channel)[i];
const sample16 = Math.max(-1, Math.min(1, sample));
view.setInt16(44 + (i * blockAlign + channel * (bitDepth / 8)), sample16 < 0 ? sample16 * 0x8000 : sample16 * 0x7FFF, true);
}
}
return new Blob([wav], { type: 'audio/wav' });
};
trimBtn.addEventListener('click', () => {
const file = trimAudioFile.files[0];
const startTime = parseFloat(trimStartInput.value);
const endTime = parseFloat(trimEndInput.value);
if (!file) {
alert("Please select an audio file first!");
return;
}
if (isNaN(startTime) || isNaN(endTime) || startTime < 0 || endTime <= startTime) {
alert("Please enter valid start and end times (end time must be greater than start time).");
return;
}
// Disable button during processing
trimBtn.disabled = true;
trimBtn.textContent = 'Trimming...';
const reader = new FileReader();
reader.onload = (e) => {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
audioContext.decodeAudioData(e.target.result, (buffer) => {
const originalSampleRate = buffer.sampleRate;
const originalNumChannels = buffer.numberOfChannels;
const startSample = Math.floor(startTime * originalSampleRate);
const endSample = Math.floor(endTime * originalSampleRate);
const trimDurationSamples = endSample - startSample;
if (trimDurationSamples <= 0 || startSample >= buffer.length) {
outputDiv.innerHTML = `Invalid trim range or exceeds audio length.
`;
outputDiv.classList.remove('hidden');
trimBtn.disabled = false;
trimBtn.textContent = 'Trim Audio';
return;
}
const trimmedBuffer = audioContext.createBuffer(originalNumChannels, trimDurationSamples, originalSampleRate);
for (let channel = 0; channel < originalNumChannels; channel++) {
const outputData = trimmedBuffer.getChannelData(channel);
const inputData = buffer.getChannelData(channel);
for (let i = 0; i < trimDurationSamples; i++) {
outputData[i] = inputData[startSample + i];
}
}
const wavBlob = bufferToWav(trimmedBuffer);
const wavUrl = URL.createObjectURL(wavBlob);
outputDiv.innerHTML = `
Trimmed audio (WAV format):
Download WAV
`;
outputDiv.classList.remove('hidden');
// Re-enable button
trimBtn.disabled = false;
trimBtn.textContent = 'Trim Audio';
}, (err) => {
console.error("Error decoding audio:", err);
outputDiv.innerHTML = `Error decoding audio file. It might be in an unsupported format.
`;
outputDiv.classList.remove('hidden');
// Re-enable button
trimBtn.disabled = false;
trimBtn.textContent = 'Trim Audio';
});
};
reader.onerror = () => {
outputDiv.innerHTML = `Error reading file.
`;
outputDiv.classList.remove('hidden');
// Re-enable button
trimBtn.disabled = false;
trimBtn.textContent = 'Trim Audio';
};
reader.readAsArrayBuffer(file);
});
}
},
// --- Social Media & Content Tools ---
"yt-title-gen": {
title: "YouTube Title Generator",
description: "Generate catchy titles for your YouTube videos.",
renderModalContent: () => `
YouTube Title Generator
`,
init: () => {
const ytTopicInput = document.getElementById('yt-topic');
const generateBtn = document.getElementById('generate-yt-title-btn');
const titleList = document.getElementById('yt-title-list');
const resultsDiv = document.getElementById('yt-title-results');
const titles = [
"How To", "Ultimate Guide", "Top 5", "Secrets", "Beginner's Guide",
"Masterclass", "Explained", "DIY", "Review", "Tutorial"
];
const adjectives = [
"Amazing", "Incredible", "Essential", "Powerful", "Simple",
"Effective", "Easy", "Fast", "Creative", "Innovative"
];
const powerWords = [
"Boost", "Transform", "Unlock", "Discover", "Achieve",
"Master", "Create", "Improve", "Enhance", "Dominate"
];
generateBtn.addEventListener('click', () => {
const topic = ytTopicInput.value.trim();
if (!topic) {
alert('Please enter a video topic or keywords.');
return;
}
titleList.innerHTML = '';
const generatedTitles = new Set();
generatedTitles.add(`"${topic}" - The Complete Guide`);
generatedTitles.add(`Learning "${topic}" For Beginners`);
generatedTitles.add(`Easy Ways to Master "${topic}"`);
for (let i = 0; i < 10; i++) {
let title = '';
const randomTemplate = Math.floor(Math.random() * 4);
switch(randomTemplate) {
case 0:
title = `${adjectives[Math.floor(Math.random() * adjectives.length)]} "${topic}" - How To ${powerWords[Math.floor(Math.random() * powerWords.length)]}`;
break;
case 1:
title = `"${topic}" Masterclass: Learn Everything`;
break;
case 2:
title = `Top ${Math.floor(Math.random() * 5) + 1} "${topic}" ${titles[Math.floor(Math.random() * titles.length)]} Tutorial`;
break;
case 3:
title = `How to ${powerWords[Math.floor(Math.random() * powerWords.length)]} Your "${topic}"`;
break;
}
generatedTitles.add(title.trim());
}
generatedTitles.forEach(t => {
const listItem = document.createElement('li');
listItem.innerHTML = `${t}
`;
titleList.appendChild(listItem);
});
resultsDiv.classList.remove('hidden');
});
}
},
"yt-channel-name-gen": {
title: "YouTube Channel Name Generator",
description: "Find the perfect name for your YouTube channel.",
renderModalContent: () => `
YouTube Channel Name Generator
`,
init: () => {
const ytChannelKeywordsInput = document.getElementById('yt-channel-keywords');
const generateBtn = document.getElementById('generate-yt-channel-btn');
const channelList = document.getElementById('yt-channel-list');
const resultsDiv = document.getElementById('yt-channel-results');
const prefixes = ["The", "Pro", "Master", "Mega", "Epic", "Creative", "Digital", "Global", "Awesome", "Smart", "Elite", "Vivid", "Zen"];
const suffixes = ["Hub", "Zone", "Verse", "Empire", "Labs", "Studio", "Nation", "Guru", "Geek", "Voyage", "Sphere", "Realm", "Dynamics"];
const descriptiveWords = ["Gamer", "Techie", "Vlogger", "Creator", "Explorer", "Innovator", "Designer", "Builder", "Analyst", "Artist", "Visionary", "Mind"];
generateBtn.addEventListener('click', () => {
const keywords = ytChannelKeywordsInput.value.trim().split(',').map(k => k.trim()).filter(k => k.length > 0);
if (keywords.length === 0) {
alert('Please enter some keywords.');
return;
}
channelList.innerHTML = '';
const generatedNames = new Set();
const capitalizeWords = (str) => str.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
keywords.forEach(kw => {
const capitalizedKw = capitalizeWords(kw);
generatedNames.add(capitalizedKw);
prefixes.forEach(p => generatedNames.add(`${p} ${capitalizedKw}`));
suffixes.forEach(s => generatedNames.add(`${capitalizedKw} ${s}`));
descriptiveWords.forEach(d => generatedNames.add(`${capitalizedKw} ${d}`));
prefixes.forEach(p => suffixes.forEach(s => generatedNames.add(`${p} ${capitalizedKw} ${s}`)));
});
for(let i=0; i < 15; i++) {
let randomName = '';
const template = Math.floor(Math.random() * 6);
const randKeyword = keywords[Math.floor(Math.random()*keywords.length)];
switch(template) {
case 0: randomName = `${prefixes[Math.floor(Math.random()*prefixes.length)]} ${descriptiveWords[Math.floor(Math.random()*descriptiveWords.length)]}`; break;
case 1: randomName = `${descriptiveWords[Math.floor(Math.random()*descriptiveWords.length)]} ${suffixes[Math.floor(Math.random()*suffixes.length)]}`; break;
case 2: randomName = `${prefixes[Math.floor(Math.random()*prefixes.length)]} ${capitalizeWords(randKeyword)}`; break;
case 3: randomName = `${capitalizeWords(randKeyword)} ${suffixes[Math.floor(Math.random()*suffixes.length)]}`; break;
case 4: randomName = `${prefixes[Math.floor(Math.random()*prefixes.length)]} ${capitalizeWords(randKeyword)} ${suffixes[Math.floor(Math.random()*suffixes.length)]}`; break;
case 5: randomName = `${capitalizeWords(randKeyword)} ${descriptiveWords[Math.floor(Math.random()*descriptiveWords.length)]}`; break;
}
generatedNames.add(capitalizeWords(randomName.trim()));
}
generatedNames.forEach(name => {
const listItem = document.createElement('li');
listItem.innerHTML = `${name}
`;
channelList.appendChild(listItem);
});
resultsDiv.classList.remove('hidden');
});
}
},
"yt-embed-code-gen": {
title: "YouTube Embed Code Generator",
description: "Generate embed codes for YouTube videos.",
renderModalContent: () => `
YouTube Embed Code Generator
`,
init: () => {
const videoUrlInput = document.getElementById('yt-video-url');
const autoplayCheckbox = document.getElementById('yt-autoplay');
const controlsCheckbox = document.getElementById('yt-controls');
const loopCheckbox = document.getElementById('yt-loop');
const muteCheckbox = document.getElementById('yt-mute');
const generateBtn = document.getElementById('generate-yt-embed-btn');
const embedCodeTextarea = document.getElementById('yt-embed-code');
const copyEmbedBtn = document.getElementById('copy-embed-code-btn');
const outputDiv = document.getElementById('yt-embed-output');
generateBtn.addEventListener('click', () => {
const url = videoUrlInput.value.trim();
if (!url) {
alert('Please enter a YouTube video URL.');
return;
}
const videoIdMatch = url.match(/(?:youtube\.com\/(?:[^/]+\/.+\/.+\/|(?:v|e)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/);
if (!videoIdMatch || !videoIdMatch[1]) {
alert('Invalid YouTube URL. Please enter a valid video URL.');
return;
}
const videoId = videoIdMatch[1];
let embedUrl = `https://www.youtube.com/embed/${videoId}`;
const params = [];
if (autoplayCheckbox.checked) params.push('autoplay=1');
if (!controlsCheckbox.checked) params.push('controls=0');
if (loopCheckbox.checked) params.push('loop=1');
if (loopCheckbox.checked && videoId) params.push(`playlist=${videoId}`);
if (muteCheckbox.checked) params.push('mute=1');
if (params.length > 0) {
embedUrl += `?${params.join('&')}`;
}
const iframeCode = `
`.trim();
embedCodeTextarea.value = iframeCode;
outputDiv.classList.remove('hidden');
});
copyEmbedBtn.addEventListener('click', () => {
if (navigator.clipboard && embedCodeTextarea.value) {
navigator.clipboard.writeText(embedCodeTextarea.value)
.then(() => alert('Embed code copied to clipboard!'))
.catch(err => console.error('Failed to copy embed code: ', err));
} else {
alert('Clipboard API not available. Copy manually.');
}
});
}
},
"yt-hashtag-gen": {
title: "YouTube Hashtag Generator",
description: "Generate relevant hashtags for YouTube.",
renderModalContent: () => `
YouTube Hashtag Generator
`,
init: () => {
const keywordsInput = document.getElementById('yt-hashtag-keywords');
const generateBtn = document.getElementById('generate-yt-hashtag-btn');
const hashtagsResultsDiv = document.getElementById('yt-hashtag-results');
const outputDiv = document.getElementById('yt-hashtag-output');
const commonCategories = [
"DIY", "Tutorial", "Review", "Unboxing", "Vlog", "Tips", "Tricks", "Guide", "HowTo", "LifeHacks",
"Tech", "Gaming", "Beauty", "Fashion", "Food", "Travel", "Fitness", "Music", "Art", "Comedy"
];
const trendSuffixes = ["Trend", "Viral", "Popular", "Now", "Latest", "Challenge"];
generateBtn.addEventListener('click', () => {
const keywords = keywordsInput.value.trim().toLowerCase().split(/[\s,]+/).filter(k => k.length > 0);
if (keywords.length === 0) {
alert('Please enter video topic or keywords.');
return;
}
hashtagsResultsDiv.innerHTML = '';
const generatedHashtags = new Set();
const capitalizeWords = (str) => str.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
keywords.forEach(kw => {
generatedHashtags.add(`#${capitalizeWords(kw).replace(/\s+/g, '')}`);
});
commonCategories.forEach(cat => {
keywords.forEach(kw => {
generatedHashtags.add(`#${cat}${capitalizeWords(kw).replace(/\s+/g, '')}`);
generatedHashtags.add(`#${capitalizeWords(kw).replace(/\s+/g, '')}${cat}`);
});
});
commonCategories.forEach(cat => generatedHashtags.add(`#${cat}`));
keywords.forEach(kw => {
trendSuffixes.forEach(ts => generatedHashtags.add(`#${capitalizeWords(kw).replace(/\s+/g, '')}${ts}`));
});
const hashtagsArray = Array.from(generatedHashtags);
hashtagsArray.forEach((tag, index) => {
const tagSpan = document.createElement('span');
tagSpan.textContent = tag;
tagSpan.classList.add('hashtag-tag');
tagSpan.style.cssText = `
background-color: var(--button-normal-bg);
border: 1px solid var(--primary-accent);
padding: 5px 10px;
border-radius: 4px;
display: inline-block;
cursor: pointer;
margin-bottom: 5px;
transition: background-color var(--transition-speed-fast) ease, box-shadow var(--transition-speed-fast) ease;
`;
tagSpan.addEventListener('mouseover', () => tagSpan.style.boxShadow = `0 0 8px var(--glow-shadow)`);
tagSpan.addEventListener('mouseout', () => tagSpan.style.boxShadow = 'none');
tagSpan.addEventListener('click', () => {
navigator.clipboard.writeText(tagSpan.textContent)
.then(() => alert(`'${tagSpan.textContent}' copied!`))
.catch(err => console.error('Failed to copy tag: ', err));
});
hashtagsResultsDiv.appendChild(tagSpan);
});
outputDiv.classList.remove('hidden');
});
}
},
"fb-page-name-gen": {
title: "Facebook Page Name Generator",
description: "Brainstorm names for your Facebook pages.",
renderModalContent: () => `
Facebook Page Name Generator
`,
init: () => {
const keywordsInput = document.getElementById('fb-page-keywords');
const generateBtn = document.getElementById('generate-fb-page-btn');
const pageList = document.getElementById('fb-page-list');
const resultsDiv = document.getElementById('fb-page-results');
const pageTypes = ["Official", "Community", "Reviews", "Tips", "Daily", "News", "Hub", "Spotlight", "Zone", "Central"];
const descriptors = ["Best", "Top", "Local", "Expert", "Creative", "Digital", "Modern", "Fresh", "Ultimate", "Global"];
generateBtn.addEventListener('click', () => {
const keywords = keywordsInput.value.trim().split(',').map(k => k.trim()).filter(k => k.length > 0);
if (keywords.length === 0) {
alert('Please enter page niche or keywords.');
return;
}
pageList.innerHTML = '';
const generatedNames = new Set();
const capitalizeWords = (str) => str.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
keywords.forEach(kw => {
const capitalizedKw = capitalizeWords(kw);
generatedNames.add(capitalizedKw);
descriptors.forEach(d => generatedNames.add(`${d} ${capitalizedKw}`));
pageTypes.forEach(pt => generatedNames.add(`${capitalizedKw} ${pt}`));
descriptors.forEach(d => pageTypes.forEach(pt => generatedNames.add(`${d} ${capitalizedKw} ${pt}`)));
});
generatedNames.add(`Your ${capitalizeWords(keywords[0])} Page`);
generatedNames.add(`The ${capitalizeWords(keywords[0])} Hub`);
generatedNames.add(`${capitalizeWords(keywords[0])} Central`);
generatedNames.forEach(name => {
const listItem = document.createElement('li');
listItem.innerHTML = `${name}
`;
pageList.appendChild(listItem);
});
resultsDiv.classList.remove('hidden');
});
}
},
"fb-page-title-gen": {
title: "Facebook Page Title Generator",
description: "Create engaging titles for Facebook posts.",
renderModalContent: () => `
Facebook Post Title Generator
`,
init: () => {
const topicInput = document.getElementById('fb-post-topic');
const generateBtn = document.getElementById('generate-fb-post-btn');
const postList = document.getElementById('fb-post-list');
const resultsDiv = document.getElementById('fb-post-results');
const hooks = ["Exciting News:", "Big Announcement:", "Don't Miss Out:", "Limited Time:", "Check This Out:", "Must-Read:", "Exclusive:", "Get Ready for:"];
const callsToAction = ["Learn More!", "Shop Now!", "Visit Us!", "Click Here!", "Get Yours Today!", "Find Out How!"];
generateBtn.addEventListener('click', () => {
const topic = topicInput.value.trim();
if (!topic) {
alert('Please enter the post topic or content.');
return;
}
postList.innerHTML = '';
const generatedTitles = new Set();
const capitalizeWords = (str) => str.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
generatedTitles.add(`Introducing: ${capitalizeWords(topic)}`);
generatedTitles.add(`Discover Our New ${capitalizeWords(topic)}`);
generatedTitles.add(`Your Guide to ${capitalizeWords(topic)}`);
generatedTitles.add(`Everything You Need to Know About ${capitalizeWords(topic)}`);
hooks.forEach(hook => generatedTitles.add(`${hook} ${capitalizeWords(topic)}!`));
generatedTitles.add(`${capitalizeWords(topic)} - ${callsToAction[Math.floor(Math.random() * callsToAction.length)]}`);
generatedTitles.add(`The Ultimate ${capitalizeWords(topic)} You Need to See!`);
generatedTitles.add(`Unlock the Secrets of ${capitalizeWords(topic)}`);
generatedTitles.add(`Why ${capitalizeWords(topic)} Matters: Our Latest Insights`);
generatedTitles.forEach(title => {
const listItem = document.createElement('li');
listItem.innerHTML = `${title}
`;
postList.appendChild(listItem);
});
resultsDiv.classList.remove('hidden');
});
}
},
"fb-caption-gen": {
title: "Facebook Caption Generator",
description: "Generate creative captions for Facebook.",
renderModalContent: () => `
Facebook Caption Generator
`,
init: () => {
const keywordsInput = document.getElementById('fb-caption-keywords');
const styleSelect = document.getElementById('fb-caption-style');
const generateBtn = document.getElementById('generate-fb-caption-btn');
const captionList = document.getElementById('fb-caption-list');
const resultsDiv = document.getElementById('fb-caption-output');
const captions = {
fun: [
"Living my best life! 😎", "Making memories.", "Good times and tan lines. ☀️",
"This is the good stuff!", "Making the most of it!", "Feeling good!", "Weekend vibes!",
"Soaking up the sun!", "Can't get enough of this!", "Pure joy!"
],
inspirational: [
"Dream big, work hard.", "Chasing sunsets and dreams.", "Adventure awaits.",
"Believe in yourself.", "Growth mindset.", "Finding joy in the journey.", "Push your limits.",
"The future is bright.", "One step at a time.", "Be the change."
],
short: [
"Bliss.", "Paradise.", "Vibes.", "Mood.", "Love it.", "Amazing!", "Perfect.", "Cheers!", "Yes!", "Wow!"
],
question: [
"What are your thoughts?", "Anyone else feel this way?", "What's your favorite part?",
"Can you relate?", "Let me know!", "Dreaming of this?", "What's next?", "Who's with me?",
"Any recommendations?"
],
product: [
"Introducing our latest!", "Get yours now!", "Limited stock available!",
"Perfect for you!", "Experience the difference.", "Shop the collection!",
"Upgrade your life!", "Discover the magic.", "You deserve this."
]
};
generateBtn.addEventListener('click', () => {
const keywords = keywordsInput.value.trim();
const style = styleSelect.value;
captionList.innerHTML = '';
const generatedCaptions = new Set();
const capitalizeWords = (str) => str.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
if (captions[style]) {
captions[style].forEach(cap => generatedCaptions.add(cap));
}
if (keywords) {
generatedCaptions.add(`${capitalizeWords(keywords)} vibes!`);
generatedCaptions.add(`Loving this ${keywords}.`);
if (style === 'question') {
generatedCaptions.add(`What's your favorite thing about ${keywords}?`);
}
}
const emojis = ["✨", "🌟", "💖", "💫", "💯", "🔥", "🎉", "😊", "👍", "🚀"];
emojis.forEach(emoji => generatedCaptions.add(`${keywords ? capitalizeWords(keywords) : 'Amazing'} ${emoji}`));
generatedCaptions.forEach(caption => {
const listItem = document.createElement('li');
listItem.innerHTML = `${caption}
`;
captionList.appendChild(listItem);
});
resultsDiv.classList.remove('hidden');
});
}
},
"viral-topic-gen": {
title: "Viral Topic Generator",
description: "Discover trending and viral topics.",
renderModalContent: () => `
Viral Topic Generator
`,
init: () => {
const keywordsInput = document.getElementById('viral-keywords');
const generateBtn = document.getElementById('generate-viral-topic-btn');
const topicList = document.getElementById('viral-topic-list');
const resultsDiv = document.getElementById('viral-topic-results');
const trendingFrameworks = [
"Top 5", "The Future of", "Unpopular Opinions About", "Hidden Secrets of",
"Is This the End of", "The Rise of", "Debunking Myths About", "Evolution of",
"Best Practices for", "DIY Guide to", "The Impact of", "Ethical Concerns in",
"New Study Reveals", "Shocking Truth About", "Why X is Changing Y"
];
const categories = [
"AI", "Sustainable Living", "Remote Work", "Mental Wellness", "Cryptocurrency",
"Creator Economy", "Personal Finance", "E-commerce Trends", "Social Media Marketing",
"Blockchain", "VR/AR", "Electric Vehicles", "Healthy Eating", "Mindfulness", "Digital Nomad",
"Cybersecurity", "Quantum Computing", "Renewable Energy"
];
generateBtn.addEventListener('click', () => {
const keywords = keywordsInput.value.trim().toLowerCase().split(/[\s,]+/).filter(k => k.length > 0);
if (keywords.length === 0) {
alert('Please enter keywords or niche.');
return;
}
topicList.innerHTML = '';
const generatedTopics = new Set();
const capitalizeWords = (str) => str.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
trendingFrameworks.forEach(frame => {
keywords.forEach(kw => {
generatedTopics.add(`${frame} ${capitalizeWords(kw)}`);
});
});
categories.forEach(cat => {
keywords.forEach(kw => {
generatedTopics.add(`${cat} & ${capitalizeWords(kw)}`);
generatedTopics.add(`Why ${cat} Matters for ${capitalizeWords(kw)}`);
});
});
categories.forEach(cat => generatedTopics.add(`${cat}: What You Need to Know`));
generatedTopics.add(`The Latest in ${capitalizeWords(keywords[0])}`);
generatedTopics.add(`2024 Trends: ${capitalizeWords(keywords[0])}`);
generatedTopics.forEach(topic => {
const listItem = document.createElement('li');
listItem.innerHTML = `${topic}
`;
topicList.appendChild(listItem);
});
resultsDiv.classList.remove('hidden');
});
}
},
"qr-scanner": {
title: "QR Code Scanner",
description: "Scan QR codes from your webcam.",
renderModalContent: () => `
QR Code Scanner
Ready
`,
init: () => {
const videoElement = document.getElementById('qr-video');
const canvasElement = document.getElementById('qr-canvas');
const canvasContext = canvasElement.getContext('2d');
const startBtn = document.getElementById('qr-scan-start-btn');
const stopBtn = document.getElementById('qr-scan-stop-btn');
const statusSpan = document.getElementById('qr-scanner-status');
const resultDiv = document.getElementById('qr-scan-result');
let stream = null;
let qrCodeData = null; // To store decoded QR code data
statusSpan.textContent = "QR scanning requires specific libraries. Webcam stream setup is ready.";
const startCamera = async () => {
if (stream) return;
try {
stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" } });
videoElement.srcObject = stream;
statusSpan.textContent = 'Camera started. Ready to scan.';
startBtn.disabled = true;
stopBtn.disabled = false;
requestAnimationFrame(tick);
} catch (err) {
console.error("Error accessing camera: ", err);
statusSpan.textContent = `Error: ${err.message}`;
alert(`Could not access the camera. Please check browser permissions and try again. Error: ${err.message}`);
startBtn.disabled = false;
stopBtn.disabled = true;
}
};
const stopCamera = () => {
if (stream) {
stream.getTracks().forEach(track => track.stop());
videoElement.srcObject = null;
stream = null;
}
canvasContext.clearRect(0, 0, canvasElement.width, canvasElement.height);
resultDiv.classList.add('hidden');
resultDiv.innerHTML = '';
statusSpan.textContent = 'Ready';
qrCodeData = null;
};
const tick = () => {
if (!stream || !videoElement.videoWidth || !videoElement.videoHeight) {
requestAnimationFrame(tick);
return;
}
canvasElement.height = videoElement.videoHeight;
canvasElement.width = videoElement.videoWidth;
canvasContext.drawImage(videoElement, 0, 0, canvasElement.width, canvasElement.height);
// *** QR Code Detection Placeholder ***
// Requires a library like jsqr.js for actual scanning.
// Example:
// const imageData = canvasContext.getImageData(0, 0, canvasElement.width, canvasElement.height);
// const code = jsQR(imageData.data, imageData.width, imageData.height, { inversionAttempts: "dontInvert" });
// if (code) { ... }
if (!qrCodeData) {
requestAnimationFrame(tick);
}
};
startBtn.addEventListener('click', startCamera);
stopBtn.addEventListener('click', stopCamera);
}
},
"note-pad": {
title: "Note Pad Generator",
description: "A simple digital notepad for your ideas.",
renderModalContent: () => `
Note Pad
`,
init: () => {
const notepadContent = document.getElementById('notepad-content');
const saveBtn = document.getElementById('save-note-btn');
const clearBtn = document.getElementById('clear-note-btn');
const statusDiv = document.getElementById('notepad-status');
const savedNote = localStorage.getItem('notepadContent');
if (savedNote) {
notepadContent.value = savedNote;
}
saveBtn.addEventListener('click', () => {
const content = notepadContent.value;
localStorage.setItem('notepadContent', content);
statusDiv.innerHTML = `Note saved successfully!
`;
statusDiv.classList.remove('hidden');
setTimeout(() => statusDiv.classList.add('hidden'), 2000);
});
clearBtn.addEventListener('click', () => {
notepadContent.value = '';
localStorage.removeItem('notepadContent');
statusDiv.innerHTML = `Note cleared.
`;
statusDiv.classList.remove('hidden');
setTimeout(() => statusDiv.classList.add('hidden'), 2000);
});
}
},
"color-code-gen": {
title: "Color Code Generator",
description: "Generate color codes in various formats.",
renderModalContent: () => `
Color Code Generator
Preview:
HEX: #000000
RGB: rgb(0, 0, 0)
HSL: hsl(0, 0%, 0%)
`,
init: () => {
const generateBtn = document.getElementById('generate-random-color-btn');
const colorPreviewSwatch = document.getElementById('color-preview-swatch');
const hexSpan = document.getElementById('color-hex');
const rgbSpan = document.getElementById('color-rgb');
const hslSpan = document.getElementById('color-hsl');
const copyHexBtn = document.getElementById('copy-color-hex-btn');
const copyRgbBtn = document.getElementById('copy-color-rgb-btn');
let currentHexColor = '#000000';
const rgbToHex = (r, g, b) => {
const toHex = c => {
const hex = c.toString(16);
return hex.length === 1 ? "0" + hex : hex;
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
};
const rgbToHsl = (r, g, b) => {
r /= 255, g /= 255, b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0;
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return [h * 100, s * 100, l * 100];
};
const updateColorDisplay = (hex) => {
currentHexColor = hex;
colorPreviewSwatch.style.backgroundColor = hex;
hexSpan.textContent = hex.toUpperCase();
const r = parseInt(hex.substring(1, 3), 16);
const g = parseInt(hex.substring(3, 5), 16);
const b = parseInt(hex.substring(5, 7), 16);
rgbSpan.textContent = `rgb(${r}, ${g}, ${b})`;
const [h, s, l] = rgbToHsl(r, g, b);
hslSpan.textContent = `hsl(${h.toFixed(0)}°, ${s.toFixed(0)}%, ${l.toFixed(0)}%)`;
};
generateBtn.addEventListener('click', () => {
const randomColor = '#' + Math.floor(Math.random()*16777215).toString(16).padStart(6, '0');
updateColorDisplay(randomColor);
});
updateColorDisplay('#000000');
copyHexBtn.addEventListener('click', () => {
if (navigator.clipboard && currentHexColor) {
navigator.clipboard.writeText(currentHexColor.toUpperCase())
.then(() => alert('HEX color copied!'))
.catch(err => console.error('Failed to copy HEX:', err));
} else {
alert('Clipboard API not available or no color selected. Copy manually.');
}
});
copyRgbBtn.addEventListener('click', () => {
const rgbValue = rgbSpan.textContent;
if (navigator.clipboard && rgbValue) {
navigator.clipboard.writeText(rgbValue)
.then(() => alert('RGB color copied!'))
.catch(err => console.error('Failed to copy RGB:', err));
} else {
alert('Clipboard API not available or no color selected. Copy manually.');
}
});
}
},
};
// --- Event Listeners for Tool Buttons ---
toolButtons.forEach(button => {
button.addEventListener('click', () => {
const toolCard = button.closest('.tool-card');
if (!toolCard) return;
const toolId = toolCard.dataset.toolId;
const tool = tools[toolId];
if (tool) {
openModal(tool);
} else {
console.error(`Tool definition not found for ID: ${toolId}`);
alert(`Tool "${toolId}" is not implemented yet.`);
}
});
});
// --- Modal Handling ---
function openModal(tool) {
modalContainer.innerHTML = `
×
${tool.renderModalContent()}
`;
const modal = modalContainer.querySelector('.modal');
const closeModalBtn = modal.querySelector('.modal-close');
closeModalBtn.addEventListener('click', () => {
closeModal(modal);
});
modal.addEventListener('click', (event) => {
if (event.target === modal) {
closeModal(modal);
}
});
if (typeof tool.init === 'function') {
tool.init();
}
}
function closeModal(modal) {
modal.classList.remove('active');
modal.addEventListener('transitionend', () => {
modalContainer.innerHTML = '';
}, { once: true });
}