|
1 | | -import { useState } from "react"; |
| 1 | +import { useState, useRef, useEffect } from "react"; |
2 | 2 | import { useTheme } from "../context/ThemeContext"; |
3 | 3 | import { useRequest } from "../context/RequestContext"; |
4 | 4 | import clsx from "clsx"; |
5 | 5 | import { ShortcutsDisplay } from "./ShortcutsDisplay"; |
6 | 6 | import { useCurlGenerator } from "../hooks/useCurlGenerator"; |
7 | | -import { CopyIcon } from "./CopyIcon"; |
8 | 7 | import { JsonViewer } from "./JsonViewer"; |
9 | | -import { Maximize2, Minimize2 } from "lucide-react"; |
| 8 | +import { Maximize2, Minimize2, Copy, ChevronDown, Check } from "lucide-react"; |
10 | 9 |
|
11 | 10 | type TabType = "response" | "headers" | "timeline"; |
12 | 11 |
|
@@ -45,21 +44,69 @@ const TabItem = ({ label, value, active, onClick }: TabItemProps) => { |
45 | 44 | ); |
46 | 45 | }; |
47 | 46 |
|
| 47 | +type ViewMode = "pretty" | "raw"; |
| 48 | + |
48 | 49 | export const ResponseView = ({ isRequestCollapsed, onToggleCollapse }: ResponseViewProps) => { |
49 | 50 | const { theme } = useTheme(); |
50 | 51 | const { |
51 | 52 | response, |
52 | 53 | error, |
53 | 54 | loading, |
54 | | - isCopied, |
55 | | - handleCopyResponse, |
56 | 55 | url, |
57 | 56 | responseTime, |
58 | 57 | statusCode, |
59 | 58 | } = useRequest(); |
60 | 59 |
|
61 | 60 | const { generateCurl } = useCurlGenerator(); |
62 | 61 | const [activeTab, setActiveTab] = useState<TabType>("response"); |
| 62 | + const [viewMode, setViewMode] = useState<ViewMode>("pretty"); |
| 63 | + const [isViewDropdownOpen, setIsViewDropdownOpen] = useState(false); |
| 64 | + const [isCopied, setIsCopied] = useState(false); |
| 65 | + const viewDropdownRef = useRef<HTMLDivElement>(null); |
| 66 | + |
| 67 | + // Close dropdown when clicking outside |
| 68 | + useEffect(() => { |
| 69 | + const handleClickOutside = (event: MouseEvent) => { |
| 70 | + if (viewDropdownRef.current && !viewDropdownRef.current.contains(event.target as Node)) { |
| 71 | + setIsViewDropdownOpen(false); |
| 72 | + } |
| 73 | + }; |
| 74 | + |
| 75 | + document.addEventListener("mousedown", handleClickOutside); |
| 76 | + return () => document.removeEventListener("mousedown", handleClickOutside); |
| 77 | + }, []); |
| 78 | + |
| 79 | + // Reset copied state after 2 seconds |
| 80 | + useEffect(() => { |
| 81 | + if (isCopied) { |
| 82 | + const timeout = setTimeout(() => setIsCopied(false), 2000); |
| 83 | + return () => clearTimeout(timeout); |
| 84 | + } |
| 85 | + }, [isCopied]); |
| 86 | + |
| 87 | + const handleCopy = async () => { |
| 88 | + let textToCopy = ""; |
| 89 | + |
| 90 | + if (activeTab === "response" && response) { |
| 91 | + textToCopy = viewMode === "pretty" |
| 92 | + ? JSON.stringify(response, null, 2) |
| 93 | + : JSON.stringify(response); |
| 94 | + } else if (url) { |
| 95 | + textToCopy = generateCurl(); |
| 96 | + } |
| 97 | + |
| 98 | + try { |
| 99 | + await navigator.clipboard.writeText(textToCopy); |
| 100 | + setIsCopied(true); |
| 101 | + } catch (err) { |
| 102 | + console.error("Failed to copy:", err); |
| 103 | + } |
| 104 | + }; |
| 105 | + |
| 106 | + const handleViewModeChange = (mode: ViewMode) => { |
| 107 | + setViewMode(mode); |
| 108 | + setIsViewDropdownOpen(false); |
| 109 | + }; |
63 | 110 |
|
64 | 111 | // Helper function to get status code color |
65 | 112 | const getStatusCodeColor = (code: number | null) => { |
@@ -121,6 +168,19 @@ export const ResponseView = ({ isRequestCollapsed, onToggleCollapse }: ResponseV |
121 | 168 | return <ShortcutsDisplay />; |
122 | 169 | } |
123 | 170 |
|
| 171 | + if (viewMode === "raw") { |
| 172 | + return ( |
| 173 | + <div |
| 174 | + className={clsx( |
| 175 | + "p-4 whitespace-pre-wrap break-all font-mono text-xs", |
| 176 | + theme === "dark" ? "text-gray-300" : "text-gray-800" |
| 177 | + )} |
| 178 | + > |
| 179 | + {JSON.stringify(response)} |
| 180 | + </div> |
| 181 | + ); |
| 182 | + } |
| 183 | + |
124 | 184 | return <JsonViewer data={response} />; |
125 | 185 |
|
126 | 186 | case "headers": |
@@ -267,11 +327,92 @@ export const ResponseView = ({ isRequestCollapsed, onToggleCollapse }: ResponseV |
267 | 327 | <span className={getStatusCodeColor(statusCode)}>{statusCode}</span> |
268 | 328 | </div> |
269 | 329 | )} |
270 | | - {url && ( |
271 | | - <div className="flex items-center"> |
272 | | - <CopyIcon content={generateCurl()} size={16} /> |
| 330 | + {/* View Mode Dropdown (Pretty/Raw) - Only for response tab */} |
| 331 | + {response && activeTab === "response" && ( |
| 332 | + <div className="relative" ref={viewDropdownRef}> |
| 333 | + <button |
| 334 | + onClick={() => setIsViewDropdownOpen(!isViewDropdownOpen)} |
| 335 | + className={clsx( |
| 336 | + "flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs font-medium cursor-pointer transition-all duration-200 border", |
| 337 | + theme === "dark" |
| 338 | + ? "bg-gray-800 text-gray-300 border-gray-700 hover:bg-gray-700 hover:border-purple-600" |
| 339 | + : "bg-gray-100 text-gray-700 border-gray-300 hover:bg-gray-200 hover:border-purple-500" |
| 340 | + )} |
| 341 | + title="View mode" |
| 342 | + > |
| 343 | + <span className="capitalize">{viewMode}</span> |
| 344 | + <ChevronDown className="w-3 h-3" /> |
| 345 | + </button> |
| 346 | + |
| 347 | + {isViewDropdownOpen && ( |
| 348 | + <div |
| 349 | + className={clsx( |
| 350 | + "absolute right-0 mt-1 w-32 rounded-lg shadow-lg border z-50", |
| 351 | + theme === "dark" |
| 352 | + ? "bg-gray-800 border-gray-700" |
| 353 | + : "bg-white border-gray-200" |
| 354 | + )} |
| 355 | + > |
| 356 | + <div className="py-1"> |
| 357 | + <button |
| 358 | + onClick={() => handleViewModeChange("pretty")} |
| 359 | + className={clsx( |
| 360 | + "w-full px-4 py-2 text-left text-sm flex items-center justify-between transition-colors", |
| 361 | + theme === "dark" |
| 362 | + ? "text-gray-300 hover:bg-gray-700" |
| 363 | + : "text-gray-700 hover:bg-gray-100", |
| 364 | + viewMode === "pretty" && "font-semibold" |
| 365 | + )} |
| 366 | + > |
| 367 | + <span>Pretty</span> |
| 368 | + {viewMode === "pretty" && <Check className="w-3.5 h-3.5 text-green-500" />} |
| 369 | + </button> |
| 370 | + <button |
| 371 | + onClick={() => handleViewModeChange("raw")} |
| 372 | + className={clsx( |
| 373 | + "w-full px-4 py-2 text-left text-sm flex items-center justify-between transition-colors", |
| 374 | + theme === "dark" |
| 375 | + ? "text-gray-300 hover:bg-gray-700" |
| 376 | + : "text-gray-700 hover:bg-gray-100", |
| 377 | + viewMode === "raw" && "font-semibold" |
| 378 | + )} |
| 379 | + > |
| 380 | + <span>Raw</span> |
| 381 | + {viewMode === "raw" && <Check className="w-3.5 h-3.5 text-green-500" />} |
| 382 | + </button> |
| 383 | + </div> |
| 384 | + </div> |
| 385 | + )} |
273 | 386 | </div> |
274 | 387 | )} |
| 388 | + |
| 389 | + {/* Copy Button */} |
| 390 | + {url && ( |
| 391 | + <button |
| 392 | + onClick={handleCopy} |
| 393 | + disabled={isCopied} |
| 394 | + className={clsx( |
| 395 | + "flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs font-medium cursor-pointer transition-all duration-200 border", |
| 396 | + theme === "dark" |
| 397 | + ? "bg-gray-800 text-gray-300 border-gray-700 hover:bg-gray-700 hover:border-purple-600" |
| 398 | + : "bg-gray-100 text-gray-700 border-gray-300 hover:bg-gray-200 hover:border-purple-500", |
| 399 | + isCopied && "border-green-500" |
| 400 | + )} |
| 401 | + title={activeTab === "response" && response ? `Copy ${viewMode} response` : "Copy cURL"} |
| 402 | + > |
| 403 | + {isCopied ? ( |
| 404 | + <> |
| 405 | + <Check className="w-3.5 h-3.5 text-green-500" /> |
| 406 | + <span className="text-green-500">Copied!</span> |
| 407 | + </> |
| 408 | + ) : ( |
| 409 | + <> |
| 410 | + <Copy className="w-3.5 h-3.5" /> |
| 411 | + <span>Copy</span> |
| 412 | + </> |
| 413 | + )} |
| 414 | + </button> |
| 415 | + )} |
275 | 416 | <div |
276 | 417 | onClick={onToggleCollapse} |
277 | 418 | className={clsx( |
@@ -321,25 +462,6 @@ export const ResponseView = ({ isRequestCollapsed, onToggleCollapse }: ResponseV |
321 | 462 | > |
322 | 463 | {renderContent()} |
323 | 464 | </div> |
324 | | - |
325 | | - {/* Copy Button - Fixed at Bottom */} |
326 | | - {response && activeTab === "response" && ( |
327 | | - <div className="flex-shrink-0 pt-2"> |
328 | | - <button |
329 | | - onClick={handleCopyResponse} |
330 | | - disabled={isCopied} |
331 | | - className={clsx( |
332 | | - "w-full py-2.5 px-4 rounded-lg cursor-pointer font-medium text-sm transition-all duration-200", |
333 | | - theme === "dark" |
334 | | - ? "bg-purple-700 text-white hover:bg-purple-600 active:bg-purple-800" |
335 | | - : "bg-purple-600 text-white hover:bg-purple-700 active:bg-purple-800", |
336 | | - isCopied && "opacity-70 cursor-not-allowed" |
337 | | - )} |
338 | | - > |
339 | | - {isCopied ? "✓ Copied!" : "Copy Response"} |
340 | | - </button> |
341 | | - </div> |
342 | | - )} |
343 | 465 | </div> |
344 | 466 | ); |
345 | 467 | }; |
0 commit comments