Skip to content

Commit 34a048a

Browse files
committed
feat: add loading states, real-time JSON validation, tooltips, and active request indicator
- Add loading spinners to Send and Format JSON buttons - Implement real-time JSON validation for HTTP body and GraphQL variables - Add comprehensive tooltips across all interactive elements - Improve GraphQL editor with better example placeholders - Enhance active request visual indicator in sidebar with border, background, and shadow
1 parent 682af7f commit 34a048a

File tree

5 files changed

+210
-34
lines changed

5 files changed

+210
-34
lines changed

src/components/Folder.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -340,15 +340,20 @@ export const FolderComponent = ({
340340
<div
341341
key={file.fileName}
342342
className={clsx(
343-
"flex items-center p-1.5 rounded hover:bg-gray-300 relative",
343+
"flex items-center p-1.5 rounded hover:bg-gray-300 relative transition-all",
344344
theme === "dark"
345345
? "bg-gray-700 hover:bg-gray-600"
346346
: "bg-gray-200 hover:bg-gray-300",
347347
file.fileName === currentRequestId &&
348348
(theme === "dark"
349-
? "border-l-purple-800 border-l-8 transition-all"
350-
: "border-l-purple-900 border-l-8 transition-all")
349+
? "border-l-purple-500 border-l-4 bg-purple-900/30 shadow-lg"
350+
: "border-l-purple-600 border-l-4 bg-purple-50 shadow-lg")
351351
)}
352+
title={
353+
file.fileName === currentRequestId
354+
? "Currently active request"
355+
: `Open ${file.displayName || "Request"}`
356+
}
352357
>
353358
{editingFileName === file.fileName ? (
354359
<div

src/components/GraphQLEditor.tsx

Lines changed: 113 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,50 @@
11
import { useTheme } from "../context/ThemeContext";
22
import { useRequest } from "../context/RequestContext";
33
import clsx from "clsx";
4+
import { AlertCircle, CheckCircle, Loader2 } from "lucide-react";
5+
import { useEffect, useState } from "react";
46

57
export const GraphQLEditor = () => {
68
const { theme } = useTheme();
7-
const { graphqlQuery, graphqlVariables, setGraphqlQuery, setGraphqlVariables, formatGraphqlVariables } = useRequest();
9+
const {
10+
graphqlQuery,
11+
graphqlVariables,
12+
setGraphqlQuery,
13+
setGraphqlVariables,
14+
formatGraphqlVariables,
15+
} = useRequest();
16+
const [isFormattingVariables, setIsFormattingVariables] = useState(false);
17+
const [variablesValidationError, setVariablesValidationError] = useState<
18+
string | null
19+
>(null);
820

921
const queryLines = graphqlQuery.split("\n");
1022
const variablesLines = graphqlVariables.split("\n");
1123

24+
// Validate GraphQL variables JSON in real-time
25+
useEffect(() => {
26+
if (graphqlVariables.trim() === "" || graphqlVariables.trim() === "{}") {
27+
setVariablesValidationError(null);
28+
return;
29+
}
30+
31+
try {
32+
JSON.parse(graphqlVariables);
33+
setVariablesValidationError(null);
34+
} catch (error) {
35+
if (error instanceof Error) {
36+
setVariablesValidationError(error.message);
37+
}
38+
}
39+
}, [graphqlVariables]);
40+
41+
const handleFormatVariables = async () => {
42+
setIsFormattingVariables(true);
43+
await new Promise((resolve) => setTimeout(resolve, 200));
44+
formatGraphqlVariables();
45+
setIsFormattingVariables(false);
46+
};
47+
1248
return (
1349
<div className="mt-4 space-y-4">
1450
{/* GraphQL Query Section */}
@@ -50,13 +86,25 @@ export const GraphQLEditor = () => {
5086
<textarea
5187
value={graphqlQuery}
5288
onChange={(e) => setGraphqlQuery(e.target.value)}
53-
placeholder="query {
54-
users {
89+
placeholder={`query GetUser($userId: ID!) {
90+
user(id: $userId) {
5591
id
5692
name
5793
email
94+
posts {
95+
title
96+
content
97+
}
5898
}
59-
}"
99+
}
100+
101+
# Or a mutation:
102+
# mutation CreateUser($input: UserInput!) {
103+
# createUser(input: $input) {
104+
# id
105+
# name
106+
# }
107+
# }`}
60108
className={clsx(
61109
"flex-1 px-4 pb-4 text-xs focus:outline-0 ring-0 resize-none border-0 bg-transparent leading-6 overflow-y-auto",
62110
theme === "dark" ? "text-white" : "text-gray-800"
@@ -69,14 +117,32 @@ export const GraphQLEditor = () => {
69117

70118
{/* GraphQL Variables Section */}
71119
<div>
72-
<label
73-
className={clsx(
74-
"block text-sm mb-2",
75-
theme === "dark" ? "text-gray-300" : "text-gray-700"
76-
)}
77-
>
78-
Variables (JSON)
79-
</label>
120+
<div className="flex items-center justify-between mb-2">
121+
<label
122+
className={clsx(
123+
"block text-sm",
124+
theme === "dark" ? "text-gray-300" : "text-gray-700"
125+
)}
126+
>
127+
Variables (JSON)
128+
</label>
129+
{graphqlVariables.trim() !== "" &&
130+
graphqlVariables.trim() !== "{}" && (
131+
<div className="flex items-center gap-1.5 text-xs">
132+
{variablesValidationError ? (
133+
<>
134+
<AlertCircle className="w-3.5 h-3.5 text-red-500" />
135+
<span className="text-red-500">Invalid JSON</span>
136+
</>
137+
) : (
138+
<>
139+
<CheckCircle className="w-3.5 h-3.5 text-green-500" />
140+
<span className="text-green-500">Valid JSON</span>
141+
</>
142+
)}
143+
</div>
144+
)}
145+
</div>
80146
<div
81147
className={clsx(
82148
"border rounded-xl min-h-[150px] max-h-[150px] overflow-hidden",
@@ -106,10 +172,14 @@ export const GraphQLEditor = () => {
106172
<textarea
107173
value={graphqlVariables}
108174
onChange={(e) => setGraphqlVariables(e.target.value)}
109-
placeholder='{
110-
"userId": 1,
111-
"limit": 10
112-
}'
175+
placeholder={`{
176+
"userId": "123",
177+
"limit": 10,
178+
"input": {
179+
"name": "John Doe",
180+
"email": "[email protected]"
181+
}
182+
}`}
113183
className={clsx(
114184
"flex-1 px-4 pb-4 text-xs focus:outline-0 ring-0 resize-none border-0 bg-transparent leading-6 overflow-y-auto",
115185
theme === "dark" ? "text-white" : "text-gray-800"
@@ -118,13 +188,37 @@ export const GraphQLEditor = () => {
118188
/>
119189
</div>
120190
</div>
191+
{variablesValidationError && (
192+
<div
193+
className={clsx(
194+
"mt-2 p-2 rounded text-xs flex items-start gap-2",
195+
theme === "dark"
196+
? "bg-red-900/20 text-red-400"
197+
: "bg-red-100 text-red-700"
198+
)}
199+
>
200+
<AlertCircle className="w-4 h-4 flex-shrink-0 mt-0.5" />
201+
<span>{variablesValidationError}</span>
202+
</div>
203+
)}
121204
<button
122-
onClick={formatGraphqlVariables}
205+
onClick={handleFormatVariables}
206+
disabled={isFormattingVariables || !!variablesValidationError}
207+
title={
208+
variablesValidationError
209+
? "Fix JSON errors before formatting"
210+
: "Format Variables JSON"
211+
}
123212
className={clsx(
124-
"mt-2 py-2 rounded text-xs font-semibold cursor-pointer",
125-
theme === "dark" ? "text-gray-600" : "text-gray-500"
213+
"mt-2 py-2 rounded text-xs font-semibold cursor-pointer flex items-center gap-2",
214+
theme === "dark" ? "text-gray-600" : "text-gray-500",
215+
(isFormattingVariables || variablesValidationError) &&
216+
"opacity-50 cursor-not-allowed"
126217
)}
127218
>
219+
{isFormattingVariables && (
220+
<Loader2 className="w-3 h-3 animate-spin" />
221+
)}
128222
Format Variables JSON
129223
</button>
130224
</div>

src/components/RequestForm.tsx

Lines changed: 86 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,14 @@ import clsx from "clsx";
99
import { SelectAuth } from "./SelectAuth";
1010
import { UsernameAndPassword } from "./UsernameAndPassword";
1111
import { BearerToken } from "./BearerToken";
12-
import { Trash2Icon, Copy, Check } from "lucide-react";
12+
import {
13+
Trash2Icon,
14+
Copy,
15+
Check,
16+
AlertCircle,
17+
CheckCircle,
18+
Loader2,
19+
} from "lucide-react";
1320
import { Checkbox } from "./Checkbox";
1421
import { VariablesTab } from "./VariablesTab";
1522
import { SchemaViewer } from "./SchemaViewer";
@@ -52,6 +59,10 @@ export const RequestForm = () => {
5259
const [urlCopied, setUrlCopied] = useState(false);
5360
const isInternalUpdate = useRef(false);
5461
const isLoadingParams = useRef(false);
62+
const [isFormattingJson, setIsFormattingJson] = useState(false);
63+
const [jsonValidationError, setJsonValidationError] = useState<string | null>(
64+
null
65+
);
5566

5667
useEffect(() => {
5768
if (isInternalUpdate.current || isLoadingParams.current) {
@@ -97,6 +108,31 @@ export const RequestForm = () => {
97108

98109
const lines = payload.split("\n");
99110

111+
// Validate JSON in real-time
112+
useEffect(() => {
113+
if (payload.trim() === "" || payload.trim() === "{}") {
114+
setJsonValidationError(null);
115+
return;
116+
}
117+
118+
try {
119+
JSON.parse(payload);
120+
setJsonValidationError(null);
121+
} catch (error) {
122+
if (error instanceof Error) {
123+
setJsonValidationError(error.message);
124+
}
125+
}
126+
}, [payload]);
127+
128+
const handleFormatJson = async () => {
129+
setIsFormattingJson(true);
130+
// Simulate a brief delay for visual feedback
131+
await new Promise((resolve) => setTimeout(resolve, 200));
132+
formatJson();
133+
setIsFormattingJson(false);
134+
};
135+
100136
const addQueryParam = () => {
101137
setQueryParams([...queryParams, { key: "", value: "", enabled: true }]);
102138
};
@@ -164,14 +200,31 @@ export const RequestForm = () => {
164200

165201
{activeTab === "body" && requestType === "http" && (
166202
<div className="mt-4">
167-
<label
168-
className={clsx(
169-
"block text-sm mb-2",
170-
theme === "dark" ? "text-gray-300" : "text-gray-700"
203+
<div className="flex items-center justify-between mb-2">
204+
<label
205+
className={clsx(
206+
"block text-sm",
207+
theme === "dark" ? "text-gray-300" : "text-gray-700"
208+
)}
209+
>
210+
JSON Payload (optional)
211+
</label>
212+
{payload.trim() !== "" && payload.trim() !== "{}" && (
213+
<div className="flex items-center gap-1.5 text-xs">
214+
{jsonValidationError ? (
215+
<>
216+
<AlertCircle className="w-3.5 h-3.5 text-red-500" />
217+
<span className="text-red-500">Invalid JSON</span>
218+
</>
219+
) : (
220+
<>
221+
<CheckCircle className="w-3.5 h-3.5 text-green-500" />
222+
<span className="text-green-500">Valid JSON</span>
223+
</>
224+
)}
225+
</div>
171226
)}
172-
>
173-
JSON Payload (optional)
174-
</label>
227+
</div>
175228
<div
176229
className={clsx(
177230
"border rounded-xl min-h-[492px] max-h-[492px] overflow-hidden",
@@ -210,13 +263,35 @@ export const RequestForm = () => {
210263
/>
211264
</div>
212265
</div>
266+
{jsonValidationError && (
267+
<div
268+
className={clsx(
269+
"mt-2 p-2 rounded text-xs flex items-start gap-2",
270+
theme === "dark"
271+
? "bg-red-900/20 text-red-400"
272+
: "bg-red-100 text-red-700"
273+
)}
274+
>
275+
<AlertCircle className="w-4 h-4 flex-shrink-0 mt-0.5" />
276+
<span>{jsonValidationError}</span>
277+
</div>
278+
)}
213279
<button
214-
onClick={formatJson}
280+
onClick={handleFormatJson}
281+
disabled={isFormattingJson || !!jsonValidationError}
282+
title={
283+
jsonValidationError
284+
? "Fix JSON errors before formatting"
285+
: "Format JSON"
286+
}
215287
className={clsx(
216-
"mt-2 py-2 rounded text-xs font-semibold cursor-pointer",
217-
theme === "dark" ? "text-gray-600" : "text-gray-500"
288+
"mt-2 py-2 rounded text-xs font-semibold cursor-pointer flex items-center gap-2",
289+
theme === "dark" ? "text-gray-600" : "text-gray-500",
290+
(isFormattingJson || jsonValidationError) &&
291+
"opacity-50 cursor-not-allowed"
218292
)}
219293
>
294+
{isFormattingJson && <Loader2 className="w-3 h-3 animate-spin" />}
220295
Format JSON
221296
</button>
222297
</div>

src/components/SelectMethod.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ export const SelectMethod: React.FC<SelectMethodProps> = ({
2121
<select
2222
value={value}
2323
onChange={(e) => onChange(e.target.value)}
24+
title={`HTTP Method: ${value}`}
2425
className={clsx(
25-
"block appearance-none w-full border rounded-lg py-2 px-4 pr-8 leading-tight focus:outline-none focus:ring-1",
26+
"block appearance-none w-full border rounded-lg py-2 px-4 pr-8 leading-tight focus:outline-none focus:ring-1 cursor-pointer",
2627
theme === "dark"
2728
? "text-white bg-[#10121b] border-purple-500 ring-purple-500 border-2"
2829
: " text-gray-700 bg-white border-purple-500 ring-purple-500 border-2"

src/components/ThemeToggle.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const ThemeToggle = () => {
1515
: "text-gray-700 hover:bg-gray-200"
1616
)}
1717
aria-label="Toggle theme"
18+
title={theme === "light" ? "Switch to dark mode" : "Switch to light mode"}
1819
>
1920
{theme === "light" ? (
2021
<Moon className="w-5 h-5" />

0 commit comments

Comments
 (0)