diff --git a/frontend/src/ToolRouter.jsx b/frontend/src/ToolRouter.jsx index c0c7b84..ebbb93f 100644 --- a/frontend/src/ToolRouter.jsx +++ b/frontend/src/ToolRouter.jsx @@ -17,6 +17,7 @@ import BarcodeGenerator from './pages/BarcodeGenerator'; import DataGenerator from './pages/DataGenerator'; import CodeFormatter from './pages/CodeFormatter'; import ColorConverter from './pages/ColorConverter'; +import UrlInspector from './pages/UrlInspector'; const toolComponents = { 'code-encrypter': CodeEncrypter, @@ -30,6 +31,7 @@ const toolComponents = { 'data-generator': DataGenerator, 'code-formatter': CodeFormatter, 'color-converter': ColorConverter, + 'url-inspector': UrlInspector, regexp: RegExpTester, cron: CronJobParser, diff: TextDiffChecker, diff --git a/frontend/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx index c0c0eba..568c210 100644 --- a/frontend/src/components/Sidebar.jsx +++ b/frontend/src/components/Sidebar.jsx @@ -26,6 +26,7 @@ import { Code2, Timer, FileCode, + Link, } from 'lucide-react'; import { ScrollArea } from './ui/scroll-area'; @@ -50,6 +51,7 @@ const TOOL_ICONS = { 'data-generator': LayoutGrid, 'code-formatter': Code2, 'color-converter': Palette, + 'url-inspector': Link, cron: Timer, regexp: Regex, diff: FileDiff, @@ -150,6 +152,7 @@ export function Sidebar({ isVisible, onOpenSettings }) { { id: 'data-generator', name: 'Data Generator', category: 'Generator' }, { id: 'code-formatter', name: 'Code Formatter', category: 'Developer' }, { id: 'color-converter', name: 'Color Converter', category: 'Developer' }, + { id: 'url-inspector', name: 'URL Inspector', category: 'Developer' }, { id: 'cron', name: 'Cron Job Parser', category: 'Developer' }, { id: 'regexp', name: 'RegExp Tester', category: 'Developer' }, { id: 'diff', name: 'Text Diff', category: 'Text' }, @@ -366,6 +369,7 @@ export function Sidebar({ isVisible, onOpenSettings }) { 'cron', 'code-formatter', 'color-converter', + 'url-inspector', 'number-converter', 'datetime-converter', ].includes(tool.id) diff --git a/frontend/src/pages/UrlInspector/index.jsx b/frontend/src/pages/UrlInspector/index.jsx new file mode 100644 index 0000000..fa0a41f --- /dev/null +++ b/frontend/src/pages/UrlInspector/index.jsx @@ -0,0 +1,357 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { Check, Copy, Plus, RefreshCw, SortAsc, Trash2 } from 'lucide-react'; +import { Button } from '../../components/ui/Button'; +import { buildUrl, parseUrlInput, sortQueryRows } from './urlUtils'; + +const SAMPLE_URL = 'https://example.com:8443/api/search?q=hello%20world&debug=true#results'; + +function Field({ label, value, onChange, placeholder }) { + return ( + + ); +} + +function createRow() { + return { id: `param-${crypto.randomUUID()}`, key: '', value: '' }; +} + +export default function UrlInspector() { + const [inputUrl, setInputUrl] = useState(SAMPLE_URL); + const [parts, setParts] = useState({ + scheme: 'https', + host: 'example.com:8443', + path: '/api/search', + hash: 'results', + }); + const [queryRows, setQueryRows] = useState([ + { id: 'q-0', key: 'q', value: 'hello world' }, + { id: 'debug-1', key: 'debug', value: 'true' }, + ]); + const [parseError, setParseError] = useState(null); + const [copied, setCopied] = useState(false); + + const builtResult = useMemo(() => buildUrl({ ...parts, queryRows }), [parts, queryRows]); + + useEffect(() => { + const result = parseUrlInput(inputUrl); + setParseError(result.error); + if (!result.error && result.parts) { + setParts(result.parts); + setQueryRows(result.queryRows.length > 0 ? result.queryRows : [createRow()]); + } + }, [inputUrl]); + + const updatePart = (name, value) => { + setParts((current) => ({ ...current, [name]: value })); + }; + + const updateRow = (id, field, value) => { + setQueryRows((rows) => rows.map((row) => (row.id === id ? { ...row, [field]: value } : row))); + }; + + const removeRow = (id) => { + setQueryRows((rows) => rows.filter((row) => row.id !== id)); + }; + + const copyBuiltUrl = async () => { + if (!builtResult.url) return; + await navigator.clipboard.writeText(builtResult.url); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + }; + + const transformValue = (id, transform) => { + setQueryRows((rows) => + rows.map((row) => { + if (row.id !== id) return row; + try { + return { ...row, value: transform(row.value) }; + } catch { + return row; + } + }) + ); + }; + + return ( +
+
+

+ URL Inspector +

+

+ Parse, edit, sort, encode, and rebuild URLs. +

+
+ +
+