better version timeline

This commit is contained in:
songtianlun 2025-08-01 22:17:51 +08:00
parent 57c1f12893
commit 63cbeaa0fc

View File

@ -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>