Files
recall/src/components/backup-list.tsx
Daniel Arroyo 8c80a12b81 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
2026-03-22 18:16:36 -03:00

137 lines
4.0 KiB
TypeScript

'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>
)
}