import { useState, memo, useMemo, useCallback, useEffect } from "react"; import type { JsonValue } from "@/utils/jsonUtils"; import clsx from "clsx"; import { Copy, CheckCheck } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useToast } from "@/hooks/use-toast"; import { getDataType, tryParseJson } from "@/utils/jsonUtils"; interface JsonViewProps { data: unknown; name?: string; initialExpandDepth?: number; className?: string; withCopyButton?: boolean; isError?: boolean; } const JsonView = memo( ({ data, name, initialExpandDepth = 3, className, withCopyButton = true, isError = false, }: JsonViewProps) => { const { toast } = useToast(); const [copied, setCopied] = useState(false); useEffect(() => { let timeoutId: NodeJS.Timeout; if (copied) { timeoutId = setTimeout(() => { setCopied(false); }, 500); } return () => { if (timeoutId) { clearTimeout(timeoutId); } }; }, [copied]); const normalizedData = useMemo(() => { return typeof data === "string" ? tryParseJson(data).success ? tryParseJson(data).data : data : data; }, [data]); const handleCopy = useCallback(() => { try { navigator.clipboard.writeText( typeof normalizedData === "string" ? normalizedData : JSON.stringify(normalizedData, null, 2), ); setCopied(true); } catch (error) { toast({ title: "Error", description: `There was an error coping result into the clipboard: ${error instanceof Error ? error.message : String(error)}`, variant: "destructive", }); } }, [toast, normalizedData]); return (
{withCopyButton && ( )}
); }, ); JsonView.displayName = "JsonView"; interface JsonNodeProps { data: JsonValue; name?: string; depth: number; initialExpandDepth: number; isError?: boolean; } const JsonNode = memo( ({ data, name, depth = 0, initialExpandDepth, isError = false, }: JsonNodeProps) => { const [isExpanded, setIsExpanded] = useState(depth < initialExpandDepth); const [typeStyleMap] = useState>({ number: "text-blue-600", boolean: "text-amber-600", null: "text-purple-600", undefined: "text-gray-600", string: "text-green-600 group-hover:text-green-500", error: "text-red-600 group-hover:text-red-500", default: "text-gray-700", }); const dataType = getDataType(data); const renderCollapsible = (isArray: boolean) => { const items = isArray ? (data as JsonValue[]) : Object.entries(data as Record); const itemCount = items.length; const isEmpty = itemCount === 0; const symbolMap = { open: isArray ? "[" : "{", close: isArray ? "]" : "}", collapsed: isArray ? "[ ... ]" : "{ ... }", empty: isArray ? "[]" : "{}", }; if (isEmpty) { return (
{name && ( {name}: )} {symbolMap.empty}
); } return (
setIsExpanded(!isExpanded)} > {name && ( {name}: )} {isExpanded ? ( {symbolMap.open} ) : ( <> {symbolMap.collapsed} {itemCount} {itemCount === 1 ? "item" : "items"} )}
{isExpanded && ( <>
{isArray ? (items as JsonValue[]).map((item, index) => (
)) : (items as [string, JsonValue][]).map(([key, value]) => (
))}
{symbolMap.close}
)}
); }; const renderString = (value: string) => { const maxLength = 100; const isTooLong = value.length > maxLength; if (!isTooLong) { return (
{name && ( {name}: )}
              "{value}"
            
); } return (
{name && ( {name}: )}
 setIsExpanded(!isExpanded)}
            title={isExpanded ? "Click to collapse" : "Click to expand"}
          >
            {isExpanded ? `"${value}"` : `"${value.slice(0, maxLength)}..."`}
          
); }; switch (dataType) { case "object": case "array": return renderCollapsible(dataType === "array"); case "string": return renderString(data as string); default: return (
{name && ( {name}: )} {data === null ? "null" : String(data)}
); } }, ); JsonNode.displayName = "JsonNode"; export default JsonView;