better version timeline
This commit is contained in:
parent
57c1f12893
commit
63cbeaa0fc
@ -184,26 +184,35 @@ export const VersionTimeline = forwardRef<VersionTimelineRef, VersionTimelinePro
|
||||
<>
|
||||
<div className="space-y-4">
|
||||
{/* Header */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<History className="h-4 w-4 text-muted-foreground" />
|
||||
<h3 className="font-medium text-foreground">{t('versionHistory')}</h3>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
({versions.length})
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<History className="h-4 w-4 text-muted-foreground" />
|
||||
<h3 className="font-semibold text-foreground">{t('versionHistory')}</h3>
|
||||
</div>
|
||||
<span className="text-xs font-medium text-muted-foreground bg-muted px-2 py-1 rounded-full">
|
||||
{versions.length} {versions.length === 1 ? 'version' : 'versions'}
|
||||
</span>
|
||||
</div>
|
||||
{userLimits && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
<div className="flex items-center justify-between">
|
||||
<span>Limit: {userLimits.versionLimit}/{userLimits.maxVersionLimit}</span>
|
||||
<span className="capitalize">{userLimits.subscribePlan}</span>
|
||||
<div className="bg-muted/50 rounded-lg p-3 space-y-2">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="font-medium text-muted-foreground">Storage used</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="font-medium text-foreground">{versions.length}/{userLimits.maxVersionLimit}</span>
|
||||
<span className="text-primary bg-primary/10 px-1.5 py-0.5 rounded text-xs font-semibold capitalize">
|
||||
{userLimits.subscribePlan}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full bg-muted rounded-full h-1 mt-1">
|
||||
<div className="w-full bg-background rounded-full h-2 overflow-hidden">
|
||||
<div
|
||||
className={`h-1 rounded-full transition-all ${
|
||||
versions.length >= userLimits.versionLimit ? 'bg-orange-500' : 'bg-primary'
|
||||
className={`h-full rounded-full transition-all duration-300 ${
|
||||
versions.length >= userLimits.versionLimit
|
||||
? 'bg-gradient-to-r from-orange-500 to-red-500'
|
||||
: 'bg-gradient-to-r from-primary to-primary/70'
|
||||
}`}
|
||||
style={{ width: `${Math.min(100, (versions.length / userLimits.versionLimit) * 100)}%` }}
|
||||
style={{ width: `${Math.min(100, (versions.length / userLimits.maxVersionLimit) * 100)}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
@ -213,9 +222,9 @@ export const VersionTimeline = forwardRef<VersionTimelineRef, VersionTimelinePro
|
||||
{/* Timeline */}
|
||||
<div className="relative">
|
||||
{/* Timeline line */}
|
||||
<div className="absolute left-4 top-0 bottom-0 w-px bg-border"></div>
|
||||
<div className="absolute left-4 top-0 bottom-0 w-0.5 bg-gradient-to-b from-primary/20 via-border to-transparent"></div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-2">
|
||||
{versions.map((version, index) => {
|
||||
const isSelected = selectedVersionId === version.id
|
||||
const isLatest = index === 0
|
||||
@ -224,90 +233,101 @@ export const VersionTimeline = forwardRef<VersionTimelineRef, VersionTimelinePro
|
||||
return (
|
||||
<div
|
||||
key={version.id}
|
||||
className={`relative pl-10 pr-2 py-2 rounded-lg cursor-pointer transition-all hover:bg-muted/50 ${
|
||||
isSelected ? 'bg-muted' : ''
|
||||
className={`group relative pl-10 pr-3 py-3 rounded-lg cursor-pointer transition-all duration-200 hover:bg-muted/70 hover:shadow-sm border border-transparent ${
|
||||
isSelected
|
||||
? 'bg-primary/5 border-primary/20 shadow-sm'
|
||||
: 'hover:border-border/50'
|
||||
}`}
|
||||
onClick={() => handleVersionSelect(version)}
|
||||
>
|
||||
{/* Timeline dot */}
|
||||
<div className={`absolute left-2.5 top-4 w-3 h-3 rounded-full border-2 border-background ${
|
||||
<div className={`absolute left-2.5 top-5 w-3.5 h-3.5 rounded-full transition-all duration-200 ${
|
||||
isCurrentVersion && hasUnsavedChanges
|
||||
? 'bg-orange-500'
|
||||
? 'bg-orange-500 ring-2 ring-orange-200 dark:ring-orange-900 animate-pulse'
|
||||
: isSelected
|
||||
? 'bg-primary'
|
||||
? 'bg-primary ring-2 ring-primary/20 scale-110'
|
||||
: isLatest
|
||||
? 'bg-green-500'
|
||||
: 'bg-muted-foreground'
|
||||
? 'bg-green-500 ring-2 ring-green-200 dark:ring-green-900'
|
||||
: 'bg-muted-foreground/60 group-hover:bg-muted-foreground group-hover:scale-105'
|
||||
}`}>
|
||||
{isCurrentVersion && hasUnsavedChanges && (
|
||||
<div className="absolute inset-0 animate-pulse bg-orange-500 rounded-full"></div>
|
||||
{/* Inner highlight for selected */}
|
||||
{isSelected && (
|
||||
<div className="absolute inset-1 bg-background rounded-full"></div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className={`text-sm font-medium ${
|
||||
isSelected ? 'text-foreground' : 'text-muted-foreground'
|
||||
<div className="space-y-2">
|
||||
{/* Version header */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center space-x-2 min-w-0 flex-1">
|
||||
<span className={`text-sm font-semibold transition-colors ${
|
||||
isSelected ? 'text-primary' : 'text-foreground'
|
||||
}`}>
|
||||
v{version.version}
|
||||
Version {version.version}
|
||||
</span>
|
||||
{isLatest && (
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300 rounded-full">
|
||||
Latest
|
||||
</span>
|
||||
)}
|
||||
{isCurrentVersion && hasUnsavedChanges && (
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 text-xs font-medium bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300 rounded-full">
|
||||
Modified
|
||||
</span>
|
||||
)}
|
||||
<div className="flex items-center space-x-1">
|
||||
{isLatest && (
|
||||
<span className="inline-flex items-center px-2 py-0.5 text-xs font-semibold bg-green-500/10 text-green-700 dark:text-green-400 rounded-full border border-green-200 dark:border-green-800">
|
||||
Latest
|
||||
</span>
|
||||
)}
|
||||
{isCurrentVersion && hasUnsavedChanges && (
|
||||
<span className="inline-flex items-center px-2 py-0.5 text-xs font-semibold bg-orange-500/10 text-orange-700 dark:text-orange-400 rounded-full border border-orange-200 dark:border-orange-800">
|
||||
Modified
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<span className="text-xs font-medium text-muted-foreground whitespace-nowrap ml-2">
|
||||
{formatDate(version.createdAt)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted-foreground line-clamp-2">
|
||||
{version.changelog}
|
||||
</p>
|
||||
{/* Changelog */}
|
||||
<div className="pl-0">
|
||||
<p className={`text-xs leading-relaxed transition-colors ${
|
||||
isSelected ? 'text-muted-foreground' : 'text-muted-foreground/80'
|
||||
} line-clamp-2`}>
|
||||
{version.changelog || 'No changes recorded'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Action buttons - only show on hover or when selected */}
|
||||
{isSelected && (
|
||||
<div className="flex items-center space-x-1 pt-1">
|
||||
{/* Action buttons - show on hover or when selected */}
|
||||
<div className={`flex items-center space-x-2 pt-1 transition-all duration-200 ${
|
||||
isSelected || 'group-hover' ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
|
||||
}`}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onVersionSelect(version)
|
||||
}}
|
||||
className="h-7 px-2 text-xs font-medium hover:bg-primary/10 hover:text-primary"
|
||||
>
|
||||
<Eye className="h-3 w-3 mr-1" />
|
||||
View
|
||||
</Button>
|
||||
{!isLatest && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onVersionSelect(version)
|
||||
handleRestoreVersion(version)
|
||||
}}
|
||||
className="h-6 px-2 text-xs"
|
||||
disabled={restoring === version.id}
|
||||
className="h-7 px-2 text-xs font-medium hover:bg-blue-50 hover:text-blue-600 dark:hover:bg-blue-950/30 dark:hover:text-blue-400"
|
||||
>
|
||||
<Eye className="h-3 w-3 mr-1" />
|
||||
View
|
||||
{restoring === version.id ? (
|
||||
<LoadingSpinner size="sm" />
|
||||
) : (
|
||||
<RotateCcw className="h-3 w-3 mr-1" />
|
||||
)}
|
||||
Restore
|
||||
</Button>
|
||||
{!isLatest && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleRestoreVersion(version)
|
||||
}}
|
||||
disabled={restoring === version.id}
|
||||
className="h-6 px-2 text-xs"
|
||||
>
|
||||
{restoring === version.id ? (
|
||||
<LoadingSpinner size="sm" />
|
||||
) : (
|
||||
<RotateCcw className="h-3 w-3 mr-1" />
|
||||
)}
|
||||
Restore
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@ -316,38 +336,45 @@ export const VersionTimeline = forwardRef<VersionTimelineRef, VersionTimelinePro
|
||||
</div>
|
||||
|
||||
{versions.length === 0 && (
|
||||
<div className="text-center py-8">
|
||||
<History className="h-8 w-8 text-muted-foreground mx-auto mb-2" />
|
||||
<p className="text-sm text-muted-foreground">No versions yet</p>
|
||||
<div className="text-center py-12">
|
||||
<div className="bg-muted/30 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4">
|
||||
<History className="h-8 w-8 text-muted-foreground" />
|
||||
</div>
|
||||
<h4 className="font-medium text-foreground mb-1">No version history</h4>
|
||||
<p className="text-sm text-muted-foreground">Versions will appear here as you save changes</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Warning Modal */}
|
||||
{showWarning && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-background border border-border rounded-lg p-6 max-w-md mx-4">
|
||||
<div className="flex items-start space-x-3">
|
||||
<AlertTriangle className="h-5 w-5 text-orange-500 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-background border border-border rounded-xl shadow-2xl p-6 max-w-md w-full mx-4 animate-in fade-in-0 zoom-in-95 duration-200">
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="flex-shrink-0 w-10 h-10 bg-orange-100 dark:bg-orange-900/30 rounded-full flex items-center justify-center">
|
||||
<AlertTriangle className="h-5 w-5 text-orange-600 dark:text-orange-400" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-semibold text-foreground mb-2">
|
||||
Unsaved Changes
|
||||
Unsaved Changes Detected
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
You have unsaved changes that will be lost if you switch versions. Are you sure you want to continue?
|
||||
<p className="text-sm text-muted-foreground mb-6 leading-relaxed">
|
||||
You currently have unsaved changes that will be lost if you switch to a different version. Would you like to continue?
|
||||
</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex items-center justify-end space-x-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={cancelVersionSwitch}
|
||||
className="font-medium"
|
||||
>
|
||||
Cancel
|
||||
Keep Editing
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={confirmVersionSwitch}
|
||||
className="font-medium"
|
||||
>
|
||||
Switch Version
|
||||
</Button>
|
||||
|
Loading…
Reference in New Issue
Block a user