Skip to content

Commit 4ece7cc

Browse files
authored
feat(core): improve DRYness and use a CopyButton component
2 parents 267a7f9 + a84bd85 commit 4ece7cc

File tree

4 files changed

+183
-96
lines changed

4 files changed

+183
-96
lines changed

src/components/CopyButton.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { useState } from "react";
2+
3+
interface CopyButtonProps {
4+
content: string;
5+
}
6+
7+
export const CopyButton = ({ content }: CopyButtonProps) => {
8+
const [copied, setCopied] = useState(false);
9+
10+
const copyToClipboard = async () => {
11+
try {
12+
await navigator.clipboard.writeText(content);
13+
setCopied(true);
14+
setTimeout(() => setCopied(false), 2000);
15+
} catch (err) {
16+
console.error("Failed to copy: ", err);
17+
alert("Failed to copy UUID to clipboard.");
18+
}
19+
};
20+
21+
return (
22+
<button
23+
onClick={copyToClipboard}
24+
style={{
25+
padding: "0.5rem 1rem",
26+
fontWeight: "bold",
27+
backgroundColor: copied ? "#7da87d" : "#1ca31e",
28+
color: "white",
29+
border: "none",
30+
borderRadius: "4px",
31+
cursor: "pointer",
32+
}}
33+
>
34+
{copied ? "Copied!" : "Copy"}
35+
</button>
36+
);
37+
};

src/components/OutputBox.tsx

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,15 @@
1-
import { useState } from "react";
1+
import { CopyButton } from "./CopyButton";
22

33
type OutputBoxProps = {
44
output: string;
55
};
66

