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..a1206fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,44 @@ +# [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/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/crontabExplainer.tsx b/app/components/developmentToolsComponent/crontabExplainer.tsx new file mode 100644 index 0000000..073173e --- /dev/null +++ b/app/components/developmentToolsComponent/crontabExplainer.tsx @@ -0,0 +1,297 @@ +"use client"; +import React, { useMemo, useState } from "react"; +import { + CRON_ALIASES, + CRON_FIELD_SPECS, + CronFieldType, + humanizeCronExpression, + isValidCronExpression, + isValidField, + parseFieldPartSpec, + resolveCronAlias, +} from "../../libs/cron"; + +const Cmd = ({ children }: { children: string }) => ( + {children} +); + +// Calculate next N executions +const getNextExecutions = (cronExpr: string, count: number = 5): Date[] => { + try { + const expr = resolveCronAlias(cronExpr); + + const parts = expr.split(/\s+/); + if (!isValidCronExpression(parts)) return []; + + const [minuteField, hourField, domField, monthField, dowField] = parts; + + const now = new Date(); + const executions: Date[] = []; + + const minuteValues = getMatchingValues(minuteField, "minute"); + const hourValues = getMatchingValues(hourField, "hour"); + if (minuteValues.length === 0 || hourValues.length === 0) return []; + + const hasDomConstraint = domField !== "*"; + const hasDowConstraint = dowField !== "*"; + + // 30 years covers sparse valid schedules (e.g. leap-day based) for at least 5 occurrences. + const maxLookaheadDays = 366 * 30; + const dayCursor = new Date(now); + dayCursor.setHours(0, 0, 0, 0); + + for (let dayOffset = 0; dayOffset <= maxLookaheadDays && executions.length < count; dayOffset++) { + if (dayOffset > 0) { + dayCursor.setDate(dayCursor.getDate() + 1); + } + + const dom = dayCursor.getDate(); + const month = dayCursor.getMonth() + 1; + const dow = dayCursor.getDay(); + + const monthMatch = matchField(month, monthField, "month"); + if (!monthMatch) continue; + + const domMatch = matchField(dom, domField, "dom"); + const dowMatch = matchField(dow, dowField, "dow"); + const dayMatch = + hasDomConstraint && hasDowConstraint ? domMatch || dowMatch : domMatch && dowMatch; + + if (!dayMatch) continue; + + for (const hour of hourValues) { + for (const minute of minuteValues) { + const candidate = new Date( + dayCursor.getFullYear(), + dayCursor.getMonth(), + dayCursor.getDate(), + hour, + minute, + 0, + 0 + ); + + if (candidate <= now) continue; + + executions.push(candidate); + if (executions.length >= count) break; + } + if (executions.length >= count) break; + } + } + + return executions; + } catch { + return []; + } +}; + +const formatExecution = (date: Date, timezone: "local" | "utc"): string => { + if (timezone === "utc") { + return date.toUTCString(); + } + return date.toLocaleString(); +}; + +const getMatchingValues = (field: string, type: CronFieldType): number[] => { + const { min, max } = CRON_FIELD_SPECS[type]; + const values: number[] = []; + for (let value = min; value <= max; value++) { + if (matchField(value, field, type)) { + values.push(value); + } + } + return values; +}; + +const matchField = (value: number, field: string, type: CronFieldType): boolean => { + if (!isValidField(field, type)) return false; + return field.split(",").some((part) => matchFieldPart(value, part.trim(), type)); +}; + +const matchFieldPart = (value: number, part: string, type: CronFieldType): boolean => { + const spec = parseFieldPartSpec(part, type); + if (!spec) return false; + if (value < spec.start || value > spec.end) return false; + return (value - spec.start) % spec.step === 0; +}; + +const CrontabExplainer = () => { + const [cronInput, setCronInput] = useState(""); + const [showExecutions, setShowExecutions] = useState(true); + const [previewTimezone, setPreviewTimezone] = useState<"local" | "utc">("local"); + + const explanation = useMemo(() => { + if (!cronInput.trim()) return ""; + return humanizeCronExpression(cronInput); + }, [cronInput]); + + const nextExecutions = useMemo(() => { + if (!cronInput.trim() || !showExecutions) return []; + return getNextExecutions(cronInput, 5); + }, [cronInput, showExecutions]); + + const isValid = useMemo(() => { + if (!cronInput.trim()) return true; + const trimmed = cronInput.trim().toLowerCase(); + if (CRON_ALIASES[trimmed]) return true; + const parts = cronInput.trim().split(/\s+/); + return isValidCronExpression(parts); + }, [cronInput]); + + const examples = [ + { label: "Every 5 minutes", cron: "*/5 * * * *" }, + { label: "Daily at 2:30 AM", cron: "30 2 * * *" }, + { label: "Weekdays at 9 AM", cron: "0 9 * * 1-5" }, + { label: "First day of month", cron: "0 0 1 * *" }, + { label: "@hourly", cron: "@hourly" }, + { label: "@daily", cron: "@daily" }, + ]; + + const handleCopy = async () => { + if (!explanation) return; + try { + await navigator.clipboard.writeText(explanation); + } catch {} + }; + + return ( +
+
+
+
+ {/* Input Section */} +
+
+ +
+ Format: minute hour day month weekday +
+
+ setCronInput(e.target.value)} + className="w-full px-4 py-3 bg-black/40 border border-white/10 rounded text-lg font-mono" + placeholder="e.g., */5 * * * * or @hourly" + /> + {!isValid && ( +
+ Invalid format. Please enter a 5-field cron expression or use an alias like @hourly, @daily, @weekly, @monthly, @yearly +
+ )} +
+ + {/* Examples */} +
+ Examples: + {examples.map((ex) => ( + + ))} +
+ + {/* Explanation Output */} + {explanation && ( +
+
+
Human-Readable Explanation
+ +
+
+ {explanation} +
+
+ )} + + {/* Next Executions */} + {cronInput.trim() && isValid && ( +
+
+
Next 5 Executions
+ +
+
+ Time context: + +
+ {showExecutions ? ( + nextExecutions.length > 0 ? ( +
    + {nextExecutions.map((exec, idx) => ( +
  • + {idx + 1}. {formatExecution(exec, previewTimezone)} +
  • + ))} +
+ ) : ( +
+ No upcoming executions found within the current preview horizon. +
+ ) + ) : ( +
+ Preview is disabled. +
+ )} +
+ )} + + {/* Help Section */} +
+
+

