feat: MVP-5 Sprint 1 - Backup/Restore system
- Add backup types and RecallBackup format - Create backup snapshot engine (createBackupSnapshot) - Add IndexedDB storage for local backups - Implement retention policy (max 10, 30-day cleanup) - Add backup validation and restore logic (merge/replace modes) - Add backup restore UI dialog with preview and confirmation - Add unsaved changes guard hook - Integrate backups section in Settings - Add backup endpoint to export-import API
This commit is contained in:
136
src/components/backup-list.tsx
Normal file
136
src/components/backup-list.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
'use client'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { getBackups, deleteBackup } from '@/lib/backup-storage'
|
||||
import { RecallBackup } from '@/types/backup'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { toast } from 'sonner'
|
||||
import { Trash2, RotateCcw, Calendar, FileText } from 'lucide-react'
|
||||
import { BackupRestoreDialog } from './backup-restore-dialog'
|
||||
|
||||
export function BackupList() {
|
||||
const [backups, setBackups] = useState<RecallBackup[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [deletingId, setDeletingId] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
loadBackups()
|
||||
}, [])
|
||||
|
||||
async function loadBackups() {
|
||||
try {
|
||||
const data = await getBackups()
|
||||
setBackups(data)
|
||||
} catch {
|
||||
toast.error('Error al cargar los backups')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(id: string) {
|
||||
setDeletingId(id)
|
||||
try {
|
||||
await deleteBackup(id)
|
||||
setBackups((prev) => prev.filter((b) => b.id !== id))
|
||||
toast.success('Backup eliminado')
|
||||
} catch {
|
||||
toast.error('Error al eliminar el backup')
|
||||
} finally {
|
||||
setDeletingId(null)
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string) {
|
||||
return new Date(dateStr).toLocaleString('es-ES', {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
})
|
||||
}
|
||||
|
||||
function getSourceBadgeVariant(source: RecallBackup['source']) {
|
||||
switch (source) {
|
||||
case 'automatic':
|
||||
return 'secondary'
|
||||
case 'manual':
|
||||
return 'default'
|
||||
case 'pre-destructive':
|
||||
return 'destructive'
|
||||
default:
|
||||
return 'secondary'
|
||||
}
|
||||
}
|
||||
|
||||
function getSourceLabel(source: RecallBackup['source']) {
|
||||
switch (source) {
|
||||
case 'automatic':
|
||||
return 'Automático'
|
||||
case 'manual':
|
||||
return 'Manual'
|
||||
case 'pre-destructive':
|
||||
return 'Pre-destrucción'
|
||||
default:
|
||||
return source
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <div className="text-sm text-muted-foreground">Cargando backups...</div>
|
||||
}
|
||||
|
||||
if (backups.length === 0) {
|
||||
return (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
No hay backups disponibles. Los backups se crean automáticamente antes de operaciones
|
||||
destructivas.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{backups.map((backup) => (
|
||||
<div
|
||||
key={backup.id}
|
||||
className="flex items-center justify-between p-3 border rounded-lg"
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Calendar className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">{formatDate(backup.createdAt)}</span>
|
||||
<Badge variant={getSourceBadgeVariant(backup.source)}>{getSourceLabel(backup.source)}</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
||||
<span className="flex items-center gap-1">
|
||||
<FileText className="h-3 w-3" />
|
||||
{backup.metadata.noteCount} nota{backup.metadata.noteCount !== 1 ? 's' : ''}
|
||||
</span>
|
||||
<span>
|
||||
{backup.metadata.tagCount} tag{backup.metadata.tagCount !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 ml-4">
|
||||
<BackupRestoreDialog
|
||||
backup={backup}
|
||||
trigger={
|
||||
<Button variant="outline" size="sm" className="gap-1 cursor-pointer">
|
||||
<RotateCcw className="h-3 w-3" />
|
||||
Restaurar
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleDelete(backup.id)}
|
||||
disabled={deletingId === backup.id}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user