diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 9943c3d..54f81f5 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -24,6 +24,7 @@ _Add screenshots of relevant screens_ - [ ] My PR follows the style guidelines of this project - [ ] I have performed a self-check on my work +- [ ] If `package.json` is unchanged, `package-lock.json` is also unchanged in this PR **If changes are made in the code:** diff --git a/.github/workflows/lockfile-guard.yml b/.github/workflows/lockfile-guard.yml new file mode 100644 index 0000000..e50cdb4 --- /dev/null +++ b/.github/workflows/lockfile-guard.yml @@ -0,0 +1,51 @@ +name: Lockfile Guard + +on: + pull_request: + branches: + - develop + - main + +permissions: + contents: read + +jobs: + lockfile-guard: + name: Prevent unintended lockfile churn + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Validate package and lockfile changes + shell: bash + run: | + set -euo pipefail + + BASE_SHA="${{ github.event.pull_request.base.sha }}" + HEAD_SHA="${{ github.event.pull_request.head.sha }}" + + CHANGED_FILES="$(git diff --name-only "$BASE_SHA" "$HEAD_SHA")" + + PACKAGE_CHANGED="false" + LOCK_CHANGED="false" + + if echo "$CHANGED_FILES" | grep -qx "package.json"; then + PACKAGE_CHANGED="true" + fi + + if echo "$CHANGED_FILES" | grep -qx "package-lock.json"; then + LOCK_CHANGED="true" + fi + + if [ "$LOCK_CHANGED" = "true" ] && [ "$PACKAGE_CHANGED" = "false" ]; then + echo "❌ package-lock.json changed without package.json changes." + echo "If your PR does not intentionally change dependencies, discard lockfile changes:" + echo "git checkout -- package-lock.json" + exit 1 + fi + + echo "βœ… Lockfile check passed." \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fb9a7fe..7a3afa3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,5 +35,6 @@ jobs: - name: Run semantic release env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} + GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} run: npx semantic-release diff --git a/CHANGELOG.md b/CHANGELOG.md index 74270e4..78453f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,65 @@ +# [1.4.0-develop.8](https://github.com/betterbugs/dev-tools/compare/v1.4.0-develop.7...v1.4.0-develop.8) (2026-03-08) + + +### Features + +* Added URL parser & Query Editor Tool ([c9933ba](https://github.com/betterbugs/dev-tools/commit/c9933bae3cfbeb65c210f6877abf367c3cb0f46e)), closes [#51](https://github.com/betterbugs/dev-tools/issues/51) + +# [1.4.0-develop.7](https://github.com/betterbugs/dev-tools/compare/v1.4.0-develop.6...v1.4.0-develop.7) (2026-03-08) + + +### Features + +* **tools:** add Unix Timestamp Converter to development tools list ([3fedf57](https://github.com/betterbugs/dev-tools/commit/3fedf57f4036f7081b37c8ca1b3c585ea1e62045)) + +# [1.4.0-develop.6](https://github.com/betterbugs/dev-tools/compare/v1.4.0-develop.5...v1.4.0-develop.6) (2026-03-08) + + +### Features + +* Add Unix Timestamp (Epoch) Converter Utility ([f277272](https://github.com/betterbugs/dev-tools/commit/f27727267639f54e4ffaa80919b1d0836621ac7d)), closes [#16](https://github.com/betterbugs/dev-tools/issues/16) + +# [1.4.0-develop.5](https://github.com/betterbugs/dev-tools/compare/v1.4.0-develop.4...v1.4.0-develop.5) (2026-03-05) + + +### Bug Fixes + +* **release:** update GITHUB_TOKEN to use RELEASE_TOKEN for semantic release ([be3a012](https://github.com/betterbugs/dev-tools/commit/be3a012c6d7df84ab5826ce03268ec8aad402c15)) + + +### Features + +* **tools:** add SVG to React/CSS utility ([218ccad](https://github.com/betterbugs/dev-tools/commit/218ccad3c5eb7121a9bd7319147520eb39713695)), closes [#50](https://github.com/betterbugs/dev-tools/issues/50) + +# [1.4.0-develop.4](https://github.com/betterbugs/dev-tools/compare/v1.4.0-develop.3...v1.4.0-develop.4) (2026-03-03) + + +### Features + +* Add lockfile guard workflow to prevent unintended lockfile changes ([aa20fb5](https://github.com/betterbugs/dev-tools/commit/aa20fb5422aae56e9d80f7472a6f974624431572)) + +# [1.4.0-develop.3](https://github.com/betterbugs/dev-tools/compare/v1.4.0-develop.2...v1.4.0-develop.3) (2026-03-03) + + +### Features + +* Add IPv4 Subnet Calculator Tool ([ad1c03f](https://github.com/betterbugs/dev-tools/commit/ad1c03fd0065f0cfce409b72882188dc7de630d0)), closes [#33](https://github.com/betterbugs/dev-tools/issues/33) +* Add Smart Repair feature to JSON Validator ([#41](https://github.com/betterbugs/dev-tools/issues/41)) ([7a3c7a5](https://github.com/betterbugs/dev-tools/commit/7a3c7a5008d4236d954e1c20482635b0a0da5ef0)), closes [#38](https://github.com/betterbugs/dev-tools/issues/38) + +# [1.4.0-develop.2](https://github.com/betterbugs/dev-tools/compare/v1.4.0-develop.1...v1.4.0-develop.2) (2026-02-28) + + +### Bug Fixes + +* **tools:** implement proper bcrypt generator ([94d19be](https://github.com/betterbugs/dev-tools/commit/94d19be7e4b8d9256557e7668898ec4d6c3ca15c)), closes [#23](https://github.com/betterbugs/dev-tools/issues/23) [#13](https://github.com/betterbugs/dev-tools/issues/13) + +# [1.4.0-develop.1](https://github.com/betterbugs/dev-tools/compare/v1.3.2...v1.4.0-develop.1) (2026-02-28) + + +### Features + +* **ui:** add reusable CopyButton and refactor wordCounter and jsonToTxt ([d5b9e83](https://github.com/betterbugs/dev-tools/commit/d5b9e8333673c5254cf39529a90869b1b741e385)), closes [#17](https://github.com/betterbugs/dev-tools/issues/17) + ## [1.3.2](https://github.com/betterbugs/dev-tools/compare/v1.3.1...v1.3.2) (2026-02-16) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 121a4c2..585e48a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,6 +47,37 @@ If your work depends on unreleased features or changes, base your work directly ## Code Contributions +### 🚨 Dependency & Lockfile Policy (Read Before PR!) + +**When to Commit `package-lock.json`** + +- **You MUST commit `package-lock.json` if:** + - You add, remove, or upgrade a dependency in `package.json` (for example, when your new tool needs a new npm package). + - You intentionally update any package version in `package.json`. + - After such changes, always run `npm install` and commit both `package.json` and `package-lock.json` together. + +- **You MUST NOT commit `package-lock.json` if:** + - You are only editing, adding, or refactoring tool components, UI, or logic, and did not touch `package.json`. + - You ran `npm install` after pulling latest develop, but did not change dependencies. If the lockfile changes, discard it (`git checkout -- package-lock.json`). + +- **Dependency/toolchain upgrades (Next.js, ESLint, etc.) must be in a separate PR, never mixed with feature/tool PRs.** + +**For Adding a New Tool:** + +- If your tool needs a new npm package: + 1. Add the dependency to `package.json`. + 2. Run `npm install` (this updates `package-lock.json`). + 3. Commit both files in your PR. +- If your tool does NOT need a new dependency, do NOT touch or commit `package-lock.json`. + +**Why?** + +- Our CI uses `npm ci`, which requires the lockfile to match `package.json` exactly. +- Random lockfile churn (from different npm versions or accidental upgrades) causes huge, noisy diffs and can break builds. +- Only the canonical lockfile in `develop` is valid. + +--- + Please ensure your pull request adheres to the following guidelines: - Search [open pull requests](https://github.com/betterbugs/dev-tools/pulls) to ensure your change hasn't already been submitted diff --git a/app/[slug]/page.tsx b/app/[slug]/page.tsx index 81a451f..b77333d 100644 --- a/app/[slug]/page.tsx +++ b/app/[slug]/page.tsx @@ -21,9 +21,12 @@ import RecorderGradientIcon from "@/app/components/theme/Icon/recorderGradientIc import SEOComponent from "@/app/components/theme/SEOComponent/SEOComponent"; import { detectBrowser } from "@/app/libs/helpers"; import EdgeIcon from "@/app/components/theme/Icon/edgeIcon"; +import { useTheme } from "@/app/contexts/themeContext"; const Page = ({ params: { slug } }: { params: { slug: string } }) => { const [browser, setBrowser] = useState("chrome"); + const { isLightTheme } = useTheme(); + const logoSrc = isLightTheme ? "/images/bb-logo-light.svg" : "/images/bb-logo.svg"; useEffect(() => { setBrowser(detectBrowser()); @@ -56,16 +59,16 @@ const Page = ({ params: { slug } }: { params: { slug: string } }) => { ogDescription={meta_data?.og_description} ogImage={meta_data?.og_image} /> -
+
{/*Hero cta section */}
-
+
{/* Left Section */}
BetterBugs Logo {

-
-
+
{/* Heading section */} {hero_section && (
@@ -209,7 +214,7 @@ const Page = ({ params: { slug } }: { params: { slug: string } }) => { {/* Tools Panel - 20% width, fixed */} {development_tools_list?.length > 0 && (
-
+

Other Tools @@ -218,7 +223,7 @@ const Page = ({ params: { slug } }: { params: { slug: string } }) => { {development_tools_list?.map((tool: any, index: any) => (
{tool?.tool}
@@ -350,7 +355,7 @@ const Page = ({ params: { slug } }: { params: { slug: string } }) => { return (

{splitDescriptions.map( @@ -383,7 +388,7 @@ const Page = ({ params: { slug } }: { params: { slug: string } }) => { return ( {text} @@ -457,122 +462,6 @@ const Page = ({ params: { slug } }: { params: { slug: string } }) => {

)} - {/* example section */} - {development_tool_example && ( -
- {development_tool_example?.example_title && ( -

- {development_tool_example?.example_title} -

- )} - {development_tool_example?.example_description && ( -

- {development_tool_example?.example_description} -

- )} - - {development_tool_example?.example_input && ( -
- {development_tool_example?.example_input?.title && ( -

- {development_tool_example?.example_input?.title} -

- )} - {development_tool_example?.example_input?.json_data && ( -
-                              
-                                {development_tool_example?.example_input?.json_data}
-                              
-                            
- )} -
- )} - - {development_tool_example?.example_outputs && ( -
- {development_tool_example?.example_outputs?.intro && ( -

- {development_tool_example?.example_outputs?.intro} -

- )} - {development_tool_example?.example_outputs?.outputs?.map( - (output: any, index: number) => ( -
- {output?.mode && ( -

- {output?.mode} -

- )} - {output?.title && ( -

- {output?.title} -

- )} - {output?.content && ( -
-                                    
-                                      {output?.content}
-                                    
-                                  
- )} - {output?.note && ( -

- {output?.note} -

- )} -
- ) - )} -
- )} -
- )} - - {/* what section */} - {development_tools_what && ( -
- {development_tools_what?.about_title && ( -

- {development_tools_what?.about_title} -

- )} - {development_tools_what?.what_description?.map( - (desc: any, index: number) => { - const descriptions = desc?.descriptions; - const splitDescriptions = - descriptions.split(/(".*?")/); // Split quoted and unquoted text - - return ( -

- {splitDescriptions.map( - (text: any, subIndex: any) => { - const isQuoted = - text.startsWith("") && text.endsWith(""); - - return ( - - {text} - - ); - } - )} -

- ); - } - )} -
- )} - {/* step-by-step guide */}
{development_tools_steps_guide?.guide_title && ( @@ -641,7 +530,7 @@ const Page = ({ params: { slug } }: { params: { slug: string } }) => { part.endsWith("") ? ( {part} @@ -664,7 +553,7 @@ const Page = ({ params: { slug } }: { params: { slug: string } }) => { ) ? ( {sub.slice( 2, @@ -806,6 +695,198 @@ const Page = ({ params: { slug } }: { params: { slug: string } }) => { )}
+ {/* example section */} + {development_tool_example && ( +
+ {development_tool_example?.example_title && ( +

+ {development_tool_example?.example_title} +

+ )} + {development_tool_example?.example_description && ( +

+ {development_tool_example?.example_description} +

+ )} + + {development_tool_example?.example_input && ( +
+ {development_tool_example?.example_input?.title && ( +

+ {development_tool_example?.example_input?.title} +

+ )} + {development_tool_example?.example_input?.json_data && ( +
+                              
+                                {development_tool_example?.example_input?.json_data}
+                              
+                            
+ )} +
+ )} + + {development_tool_example?.example_outputs && ( +
+ {development_tool_example?.example_outputs?.intro && ( +

+ {development_tool_example?.example_outputs?.intro} +

+ )} + {development_tool_example?.example_outputs?.outputs?.map( + (output: any, index: number) => ( +
+ {output?.mode && ( +

+ {output?.mode} +

+ )} + {output?.title && ( +

+ {output?.title} +

+ )} + {output?.content && ( +
+                                    
+                                      {output?.content}
+                                    
+                                  
+ )} + {output?.note && ( +

+ {output?.note} +

+ )} +
+ ) + )} +
+ )} + + {/* JavaScript example section (optional) */} + {development_tool_example?.javascript_example && ( +
+ {development_tool_example?.javascript_example?.title && ( +
+ {development_tool_example?.javascript_example?.title} +
+ )} + {development_tool_example?.javascript_example?.description && ( +

+ {development_tool_example?.javascript_example?.description} +

+ )} + + {development_tool_example?.javascript_example?.methods?.length > + 0 && ( +
+
    + {development_tool_example?.javascript_example?.methods?.map( + (method: any, index: number) => ( +
  • + {method?.name && ( + + {method?.name}{" "} + + )} + {method?.description && ( + + {method?.description} + + )} +
  • + ) + )} +
+
+ )} + + {development_tool_example?.javascript_example?.examples?.length > + 0 && ( +
+ {development_tool_example?.javascript_example?.examples?.map( + (example: any, index: number) => ( +
+ {example?.title && ( +

+ {example?.title} +

+ )} + {example?.code && ( +
+                                          
+                                            {example?.code}
+                                          
+                                        
+ )} +
+ ) + )} +
+ )} + + {development_tool_example?.javascript_example?.note && ( +

+ {development_tool_example?.javascript_example?.note} +

+ )} +
+ )} +
+ )} + + {/* what section */} + {development_tools_what && ( +
+ {development_tools_what?.about_title && ( +

+ {development_tools_what?.about_title} +

+ )} + {development_tools_what?.what_description?.map( + (desc: any, index: number) => { + const descriptions = desc?.descriptions; + const splitDescriptions = + descriptions.split(/(".*?")/); // Split quoted and unquoted text + + return ( +

+ {splitDescriptions.map( + (text: any, subIndex: any) => { + const isQuoted = + text.startsWith("") && text.endsWith(""); + + return ( + + {text} + + ); + } + )} +

+ ); + } + )} +
+ )} + {/* how to use */}
{development_tools_how_use?.how_use_title && ( @@ -884,12 +965,12 @@ const Page = ({ params: { slug } }: { params: { slug: string } }) => { {/* cta section */}
-
+
{/* Left Section */}
BetterBugs Logo {

-

diff --git a/app/components/comparisonsComponent/comparisonsStyles.module.scss b/app/components/comparisonsComponent/comparisonsStyles.module.scss index 6f28fed..7aa1652 100644 --- a/app/components/comparisonsComponent/comparisonsStyles.module.scss +++ b/app/components/comparisonsComponent/comparisonsStyles.module.scss @@ -27,6 +27,13 @@ ); } +:global([data-theme="light"]) .ctaCardGridBg { + background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%); + border: 1px solid #e5e7eb; + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.06), + inset 0 1px 0 rgba(255, 255, 255, 0.9); +} + .collapse { :global(.ant-collapse) { background-color: transparent !important; diff --git a/app/components/developmentToolsComponent/bcryptGenerator.tsx b/app/components/developmentToolsComponent/bcryptGenerator.tsx index 84e0e63..aa5514b 100644 --- a/app/components/developmentToolsComponent/bcryptGenerator.tsx +++ b/app/components/developmentToolsComponent/bcryptGenerator.tsx @@ -1,5 +1,6 @@ "use client"; import React, { useState, useMemo } from "react"; +import bcrypt from 'bcryptjs'; // Custom styles for the range slider const sliderStyles = ` @@ -48,28 +49,29 @@ const BcryptGenerator = () => { // Simple bcrypt-like hash function (for demonstration - not cryptographically secure) const generateHash = async (text: string, rounds: number): Promise => { - // This is a simplified implementation for demo purposes - // In production, you would use a proper bcrypt library - const encoder = new TextEncoder(); - const data = encoder.encode(text + rounds.toString()); - const hashBuffer = await crypto.subtle.digest('SHA-256', data); - const hashArray = Array.from(new Uint8Array(hashBuffer)); - const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); - return `$2b$${rounds}$${hashHex.substring(0, 53)}`; + return new Promise((resolve, reject) => { + bcrypt.genSalt(rounds, (err, salt) => { + if (err) return reject(err); + bcrypt.hash(text, salt, (err2, hash) => { + if (err2) return reject(err2); + resolve(hash); + }); + }); + }); }; // Simple verification function (for demonstration) const verifyHash = async (password: string, hash: string): Promise => { - try { - const parts = hash.split('$'); - if (parts.length !== 4 || parts[1] !== '2b') return false; - - const rounds = parseInt(parts[2]); - const generatedHash = await generateHash(password, rounds); - return generatedHash === hash; - } catch { - return false; - } + return new Promise((resolve) => { + try { + bcrypt.compare(password, hash, (err, res) => { + if (err) return resolve(false); + resolve(Boolean(res)); + }); + } catch { + resolve(false); + } + }); }; const handleGenerateHash = async () => { diff --git a/app/components/developmentToolsComponent/curlToCodeConverter.tsx b/app/components/developmentToolsComponent/curlToCodeConverter.tsx new file mode 100644 index 0000000..039a945 --- /dev/null +++ b/app/components/developmentToolsComponent/curlToCodeConverter.tsx @@ -0,0 +1,738 @@ +"use client"; +import React, { useState, useCallback } from "react"; +import DevelopmentToolsStyles from "../../developmentToolsStyles.module.scss"; +import CopyIcon from "../theme/Icon/copyIcon"; +import ReloadIcon from "../theme/Icon/reload"; + +// ─── Types ─────────────────────────────────────────────────────────────────── + +interface ParsedCurl { + url: string; + method: string; + headers: Record; + body: string | null; + auth: { user: string; password: string } | null; + isFormData: boolean; + isJson: boolean; +} + +type Language = + | "js-fetch" + | "js-axios" + | "python-requests" + | "go" + | "nodejs"; + +// ─── Parser ────────────────────────────────────────────────────────────────── + +function tokenizeCurl(input: string): string[] { + const tokens: string[] = []; + let i = 0; + const s = input.trim().replace(/\\\n/g, " ").replace(/\\\r\n/g, " "); + + while (i < s.length) { + // Skip whitespace + while (i < s.length && /\s/.test(s[i])) i++; + if (i >= s.length) break; + + if (s[i] === "'") { + // Single-quoted string + i++; + let tok = ""; + while (i < s.length && s[i] !== "'") { + tok += s[i++]; + } + i++; // closing quote + tokens.push(tok); + } else if (s[i] === '"') { + // Double-quoted string (handle basic escapes) + i++; + let tok = ""; + while (i < s.length && s[i] !== '"') { + if (s[i] === "\\" && i + 1 < s.length) { + i++; + const c = s[i]; + if (c === "n") tok += "\n"; + else if (c === "t") tok += "\t"; + else if (c === "r") tok += "\r"; + else tok += c; + } else { + tok += s[i]; + } + i++; + } + i++; // closing quote + tokens.push(tok); + } else { + // Unquoted token + let tok = ""; + while (i < s.length && !/\s/.test(s[i])) { + tok += s[i++]; + } + tokens.push(tok); + } + } + return tokens; +} + +function parseCurl(raw: string): ParsedCurl | null { + const tokens = tokenizeCurl(raw); + if (!tokens.length) return null; + + // Must start with 'curl' + if (tokens[0].toLowerCase() !== "curl") return null; + + let url = ""; + let method = ""; + const headers: Record = {}; + let body: string | null = null; + let auth: { user: string; password: string } | null = null; + + for (let i = 1; i < tokens.length; i++) { + const tok = tokens[i]; + + if (tok === "-X" || tok === "--request") { + method = tokens[++i]?.toUpperCase() ?? ""; + } else if (tok === "-H" || tok === "--header") { + const hdr = tokens[++i] ?? ""; + const colon = hdr.indexOf(":"); + if (colon !== -1) { + const key = hdr.slice(0, colon).trim(); + const val = hdr.slice(colon + 1).trim(); + headers[key] = val; + } + } else if ( + tok === "-d" || + tok === "--data" || + tok === "--data-raw" || + tok === "--data-binary" || + tok === "--data-ascii" + ) { + body = tokens[++i] ?? ""; + } else if (tok === "-u" || tok === "--user") { + const creds = tokens[++i] ?? ""; + const idx = creds.indexOf(":"); + if (idx !== -1) { + auth = { + user: creds.slice(0, idx), + password: creds.slice(idx + 1), + }; + } else { + auth = { user: creds, password: "" }; + } + } else if (tok === "--json") { + body = tokens[++i] ?? ""; + headers["Content-Type"] = "application/json"; + headers["Accept"] = "application/json"; + } else if (tok === "--form" || tok === "-F") { + // Basic form data support - collect first occurrence + if (!body) body = tokens[++i] ?? ""; + else i++; // skip but don't overwrite + } else if ( + tok === "--compressed" || + tok === "-L" || + tok === "--location" || + tok === "-k" || + tok === "--insecure" || + tok === "-s" || + tok === "--silent" || + tok === "-v" || + tok === "--verbose" || + tok === "-i" || + tok === "--include" + ) { + // Silently ignore these flags + } else if (tok === "-b" || tok === "--cookie") { + const cookie = tokens[++i] ?? ""; + headers["Cookie"] = cookie; + } else if (tok === "-A" || tok === "--user-agent") { + headers["User-Agent"] = tokens[++i] ?? ""; + } else if (tok === "-e" || tok === "--referer") { + headers["Referer"] = tokens[++i] ?? ""; + } else if (!tok.startsWith("-")) { + // Positional arg = URL + if (!url) url = tok; + } + } + + if (!url) return null; + + // Detect method + if (!method) { + method = body ? "POST" : "GET"; + } + + // Detect content type + const contentType = headers["Content-Type"] || headers["content-type"] || ""; + const isJson = + contentType.includes("application/json") || + (body !== null && body.trimStart().startsWith("{")) || + (body !== null && body.trimStart().startsWith("[")); + const isFormData = contentType.includes("application/x-www-form-urlencoded"); + + return { url, method, headers, body, auth, isFormData, isJson }; +} + +// ─── Code Generators ───────────────────────────────────────────────────────── + +function indent(n: number): string { + return " ".repeat(n); +} + +function jsStringify(val: string): string { + return JSON.stringify(val); +} + +function generateJsFetch(p: ParsedCurl): string { + const lines: string[] = []; + const options: string[] = []; + + if (p.method !== "GET") { + options.push(`${indent(1)}method: ${jsStringify(p.method)},`); + } + + const headerEntries = Object.entries(p.headers); + if (p.auth) { + const encoded = Buffer.from( + `${p.auth.user}:${p.auth.password}` + ).toString("base64"); + headerEntries.push(["Authorization", `Basic ${encoded}`]); + } + + if (headerEntries.length) { + options.push(`${indent(1)}headers: {`); + for (const [k, v] of headerEntries) { + options.push(`${indent(2)}${jsStringify(k)}: ${jsStringify(v)},`); + } + options.push(`${indent(1)}},`); + } + + if (p.body !== null) { + if (p.isJson) { + try { + const parsed = JSON.parse(p.body); + options.push( + `${indent(1)}body: JSON.stringify(${JSON.stringify(parsed, null, 2) + .split("\n") + .join("\n" + indent(1))}),` + ); + } catch { + options.push(`${indent(1)}body: ${jsStringify(p.body)},`); + } + } else { + options.push(`${indent(1)}body: ${jsStringify(p.body)},`); + } + } + + lines.push(`const response = await fetch(${jsStringify(p.url)}, {`); + lines.push(...options); + lines.push(`});`); + lines.push(`const data = await response.json();`); + lines.push(`console.log(data);`); + + return lines.join("\n"); +} + +function generateJsAxios(p: ParsedCurl): string { + const lines: string[] = []; + + const configParts: string[] = []; + configParts.push(`${indent(1)}method: ${jsStringify(p.method.toLowerCase())},`); + configParts.push(`${indent(1)}url: ${jsStringify(p.url)},`); + + const headerEntries = Object.entries(p.headers); + if (p.auth) { + lines.unshift(`// Axios handles basic auth natively:`); + configParts.push(`${indent(1)}auth: {`); + configParts.push(`${indent(2)}username: ${jsStringify(p.auth.user)},`); + configParts.push(`${indent(2)}password: ${jsStringify(p.auth.password)},`); + configParts.push(`${indent(1)}},`); + } + + if (headerEntries.length) { + configParts.push(`${indent(1)}headers: {`); + for (const [k, v] of headerEntries) { + configParts.push(`${indent(2)}${jsStringify(k)}: ${jsStringify(v)},`); + } + configParts.push(`${indent(1)}},`); + } + + if (p.body !== null) { + if (p.isJson) { + try { + const parsed = JSON.parse(p.body); + const jsonStr = JSON.stringify(parsed, null, 2) + .split("\n") + .join("\n" + indent(1)); + configParts.push(`${indent(1)}data: ${jsonStr},`); + } catch { + configParts.push(`${indent(1)}data: ${jsStringify(p.body)},`); + } + } else { + configParts.push(`${indent(1)}data: ${jsStringify(p.body)},`); + } + } + + lines.push(`import axios from 'axios';`); + lines.push(``); + lines.push(`const response = await axios({`); + lines.push(...configParts); + lines.push(`});`); + lines.push(`console.log(response.data);`); + + return lines.join("\n"); +} + +function generatePythonRequests(p: ParsedCurl): string { + const lines: string[] = []; + lines.push(`import requests`); + lines.push(``); + + const args: string[] = ["url"]; + + // Headers + const headerEntries = Object.entries(p.headers); + if (headerEntries.length) { + lines.push(`headers = {`); + for (const [k, v] of headerEntries) { + lines.push(` ${JSON.stringify(k)}: ${JSON.stringify(v)},`); + } + lines.push(`}`); + lines.push(``); + args.push(`headers=headers`); + } + + // Auth + if (p.auth) { + args.push( + `auth=(${JSON.stringify(p.auth.user)}, ${JSON.stringify(p.auth.password)})` + ); + } + + // Body + if (p.body !== null) { + if (p.isJson) { + try { + const parsed = JSON.parse(p.body); + const pyJson = JSON.stringify(parsed, null, 4) + .replace(/: null/g, ": None") + .replace(/: true/g, ": True") + .replace(/: false/g, ": False"); + lines.push(`payload = ${pyJson}`); + } catch { + lines.push(`payload = ${JSON.stringify(p.body)}`); + } + lines.push(``); + args.push(`json=payload`); + } else if (p.isFormData) { + lines.push(`data = ${JSON.stringify(p.body)}`); + lines.push(``); + args.push(`data=data`); + } else { + lines.push(`data = ${JSON.stringify(p.body)}`); + lines.push(``); + args.push(`data=data`); + } + } + + const methodLower = p.method.toLowerCase(); + lines.push(`url = ${JSON.stringify(p.url)}`); + lines.push(``); + lines.push(`response = requests.${methodLower}(${args.join(", ")})`); + lines.push(`print(response.json())`); + + return lines.join("\n"); +} + +function generateGo(p: ParsedCurl): string { + const lines: string[] = []; + + lines.push(`package main`); + lines.push(``); + lines.push(`import (`); + lines.push(`\t"fmt"`); + if (p.body) lines.push(`\t"strings"`); + lines.push(`\t"net/http"`); + lines.push(`\t"io"`); + lines.push(`)`); + lines.push(``); + lines.push(`func main() {`); + lines.push(`\turl := ${JSON.stringify(p.url)}`); + lines.push(``); + + if (p.body) { + const bodyStr = p.isJson + ? (() => { + try { + return JSON.stringify(JSON.parse(p.body), null, 2); + } catch { + return p.body; + } + })() + : p.body; + lines.push(`\tbody := strings.NewReader(${JSON.stringify(bodyStr)})`); + lines.push( + `\treq, err := http.NewRequest(${JSON.stringify(p.method)}, url, body)` + ); + } else { + lines.push( + `\treq, err := http.NewRequest(${JSON.stringify(p.method)}, url, nil)` + ); + } + + lines.push(`\tif err != nil {`); + lines.push(`\t\tpanic(err)`); + lines.push(`\t}`); + lines.push(``); + + for (const [k, v] of Object.entries(p.headers)) { + lines.push(`\treq.Header.Set(${JSON.stringify(k)}, ${JSON.stringify(v)})`); + } + + if (p.auth) { + lines.push( + `\treq.SetBasicAuth(${JSON.stringify(p.auth.user)}, ${JSON.stringify(p.auth.password)})` + ); + } + + lines.push(``); + lines.push(`\tclient := &http.Client{}`); + lines.push(`\tresp, err := client.Do(req)`); + lines.push(`\tif err != nil {`); + lines.push(`\t\tpanic(err)`); + lines.push(`\t}`); + lines.push(`\tdefer resp.Body.Close()`); + lines.push(``); + lines.push(`\tbody2, _ := io.ReadAll(resp.Body)`); + lines.push(`\tfmt.Println(string(body2))`); + lines.push(`}`); + + return lines.join("\n"); +} + +function generateNodeJs(p: ParsedCurl): string { + const lines: string[] = []; + + try { + const urlObj = new URL(p.url); + const protocol = urlObj.protocol.replace(":", ""); + const hostname = urlObj.hostname; + const port = + urlObj.port || (protocol === "https" ? "443" : "80"); + const path = urlObj.pathname + (urlObj.search || ""); + + lines.push(`const ${protocol} = require('${protocol}');`); + lines.push(``); + + if (p.body) { + const bodyStr = p.isJson + ? (() => { + try { + return JSON.stringify(JSON.parse(p.body), null, 2); + } catch { + return p.body; + } + })() + : p.body; + lines.push(`const postData = ${JSON.stringify(bodyStr)};`); + lines.push(``); + } + + lines.push(`const options = {`); + lines.push(` hostname: ${JSON.stringify(hostname)},`); + if ( + (protocol === "https" && port !== "443") || + (protocol === "http" && port !== "80") + ) { + lines.push(` port: ${port},`); + } + lines.push(` path: ${JSON.stringify(path || "/")},`); + lines.push(` method: ${JSON.stringify(p.method)},`); + + const headerEntries = Object.entries(p.headers); + if (p.body) { + headerEntries.push(["Content-Length", "Buffer.byteLength(postData)"]); + } + if (p.auth) { + const creds = `${p.auth.user}:${p.auth.password}`; + const encoded = Buffer.from(creds).toString("base64"); + headerEntries.push(["Authorization", `Basic ${encoded}`]); + } + + if (headerEntries.length) { + lines.push(` headers: {`); + for (const [k, v] of headerEntries) { + if (k === "Content-Length") { + lines.push(` ${JSON.stringify(k)}: ${v},`); + } else { + lines.push(` ${JSON.stringify(k)}: ${JSON.stringify(v)},`); + } + } + lines.push(` },`); + } + + lines.push(`};`); + lines.push(``); + lines.push(`const req = ${protocol}.request(options, (res) => {`); + lines.push(` let data = '';`); + lines.push(` res.on('data', (chunk) => { data += chunk; });`); + lines.push(` res.on('end', () => { console.log(JSON.parse(data)); });`); + lines.push(`});`); + lines.push(``); + lines.push(`req.on('error', (e) => { console.error(e); });`); + + if (p.body) { + lines.push(`req.write(postData);`); + } + + lines.push(`req.end();`); + } catch { + // Fallback if URL parsing fails + lines.push(`// Could not fully parse the URL: ${p.url}`); + lines.push( + `// Please adjust hostname, path and port below.` + ); + lines.push(`const https = require('https');`); + lines.push(`// ... (manual setup required)`); + } + + return lines.join("\n"); +} + +function generateCode(parsed: ParsedCurl, lang: Language): string { + switch (lang) { + case "js-fetch": + return generateJsFetch(parsed); + case "js-axios": + return generateJsAxios(parsed); + case "python-requests": + return generatePythonRequests(parsed); + case "go": + return generateGo(parsed); + case "nodejs": + return generateNodeJs(parsed); + default: + return ""; + } +} + +// ─── Sample cURL commands ──────────────────────────────────────────────────── + +const SAMPLES: Record = { + "GET with headers": `curl https://api.example.com/users \\\n -H 'Authorization: Bearer YOUR_TOKEN' \\\n -H 'Accept: application/json'`, + "POST JSON": `curl -X POST https://api.example.com/users \\\n -H 'Content-Type: application/json' \\\n -H 'Authorization: Bearer YOUR_TOKEN' \\\n -d '{"name":"John Doe","email":"john@example.com"}'`, + "PUT with body": `curl -X PUT https://api.example.com/users/42 \\\n -H 'Content-Type: application/json' \\\n -d '{"name":"Jane Doe"}'`, + "Basic auth": `curl https://api.example.com/secure \\\n -u username:password`, + "Form data": `curl -X POST https://api.example.com/login \\\n -H 'Content-Type: application/x-www-form-urlencoded' \\\n -d 'username=john&password=secret'`, +}; + +const LANGUAGES: { value: Language; label: string }[] = [ + { value: "js-fetch", label: "JavaScript (Fetch)" }, + { value: "js-axios", label: "JavaScript (Axios)" }, + { value: "python-requests", label: "Python (Requests)" }, + { value: "go", label: "Go" }, + { value: "nodejs", label: "Node.js" }, +]; + +// ─── Component ──────────────────────────────────────────────────────────────── + +const CurlToCodeConverter = () => { + const [curlInput, setCurlInput] = useState(""); + const [language, setLanguage] = useState("js-fetch"); + const [output, setOutput] = useState(""); + const [error, setError] = useState(""); + const [copySuccess, setCopySuccess] = useState(false); + + const convert = useCallback( + (raw: string, lang: Language) => { + const trimmed = raw.trim(); + if (!trimmed) { + setOutput(""); + setError(""); + return; + } + const parsed = parseCurl(trimmed); + if (!parsed) { + setOutput(""); + setError( + "Could not parse the cURL command. Make sure it starts with 'curl' and includes a URL." + ); + return; + } + setError(""); + setOutput(generateCode(parsed, lang)); + }, + [] + ); + + const handleInputChange = (e: React.ChangeEvent) => { + const val = e.target.value; + setCurlInput(val); + convert(val, language); + }; + + const handleLanguageChange = (e: React.ChangeEvent) => { + const lang = e.target.value as Language; + setLanguage(lang); + convert(curlInput, lang); + }; + + const handleLoadSample = (sampleKey: string) => { + const sample = SAMPLES[sampleKey]; + if (sample) { + setCurlInput(sample); + convert(sample, language); + } + }; + + const handleCopy = async () => { + if (!output) return; + try { + await navigator.clipboard.writeText(output); + setCopySuccess(true); + setTimeout(() => setCopySuccess(false), 2000); + } catch { + setError("Failed to copy to clipboard."); + } + }; + + const handleClear = () => { + setCurlInput(""); + setOutput(""); + setError(""); + setCopySuccess(false); + }; + + return ( +

+
+
+
+
+ {/* Controls row */} +
+ {/* Language selector */} +
+ + +
+ + {/* Samples */} +
+ + +
+ + {/* Action buttons */} +
+ + +
+
+ + {/* Editor area */} +
+ {/* Input */} +
+ + + {svgInput && ( + + )} + +
+ {error && ( +
{error}
+ )} +
+ + {/* Output */} +
+

Optimized Output

+
+ + {output && ( + + )} +
+
+
+
+
+
+
+
+ ); +}; + +export default SvgConverter; diff --git a/app/components/developmentToolsComponent/urlParser.tsx b/app/components/developmentToolsComponent/urlParser.tsx new file mode 100644 index 0000000..ea6eb9c --- /dev/null +++ b/app/components/developmentToolsComponent/urlParser.tsx @@ -0,0 +1,339 @@ +"use client"; +import React, { useState, useEffect } from "react"; +import DevelopmentToolsStyles from "../../developmentToolsStyles.module.scss"; + +interface QueryParam { + id: string; + key: string; + value: string; + active: boolean; + encoded: boolean; +} + +const UrlParser = () => { + const [urlInput, setUrlInput] = useState(""); + const [parsedUrl, setParsedUrl] = useState(null); + const [queryParams, setQueryParams] = useState([]); + const [error, setError] = useState(""); + const isInternalUpdate = React.useRef(false); + + + // Function to update the input and state from a URL object + const updateStateFromUrl = (url: URL) => { + setParsedUrl(url); + const params: QueryParam[] = []; + let index = 0; + url.searchParams.forEach((value, key) => { + // Use a stable ID based on index and key to avoid unnecessary re-renders + // when only values change, or random regeneration on every parse. + const id = `param-${index}-${key}`; + params.push({ + id, + key, + value, + active: true, + encoded: true + }); + index++; + }); + setQueryParams(params); + } + + // Initial parse on input change + useEffect(() => { + if (isInternalUpdate.current) { + isInternalUpdate.current = false; + return; + } + + if (!urlInput.trim()) { + setParsedUrl(null); + setQueryParams([]); + setError(""); + return; + } + + try { + // Basic heuristic to add protocol if missing for better UX + let urlToParse = urlInput; + if (!urlInput.match(/^[a-zA-Z][a-zA-Z\d+\-.]*:/)) { + // If doesn't start with scheme, assume http:// if it looks like a domain + if (urlInput.includes('.') && !urlInput.startsWith('/')) { + urlToParse = "http://" + urlInput; + } + } + + const url = new URL(urlToParse); + updateStateFromUrl(url); + setError(""); + } catch (err) { + setParsedUrl(null); + setError("Invalid URL"); + } + }, [urlInput]); + + // Re-construct URL when params change + const updateUrlFromParams = (newParams: QueryParam[]) => { + if (!parsedUrl) return; + + try { + const newUrl = new URL(parsedUrl.toString()); + const queryParts: string[] = []; + + newParams.forEach(p => { + if (p.active && p.key) { + const key = p.encoded ? encodeURIComponent(p.key) : p.key; + const value = p.encoded ? encodeURIComponent(p.value) : p.value; + queryParts.push(`${key}=${value}`); + } + }); + + newUrl.search = queryParts.join('&'); + + if (newUrl.toString() !== urlInput) { + isInternalUpdate.current = true; + setUrlInput(newUrl.toString()); + } + } catch (e) { + console.error("Error updating URL from params", e); + } + }; + + const handleParamChange = (id: string, field: 'key' | 'value', newValue: string) => { + const updatedParams = queryParams.map(p => + p.id === id ? { ...p, [field]: newValue } : p + ); + // Optimistically update params state to feel responsive + setQueryParams(updatedParams); + // Then update the full URL + updateUrlFromParams(updatedParams); + }; + + const handleToggleParamEncoding = (id: string) => { + const updatedParams = queryParams.map(p => + p.id === id ? { ...p, encoded: !p.encoded } : p + ); + setQueryParams(updatedParams); + updateUrlFromParams(updatedParams); + }; + + const handleDeleteParam = (id: string) => { + const updatedParams = queryParams.filter(p => p.id !== id); + setQueryParams(updatedParams); + updateUrlFromParams(updatedParams); + }; + + const handleAddParam = () => { + const newParam: QueryParam = { + id: Math.random().toString(36).substr(2, 9), + key: "new_key", + value: "value", + active: true, + encoded: true + }; + const updatedParams = [...queryParams, newParam]; + setQueryParams(updatedParams); + updateUrlFromParams(updatedParams); + }; + + const updateUrlPart = (part: 'protocol' | 'hostname' | 'pathname' | 'hash', value: string) => { + if (!parsedUrl) return; + try { + const newUrl = new URL(parsedUrl.toString()); + if (part === 'protocol') { + // Protocol needs the colon usually + const protocol = value.endsWith(':') ? value : value + ':'; + // prevent invalid protocol assignment crash if possible, but URL object throws + newUrl.protocol = protocol; + } else if (part === 'hostname') { + newUrl.hostname = value; + } else if (part === 'pathname') { + newUrl.pathname = value.startsWith('/') ? value : '/' + value; + } else if (part === 'hash') { + newUrl.hash = value; + } + if (newUrl.toString() !== urlInput) { + isInternalUpdate.current = true; + setUrlInput(newUrl.toString()); + } + } catch (e) { + // invalid part update, ignore or show hint + } + }; + + return ( +
+
+
+
+
+
+ + {/* Title */} +
+

URL Parser & Query Editor

+
+ + {/* Main URL Input */} +
+ +
+