77
export function OutputBox({ output }: OutputBoxProps) {
8-
const [copied, setCopied] = useState(false);
9-
10-
const handleCopy = async () => {
11-
try {
12-
await navigator.clipboard.writeText(output);
13-
setCopied(true);
14-
setTimeout(() => setCopied(false), 2000);
15-
} catch (err) {
16-
console.error("Failed to copy:", err);
17-
}
18-
};
19-
208
return (
219
<div style={{ marginTop: "16px" }}>
2210
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
2311
<h3>Output</h3>
24-
<button onClick={handleCopy} style={{ padding: "4px 8px", fontSize: "0.9rem" }}>
25-
{copied ? "Copied!" : "Copy"}
26-
</button>
12+
<CopyButton content={output} />
2713
</div>
2814
<textarea
2915
readOnly

src/components/URLVisualizer.tsx

Lines changed: 132 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import React, { useState, useEffect, CSSProperties } from "react";
1+
import { useState, useEffect, CSSProperties } from "react";
2+
import { CopyButton } from "./CopyButton";
23

34
const parseParams = (paramString: string): Record<string, string> => {
45
const params = new URLSearchParams(paramString);
@@ -12,7 +13,9 @@ const parseParams = (paramString: string): Record<string, string> => {
1213
const stringifyParams = (params: Record<string, string>): string => {
1314
const urlParams = new URLSearchParams();
1415
Object.entries(params).forEach(([key, value]) => {
15-
urlParams.set(key, encodeURIComponent(value));
16+
if (key.trim() !== "") {
17+
urlParams.set(key, encodeURIComponent(value));
18+
}
1619
});
1720
return urlParams.toString();
1821
};
@@ -74,11 +77,85 @@ const styles: Record<string, CSSProperties> = {
7477
},
7578
};
7679

77-
const URLVisualizer: React.FC = () => {
80+
interface ParamsEditorProps {
81+
type: "query" | "hash";
82+
params: Record<string, string>;
83+
onChange: (key: string, value: string) => void;
84+
onRemove: (key: string) => void;
85+
onAdd: (key: string, value: string) => void;
86+
newKey: string;
87+
newValue: string;
88+
setNewKey: (val: string) => void;
89+
setNewValue: (val: string) => void;
90+
}
91+
92+
const ParamsEditor = ({
93+
type,
94+
params,
95+
onChange,
96+
onRemove,
97+
onAdd,
98+
newKey,
99+
newValue,
100+
setNewKey,
101+
setNewValue,
102+
}: ParamsEditorProps) => (
103+
<div style={styles.section}>
104+
<strong>{type === "query" ? "Query" : "Hash"} Parameters:</strong>
105+
{Object.keys(params).length === 0 ? (
106+
<div style={styles.empty}>None</div>
107+
) : (
108+
Object.entries(params).map(([key, value]) => (
109+
<div key={key} style={styles.paramItem}>
110+
<label style={styles.label}>{key}</label>
111+
<input
112+
type="text"
113+
value={value}
114+
onChange={(e) => onChange(key, e.target.value)}
115+
style={styles.paramInput}
116+
/>
117+
<button
118+
onClick={() => onRemove(key)}
119+
style={{ ...styles.button, backgroundColor: "#eb3f59", marginLeft: "8px" }}
120+
>
121+
122+
</button>
123+
</div>
124+
))
125+
)}
126+
<div style={styles.paramItem}>
127+
<input placeholder="key" value={newKey} onChange={(e) => setNewKey(e.target.value)} style={styles.paramInput} />
128+
<input
129+
placeholder="value"
130+
value={newValue}
131+
onChange={(e) => setNewValue(e.target.value)}
132+
style={{ ...styles.paramInput, marginLeft: "8px" }}
133+
/>
134+
<button
135+
onClick={() => {
136+
if (newKey.trim()) {
137+
onAdd(newKey, newValue);
138+
setNewKey("");
139+
setNewValue("");
140+
}
141+
}}
142+
style={{ ...styles.button, marginLeft: "8px" }}
143+
>
144+
+
145+
</button>
146+
</div>
147+
</div>
148+
);
149+
150+
const URLVisualizer = () => {
78151
const [urlInput, setUrlInput] = useState<string>("");
79152
const [urlObject, setUrlObject] = useState<URL | null>(null);
80153
const [queryParams, setQueryParams] = useState<Record<string, string>>({});
81154
const [hashParams, setHashParams] = useState<Record<string, string>>({});
155+
const [newQueryKey, setNewQueryKey] = useState("");
156+
const [newQueryValue, setNewQueryValue] = useState("");
157+
const [newHashKey, setNewHashKey] = useState("");
158+
const [newHashValue, setNewHashValue] = useState("");
82159

83160
useEffect(() => {
84161
try {
@@ -93,29 +170,15 @@ const URLVisualizer: React.FC = () => {
93170
}
94171
}, [urlInput]);
95172

96-
const handleParamChange = (type: "query" | "hash", key: string, value: string) => {
97-
const newParams = {
98-
...(type === "query" ? queryParams : hashParams),
99-
[key]: value,
100-
};
101-
if (type === "query") {
102-
setQueryParams(newParams);
103-
} else {
104-
setHashParams(newParams);
105-
}
106-
173+
const updateUrlInput = (updatedQueryParams: Record<string, string>, updatedHashParams: Record<string, string>) => {
107174
if (urlObject) {
108175
const updatedUrl = new URL(urlObject.toString());
109-
updatedUrl.search = stringifyParams(type === "query" ? newParams : queryParams);
110-
updatedUrl.hash = stringifyParams(type === "hash" ? newParams : hashParams);
176+
updatedUrl.search = stringifyParams(updatedQueryParams);
177+
updatedUrl.hash = stringifyParams(updatedHashParams);
111178
setUrlInput(updatedUrl.toString());
112179
}
113180
};
114181

115-
const copyToClipboard = () => {
116-
navigator.clipboard.writeText(urlInput);
117-
};
118-
119182
return (
120183
<div style={styles.container}>
121184
<div style={styles.inputGroup}>
@@ -127,9 +190,7 @@ const URLVisualizer: React.FC = () => {
127190
onChange={(e) => setUrlInput(e.target.value)}
128191
style={styles.input}
129192
/>
130-
<button onClick={copyToClipboard} style={styles.button}>
131-
Copy URL
132-
</button>
193+
<CopyButton content={urlInput} />
133194
</div>
134195

135196
{urlObject && (
@@ -144,43 +205,55 @@ const URLVisualizer: React.FC = () => {
144205
<strong>Path:</strong> {urlObject.pathname}
145206
</div>
146207

147-
<div style={styles.section}>
148-
<strong>Query Parameters:</strong>
149-
{Object.keys(queryParams).length === 0 ? (
150-
<div style={styles.empty}>None</div>
151-
) : (
152-
Object.entries(queryParams).map(([key, value]) => (
153-
<div key={key} style={styles.paramItem}>
154-
<label style={styles.label}>{key}</label>
155-
<input
156-
type="text"
157-
value={value}
158-
onChange={(e) => handleParamChange("query", key, e.target.value)}
159-
style={styles.paramInput}
160-
/>
161-
</div>
162-
))
163-
)}
164-
</div>
208+
<ParamsEditor
209+
type="query"
210+
params={queryParams}
211+
onChange={(key, value) => {
212+
const updated = { ...queryParams, [key]: value };
213+
setQueryParams(updated);
214+
updateUrlInput(updated, hashParams);
215+
}}
216+
onRemove={(key) => {
217+
const updated = { ...queryParams };
218+
delete updated[key];
219+
setQueryParams(updated);
220+
updateUrlInput(updated, hashParams);
221+
}}
222+
onAdd={(key, value) => {
223+
const updated = { ...queryParams, [key]: value };
224+
setQueryParams(updated);
225+
updateUrlInput(updated, hashParams);
226+
}}
227+
newKey={newQueryKey}
228+
newValue={newQueryValue}
229+
setNewKey={setNewQueryKey}
230+
setNewValue={setNewQueryValue}
231+
/>
165232

166-
<div style={styles.section}>
167-
<strong>Hash Parameters:</strong>
168-
{Object.keys(hashParams).length === 0 ? (
169-
<div style={styles.empty}>None</div>
170-
) : (
171-
Object.entries(hashParams).map(([key, value]) => (
172-
<div key={key} style={styles.paramItem}>
173-
<label style={styles.label}>{key}</label>
174-
<input
175-
type="text"
176-
value={value}
177-
onChange={(e) => handleParamChange("hash", key, e.target.value)}
178-
style={styles.paramInput}
179-
/>
180-
</div>
181-
))
182-
)}
183-
</div>
233+
<ParamsEditor
234+
type="hash"
235+
params={hashParams}
236+
onChange={(key, value) => {
237+
const updated = { ...hashParams, [key]: value };
238+
setHashParams(updated);
239+
updateUrlInput(queryParams, updated);
240+
}}
241+
onRemove={(key) => {
242+
const updated = { ...hashParams };
243+
delete updated[key];
244+
setHashParams(updated);
245+
updateUrlInput(queryParams, updated);
246+
}}
247+
onAdd={(key, value) => {
248+
const updated = { ...hashParams, [key]: value };
249+
setHashParams(updated);
250+
updateUrlInput(queryParams, updated);
251+
}}
252+
newKey={newHashKey}
253+
newValue={newHashValue}
254+
setNewKey={setNewHashKey}
255+
setNewValue={setNewHashValue}
256+
/>
184257
</div>
185258
)}
186259
</div>

src/components/UUIDGenerator.tsx

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useState, CSSProperties } from "react";
22
import { v4 as uuidv4, v6 as uuidv6 } from "uuid";
33
import { Switch } from "@headlessui/react";
4+
import { CopyButton } from "./CopyButton";
45

56
const styles: Record<string, CSSProperties> = {
67
container: {
@@ -53,27 +54,25 @@ const styles: Record<string, CSSProperties> = {
5354
export const UUIDGenerator = () => {
5455
const [uuid, setUuid] = useState(uuidv4());
5556
const [version, setVersion] = useState("v4");
56-
const [copied, setCopied] = useState(false);
5757

5858
const generateUUID = () => {
5959
if (version === "v4") {
6060
setUuid(uuidv4());
6161
} else if (version === "v6") {
6262
setUuid(uuidv6());
6363
}
64-
setCopied(false);
6564
};
6665

67-
const copyToClipboard = async () => {
68-
try {
69-
await navigator.clipboard.writeText(uuid);
70-
setCopied(true);
71-
setTimeout(() => setCopied(false), 2000);
72-
} catch (err) {
73-
console.error("Failed to copy: ", err);
74-
alert("Failed to copy UUID to clipboard.");
75-
}
76-
};
66+
// const copyToClipboard = async () => {
67+
// try {
68+
// await navigator.clipboard.writeText(uuid);
69+
// setCopied(true);
70+
// setTimeout(() => setCopied(false), 2000);
71+
// } catch (err) {
72+
// console.error("Failed to copy: ", err);
73+
// alert("Failed to copy UUID to clipboard.");
74+
// }
75+
// };
7776

7877
const toggleVersion = () => {
7978
const newVersion = version === "v4" ? "v6" : "v4";
@@ -94,15 +93,7 @@ export const UUIDGenerator = () => {
9493
<button onClick={generateUUID} style={styles.button}>
9594
Generate
9695
</button>
97-
<button
98-
onClick={copyToClipboard}
99-
style={{
100-
...styles.button,
101-
backgroundColor: "green",
102-
}}
103-
>
104-
{copied ? "Copied!" : "Copy"}
105-
</button>
96+
<CopyButton content={uuid} />
10697
</div>
10798

10899
<div style={{ display: "flex", alignItems: "center", gap: "16px", marginTop: "8px" }}>

0 commit comments

Comments
 (0)