}
diff --git a/src/components/ToolCard.tsx b/src/components/ToolCard.tsx
index b92846f..70fcbb8 100644
--- a/src/components/ToolCard.tsx
+++ b/src/components/ToolCard.tsx
@@ -1,33 +1,18 @@
-// src/components/ToolCard.tsx
import Link from 'next/link';
-import type { Tool } from '@/app/page'; // Or from '@/types'
+import type { Tool } from '@/types/tool';
-interface ToolCardProps {
- tool: Tool;
-}
+interface ToolCardProps { tool: Tool }
-const ToolCard = ({ tool }: ToolCardProps) => {
+export default function ToolCard({ tool }: ToolCardProps) {
return (
-
- {/* You can add an icon here later if you have one */}
- {/* {tool.icon && } */}
-
-
{tool.name}
-
{tool.description}
-
- {tool.tags && tool.tags.length > 0 && (
-
- {tool.tags.map((tag) => (
-
- {tag}
-
- ))}
-
- )}
+
+
+
{tool.name}
+ Local
+
+
{tool.description}
+
{tool.category}
+
{tool.tags.slice(0,5).map(tag => {tag})}
);
-};
-
-export default ToolCard;
+}
From 3456395fb5c5a76285a4eb73f8fe32bc72be8850 Mon Sep 17 00:00:00 2001
From: Suraj Verma <5441349+surajverma@users.noreply.github.com>
Date: Fri, 4 Sep 2026 18:01:55 +0530
Subject: [PATCH 05/42] fix: harden markdown regex and privacy enforcement
---
scripts/check-privacy.mjs | 10 ++
src/app/markdown-editor/page.tsx | 121 +------------
src/app/regex-tester/page.tsx | 293 +------------------------------
src/components/ToolList.tsx | 26 +--
4 files changed, 29 insertions(+), 421 deletions(-)
create mode 100644 scripts/check-privacy.mjs
diff --git a/scripts/check-privacy.mjs b/scripts/check-privacy.mjs
new file mode 100644
index 0000000..6d920e2
--- /dev/null
+++ b/scripts/check-privacy.mjs
@@ -0,0 +1,10 @@
+import { readdir, readFile } from 'node:fs/promises';
+import { join } from 'node:path';
+
+const root = new URL('../src/app/', import.meta.url);
+const banned = [/\bfetch\s*\(/, /\bXMLHttpRequest\b/, /\bWebSocket\s*\(/, /\bEventSource\s*\(/, /navigator\.sendBeacon\s*\(/];
+const violations=[];
+async function walk(path){for(const entry of await readdir(path,{withFileTypes:true})){const full=join(path,entry.name);if(entry.isDirectory())await walk(full);else if(/\.(ts|tsx|js|jsx)$/.test(entry.name)){const text=await readFile(full,'utf8');for(const rule of banned)if(rule.test(text))violations.push(`${full}: ${rule}`)}}}
+await walk(root);
+if(violations.length){console.error('Privacy check failed. Tool code contains network-capable APIs:\n'+violations.join('\n'));process.exit(1)}
+console.log('Privacy check passed: no network APIs found in src/app tool code.');
diff --git a/src/app/markdown-editor/page.tsx b/src/app/markdown-editor/page.tsx
index 0a1f566..475c993 100644
--- a/src/app/markdown-editor/page.tsx
+++ b/src/app/markdown-editor/page.tsx
@@ -1,117 +1,14 @@
-// src/app/markdown-editor/page.tsx
'use client';
-
-import React, { useState, useMemo } from 'react';
-import Link from 'next/link';
+import { useMemo, useState } from 'react';
import showdown from 'showdown';
-import Navbar from '@/components/Navbar';
-import Footer from '@/components/Footer';
-import Breadcrumbs from '@/components/Breadcrumbs';
-
-const SAMPLE_MARKDOWN = `# Welcome to the Markdown Editor!
-
-This is a **live preview** of your Markdown text.
-
-## Features
-- Real-time rendering
-- Supports all standard Markdown syntax
-- HTML output copying
-
-### Example Code Block
-\`\`\`javascript
-function greet(name) {
- return "Hello, " + name + "!";
-}
-\`\`\`
-
-> This is a blockquote. Feel free to edit this text and see what happens!
-`;
-
-export default function MarkdownEditorPage() {
- const [markdown, setMarkdown] = useState(SAMPLE_MARKDOWN);
- const [copySuccess, setCopySuccess] = useState('');
-
- const converter = useMemo(
- () =>
- new showdown.Converter({
- tables: true,
- simplifiedAutoLink: true,
- strikethrough: true,
- tasklists: true,
- }),
- []
- );
-
- const html = useMemo(() => converter.makeHtml(markdown), [markdown, converter]);
-
- const handleCopyHtml = () => {
- navigator.clipboard.writeText(html).then(
- () => {
- setCopySuccess('HTML copied to clipboard!');
- setTimeout(() => setCopySuccess(''), 2000); // Clear message after 2 seconds
- },
- (err) => {
- setCopySuccess('Failed to copy HTML.');
- console.error('Could not copy text: ', err);
- }
- );
- };
-
- return (
-
-
-
-
-
-
Markdown Editor
-
- Type Markdown on the left and see the rendered HTML on the right.
-
+import LocalToolLayout from '@/components/LocalToolLayout';
- {/* HTML Preview */}
-
-
-
Preview
-
-
- {copySuccess && (
-
- {copySuccess}
-
- )}
-
-
-
+const SAMPLE='# Welcome to the Markdown Editor!\n\nThis preview renders **Markdown** locally. Raw HTML is escaped for safety.\n\n- No upload\n- Live preview\n- Copy generated HTML';
+const escapeRawHtml=(s:string)=>s.replace(/&/g,'&').replace(//g,'>');
-
setTestString(e.target.value)}
- className='w-full p-3 border border-slate-300 rounded-lg font-mono focus:ring-2 focus:ring-sky-500 focus:border-sky-500 outline-none transition duration-150 ease-in-out'
- rows={8}
- placeholder='Enter the string to test against...'
- />
-
-
- {/* Results Area */}
-
-
Results
- {error ? (
-
- Error: {error}
-
- ) : matches.length > 0 ? (
-
-
- Found {matches.length} match{matches.length === 1 ? '' : 'es'}
- {/* FIX: Only render execution time on the client after hydration */}
- {isClient && ` in ${executionTime.toFixed(2)}ms.`}
-
- {matches.map((match, i) => (
-
- ))}
-
- ) : (
-
No matches found.
- )}
-
-
-
- {/* Placeholder for the explainer */}
-
-
Explanation
-
-
-
-
-
- ← Back to All Tools
-
-
-
-
-
- );
-}
+const TOKEN:Record={'\\d':'digit','\\w':'word character','\\s':'whitespace','.':'any character except newline','^':'start of input','$':'end of input','+':'one or more','*':'zero or more','?':'optional','|':'OR'};
+function explain(pattern:string){const out:string[]=[];for(let i=0;i{if(!pattern)return{matches:[] as RegExpMatchArray[],error:'',time:0};try{const start=performance.now();const regex=new RegExp(pattern,flags);let matches:RegExpMatchArray[];if(regex.global)matches=Array.from(text.matchAll(regex));else{const m=regex.exec(text);matches=m?[m]:[]}return{matches,error:'',time:performance.now()-start}}catch(e){return{matches:[] as RegExpMatchArray[],error:(e as Error).message,time:0}}},[pattern,flags,text]);return
This is a lightweight explanation, not a full regex parser.
}
diff --git a/src/components/ToolList.tsx b/src/components/ToolList.tsx
index 644ac5d..2bed586 100644
--- a/src/components/ToolList.tsx
+++ b/src/components/ToolList.tsx
@@ -1,23 +1,7 @@
-// src/components/ToolList.tsx
-import ToolCard from './ToolCard'; // Import the ToolCard component
-import type { Tool } from '@/app/page'; // Or from '@/types' if you moved it
+import ToolCard from './ToolCard';
+import type { Tool } from '@/types/tool';
-interface ToolListProps {
- tools: Tool[];
+export default function ToolList({ tools }: { tools: Tool[] }) {
+ if (tools.length === 0) return null;
+ return
- );
-};
-
-export default ToolList;
From b8662c8724d07946359123f1ae3520c20c70e228 Mon Sep 17 00:00:00 2001
From: Suraj Verma <5441349+surajverma@users.noreply.github.com>
Date: Fri, 4 Sep 2026 18:02:26 +0530
Subject: [PATCH 06/42] chore: add privacy verification security headers and
project docs
---
README.md | 140 +++++++++++++++++++++++----------------------
next.config.ts | 27 ++++++++-
package.json | 9 ++-
src/app/robots.ts | 3 +
src/app/sitemap.ts | 7 +++
5 files changed, 113 insertions(+), 73 deletions(-)
create mode 100644 src/app/robots.ts
create mode 100644 src/app/sitemap.ts
diff --git a/README.md b/README.md
index 708d3c4..94ced20 100644
--- a/README.md
+++ b/README.md
@@ -1,86 +1,88 @@
# ToolStack
-**A privacy-first collection of useful web tools and utilities. All tools run entirely in your browser—your data never leaves your device.**
----
-
-## ✨ Features
-
-- **Privacy-focused:** All processing happens locally in your browser. No data is sent to any server.
-- **No sign-up, no ads, no tracking.**
-- **Open source:** Contributions welcome!
-- **Modern UI:** Built with Next.js, React, and Tailwind CSS.
-
----
-
-## 🧰 Available Tools
-
-- **Image Compressor** – Reduce image file sizes quickly and easily.
-- **Regex Tester & Explainer** – Test your regular expressions and get explanations.
-- **Text Diff Checker** – Compare two pieces of text and highlight differences.
-- **Color Code Converter** – Convert HEX, RGB, HSL color codes instantly.
-- **Placeholder Image Generator** – Create custom placeholder images on the fly.
-- **Text Statistics / Analyzer** – Get instant stats on your text, like word count, characters, and more.
-- **Image Metadata Remover** – View and remove EXIF metadata from your JPEG images to protect your privacy.
-- **Markdown Editor** – A simple Markdown editor with a live preview and HTML export.
-- **CSV to JSON/XML Converter** – Convert CSV data into JSON or XML formats.
-- **Text Contrast Checker** – Check if the contrast between two colors meets accessibility standards.
-- **Image Resizer** – Quickly resize images to your specified dimensions, all in your browser.
-- **Image Cropper** – Crop images to your desired dimensions with an easy-to-use interface.
-- **Lorem Ipsum Generator** – Generate placeholder text with options for paragraphs, sentences, or words.
-- **Easy Regex Generator** – Create common regular expressions by building a sentence, no syntax required.
-- **Find & Replace in Text** – Perform a find and replace on a block of text, with optional Regex support.
-- **Data Anonymizer** – Automatically find and scrub sensitive data like emails and IP addresses from text.
-- **CSS Specificity Calculator** – Calculate and understand the specificity of CSS selectors.
-- **Unit Converter** – Convert between various units of measurement.
-- **HTTP Status Code Explainer** – Quickly find and understand the meaning of HTTP status codes.
-- **List Cleaner & Formatter** – Quickly sort, clean, and format lists of text with various options.
-- **HTML Tag Explainer** – A quick reference guide for all standard HTML tags and their uses.
-
----
-
-## 🚀 Getting Started
-
-Clone the repo and install dependencies:
-```bash
-git clone https://github.com/SurajVerma/my-toolbox.git
-cd my-toolbox
-npm install
-```
-
-Start the development server:
+**Privacy-first browser tools. Your working data stays on your device.**
+
+ToolStack is an open-source collection of utilities designed around a strict local-processing rule: tool input is processed in the browser and is not sent to an application API for processing.
+
+## Privacy contract
+
+- Processing is local to the browser.
+- No account is required.
+- No advertising or analytics SDK is included.
+- Tool pages are checked for network-capable APIs by `npm run privacy-check`.
+- Tools that fundamentally require sending user input to a third party are intentionally excluded from the core collection.
+
+## Included tools
+
+### Privacy & Security
+- Image Metadata Remover
+- Data Anonymizer
+- File Hash & Checksum
+- Tracking URL Cleaner
+- Password & Passphrase Generator
+- JWT Inspector
+- Browser Privacy Inspector
+- Text Privacy Cleaner
+
+### Images
+- Image Compressor
+- Image Resizer
+- Image Cropper
+- Image Format Converter
+- Placeholder Image Generator
+
+### Developer / Data
+- JSON Toolbox
+- Regex Tester & Explainer
+- Easy Regex Generator
+- Text Diff Checker
+- Markdown Editor
+- CSV to JSON/XML Converter
+- CSS Specificity Calculator
+- HTTP Status Code Explainer
+- HTML Tag Explainer
+- Encoder / Decoder
+- UUID / ULID Generator
+
+### Text, Accessibility, Converters & Generators
+- Text Statistics / Analyzer
+- Find & Replace
+- List Cleaner & Formatter
+- Text Contrast Checker
+- Color Code Converter
+- Unit Converter
+- Timestamp & Date Toolkit
+- Lorem Ipsum Generator
+
+## Development
```bash
+git clone https://github.com/surajverma/toolstack.git
+cd toolstack
+npm install
npm run dev
-# or
-yarn dev
-# or
-pnpm dev
-# or
-bun dev
```
-Open [http://localhost:3000](http://localhost:3000) in your browser.
+Open `http://localhost:3000`.
----
+Before submitting a change:
-## 🛡️ Privacy
-
-All tools work 100% in your browser. No data is ever uploaded or stored remotely. You can use these tools offline after the first load.
-
----
+```bash
+npm run verify
+```
-## 🤝 Contributing
+`verify` runs the privacy check, ESLint, TypeScript checking and the production build.
-Contributions, bug reports, and suggestions are welcome! Please open an issue or submit a pull request.
+## Security and privacy notes
----
+ToolStack's privacy promise concerns the data entered into its tools. Dependencies and the hosting platform still need normal supply-chain and infrastructure maintenance. Security-sensitive tools should prefer browser-standard cryptographic APIs such as Web Crypto rather than custom cryptography.
-## 📄 License
+## Contributing
-MIT. See [LICENSE](LICENSE) for details.
+Bug reports, privacy improvements, accessibility fixes and new local-only tools are welcome. A proposed tool should be able to perform its main function without transmitting the user's input to another service.
----
+## License
-## 🙏 Thank you
+MIT. See [LICENSE](LICENSE).
-If you like my work, you can [buy me a coffee ☕](https://ko-fi.com/skv)
+If you find ToolStack useful, you can support the project at https://ko-fi.com/skv.
diff --git a/next.config.ts b/next.config.ts
index 5e891cf..83ebd42 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -1,7 +1,32 @@
import type { NextConfig } from 'next';
+const contentSecurityPolicy = [
+ "default-src 'self'",
+ "img-src 'self' data: blob:",
+ "media-src 'self' data: blob:",
+ "style-src 'self' 'unsafe-inline'",
+ "script-src 'self' 'unsafe-inline'",
+ "font-src 'self' data:",
+ "connect-src 'self'",
+ "object-src 'none'",
+ "base-uri 'self'",
+ "frame-ancestors 'none'",
+ "form-action 'self'",
+].join('; ');
+
const nextConfig: NextConfig = {
- /* config options here */
+ async headers() {
+ return [{
+ source: '/(.*)',
+ headers: [
+ { key: 'Content-Security-Policy', value: contentSecurityPolicy },
+ { key: 'Referrer-Policy', value: 'no-referrer' },
+ { key: 'X-Content-Type-Options', value: 'nosniff' },
+ { key: 'X-Frame-Options', value: 'DENY' },
+ { key: 'Permissions-Policy', value: 'camera=(self), microphone=(), geolocation=(), payment=(), usb=()' },
+ ],
+ }];
+ },
};
export default nextConfig;
diff --git a/package.json b/package.json
index 533194a..07df86b 100644
--- a/package.json
+++ b/package.json
@@ -1,12 +1,15 @@
{
"name": "toolstack",
- "version": "0.1.0",
+ "version": "0.2.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
- "start": "NODE_ENV=production node server.js",
- "lint": "next lint"
+ "start": "next start",
+ "lint": "eslint .",
+ "typecheck": "tsc --noEmit",
+ "privacy-check": "node scripts/check-privacy.mjs",
+ "verify": "npm run privacy-check && npm run lint && npm run typecheck && npm run build"
},
"dependencies": {
"@types/diff": "^7.0.2",
diff --git a/src/app/robots.ts b/src/app/robots.ts
new file mode 100644
index 0000000..38d034e
--- /dev/null
+++ b/src/app/robots.ts
@@ -0,0 +1,3 @@
+import type { MetadataRoute } from 'next';
+const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://toolstack.surajverma.in';
+export default function robots(): MetadataRoute.Robots { return { rules: { userAgent: '*', allow: '/' }, sitemap: `${SITE_URL}/sitemap.xml` }; }
diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts
new file mode 100644
index 0000000..b0cc753
--- /dev/null
+++ b/src/app/sitemap.ts
@@ -0,0 +1,7 @@
+import type { MetadataRoute } from 'next';
+import { TOOLS } from '@/config/tools';
+
+const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://toolstack.surajverma.in';
+export default function sitemap(): MetadataRoute.Sitemap {
+ return [{ url: SITE_URL, changeFrequency: 'weekly', priority: 1 }, ...TOOLS.map(tool => ({ url: `${SITE_URL}${tool.slug}`, changeFrequency: 'monthly' as const, priority: 0.8 }))];
+}
From d30aabf73e824049c48e63c717c14d7e98192b41 Mon Sep 17 00:00:00 2001
From: Suraj Verma <5441349+surajverma@users.noreply.github.com>
Date: Fri, 4 Sep 2026 18:03:30 +0530
Subject: [PATCH 07/42] feat: add local file encryption and CI verification
---
.github/workflows/ci.yml | 21 ++++++++
src/app/file-encryption/page.tsx | 10 ++++
src/app/robots.ts | 2 +-
src/app/sitemap.ts | 4 +-
src/config/tools.ts | 82 +++++++++++++++++---------------
5 files changed, 77 insertions(+), 42 deletions(-)
create mode 100644 .github/workflows/ci.yml
create mode 100644 src/app/file-encryption/page.tsx
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..8810b3a
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,21 @@
+name: Verify ToolStack
+on:
+ pull_request:
+ push:
+ branches: [master]
+permissions:
+ contents: read
+jobs:
+ verify:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: npm
+ - run: npm ci
+ - run: npm run privacy-check
+ - run: npm run lint
+ - run: npm run typecheck
+ - run: npm run build
diff --git a/src/app/file-encryption/page.tsx b/src/app/file-encryption/page.tsx
new file mode 100644
index 0000000..6dd88fe
--- /dev/null
+++ b/src/app/file-encryption/page.tsx
@@ -0,0 +1,10 @@
+'use client';
+import { useState } from 'react';
+import LocalToolLayout from '@/components/LocalToolLayout';
+
+const MAGIC = new TextEncoder().encode('TSTACK1');
+const ITERATIONS = 250_000;
+const concat=(...parts:Uint8Array[])=>{const out=new Uint8Array(parts.reduce((n,p)=>n+p.length,0));let o=0;for(const p of parts){out.set(p,o);o+=p.length}return out};
+async function derive(password:string,salt:Uint8Array){const material=await crypto.subtle.importKey('raw',new TextEncoder().encode(password),'PBKDF2',false,['deriveKey']);return crypto.subtle.deriveKey({name:'PBKDF2',hash:'SHA-256',salt,iterations:ITERATIONS},material,{name:'AES-GCM',length:256},false,['encrypt','decrypt'])}
+function download(bytes:Uint8Array,name:string,type='application/octet-stream'){const blob=new Blob([bytes],{type});const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url;a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(url),0)}
+export default function FileEncryptionPage(){const [file,setFile]=useState(null);const [password,setPassword]=useState('');const [busy,setBusy]=useState(false);const [error,setError]=useState('');const encrypt=async()=>{if(!file||!password)return;setBusy(true);setError('');try{const salt=crypto.getRandomValues(new Uint8Array(16));const iv=crypto.getRandomValues(new Uint8Array(12));const key=await derive(password,salt);const ciphertext=new Uint8Array(await crypto.subtle.encrypt({name:'AES-GCM',iv},key,await file.arrayBuffer()));download(concat(MAGIC,salt,iv,ciphertext),`${file.name}.tstack`)}catch(e){setError((e as Error).message)}finally{setBusy(false)}};const decrypt=async()=>{if(!file||!password)return;setBusy(true);setError('');try{const bytes=new Uint8Array(await file.arrayBuffer());if(bytes.lengthbytes[i]===v))throw new Error('Not a ToolStack encrypted file.');let o=MAGIC.length;const salt=bytes.slice(o,o+=16);const iv=bytes.slice(o,o+=12);const ciphertext=bytes.slice(o);const key=await derive(password,salt);const plain=new Uint8Array(await crypto.subtle.decrypt({name:'AES-GCM',iv},key,ciphertext));download(plain,file.name.replace(/\.tstack$/,'' )||'decrypted-file')}catch(e){setError('Decryption failed. Check the password and file.')}finally{setBusy(false)}};return setFile(e.target.files?.[0]??null)} className='w-full rounded border p-3'/>{error&&
{error}
}
Important: There is no password recovery. Keep the password separately. This format is ToolStack-specific and intended for local convenience, not archival interoperability.
}
diff --git a/src/app/robots.ts b/src/app/robots.ts
index 38d034e..d74af4f 100644
--- a/src/app/robots.ts
+++ b/src/app/robots.ts
@@ -1,3 +1,3 @@
import type { MetadataRoute } from 'next';
-const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://toolstack.surajverma.in';
+const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
export default function robots(): MetadataRoute.Robots { return { rules: { userAgent: '*', allow: '/' }, sitemap: `${SITE_URL}/sitemap.xml` }; }
diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts
index b0cc753..b7065cf 100644
--- a/src/app/sitemap.ts
+++ b/src/app/sitemap.ts
@@ -1,7 +1,7 @@
import type { MetadataRoute } from 'next';
import { TOOLS } from '@/config/tools';
-const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://toolstack.surajverma.in';
+const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
export default function sitemap(): MetadataRoute.Sitemap {
- return [{ url: SITE_URL, changeFrequency: 'weekly', priority: 1 }, ...TOOLS.map(tool => ({ url: `${SITE_URL}${tool.slug}`, changeFrequency: 'monthly' as const, priority: 0.8 }))];
+ return [{ url: SITE_URL, changeFrequency: 'weekly', priority: 1 }, ...TOOLS.map(item => ({ url: `${SITE_URL}${item.slug}`, changeFrequency: 'monthly' as const, priority: 0.8 }))];
}
diff --git a/src/config/tools.ts b/src/config/tools.ts
index 0914941..5ff6395 100644
--- a/src/config/tools.ts
+++ b/src/config/tools.ts
@@ -1,40 +1,7 @@
-import type { Tool } from '@/types/tool';
+import type { Tool, ToolCategory } from '@/types/tool';
-export const TOOLS: Tool[] = [
- ['image-compressor','Image Compressor','Reduce image file sizes locally.','Images',['image','compress','optimize','media']],
- ['regex-tester','Regex Tester & Explainer','Test regular expressions and understand their matches.','Developer',['text','developer','regex','code']],
- ['text-diff','Text Diff Checker','Compare two pieces of text and highlight differences.','Text & Data',['text','compare','diff']],
- ['color-converter','Color Code Converter','Convert HEX, RGB and HSL color values.','Converters',['color','design','developer','converter']],
- ['placeholder-generator','Placeholder Image Generator','Create custom placeholder images locally.','Images',['image','design','placeholder']],
- ['text-analyzer','Text Statistics / Analyzer','Count words, characters, sentences and more.','Text & Data',['text','stats','analyzer','counter']],
- ['image-metadata','Image Metadata Remover','View and remove EXIF metadata from JPEG images.','Privacy & Security',['image','privacy','exif','metadata','security']],
- ['markdown-editor','Markdown Editor','Edit Markdown with a live local preview.','Text & Data',['text','markdown','editor','writing']],
- ['csv-converter','CSV to JSON/XML Converter','Convert CSV data to JSON or XML locally.','Converters',['csv','json','xml','converter','data','developer']],
- ['contrast-checker','Text Contrast Checker','Check color contrast against accessibility guidance.','Accessibility',['accessibility','color','design','wcag']],
- ['image-resizer','Image Resizer','Resize images to specific dimensions locally.','Images',['image','resize','dimensions','optimize']],
- ['image-cropper','Image Cropper','Crop images locally in your browser.','Images',['image','crop','edit','dimensions']],
- ['lorem-ipsum-generator','Lorem Ipsum Generator','Generate placeholder text.','Generators',['text','generator','placeholder','design']],
- ['regex-generator','Easy Regex Generator','Build common regular expressions without memorizing syntax.','Developer',['regex','generator','developer','text','code']],
- ['find-replace','Find & Replace in Text','Find and replace text with optional regex support.','Text & Data',['text','find','replace','regex']],
- ['data-anonymizer','Data Anonymizer','Detect and redact common sensitive data locally.','Privacy & Security',['text','privacy','security','anonymize','redact']],
- ['css-specificity-calculator','CSS Specificity Calculator','Calculate and explain CSS selector specificity.','Developer',['css','developer','specificity','design']],
- ['unit-converter','Unit Converter','Convert common measurement units.','Converters',['converter','units','measurement','math']],
- ['http-status-codes','HTTP Status Code Explainer','Look up HTTP status codes and meanings.','Developer',['http','developer','reference','network']],
- ['list-cleaner','List Cleaner & Formatter','Sort, deduplicate and format text lists.','Text & Data',['text','list','format','cleaner','sort']],
- ['html-tag-explainer','HTML Tag Explainer','Reference standard HTML tags and their uses.','Developer',['html','developer','reference','code']],
- ['file-hash','File Hash & Checksum','Generate SHA-256, SHA-384 and SHA-512 checksums without uploading files.','Privacy & Security',['privacy','hash','checksum','file','security']],
- ['json-toolbox','JSON Toolbox','Format, minify, validate and sort JSON locally.','Developer',['json','format','validate','developer','data']],
- ['url-cleaner','Tracking URL Cleaner','Remove common tracking parameters and inspect URL parts locally.','Privacy & Security',['privacy','url','tracking','utm','cleaner']],
- ['password-generator','Password & Passphrase Generator','Generate cryptographically secure passwords and passphrases locally.','Privacy & Security',['password','security','privacy','generator']],
- ['encoder-decoder','Encoder / Decoder','Encode and decode Base64, URLs, HTML entities and Unicode text.','Developer',['base64','url','html','unicode','encode','decode']],
- ['jwt-inspector','JWT Inspector','Decode JWT headers and payloads locally without sending tokens anywhere.','Privacy & Security',['jwt','token','developer','privacy','security']],
- ['timestamp-tool','Timestamp & Date Toolkit','Convert Unix timestamps, ISO dates, UTC and local time.','Converters',['date','time','timestamp','unix','converter']],
- ['uuid-generator','UUID Generator','Generate and validate UUIDs locally.','Generators',['uuid','generator','developer','identifier']],
- ['browser-privacy','Browser Privacy Inspector','See what your browser exposes to websites without transmitting the results.','Privacy & Security',['privacy','browser','fingerprint','security']],
- ['text-privacy-cleaner','Text Privacy Cleaner','Reveal or remove hidden Unicode, control and zero-width characters.','Privacy & Security',['privacy','text','unicode','cleaner','security']],
- ['image-converter','Image Format Converter','Convert supported images to PNG, JPEG or WebP locally.','Images',['image','convert','png','jpeg','webp']],
-].map(([slug,name,description,category,tags], index) => ({
- id: String(index + 1),
+const tool = (slug: string, name: string, description: string, category: ToolCategory, tags: string[], options: Partial> = {}): Tool => ({
+ id: slug,
slug: `/${slug}`,
name,
description,
@@ -43,7 +10,44 @@ export const TOOLS: Tool[] = [
processing: 'local',
networkRequired: false,
storesData: false,
-})) as Tool[];
+ ...options,
+});
+
+export const TOOLS: Tool[] = [
+ tool('image-compressor','Image Compressor','Reduce image file sizes locally.','Images',['image','compress','optimize','media']),
+ tool('regex-tester','Regex Tester & Explainer','Test regular expressions and understand their matches.','Developer',['text','developer','regex','code']),
+ tool('text-diff','Text Diff Checker','Compare two pieces of text and highlight differences.','Text & Data',['text','compare','diff']),
+ tool('color-converter','Color Code Converter','Convert HEX, RGB and HSL color values.','Converters',['color','design','developer','converter']),
+ tool('placeholder-generator','Placeholder Image Generator','Create custom placeholder images locally.','Images',['image','design','placeholder']),
+ tool('text-analyzer','Text Statistics / Analyzer','Count words, characters, sentences and more.','Text & Data',['text','stats','analyzer','counter']),
+ tool('image-metadata','Image Metadata Remover','View and remove EXIF metadata from JPEG images.','Privacy & Security',['image','privacy','exif','metadata','security'],{featured:true}),
+ tool('markdown-editor','Markdown Editor','Edit Markdown with a live local preview.','Text & Data',['text','markdown','editor','writing']),
+ tool('csv-converter','CSV to JSON/XML Converter','Convert CSV data to JSON or XML locally.','Converters',['csv','json','xml','converter','data','developer']),
+ tool('contrast-checker','Text Contrast Checker','Check color contrast against accessibility guidance.','Accessibility',['accessibility','color','design','wcag']),
+ tool('image-resizer','Image Resizer','Resize images to specific dimensions locally.','Images',['image','resize','dimensions','optimize']),
+ tool('image-cropper','Image Cropper','Crop images locally in your browser.','Images',['image','crop','edit','dimensions']),
+ tool('lorem-ipsum-generator','Lorem Ipsum Generator','Generate placeholder text.','Generators',['text','generator','placeholder','design']),
+ tool('regex-generator','Easy Regex Generator','Build common regular expressions without memorizing syntax.','Developer',['regex','generator','developer','text','code']),
+ tool('find-replace','Find & Replace in Text','Find and replace text with optional regex support.','Text & Data',['text','find','replace','regex']),
+ tool('data-anonymizer','Data Anonymizer','Detect and redact common sensitive data locally.','Privacy & Security',['text','privacy','security','anonymize','redact'],{featured:true}),
+ tool('css-specificity-calculator','CSS Specificity Calculator','Calculate and explain CSS selector specificity.','Developer',['css','developer','specificity','design']),
+ tool('unit-converter','Unit Converter','Convert common measurement units.','Converters',['converter','units','measurement','math']),
+ tool('http-status-codes','HTTP Status Code Explainer','Look up HTTP status codes and meanings.','Developer',['http','developer','reference','network']),
+ tool('list-cleaner','List Cleaner & Formatter','Sort, deduplicate and format text lists.','Text & Data',['text','list','format','cleaner','sort']),
+ tool('html-tag-explainer','HTML Tag Explainer','Reference standard HTML tags and their uses.','Developer',['html','developer','reference','code']),
+ tool('file-hash','File Hash & Checksum','Generate SHA-256, SHA-384 and SHA-512 checksums without uploading files.','Privacy & Security',['privacy','hash','checksum','file','security'],{featured:true,new:true}),
+ tool('json-toolbox','JSON Toolbox','Format, minify, validate and sort JSON locally.','Developer',['json','format','validate','developer','data'],{new:true}),
+ tool('url-cleaner','Tracking URL Cleaner','Remove common tracking parameters and inspect URL parts locally.','Privacy & Security',['privacy','url','tracking','utm','cleaner'],{featured:true,new:true}),
+ tool('password-generator','Password & Passphrase Generator','Generate cryptographically secure passwords and passphrases locally.','Privacy & Security',['password','security','privacy','generator'],{featured:true,new:true}),
+ tool('encoder-decoder','Encoder / Decoder','Encode and decode Base64, URLs and HTML entities.','Developer',['base64','url','html','unicode','encode','decode'],{new:true}),
+ tool('jwt-inspector','JWT Inspector','Decode JWT headers and payloads locally without sending tokens anywhere.','Privacy & Security',['jwt','token','developer','privacy','security'],{new:true}),
+ tool('timestamp-tool','Timestamp & Date Toolkit','Convert Unix timestamps, ISO dates, UTC and local time.','Converters',['date','time','timestamp','unix','converter'],{new:true}),
+ tool('uuid-generator','UUID / ULID Generator','Generate and validate UUIDs and generate ULIDs locally.','Generators',['uuid','ulid','generator','developer','identifier'],{new:true}),
+ tool('browser-privacy','Browser Privacy Inspector','See what your browser exposes to websites without transmitting the results.','Privacy & Security',['privacy','browser','fingerprint','security'],{featured:true,new:true}),
+ tool('text-privacy-cleaner','Text Privacy Cleaner','Reveal or remove hidden Unicode, control and zero-width characters.','Privacy & Security',['privacy','text','unicode','cleaner','security'],{new:true}),
+ tool('image-converter','Image Format Converter','Convert supported images to PNG, JPEG or WebP locally.','Images',['image','convert','png','jpeg','webp'],{new:true}),
+ tool('file-encryption','File Encryption','Encrypt and decrypt files locally with AES-GCM and a password-derived key.','Privacy & Security',['privacy','encryption','aes','file','security'],{featured:true,new:true}),
+];
-export const TOOL_BY_SLUG = new Map(TOOLS.map((tool) => [tool.slug, tool]));
-export const TOOL_CATEGORIES = [...new Set(TOOLS.map((tool) => tool.category))];
+export const TOOL_BY_SLUG = new Map(TOOLS.map(item => [item.slug, item]));
+export const TOOL_CATEGORIES = [...new Set(TOOLS.map(item => item.category))];
From 5d5a5e319490687e5629f872ebd13d83cda39b4d Mon Sep 17 00:00:00 2001
From: Suraj Verma <5441349+surajverma@users.noreply.github.com>
Date: Fri, 4 Sep 2026 18:04:24 +0530
Subject: [PATCH 08/42] fix: correct XML conversion and storage unit semantics
---
README.md | 40 +----
src/app/csv-converter/page.tsx | 147 +-----------------
src/app/unit-converter/page.tsx | 263 ++------------------------------
3 files changed, 26 insertions(+), 424 deletions(-)
diff --git a/README.md b/README.md
index 94ced20..8a6bff3 100644
--- a/README.md
+++ b/README.md
@@ -15,44 +15,16 @@ ToolStack is an open-source collection of utilities designed around a strict loc
## Included tools
### Privacy & Security
-- Image Metadata Remover
-- Data Anonymizer
-- File Hash & Checksum
-- Tracking URL Cleaner
-- Password & Passphrase Generator
-- JWT Inspector
-- Browser Privacy Inspector
-- Text Privacy Cleaner
+Image Metadata Remover, Data Anonymizer, File Hash & Checksum, Tracking URL Cleaner, Password & Passphrase Generator, JWT Inspector, Browser Privacy Inspector, Text Privacy Cleaner, and AES-GCM File Encryption.
### Images
-- Image Compressor
-- Image Resizer
-- Image Cropper
-- Image Format Converter
-- Placeholder Image Generator
+Image Compressor, Image Resizer, Image Cropper, Image Format Converter, and Placeholder Image Generator.
### Developer / Data
-- JSON Toolbox
-- Regex Tester & Explainer
-- Easy Regex Generator
-- Text Diff Checker
-- Markdown Editor
-- CSV to JSON/XML Converter
-- CSS Specificity Calculator
-- HTTP Status Code Explainer
-- HTML Tag Explainer
-- Encoder / Decoder
-- UUID / ULID Generator
+JSON Toolbox, Regex Tester & Explainer, Easy Regex Generator, Text Diff Checker, Markdown Editor, CSV to JSON/XML Converter, CSS Specificity Calculator, HTTP Status Code Explainer, HTML Tag Explainer, Encoder / Decoder, and UUID / ULID Generator.
### Text, Accessibility, Converters & Generators
-- Text Statistics / Analyzer
-- Find & Replace
-- List Cleaner & Formatter
-- Text Contrast Checker
-- Color Code Converter
-- Unit Converter
-- Timestamp & Date Toolkit
-- Lorem Ipsum Generator
+Text Statistics / Analyzer, Find & Replace, List Cleaner & Formatter, Text Contrast Checker, Color Code Converter, Unit Converter, Timestamp & Date Toolkit, and Lorem Ipsum Generator.
## Development
@@ -65,6 +37,8 @@ npm run dev
Open `http://localhost:3000`.
+For production SEO, set `NEXT_PUBLIC_SITE_URL` to the deployed site's absolute origin before building.
+
Before submitting a change:
```bash
@@ -75,7 +49,7 @@ npm run verify
## Security and privacy notes
-ToolStack's privacy promise concerns the data entered into its tools. Dependencies and the hosting platform still need normal supply-chain and infrastructure maintenance. Security-sensitive tools should prefer browser-standard cryptographic APIs such as Web Crypto rather than custom cryptography.
+ToolStack's privacy promise concerns the data entered into its tools. Dependencies and the hosting platform still need normal supply-chain and infrastructure maintenance. Security-sensitive tools prefer browser-standard cryptographic APIs such as Web Crypto rather than custom cryptography.
## Contributing
diff --git a/src/app/csv-converter/page.tsx b/src/app/csv-converter/page.tsx
index 9c8ecde..00fa16f 100644
--- a/src/app/csv-converter/page.tsx
+++ b/src/app/csv-converter/page.tsx
@@ -1,144 +1,9 @@
-// src/app/csv-converter/page.tsx
'use client';
-
-import React, { useState, useMemo } from 'react';
-import Link from 'next/link';
+import { useMemo, useState } from 'react';
import Papa from 'papaparse';
-import Navbar from '@/components/Navbar';
-import Footer from '@/components/Footer';
-import Breadcrumbs from '@/components/Breadcrumbs';
-
-const SAMPLE_CSV = `Year,Make,Model,Length
-1997,Ford,E350,2.34
-2000,Mercury,Cougar,2.38`;
-
-// Helper function to convert JSON to a simple XML string
-const jsonToXml = (json: Record[]): string => {
- if (!json || json.length === 0) return '';
- let xml = '\n\n';
- json.forEach((item) => {
- xml += ' \n';
- for (const key in item) {
- if (Object.prototype.hasOwnProperty.call(item, key)) {
- xml += ` <${key}>${item[key]}${key}>\n`;
- }
- }
- xml += ' \n';
- });
- xml += '';
- return xml;
-};
-
-export default function CsvConverterPage() {
- const [csvText, setCsvText] = useState(SAMPLE_CSV);
- const [activeTab, setActiveTab] = useState<'json' | 'xml'>('json');
- const [copySuccess, setCopySuccess] = useState('');
-
- const parsedJson = useMemo(() => {
- try {
- const result = Papa.parse(csvText, {
- header: true,
- skipEmptyLines: true,
- });
- if (result.errors.length > 0) {
- console.warn('CSV Parsing Errors:', result.errors);
- }
- return result.data;
- } catch (error) {
- console.error('CSV Parsing Failed:', error);
- return [];
- }
- }, [csvText]);
-
- const jsonOutput = useMemo(() => {
- return JSON.stringify(parsedJson, null, 2);
- }, [parsedJson]);
-
- const xmlOutput = useMemo(() => {
- return jsonToXml(parsedJson as Record[]);
- }, [parsedJson]);
-
- const handleCopy = () => {
- const textToCopy = activeTab === 'json' ? jsonOutput : xmlOutput;
- navigator.clipboard
- .writeText(textToCopy)
- .then(() => {
- setCopySuccess(`${activeTab.toUpperCase()} copied!`);
- setTimeout(() => setCopySuccess(''), 2000);
- })
- .catch((err) => {
- setCopySuccess('Failed to copy.');
- console.error('Copy failed:', err);
- });
- };
-
- return (
-
-
-
-
-
-
CSV to JSON/XML Converter
-
Paste your CSV data to convert it into structured formats.