From 6ad2d22bcda2c6f59d7c8e73acdc13140530286e Mon Sep 17 00:00:00 2001 From: Sai Thi Han <58659499+SaiThihan@users.noreply.github.com> Date: Sat, 10 Oct 2026 20:12:04 +0630 Subject: [PATCH 01/32] feat(url-encoder): add URL encoder/decoder tool (#137) --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 + app/(tools)/url-encoder/UrlEncoderClient.tsx | 185 +++++++++++++++++++ app/(tools)/url-encoder/opengraph-image.tsx | 12 ++ app/(tools)/url-encoder/page.tsx | 17 ++ lib/__tests__/url-utils.test.ts | 66 +++++++ lib/tools.ts | 52 ++++++ lib/url-utils.ts | 49 +++++ 7 files changed, 382 insertions(+) create mode 100644 app/(tools)/url-encoder/UrlEncoderClient.tsx create mode 100644 app/(tools)/url-encoder/opengraph-image.tsx create mode 100644 app/(tools)/url-encoder/page.tsx create mode 100644 lib/__tests__/url-utils.test.ts create mode 100644 lib/url-utils.ts diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index bf9b8dd..c51984c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -14,6 +14,7 @@ body: - JSON formatter - JWT decoder - Base64 + - URL encoder/decoder - Invoice generator - Shell (theme, command palette, header) - Other / not sure diff --git a/app/(tools)/url-encoder/UrlEncoderClient.tsx b/app/(tools)/url-encoder/UrlEncoderClient.tsx new file mode 100644 index 0000000..8fa66a3 --- /dev/null +++ b/app/(tools)/url-encoder/UrlEncoderClient.tsx @@ -0,0 +1,185 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { AlertCircle, ArrowLeftRight } from "lucide-react"; +import { encodeUrl, decodeUrl, parseUrl, type UrlEncodeMode } from "@/lib/url-utils"; +import { CopyButton } from "@/components/shared/CopyButton"; +import { useRegisterCommands } from "@/components/shared/CommandPalette"; + +type Direction = "encode" | "decode"; + +export function UrlEncoderClient() { + const [direction, setDirection] = useState("encode"); + const [mode, setMode] = useState("component"); + const [plusAsSpace, setPlusAsSpace] = useState(false); + const [input, setInput] = useState(""); + + // Pure, synchronous conversion — derived from input/settings, no effect needed. + const { output, error } = useMemo((): { output: string; error: string | null } => { + if (!input) return { output: "", error: null }; + if (direction === "encode") { + return { output: encodeUrl(input, mode), error: null }; + } + const r = decodeUrl(input, { plusAsSpace }); + return r.ok ? { output: r.value, error: null } : { output: "", error: r.error }; + }, [input, direction, mode, plusAsSpace]); + + // When either side looks like an absolute URL, show its breakdown. + const parsed = useMemo(() => { + for (const candidate of [input, output]) { + if (!candidate) continue; + const r = parseUrl(candidate); + if (r.ok) return r.value; + } + return null; + }, [input, output]); + + function swap() { + setDirection((d) => (d === "encode" ? "decode" : "encode")); + setInput(output); + } + + useRegisterCommands( + "url-encoder", + [ + { id: "url:encode", section: "URL Encoder", title: "Encode tab", run: () => setDirection("encode") }, + { id: "url:decode", section: "URL Encoder", title: "Decode tab", run: () => setDirection("decode") }, + { id: "url:swap", section: "URL Encoder", title: "Swap direction", run: swap }, + ], + [output] + ); + + return ( +
+
+
+ + +
+ + {direction === "encode" ? ( +
+ + +
+ ) : ( + + )} +
+ +
+
+
+ + {direction === "encode" ? "Text or URL" : "Percent-encoded input"} + + {input.length} chars +
+