mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 07:51:13 -05:00
166 lines
6.4 KiB
TypeScript
166 lines
6.4 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
import { Separator } from '@/components/ui/separator';
|
|
import { ArrowRight, Plus, Minus, Edit } from 'lucide-react';
|
|
import { useEntityVersions } from '@/hooks/useEntityVersions';
|
|
import type { EntityType } from '@/types/versioning';
|
|
|
|
interface VersionComparisonDialogProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
entityType: EntityType;
|
|
entityId: string;
|
|
fromVersionId: string;
|
|
toVersionId: string;
|
|
}
|
|
|
|
interface VersionDiff {
|
|
[fieldName: string]: {
|
|
from: unknown;
|
|
to: unknown;
|
|
};
|
|
}
|
|
|
|
export function VersionComparisonDialog({
|
|
open,
|
|
onOpenChange,
|
|
entityType,
|
|
entityId,
|
|
fromVersionId,
|
|
toVersionId,
|
|
}: VersionComparisonDialogProps) {
|
|
const { versions, compareVersions } = useEntityVersions(entityType, entityId);
|
|
const [diff, setDiff] = useState<VersionDiff | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const fromVersion = versions.find(v => v.version_id === fromVersionId);
|
|
const toVersion = versions.find(v => v.version_id === toVersionId);
|
|
|
|
useEffect(() => {
|
|
const loadDiff = async () => {
|
|
if (open && fromVersionId && toVersionId) {
|
|
setLoading(true);
|
|
const result = await compareVersions(fromVersionId, toVersionId);
|
|
setDiff(result as VersionDiff);
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
loadDiff();
|
|
}, [open, fromVersionId, toVersionId, compareVersions]);
|
|
|
|
const formatValue = (value: any): string => {
|
|
if (value === null || value === undefined) return 'null';
|
|
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
if (typeof value === 'object') return JSON.stringify(value, null, 2);
|
|
return String(value);
|
|
};
|
|
|
|
const formatFieldName = (fieldName: string): string => {
|
|
return fieldName
|
|
.split('_')
|
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
.join(' ');
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>Compare Versions</DialogTitle>
|
|
<div className="flex items-center gap-4 text-sm text-muted-foreground mt-2">
|
|
<div className="flex items-center gap-2">
|
|
<Badge variant="outline">Version {fromVersion?.version_number}</Badge>
|
|
<span>{new Date(fromVersion?.created_at || '').toLocaleString()}</span>
|
|
</div>
|
|
<ArrowRight className="h-4 w-4" />
|
|
<div className="flex items-center gap-2">
|
|
<Badge variant="outline">Version {toVersion?.version_number}</Badge>
|
|
<span>{new Date(toVersion?.created_at || '').toLocaleString()}</span>
|
|
</div>
|
|
</div>
|
|
</DialogHeader>
|
|
|
|
<ScrollArea className="h-[500px] pr-4">
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-8">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
|
</div>
|
|
) : diff && Object.keys(diff).length > 0 ? (
|
|
<div className="space-y-4">
|
|
{Object.entries(diff).map(([fieldName, changes]: [string, any]) => {
|
|
const isAdded = changes.from === null;
|
|
const isRemoved = changes.to === null;
|
|
const isModified = changes.from !== null && changes.to !== null;
|
|
|
|
return (
|
|
<div key={fieldName} className="border rounded-lg p-4 space-y-2">
|
|
<div className="flex items-center gap-2">
|
|
{isAdded && <Plus className="h-4 w-4 text-green-500" />}
|
|
{isRemoved && <Minus className="h-4 w-4 text-red-500" />}
|
|
{isModified && <Edit className="h-4 w-4 text-blue-500" />}
|
|
<span className="font-medium">{formatFieldName(fieldName)}</span>
|
|
<Badge
|
|
variant="outline"
|
|
className={
|
|
isAdded
|
|
? 'bg-green-500/10 text-green-700 dark:text-green-400'
|
|
: isRemoved
|
|
? 'bg-red-500/10 text-red-700 dark:text-red-400'
|
|
: 'bg-blue-500/10 text-blue-700 dark:text-blue-400'
|
|
}
|
|
>
|
|
{isAdded ? 'Added' : isRemoved ? 'Removed' : 'Modified'}
|
|
</Badge>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
{/* From value */}
|
|
<div className="space-y-1">
|
|
<div className="text-xs text-muted-foreground font-medium">Before</div>
|
|
<div
|
|
className={`p-3 rounded-md font-mono text-xs whitespace-pre-wrap ${
|
|
isRemoved
|
|
? 'bg-red-500/10 text-red-700 dark:text-red-400 line-through'
|
|
: 'bg-muted'
|
|
}`}
|
|
>
|
|
{formatValue(changes.from)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* To value */}
|
|
<div className="space-y-1">
|
|
<div className="text-xs text-muted-foreground font-medium">After</div>
|
|
<div
|
|
className={`p-3 rounded-md font-mono text-xs whitespace-pre-wrap ${
|
|
isAdded
|
|
? 'bg-green-500/10 text-green-700 dark:text-green-400 font-semibold'
|
|
: isModified
|
|
? 'bg-blue-500/10 text-blue-700 dark:text-blue-400 font-semibold'
|
|
: 'bg-muted'
|
|
}`}
|
|
>
|
|
{formatValue(changes.to)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
<p>No differences found between these versions</p>
|
|
</div>
|
|
)}
|
|
</ScrollArea>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|