Skip to content

Commit 507cb95

Browse files
committed
Added comparator page
1 parent 23cafc3 commit 507cb95

File tree

17 files changed

+898
-40
lines changed

17 files changed

+898
-40
lines changed
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
"use client"
2+
3+
import type React from "react"
4+
5+
import { useEffect, useState } from "react"
6+
import { Button } from "~/components/ui/button"
7+
import { X, ChevronDown, ChevronRight } from "lucide-react"
8+
import { generateUnifiedDiff } from "~/lib/diff-generator"
9+
10+
interface DiffViewerProps {
11+
file1: { name: string; content: any }
12+
file2: { name: string; content: any }
13+
onClose: () => void
14+
}
15+
16+
interface DiffNode {
17+
key: string
18+
path: string
19+
type: "added" | "removed" | "changed" | "unchanged"
20+
value1?: any
21+
value2?: any
22+
children?: DiffNode[]
23+
isExpanded?: boolean
24+
}
25+
26+
export default function DiffViewer({ file1, file2, onClose }: DiffViewerProps) {
27+
const [diffTree, setDiffTree] = useState<DiffNode | null>(null)
28+
const [isLoading, setIsLoading] = useState(true)
29+
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set())
30+
31+
useEffect(() => {
32+
setIsLoading(true)
33+
const result = generateUnifiedDiff(file1.content, file2.content)
34+
35+
// Initially expand all nodes that have changes
36+
const nodesToExpand = new Set<string>()
37+
38+
function collectExpandedNodes(node: DiffNode) {
39+
if (node.type !== "unchanged" || (node.children && node.children.some((child) => child.type !== "unchanged"))) {
40+
nodesToExpand.add(node.path)
41+
}
42+
43+
if (node.children) {
44+
node.children.forEach(collectExpandedNodes)
45+
}
46+
}
47+
48+
collectExpandedNodes(result)
49+
setExpandedNodes(nodesToExpand)
50+
setDiffTree(result)
51+
setIsLoading(false)
52+
}, [file1, file2])
53+
54+
useEffect(() => {
55+
// Disable scrolling on body when modal is open
56+
document.body.style.overflow = "hidden"
57+
return () => {
58+
document.body.style.overflow = "auto"
59+
}
60+
}, [])
61+
62+
const toggleNode = (path: string, e: React.MouseEvent) => {
63+
e.stopPropagation() // Prevent event bubbling
64+
setExpandedNodes((prev) => {
65+
const newSet = new Set(prev)
66+
if (newSet.has(path)) {
67+
newSet.delete(path)
68+
} else {
69+
newSet.add(path)
70+
}
71+
return newSet
72+
})
73+
}
74+
75+
const renderDiffTree = (node: DiffNode, level = 0, isLastChild = true) => {
76+
const indent = level * 20
77+
const isExpanded = expandedNodes.has(node.path)
78+
const hasChildren = node.children && node.children.length > 0
79+
80+
// Determine if this is a primitive value or an object/array
81+
const isPrimitive =
82+
(typeof node.value1 !== "object" || node.value1 === null || Array.isArray(node.value1)) &&
83+
(typeof node.value2 !== "object" || node.value2 === null || Array.isArray(node.value2))
84+
85+
// Determine if this node or any of its children have changes
86+
const hasChanges =
87+
node.type !== "unchanged" ||
88+
(node.children &&
89+
node.children.some(
90+
(child) =>
91+
child.type !== "unchanged" ||
92+
(child.children && child.children.some((grandchild) => grandchild.type !== "unchanged")),
93+
))
94+
95+
return (
96+
<div key={node.path} className="relative">
97+
{/* Render the node itself */}
98+
<div
99+
className={`flex items-start rounded-sm ${getNodeBackground(node.type)}`}
100+
style={{ paddingLeft: `${indent}px` }}
101+
>
102+
{/* Expand/collapse button for objects/arrays */}
103+
{hasChildren && (
104+
<button onClick={(e) => toggleNode(node.path, e)} className="mr-1 p-1 hover:bg-gray-200 rounded">
105+
{isExpanded ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
106+
</button>
107+
)}
108+
109+
{/* Key name */}
110+
<div className={`font-mono py-1 pr-2 flex-shrink-0 ${getTextColor(node.type)}`}>
111+
{node.type === "added" && "+ "}
112+
{node.type === "removed" && "- "}
113+
{node.key}
114+
{hasChildren && !isPrimitive && ": {"}
115+
{!hasChildren && !isPrimitive && node.type === "unchanged" && ": {}"}
116+
</div>
117+
118+
{/* Value for primitive types */}
119+
{isPrimitive && (
120+
<div className="flex flex-col w-full">
121+
{node.type === "removed" && (
122+
<div className="bg-red-50 text-red-800 py-1 px-2 rounded font-mono">{formatValue(node.value1)}</div>
123+
)}
124+
{node.type === "added" && (
125+
<div className="bg-green-50 text-green-800 py-1 px-2 rounded font-mono">{formatValue(node.value2)}</div>
126+
)}
127+
{node.type === "changed" && (
128+
<>
129+
<div className="bg-red-50 text-red-800 py-1 px-2 rounded font-mono mb-1">
130+
- {formatValue(node.value1)}
131+
</div>
132+
<div className="bg-green-50 text-green-800 py-1 px-2 rounded font-mono">
133+
+ {formatValue(node.value2)}
134+
</div>
135+
</>
136+
)}
137+
{node.type === "unchanged" && (
138+
<div className="py-1 px-2 font-mono">
139+
{formatValue(node.value1 !== undefined ? node.value1 : node.value2)}
140+
</div>
141+
)}
142+
</div>
143+
)}
144+
</div>
145+
146+
{/* Render children if expanded */}
147+
{hasChildren && isExpanded && (
148+
<div>
149+
{node.children!.map((child, index) =>
150+
renderDiffTree(child, level + 1, index === node.children!.length - 1),
151+
)}
152+
{!isPrimitive && (
153+
<div className={`font-mono py-1 ${getTextColor(node.type)}`} style={{ paddingLeft: `${indent}px` }}>
154+
{node.type === "added" && "+ "}
155+
{node.type === "removed" && "- "}
156+
{"}"}
157+
</div>
158+
)}
159+
</div>
160+
)}
161+
</div>
162+
)
163+
}
164+
165+
const getNodeBackground = (type: string) => {
166+
switch (type) {
167+
case "added":
168+
return "bg-green-50"
169+
case "removed":
170+
return "bg-red-50"
171+
case "changed":
172+
return "bg-orange-50"
173+
default:
174+
return ""
175+
}
176+
}
177+
178+
const getTextColor = (type: string) => {
179+
switch (type) {
180+
case "added":
181+
return "text-green-800"
182+
case "removed":
183+
return "text-red-800"
184+
case "changed":
185+
return "text-orange-800"
186+
default:
187+
return ""
188+
}
189+
}
190+
191+
const formatValue = (value: any): string => {
192+
if (value === undefined) return "undefined"
193+
if (value === null) return "null"
194+
195+
if (typeof value === "object") {
196+
return JSON.stringify(value)
197+
}
198+
199+
return String(value)
200+
}
201+
202+
return (
203+
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
204+
<div className="bg-white rounded-lg shadow-xl w-full max-w-6xl max-h-[90vh] flex flex-col">
205+
<div className="flex items-center justify-between p-4 border-b">
206+
<h2 className="text-xl font-bold">File Comparison Results</h2>
207+
<Button variant="ghost" size="icon" onClick={onClose}>
208+
<X className="h-5 w-5" />
209+
</Button>
210+
</div>
211+
212+
<div className="p-4 border-b bg-gray-50">
213+
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
214+
<div>
215+
<p className="text-sm font-medium">
216+
Old File: <span className="font-normal">{file1.name}</span>
217+
</p>
218+
<p className="text-sm font-medium">
219+
New File: <span className="font-normal">{file2.name}</span>
220+
</p>
221+
</div>
222+
<div className="flex items-center gap-4">
223+
<div className="flex items-center gap-2">
224+
<span className="w-3 h-3 rounded-full bg-green-500"></span>
225+
<span className="text-sm">Added</span>
226+
</div>
227+
<div className="flex items-center gap-2">
228+
<span className="w-3 h-3 rounded-full bg-orange-500"></span>
229+
<span className="text-sm">Changed</span>
230+
</div>
231+
<div className="flex items-center gap-2">
232+
<span className="w-3 h-3 rounded-full bg-red-500"></span>
233+
<span className="text-sm">Removed</span>
234+
</div>
235+
</div>
236+
</div>
237+
</div>
238+
239+
<div className="flex-1 overflow-auto p-4">
240+
{isLoading ? (
241+
<div className="flex items-center justify-center h-full">
242+
<p>Loading comparison...</p>
243+
</div>
244+
) : diffTree?.type === "unchanged" ? (
245+
<>
246+
<div className="flex items-center justify-center h-full">
247+
<p className="text-gray-500">No differences found. The files are identical.</p>
248+
</div>
249+
<div className="text-sm">
250+
{diffTree.children?.map((child, index) =>
251+
renderDiffTree(child, 1, index === (diffTree.children?.length || 0) - 1),
252+
)}
253+
</div>
254+
</>
255+
) : !!diffTree ? (
256+
<div className="text-sm">
257+
{diffTree.children?.map((child, index) =>
258+
renderDiffTree(child, 1, index === (diffTree.children?.length || 0) - 1),
259+
)}
260+
</div>
261+
) : (
262+
<div className="flex items-center justify-center h-full">
263+
<p className="text-red-500">An Error Occurred :/.</p>
264+
</div>
265+
)}
266+
</div>
267+
268+
<div className="p-4 border-t flex justify-end">
269+
<Button onClick={onClose}>Close</Button>
270+
</div>
271+
</div>
272+
</div>
273+
)
274+
}
275+

0 commit comments

Comments
 (0)