Skip to content

Commit c9933ba

Browse files
authored
feat: Added URL parser & Query Editor Tool
Closes : #51
1 parent 449ff4c commit c9933ba

3 files changed

Lines changed: 439 additions & 2 deletions

File tree

Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
"use client";
2+
import React, { useState, useEffect } from "react";
3+
import DevelopmentToolsStyles from "../../developmentToolsStyles.module.scss";
4+
5+
interface QueryParam {
6+
id: string;
7+
key: string;
8+
value: string;
9+
active: boolean;
10+
encoded: boolean;
11+
}
12+
13+
const UrlParser = () => {
14+
const [urlInput, setUrlInput] = useState<string>("");
15+
const [parsedUrl, setParsedUrl] = useState<URL | null>(null);
16+
const [queryParams, setQueryParams] = useState<QueryParam[]>([]);
17+
const [error, setError] = useState<string>("");
18+
const isInternalUpdate = React.useRef(false);
19+
20+
21+
// Function to update the input and state from a URL object
22+
const updateStateFromUrl = (url: URL) => {
23+
setParsedUrl(url);
24+
const params: QueryParam[] = [];
25+
let index = 0;
26+
url.searchParams.forEach((value, key) => {
27+
// Use a stable ID based on index and key to avoid unnecessary re-renders
28+
// when only values change, or random regeneration on every parse.
29+
const id = `param-${index}-${key}`;
30+
params.push({
31+
id,
32+
key,
33+
value,
34+
active: true,
35+
encoded: true
36+
});
37+
index++;
38+
});
39+
setQueryParams(params);
40+
}
41+
42+
// Initial parse on input change
43+
useEffect(() => {
44+
if (isInternalUpdate.current) {
45+
isInternalUpdate.current = false;
46+
return;
47+
}
48+
49+
if (!urlInput.trim()) {
50+
setParsedUrl(null);
51+
setQueryParams([]);
52+
setError("");
53+
return;
54+
}
55+
56+
try {
57+
// Basic heuristic to add protocol if missing for better UX
58+
let urlToParse = urlInput;
59+
if (!urlInput.match(/^[a-zA-Z][a-zA-Z\d+\-.]*:/)) {
60+
// If doesn't start with scheme, assume http:// if it looks like a domain
61+
if (urlInput.includes('.') && !urlInput.startsWith('/')) {
62+
urlToParse = "http://" + urlInput;
63+
}
64+
}
65+
66+
const url = new URL(urlToParse);
67+
updateStateFromUrl(url);
68+
setError("");
69+
} catch (err) {
70+
setParsedUrl(null);
71+
setError("Invalid URL");
72+
}
73+
}, [urlInput]);
74+
75+
// Re-construct URL when params change
76+
const updateUrlFromParams = (newParams: QueryParam[]) => {
77+
if (!parsedUrl) return;
78+
79+
try {
80+
const newUrl = new URL(parsedUrl.toString());
81+
const queryParts: string[] = [];
82+
83+
newParams.forEach(p => {
84+
if (p.active && p.key) {
85+
const key = p.encoded ? encodeURIComponent(p.key) : p.key;
86+
const value = p.encoded ? encodeURIComponent(p.value) : p.value;
87+
queryParts.push(`${key}=${value}`);
88+
}
89+
});
90+
91+
newUrl.search = queryParts.join('&');
92+
93+
if (newUrl.toString() !== urlInput) {
94+
isInternalUpdate.current = true;
95+
setUrlInput(newUrl.toString());
96+
}
97+
} catch (e) {
98+
console.error("Error updating URL from params", e);
99+
}
100+
};
101+
102+
const handleParamChange = (id: string, field: 'key' | 'value', newValue: string) => {
103+
const updatedParams = queryParams.map(p =>
104+
p.id === id ? { ...p, [field]: newValue } : p
105+
);
106+
// Optimistically update params state to feel responsive
107+
setQueryParams(updatedParams);
108+
// Then update the full URL
109+
updateUrlFromParams(updatedParams);
110+
};
111+
112+
const handleToggleParamEncoding = (id: string) => {
113+
const updatedParams = queryParams.map(p =>
114+
p.id === id ? { ...p, encoded: !p.encoded } : p
115+
);
116+
setQueryParams(updatedParams);
117+
updateUrlFromParams(updatedParams);
118+
};
119+
120+
const handleDeleteParam = (id: string) => {
121+
const updatedParams = queryParams.filter(p => p.id !== id);
122+
setQueryParams(updatedParams);
123+
updateUrlFromParams(updatedParams);
124+
};
125+
126+
const handleAddParam = () => {
127+
const newParam: QueryParam = {
128+
id: Math.random().toString(36).substr(2, 9),
129+
key: "new_key",
130+
value: "value",
131+
active: true,
132+
encoded: true
133+
};
134+
const updatedParams = [...queryParams, newParam];
135+
setQueryParams(updatedParams);
136+
updateUrlFromParams(updatedParams);
137+
};
138+
139+
const updateUrlPart = (part: 'protocol' | 'hostname' | 'pathname' | 'hash', value: string) => {
140+
if (!parsedUrl) return;
141+
try {
142+
const newUrl = new URL(parsedUrl.toString());
143+
if (part === 'protocol') {
144+
// Protocol needs the colon usually
145+
const protocol = value.endsWith(':') ? value : value + ':';
146+
// prevent invalid protocol assignment crash if possible, but URL object throws
147+
newUrl.protocol = protocol;
148+
} else if (part === 'hostname') {
149+
newUrl.hostname = value;
150+
} else if (part === 'pathname') {
151+
newUrl.pathname = value.startsWith('/') ? value : '/' + value;
152+
} else if (part === 'hash') {
153+
newUrl.hash = value;
154+
}
155+
if (newUrl.toString() !== urlInput) {
156+
isInternalUpdate.current = true;
157+
setUrlInput(newUrl.toString());
158+
}
159+
} catch (e) {
160+
// invalid part update, ignore or show hint
161+
}
162+
};
163+
164+
return (
165+
<section>
166+
<div className="md:mt-8 mt-4">
167+
<div className="flex-1 flex items-center justify-center">
168+
<div className="w-full bg-[#FFFFFF1A] rounded-2xl shadow-lg p-8">
169+
<div className="md:w-[900px] mx-auto">
170+
<div className="flex flex-col gap-6 md:my-5 mt-2">
171+
172+
{/* Title */}
173+
<div className="flex items-center gap-4">
174+
<h3 className="text-xl font-medium text-white">URL Parser & Query Editor</h3>
175+
</div>
176+
177+
{/* Main URL Input */}
178+
<div className="w-full">
179+
<label className="block text-sm font-medium mb-2 text-white">URL</label>
180+
<div className="relative">
181+
<textarea
182+
value={urlInput}
183+
onChange={(e) => setUrlInput(e.target.value)}
184+
placeholder="Enter URL to parse... (e.g. https://example.com/search?q=test)"
185+
className={`${DevelopmentToolsStyles.scrollbar} w-full min-h-[80px] bg-black border border-[#222222] p-4 rounded-xl text-white focus:outline-none focus:border-blue-500 transition-colors`}
186+
/>
187+
{error && <p className="text-red-400 text-sm mt-2">{error}</p>}
188+
</div>
189+
</div>
190+
191+
{parsedUrl && (
192+
<div className="animate-fade-in space-y-6">
193+
{/* URL Components Grid */}
194+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 bg-black/20 p-6 rounded-xl border border-[#222222]">
195+
<h4 className="col-span-full text-lg font-medium text-white border-b border-white/10 pb-2">Components</h4>
196+
197+
<div>
198+
<label className="block text-xs font-medium mb-1 text-white/60 uppercase tracking-wider">Scheme</label>
199+
<input
200+
type="text"
201+
value={parsedUrl.protocol.replace(':', '')}
202+
onChange={(e) => updateUrlPart('protocol', e.target.value)}
203+
className="w-full bg-black/40 border border-[#222222] p-2.5 rounded-lg text-white focus:outline-none focus:border-blue-500 transition-colors"
204+
/>
205+
</div>
206+
<div>
207+
<label className="block text-xs font-medium mb-1 text-white/60 uppercase tracking-wider">Host</label>
208+
<input
209+
type="text"
210+
value={parsedUrl.hostname}
211+
onChange={(e) => updateUrlPart('hostname', e.target.value)}
212+
className="w-full bg-black/40 border border-[#222222] p-2.5 rounded-lg text-white focus:outline-none focus:border-blue-500 transition-colors"
213+
/>
214+
</div>
215+
<div className="md:col-span-2">
216+
<label className="block text-xs font-medium mb-1 text-white/60 uppercase tracking-wider">Path</label>
217+
<input
218+
type="text"
219+
value={parsedUrl.pathname}
220+
onChange={(e) => updateUrlPart('pathname', e.target.value)}
221+
className="w-full bg-black/40 border border-[#222222] p-2.5 rounded-lg text-white focus:outline-none focus:border-blue-500 transition-colors"
222+
/>
223+
</div>
224+
<div>
225+
<label className="block text-xs font-medium mb-1 text-white/60 uppercase tracking-wider">Hash</label>
226+
<input
227+
type="text"
228+
value={parsedUrl.hash}
229+
onChange={(e) => updateUrlPart('hash', e.target.value)}
230+
className="w-full bg-black/40 border border-[#222222] p-2.5 rounded-lg text-white focus:outline-none focus:border-blue-500 transition-colors"
231+
/>
232+
</div>
233+
<div>
234+
<label className="block text-xs font-medium mb-1 text-white/60 uppercase tracking-wider">Port</label>
235+
<input
236+
type="text"
237+
value={parsedUrl.port}
238+
readOnly
239+
placeholder="80/443"
240+
className="w-full bg-black/40 border border-[#222222] p-2.5 rounded-lg text-white/50 cursor-not-allowed"
241+
/>
242+
</div>
243+
</div>
244+
245+
{/* Query Parameters Table */}
246+
<div className="w-full">
247+
<div className="flex justify-between items-center mb-4">
248+
<h4 className="text-lg font-medium text-white">Query Parameters</h4>
249+
<button
250+
onClick={handleAddParam}
251+
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition flex items-center gap-2"
252+
>
253+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-5 h-5">
254+
<path d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z" />
255+
</svg>
256+
Add New
257+
</button>
258+
</div>
259+
260+
<div className="bg-black/20 rounded-xl overflow-hidden border border-[#222222]">
261+
{queryParams.length === 0 ? (
262+
<div className="p-8 text-center text-white/40 text-sm flex flex-col items-center">
263+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-10 h-10 mb-2 opacity-30">
264+
<path strokeLinecap="round" strokeLinejoin="round" d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
265+
</svg>
266+
No query parameters found. Add one or paste a URL with params.
267+
</div>
268+
) : (
269+
<div className="w-full overflow-x-auto">
270+
<table className="w-full text-left text-sm text-white">
271+
<thead className="bg-white/5 uppercase text-xs">
272+
<tr>
273+
<th className="px-4 py-3 font-semibold text-white/70 w-1/3">Key</th>
274+
<th className="px-4 py-3 font-semibold text-white/70 w-1/3">Value</th>
275+
<th className="px-4 py-3 font-semibold text-white/70 w-24 text-center">Encoded</th>
276+
<th className="px-4 py-3 font-semibold text-white/70 text-right w-24">Actions</th>
277+
</tr>
278+
</thead>
279+
<tbody className="divide-y divide-white/5">
280+
{queryParams.map((param) => (
281+
<tr key={param.id} className="hover:bg-white/5 transition group">
282+
<td className="px-4 py-2">
283+
<input
284+
type="text"
285+
value={param.key}
286+
onChange={(e) => handleParamChange(param.id, 'key', e.target.value)}
287+
className="bg-transparent w-full p-2 rounded border border-transparent focus:border-blue-500/50 focus:bg-black/20 focus:outline-none transition-colors"
288+
placeholder="Key"
289+
/>
290+
</td>
291+
<td className="px-4 py-2">
292+
<input
293+
type="text"
294+
value={param.value}
295+
onChange={(e) => handleParamChange(param.id, 'value', e.target.value)}
296+
className="bg-transparent w-full p-2 rounded border border-transparent focus:border-blue-500/50 focus:bg-black/20 focus:outline-none transition-colors"
297+
placeholder="Value"
298+
/>
299+
</td>
300+
<td className="px-4 py-2 text-center">
301+
<input
302+
type="checkbox"
303+
checked={param.encoded}
304+
onChange={() => handleToggleParamEncoding(param.id)}
305+
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 bg-gray-700 border-gray-600 accent-blue-600 cursor-pointer"
306+
title="Toggle URL Encoding for this parameter"
307+
/>
308+
</td>
309+
<td className="px-4 py-2 text-right">
310+
<button
311+
onClick={() => handleDeleteParam(param.id)}
312+
className="text-white/40 hover:text-red-400 p-2 rounded-lg hover:bg-white/5 transition"
313+
title="Delete Parameter"
314+
>
315+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5">
316+
<path strokeLinecap="round" strokeLinejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
317+
</svg>
318+
</button>
319+
</td>
320+
</tr>
321+
))}
322+
</tbody>
323+
</table>
324+
</div>
325+
)}
326+
</div>
327+
</div>
328+
</div>
329+
)}
330+
</div>
331+
</div>
332+
</div>
333+
</div>
334+
</div>
335+
</section>
336+
);
337+
};
338+
339+
export default UrlParser;

app/libs/constants.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ import TextToHtmlEntitiesConvertor from '../components/developmentToolsComponent
163163
import TypescriptFormatter from '../components/developmentToolsComponent/typescriptFormatter';
164164
import UnicodeToAsciiConverter from '../components/developmentToolsComponent/unicodeToAsciiConverter';
165165
import URLDecode from '../components/developmentToolsComponent/urlDecode';
166+
import UrlParser from '../components/developmentToolsComponent/urlParser';
166167
import URLEncode from '../components/developmentToolsComponent/urlEncode';
167168
import Utf8Decode from '../components/developmentToolsComponent/utf8Decode';
168169
import Utf8Encode from '../components/developmentToolsComponent/utf8Encode';
@@ -1625,6 +1626,14 @@ export const developmentToolsCategoryContent: any = {
16251626
description: 'Convert Unix timestamps to readable dates and vice versa.',
16261627
},
16271628
],
1629+
Category179: [
1630+
{
1631+
url: '/url-parser',
1632+
title: 'URL Parser & Query String Editor',
1633+
description:
1634+
'Decompose complex URLs into legible components and edit query parameters in a visual table.',
1635+
},
1636+
],
16281637
};
16291638

16301639
export const PATHS = {
@@ -1738,6 +1747,7 @@ export const PATHS = {
17381747
JSON_COMPARE: '/json-compare',
17391748
TEXT_COMPARE: '/text-compare',
17401749
URL_DECODE: '/url-decode',
1750+
URL_PARSER: '/url-parser',
17411751
URL_ENCODE: '/url-encode',
17421752
TEXT_TO_HTML_ENTITIES_CONVERTOR: '/text-to-html-entities-convertor',
17431753
HTML_ENTITIES_TO_TEXT_CONVERTER: '/html-entities-to-text-converter',
@@ -2221,6 +2231,10 @@ export const developmentToolsRoutes = [
22212231
path: PATHS.URL_DECODE,
22222232
component: <URLDecode />,
22232233
},
2234+
{
2235+
path: PATHS.URL_PARSER,
2236+
component: <UrlParser />,
2237+
},
22242238
{
22252239
path: PATHS.URL_ENCODE,
22262240
component: <URLEncode />,

0 commit comments

Comments
 (0)