diff --git a/AGENTS.md b/AGENTS.md
deleted file mode 100644
index 643577d..0000000
--- a/AGENTS.md
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-# This is NOT the Next.js you know
-
-This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
-
-This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
-
-
diff --git a/CLAUDE.md b/CLAUDE.md
deleted file mode 100644
index 43c994c..0000000
--- a/CLAUDE.md
+++ /dev/null
@@ -1 +0,0 @@
-@AGENTS.md
diff --git a/README.md b/README.md
index e215bc4..64cf9cc 100644
--- a/README.md
+++ b/README.md
@@ -1,36 +1,43 @@
-This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
+# kattoolbox
-## Getting Started
+Free, fast, browser-based developer tools. No sign-up. No data collection.
-First, run the development server:
+**Live →** *coming soon*
+
+## Tools
+
+- JSON Formatter & Validator
+- Markdown Preview
+- Base64 Encode/Decode
+- URL Encode/Decode
+- HTML Entity Encode/Decode
+- JWT Decoder
+- UUID Generator
+- Lorem Ipsum Generator
+- Hash Generator (SHA-1/256/384/512)
+- QR Code Generator
+- Password Generator
+- Unix Timestamp Converter
+- Color Converter (HEX/RGB/HSL)
+- Number Base Converter
+- Word & Character Counter
+- Regex Tester
+- Text Diff Checker
+- Case Converter
+- CSS Gradient Generator
+- CSS Box Shadow Generator
+
+## Stack
+
+Next.js · TypeScript · Tailwind CSS · Static Export
+
+## Run locally
```bash
+npm install
npm run dev
-# or
-yarn dev
-# or
-pnpm dev
-# or
-bun dev
```
-Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+## License
-You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
-
-This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
-
-## Learn More
-
-To learn more about Next.js, take a look at the following resources:
-
-- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
-- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
-
-You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
-
-## Deploy on Vercel
-
-The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
-
-Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
+MIT
diff --git a/next.config.ts b/next.config.ts
index e9ffa30..00c940e 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -1,7 +1,10 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
- /* config options here */
+ output: "export",
+ images: {
+ unoptimized: true,
+ },
};
export default nextConfig;
diff --git a/package-lock.json b/package-lock.json
index 1cbd441..0b18263 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,13 +8,16 @@
"name": "toolbox",
"version": "0.1.0",
"dependencies": {
+ "marked": "^18.0.11",
"next": "16.3.3",
+ "qrcode": "^1.5.4",
"react": "19.2.8",
"react-dom": "19.2.8"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
+ "@types/qrcode": "^1.5.6",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
@@ -1718,6 +1721,16 @@
"undici-types": "~6.21.0"
}
},
+ "node_modules/@types/qrcode": {
+ "version": "1.5.6",
+ "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz",
+ "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/react": {
"version": "19.2.18",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz",
@@ -2427,11 +2440,19 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
@@ -2810,6 +2831,15 @@
"node": ">=6"
}
},
+ "node_modules/camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/caniuse-lite": {
"version": "1.0.30001810",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz",
@@ -2853,11 +2883,21 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
+ "node_modules/cliui": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+ "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^6.2.0"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
@@ -2870,7 +2910,6 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true,
"license": "MIT"
},
"node_modules/concat-map": {
@@ -2988,6 +3027,15 @@
}
}
},
+ "node_modules/decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -3041,6 +3089,12 @@
"node": ">=8"
}
},
+ "node_modules/dijkstrajs": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
+ "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
+ "license": "MIT"
+ },
"node_modules/doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
@@ -3931,6 +3985,15 @@
"node": ">=6.9.0"
}
},
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -4427,6 +4490,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/is-generator-function": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
@@ -5166,6 +5238,18 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
+ "node_modules/marked": {
+ "version": "18.0.11",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.11.tgz",
+ "integrity": "sha512-HnslJfsZkRPBDJRHvVtAaWlZHEpSu7u8LgQuJCELjRKuWR+hpq4A7sLq3p8HaI9ypVoXDXxV34CsQJEe1+J5Aw==",
+ "license": "MIT",
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -5573,6 +5657,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -5590,7 +5683,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -5632,6 +5724,15 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/pngjs": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
+ "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@@ -5703,6 +5804,23 @@
"node": ">=6"
}
},
+ "node_modules/qrcode": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
+ "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
+ "license": "MIT",
+ "dependencies": {
+ "dijkstrajs": "^1.0.1",
+ "pngjs": "^5.0.0",
+ "yargs": "^15.3.1"
+ },
+ "bin": {
+ "qrcode": "bin/qrcode"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -5796,6 +5914,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-main-filename": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+ "license": "ISC"
+ },
"node_modules/resolve": {
"version": "2.0.0-next.7",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz",
@@ -5946,6 +6079,12 @@
"semver": "bin/semver.js"
}
},
+ "node_modules/set-blocking": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
+ "license": "ISC"
+ },
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -6187,6 +6326,26 @@
"node": ">= 0.4"
}
},
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
"node_modules/string.prototype.includes": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
@@ -6301,6 +6460,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/strip-bom": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
@@ -6817,6 +6988,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/which-module": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
+ "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
+ "license": "ISC"
+ },
"node_modules/which-typed-array": {
"version": "1.1.22",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz",
@@ -6849,6 +7026,26 @@
"node": ">=0.10.0"
}
},
+ "node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
+ "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
+ "license": "ISC"
+ },
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
@@ -6856,6 +7053,93 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/yargs": {
+ "version": "15.4.1",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
+ "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^6.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^4.1.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^4.2.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^18.1.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "18.1.3",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+ "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+ "license": "ISC",
+ "dependencies": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/yargs/node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs/node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs/node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/yargs/node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/package.json b/package.json
index e818b8d..90db118 100644
--- a/package.json
+++ b/package.json
@@ -9,13 +9,16 @@
"lint": "eslint"
},
"dependencies": {
+ "marked": "^18.0.11",
"next": "16.3.3",
+ "qrcode": "^1.5.4",
"react": "19.2.8",
"react-dom": "19.2.8"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
+ "@types/qrcode": "^1.5.6",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000..5427f17
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,4 @@
+User-agent: *
+Allow: /
+
+Sitemap: https://devtoolbox.com/sitemap.xml
diff --git a/src/app/globals.css b/src/app/globals.css
index a2dc41e..5f9d308 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -1,8 +1,8 @@
@import "tailwindcss";
:root {
- --background: #ffffff;
- --foreground: #171717;
+ --background: #f9fafb;
+ --foreground: #111827;
}
@theme inline {
@@ -14,13 +14,39 @@
@media (prefers-color-scheme: dark) {
:root {
- --background: #0a0a0a;
- --foreground: #ededed;
+ --background: #030712;
+ --foreground: #f3f4f6;
}
}
body {
background: var(--background);
color: var(--foreground);
- font-family: Arial, Helvetica, sans-serif;
+ font-family: var(--font-sans), Arial, Helvetica, sans-serif;
+}
+
+/* AdSense placeholder styling — remove once real ads are connected */
+.ad-container ins.adsbygoogle {
+ min-height: 90px;
+ background: repeating-linear-gradient(
+ -45deg,
+ transparent,
+ transparent 10px,
+ rgba(128, 128, 128, 0.03) 10px,
+ rgba(128, 128, 128, 0.03) 20px
+ );
+ border: 1px dashed rgba(128, 128, 128, 0.2);
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+}
+
+.ad-container ins.adsbygoogle::after {
+ content: "Ad Space";
+ font-size: 0.75rem;
+ color: rgba(128, 128, 128, 0.4);
+ font-weight: 500;
+ letter-spacing: 0.05em;
}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 9852c15..1d0a50e 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,6 +1,8 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
+import Header from "@/components/Header";
+import Footer from "@/components/Footer";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -13,17 +15,60 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
- title: "Create Next App",
- description: "Generated by create next app",
+ title: {
+ default: "DevToolbox — Free Online Developer Tools",
+ template: "%s | DevToolbox",
+ },
+ description:
+ "A collection of free, fast, and privacy-first online developer tools. JSON formatter, Base64 encoder, UUID generator, hash generator, and 20+ more. All tools run 100% in your browser.",
+ keywords: [
+ "developer tools",
+ "online tools",
+ "JSON formatter",
+ "Base64 encoder",
+ "UUID generator",
+ "hash generator",
+ "regex tester",
+ "free dev tools",
+ ],
+ openGraph: {
+ type: "website",
+ title: "DevToolbox — Free Online Developer Tools",
+ description:
+ "20+ free, fast, privacy-first developer tools. JSON formatter, Base64 encoder, UUID generator, and more.",
+ siteName: "DevToolbox",
+ },
+ twitter: {
+ card: "summary_large_image",
+ title: "DevToolbox — Free Online Developer Tools",
+ description:
+ "20+ free, fast, privacy-first developer tools. JSON formatter, Base64 encoder, UUID generator, and more.",
+ },
+ robots: {
+ index: true,
+ follow: true,
+ },
};
-export default function RootLayout({ children }: LayoutProps<"/">) {
+export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
-
{children}
+
+ {/* Google AdSense — replace ca-pub-XXXXXXXXXXXXXXXX with your publisher ID */}
+
+
+
+
+ {children}
+
+
);
}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index c887311..e13b9a6 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,69 +1,31 @@
-import Image from "next/image";
+import ToolCard from "@/components/ToolCard";
+import AdSlot from "@/components/AdSlot";
+import { getToolsByCategory } from "@/lib/tools";
export default function Home() {
+ const toolsByCategory = getToolsByCategory();
+
return (
-
-
-
-
-
- To get started, edit the{" "}
-
- page.tsx
- {" "}
- file.
-
-
- Looking for a starting point or more instructions? Head over to{" "}
-
- Templates
- {" "}
- or the{" "}
-
- Learning
- {" "}
- center.
-
-
-
-
+
+
Developer Tools
+
Free. Private. Browser-based.
+
+
+
+ {Object.entries(toolsByCategory).map(([category, categoryTools]) => (
+
+
+ {category}
+
+
+ {categoryTools.map((tool) => (
+
+ ))}
+
+
+ ))}
+
+
);
}
diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts
new file mode 100644
index 0000000..eef7e6a
--- /dev/null
+++ b/src/app/sitemap.ts
@@ -0,0 +1,26 @@
+import type { MetadataRoute } from "next";
+import { tools } from "@/lib/tools";
+
+export const dynamic = "force-static";
+
+// TODO: Replace with your actual domain
+const BASE_URL = "https://devtoolbox.com";
+
+export default function sitemap(): MetadataRoute.Sitemap {
+ const toolPages = tools.map((tool) => ({
+ url: `${BASE_URL}/tools/${tool.slug}`,
+ lastModified: new Date(),
+ changeFrequency: "monthly" as const,
+ priority: 0.8,
+ }));
+
+ return [
+ {
+ url: BASE_URL,
+ lastModified: new Date(),
+ changeFrequency: "weekly",
+ priority: 1,
+ },
+ ...toolPages,
+ ];
+}
diff --git a/src/app/tools/base64/page.tsx b/src/app/tools/base64/page.tsx
new file mode 100644
index 0000000..129c401
--- /dev/null
+++ b/src/app/tools/base64/page.tsx
@@ -0,0 +1,377 @@
+"use client";
+
+import { useState, useCallback, useId } from "react";
+import ToolLayout from "@/components/ToolLayout";
+
+export default function Base64Page() {
+ const inputId = useId();
+ const outputId = useId();
+ const [input, setInput] = useState("");
+ const [output, setOutput] = useState("");
+ const [error, setError] = useState
(null);
+ const [copied, setCopied] = useState(false);
+ const [urlSafe, setUrlSafe] = useState(false);
+ const [splitLines, setSplitLines] = useState(false);
+ const [lastAction, setLastAction] = useState<"encode" | "decode" | null>(null);
+
+ const encodeBase64 = useCallback((text: string, isUrlSafe: boolean, isSplit: boolean) => {
+ if (!text) {
+ setOutput("");
+ setError(null);
+ return;
+ }
+ try {
+ const bytes = new TextEncoder().encode(text);
+ let binary = "";
+ for (let i = 0; i < bytes.length; i++) {
+ binary += String.fromCharCode(bytes[i]);
+ }
+ let base64 = btoa(binary);
+ if (isUrlSafe) {
+ base64 = base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
+ }
+ if (isSplit && base64.length > 76) {
+ base64 = base64.match(/.{1,76}/g)?.join("\n") || base64;
+ }
+ setOutput(base64);
+ setError(null);
+ setLastAction("encode");
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to encode input.");
+ setOutput("");
+ }
+ }, []);
+
+ const decodeBase64 = useCallback((text: string) => {
+ if (!text.trim()) {
+ setOutput("");
+ setError(null);
+ return;
+ }
+ try {
+ let cleaned = text.trim().replace(/\s+/g, "");
+ cleaned = cleaned.replace(/-/g, "+").replace(/_/g, "/");
+ while (cleaned.length % 4 !== 0) {
+ cleaned += "=";
+ }
+
+ if (!/^[A-Za-z0-9+/=]+$/.test(cleaned)) {
+ throw new Error("Invalid Base64 string: Contains characters outside the Base64 alphabet.");
+ }
+
+ const binary = atob(cleaned);
+ const bytes = new Uint8Array(binary.length);
+ for (let i = 0; i < binary.length; i++) {
+ bytes[i] = binary.charCodeAt(i);
+ }
+
+ try {
+ const decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
+ setOutput(decoded);
+ } catch {
+ setOutput(binary);
+ }
+ setError(null);
+ setLastAction("decode");
+ } catch (err) {
+ setError(
+ err instanceof Error
+ ? err.message
+ : "Invalid Base64 string. Please check your input and try again."
+ );
+ setOutput("");
+ }
+ }, []);
+
+ const handleEncode = () => {
+ encodeBase64(input, urlSafe, splitLines);
+ };
+
+ const handleDecode = () => {
+ decodeBase64(input);
+ };
+
+ const handleCopy = async () => {
+ if (!output) return;
+ try {
+ await navigator.clipboard.writeText(output);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ } catch {
+ const textArea = document.createElement("textarea");
+ textArea.value = output;
+ document.body.appendChild(textArea);
+ textArea.select();
+ document.execCommand("copy");
+ document.body.removeChild(textArea);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ }
+ };
+
+ const handleSwap = () => {
+ const temp = output;
+ setInput(temp);
+ setOutput(input);
+ setError(null);
+ if (lastAction === "encode") {
+ setLastAction("decode");
+ } else if (lastAction === "decode") {
+ setLastAction("encode");
+ }
+ };
+
+ const handleClear = () => {
+ setInput("");
+ setOutput("");
+ setError(null);
+ setLastAction(null);
+ };
+
+ const loadSample = (type: "text" | "unicode" | "base64") => {
+ setError(null);
+ if (type === "text") {
+ const sample = "Hello, Developer! DevToolbox makes Base64 encoding fast and easy.";
+ setInput(sample);
+ encodeBase64(sample, urlSafe, splitLines);
+ } else if (type === "unicode") {
+ const sample = "Unicode test: 🚀 🌍 Привет! こんにちは 你好 €100 — café & naïve.";
+ setInput(sample);
+ encodeBase64(sample, urlSafe, splitLines);
+ } else {
+ const sample = "SGVsbG8sIFdvcmxkISBUaGlzIGlzIGEgZGVjb2RlZCBCYXNlNjQgbWVzc2FnZS4=";
+ setInput(sample);
+ decodeBase64(sample);
+ }
+ };
+
+ const inputBytes = new TextEncoder().encode(input).length;
+ const outputBytes = new TextEncoder().encode(output).length;
+
+ return (
+
+ Base64 Encode & Decode Online — DevToolbox
+
+
+
+ {/* Controls Bar */}
+
+
+
+
+
+
+ Encode to Base64
+
+
+
+
+
+ Decode from Base64
+
+
+
+
+
+ Swap
+
+
+ Clear
+
+
+
+ {/* Sample buttons */}
+
+ Samples:
+ loadSample("text")}
+ className="px-2.5 py-1 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 rounded transition-colors cursor-pointer"
+ >
+ Plain Text
+
+ loadSample("unicode")}
+ className="px-2.5 py-1 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 rounded transition-colors cursor-pointer"
+ >
+ Unicode / Emojis
+
+ loadSample("base64")}
+ className="px-2.5 py-1 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 rounded transition-colors cursor-pointer"
+ >
+ Base64
+
+
+
+
+ {/* Options */}
+
+
+ {
+ const val = e.target.checked;
+ setUrlSafe(val);
+ if (lastAction === "encode" && input) {
+ encodeBase64(input, val, splitLines);
+ }
+ }}
+ className="w-4 h-4 text-blue-600 rounded border-gray-300 dark:border-gray-600 focus:ring-blue-500 dark:bg-gray-700"
+ />
+ URL-safe Base64 (-_ instead of +/)
+
+
+ {
+ const val = e.target.checked;
+ setSplitLines(val);
+ if (lastAction === "encode" && input) {
+ encodeBase64(input, urlSafe, val);
+ }
+ }}
+ className="w-4 h-4 text-blue-600 rounded border-gray-300 dark:border-gray-600 focus:ring-blue-500 dark:bg-gray-700"
+ />
+ Wrap lines at 76 characters (MIME)
+
+
+
+ {/* Error Notification */}
+ {error && (
+
+
+
+
+
+
Decoding Error
+
{error}
+
+
+ )}
+
+ {/* Two-Column Editor Grid */}
+
+ {/* Input Area */}
+
+
+
+ Input String
+
+
+ {input.length} chars | {inputBytes} bytes
+
+
+
+
+ {/* Output Area */}
+
+
+
+
+ Output Result
+
+ {lastAction && (
+
+ {lastAction === "encode" ? "Base64 Encoded" : "Decoded Text"}
+
+ )}
+
+
+
+ {output.length} chars | {outputBytes} bytes
+
+
+ {copied ? (
+ <>
+
+
+
+ Copied!
+ >
+ ) : (
+ <>
+
+
+
+ Copy
+ >
+ )}
+
+
+
+
+
+
+
+ {/* Information & Feature Highlights */}
+
+
+
Full UTF-8 Unicode Support
+
Handles international character sets, special symbols, and emojis accurately without Latin1 byte truncation errors.
+
+
+
100% Client-Side & Private
+
Your data never leaves your browser. All encoding and decoding operations execute locally in JavaScript.
+
+
+
URL-Safe & MIME Formatting
+
Convert standard Base64 characters into URL-friendly variants or wrap output lines for MIME/email standards.
+
+
+
+
+ );
+}
diff --git a/src/app/tools/box-shadow/page.tsx b/src/app/tools/box-shadow/page.tsx
new file mode 100644
index 0000000..67e8f34
--- /dev/null
+++ b/src/app/tools/box-shadow/page.tsx
@@ -0,0 +1,726 @@
+"use client";
+
+import React, { useState, useMemo } from "react";
+import ToolLayout from "@/components/ToolLayout";
+
+interface ShadowPreset {
+ name: string;
+ horizontal: number;
+ vertical: number;
+ blur: number;
+ spread: number;
+ color: string;
+ opacity: number;
+ inset: boolean;
+}
+
+const PRESETS: ShadowPreset[] = [
+ {
+ name: "Default Soft",
+ horizontal: 5,
+ vertical: 5,
+ blur: 10,
+ spread: 0,
+ color: "#000000",
+ opacity: 0.3,
+ inset: false,
+ },
+ {
+ name: "Subtle Card",
+ horizontal: 0,
+ vertical: 4,
+ blur: 6,
+ spread: -1,
+ color: "#000000",
+ opacity: 0.1,
+ inset: false,
+ },
+ {
+ name: "Floating Depth",
+ horizontal: 0,
+ vertical: 20,
+ blur: 25,
+ spread: -5,
+ color: "#000000",
+ opacity: 0.15,
+ inset: false,
+ },
+ {
+ name: "Hard Brutalism",
+ horizontal: 6,
+ vertical: 6,
+ blur: 0,
+ spread: 0,
+ color: "#000000",
+ opacity: 1,
+ inset: false,
+ },
+ {
+ name: "Blue Glow",
+ horizontal: 0,
+ vertical: 0,
+ blur: 25,
+ spread: 5,
+ color: "#3b82f6",
+ opacity: 0.5,
+ inset: false,
+ },
+ {
+ name: "Purple Halo",
+ horizontal: 0,
+ vertical: 12,
+ blur: 30,
+ spread: 2,
+ color: "#8b5cf6",
+ opacity: 0.35,
+ inset: false,
+ },
+ {
+ name: "Inset Well",
+ horizontal: 0,
+ vertical: 4,
+ blur: 8,
+ spread: 0,
+ color: "#000000",
+ opacity: 0.25,
+ inset: true,
+ },
+ {
+ name: "Emerald Light",
+ horizontal: 0,
+ vertical: 8,
+ blur: 20,
+ spread: -2,
+ color: "#10b981",
+ opacity: 0.4,
+ inset: false,
+ },
+];
+
+// Helper to convert hex to RGB
+function hexToRgb(hex: string): { r: number; g: number; b: number } {
+ let cleanHex = hex.replace("#", "").trim();
+ if (cleanHex.length === 3) {
+ cleanHex = cleanHex
+ .split("")
+ .map((c) => c + c)
+ .join("");
+ }
+ const num = parseInt(cleanHex, 16);
+ if (isNaN(num) || cleanHex.length !== 6) {
+ return { r: 0, g: 0, b: 0 };
+ }
+ return {
+ r: (num >> 16) & 255,
+ g: (num >> 8) & 255,
+ b: num & 255,
+ };
+}
+
+export default function BoxShadowGeneratorPage() {
+ // Shadow state variables
+ const [horizontal, setHorizontal] = useState(5);
+ const [vertical, setVertical] = useState(5);
+ const [blur, setBlur] = useState(10);
+ const [spread, setSpread] = useState(0);
+ const [color, setColor] = useState("#000000");
+ const [opacity, setOpacity] = useState(0.3);
+ const [inset, setInset] = useState(false);
+
+ // Box & Preview customization
+ const [boxColor, setBoxColor] = useState("#ffffff");
+ const [borderRadius, setBorderRadius] = useState(16);
+ const [previewBg, setPreviewBg] = useState<"light" | "dark" | "checker">("light");
+ const [includeVendorPrefixes, setIncludeVendorPrefixes] = useState(false);
+ const [codeFormat, setCodeFormat] = useState<"standard" | "full" | "tailwind">("standard");
+
+ const [copied, setCopied] = useState(false);
+
+ // Computed RGBA string
+ const rgbaColor = useMemo(() => {
+ const { r, g, b } = hexToRgb(color);
+ const roundedOpacity = Math.round(opacity * 100) / 100;
+ return `rgba(${r}, ${g}, ${b}, ${roundedOpacity})`;
+ }, [color, opacity]);
+
+ // Computed box-shadow value
+ const shadowValue = useMemo(() => {
+ const insetText = inset ? "inset " : "";
+ return `${insetText}${horizontal}px ${vertical}px ${blur}px ${spread}px ${rgbaColor}`;
+ }, [inset, horizontal, vertical, blur, spread, rgbaColor]);
+
+ // Generated CSS Code
+ const generatedCode = useMemo(() => {
+ if (codeFormat === "tailwind") {
+ const tailwindArbitrary = shadowValue.replace(/\s+/g, "_");
+ return `className="shadow-[${tailwindArbitrary}]"`;
+ }
+
+ let code = "";
+ if (includeVendorPrefixes) {
+ code += `-webkit-box-shadow: ${shadowValue};\n`;
+ code += `-moz-box-shadow: ${shadowValue};\n`;
+ }
+ code += `box-shadow: ${shadowValue};`;
+
+ if (codeFormat === "full") {
+ return `.my-custom-box {\n background-color: ${boxColor};\n border-radius: ${borderRadius}px;\n ${code.split("\n").join("\n ")}\n}`;
+ }
+
+ return code;
+ }, [shadowValue, includeVendorPrefixes, codeFormat, boxColor, borderRadius]);
+
+ const handleCopy = async () => {
+ try {
+ await navigator.clipboard.writeText(generatedCode);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ } catch {
+ const textArea = document.createElement("textarea");
+ textArea.value = generatedCode;
+ document.body.appendChild(textArea);
+ textArea.select();
+ document.execCommand("copy");
+ document.body.removeChild(textArea);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ }
+ };
+
+ const applyPreset = (preset: ShadowPreset) => {
+ setHorizontal(preset.horizontal);
+ setVertical(preset.vertical);
+ setBlur(preset.blur);
+ setSpread(preset.spread);
+ setColor(preset.color);
+ setOpacity(preset.opacity);
+ setInset(preset.inset);
+ };
+
+ const resetDefaults = () => {
+ setHorizontal(5);
+ setVertical(5);
+ setBlur(10);
+ setSpread(0);
+ setColor("#000000");
+ setOpacity(0.3);
+ setInset(false);
+ setBoxColor("#ffffff");
+ setBorderRadius(16);
+ };
+
+ return (
+
+
+ {/* Live Preview Area */}
+
+
+
+ Live Preview
+
+
+
+ Background:
+
+
+ setPreviewBg("light")}
+ className={`px-2.5 py-1 rounded-md transition-colors ${
+ previewBg === "light"
+ ? "bg-white dark:bg-gray-700 text-gray-900 dark:text-white font-medium shadow-xs"
+ : "text-gray-600 dark:text-gray-400 hover:text-gray-900"
+ }`}
+ >
+ Light
+
+ setPreviewBg("dark")}
+ className={`px-2.5 py-1 rounded-md transition-colors ${
+ previewBg === "dark"
+ ? "bg-white dark:bg-gray-700 text-gray-900 dark:text-white font-medium shadow-xs"
+ : "text-gray-600 dark:text-gray-400 hover:text-gray-900"
+ }`}
+ >
+ Dark
+
+ setPreviewBg("checker")}
+ className={`px-2.5 py-1 rounded-md transition-colors ${
+ previewBg === "checker"
+ ? "bg-white dark:bg-gray-700 text-gray-900 dark:text-white font-medium shadow-xs"
+ : "text-gray-600 dark:text-gray-400 hover:text-gray-900"
+ }`}
+ >
+ Pattern
+
+
+
+
+
+
+ {/* The Shadow Card */}
+
+
+
+ Shadow Preview
+
+
+ {horizontal}px {vertical}px {blur}px {spread}px
+
+
+
+
+
+ {/* Presets Gallery */}
+
+
+
+ Quick Presets
+
+
+ Reset to Default
+
+
+
+ {PRESETS.map((preset) => {
+ const { r, g, b } = hexToRgb(preset.color);
+ const pRgba = `rgba(${r}, ${g}, ${b}, ${preset.opacity})`;
+ const pShadow = `${preset.inset ? "inset " : ""}${preset.horizontal}px ${preset.vertical}px ${preset.blur}px ${preset.spread}px ${pRgba}`;
+
+ return (
+
applyPreset(preset)}
+ className="group flex flex-col items-center justify-center p-3 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:border-blue-500 dark:hover:border-blue-500 transition-all hover:scale-[1.03]"
+ >
+
+
+ {preset.name}
+
+
+ );
+ })}
+
+
+
+ {/* Shadow Control Sliders Grid */}
+
+ {/* Controls Column 1: Dimensions & Geometry */}
+
+
+
+
+
+ Shadow Geometry
+
+
+ {/* Horizontal Offset */}
+
+
+
+ Horizontal Offset (X)
+
+
+ setHorizontal(Number(e.target.value))}
+ className="w-16 px-1.5 py-0.5 text-xs font-mono text-right rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:outline-none"
+ />
+ px
+
+
+
setHorizontal(Number(e.target.value))}
+ className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700 accent-blue-600"
+ />
+
+ -50px
+ 0px
+ 50px
+
+
+
+ {/* Vertical Offset */}
+
+
+
+ Vertical Offset (Y)
+
+
+ setVertical(Number(e.target.value))}
+ className="w-16 px-1.5 py-0.5 text-xs font-mono text-right rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:outline-none"
+ />
+ px
+
+
+
setVertical(Number(e.target.value))}
+ className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700 accent-blue-600"
+ />
+
+ -50px
+ 0px
+ 50px
+
+
+
+ {/* Blur Radius */}
+
+
+
+ Blur Radius
+
+
+ setBlur(Math.max(0, Number(e.target.value)))}
+ className="w-16 px-1.5 py-0.5 text-xs font-mono text-right rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:outline-none"
+ />
+ px
+
+
+
setBlur(Number(e.target.value))}
+ className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700 accent-blue-600"
+ />
+
+ 0px
+ 50px
+ 100px
+
+
+
+ {/* Spread Radius */}
+
+
+
+ Spread Radius
+
+
+ setSpread(Number(e.target.value))}
+ className="w-16 px-1.5 py-0.5 text-xs font-mono text-right rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:outline-none"
+ />
+ px
+
+
+
setSpread(Number(e.target.value))}
+ className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700 accent-blue-600"
+ />
+
+ -50px
+ 0px
+ 50px
+
+
+
+
+ {/* Controls Column 2: Color, Opacity & Box Appearance */}
+
+
+
+
+
+ Shadow Color & Box Styling
+
+
+ {/* Shadow Color & Inset Toggle */}
+
+
+
+ {/* Inset Toggle Checkbox */}
+
+
+ Shadow Type
+
+
+ setInset(e.target.checked)}
+ className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
+ />
+
+ Inset Shadow (Inner)
+
+
+
+
+
+ {/* Opacity Slider */}
+
+
+
+ Shadow Opacity
+
+
+ {
+ const val = Math.max(0, Math.min(1, Number(e.target.value)));
+ setOpacity(isNaN(val) ? 0 : val);
+ }}
+ className="w-16 px-1.5 py-0.5 text-xs font-mono text-right rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:outline-none"
+ />
+
+ ({Math.round(opacity * 100)}%)
+
+
+
+
setOpacity(Number(e.target.value))}
+ className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700 accent-blue-600"
+ />
+
+ 0 (Transparent)
+ 0.5
+ 1.0 (Opaque)
+
+
+
+ {/* Box Appearance: Box Background & Border Radius */}
+
+ {/* Box Color */}
+
+
+ Box Background Color
+
+
+
+
+ {/* Border Radius */}
+
+
+
+ Border Radius
+
+
+ {borderRadius}px
+
+
+
setBorderRadius(Number(e.target.value))}
+ className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700 accent-blue-600"
+ />
+
+
+
+
+
+ {/* Generated CSS Code Block */}
+
+
+
+
+ Generated CSS
+
+
+ setCodeFormat("standard")}
+ className={`px-2.5 py-1 rounded-md transition-colors ${
+ codeFormat === "standard"
+ ? "bg-white dark:bg-gray-700 text-gray-900 dark:text-white font-medium shadow-xs"
+ : "text-gray-600 dark:text-gray-400 hover:text-gray-900"
+ }`}
+ >
+ Standard
+
+ setCodeFormat("full")}
+ className={`px-2.5 py-1 rounded-md transition-colors ${
+ codeFormat === "full"
+ ? "bg-white dark:bg-gray-700 text-gray-900 dark:text-white font-medium shadow-xs"
+ : "text-gray-600 dark:text-gray-400 hover:text-gray-900"
+ }`}
+ >
+ Full CSS Class
+
+ setCodeFormat("tailwind")}
+ className={`px-2.5 py-1 rounded-md transition-colors ${
+ codeFormat === "tailwind"
+ ? "bg-white dark:bg-gray-700 text-gray-900 dark:text-white font-medium shadow-xs"
+ : "text-gray-600 dark:text-gray-400 hover:text-gray-900"
+ }`}
+ >
+ Tailwind
+
+
+
+ {codeFormat !== "tailwind" && (
+
+ setIncludeVendorPrefixes(e.target.checked)}
+ className="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
+ />
+ Vendor prefixes (-webkit, -moz)
+
+ )}
+
+
+
+ {copied ? (
+ <>
+
+
+
+ Copied!
+ >
+ ) : (
+ <>
+
+
+
+ Copy CSS
+ >
+ )}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/tools/case-converter/page.tsx b/src/app/tools/case-converter/page.tsx
new file mode 100644
index 0000000..ec4ade7
--- /dev/null
+++ b/src/app/tools/case-converter/page.tsx
@@ -0,0 +1,508 @@
+"use client";
+
+import { useState, useId, useCallback, useEffect } from "react";
+import ToolLayout from "@/components/ToolLayout";
+
+// Helper to extract words from various formats (camelCase, PascalCase, snake_case, kebab-case, normal text)
+function getWords(str: string): string[] {
+ if (!str) return [];
+ return (
+ str
+ // Insert space before capital letters in camelCase / PascalCase
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
+ // Insert space between consecutive uppercase letters followed by lowercase (e.g. "JSONParser" -> "JSON Parser")
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
+ // Replace punctuation, underscores, hyphens, slashes, dots with space
+ .replace(/[-_./\\:[\](){}<>]+/g, " ")
+ .trim()
+ .split(/\s+/)
+ .filter(Boolean)
+ );
+}
+
+// Case conversion algorithms
+const CONVERTERS: Record<
+ string,
+ { name: string; description: string; example: string; fn: (text: string) => string }
+> = {
+ uppercase: {
+ name: "UPPERCASE",
+ description: "Convert all characters to uppercase",
+ example: "HELLO WORLD",
+ fn: (text) => text.toUpperCase(),
+ },
+ lowercase: {
+ name: "lowercase",
+ description: "Convert all characters to lowercase",
+ example: "hello world",
+ fn: (text) => text.toLowerCase(),
+ },
+ titleCase: {
+ name: "Title Case",
+ description: "Capitalize the first letter of every word",
+ example: "Hello World",
+ fn: (text) =>
+ text
+ .toLowerCase()
+ .replace(/(^|\s|[-_./])([a-z0-9])/g, (_, boundary, char) => boundary + char.toUpperCase()),
+ },
+ sentenceCase: {
+ name: "Sentence case",
+ description: "Capitalize the first letter of each sentence",
+ example: "Hello world. This is a sentence.",
+ fn: (text) =>
+ text
+ .toLowerCase()
+ .replace(/(^\s*|\.\s*|\?\s*|!\s*|\n\s*)([a-z])/g, (_, p1, p2) => p1 + p2.toUpperCase()),
+ },
+ camelCase: {
+ name: "camelCase",
+ description: "First word lowercase, subsequent words capitalized without spaces",
+ example: "helloWorld",
+ fn: (text) => {
+ const lines = text.split("\n");
+ return lines
+ .map((line) => {
+ const words = getWords(line);
+ if (words.length === 0) return "";
+ return (
+ words[0].toLowerCase() +
+ words
+ .slice(1)
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
+ .join("")
+ );
+ })
+ .join("\n");
+ },
+ },
+ pascalCase: {
+ name: "PascalCase",
+ description: "Capitalize every word without spaces",
+ example: "HelloWorld",
+ fn: (text) => {
+ const lines = text.split("\n");
+ return lines
+ .map((line) => {
+ const words = getWords(line);
+ return words
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
+ .join("");
+ })
+ .join("\n");
+ },
+ },
+ snakeCase: {
+ name: "snake_case",
+ description: "Lowercase words separated by underscores",
+ example: "hello_world",
+ fn: (text) => {
+ const lines = text.split("\n");
+ return lines
+ .map((line) => {
+ const words = getWords(line);
+ return words.map((w) => w.toLowerCase()).join("_");
+ })
+ .join("\n");
+ },
+ },
+ kebabCase: {
+ name: "kebab-case",
+ description: "Lowercase words separated by hyphens",
+ example: "hello-world",
+ fn: (text) => {
+ const lines = text.split("\n");
+ return lines
+ .map((line) => {
+ const words = getWords(line);
+ return words.map((w) => w.toLowerCase()).join("-");
+ })
+ .join("\n");
+ },
+ },
+ constantCase: {
+ name: "CONSTANT_CASE",
+ description: "Uppercase words separated by underscores",
+ example: "HELLO_WORLD",
+ fn: (text) => {
+ const lines = text.split("\n");
+ return lines
+ .map((line) => {
+ const words = getWords(line);
+ return words.map((w) => w.toUpperCase()).join("_");
+ })
+ .join("\n");
+ },
+ },
+ dotCase: {
+ name: "dot.case",
+ description: "Lowercase words separated by dots",
+ example: "hello.world",
+ fn: (text) => {
+ const lines = text.split("\n");
+ return lines
+ .map((line) => {
+ const words = getWords(line);
+ return words.map((w) => w.toLowerCase()).join(".");
+ })
+ .join("\n");
+ },
+ },
+ headerCase: {
+ name: "Header-Case",
+ description: "Capitalized words separated by hyphens (Train-Case)",
+ example: "Hello-World",
+ fn: (text) => {
+ const lines = text.split("\n");
+ return lines
+ .map((line) => {
+ const words = getWords(line);
+ return words
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
+ .join("-");
+ })
+ .join("\n");
+ },
+ },
+ pathCase: {
+ name: "path/case",
+ description: "Lowercase words separated by forward slashes",
+ example: "hello/world",
+ fn: (text) => {
+ const lines = text.split("\n");
+ return lines
+ .map((line) => {
+ const words = getWords(line);
+ return words.map((w) => w.toLowerCase()).join("/");
+ })
+ .join("\n");
+ },
+ },
+ alternatingCase: {
+ name: "aLtErNaTiNg cAsE",
+ description: "Alternate between lowercase and uppercase characters",
+ example: "hElLo wOrLd",
+ fn: (text) => {
+ let isUpper = false;
+ return text
+ .split("")
+ .map((char) => {
+ if (/[a-zA-Z]/.test(char)) {
+ const res = isUpper ? char.toUpperCase() : char.toLowerCase();
+ isUpper = !isUpper;
+ return res;
+ }
+ return char;
+ })
+ .join("");
+ },
+ },
+ inverseCase: {
+ name: "InVeRsE cAsE",
+ description: "Invert case of each individual character",
+ example: "hELLO wORLD",
+ fn: (text) =>
+ text
+ .split("")
+ .map((c) =>
+ c === c.toUpperCase() ? c.toLowerCase() : c.toUpperCase()
+ )
+ .join(""),
+ },
+};
+
+const SAMPLE_TEXT =
+ "the quick brown fox jumps over the lazy dog. developer tools make life easier!";
+
+export default function CaseConverterPage() {
+ const inputId = useId();
+ const outputId = useId();
+
+ const [input, setInput] = useState(SAMPLE_TEXT);
+ const [output, setOutput] = useState("");
+ const [activeCaseKey, setActiveCaseKey] = useState("titleCase");
+ const [copied, setCopied] = useState(false);
+ const [previewCopiedKey, setPreviewCopiedKey] = useState(null);
+
+ // Auto-convert when input or activeCaseKey changes
+ const applyConversion = useCallback(
+ (key: string, textToConvert: string) => {
+ const converter = CONVERTERS[key];
+ if (converter) {
+ setOutput(converter.fn(textToConvert));
+ }
+ },
+ []
+ );
+
+ useEffect(() => {
+ applyConversion(activeCaseKey, input);
+ }, [input, activeCaseKey, applyConversion]);
+
+ const handleCaseButtonClick = (key: string) => {
+ setActiveCaseKey(key);
+ applyConversion(key, input);
+ };
+
+ const handleCopy = async () => {
+ if (!output) return;
+ try {
+ await navigator.clipboard.writeText(output);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ } catch {
+ // Fallback
+ }
+ };
+
+ const handleCopyPreview = async (val: string, key: string) => {
+ try {
+ await navigator.clipboard.writeText(val);
+ setPreviewCopiedKey(key);
+ setTimeout(() => setPreviewCopiedKey(null), 2000);
+ } catch {
+ // Fallback
+ }
+ };
+
+ const handleSwap = () => {
+ const temp = output;
+ setInput(temp);
+ setOutput(input);
+ };
+
+ const handleClear = () => {
+ setInput("");
+ setOutput("");
+ };
+
+ const handleLoadSample = () => {
+ setInput(SAMPLE_TEXT);
+ };
+
+ return (
+
+ Case Converter Online — DevToolbox
+
+
+
+ {/* Top Actions Bar */}
+
+
+
+ Load Sample
+
+
+
+
+
+ Swap
+
+
+ Clear
+
+
+
+
+ Click any case button below to transform immediately
+
+
+
+ {/* Case Transformation Buttons Grid */}
+
+
+ Choose Transformation Case
+
+
+ {Object.entries(CONVERTERS).map(([key, conv]) => {
+ const isActive = activeCaseKey === key;
+ return (
+
handleCaseButtonClick(key)}
+ className={`p-2.5 rounded-lg border text-left transition-all cursor-pointer shadow-xs ${
+ isActive
+ ? "bg-blue-600 border-blue-600 text-white ring-2 ring-blue-500/40"
+ : "bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-gray-900 dark:text-gray-100 hover:bg-blue-50/70 dark:hover:bg-blue-950/30 hover:border-blue-300 dark:hover:border-blue-700"
+ }`}
+ >
+ {conv.name}
+
+ {conv.example}
+
+
+ );
+ })}
+
+
+
+ {/* Input & Output Two-Column Grid */}
+
+ {/* Input Area */}
+
+
+
+ Input Text
+
+
+ {input.length} chars | {input.trim() ? input.trim().split(/\s+/).length : 0} words
+
+
+
+
+ {/* Output Area */}
+
+
+
+
+ Converted Output
+
+
+ {CONVERTERS[activeCaseKey]?.name || "Result"}
+
+
+
+
+ {output.length} chars
+
+
+ {copied ? (
+ <>
+
+
+
+ Copied!
+ >
+ ) : (
+ <>
+
+
+
+ Copy
+ >
+ )}
+
+
+
+
+
+
+
+ {/* All Cases Live Preview Cards */}
+
+
+
+ Live Preview of All Cases
+
+
+ Click "Apply" or "Copy" on any case
+
+
+
+
+ {Object.entries(CONVERTERS).map(([key, conv]) => {
+ const convertedVal = input ? conv.fn(input) : conv.example;
+ const isSelected = activeCaseKey === key;
+ return (
+
+
+
+
+ {conv.name}
+
+ {isSelected && (
+
+ Active
+
+ )}
+
+
+ handleCaseButtonClick(key)}
+ className="px-2 py-0.5 text-xs text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-950/40 rounded transition-colors cursor-pointer font-medium"
+ >
+ Apply
+
+ handleCopyPreview(convertedVal, key)}
+ className="px-2 py-0.5 text-xs text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors cursor-pointer"
+ >
+ {previewCopiedKey === key ? "Copied!" : "Copy"}
+
+
+
+
+
+ {convertedVal || Empty }
+
+
+ );
+ })}
+
+
+
+
+ );
+}
diff --git a/src/app/tools/color-converter/page.tsx b/src/app/tools/color-converter/page.tsx
new file mode 100644
index 0000000..cb1f76d
--- /dev/null
+++ b/src/app/tools/color-converter/page.tsx
@@ -0,0 +1,1015 @@
+"use client";
+
+import { useState, useCallback, useMemo } from "react";
+import ToolLayout from "@/components/ToolLayout";
+
+// Helper: clamp number
+function clamp(val: number, min: number, max: number): number {
+ return Math.min(Math.max(val, min), max);
+}
+
+// Convert RGB to HEX (6-digit)
+function rgbToHex(r: number, g: number, b: number): string {
+ const toHex = (n: number) => clamp(Math.round(n), 0, 255).toString(16).padStart(2, "0").toUpperCase();
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
+}
+
+// Convert RGBA to HEX8 (8-digit)
+function rgbaToHex8(r: number, g: number, b: number, a: number): string {
+ const toHex = (n: number) => clamp(Math.round(n), 0, 255).toString(16).padStart(2, "0").toUpperCase();
+ const alphaHex = clamp(Math.round(a * 255), 0, 255).toString(16).padStart(2, "0").toUpperCase();
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}${alphaHex}`;
+}
+
+// Convert RGB to HSL
+function rgbToHsl(r: number, g: number, b: number): { h: number; s: number; l: number } {
+ const rNorm = clamp(r, 0, 255) / 255;
+ const gNorm = clamp(g, 0, 255) / 255;
+ const bNorm = clamp(b, 0, 255) / 255;
+
+ const max = Math.max(rNorm, gNorm, bNorm);
+ const min = Math.min(rNorm, gNorm, bNorm);
+ const d = max - min;
+ const l = (max + min) / 2;
+
+ if (d === 0) {
+ return { h: 0, s: 0, l: Math.round(l * 100) };
+ }
+
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+ let h = 0;
+
+ if (max === rNorm) {
+ h = (gNorm - bNorm) / d + (gNorm < bNorm ? 6 : 0);
+ } else if (max === gNorm) {
+ h = (bNorm - rNorm) / d + 2;
+ } else {
+ h = (rNorm - gNorm) / d + 4;
+ }
+
+ h = Math.round(h * 60) % 360;
+ return {
+ h: h < 0 ? h + 360 : h,
+ s: Math.round(s * 100),
+ l: Math.round(l * 100),
+ };
+}
+
+// Convert HSL to RGB
+function hslToRgb(h: number, s: number, l: number): { r: number; g: number; b: number } {
+ const hNorm = ((h % 360) + 360) % 360 / 360;
+ const sNorm = clamp(s, 0, 100) / 100;
+ const lNorm = clamp(l, 0, 100) / 100;
+
+ if (sNorm === 0) {
+ const val = Math.round(lNorm * 255);
+ return { r: val, g: val, b: val };
+ }
+
+ const hue2rgb = (p: number, q: number, t: number) => {
+ let tAdj = t;
+ if (tAdj < 0) tAdj += 1;
+ if (tAdj > 1) tAdj -= 1;
+ if (tAdj < 1 / 6) return p + (q - p) * 6 * tAdj;
+ if (tAdj < 1 / 2) return q;
+ if (tAdj < 2 / 3) return p + (q - p) * (2 / 3 - tAdj) * 6;
+ return p;
+ };
+
+ const q = lNorm < 0.5 ? lNorm * (1 + sNorm) : lNorm + sNorm - lNorm * sNorm;
+ const p = 2 * lNorm - q;
+
+ const r = Math.round(hue2rgb(p, q, hNorm + 1 / 3) * 255);
+ const g = Math.round(hue2rgb(p, q, hNorm) * 255);
+ const b = Math.round(hue2rgb(p, q, hNorm - 1 / 3) * 255);
+
+ return { r, g, b };
+}
+
+// Convert RGB to HSV
+function rgbToHsv(r: number, g: number, b: number): { h: number; s: number; v: number } {
+ const rNorm = clamp(r, 0, 255) / 255;
+ const gNorm = clamp(g, 0, 255) / 255;
+ const bNorm = clamp(b, 0, 255) / 255;
+
+ const max = Math.max(rNorm, gNorm, bNorm);
+ const min = Math.min(rNorm, gNorm, bNorm);
+ const d = max - min;
+ const v = max;
+
+ if (max === 0) {
+ return { h: 0, s: 0, v: 0 };
+ }
+
+ const s = d / max;
+ let h = 0;
+
+ if (d !== 0) {
+ if (max === rNorm) {
+ h = (gNorm - bNorm) / d + (gNorm < bNorm ? 6 : 0);
+ } else if (max === gNorm) {
+ h = (bNorm - rNorm) / d + 2;
+ } else {
+ h = (rNorm - gNorm) / d + 4;
+ }
+ h = Math.round(h * 60) % 360;
+ }
+
+ return {
+ h: h < 0 ? h + 360 : h,
+ s: Math.round(s * 100),
+ v: Math.round(v * 100),
+ };
+}
+
+// Convert RGB to CMYK
+function rgbToCmyk(r: number, g: number, b: number): { c: number; m: number; y: number; k: number } {
+ const rNorm = clamp(r, 0, 255) / 255;
+ const gNorm = clamp(g, 0, 255) / 255;
+ const bNorm = clamp(b, 0, 255) / 255;
+
+ const max = Math.max(rNorm, gNorm, bNorm);
+ const k = 1 - max;
+
+ if (k === 1) {
+ return { c: 0, m: 0, y: 0, k: 100 };
+ }
+
+ const c = Math.round(((1 - rNorm - k) / (1 - k)) * 100);
+ const m = Math.round(((1 - gNorm - k) / (1 - k)) * 100);
+ const y = Math.round(((1 - bNorm - k) / (1 - k)) * 100);
+
+ return {
+ c: clamp(c, 0, 100),
+ m: clamp(m, 0, 100),
+ y: clamp(y, 0, 100),
+ k: Math.round(k * 100),
+ };
+}
+
+// Relative Luminance for WCAG
+function getLuminance(r: number, g: number, b: number): number {
+ const a = [r, g, b].map((v) => {
+ const s = v / 255;
+ return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
+ });
+ return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
+}
+
+// Contrast ratio
+function getContrastRatio(l1: number, l2: number): number {
+ const lighter = Math.max(l1, l2);
+ const darker = Math.min(l1, l2);
+ return (lighter + 0.05) / (darker + 0.05);
+}
+
+// Parse various HEX formats (#RGB, #RGBA, #RRGGBB, #RRGGBBAA, and without #)
+function parseHex(input: string): { r: number; g: number; b: number; a: number } | null {
+ let cleaned = input.trim().replace(/^#/, "");
+ if (!/^[0-9a-fA-F]+$/.test(cleaned)) return null;
+
+ if (cleaned.length === 3) {
+ // RGB -> RRGGBB
+ cleaned = cleaned
+ .split("")
+ .map((c) => c + c)
+ .join("");
+ } else if (cleaned.length === 4) {
+ // RGBA -> RRGGBBAA
+ cleaned = cleaned
+ .split("")
+ .map((c) => c + c)
+ .join("");
+ }
+
+ if (cleaned.length === 6) {
+ const r = parseInt(cleaned.slice(0, 2), 16);
+ const g = parseInt(cleaned.slice(2, 4), 16);
+ const b = parseInt(cleaned.slice(4, 6), 16);
+ return { r, g, b, a: 1 };
+ } else if (cleaned.length === 8) {
+ const r = parseInt(cleaned.slice(0, 2), 16);
+ const g = parseInt(cleaned.slice(2, 4), 16);
+ const b = parseInt(cleaned.slice(4, 6), 16);
+ const a = Math.round((parseInt(cleaned.slice(6, 8), 16) / 255) * 100) / 100;
+ return { r, g, b, a };
+ }
+
+ return null;
+}
+
+// Parse RGB / RGBA strings like "rgb(255, 87, 51)", "255, 87, 51", "rgba(255, 87, 51, 0.8)"
+function parseRgbString(input: string): { r: number; g: number; b: number; a: number } | null {
+ const match = input
+ .trim()
+ .match(/^(?:rgba?\(?\s*)?(\d{1,3})[\s,]+(\d{1,3})[\s,]+(\d{1,3})(?:[\s,/]+([\d.]+))?\s*\)?$/i);
+ if (!match) return null;
+
+ const r = parseInt(match[1], 10);
+ const g = parseInt(match[2], 10);
+ const b = parseInt(match[3], 10);
+ const a = match[4] !== undefined ? parseFloat(match[4]) : 1;
+
+ if (r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255 || isNaN(a) || a < 0 || a > 1) {
+ return null;
+ }
+
+ return { r, g, b, a };
+}
+
+// Parse HSL / HSLA strings like "hsl(11, 100%, 60%)", "11, 100, 60", "hsla(11, 100%, 60%, 0.8)"
+function parseHslString(input: string): { h: number; s: number; l: number; a: number } | null {
+ const match = input
+ .trim()
+ .match(
+ /^(?:hsla?\(?\s*)?(\d{1,3}(?:\.\d+)?(?:deg)?|\d+(?:\.\d+)?)[\s,]+(\d{1,3}(?:\.\d+)?%?)[\s,]+(\d{1,3}(?:\.\d+)?%?)(?:[\s,/]+([\d.]+))?\s*\)?$/i
+ );
+ if (!match) return null;
+
+ const h = parseFloat(match[1]);
+ const s = parseFloat(match[2]);
+ const l = parseFloat(match[3]);
+ const a = match[4] !== undefined ? parseFloat(match[4]) : 1;
+
+ if (isNaN(h) || isNaN(s) || isNaN(l) || s < 0 || s > 100 || l < 0 || l > 100 || isNaN(a) || a < 0 || a > 1) {
+ return null;
+ }
+
+ return { h: ((Math.round(h) % 360) + 360) % 360, s: Math.round(s), l: Math.round(l), a };
+}
+
+const PRESET_PALETTES = [
+ { name: "Coral Red", hex: "#FF5733" },
+ { name: "Emerald", hex: "#10B981" },
+ { name: "Ocean Blue", hex: "#0284C7" },
+ { name: "Royal Purple", hex: "#8B5CF6" },
+ { name: "Amber Gold", hex: "#F59E0B" },
+ { name: "Rose Pink", hex: "#F43F5E" },
+ { name: "Cyan Teal", hex: "#06B6D4" },
+ { name: "Dark Slate", hex: "#1E293B" },
+];
+
+export default function ColorConverterPage() {
+ // Master color representation: RGB + Alpha
+ const [color, setColor] = useState({ r: 255, g: 87, b: 51, a: 1 });
+ const [copiedKey, setCopiedKey] = useState(null);
+
+ // Editable input text state
+ const [hexInput, setHexInput] = useState("#FF5733");
+ const [rgbInput, setRgbInput] = useState("rgb(255, 87, 51)");
+ const [hslInput, setHslInput] = useState("hsl(11, 100%, 60%)");
+ const [rgbaInput, setRgbaInput] = useState("rgba(255, 87, 51, 1)");
+
+ // Validation error states
+ const [inputErrors, setInputErrors] = useState<{ [key: string]: string | null }>({});
+
+ // Sync inputs from RGB+A
+ const updateAllFromRgba = useCallback((r: number, g: number, b: number, a: number) => {
+ setColor({ r, g, b, a });
+ const hex = rgbToHex(r, g, b);
+ const hsl = rgbToHsl(r, g, b);
+ const alphaFormatted = Number(a.toFixed(2));
+
+ setHexInput(a < 1 ? rgbaToHex8(r, g, b, a) : hex);
+ setRgbInput(`rgb(${r}, ${g}, ${b})`);
+ setHslInput(`hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`);
+ setRgbaInput(`rgba(${r}, ${g}, ${b}, ${alphaFormatted})`);
+ setInputErrors({});
+ }, []);
+
+ // Handle HEX text input change
+ const handleHexChange = (value: string) => {
+ setHexInput(value);
+ const parsed = parseHex(value);
+ if (parsed) {
+ setColor(parsed);
+ const hsl = rgbToHsl(parsed.r, parsed.g, parsed.b);
+ setRgbInput(`rgb(${parsed.r}, ${parsed.g}, ${parsed.b})`);
+ setHslInput(`hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`);
+ setRgbaInput(`rgba(${parsed.r}, ${parsed.g}, ${parsed.b}, ${Number(parsed.a.toFixed(2))})`);
+ setInputErrors((prev) => ({ ...prev, hex: null }));
+ } else {
+ setInputErrors((prev) => ({ ...prev, hex: "Invalid HEX color format (e.g. #FF5733)" }));
+ }
+ };
+
+ // Handle RGB text input change
+ const handleRgbChange = (value: string) => {
+ setRgbInput(value);
+ const parsed = parseRgbString(value);
+ if (parsed) {
+ setColor((prev) => ({ ...parsed, a: prev.a }));
+ const hex = rgbToHex(parsed.r, parsed.g, parsed.b);
+ const hsl = rgbToHsl(parsed.r, parsed.g, parsed.b);
+ setHexInput(color.a < 1 ? rgbaToHex8(parsed.r, parsed.g, parsed.b, color.a) : hex);
+ setHslInput(`hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`);
+ setRgbaInput(`rgba(${parsed.r}, ${parsed.g}, ${parsed.b}, ${Number(color.a.toFixed(2))})`);
+ setInputErrors((prev) => ({ ...prev, rgb: null }));
+ } else {
+ setInputErrors((prev) => ({ ...prev, rgb: "Invalid RGB format (e.g. 255, 87, 51 or rgb(255, 87, 51))" }));
+ }
+ };
+
+ // Handle HSL text input change
+ const handleHslChange = (value: string) => {
+ setHslInput(value);
+ const parsed = parseHslString(value);
+ if (parsed) {
+ const rgb = hslToRgb(parsed.h, parsed.s, parsed.l);
+ setColor((prev) => ({ ...rgb, a: prev.a }));
+ const hex = rgbToHex(rgb.r, rgb.g, rgb.b);
+ setHexInput(color.a < 1 ? rgbaToHex8(rgb.r, rgb.g, rgb.b, color.a) : hex);
+ setRgbInput(`rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`);
+ setRgbaInput(`rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${Number(color.a.toFixed(2))})`);
+ setInputErrors((prev) => ({ ...prev, hsl: null }));
+ } else {
+ setInputErrors((prev) => ({ ...prev, hsl: "Invalid HSL format (e.g. 11, 100%, 60% or hsl(11, 100%, 60%))" }));
+ }
+ };
+
+ // Handle RGBA text input change
+ const handleRgbaChange = (value: string) => {
+ setRgbaInput(value);
+ const parsed = parseRgbString(value);
+ if (parsed) {
+ setColor(parsed);
+ const hex = rgbToHex(parsed.r, parsed.g, parsed.b);
+ const hsl = rgbToHsl(parsed.r, parsed.g, parsed.b);
+ setHexInput(parsed.a < 1 ? rgbaToHex8(parsed.r, parsed.g, parsed.b, parsed.a) : hex);
+ setRgbInput(`rgb(${parsed.r}, ${parsed.g}, ${parsed.b})`);
+ setHslInput(`hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`);
+ setInputErrors((prev) => ({ ...prev, rgba: null }));
+ } else {
+ setInputErrors((prev) => ({ ...prev, rgba: "Invalid RGBA format (e.g. rgba(255, 87, 51, 0.8))" }));
+ }
+ };
+
+ // Handle Native Color Picker ( )
+ const handleColorPickerChange = (hexValue: string) => {
+ const parsed = parseHex(hexValue);
+ if (parsed) {
+ updateAllFromRgba(parsed.r, parsed.g, parsed.b, color.a);
+ }
+ };
+
+ // Handle Alpha Slider change
+ const handleAlphaChange = (newAlpha: number) => {
+ const clamped = clamp(newAlpha, 0, 1);
+ updateAllFromRgba(color.r, color.g, color.b, clamped);
+ };
+
+ // Copy to clipboard helper
+ const copyToClipboard = useCallback(async (text: string, key: string) => {
+ try {
+ await navigator.clipboard.writeText(text);
+ setCopiedKey(key);
+ setTimeout(() => setCopiedKey((prev) => (prev === key ? null : prev)), 2000);
+ } catch {
+ const textarea = document.createElement("textarea");
+ textarea.value = text;
+ textarea.style.position = "fixed";
+ textarea.style.opacity = "0";
+ document.body.appendChild(textarea);
+ textarea.select();
+ document.execCommand("copy");
+ document.body.removeChild(textarea);
+ setCopiedKey(key);
+ setTimeout(() => setCopiedKey((prev) => (prev === key ? null : prev)), 2000);
+ }
+ }, []);
+
+ // Generate random color
+ const generateRandomColor = () => {
+ const r = Math.floor(Math.random() * 256);
+ const g = Math.floor(Math.random() * 256);
+ const b = Math.floor(Math.random() * 256);
+ updateAllFromRgba(r, g, b, 1);
+ };
+
+ // Computed values
+ const currentHex = useMemo(() => rgbToHex(color.r, color.g, color.b), [color.r, color.g, color.b]);
+ const currentHex8 = useMemo(
+ () => rgbaToHex8(color.r, color.g, color.b, color.a),
+ [color.r, color.g, color.b, color.a]
+ );
+ const currentHsl = useMemo(() => rgbToHsl(color.r, color.g, color.b), [color.r, color.g, color.b]);
+ const currentHsv = useMemo(() => rgbToHsv(color.r, color.g, color.b), [color.r, color.g, color.b]);
+ const currentCmyk = useMemo(() => rgbToCmyk(color.r, color.g, color.b), [color.r, color.g, color.b]);
+
+ const cssRgbString = `rgb(${color.r}, ${color.g}, ${color.b})`;
+ const cssRgbaString = `rgba(${color.r}, ${color.g}, ${color.b}, ${Number(color.a.toFixed(2))})`;
+ const cssHslString = `hsl(${currentHsl.h}, ${currentHsl.s}%, ${currentHsl.l}%)`;
+ const cssHslaString = `hsla(${currentHsl.h}, ${currentHsl.s}%, ${currentHsl.l}%, ${Number(color.a.toFixed(2))})`;
+ const cssHsvString = `hsv(${currentHsv.h}, ${currentHsv.s}%, ${currentHsv.v}%)`;
+ const cssCmykString = `cmyk(${currentCmyk.c}%, ${currentCmyk.m}%, ${currentCmyk.y}%, ${currentCmyk.k}%)`;
+
+ // Luminance & WCAG
+ const luminance = useMemo(() => getLuminance(color.r, color.g, color.b), [color.r, color.g, color.b]);
+ const contrastWhite = useMemo(() => getContrastRatio(1.0, luminance), [luminance]);
+ const contrastBlack = useMemo(() => getContrastRatio(luminance, 0.0), [luminance]);
+
+ // Color Harmonies
+ const harmonies = useMemo(() => {
+ const { h, s, l } = currentHsl;
+ const makeColor = (deg: number) => {
+ const hNorm = ((deg % 360) + 360) % 360;
+ const rgb = hslToRgb(hNorm, s, l);
+ return {
+ hex: rgbToHex(rgb.r, rgb.g, rgb.b),
+ rgb,
+ hsl: { h: hNorm, s, l },
+ };
+ };
+
+ return {
+ complementary: makeColor(h + 180),
+ analogous1: makeColor(h - 30),
+ analogous2: makeColor(h + 30),
+ triadic1: makeColor(h + 120),
+ triadic2: makeColor(h + 240),
+ tints: [15, 30, 45, 60, 75, 90].map((lightness) => {
+ const rgb = hslToRgb(h, s, lightness);
+ return {
+ hex: rgbToHex(rgb.r, rgb.g, rgb.b),
+ rgb,
+ lightness,
+ };
+ }),
+ };
+ }, [currentHsl]);
+
+ return (
+
+
+ {/* Top: Swatch & Quick Picker Controls */}
+
+ {/* Large Color Swatch Box */}
+
+
+ {/* Checkerboard backdrop for transparency */}
+
+ {/* Color Layer */}
+
+
+
+ Alpha: {Math.round(color.a * 100)}%
+
+ copyToClipboard(currentHex, "swatch-hex")}
+ className="inline-flex items-center gap-1 px-2.5 py-1 rounded-md text-xs font-bold shadow-sm backdrop-blur-md bg-white/70 dark:bg-black/70 text-gray-900 dark:text-white hover:bg-white dark:hover:bg-black transition-colors"
+ >
+ {copiedKey === "swatch-hex" ? "Copied!" : currentHex}
+
+
+
+
= contrastBlack ? "#FFFFFF" : "#000000" }}>
+ {cssRgbString}
+
+
+
+
+ {/* Native Color Picker button + Random Color */}
+
+
+ handleColorPickerChange(e.target.value)}
+ className="sr-only"
+ />
+
+
+ Pick Color (Visual)
+
+
+
+
+
+
+
+ Random
+
+
+
+
+ {/* Right Controls: Sliders & Quick Palettes */}
+
+ {/* Channel Sliders */}
+
+
+ {/* Quick Preset Colors */}
+
+
+ Preset Palettes
+
+
+ {PRESET_PALETTES.map((preset) => (
+ {
+ const p = parseHex(preset.hex);
+ if (p) updateAllFromRgba(p.r, p.g, p.b, 1);
+ }}
+ className="flex items-center gap-1.5 rounded-lg border border-gray-200 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-sm hover:scale-105 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200 transition-all"
+ title={preset.name}
+ >
+
+ {preset.name}
+
+ ))}
+
+
+
+
+
+ {/* Live Two-Way Formats Conversion Section */}
+
+
+ Color Formats & Two-Way Editors
+
+
+ Edit any input below to convert instantly into all other formats.
+
+
+
+ {/* HEX Input Card */}
+
+
+
+
+ HEX Value
+
+ copyToClipboard(currentHex, "fmt-hex")}
+ className="text-xs font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 flex items-center gap-1"
+ >
+ {copiedKey === "fmt-hex" ? "Copied!" : "Copy"}
+
+
+
handleHexChange(e.target.value)}
+ placeholder="#FF5733"
+ className="w-full p-3 font-mono text-base rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:outline-none"
+ />
+ {inputErrors.hex &&
{inputErrors.hex}
}
+
+
+ 8-digit HEX (with Alpha):
+ copyToClipboard(currentHex8, "fmt-hex8")}
+ className="font-mono text-gray-800 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400 font-semibold"
+ >
+ {copiedKey === "fmt-hex8" ? "Copied!" : currentHex8}
+
+
+
+
+ {/* RGB Input Card */}
+
+
+
+
+ RGB Value
+
+ copyToClipboard(cssRgbString, "fmt-rgb")}
+ className="text-xs font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 flex items-center gap-1"
+ >
+ {copiedKey === "fmt-rgb" ? "Copied!" : "Copy"}
+
+
+
handleRgbChange(e.target.value)}
+ placeholder="rgb(255, 87, 51)"
+ className="w-full p-3 font-mono text-base rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:outline-none"
+ />
+ {inputErrors.rgb &&
{inputErrors.rgb}
}
+
+
+ Array values:
+
+ [{color.r}, {color.g}, {color.b}]
+
+
+
+
+ {/* RGBA Input Card */}
+
+
+
+
+ RGBA Value (with Alpha)
+
+ copyToClipboard(cssRgbaString, "fmt-rgba")}
+ className="text-xs font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 flex items-center gap-1"
+ >
+ {copiedKey === "fmt-rgba" ? "Copied!" : "Copy"}
+
+
+
handleRgbaChange(e.target.value)}
+ placeholder="rgba(255, 87, 51, 1)"
+ className="w-full p-3 font-mono text-base rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:outline-none"
+ />
+ {inputErrors.rgba &&
{inputErrors.rgba}
}
+
+
+ CSS Output:
+ {cssRgbaString}
+
+
+
+ {/* HSL Input Card */}
+
+
+
+
+ HSL Value
+
+ copyToClipboard(cssHslString, "fmt-hsl")}
+ className="text-xs font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 flex items-center gap-1"
+ >
+ {copiedKey === "fmt-hsl" ? "Copied!" : "Copy"}
+
+
+
handleHslChange(e.target.value)}
+ placeholder="hsl(11, 100%, 60%)"
+ className="w-full p-3 font-mono text-base rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:outline-none"
+ />
+ {inputErrors.hsl &&
{inputErrors.hsl}
}
+
+
+ HSLA (with Alpha):
+ copyToClipboard(cssHslaString, "fmt-hsla")}
+ className="font-mono text-gray-800 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400 font-semibold"
+ >
+ {copiedKey === "fmt-hsla" ? "Copied!" : cssHslaString}
+
+
+
+
+
+
+ {/* Additional Formats (HSV & CMYK) */}
+
+ {/* HSV Card */}
+
+
+
+ HSV / HSB Format
+
+
+ {cssHsvString}
+
+
+
copyToClipboard(cssHsvString, "fmt-hsv")}
+ className="rounded bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 border border-gray-300 hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-700"
+ >
+ {copiedKey === "fmt-hsv" ? "Copied!" : "Copy"}
+
+
+
+ {/* CMYK Card */}
+
+
+
+ CMYK Format (Print)
+
+
+ {cssCmykString}
+
+
+
copyToClipboard(cssCmykString, "fmt-cmyk")}
+ className="rounded bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 border border-gray-300 hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-700"
+ >
+ {copiedKey === "fmt-cmyk" ? "Copied!" : "Copy"}
+
+
+
+
+ {/* Accessibility & Contrast Info */}
+
+
+ WCAG Accessibility Contrast
+
+
+
+ {/* Contrast with White */}
+
+
+ White Text (#FFFFFF)
+
+ {contrastWhite.toFixed(2)}:1
+
+
+
+ Sample White Text
+
+
+ = 4.5
+ ? "bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300"
+ : "bg-red-100 text-red-800 dark:bg-red-950 dark:text-red-300"
+ }`}
+ >
+ AA Normal {contrastWhite >= 4.5 ? "✓ Pass" : "✕ Fail"}
+
+ = 7
+ ? "bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300"
+ : "bg-red-100 text-red-800 dark:bg-red-950 dark:text-red-300"
+ }`}
+ >
+ AAA {contrastWhite >= 7 ? "✓ Pass" : "✕ Fail"}
+
+
+
+
+ {/* Contrast with Black */}
+
+
+ Black Text (#000000)
+
+ {contrastBlack.toFixed(2)}:1
+
+
+
+ Sample Black Text
+
+
+ = 4.5
+ ? "bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300"
+ : "bg-red-100 text-red-800 dark:bg-red-950 dark:text-red-300"
+ }`}
+ >
+ AA Normal {contrastBlack >= 4.5 ? "✓ Pass" : "✕ Fail"}
+
+ = 7
+ ? "bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300"
+ : "bg-red-100 text-red-800 dark:bg-red-950 dark:text-red-300"
+ }`}
+ >
+ AAA {contrastBlack >= 7 ? "✓ Pass" : "✕ Fail"}
+
+
+
+
+
+
+ {/* Color Harmonies */}
+
+
+ Harmonies & Shades (Click to load)
+
+
+
+ {/* Complementary */}
+
+
+ Complementary
+
+ updateAllFromRgba(harmonies.complementary.rgb.r, harmonies.complementary.rgb.g, harmonies.complementary.rgb.b, 1)}
+ className="w-full flex items-center gap-2 p-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 hover:scale-[1.02] transition-transform text-left"
+ >
+
+
+ {harmonies.complementary.hex}
+
+
+
+
+ {/* Analogous */}
+
+
+ Analogous
+
+
+ updateAllFromRgba(harmonies.analogous1.rgb.r, harmonies.analogous1.rgb.g, harmonies.analogous1.rgb.b, 1)}
+ className="flex-1 flex items-center gap-1.5 p-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 hover:scale-[1.02] transition-transform text-left"
+ >
+
+
+ {harmonies.analogous1.hex}
+
+
+ updateAllFromRgba(harmonies.analogous2.rgb.r, harmonies.analogous2.rgb.g, harmonies.analogous2.rgb.b, 1)}
+ className="flex-1 flex items-center gap-1.5 p-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 hover:scale-[1.02] transition-transform text-left"
+ >
+
+
+ {harmonies.analogous2.hex}
+
+
+
+
+
+ {/* Triadic */}
+
+
+ Triadic
+
+
+ updateAllFromRgba(harmonies.triadic1.rgb.r, harmonies.triadic1.rgb.g, harmonies.triadic1.rgb.b, 1)}
+ className="flex-1 flex items-center gap-1.5 p-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 hover:scale-[1.02] transition-transform text-left"
+ >
+
+
+ {harmonies.triadic1.hex}
+
+
+ updateAllFromRgba(harmonies.triadic2.rgb.r, harmonies.triadic2.rgb.g, harmonies.triadic2.rgb.b, 1)}
+ className="flex-1 flex items-center gap-1.5 p-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 hover:scale-[1.02] transition-transform text-left"
+ >
+
+
+ {harmonies.triadic2.hex}
+
+
+
+
+
+
+ {/* Tints & Shades scale */}
+
+
+ Lightness Spectrum (15% to 90%)
+
+
+ {harmonies.tints.map((tint) => (
+ updateAllFromRgba(tint.rgb.r, tint.rgb.g, tint.rgb.b, 1)}
+ className="h-10 rounded-md border border-black/10 hover:scale-105 transition-transform flex items-center justify-center"
+ style={{ backgroundColor: tint.hex }}
+ title={`${tint.hex} (${tint.lightness}%)`}
+ />
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/src/app/tools/css-gradient/page.tsx b/src/app/tools/css-gradient/page.tsx
new file mode 100644
index 0000000..543798c
--- /dev/null
+++ b/src/app/tools/css-gradient/page.tsx
@@ -0,0 +1,723 @@
+"use client";
+
+import React, { useState, useMemo } from "react";
+import ToolLayout from "@/components/ToolLayout";
+
+interface ColorStop {
+ id: string;
+ color: string;
+ stop: number;
+}
+
+type GradientType = "linear" | "radial";
+
+interface Preset {
+ name: string;
+ type: GradientType;
+ angle?: number;
+ shape?: "circle" | "ellipse";
+ position?: string;
+ stops: { color: string; stop: number }[];
+}
+
+const PRESETS: Preset[] = [
+ {
+ name: "Ocean Blue",
+ type: "linear",
+ angle: 135,
+ stops: [
+ { color: "#2563eb", stop: 0 },
+ { color: "#06b6d4", stop: 100 },
+ ],
+ },
+ {
+ name: "Sunset Glow",
+ type: "linear",
+ angle: 135,
+ stops: [
+ { color: "#f97316", stop: 0 },
+ { color: "#ec4899", stop: 100 },
+ ],
+ },
+ {
+ name: "Purple Haze",
+ type: "linear",
+ angle: 135,
+ stops: [
+ { color: "#6366f1", stop: 0 },
+ { color: "#a855f7", stop: 50 },
+ { color: "#ec4899", stop: 100 },
+ ],
+ },
+ {
+ name: "Emerald Forest",
+ type: "linear",
+ angle: 135,
+ stops: [
+ { color: "#059669", stop: 0 },
+ { color: "#10b981", stop: 50 },
+ { color: "#6ee7b7", stop: 100 },
+ ],
+ },
+ {
+ name: "Neon Cyber",
+ type: "linear",
+ angle: 90,
+ stops: [
+ { color: "#00f2fe", stop: 0 },
+ { color: "#4facfe", stop: 100 },
+ ],
+ },
+ {
+ name: "Flame",
+ type: "linear",
+ angle: 45,
+ stops: [
+ { color: "#f12711", stop: 0 },
+ { color: "#f5af19", stop: 100 },
+ ],
+ },
+ {
+ name: "Radial Glow",
+ type: "radial",
+ shape: "circle",
+ position: "center",
+ stops: [
+ { color: "#3b82f6", stop: 0 },
+ { color: "#1e1b4b", stop: 100 },
+ ],
+ },
+ {
+ name: "Midnight Aura",
+ type: "radial",
+ shape: "circle",
+ position: "center",
+ stops: [
+ { color: "#8b5cf6", stop: 0 },
+ { color: "#0f172a", stop: 100 },
+ ],
+ },
+];
+
+const LINEAR_DIRECTIONS = [
+ { label: "→ Right", angle: 90, cssDir: "to right" },
+ { label: "← Left", angle: 270, cssDir: "to left" },
+ { label: "↓ Bottom", angle: 180, cssDir: "to bottom" },
+ { label: "↑ Top", angle: 0, cssDir: "to top" },
+ { label: "↗ Top Right", angle: 45, cssDir: "to top right" },
+ { label: "↘ Bottom Right", angle: 135, cssDir: "to bottom right" },
+ { label: "↙ Bottom Left", angle: 225, cssDir: "to bottom left" },
+ { label: "↖ Top Left", angle: 315, cssDir: "to top left" },
+];
+
+const RADIAL_POSITIONS = [
+ { label: "Center", value: "center" },
+ { label: "Top", value: "top center" },
+ { label: "Bottom", value: "bottom center" },
+ { label: "Left", value: "center left" },
+ { label: "Right", value: "center right" },
+ { label: "Top Left", value: "top left" },
+ { label: "Top Right", value: "top right" },
+ { label: "Bottom Left", value: "bottom left" },
+ { label: "Bottom Right", value: "bottom right" },
+];
+
+export default function CssGradientGeneratorPage() {
+ const [gradientType, setGradientType] = useState("linear");
+ const [angle, setAngle] = useState(135);
+ const [radialShape, setRadialShape] = useState<"circle" | "ellipse">("circle");
+ const [radialPosition, setRadialPosition] = useState("center");
+
+ const [colorStops, setColorStops] = useState([
+ { id: "1", color: "#3b82f6", stop: 0 },
+ { id: "2", color: "#8b5cf6", stop: 100 },
+ ]);
+
+ const [copied, setCopied] = useState(false);
+ const [previewText, setPreviewText] = useState(true);
+ const [codeFormat, setCodeFormat] = useState<"standard" | "full" | "tailwind">("standard");
+
+ // Sort stops for rendering CSS
+ const sortedStops = useMemo(() => {
+ return [...colorStops].sort((a, b) => a.stop - b.stop);
+ }, [colorStops]);
+
+ // Construct CSS gradient value
+ const gradientValue = useMemo(() => {
+ const stopsString = sortedStops
+ .map((s) => `${s.color} ${s.stop}%`)
+ .join(", ");
+
+ if (gradientType === "linear") {
+ return `linear-gradient(${angle}deg, ${stopsString})`;
+ } else {
+ return `radial-gradient(${radialShape} at ${radialPosition}, ${stopsString})`;
+ }
+ }, [gradientType, angle, radialShape, radialPosition, sortedStops]);
+
+ // Output CSS code block
+ const generatedCode = useMemo(() => {
+ const fallbackColor = sortedStops[0]?.color || "#3b82f6";
+
+ if (codeFormat === "tailwind") {
+ return `className="bg-[${gradientValue}]"`;
+ }
+
+ if (codeFormat === "full") {
+ return `/* Fallback for older browsers */\nbackground-color: ${fallbackColor};\n\n/* Modern CSS Gradient */\nbackground: ${gradientValue};`;
+ }
+
+ return `background: ${gradientValue};`;
+ }, [gradientValue, sortedStops, codeFormat]);
+
+ const handleCopy = async () => {
+ try {
+ await navigator.clipboard.writeText(generatedCode);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ } catch {
+ // Fallback
+ const textArea = document.createElement("textarea");
+ textArea.value = generatedCode;
+ document.body.appendChild(textArea);
+ textArea.select();
+ document.execCommand("copy");
+ document.body.removeChild(textArea);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ }
+ };
+
+ const updateStopColor = (id: string, newColor: string) => {
+ setColorStops((prev) =>
+ prev.map((stop) => (stop.id === id ? { ...stop, color: newColor } : stop))
+ );
+ };
+
+ const updateStopPosition = (id: string, newStop: number) => {
+ const clamped = Math.max(0, Math.min(100, isNaN(newStop) ? 0 : newStop));
+ setColorStops((prev) =>
+ prev.map((stop) => (stop.id === id ? { ...stop, stop: clamped } : stop))
+ );
+ };
+
+ const addColorStop = () => {
+ if (colorStops.length >= 6) return;
+
+ // Pick a midpoint or random color
+ const newId = Date.now().toString();
+ if (colorStops.length === 2) {
+ // Add a nice middle color
+ const middleColor = "#ec4899";
+ setColorStops([
+ colorStops[0],
+ { id: newId, color: middleColor, stop: 50 },
+ colorStops[1],
+ ]);
+ } else {
+ const lastStop = colorStops[colorStops.length - 1];
+ const secondLast = colorStops[colorStops.length - 2];
+ const midPosition = Math.round((lastStop.stop + (secondLast?.stop ?? 0)) / 2);
+ setColorStops((prev) => [
+ ...prev,
+ { id: newId, color: "#10b981", stop: midPosition },
+ ]);
+ }
+ };
+
+ const removeColorStop = (id: string) => {
+ if (colorStops.length <= 2) return;
+ setColorStops((prev) => prev.filter((stop) => stop.id !== id));
+ };
+
+ const reverseColors = () => {
+ setColorStops((prev) => {
+ const reversed = [...prev].reverse();
+ return reversed.map((stop, idx) => ({
+ ...stop,
+ stop: prev[idx].stop,
+ }));
+ });
+ };
+
+ const generateRandomGradient = () => {
+ const randomHex = () =>
+ "#" +
+ Math.floor(Math.random() * 16777215)
+ .toString(16)
+ .padStart(6, "0");
+ const count = colorStops.length;
+ const newStops = colorStops.map((stop, idx) => ({
+ ...stop,
+ color: randomHex(),
+ stop: Math.round((idx / (count - 1)) * 100),
+ }));
+ setColorStops(newStops);
+ if (gradientType === "linear") {
+ setAngle(Math.floor(Math.random() * 360));
+ }
+ };
+
+ const applyPreset = (preset: Preset) => {
+ setGradientType(preset.type);
+ if (preset.angle !== undefined) setAngle(preset.angle);
+ if (preset.shape) setRadialShape(preset.shape);
+ if (preset.position) setRadialPosition(preset.position);
+ setColorStops(
+ preset.stops.map((s, idx) => ({
+ id: (idx + 1).toString(),
+ color: s.color,
+ stop: s.stop,
+ }))
+ );
+ };
+
+ return (
+
+
+ {/* Live Preview Area */}
+
+
+
+ Live Preview
+
+
+
+ setPreviewText(e.target.checked)}
+ className="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
+ />
+ Show Text Overlay
+
+
+
+
+
+ Randomize
+
+
+
+
+
+ {previewText && (
+
+
+ CSS Gradient Preview
+
+
+ {gradientType === "linear" ? `${angle}° Linear` : `Radial ${radialShape}`}
+
+
+ )}
+
+
+
+ {/* Presets Gallery */}
+
+
+ Quick Presets
+
+
+ {PRESETS.map((preset) => {
+ const bgString =
+ preset.type === "linear"
+ ? `linear-gradient(${preset.angle}deg, ${preset.stops.map((s) => `${s.color} ${s.stop}%`).join(", ")})`
+ : `radial-gradient(${preset.shape} at ${preset.position}, ${preset.stops.map((s) => `${s.color} ${s.stop}%`).join(", ")})`;
+ return (
+
applyPreset(preset)}
+ className="group relative flex flex-col items-center gap-1.5 p-1.5 rounded-xl border border-gray-200 dark:border-gray-700 hover:border-blue-500 dark:hover:border-blue-500 bg-white dark:bg-gray-800 transition-all hover:scale-[1.03] text-left"
+ >
+
+
+ {preset.name}
+
+
+ );
+ })}
+
+
+
+ {/* Editor Controls Grid */}
+
+ {/* Left Column: Type & Direction Settings */}
+
+ {/* Gradient Type */}
+
+
+ Gradient Type
+
+
+ setGradientType("linear")}
+ className={`py-2.5 px-4 text-sm font-medium rounded-lg border transition-all ${
+ gradientType === "linear"
+ ? "bg-blue-600 border-blue-600 text-white shadow-sm"
+ : "bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50"
+ }`}
+ >
+ Linear
+
+ setGradientType("radial")}
+ className={`py-2.5 px-4 text-sm font-medium rounded-lg border transition-all ${
+ gradientType === "radial"
+ ? "bg-blue-600 border-blue-600 text-white shadow-sm"
+ : "bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50"
+ }`}
+ >
+ Radial
+
+
+
+
+ {/* Linear Direction Options */}
+ {gradientType === "linear" && (
+
+
+
+ {/* Quick Direction Selector */}
+
+
+ Quick Directions
+
+
+ {LINEAR_DIRECTIONS.map((dir) => (
+ setAngle(dir.angle)}
+ className={`py-2 px-1.5 text-xs font-medium rounded-lg border text-center transition-all ${
+ angle === dir.angle
+ ? "bg-blue-50 dark:bg-blue-900/30 border-blue-500 text-blue-700 dark:text-blue-300 font-semibold"
+ : "bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700"
+ }`}
+ >
+ {dir.label}
+
+ ))}
+
+
+
+ )}
+
+ {/* Radial Options */}
+ {gradientType === "radial" && (
+
+
+
+ Radial Shape
+
+
+ setRadialShape("circle")}
+ className={`py-2 px-3 text-sm font-medium rounded-lg border transition-all ${
+ radialShape === "circle"
+ ? "bg-blue-600 border-blue-600 text-white"
+ : "bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700"
+ }`}
+ >
+ Circle
+
+ setRadialShape("ellipse")}
+ className={`py-2 px-3 text-sm font-medium rounded-lg border transition-all ${
+ radialShape === "ellipse"
+ ? "bg-blue-600 border-blue-600 text-white"
+ : "bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700"
+ }`}
+ >
+ Ellipse
+
+
+
+
+
+
+ Radial Position
+
+ setRadialPosition(e.target.value)}
+ className="w-full p-2.5 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
+ >
+ {RADIAL_POSITIONS.map((pos) => (
+
+ {pos.label}
+
+ ))}
+
+
+
+ )}
+
+
+ {/* Right Column: Color Stops */}
+
+
+
+
+ Color Stops ({colorStops.length})
+
+
+ Customize start, middle, and end colors with position stops.
+
+
+
+
+
+
+
+ Swap
+
+
= 6}
+ className={`px-3 py-1.5 text-xs font-medium rounded-lg transition-colors flex items-center gap-1 ${
+ colorStops.length >= 6
+ ? "bg-gray-200 text-gray-400 cursor-not-allowed dark:bg-gray-800 dark:text-gray-600"
+ : "bg-blue-600 hover:bg-blue-700 text-white shadow-sm"
+ }`}
+ >
+
+
+
+ Add Stop
+
+
+
+
+ {/* Stops List */}
+
+ {colorStops.map((stop, index) => {
+ const label =
+ index === 0
+ ? "Start Color"
+ : index === colorStops.length - 1
+ ? "End Color"
+ : `Stop ${index + 1} (Middle)`;
+
+ return (
+
+
+
+
+
+ {label}
+
+
+
+ {colorStops.length > 2 && (
+
removeColorStop(stop.id)}
+ className="text-xs text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 transition-colors p-1 rounded"
+ title="Remove Stop"
+ >
+
+
+
+
+ )}
+
+
+
+ {/* Color Picker and Hex Input */}
+
+
+ {/* Position Slider and Input */}
+
+
+
+ );
+ })}
+
+
+
+
+ {/* Generated CSS Output Block */}
+
+
+
+
+ Generated CSS
+
+
+ setCodeFormat("standard")}
+ className={`px-2.5 py-1 rounded-md transition-colors ${
+ codeFormat === "standard"
+ ? "bg-white dark:bg-gray-700 text-gray-900 dark:text-white font-medium shadow-xs"
+ : "text-gray-600 dark:text-gray-400 hover:text-gray-900"
+ }`}
+ >
+ Standard
+
+ setCodeFormat("full")}
+ className={`px-2.5 py-1 rounded-md transition-colors ${
+ codeFormat === "full"
+ ? "bg-white dark:bg-gray-700 text-gray-900 dark:text-white font-medium shadow-xs"
+ : "text-gray-600 dark:text-gray-400 hover:text-gray-900"
+ }`}
+ >
+ Full CSS
+
+ setCodeFormat("tailwind")}
+ className={`px-2.5 py-1 rounded-md transition-colors ${
+ codeFormat === "tailwind"
+ ? "bg-white dark:bg-gray-700 text-gray-900 dark:text-white font-medium shadow-xs"
+ : "text-gray-600 dark:text-gray-400 hover:text-gray-900"
+ }`}
+ >
+ Tailwind
+
+
+
+
+
+ {copied ? (
+ <>
+
+
+
+ Copied!
+ >
+ ) : (
+ <>
+
+
+
+ Copy CSS
+ >
+ )}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/tools/diff-checker/page.tsx b/src/app/tools/diff-checker/page.tsx
new file mode 100644
index 0000000..d6c9d71
--- /dev/null
+++ b/src/app/tools/diff-checker/page.tsx
@@ -0,0 +1,552 @@
+"use client";
+
+import { useState, useMemo, useCallback } from "react";
+import ToolLayout from "@/components/ToolLayout";
+
+type DiffType = "unchanged" | "added" | "removed";
+
+interface DiffItem {
+ type: DiffType;
+ text: string;
+ origLine?: number;
+ modLine?: number;
+}
+
+const SAMPLE_ORIGINAL = `// User Authentication Service
+function authenticateUser(username, password) {
+ if (!username || !password) {
+ return { success: false, error: "Missing credentials" };
+ }
+
+ const user = database.findUserByUsername(username);
+ if (!user) {
+ return { success: false, error: "User not found" };
+ }
+
+ // Legacy plain text check (insecure)
+ if (user.password === password) {
+ return { success: true, token: "legacy-session-token" };
+ }
+
+ return { success: false, error: "Invalid password" };
+}`;
+
+const SAMPLE_MODIFIED = `// User Authentication Service v2.0
+import { verifyPasswordHash, generateJwtToken } from "@/lib/auth";
+
+export async function authenticateUser(username, password) {
+ if (!username || !password) {
+ return { success: false, error: "Missing credentials" };
+ }
+
+ const user = await database.findUserByUsername(username);
+ if (!user || !user.isActive) {
+ return { success: false, error: "User not found or disabled" };
+ }
+
+ // Secure bcrypt password verification
+ const isValid = await verifyPasswordHash(password, user.passwordHash);
+ if (isValid) {
+ const token = generateJwtToken(user.id, user.role);
+ return { success: true, token, user: { id: user.id, name: user.name } };
+ }
+
+ return { success: false, error: "Invalid credentials" };
+}`;
+
+function computeLcsDiff(
+ originalText: string,
+ modifiedText: string,
+ ignoreWhitespace: boolean,
+ ignoreCase: boolean
+): DiffItem[] {
+ const origLines = originalText ? originalText.split(/\r\n|\r|\n/) : [];
+ const modLines = modifiedText ? modifiedText.split(/\r\n|\r|\n/) : [];
+
+ const norm = (line: string) => {
+ let s = line;
+ if (ignoreWhitespace) s = s.trim().replace(/\s+/g, " ");
+ if (ignoreCase) s = s.toLowerCase();
+ return s;
+ };
+
+ const n = origLines.length;
+ const m = modLines.length;
+
+ if (n === 0 && m === 0) return [];
+
+ // 2D LCS Table
+ const dp: number[][] = Array.from({ length: n + 1 }, () =>
+ new Array(m + 1).fill(0)
+ );
+
+ for (let i = 1; i <= n; i++) {
+ const s1 = norm(origLines[i - 1]);
+ for (let j = 1; j <= m; j++) {
+ const s2 = norm(modLines[j - 1]);
+ if (s1 === s2) {
+ dp[i][j] = dp[i - 1][j - 1] + 1;
+ } else {
+ dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
+ }
+ }
+ }
+
+ // Backtrack
+ let i = n;
+ let j = m;
+ const result: DiffItem[] = [];
+
+ while (i > 0 || j > 0) {
+ if (
+ i > 0 &&
+ j > 0 &&
+ norm(origLines[i - 1]) === norm(modLines[j - 1])
+ ) {
+ result.push({
+ type: "unchanged",
+ text: origLines[i - 1],
+ origLine: i,
+ modLine: j,
+ });
+ i--;
+ j--;
+ } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
+ result.push({
+ type: "added",
+ text: modLines[j - 1],
+ modLine: j,
+ });
+ j--;
+ } else if (i > 0 && (j === 0 || dp[i][j - 1] < dp[i - 1][j])) {
+ result.push({
+ type: "removed",
+ text: origLines[i - 1],
+ origLine: i,
+ });
+ i--;
+ }
+ }
+
+ return result.reverse();
+}
+
+export default function DiffCheckerPage() {
+ const [original, setOriginal] = useState(SAMPLE_ORIGINAL);
+ const [modified, setModified] = useState(SAMPLE_MODIFIED);
+ const [ignoreWhitespace, setIgnoreWhitespace] = useState(false);
+ const [ignoreCase, setIgnoreCase] = useState(false);
+ const [viewMode, setViewMode] = useState<"unified" | "split">("unified");
+ const [copiedPatch, setCopiedPatch] = useState(false);
+
+ const diffResult = useMemo(() => {
+ return computeLcsDiff(original, modified, ignoreWhitespace, ignoreCase);
+ }, [original, modified, ignoreWhitespace, ignoreCase]);
+
+ const stats = useMemo(() => {
+ let added = 0;
+ let removed = 0;
+ let unchanged = 0;
+
+ diffResult.forEach((d) => {
+ if (d.type === "added") added++;
+ else if (d.type === "removed") removed++;
+ else if (d.type === "unchanged") unchanged++;
+ });
+
+ const totalLines = added + removed + unchanged;
+ const similarity =
+ totalLines > 0
+ ? Math.round((unchanged / (unchanged + Math.max(added, removed))) * 100)
+ : 100;
+
+ return { added, removed, unchanged, totalLines, similarity };
+ }, [diffResult]);
+
+ const handleSwap = () => {
+ const temp = original;
+ setOriginal(modified);
+ setModified(temp);
+ };
+
+ const handleClear = () => {
+ setOriginal("");
+ setModified("");
+ };
+
+ const handleLoadSample = () => {
+ setOriginal(SAMPLE_ORIGINAL);
+ setModified(SAMPLE_MODIFIED);
+ };
+
+ const handleCopyUnifiedDiff = useCallback(async () => {
+ if (diffResult.length === 0) return;
+ const patchHeader = `--- original\n+++ modified\n@@ -1,${stats.unchanged + stats.removed} +1,${stats.unchanged + stats.added} @@\n`;
+ const patchBody = diffResult
+ .map((d) => {
+ const prefix = d.type === "added" ? "+" : d.type === "removed" ? "-" : " ";
+ return `${prefix} ${d.text}`;
+ })
+ .join("\n");
+
+ try {
+ await navigator.clipboard.writeText(patchHeader + patchBody);
+ setCopiedPatch(true);
+ setTimeout(() => setCopiedPatch(false), 2000);
+ } catch {
+ // Fallback
+ }
+ }, [diffResult, stats]);
+
+ // Generate split view rows
+ const splitRows = useMemo(() => {
+ const rows: {
+ left?: { text: string; line?: number; type: "unchanged" | "removed" };
+ right?: { text: string; line?: number; type: "unchanged" | "added" };
+ }[] = [];
+
+ let i = 0;
+ while (i < diffResult.length) {
+ const item = diffResult[i];
+ if (item.type === "unchanged") {
+ rows.push({
+ left: { text: item.text, line: item.origLine, type: "unchanged" },
+ right: { text: item.text, line: item.modLine, type: "unchanged" },
+ });
+ i++;
+ } else if (item.type === "removed") {
+ // Check if immediately followed by an added line (replacement)
+ if (i + 1 < diffResult.length && diffResult[i + 1].type === "added") {
+ rows.push({
+ left: { text: item.text, line: item.origLine, type: "removed" },
+ right: {
+ text: diffResult[i + 1].text,
+ line: diffResult[i + 1].modLine,
+ type: "added",
+ },
+ });
+ i += 2;
+ } else {
+ rows.push({
+ left: { text: item.text, line: item.origLine, type: "removed" },
+ });
+ i++;
+ }
+ } else if (item.type === "added") {
+ rows.push({
+ right: { text: item.text, line: item.modLine, type: "added" },
+ });
+ i++;
+ }
+ }
+ return rows;
+ }, [diffResult]);
+
+ return (
+
+ Text Diff Checker Online — DevToolbox
+
+
+
+ {/* Controls and Stats Bar */}
+
+
+
+
+
+
+ Swap Sides
+
+
+ Load Sample
+
+
+ Clear
+
+
+
+ {/* Options & View mode */}
+
+
+
+ {/* Input Textareas Side by Side */}
+
+
+
+
+
+ Original Text
+
+
+ {original ? original.split("\n").length : 0} lines
+
+
+
+
+
+
+
+
+ Modified Text
+
+
+ {modified ? modified.split("\n").length : 0} lines
+
+
+
+
+
+ {/* Diff Stats Banner */}
+
+
+
+ + Lines Added
+
+
+ +{stats.added}
+
+
+
+
+
+ - Lines Removed
+
+
+ -{stats.removed}
+
+
+
+
+
+ Unchanged Lines
+
+
+ {stats.unchanged}
+
+
+
+
+
+ Similarity
+
+
+ {stats.similarity}%
+
+
+
+
+ {/* Diff Output Viewer */}
+
+
+
+ Diff Comparison Output
+
+ ({viewMode === "unified" ? "Unified View" : "Split View"})
+
+
+
+
+ {copiedPatch ? "Copied Patch!" : "Copy Diff Patch"}
+
+
+
+ {diffResult.length === 0 ? (
+
+ Enter original and modified texts above to calculate diff.
+
+ ) : viewMode === "unified" ? (
+ /* Unified Diff View */
+
+
+ {diffResult.map((item, idx) => {
+ let bgClass = "bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200";
+ let badge =
;
+ let borderClass = "border-l-4 border-transparent";
+
+ if (item.type === "added") {
+ bgClass = "bg-emerald-50/80 dark:bg-emerald-950/30 text-emerald-950 dark:text-emerald-200";
+ borderClass = "border-l-4 border-emerald-500";
+ badge =
+ ;
+ } else if (item.type === "removed") {
+ bgClass = "bg-rose-50/80 dark:bg-rose-950/30 text-rose-950 dark:text-rose-200";
+ borderClass = "border-l-4 border-rose-500";
+ badge =
- ;
+ }
+
+ return (
+
+ {/* Line numbers */}
+
+ {item.origLine || ""}
+
+
+ {item.modLine || ""}
+
+ {/* Symbol */}
+
+ {badge}
+
+ {/* Line content */}
+
+ {item.text || . }
+
+
+ );
+ })}
+
+
+ ) : (
+ /* Split Diff View */
+
+
+
Original (Before)
+
Modified (After)
+
+
+ {splitRows.map((row, rIdx) => (
+
+ {/* Left Column */}
+
+
+ {row.left?.line || ""}
+
+
+ {row.left?.type === "removed" ? - : ""}
+
+
+ {row.left?.text || ""}
+
+
+
+ {/* Right Column */}
+
+
+ {row.right?.line || ""}
+
+
+ {row.right?.type === "added" ? + : ""}
+
+
+ {row.right?.text || ""}
+
+
+
+ ))}
+
+
+ )}
+
+
+
+ );
+}
diff --git a/src/app/tools/hash-generator/page.tsx b/src/app/tools/hash-generator/page.tsx
new file mode 100644
index 0000000..0513451
--- /dev/null
+++ b/src/app/tools/hash-generator/page.tsx
@@ -0,0 +1,468 @@
+"use client";
+
+import { useState, useEffect, useCallback } from "react";
+import ToolLayout from "@/components/ToolLayout";
+
+// Pure JavaScript MD5 Implementation (RFC 1321)
+function md5(string: string): string {
+ function add32(a: number, b: number): number {
+ return (a + b) & 0xffffffff;
+ }
+
+ function cmn(q: number, a: number, b: number, x: number, s: number, t: number): number {
+ a = add32(add32(a, q), add32(x, t));
+ return add32((a << s) | (a >>> (32 - s)), b);
+ }
+
+ function ff(a: number, b: number, c: number, d: number, x: number, s: number, t: number): number {
+ return cmn((b & c) | (~b & d), a, b, x, s, t);
+ }
+
+ function gg(a: number, b: number, c: number, d: number, x: number, s: number, t: number): number {
+ return cmn((b & d) | (c & ~d), a, b, x, s, t);
+ }
+
+ function hh(a: number, b: number, c: number, d: number, x: number, s: number, t: number): number {
+ return cmn(b ^ c ^ d, a, b, x, s, t);
+ }
+
+ function ii(a: number, b: number, c: number, d: number, x: number, s: number, t: number): number {
+ return cmn(c ^ (b | ~d), a, b, x, s, t);
+ }
+
+ function md5cycle(x: number[], k: number[]) {
+ let a = x[0],
+ b = x[1],
+ c = x[2],
+ d = x[3];
+
+ a = ff(a, b, c, d, k[0], 7, -680876936);
+ d = ff(d, a, b, c, k[1], 12, -389564586);
+ c = ff(c, d, a, b, k[2], 17, 606105819);
+ b = ff(b, c, d, a, k[3], 22, -1044525330);
+ a = ff(a, b, c, d, k[4], 7, -176418897);
+ d = ff(d, a, b, c, k[5], 12, 1200080426);
+ c = ff(c, d, a, b, k[6], 17, -1473231341);
+ b = ff(b, c, d, a, k[7], 22, -45705983);
+ a = ff(a, b, c, d, k[8], 7, 1770035416);
+ d = ff(d, a, b, c, k[9], 12, -1958414417);
+ c = ff(c, d, a, b, k[10], 17, -42063);
+ b = ff(b, c, d, a, k[11], 22, -1990404162);
+ a = ff(a, b, c, d, k[12], 7, 1804603682);
+ d = ff(d, a, b, c, k[13], 12, -40341101);
+ c = ff(c, d, a, b, k[14], 17, -1502002290);
+ b = ff(b, c, d, a, k[15], 22, 1236535329);
+
+ a = gg(a, b, c, d, k[1], 5, -165796510);
+ d = gg(d, a, b, c, k[6], 9, -1069501632);
+ c = gg(c, d, a, b, k[11], 14, 643717713);
+ b = gg(b, c, d, a, k[0], 20, -373897302);
+ a = gg(a, b, c, d, k[5], 5, -701558691);
+ d = gg(d, a, b, c, k[10], 9, 38016083);
+ c = gg(c, d, a, b, k[15], 14, -660478335);
+ b = gg(b, c, d, a, k[4], 20, -405537848);
+ a = gg(a, b, c, d, k[9], 5, 568446438);
+ d = gg(d, a, b, c, k[14], 9, -1019803690);
+ c = gg(c, d, a, b, k[3], 14, -187363961);
+ b = gg(b, c, d, a, k[8], 20, 1163531501);
+ a = gg(a, b, c, d, k[13], 5, -1444681467);
+ d = gg(d, a, b, c, k[2], 9, -51403784);
+ c = gg(c, d, a, b, k[7], 14, 1735328473);
+ b = gg(b, c, d, a, k[12], 20, -1926607734);
+
+ a = hh(a, b, c, d, k[5], 4, -378558);
+ d = hh(d, a, b, c, k[8], 11, -2022574463);
+ c = hh(d, a, b, c, k[11], 16, 1839030562);
+ b = hh(b, c, d, a, k[14], 23, -35309556);
+ a = hh(a, b, c, d, k[1], 4, -1530992060);
+ d = hh(d, a, b, c, k[4], 11, 1272893353);
+ c = hh(c, d, a, b, k[7], 16, -155497632);
+ b = hh(b, c, d, a, k[10], 23, -1094730640);
+ a = hh(a, b, c, d, k[13], 4, 681279174);
+ d = hh(d, a, b, c, k[0], 11, -358537222);
+ c = hh(c, d, a, b, k[3], 16, -722521979);
+ b = hh(b, c, d, a, k[6], 23, 76029189);
+ a = hh(a, b, c, d, k[9], 4, -640364487);
+ d = hh(d, a, b, c, k[12], 11, -421815835);
+ c = hh(c, d, a, b, k[15], 16, 530742520);
+ b = hh(b, c, d, a, k[2], 23, -995338651);
+
+ a = ii(a, b, c, d, k[0], 6, -198630844);
+ d = ii(d, a, b, c, k[7], 10, 1126891415);
+ c = ii(c, d, a, b, k[14], 15, -1416354905);
+ b = ii(b, c, d, a, k[5], 21, -57434055);
+ a = ii(a, b, c, d, k[12], 6, 1700485571);
+ d = ii(d, a, b, c, k[3], 10, -1894986606);
+ c = ii(c, d, a, b, k[10], 15, -1051523);
+ b = ii(b, c, d, a, k[1], 21, -2054922799);
+ a = ii(a, b, c, d, k[8], 6, 1873313359);
+ d = ii(d, a, b, c, k[15], 10, -30611744);
+ c = ii(c, d, a, b, k[6], 15, -1560198380);
+ b = ii(b, c, d, a, k[13], 21, 1309151649);
+ a = ii(a, b, c, d, k[4], 6, -145523070);
+ d = ii(d, a, b, c, k[11], 10, -1120210379);
+ c = ii(c, d, a, b, k[2], 15, 718787259);
+ b = ii(b, c, d, a, k[9], 21, -343485551);
+
+ x[0] = add32(a, x[0]);
+ x[1] = add32(b, x[1]);
+ x[2] = add32(c, x[2]);
+ x[3] = add32(d, x[3]);
+ }
+
+ const utf8 = new TextEncoder().encode(string);
+ const n = utf8.length;
+ const state = [1732584193, -271733879, -1732584194, 271733878];
+
+ let i = 0;
+ for (i = 64; i <= n; i += 64) {
+ const blk: number[] = [];
+ for (let j = 0; j < 64; j += 4) {
+ const idx = i - 64 + j;
+ blk[j >> 2] =
+ utf8[idx] | (utf8[idx + 1] << 8) | (utf8[idx + 2] << 16) | (utf8[idx + 3] << 24);
+ }
+ md5cycle(state, blk);
+ }
+
+ const tail = utf8.slice(i - 64);
+ const tailBlk: number[] = new Array(16).fill(0);
+ for (let j = 0; j < tail.length; j++) {
+ tailBlk[j >> 2] |= tail[j] << ((j % 4) << 3);
+ }
+ tailBlk[tail.length >> 2] |= 0x80 << ((tail.length % 4) << 3);
+
+ if (tail.length > 55) {
+ md5cycle(state, tailBlk);
+ for (let j = 0; j < 16; j++) tailBlk[j] = 0;
+ }
+
+ const bitLen = n * 8;
+ tailBlk[14] = bitLen & 0xffffffff;
+ tailBlk[15] = Math.floor(bitLen / 0x100000000);
+ md5cycle(state, tailBlk);
+
+ const hex_chr = "0123456789abcdef";
+ let result = "";
+ for (let j = 0; j < 4; j++) {
+ const val = state[j];
+ for (let k = 0; k <= 3; k++) {
+ result += hex_chr[(val >> (k * 8 + 4)) & 0x0f] + hex_chr[(val >> (k * 8)) & 0x0f];
+ }
+ }
+
+ return result;
+}
+
+interface HashResult {
+ algorithm: string;
+ name: string;
+ bitLength: number;
+ hash: string;
+ securityNote: string;
+}
+
+export default function HashGeneratorPage() {
+ const [inputText, setInputText] = useState("Hello, DevToolbox!");
+ const [uppercase, setUppercase] = useState(false);
+ const [hashes, setHashes] = useState([]);
+ const [copiedKey, setCopiedKey] = useState(null);
+ const [copiedAll, setCopiedAll] = useState(false);
+
+ const computeHashes = useCallback(async (text: string) => {
+ const encoder = new TextEncoder();
+ const data = encoder.encode(text);
+
+ // 1. MD5
+ const md5Hash = md5(text);
+
+ // 2. Web Crypto subtle digests
+ const getSubtleHash = async (algo: string): Promise => {
+ try {
+ const buffer = await crypto.subtle.digest(algo, data);
+ const hashArray = Array.from(new Uint8Array(buffer));
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
+ } catch (err) {
+ console.error(`Failed to calculate ${algo}:`, err);
+ return "";
+ }
+ };
+
+ const [sha1Hash, sha256Hash, sha384Hash, sha512Hash] = await Promise.all([
+ getSubtleHash("SHA-1"),
+ getSubtleHash("SHA-256"),
+ getSubtleHash("SHA-384"),
+ getSubtleHash("SHA-512"),
+ ]);
+
+ setHashes([
+ {
+ algorithm: "MD5",
+ name: "MD5 (Message Digest 5)",
+ bitLength: 128,
+ hash: md5Hash,
+ securityNote: "Legacy / Checksums only (vulnerable to collisions)",
+ },
+ {
+ algorithm: "SHA-1",
+ name: "SHA-1 (Secure Hash Algorithm 1)",
+ bitLength: 160,
+ hash: sha1Hash,
+ securityNote: "Legacy (Git commits, legacy checksums)",
+ },
+ {
+ algorithm: "SHA-256",
+ name: "SHA-256 (SHA-2 Family)",
+ bitLength: 256,
+ hash: sha256Hash,
+ securityNote: "Industry Standard (High security, SSL, Bitcoin)",
+ },
+ {
+ algorithm: "SHA-384",
+ name: "SHA-384 (SHA-2 Family)",
+ bitLength: 384,
+ hash: sha384Hash,
+ securityNote: "High Security (Government & enterprise grade)",
+ },
+ {
+ algorithm: "SHA-512",
+ name: "SHA-512 (SHA-2 Family)",
+ bitLength: 512,
+ hash: sha512Hash,
+ securityNote: "Maximum Security (64-bit optimized systems)",
+ },
+ ]);
+ }, []);
+
+ useEffect(() => {
+ computeHashes(inputText);
+ }, [inputText, computeHashes]);
+
+ const copyHash = async (hashValue: string, key: string) => {
+ const formatted = uppercase ? hashValue.toUpperCase() : hashValue.toLowerCase();
+ try {
+ await navigator.clipboard.writeText(formatted);
+ setCopiedKey(key);
+ setTimeout(() => setCopiedKey(null), 2000);
+ } catch (err) {
+ console.error("Failed to copy hash: ", err);
+ }
+ };
+
+ const copyAllHashes = async () => {
+ const allFormatted = hashes
+ .map((h) => {
+ const val = uppercase ? h.hash.toUpperCase() : h.hash.toLowerCase();
+ return `${h.algorithm}: ${val}`;
+ })
+ .join("\n");
+
+ try {
+ await navigator.clipboard.writeText(allFormatted);
+ setCopiedAll(true);
+ setTimeout(() => setCopiedAll(false), 2000);
+ } catch (err) {
+ console.error("Failed to copy all hashes: ", err);
+ }
+ };
+
+ const sampleTexts = [
+ { label: "Hello World", text: "Hello, world!" },
+ { label: "Password", text: "correct horse battery staple" },
+ { label: "Empty String", text: "" },
+ { label: "Quick Fox", text: "The quick brown fox jumps over the lazy dog" },
+ ];
+
+ const byteLength = new TextEncoder().encode(inputText).length;
+
+ return (
+
+
+ {/* Input Section */}
+
+
+
+ Input Text
+
+
+ {inputText.length} characters
+ •
+ {byteLength} UTF-8 bytes
+
+
+
+
+
+ {/* Hashes Section Toolbar */}
+
+
+ Computed Hashes
+
+
+
+
+ setUppercase(e.target.checked)}
+ className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
+ />
+ UPPERCASE HEX
+
+
+
+ {copiedAll ? (
+ <>
+
+
+
+ All Copied!
+ >
+ ) : (
+ <>
+
+
+
+ Copy All Hashes
+ >
+ )}
+
+
+
+
+ {/* Hashes List */}
+
+ {hashes.map((item) => {
+ const displayHash = uppercase ? item.hash.toUpperCase() : item.hash.toLowerCase();
+ const isCopied = copiedKey === item.algorithm;
+
+ return (
+
+
+
+
+ {item.algorithm}
+
+
+ {item.bitLength}-bit ({item.hash.length} chars)
+
+
+
+ {item.securityNote}
+
+
+
+
+
+
+ {displayHash || "—"}
+
+
+
+
copyHash(item.hash, item.algorithm)}
+ aria-label={`Copy ${item.algorithm} hash`}
+ className={`flex shrink-0 items-center gap-1.5 rounded-md px-3 py-2 text-xs font-medium transition-all ${
+ isCopied
+ ? "bg-green-600 text-white"
+ : "bg-blue-600 text-white hover:bg-blue-700 active:scale-95"
+ }`}
+ >
+ {isCopied ? (
+ <>
+
+
+
+ Copied
+ >
+ ) : (
+ <>
+
+
+
+ Copy
+ >
+ )}
+
+
+
+ );
+ })}
+
+
+ {/* Educational / Comparison Guide */}
+
+
+ About Cryptographic Hashes
+
+
+ A cryptographic hash function is a one-way deterministic mathematical algorithm that maps arbitrary-size data to a fixed-size string. Hashes are widely used for data integrity verification, password storage with salt, digital signatures, and content-addressable storage systems.
+
+
+
+
+ );
+}
diff --git a/src/app/tools/html-entities/page.tsx b/src/app/tools/html-entities/page.tsx
new file mode 100644
index 0000000..99ada79
--- /dev/null
+++ b/src/app/tools/html-entities/page.tsx
@@ -0,0 +1,458 @@
+"use client";
+
+import { useState, useCallback, useId } from "react";
+import ToolLayout from "@/components/ToolLayout";
+
+const BASIC_ENTITIES_MAP: Record = {
+ "&": "&",
+ "<": "<",
+ ">": ">",
+ '"': """,
+ "'": "'",
+};
+
+const EXTENDED_ENTITIES_MAP: Record = {
+ "&": "&",
+ "<": "<",
+ ">": ">",
+ '"': """,
+ "'": "'",
+ "©": "©",
+ "®": "®",
+ "™": "™",
+ "€": "€",
+ "£": "£",
+ "¥": "¥",
+ "¢": "¢",
+ "§": "§",
+ "°": "°",
+ "±": "±",
+ "«": "«",
+ "»": "»",
+ "•": "•",
+ "…": "…",
+ "—": "—",
+ "–": "–",
+ "“": "“",
+ "”": "”",
+ "‘": "‘",
+ "’": "’",
+ "×": "×",
+ "÷": "÷",
+ "µ": "µ",
+ "¶": "¶",
+ "¼": "¼",
+ "½": "½",
+ "¾": "¾",
+};
+
+type EncodeMode = "basic" | "extended" | "decimal" | "hex";
+
+export default function HtmlEntitiesPage() {
+ const inputId = useId();
+ const outputId = useId();
+ const [input, setInput] = useState("");
+ const [output, setOutput] = useState("");
+ const [error, setError] = useState(null);
+ const [copied, setCopied] = useState(false);
+ const [encodeMode, setEncodeMode] = useState("basic");
+ const [lastAction, setLastAction] = useState<"encode" | "decode" | null>(null);
+
+ const encodeText = useCallback((text: string, mode: EncodeMode) => {
+ if (!text) {
+ setOutput("");
+ setError(null);
+ return;
+ }
+ try {
+ let result = "";
+ if (mode === "basic") {
+ result = text.replace(/[&<>"']/g, (char) => BASIC_ENTITIES_MAP[char] || char);
+ } else if (mode === "extended") {
+ result = text.replace(
+ /[&<>"'©®™€£¥¢§°±«»•…—–“”‘’×÷µ¶¼½¾]/g,
+ (char) => EXTENDED_ENTITIES_MAP[char] || char
+ );
+ } else if (mode === "decimal") {
+ result = text.replace(/[&<>"']|[^\x20-\x7E\r\n\t]/g, (char) => {
+ const code = char.codePointAt(0);
+ return code !== undefined ? `${code};` : char;
+ });
+ } else if (mode === "hex") {
+ result = text.replace(/[&<>"']|[^\x20-\x7E\r\n\t]/g, (char) => {
+ const code = char.codePointAt(0);
+ return code !== undefined ? `${code.toString(16).toUpperCase()};` : char;
+ });
+ }
+ setOutput(result);
+ setError(null);
+ setLastAction("encode");
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to encode HTML entities.");
+ setOutput("");
+ }
+ }, []);
+
+ const decodeText = useCallback((text: string) => {
+ if (!text) {
+ setOutput("");
+ setError(null);
+ return;
+ }
+ try {
+ const textarea = document.createElement("textarea");
+ textarea.innerHTML = text;
+ const decoded = textarea.value;
+ setOutput(decoded);
+ setError(null);
+ setLastAction("decode");
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to decode HTML entities.");
+ setOutput("");
+ }
+ }, []);
+
+ const handleEncode = () => {
+ encodeText(input, encodeMode);
+ };
+
+ const handleDecode = () => {
+ decodeText(input);
+ };
+
+ const handleCopy = async () => {
+ if (!output) return;
+ try {
+ await navigator.clipboard.writeText(output);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ } catch {
+ const textArea = document.createElement("textarea");
+ textArea.value = output;
+ document.body.appendChild(textArea);
+ textArea.select();
+ document.execCommand("copy");
+ document.body.removeChild(textArea);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ }
+ };
+
+ const handleSwap = () => {
+ const temp = output;
+ setInput(temp);
+ setOutput(input);
+ setError(null);
+ if (lastAction === "encode") {
+ setLastAction("decode");
+ } else if (lastAction === "decode") {
+ setLastAction("encode");
+ }
+ };
+
+ const handleClear = () => {
+ setInput("");
+ setOutput("");
+ setError(null);
+ setLastAction(null);
+ };
+
+ const loadSample = (type: "html" | "symbols" | "entities") => {
+ setError(null);
+ if (type === "html") {
+ const sample = '\n
Title: "Welcome & Enjoy" \n
5 > 3 and 2 < 4 are true \'math\' facts!
\n
';
+ setInput(sample);
+ encodeText(sample, encodeMode);
+ } else if (type === "symbols") {
+ const sample = 'Price: €99.99 / £85.00 © 2026 DevToolbox™ — "Fast & Secure" — 100° angle • ½ cup';
+ setInput(sample);
+ setEncodeMode("extended");
+ encodeText(sample, "extended");
+ } else {
+ const sample = '<div class="card">\n <h2>Hello & Welcome!</h2>\n <p>Price: €50 © 2026</p>\n</div>';
+ setInput(sample);
+ decodeText(sample);
+ }
+ };
+
+ return (
+
+ HTML Entity Encode & Decode Online — DevToolbox
+
+
+
+ {/* Controls Bar */}
+
+
+
+
+
+
+ Encode HTML Entities
+
+
+
+
+
+ Decode to Plain Text
+
+
+
+
+
+ Swap
+
+
+ Clear
+
+
+
+ {/* Sample buttons */}
+
+ Samples:
+ loadSample("html")}
+ className="px-2.5 py-1 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 rounded transition-colors cursor-pointer"
+ >
+ HTML Code
+
+ loadSample("symbols")}
+ className="px-2.5 py-1 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 rounded transition-colors cursor-pointer"
+ >
+ Symbols & Quotes
+
+ loadSample("entities")}
+ className="px-2.5 py-1 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 rounded transition-colors cursor-pointer"
+ >
+ Encoded Entities
+
+
+
+
+ {/* Encoding Mode Selector */}
+
+
+ {/* Error Notification */}
+ {error && (
+
+ )}
+
+ {/* Two-Column Editor Grid */}
+
+ {/* Input Area */}
+
+
+
+ Input Text / HTML
+
+
+ {input.length} chars
+
+
+
+
+ {/* Output Area */}
+
+
+
+
+ Output Result
+
+ {lastAction && (
+
+ {lastAction === "encode" ? "HTML Entities Encoded" : "Decoded Plain Text"}
+
+ )}
+
+
+
+ {output.length} chars
+
+
+ {copied ? (
+ <>
+
+
+
+ Copied!
+ >
+ ) : (
+ <>
+
+
+
+ Copy
+ >
+ )}
+
+
+
+
+
+
+
+ {/* HTML Entities Reference Table */}
+
+
Key HTML Entities Reference
+
+
+ < (Less than)
+ <
+
+
+ > (Greater)
+ >
+
+
+ & (Ampersand)
+ &
+
+
+ " (Double quote)
+ "
+
+
+ ' (Single quote)
+ '
+
+
+ © (Copyright)
+ ©
+
+
+
+
+
+ );
+}
diff --git a/src/app/tools/json-formatter/page.tsx b/src/app/tools/json-formatter/page.tsx
new file mode 100644
index 0000000..8169293
--- /dev/null
+++ b/src/app/tools/json-formatter/page.tsx
@@ -0,0 +1,563 @@
+"use client";
+
+import { useState, useCallback, useId } from "react";
+import ToolLayout from "@/components/ToolLayout";
+
+interface StatusInfo {
+ type: "idle" | "success" | "error";
+ message?: string;
+ line?: number;
+ column?: number;
+ byteSize?: number;
+ itemCount?: number;
+ depth?: number;
+ reduction?: number;
+}
+
+const SAMPLE_JSON = {
+ name: "DevToolbox",
+ version: "1.0.0",
+ description: "Fast, privacy-first developer utilities",
+ active: true,
+ rating: 4.95,
+ tags: ["developer", "tools", "utilities", "open-source"],
+ author: {
+ name: "DevToolbox Team",
+ email: "support@example.com",
+ social: {
+ github: "https://github.com",
+ twitter: null
+ }
+ },
+ features: [
+ { id: 1, name: "JSON Formatter", status: "ready" },
+ { id: 2, name: "Markdown Preview", status: "ready" },
+ { id: 3, name: "Base64 Encoder", status: "ready" }
+ ],
+ stats: {
+ totalTools: 20,
+ dailyUsers: 15400,
+ uptimePercent: 99.98
+ }
+};
+
+function parseJsonError(input: string, err: unknown): { message: string; line?: number; column?: number } {
+ if (!(err instanceof Error)) {
+ return { message: String(err) };
+ }
+ const msg = err.message;
+ // Match "at position 123"
+ const posMatch = msg.match(/at position (\d+)/i);
+ if (posMatch) {
+ const pos = parseInt(posMatch[1], 10);
+ const before = input.slice(0, pos);
+ const lines = before.split("\n");
+ const line = lines.length;
+ const column = lines[lines.length - 1].length + 1;
+ return { message: msg, line, column };
+ }
+ // Match "line X column Y"
+ const lineColMatch = msg.match(/line (\d+) column (\d+)/i);
+ if (lineColMatch) {
+ return {
+ message: msg,
+ line: parseInt(lineColMatch[1], 10),
+ column: parseInt(lineColMatch[2], 10),
+ };
+ }
+ return { message: msg };
+}
+
+function sortJsonKeys(obj: unknown): unknown {
+ if (Array.isArray(obj)) {
+ return obj.map(sortJsonKeys);
+ } else if (obj !== null && typeof obj === "object") {
+ const sortedObj: Record = {};
+ const keys = Object.keys(obj as Record).sort();
+ for (const key of keys) {
+ sortedObj[key] = sortJsonKeys((obj as Record)[key]);
+ }
+ return sortedObj;
+ }
+ return obj;
+}
+
+function calculateDepth(obj: unknown, currentDepth = 1): number {
+ if (obj === null || typeof obj !== "object") return currentDepth;
+ const values = Array.isArray(obj) ? obj : Object.values(obj as Record);
+ if (values.length === 0) return currentDepth;
+ return Math.max(...values.map((v) => calculateDepth(v, currentDepth + 1)));
+}
+
+function countTotalKeys(obj: unknown): number {
+ if (obj === null || typeof obj !== "object") return 0;
+ let count = 0;
+ if (Array.isArray(obj)) {
+ for (const item of obj) {
+ count += countTotalKeys(item);
+ }
+ } else {
+ const entries = Object.entries(obj as Record);
+ count += entries.length;
+ for (const [, val] of entries) {
+ count += countTotalKeys(val);
+ }
+ }
+ return count;
+}
+
+function formatBytes(bytes: number): string {
+ if (bytes === 0) return "0 B";
+ const k = 1024;
+ const sizes = ["B", "KB", "MB"];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`;
+}
+
+export default function JsonFormatterPage() {
+ const [input, setInput] = useState("");
+ const [output, setOutput] = useState("");
+ const [indent, setIndent] = useState("2");
+ const [sortKeys, setSortKeys] = useState(false);
+ const [status, setStatus] = useState({ type: "idle" });
+ const [copied, setCopied] = useState(false);
+ const indentSelectId = useId();
+
+ const getIndentValue = useCallback(() => {
+ if (indent === "tab") return "\t";
+ return parseInt(indent, 10) || 2;
+ }, [indent]);
+
+ const handleFormat = useCallback(() => {
+ if (!input.trim()) {
+ setOutput("");
+ setStatus({ type: "idle" });
+ return;
+ }
+
+ try {
+ let parsed = JSON.parse(input);
+ if (sortKeys) {
+ parsed = sortJsonKeys(parsed);
+ }
+ const formatted = JSON.stringify(parsed, null, getIndentValue());
+ setOutput(formatted);
+
+ const byteSize = new Blob([formatted]).size;
+ const keyCount = countTotalKeys(parsed);
+ const depth = calculateDepth(parsed);
+
+ setStatus({
+ type: "success",
+ message: "JSON formatted successfully! Valid JSON structure.",
+ byteSize,
+ itemCount: keyCount,
+ depth,
+ });
+ } catch (err) {
+ const errorInfo = parseJsonError(input, err);
+ setStatus({
+ type: "error",
+ message: errorInfo.message,
+ line: errorInfo.line,
+ column: errorInfo.column,
+ });
+ }
+ }, [input, sortKeys, getIndentValue]);
+
+ const handleMinify = useCallback(() => {
+ if (!input.trim()) {
+ setOutput("");
+ setStatus({ type: "idle" });
+ return;
+ }
+
+ try {
+ let parsed = JSON.parse(input);
+ if (sortKeys) {
+ parsed = sortJsonKeys(parsed);
+ }
+ const minified = JSON.stringify(parsed);
+ setOutput(minified);
+
+ const origSize = new Blob([input]).size;
+ const minSize = new Blob([minified]).size;
+ const reduction = origSize > 0 ? Math.max(0, ((origSize - minSize) / origSize) * 100) : 0;
+
+ setStatus({
+ type: "success",
+ message: `JSON minified! Saved ${reduction.toFixed(1)}% (${formatBytes(origSize - minSize)})`,
+ byteSize: minSize,
+ reduction,
+ });
+ } catch (err) {
+ const errorInfo = parseJsonError(input, err);
+ setStatus({
+ type: "error",
+ message: errorInfo.message,
+ line: errorInfo.line,
+ column: errorInfo.column,
+ });
+ }
+ }, [input, sortKeys]);
+
+ const handleValidate = useCallback(() => {
+ if (!input.trim()) {
+ setStatus({
+ type: "error",
+ message: "Input is empty. Please enter or paste JSON to validate.",
+ });
+ return;
+ }
+
+ try {
+ const parsed = JSON.parse(input);
+ const byteSize = new Blob([input]).size;
+ const keyCount = countTotalKeys(parsed);
+ const depth = calculateDepth(parsed);
+
+ setStatus({
+ type: "success",
+ message: "Valid JSON syntax! No parsing errors detected.",
+ byteSize,
+ itemCount: keyCount,
+ depth,
+ });
+ } catch (err) {
+ const errorInfo = parseJsonError(input, err);
+ setStatus({
+ type: "error",
+ message: errorInfo.message,
+ line: errorInfo.line,
+ column: errorInfo.column,
+ });
+ }
+ }, [input]);
+
+ const handleLoadSample = useCallback(() => {
+ const sampleStr = JSON.stringify(SAMPLE_JSON, null, getIndentValue());
+ setInput(sampleStr);
+ setOutput(sampleStr);
+ const byteSize = new Blob([sampleStr]).size;
+ const keyCount = countTotalKeys(SAMPLE_JSON);
+ const depth = calculateDepth(SAMPLE_JSON);
+ setStatus({
+ type: "success",
+ message: "Sample JSON loaded successfully.",
+ byteSize,
+ itemCount: keyCount,
+ depth,
+ });
+ }, [getIndentValue]);
+
+ const handleClear = useCallback(() => {
+ setInput("");
+ setOutput("");
+ setStatus({ type: "idle" });
+ }, []);
+
+ const handleCopy = useCallback(async () => {
+ const textToCopy = output || input;
+ if (!textToCopy) return;
+
+ try {
+ await navigator.clipboard.writeText(textToCopy);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ } catch {
+ // Fallback
+ const textarea = document.createElement("textarea");
+ textarea.value = textToCopy;
+ document.body.appendChild(textarea);
+ textarea.select();
+ document.execCommand("copy");
+ document.body.removeChild(textarea);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ }
+ }, [output, input]);
+
+ const handleDownload = useCallback(() => {
+ const textToDownload = output || input;
+ if (!textToDownload) return;
+
+ const blob = new Blob([textToDownload], { type: "application/json;charset=utf-8" });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = "data.json";
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ }, [output, input]);
+
+ const inputLineCount = input ? input.split("\n").length : 0;
+ const outputLineCount = output ? output.split("\n").length : 0;
+
+ return (
+
+
+ {/* Controls Toolbar */}
+
+
+
+
+
+
+ Format / Beautify
+
+
+
+
+
+
+ Minify
+
+
+
+
+
+
+ Validate
+
+
+
+
+ {/* Indent Selector */}
+
+
+ Indent:
+
+ setIndent(e.target.value)}
+ className="rounded-md border border-gray-300 bg-white px-2.5 py-1 text-sm text-gray-800 shadow-xs focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200"
+ >
+ 2 Spaces
+ 4 Spaces
+ Tab
+ 1 Space
+
+
+
+ {/* Sort Keys Toggle */}
+
+ setSortKeys(e.target.checked)}
+ className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800"
+ />
+ Sort Keys
+
+
+
+ Load Sample
+
+
+
+ Clear
+
+
+
+
+ {/* Status Notification Banner */}
+ {status.type === "success" && (
+
+
+
+
+
+
{status.message}
+
+
+ {status.byteSize !== undefined && Size: {formatBytes(status.byteSize)} }
+ {status.itemCount !== undefined && Keys: {status.itemCount} }
+ {status.depth !== undefined && Depth: {status.depth} }
+
+
+ )}
+
+ {status.type === "error" && (
+
+
+
+
+
+
+
Invalid JSON Syntax
+
{status.message}
+ {(status.line !== undefined || status.column !== undefined) && (
+
+ 📍 Error location:
+ {status.line !== undefined && Line {status.line} }
+ {status.column !== undefined && Column {status.column} }
+
+ )}
+
+
+
+ )}
+
+ {/* Input & Output Split Editor */}
+
+ {/* Input Area */}
+
+
+
+ Input JSON
+
+ ({inputLineCount} {inputLineCount === 1 ? "line" : "lines"}, {input.length} chars)
+
+
+
+ {
+ try {
+ const text = await navigator.clipboard.readText();
+ setInput(text);
+ } catch {
+ // Clipboard permission denied or unavailable
+ }
+ }}
+ className="rounded px-2 py-1 text-xs font-medium text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800"
+ title="Paste from clipboard"
+ >
+ Paste
+
+ setInput("")}
+ className="rounded px-2 py-1 text-xs font-medium text-gray-500 hover:bg-gray-100 hover:text-red-600 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-red-400"
+ title="Clear input"
+ >
+ Clear
+
+
+
+
+
+ {/* Output Area */}
+
+
+
+ Output Result
+
+ ({outputLineCount} {outputLineCount === 1 ? "line" : "lines"}, {output.length} chars)
+
+
+
+
+
+
+
+ Download
+
+
+ {copied ? (
+ <>
+
+
+
+ Copied!
+ >
+ ) : (
+ <>
+
+
+
+ Copy Output
+ >
+ )}
+
+
+
+
+
+
+
+ {/* Feature Highlights & Guide */}
+
+
About JSON Formatter & Validator
+
+
+
✨ Beautify & Format
+
+ Format unorganized or compact JSON with customizable 2-space, 4-space, or tab indentation for maximum readability.
+
+
+
+
🔍 Strict Validation
+
+ Identify syntax mistakes, unquoted keys, trailing commas, and locate exact line numbers and column coordinates instantly.
+
+
+
+
🔒 100% Client-Side
+
+ Your data never leaves your browser. Parsing and formatting run entirely on your local machine for complete privacy.
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/tools/jwt-decoder/page.tsx b/src/app/tools/jwt-decoder/page.tsx
new file mode 100644
index 0000000..f6292f4
--- /dev/null
+++ b/src/app/tools/jwt-decoder/page.tsx
@@ -0,0 +1,517 @@
+"use client";
+
+import { useState, useMemo, useId } from "react";
+import ToolLayout from "@/components/ToolLayout";
+
+interface DecodedToken {
+ header: Record | null;
+ payload: Record | null;
+ signature: string;
+ rawHeader: string;
+ rawPayload: string;
+ rawSignature: string;
+}
+
+function base64UrlDecode(str: string): string {
+ let base64 = str.trim().replace(/-/g, "+").replace(/_/g, "/");
+ while (base64.length % 4 !== 0) {
+ base64 += "=";
+ }
+ const binary = atob(base64);
+ const bytes = new Uint8Array(binary.length);
+ for (let i = 0; i < binary.length; i++) {
+ bytes[i] = binary.charCodeAt(i);
+ }
+ return new TextDecoder("utf-8").decode(bytes);
+}
+
+function formatTimestamp(timestamp: number): { formatted: string; relative: string; isExpired: boolean; diffMs: number } {
+ const date = new Date(timestamp > 1e11 ? timestamp : timestamp * 1000);
+ const now = new Date();
+ const diffMs = date.getTime() - now.getTime();
+ const isExpired = diffMs <= 0;
+
+ const formatted = date.toUTCString() + ` (${date.toLocaleString()})`;
+
+ const absDiffSec = Math.floor(Math.abs(diffMs) / 1000);
+ const days = Math.floor(absDiffSec / 86400);
+ const hours = Math.floor((absDiffSec % 86400) / 3600);
+ const minutes = Math.floor((absDiffSec % 3600) / 60);
+ const seconds = absDiffSec % 60;
+
+ let relative = "";
+ if (days > 0) relative += `${days}d `;
+ if (hours > 0 || days > 0) relative += `${hours}h `;
+ if (minutes > 0 || hours > 0 || days > 0) relative += `${minutes}m `;
+ relative += `${seconds}s`;
+
+ if (isExpired) {
+ relative = `Expired ${relative} ago`;
+ } else {
+ relative = `Expires in ${relative}`;
+ }
+
+ return { formatted, relative, isExpired, diffMs };
+}
+
+// Pre-computed sample tokens
+const SAMPLE_VALID =
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +
+ "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkphbmUgRG9lIiwiZW1haWwiOiJqYW5lLmRvZUBleGFtcGxlLmNvbSIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTczNTY4OTYwMCwiZXhwIjoyMDgwNzg5NjAwLCJpc3MiOiJodHRwczovL2F1dGguZGV2dG9vbGJveC5pbyIsImF1ZCI6Imh0dHBzOi8vYXBpLmRldnRvb2xib3guaW8ifQ." +
+ "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
+
+const SAMPLE_EXPIRED =
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +
+ "eyJzdWIiOiJ1c3JfOTA4MTIzNDU2IiwibmFtZSI6IkFsZXggSm9obnNvbiIsImVtYWlsIjoiYWxleEBleGFtcGxlLmNvbSIsInJvbGUiOiJ1c2VyIiwiaWF0IjoxNjcyNTMxMjAwLCJleHAiOjE2NzI1MzQ4MDAsImlzcyI6Imh0dHBzOi8vYXV0aC5leGFtcGxlLmNvbSJ9." +
+ "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
+
+const SAMPLE_FIREBASE =
+ "eyJhbGciOiJSUzI1NiIsImtpZCI6ImFkNWUxMWExYmRlODFjODg3ZjAwOGQ0NDYwNWI1MzA0NDAxMjM0NTYiLCJ0eXAiOiJKV1QifQ." +
+ "eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vbXktYXBwIiwiYXVkIjoibXktYXBwIiwiYXV0aF90aW1lIjoxNzM1Njg5NjAwLCJ1c2VyX2lkIjoidzlVejFXcDlQWlhrOHRNYkEiLCJzdWIiOiJ3OVV6MVdwOVBaWGs4dE1iQSIsImlhdCI6MTczNTY4OTYwMCwiZXhwIjoyMDgwNzg5NjAwLCJlbWFpbCI6ImRldkBkZXZ0b29sYm94LmlvIiwiZW1haWxfdmVyaWZpZWQiOnRydWV9." +
+ "K3wF9m5R6Y3N2X4a9_7L8P1Q0Z3v9R6Y3N2X4a9_7L8P1Q0Z3v9R6Y3N2X4a9_7L8P1Q0Z3v";
+
+export default function JwtDecoderPage() {
+ const inputId = useId();
+ const [tokenInput, setTokenInput] = useState(SAMPLE_VALID);
+ const [copiedHeader, setCopiedHeader] = useState(false);
+ const [copiedPayload, setCopiedPayload] = useState(false);
+ const [copiedToken, setCopiedToken] = useState(false);
+
+ const decodedResult = useMemo(() => {
+ const raw = tokenInput.trim();
+ if (!raw) {
+ return { token: null, error: null, parts: [] };
+ }
+
+ const parts = raw.split(".");
+ if (parts.length !== 3) {
+ return {
+ token: null,
+ error: `Invalid JWT format: A JSON Web Token must contain exactly 3 dot-separated parts (header.payload.signature). Found ${parts.length} part(s).`,
+ parts,
+ };
+ }
+
+ const [rawHeader, rawPayload, rawSignature] = parts;
+ let headerObj: Record | null = null;
+ let payloadObj: Record | null = null;
+
+ try {
+ const decodedHeaderJson = base64UrlDecode(rawHeader);
+ headerObj = JSON.parse(decodedHeaderJson);
+ } catch (err) {
+ return {
+ token: null,
+ error: `Failed to decode JWT Header: ${err instanceof Error ? err.message : "Invalid Base64URL or JSON"}`,
+ parts,
+ };
+ }
+
+ try {
+ const decodedPayloadJson = base64UrlDecode(rawPayload);
+ payloadObj = JSON.parse(decodedPayloadJson);
+ } catch (err) {
+ return {
+ token: null,
+ error: `Failed to decode JWT Payload: ${err instanceof Error ? err.message : "Invalid Base64URL or JSON"}`,
+ parts,
+ };
+ }
+
+ const token: DecodedToken = {
+ header: headerObj,
+ payload: payloadObj,
+ signature: rawSignature,
+ rawHeader,
+ rawPayload,
+ rawSignature,
+ };
+
+ return { token, error: null, parts };
+ }, [tokenInput]);
+
+ const { token, error, parts } = decodedResult;
+
+ const copyToClipboard = async (text: string, setFn: (val: boolean) => void) => {
+ try {
+ await navigator.clipboard.writeText(text);
+ setFn(true);
+ setTimeout(() => setFn(false), 2000);
+ } catch {
+ const textArea = document.createElement("textarea");
+ textArea.value = text;
+ document.body.appendChild(textArea);
+ textArea.select();
+ document.execCommand("copy");
+ document.body.removeChild(textArea);
+ setFn(true);
+ setTimeout(() => setFn(false), 2000);
+ }
+ };
+
+ const expClaim = token?.payload && typeof token.payload.exp === "number" ? token.payload.exp : null;
+ const iatClaim = token?.payload && typeof token.payload.iat === "number" ? token.payload.iat : null;
+ const nbfClaim = token?.payload && typeof token.payload.nbf === "number" ? token.payload.nbf : null;
+
+ const expInfo = expClaim ? formatTimestamp(expClaim) : null;
+ const iatInfo = iatClaim ? formatTimestamp(iatClaim) : null;
+ const nbfInfo = nbfClaim ? formatTimestamp(nbfClaim) : null;
+
+ const headerString = token?.header ? JSON.stringify(token.header, null, 2) : "";
+ const payloadString = token?.payload ? JSON.stringify(token.payload, null, 2) : "";
+
+ return (
+
+ JWT Decoder Online — DevToolbox
+
+
+
+ {/* Samples & Actions Bar */}
+
+
+ Sample Tokens:
+ setTokenInput(SAMPLE_VALID)}
+ className="px-2.5 py-1 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 rounded text-xs font-medium transition-colors cursor-pointer"
+ >
+ Active Token
+
+ setTokenInput(SAMPLE_EXPIRED)}
+ className="px-2.5 py-1 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 rounded text-xs font-medium transition-colors cursor-pointer"
+ >
+ Expired Token
+
+ setTokenInput(SAMPLE_FIREBASE)}
+ className="px-2.5 py-1 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 rounded text-xs font-medium transition-colors cursor-pointer"
+ >
+ RS256 Token
+
+
+
+
+
copyToClipboard(tokenInput, setCopiedToken)}
+ disabled={!tokenInput}
+ className="px-3 py-1.5 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-40 disabled:cursor-not-allowed text-gray-700 dark:text-gray-300 rounded text-xs font-medium transition-colors cursor-pointer flex items-center gap-1"
+ >
+ {copiedToken ? (
+ <>
+
+
+
+ Copied!
+ >
+ ) : (
+ <>
+
+
+
+ Copy Token
+ >
+ )}
+
+
setTokenInput("")}
+ disabled={!tokenInput}
+ className="px-3 py-1.5 bg-gray-100 dark:bg-gray-800 hover:bg-red-50 dark:hover:bg-red-950/30 text-gray-700 dark:text-gray-300 hover:text-red-600 dark:hover:text-red-400 disabled:opacity-40 disabled:cursor-not-allowed rounded text-xs font-medium transition-colors cursor-pointer"
+ >
+ Clear
+
+
+
+
+ {/* Input Area */}
+
+
+
+ Encoded JWT Token
+
+ (Paste token below)
+
+
+
+
+
+ Header
+
+
+
+ Payload
+
+
+
+ Signature
+
+
+
+
+
+
+ {/* Error Notification */}
+ {error && (
+
+
+
+
+
+
Invalid JWT Token
+
{error}
+
+
+ )}
+
+ {/* Expiration Status Banner */}
+ {token && (
+
+ {expInfo ? (
+
+
+
+ {expInfo.isExpired ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+
+ {expInfo.isExpired ? "Token is Expired" : "Token is Active"}
+
+ {expInfo.relative}
+
+
+
+ Expiration (exp): {expInfo.formatted}
+
+
+
+
+ {iatInfo && (
+
+
Issued At (iat): {iatInfo.formatted}
+
+ )}
+
+ ) : (
+
+
+
+
+
No expiration (exp) claim present in this token payload.
+
+ )}
+
+ )}
+
+ {/* Decoded Content Grid */}
+
+ {/* Header Panel */}
+
+
+
+
+
+ Decoded Header
+
+
+ (Algorithm & Token Type)
+
+
+
copyToClipboard(headerString, setCopiedHeader)}
+ disabled={!headerString}
+ className="px-2.5 py-1 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-40 disabled:cursor-not-allowed text-gray-700 dark:text-gray-300 rounded text-xs font-medium transition-colors cursor-pointer flex items-center gap-1"
+ >
+ {copiedHeader ? (
+ <>
+
+
+
+ Copied!
+ >
+ ) : (
+ <>
+
+
+
+ Copy JSON
+ >
+ )}
+
+
+
+
+ {headerString || "// Decoded header JSON will appear here"}
+
+
+ {token?.header && (
+
+
+ Algorithm (alg): {" "}
+ {String(token.header.alg || "none")}
+
+
+ Type (typ): {" "}
+ {String(token.header.typ || "JWT")}
+
+
+ )}
+
+
+ {/* Payload Panel */}
+
+
+
+
+
+ Decoded Payload
+
+
+ (Data Claims)
+
+
+
copyToClipboard(payloadString, setCopiedPayload)}
+ disabled={!payloadString}
+ className="px-2.5 py-1 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-40 disabled:cursor-not-allowed text-gray-700 dark:text-gray-300 rounded text-xs font-medium transition-colors cursor-pointer flex items-center gap-1"
+ >
+ {copiedPayload ? (
+ <>
+
+
+
+ Copied!
+ >
+ ) : (
+ <>
+
+
+
+ Copy JSON
+ >
+ )}
+
+
+
+
+ {payloadString || "// Decoded payload JSON will appear here"}
+
+
+ {token?.payload && (
+
+ {token.payload.sub !== undefined && (
+
+ Subject (sub):
+ {String(token.payload.sub)}
+
+ )}
+ {token.payload.iss !== undefined && (
+
+ Issuer (iss):
+ {String(token.payload.iss)}
+
+ )}
+ {token.payload.aud !== undefined && (
+
+ Audience (aud):
+ {String(token.payload.aud)}
+
+ )}
+ {nbfInfo && (
+
+ Not Before (nbf):
+ {nbfInfo.formatted}
+
+ )}
+
+ )}
+
+
+
+ {/* Signature Inspection Card */}
+ {token && (
+
+
+
+
+ Signature Component
+
+
+
+ The signature is calculated by taking the encoded header, the encoded payload, a secret key, and signing them using the specified algorithm ({String(token.header?.alg || "HS256")}).
+
+
+ {token.signature || "// No signature"}
+
+
+ )}
+
+ {/* Informative Guide */}
+
+
+
🔴 Header (Red)
+
Contains metadata about the token, such as the signing algorithm (e.g. HS256, RS256) and token type (JWT).
+
+
+
🟣 Payload (Purple)
+
Contains the claims and entity statements (user ID, roles, permissions, expiration timestamps).
+
+
+
🔵 Signature (Blue)
+
Cryptographic hash used by the server to verify that the message wasn't changed along the way.
+
+
+
+
+ );
+}
diff --git a/src/app/tools/lorem-ipsum/page.tsx b/src/app/tools/lorem-ipsum/page.tsx
new file mode 100644
index 0000000..bb0df74
--- /dev/null
+++ b/src/app/tools/lorem-ipsum/page.tsx
@@ -0,0 +1,394 @@
+"use client";
+
+import { useState, useEffect, useCallback } from "react";
+import ToolLayout from "@/components/ToolLayout";
+
+const LOREM_WORDS = [
+ "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit",
+ "sed", "do", "eiusmod", "tempor", "incididunt", "ut", "labore", "et",
+ "dolore", "magna", "aliqua", "enim", "ad", "minim", "veniam", "quis",
+ "nostrud", "exercitation", "ullamco", "laboris", "nisi", "ut", "aliquip",
+ "ex", "ea", "commodo", "consequat", "duis", "aute", "irure", "in",
+ "reprehenderit", "in", "voluptate", "velit", "esse", "cillum", "dolore",
+ "eu", "fugiat", "nulla", "pariatur", "excepteur", "sint", "occaecat",
+ "cupidatat", "non", "proident", "sunt", "in", "culpa", "qui", "officia",
+ "deserunt", "mollit", "anim", "id", "est", "laborum", "curabitur",
+ "pretium", "tincidunt", "lacus", "nulla", "gravida", "orci", "a",
+ "odio", "nullam", "varius", "turpis", "et", "commodo", "pharetra",
+ "est", "eros", "bibendum", "elit", "nec", "luctus", "magna", "felis",
+ "sollicitudin", "mauris", "integer", "in", "mauris", "eu", "nibh",
+ "euismod", "gravida", "duis", "ac", "tellus", "et", "risus", "vulputate",
+ "vehicula", "donec", "lobortis", "risus", "a", "elit", "etiam", "tempor",
+ "ut", "ullamcorper", "venenatis", "pellentesque", "faucibus", "viverra",
+ "sapien", "auctor", "vitae", "massa", "porta", "semper", "quisque",
+ "posuere", "feugiat", "nunc", "molestie", "efficitur", "rutrum"
+];
+
+const CLASSIC_INTRO = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
+
+function getRandomWord(): string {
+ const index = Math.floor(Math.random() * LOREM_WORDS.length);
+ return LOREM_WORDS[index];
+}
+
+function generateSentence(targetWords: number): string {
+ const words: string[] = [];
+ for (let i = 0; i < targetWords; i++) {
+ words.push(getRandomWord());
+ }
+
+ // Capitalize first letter
+ if (words.length > 0) {
+ words[0] = words[0].charAt(0).toUpperCase() + words[0].slice(1);
+ }
+
+ // Add random comma for longer sentences
+ if (words.length > 7 && Math.random() > 0.4) {
+ const commaIndex = Math.floor(Math.random() * (words.length - 4)) + 2;
+ words[commaIndex] = words[commaIndex] + ",";
+ }
+
+ return words.join(" ") + ".";
+}
+
+function generateParagraph(targetWordCount: number, isFirst: boolean, startWithLorem: boolean): string {
+ if (isFirst && startWithLorem) {
+ let currentWords = CLASSIC_INTRO.split(" ").length;
+ const sentences = [CLASSIC_INTRO];
+
+ while (currentWords < targetWordCount) {
+ const sentenceLen = Math.floor(Math.random() * 8) + 6; // 6 - 13 words
+ sentences.push(generateSentence(sentenceLen));
+ currentWords += sentenceLen;
+ }
+ return sentences.join(" ");
+ }
+
+ let currentWords = 0;
+ const sentences: string[] = [];
+
+ while (currentWords < targetWordCount) {
+ const sentenceLen = Math.floor(Math.random() * 8) + 6; // 6 - 13 words
+ sentences.push(generateSentence(sentenceLen));
+ currentWords += sentenceLen;
+ }
+
+ return sentences.join(" ");
+}
+
+type OutputFormat = "plain" | "html" | "markdown";
+type ParagraphLength = "short" | "medium" | "long" | "custom";
+
+const LENGTH_RANGES: Record = {
+ short: { min: 30, max: 45, label: "Short (~35 words)" },
+ medium: { min: 65, max: 85, label: "Medium (~75 words)" },
+ long: { min: 110, max: 140, label: "Long (~125 words)" },
+ custom: { min: 20, max: 200, label: "Custom" },
+};
+
+export default function LoremIpsumPage() {
+ const [numParagraphs, setNumParagraphs] = useState(3);
+ const [lengthPreset, setLengthPreset] = useState("medium");
+ const [customWords, setCustomWords] = useState(75);
+ const [startWithLorem, setStartWithLorem] = useState(true);
+ const [format, setFormat] = useState("plain");
+ const [generatedText, setGeneratedText] = useState("");
+ const [copied, setCopied] = useState(false);
+
+ const generate = useCallback(() => {
+ const targetWordsPerPara =
+ lengthPreset === "custom"
+ ? customWords
+ : Math.floor(
+ Math.random() * (LENGTH_RANGES[lengthPreset].max - LENGTH_RANGES[lengthPreset].min + 1)
+ ) + LENGTH_RANGES[lengthPreset].min;
+
+ const paras: string[] = [];
+ for (let i = 0; i < numParagraphs; i++) {
+ paras.push(generateParagraph(targetWordsPerPara, i === 0, startWithLorem));
+ }
+
+ if (format === "html") {
+ setGeneratedText(paras.map((p) => `${p}
`).join("\n\n"));
+ } else if (format === "markdown") {
+ setGeneratedText(paras.join("\n\n"));
+ } else {
+ setGeneratedText(paras.join("\n\n"));
+ }
+ }, [numParagraphs, lengthPreset, customWords, startWithLorem, format]);
+
+ useEffect(() => {
+ generate();
+ }, [generate]);
+
+ const handleCopy = async () => {
+ try {
+ await navigator.clipboard.writeText(generatedText);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ } catch (err) {
+ console.error("Failed to copy lorem ipsum: ", err);
+ }
+ };
+
+ const handleDownload = () => {
+ const ext = format === "html" ? "html" : "txt";
+ const mime = format === "html" ? "text/html" : "text/plain";
+ const blob = new Blob([generatedText], { type: `${mime};charset=utf-8` });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `lorem-ipsum-${Date.now()}.${ext}`;
+ a.click();
+ URL.revokeObjectURL(url);
+ };
+
+ // Stats calculation
+ const wordCount = generatedText
+ .replace(/<[^>]*>/g, "")
+ .trim()
+ .split(/\s+/)
+ .filter(Boolean).length;
+ const charCount = generatedText.length;
+ const paragraphCount = numParagraphs;
+
+ return (
+
+
+ {/* Controls Section */}
+
+
+ {/* Number of Paragraphs */}
+
+
+ Paragraphs (1 - 20)
+
+
+
{
+ const val = Math.min(Math.max(1, parseInt(e.target.value) || 1), 20);
+ setNumParagraphs(val);
+ }}
+ className="w-20 rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
+ />
+
+ {[1, 3, 5, 10].map((count) => (
+ setNumParagraphs(count)}
+ className={`rounded px-2 py-1 text-xs font-medium transition-colors ${
+ numParagraphs === count
+ ? "bg-blue-600 text-white"
+ : "bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
+ }`}
+ >
+ {count}
+
+ ))}
+
+
+
+
+ {/* Paragraph Length */}
+
+
+ Paragraph Length
+
+
setLengthPreset(e.target.value as ParagraphLength)}
+ className="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
+ >
+ Short (~35 words)
+ Medium (~75 words)
+ Long (~125 words)
+ Custom word count
+
+
+ {lengthPreset === "custom" && (
+
+ Words:
+
+ setCustomWords(Math.min(Math.max(10, parseInt(e.target.value) || 10), 300))
+ }
+ className="w-24 rounded border border-gray-300 bg-white px-2 py-1 text-xs font-medium text-gray-900 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
+ />
+
+ )}
+
+
+ {/* Output Format */}
+
+
+ Format
+
+
+ setFormat("plain")}
+ className={`flex-1 rounded-md py-1.5 text-xs font-medium transition-colors ${
+ format === "plain"
+ ? "bg-white text-gray-900 shadow-xs dark:bg-gray-700 dark:text-white"
+ : "text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
+ }`}
+ >
+ Plain Text
+
+ setFormat("html")}
+ className={`flex-1 rounded-md py-1.5 text-xs font-medium transition-colors ${
+ format === "html"
+ ? "bg-white text-gray-900 shadow-xs dark:bg-gray-700 dark:text-white"
+ : "text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
+ }`}
+ >
+ HTML <p>
+
+
+
+
+ {/* Action Options */}
+
+
+ Options
+
+
+ setStartWithLorem(e.target.checked)}
+ className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
+ />
+ Start with “Lorem ipsum...”
+
+
+
+
+
+
+ Regenerate Text
+
+
+
+
+
+ {/* Output Section Header with Stats & Actions */}
+
+
+
+ {paragraphCount} paragraphs
+
+ •
+
+ {wordCount} words
+
+ •
+
+ {charCount} characters
+
+
+
+
+
+ {copied ? (
+ <>
+
+
+
+ Copied!
+ >
+ ) : (
+ <>
+
+
+
+ Copy Text
+ >
+ )}
+
+
+
+
+
+
+ Download
+
+
+
+
+ {/* Text Area Output */}
+
+
+
+
+ {/* Informational Details */}
+
+
+
Origin of Lorem Ipsum
+
+ Lorem Ipsum has been standard placeholder text since the 1500s, adapted from Cicero's philosophical work “de Finibus Bonorum et Malorum” written in 45 BC.
+
+
+
+
Design & Layout Utility
+
+ Using realistic filler text with natural word and sentence distribution avoids distracting reviewers with readable content while focusing purely on layout and typography.
+
+
+
+
+
+ );
+}
diff --git a/src/app/tools/markdown-preview/page.tsx b/src/app/tools/markdown-preview/page.tsx
new file mode 100644
index 0000000..0d55763
--- /dev/null
+++ b/src/app/tools/markdown-preview/page.tsx
@@ -0,0 +1,684 @@
+"use client";
+
+import { useState, useMemo, useCallback } from "react";
+import { marked } from "marked";
+import ToolLayout from "@/components/ToolLayout";
+
+const SAMPLE_MARKDOWN = `# DevToolbox Markdown Preview
+
+Welcome to the **live Markdown previewer**! Write or paste your Markdown in the editor on the left, and watch the formatted HTML update in *real time*.
+
+---
+
+## Typography & Formatting
+
+You can format text with **bold**, *italic*, ***bold and italic***, or ~~strikethrough~~. You can also highlight \`inline code\` or add [links to DevToolbox](https://example.com).
+
+> 💡 **Pro tip:** Markdown is lightweight, readable, and widely supported across GitHub, documentation sites, and content platforms.
+
+---
+
+## Lists & Checklists
+
+### Features Roadmap
+- [x] Instant client-side live rendering
+- [x] Full GitHub Flavored Markdown (GFM) support
+- [x] Syntax-styled tables & code blocks
+- [ ] Export directly to PDF
+- [ ] Custom CSS themes
+
+### Developer Resources
+1. **Frontend**: Next.js 15 App Router, React 19, Tailwind CSS 4
+2. **Parsing**: High-performance \`marked\` engine
+3. **Privacy**: 100% in-browser processing with zero server calls
+
+---
+
+## Code Blocks
+
+\`\`\`typescript
+interface UserProfile {
+ id: string;
+ name: string;
+ role: "admin" | "developer" | "viewer";
+ preferences: {
+ theme: "light" | "dark" | "system";
+ autoSave: boolean;
+ };
+}
+
+function greet(user: UserProfile): string {
+ return \`Hello, \${user.name}! Welcome back to DevToolbox.\`;
+}
+\`\`\`
+
+---
+
+## Structured Tables
+
+| Tool Name | Category | Status | Speed |
+| :--- | :--- | :---: | :--- |
+| **JSON Formatter** | Formatters | ✅ Ready | Instant |
+| **Markdown Preview** | Formatters | ✅ Ready | Instant |
+| **Base64 Encoder** | Encoders | ✅ Ready | Instant |
+| **UUID Generator** | Generators | ✅ Ready | Instant |
+
+---
+
+## Blockquotes & Citations
+
+> "Simplicity is prerequisite for reliability."
+> — *Edsger W. Dijkstra*
+`;
+
+export default function MarkdownPreviewPage() {
+ const [markdown, setMarkdown] = useState(SAMPLE_MARKDOWN);
+ const [viewMode, setViewMode] = useState<"split" | "preview" | "rawHtml" | "edit">("split");
+ const [copiedHtml, setCopiedHtml] = useState(false);
+ const [copiedMd, setCopiedMd] = useState(false);
+
+ // Parse markdown live using marked
+ const renderedHtml = useMemo(() => {
+ try {
+ return marked.parse(markdown, {
+ gfm: true,
+ breaks: true,
+ }) as string;
+ } catch (err) {
+ return `Error parsing markdown: ${String(err)}
`;
+ }
+ }, [markdown]);
+
+ // Statistics
+ const stats = useMemo(() => {
+ const trimmed = markdown.trim();
+ const chars = markdown.length;
+ const words = trimmed ? trimmed.split(/\s+/).filter(Boolean).length : 0;
+ const lines = markdown ? markdown.split("\n").length : 0;
+ const readingTimeMinutes = Math.max(1, Math.ceil(words / 200));
+ return { chars, words, lines, readingTimeMinutes };
+ }, [markdown]);
+
+ const handleCopyHtml = useCallback(async () => {
+ try {
+ await navigator.clipboard.writeText(renderedHtml);
+ setCopiedHtml(true);
+ setTimeout(() => setCopiedHtml(false), 2000);
+ } catch {
+ const textarea = document.createElement("textarea");
+ textarea.value = renderedHtml;
+ document.body.appendChild(textarea);
+ textarea.select();
+ document.execCommand("copy");
+ document.body.removeChild(textarea);
+ setCopiedHtml(true);
+ setTimeout(() => setCopiedHtml(false), 2000);
+ }
+ }, [renderedHtml]);
+
+ const handleCopyMarkdown = useCallback(async () => {
+ try {
+ await navigator.clipboard.writeText(markdown);
+ setCopiedMd(true);
+ setTimeout(() => setCopiedMd(false), 2000);
+ } catch {
+ const textarea = document.createElement("textarea");
+ textarea.value = markdown;
+ document.body.appendChild(textarea);
+ textarea.select();
+ document.execCommand("copy");
+ document.body.removeChild(textarea);
+ setCopiedMd(true);
+ setTimeout(() => setCopiedMd(false), 2000);
+ }
+ }, [markdown]);
+
+ const handleDownloadHtml = useCallback(() => {
+ const fullHtml = `
+
+
+
+
+ Rendered Markdown
+
+
+
+${renderedHtml}
+
+`;
+
+ const blob = new Blob([fullHtml], { type: "text/html;charset=utf-8" });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = "document.html";
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ }, [renderedHtml]);
+
+ const handleDownloadMd = useCallback(() => {
+ const blob = new Blob([markdown], { type: "text/markdown;charset=utf-8" });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = "document.md";
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ }, [markdown]);
+
+ const insertSnippet = (prefix: string, suffix = "", placeholder = "") => {
+ const textarea = document.getElementById("markdown-editor-input") as HTMLTextAreaElement | null;
+ if (!textarea) {
+ setMarkdown((prev) => prev + prefix + placeholder + suffix);
+ return;
+ }
+
+ const start = textarea.selectionStart;
+ const end = textarea.selectionEnd;
+ const selected = markdown.substring(start, end) || placeholder;
+ const replacement = prefix + selected + suffix;
+
+ const updated = markdown.substring(0, start) + replacement + markdown.substring(end);
+ setMarkdown(updated);
+
+ setTimeout(() => {
+ textarea.focus();
+ textarea.setSelectionRange(start + prefix.length, start + prefix.length + selected.length);
+ }, 0);
+ };
+
+ return (
+
+
+ {/* Style Block for Rendered HTML Typography */}
+