Special Characters

+
    +
  • * - Any value (every)
  • +
  • , - List of values (e.g., 1,15,30)
  • +
  • - - Range of values (e.g., 9-17)
  • +
  • / - Step values (e.g., */5)
  • +
+
+
+

Supported Aliases

+
    +
  • @yearly or @annually - Once a year (Jan 1, 00:00)
  • +
  • @monthly - Once a month (1st, 00:00)
  • +
  • @weekly - Once a week (Sunday, 00:00)
  • +
  • @daily or @midnight - Once a day (00:00)
  • +
  • @hourly - Once an hour (minute 0)
  • +
+
+
+
+
+
+
+ ); +}; + +export default CrontabExplainer; diff --git a/app/components/developmentToolsComponent/crontabGenerator.tsx b/app/components/developmentToolsComponent/crontabGenerator.tsx index 3e37b26..66a7592 100644 --- a/app/components/developmentToolsComponent/crontabGenerator.tsx +++ b/app/components/developmentToolsComponent/crontabGenerator.tsx @@ -1,5 +1,6 @@ "use client"; import React, { useMemo, useState } from "react"; +import { humanizeCronFields } from "../../libs/cron"; type Field = "minute" | "hour" | "dom" | "month" | "dow"; @@ -56,16 +57,6 @@ const fieldToCron = (f: FieldState): string => { } }; -const humanize = (m: FieldState, h: FieldState, dom: FieldState, mo: FieldState, dow: FieldState): string => { - const map = (f: FieldState, name: string) => { - if (f.mode === "every") return `every ${name}`; - if (f.mode === "specific") return `${name} ${f.values}`; - if (f.mode === "range") return `${name} ${f.start}-${f.endOrStep}`; - return `${name} every ${f.endOrStep || 1} starting at ${f.start || 0}`; - }; - return `Runs at ${map(m, "minute")}, ${map(h, "hour")}, ${map(dom, "day")}, ${map(mo, "month")}, ${map(dow, "weekday")}`; -}; - const Presets: { label: string; cron: string }[] = [ { label: "Every minute", cron: "* * * * *" }, { label: "Every 5 minutes", cron: "*/5 * * * *" }, @@ -214,7 +205,7 @@ const CrontabGenerator = () => { return [fieldToCron(minute), fieldToCron(hour), fieldToCron(dom), fieldToCron(month), fieldToCron(dow)].join(" "); }, [minute, hour, dom, month, dow]); - const description = useMemo(() => humanize(minute, hour, dom, month, dow), [minute, hour, dom, month, dow]); + const description = useMemo(() => humanizeCronFields(minute, hour, dom, month, dow), [minute, hour, dom, month, dow]); const applyPreset = (expr: string) => { const [m, h, d, mo, dw] = expr.split(" "); 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/wordCounterComponent.tsx b/app/components/developmentToolsComponent/wordCounterComponent.tsx index 9f5b2e5..d6c3c78 100644 --- a/app/components/developmentToolsComponent/wordCounterComponent.tsx +++ b/app/components/developmentToolsComponent/wordCounterComponent.tsx @@ -1,6 +1,7 @@ "use client"; import React, { useState } from "react"; import DevelopmentToolsStyles from "../../developmentToolsStyles.module.scss"; +import CopyButton from "../ui/CopyButton"; const WordCounterComponent = () => { const [text, setText] = useState(""); @@ -58,6 +59,13 @@ const WordCounterComponent = () => { > Clear Text + + + diff --git a/app/components/ui/CopyButton.tsx b/app/components/ui/CopyButton.tsx new file mode 100644 index 0000000..e176ba0 --- /dev/null +++ b/app/components/ui/CopyButton.tsx @@ -0,0 +1,58 @@ +"use client"; + +import React, { useState } from "react"; +import { Copy, Check } from "@phosphor-icons/react"; +import { toast } from "react-toastify"; + +interface CopyButtonProps { + text: string; + variant?: "icon" | "text"; + className?: string; +} + +const CopyButton: React.FC = ({ + text, + variant = "icon", + className = "", +}) => { + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + if (!text) return; + + try { + await navigator.clipboard.writeText(text); + setCopied(true); + toast.success("Copied!"); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + toast.error("Failed to copy"); + } + }; + + return ( + + ); +}; + +export default CopyButton; \ No newline at end of file diff --git a/app/libs/constants.tsx b/app/libs/constants.tsx index c51e71e..ef6f8f2 100644 --- a/app/libs/constants.tsx +++ b/app/libs/constants.tsx @@ -70,6 +70,7 @@ import CmykToRgbConverter from '../components/developmentToolsComponent/cmykToRg import ColorInvertor from '../components/developmentToolsComponent/colorInvertor'; import ColorPickerTool from '../components/developmentToolsComponent/colorPickerTool'; import CrontabGenerator from '../components/developmentToolsComponent/crontabGenerator'; +import CrontabExplainer from '../components/developmentToolsComponent/crontabExplainer'; import CssMinify from '../components/developmentToolsComponent/cssMinify'; import CssPrettify from '../components/developmentToolsComponent/cssPrettify'; import CssToLess from '../components/developmentToolsComponent/cssToLess'; @@ -109,6 +110,7 @@ import IdnDecode from '../components/developmentToolsComponent/idnDecode'; import IdnEncode from '../components/developmentToolsComponent/idnEncode'; import InternetSpeedTest from '../components/developmentToolsComponent/internetSpeedTest'; import IpToHexConverter from '../components/developmentToolsComponent/ipToHexConverter'; +import Ipv4SubnetCalculator from '../components/developmentToolsComponent/ipv4SubnetCalculator'; import JavaScriptEscape from '../components/developmentToolsComponent/javascriptEscape'; import JavascriptRegexTester from '../components/developmentToolsComponent/javascriptRegexTester'; import JavaScriptTester from '../components/developmentToolsComponent/javascriptTester'; @@ -116,6 +118,7 @@ import JavaScriptValidatorLinter from '../components/developmentToolsComponent/j import JSONCompare from '../components/developmentToolsComponent/jsonCompare'; import JsonToTypeScript from '../components/developmentToolsComponent/jsonToTypeScript'; import JsonToXmlConverter from '../components/developmentToolsComponent/jsonToXmlConverter'; +import JsonToCsvConverter from '../components/developmentToolsComponent/jsonToCsvConverter'; import JsonToYamlConverter from '../components/developmentToolsComponent/jsonToYamlConverter'; import JwtDecoder from '../components/developmentToolsComponent/jwtDecoder'; import KmToMilesConverter from '../components/developmentToolsComponent/kmToMilesConverter'; @@ -151,6 +154,7 @@ import SqlFormatterAndBeautifier from '../components/developmentToolsComponent/s import SqlMinify from '../components/developmentToolsComponent/sqlMinify'; import SqlToCsvConverter from '../components/developmentToolsComponent/sqlToCsvConverter'; import SqlToJson from '../components/developmentToolsComponent/sqlToJson'; +import SvgConverter from '../components/developmentToolsComponent/svgConverter'; import StringDiffrenceChecker from '../components/developmentToolsComponent/stringDiffrenceChecker'; import StripHTML from '../components/developmentToolsComponent/stripHTML'; import TextCompare from '../components/developmentToolsComponent/textCompare'; @@ -183,6 +187,7 @@ import XmlMinify from '../components/developmentToolsComponent/xmlMinify'; import XmlPrettify from '../components/developmentToolsComponent/xmlPrettify'; import XmlToJsonConverter from '../components/developmentToolsComponent/xmlToJsonConverter'; import XorCalculator from '../components/developmentToolsComponent/xorCalculator'; +import CurlToCodeConverter from '../components/developmentToolsComponent/curlToCodeConverter'; import YAMLFormatterAndBeautifier from '../components/developmentToolsComponent/yamlFormatterAndBeautifier'; export const WEB_URL = 'https://www.betterbugs.io'; @@ -1109,6 +1114,14 @@ export const developmentToolsCategoryContent: any = { 'Convert CSV to JSON; set delimiter, header row, and basic type inference.', }, ], + Category109_1: [ + { + url: '/json-to-csv', + title: 'JSON to CSV Converter', + description: + 'Convert JSON arrays to CSV format with support for nested objects, custom delimiters, and headers.', + }, + ], Category110: [ { url: '/markdown-formatter', @@ -1539,6 +1552,11 @@ export const developmentToolsCategoryContent: any = { title: 'Crontab Generator', description: 'Generate Crontab.', }, + { + url: '/crontab-explainer', + title: 'Crontab Explainer', + description: 'Explain Crontab expressions in plain English.', + }, ], Category169: [ { @@ -1589,6 +1607,21 @@ export const developmentToolsCategoryContent: any = { description: 'Convert HTML to Jade.', }, ], + Category176: [ + { + url: '/ipv4-subnet-calculator', + title: 'IPv4 Subnet Calculator', + description: 'Calculate subnet details like network address, broadcast address, and usable host range.', + }, + ], + Category177: [ + { + url: '/svg-converter', + title: 'SVG to React/CSS Utility', + description: + 'Convert SVG to optimized React components, CSS Data URIs, or CSS Masks.', + }, + ], }; export const PATHS = { @@ -1630,6 +1663,7 @@ export const PATHS = { CSV_TO_TEXT_CONVERTER: '/csv-to-text-converter', TXT_TO_CSV_CONVERTER: '/txt-to-csv-converter', JSON_TO_TEXT: '/json-to-text', + JSON_TO_CSV_CONVERTOR: '/json-to-csv', HTML_VALIDATOR: '/html-validator', JSON_VALIDATOR: '/json-validator', CODE_COMPARE_TOOL: '/code-compare-tool', @@ -1732,6 +1766,7 @@ export const PATHS = { KILOMETERS_TO_MILES: '/kilometers-to-miles', JWT_DECODER: '/jwt-decoder', IP_TO_HEX: '/ip-to-hex', + IPV4_SUBNET_CALCULATOR: '/ipv4-subnet-calculator', WORDS_TO_NUMBERS: '/words-to-numbers', NUMBERS_TO_WORDS: '/numbers-to-words', FABONACCI_CALCULATOR: '/fabonacci-calculator', @@ -1745,6 +1780,7 @@ export const PATHS = { HTML_UNESCAPE: '/html-unescape', JAVASCRIPT_REGEX_TESTER: '/javascript-regex-tester', STRIP_HTML: '/strip-html', + SVG_CONVERTER: '/svg-converter', WHAT_IS_MY_LOCAL_IP_ADDRESS: '/what-is-my-local-ip-address', JAVASCRIPT_TESTER: '/javascript-tester', WHAT_VERSION_OF_JAVA: '/what-version-of-java-do-i-have', @@ -1766,7 +1802,9 @@ export const PATHS = { CSS_TO_SASS: '/css-to-sass', CSS_TO_LESS: '/css-to-less', CRONTAB_GENERATOR: '/crontab-generator', + CRONTAB_EXPLAINER: '/crontab-explainer', MORSE_CODE_TRANSLATOR: '/morse-code-translator', + CURL_TO_CODE_CONVERTER: '/curl-to-code-converter', }; export const developmentToolsRoutes = [ @@ -2249,6 +2287,10 @@ export const developmentToolsRoutes = [ { path: PATHS.JSON_TO_YAML_CONVERTER, component: , + }, + { + path: PATHS.JSON_TO_CSV_CONVERTOR, + component: , }, { path: PATHS.UTF8_DECODE, @@ -2306,6 +2348,10 @@ export const developmentToolsRoutes = [ path: PATHS.IP_TO_HEX, component: , }, + { + path: PATHS.IPV4_SUBNET_CALCULATOR, + component: , + }, { path: PATHS.WORDS_TO_NUMBERS, component: , @@ -2442,6 +2488,10 @@ export const developmentToolsRoutes = [ path: PATHS.CRONTAB_GENERATOR, component: , }, + { + path: PATHS.CRONTAB_EXPLAINER, + component: , + }, { path: PATHS.MORSE_CODE_TRANSLATOR, component: , @@ -2462,10 +2512,18 @@ export const developmentToolsRoutes = [ path: PATHS.SQL_TO_JSON, component: , }, + { + path: PATHS.SVG_CONVERTER, + component: , + }, { path: PATHS.HTML_TO_JADE, component: , }, + { + path: PATHS.CURL_TO_CODE_CONVERTER, + component: , + }, ]; // lorem ipsum text diff --git a/app/libs/cron.ts b/app/libs/cron.ts new file mode 100644 index 0000000..441ee7a --- /dev/null +++ b/app/libs/cron.ts @@ -0,0 +1,261 @@ +export const CRON_ALIASES: Record = { + "@yearly": "0 0 1 1 *", + "@annually": "0 0 1 1 *", + "@monthly": "0 0 1 * *", + "@weekly": "0 0 * * 0", + "@daily": "0 0 * * *", + "@midnight": "0 0 * * *", + "@hourly": "0 * * * *", +}; + +const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; +const DOW = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + +export type CronFieldType = "minute" | "hour" | "dom" | "month" | "dow"; + +export const CRON_FIELD_SPECS: Record }> = { + minute: { min: 0, max: 59 }, + hour: { min: 0, max: 23 }, + dom: { min: 1, max: 31 }, + month: { + min: 1, + max: 12, + names: { + jan: 1, feb: 2, mar: 3, apr: 4, may: 5, jun: 6, + jul: 7, aug: 8, sep: 9, oct: 10, nov: 11, dec: 12, + }, + }, + dow: { + min: 0, + max: 6, + names: { sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6 }, + }, +}; + +export type FieldPartSpec = { + start: number; + end: number; + step: number; +}; + +export type CronHumanizeFieldState = { + mode: "every" | "specific" | "range" | "interval"; + values: string; + start?: string; + endOrStep?: string; +}; + +export const resolveCronAlias = (cronExpr: string): string => { + const trimmed = cronExpr.trim().toLowerCase(); + return CRON_ALIASES[trimmed] || cronExpr.trim(); +}; + +const formatValue = (value: string, type: CronFieldType): string => { + const num = parseInt(value, 10); + if (Number.isNaN(num)) { + if (type === "month") { + const monthIndex = MONTHS.findIndex((m) => m.toLowerCase() === value.toLowerCase()); + return monthIndex >= 0 ? MONTHS[monthIndex] : value; + } + if (type === "dow") { + const dowIndex = DOW.findIndex((d) => d.toLowerCase() === value.toLowerCase()); + return dowIndex >= 0 ? DOW[dowIndex] : value; + } + return value; + } + + if (type === "month" && num >= 1 && num <= 12) return MONTHS[num - 1]; + if (type === "dow" && num >= 0 && num <= 6) return DOW[num]; + if (type === "hour") return `${num.toString().padStart(2, "0")}:00`; + if (type === "minute") return `minute ${num}`; + return value; +}; + +const parseField = (field: string, type: CronFieldType): string => { + if (field === "*") { + if (type === "minute") return "every minute"; + if (type === "hour") return "every hour"; + if (type === "dom") return "every day"; + if (type === "month") return "every month"; + return "every day of week"; + } + + if (field.includes("/")) { + const [base, step] = field.split("/"); + const stepNum = parseInt(step, 10); + if (base === "*") { + if (type === "minute") return `every ${stepNum} minute${stepNum > 1 ? "s" : ""}`; + if (type === "hour") return `every ${stepNum} hour${stepNum > 1 ? "s" : ""}`; + if (type === "dom") return `every ${stepNum} day${stepNum > 1 ? "s" : ""}`; + if (type === "month") return `every ${stepNum} month${stepNum > 1 ? "s" : ""}`; + return `every ${stepNum} day${stepNum > 1 ? "s" : ""} of week`; + } + return `every ${stepNum} starting from ${base}`; + } + + if (field.includes("-") && !field.includes(",")) { + const [start, end] = field.split("-"); + return `from ${formatValue(start, type)} to ${formatValue(end, type)}`; + } + + if (field.includes(",")) { + const values = field.split(",").map((v) => formatValue(v.trim(), type)); + if (values.length > 3) { + return `at ${values.slice(0, 3).join(", ")}, and ${values.length - 3} more`; + } + return `at ${values.join(", ")}`; + } + + return `at ${formatValue(field, type)}`; +}; + +export const parseFieldValue = (raw: string, type: CronFieldType): number | null => { + const token = raw.trim().toLowerCase(); + const spec = CRON_FIELD_SPECS[type]; + + if (spec.names && token in spec.names) return spec.names[token]; + if (!/^\d+$/.test(token)) return null; + + const value = parseInt(token, 10); + if (value < spec.min || value > spec.max) return null; + return value; +}; + +export const parseFieldPartSpec = (part: string, type: CronFieldType): FieldPartSpec | null => { + const [base, step] = part.split("/"); + if (!base || part.split("/").length > 2) return null; + + let stepValue = 1; + if (step !== undefined) { + if (!/^\d+$/.test(step)) return null; + stepValue = parseInt(step, 10); + if (stepValue <= 0) return null; + } + + const spec = CRON_FIELD_SPECS[type]; + if (base === "*") return { start: spec.min, end: spec.max, step: stepValue }; + + if (base.includes("-")) { + const [startRaw, endRaw] = base.split("-"); + if (!startRaw || !endRaw || base.split("-").length > 2) return null; + const start = parseFieldValue(startRaw, type); + const end = parseFieldValue(endRaw, type); + if (start === null || end === null || start > end) return null; + return { start, end, step: stepValue }; + } + + const start = parseFieldValue(base, type); + if (start === null) return null; + return { start, end: spec.max, step: stepValue }; +}; + +export const isValidField = (field: string, type: CronFieldType): boolean => { + if (!field || field.trim() === "") return false; + const parts = field.split(","); + if (parts.some((part) => part.trim() === "")) return false; + return parts.every((part) => parseFieldPartSpec(part.trim(), type) !== null); +}; + +export const isValidCronExpression = (parts: string[]): boolean => { + if (parts.length !== 5) return false; + const [minute, hour, dom, month, dow] = parts; + return ( + isValidField(minute, "minute") && + isValidField(hour, "hour") && + isValidField(dom, "dom") && + isValidField(month, "month") && + isValidField(dow, "dow") + ); +}; + +export const explainCronExpression = (cronExpr: string): string => { + const trimmed = cronExpr.trim().toLowerCase(); + if (CRON_ALIASES[trimmed]) { + return `Runs ${trimmed.substring(1)} (${CRON_ALIASES[trimmed]})`; + } + + const parts = cronExpr.trim().split(/\s+/); + if (!isValidCronExpression(parts)) { + return "Invalid cron expression. Expected 5 fields: minute hour day month weekday"; + } + + const [minute, hour, dom, month, dow] = parts; + const minuteText = parseField(minute, "minute"); + const hourText = parseField(hour, "hour"); + const domText = parseField(dom, "dom"); + const monthText = parseField(month, "month"); + const dowText = parseField(dow, "dow"); + + let explanation = "Runs "; + + if (minute === "*" && hour === "*") { + explanation += "every minute"; + } else if (minute !== "*" && hour === "*") { + explanation += `${minuteText} of every hour`; + } else if (minute === "*" && hour !== "*") { + explanation += `every minute ${hourText}`; + } else { + explanation += `at ${hour.padStart(2, "0")}:${minute.padStart(2, "0")}`; + } + + const hasDom = dom !== "*"; + const hasDow = dow !== "*"; + + if (hasDom && hasDow) explanation += `, ${domText} of the month OR ${dowText}`; + else if (hasDom) explanation += `, ${domText} of the month`; + else if (hasDow) explanation += `, ${dowText}`; + + if (month !== "*") explanation += `, ${monthText}`; + return explanation; +}; + +export const humanizeCronFields = ( + m: CronHumanizeFieldState, + h: CronHumanizeFieldState, + dom: CronHumanizeFieldState, + mo: CronHumanizeFieldState, + dow: CronHumanizeFieldState +): string => { + const map = (f: CronHumanizeFieldState, name: string) => { + if (f.mode === "every") return `every ${name}`; + if (f.mode === "specific") return `${name} ${f.values}`; + if (f.mode === "range") return `${name} ${f.start}-${f.endOrStep}`; + return `${name} every ${f.endOrStep || 1} starting at ${f.start || 0}`; + }; + return `Runs at ${map(m, "minute")}, ${map(h, "hour")}, ${map(dom, "day")}, ${map(mo, "month")}, ${map(dow, "weekday")}`; +}; + +const tokenToHumanizeField = (token: string): CronHumanizeFieldState => { + if (token === "*") return { mode: "every", values: "", start: "", endOrStep: "" }; + if (token.includes("-")) { + const [start, endOrStep] = token.split("-"); + return { mode: "range", values: "", start, endOrStep }; + } + if (token.includes("/")) { + const [start, endOrStep] = token.split("/"); + return { mode: "interval", values: "", start: start === "*" ? "" : start, endOrStep }; + } + return { mode: "specific", values: token, start: "", endOrStep: "" }; +}; + +export const humanizeCronExpression = (cronExpr: string): string => { + const trimmed = cronExpr.trim().toLowerCase(); + if (CRON_ALIASES[trimmed]) { + const aliasName = trimmed.substring(1); + return `Runs ${aliasName} (${CRON_ALIASES[trimmed]})`; + } + + const parts = cronExpr.trim().split(/\s+/); + if (!isValidCronExpression(parts)) { + return "Invalid cron expression. Expected 5 fields: minute hour day month weekday"; + } + + const [m, h, dom, mo, dow] = parts; + return humanizeCronFields( + tokenToHumanizeField(m), + tokenToHumanizeField(h), + tokenToHumanizeField(dom), + tokenToHumanizeField(mo), + tokenToHumanizeField(dow) + ); +}; diff --git a/app/libs/developmentToolsConstant.tsx b/app/libs/developmentToolsConstant.tsx index aea9220..fe3dfae 100644 --- a/app/libs/developmentToolsConstant.tsx +++ b/app/libs/developmentToolsConstant.tsx @@ -12948,6 +12948,105 @@ family[1]: "Beth"`, og_image: '/images/og-images/Cover.png', }, }, + [`json-to-csv`]: { + hero_section: { + title: 'JSON to CSV Converter', + description: + 'Convert JSON data into CSV format instantly – perfect for exporting data to spreadsheets, business reports, and data analysis tools.', + }, + development_tools_list: [ + { tool: 'CSV to JSON', url: PATHS.CSV_TO_JSON }, + { tool: 'JSON to XML Converter', url: PATHS.JSON_TO_XML_CONVERTER }, + { tool: 'JSON to YAML Converter', url: PATHS.JSON_TO_YAML_CONVERTER }, + { tool: 'JSON Prettifier', url: PATHS.JSON_PRETTIFIER }, + { tool: 'JSON Minifier', url: PATHS.JSON_MINIFIER }, + { tool: 'Text to CSV', url: PATHS.TEXT_TO_CSV }, + { tool: 'CSV to Text Converter', url: PATHS.CSV_TO_TEXT_CONVERTER }, + ], + development_tools_about_details: { + about_title: 'What is the JSON to CSV Converter?', + about_description: [ + { + description: + 'The JSON to CSV Converter transforms structured JSON arrays into CSV (Comma-Separated Values) format, making it easy to export data for use in spreadsheet applications like Excel, Google Sheets, or data analysis tools.', + }, + { + description: + 'It\'s ideal for developers, data analysts, and business professionals who need to convert API responses, database exports, or JSON files into a format that\'s compatible with spreadsheet software and reporting tools.', + }, + ], + }, + development_tools_steps_guide: { + guide_title: 'How to Use the JSON to CSV Converter', + guide_description: 'Follow these simple steps:', + steps: [ + { + step_key: 'Step 1:', + step_title: 'Paste JSON data:', + step_description: + 'Enter or paste the JSON array you want to convert into the input box, or upload a JSON file.', + }, + { + step_key: 'Step 2:', + step_title: 'Configure options:', + step_description: + 'Choose your delimiter (comma, semicolon, or tab), decide whether to include headers, and enable nested object flattening if needed.', + }, + { + step_key: 'Step 3:', + step_title: 'Convert to CSV:', + step_description: + 'Click the convert button to transform the JSON into properly formatted CSV.', + }, + { + step_key: 'Step 4:', + step_title: 'Copy or download:', + step_description: + 'Copy the CSV output or download it as a .csv file for use in Excel, Google Sheets, or other applications.', + }, + ], + }, + development_tools_how_use: { + how_use_title: 'Common Uses', + how_use_description: 'Why you might use this tool:', + point: [ + { + title: 'Export API data', + description: + 'Convert JSON responses from APIs into CSV format for analysis in spreadsheet applications.', + }, + { + title: 'Business reports', + description: + 'Transform JSON data into CSV for creating business reports, dashboards, and presentations.', + }, + { + title: 'Data migration', + description: + 'Export JSON data from databases or applications into CSV format for importing into other systems.', + }, + { + title: 'Data analysis', + description: + 'Convert JSON datasets into CSV for statistical analysis, data visualization, or machine learning workflows.', + }, + { + title: 'Nested object handling', + description: + 'Flatten complex nested JSON structures into a tabular CSV format with dot notation (e.g., user.name).', + }, + ], + }, + meta_data: { + meta_title: 'JSON to CSV Converter – Convert JSON Data to CSV Online', + meta_description: + 'Convert JSON data into CSV format online for free. Perfect for exporting data to Excel, Google Sheets, business reports, and data analysis. Supports nested objects and custom delimiters.', + og_title: 'JSON to CSV Converter – Free Online Tool', + og_description: + 'Easily transform JSON into CSV format with support for nested objects, custom delimiters, and large files up to 5MB. Ideal for developers and data analysts.', + og_image: '/images/og-images/Cover.png', + }, + }, [`utf8-decode`]: { hero_section: { title: 'UTF8 Decode', @@ -14195,6 +14294,109 @@ family[1]: "Beth"`, og_image: '/images/og-images/Cover.png', }, }, + [`ipv4-subnet-calculator`]: { + hero_section: { + title: 'IPv4 Subnet Calculator', + description: + 'Calculate subnet details like network address, broadcast address, usable host range, and CIDR masks instantly – perfect for network engineers and DevOps professionals.', + }, + development_tools_list: [ + { tool: 'Random IP Generator', url: PATHS.RANDOM_IP_GENERATOR }, + { tool: 'IP to Hex Converter', url: PATHS.IP_TO_HEX }, + { tool: 'What Is My Local IP', url: PATHS.WHAT_IS_MY_LOCAL_IP_ADDRESS }, + { tool: 'Bitwise Calculator', url: PATHS.BITWISE_CALCULATOR }, + { tool: 'Decimal to Binary', url: PATHS.DECIMAL_TO_BINARY_CONVERTER }, + { tool: 'Binary to Decimal', url: PATHS.BINARY_TO_DECIMAL_CONVERTER }, + ], + development_tools_about_details: { + about_title: 'What is the IPv4 Subnet Calculator?', + about_description: [ + { + description: + 'The IPv4 Subnet Calculator is a specialist tool for network engineers and DevOps professionals that instantly calculates subnet details from an IP address and CIDR prefix or subnet mask.', + }, + { + description: + 'It provides network address, broadcast address, usable host ranges, total and usable host counts, subnet masks, and wildcard masks with binary visualization for deep network planning and infrastructure setup.', + }, + ], + }, + development_tools_steps_guide: { + guide_title: 'How to Use the IPv4 Subnet Calculator', + guide_description: 'Follow these simple steps:', + steps: [ + { + step_key: 'Step 1:', + step_title: 'Enter IP address:', + step_description: + 'Type or paste the IPv4 address (e.g., 192.168.1.100) into the IP address field.', + }, + { + step_key: 'Step 2:', + step_title: 'Specify CIDR or subnet mask:', + step_description: + 'Enter the CIDR prefix (0-32) or subnet mask. Use the "Switch" button to toggle between CIDR and subnet mask input modes.', + }, + { + step_key: 'Step 3:', + step_title: 'View subnet calculations:', + step_description: + 'The calculator instantly displays network address, broadcast address, usable host range, total hosts, subnet mask, and wildcard mask with binary representations.', + }, + { + step_key: 'Step 4:', + step_title: 'Use for planning:', + step_description: + 'Use the results for network planning, infrastructure setup, VLAN configuration, and routing table management.', + }, + ], + }, + development_tools_how_use: { + how_use_title: 'Common Uses', + how_use_description: 'Why you might use this tool:', + point: [ + { + title: 'Network Planning', + description: + 'Simplify network planning and infrastructure setup tasks for cloud deployments, on-premises networks, and hybrid environments.', + }, + { + title: 'Subnet Management', + description: + 'Calculate subnet details like host ranges, broadcast addresses, and CIDR masks for efficient subnet allocation and management.', + }, + { + title: 'VLAN Configuration', + description: + 'Determine correct subnet masks and network addresses when configuring VLANs and routing in enterprise networks.', + }, + { + title: 'Network Troubleshooting', + description: + 'Verify IP address assignments, subnet ranges, and broadcast domains when diagnosing network connectivity issues.', + }, + { + title: 'IP Address Planning', + description: + 'Plan IP address allocation strategies for organizations and ensure proper utilization of address space.', + }, + { + title: 'Learning Networking', + description: + 'Understand subnet calculations, binary representations, and networking concepts with visual binary breakdowns.', + }, + ], + }, + meta_data: { + meta_title: 'IPv4 Subnet Calculator – Free Online Network Tool', + meta_description: + 'Calculate subnet details, network addresses, broadcast addresses, and CIDR masks instantly. Perfect for network engineers and DevOps professionals.', + og_title: 'IPv4 Subnet Calculator – Free Online Tool', + og_description: + 'Quickly calculate subnet information including network address, broadcast address, usable hosts, and wildcard masks. Ideal for network planning and infrastructure setup.', + og_image: '/images/og-images/Cover.png', + }, + }, [`words-to-numbers`]: { hero_section: { title: 'Words to Numbers Converter', @@ -15341,6 +15543,95 @@ family[1]: "Beth"`, og_image: '/images/og-images/Cover.png', }, }, + [`svg-converter`]: { + hero_section: { + title: 'SVG to React/CSS Utility', + description: + 'Convert raw SVG code to optimized React components, CSS Data URIs, or CSS Masks for different development needs.', + }, + development_tools_list: [ + { tool: 'Base64 Encoder', url: PATHS.BASE64_ENCODER }, + { tool: 'CSS Minify', url: PATHS.CSS_MINIFY }, + { tool: 'Color Inveror', url: PATHS.COLOR_INVERTOR }, + { tool: 'QR Code Generator', url: PATHS.QR_CODE_GENERATOR }, + ], + development_tools_about_details: { + about_title: 'What is the SVG Converter?', + about_description: [ + { + description: + 'The SVG Converter transforms raw SVG code into optimized variants for different use cases: clean React components, CSS Data URIs, or CSS Masks.', + }, + { + description: + 'Automates SVG cleanup (removing metadata), handles viewBox preservation, and supports dynamic sizing and color theming options.', + }, + ], + }, + development_tools_steps_guide: { + guide_title: 'How to Use', + guide_description: 'Follow these simple steps:', + steps: [ + { + step_key: 'Step 1:', + step_title: 'Paste SVG Code:', + step_description: 'Paste your SVG code or upload an SVG file.', + }, + { + step_key: 'Step 2:', + step_title: 'Choose Output Format:', + step_description: + 'Select React Component, CSS Data URI, or CSS Mask from the dropdown.', + }, + { + step_key: 'Step 3:', + step_title: 'Configure Options:', + step_description: + 'Set default width, height, and choose whether to use currentColor for fills.', + }, + { + step_key: 'Step 4:', + step_title: 'Copy Output:', + step_description: 'Copy the optimized code and use it in your project.', + }, + ], + }, + development_tools_how_use: { + how_use_title: 'Common Use Cases', + how_use_description: 'Popular reasons to use this tool:', + point: [ + { + title: 'React Component Generation', + description: + 'Automatically create reusable React icon components from SVG files with dynamic sizing and theming support.', + }, + { + title: 'CSS Background Images', + description: + 'Generate inline SVG Data URIs for use as CSS background images without external file requests.', + }, + { + title: 'Icon Masking', + description: + 'Convert SVGs to CSS mask properties for flexible icon styling and color customization.', + }, + { + title: 'Metadata Cleanup', + description: + 'Automatically remove unnecessary metadata, comments, and attributes from design tool exports.', + }, + ], + }, + meta_data: { + meta_title: 'SVG to React/CSS Converter – Free Online Tool', + meta_description: + 'Convert SVG to React components, CSS Data URIs, or CSS Masks. Remove metadata and optimize for web development.', + og_title: 'SVG Converter – Optimize SVGs for Development', + og_description: + 'Transform SVG files into React components, CSS URIs, or masks with one click. Supports custom dimensions and color theming.', + og_image: '/images/og-images/Cover.png', + }, + }, [`what-is-my-local-ip-address`]: { hero_section: { title: 'What Is My Local IP Address', @@ -16973,6 +17264,88 @@ family[1]: "Beth"`, og_image: '/images/og-images/Cover.png', }, }, + [`crontab-explainer`]: { + hero_section: { + title: 'Crontab Explainer', + description: + 'Decode cron expressions into plain English and preview upcoming execution times.', + }, + development_tools_list: [ + { tool: 'Crontab Generator', url: PATHS.CRONTAB_GENERATOR }, + { tool: 'Regex Tester', url: PATHS.JAVASCRIPT_REGEX_TESTER }, + ], + development_tools_about_details: { + about_title: 'About Crontab Explainer', + about_description: [ + { + description: + 'The Crontab Explainer takes an existing cron expression and translates it into human-readable language. It supports standard 5-field cron syntax and common aliases like @hourly, @daily, @weekly, @monthly, and @yearly.', + }, + { + description: + 'This tool is perfect for understanding cron expressions you find in existing codebases, verifying that your scheduled jobs run at the expected times, and learning how cron syntax works.', + }, + ], + }, + development_tools_steps_guide: { + guide_title: 'How to Use', + guide_description: 'Explain a cron expression:', + steps: [ + { + step_key: 'Step 1:', + step_title: 'Enter expression', + step_description: + 'Paste or type a 5-field cron expression (e.g., */5 * * * *) or use an alias like @hourly.', + }, + { + step_key: 'Step 2:', + step_title: 'Read explanation', + step_description: + 'View the human-readable description of when the job will run.', + }, + { + step_key: 'Step 3:', + step_title: 'Preview executions', + step_description: + 'Check the next 5 scheduled execution times to verify the schedule.', + }, + ], + }, + development_tools_how_use: { + how_use_title: 'Use Cases', + how_use_description: 'When this tool helps:', + point: [ + { + title: 'Understanding existing cron jobs', + description: 'Decode cron expressions found in legacy code or server configurations.', + }, + { + title: 'Verifying schedules', + description: 'Confirm that a cron expression runs at the expected times before deployment.', + }, + { + title: 'Learning cron syntax', + description: 'Experiment with different expressions to understand how cron scheduling works.', + }, + { + title: 'Debugging scheduled tasks', + description: 'Identify why a job is not running as expected by checking its schedule.', + }, + { + title: 'Documentation', + description: 'Generate plain English descriptions for cron jobs in your documentation.', + }, + ], + }, + meta_data: { + meta_title: 'Crontab Explainer - Decode Cron Expressions', + meta_description: + 'Translate cron expressions into plain English and preview next execution times. Supports standard syntax and aliases like @hourly, @daily, @weekly.', + og_title: 'Crontab Explainer - Developer Utility', + og_description: 'Understand cron expressions with human-readable explanations and execution previews.', + og_image: '/images/og-images/Cover.png', + }, + }, [`morse-code-translator`]: { hero_section: { title: 'Morse Code Translator', @@ -17169,4 +17542,97 @@ family[1]: "Beth"`, og_image: '/images/og-images/Cover.png', }, }, + [`curl-to-code-converter`]: { + hero_section: { + title: 'cURL to Code Converter', + description: + 'Paste any cURL command and instantly convert it to JavaScript (Fetch or Axios), Python Requests, Go, or Node.js code β€” ready to copy and use.', + }, + development_tools_list: [ + { tool: 'JSON to TypeScript', url: PATHS.JSON_TO_TYPESCRIPT }, + { tool: 'Base64 Encoder', url: PATHS.BASE64_ENCODER }, + { tool: 'Base64 Decoder', url: PATHS.BASE64_DECODER }, + { tool: 'JWT Decoder', url: PATHS.JWT_DECODER }, + { tool: 'API Key Generator', url: PATHS.API_KEY_GENERATOR }, + { tool: 'URL Encode', url: PATHS.URL_ENCODE }, + ], + development_tools_about_details: { + about_title: 'What is the cURL to Code Converter?', + about_description: [ + { + description: + 'The cURL to Code Converter is a free online developer tool on BetterBugs.io that translates cURL commands into ready-to-run code snippets for popular programming languages and HTTP libraries.', + }, + { + description: + 'It supports JavaScript Fetch, JavaScript Axios, Python Requests, Go net/http, and Node.js β€” covering the most common HTTP clients used in modern development.', + }, + ], + }, + development_tools_steps_guide: { + guide_title: 'Step-by-Step Guide', + guide_description: 'Converting a cURL command is quick and simple:', + steps: [ + { + step_key: 'Step 1:', + step_title: 'Paste Your cURL Command:', + step_description: + 'Copy a cURL command (e.g., from browser DevTools or API docs) and paste it into the input area on the left.', + }, + { + step_key: 'Step 2:', + step_title: 'Select a Target Language:', + step_description: + 'Choose the programming language or HTTP library you want to convert to from the dropdown (Fetch, Axios, Python, Go, or Node.js).', + }, + { + step_key: 'Step 3:', + step_title: 'Copy the Generated Code:', + step_description: + 'The equivalent code appears on the right instantly. Use the Copy button to grab it and paste it directly into your project.', + }, + ], + }, + development_tools_how_use: { + how_use_title: 'Common Use Cases', + how_use_description: + 'The cURL to Code Converter is useful in many developer workflows:', + point: [ + { + title: 'API Integration', + description: + 'Quickly turn curl examples from API documentation into real code for your language of choice.', + }, + { + title: 'Debugging & Testing', + description: + 'Convert browser-copied curl commands into testable code snippets without manual translation.', + }, + { + title: 'Language Migration', + description: + 'Reuse existing curl-based scripts in a different language when migrating or refactoring a project.', + }, + { + title: 'Learning HTTP Clients', + description: + 'Compare how the same request looks across different HTTP libraries to understand their APIs.', + }, + { + title: 'CI/CD & Automation', + description: + 'Convert one-off curl calls into structured code blocks that can be embedded in automated pipelines.', + }, + ], + }, + meta_data: { + meta_title: 'cURL to Code Converter β€” JavaScript, Python, Go | BetterBugs.io', + meta_description: + 'Convert cURL commands to JavaScript Fetch, Axios, Python Requests, Go, or Node.js code instantly. Free online tool on BetterBugs.io.', + og_title: 'cURL to Code Converter β€” BetterBugs.io', + og_description: + 'Paste a cURL command and get the equivalent JavaScript, Python, Go, or Node.js code in one click.', + og_image: '/images/og-images/Cover.png', + }, + }, }; diff --git a/package-lock.json b/package-lock.json index ec0383b..a9fbe03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,8 @@ "@types/turndown": "5.0.5", "animejs": "3.2.2", "antd": "5.16.2", + "bcryptjs": "^2.4.3", + "curlconverter": "^4.12.0", "framer-motion": "11.2.6", "gleap": "^15.1.8", "javascript-obfuscator": "4.1.1", @@ -44,6 +46,7 @@ "@semantic-release/github": "^9.2.6", "@semantic-release/release-notes-generator": "^12.1.0", "@types/animejs": "^3.1.12", + "@types/bcryptjs": "^2.4.6", "@types/lodash.debounce": "^4.0.9", "@types/node": "^20", "@types/react": "^18", @@ -54,6 +57,10 @@ "semantic-release": "^23.0.8", "tailwindcss": "^3.4.1", "typescript": "^5" + }, + "engines": { + "node": ">=20.8.0", + "npm": ">=10.0.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -3657,6 +3664,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/css-font-loading-module": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz", @@ -4816,6 +4830,12 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, "node_modules/before-after-hook": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", @@ -5676,6 +5696,23 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/curlconverter": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/curlconverter/-/curlconverter-4.12.0.tgz", + "integrity": "sha512-NcwPKJgu9DkCH4gQsnjnXuUtPrhLhoNwvIYTTS5rRrsCC/X2flUswtgmeCyV9ePGszXzFReXk5y/CdBxrsAQ8Q==", + "license": "MIT", + "dependencies": { + "jsesc": "^3.0.2", + "lossless-json": "^4.0.2", + "tree-sitter": "^0.21.1", + "tree-sitter-bash": "^0.23.1", + "web-tree-sitter": "^0.24.3", + "yamljs": "^0.3.0" + }, + "bin": { + "curlconverter": "dist/src/cli.js" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -9161,6 +9198,12 @@ "loose-envify": "cli.js" } }, + "node_modules/lossless-json": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lossless-json/-/lossless-json-4.3.0.tgz", + "integrity": "sha512-ToxOC+SsduRmdSuoLZLYAr5zy1Qu7l5XhmPWM3zefCZ5IcrzW/h108qbJUKfOlDlhvhjUK84+8PSVX0kxnit0g==", + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -9682,6 +9725,15 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-addon-api": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.6.0.tgz", + "integrity": "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/node-emoji": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", @@ -9698,6 +9750,17 @@ "node": ">=18" } }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -15827,6 +15890,12 @@ "node": ">= 10.x" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, "node_modules/state-local": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", @@ -16615,6 +16684,36 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tree-sitter": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.21.1.tgz", + "integrity": "sha512-7dxoA6kYvtgWw80265MyqJlkRl4yawIjO7S5MigytjELkX43fV2WsAXzsNfO7sBpPPCF5Gp0+XzHk0DwLCq3xQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.0.0", + "node-gyp-build": "^4.8.0" + } + }, + "node_modules/tree-sitter-bash": { + "version": "0.23.3", + "resolved": "https://registry.npmjs.org/tree-sitter-bash/-/tree-sitter-bash-0.23.3.tgz", + "integrity": "sha512-36cg/GQ2YmIbeiBeqeuh4fBJ6i4kgVouDaqTxqih5ysPag+zHufyIaxMOFeM8CeplwAK/Luj1o5XHqgdAfoCZg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.2.1", + "node-gyp-build": "^4.8.2" + }, + "peerDependencies": { + "tree-sitter": "^0.21.1" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", @@ -17043,6 +17142,12 @@ "node": ">=10.13.0" } }, + "node_modules/web-tree-sitter": { + "version": "0.24.7", + "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.24.7.tgz", + "integrity": "sha512-CdC/TqVFbXqR+C51v38hv6wOPatKEUGxa39scAeFSm98wIhZxAYonhRQPSMmfZ2w7JDI0zQDdzdmgtNk06/krQ==", + "license": "MIT" + }, "node_modules/web-worker": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.2.0.tgz", @@ -17761,6 +17866,50 @@ "node": ">= 14" } }, + "node_modules/yamljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", + "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "glob": "^7.0.5" + }, + "bin": { + "json2yaml": "bin/json2yaml", + "yaml2json": "bin/yaml2json" + } + }, + "node_modules/yamljs/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/yamljs/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index ffbbfb0..3b92088 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,8 @@ "@types/turndown": "5.0.5", "animejs": "3.2.2", "antd": "5.16.2", + "curlconverter": "^4.12.0", + "bcryptjs": "^2.4.3", "framer-motion": "11.2.6", "gleap": "^15.1.8", "javascript-obfuscator": "4.1.1", @@ -70,6 +72,7 @@ "@semantic-release/github": "^9.2.6", "@semantic-release/release-notes-generator": "^12.1.0", "@types/animejs": "^3.1.12", + "@types/bcryptjs": "^2.4.6", "@types/lodash.debounce": "^4.0.9", "@types/node": "^20", "@types/react": "^18",