Refactor code structure and remove redundant changes

This commit is contained in:
pacnpal
2025-11-09 16:31:34 -05:00
parent 2884bc23ce
commit eb68cf40c6
1080 changed files with 27361 additions and 56687 deletions

View File

@@ -0,0 +1,216 @@
import { useState } from 'react';
import { Clock, GitBranch, User, FileText, RotateCcw, GitCompare } from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Separator } from '@/components/ui/separator';
import { ScrollArea } from '@/components/ui/scroll-area';
import { VersionComparisonDialog } from './VersionComparisonDialog';
import { RollbackDialog } from './RollbackDialog';
import { useEntityVersions } from '@/hooks/useEntityVersions';
import { useUserRole } from '@/hooks/useUserRole';
import type { EntityType } from '@/types/versioning';
interface EntityVersionHistoryProps {
entityType: EntityType;
entityId: string;
entityName: string;
}
const changeTypeColors = {
created: 'bg-green-500/10 text-green-700 dark:text-green-400',
updated: 'bg-blue-500/10 text-blue-700 dark:text-blue-400',
deleted: 'bg-red-500/10 text-red-700 dark:text-red-400',
restored: 'bg-purple-500/10 text-purple-700 dark:text-purple-400',
archived: 'bg-gray-500/10 text-gray-700 dark:text-gray-400',
};
export function EntityVersionHistory({ entityType, entityId, entityName }: EntityVersionHistoryProps) {
const { versions, loading, rollbackToVersion } = useEntityVersions(entityType, entityId);
const { isModerator } = useUserRole();
const [selectedVersions, setSelectedVersions] = useState<string[]>([]);
const [compareDialogOpen, setCompareDialogOpen] = useState(false);
const [rollbackDialogOpen, setRollbackDialogOpen] = useState(false);
const [selectedVersionForRollback, setSelectedVersionForRollback] = useState<string | null>(null);
const handleVersionSelect = (versionId: string) => {
setSelectedVersions(prev => {
if (prev.includes(versionId)) {
return prev.filter(id => id !== versionId);
}
if (prev.length >= 2) {
return [prev[1], versionId];
}
return [...prev, versionId];
});
};
const handleCompare = () => {
if (selectedVersions.length === 2) {
setCompareDialogOpen(true);
}
};
const handleRollback = (versionId: string) => {
setSelectedVersionForRollback(versionId);
setRollbackDialogOpen(true);
};
if (loading) {
return (
<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>
);
}
if (versions.length === 0) {
return (
<div className="text-center py-8 text-muted-foreground">
<FileText className="h-12 w-12 mx-auto mb-3 opacity-50" />
<p>No version history available</p>
</div>
);
}
return (
<div className="space-y-4">
{/* Header Actions */}
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold">Version History</h3>
<p className="text-sm text-muted-foreground">{versions.length} versions</p>
</div>
{selectedVersions.length === 2 && (
<Button onClick={handleCompare} size="sm">
<GitCompare className="h-4 w-4 mr-2" />
Compare Selected
</Button>
)}
</div>
{/* Timeline */}
<ScrollArea className="h-[600px] pr-4">
<div className="relative space-y-4">
{/* Timeline line */}
<div className="absolute left-6 top-0 bottom-0 w-px bg-border" />
{versions.map((version, index) => (
<Card
key={version.version_id}
className={`relative pl-16 pr-4 py-4 cursor-pointer transition-colors ${
selectedVersions.includes(version.version_id)
? 'border-primary bg-accent'
: 'hover:border-primary/50'
} ${version.is_current ? 'border-primary shadow-md' : ''}`}
onClick={() => handleVersionSelect(version.version_id)}
>
{/* Timeline dot */}
<div
className={`absolute left-4 top-4 w-5 h-5 rounded-full border-4 border-background ${
version.is_current
? 'bg-primary'
: 'bg-muted-foreground'
}`}
/>
<div className="space-y-3">
{/* Header */}
<div className="flex items-start justify-between gap-4">
<div className="flex items-center gap-2 flex-1">
<Badge variant="outline" className={changeTypeColors[version.change_type]}>
{version.change_type}
</Badge>
<span className="font-medium">Version {version.version_number}</span>
{version.is_current && (
<Badge variant="default">Current</Badge>
)}
</div>
{!version.is_current && isModerator() && (
<Button
size="sm"
variant="ghost"
onClick={(e) => {
e.stopPropagation();
handleRollback(version.version_id);
}}
title="Moderator only: Restore this version"
>
<RotateCcw className="h-4 w-4 mr-1" />
Restore
</Button>
)}
</div>
{/* Metadata */}
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<div className="flex items-center gap-2">
<Avatar className="h-6 w-6">
<AvatarImage src={version.profiles?.avatar_url || undefined} />
<AvatarFallback>
<User className="h-3 w-3" />
</AvatarFallback>
</Avatar>
<span>{version.profiles?.display_name || version.profiles?.username || 'Unknown'}</span>
</div>
<div className="flex items-center gap-1">
<Clock className="h-4 w-4" />
<span>{formatDistanceToNow(new Date(version.created_at), { addSuffix: true })}</span>
</div>
{version.submission_id && (
<div className="flex items-center gap-1">
<GitBranch className="h-4 w-4" />
<span>Submission</span>
</div>
)}
</div>
{/* Change Reason */}
{version.change_reason && (
<>
<Separator />
<p className="text-sm italic">{version.change_reason}</p>
</>
)}
</div>
</Card>
))}
</div>
</ScrollArea>
{/* Dialogs */}
{selectedVersions.length === 2 && (
<VersionComparisonDialog
open={compareDialogOpen}
onOpenChange={setCompareDialogOpen}
entityType={entityType}
entityId={entityId}
fromVersionId={selectedVersions[0]}
toVersionId={selectedVersions[1]}
/>
)}
{selectedVersionForRollback && (
<RollbackDialog
open={rollbackDialogOpen}
onOpenChange={setRollbackDialogOpen}
versionId={selectedVersionForRollback}
entityType={entityType}
entityId={entityId}
entityName={entityName}
onRollback={async (reason) => {
await rollbackToVersion(selectedVersionForRollback, reason);
setRollbackDialogOpen(false);
setSelectedVersionForRollback(null);
}}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,57 @@
import { useEffect, useState } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { ScrollArea } from '@/components/ui/scroll-area';
import { AlertCircle } from 'lucide-react';
import { Alert, AlertDescription } from '@/components/ui/alert';
import type { EntityType } from '@/types/versioning';
interface FieldHistoryDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
entityType: EntityType;
entityId: string;
fieldName: string;
}
/**
* Field-level history has been removed in the relational versioning system.
* Use version comparison instead to see field changes between versions.
* This component now shows a helpful message directing users to use version comparison.
*/
export function FieldHistoryDialog({
open,
onOpenChange,
entityType,
entityId,
fieldName,
}: FieldHistoryDialogProps) {
const formatFieldName = (name: string): string => {
return name
.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Field History: {formatFieldName(fieldName)}</DialogTitle>
</DialogHeader>
<div className="py-6">
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
<p className="font-medium mb-2">Field-level history is not available in the current versioning system.</p>
<p className="text-sm text-muted-foreground">
To see changes to the "{formatFieldName(fieldName)}" field, please use the <strong>Version Comparison</strong> feature
in the Version History tab. This will show you all field changes between any two versions.
</p>
</AlertDescription>
</Alert>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,115 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Calendar, MapPin, ArrowRight, Building2 } from 'lucide-react';
import { format } from 'date-fns';
import { parseDateForDisplay } from '@/lib/dateUtils';
interface HistoricalEntityCardProps {
type: 'park' | 'ride';
entity: {
id: string;
name: string;
operated_from?: string;
operated_until?: string;
closure_reason?: string;
removal_reason?: string;
location?: {
name: string;
city?: string;
country: string;
};
successor?: {
id: string;
name: string;
slug: string;
};
relocated_to?: {
id: string;
name: string;
slug: string;
};
};
onViewDetails?: () => void;
}
export function HistoricalEntityCard({ type, entity, onViewDetails }: HistoricalEntityCardProps) {
const reason = type === 'park' ? entity.closure_reason : entity.removal_reason;
const hasSuccessor = type === 'park' ? !!entity.successor : !!entity.relocated_to;
const successorInfo = type === 'park' ? entity.successor : entity.relocated_to;
return (
<Card className="border-dashed">
<CardHeader>
<div className="flex items-start justify-between">
<div>
<CardTitle className="flex items-center gap-2">
{entity.name}
<Badge variant="secondary" className="bg-gray-500/10">
Historical
</Badge>
</CardTitle>
<CardDescription className="flex items-center gap-1 mt-1">
<MapPin className="h-3 w-3" />
{entity.location?.city && `${entity.location.city}, `}
{entity.location?.country || entity.location?.name}
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* Operating Dates */}
<div className="flex items-center gap-4 text-sm">
<div className="flex items-center gap-1 text-muted-foreground">
<Calendar className="h-4 w-4" />
<span>Operated:</span>
</div>
<div className="font-medium">
{/* ⚠️ Use parseDateForDisplay to prevent timezone shifts */}
{entity.operated_from && format(parseDateForDisplay(entity.operated_from), 'MMM d, yyyy')}
{' - '}
{entity.operated_until && format(parseDateForDisplay(entity.operated_until), 'MMM d, yyyy')}
</div>
</div>
{/* Closure/Removal Reason */}
{reason && (
<div className="p-3 rounded-md bg-muted">
<div className="text-xs text-muted-foreground font-medium mb-1">
{type === 'park' ? 'Closure Reason' : 'Removal Reason'}
</div>
<p className="text-sm">{reason}</p>
</div>
)}
{/* Successor/Relocation Info */}
{hasSuccessor && successorInfo && (
<div className="flex items-center gap-2 p-3 rounded-md bg-primary/5 border border-primary/20">
<Building2 className="h-4 w-4 text-primary" />
<span className="text-sm">
{type === 'park' ? 'Succeeded by' : 'Relocated to'}:
</span>
<Button
variant="link"
className="h-auto p-0 text-primary"
onClick={() => {
window.location.href = `/${type}s/${successorInfo.slug}`;
}}
>
{successorInfo.name}
<ArrowRight className="h-3 w-3 ml-1" />
</Button>
</div>
)}
{/* View Details Button */}
{onViewDetails && (
<Button variant="outline" className="w-full" onClick={onViewDetails}>
View Full History
</Button>
)}
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,92 @@
import { useState } from 'react';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { AlertTriangle } from 'lucide-react';
import { Alert, AlertDescription } from '@/components/ui/alert';
interface RollbackDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
versionId: string;
entityType: string;
entityId: string;
entityName: string;
onRollback: (reason: string) => Promise<void>;
}
export function RollbackDialog({
open,
onOpenChange,
versionId,
entityName,
onRollback,
}: RollbackDialogProps) {
const [reason, setReason] = useState('');
const [loading, setLoading] = useState(false);
const handleRollback = async () => {
if (!reason.trim()) return;
setLoading(true);
try {
await onRollback(reason);
setReason('');
onOpenChange(false);
} finally {
setLoading(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Restore Previous Version (Moderator Action)</DialogTitle>
<DialogDescription>
You are about to restore "{entityName}" to a previous version. This will create a new version with the restored data. This action will be logged in the audit trail.
</DialogDescription>
</DialogHeader>
<Alert variant="default" className="border-yellow-500/50 bg-yellow-500/10">
<AlertTriangle className="h-4 w-4 text-yellow-600 dark:text-yellow-500" />
<AlertDescription className="text-yellow-900 dark:text-yellow-200">
This action will restore the entity to its previous state. The current version will be preserved in history.
</AlertDescription>
</Alert>
<div className="space-y-2">
<Label htmlFor="rollback-reason">Reason for rollback *</Label>
<Textarea
id="rollback-reason"
placeholder="Explain why you're rolling back to this version..."
value={reason}
onChange={(e) => setReason(e.target.value)}
rows={4}
required
/>
<p className="text-xs text-muted-foreground">
This reason will be logged in the version history for audit purposes.
</p>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => onOpenChange(false)}
disabled={loading}
>
Cancel
</Button>
<Button
onClick={handleRollback}
disabled={!reason.trim() || loading}
>
{loading ? 'Restoring...' : 'Restore Version'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,165 @@
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>
);
}

View File

@@ -0,0 +1,101 @@
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { History, Clock } from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
import { EntityVersionHistory } from './EntityVersionHistory';
import { useEntityVersions } from '@/hooks/useEntityVersions';
import type { EntityType } from '@/types/versioning';
interface VersionIndicatorProps {
entityType: EntityType;
entityId: string;
entityName: string;
compact?: boolean;
}
export function VersionIndicator({
entityType,
entityId,
entityName,
compact = false,
}: VersionIndicatorProps) {
const [showHistory, setShowHistory] = useState(false);
const { currentVersion, loading } = useEntityVersions(entityType, entityId);
if (loading || !currentVersion) {
return null;
}
const timeAgo = currentVersion.created_at
? formatDistanceToNow(new Date(currentVersion.created_at), { addSuffix: true })
: 'Unknown';
if (compact) {
return (
<>
<Button
variant="ghost"
size="sm"
onClick={() => setShowHistory(true)}
className="gap-2"
>
<History className="h-4 w-4" />
<span className="text-xs text-muted-foreground">
v{currentVersion.version_number}
</span>
</Button>
<Dialog open={showHistory} onOpenChange={setShowHistory}>
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Version History: {entityName}</DialogTitle>
</DialogHeader>
<EntityVersionHistory
entityType={entityType}
entityId={entityId}
entityName={entityName}
/>
</DialogContent>
</Dialog>
</>
);
}
return (
<>
<div className="flex items-center gap-3">
<Badge variant="outline" className="gap-1.5">
<Clock className="h-3 w-3" />
Version {currentVersion.version_number}
</Badge>
<span className="text-sm text-muted-foreground">
Last edited {timeAgo}
</span>
<Button
variant="outline"
size="sm"
onClick={() => setShowHistory(true)}
className="gap-2"
>
<History className="h-4 w-4" />
View History
</Button>
</div>
<Dialog open={showHistory} onOpenChange={setShowHistory}>
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Version History: {entityName}</DialogTitle>
</DialogHeader>
<EntityVersionHistory
entityType={entityType}
entityId={entityId}
entityName={entityName}
/>
</DialogContent>
</Dialog>
</>
);
}