app/components/DiffViewer.tsx (137 lines of code) (raw):
import { FileDiff } from "~/types/job";
interface DiffViewerProps {
files: FileDiff[];
}
interface FileHeaderProps {
file: FileDiff;
}
const FileHeader = ({ file }: FileHeaderProps) => {
const getStatusColor = (status: FileDiff["status"]) => {
switch (status) {
case "added":
return "text-green-600";
case "deleted":
return "text-red-600";
case "modified":
return "text-blue-600";
case "renamed":
return "text-purple-600";
default:
return "text-gray-600";
}
};
const getStatusIcon = (status: FileDiff["status"]) => {
switch (status) {
case "added":
return <i className="fas fa-plus"></i>;
case "deleted":
return <i className="fas fa-minus"></i>;
case "modified":
return <i className="fas fa-edit"></i>;
case "renamed":
return <i className="fas fa-arrow-right"></i>;
default:
return <i className="fas fa-question"></i>;
}
};
return (
<div className="border-b border-gray-200 bg-gray-50 px-4 py-3 dark:border-gray-600">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className={`font-mono text-sm ${getStatusColor(file.status)}`}>
{getStatusIcon(file.status)}
</span>
<span className="font-mono text-sm">
{file.oldFilename && file.status === "renamed"
? `${file.oldFilename} → ${file.filename}`
: file.filename}
</span>
</div>
<div className="flex items-center gap-4 text-sm text-gray-600">
{file.additions > 0 && (
<span className="text-green-600">+{file.additions}</span>
)}
{file.deletions > 0 && (
<span className="text-red-600">−{file.deletions}</span>
)}
</div>
</div>
</div>
);
};
// const DiffLines = ({ patch }: { patch: string }) => {
// const lines = patch.split('\n');
// return (
// <div className="font-mono text-sm">
// {lines.map((line, index) => {
// let lineClass = 'px-4 py-0.5 ';
// let lineNumberClass = 'text-gray-400 select-none pr-4 ';
// if (line.startsWith('+')) {
// lineClass += 'bg-green-50 text-green-800';
// } else if (line.startsWith('-')) {
// lineClass += 'bg-red-50 text-red-800';
// } else if (line.startsWith('@@')) {
// lineClass += 'bg-blue-50 text-blue-800 font-medium';
// } else {
// lineClass += 'text-gray-700';
// }
// return (
// <div
// key={index}
// className={`flex ${lineClass} hover:bg-opacity-75`}
// >
// <span className={lineNumberClass}>
// {index + 1}
// </span>
// <span className="flex-1 whitespace-pre-wrap break-all">
// {line}
// </span>
// </div>
// );
// })}
// </div>
// );
// };
const DiffLines = ({ patch }: { patch: string }) => {
const lines = patch.split("\n");
return (
<div className="font-mono text-sm">
{lines.map((line, index) => {
let lineClass = "px-4 py-0.5 ";
let lineNumberClass =
"text-gray-400 dark:text-gray-500 select-none pr-4 ";
if (line.startsWith("+")) {
lineClass +=
"bg-green-50 text-green-800 dark:bg-green-900/40 dark:text-green-300";
} else if (line.startsWith("-")) {
lineClass +=
"bg-red-50 text-red-800 dark:bg-red-900/40 dark:text-red-300";
} else if (line.startsWith("@@")) {
lineClass +=
"bg-blue-50 text-blue-800 font-medium dark:bg-blue-900/40 dark:text-blue-300";
} else {
lineClass += "text-gray-700 dark:text-gray-300";
}
return (
<div key={index} className={`flex ${lineClass} hover:bg-opacity-75`}>
<span className={lineNumberClass}>{index + 1}</span>
<span className="flex-1 whitespace-pre-wrap break-all">{line}</span>
</div>
);
})}
</div>
);
};
export const DiffViewer = ({ files }: DiffViewerProps) => {
if (files.length === 0) {
return (
<div className="py-12 text-center text-gray-500">
<p>No changes to display</p>
</div>
);
}
return (
<div className="space-y-6">
{files.map((file, index) => (
<div
key={index}
className="overflow-hidden rounded-lg border border-gray-200 dark:border-gray-600"
>
<FileHeader file={file} />
<div className="max-h-[50vh] overflow-y-auto">
<DiffLines patch={file.patch} />
</div>
</div>
))}
</div>
);
};