1- import { useState , memo } from "react" ;
1+ import { useState , memo , useMemo , useCallback , useEffect } from "react" ;
22import { JsonValue } from "./DynamicJsonForm" ;
33import clsx from "clsx" ;
4+ import { Copy , CheckCheck } from "lucide-react" ;
5+ import { Button } from "@/components/ui/button" ;
6+ import { useToast } from "@/hooks/use-toast" ;
47
58interface JsonViewProps {
69 data : unknown ;
710 name ?: string ;
811 initialExpandDepth ?: number ;
12+ className ?: string ;
13+ withCopyButton ?: boolean ;
914}
1015
1116function tryParseJson ( str : string ) : { success : boolean ; data : JsonValue } {
@@ -24,22 +29,79 @@ function tryParseJson(str: string): { success: boolean; data: JsonValue } {
2429}
2530
2631const JsonView = memo (
27- ( { data, name, initialExpandDepth = 3 } : JsonViewProps ) => {
28- const normalizedData =
29- typeof data === "string"
32+ ( {
33+ data,
34+ name,
35+ initialExpandDepth = 3 ,
36+ className,
37+ withCopyButton = true ,
38+ } : JsonViewProps ) => {
39+ const { toast } = useToast ( ) ;
40+ const [ copied , setCopied ] = useState ( false ) ;
41+
42+ useEffect ( ( ) => {
43+ let timeoutId : NodeJS . Timeout ;
44+ if ( copied ) {
45+ timeoutId = setTimeout ( ( ) => {
46+ setCopied ( false ) ;
47+ } , 500 ) ;
48+ }
49+ return ( ) => {
50+ if ( timeoutId ) {
51+ clearTimeout ( timeoutId ) ;
52+ }
53+ } ;
54+ } , [ copied ] ) ;
55+
56+ const normalizedData = useMemo ( ( ) => {
57+ return typeof data === "string"
3058 ? tryParseJson ( data ) . success
3159 ? tryParseJson ( data ) . data
3260 : data
3361 : data ;
62+ } , [ data ] ) ;
63+
64+ const handleCopy = useCallback ( ( ) => {
65+ try {
66+ navigator . clipboard . writeText (
67+ typeof normalizedData === "string"
68+ ? normalizedData
69+ : JSON . stringify ( normalizedData , null , 2 ) ,
70+ ) ;
71+ setCopied ( true ) ;
72+ } catch ( error ) {
73+ toast ( {
74+ title : "Error" ,
75+ description : `There was an error coping result into the clipboard: ${ error instanceof Error ? error . message : String ( error ) } ` ,
76+ variant : "destructive" ,
77+ } ) ;
78+ }
79+ } , [ toast , normalizedData ] ) ;
3480
3581 return (
36- < div className = "font-mono text-sm transition-all duration-300 " >
37- < JsonNode
38- data = { normalizedData as JsonValue }
39- name = { name }
40- depth = { 0 }
41- initialExpandDepth = { initialExpandDepth }
42- />
82+ < div className = { clsx ( "p-4 border rounded relative" , className ) } >
83+ { withCopyButton && (
84+ < Button
85+ size = "icon"
86+ variant = "ghost"
87+ className = "absolute top-2 right-2"
88+ onClick = { handleCopy }
89+ >
90+ { copied ? (
91+ < CheckCheck className = "size-4 dark:text-green-700 text-green-600" />
92+ ) : (
93+ < Copy className = "size-4 text-foreground" />
94+ ) }
95+ </ Button >
96+ ) }
97+ < div className = "font-mono text-sm transition-all duration-300" >
98+ < JsonNode
99+ data = { normalizedData as JsonValue }
100+ name = { name }
101+ depth = { 0 }
102+ initialExpandDepth = { initialExpandDepth }
103+ />
104+ </ div >
43105 </ div >
44106 ) ;
45107 } ,
0 commit comments