1
- import type React from "react "
1
+ "use client "
2
2
3
+ import type React from "react"
3
4
import { useState } from "react"
4
5
import { Button } from "~/components/ui/button"
5
6
import { Card , CardContent } from "~/components/ui/card"
6
7
import { Alert , AlertDescription } from "~/components/ui/alert"
7
- import { AlertCircle , FileJson , FileUp } from "lucide-react"
8
+ import { AlertCircle , FileJson , FileUp , LinkIcon } from "lucide-react"
9
+ import { Input } from "~/components/ui/input"
10
+ import { Tabs , TabsContent , TabsList , TabsTrigger } from "~/components/ui/tabs"
8
11
import DiffViewer from "./diff-viewer"
9
12
import { parseFileContent } from "~/lib/file-parser"
10
13
14
+ interface FileData {
15
+ name : string
16
+ content : any
17
+ source : "upload" | "url"
18
+ }
19
+
11
20
export default function FileComparisonTool ( ) {
12
- const [ file1 , setFile1 ] = useState < { name : string ; content : any } | null > ( null )
13
- const [ file2 , setFile2 ] = useState < { name : string ; content : any } | null > ( null )
21
+ const [ file1 , setFile1 ] = useState < FileData | null > ( null )
22
+ const [ file2 , setFile2 ] = useState < FileData | null > ( null )
23
+ const [ url1 , setUrl1 ] = useState ( "" )
24
+ const [ url2 , setUrl2 ] = useState ( "" )
25
+ const [ activeTab1 , setActiveTab1 ] = useState < "upload" | "url" > ( "upload" )
26
+ const [ activeTab2 , setActiveTab2 ] = useState < "upload" | "url" > ( "upload" )
14
27
const [ error , setError ] = useState < string | null > ( null )
28
+ const [ isLoading , setIsLoading ] = useState ( false )
15
29
const [ showDiff , setShowDiff ] = useState ( false )
16
30
17
31
const handleFileUpload = async ( e : React . ChangeEvent < HTMLInputElement > , fileNumber : 1 | 2 ) => {
@@ -31,9 +45,9 @@ export default function FileComparisonTool() {
31
45
const parsedContent = parseFileContent ( content , fileExtension as string )
32
46
33
47
if ( fileNumber === 1 ) {
34
- setFile1 ( { name : file . name , content : parsedContent } )
48
+ setFile1 ( { name : file . name , content : parsedContent , source : "upload" } )
35
49
} else {
36
- setFile2 ( { name : file . name , content : parsedContent } )
50
+ setFile2 ( { name : file . name , content : parsedContent , source : "upload" } )
37
51
}
38
52
} catch ( err ) {
39
53
setError ( `Error parsing file: ${ ( err as Error ) . message } ` )
@@ -49,18 +63,173 @@ export default function FileComparisonTool() {
49
63
} )
50
64
}
51
65
52
- const handleCompare = ( ) => {
53
- if ( ! file1 || ! file2 ) {
54
- setError ( "Please upload both files to compare" )
55
- return
66
+ const fetchFileFromUrl = async ( url : string ) : Promise < { name : string ; content : any } > => {
67
+ if ( ! url ) {
68
+ throw new Error ( "Please enter a URL" )
69
+ }
70
+
71
+ // Extract filename from URL
72
+ const urlObj = new URL ( url )
73
+ const pathname = urlObj . pathname
74
+ const filename = pathname . substring ( pathname . lastIndexOf ( "/" ) + 1 )
75
+
76
+ // Check file extension
77
+ const fileExtension = filename . split ( "." ) . pop ( ) ?. toLowerCase ( )
78
+ if ( ! [ "json" , "yaml" , "yml" ] . includes ( fileExtension || "" ) ) {
79
+ throw new Error ( "Only JSON and YAML files are supported" )
80
+ }
81
+
82
+ // Fetch the file
83
+ const response = await fetch ( url )
84
+ if ( ! response . ok ) {
85
+ throw new Error ( `Failed to fetch file: ${ response . statusText } ` )
86
+ }
87
+
88
+ const content = await response . text ( )
89
+ const parsedContent = parseFileContent ( content , fileExtension as string )
90
+
91
+ return {
92
+ name : filename || `file-${ Date . now ( ) } ` ,
93
+ content : parsedContent ,
94
+ }
95
+ }
96
+
97
+ const handleCompare = async ( ) => {
98
+ setError ( null )
99
+
100
+ try {
101
+ setIsLoading ( true )
102
+
103
+ // Check if we need to fetch files from URLs
104
+ let file1Data = file1
105
+ let file2Data = file2
106
+
107
+ // Fetch file 1 if URL is selected and provided
108
+ if ( activeTab1 === "url" && url1 ) {
109
+ if ( ! file1 || file1 . source !== "url" || file1 . name !== url1 ) {
110
+ try {
111
+ const fetchedFile = await fetchFileFromUrl ( url1 )
112
+ file1Data = { ...fetchedFile , source : "url" }
113
+ setFile1 ( file1Data )
114
+ } catch ( err ) {
115
+ throw new Error ( `Error with File 1: ${ ( err as Error ) . message } ` )
116
+ }
117
+ }
118
+ } else if ( activeTab1 === "upload" && ! file1 ) {
119
+ throw new Error ( "Please upload the first file" )
120
+ }
121
+
122
+ // Fetch file 2 if URL is selected and provided
123
+ if ( activeTab2 === "url" && url2 ) {
124
+ if ( ! file2 || file2 . source !== "url" || file2 . name !== url2 ) {
125
+ try {
126
+ const fetchedFile = await fetchFileFromUrl ( url2 )
127
+ file2Data = { ...fetchedFile , source : "url" }
128
+ setFile2 ( file2Data )
129
+ } catch ( err ) {
130
+ throw new Error ( `Error with File 2: ${ ( err as Error ) . message } ` )
131
+ }
132
+ }
133
+ } else if ( activeTab2 === "upload" && ! file2 ) {
134
+ throw new Error ( "Please upload the second file" )
135
+ }
136
+
137
+ // Ensure we have both files
138
+ if ( ! file1Data || ! file2Data ) {
139
+ throw new Error ( "Please provide both files to compare" )
140
+ }
141
+
142
+ // Show diff viewer
143
+ setShowDiff ( true )
144
+ } catch ( err ) {
145
+ setError ( ( err as Error ) . message )
146
+ } finally {
147
+ setIsLoading ( false )
56
148
}
57
- setShowDiff ( true )
58
149
}
59
150
60
151
const handleCloseDiff = ( ) => {
61
152
setShowDiff ( false )
62
153
}
63
154
155
+ const renderFileCard = ( fileNumber : 1 | 2 ) => {
156
+ const file = fileNumber === 1 ? file1 : file2
157
+ const activeTab = fileNumber === 1 ? activeTab1 : activeTab2
158
+ const setActiveTab = fileNumber === 1 ? setActiveTab1 : setActiveTab2
159
+
160
+ return (
161
+ < Card >
162
+ < CardContent className = "pt-6" >
163
+ < Tabs
164
+ defaultValue = "upload"
165
+ value = { activeTab }
166
+ onValueChange = { ( value ) => setActiveTab ( value as "upload" | "url" ) }
167
+ className = "w-full"
168
+ >
169
+ < TabsList className = "grid w-full grid-cols-2 mb-4" >
170
+ < TabsTrigger value = "upload" > Upload File</ TabsTrigger >
171
+ < TabsTrigger value = "url" > URL</ TabsTrigger >
172
+ </ TabsList >
173
+
174
+ < TabsContent value = "upload" >
175
+ < div className = "flex flex-col items-center justify-center p-6 border-2 border-dashed rounded-lg border-gray-300 bg-gray-50 hover:bg-gray-100 transition-colors" >
176
+ < FileJson className = "h-10 w-10 text-gray-400 mb-2" />
177
+ < p className = "text-sm font-medium mb-2" >
178
+ { file && file . source === "upload" ? file . name : `Upload file ${ fileNumber } (JSON/YAML)` }
179
+ </ p >
180
+ < div className = "relative" >
181
+ < Button variant = "outline" size = "sm" className = "mt-2" >
182
+ < FileUp className = "h-4 w-4 mr-2" />
183
+ Select File
184
+ </ Button >
185
+ < input
186
+ type = "file"
187
+ className = "absolute inset-0 w-full h-full opacity-0 cursor-pointer"
188
+ accept = ".json,.yaml,.yml"
189
+ onChange = { ( e ) => handleFileUpload ( e , fileNumber ) }
190
+ />
191
+ </ div >
192
+ </ div >
193
+ </ TabsContent >
194
+
195
+ < TabsContent value = "url" >
196
+ < div className = "flex flex-col space-y-4" >
197
+ < div className = "flex items-center space-x-2" >
198
+ < LinkIcon className = "h-5 w-5 text-gray-400" />
199
+ < p className = "text-sm font-medium" >
200
+ { file && file . source === "url" ? file . name : `Enter URL to file ${ fileNumber } ` }
201
+ </ p >
202
+ </ div >
203
+
204
+ < Input
205
+ type = "url"
206
+ placeholder = "https://example.com/file.json"
207
+ value = { fileNumber === 1 ? url1 : url2 }
208
+ onChange = { ( e ) => ( fileNumber === 1 ? setUrl1 ( e . target . value ) : setUrl2 ( e . target . value ) ) }
209
+ className = "w-full"
210
+ />
211
+
212
+ < p className = "text-xs text-gray-500" > File will be fetched when you click "Compare Files"</ p >
213
+ </ div >
214
+ </ TabsContent >
215
+ </ Tabs >
216
+ </ CardContent >
217
+ </ Card >
218
+ )
219
+ }
220
+
221
+ const isCompareDisabled = ( ) => {
222
+ if ( isLoading ) return true
223
+
224
+ // Check if we have what we need based on active tabs
225
+ if ( activeTab1 === "upload" && ! file1 ) return true
226
+ if ( activeTab2 === "upload" && ! file2 ) return true
227
+ if ( activeTab1 === "url" && ! url1 ) return true
228
+ if ( activeTab2 === "url" && ! url2 ) return true
229
+
230
+ return false
231
+ }
232
+
64
233
return (
65
234
< div className = "space-y-6" >
66
235
{ error && (
@@ -71,52 +240,13 @@ export default function FileComparisonTool() {
71
240
) }
72
241
73
242
< div className = "grid grid-cols-1 md:grid-cols-2 gap-6" >
74
- < Card >
75
- < CardContent className = "pt-6" >
76
- < div className = "flex flex-col items-center justify-center p-6 border-2 border-dashed rounded-lg border-gray-300 bg-gray-50 hover:bg-gray-100 transition-colors" >
77
- < FileJson className = "h-10 w-10 text-gray-400 mb-2" />
78
- < p className = "text-sm font-medium mb-2" > { file1 ? file1 . name : "Upload old file (JSON/YAML)" } </ p >
79
- < div className = "relative" >
80
- < Button variant = "outline" size = "sm" className = "mt-2" >
81
- < FileUp className = "h-4 w-4 mr-2" />
82
- Select File
83
- </ Button >
84
- < input
85
- type = "file"
86
- className = "absolute inset-0 w-full h-full opacity-0 cursor-pointer"
87
- accept = ".json,.yaml,.yml"
88
- onChange = { ( e ) => handleFileUpload ( e , 1 ) }
89
- />
90
- </ div >
91
- </ div >
92
- </ CardContent >
93
- </ Card >
94
-
95
- < Card >
96
- < CardContent className = "pt-6" >
97
- < div className = "flex flex-col items-center justify-center p-6 border-2 border-dashed rounded-lg border-gray-300 bg-gray-50 hover:bg-gray-100 transition-colors" >
98
- < FileJson className = "h-10 w-10 text-gray-400 mb-2" />
99
- < p className = "text-sm font-medium mb-2" > { file2 ? file2 . name : "Upload new file (JSON/YAML)" } </ p >
100
- < div className = "relative" >
101
- < Button variant = "outline" size = "sm" className = "mt-2" >
102
- < FileUp className = "h-4 w-4 mr-2" />
103
- Select File
104
- </ Button >
105
- < input
106
- type = "file"
107
- className = "absolute inset-0 w-full h-full opacity-0 cursor-pointer"
108
- accept = ".json,.yaml,.yml"
109
- onChange = { ( e ) => handleFileUpload ( e , 2 ) }
110
- />
111
- </ div >
112
- </ div >
113
- </ CardContent >
114
- </ Card >
243
+ { renderFileCard ( 1 ) }
244
+ { renderFileCard ( 2 ) }
115
245
</ div >
116
246
117
247
< div className = "flex justify-center mt-8" >
118
- < Button size = "lg" onClick = { handleCompare } disabled = { ! file1 || ! file2 } >
119
- Compare Files
248
+ < Button size = "lg" onClick = { handleCompare } disabled = { isCompareDisabled ( ) } >
249
+ { isLoading ? "Fetching Files..." : " Compare Files" }
120
250
</ Button >
121
251
</ div >
122
252
0 commit comments